From 64eabaee298afe8379886ea966790e6c4c55063e Mon Sep 17 00:00:00 2001
From: Scrub <72096833+ScrubN@users.noreply.github.com>
Date: Sun, 12 Nov 2023 03:27:59 -0500
Subject: [PATCH] Prerequisites for CLI filename template support (#886)
* Move FilenameService to Core
* Reduce filename generator memory footprint
* Fix game name not being sanitized
* string.IsNullOrWhiteSpace -> string.IsNullOrEmpty
* Support escaping with quote marks in both ReadOnlySpanExtensions.TryReplaceNonEscaped and TimeSpanHFormat
* Rename TimeSpanExtensions to UrlTimeCode
---
TwitchDownloaderCore/Chat/ChatJson.cs | 2 +-
.../Extensions/ReadOnlySpanExtensions.cs | 77 ++++++++--
.../Extensions/StringExtensions.cs | 43 ++++++
TwitchDownloaderCore/Tools/FilenameService.cs | 90 +++++++++++
TwitchDownloaderCore/Tools/TimeSpanHFormat.cs | 88 ++++++++---
.../UrlTimeCode.cs} | 6 +-
TwitchDownloaderWPF/PageChatDownload.xaml.cs | 2 +-
TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 1 +
TwitchDownloaderWPF/PageVodDownload.xaml.cs | 2 +-
.../Services/FilenameService.cs | 141 ------------------
.../WindowQueueOptions.xaml.cs | 1 +
11 files changed, 270 insertions(+), 183 deletions(-)
create mode 100644 TwitchDownloaderCore/Extensions/StringExtensions.cs
create mode 100644 TwitchDownloaderCore/Tools/FilenameService.cs
rename TwitchDownloaderCore/{Extensions/TimeSpanExtensions.cs => Tools/UrlTimeCode.cs} (91%)
delete mode 100644 TwitchDownloaderWPF/Services/FilenameService.cs
diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs
index b156a184..bdfe1bc4 100644
--- a/TwitchDownloaderCore/Chat/ChatJson.cs
+++ b/TwitchDownloaderCore/Chat/ChatJson.cs
@@ -207,7 +207,7 @@ private static async Task UpgradeChatJson(ChatRoot chatRoot)
if (chatRoot.video.duration is not null)
{
- chatRoot.video.length = TimeSpanExtensions.ParseTimeCode(chatRoot.video.duration).TotalSeconds;
+ chatRoot.video.length = UrlTimeCode.Parse(chatRoot.video.duration).TotalSeconds;
chatRoot.video.end = chatRoot.video.length;
chatRoot.video.duration = null;
}
diff --git a/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs b/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs
index 5a3fcaf3..433553bd 100644
--- a/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs
+++ b/TwitchDownloaderCore/Extensions/ReadOnlySpanExtensions.cs
@@ -4,44 +4,91 @@ namespace TwitchDownloaderCore.Extensions
{
public static class ReadOnlySpanExtensions
{
- /// Replaces all occurrences of not prepended by a backslash with .
- public static bool TryReplaceNonEscaped(this ReadOnlySpan str, Span destination, out int charsWritten, char oldChar, char newChar)
+ /// Replaces all occurrences of not prepended by a backslash or contained within quotation marks with .
+ public static bool TryReplaceNonEscaped(this ReadOnlySpan str, Span destination, char oldChar, char newChar)
{
+ const string ESCAPE_CHARS = @"\'""";
+
if (destination.Length < str.Length)
- {
- charsWritten = 0;
return false;
- }
str.CopyTo(destination);
- charsWritten = str.Length;
var firstIndex = destination.IndexOf(oldChar);
-
if (firstIndex == -1)
- {
return true;
- }
- firstIndex = Math.Min(firstIndex, destination.IndexOf('\\'));
+ var firstEscapeIndex = destination.IndexOfAny(ESCAPE_CHARS);
+ if (firstEscapeIndex != -1 && firstEscapeIndex < firstIndex)
+ firstIndex = firstEscapeIndex;
- for (var i = firstIndex; i < str.Length; i++)
+ var lastIndex = destination.LastIndexOf(oldChar);
+ var lastEscapeIndex = destination.LastIndexOfAny(ESCAPE_CHARS);
+ if (lastEscapeIndex != -1 && lastEscapeIndex > lastIndex)
+ lastIndex = lastEscapeIndex;
+
+ lastIndex++;
+ for (var i = firstIndex; i < lastIndex; i++)
{
var readChar = destination[i];
- if (readChar == '\\' && i + 1 < str.Length)
+ switch (readChar)
+ {
+ case '\\':
+ i++;
+ break;
+ case '\'':
+ case '\"':
+ {
+ i = FindCloseQuoteMark(destination, i, lastIndex, readChar);
+
+ if (i == -1)
+ {
+ destination.Clear();
+ return false;
+ }
+
+ break;
+ }
+ default:
+ {
+ if (readChar == oldChar)
+ {
+ destination[i] = newChar;
+ }
+
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static int FindCloseQuoteMark(ReadOnlySpan destination, int openQuoteIndex, int endIndex, char readChar)
+ {
+ var i = openQuoteIndex + 1;
+ var quoteFound = false;
+ while (i < endIndex)
+ {
+ var readCharQuote = destination[i];
+ i++;
+
+ if (readCharQuote == '\\')
{
i++;
continue;
}
- if (readChar == oldChar)
+ if (readCharQuote == readChar)
{
- destination[i] = newChar;
+ i--;
+ quoteFound = true;
+ break;
}
}
- return true;
+ return quoteFound ? i : -1;
}
}
}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/Extensions/StringExtensions.cs b/TwitchDownloaderCore/Extensions/StringExtensions.cs
new file mode 100644
index 00000000..cb0fa60d
--- /dev/null
+++ b/TwitchDownloaderCore/Extensions/StringExtensions.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace TwitchDownloaderCore.Extensions
+{
+ public static class StringExtensions
+ {
+ public static string ReplaceAny(this string str, ReadOnlySpan oldChars, char newChar)
+ {
+ if (string.IsNullOrEmpty(str))
+ {
+ return str;
+ }
+
+ var index = str.AsSpan().IndexOfAny(oldChars);
+ if (index == -1)
+ {
+ return str;
+ }
+
+ const ushort MAX_STACK_SIZE = 512;
+ var span = str.Length <= MAX_STACK_SIZE
+ ? stackalloc char[str.Length]
+ : str.ToCharArray();
+
+ // Unfortunately this cannot be inlined with the previous statement because a ternary is required for the stackalloc to compile
+ if (str.Length <= MAX_STACK_SIZE)
+ str.CopyTo(span);
+
+ var tempSpan = span;
+ do
+ {
+ tempSpan[index] = newChar;
+ tempSpan = tempSpan[(index + 1)..];
+
+ index = tempSpan.IndexOfAny(oldChars);
+ if (index == -1)
+ break;
+ } while (true);
+
+ return span.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/Tools/FilenameService.cs b/TwitchDownloaderCore/Tools/FilenameService.cs
new file mode 100644
index 00000000..408116a1
--- /dev/null
+++ b/TwitchDownloaderCore/Tools/FilenameService.cs
@@ -0,0 +1,90 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using TwitchDownloaderCore.Extensions;
+
+namespace TwitchDownloaderCore.Tools
+{
+ public static class FilenameService
+ {
+ private static string[] GetTemplateSubfolders(ref string fullPath)
+ {
+ var returnString = fullPath.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
+ fullPath = returnString[^1];
+ Array.Resize(ref returnString, returnString.Length - 1);
+
+ for (var i = 0; i < returnString.Length; i++)
+ {
+ returnString[i] = RemoveInvalidFilenameChars(returnString[i]);
+ }
+
+ return returnString;
+ }
+
+ public static string GetFilename(string template, string title, string id, DateTime date, string channel, TimeSpan cropStart, TimeSpan cropEnd, string viewCount, string game)
+ {
+ var videoLength = cropEnd - cropStart;
+
+ var stringBuilder = new StringBuilder(template)
+ .Replace("{title}", RemoveInvalidFilenameChars(title))
+ .Replace("{id}", id)
+ .Replace("{channel}", RemoveInvalidFilenameChars(channel))
+ .Replace("{date}", date.ToString("Mdyy"))
+ .Replace("{random_string}", Path.GetRandomFileName().Replace(".", ""))
+ .Replace("{crop_start}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", cropStart))
+ .Replace("{crop_end}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", cropEnd))
+ .Replace("{length}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", videoLength))
+ .Replace("{views}", viewCount)
+ .Replace("{game}", RemoveInvalidFilenameChars(game));
+
+ if (template.Contains("{date_custom="))
+ {
+ var dateRegex = new Regex("{date_custom=\"(.*)\"}");
+ ReplaceCustomWithFormattable(stringBuilder, dateRegex, date);
+ }
+
+ if (template.Contains("{crop_start_custom="))
+ {
+ var cropStartRegex = new Regex("{crop_start_custom=\"(.*)\"}");
+ ReplaceCustomWithFormattable(stringBuilder, cropStartRegex, cropStart);
+ }
+
+ if (template.Contains("{crop_end_custom="))
+ {
+ var cropEndRegex = new Regex("{crop_end_custom=\"(.*)\"}");
+ ReplaceCustomWithFormattable(stringBuilder, cropEndRegex, cropEnd);
+ }
+
+ if (template.Contains("{length_custom="))
+ {
+ var lengthRegex = new Regex("{length_custom=\"(.*)\"}");
+ ReplaceCustomWithFormattable(stringBuilder, lengthRegex, videoLength);
+ }
+
+ var fileName = stringBuilder.ToString();
+ var additionalSubfolders = GetTemplateSubfolders(ref fileName);
+ return Path.Combine(Path.Combine(additionalSubfolders), RemoveInvalidFilenameChars(fileName));
+ }
+
+ private static void ReplaceCustomWithFormattable(StringBuilder sb, Regex regex, IFormattable formattable, IFormatProvider formatProvider = null)
+ {
+ do
+ {
+ // There's probably a better way to do this that doesn't require calling ToString()
+ // However we need .NET7+ for span support in the regex matcher.
+ var match = regex.Match(sb.ToString());
+ if (!match.Success)
+ break;
+
+ var formatString = match.Groups[1].Value;
+ sb.Remove(match.Groups[0].Index, match.Groups[0].Length);
+ sb.Insert(match.Groups[0].Index, RemoveInvalidFilenameChars(formattable.ToString(formatString, formatProvider)));
+ } while (true);
+ }
+
+ private static readonly char[] FilenameInvalidChars = Path.GetInvalidFileNameChars();
+
+ private static string RemoveInvalidFilenameChars(string filename) => filename.ReplaceAny(FilenameInvalidChars, '_');
+ }
+}
\ No newline at end of file
diff --git a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs
index f9fbb120..531f04cb 100644
--- a/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs
+++ b/TwitchDownloaderCore/Tools/TimeSpanHFormat.cs
@@ -49,76 +49,121 @@ public string Format(string format, TimeSpan timeSpan, IFormatProvider formatPro
if (timeSpan.Days == 0)
{
var newFormat = format.Length <= 256 ? stackalloc char[format.Length] : new char[format.Length];
- if (!format.AsSpan().TryReplaceNonEscaped(newFormat, out var charsWritten, 'H', 'h'))
+ if (!format.AsSpan().TryReplaceNonEscaped(newFormat, 'H', 'h'))
{
- throw new Exception("Failed to generate ToString() compatible format. This should not have been possible.");
+ throw new FormatException($"Invalid character escaping in the format string: {format}");
}
// If the format contains more than 2 sequential unescaped h's, it will throw a format exception. If so, we can fallback to our parser.
if (newFormat.IndexOf("hhh") == -1)
{
- return HandleOtherFormats(newFormat[..charsWritten].ToString(), timeSpan, formatProvider);
+ return HandleOtherFormats(newFormat.ToString(), timeSpan, formatProvider);
}
}
- var sb = new StringBuilder(format.Length);
+ return HandleBigHFormat(format.AsSpan(), timeSpan);
+ }
+
+ private static string HandleBigHFormat(ReadOnlySpan format, TimeSpan timeSpan)
+ {
+ var formatLength = format.Length;
+ var sb = new StringBuilder(formatLength);
var regularFormatCharStart = -1;
var bigHStart = -1;
- var formatSpan = format.AsSpan();
- for (var i = 0; i < formatSpan.Length; i++)
+ for (var i = 0; i < formatLength; i++)
{
- var readChar = formatSpan[i];
+ var readChar = format[i];
if (readChar == 'H')
{
if (bigHStart == -1)
- {
bigHStart = i;
- }
if (regularFormatCharStart != -1)
{
- AppendRegularFormat(sb, timeSpan, format, regularFormatCharStart, i - regularFormatCharStart);
+ var formatEnd = i - regularFormatCharStart;
+ AppendRegularFormat(sb, timeSpan, format.Slice(regularFormatCharStart, formatEnd));
regularFormatCharStart = -1;
}
}
else
{
if (regularFormatCharStart == -1)
- {
regularFormatCharStart = i;
- }
if (bigHStart != -1)
{
- AppendBigHFormat(sb, timeSpan, i - bigHStart);
+ var bigHCount = i - bigHStart;
+ AppendBigHFormat(sb, timeSpan, bigHCount);
bigHStart = -1;
}
- // If the current char is an escape we can skip the next char
- if (readChar == '\\' && i + 1 < formatSpan.Length)
+ switch (readChar)
{
- i++;
+ // If the current char is an escape we can skip the next char
+ case '\\' when i + 1 < formatLength:
+ i++;
+ continue;
+ // If the current char is a quote we can skip the next quote, if it exists
+ case '\'' when i + 1 < formatLength:
+ case '\"' when i + 1 < formatLength:
+ {
+ i = FindCloseQuoteMark(format, i, formatLength, readChar);
+
+ if (i == -1)
+ {
+ throw new FormatException($"Invalid character escaping in the format string: {format}");
+ }
+
+ continue;
+ }
}
}
}
if (regularFormatCharStart != -1)
{
- AppendRegularFormat(sb, timeSpan, format, regularFormatCharStart, formatSpan.Length - regularFormatCharStart);
+ var formatEnd = format.Length - regularFormatCharStart;
+ AppendRegularFormat(sb, timeSpan, format.Slice(regularFormatCharStart, formatEnd));
}
else if (bigHStart != -1)
{
- AppendBigHFormat(sb, timeSpan, formatSpan.Length - bigHStart);
+ var bigHCount = format.Length - bigHStart;
+ AppendBigHFormat(sb, timeSpan, bigHCount);
}
return sb.ToString();
}
- private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, string formatString, int start, int length)
+ private static int FindCloseQuoteMark(ReadOnlySpan format, int openQuoteIndex, int endIndex, char readChar)
+ {
+ var i = openQuoteIndex + 1;
+ var quoteFound = false;
+ while (i < endIndex)
+ {
+ var readCharQuote = format[i];
+ i++;
+
+ if (readCharQuote == '\\')
+ {
+ i++;
+ continue;
+ }
+
+ if (readCharQuote == readChar)
+ {
+ i--;
+ quoteFound = true;
+ break;
+ }
+ }
+
+ return quoteFound ? i : -1;
+ }
+
+ private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, ReadOnlySpan format)
{
Span destination = stackalloc char[256];
- var format = formatString.AsSpan(start, length);
if (timeSpan.TryFormat(destination, out var charsWritten, format))
{
@@ -132,7 +177,8 @@ private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, str
private static void AppendBigHFormat(StringBuilder sb, TimeSpan timeSpan, int count)
{
- Span destination = stackalloc char[8];
+ const int TIMESPAN_MAX_HOURS_LENGTH = 9; // The maximum integer hours a TimeSpan can hold is 256204778.
+ Span destination = stackalloc char[TIMESPAN_MAX_HOURS_LENGTH];
Span format = stackalloc char[count];
format.Fill('0');
diff --git a/TwitchDownloaderCore/Extensions/TimeSpanExtensions.cs b/TwitchDownloaderCore/Tools/UrlTimeCode.cs
similarity index 91%
rename from TwitchDownloaderCore/Extensions/TimeSpanExtensions.cs
rename to TwitchDownloaderCore/Tools/UrlTimeCode.cs
index 508ff4d0..57498951 100644
--- a/TwitchDownloaderCore/Extensions/TimeSpanExtensions.cs
+++ b/TwitchDownloaderCore/Tools/UrlTimeCode.cs
@@ -1,15 +1,15 @@
using System;
-namespace TwitchDownloaderCore.Extensions
+namespace TwitchDownloaderCore.Tools
{
- public static class TimeSpanExtensions
+ public static class UrlTimeCode
{
///
/// Converts the span representation of a time interval in the format of '2d21h11m9s' to its equivalent.
///
/// A span containing the characters that represent the time interval to convert.
/// The equivalent to the time interval contained in the span.
- public static TimeSpan ParseTimeCode(ReadOnlySpan input)
+ public static TimeSpan Parse(ReadOnlySpan input)
{
var dayIndex = input.IndexOf('d');
var hourIndex = input.IndexOf('h');
diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
index 08fa9ac7..4ed52a29 100644
--- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs
@@ -137,7 +137,7 @@ private async Task GetVideoInfo()
var urlTimeCodeMatch = TwitchRegex.UrlTimeCode.Match(textUrl.Text);
if (urlTimeCodeMatch.Success)
{
- var time = TimeSpanExtensions.ParseTimeCode(urlTimeCodeMatch.ValueSpan);
+ var time = UrlTimeCode.Parse(urlTimeCodeMatch.ValueSpan);
checkCropStart.IsChecked = true;
numStartHour.Value = time.Hours;
numStartMinute.Value = time.Minutes;
diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
index 706ac3f3..38cb7d5d 100644
--- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
+++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs
@@ -12,6 +12,7 @@
using TwitchDownloaderCore;
using TwitchDownloaderCore.Chat;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects;
using TwitchDownloaderCore.TwitchObjects.Gql;
using TwitchDownloaderWPF.Properties;
diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
index 01cd107a..98717176 100644
--- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs
+++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs
@@ -146,7 +146,7 @@ private async Task GetVideoInfo()
var urlTimeCodeMatch = TwitchRegex.UrlTimeCode.Match(textUrl.Text);
if (urlTimeCodeMatch.Success)
{
- var time = TimeSpanExtensions.ParseTimeCode(urlTimeCodeMatch.ValueSpan);
+ var time = UrlTimeCode.Parse(urlTimeCodeMatch.ValueSpan);
checkStart.IsChecked = true;
numStartHour.Value = time.Hours;
numStartMinute.Value = time.Minutes;
diff --git a/TwitchDownloaderWPF/Services/FilenameService.cs b/TwitchDownloaderWPF/Services/FilenameService.cs
deleted file mode 100644
index 5cb3a678..00000000
--- a/TwitchDownloaderWPF/Services/FilenameService.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-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)
- {
- var returnString = fullPath.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
- fullPath = returnString[^1];
- Array.Resize(ref returnString, returnString.Length - 1);
-
- for (var i = 0; i < returnString.Length; i++)
- {
- returnString[i] = RemoveInvalidFilenameChars(returnString[i]);
- }
-
- return returnString;
- }
-
- internal static string GetFilename(string template, string title, string id, DateTime date, string channel, TimeSpan cropStart, TimeSpan cropEnd, string viewCount, string game)
- {
- var videoLength = cropEnd - cropStart;
-
- var stringBuilder = new StringBuilder(template)
- .Replace("{title}", RemoveInvalidFilenameChars(title))
- .Replace("{id}", id)
- .Replace("{channel}", RemoveInvalidFilenameChars(channel))
- .Replace("{date}", date.ToString("Mdyy"))
- .Replace("{random_string}", Path.GetRandomFileName().Replace(".", ""))
- .Replace("{crop_start}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", cropStart))
- .Replace("{crop_end}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", cropEnd))
- .Replace("{length}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", videoLength))
- .Replace("{views}", viewCount)
- .Replace("{game}", game);
-
- 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, RemoveInvalidFilenameChars(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, RemoveInvalidFilenameChars(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, RemoveInvalidFilenameChars(cropEnd.ToString(formatString)));
- }
- else
- {
- cropEndDone = true;
- }
- }
- }
-
- if (template.Contains("{length_custom="))
- {
- var lengthRegex = new Regex("{length_custom=\"(.*)\"}");
- var lengthDone = false;
- while (!lengthDone)
- {
- var lengthMatch = lengthRegex.Match(stringBuilder.ToString());
- if (lengthMatch.Success)
- {
- var formatString = lengthMatch.Groups[1].Value;
- stringBuilder.Remove(lengthMatch.Groups[0].Index, lengthMatch.Groups[0].Length);
- stringBuilder.Insert(lengthMatch.Groups[0].Index, RemoveInvalidFilenameChars(videoLength.ToString(formatString)));
- }
- else
- {
- lengthDone = true;
- }
- }
- }
-
- var fileName = stringBuilder.ToString();
- var additionalSubfolders = GetTemplateSubfolders(ref fileName);
- return Path.Combine(Path.Combine(additionalSubfolders), RemoveInvalidFilenameChars(fileName));
- }
-
- private static string RemoveInvalidFilenameChars(string filename)
- {
- if (string.IsNullOrWhiteSpace(filename))
- {
- return filename;
- }
-
- if (filename.IndexOfAny(Path.GetInvalidFileNameChars()) == -1)
- {
- return filename;
- }
-
- return string.Join('_', filename.Split(Path.GetInvalidFileNameChars()));
- }
- }
-}
\ No newline at end of file
diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
index db63129b..95db422f 100644
--- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
+++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs
@@ -7,6 +7,7 @@
using System.Windows.Media;
using TwitchDownloaderCore.Chat;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
using TwitchDownloaderWPF.Properties;
using TwitchDownloaderWPF.Services;
using TwitchDownloaderWPF.TwitchTasks;