new_version_init

This commit is contained in:
2024-02-13 02:59:40 +01:00
Unverified
parent e36c1404ee
commit 91f9b645bd
352 changed files with 6777 additions and 8326 deletions

View File

@@ -1,12 +0,0 @@
namespace VDownload.Core.Enums
{
public enum AudioFileExtension
{
MP3 = 3,
FLAC = 4,
WAV = 5,
M4A = 6,
ALAC = 7,
WMA = 8,
}
}

View File

@@ -1,14 +0,0 @@
namespace VDownload.Core.Enums
{
public enum DownloadTaskStatus
{
Idle,
Scheduled,
Queued,
Downloading,
Processing,
Finalizing,
EndedSuccessfully,
EndedUnsuccessfully,
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum DownloadTasksAddingRequestSource
{
Video,
Playlist,
Subscriptions,
}
}

View File

@@ -1,15 +0,0 @@
namespace VDownload.Core.Enums
{
public enum MediaFileExtension
{
MP4 = VideoFileExtension.MP4,
WMV = VideoFileExtension.WMV,
HEVC = VideoFileExtension.HEVC,
MP3 = AudioFileExtension.MP3,
FLAC = AudioFileExtension.FLAC,
WAV = AudioFileExtension.WAV,
M4A = AudioFileExtension.M4A,
ALAC = AudioFileExtension.ALAC,
WMA = AudioFileExtension.WMA,
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum MediaType
{
AudioVideo = 0,
OnlyAudio = 1,
OnlyVideo = 2,
}
}

View File

@@ -1,8 +0,0 @@
namespace VDownload.Core.Enums
{
public enum PlaylistSource
{
TwitchChannel,
Null
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum VideoFileExtension
{
MP4 = 0,
WMV = 1,
HEVC = 2,
}
}

View File

@@ -1,9 +0,0 @@
namespace VDownload.Core.Enums
{
public enum VideoSource
{
TwitchVod,
TwitchClip,
Null
}
}

View File

@@ -1,61 +0,0 @@
using System;
using VDownload.Core.Enums;
namespace VDownload.Core.EventArgs
{
public class DownloadTaskStatusChangedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status)
{
Status = status;
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, DateTime scheduledFor)
{
Status = status;
ScheduledFor = scheduledFor;
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, double progress)
{
Status = status;
if (Status == DownloadTaskStatus.Downloading)
{
DownloadingProgress = progress;
}
else if (Status == DownloadTaskStatus.Processing)
{
ProcessingProgress = progress;
}
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, TimeSpan elapsedTime)
{
Status = status;
ElapsedTime = elapsedTime;
}
public DownloadTaskStatusChangedEventArgs(DownloadTaskStatus status, Exception exception)
{
Status = status;
Exception = exception;
}
#endregion
#region PROPERTIES
public DownloadTaskStatus Status { get; private set; }
public DateTime ScheduledFor { get; private set; }
public double DownloadingProgress { get; private set; }
public double ProcessingProgress { get; private set; }
public TimeSpan ElapsedTime { get; private set; }
public Exception Exception { get; private set; }
#endregion
}
}

View File

@@ -1,27 +0,0 @@
using VDownload.Core.Enums;
using VDownload.Core.Structs;
namespace VDownload.Core.EventArgs
{
public class DownloadTasksAddingRequestedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public DownloadTasksAddingRequestedEventArgs(DownloadTask[] downloadTasks, DownloadTasksAddingRequestSource requestSource)
{
DownloadTasks = downloadTasks;
RequestSource = requestSource;
}
#endregion
#region PROPERTIES
public DownloadTask[] DownloadTasks { get; private set; }
public DownloadTasksAddingRequestSource RequestSource { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
using VDownload.Core.Interfaces;
namespace VDownload.Core.EventArgs
{
public class PlaylistSearchSuccessedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public PlaylistSearchSuccessedEventArgs(IPlaylist playlist)
{
Playlist = playlist;
}
#endregion
#region PROPERTIES
public IPlaylist Playlist { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
namespace VDownload.Core.EventArgs
{
public class ProgressChangedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public ProgressChangedEventArgs(double progress, bool isCompleted = false)
{
Progress = progress;
IsCompleted = isCompleted;
}
#endregion
#region PROPERTIES
public double Progress { get; private set; }
public bool IsCompleted { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
using VDownload.Core.Interfaces;
namespace VDownload.Core.EventArgs
{
public class SubscriptionLoadSuccessedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public SubscriptionLoadSuccessedEventArgs(IVideo[] videos)
{
Videos = videos;
}
#endregion
#region PROPERTIES
public IVideo[] Videos { get; private set; }
#endregion
}
}

View File

@@ -1,24 +0,0 @@
using VDownload.Core.Interfaces;
namespace VDownload.Core.EventArgs
{
public class VideoSearchSuccessedEventArgs : System.EventArgs
{
#region CONSTRUCTORS
public VideoSearchSuccessedEventArgs(IVideo video)
{
Video = video;
}
#endregion
#region PROPERTIES
public IVideo Video { get; set; }
#endregion
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace VDownload.Core.Exceptions
{
public class MediaNotFoundException : Exception
{
public MediaNotFoundException() { }
public MediaNotFoundException(string message) : base(message) { }
public MediaNotFoundException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Exceptions
{
public class SubscriptionExistsException : Exception
{
public SubscriptionExistsException() { }
public SubscriptionExistsException(string message) : base(message) { }
public SubscriptionExistsException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace VDownload.Core.Exceptions
{
public class TwitchAccessTokenNotFoundException : Exception
{
public TwitchAccessTokenNotFoundException() { }
public TwitchAccessTokenNotFoundException(string message) : base(message) { }
public TwitchAccessTokenNotFoundException(string message, Exception inner) :base(message, inner) { }
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace VDownload.Core.Exceptions
{
public class TwitchAccessTokenNotValidException : Exception
{
public TwitchAccessTokenNotValidException() { }
public TwitchAccessTokenNotValidException(string message) : base(message) { }
public TwitchAccessTokenNotValidException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Linq;
using System.Text;
namespace VDownload.Core.Extensions
{
public static class TimeSpanExtension
{
// To string (TH:)MM:SS
public static string ToStringOptTHBaseMMSS(this TimeSpan timeSpan, params TimeSpan[] formatBase)
{
StringBuilder formattedTimeSpan = new StringBuilder();
int maxTHLength = 0;
foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan }))
{
int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0;
if (THLength > maxTHLength)
{
maxTHLength = THLength;
}
}
if (maxTHLength > 0)
{
formattedTimeSpan.Append($"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:");
}
formattedTimeSpan.Append(maxTHLength == 0 ? $"{timeSpan.Minutes}:" : $"{timeSpan.Minutes:00}:");
formattedTimeSpan.Append($"{timeSpan.Seconds:00}");
return formattedTimeSpan.ToString();
}
// To string ((TH:)MM:)SS
public static string ToStringOptTHMMBaseSS(this TimeSpan timeSpan, params TimeSpan[] formatBase)
{
StringBuilder formattedTimeSpan = new StringBuilder();
int maxTHLength = 0;
foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan }))
{
int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0;
if (THLength > maxTHLength)
{
maxTHLength = THLength;
}
}
if (maxTHLength > 0)
{
formattedTimeSpan.Append($"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:");
}
bool MM = false;
if (Math.Floor(timeSpan.TotalMinutes) > 0 || maxTHLength > 0)
{
formattedTimeSpan.Append(maxTHLength > 0 ? $"{timeSpan.Minutes:00}:" : $"{timeSpan.Minutes}:");
MM = true;
}
formattedTimeSpan.Append(MM ? $"{timeSpan.Seconds:00}" : $"{timeSpan.Seconds}");
return formattedTimeSpan.ToString();
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
namespace VDownload.Core.Interfaces
{
public interface IPlaylist
{
#region PROPERTIES
PlaylistSource Source { get; }
string ID { get; }
Uri Url { get; }
string Name { get; }
IVideo[] Videos { get; }
#endregion
#region METHODS
Task GetMetadataAsync(CancellationToken cancellationToken = default);
Task GetVideosAsync(CancellationToken cancellationToken = default);
Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default);
#endregion
}
}

View File

@@ -1,49 +0,0 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Interfaces
{
public interface IVideo
{
#region PROPERTIES
VideoSource Source { get; }
string ID { get; }
Uri Url { get; }
string Title { get; }
string Author { get; }
DateTime Date { get; }
TimeSpan Duration { get; }
long Views { get; }
Uri Thumbnail { get; }
BaseStream[] BaseStreams { get; }
#endregion
#region METHODS
Task GetMetadataAsync(CancellationToken cancellationToken = default);
Task GetStreamsAsync(CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default);
#endregion
#region EVENTS
event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -1,76 +0,0 @@
using System.Collections.Generic;
using VDownload.Core.Enums;
using Windows.Media.Editing;
using Windows.Media.Transcoding;
using Windows.Storage;
namespace VDownload.Core.Services
{
public static class Config
{
#region CONSTANTS
private static readonly Dictionary<string, object> DefaultSettings = new Dictionary<string, object>()
{
{ "delete_temp_on_start", true },
{ "twitch_vod_passive_trim", true },
{ "twitch_vod_downloading_chunk_retry_after_error", false },
{ "twitch_vod_downloading_chunk_max_retries", 10 },
{ "twitch_vod_downloading_chunk_retries_delay", 5000 },
{ "media_transcoding_use_hardware_acceleration", true },
{ "media_transcoding_algorithm", (int)MediaVideoProcessingAlgorithm.MrfCrf444 },
{ "media_editing_algorithm", (int)MediaTrimmingPreference.Fast },
{ "default_max_playlist_videos", 0 },
{ "default_media_type", (int)MediaType.AudioVideo },
{ "default_filename", "[<date_pub:yyyy.MM.dd>] <title>" },
{ "default_video_extension", (int)VideoFileExtension.MP4 },
{ "default_audio_extension", (int)AudioFileExtension.MP3 },
{ "custom_media_location", false },
{ "max_active_video_task", 5 },
{ "replace_output_file_if_exists", false },
{ "remove_task_when_successfully_ended", false },
{ "delete_task_temp_when_ended_with_error", false },
{ "show_notification_when_task_ended_successfully", false },
{ "show_notification_when_task_ended_unsuccessfully", false },
{ "show_warning_when_task_starts_on_metered_network", true },
{ "delay_task_when_queued_task_starts_on_metered_network", true }
};
#endregion
#region PUBLIC METHODS
public static object GetValue(string key)
{
return ApplicationData.Current.LocalSettings.Values[key];
}
public static void SetValue(string key, object value)
{
ApplicationData.Current.LocalSettings.Values[key] = value;
}
public static void SetDefault()
{
foreach (KeyValuePair<string, object> s in DefaultSettings)
{
ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value;
}
}
public static void Rebuild()
{
foreach (KeyValuePair<string, object> s in DefaultSettings)
{
if (!ApplicationData.Current.LocalSettings.Values.ContainsKey(s.Key))
{
ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value;
}
}
}
#endregion
}
}

View File

@@ -1,178 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.EventArgs;
using VDownload.Core.Interfaces;
using VDownload.Core.Services;
using Windows.ApplicationModel.ExtendedExecution;
using Windows.Storage;
using Windows.Storage.AccessCache;
namespace VDownload.Core.Structs
{
public class DownloadTask
{
#region CONSTRUCTORS
public DownloadTask(string id, IVideo video, MediaType mediaType, BaseStream selectedStream, TrimData trim, OutputFile file, double schedule)
{
Id = id;
Video = video;
MediaType = mediaType;
SelectedStream = selectedStream;
Trim = trim;
File = file;
Schedule = schedule;
Status = DownloadTaskStatus.Idle;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status);
CancellationTokenSource = new CancellationTokenSource();
}
#endregion
#region PROPERTIES
public string Id { get; set; }
public IVideo Video { get; set; }
public MediaType MediaType { get; set; }
public BaseStream SelectedStream { get; set; }
public TrimData Trim { get; set; }
public OutputFile File { get; set; }
public double Schedule { get; set; }
public DownloadTaskStatus Status { get; private set; }
public DownloadTaskStatusChangedEventArgs LastStatusChangedEventArgs { get; private set; }
public CancellationTokenSource CancellationTokenSource { get; private set; }
#endregion
#region METHODS
public async Task Run(bool delayWhenOnMeteredConnection)
{
StatusChanged.Invoke(this, new DownloadTaskStatusChangedEventArgs(Status));
CancellationTokenSource = new CancellationTokenSource();
if (Schedule > 0)
{
DateTime scheduleFor = DateTime.Now.AddMinutes(Schedule);
Status = DownloadTaskStatus.Scheduled;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, scheduleFor);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
while (DateTime.Now < scheduleFor && !CancellationTokenSource.Token.IsCancellationRequested)
{
await Task.Delay(100);
}
}
Status = DownloadTaskStatus.Queued;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
await DownloadTasksCollectionManagement.WaitInQueue(delayWhenOnMeteredConnection, CancellationTokenSource.Token);
if (!CancellationTokenSource.Token.IsCancellationRequested)
{
Status = DownloadTaskStatus.Downloading;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, 0);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
StorageFolder tempFolder;
if (StorageApplicationPermissions.FutureAccessList.ContainsItem("custom_temp_location"))
{
tempFolder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync("custom_temp_location");
}
else
{
tempFolder = ApplicationData.Current.TemporaryFolder;
}
tempFolder = await tempFolder.CreateFolderAsync(Id);
bool endedWithError = false;
try
{
CancellationTokenSource.Token.ThrowIfCancellationRequested();
Stopwatch taskStopwatch = Stopwatch.StartNew();
ExtendedExecutionSession session = new ExtendedExecutionSession { Reason = ExtendedExecutionReason.Unspecified };
await session.RequestExtensionAsync();
CancellationTokenSource.Token.ThrowIfCancellationRequested();
Video.DownloadingProgressChanged += DownloadingProgressChanged;
Video.ProcessingProgressChanged += ProcessingProgressChanged;
StorageFile tempOutputFile = await Video.DownloadAndTranscodeAsync(tempFolder, SelectedStream, File.Extension, MediaType, Trim, CancellationTokenSource.Token);
session.Dispose();
Status = DownloadTaskStatus.Finalizing;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
StorageFile outputFile = await File.Create();
CancellationTokenSource.Token.ThrowIfCancellationRequested();
await tempOutputFile.MoveAndReplaceAsync(outputFile);
taskStopwatch.Stop();
Status = DownloadTaskStatus.EndedSuccessfully;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, taskStopwatch.Elapsed);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
catch (Exception ex)
{
endedWithError = true;
Status = DownloadTaskStatus.EndedUnsuccessfully;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, ex);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
finally
{
if (!endedWithError || (bool)Config.GetValue("delete_task_temp_when_ended_with_error"))
{
// Delete temporary files
await tempFolder.DeleteAsync();
}
}
}
else
{
Status = DownloadTaskStatus.EndedUnsuccessfully;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, new OperationCanceledException(CancellationTokenSource.Token));
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
}
private void DownloadingProgressChanged(object sender, ProgressChangedEventArgs e)
{
Status = DownloadTaskStatus.Downloading;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, e.Progress);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
private void ProcessingProgressChanged(object sender, ProgressChangedEventArgs e)
{
Status = DownloadTaskStatus.Processing;
LastStatusChangedEventArgs = new DownloadTaskStatusChangedEventArgs(Status, e.Progress);
StatusChanged.Invoke(this, LastStatusChangedEventArgs);
}
#endregion
#region EVENT
public event EventHandler<DownloadTaskStatusChangedEventArgs> StatusChanged;
#endregion
}
}

View File

@@ -1,64 +0,0 @@
using Microsoft.Toolkit.Uwp.Connectivity;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Structs;
namespace VDownload.Core.Services
{
public static class DownloadTasksCollectionManagement
{
#region PROPERTIES
private static readonly Dictionary<string, DownloadTask> ChangeableDownloadTasksCollection = new Dictionary<string, DownloadTask>();
public static readonly ReadOnlyDictionary<string, DownloadTask> DownloadTasksCollection = new ReadOnlyDictionary<string, DownloadTask>(ChangeableDownloadTasksCollection);
#endregion
#region PUBLIC METHODS
public static string GenerateID()
{
char[] idChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
int idLength = 10;
string id;
do
{
id = "";
while (id.Length < idLength)
{
id += idChars[new Random().Next(0, idChars.Length)];
}
} while (DownloadTasksCollection.Keys.Contains(id));
return id;
}
public static void Add(DownloadTask downloadTask, string id)
{
ChangeableDownloadTasksCollection[id] = downloadTask;
}
public static void Remove(string id)
{
ChangeableDownloadTasksCollection.Remove(id);
}
public static async Task WaitInQueue(bool delayWhenOnMeteredConnection, CancellationToken cancellationToken = default)
{
while ((ChangeableDownloadTasksCollection.Values.Where((DownloadTask task) => task.Status == Enums.DownloadTaskStatus.Downloading || task.Status == Enums.DownloadTaskStatus.Processing || task.Status == Enums.DownloadTaskStatus.Finalizing).Count() >= (int)Config.GetValue("max_active_video_task") || (delayWhenOnMeteredConnection && NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection)) && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(100);
}
}
#endregion
}
}

View File

@@ -1,146 +0,0 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Structs;
using Windows.Foundation;
using Windows.Media.Editing;
using Windows.Media.MediaProperties;
using Windows.Media.Transcoding;
using Windows.Storage;
using Windows.Storage.Streams;
namespace VDownload.Core.Services
{
public class MediaProcessor
{
#region PUBLIC METHODS
public async Task Run(StorageFile mediaFile, MediaFileExtension extension, MediaType mediaType, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
MediaTranscoder mediaTranscoder = new MediaTranscoder
{
HardwareAccelerationEnabled = (bool)Config.GetValue("media_transcoding_use_hardware_acceleration"),
VideoProcessingAlgorithm = (MediaVideoProcessingAlgorithm)Config.GetValue("media_transcoding_algorithm"),
};
if (trim.Start != null) mediaTranscoder.TrimStartTime = trim.Start;
if (trim.End != null) mediaTranscoder.TrimStopTime = trim.End;
using (IRandomAccessStream openedOutputFile = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
{
PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await mediaFile.OpenAsync(FileAccessMode.Read), openedOutputFile, await GetMediaEncodingProfile(mediaFile, extension, mediaType));
IAsyncActionWithProgress<double> transcodingTask = transcodingPreparated.TranscodeAsync();
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(percent)); }));
cancellationToken.ThrowIfCancellationRequested();
await openedOutputFile.FlushAsync();
transcodingTask.Close();
}
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
}
public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
MediaComposition mediaEditor = new MediaComposition();
cancellationToken.ThrowIfCancellationRequested();
Task<MediaClip> getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask();
Task<BackgroundAudioTrack> getAudioFileTask = BackgroundAudioTrack.CreateFromFileAsync(audioFile).AsTask();
await Task.WhenAll(getVideoFileTask, getAudioFileTask);
MediaClip videoElement = getVideoFileTask.Result;
if (trim.Start != null) videoElement.TrimTimeFromStart = trim.Start;
if (trim.End != null) videoElement.TrimTimeFromEnd = trim.End;
BackgroundAudioTrack audioElement = getAudioFileTask.Result;
if (trim.Start != null) audioElement.TrimTimeFromStart = trim.Start;
if (trim.End != null) audioElement.TrimTimeFromEnd = trim.End;
mediaEditor.Clips.Add(videoElement);
mediaEditor.BackgroundAudioTracks.Add(audioElement);
var renderOperation = mediaEditor.RenderToFileAsync(outputFile, (MediaTrimmingPreference)Config.GetValue("media_editing_algorithm"), await GetMediaEncodingProfile(videoFile, audioFile, (MediaFileExtension)extension, MediaType.AudioVideo));
renderOperation.Progress += (info, progress) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(progress)); };
cancellationToken.ThrowIfCancellationRequested();
await renderOperation.AsTask(cancellationToken);
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
}
public async Task Run(StorageFile audioFile, AudioFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, outputFile, trim, cancellationToken);
}
public async Task Run(StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default)
{
await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, outputFile, trim, cancellationToken);
}
#endregion
#region PRIVATE METHODS
private static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType)
{
MediaEncodingProfile profile;
switch (extension)
{
default:
case MediaFileExtension.MP4: profile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.WMV: profile = MediaEncodingProfile.CreateWmv(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.HEVC: profile = MediaEncodingProfile.CreateHevc(VideoEncodingQuality.HD1080p); break;
case MediaFileExtension.MP3: profile = MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High); break;
case MediaFileExtension.FLAC: profile = MediaEncodingProfile.CreateFlac(AudioEncodingQuality.High); break;
case MediaFileExtension.WAV: profile = MediaEncodingProfile.CreateWav(AudioEncodingQuality.High); break;
case MediaFileExtension.M4A: profile = MediaEncodingProfile.CreateM4a(AudioEncodingQuality.High); break;
case MediaFileExtension.ALAC: profile = MediaEncodingProfile.CreateAlac(AudioEncodingQuality.High); break;
case MediaFileExtension.WMA: profile = MediaEncodingProfile.CreateWma(AudioEncodingQuality.High); break;
}
if (mediaType != MediaType.OnlyAudio)
{
var videoData = await videoFile.Properties.GetVideoPropertiesAsync();
profile.Video.Height = videoData.Height;
profile.Video.Width = videoData.Width;
profile.Video.Bitrate = videoData.Bitrate;
}
if (mediaType != MediaType.OnlyVideo)
{
var audioData = await audioFile.Properties.GetMusicPropertiesAsync();
profile.Audio.Bitrate = audioData.Bitrate;
if (mediaType == MediaType.AudioVideo) profile.Video.Bitrate -= audioData.Bitrate;
}
if (mediaType == MediaType.OnlyVideo)
{
var audioTracks = profile.GetAudioTracks();
audioTracks.Clear();
profile.SetAudioTracks(audioTracks.AsEnumerable());
}
return profile;
}
private static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType)
{
return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType);
}
#endregion
#region EVENTS
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProgressChanged;
#endregion
}
}

View File

@@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using Windows.Storage;
namespace VDownload.Core.Services
{
public class OutputFile
{
#region CONSTRUCTORS
public OutputFile(string name, MediaFileExtension extension, StorageFolder location)
{
Name = name;
Extension = extension;
Location = location;
}
public OutputFile(string name, MediaFileExtension extension)
{
Name = name;
Extension = extension;
Location = null;
}
#endregion
#region PROPERTIES
public string Name { get; private set; }
public MediaFileExtension Extension { get; private set; }
public StorageFolder Location { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task<StorageFile> Create()
{
string filename = $"{Name}.{Extension.ToString().ToLower()}";
CreationCollisionOption collisionOption = (bool)Config.GetValue("replace_output_file_if_exists") ? CreationCollisionOption.ReplaceExisting : CreationCollisionOption.GenerateUniqueName;
return await(!(Location is null) ? Location.CreateFileAsync(filename, collisionOption) : DownloadsFolder.CreateFileAsync(filename, collisionOption));
}
public string GetPath() => $@"{(Location != null ? Location.Path : $@"{UserDataPaths.GetDefault().Downloads}\VDownload")}\{Name}.{Extension.ToString().ToLower()}";
#endregion
}
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services
{
[Serializable]
public class Subscription
{
#region CONSTRUCTORS
public Subscription(IPlaylist playlist)
{
Playlist = playlist;
SavedVideos = Playlist.Videos;
}
#endregion
#region PROPERTIES
public IPlaylist Playlist { get; private set; }
public IVideo[] SavedVideos { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task<IVideo[]> GetNewVideosAsync(CancellationToken cancellationToken = default)
{
await Playlist.GetVideosAsync(cancellationToken);
return GetUnsavedVideos();
}
public async Task<IVideo[]> GetNewVideosAndUpdateAsync(CancellationToken cancellationToken = default)
{
await Playlist.GetVideosAsync(cancellationToken);
IVideo[] newVideos = GetUnsavedVideos();
SavedVideos = Playlist.Videos;
return newVideos;
}
#endregion
#region PRIVATE METHODS
private IVideo[] GetUnsavedVideos()
{
List<IVideo> newVideos = Playlist.Videos.ToList();
foreach (IVideo savedVideo in SavedVideos)
{
newVideos.RemoveAll((v) => v.Source == savedVideo.Source && v.ID == savedVideo.ID);
}
return newVideos.ToArray();
}
#endregion
}
}

View File

@@ -1,79 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
using Windows.Storage;
namespace VDownload.Core.Services
{
public static class SubscriptionsCollectionManagement
{
#region CONSTANTS
private static readonly StorageFolder SubscriptionFolderLocation = ApplicationData.Current.LocalFolder;
private static readonly string SubscriptionsFolderName = "Subscriptions";
private static readonly string SubscriptionFileExtension = "vsub";
#endregion
#region PUBLIC METHODS
public static async Task<(Subscription Subscription, StorageFile SubscriptionFile)[]> GetSubscriptionsAsync()
{
List<(Subscription Subscription, StorageFile SubscriptionFile)> subscriptions = new List<(Subscription Subscription,StorageFile SubscriptionFile)> ();
StorageFolder subscriptionsFolder = await SubscriptionFolderLocation.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists);
BinaryFormatter formatter = new BinaryFormatter();
foreach (StorageFile file in await subscriptionsFolder.GetFilesAsync())
{
if (file.Name.EndsWith(SubscriptionFileExtension))
{
Stream fileStream = await file.OpenStreamForReadAsync();
Subscription subscription = (Subscription)formatter.Deserialize(fileStream);
subscriptions.Add((subscription, file));
}
}
return subscriptions.ToArray();
}
public static async Task<StorageFile> CreateSubscriptionFileAsync(Subscription subscription)
{
StorageFolder subscriptionsFolder = await SubscriptionFolderLocation.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists);
try
{
StorageFile subscriptionFile = await subscriptionsFolder.CreateFileAsync($"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}.{SubscriptionFileExtension}", CreationCollisionOption.FailIfExists);
BinaryFormatter formatter = new BinaryFormatter();
Stream subscriptionFileStream = await subscriptionFile.OpenStreamForWriteAsync();
formatter.Serialize(subscriptionFileStream, subscription);
return subscriptionFile;
}
catch (Exception ex)
{
if ((uint)ex.HResult == 0x800700B7)
{
throw new SubscriptionExistsException($"Subscription with id \"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}\" already exists");
}
else
{
throw;
}
}
}
public static async Task UpdateSubscriptionFileAsync(Subscription subscription, StorageFile subscriptionFile)
{
BinaryFormatter formatter = new BinaryFormatter();
Stream subscriptionFileStream = await subscriptionFile.OpenStreamForWriteAsync();
formatter.Serialize(subscriptionFileStream, subscription);
}
public static async Task DeleteSubscriptionFileAsync(StorageFile subscriptionFile) => await subscriptionFile.DeleteAsync(StorageDeleteOption.PermanentDelete);
#endregion
}
}

View File

@@ -1,11 +0,0 @@
using Windows.Storage;
namespace VDownload.Core.Sources
{
internal static class AuthorizationData
{
internal static StorageFolder FolderLocation = ApplicationData.Current.LocalCacheFolder;
internal static string FolderName = "AuthData";
internal static string FilesExtension = "auth";
}
}

View File

@@ -1,86 +0,0 @@
using System.Text.RegularExpressions;
using VDownload.Core.Enums;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services.Sources
{
public static class Source
{
#region CONSTANTS
// VIDEO SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, VideoSource Type)[] VideoSources = new (Regex Regex, VideoSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/videos/(?<id>\d+)"), VideoSource.TwitchVod),
(new Regex(@"^https://www.twitch.tv/\S+/clip/(?<id>[^?]+)"), VideoSource.TwitchClip),
(new Regex(@"^https://clips.twitch.tv/(?<id>[^?]+)"), VideoSource.TwitchClip),
};
// PLAYLIST SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, PlaylistSource Type)[] PlaylistSources = new (Regex Regex, PlaylistSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/(?<id>[^?/]+)"), PlaylistSource.TwitchChannel),
};
#endregion
#region METHODS
// GET VIDEO SOURCE
public static IVideo GetVideo(string url)
{
VideoSource source = VideoSource.Null;
string id = string.Empty;
foreach ((Regex Regex, VideoSource Type) Source in VideoSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success)
{
source = Source.Type;
id = sourceMatch.Groups["id"].Value;
}
}
return GetVideo(source, id);
}
public static IVideo GetVideo(VideoSource source, string id)
{
IVideo videoService = null;
switch (source)
{
case VideoSource.TwitchVod: videoService = new Twitch.Vod(id); break;
case VideoSource.TwitchClip: videoService = new Twitch.Clip(id); break;
}
return videoService;
}
// GET PLAYLIST SOURCE
public static IPlaylist GetPlaylist(string url)
{
PlaylistSource source = PlaylistSource.Null;
string id = string.Empty;
foreach ((Regex Regex, PlaylistSource Type) Source in PlaylistSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success)
{
source = Source.Type;
id = sourceMatch.Groups["id"].Value;
}
}
return GetPlaylist(source, id);
}
public static IPlaylist GetPlaylist(PlaylistSource source, string id)
{
IPlaylist playlistService = null;
switch (source)
{
case PlaylistSource.TwitchChannel: playlistService = new Twitch.Channel(id); break;
}
return playlistService;
}
#endregion
}
}

View File

@@ -1,113 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services.Sources.Twitch.Helpers;
namespace VDownload.Core.Services.Sources.Twitch
{
[Serializable]
public class Channel : IPlaylist
{
#region CONSTRUCTORS
public Channel(string id)
{
Source = PlaylistSource.TwitchChannel;
ID = id;
Url = new Uri($"https://twitch.tv/{ID}");
}
#endregion
#region PROPERTIES
public PlaylistSource Source { get; private set; }
public string ID { get; private set; }
private string UniqueID { get; set; }
public Uri Url { get; private set; }
public string Name { get; private set; }
public IVideo[] Videos { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("login", ID);
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"];
if (((JArray)response).Count > 0) response = response[0];
else throw new MediaNotFoundException($"Twitch Channel (ID: {ID}) was not found");
}
UniqueID = (string)response["id"];
Name = (string)response["display_name"];
}
public async Task GetVideosAsync(CancellationToken cancellationToken = default) => await GetVideosAsync(0, cancellationToken);
public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string pagination = "";
List<Vod> videos = new List<Vod>();
bool getAll = numberOfVideos == 0;
int count;
JToken[] videosData;
List<Task> getStreamsTasks = new List<Task>();
do
{
count = numberOfVideos < 100 && !getAll ? numberOfVideos : 100;
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("user_id", UniqueID);
client.QueryString.Add("first", count.ToString());
client.QueryString.Add("after", pagination);
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos"));
}
pagination = (string)response["pagination"]["cursor"];
videosData = response["data"].ToArray();
foreach (JToken videoData in videosData)
{
Vod video = new Vod((string)videoData["id"]);
video.GetMetadataAsync(videoData);
getStreamsTasks.Add(video.GetStreamsAsync());
videos.Add(video);
numberOfVideos--;
}
}
while ((getAll || numberOfVideos > 0) && count == videosData.Length);
await Task.WhenAll(getStreamsTasks);
Videos = videos.ToArray();
}
#endregion
}
}

View File

@@ -1,158 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services.Sources.Twitch.Helpers;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch
{
[Serializable]
public class Clip : IVideo
{
#region CONSTRUCTORS
public Clip(string id)
{
Source = VideoSource.TwitchClip;
ID = id;
Url = new Uri($"https://clips.twitch.tv/{ID}");
}
#endregion
#region PROPERTIES
public VideoSource Source { get; private set; }
public string ID { get; private set; }
public Uri Url { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public DateTime Date { get; private set; }
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public BaseStream[] BaseStreams { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("id", ID);
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/clips")).GetValue("data");
if (((JArray)response).Count > 0) response = response[0];
else throw new MediaNotFoundException($"Twitch Clip (ID: {ID}) was not found");
}
Title = (string)response["title"];
Author = (string)response["broadcaster_name"];
Date = Convert.ToDateTime(response["created_at"]);
Duration = TimeSpan.FromSeconds((double)response["duration"]);
Views = (long)response["view_count"];
Thumbnail = new Uri((string)response["thumbnail_url"]);
}
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken[] response;
using (WebClient client = Client.GQL())
{
response = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["videoQualities"].ToArray();
}
List<BaseStream> streams = new List<BaseStream>();
foreach (JToken streamData in response)
{
BaseStream stream = new BaseStream()
{
Url = new Uri((string)streamData["sourceURL"]),
Height = int.Parse((string)streamData["quality"]),
FrameRate = (int)streamData["frameRate"],
};
streams.Add(stream);
}
BaseStreams = streams.ToArray();
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
JToken videoAccessToken = null;
using (WebClient client = Client.GQL())
{
videoAccessToken = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["playbackAccessToken"];
}
// Download
cancellationToken.ThrowIfCancellationRequested();
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.mp4");
using (WebClient client = new WebClient())
{
client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(a.ProgressPercentage)); };
client.QueryString.Add("sig", (string)videoAccessToken["signature"]);
client.QueryString.Add("token", HttpUtility.UrlEncode((string)videoAccessToken["value"]));
cancellationToken.ThrowIfCancellationRequested();
using (cancellationToken.Register(client.CancelAsync))
{
await client.DownloadFileTaskAsync(baseStream.Url, rawFile.Path);
}
}
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
// Processing
StorageFile outputFile = rawFile;
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trim.Start != null || trim.End != null)
{
cancellationToken.ThrowIfCancellationRequested();
outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor();
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken);
}
return outputFile;
}
#endregion
#region EVENTS
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -1,107 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Sources;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch.Helpers
{
public static class Authorization
{
#region CONSTANTS
public readonly static string ClientID = "yukkqkwp61wsv3u1pya17crpyaa98y";
public readonly static string GQLApiClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko";
public readonly static Uri RedirectUrl = new Uri("https://www.vd.com");
private readonly static string ResponseType = "token";
private readonly static string[] Scopes = new[]
{
"user:read:subscriptions",
};
public readonly static Uri AuthorizationUrl = new Uri($"https://id.twitch.tv/oauth2/authorize?client_id={ClientID}&redirect_uri={RedirectUrl.OriginalString}&response_type={ResponseType}&scope={string.Join(" ", Scopes)}");
#endregion
#region METHODS
public static async Task<string> ReadAccessTokenAsync()
{
try
{
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName);
StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}");
return await FileIO.ReadTextAsync(authDataFile);
}
catch (FileNotFoundException)
{
return null;
}
}
public static async Task SaveAccessTokenAsync(string accessToken)
{
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.CreateFolderAsync(AuthorizationData.FolderName, CreationCollisionOption.OpenIfExists);
StorageFile authDataFile = await authDataFolder.CreateFileAsync($"Twitch.{AuthorizationData.FilesExtension}", CreationCollisionOption.ReplaceExisting);
await FileIO.WriteTextAsync(authDataFile, accessToken);
}
public static async Task DeleteAccessTokenAsync()
{
try
{
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName);
StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}");
await authDataFile.DeleteAsync();
}
catch (FileNotFoundException) { }
}
public static async Task<TwitchAccessTokenValidationData> ValidateAccessTokenAsync(string accessToken)
{
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
client.Headers.Add("Authorization", $"Bearer {accessToken}");
try
{
JObject response = JObject.Parse(await client.DownloadStringTaskAsync("https://id.twitch.tv/oauth2/validate"));
string login = response["login"].ToString();
DateTime? expirationDate = DateTime.Now.AddSeconds(long.Parse(response["expires_in"].ToString()));
return new TwitchAccessTokenValidationData(accessToken, true, login, expirationDate);
}
catch (WebException ex)
{
if (ex.Response is null)
{
throw;
}
else
{
JObject exInfo = JObject.Parse(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd());
if ((int)exInfo["status"] == 401) return new TwitchAccessTokenValidationData(accessToken, false, null, null);
else throw;
}
}
}
public static async Task RevokeAccessTokenAsync(string accessToken)
{
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
await client.UploadStringTaskAsync(new Uri("https://id.twitch.tv/oauth2/revoke"), $"client_id={ClientID}&token={accessToken}");
}
#endregion
}
}

View File

@@ -1,32 +0,0 @@
using System.Net;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
namespace VDownload.Core.Services.Sources.Twitch.Helpers
{
internal static class Client
{
internal static async Task<WebClient> Helix()
{
string accessToken = await Authorization.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
var twitchAccessTokenValidation = await Authorization.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
WebClient client = new WebClient();
client.Headers.Add("Authorization", $"Bearer {accessToken}");
client.Headers.Add("Client-Id", Authorization.ClientID);
return client;
}
internal static WebClient GQL()
{
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Authorization.GQLApiClientID);
return client;
}
}
}

View File

@@ -1,285 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services.Sources.Twitch.Helpers;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Services.Sources.Twitch
{
[Serializable]
public class Vod : IVideo
{
#region CONSTRUCTORS
public Vod(string id)
{
Source = VideoSource.TwitchVod;
ID = id;
Url = new Uri($"https://www.twitch.tv/videos/{ID}");
}
#endregion
#region PROPERTIES
public VideoSource Source { get; private set; }
public string ID { get; private set; }
public Uri Url { get; private set; }
public string Title { get; private set; }
public string Author { get; private set; }
public DateTime Date { get; private set; }
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public BaseStream[] BaseStreams { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JToken response = null;
using (WebClient client = await Client.Helix())
{
client.QueryString.Add("id", ID);
cancellationToken.ThrowIfCancellationRequested();
try
{
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0];
}
catch (WebException ex)
{
if (ex.Response != null && new StreamReader(ex.Response.GetResponseStream()).ReadToEnd().Contains("Not Found")) throw new MediaNotFoundException($"Twitch VOD (ID: {ID}) was not found");
else if (ex.Response != null && new StreamReader(ex.Response.GetResponseStream()).ReadToEnd() == string.Empty && ex.Message.Contains("400")) throw new MediaNotFoundException($"Twitch VOD (ID: {ID}) was not found");
else throw;
}
}
GetMetadataAsync(response);
}
internal void GetMetadataAsync(JToken response)
{
Title = ((string)response["title"]).Replace("\n", "");
Author = (string)response["user_name"];
Date = Convert.ToDateTime(response["created_at"]);
Duration = ParseDuration((string)response["duration"]);
Views = (long)response["view_count"];
Thumbnail = (string)response["thumbnail_url"] == string.Empty ? null : new Uri(((string)response["thumbnail_url"]).Replace("%{width}", "1920").Replace("%{height}", "1080"));
}
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string[] response = null;
using (WebClient client = Client.GQL())
{
cancellationToken.ThrowIfCancellationRequested();
JToken videoAccessToken = JObject.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "{\"operationName\":\"PlaybackAccessToken_Template\",\"query\":\"query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}\",\"variables\":{\"isLive\":false,\"login\":\"\",\"isVod\":true,\"vodID\":\"" + ID + "\",\"playerType\":\"embed\"}}"))["data"]["videoPlaybackAccessToken"];
cancellationToken.ThrowIfCancellationRequested();
response = (await client.DownloadStringTaskAsync($"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken["value"]}&nauthsig={videoAccessToken["signature"]}&allow_source=true&player=twitchweb")).Split("\n");
}
List<BaseStream> streams = new List<BaseStream>();
Regex streamDataL2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""\S+,\S+"",RESOLUTION=\d+x(?<height>\d+),VIDEO=""\w+""(,FRAME-RATE=(?<frame_rate>\d+.\d+))?");
for (int i = 2; i < response.Length; i += 3)
{
Match line2 = streamDataL2Regex.Match(response[i + 1]);
BaseStream stream = new BaseStream()
{
Url = new Uri(response[i + 2]),
Height = int.Parse(line2.Groups["height"].Value),
FrameRate = line2.Groups["frame_rate"].Value != string.Empty ? (int)Math.Round(double.Parse(line2.Groups["frame_rate"].Value)) : 0,
};
streams.Add(stream);
}
BaseStreams = streams.ToArray();
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
DownloadingProgressChanged.Invoke(this, new EventArgs.ProgressChangedEventArgs(0));
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken);
TimeSpan duration = Duration;
// Passive trim
if ((bool)Config.GetValue("twitch_vod_passive_trim") && trim.Start != TimeSpan.Zero && trim.End != duration) (trim, duration) = PassiveVideoTrim(chunksList, trim, Duration);
// Download
cancellationToken.ThrowIfCancellationRequested();
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
double chunksDownloaded = 0;
Task<byte[]> downloadTask;
Task writeTask;
cancellationToken.ThrowIfCancellationRequested();
downloadTask = DownloadChunkAsync(chunksList[0].ChunkUrl);
await downloadTask;
for (int i = 1; i < chunksList.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
writeTask = WriteChunkToFileAsync(rawFile, downloadTask.Result);
downloadTask = DownloadChunkAsync(chunksList[i].ChunkUrl);
await Task.WhenAll(writeTask, downloadTask);
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(++chunksDownloaded * 100 / chunksList.Count));
}
cancellationToken.ThrowIfCancellationRequested();
await WriteChunkToFileAsync(rawFile, downloadTask.Result);
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
// Processing
cancellationToken.ThrowIfCancellationRequested();
StorageFile outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor();
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken);
// Return output file
return outputFile;
}
#endregion
#region PRIVATE METHODS
private static async Task<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> ExtractChunksFromM3U8Async(Uri streamUrl, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
string response = null;
using (WebClient client = Client.GQL())
{
response = await client.DownloadStringTaskAsync(streamUrl);
}
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>();
Regex chunkDataRegex = new Regex(@"#EXTINF:(?<duration>\d+.\d+),\n(?<filename>\S+.ts)");
string chunkLocationPath = streamUrl.AbsoluteUri.Replace(Path.GetFileName(streamUrl.AbsoluteUri), "");
foreach (Match chunk in chunkDataRegex.Matches(response))
{
Uri chunkUrl = new Uri($"{chunkLocationPath}{chunk.Groups["filename"].Value}");
TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value));
chunks.Add((chunkUrl, chunkDuration));
}
return chunks;
}
private static (TrimData Trim, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TrimData trim, TimeSpan duration)
{
TimeSpan newDuration = duration;
while (chunksList[0].ChunkDuration <= trim.Start)
{
trim.Start = trim.Start.Subtract(chunksList[0].ChunkDuration);
trim.End = trim.End.Subtract(chunksList[0].ChunkDuration);
newDuration = newDuration.Subtract(chunksList[0].ChunkDuration);
chunksList.RemoveAt(0);
}
while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trim.End))
{
newDuration = newDuration.Subtract(chunksList.Last().ChunkDuration);
chunksList.RemoveAt(chunksList.Count - 1);
}
return (trim, newDuration);
}
private static async Task<byte[]> DownloadChunkAsync(Uri chunkUrl, CancellationToken cancellationToken = default)
{
int retriesCount = 0;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (WebClient client = new WebClient())
{
return await client.DownloadDataTaskAsync(chunkUrl);
}
}
catch (WebException wex)
{
if ((bool)Config.GetValue("twitch_vod_downloading_chunk_retry_after_error") && retriesCount < (int)Config.GetValue("twitch_vod_downloading_chunk_max_retries"))
{
retriesCount++;
await Task.Delay((int)Config.GetValue("twitch_vod_downloading_chunk_retries_delay"));
}
else throw wex;
}
}
}
private static Task WriteChunkToFileAsync(StorageFile file, byte[] chunk)
{
return Task.Factory.StartNew(() =>
{
using (var stream = new FileStream(file.Path, FileMode.Append))
{
stream.Write(chunk, 0, chunk.Length);
stream.Close();
}
});
}
private static TimeSpan ParseDuration(string duration)
{
char[] separators = { 'h', 'm', 's' };
string[] durationParts = duration.Split(separators, StringSplitOptions.RemoveEmptyEntries).Reverse().ToArray();
TimeSpan timeSpan = new TimeSpan(
durationParts.Count() > 2 ? int.Parse(durationParts[2]) : 0,
durationParts.Count() > 1 ? int.Parse(durationParts[1]) : 0,
int.Parse(durationParts[0]));
return timeSpan;
}
#endregion
#region EVENTS
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Structs
{
[Serializable]
public struct BaseStream
{
#region PROPERTIES
public Uri Url { get; set; }
public int Height { get; set; }
public int FrameRate { get; set; }
#endregion
#region METHODS
public override string ToString() => $"{Height}p{(FrameRate > 0 ? FrameRate.ToString() : "N/A")}";
#endregion
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Structs
{
public struct TrimData
{
#region PROPERTIES
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
#endregion
}
}

View File

@@ -1,32 +0,0 @@
using System;
namespace VDownload.Core.Structs
{
public struct TwitchAccessTokenValidationData
{
#region CONSTRUCTORS
public TwitchAccessTokenValidationData(string accessToken, bool isValid, string login, DateTime? expirationDate)
{
AccessToken = accessToken;
IsValid = isValid;
Login = login;
ExpirationDate = expirationDate;
}
public static TwitchAccessTokenValidationData Null = new TwitchAccessTokenValidationData(string.Empty, false, string.Empty, null);
#endregion
#region PROPERTIES
public string AccessToken { get; private set; }
public bool IsValid { get; private set; }
public string Login { get; private set; }
public DateTime? ExpirationDate { get; private set; }
#endregion
}
}

View File

@@ -1,190 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{65C44D96-9C6C-47AD-A1F5-86BFAF2B15B8}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>VDownload.Core</RootNamespace>
<AssemblyName>VDownload.Core</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22000.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.18362.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<OutputPath>bin\ARM\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<PlatformTarget>ARM64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<PlatformTarget>ARM64</PlatformTarget>
<OutputPath>bin\ARM64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="Enums\AudioFileExtension.cs" />
<Compile Include="Enums\MediaFileExtension.cs" />
<Compile Include="Enums\MediaType.cs" />
<Compile Include="Enums\PlaylistSource.cs" />
<Compile Include="Enums\DownloadTasksAddingRequestSource.cs" />
<Compile Include="Enums\VideoFileExtension.cs" />
<Compile Include="Enums\VideoSource.cs" />
<Compile Include="Enums\DownloadTaskStatus.cs" />
<Compile Include="EventArgs\DownloadTasksAddingRequestedEventArgs.cs" />
<Compile Include="EventArgs\DownloadTaskStatusChangedEventArgs.cs" />
<Compile Include="EventArgs\ProgressChangedEventArgs.cs" />
<Compile Include="EventArgs\PlaylistSearchSuccessedEventArgs.cs" />
<Compile Include="EventArgs\SubscriptionLoadSuccessedEventArgs.cs" />
<Compile Include="EventArgs\VideoSearchSuccessedEventArgs.cs" />
<Compile Include="Exceptions\MediaNotFoundException.cs" />
<Compile Include="Exceptions\SubscriptionExistsException.cs" />
<Compile Include="Exceptions\TwitchAccessTokenNotFoundException.cs" />
<Compile Include="Exceptions\TwitchAccessTokenNotValidException.cs" />
<Compile Include="Extensions\TimeSpanExtension.cs" />
<Compile Include="Interfaces\IPlaylist.cs" />
<Compile Include="Interfaces\IVideo.cs" />
<Compile Include="Services\DownloadTasksCollectionManagement.cs" />
<Compile Include="Services\OutputFile.cs" />
<Compile Include="Sources\AuthorizationData.cs" />
<Compile Include="Sources\Twitch\Helpers\Client.cs" />
<Compile Include="Services\Subscription.cs" />
<Compile Include="Services\SubscriptionsCollectionManagement.cs" />
<Compile Include="Structs\BaseStream.cs" />
<Compile Include="Services\DownloadTask.cs" />
<Compile Include="Services\Config.cs" />
<Compile Include="Services\MediaProcessor.cs" />
<Compile Include="Sources\Source.cs" />
<Compile Include="Sources\Twitch\Helpers\Authorization.cs" />
<Compile Include="Sources\Twitch\Channel.cs" />
<Compile Include="Sources\Twitch\Clip.cs" />
<Compile Include="Sources\Twitch\Vod.cs" />
<Compile Include="Structs\TrimData.cs" />
<Compile Include="Structs\TwitchAccessTokenValidationData.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.13</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.Connectivity">
<Version>7.1.2</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>13.0.1</Version>
</PackageReference>
<PackageReference Include="System.ValueTuple">
<Version>4.5.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>