From 479ac7abaa55ca15c17a03a404ea2459b6660d82 Mon Sep 17 00:00:00 2001 From: Scrub <72096833+ScrubN@users.noreply.github.com> Date: Mon, 3 Apr 2023 11:32:42 -0400 Subject: [PATCH] Crop filename parameters (#650) * Add 'crop_start' 'crop_end' 'crop_start_custom' and 'crop_end_custom' filename parameters, cleanup filename computation. * Fix txt chat relative timestamps resetting after 24 hours * Adjust window height --- TwitchDownloaderCore/Chat/ChatText.cs | 10 +- TwitchDownloaderCore/Tools/TimeSpanHFormat.cs | 109 ++++++++++++++++++ TwitchDownloaderWPF/MainWindow.xaml.cs | 38 ------ TwitchDownloaderWPF/PageChatDownload.xaml.cs | 7 +- TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 24 ++-- TwitchDownloaderWPF/PageClipDownload.xaml.cs | 5 +- TwitchDownloaderWPF/PageVodDownload.xaml.cs | 34 +++--- .../Services/FilenameService.cs | 100 ++++++++++++++++ .../Translations/Strings.Designer.cs | 20 +++- .../Translations/Strings.fr.resx | 8 +- .../Translations/Strings.pl.resx | 8 +- TwitchDownloaderWPF/Translations/Strings.resx | 8 +- .../Translations/Strings.ru.resx | 8 +- .../Translations/Strings.tr.resx | 5 +- TwitchDownloaderWPF/TwitchTasks/TaskData.cs | 13 +-- .../WindowQueueOptions.xaml.cs | 27 +++-- TwitchDownloaderWPF/WindowSettings.xaml | 5 +- 17 files changed, 334 insertions(+), 95 deletions(-) create mode 100644 TwitchDownloaderCore/Tools/TimeSpanHFormat.cs create mode 100644 TwitchDownloaderWPF/Services/FilenameService.cs diff --git a/TwitchDownloaderCore/Chat/ChatText.cs b/TwitchDownloaderCore/Chat/ChatText.cs index 79e4f724..7aa1911b 100644 --- a/TwitchDownloaderCore/Chat/ChatText.cs +++ b/TwitchDownloaderCore/Chat/ChatText.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using TwitchDownloaderCore.Tools; using TwitchDownloaderCore.TwitchObjects; namespace TwitchDownloaderCore.Chat @@ -28,17 +29,16 @@ public static async Task SerializeAsync(string filePath, ChatRoot chatRoot, Time if (timeFormat == TimestampFormat.Utc) { string timestamp = comment.created_at.ToString("u").Replace("Z", " UTC"); - await sw.WriteLineAsync(string.Format("[{0}] {1}: {2}", timestamp, username, message)); + await sw.WriteLineAsync($"[{timestamp}] {username}: {message}"); } else if (timeFormat == TimestampFormat.Relative) { - var time = new TimeSpan(0, 0, (int)comment.content_offset_seconds); - string timestamp = time.ToString(@"h\:mm\:ss"); - await sw.WriteLineAsync(string.Format("[{0}] {1}: {2}", timestamp, username, message)); + var time = TimeSpan.FromSeconds(comment.content_offset_seconds); + await sw.WriteLineAsync(string.Format(new TimeSpanHFormat(), @"[{0:H\:mm\:ss}] {1}: {2}", time, username, message)); } else if (timeFormat == TimestampFormat.None) { - await sw.WriteLineAsync(string.Format("{0}: {1}", username, message)); + await sw.WriteLineAsync($"{username}: {message}"); } } diff --git a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs new file mode 100644 index 00000000..8790cc9d --- /dev/null +++ b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs @@ -0,0 +1,109 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; + +namespace TwitchDownloaderCore.Tools +{ + /// + /// Adds an 'H' parameter to TimeSpan string formatting. The 'H' parameter is equivalent to flooring .TotalHours. + /// + /// + /// The fact that this is not part of .NET is stupid. + /// + public class TimeSpanHFormat : IFormatProvider, ICustomFormatter + { + public object GetFormat(Type formatType) + { + if (formatType == typeof(ICustomFormatter)) + return this; + else + return null; + } + + public string Format(string format, object arg, IFormatProvider formatProvider) + { + if (!(arg is TimeSpan timeSpan)) + { + return HandleOtherFormats(format, arg); + } + + if (!format.Contains('H')) + { + return HandleOtherFormats(format, arg); + } + + var reader = new StringReader(format); + var builder = new StringBuilder(format.Length); + var regularFormatCharStart = -1; + var bigHStart = -1; + var position = -1; + do + { + var readChar = reader.Read(); + position++; + + if (readChar == 'H') + { + if (bigHStart == -1) + { + bigHStart = position; + } + + if (regularFormatCharStart != -1) + { + builder.Append(timeSpan.ToString(format.Substring(regularFormatCharStart, position - regularFormatCharStart))); + regularFormatCharStart = -1; + } + } + else + { + if (regularFormatCharStart == -1) + { + regularFormatCharStart = position; + } + + if (bigHStart != -1) + { + var formatString = ""; + for (var i = 0; i < position - bigHStart; i++) + { + formatString += "0"; + } + + builder.Append(((int)timeSpan.TotalHours).ToString(formatString)); + bigHStart = -1; + } + } + } while (reader.Peek() != -1); + + position++; + if (regularFormatCharStart != -1) + { + builder.Append(timeSpan.ToString(format.Substring(regularFormatCharStart, position - regularFormatCharStart))); + } + else if (bigHStart != -1) + { + var formatString = ""; + for (var i = 0; i < position - bigHStart; i++) + { + formatString += "0"; + } + + builder.Append(((int)timeSpan.TotalHours).ToString(formatString)); + } + + return builder.ToString(); + } + + private string HandleOtherFormats(string format, object arg) + { + if (arg is IFormattable) + return ((IFormattable)arg).ToString(format, CultureInfo.CurrentCulture); + else if (arg != null) + return arg.ToString(); + else + return ""; + } + } +} \ No newline at end of file diff --git a/TwitchDownloaderWPF/MainWindow.xaml.cs b/TwitchDownloaderWPF/MainWindow.xaml.cs index e50a0b19..f6830ae5 100644 --- a/TwitchDownloaderWPF/MainWindow.xaml.cs +++ b/TwitchDownloaderWPF/MainWindow.xaml.cs @@ -83,43 +83,5 @@ private async void Window_Loaded(object sender, RoutedEventArgs e) AutoUpdater.Start("https://downloader-update.twitcharchives.workers.dev"); #endif } - - internal static string[] GetTemplateSubfolders(ref string fullPath) - { - string[] returnString = fullPath.Split(new char[] { '\\', '/'}, StringSplitOptions.RemoveEmptyEntries); - fullPath = returnString[returnString.Length- 1]; - Array.Resize(ref returnString, returnString.Length - 1); - return returnString; - } - - internal static string GetFilename(string template, string title, string id, DateTime date, string channel) - { - StringBuilder returnString = new StringBuilder(template.Replace("{title}", title).Replace("{id}", id).Replace("{channel}", channel).Replace("{date}", date.ToString("Mdyy")).Replace("{random_string}", Path.GetRandomFileName().Split('.').First())); - Regex dateRegex = new Regex("{date_custom=\"(.*)\"}"); - bool done = false; - while (!done) - { - Match dateRegexMatch = dateRegex.Match(returnString.ToString()); - if (dateRegexMatch.Success) - { - string formatString = dateRegexMatch.Groups[1].Value; - returnString.Remove(dateRegexMatch.Groups[0].Index, dateRegexMatch.Groups[0].Length); - returnString.Insert(dateRegexMatch.Groups[0].Index, date.ToString(formatString)); - } - else - { - done = true; - } - } - - string fileName = returnString.ToString(); - string[] additionalSubfolders = GetTemplateSubfolders(ref fileName); - return Path.Combine(Path.Combine(additionalSubfolders), RemoveInvalidChars(fileName)); - } - - public static string RemoveInvalidChars(string filename) - { - return string.Concat(filename.Split(Path.GetInvalidFileNameChars())); - } } } diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs index 3135f185..823f3262 100644 --- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs @@ -30,6 +30,7 @@ public partial class PageChatDownload : Page public string downloadId; public int streamerId; public DateTime currentVideoTime; + public TimeSpan vodLength; private CancellationTokenSource _cancellationTokenSource; public PageChatDownload() @@ -121,7 +122,7 @@ private async void btnGetInfo_Click(object sender, RoutedEventArgs e) imgThumbnail.Source = image; } } - TimeSpan vodLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds); + vodLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds); textTitle.Text = videoInfo.data.video.title; textStreamer.Text = videoInfo.data.video.owner.displayName; var videoTime = videoInfo.data.video.createdAt; @@ -456,7 +457,9 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) else if (radioText.IsChecked == true) saveFileDialog.Filter = "TXT Files | *.txt"; - saveFileDialog.FileName = MainWindow.GetFilename(Settings.Default.TemplateChat, textTitle.Text, downloadId, currentVideoTime, textStreamer.Text); + saveFileDialog.FileName = FilenameService.GetFilename(Settings.Default.TemplateChat, textTitle.Text, downloadId, currentVideoTime, textStreamer.Text, + checkCropStart.IsChecked == true ? new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value) : TimeSpan.Zero, + checkCropEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength); if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs index 4b1fc3c3..86893d67 100644 --- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs +++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs @@ -30,6 +30,7 @@ public partial class PageChatUpdate : Page public ChatRoot ChatJsonInfo; public string VideoId; public DateTime VideoCreatedAt; + public TimeSpan VideoLength; private CancellationTokenSource _cancellationTokenSource; public PageChatUpdate() @@ -76,9 +77,9 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) numEndMinute.Value = chatEnd.Minutes; numEndSecond.Value = chatEnd.Seconds; - TimeSpan videoLength = TimeSpan.FromSeconds(double.IsNegative(ChatJsonInfo.video.length) ? 0.0 : ChatJsonInfo.video.length); - labelLength.Text = videoLength.Seconds > 0 - ? videoLength.ToString("c") + VideoLength = TimeSpan.FromSeconds(double.IsNegative(ChatJsonInfo.video.length) ? 0.0 : ChatJsonInfo.video.length); + labelLength.Text = VideoLength.Seconds > 0 + ? VideoLength.ToString("c") : Translations.Strings.Unknown; VideoId = ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1"; @@ -99,10 +100,10 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) } else { - videoLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds); - labelLength.Text = videoLength.ToString("c"); - numStartHour.Maximum = (int)videoLength.TotalHours; - numEndHour.Maximum = (int)videoLength.TotalHours; + VideoLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds); + labelLength.Text = VideoLength.ToString("c"); + numStartHour.Maximum = (int)VideoLength.TotalHours; + numEndHour.Maximum = (int)VideoLength.TotalHours; try { @@ -136,8 +137,8 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) } else { - videoLength = TimeSpan.FromSeconds(videoInfo.data.clip.durationSeconds); - labelLength.Text = videoLength.ToString("c"); + VideoLength = TimeSpan.FromSeconds(videoInfo.data.clip.durationSeconds); + labelLength.Text = VideoLength.ToString("c"); try { @@ -460,7 +461,10 @@ private async void SplitBtnUpdate_Click(object sender, RoutedEventArgs e) else if (radioText.IsChecked == true) saveFileDialog.Filter = "TXT Files | *.txt"; - saveFileDialog.FileName = MainWindow.GetFilename(Settings.Default.TemplateChat, textTitle.Text, ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1", VideoCreatedAt, textStreamer.Text); + saveFileDialog.FileName = FilenameService.GetFilename(Settings.Default.TemplateChat, textTitle.Text, + ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1", VideoCreatedAt, textStreamer.Text, + checkStart.IsChecked == true ? new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value) : TimeSpan.FromSeconds(double.IsNegative(ChatJsonInfo.video.start) ? 0.0 : ChatJsonInfo.video.start), + checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : VideoLength); if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs index d0043924..bee2d82a 100644 --- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs @@ -25,6 +25,7 @@ public partial class PageClipDownload : Page { public string clipId = ""; public DateTime currentVideoTime; + public TimeSpan clipLength; private CancellationTokenSource _cancellationTokenSource; public PageClipDownload() @@ -65,7 +66,7 @@ private async void btnGetInfo_Click(object sender, RoutedEventArgs e) imgThumbnail.Source = image; } } - TimeSpan clipLength = TimeSpan.FromSeconds(taskClipInfo.Result.data.clip.durationSeconds); + clipLength = TimeSpan.FromSeconds(taskClipInfo.Result.data.clip.durationSeconds); textStreamer.Text = clipData.data.clip.broadcaster.displayName; var clipCreatedAt = clipData.data.clip.createdAt; textCreatedAt.Text = Settings.Default.UTCVideoTime ? clipCreatedAt.ToString(CultureInfo.CurrentCulture) : clipCreatedAt.ToLocalTime().ToString(CultureInfo.CurrentCulture); @@ -173,7 +174,7 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) SaveFileDialog saveFileDialog = new SaveFileDialog { Filter = "MP4 Files | *.mp4", - FileName = MainWindow.GetFilename(Settings.Default.TemplateClip, textTitle.Text, clipId, currentVideoTime, textStreamer.Text) + FileName = FilenameService.GetFilename(Settings.Default.TemplateClip, textTitle.Text, clipId, currentVideoTime, textStreamer.Text, TimeSpan.Zero, clipLength) }; if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs index b7123e59..379a7af4 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs @@ -30,6 +30,7 @@ public partial class PageVodDownload : Page public Dictionary videoQualties = new(); public int currentVideoId; public DateTime currentVideoTime; + public TimeSpan vodLength; private CancellationTokenSource _cancellationTokenSource; public PageVodDownload() @@ -129,7 +130,7 @@ private async void btnGetInfo_Click(object sender, RoutedEventArgs e) } comboQuality.SelectedIndex = 0; - TimeSpan vodLength = TimeSpan.FromSeconds(taskVideoInfo.Result.data.video.lengthSeconds); + vodLength = TimeSpan.FromSeconds(taskVideoInfo.Result.data.video.lengthSeconds); textStreamer.Text = taskVideoInfo.Result.data.video.owner.displayName; textTitle.Text = taskVideoInfo.Result.data.video.title; var videoCreatedAt = taskVideoInfo.Result.data.video.createdAt; @@ -192,7 +193,9 @@ public VideoDownloadOptions GetOptions(string filename, string folder) { DownloadThreads = (int)numDownloadThreads.Value, ThrottleKb = Settings.Default.MaximumBandwidthKb, - Filename = filename ?? Path.Combine(folder, MainWindow.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text) + ".mp4"), + Filename = filename ?? Path.Combine(folder, FilenameService.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text, + checkStart.IsChecked == true ? new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value) : TimeSpan.Zero, + checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength) + ".mp4"), Oauth = passwordOauth.Password, Quality = GetQualityWithoutSize(comboQuality.Text).ToString(), Id = currentVideoId, @@ -215,7 +218,7 @@ private void UpdateVideoSizeEstimates() : TimeSpan.Zero; var cropEnd = checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) - : TimeSpan.Parse(labelLength.Text); + : vodLength; for (int i = 0; i < comboQuality.Items.Count; i++) { var qualityWithSize = (string)comboQuality.Items[i]; @@ -229,7 +232,7 @@ private void UpdateVideoSizeEstimates() comboQuality.SelectedIndex = selectedIndex; } - private ReadOnlySpan GetQualityWithoutSize(string qualityWithSize) + private static ReadOnlySpan GetQualityWithoutSize(string qualityWithSize) { int qualityIndex = qualityWithSize.LastIndexOf(" - "); return qualityIndex == -1 @@ -242,17 +245,17 @@ private static string EstimateVideoSize(int bandwidth, TimeSpan startTime, TimeS { var sizeInBytes = EstimateVideoSizeBytes(bandwidth, startTime, endTime); - const long ONE_KILOBYTE = 1024; - const long ONE_MEGABYTE = 1_048_576; - const long ONE_GIGABYTE = 1_073_741_824; + const long ONE_KIBIBYTE = 1024; + const long ONE_MEBIBYTE = 1_048_576; + const long ONE_GIBIBYTE = 1_073_741_824; return sizeInBytes switch { < 1 => "", - < ONE_KILOBYTE => $" - {sizeInBytes}B", - < ONE_MEGABYTE => $" - {(float)sizeInBytes / ONE_KILOBYTE:F1}KB", - < ONE_GIGABYTE => $" - {(float)sizeInBytes / ONE_MEGABYTE:F1}MB", - _ => $" - {(float)sizeInBytes / ONE_GIGABYTE:F1}GB", + < ONE_KIBIBYTE => $" - {sizeInBytes}B", + < ONE_MEBIBYTE => $" - {(float)sizeInBytes / ONE_KIBIBYTE:F1}KiB", + < ONE_GIBIBYTE => $" - {(float)sizeInBytes / ONE_MEBIBYTE:F1}MiB", + _ => $" - {(float)sizeInBytes / ONE_GIBIBYTE:F1}GiB", }; } @@ -314,9 +317,8 @@ public bool ValidateInputs() { if ((bool)checkStart.IsChecked) { - var videoLength = TimeSpan.Parse(labelLength.Text); var beginTime = new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value); - if (beginTime.TotalSeconds >= videoLength.TotalSeconds) + if (beginTime.TotalSeconds >= vodLength.TotalSeconds) { return false; } @@ -324,7 +326,7 @@ public bool ValidateInputs() if ((bool)checkEnd.IsChecked) { var endTime = new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value); - if (endTime.TotalSeconds < beginTime.TotalSeconds) + if (endTime.TotalSeconds < vodLength.TotalSeconds) { return false; } @@ -416,7 +418,9 @@ private async void SplitBtnDownloader_Click(object sender, RoutedEventArgs e) SaveFileDialog saveFileDialog = new SaveFileDialog { Filter = "MP4 Files | *.mp4", - FileName = MainWindow.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text) + FileName = FilenameService.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text, + checkStart.IsChecked == true ? new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value) : TimeSpan.Zero, + checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength) }; if (saveFileDialog.ShowDialog() == false) { diff --git a/TwitchDownloaderWPF/Services/FilenameService.cs b/TwitchDownloaderWPF/Services/FilenameService.cs new file mode 100644 index 00000000..aac2788e --- /dev/null +++ b/TwitchDownloaderWPF/Services/FilenameService.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using TwitchDownloaderCore.Tools; + +namespace TwitchDownloaderWPF.Services +{ + public static class FilenameService + { + private static string[] GetTemplateSubfolders(ref string fullPath) + { + string[] returnString = fullPath.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + fullPath = returnString[^1]; + Array.Resize(ref returnString, returnString.Length - 1); + return returnString; + } + + internal static string GetFilename(string template, string title, string id, DateTime date, string channel, TimeSpan cropStart, TimeSpan cropEnd) + { + var stringBuilder = new StringBuilder(template) + .Replace("{title}", title) + .Replace("{id}", id) + .Replace("{channel}", channel) + .Replace("{date}", date.ToString("Mdyy")) + .Replace("{random_string}", Path.GetFileNameWithoutExtension(Path.GetRandomFileName())) + .Replace("{crop_start}", string.Format(new TimeSpanHFormat(), @"{0:HH\-mm\-ss}", cropStart)) + .Replace("{crop_end}", string.Format(new TimeSpanHFormat(), @"{0:HH\-mm\-ss}", cropEnd)); + + if (template.Contains("{date_custom=")) + { + var dateRegex = new Regex("{date_custom=\"(.*)\"}"); + var dateDone = false; + while (!dateDone) + { + var dateMatch = dateRegex.Match(stringBuilder.ToString()); + if (dateMatch.Success) + { + var formatString = dateMatch.Groups[1].Value; + stringBuilder.Remove(dateMatch.Groups[0].Index, dateMatch.Groups[0].Length); + stringBuilder.Insert(dateMatch.Groups[0].Index, date.ToString(formatString)); + } + else + { + dateDone = true; + } + } + } + + if (template.Contains("{crop_start_custom=")) + { + var cropStartRegex = new Regex("{crop_start_custom=\"(.*)\"}"); + var cropStartDone = false; + while (!cropStartDone) + { + var cropStartMatch = cropStartRegex.Match(stringBuilder.ToString()); + if (cropStartMatch.Success) + { + var formatString = cropStartMatch.Groups[1].Value; + stringBuilder.Remove(cropStartMatch.Groups[0].Index, cropStartMatch.Groups[0].Length); + stringBuilder.Insert(cropStartMatch.Groups[0].Index, cropStart.ToString(formatString)); + } + else + { + cropStartDone = true; + } + } + } + + if (template.Contains("{crop_end_custom=")) + { + var cropEndRegex = new Regex("{crop_end_custom=\"(.*)\"}"); + var cropEndDone = false; + while (!cropEndDone) + { + var cropEndMatch = cropEndRegex.Match(stringBuilder.ToString()); + if (cropEndMatch.Success) + { + var formatString = cropEndMatch.Groups[1].Value; + stringBuilder.Remove(cropEndMatch.Groups[0].Index, cropEndMatch.Groups[0].Length); + stringBuilder.Insert(cropEndMatch.Groups[0].Index, cropEnd.ToString(formatString)); + } + else + { + cropEndDone = true; + } + } + } + + string fileName = stringBuilder.ToString(); + string[] additionalSubfolders = GetTemplateSubfolders(ref fileName); + return Path.Combine(Path.Combine(additionalSubfolders), RemoveInvalidFilenameChars(fileName)); + } + + private static string RemoveInvalidFilenameChars(string filename) + { + return string.Concat(filename.Split(Path.GetInvalidFileNameChars())); + } + } +} \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs index 85070ee1..31b1b978 100644 --- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs +++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs @@ -717,7 +717,7 @@ public static string FfzEmotes { } /// - /// Looks up a localized string similar to {title} {id} {date} {channel} {date_custom=""} {random_string}. + /// Looks up a localized string similar to {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""}. /// public static string FilenameTemplateParameters { get { @@ -1490,6 +1490,24 @@ public static string ThirdPartyEmotesTooltip { } } + /// + /// Looks up a localized string similar to crop_start_custom and crop_end_custom formattings are based on the. + /// + public static string TimeSpanCustomFormatting { + get { + return ResourceManager.GetString("TimeSpanCustomFormatting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to C# standard TimeSpan format strings. + /// + public static string TimeSpanCustomFormattingHyperlink { + get { + return ResourceManager.GetString("TimeSpanCustomFormattingHyperlink", resourceCulture); + } + } + /// /// Looks up a localized string similar to Timestamp Format:. /// diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx index 07546e8c..f0369b31 100644 --- a/TwitchDownloaderWPF/Translations/Strings.fr.resx +++ b/TwitchDownloaderWPF/Translations/Strings.fr.resx @@ -298,7 +298,7 @@ Emoticônes FFZ: - {title} {id} {date} {channel} {date_custom=""} {random_string} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} Do not translate @@ -732,4 +732,10 @@ Échelle des contours: + + C# standard TimeSpan format strings + + + crop_start_custom and crop_end_custom formattings are based on the + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx index 35dd3412..bdd0bb08 100644 --- a/TwitchDownloaderWPF/Translations/Strings.pl.resx +++ b/TwitchDownloaderWPF/Translations/Strings.pl.resx @@ -298,7 +298,7 @@ Emotki FFZ: - {title} {id} {date} {channel} {date_custom=""} {random_string} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} Do not translate @@ -732,4 +732,10 @@ Outline Scale: + + C# standard TimeSpan format strings + + + crop_start_custom and crop_end_custom formattings are based on the + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx index 399ba94a..6db41472 100644 --- a/TwitchDownloaderWPF/Translations/Strings.resx +++ b/TwitchDownloaderWPF/Translations/Strings.resx @@ -298,7 +298,7 @@ FFZ Emotes: - {title} {id} {date} {channel} {date_custom=""} {random_string} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} Do not translate @@ -731,4 +731,10 @@ Outline Scale: + + crop_start_custom and crop_end_custom formattings are based on the + + + C# standard TimeSpan format strings + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx index db292c13..688501b3 100644 --- a/TwitchDownloaderWPF/Translations/Strings.ru.resx +++ b/TwitchDownloaderWPF/Translations/Strings.ru.resx @@ -298,7 +298,7 @@ FFZ Эмодзи: - {title} {id} {date} {channel} {date_custom=""} {random_string} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} Do not translate @@ -732,4 +732,10 @@ Outline Scale: + + C# standard TimeSpan format strings + + + crop_start_custom and crop_end_custom formattings are based on the + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx index 29125b59..9da4092e 100644 --- a/TwitchDownloaderWPF/Translations/Strings.tr.resx +++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx @@ -299,7 +299,7 @@ FFZ Emojileri: - {title} {id} {date} {channel} {date_custom=""} {random_string} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} Do not translate @@ -733,4 +733,7 @@ Outline Scale: + + C# standard TimeSpan format strings + diff --git a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs index 51a6a1cc..c064af5f 100644 --- a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs +++ b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs @@ -17,18 +17,17 @@ public string LengthFormatted get { TimeSpan time = TimeSpan.FromSeconds(Length); - if ((int)time.TotalHours > 0) + if ((int)time.TotalHours > 0) { - return (int)time.TotalHours + ":" + time.Hours.ToString("D2") + ":" + time.Seconds.ToString("D2"); + return (int)time.TotalHours + ":" + time.Minutes.ToString("D2") + ":" + time.Seconds.ToString("D2"); } - else if ((int)time.TotalMinutes > 0) + + if ((int)time.TotalMinutes > 0) { return time.Minutes.ToString("D2") + ":" + time.Seconds.ToString("D2"); } - else - { - return time.Seconds.ToString("D2") + "s"; - } + + return time.Seconds.ToString("D2") + "s"; } } } diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs index 52947a97..6f26a4c8 100644 --- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs +++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs @@ -138,7 +138,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) else chatOptions.DownloadFormat = ChatFormat.Text; chatOptions.EmbedData = (bool)checkEmbed.IsChecked; - chatOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id, vodPage.currentVideoTime, vodPage.textStreamer.Text) + "." + chatOptions.DownloadFormat); + chatOptions.Filename = Path.Combine(folderPath, Path.GetFileNameWithoutExtension(downloadOptions.Filename) + "." + chatOptions.DownloadFormat); if (downloadOptions.CropBeginning) { @@ -200,7 +200,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ClipDownloadTask downloadTask = new ClipDownloadTask(); ClipDownloadOptions downloadOptions = new ClipDownloadOptions(); - downloadOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateClip, clipPage.textTitle.Text, clipPage.clipId, clipPage.currentVideoTime, clipPage.textStreamer.Text) + ".mp4"); + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateClip, clipPage.textTitle.Text, clipPage.clipId, clipPage.currentVideoTime, clipPage.textStreamer.Text, TimeSpan.Zero, clipPage.clipLength) + ".mp4"); downloadOptions.Id = clipPage.clipId; downloadOptions.Quality = clipPage.comboQuality.Text; downloadTask.DownloadOptions = downloadOptions; @@ -217,7 +217,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) { ChatDownloadTask chatTask = new ChatDownloadTask(); ChatDownloadOptions chatOptions = MainWindow.pageChatDownload.GetOptions(null); - chatOptions.Id = downloadOptions.Id.ToString(); + chatOptions.Id = downloadOptions.Id; if (radioJson.IsChecked == true) chatOptions.DownloadFormat = ChatFormat.Json; else if (radioHTML.IsChecked == true) @@ -226,7 +226,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) chatOptions.DownloadFormat = ChatFormat.Text; chatOptions.TimeFormat = TimestampFormat.Relative; chatOptions.EmbedData = (bool)checkEmbed.IsChecked; - chatOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id, clipPage.currentVideoTime, clipPage.textStreamer.Text) + "." + chatOptions.FileExtension); + chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id, clipPage.currentVideoTime, clipPage.textStreamer.Text, TimeSpan.Zero, clipPage.clipLength) + "." + chatOptions.FileExtension); chatTask.DownloadOptions = chatOptions; chatTask.Info.Title = clipPage.textTitle.Text; @@ -277,7 +277,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ChatDownloadTask chatTask = new ChatDownloadTask(); ChatDownloadOptions chatOptions = MainWindow.pageChatDownload.GetOptions(null); chatOptions.Id = chatPage.downloadId; - chatOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateChat, chatPage.textTitle.Text, chatOptions.Id, chatPage.currentVideoTime, chatPage.textStreamer.Text) + "." + chatOptions.FileExtension); + chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, chatPage.textTitle.Text, chatOptions.Id, chatPage.currentVideoTime, chatPage.textStreamer.Text, + chatOptions.CropBeginning ? TimeSpan.FromSeconds(chatOptions.CropBeginningTime) : TimeSpan.Zero, chatOptions.CropEnding ? TimeSpan.FromSeconds(chatOptions.CropEndingTime) : chatPage.vodLength + ) + "." + chatOptions.FileExtension); chatTask.DownloadOptions = chatOptions; chatTask.Info.Title = chatPage.textTitle.Text; @@ -322,7 +324,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ChatUpdateTask chatTask = new ChatUpdateTask(); ChatUpdateOptions chatOptions = MainWindow.pageChatUpdate.GetOptions(null); chatOptions.InputFile = chatPage.InputFile; - chatOptions.OutputFile = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateChat, chatPage.textTitle.Text, chatPage.VideoId, chatPage.VideoCreatedAt, chatPage.textStreamer.Text) + "." + chatOptions.FileExtension); + chatOptions.OutputFile = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, chatPage.textTitle.Text, chatPage.VideoId, chatPage.VideoCreatedAt, chatPage.textStreamer.Text, + chatOptions.CropBeginning ? TimeSpan.FromSeconds(chatOptions.CropBeginningTime) : TimeSpan.Zero, chatOptions.CropEnding ? TimeSpan.FromSeconds(chatOptions.CropEndingTime) : chatPage.VideoLength + ) + "." + chatOptions.FileExtension); chatTask.UpdateOptions = chatOptions; chatTask.Info.Title = chatPage.textTitle.Text; @@ -399,7 +403,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) downloadOptions.CropEnding = false; downloadOptions.DownloadThreads = Settings.Default.VodDownloadThreads; downloadOptions.ThrottleKb = Settings.Default.MaximumBandwidthKb; - downloadOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateVod, dataList[i].Title, dataList[i].Id, dataList[i].Time, dataList[i].Streamer) + ".mp4"); + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateVod, dataList[i].Title, dataList[i].Id, dataList[i].Time, dataList[i].Streamer, + downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero, downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : TimeSpan.FromSeconds(dataList[i].Length) + ) + ".mp4"); downloadTask.DownloadOptions = downloadOptions; downloadTask.Info.Title = dataList[i].Title; downloadTask.Info.Thumbnail = dataList[i].Thumbnail; @@ -415,7 +421,8 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ClipDownloadTask downloadTask = new ClipDownloadTask(); ClipDownloadOptions downloadOptions = new ClipDownloadOptions(); downloadOptions.Id = dataList[i].Id; - downloadOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateClip, dataList[i].Title, dataList[i].Id, dataList[i].Time, dataList[i].Streamer) + ".mp4"); + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateClip, dataList[i].Title, dataList[i].Id, dataList[i].Time, dataList[i].Streamer, + TimeSpan.Zero, TimeSpan.FromSeconds(dataList[i].Length)) + ".mp4"); downloadTask.DownloadOptions = downloadOptions; downloadTask.Info.Title = dataList[i].Title; downloadTask.Info.Thumbnail = dataList[i].Thumbnail; @@ -444,7 +451,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) downloadOptions.Id = dataList[i].Id; downloadOptions.CropBeginning = false; downloadOptions.CropEnding = false; - downloadOptions.Filename = Path.Combine(folderPath, MainWindow.GetFilename(Settings.Default.TemplateChat, dataList[i].Title, dataList[i].Id, dataList[i].Time, dataList[i].Streamer) + "." + downloadOptions.FileExtension); + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, dataList[i].Title, dataList[i].Id, dataList[i].Time, dataList[i].Streamer, + downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero, downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : TimeSpan.FromSeconds(dataList[i].Length) + ) + "." + downloadOptions.FileExtension); downloadTask.DownloadOptions = downloadOptions; downloadTask.Info.Title = dataList[i].Title; downloadTask.Info.Thumbnail = dataList[i].Thumbnail; diff --git a/TwitchDownloaderWPF/WindowSettings.xaml b/TwitchDownloaderWPF/WindowSettings.xaml index fb6800e0..b44527f0 100644 --- a/TwitchDownloaderWPF/WindowSettings.xaml +++ b/TwitchDownloaderWPF/WindowSettings.xaml @@ -11,7 +11,7 @@ xmlns:hc="https://handyorg.github.io/handycontrol" xmlns:fa="http://schemas.fontawesome.com/icons/" mc:Ignorable="d" - Title="Global Settings" MinWidth="400" MinHeight="450" Width="500" Height="500" Initialized="Window_Initialized" Closing="Window_Closing"> + Title="Global Settings" MinWidth="400" MinHeight="460" Width="500" Height="547" Initialized="Window_Initialized" Closing="Window_Closing"> @@ -79,6 +79,9 @@ + + +