From 8bb9fc1944169f986d632b5a75857cb634e6cfce Mon Sep 17 00:00:00 2001 From: Scrub <72096833+ScrubN@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:34:41 -0500 Subject: [PATCH] Add `{channel_id}`, `{clipper}` and `{clipper_id}` filename parameters (#1247) * Add support for channel_id, clipper, and clipper_id filename params * Update tests * Implement new filename params support in GUI * Add new filename params to settings window & update translations * Add clipper to ChatRoot * Support clip name params in chat updater page * Fallback to web request if null * Support new filename params in non-url mass downloaders * Bump ChatRootVersion * TaskData.Streamer -> TaskData.StreamerName * Return streamer displayName instead of login * Forgot to stage * Store streamer/clipper logins in chatroot * Use array instead of list * Rename broadcaster comment variable * Ignore case when comparing channel text --- .../ToolTests/FilenameServiceTests.cs | 67 ++++++++++--------- TwitchDownloaderCore/Chat/ChatJson.cs | 40 +++++++++-- TwitchDownloaderCore/ChatDownloader.cs | 8 +++ TwitchDownloaderCore/ChatUpdater.cs | 7 ++ TwitchDownloaderCore/Tools/FilenameService.cs | 6 +- TwitchDownloaderCore/TwitchHelper.cs | 20 +++--- .../TwitchObjects/ChatRoot.cs | 9 +++ .../TwitchObjects/ChatRootInfo.cs | 2 +- .../Gql/GqlClipSearchResponse.cs | 7 ++ .../TwitchObjects/Gql/GqlUserIdResponse.cs | 18 +++++ .../TwitchObjects/Gql/GqlUserInfoResponse.cs | 1 + TwitchDownloaderWPF/PageChatDownload.xaml.cs | 16 +++-- TwitchDownloaderWPF/PageChatUpdate.xaml.cs | 24 ++++--- TwitchDownloaderWPF/PageClipDownload.xaml.cs | 9 ++- TwitchDownloaderWPF/PageVodDownload.xaml.cs | 6 +- .../Translations/Strings.Designer.cs | 27 ++++++++ .../Translations/Strings.es.resx | 9 +++ .../Translations/Strings.fr.resx | 9 +++ .../Translations/Strings.it.resx | 9 +++ .../Translations/Strings.ja.resx | 9 +++ .../Translations/Strings.pl.resx | 9 +++ .../Translations/Strings.pt-br.resx | 9 +++ TwitchDownloaderWPF/Translations/Strings.resx | 9 +++ .../Translations/Strings.ru.resx | 9 +++ .../Translations/Strings.tr.resx | 9 +++ .../Translations/Strings.uk.resx | 9 +++ .../Translations/Strings.zh-cn.resx | 9 +++ .../Translations/Strings.zh-tw.resx | 9 +++ TwitchDownloaderWPF/TwitchTasks/TaskData.cs | 5 +- .../WindowMassDownload.xaml.cs | 43 +++++++++--- .../WindowQueueOptions.xaml.cs | 22 +++--- TwitchDownloaderWPF/WindowSettings.xaml | 9 +++ TwitchDownloaderWPF/WindowUrlList.xaml.cs | 8 ++- 33 files changed, 377 insertions(+), 85 deletions(-) create mode 100644 TwitchDownloaderCore/TwitchObjects/Gql/GqlUserIdResponse.cs diff --git a/TwitchDownloaderCore.Tests/ToolTests/FilenameServiceTests.cs b/TwitchDownloaderCore.Tests/ToolTests/FilenameServiceTests.cs index 2a6a02cc..8765975b 100644 --- a/TwitchDownloaderCore.Tests/ToolTests/FilenameServiceTests.cs +++ b/TwitchDownloaderCore.Tests/ToolTests/FilenameServiceTests.cs @@ -4,28 +4,31 @@ namespace TwitchDownloaderCore.Tests.ToolTests { public class FilenameServiceTests { - private static (string title, string id, DateTime date, string channel, TimeSpan trimStart, TimeSpan trimEnd, int viewCount, string game) GetExampleInfo() => - ("A Title", "abc123", new DateTime(1984, 11, 1, 9, 43, 21), "streamer8", new TimeSpan(0, 1, 2, 3, 4), new TimeSpan(0, 5, 6, 7, 8), 123456789, "A Game"); + private static (string title, string id, DateTime date, string channel, string channelId, TimeSpan trimStart, TimeSpan trimEnd, int viewCount, string game, string clipper, string clipperId) GetExampleInfo() => + ("A Title", "abc123", new DateTime(1984, 11, 1, 9, 43, 21), "streamer8", "123456789", new TimeSpan(0, 1, 2, 3, 4), new TimeSpan(0, 5, 6, 7, 8), 123456789, "A Game", "viewer8", "987654321"); [Theory] [InlineData("{title}", "A Title")] [InlineData("{id}", "abc123")] [InlineData("{channel}", "streamer8")] + [InlineData("{channel_id}", "123456789")] [InlineData("{date}", "11-1-84")] [InlineData("{trim_start}", "01-02-03")] [InlineData("{trim_end}", "05-06-07")] [InlineData("{length}", "04-04-04")] [InlineData("{views}", "123456789")] [InlineData("{game}", "A Game")] + [InlineData("{clipper}", "viewer8")] + [InlineData("{clipper_id}", "987654321")] [InlineData("{date_custom=\"s\"}", "1984-11-01T09_43_21")] [InlineData("{trim_start_custom=\"hh\\-mm\\-ss\"}", "01-02-03")] [InlineData("{trim_end_custom=\"hh\\-mm\\-ss\"}", "05-06-07")] [InlineData("{length_custom=\"hh\\-mm\\-ss\"}", "04-04-04")] public void CorrectlyGeneratesIndividualTemplates(string template, string expected) { - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(template, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(template, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(expected, result); } @@ -36,9 +39,9 @@ public void CorrectlyGeneratesIndividualTemplates(string template, string expect [InlineData("{title} by {channel} playing {game} on {date_custom=\"M dd, yyyy\"} for {length_custom=\"h'h 'm'm 's's'\"} with {views} views", "A Title by streamer8 playing A Game on 11 01, 1984 for 4h 4m 4s with 123456789 views")] public void CorrectlyGeneratesLargeTemplates(string template, string expected) { - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(template, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(template, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(expected, result); } @@ -48,9 +51,9 @@ public void CorrectlyInterpretsMultipleCustomParameters() { const string TEMPLATE = "{date_custom=\"yyyy\"} {date_custom=\"MM\"} {date_custom=\"dd\"} {trim_start_custom=\"hh\\-mm\\-ss\"} {trim_end_custom=\"hh\\-mm\\-ss\"} {length_custom=\"hh\\-mm\\-ss\"}"; const string EXPECTED = "1984 11 01 01-02-03 05-06-07 04-04-04"; - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(EXPECTED, result); } @@ -60,9 +63,9 @@ public void CorrectlyGeneratesSubFolders_WithForwardSlash() { const string TEMPLATE = "{channel}/{date_custom=\"yyyy\"}/{date_custom=\"MM\"}/{date_custom=\"dd\"}/{title}.mp4"; var expected = Path.Combine("streamer8", "1984", "11", "01", "A Title.mp4"); - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(expected, result); } @@ -72,25 +75,29 @@ public void CorrectlyGeneratesSubFolders_WithBackSlash() { const string TEMPLATE = "{channel}\\{date_custom=\"yyyy\"}\\{date_custom=\"MM\"}\\{date_custom=\"dd\"}\\{title}"; var expected = Path.Combine("streamer8", "1984", "11", "01", "A Title"); - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(expected, result); } [Theory] - [InlineData("{title}", ""*:<>?|/\")] - [InlineData("{id}", ""*:<>?|/\")] - [InlineData("{channel}", ""*:<>?|/\")] - [InlineData("{game}", ""*:<>?|/\")] - public void CorrectlyReplacesInvalidCharactersForNonCustomTemplates(string template, string expected) + [InlineData("{title}")] + [InlineData("{id}")] + [InlineData("{channel}")] + [InlineData("{channel_id}")] + [InlineData("{clipper}")] + [InlineData("{clipper_id}")] + [InlineData("{game}")] + public void CorrectlyReplacesInvalidCharactersForNonCustomTemplates(string template) { const string INVALID_CHARS = "\"*:<>?|/\\"; + const string EXPECTED = ""*:<>?|/\"; - var result = FilenameService.GetFilename(template, INVALID_CHARS, INVALID_CHARS, default, INVALID_CHARS, default, default, default, INVALID_CHARS); + var result = FilenameService.GetFilename(template, INVALID_CHARS, INVALID_CHARS, default, INVALID_CHARS, INVALID_CHARS, default, default, default, INVALID_CHARS, INVALID_CHARS, INVALID_CHARS); - Assert.Equal(expected, result); + Assert.Equal(EXPECTED, result); } [Theory] @@ -104,7 +111,7 @@ public void CorrectlyReplacesInvalidCharactersForCustomTemplates(string template const string INVALID_CHARS = "\"*:<>?|/\\\\"; var template = templateStart + INVALID_CHARS + "'\"}"; - var result = FilenameService.GetFilename(template, INVALID_CHARS, INVALID_CHARS, default, INVALID_CHARS, default, default, default, INVALID_CHARS); + var result = FilenameService.GetFilename(template, INVALID_CHARS, INVALID_CHARS, default, INVALID_CHARS, INVALID_CHARS, default, default, default, INVALID_CHARS, INVALID_CHARS, INVALID_CHARS); Assert.Equal(EXPECTED, result); } @@ -116,9 +123,9 @@ public void CorrectlyReplacesInvalidCharactersForSubFolders() const string FULL_WIDTH_CHARS = ""*:<>?|"; const string TEMPLATE = INVALID_CHARS + "\\{title}"; var expected = Path.Combine(FULL_WIDTH_CHARS, "A Title"); - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(expected, result); } @@ -127,10 +134,10 @@ public void CorrectlyReplacesInvalidCharactersForSubFolders() public void RandomStringIsRandom() { const string TEMPLATE = "{random_string}"; - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); - var result2 = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); + var result2 = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.NotEqual(result, result2); } @@ -140,9 +147,9 @@ public void DoesNotInterpretBogusTemplateParameter() { const string TEMPLATE = "{foobar}"; const string EXPECTED = "{foobar}"; - var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo(); + var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo(); - var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game); + var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId); Assert.Equal(EXPECTED, result); } @@ -150,10 +157,10 @@ public void DoesNotInterpretBogusTemplateParameter() [Fact] public void GetFilenameDoesNotThrow_WhenNullOrDefaultInput() { - const string TEMPLATE = "{title}_{id}_{date}_{channel}_{trim_start}_{trim_end}_{length}_{views}_{game}_{date_custom=\"s\"}_{trim_start_custom=\"hh\\-mm\\-ss\"}_{trim_end_custom=\"hh\\-mm\\-ss\"}_{length_custom=\"hh\\-mm\\-ss\"}"; - const string EXPECTED = "__1-1-01__00-00-00_00-00-00_00-00-00_0__0001-01-01T00_00_00_00-00-00_00-00-00_00-00-00"; + const string TEMPLATE = "{title}_{id}_{date}_{channel}_{channel_id}_{trim_start}_{trim_end}_{length}_{views}_{game}_{clipper}_{clipper_id}_{date_custom=\"s\"}_{trim_start_custom=\"hh\\-mm\\-ss\"}_{trim_end_custom=\"hh\\-mm\\-ss\"}_{length_custom=\"hh\\-mm\\-ss\"}"; + const string EXPECTED = "__1-1-01___00-00-00_00-00-00_00-00-00_0____0001-01-01T00_00_00_00-00-00_00-00-00_00-00-00"; - var result = FilenameService.GetFilename(TEMPLATE, default, default, default, default, default, default, default, default); + var result = FilenameService.GetFilename(TEMPLATE, default, default, default, default, default, default, default, default, default, default, default); Assert.Equal(EXPECTED, result); } diff --git a/TwitchDownloaderCore/Chat/ChatJson.cs b/TwitchDownloaderCore/Chat/ChatJson.cs index 137f67b4..65f88eba 100644 --- a/TwitchDownloaderCore/Chat/ChatJson.cs +++ b/TwitchDownloaderCore/Chat/ChatJson.cs @@ -192,25 +192,51 @@ private static async Task UpgradeChatJson(ChatRoot chatRoot) if (chatRoot.streamer is null) { - var broadcaster = new Lazy(() => - chatRoot.comments - .Where(x => x.message.user_badges != null) - .FirstOrDefault(x => x.message.user_badges.Any(b => b._id.Equals("broadcaster")))); + var broadcasterComment = chatRoot.comments + .Where(x => x.message.user_badges != null) + .FirstOrDefault(x => x.message.user_badges.Any(b => b._id.Equals("broadcaster"))); if (!int.TryParse(chatRoot.video.user_id, out var assumedId)) { if (chatRoot.comments.FirstOrDefault(x => int.TryParse(x.channel_id, out assumedId)) is null) { - if (!int.TryParse(broadcaster.Value?.commenter._id, out assumedId)) + if (!int.TryParse(broadcasterComment?.commenter._id, out assumedId)) { assumedId = 0; } } } - var assumedName = chatRoot.video.user_name ?? broadcaster.Value?.commenter.display_name ?? await TwitchHelper.GetStreamerName(assumedId); + var assumedName = chatRoot.video.user_name ?? broadcasterComment?.commenter.display_name; + var assumedLogin = broadcasterComment?.commenter.name; - chatRoot.streamer = new Streamer { id = assumedId, name = assumedName }; + if ((assumedName is null || assumedLogin is null) && assumedId != 0) + { + try + { + var userInfo = await TwitchHelper.GetUserInfo(new[] { assumedId.ToString() }); + assumedName ??= userInfo.data.users.FirstOrDefault()?.displayName; + assumedLogin ??= userInfo.data.users.FirstOrDefault()?.login; + } + catch { /* ignored */ } + } + + chatRoot.streamer = new Streamer + { + name = assumedName, + login = assumedLogin, + id = assumedId + }; + } + + if (chatRoot.streamer.login is null && chatRoot.streamer.id != 0) + { + try + { + var userInfo = await TwitchHelper.GetUserInfo(new[] { chatRoot.streamer.id.ToString() }); + chatRoot.streamer.login = userInfo.data.users.FirstOrDefault()?.login; + } + catch { /* ignored */ } } if (chatRoot.video.user_name is not null) diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs index f33ae961..0e20aaf0 100644 --- a/TwitchDownloaderCore/ChatDownloader.cs +++ b/TwitchDownloaderCore/ChatDownloader.cs @@ -331,6 +331,7 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF } chatRoot.streamer.name = videoInfoResponse.data.video.owner.displayName; + chatRoot.streamer.login = videoInfoResponse.data.video.owner.login; chatRoot.streamer.id = int.Parse(videoInfoResponse.data.video.owner.id); chatRoot.video.description = videoInfoResponse.data.video.description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd(); chatRoot.video.title = videoInfoResponse.data.video.title; @@ -371,7 +372,14 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF videoId = clipInfoResponse.data.clip.video.id; chatRoot.streamer.name = clipInfoResponse.data.clip.broadcaster.displayName; + chatRoot.streamer.login = clipInfoResponse.data.clip.broadcaster.login; chatRoot.streamer.id = int.Parse(clipInfoResponse.data.clip.broadcaster.id); + chatRoot.clipper = new Clipper + { + name = clipInfoResponse.data.clip.curator.displayName, + login = clipInfoResponse.data.clip.curator.login, + id = int.Parse(clipInfoResponse.data.clip.curator.id), + }; chatRoot.video.title = clipInfoResponse.data.clip.title; chatRoot.video.created_at = clipInfoResponse.data.clip.createdAt; chatRoot.video.start = (double)clipInfoResponse.data.clip.videoOffsetSeconds + (downloadOptions.TrimBeginning ? downloadOptions.TrimBeginningTime : 0); diff --git a/TwitchDownloaderCore/ChatUpdater.cs b/TwitchDownloaderCore/ChatUpdater.cs index 1edcaa41..aee1f5f1 100644 --- a/TwitchDownloaderCore/ChatUpdater.cs +++ b/TwitchDownloaderCore/ChatUpdater.cs @@ -174,6 +174,13 @@ private async Task UpdateVideoInfo(int totalSteps, int currentStep, Cancellation return; } + chatRoot.clipper ??= new Clipper + { + name = clipInfo.curator.displayName, + login = clipInfo.curator.login, + id = int.Parse(clipInfo.curator.id), + }; + chatRoot.video.title = clipInfo.title; chatRoot.video.created_at = clipInfo.createdAt; chatRoot.video.length = clipInfo.durationSeconds; diff --git a/TwitchDownloaderCore/Tools/FilenameService.cs b/TwitchDownloaderCore/Tools/FilenameService.cs index e59cb99e..4ba70011 100644 --- a/TwitchDownloaderCore/Tools/FilenameService.cs +++ b/TwitchDownloaderCore/Tools/FilenameService.cs @@ -10,7 +10,8 @@ namespace TwitchDownloaderCore.Tools { public static class FilenameService { - public static string GetFilename(string template, [AllowNull] string title, [AllowNull] string id, DateTime date, [AllowNull] string channel, TimeSpan trimStart, TimeSpan trimEnd, long viewCount, [AllowNull] string game) + public static string GetFilename(string template, [AllowNull] string title, [AllowNull] string id, DateTime date, [AllowNull] string channel, [AllowNull] string channelId, TimeSpan trimStart, TimeSpan trimEnd, long viewCount, + [AllowNull] string game, [AllowNull] string clipper = null, [AllowNull] string clipperId = null) { var videoLength = trimEnd - trimStart; @@ -18,6 +19,9 @@ public static string GetFilename(string template, [AllowNull] string title, [All .Replace("{title}", ReplaceInvalidFilenameChars(title)) .Replace("{id}", ReplaceInvalidFilenameChars(id)) .Replace("{channel}", ReplaceInvalidFilenameChars(channel)) + .Replace("{channel_id}", ReplaceInvalidFilenameChars(channelId)) + .Replace("{clipper}", ReplaceInvalidFilenameChars(clipper)) + .Replace("{clipper_id}", ReplaceInvalidFilenameChars(clipperId)) .Replace("{date}", date.ToString("M-d-yy")) .Replace("{random_string}", Path.GetRandomFileName().Remove(8)) // Remove the period .Replace("{trim_start}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", trimStart)) diff --git a/TwitchDownloaderCore/TwitchHelper.cs b/TwitchDownloaderCore/TwitchHelper.cs index 98133642..3bd91882 100644 --- a/TwitchDownloaderCore/TwitchHelper.cs +++ b/TwitchDownloaderCore/TwitchHelper.cs @@ -173,7 +173,7 @@ public static async Task GetGqlClips(string channelName, { RequestUri = new Uri("https://gql.twitch.tv/gql"), Method = HttpMethod.Post, - Content = new StringContent("{\"query\":\"query{user(login:\\\"" + channelName + "\\\"){clips(first: " + limit + (cursor == "" ? "" : ", after: \\\"" + cursor + "\\\"") +", criteria: { period: " + period + " }) { edges { cursor, node { id, slug, title, createdAt, durationSeconds, thumbnailURL, viewCount, game { id, displayName } } }, pageInfo { hasNextPage, hasPreviousPage } }}}\",\"variables\":{}}", Encoding.UTF8, "application/json") + Content = new StringContent("{\"query\":\"query{user(login:\\\"" + channelName + "\\\"){clips(first: " + limit + (cursor == "" ? "" : ", after: \\\"" + cursor + "\\\"") +", criteria: { period: " + period + " }) { edges { cursor, node { id, slug, title, createdAt, curator, { id, displayName }, durationSeconds, thumbnailURL, viewCount, game { id, displayName } } }, pageInfo { hasNextPage, hasPreviousPage } }}}\",\"variables\":{}}", Encoding.UTF8, "application/json") }; request.Headers.Add("Client-ID", "kd1unb4b3q4t58fwlpcbzcbnm76a8fp"); using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); @@ -1033,14 +1033,18 @@ where DateTime.UtcNow.Ticks - directoryInfo.LastWriteTimeUtc.Ticks > TimeSpan.Ti : $"{wasDeleted} old video caches were deleted, {toDelete.Length - wasDeleted} could not be deleted."); } - public static async Task GetStreamerName(int id) + public static async Task GetUserIds(IEnumerable nameList) { - try + var request = new HttpRequestMessage() { - GqlUserInfoResponse info = await GetUserInfo(new List { id.ToString() }); - return info.data.users[0].login; - } - catch { return ""; } + RequestUri = new Uri("https://gql.twitch.tv/gql"), + Method = HttpMethod.Post, + Content = new StringContent("{\"query\":\"query{users(logins:[" + string.Join(",", nameList.Select(x => "\\\"" + x + "\\\"").ToArray()) + "]){id}}\",\"variables\":{}}", Encoding.UTF8, "application/json") + }; + request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko"); + using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); } public static async Task GetUserInfo(IEnumerable idList) @@ -1049,7 +1053,7 @@ public static async Task GetUserInfo(IEnumerable id { RequestUri = new Uri("https://gql.twitch.tv/gql"), Method = HttpMethod.Post, - Content = new StringContent("{\"query\":\"query{users(ids:[" + string.Join(",", idList.Select(x => "\\\"" + x + "\\\"").ToArray()) + "]){id,login,createdAt,updatedAt,description,profileImageURL(width:300)}}\",\"variables\":{}}", Encoding.UTF8, "application/json") + Content = new StringContent("{\"query\":\"query{users(ids:[" + string.Join(",", idList.Select(x => "\\\"" + x + "\\\"").ToArray()) + "]){id,displayName,login,createdAt,updatedAt,description,profileImageURL(width:300)}}\",\"variables\":{}}", Encoding.UTF8, "application/json") }; request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko"); using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs index c0604a1e..5d83c795 100644 --- a/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs +++ b/TwitchDownloaderCore/TwitchObjects/ChatRoot.cs @@ -8,6 +8,14 @@ namespace TwitchDownloaderCore.TwitchObjects public class Streamer { public string name { get; set; } + public string login { get; set; } + public int id { get; set; } + } + + public class Clipper + { + public string name { get; set; } + public string login { get; set; } public int id { get; set; } } @@ -271,6 +279,7 @@ public class ChatRoot { public ChatRootInfo FileInfo { get; set; } = new(); public Streamer streamer { get; set; } + public Clipper clipper { get; set; } public Video video { get; set; } public List comments { get; set; } public EmbeddedData embeddedData { get; set; } diff --git a/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs b/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs index 689de2a9..6eb27389 100644 --- a/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs +++ b/TwitchDownloaderCore/TwitchObjects/ChatRootInfo.cs @@ -15,7 +15,7 @@ public record ChatRootVersion public uint Minor { get; init; } public uint Patch { get; init; } - public static ChatRootVersion CurrentVersion { get; } = new(1, 3, 1); + public static ChatRootVersion CurrentVersion { get; } = new(1, 4, 0); /// /// Initializes a new object with the default version of 1.0.0 diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs index d11f5482..0fcac8cb 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlClipSearchResponse.cs @@ -9,12 +9,19 @@ public class ClipNodeGame public string displayName { get; set; } } + public class ClipNodeCurator + { + public string id { get; set; } + public string displayName { get; set; } + } + public class ClipNode { public string id { get; set; } public string slug { get; set; } public string title { get; set; } public DateTime createdAt { get; set; } + public ClipNodeCurator curator { get; set; } public int durationSeconds { get; set; } public string thumbnailURL { get; set; } public int viewCount { get; set; } diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserIdResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserIdResponse.cs new file mode 100644 index 00000000..39fa5860 --- /dev/null +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserIdResponse.cs @@ -0,0 +1,18 @@ +namespace TwitchDownloaderCore.TwitchObjects.Gql +{ + public class UserId + { + public string id { get; set; } + } + + public class UserIdData + { + public UserId[] users { get; set; } + } + + public class GqlUserIdResponse + { + public UserIdData data { get; set; } + public Extensions extensions { get; set; } + } +} \ No newline at end of file diff --git a/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserInfoResponse.cs b/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserInfoResponse.cs index 2f228c90..c6770d72 100644 --- a/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserInfoResponse.cs +++ b/TwitchDownloaderCore/TwitchObjects/Gql/GqlUserInfoResponse.cs @@ -17,6 +17,7 @@ public class GqlUserInfoResponse public class User { public string id { get; set; } + public string displayName { get; set; } public string login { get; set; } public DateTime createdAt { get; set; } public DateTime updatedAt { get; set; } diff --git a/TwitchDownloaderWPF/PageChatDownload.xaml.cs b/TwitchDownloaderWPF/PageChatDownload.xaml.cs index d0e244fb..adcdaab7 100644 --- a/TwitchDownloaderWPF/PageChatDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageChatDownload.xaml.cs @@ -29,7 +29,9 @@ public partial class PageChatDownload : Page { public DownloadType downloadType; public string downloadId; - public int streamerId; + public string streamerId; + public string clipper; + public string clipperId; public DateTime currentVideoTime; public TimeSpan vodLength; public int viewCount; @@ -144,7 +146,9 @@ private async Task GetVideoInfo() var videoTime = videoInfo.data.video.createdAt; 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); + streamerId = videoInfo.data.video.owner.id; + clipper = null; + clipperId = null; viewCount = videoInfo.data.video.viewCount; game = videoInfo.data.video.game?.displayName ?? Translations.Strings.UnknownGame; @@ -193,7 +197,9 @@ private async Task GetVideoInfo() textCreatedAt.Text = Settings.Default.UTCVideoTime ? clipCreatedAt.ToString(CultureInfo.CurrentCulture) : clipCreatedAt.ToLocalTime().ToString(CultureInfo.CurrentCulture); currentVideoTime = Settings.Default.UTCVideoTime ? clipCreatedAt : clipCreatedAt.ToLocalTime(); textTitle.Text = clipInfo.data.clip.title; - streamerId = int.Parse(clipInfo.data.clip.broadcaster?.id ?? "-1"); + streamerId = clipInfo.data.clip.broadcaster?.id; + clipper = clipInfo.data.clip.curator.displayName; + clipperId = clipInfo.data.clip.curator.id; labelLength.Text = vodLength.ToString("c"); SetEnabled(true); @@ -498,10 +504,10 @@ private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e) var saveFileDialog = new SaveFileDialog { - FileName = FilenameService.GetFilename(Settings.Default.TemplateChat, textTitle.Text, downloadId, currentVideoTime, textStreamer.Text, + FileName = FilenameService.GetFilename(Settings.Default.TemplateChat, textTitle.Text, downloadId, currentVideoTime, textStreamer.Text, streamerId, CheckTrimStart.IsChecked == true ? new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value) : TimeSpan.Zero, CheckTrimEnd.IsChecked == true ? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value) : vodLength, - viewCount, game) + viewCount, game, clipper, clipperId) }; if (radioJson.IsChecked == true) diff --git a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs index 19fcecc3..9bc92d01 100644 --- a/TwitchDownloaderWPF/PageChatUpdate.xaml.cs +++ b/TwitchDownloaderWPF/PageChatUpdate.xaml.cs @@ -31,6 +31,9 @@ public partial class PageChatUpdate : Page public string InputFile; public ChatRoot ChatJsonInfo; public string VideoId; + public string StreamerId; + public string ClipperName; + public string ClipperId; public DateTime VideoCreatedAt; public TimeSpan VideoLength; public int ViewCount; @@ -109,6 +112,9 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) : Translations.Strings.UnknownVideoLength; VideoId = ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1"; + StreamerId = ChatJsonInfo.streamer.id.ToString(CultureInfo.InvariantCulture); + ClipperName = ChatJsonInfo.clipper?.name; + ClipperId = ChatJsonInfo.clipper?.id.ToString(CultureInfo.InvariantCulture); ViewCount = ChatJsonInfo.video.viewCount; Game = ChatJsonInfo.video.game ?? ChatJsonInfo.video.chapters.FirstOrDefault()?.gameDisplayName ?? Translations.Strings.UnknownGame; @@ -153,8 +159,8 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) numEndHour.Maximum = 0; } - GqlClipResponse videoInfo = await TwitchHelper.GetClipInfo(VideoId); - if (videoInfo.data.clip.video == null) + GqlClipResponse clipInfo = await TwitchHelper.GetClipInfo(VideoId); + if (clipInfo.data.clip.video == null) { AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail + ": " + Translations.Strings.VodExpiredOrIdCorrupt); _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out var image); @@ -162,12 +168,14 @@ private async void btnBrowse_Click(object sender, RoutedEventArgs e) } else { - VideoLength = TimeSpan.FromSeconds(videoInfo.data.clip.durationSeconds); + VideoLength = TimeSpan.FromSeconds(clipInfo.data.clip.durationSeconds); labelLength.Text = VideoLength.ToString("c"); - ViewCount = videoInfo.data.clip.viewCount; - Game = videoInfo.data.clip.game?.displayName; + ViewCount = clipInfo.data.clip.viewCount; + Game = clipInfo.data.clip.game?.displayName; + ClipperName ??= clipInfo.data.clip.curator.displayName; + ClipperId ??= clipInfo.data.clip.curator.id; - var thumbUrl = videoInfo.data.clip.thumbnailURL; + var thumbUrl = clipInfo.data.clip.thumbnailURL; if (!ThumbnailService.TryGetThumb(thumbUrl, out var image)) { AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail); @@ -497,10 +505,10 @@ private async void SplitBtnUpdate_Click(object sender, RoutedEventArgs e) var saveFileDialog = new SaveFileDialog { FileName = FilenameService.GetFilename(Settings.Default.TemplateChat, textTitle.Text, - ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1", VideoCreatedAt, textStreamer.Text, + ChatJsonInfo.video.id ?? ChatJsonInfo.comments.FirstOrDefault()?.content_id ?? "-1", VideoCreatedAt, textStreamer.Text, StreamerId, 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, - ViewCount, Game) + ViewCount, Game, ClipperName, ClipperId) }; if (radioJson.IsChecked == true) diff --git a/TwitchDownloaderWPF/PageClipDownload.xaml.cs b/TwitchDownloaderWPF/PageClipDownload.xaml.cs index 05a8410d..0761acc7 100644 --- a/TwitchDownloaderWPF/PageClipDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageClipDownload.xaml.cs @@ -26,6 +26,9 @@ namespace TwitchDownloaderWPF public partial class PageClipDownload : Page { public string clipId = ""; + public string streamerId; + public string clipperName; + public string clipperId; public DateTime currentVideoTime; public TimeSpan clipLength; public int viewCount; @@ -71,6 +74,9 @@ private async Task GetClipInfo() clipLength = TimeSpan.FromSeconds(taskClipInfo.Result.data.clip.durationSeconds); textStreamer.Text = clipData.data.clip.broadcaster?.displayName ?? Translations.Strings.UnknownUser; + streamerId = clipData.data.clip.broadcaster?.id; + clipperName = clipData.data.clip.curator.displayName; + clipperId = clipData.data.clip.curator.id; var clipCreatedAt = clipData.data.clip.createdAt; textCreatedAt.Text = Settings.Default.UTCVideoTime ? clipCreatedAt.ToString(CultureInfo.CurrentCulture) : clipCreatedAt.ToLocalTime().ToString(CultureInfo.CurrentCulture); currentVideoTime = Settings.Default.UTCVideoTime ? clipCreatedAt : clipCreatedAt.ToLocalTime(); @@ -201,7 +207,8 @@ 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, viewCount, game) + ".mp4" + FileName = FilenameService.GetFilename(Settings.Default.TemplateClip, textTitle.Text, clipId, currentVideoTime, textStreamer.Text, streamerId, TimeSpan.Zero, clipLength, viewCount, game, clipperName, + clipperId) + ".mp4" }; if (saveFileDialog.ShowDialog() != true) { diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs index a2ba8a68..713d7cc6 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs @@ -37,6 +37,7 @@ public partial class PageVodDownload : Page public TimeSpan vodLength; public int viewCount; public string game; + public string streamerId; private CancellationTokenSource _cancellationTokenSource; public PageVodDownload() @@ -138,6 +139,7 @@ private async Task GetVideoInfo() vodLength = TimeSpan.FromSeconds(taskVideoInfo.Result.data.video.lengthSeconds); textStreamer.Text = taskVideoInfo.Result.data.video.owner.displayName; + streamerId = taskVideoInfo.Result.data.video.owner.id; textTitle.Text = taskVideoInfo.Result.data.video.title; var videoCreatedAt = taskVideoInfo.Result.data.video.createdAt; textCreatedAt.Text = Settings.Default.UTCVideoTime ? videoCreatedAt.ToString(CultureInfo.CurrentCulture) : videoCreatedAt.ToLocalTime().ToString(CultureInfo.CurrentCulture); @@ -203,7 +205,7 @@ public VideoDownloadOptions GetOptions(string filename, string folder) ThrottleKib = Settings.Default.DownloadThrottleEnabled ? Settings.Default.MaximumBandwidthKib : -1, - Filename = filename ?? Path.Combine(folder, FilenameService.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text, + Filename = filename ?? Path.Combine(folder, FilenameService.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text, streamerId, 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, viewCount, game) + FilenameService.GuessVodFileExtension(comboQuality.Text)), @@ -422,7 +424,7 @@ private async void SplitBtnDownloader_Click(object sender, RoutedEventArgs e) SaveFileDialog saveFileDialog = new SaveFileDialog { Filter = comboQuality.Text.Contains("Audio", StringComparison.OrdinalIgnoreCase) ? "M4A Files | *.m4a" : "MP4 Files | *.mp4", - FileName = FilenameService.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text, + FileName = FilenameService.GetFilename(Settings.Default.TemplateVod, textTitle.Text, currentVideoId.ToString(), currentVideoTime, textStreamer.Text, streamerId, 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, viewCount, game) + FilenameService.GuessVodFileExtension(comboQuality.Text) diff --git a/TwitchDownloaderWPF/Translations/Strings.Designer.cs b/TwitchDownloaderWPF/Translations/Strings.Designer.cs index 43bb4510..d95785b6 100644 --- a/TwitchDownloaderWPF/Translations/Strings.Designer.cs +++ b/TwitchDownloaderWPF/Translations/Strings.Designer.cs @@ -1022,6 +1022,15 @@ public static string FileCollisionBehaviorTooltip { } } + /// + /// Looks up a localized string similar to The ID of the channel which owns the video/clip/chat.. + /// + public static string FilenameParameterChannelIdTooltip { + get { + return ResourceManager.GetString("FilenameParameterChannelIdTooltip", resourceCulture); + } + } + /// /// Looks up a localized string similar to The display name of the channel which owns the video/clip/chat.. /// @@ -1031,6 +1040,24 @@ public static string FilenameParameterChannelTooltip { } } + /// + /// Looks up a localized string similar to The ID of the channel which created the clip, or empty for videos.. + /// + public static string FilenameParameterClipperIdTooltip { + get { + return ResourceManager.GetString("FilenameParameterClipperIdTooltip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The display name of the channel which created the clip, or empty for videos.. + /// + public static string FilenameParameterClipperTooltip { + get { + return ResourceManager.GetString("FilenameParameterClipperTooltip", resourceCulture); + } + } + /// /// Looks up a localized string similar to The date that the video/clip was created in a customizable format.. /// diff --git a/TwitchDownloaderWPF/Translations/Strings.es.resx b/TwitchDownloaderWPF/Translations/Strings.es.resx index d2ee2578..e2270d98 100644 --- a/TwitchDownloaderWPF/Translations/Strings.es.resx +++ b/TwitchDownloaderWPF/Translations/Strings.es.resx @@ -1006,4 +1006,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + diff --git a/TwitchDownloaderWPF/Translations/Strings.fr.resx b/TwitchDownloaderWPF/Translations/Strings.fr.resx index c12a158f..b0615498 100644 --- a/TwitchDownloaderWPF/Translations/Strings.fr.resx +++ b/TwitchDownloaderWPF/Translations/Strings.fr.resx @@ -1005,4 +1005,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.it.resx b/TwitchDownloaderWPF/Translations/Strings.it.resx index fa1db3ff..3cc28b5e 100644 --- a/TwitchDownloaderWPF/Translations/Strings.it.resx +++ b/TwitchDownloaderWPF/Translations/Strings.it.resx @@ -1006,4 +1006,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + diff --git a/TwitchDownloaderWPF/Translations/Strings.ja.resx b/TwitchDownloaderWPF/Translations/Strings.ja.resx index 34b3998f..d9d4a79c 100644 --- a/TwitchDownloaderWPF/Translations/Strings.ja.resx +++ b/TwitchDownloaderWPF/Translations/Strings.ja.resx @@ -1004,4 +1004,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.pl.resx b/TwitchDownloaderWPF/Translations/Strings.pl.resx index 4e806e55..3a505ed0 100644 --- a/TwitchDownloaderWPF/Translations/Strings.pl.resx +++ b/TwitchDownloaderWPF/Translations/Strings.pl.resx @@ -1005,4 +1005,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.pt-br.resx b/TwitchDownloaderWPF/Translations/Strings.pt-br.resx index 1b1e1f4c..91b76f4e 100644 --- a/TwitchDownloaderWPF/Translations/Strings.pt-br.resx +++ b/TwitchDownloaderWPF/Translations/Strings.pt-br.resx @@ -1008,4 +1008,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.resx b/TwitchDownloaderWPF/Translations/Strings.resx index 9a620884..b4a139ea 100644 --- a/TwitchDownloaderWPF/Translations/Strings.resx +++ b/TwitchDownloaderWPF/Translations/Strings.resx @@ -1004,4 +1004,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.ru.resx b/TwitchDownloaderWPF/Translations/Strings.ru.resx index 591d0baf..03c731b9 100644 --- a/TwitchDownloaderWPF/Translations/Strings.ru.resx +++ b/TwitchDownloaderWPF/Translations/Strings.ru.resx @@ -1005,4 +1005,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.tr.resx b/TwitchDownloaderWPF/Translations/Strings.tr.resx index 23ea271b..ce385816 100644 --- a/TwitchDownloaderWPF/Translations/Strings.tr.resx +++ b/TwitchDownloaderWPF/Translations/Strings.tr.resx @@ -1006,4 +1006,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + \ No newline at end of file diff --git a/TwitchDownloaderWPF/Translations/Strings.uk.resx b/TwitchDownloaderWPF/Translations/Strings.uk.resx index 95df1ada..0070cf7d 100644 --- a/TwitchDownloaderWPF/Translations/Strings.uk.resx +++ b/TwitchDownloaderWPF/Translations/Strings.uk.resx @@ -1005,4 +1005,13 @@ Cancel + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + diff --git a/TwitchDownloaderWPF/Translations/Strings.zh-cn.resx b/TwitchDownloaderWPF/Translations/Strings.zh-cn.resx index 06d5de43..da6882f9 100644 --- a/TwitchDownloaderWPF/Translations/Strings.zh-cn.resx +++ b/TwitchDownloaderWPF/Translations/Strings.zh-cn.resx @@ -1007,4 +1007,13 @@ 取消 + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + diff --git a/TwitchDownloaderWPF/Translations/Strings.zh-tw.resx b/TwitchDownloaderWPF/Translations/Strings.zh-tw.resx index 766ddbaa..7ea0ae79 100644 --- a/TwitchDownloaderWPF/Translations/Strings.zh-tw.resx +++ b/TwitchDownloaderWPF/Translations/Strings.zh-tw.resx @@ -1007,4 +1007,13 @@ 取消 + + The ID of the channel which owns the video/clip/chat. + + + The display name of the channel which created the clip, or empty for videos. + + + The ID of the channel which created the clip, or empty for videos. + diff --git a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs index 5d2c8872..b3b64fd3 100644 --- a/TwitchDownloaderWPF/TwitchTasks/TaskData.cs +++ b/TwitchDownloaderWPF/TwitchTasks/TaskData.cs @@ -6,7 +6,10 @@ namespace TwitchDownloaderWPF.TwitchTasks public class TaskData { public string Id { get; set; } - public string Streamer { get; set; } + public string StreamerName { get; set; } + public string StreamerId { get; set; } + public string ClipperName { get; set; } + public string ClipperId { get; set; } public string Title { get; set; } public ImageSource Thumbnail { get; set; } public DateTime Time { get; set; } diff --git a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs index 3e65bce0..b02245ad 100644 --- a/TwitchDownloaderWPF/WindowMassDownload.xaml.cs +++ b/TwitchDownloaderWPF/WindowMassDownload.xaml.cs @@ -26,7 +26,7 @@ public partial class WindowMassDownload : Window public readonly List selectedItems = new List(); public readonly List cursorList = new List(); public int cursorIndex = 0; - public string currentChannel = ""; + public User currentChannel; public string period = ""; public int videoCount = 50; @@ -57,11 +57,32 @@ private void ResetLists() cursorIndex = 0; } - private Task ChangeCurrentChannel() + private async Task ChangeCurrentChannel() { - currentChannel = textChannel.Text; + var textTrimmed = textChannel.Text.Trim(); + if (!textTrimmed.Equals(currentChannel?.login, StringComparison.InvariantCultureIgnoreCase)) + { + currentChannel = null; + if (!string.IsNullOrEmpty(textTrimmed)) + { + try + { + var idRes = await TwitchHelper.GetUserIds(new[] { textTrimmed }); + var infoRes = await TwitchHelper.GetUserInfo(idRes.data.users.Select(x => x.id)); + currentChannel = infoRes.data.users[0]; + } + catch (Exception ex) + { + if (Settings.Default.VerboseErrors) + { + MessageBox.Show(this, ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } + } + ResetLists(); - return UpdateList(); + await UpdateList(); } private async Task UpdateList() @@ -71,7 +92,7 @@ private async Task UpdateList() StatusImage.Visibility = Visibility.Visible; - if (string.IsNullOrWhiteSpace(currentChannel)) + if (string.IsNullOrWhiteSpace(currentChannel?.login)) { // Pretend we are doing something so the status icon has time to show await Task.Delay(50); @@ -91,7 +112,7 @@ private async Task UpdateList() GqlVideoSearchResponse res; try { - res = await TwitchHelper.GetGqlVideos(currentChannel, currentCursor, videoCount); + res = await TwitchHelper.GetGqlVideos(currentChannel.login, currentCursor, videoCount); } catch (Exception ex) { @@ -123,7 +144,8 @@ private async Task UpdateList() Id = video.node.id, Time = Settings.Default.UTCVideoTime ? video.node.createdAt : video.node.createdAt.ToLocalTime(), Views = video.node.viewCount, - Streamer = currentChannel, + StreamerName = currentChannel.displayName, + StreamerId = currentChannel.id, Game = video.node.game?.displayName ?? Translations.Strings.UnknownGame, Thumbnail = thumbnail }); @@ -155,7 +177,7 @@ private async Task UpdateList() GqlClipSearchResponse res; try { - res = await TwitchHelper.GetGqlClips(currentChannel, period, currentCursor, videoCount); + res = await TwitchHelper.GetGqlClips(currentChannel.login, period, currentCursor, videoCount); } catch (Exception ex) { @@ -187,7 +209,10 @@ private async Task UpdateList() Id = clip.node.slug, Time = Settings.Default.UTCVideoTime ? clip.node.createdAt : clip.node.createdAt.ToLocalTime(), Views = clip.node.viewCount, - Streamer = currentChannel, + StreamerName = currentChannel.displayName, + StreamerId = currentChannel.id, + ClipperName = clip.node.curator.displayName, + ClipperId = clip.node.curator.id, Game = clip.node.game?.displayName ?? Translations.Strings.UnknownGame, Thumbnail = thumbnail }); diff --git a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs index 54a80b08..69781ec0 100644 --- a/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs +++ b/TwitchDownloaderWPF/WindowQueueOptions.xaml.cs @@ -260,8 +260,8 @@ private void btnQueue_Click(object sender, RoutedEventArgs e) ClipDownloadOptions downloadOptions = new ClipDownloadOptions { Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateClip, clipDownloadPage.textTitle.Text, clipDownloadPage.clipId, - clipDownloadPage.currentVideoTime, clipDownloadPage.textStreamer.Text, TimeSpan.Zero, clipDownloadPage.clipLength, - clipDownloadPage.viewCount, clipDownloadPage.game) + ".mp4"), + clipDownloadPage.currentVideoTime, clipDownloadPage.textStreamer.Text, clipDownloadPage.streamerId, TimeSpan.Zero, clipDownloadPage.clipLength, + clipDownloadPage.viewCount, clipDownloadPage.game, clipDownloadPage.clipperName, clipDownloadPage.clipperId) + ".mp4"), Id = clipDownloadPage.clipId, Quality = clipDownloadPage.comboQuality.Text, ThrottleKib = Settings.Default.DownloadThrottleEnabled @@ -301,8 +301,8 @@ private void btnQueue_Click(object sender, RoutedEventArgs e) chatOptions.TimeFormat = TimestampFormat.Relative; chatOptions.EmbedData = checkEmbed.IsChecked.GetValueOrDefault(); chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, downloadTask.Info.Title, chatOptions.Id, - clipDownloadPage.currentVideoTime, clipDownloadPage.textStreamer.Text, TimeSpan.Zero, clipDownloadPage.clipLength, - clipDownloadPage.viewCount, clipDownloadPage.game) + "." + chatOptions.FileExtension); + clipDownloadPage.currentVideoTime, clipDownloadPage.textStreamer.Text, clipDownloadPage.streamerId, TimeSpan.Zero, clipDownloadPage.clipLength, + clipDownloadPage.viewCount, clipDownloadPage.game, clipDownloadPage.clipperName, clipDownloadPage.clipId) + "." + chatOptions.FileExtension); chatOptions.FileCollisionCallback = HandleFileCollisionCallback; ChatDownloadTask chatTask = new ChatDownloadTask @@ -377,6 +377,7 @@ private void btnQueue_Click(object sender, RoutedEventArgs e) ChatDownloadOptions chatOptions = MainWindow.pageChatDownload.GetOptions(null); chatOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, chatDownloadPage.textTitle.Text, chatOptions.Id,chatDownloadPage.currentVideoTime, chatDownloadPage.textStreamer.Text, + chatDownloadPage.streamerId, chatOptions.TrimBeginning ? TimeSpan.FromSeconds(chatOptions.TrimBeginningTime) : TimeSpan.Zero, chatOptions.TrimEnding ? TimeSpan.FromSeconds(chatOptions.TrimEndingTime) : chatDownloadPage.vodLength, chatDownloadPage.viewCount, chatDownloadPage.game) + "." + chatOptions.FileExtension); @@ -449,9 +450,10 @@ private void btnQueue_Click(object sender, RoutedEventArgs e) ChatUpdateOptions chatOptions = MainWindow.pageChatUpdate.GetOptions(null); chatOptions.InputFile = chatUpdatePage.InputFile; chatOptions.OutputFile = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, chatUpdatePage.textTitle.Text, chatUpdatePage.VideoId, chatUpdatePage.VideoCreatedAt, chatUpdatePage.textStreamer.Text, + chatUpdatePage.StreamerId, chatOptions.TrimBeginning ? TimeSpan.FromSeconds(chatOptions.TrimBeginningTime) : TimeSpan.Zero, chatOptions.TrimEnding ? TimeSpan.FromSeconds(chatOptions.TrimEndingTime) : chatUpdatePage.VideoLength, - chatUpdatePage.ViewCount, chatUpdatePage.Game) + "." + chatOptions.FileExtension); + chatUpdatePage.ViewCount, chatUpdatePage.Game, chatUpdatePage.ClipperName, chatUpdatePage.ClipperId) + "." + chatOptions.FileExtension); chatOptions.FileCollisionCallback = HandleFileCollisionCallback; ChatUpdateTask chatTask = new ChatUpdateTask @@ -574,7 +576,7 @@ private void EnqueueDataList() : -1, FileCollisionCallback = HandleFileCollisionCallback, }; - downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateVod, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer, + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateVod, taskData.Title, taskData.Id, taskData.Time, taskData.StreamerName, taskData.StreamerId, downloadOptions.TrimBeginning ? downloadOptions.TrimBeginningTime : TimeSpan.Zero, downloadOptions.TrimEnding ? downloadOptions.TrimEndingTime : TimeSpan.FromSeconds(taskData.Length), taskData.Views, taskData.Game) + FilenameService.GuessVodFileExtension(downloadOptions.Quality)); @@ -600,8 +602,8 @@ private void EnqueueDataList() { Id = taskData.Id, Quality = (ComboPreferredQuality.SelectedItem as ComboBoxItem)?.Content as string, - 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, taskData.Game) + ".mp4"), + Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateClip, taskData.Title, taskData.Id, taskData.Time, taskData.StreamerName, taskData.StreamerId, + TimeSpan.Zero, TimeSpan.FromSeconds(taskData.Length), taskData.Views, taskData.Game, taskData.ClipperName, taskData.ClipperId) + ".mp4"), ThrottleKib = Settings.Default.DownloadThrottleEnabled ? Settings.Default.MaximumBandwidthKib : -1, @@ -646,10 +648,10 @@ private void EnqueueDataList() downloadOptions.DownloadFormat = ChatFormat.Html; else downloadOptions.DownloadFormat = ChatFormat.Text; - downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, taskData.Title, taskData.Id, taskData.Time, taskData.Streamer, + downloadOptions.Filename = Path.Combine(folderPath, FilenameService.GetFilename(Settings.Default.TemplateChat, taskData.Title, taskData.Id, taskData.Time, taskData.StreamerName, taskData.StreamerId, downloadOptions.TrimBeginning ? TimeSpan.FromSeconds(downloadOptions.TrimBeginningTime) : TimeSpan.Zero, downloadOptions.TrimEnding ? TimeSpan.FromSeconds(downloadOptions.TrimEndingTime) : TimeSpan.FromSeconds(taskData.Length), - taskData.Views, taskData.Game) + "." + downloadOptions.FileExtension); + taskData.Views, taskData.Game, taskData.ClipperName, taskData.ClipperId) + "." + downloadOptions.FileExtension); ChatDownloadTask downloadTask = new ChatDownloadTask { diff --git a/TwitchDownloaderWPF/WindowSettings.xaml b/TwitchDownloaderWPF/WindowSettings.xaml index f1341d41..e7a2707d 100644 --- a/TwitchDownloaderWPF/WindowSettings.xaml +++ b/TwitchDownloaderWPF/WindowSettings.xaml @@ -136,6 +136,15 @@ {channel} + + {channel_id} + + + {clipper} + + + {clipper_id} + {random_string} diff --git a/TwitchDownloaderWPF/WindowUrlList.xaml.cs b/TwitchDownloaderWPF/WindowUrlList.xaml.cs index 08c5c168..f80b81df 100644 --- a/TwitchDownloaderWPF/WindowUrlList.xaml.cs +++ b/TwitchDownloaderWPF/WindowUrlList.xaml.cs @@ -98,7 +98,8 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) Id = id, Thumbnail = thumbnail, Title = videoInfo.title, - Streamer = videoInfo.owner.displayName, + StreamerName = videoInfo.owner.displayName, + StreamerId = videoInfo.owner.id, Time = Settings.Default.UTCVideoTime ? videoInfo.createdAt : videoInfo.createdAt.ToLocalTime(), Views = videoInfo.viewCount, Game = videoInfo.game?.displayName ?? Translations.Strings.UnknownGame, @@ -136,7 +137,10 @@ private async void btnQueue_Click(object sender, RoutedEventArgs e) Id = id, Thumbnail = thumbnail, Title = clipInfo.title, - Streamer = clipInfo.broadcaster?.displayName ?? Translations.Strings.UnknownUser, + StreamerName = clipInfo.broadcaster?.displayName ?? Translations.Strings.UnknownUser, + StreamerId = clipInfo.broadcaster?.id, + ClipperName = clipInfo.curator.displayName, + ClipperId = clipInfo.curator.id, Time = Settings.Default.UTCVideoTime ? clipInfo.createdAt : clipInfo.createdAt.ToLocalTime(), Views = clipInfo.viewCount, Game = clipInfo.game?.displayName ?? Translations.Strings.UnknownGame,