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 @@
+
+
+