From 0c7e5ef09959c292914ce46c18e3528c58bc7142 Mon Sep 17 00:00:00 2001 From: Mateusz Skoczek Date: Wed, 16 Feb 2022 03:15:41 +0100 Subject: [PATCH] 1.0-dev4 (New GUI and Twitch API change to Helix) --- VDownload/App.xaml.cs | 21 +- VDownload/Assets/Icons/Error.png | Bin 0 -> 4465 bytes VDownload/Assets/Icons/MainPage/Sources.png | Bin 0 -> 22293 bytes .../Dark/Author.png | Bin .../Dark/Cancelled.png | Bin .../Dark/Date.png | Bin .../Dark/Done.png | Bin .../Dark/Downloading.png | Bin .../Dark/Duration.png | Bin .../Dark/Error.png | Bin .../Path.png => VideoMetadata/Dark/File.png} | Bin .../Dark/Finalizing.png | Bin .../Dark/Idle.png | Bin .../Dark/MediaType.png | Bin .../Dark/Quality.png | Bin .../Dark/Transcoding.png | Bin .../Dark/Trim.png | Bin .../Dark/Views.png | Bin .../Dark/Waiting.png | Bin .../Light/Author.png | Bin .../Light/Cancelled.png | Bin .../Light/Date.png | Bin .../Light/Done.png | Bin .../Light/Downloading.png | Bin .../Light/Duration.png | Bin .../Light/Error.png | Bin .../Path.png => VideoMetadata/Light/File.png} | Bin .../Light/Finalizing.png | Bin .../Light/Idle.png | Bin .../Light/MediaType.png | Bin .../Light/Quality.png | Bin .../Light/Transcoding.png | Bin .../Light/Trim.png | Bin .../Light/Views.png | Bin .../Light/Waiting.png | Bin VDownload/Core/Enums/AudioFileExtension.cs | 12 + VDownload/Core/Enums/LogMessageType.cs | 9 + VDownload/Core/Enums/MediaFileExtension.cs | 15 + VDownload/Core/Enums/MediaType.cs | 9 + VDownload/Core/Enums/StreamType.cs | 9 + VDownload/Core/Enums/VideoFileExtension.cs | 9 + .../TwitchAccessTokenNotFoundException.cs | 11 + .../TwitchAccessTokenNotValidException.cs | 11 + VDownload/Core/Globals/Assets.cs | 13 + VDownload/Core/Interfaces/IAStream.cs | 22 + VDownload/Core/Interfaces/IPlaylistService.cs | 28 + VDownload/Core/Interfaces/IVStream.cs | 20 + VDownload/Core/Interfaces/IVideoService.cs | 65 ++ VDownload/Core/Models/Stream.cs | 36 + VDownload/Core/Services/Config.cs | 72 ++ VDownload/Core/Services/Log.cs | 35 + VDownload/Core/Services/MediaProcessor.cs | 177 ++++ .../Core/Services/Sources/Twitch/Auth.cs | 122 +++ .../Core/Services/Sources/Twitch/Channel.cs | 126 +++ .../Core/Services/Sources/Twitch/Clip.cs | 189 +++++ VDownload/Core/Services/Sources/Twitch/Vod.cs | 329 ++++++++ .../GUI/Controls/MainPageLayoutControl.xaml | 21 + .../Controls/MainPageLayoutControl.xaml.cs | 39 + VDownload/GUI/Controls/SettingControl.xaml | 48 ++ VDownload/GUI/Controls/SettingControl.xaml.cs | 65 ++ .../Views/Home/HomeMain.xaml} | 12 +- VDownload/GUI/Views/Home/HomeMain.xaml.cs | 46 ++ VDownload/GUI/Views/MainPage.xaml | 56 ++ VDownload/GUI/Views/MainPage.xaml.cs | 60 ++ .../Views/Settings/SettingsMain.xaml} | 13 +- .../Views/Settings/SettingsMain.xaml.cs} | 6 +- VDownload/GUI/Views/Sources/SourcesMain.xaml | 31 + .../GUI/Views/Sources/SourcesMain.xaml.cs | 191 +++++ .../Subscriptions/SubscriptionsMain.xaml | 14 + .../Subscriptions/SubscriptionsMain.xaml.cs} | 6 +- VDownload/Objects/Enums/PlaylistSource.cs | 9 - VDownload/Objects/Enums/VideoSource.cs | 10 - VDownload/Objects/Enums/VideoStatus.cs | 10 - VDownload/Services/Config.cs | 102 --- VDownload/Services/Media.cs | 119 --- VDownload/Services/Source.cs | 69 -- VDownload/Services/Videos.cs | 64 -- VDownload/Sources/PObject.cs | 777 ------------------ VDownload/Sources/Twitch/Channel.cs | 83 -- VDownload/Sources/Twitch/Clip.cs | 196 ----- VDownload/Sources/Twitch/Vod.cs | 266 ------ VDownload/Sources/VObject.cs | 686 ---------------- VDownload/Strings/en-US/Resources.resw | 181 +--- VDownload/Strings/en-US/ResourcesOld.resw | 348 ++++++++ VDownload/VDownload.csproj | 186 ++--- .../Views/AddPlaylist/AddPlaylistBase.xaml | 39 - .../Views/AddPlaylist/AddPlaylistBase.xaml.cs | 89 -- .../AddPlaylist/AddPlaylistLoading.xaml.cs | 13 - .../Views/AddPlaylist/AddPlaylistMain.xaml | 41 - .../Views/AddPlaylist/AddPlaylistMain.xaml.cs | 110 --- .../AddPlaylist/AddPlaylistNotFound.xaml | 22 - .../AddPlaylist/AddPlaylistNotFound.xaml.cs | 13 - .../Views/AddPlaylist/AddPlaylistStart.xaml | 22 - .../AddPlaylist/AddPlaylistStart.xaml.cs | 13 - VDownload/Views/AddVideo/AddVideoBase.xaml | 39 - VDownload/Views/AddVideo/AddVideoBase.xaml.cs | 81 -- .../Views/AddVideo/AddVideoLoading.xaml.cs | 30 - VDownload/Views/AddVideo/AddVideoMain.xaml | 174 ---- VDownload/Views/AddVideo/AddVideoMain.xaml.cs | 272 ------ .../Views/AddVideo/AddVideoNotFound.xaml | 22 - VDownload/Views/AddVideo/AddVideoStart.xaml | 22 - VDownload/Views/MainPage.xaml | 54 -- VDownload/Views/MainPage.xaml.cs | 99 --- 103 files changed, 2384 insertions(+), 3825 deletions(-) create mode 100644 VDownload/Assets/Icons/Error.png create mode 100644 VDownload/Assets/Icons/MainPage/Sources.png rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Author.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Cancelled.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Date.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Done.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Downloading.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Duration.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Error.png (100%) rename VDownload/Assets/Icons/{Universal/Dark/Path.png => VideoMetadata/Dark/File.png} (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Finalizing.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Idle.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/MediaType.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Quality.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Transcoding.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Trim.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Views.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Dark/Waiting.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Author.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Cancelled.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Date.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Done.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Downloading.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Duration.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Error.png (100%) rename VDownload/Assets/Icons/{Universal/Light/Path.png => VideoMetadata/Light/File.png} (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Finalizing.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Idle.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/MediaType.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Quality.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Transcoding.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Trim.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Views.png (100%) rename VDownload/Assets/Icons/{Universal => VideoMetadata}/Light/Waiting.png (100%) create mode 100644 VDownload/Core/Enums/AudioFileExtension.cs create mode 100644 VDownload/Core/Enums/LogMessageType.cs create mode 100644 VDownload/Core/Enums/MediaFileExtension.cs create mode 100644 VDownload/Core/Enums/MediaType.cs create mode 100644 VDownload/Core/Enums/StreamType.cs create mode 100644 VDownload/Core/Enums/VideoFileExtension.cs create mode 100644 VDownload/Core/Exceptions/TwitchAccessTokenNotFoundException.cs create mode 100644 VDownload/Core/Exceptions/TwitchAccessTokenNotValidException.cs create mode 100644 VDownload/Core/Globals/Assets.cs create mode 100644 VDownload/Core/Interfaces/IAStream.cs create mode 100644 VDownload/Core/Interfaces/IPlaylistService.cs create mode 100644 VDownload/Core/Interfaces/IVStream.cs create mode 100644 VDownload/Core/Interfaces/IVideoService.cs create mode 100644 VDownload/Core/Models/Stream.cs create mode 100644 VDownload/Core/Services/Config.cs create mode 100644 VDownload/Core/Services/Log.cs create mode 100644 VDownload/Core/Services/MediaProcessor.cs create mode 100644 VDownload/Core/Services/Sources/Twitch/Auth.cs create mode 100644 VDownload/Core/Services/Sources/Twitch/Channel.cs create mode 100644 VDownload/Core/Services/Sources/Twitch/Clip.cs create mode 100644 VDownload/Core/Services/Sources/Twitch/Vod.cs create mode 100644 VDownload/GUI/Controls/MainPageLayoutControl.xaml create mode 100644 VDownload/GUI/Controls/MainPageLayoutControl.xaml.cs create mode 100644 VDownload/GUI/Controls/SettingControl.xaml create mode 100644 VDownload/GUI/Controls/SettingControl.xaml.cs rename VDownload/{Views/AddVideo/AddVideoLoading.xaml => GUI/Views/Home/HomeMain.xaml} (57%) create mode 100644 VDownload/GUI/Views/Home/HomeMain.xaml.cs create mode 100644 VDownload/GUI/Views/MainPage.xaml create mode 100644 VDownload/GUI/Views/MainPage.xaml.cs rename VDownload/{Views/AddPlaylist/AddPlaylistLoading.xaml => GUI/Views/Settings/SettingsMain.xaml} (55%) rename VDownload/{Views/AddVideo/AddVideoStart.xaml.cs => GUI/Views/Settings/SettingsMain.xaml.cs} (85%) create mode 100644 VDownload/GUI/Views/Sources/SourcesMain.xaml create mode 100644 VDownload/GUI/Views/Sources/SourcesMain.xaml.cs create mode 100644 VDownload/GUI/Views/Subscriptions/SubscriptionsMain.xaml rename VDownload/{Views/AddVideo/AddVideoNotFound.xaml.cs => GUI/Views/Subscriptions/SubscriptionsMain.xaml.cs} (84%) delete mode 100644 VDownload/Objects/Enums/PlaylistSource.cs delete mode 100644 VDownload/Objects/Enums/VideoSource.cs delete mode 100644 VDownload/Objects/Enums/VideoStatus.cs delete mode 100644 VDownload/Services/Config.cs delete mode 100644 VDownload/Services/Media.cs delete mode 100644 VDownload/Services/Source.cs delete mode 100644 VDownload/Services/Videos.cs delete mode 100644 VDownload/Sources/PObject.cs delete mode 100644 VDownload/Sources/Twitch/Channel.cs delete mode 100644 VDownload/Sources/Twitch/Clip.cs delete mode 100644 VDownload/Sources/Twitch/Vod.cs delete mode 100644 VDownload/Sources/VObject.cs create mode 100644 VDownload/Strings/en-US/ResourcesOld.resw delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistBase.xaml delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistBase.xaml.cs delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistLoading.xaml.cs delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistMain.xaml delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistMain.xaml.cs delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistNotFound.xaml delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistNotFound.xaml.cs delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistStart.xaml delete mode 100644 VDownload/Views/AddPlaylist/AddPlaylistStart.xaml.cs delete mode 100644 VDownload/Views/AddVideo/AddVideoBase.xaml delete mode 100644 VDownload/Views/AddVideo/AddVideoBase.xaml.cs delete mode 100644 VDownload/Views/AddVideo/AddVideoLoading.xaml.cs delete mode 100644 VDownload/Views/AddVideo/AddVideoMain.xaml delete mode 100644 VDownload/Views/AddVideo/AddVideoMain.xaml.cs delete mode 100644 VDownload/Views/AddVideo/AddVideoNotFound.xaml delete mode 100644 VDownload/Views/AddVideo/AddVideoStart.xaml delete mode 100644 VDownload/Views/MainPage.xaml delete mode 100644 VDownload/Views/MainPage.xaml.cs diff --git a/VDownload/App.xaml.cs b/VDownload/App.xaml.cs index 8e53bc2..19bb81a 100644 --- a/VDownload/App.xaml.cs +++ b/VDownload/App.xaml.cs @@ -1,5 +1,5 @@ // Internal -using VDownload.Services; +using VDownload.Core.Services; // System using System; @@ -11,6 +11,7 @@ using Windows.Storage; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; +using System.Diagnostics; namespace VDownload { @@ -24,16 +25,30 @@ namespace VDownload protected override async void OnLaunched(LaunchActivatedEventArgs e) { + Log.AddHeader("APP LAUNCHED"); + Log.Break(); + // Rebuild configuration file + Log.AddHeader("REBUILDING CONFIGURATION FILE"); + Config.Rebuild(); + Log.Add("Configuration file rebuilded successfully"); + Log.Break(); + + // Delete temp on start - if (Config.GetValue("delete_temp_on_start") == "1") + // TODO + Debug.WriteLine(Config.GetValue("delete_temp_on_start")); + if ((bool)Config.GetValue("delete_temp_on_start")) { + Log.AddHeader("DELETING TEMPORARY FILES"); IReadOnlyList tempItems = await ApplicationData.Current.TemporaryFolder.GetItemsAsync(); List tasks = new List(); foreach (IStorageItem item in tempItems) tasks.Add(item.DeleteAsync().AsTask()); await Task.WhenAll(tasks); + Log.Add("Temporary files deleted successfully"); + Log.Break(); } // Do not repeat app initialization when the Window already has content, @@ -55,7 +70,7 @@ namespace VDownload // When the navigation stack isn't restored navigate to the first page, // configuring the new page by passing required information as a navigation // parameter - rootFrame.Navigate(typeof(MainPage), e.Arguments); + rootFrame.Navigate(typeof(GUI.Views.MainPage), e.Arguments); } // Ensure the current window is active diff --git a/VDownload/Assets/Icons/Error.png b/VDownload/Assets/Icons/Error.png new file mode 100644 index 0000000000000000000000000000000000000000..6031a84ca3f204cdeb64cbc97346d09aa4c8aad5 GIT binary patch literal 4465 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 literal 0 HcmV?d00001 diff --git a/VDownload/Assets/Icons/MainPage/Sources.png b/VDownload/Assets/Icons/MainPage/Sources.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0c35e7f44988fccfe58d9b86aff1bb2ed48ea7 GIT binary patch literal 22293 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 DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default); + + #endregion + + + + #region EVENT HANDLERS + + event EventHandler DownloadingStarted; + + event EventHandler DownloadingProgressChanged; + + event EventHandler DownloadingCompleted; + + event EventHandler ProcessingStarted; + + event EventHandler ProcessingProgressChanged; + + event EventHandler ProcessingCompleted; + + #endregion + } +} diff --git a/VDownload/Core/Models/Stream.cs b/VDownload/Core/Models/Stream.cs new file mode 100644 index 0000000..a4dca63 --- /dev/null +++ b/VDownload/Core/Models/Stream.cs @@ -0,0 +1,36 @@ +using System; +using VDownload.Core.Enums; +using VDownload.Core.Interfaces; + +namespace VDownload.Core.Models +{ + public class Stream : IVStream, IAStream + { + #region PARAMETERS + + public Uri Url { get; private set; } + public bool IsChunked { get; private set; } + public StreamType StreamType { get; private set; } + public int Width { get; set; } + public int Height { get; set; } + public int FrameRate { get; set; } + public string VideoCodec { get; set; } + public int AudioBitrate { get; set; } + public string AudioCodec { get; set; } + + #endregion + + + + #region CONSTRUCTORS + + public Stream(Uri url, bool isChunked, StreamType streamType) + { + Url = url; + IsChunked = isChunked; + StreamType = streamType; + } + + #endregion + } +} diff --git a/VDownload/Core/Services/Config.cs b/VDownload/Core/Services/Config.cs new file mode 100644 index 0000000..61d8341 --- /dev/null +++ b/VDownload/Core/Services/Config.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.Media.Editing; +using Windows.Storage; + +namespace VDownload.Core.Services +{ + internal class Config + { + #region CONSTANTS + + // SETTINGS CONTAINER + private static readonly ApplicationDataContainer SettingsContainer = ApplicationData.Current.LocalSettings; + + // DEFAULT SETTINGS + private static readonly Dictionary DefaultSettings = new Dictionary() + { + { "delete_temp_on_start", true }, + { "twitch_vod_passive_trim", true }, + { "twitch_vod_downloading_chunk_retry_after_error", true }, + { "twitch_vod_downloading_chunk_max_retries", 10 }, + { "twitch_vod_downloading_chunk_retries_delay", 5000 }, + { "media_transcoding_use_hardware_acceleration", true }, + { "media_transcoding_use_mrfcrf444_algorithm", true }, + { "media_editing_algorithm", MediaTrimmingPreference.Fast } + }; + + #endregion + + + + #region METHODS + + // GET VALUE + public static object GetValue(string key) + { + return SettingsContainer.Values[key]; + } + + // SET VALUE + public static void SetValue(string key, object value) + { + SettingsContainer.Values[key] = value; + } + + // SET DEFAULT + public static void SetDefault() + { + foreach (KeyValuePair s in DefaultSettings) + { + SettingsContainer.Values[s.Key] = s.Value; + } + } + + // REBUILD + public static void Rebuild() + { + foreach (KeyValuePair s in DefaultSettings) + { + if (!SettingsContainer.Values.ContainsKey(s.Key)) + { + SettingsContainer.Values[s.Key] = s.Value; + } + } + } + + #endregion + } +} diff --git a/VDownload/Core/Services/Log.cs b/VDownload/Core/Services/Log.cs new file mode 100644 index 0000000..e1b8a1a --- /dev/null +++ b/VDownload/Core/Services/Log.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using VDownload.Core.Enums; + +namespace VDownload.Core.Services +{ + public class Log + { + private static List<(DateTime? Time, string Message, LogMessageType MessageType)> MessageList = new List<(DateTime? Time, string Message, LogMessageType MessageType)>(); + + public static void AddHeader(string message) + { + MessageList.Add((DateTime.Now, message, LogMessageType.Header)); + Debug.WriteLine(message); + } + + public static void Add(string message) + { + MessageList.Add((DateTime.Now, message, LogMessageType.Normal)); + Debug.WriteLine(message); + } + + public static void Break() + { + MessageList.Add((null, string.Empty, LogMessageType.Break)); + } + + + public static void Clear() + { + MessageList.Clear(); + } + } +} diff --git a/VDownload/Core/Services/MediaProcessor.cs b/VDownload/Core/Services/MediaProcessor.cs new file mode 100644 index 0000000..0fadcc4 --- /dev/null +++ b/VDownload/Core/Services/MediaProcessor.cs @@ -0,0 +1,177 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using VDownload.Core.Enums; +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 PARAMETERS + + public StorageFile OutputFile { get; private set; } + public TimeSpan TrimStart { get; private set; } + public TimeSpan TrimEnd { get; private set; } + + #endregion + + + + #region CONSTRUCTOR + + public MediaProcessor(StorageFile outputFile, TimeSpan trimStart, TimeSpan trimEnd) + { + OutputFile = outputFile; + TrimStart = trimStart; + TrimEnd = trimEnd; + } + + #endregion + + + + #region STANDARD METHODS + + public async Task Run(StorageFile audioVideoInputFile, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) + { + // Invoke ProcessingStarted event + ProcessingStarted?.Invoke(this, EventArgs.Empty); + + // Init transcoder + MediaTranscoder mediaTranscoder = new MediaTranscoder + { + HardwareAccelerationEnabled = (bool)Config.GetValue("media_processor_use_hardware_acceleration"), + VideoProcessingAlgorithm = (bool)Config.GetValue("media_processor_use_mrfcrf444_algorithm") ? MediaVideoProcessingAlgorithm.MrfCrf444 : MediaVideoProcessingAlgorithm.Default, + TrimStartTime = TrimStart, + TrimStopTime = TrimEnd, + }; + + // Start transcoding operation + using (IRandomAccessStream outputFileOpened = await OutputFile.OpenAsync(FileAccessMode.ReadWrite)) + { + PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await audioVideoInputFile.OpenAsync(FileAccessMode.Read), outputFileOpened, await GetMediaEncodingProfile(audioVideoInputFile, extension, mediaType)); + IAsyncActionWithProgress transcodingTask = transcodingPreparated.TranscodeAsync(); + try + { + await transcodingTask.AsTask(cancellationToken, new Progress((percent) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(percent), null)); })); + await outputFileOpened.FlushAsync(); + } + catch (TaskCanceledException) { } + transcodingTask.Close(); + } + + // Invoke ProcessingCompleted event + ProcessingCompleted?.Invoke(this, EventArgs.Empty); + } + public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default) + { + // Invoke ProcessingStarted event + ProcessingStarted?.Invoke(this, EventArgs.Empty); + + // Init editor + MediaComposition mediaEditor = new MediaComposition(); + + // Add media files + Task getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask(); + Task getAudioFileTask = BackgroundAudioTrack.CreateFromFileAsync(audioFile).AsTask(); + await Task.WhenAll(getVideoFileTask, getAudioFileTask); + + MediaClip videoElement = getVideoFileTask.Result; + videoElement.TrimTimeFromStart = TrimStart; + videoElement.TrimTimeFromEnd = TrimEnd; + BackgroundAudioTrack audioElement = getAudioFileTask.Result; + audioElement.TrimTimeFromStart = TrimStart; + audioElement.TrimTimeFromEnd = TrimEnd; + + mediaEditor.Clips.Add(getVideoFileTask.Result); + mediaEditor.BackgroundAudioTracks.Add(getAudioFileTask.Result); + + // Start rendering operation + var renderOperation = mediaEditor.RenderToFileAsync(OutputFile, (MediaTrimmingPreference)Config.GetValue("media_editing_algorithm"), await GetMediaEncodingProfile(videoFile, audioFile, (MediaFileExtension)extension, MediaType.AudioVideo)); + renderOperation.Progress += (info, progress) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(progress), null)); }; + await renderOperation.AsTask(cancellationToken); + + // Invoke ProcessingCompleted event + ProcessingCompleted?.Invoke(this, EventArgs.Empty); + } + public async Task Run(StorageFile audioFile, AudioFileExtension extension, CancellationToken cancellationToken = default) { await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, cancellationToken); } + public async Task Run(StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default) { await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, cancellationToken); } + + #endregion + + + + #region LOCAL METHODS + + // GET ENCODING PROFILE + public static async Task GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType) + { + // Create profile object + MediaEncodingProfile profile; + + // Set extension + 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; + } + + // Set video parameters + 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; + } + + // Set audio parameters + if (mediaType != MediaType.OnlyVideo) + { + var audioData = await audioFile.Properties.GetMusicPropertiesAsync(); + profile.Audio.Bitrate = audioData.Bitrate; + if (mediaType == MediaType.AudioVideo) profile.Video.Bitrate -= audioData.Bitrate; + } + + // Delete audio tracks + if (mediaType == MediaType.OnlyVideo) + { + var audioTracks = profile.GetAudioTracks(); + audioTracks.Clear(); + profile.SetAudioTracks(audioTracks.AsEnumerable()); + } + + // Return profile + return profile; + } + public static async Task GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType) { return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType); } + + #endregion + + + + #region EVENT HANDLERS + + public event EventHandler ProcessingStarted; + public event EventHandler ProcessingProgressChanged; + public event EventHandler ProcessingCompleted; + + #endregion + } +} diff --git a/VDownload/Core/Services/Sources/Twitch/Auth.cs b/VDownload/Core/Services/Sources/Twitch/Auth.cs new file mode 100644 index 0000000..59671de --- /dev/null +++ b/VDownload/Core/Services/Sources/Twitch/Auth.cs @@ -0,0 +1,122 @@ +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; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace VDownload.Core.Services.Sources.Twitch +{ + public class Auth + { + + #region CONSTANTS + + // CLIENT ID + public readonly static string ClientID = "yukkqkwp61wsv3u1pya17crpyaa98y"; + + // GQL API CLIENT ID + public readonly static string GQLApiClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko"; + + // REDIRECT URL + public readonly static Uri RedirectUrl = new Uri("https://www.vd.com"); + + // AUTHORIZATION URL + 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 + + // READ ACCESS TOKEN + public static async Task ReadAccessTokenAsync() + { + try + { + // Get file + StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.GetFolderAsync("AuthData"); + StorageFile authDataFile = await authDataFolder.GetFileAsync("Twitch.auth"); + + // Return data + return await FileIO.ReadTextAsync(authDataFile); + } + catch (FileNotFoundException) + { + return null; + } + } + + // SAVE ACCESS TOKEN + public static async Task SaveAccessTokenAsync(string accessToken) + { + // Get file + StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("AuthData", CreationCollisionOption.OpenIfExists); + StorageFile authDataFile = await authDataFolder.CreateFileAsync("Twitch.auth", CreationCollisionOption.ReplaceExisting); + + // Save data + FileIO.WriteTextAsync(authDataFile, accessToken); + } + + // DELETE ACCESS TOKEN + public static async Task DeleteAccessTokenAsync() + { + try + { + // Get file + StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.GetFolderAsync("AuthData"); + StorageFile authDataFile = await authDataFolder.GetFileAsync("Twitch.auth"); + + // Delete file + await authDataFile.DeleteAsync(); + } + catch (FileNotFoundException) { } + } + + // VALIDATE ACCESS TOKEN + public static async Task<(bool IsValid, string Login, DateTime? ExpirationDate)> ValidateAccessTokenAsync(string accessToken) + { + // Create client + WebClient client = new WebClient { Encoding = Encoding.UTF8 }; + client.Headers.Add("Authorization", $"Bearer {accessToken}"); + + try + { + // Check access token + 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 (true, login, expirationDate); + } + catch (WebException) + { + return (false, null, null); + } + } + + // REVOKE ACCESS TOKEN + public static async Task RevokeAccessTokenAsync(string accessToken) + { + // Create client + WebClient client = new WebClient { Encoding = Encoding.UTF8 }; + + // Revoke access token + await client.UploadStringTaskAsync(new Uri("https://id.twitch.tv/oauth2/revoke"), $"client_id={ClientID}&token={accessToken}"); + } + + #endregion + } +} diff --git a/VDownload/Core/Services/Sources/Twitch/Channel.cs b/VDownload/Core/Services/Sources/Twitch/Channel.cs new file mode 100644 index 0000000..3e21bb2 --- /dev/null +++ b/VDownload/Core/Services/Sources/Twitch/Channel.cs @@ -0,0 +1,126 @@ +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.Tasks; +using VDownload.Core.Exceptions; +using VDownload.Core.Interfaces; + +namespace VDownload.Core.Services.Sources.Twitch +{ + public class Channel : IPlaylistService + { + #region CONSTRUCTORS + + public Channel(string id) + { + ID = id; + } + + #endregion + + + + #region PARAMETERS + + public string ID { get; private set; } + public string Name { get; private set; } + public Vod[] Videos { get; private set; } + + #endregion + + + + #region STANDARD METHODS + + // GET CHANNEL METADATA + public async Task GetMetadataAsync() + { + // Get access token + string accessToken = await Auth.ReadAccessTokenAsync(); + if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); + + // Check access token + var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken); + if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException(); + + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Authorization", $"Bearer {accessToken}"); + client.Headers.Add("Client-Id", Auth.ClientID); + + // Get response + client.QueryString.Add("login", ID); + JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"][0]; + + // Set parameters + if (!ID.All(char.IsDigit)) ID = (string)response["id"]; + Name = (string)response["display_name"]; + } + + // GET CHANNEL VIDEOS + public async Task GetVideosAsync(int numberOfVideos) + { + // Get access token + string accessToken = await Auth.ReadAccessTokenAsync(); + if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); + + // Set pagination + string pagination = ""; + + // Set array of videos + List videos = new List(); + + // Get videos + int count; + JToken[] videosData; + List getStreamsTasks = new List(); + do + { + // Check access token + var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken); + if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException(); + + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Authorization", $"Bearer {accessToken}"); + client.Headers.Add("Client-Id", Auth.ClientID); + + // Set number of videos to get in this iteration + count = numberOfVideos < 100 ? numberOfVideos : 100; + + // Get response + client.QueryString.Add("user_id", ID); + client.QueryString.Add("first", count.ToString()); + client.QueryString.Add("after", pagination); + + JToken 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 (numberOfVideos > 0 && count == videosData.Length); + + // Wait for all getStreams tasks + await Task.WhenAll(getStreamsTasks); + + // Set Videos parameter + Videos = videos.ToArray(); + } + + #endregion + } +} diff --git a/VDownload/Core/Services/Sources/Twitch/Clip.cs b/VDownload/Core/Services/Sources/Twitch/Clip.cs new file mode 100644 index 0000000..739da9c --- /dev/null +++ b/VDownload/Core/Services/Sources/Twitch/Clip.cs @@ -0,0 +1,189 @@ +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.Models; +using Windows.Storage; + +namespace VDownload.Core.Services.Sources.Twitch +{ + public class Clip : IVideoService + { + #region CONSTANTS + + + + #endregion + + + + #region CONSTRUCTORS + + public Clip(string id) + { + ID = id; + } + + #endregion + + + + #region PARAMETERS + + public string ID { 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 Stream[] Streams { get; private set; } + + #endregion + + + + #region STANDARD METHODS + + // GET CLIP METADATA + public async Task GetMetadataAsync() + { + // Get access token + string accessToken = await Auth.ReadAccessTokenAsync(); + if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); + + // Check access token + var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken); + if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException(); + + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Authorization", $"Bearer {accessToken}"); + client.Headers.Add("Client-Id", Auth.ClientID); + + // Get response + client.QueryString.Add("id", ID); + JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/clips")).GetValue("data")[0]; + + // Set parameters + 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() + { + // Create client + WebClient client = new WebClient { Encoding = Encoding.UTF8 }; + client.Headers.Add("Client-ID", Auth.GQLApiClientID); + + // Get video streams + JToken[] 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(); + + // Init streams list + List streams = new List(); + + // Parse response + foreach (JToken streamData in response) + { + // Get info + Uri url = new Uri((string)streamData["sourceURL"]); + int height = int.Parse((string)streamData["quality"]); + int frameRate = (int)streamData["frameRate"]; + + // Create stream + Stream stream = new Stream(url, false, StreamType.AudioVideo) + { + Height = height, + FrameRate = frameRate + }; + + // Add stream + streams.Add(stream); + } + + // Set Streams parameter + Streams = streams.ToArray(); + } + + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) + { + // Set cancellation token + cancellationToken.ThrowIfCancellationRequested(); + + // Invoke DownloadingStarted event + DownloadingStarted?.Invoke(this, EventArgs.Empty); + + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Client-Id", Auth.GQLApiClientID); + + // Get video GQL access token + JToken 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 + StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.mp4"); + using (client = new WebClient()) + { + client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new ProgressChangedEventArgs(a.ProgressPercentage, null)); }; + client.QueryString.Add("sig", (string)videoAccessToken["signature"]); + client.QueryString.Add("token", HttpUtility.UrlEncode((string)videoAccessToken["value"])); + using (cancellationToken.Register(client.CancelAsync)) + { + await client.DownloadFileTaskAsync(audioVideoStream.Url, rawFile.Path); + } + } + DownloadingCompleted?.Invoke(this, EventArgs.Empty); + + // Processing + StorageFile outputFile = rawFile; + if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart > new TimeSpan(0) || trimEnd < Duration) + { + outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}"); + MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd); + mediaProcessor.ProcessingStarted += ProcessingStarted; + mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged; + mediaProcessor.ProcessingCompleted += ProcessingCompleted; + await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken); + } + + // Return output file + return outputFile; + } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioVideoStream, extension, mediaType, new TimeSpan(0), Duration, cancellationToken); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, extension, new TimeSpan(0), Duration, cancellationToken); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); } + + #endregion + + + + #region EVENT HANDLERS + + public event EventHandler DownloadingStarted; + public event EventHandler DownloadingProgressChanged; + public event EventHandler DownloadingCompleted; + public event EventHandler ProcessingStarted; + public event EventHandler ProcessingProgressChanged; + public event EventHandler ProcessingCompleted; + + #endregion + } +} diff --git a/VDownload/Core/Services/Sources/Twitch/Vod.cs b/VDownload/Core/Services/Sources/Twitch/Vod.cs new file mode 100644 index 0000000..a734f08 --- /dev/null +++ b/VDownload/Core/Services/Sources/Twitch/Vod.cs @@ -0,0 +1,329 @@ +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.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using VDownload.Core.Enums; +using VDownload.Core.Exceptions; +using VDownload.Core.Interfaces; +using VDownload.Core.Models; +using Windows.Storage; + +namespace VDownload.Core.Services.Sources.Twitch +{ + public class Vod : IVideoService + { + #region CONSTANTS + + // METADATA TIME FORMATS + private static readonly string[] TimeFormats = new[] + { + @"h\hm\ms\s", + @"m\ms\s", + @"s\s", + }; + + // STREAMS RESPONSE REGULAR EXPRESSIONS + private static readonly Regex L2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""(?\S+),(?\S+)"",RESOLUTION=(?\d+)x(?\d+),VIDEO=""\w+"",FRAME-RATE=(?\d+.\d+)"); + + // CHUNK RESPONSE REGULAR EXPRESSION + private static readonly Regex ChunkRegex = new Regex(@"#EXTINF:(?\d+.\d+),\n(?\S+.ts)"); + + #endregion + + + + #region CONSTRUCTORS + + public Vod(string id) + { + ID = id; + } + + #endregion + + + + #region PARAMETERS + + public string ID { 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 Stream[] Streams { get; private set; } + + #endregion + + + + #region STANDARD METHODS + + // GET VOD METADATA + public async Task GetMetadataAsync() + { + // Get access token + string accessToken = await Auth.ReadAccessTokenAsync(); + if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); + + // Check access token + var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken); + if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException(); + + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Authorization", $"Bearer {accessToken}"); + client.Headers.Add("Client-Id", Auth.ClientID); + + // Get response + client.QueryString.Add("id", ID); + JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0]; + + // Set parameters + Title = ((string)response["title"]).Replace("\n", ""); + Author = (string)response["user_name"]; + Date = Convert.ToDateTime(response["created_at"]); + Duration = TimeSpan.ParseExact((string)response["duration"], TimeFormats, null); + Views = (long)response["view_count"]; + Thumbnail = (string)response["thumbnail_url"] == string.Empty ? Globals.Assets.UnknownThumbnailImage : new Uri((string)response["thumbnail_url"]); + } + public void GetMetadataAsync(JToken response) + { + // Set parameters + Title = ((string)response["title"]).Replace("\n", ""); + Author = (string)response["user_name"]; + Date = Convert.ToDateTime(response["created_at"]); + Duration = TimeSpan.ParseExact((string)response["duration"], TimeFormats, null); + Views = (long)response["view_count"]; + Thumbnail = (string)response["thumbnail_url"] == string.Empty ? Globals.Assets.UnknownThumbnailImage : new Uri((string)response["thumbnail_url"]); + } + + // GET VOD STREAMS + public async Task GetStreamsAsync() + { + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Client-Id", Auth.GQLApiClientID); + + // Get video GQL access token + 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"]; + + // Get video streams + string[] response = (await client.DownloadStringTaskAsync($"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken["value"]}&nauthsig={videoAccessToken["signature"]}&allow_source=true&player=twitchweb")).Split("\n"); + + // Init streams list + List streams = new List(); + + // Parse response + for (int i = 2; i < response.Length; i += 3) + { + // Parse line 2 + Match line2 = L2Regex.Match(response[i + 1]); + + // Get info + Uri url = new Uri(response[i + 2]); + int width = int.Parse(line2.Groups["width"].Value); + int height = int.Parse(line2.Groups["height"].Value); + int frameRate = (int)Math.Round(double.Parse(line2.Groups["frame_rate"].Value)); + string videoCodec = line2.Groups["video_codec"].Value; + string audioCodec = line2.Groups["audio_codec"].Value; + + // Create stream + Stream stream = new Stream(url, true, StreamType.AudioVideo) + { + Width = width, + Height = height, + FrameRate = frameRate, + VideoCodec = videoCodec, + AudioCodec = audioCodec, + }; + + // Add stream + streams.Add(stream); + } + + // Set Streams parameter + Streams = streams.ToArray(); + } + + // DOWNLOAD AND TRANSCODE VOD + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) + { + // Set cancellation token + cancellationToken.ThrowIfCancellationRequested(); + + // Invoke DownloadingStarted event + DownloadingStarted?.Invoke(this, EventArgs.Empty); + + // Get video chunks + List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(audioVideoStream.Url); + + // Passive trim + if ((bool)Config.GetValue("twitch_vod_passive_trim")) + { + var trimResult = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration); + trimStart = trimResult.TrimStart; + trimEnd = trimResult.TrimEnd; + } + + // Download + StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts"); + float chunksDownloaded = 0; + + Task downloadTask; + Task writeTask; + + downloadTask = DownloadChunkAsync(chunksList[0].ChunkUrl); + await downloadTask; + for (int i = 1; i < chunksList.Count; i++) + { + writeTask = WriteChunkToFileAsync(rawFile, downloadTask.Result); + downloadTask = DownloadChunkAsync(chunksList[i].ChunkUrl); + await Task.WhenAll(writeTask, downloadTask); + DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null)); + } + await WriteChunkToFileAsync(rawFile, downloadTask.Result); + DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null)); + + DownloadingCompleted?.Invoke(this, EventArgs.Empty); + + // Processing + StorageFile outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}"); + + MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd); + mediaProcessor.ProcessingStarted += ProcessingStarted; + mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged; + mediaProcessor.ProcessingCompleted += ProcessingCompleted; + await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken); + + // Return output file + return outputFile; + } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioVideoStream, extension, mediaType, new TimeSpan(0), Duration, cancellationToken); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, extension, new TimeSpan(0), Duration, cancellationToken); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); } + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); } + + #endregion + + + + #region LOCAL METHODS + + // GET CHUNKS DATA FROM M3U8 PLAYLIST + private static async Task> ExtractChunksFromM3U8Async(Uri streamUrl) + { + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Client-Id", Auth.GQLApiClientID); + + // Get playlist + string response = await client.DownloadStringTaskAsync(streamUrl); + Debug.WriteLine(response); + // Create dictionary + List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>(); + + // Pack data into dictionary + foreach (Match chunk in ChunkRegex.Matches(response)) + { + Uri chunkUrl = new Uri($"{streamUrl.AbsoluteUri.Replace(System.IO.Path.GetFileName(streamUrl.AbsoluteUri), "")}{chunk.Groups["filename"].Value}"); + TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value)); + chunks.Add((chunkUrl, chunkDuration)); + } + + // Return chunks data + return chunks; + } + + // PASSIVE TRIM + private static (TimeSpan TrimStart, TimeSpan TrimEnd) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration) + { + // Copy duration + TimeSpan newDuration = duration; + + // Trim at start + while (chunksList[0].ChunkDuration <= trimStart) + { + trimStart = trimStart.Subtract(chunksList[0].ChunkDuration); + trimEnd = trimEnd.Subtract(chunksList[0].ChunkDuration); + newDuration = newDuration.Subtract(chunksList[0].ChunkDuration); + chunksList.RemoveAt(0); + } + + // Trim at end + while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trimEnd)) + { + newDuration = newDuration.Subtract(chunksList.Last().ChunkDuration); + chunksList.RemoveAt(chunksList.Count - 1); + } + + // Return data + return (trimStart, trimEnd); + } + + // DOWNLOAD CHUNK + private static async Task DownloadChunkAsync(Uri chunkUrl) + { + int retriesCount = 0; + while ((bool)Config.GetValue("twitch_vod_downloading_chunk_retry_after_error") && retriesCount < (int)Config.GetValue("twitch_vod_downloading_chunk_max_retries")) + { + try + { + using (WebClient client = new WebClient()) + { + return await client.DownloadDataTaskAsync(chunkUrl); + } + } + catch + { + retriesCount++; + await Task.Delay((int)Config.GetValue("twitch_vod_downloading_chunk_retries_delay")); + } + } + throw new WebException("An error occurs while downloading a Twitch VOD chunk"); + } + + // WRITE CHUNK TO FILE + private static Task WriteChunkToFileAsync(StorageFile file, byte[] chunk) + { + return Task.Factory.StartNew(() => + { + using (var stream = new System.IO.FileStream(file.Path, System.IO.FileMode.Append)) + { + stream.Write(chunk, 0, chunk.Length); + stream.Close(); + } + }); + } + + #endregion + + + + #region EVENT HANDLERS + + public event EventHandler DownloadingStarted; + + public event EventHandler DownloadingProgressChanged; + + public event EventHandler DownloadingCompleted; + + public event EventHandler ProcessingStarted; + + public event EventHandler ProcessingProgressChanged; + + public event EventHandler ProcessingCompleted; + + #endregion + } +} diff --git a/VDownload/GUI/Controls/MainPageLayoutControl.xaml b/VDownload/GUI/Controls/MainPageLayoutControl.xaml new file mode 100644 index 0000000..df95278 --- /dev/null +++ b/VDownload/GUI/Controls/MainPageLayoutControl.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/VDownload/GUI/Controls/MainPageLayoutControl.xaml.cs b/VDownload/GUI/Controls/MainPageLayoutControl.xaml.cs new file mode 100644 index 0000000..9d63ccc --- /dev/null +++ b/VDownload/GUI/Controls/MainPageLayoutControl.xaml.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace VDownload.GUI.Controls +{ + public sealed partial class MainPageLayoutControl : UserControl + { + // INIT + public MainPageLayoutControl() + { + this.InitializeComponent(); + } + + // PAGE CONTENT + public FrameworkElement PageContent { get; set; } + + // TITLE + public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(MainPageLayoutControl), new PropertyMetadata(string.Empty)); + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + } +} diff --git a/VDownload/GUI/Controls/SettingControl.xaml b/VDownload/GUI/Controls/SettingControl.xaml new file mode 100644 index 0000000..e071e9d --- /dev/null +++ b/VDownload/GUI/Controls/SettingControl.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VDownload/GUI/Controls/SettingControl.xaml.cs b/VDownload/GUI/Controls/SettingControl.xaml.cs new file mode 100644 index 0000000..4c951ab --- /dev/null +++ b/VDownload/GUI/Controls/SettingControl.xaml.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace VDownload.GUI.Controls +{ + public sealed partial class SettingControl : UserControl + { + // INIT + public SettingControl() + { + this.InitializeComponent(); + } + + // SETTING CONTENT + public FrameworkElement SettingContent { get; set; } + + // ICON + public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(IconElement), typeof(SettingControl), new PropertyMetadata(null)); + public IconElement Icon + { + get => (IconElement)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + // TITLE + public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(SettingControl), new PropertyMetadata(string.Empty)); + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + // DESCRIPTION + public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register("Description", typeof(string), typeof(SettingControl), new PropertyMetadata(string.Empty)); + public string Description + { + get => (string)GetValue(DescriptionProperty); + set => SetValue(DescriptionProperty, value); + } + + // DESCRIPTION COLOR + public static readonly DependencyProperty DescriptionColorProperty = DependencyProperty.Register("DescriptionColor", typeof(Brush), typeof(SettingControl), new PropertyMetadata(new SolidColorBrush((Color)Application.Current.Resources["SystemBaseMediumColor"]))); + public Brush DescriptionColor + { + get => (Brush)GetValue(DescriptionColorProperty); + set => SetValue(DescriptionColorProperty, value); + } + } +} diff --git a/VDownload/Views/AddVideo/AddVideoLoading.xaml b/VDownload/GUI/Views/Home/HomeMain.xaml similarity index 57% rename from VDownload/Views/AddVideo/AddVideoLoading.xaml rename to VDownload/GUI/Views/Home/HomeMain.xaml index 3864305..e8a62c3 100644 --- a/VDownload/Views/AddVideo/AddVideoLoading.xaml +++ b/VDownload/GUI/Views/Home/HomeMain.xaml @@ -1,17 +1,15 @@  + Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - - +