using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Models;
using TwitchDownloaderCore.Options;
using TwitchDownloaderCore.Services;
using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects.Gql;
using TwitchDownloaderWPF.Models;
using TwitchDownloaderWPF.Properties;
using TwitchDownloaderWPF.Services;
using TwitchDownloaderWPF.Utils;
using WpfAnimatedGif;
namespace TwitchDownloaderWPF
{
public enum DownloadType { Clip, Video }
///
/// Interaction logic for PageChatDownload.xaml
///
public partial class PageChatDownload : Page
{
public DownloadType downloadType;
public string downloadId;
public string streamerId;
public string clipper;
public string clipperId;
public DateTime currentVideoTime;
public TimeSpan vodLength;
public int viewCount;
public string game;
private CancellationTokenSource _cancellationTokenSource;
public PageChatDownload()
{
InitializeComponent();
}
private void Page_Initialized(object sender, EventArgs e)
{
SetEnabled(false);
SetEnabledTrimStart(false);
SetEnabledTrimEnd(false);
checkEmbed.IsChecked = Settings.Default.ChatEmbedEmotes;
checkBttvEmbed.IsChecked = Settings.Default.BTTVEmotes;
checkFfzEmbed.IsChecked = Settings.Default.FFZEmotes;
checkStvEmbed.IsChecked = Settings.Default.STVEmotes;
NumChatDownloadThreads.Value = Settings.Default.ChatDownloadThreads;
_ = (ChatFormat)Settings.Default.ChatDownloadType switch
{
ChatFormat.Text => radioText.IsChecked = true,
ChatFormat.Html => radioHTML.IsChecked = true,
ChatFormat.Json => radioJson.IsChecked = true,
_ => null,
};
_ = (ChatCompression)Settings.Default.ChatJsonCompression switch
{
ChatCompression.None => radioCompressionNone.IsChecked = true,
ChatCompression.Gzip => radioCompressionGzip.IsChecked = true,
_ => null,
};
_ = (TimestampFormat)Settings.Default.ChatTextTimestampStyle switch
{
TimestampFormat.Utc => radioTimestampUTC.IsChecked = true,
TimestampFormat.Relative => radioTimestampRelative.IsChecked = true,
TimestampFormat.None => radioTimestampNone.IsChecked = true,
_ => null,
};
}
private void SetEnabled(bool isEnabled)
{
CheckTrimStart.IsEnabled = isEnabled;
CheckTrimEnd.IsEnabled = isEnabled;
radioTimestampRelative.IsEnabled = isEnabled;
radioTimestampUTC.IsEnabled = isEnabled;
radioTimestampNone.IsEnabled = isEnabled;
radioCompressionNone.IsEnabled = isEnabled;
radioCompressionGzip.IsEnabled = isEnabled;
checkEmbed.IsEnabled = isEnabled;
checkBttvEmbed.IsEnabled = isEnabled;
checkFfzEmbed.IsEnabled = isEnabled;
checkStvEmbed.IsEnabled = isEnabled;
SplitBtnDownload.IsEnabled = isEnabled;
MenuItemEnqueue.IsEnabled = isEnabled;
radioJson.IsEnabled = isEnabled;
radioText.IsEnabled = isEnabled;
radioHTML.IsEnabled = isEnabled;
}
private void SetEnabledTrimStart(bool isEnabled)
{
numStartHour.IsEnabled = isEnabled;
numStartMinute.IsEnabled = isEnabled;
numStartSecond.IsEnabled = isEnabled;
}
private void SetEnabledTrimEnd(bool isEnabled)
{
numEndHour.IsEnabled = isEnabled;
numEndMinute.IsEnabled = isEnabled;
numEndSecond.IsEnabled = isEnabled;
}
private async void btnGetInfo_Click(object sender, RoutedEventArgs e)
{
await GetVideoInfo();
}
private async Task GetVideoInfo()
{
string id = ValidateUrl(textUrl.Text.Trim());
if (string.IsNullOrWhiteSpace(id))
{
MessageBox.Show(Application.Current.MainWindow!, Translations.Strings.UnableToParseLinkMessage, Translations.Strings.UnableToParseLink, MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
btnGetInfo.IsEnabled = false;
downloadId = id;
downloadType = id.All(char.IsDigit) ? DownloadType.Video : DownloadType.Clip;
try
{
if (downloadType == DownloadType.Video)
{
GqlVideoResponse videoInfo = await TwitchHelper.GetVideoInfo(long.Parse(downloadId));
var thumbUrl = videoInfo.data.video.thumbnailURLs.FirstOrDefault();
if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
_ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
imgThumbnail.Source = image;
vodLength = TimeSpan.FromSeconds(videoInfo.data.video.lengthSeconds);
textTitle.Text = videoInfo.data.video.title;
textStreamer.Text = videoInfo.data.video.owner?.displayName ?? Translations.Strings.UnknownUser;
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 = videoInfo.data.video.owner?.id;
clipper = null;
clipperId = null;
viewCount = videoInfo.data.video.viewCount;
game = videoInfo.data.video.game?.displayName ?? Translations.Strings.UnknownGame;
var urlTimeCodeMatch = TwitchRegex.UrlTimeCode.Match(textUrl.Text);
if (urlTimeCodeMatch.Success)
{
var time = UrlTimeCode.Parse(urlTimeCodeMatch.ValueSpan);
CheckTrimStart.IsChecked = true;
numStartHour.Value = time.Hours;
numStartMinute.Value = time.Minutes;
numStartSecond.Value = time.Seconds;
}
else
{
numStartHour.Value = 0;
numStartMinute.Value = 0;
numStartSecond.Value = 0;
}
if (vodLength > TimeSpan.Zero)
{
numStartHour.Maximum = (int)vodLength.TotalHours;
numEndHour.Maximum = (int)vodLength.TotalHours;
}
else
{
numStartHour.Maximum = 48;
numEndHour.Maximum = 48;
}
numEndHour.Value = (int)vodLength.TotalHours;
numEndMinute.Value = vodLength.Minutes;
numEndSecond.Value = vodLength.Seconds;
labelLength.Text = vodLength.ToString("c");
SetEnabled(true);
}
else if (downloadType == DownloadType.Clip)
{
string clipId = downloadId;
GqlClipResponse clipInfo = await TwitchHelper.GetClipInfo(clipId);
var thumbUrl = clipInfo.data.clip.thumbnailURL;
if (!ThumbnailService.TryGetThumb(thumbUrl, out var image))
{
AppendLog(Translations.Strings.ErrorLog + Translations.Strings.UnableToFindThumbnail);
_ = ThumbnailService.TryGetThumb(ThumbnailService.THUMBNAIL_MISSING_URL, out image);
}
imgThumbnail.Source = image;
vodLength = TimeSpan.FromSeconds(clipInfo.data.clip.durationSeconds);
textStreamer.Text = clipInfo.data.clip.broadcaster?.displayName ?? Translations.Strings.UnknownUser;
var clipCreatedAt = clipInfo.data.clip.createdAt;
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 = clipInfo.data.clip.broadcaster?.id;
clipper = clipInfo.data.clip.curator?.displayName ?? Translations.Strings.UnknownUser;
clipperId = clipInfo.data.clip.curator?.id;
labelLength.Text = vodLength.ToString("c");
SetEnabled(true);
numStartHour.Maximum = 0;
numStartHour.Value = 0;
numStartMinute.Maximum = vodLength.Minutes;
numStartMinute.Value = 0;
numStartSecond.Value = 0;
numEndHour.Maximum = 0;
numEndHour.Value = 0;
numEndMinute.Maximum = vodLength.Minutes;
numEndMinute.Value = vodLength.Minutes;
numEndSecond.Value = vodLength.Seconds;
}
btnGetInfo.IsEnabled = true;
}
catch (Exception ex)
{
MessageBox.Show(Application.Current.MainWindow!, Translations.Strings.UnableToGetInfoMessage, Translations.Strings.UnableToGetInfo, MessageBoxButton.OK, MessageBoxImage.Error);
AppendLog(Translations.Strings.ErrorLog + ex.Message);
btnGetInfo.IsEnabled = true;
if (Settings.Default.VerboseErrors)
{
MessageBox.Show(Application.Current.MainWindow!, ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
private void UpdateActionButtons(bool isDownloading)
{
if (isDownloading)
{
SplitBtnDownload.Visibility = Visibility.Collapsed;
BtnCancel.Visibility = Visibility.Visible;
return;
}
SplitBtnDownload.Visibility = Visibility.Visible;
BtnCancel.Visibility = Visibility.Collapsed;
}
public static string ValidateUrl(string text)
{
var vodClipIdMatch = IdParse.MatchVideoOrClipId(text);
return vodClipIdMatch is { Success: true }
? vodClipIdMatch.Value
: null;
}
private void SetPercent(int percent)
{
Dispatcher.BeginInvoke(() =>
statusProgressBar.Value = percent
);
}
private void SetStatus(string message)
{
Dispatcher.BeginInvoke(() =>
statusMessage.Text = message
);
}
private void AppendLog(string message)
{
textLog.Dispatcher.BeginInvoke(() =>
textLog.AppendText(message + Environment.NewLine)
);
}
public ChatDownloadOptions GetOptions(string filename)
{
ChatDownloadOptions options = new ChatDownloadOptions();
if (radioJson.IsChecked == true)
options.DownloadFormat = ChatFormat.Json;
else if (radioHTML.IsChecked == true)
options.DownloadFormat = ChatFormat.Html;
else if (radioText.IsChecked == true)
options.DownloadFormat = ChatFormat.Text;
// TODO: Support non-json chat compression
if (radioCompressionNone.IsChecked == true || options.DownloadFormat != ChatFormat.Json)
options.Compression = ChatCompression.None;
else if (radioCompressionGzip.IsChecked == true)
options.Compression = ChatCompression.Gzip;
if (CheckTrimStart.IsChecked == true)
{
options.TrimBeginning = true;
TimeSpan start = new TimeSpan((int)numStartHour.Value, (int)numStartMinute.Value, (int)numStartSecond.Value);
options.TrimBeginningTime = (int)start.TotalSeconds;
}
if (CheckTrimEnd.IsChecked == true)
{
options.TrimEnding = true;
TimeSpan end = new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value);
options.TrimEndingTime = (int)end.TotalSeconds;
}
if (radioTimestampUTC.IsChecked == true)
options.TimeFormat = TimestampFormat.Utc;
else if (radioTimestampRelative.IsChecked == true)
options.TimeFormat = TimestampFormat.Relative;
else if (radioTimestampNone.IsChecked == true)
options.TimeFormat = TimestampFormat.None;
options.Id = downloadId;
options.EmbedData = checkEmbed.IsChecked.GetValueOrDefault();
options.BttvEmotes = checkBttvEmbed.IsChecked.GetValueOrDefault();
options.FfzEmotes = checkFfzEmbed.IsChecked.GetValueOrDefault();
options.StvEmotes = checkStvEmbed.IsChecked.GetValueOrDefault();
options.Filename = filename;
options.DownloadThreads = (int)NumChatDownloadThreads.Value;
return options;
}
public void SetImage(string imageUri, bool isGif)
{
var image = new BitmapImage();
image.BeginInit();
image.UriSource = new Uri(imageUri, UriKind.Relative);
image.EndInit();
if (isGif)
{
ImageBehavior.SetAnimatedSource(statusImage, image);
}
else
{
ImageBehavior.SetAnimatedSource(statusImage, null);
statusImage.Source = image;
}
}
private void btnDonate_Click(object sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo("https://www.buymeacoffee.com/lay295") { UseShellExecute = true });
}
private void btnSettings_Click(object sender, RoutedEventArgs e)
{
var settings = new WindowSettings
{
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
settings.ShowDialog();
btnDonate.Visibility = Settings.Default.HideDonation ? Visibility.Collapsed : Visibility.Visible;
statusImage.Visibility = Settings.Default.ReduceMotion ? Visibility.Collapsed : Visibility.Visible;
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
btnDonate.Visibility = Settings.Default.HideDonation ? Visibility.Collapsed : Visibility.Visible;
statusImage.Visibility = Settings.Default.ReduceMotion ? Visibility.Collapsed : Visibility.Visible;
}
private void NumChatDownloadThreads_ValueChanged(object sender, HandyControl.Data.FunctionEventArgs e)
{
if (this.IsInitialized)
{
NumChatDownloadThreads.Value = Math.Clamp((int)NumChatDownloadThreads.Value, 1, 50);
Settings.Default.ChatDownloadThreads = (int)NumChatDownloadThreads.Value;
Settings.Default.Save();
}
}
private void checkEmbed_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.ChatEmbedEmotes = true;
Settings.Default.Save();
checkBttvEmbed.IsEnabled = true;
checkFfzEmbed.IsEnabled = true;
checkStvEmbed.IsEnabled = true;
}
}
private void checkEmbed_Unchecked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.ChatEmbedEmotes = false;
Settings.Default.Save();
checkBttvEmbed.IsEnabled = false;
checkFfzEmbed.IsEnabled = false;
checkStvEmbed.IsEnabled = false;
}
}
private void checkBttvEmbed_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.BTTVEmotes = true;
Settings.Default.Save();
}
}
private void checkBttvEmbed_Unchecked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.BTTVEmotes = false;
Settings.Default.Save();
}
}
private void checkFfzEmbed_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.FFZEmotes = true;
Settings.Default.Save();
}
}
private void checkFfzEmbed_Unchecked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.FFZEmotes = false;
Settings.Default.Save();
}
}
private void checkStvEmbed_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.STVEmotes = true;
Settings.Default.Save();
}
}
private void checkStvEmbed_Unchecked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
Settings.Default.STVEmotes = false;
Settings.Default.Save();
}
}
private void radioJson_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
timeText.Visibility = Visibility.Collapsed;
timeOptions.Visibility = Visibility.Collapsed;
stackEmbedText.Visibility = Visibility.Visible;
stackEmbedChecks.Visibility = Visibility.Visible;
compressionText.Visibility = Visibility.Visible;
compressionOptions.Visibility = Visibility.Visible;
textTrim.Margin = new Thickness(0, 10, 0, 33);
Settings.Default.ChatDownloadType = (int)ChatFormat.Json;
Settings.Default.Save();
}
}
private void radioHTML_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
timeText.Visibility = Visibility.Collapsed;
timeOptions.Visibility = Visibility.Collapsed;
stackEmbedText.Visibility = Visibility.Visible;
stackEmbedChecks.Visibility = Visibility.Visible;
compressionText.Visibility = Visibility.Collapsed;
compressionOptions.Visibility = Visibility.Collapsed;
textTrim.Margin = new Thickness(0, 17, 0, 33);
Settings.Default.ChatDownloadType = (int)ChatFormat.Html;
Settings.Default.Save();
}
}
private void radioText_Checked(object sender, RoutedEventArgs e)
{
if (this.IsInitialized)
{
timeText.Visibility = Visibility.Visible;
timeOptions.Visibility = Visibility.Visible;
stackEmbedText.Visibility = Visibility.Collapsed;
stackEmbedChecks.Visibility = Visibility.Collapsed;
compressionText.Visibility = Visibility.Collapsed;
compressionOptions.Visibility = Visibility.Collapsed;
textTrim.Margin = new Thickness(0, 10, 0, 36);
Settings.Default.ChatDownloadType = (int)ChatFormat.Text;
Settings.Default.Save();
}
}
private async void SplitBtnDownload_Click(object sender, RoutedEventArgs e)
{
if (((HandyControl.Controls.SplitButton)sender).IsDropDownOpen)
{
return;
}
var saveFileDialog = new SaveFileDialog
{
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,
vodLength, viewCount, game, clipper, clipperId)
};
if (radioJson.IsChecked == true)
{
if (radioCompressionNone.IsChecked == true)
{
saveFileDialog.Filter = "JSON Files | *.json";
saveFileDialog.FileName += ".json";
}
else if (radioCompressionGzip.IsChecked == true)
{
saveFileDialog.Filter = "GZip JSON Files | *.json.gz";
saveFileDialog.FileName += ".json.gz";
}
}
else if (radioHTML.IsChecked == true)
{
saveFileDialog.Filter = "HTML Files | *.html";
saveFileDialog.FileName += ".html";
}
else if (radioText.IsChecked == true)
{
saveFileDialog.Filter = "TXT Files | *.txt";
saveFileDialog.FileName += ".txt";
}
if (saveFileDialog.ShowDialog() != true)
{
return;
}
try
{
ChatDownloadOptions downloadOptions = GetOptions(saveFileDialog.FileName);
var downloadProgress = new WpfTaskProgress((LogLevel)Settings.Default.LogLevels, SetPercent, SetStatus, AppendLog);
var currentDownload = new ChatDownloader(downloadOptions, downloadProgress);
btnGetInfo.IsEnabled = false;
SetEnabled(false);
SetImage("Images/ppOverheat.gif", true);
statusMessage.Text = Translations.Strings.StatusDownloading;
_cancellationTokenSource = new CancellationTokenSource();
UpdateActionButtons(true);
try
{
await currentDownload.DownloadAsync(_cancellationTokenSource.Token);
downloadProgress.SetStatus(Translations.Strings.StatusDone);
SetImage("Images/ppHop.gif", true);
}
catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException && _cancellationTokenSource.IsCancellationRequested)
{
downloadProgress.SetStatus(Translations.Strings.StatusCanceled);
SetImage("Images/ppHop.gif", true);
}
catch (Exception ex)
{
downloadProgress.SetStatus(Translations.Strings.StatusError);
SetImage("Images/peepoSad.png", false);
AppendLog(Translations.Strings.ErrorLog + ex.Message);
if (Settings.Default.VerboseErrors)
{
MessageBox.Show(Application.Current.MainWindow!, ex.ToString(), Translations.Strings.VerboseErrorOutput, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
btnGetInfo.IsEnabled = true;
downloadProgress.ReportProgress(0);
_cancellationTokenSource.Dispose();
UpdateActionButtons(false);
GC.Collect();
}
catch (Exception ex)
{
AppendLog(Translations.Strings.ErrorLog + ex.Message);
}
}
private void BtnCancel_Click(object sender, RoutedEventArgs e)
{
statusMessage.Text = Translations.Strings.StatusCanceling;
SetImage("Images/ppStretch.gif", true);
try
{
_cancellationTokenSource.Cancel();
}
catch (ObjectDisposedException) { }
}
private void CheckTrimStart_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
SetEnabledTrimStart(CheckTrimStart.IsChecked.GetValueOrDefault());
}
private void CheckTrimEnd_OnCheckStateChanged(object sender, RoutedEventArgs e)
{
SetEnabledTrimEnd(CheckTrimEnd.IsChecked.GetValueOrDefault());
}
private void MenuItemEnqueue_Click(object sender, RoutedEventArgs e)
{
var queueOptions = new WindowQueueOptions(this)
{
Owner = Application.Current.MainWindow,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
queueOptions.ShowDialog();
}
private async void TextUrl_OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
await GetVideoInfo();
e.Handled = true;
}
}
private void RadioCompressionNone_OnCheckedChanged(object sender, RoutedEventArgs e)
{
if (!IsInitialized)
return;
Settings.Default.ChatJsonCompression = (int)ChatCompression.None;
Settings.Default.Save();
}
private void RadioCompressionGzip_OnCheckedChanged(object sender, RoutedEventArgs e)
{
if (!IsInitialized)
return;
Settings.Default.ChatJsonCompression = (int)ChatCompression.Gzip;
Settings.Default.Save();
}
private void RadioTimestampUTC_OnCheckedChanged(object sender, RoutedEventArgs e)
{
if (!IsInitialized)
return;
Settings.Default.ChatTextTimestampStyle = (int)TimestampFormat.Utc;
Settings.Default.Save();
}
private void RadioTimestampRelative_OnCheckedChanged(object sender, RoutedEventArgs e)
{
if (!IsInitialized)
return;
Settings.Default.ChatTextTimestampStyle = (int)TimestampFormat.Relative;
Settings.Default.Save();
}
private void RadioTimestampNone_OnCheckedChanged(object sender, RoutedEventArgs e)
{
if (!IsInitialized)
return;
Settings.Default.ChatTextTimestampStyle = (int)TimestampFormat.None;
Settings.Default.Save();
}
}
}