using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using TwitchDownloaderCore; using TwitchDownloaderCore.TwitchObjects.Gql; using TwitchDownloaderWPF.Properties; using TwitchDownloaderWPF.Services; using TwitchDownloaderWPF.TwitchTasks; namespace TwitchDownloaderWPF { /// /// Interaction logic for WindowMassDownload.xaml /// public partial class WindowMassDownload : Window { private readonly string _clearChannelsConstant = Guid.NewGuid().ToString(); public DownloadType downloaderType { get; set; } public ObservableCollection videoList { get; set; } = new ObservableCollection(); public readonly List selectedItems = new List(); public readonly List cursorList = new List(); public int cursorIndex = 0; public User currentChannel; public string period = ""; public string videoType = ""; public int videoCount = 50; public WindowMassDownload(DownloadType type) { downloaderType = type; InitializeComponent(); itemList.ItemsSource = videoList; if (downloaderType == DownloadType.Video) { ComboSortByDate.Visibility = Visibility.Collapsed; } else if (downloaderType == DownloadType.Clip) { ComboSortByVideoType.Visibility = Visibility.Collapsed; } btnNext.IsEnabled = false; btnPrev.IsEnabled = false; UpdateRecentChannels(); ComboChannel.Text = ""; } private async void btnChannel_Click(object sender, RoutedEventArgs e) { await ChangeCurrentChannel(); } private void ResetLists() { videoList.Clear(); cursorList.Clear(); cursorList.Add(""); cursorIndex = 0; } private async Task ChangeCurrentChannel() { if (!IsInitialized) return; var textTrimmed = ComboChannel.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(); await UpdateList(); } private async Task UpdateList() { if (!IsInitialized) return; if (!Settings.Default.ReduceMotion) { StatusImage.Visibility = Visibility.Visible; } if (string.IsNullOrWhiteSpace(currentChannel?.login)) { // Pretend we are doing something so the status icon has time to show await Task.Delay(50); ResetLists(); StatusImage.Visibility = Visibility.Hidden; return; } if (downloaderType == DownloadType.Video) { string currentCursor = ""; if (cursorList.Count > 0 && cursorIndex >= 0) { currentCursor = cursorList[cursorIndex]; } GqlVideoSearchResponse res; try { res = await TwitchHelper.GetGqlVideos(currentChannel.login, currentCursor, videoCount, videoType); } catch (Exception ex) { MessageBox.Show(this, string.Format(Translations.Strings.UnableToGetChannelVideosMessage, ex.Message), Translations.Strings.UnableToGetChannelVideos, MessageBoxButton.OK, MessageBoxImage.Error); if (Settings.Default.VerboseErrors) { MessageBox.Show(this, ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error); } return; } UpdateRecentChannels(); videoList.Clear(); if (res.data.user != null) { foreach (var video in res.data.user.videos.edges) { var thumbUrl = video.node.previewThumbnailURL; if (!ThumbnailService.TryGetThumb(thumbUrl, out var thumbnail)) { _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out thumbnail); } videoList.Add(new TaskData { Title = video.node.title, Length = video.node.lengthSeconds, Id = video.node.id, Time = Settings.Default.UTCVideoTime ? video.node.createdAt : video.node.createdAt.ToLocalTime(), Views = video.node.viewCount, StreamerName = currentChannel.displayName, StreamerId = currentChannel.id, Game = video.node.game?.displayName ?? Translations.Strings.UnknownGame, Thumbnail = thumbnail }); } btnPrev.IsEnabled = cursorIndex > 0; if (res.data.user.videos.pageInfo.hasNextPage) { string newCursor = res.data.user.videos.edges.FirstOrDefault()?.cursor; if (newCursor is not null) { btnNext.IsEnabled = true; if (!cursorList.Contains(newCursor)) { cursorList.Add(newCursor); } } } } } else { string currentCursor = ""; if (cursorList.Count > 0 && cursorIndex >= 0) { currentCursor = cursorList[cursorIndex]; } GqlClipSearchResponse res; try { res = await TwitchHelper.GetGqlClips(currentChannel.login, period, currentCursor, videoCount); } catch (Exception ex) { MessageBox.Show(this, string.Format(Translations.Strings.UnableToGetChannelClipsMessage, ex.Message), Translations.Strings.UnableToGetChannelClips, MessageBoxButton.OK, MessageBoxImage.Error); if (Settings.Default.VerboseErrors) { MessageBox.Show(this, ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error); } return; } UpdateRecentChannels(); videoList.Clear(); if (res.data.user != null) { foreach (var clip in res.data.user.clips.edges) { var thumbUrl = clip.node.thumbnailURL; if (!ThumbnailService.TryGetThumb(thumbUrl, out var thumbnail)) { _ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out thumbnail); } videoList.Add(new TaskData { Title = clip.node.title, Length = clip.node.durationSeconds, Id = clip.node.slug, Time = Settings.Default.UTCVideoTime ? clip.node.createdAt : clip.node.createdAt.ToLocalTime(), Views = clip.node.viewCount, StreamerName = currentChannel.displayName, StreamerId = currentChannel.id, ClipperName = clip.node.curator?.displayName ?? Translations.Strings.UnknownUser, ClipperId = clip.node.curator?.id, Game = clip.node.game?.displayName ?? Translations.Strings.UnknownGame, Thumbnail = thumbnail }); } btnPrev.IsEnabled = cursorIndex > 0; if (res.data.user.clips.pageInfo.hasNextPage) { string newCursor = res.data.user.clips.edges.FirstOrDefault(x => x.cursor != null)?.cursor; if (newCursor is not null) { btnNext.IsEnabled = true; if (!cursorList.Contains(newCursor)) { cursorList.Add(newCursor); } } } } } StatusImage.Visibility = Visibility.Hidden; } private void Border_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (sender is not Border border) return; if (border.DataContext is not TaskData taskData) return; if (selectedItems.Any(x => x.Id == taskData.Id)) { border.Background = Brushes.Transparent; selectedItems.RemoveAll(x => x.Id == taskData.Id); } else { border.Background = Brushes.LightBlue; selectedItems.Add(taskData); } textCount.Text = selectedItems.Count.ToString(); } private async void btnNext_Click(object sender, RoutedEventArgs e) { btnNext.IsEnabled = false; btnPrev.IsEnabled = false; if (cursorIndex < cursorList.Count - 1) { cursorIndex++; } await UpdateList(); } private async void btnPrev_Click(object sender, RoutedEventArgs e) { btnNext.IsEnabled = false; btnPrev.IsEnabled = false; if (cursorIndex > 0) { cursorIndex--; } await UpdateList(); } private void Border_Initialized(object sender, EventArgs e) { if (sender is not Border border) return; if (border.DataContext is not TaskData taskData) return; if (selectedItems.Any(x => x.Id == taskData.Id)) { border.Background = Brushes.LightBlue; } } private void btnQueue_Click(object sender, RoutedEventArgs e) { if (selectedItems.Count > 0) { var queue = new WindowQueueOptions(selectedItems) { Owner = this, WindowStartupLocation = WindowStartupLocation.CenterOwner }; if (queue.ShowDialog().GetValueOrDefault()) this.Close(); } } private async void ComboSortByDate_SelectionChanged(object sender, SelectionChangedEventArgs e) { period = ((ComboBoxItem)ComboSortByDate.SelectedItem).Tag.ToString(); ResetLists(); await UpdateList(); } private async void ComboSortByVideoType_SelectionChanged(object sender, SelectionChangedEventArgs e) { videoType = ((ComboBoxItem)ComboSortByVideoType.SelectedItem).Tag.ToString(); ResetLists(); await UpdateList(); } private void btnSelectAll_Click(object sender, RoutedEventArgs e) { //I'm sure there is a much better way to do this. Could not find a way to iterate over each itemcontrol border foreach (var video in videoList) { if (selectedItems.All(x => x.Id != video.Id)) { selectedItems.Add(video); } } // Remove and re-add all of the items to trigger Border_Initialized var oldData = videoList.ToArray(); videoList.Clear(); foreach (var item in oldData) { videoList.Add(item); } textCount.Text = selectedItems.Count.ToString(); } private void Window_OnSourceInitialized(object sender, EventArgs e) { App.RequestTitleBarChange(); } private void Window_Loaded(object sender, RoutedEventArgs e) { Title = downloaderType == DownloadType.Video ? Translations.Strings.TitleVideoMassDownloader : Translations.Strings.TitleClipMassDownloader; } private async void ComboChannel_OnKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { await ChangeCurrentChannel(); e.Handled = true; } } private async void ComboVideoCount_SelectionChanged(object sender, SelectionChangedEventArgs e) { videoCount = int.Parse((string)((ComboBoxItem)ComboVideoCount.SelectedValue).Content); ResetLists(); await UpdateList(); } private void MenuItemCopyVideoID_OnClick(object sender, RoutedEventArgs e) { if (sender is not MenuItem { DataContext: TaskData taskData }) return; var id = taskData.Id; if (!ClipboardService.TrySetText(id, out var exception)) { MessageBox.Show(this, exception.ToString(), Translations.Strings.FailedToCopyToClipboard, MessageBoxButton.OK, MessageBoxImage.Error); } e.Handled = true; } private void MenuItemCopyVideoUrl_OnClick(object sender, RoutedEventArgs e) { if (sender is not MenuItem { DataContext: TaskData taskData }) return; var id = taskData.Id; var url = id.All(char.IsDigit) ? $"https://twitch.tv/videos/{id}" : $"https://clips.twitch.tv/{id}"; if (!ClipboardService.TrySetText(url, out var exception)) { MessageBox.Show(this, exception.ToString(), Translations.Strings.FailedToCopyToClipboard, MessageBoxButton.OK, MessageBoxImage.Error); } e.Handled = true; } private void MenuItemOpenInBrowser_OnClick(object sender, RoutedEventArgs e) { if (sender is not MenuItem { DataContext: TaskData taskData }) return; var id = taskData.Id; var url = id.All(char.IsDigit) ? $"https://twitch.tv/videos/{id}" : $"https://clips.twitch.tv/{id}"; Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); e.Handled = true; } private async void ComboChannel_OnDropDownClosed(object sender, EventArgs e) { if (!IsInitialized || ComboChannel.SelectedItem is not ComboBoxItem comboBoxItem) return; if (string.Equals(comboBoxItem.Content as string, currentChannel?.login, StringComparison.OrdinalIgnoreCase)) return; if (comboBoxItem.Tag as string == _clearChannelsConstant) { Settings.Default.RecentChannels.Clear(); currentChannel = null; UpdateRecentChannels(); } await ChangeCurrentChannel(); } private void UpdateRecentChannels() { var recentChannels = Settings.Default.RecentChannels ?? new StringCollection(); if (!string.IsNullOrWhiteSpace(currentChannel?.login)) { // Move the current channel to the top of the list recentChannels.Remove(currentChannel.login); recentChannels.Insert(0, currentChannel.login); } while (recentChannels.Count > 15) { recentChannels.RemoveAt(recentChannels.Count - 1); } ComboChannel.Items.Clear(); foreach (var channel in recentChannels) { ComboChannel.Items.Add(new ComboBoxItem { Content = channel }); } // Select the most recent channel, if there is one if (recentChannels.Count > 0) { ComboChannel.SelectedIndex = 0; } ComboChannel.Items.Add(new ComboBoxItem { Content = Translations.Strings.ClearRecentChannels, Tag = _clearChannelsConstant }); Settings.Default.RecentChannels = recentChannels; Settings.Default.Save(); } } }