From 5b3ea387f53a935260f442d677e1a65a55f7900a Mon Sep 17 00:00:00 2001 From: Scrub <72096833+ScrubN@users.noreply.github.com> Date: Wed, 2 Aug 2023 21:05:31 -0400 Subject: [PATCH] More video info (#774) * Add views, game, length, and length_custom filename parameters * Update translations Note: Google translate was used to check punctuation * Better memory performance when serializing FFmetadata * Bump ChatRootVersion --- TwitchDownloaderCore/ChatDownloader.cs | 8 +++ TwitchDownloaderCore/Tools/FfmpegMetadata.cs | 13 ++++- .../TwitchObjects/ChatRoot.cs | 2 + .../TwitchObjects/ChatRootInfo.cs | 2 +- .../TwitchObjects/Gql/GqlClipResponse.cs | 10 +++- .../Gql/GqlClipSearchResponse.cs | 8 ++- .../Gql/GqlVideoChapterResponse.cs | 2 +- .../TwitchObjects/Gql/GqlVideoResponse.cs | 8 +++ .../Gql/GqlVideoSearchResponse.cs | 8 ++- TwitchDownloaderWPF/PageChatDownload.xaml.cs | 7 ++- TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 13 ++++- TwitchDownloaderWPF/PageClipDownload.xaml.cs | 6 +- TwitchDownloaderWPF/PageVodDownload.xaml.cs | 10 +++- .../Services/FilenameService.cs | 35 ++++++++++-- .../Translations/Strings.Designer.cs | 4 +- .../Translations/Strings.es.resx | 4 +- .../Translations/Strings.fr.resx | 4 +- .../Translations/Strings.pl.resx | 4 +- TwitchDownloaderWPF/Translations/Strings.resx | 4 +- .../Translations/Strings.ru.resx | 2 +- .../Translations/Strings.tr.resx | 4 +- .../Translations/Strings.zh.resx | 4 +- TwitchDownloaderWPF/TwitchTasks/TaskData.cs | 1 + .../WindowMassDownload.xaml.cs | 2 + .../WindowQueueOptions.xaml.cs | 57 ++++++++++--------- 25 files changed, 161 insertions(+), 61 deletions(-) diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs index 48788d21..e4f2a0dc 100644 --- a/TwitchDownloaderCore/ChatDownloader.cs +++ b/TwitchDownloaderCore/ChatDownloader.cs @@ -269,6 +269,8 @@ public async Task DownloadAsync(IProgress progress, Cancellation double videoEnd = 0.0; double videoDuration = 0.0; double videoTotalLength; + int viewCount; + string game; int connectionCount = downloadOptions.ConnectionCount; if (downloadType == DownloadType.Video) @@ -286,6 +288,8 @@ public async Task DownloadAsync(IProgress progress, Cancellation videoStart = downloadOptions.CropBeginning ? downloadOptions.CropBeginningTime : 0.0; videoEnd = downloadOptions.CropEnding ? downloadOptions.CropEndingTime : videoInfoResponse.data.video.lengthSeconds; videoTotalLength = videoInfoResponse.data.video.lengthSeconds; + viewCount = videoInfoResponse.data.video.viewCount; + game = videoInfoResponse.data.video.game?.displayName ?? "Unknown"; GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(int.Parse(videoId)); foreach (var responseChapter in videoChapterResponse.data.video.moments.edges) @@ -325,6 +329,8 @@ public async Task DownloadAsync(IProgress progress, Cancellation videoStart = (int)clipInfoResponse.data.clip.videoOffsetSeconds; videoEnd = (int)clipInfoResponse.data.clip.videoOffsetSeconds + clipInfoResponse.data.clip.durationSeconds; videoTotalLength = clipInfoResponse.data.clip.durationSeconds; + viewCount = clipInfoResponse.data.clip.viewCount; + game = clipInfoResponse.data.clip.game?.displayName ?? "Unknown"; connectionCount = 1; } @@ -334,6 +340,8 @@ public async Task DownloadAsync(IProgress progress, Cancellation chatRoot.video.start = videoStart; chatRoot.video.end = videoEnd; chatRoot.video.length = videoTotalLength; + chatRoot.video.viewCount = viewCount; + chatRoot.video.game = game; videoDuration = videoEnd - videoStart; var tasks = new List>>(); diff --git a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs index 7731c5f0..aafff4b2 100644 --- a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs +++ b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore.TwitchObjects.Gql; @@ -36,7 +37,7 @@ private static async Task SerializeGlobalMetadata(StreamWriter sw, string stream private static async Task SerializeChapters(StreamWriter sw, List videoMomentEdges, double startOffsetSeconds) { - // Note: Ffmpeg automatically handles out of range chapters for us + // Note: FFmpeg automatically handles out of range chapters for us var startOffsetMillis = (int)(startOffsetSeconds * 1000); foreach (var momentEdge in videoMomentEdges) { @@ -64,11 +65,17 @@ private static string SanitizeKeyValue(string str) return str; } - return str + if (str.AsSpan().IndexOfAny(@"=;#\") == -1) + { + return str; + } + + return new StringBuilder(str) .Replace("=", @"\=") .Replace(";", @"\;") .Replace("#", @"\#") - .Replace(@"\", @"\\"); + .Replace(@"\", @"\\") + .ToString(); } } } diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs index 58cc4d45..7413b469 100644 --- a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs +++ b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs @@ -198,6 +198,8 @@ public class Video public double start { get; set; } public double end { get; set; } public double length { get; set; } = -1; + public int viewCount { get; set; } + public string game { get; set; } public List chapters { get; set; } = new(); #region DeprecatedProperties diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs b/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs index 9a16854c..e4b60225 100644 --- a/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs +++ b/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs @@ -18,7 +18,7 @@ public class ChatRootVersion public int Minor { get; set; } = 0; public int Patch { get; set; } = 0; - public static ChatRootVersion CurrentVersion { get; } = new(1, 3, 0); + public static ChatRootVersion CurrentVersion { get; } = new(1, 3, 1); // Constructors /// diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs index fe5bd79b..0ee116c8 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace TwitchDownloaderCore.TwitchObjects.Gql { @@ -15,6 +13,12 @@ public class ClipVideo public string id { get; set; } } + public class ClipGame + { + public string id { get; set; } + public string displayName { get; set; } + } + public class Clip { public string title { get; set; } @@ -24,6 +28,8 @@ public class Clip public ClipBroadcaster broadcaster { get; set; } public int? videoOffsetSeconds { get; set; } public ClipVideo video { get; set; } + public int viewCount { get; set; } + public ClipGame game { get; set; } } public class ClipData diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs index 25b7ff6a..b6c5b2eb 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; -using System.Text; namespace TwitchDownloaderCore.TwitchObjects.Gql { + public class ClipNodeGame + { + public string id { get; set; } + public string displayName { get; set; } + } + public class ClipNode { public string id { get; set; } @@ -13,6 +18,7 @@ public class ClipNode public int durationSeconds { get; set; } public string thumbnailURL { get; set; } public int viewCount { get; set; } + public ClipNodeGame game { get; set; } } public class Edge diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoChapterResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoChapterResponse.cs index ddfa03b5..a2dadfd2 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoChapterResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoChapterResponse.cs @@ -23,7 +23,7 @@ public class GameChangeMomentDetails public class VideoMoment { - public VideoMomentConnection moments { get; set; } // seemingly always blank. Probably needs Oauth in the request to be populated + public VideoMomentConnection moments { get; set; } // seemingly always blank. Oauth does not seem to make a difference public string id { get; set; } public int durationMilliseconds { get; set; } public int positionMilliseconds { get; set; } diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs index 914d0800..d1489901 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs @@ -9,6 +9,12 @@ public class VideoOwner public string displayName { get; set; } } + public class VideoGame + { + public string id { get; set; } + public string displayName { get; set; } + } + public class VideoInfo { public string title { get; set; } @@ -16,6 +22,8 @@ public class VideoInfo public DateTime createdAt { get; set; } public int lengthSeconds { get; set; } public VideoOwner owner { get; set; } + public int viewCount { get; set; } + public VideoGame game { get; set; } } public class VideoData diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoSearchResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoSearchResponse.cs index 857697fd..99b0902d 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoSearchResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoSearchResponse.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; -using System.Text; namespace TwitchDownloaderCore.TwitchObjects.Gql { + public class VideoNodeGame + { + public string id { get; set; } + public string displayName { get; set; } + } + public class VideoNode { public string title { get; set; } @@ -12,6 +17,7 @@ public class VideoNode public string previewThumbnailURL { get; set; } public DateTime createdAt { get; set; } public int viewCount { get; set; } + public VideoNodeGame game { get; set; } } public class VideoEdge diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs index 73efa0ee..0e3e8ce0 100644 --- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs @@ -33,6 +33,8 @@ public partial class PageChatDownload : Page public int streamerId; public DateTime currentVideoTime; public TimeSpan vodLength; + public int viewCount; + public string game; private CancellationTokenSource _cancellationTokenSource; public PageChatDownload() @@ -136,6 +138,8 @@ private async Task GetVideoInfo() textCreatedAt.Text = Settings.Default.UTCVideoTime ? videoTime.ToString(CultureInfo.CurrentCulture) : videoTime.ToLocalTime().ToString(CultureInfo.CurrentCulture); currentVideoTime = Settings.Default.UTCVideoTime ? videoTime : videoTime.ToLocalTime(); streamerId = int.Parse(videoInfo.data.video.owner.id); + viewCount = videoInfo.data.video.viewCount; + game = videoInfo.data.video.game?.displayName ?? "Unknown"; var urlTimeCodeMatch = Regex.Match(textUrl.Text, @"(?<=\?t=)\d+h\d+m\d+s"); if (urlTimeCodeMatch.Success) { @@ -469,7 +473,8 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) 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); + checkCropEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength, + viewCount.ToString(), game); if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs index b80659a9..1650595e 100644 --- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs +++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs @@ -31,6 +31,8 @@ public partial class PageChatUpdate : Page public string VideoId; public DateTime VideoCreatedAt; public TimeSpan VideoLength; + public int ViewCount; + public string Game; private CancellationTokenSource _cancellationTokenSource; public PageChatUpdate() @@ -62,7 +64,7 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) ChatJsonInfo.comments.RemoveRange(1, ChatJsonInfo.comments.Count - 2); GC.Collect(); - var videoCreatedAt = ChatJsonInfo.video.created_at == DateTime.MinValue + var videoCreatedAt = ChatJsonInfo.video.created_at == default ? ChatJsonInfo.comments[0].created_at - TimeSpan.FromSeconds(ChatJsonInfo.comments[0].content_offset_seconds) : ChatJsonInfo.video.created_at; textCreatedAt.Text = Settings.Default.UTCVideoTime ? videoCreatedAt.ToString(CultureInfo.CurrentCulture) : videoCreatedAt.ToLocalTime().ToString(CultureInfo.CurrentCulture); @@ -87,6 +89,8 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) : Translations.Strings.Unknown; VideoId = ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1"; + ViewCount = ChatJsonInfo.video.viewCount; + Game = ChatJsonInfo.video.game ?? ChatJsonInfo.video.chapters.FirstOrDefault()?.gameDisplayName ?? "Unknown"; if (VideoId.All(char.IsDigit)) { @@ -108,6 +112,8 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) labelLength.Text = VideoLength.ToString("c"); numStartHour.Maximum = (int)VideoLength.TotalHours; numEndHour.Maximum = (int)VideoLength.TotalHours; + ViewCount = videoInfo.data.video.viewCount; + Game = videoInfo.data.video.game?.displayName; try { @@ -146,6 +152,8 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) { VideoLength = TimeSpan.FromSeconds(videoInfo.data.clip.durationSeconds); labelLength.Text = VideoLength.ToString("c"); + ViewCount = videoInfo.data.clip.viewCount; + Game = videoInfo.data.clip.game?.displayName; try { @@ -471,7 +479,8 @@ private async void SplitBtnUpdate_Click(object sender, RoutedEventArgs e) 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); + checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : VideoLength, + ViewCount.ToString(), Game); if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs index 8740483a..cbaeac7e 100644 --- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs @@ -27,6 +27,8 @@ public partial class PageClipDownload : Page public string clipId = ""; public DateTime currentVideoTime; public TimeSpan clipLength; + public int viewCount; + public string game; private CancellationTokenSource _cancellationTokenSource; public PageClipDownload() @@ -79,6 +81,8 @@ private async Task GetClipInfo() currentVideoTime = Settings.Default.UTCVideoTime ? clipCreatedAt : clipCreatedAt.ToLocalTime(); textTitle.Text = clipData.data.clip.title; labelLength.Text = clipLength.ToString("c"); + viewCount = taskClipInfo.Result.data.clip.viewCount; + game = taskClipInfo.Result.data.clip.game?.displayName ?? "Unknown"; foreach (var quality in taskLinks.Result[0].data.clip.videoQualities) { @@ -180,7 +184,7 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) SaveFileDialog saveFileDialog = new SaveFileDialog { Filter = "MP4 Files | *.mp4", - FileName = FilenameService.GetFilename(Settings.Default.TemplateClip, textTitle.Text, clipId, currentVideoTime, textStreamer.Text, TimeSpan.Zero, clipLength) + FileName = FilenameService.GetFilename(Settings.Default.TemplateClip, textTitle.Text, clipId, currentVideoTime, textStreamer.Text, TimeSpan.Zero, clipLength, viewCount.ToString(), game) }; if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs index 1f2d3893..ea923e08 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs @@ -34,6 +34,8 @@ public partial class PageVodDownload : Page public int currentVideoId; public DateTime currentVideoTime; public TimeSpan vodLength; + public int viewCount; + public string game; private CancellationTokenSource _cancellationTokenSource; public PageVodDownload() @@ -170,6 +172,8 @@ private async Task GetVideoInfo() numEndMinute.Value = vodLength.Minutes; numEndSecond.Value = vodLength.Seconds; labelLength.Text = vodLength.ToString("c"); + viewCount = taskVideoInfo.Result.data.video.viewCount; + game = taskVideoInfo.Result.data.video.game?.displayName ?? "Unknown"; UpdateVideoSizeEstimates(); @@ -209,7 +213,8 @@ public VideoDownloadOptions GetOptions(string filename, string folder) : -1, 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"), + checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength, + viewCount.ToString(), game) + ".mp4"), Oauth = TextOauth.Text, Quality = GetQualityWithoutSize(comboQuality.Text).ToString(), Id = currentVideoId, @@ -407,7 +412,8 @@ private async void SplitBtnDownloader_Click(object sender, RoutedEventArgs e) Filter = "MP4 Files | *.mp4", 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) + checkEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength, + viewCount.ToString(), game) }; if (saveFileDialog.ShowDialog() == false) { diff --git a/TwitchDownloaderWPF/Services/FilenameService.cs b/TwitchDownloaderWPF/Services/FilenameService.cs index 17f51785..5cb3a678 100644 --- a/TwitchDownloaderWPF/Services/FilenameService.cs +++ b/TwitchDownloaderWPF/Services/FilenameService.cs @@ -22,16 +22,21 @@ private static string[] GetTemplateSubfolders(ref string fullPath) return returnString; } - internal static string GetFilename(string template, string title, string id, DateTime date, string channel, TimeSpan cropStart, TimeSpan cropEnd) + 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.GetFileNameWithoutExtension(Path.GetRandomFileName())) + .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("{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=")) { @@ -93,8 +98,28 @@ internal static string GetFilename(string template, string title, string id, Dat } } - string fileName = stringBuilder.ToString(); - string[] additionalSubfolders = GetTemplateSubfolders(ref fileName); + 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)); } diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs index 87c5fdd4..88f1bd47 100644 --- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs +++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs @@ -753,7 +753,7 @@ public static string FfzEmotes { } /// - /// 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=""}. + /// 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=""} {length} {length_custom=""} {views} {game}. /// public static string FilenameTemplateParameters { get { @@ -1536,7 +1536,7 @@ public static string ThirdPartyEmotesTooltip { } /// - /// Looks up a localized string similar to crop_start_custom and crop_end_custom formattings are based on the. + /// Looks up a localized string similar to crop_start_custom, crop_end_custom, and length_custom formattings are based on the. /// public static string TimeSpanCustomFormatting { get { diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx index 3528bfd1..cbd5423d 100644 --- a/TwitchDownloaderWPF/Translations/Strings.es.resx +++ b/TwitchDownloaderWPF/Translations/Strings.es.resx @@ -298,7 +298,7 @@ Emotes FFZ: - {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} No traducir @@ -733,7 +733,7 @@ Escala de contorno: - Los formatos crop_start_custom y crop_end_custom se basan en el formato + Los formatos crop_start_custom, crop_end_custom y length_custom se basan en el formato Cadenas de formato de intervalo de tiempo estándar de C# diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx index 90717194..d41dc472 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} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} Do not translate @@ -733,7 +733,7 @@ Échelle des contours: - crop_start_custom and crop_end_custom formattings are based on the + crop_start_custom, crop_end_custom et length_custom formattings are based on the C# standard TimeSpan format strings diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx index 4d9423af..b97f3de9 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} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} Do not translate @@ -733,7 +733,7 @@ Outline Scale: - formatowanie crop_start_custom i crop_end_custom jest bazowane na + formatowanie crop_start_custom, crop_end_custom, i length_custom jest bazowane na Standardowych ciągach formatujących TimeSpan C# diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx index ef3ae3a5..39fa3d4f 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} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} Do not translate @@ -732,7 +732,7 @@ Outline Scale: - crop_start_custom and crop_end_custom formattings are based on the + crop_start_custom, crop_end_custom, and length_custom formattings are based on the C# standard TimeSpan format strings diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx index afcbc504..5fe8b395 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} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} Do not translate diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx index ba1419aa..ad86426c 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} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} Do not translate @@ -734,7 +734,7 @@ Anahat Ölçeği: - crop_start_custom and crop_end_custom formattings are based on the + crop_start_custom, crop_end_custom, and length_custom formattings are based on the C# standart TimeSpan biçim dizeleri diff --git a/TwitchDownloaderWPF/Translations/Strings.zh.resx b/TwitchDownloaderWPF/Translations/Strings.zh.resx index b919301d..5e10962e 100644 --- a/TwitchDownloaderWPF/Translations/Strings.zh.resx +++ b/TwitchDownloaderWPF/Translations/Strings.zh.resx @@ -298,7 +298,7 @@ FFZ Emotes: - {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} + {title} {id} {date} {channel} {date_custom=""} {random_string} {crop_start} {crop_end} {crop_start_custom=""} {crop_end_custom=""} {length} {length_custom=""} {views} {game} Do not translate @@ -732,7 +732,7 @@ 边框比例: - crop_start_custom和crop_end_custom格式基于 + crop_start_custom、crop_end_custom和length_custom格式基于 C#标准的时间跨度格式字符串 diff --git a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs index 2fe618b7..29f89b0b 100644 --- a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs +++ b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs @@ -12,6 +12,7 @@ public class TaskData public DateTime Time { get; set; } public int Length { get; set; } public int Views { get; set; } + public string Game { get; set; } public string LengthFormatted { get diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs index 1db4ae4f..3233edca 100644 --- a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs +++ b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs @@ -78,6 +78,7 @@ private async Task UpdateList() data.Time = Settings.Default.UTCVideoTime ? video.node.createdAt : video.node.createdAt.ToLocalTime(); data.Views = video.node.viewCount; data.Streamer = currentChannel; + data.Game = video.node.game?.displayName ?? "Unknown"; try { var bitmapImage = new BitmapImage(); @@ -126,6 +127,7 @@ private async Task UpdateList() data.Time = Settings.Default.UTCVideoTime ? clip.node.createdAt : clip.node.createdAt.ToLocalTime(); data.Views = clip.node.viewCount; data.Streamer = currentChannel; + data.Game = clip.node.game?.displayName ?? "Unknown"; try { var bitmapImage = new BitmapImage(); diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs index dedd9591..93874879 100644 --- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs +++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs @@ -198,7 +198,7 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ClipDownloadTask downloadTask = new ClipDownloadTask(); ClipDownloadOptions downloadOptions = new ClipDownloadOptions(); - 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.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateClip, clipPage.textTitle.Text, clipPage.clipId, clipPage.currentVideoTime, clipPage.textStreamer.Text, TimeSpan.Zero, clipPage.clipLength, clipPage.viewCount.ToString(), clipPage.game) + ".mp4"); downloadOptions.Id = clipPage.clipId; downloadOptions.Quality = clipPage.comboQuality.Text; downloadOptions.ThrottleKib = Settings.Default.DownloadThrottleEnabled @@ -227,7 +227,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, FilenameService.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id, clipPage.currentVideoTime, clipPage.textStreamer.Text, TimeSpan.Zero, clipPage.clipLength) + "." + 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, clipPage.viewCount.ToString(), clipPage.game) + "." + chatOptions.FileExtension); chatTask.DownloadOptions = chatOptions; chatTask.Info.Title = clipPage.textTitle.Text; @@ -279,8 +279,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ChatDownloadOptions chatOptions = MainWindow.pageChatDownload.GetOptions(null); chatOptions.Id = chatPage.downloadId; 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); + chatOptions.CropBeginning ? TimeSpan.FromSeconds(chatOptions.CropBeginningTime) : TimeSpan.Zero, + chatOptions.CropEnding ? TimeSpan.FromSeconds(chatOptions.CropEndingTime) : chatPage.vodLength, + chatPage.viewCount.ToString(), chatPage.game) + "." + chatOptions.FileExtension); chatTask.DownloadOptions = chatOptions; chatTask.Info.Title = chatPage.textTitle.Text; @@ -326,8 +327,9 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) ChatUpdateOptions chatOptions = MainWindow.pageChatUpdate.GetOptions(null); chatOptions.InputFile = chatPage.InputFile; 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); + chatOptions.CropBeginning ? TimeSpan.FromSeconds(chatOptions.CropBeginningTime) : TimeSpan.Zero, + chatOptions.CropEnding ? TimeSpan.FromSeconds(chatOptions.CropEndingTime) : chatPage.VideoLength, + chatPage.ViewCount.ToString(), chatPage.Game) + "." + chatOptions.FileExtension); chatTask.UpdateOptions = chatOptions; chatTask.Info.Title = chatPage.textTitle.Text; @@ -390,15 +392,16 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) for (int i = 0; i < dataList.Count; i++) { + var taskData = dataList[i]; if ((bool)checkVideo.IsChecked) { - if (dataList[i].Id.All(Char.IsDigit)) + if (taskData.Id.All(Char.IsDigit)) { VodDownloadTask downloadTask = new VodDownloadTask(); VideoDownloadOptions downloadOptions = new VideoDownloadOptions(); downloadOptions.Oauth = Settings.Default.OAuth; downloadOptions.TempFolder = Settings.Default.TempPath; - downloadOptions.Id = int.Parse(dataList[i].Id); + downloadOptions.Id = int.Parse(taskData.Id); downloadOptions.FfmpegPath = "ffmpeg"; downloadOptions.CropBeginning = false; downloadOptions.CropEnding = false; @@ -406,12 +409,13 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) downloadOptions.ThrottleKib = Settings.Default.DownloadThrottleEnabled ? Settings.Default.MaximumBandwidthKib : -1; - 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"); + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateVod, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer, + downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero, + downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : TimeSpan.FromSeconds(taskData.Length), + taskData.Views.ToString(), taskData.Game) + ".mp4"); downloadTask.DownloadOptions = downloadOptions; - downloadTask.Info.Title = dataList[i].Title; - downloadTask.Info.Thumbnail = dataList[i].Thumbnail; + downloadTask.Info.Title = taskData.Title; + downloadTask.Info.Thumbnail = taskData.Thumbnail; downloadTask.ChangeStatus(TwitchTaskStatus.Ready); lock (PageQueue.taskLock) @@ -423,15 +427,15 @@ 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, 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"); + downloadOptions.Id = taskData.Id; + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateClip, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer, + TimeSpan.Zero, TimeSpan.FromSeconds(taskData.Length), taskData.Views.ToString(), taskData.Game) + ".mp4"); downloadOptions.ThrottleKib = Settings.Default.DownloadThrottleEnabled ? Settings.Default.MaximumBandwidthKib : -1; downloadTask.DownloadOptions = downloadOptions; - downloadTask.Info.Title = dataList[i].Title; - downloadTask.Info.Thumbnail = dataList[i].Thumbnail; + downloadTask.Info.Title = taskData.Title; + downloadTask.Info.Thumbnail = taskData.Thumbnail; downloadTask.ChangeStatus(TwitchTaskStatus.Ready); lock (PageQueue.taskLock) @@ -454,15 +458,16 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) downloadOptions.Compression = RadioCompressionNone.IsChecked == true ? ChatCompression.None : ChatCompression.Gzip; downloadOptions.EmbedData = (bool)checkEmbed.IsChecked; downloadOptions.TimeFormat = TimestampFormat.Relative; - downloadOptions.Id = dataList[i].Id; + downloadOptions.Id = taskData.Id; downloadOptions.CropBeginning = false; downloadOptions.CropEnding = false; - 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); + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer, + downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero, + downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : TimeSpan.FromSeconds(taskData.Length), + taskData.Views.ToString(), taskData.Game) + "." + downloadOptions.FileExtension); downloadTask.DownloadOptions = downloadOptions; - downloadTask.Info.Title = dataList[i].Title; - downloadTask.Info.Thumbnail = dataList[i].Thumbnail; + downloadTask.Info.Title = taskData.Title; + downloadTask.Info.Thumbnail = taskData.Thumbnail; downloadTask.ChangeStatus(TwitchTaskStatus.Ready); lock (PageQueue.taskLock) @@ -481,8 +486,8 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) } renderOptions.InputFile = downloadOptions.Filename; renderTask.DownloadOptions = renderOptions; - renderTask.Info.Title = dataList[i].Title; - renderTask.Info.Thumbnail = dataList[i].Thumbnail; + renderTask.Info.Title = taskData.Title; + renderTask.Info.Thumbnail = taskData.Thumbnail; renderTask.ChangeStatus(TwitchTaskStatus.Waiting); renderTask.DependantTask = downloadTask;