diff --git a/.github/Images/Home.png b/.github/Images/Home.png new file mode 100644 index 0000000..fc42a5b Binary files /dev/null and b/.github/Images/Home.png differ diff --git a/README.md b/README.md index 8840318..c8ea868 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # VDownload VDownload is universal video downloader written in .NET/C# and Universal Windows Platform. +![VDownload Home Page](.github/Images/Home.png) ## Requirements diff --git a/VDownload.Core/Enums/StreamType.cs b/VDownload.Core/Enums/StreamType.cs deleted file mode 100644 index 6f0d154..0000000 --- a/VDownload.Core/Enums/StreamType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace VDownload.Core.Enums -{ - public enum StreamType - { - AudioVideo, - OnlyAudio, - OnlyVideo, - } -} diff --git a/VDownload.Core/Enums/TaskAddingRequestSource.cs b/VDownload.Core/Enums/TaskAddingRequestSource.cs new file mode 100644 index 0000000..b2058c1 --- /dev/null +++ b/VDownload.Core/Enums/TaskAddingRequestSource.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VDownload.Core.Enums +{ + public enum TaskAddingRequestSource + { + Video, + Playlist + } +} diff --git a/VDownload.Core/EventArgs/PlaylistAddEventArgs.cs b/VDownload.Core/EventArgs/PlaylistAddEventArgs.cs deleted file mode 100644 index cbe6648..0000000 --- a/VDownload.Core/EventArgs/PlaylistAddEventArgs.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using VDownload.Core.Enums; -using VDownload.Core.Interfaces; -using VDownload.Core.Objects; -using Windows.Storage; - -namespace VDownload.Core.EventArgs -{ - public class PlaylistAddEventArgs : System.EventArgs - { - public ( - IVideoService VideoService, - MediaType MediaType, - IBaseStream Stream, - TimeSpan TrimStart, - TimeSpan TrimEnd, - string Filename, - MediaFileExtension Extension, - StorageFolder Location, - double Schedule - )[] Videos { get; set; } - } -} diff --git a/VDownload.Core/EventArgs/PlaylistSearchEventArgs.cs b/VDownload.Core/EventArgs/PlaylistSearchEventArgs.cs index d0b9036..32946a0 100644 --- a/VDownload.Core/EventArgs/PlaylistSearchEventArgs.cs +++ b/VDownload.Core/EventArgs/PlaylistSearchEventArgs.cs @@ -2,7 +2,7 @@ { public class PlaylistSearchEventArgs : System.EventArgs { - public string Phrase { get; set; } - public int Count { get; set; } + public string Url { get; set; } + public int VideosCount { get; set; } } } diff --git a/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs b/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs new file mode 100644 index 0000000..d4dfffa --- /dev/null +++ b/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs @@ -0,0 +1,24 @@ +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; set; } + public bool IsCompleted { get; set; } + + #endregion + } +} diff --git a/VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs b/VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs new file mode 100644 index 0000000..8da4118 --- /dev/null +++ b/VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs @@ -0,0 +1,11 @@ +using VDownload.Core.Enums; +using VDownload.Core.Structs; + +namespace VDownload.Core.EventArgs +{ + public class TasksAddingRequestedEventArgs : System.EventArgs + { + public TaskData[] TaskData { get; set; } + public TaskAddingRequestSource RequestSource { get; set; } + } +} diff --git a/VDownload.Core/EventArgs/VideoSearchEventArgs.cs b/VDownload.Core/EventArgs/VideoSearchEventArgs.cs index 720fd91..7028722 100644 --- a/VDownload.Core/EventArgs/VideoSearchEventArgs.cs +++ b/VDownload.Core/EventArgs/VideoSearchEventArgs.cs @@ -2,6 +2,6 @@ { public class VideoSearchEventArgs : System.EventArgs { - public string Phrase { get; set; } + public string Url { get; set; } } } diff --git a/VDownload.Core/Interfaces/IBaseStream.cs b/VDownload.Core/Interfaces/IBaseStream.cs deleted file mode 100644 index 5810605..0000000 --- a/VDownload.Core/Interfaces/IBaseStream.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using VDownload.Core.Enums; - -namespace VDownload.Core.Interfaces -{ - public interface IBaseStream - { - #region PROPERTIES - - Uri Url { get; } - bool IsChunked { get; } - StreamType StreamType { get; } - int Height { get; } - int FrameRate { get; } - - #endregion - } -} diff --git a/VDownload.Core/Interfaces/IVideoService.cs b/VDownload.Core/Interfaces/IVideoService.cs index 3834641..7ad0db4 100644 --- a/VDownload.Core/Interfaces/IVideoService.cs +++ b/VDownload.Core/Interfaces/IVideoService.cs @@ -3,6 +3,7 @@ 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 @@ -14,13 +15,8 @@ namespace VDownload.Core.Interfaces // VIDEO PROPERTIES string ID { get; } Uri VideoUrl { get; } - string Title { get; } - string Author { get; } - DateTime Date { get; } - TimeSpan Duration { get; } - long Views { get; } - Uri Thumbnail { get; } - IBaseStream[] BaseStreams { get; } + Metadata Metadata { get; } + BaseStream[] BaseStreams { get; } #endregion @@ -35,7 +31,7 @@ namespace VDownload.Core.Interfaces Task GetStreamsAsync(CancellationToken cancellationToken = default); // DOWNLOAD VIDEO - Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); #endregion @@ -43,12 +39,8 @@ namespace VDownload.Core.Interfaces #region EVENT HANDLERS - event EventHandler DownloadingStarted; - event EventHandler DownloadingProgressChanged; - event EventHandler DownloadingCompleted; - event EventHandler ProcessingStarted; - event EventHandler ProcessingProgressChanged; - event EventHandler ProcessingCompleted; + event EventHandler DownloadingProgressChanged; + event EventHandler ProcessingProgressChanged; #endregion } diff --git a/VDownload.Core/Objects/Stream.cs b/VDownload.Core/Objects/Stream.cs deleted file mode 100644 index 30d3449..0000000 --- a/VDownload.Core/Objects/Stream.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using VDownload.Core.Enums; -using VDownload.Core.Interfaces; - -namespace VDownload.Core.Objects -{ - public class Stream : IBaseStream - { - #region CONSTRUCTORS - - public Stream(Uri url, bool isChunked, StreamType streamType) - { - Url = url; - IsChunked = isChunked; - StreamType = streamType; - } - - #endregion - - - - #region PROPERTIES - - 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 - } -} diff --git a/VDownload.Core/Services/Config.cs b/VDownload.Core/Services/Config.cs index ee7bfdd..23201d3 100644 --- a/VDownload.Core/Services/Config.cs +++ b/VDownload.Core/Services/Config.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using VDownload.Core.Enums; using Windows.Media.Editing; +using Windows.Media.Transcoding; using Windows.Storage; namespace VDownload.Core.Services @@ -21,7 +22,7 @@ namespace VDownload.Core.Services { "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_transcoding_algorithm", (int)MediaVideoProcessingAlgorithm.MrfCrf444 }, { "media_editing_algorithm", (int)MediaTrimmingPreference.Fast }, { "default_max_playlist_videos", 0 }, { "default_media_type", (int)MediaType.AudioVideo }, diff --git a/VDownload.Core/Services/MediaProcessor.cs b/VDownload.Core/Services/MediaProcessor.cs index 1f59839..d75caef 100644 --- a/VDownload.Core/Services/MediaProcessor.cs +++ b/VDownload.Core/Services/MediaProcessor.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,66 +14,50 @@ namespace VDownload.Core.Services { public class MediaProcessor { - #region CONSTRUCTORS - - public MediaProcessor(StorageFile outputFile, TimeSpan trimStart, TimeSpan trimEnd) - { - OutputFile = outputFile; - TrimStart = trimStart; - TrimEnd = trimEnd; - } - - #endregion - - - - #region PROPERTIES - - public StorageFile OutputFile { get; private set; } - public TimeSpan TrimStart { get; private set; } - public TimeSpan TrimEnd { get; private set; } - - #endregion - - - #region STANDARD METHODS // SINGLE AUDIO & VIDEO FILE PROCESSING - public async Task Run(StorageFile audioVideoInputFile, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) + public async Task Run(StorageFile mediaFile, MediaFileExtension extension, MediaType mediaType, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) { - // Invoke ProcessingStarted event - ProcessingStarted?.Invoke(this, System.EventArgs.Empty); + // Invoke event at start + cancellationToken.ThrowIfCancellationRequested(); + ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); // Init transcoder MediaTranscoder mediaTranscoder = new MediaTranscoder { HardwareAccelerationEnabled = (bool)Config.GetValue("media_transcoding_use_hardware_acceleration"), - VideoProcessingAlgorithm = (bool)Config.GetValue("media_transcoding_use_mrfcrf444_algorithm") ? MediaVideoProcessingAlgorithm.MrfCrf444 : MediaVideoProcessingAlgorithm.Default, - TrimStartTime = TrimStart, - TrimStopTime = TrimEnd, + VideoProcessingAlgorithm = (MediaVideoProcessingAlgorithm)Config.GetValue("media_transcoding_algorithm"), }; + if (trimStart != null) mediaTranscoder.TrimStartTime = (TimeSpan)trimStart; + if (trimEnd != null) mediaTranscoder.TrimStopTime = (TimeSpan)trimEnd; // Start transcoding operation - cancellationToken.ThrowIfCancellationRequested(); - using (IRandomAccessStream outputFileOpened = await OutputFile.OpenAsync(FileAccessMode.ReadWrite)) + using (IRandomAccessStream openedOutputFile = await outputFile.OpenAsync(FileAccessMode.ReadWrite)) { - PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await audioVideoInputFile.OpenAsync(FileAccessMode.Read), outputFileOpened, await GetMediaEncodingProfile(audioVideoInputFile, extension, mediaType)); + // Prepare transcode task + PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await mediaFile.OpenAsync(FileAccessMode.Read), openedOutputFile, await GetMediaEncodingProfile(mediaFile, extension, mediaType)); + + // Start transcoding IAsyncActionWithProgress transcodingTask = transcodingPreparated.TranscodeAsync(); - await transcodingTask.AsTask(cancellationToken, new Progress((percent) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(percent), null)); })); - await outputFileOpened.FlushAsync(); + await transcodingTask.AsTask(cancellationToken, new Progress((percent) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(percent)); })); + cancellationToken.ThrowIfCancellationRequested(); + + // Finalizing + await openedOutputFile.FlushAsync(); transcodingTask.Close(); } - // Invoke ProcessingCompleted event - ProcessingCompleted?.Invoke(this, System.EventArgs.Empty); + // Invoke event at end + ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true)); } // SEPARATE AUDIO & VIDEO FILES PROCESSING - public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default) + public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) { - // Invoke ProcessingStarted event - ProcessingStarted?.Invoke(this, System.EventArgs.Empty); + // Invoke event at start + cancellationToken.ThrowIfCancellationRequested(); + ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); // Init editor MediaComposition mediaEditor = new MediaComposition(); @@ -86,28 +69,36 @@ namespace VDownload.Core.Services await Task.WhenAll(getVideoFileTask, getAudioFileTask); MediaClip videoElement = getVideoFileTask.Result; - videoElement.TrimTimeFromStart = TrimStart; - videoElement.TrimTimeFromEnd = TrimEnd; + if (trimStart != null) videoElement.TrimTimeFromStart = (TimeSpan)trimStart; + if (trimEnd != null) videoElement.TrimTimeFromEnd = (TimeSpan)trimEnd; BackgroundAudioTrack audioElement = getAudioFileTask.Result; - audioElement.TrimTimeFromStart = TrimStart; - audioElement.TrimTimeFromEnd = TrimEnd; + if (trimStart != null) audioElement.TrimTimeFromStart = (TimeSpan)trimStart; + if (trimEnd != null) audioElement.TrimTimeFromEnd = (TimeSpan)trimEnd; - mediaEditor.Clips.Add(getVideoFileTask.Result); - mediaEditor.BackgroundAudioTracks.Add(getAudioFileTask.Result); + mediaEditor.Clips.Add(videoElement); + mediaEditor.BackgroundAudioTracks.Add(audioElement); // 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)); }; + 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); - // Invoke ProcessingCompleted event - ProcessingCompleted?.Invoke(this, System.EventArgs.Empty); + // Invoke event at end + ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true)); } - // SINGLE AUDIO OR VIDEO FILES PROCESSING - 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); } + // AUDIO FILE PROCESSING + public async Task Run(StorageFile audioFile, AudioFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) + { + await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, outputFile, trimStart, trimEnd, cancellationToken); + } + + // VIDEO FILE PROCESSING + public async Task Run(StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) + { + await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, outputFile, trimStart, trimEnd, cancellationToken); + } #endregion @@ -116,7 +107,7 @@ namespace VDownload.Core.Services #region LOCAL METHODS // GET ENCODING PROFILE - public static async Task GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType) + private static async Task GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType) { // Create profile object MediaEncodingProfile profile; @@ -164,7 +155,10 @@ namespace VDownload.Core.Services // Return profile return profile; } - public static async Task GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType) { return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType); } + private static async Task GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType) + { + return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType); + } #endregion @@ -172,9 +166,7 @@ namespace VDownload.Core.Services #region EVENT HANDLERS - public event EventHandler ProcessingStarted; - public event EventHandler ProcessingProgressChanged; - public event EventHandler ProcessingCompleted; + public event EventHandler ProgressChanged; #endregion } diff --git a/VDownload.Core/Services/Sources/Twitch/Channel.cs b/VDownload.Core/Services/Sources/Twitch/Channel.cs index 4b39606..a1d031f 100644 --- a/VDownload.Core/Services/Sources/Twitch/Channel.cs +++ b/VDownload.Core/Services/Sources/Twitch/Channel.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using VDownload.Core.Exceptions; using VDownload.Core.Interfaces; +using VDownload.Core.Services.Sources.Twitch.Helpers; namespace VDownload.Core.Services.Sources.Twitch { @@ -41,25 +42,15 @@ namespace VDownload.Core.Services.Sources.Twitch // GET CHANNEL METADATA public async Task GetMetadataAsync(CancellationToken cancellationToken = default) { - // Get access token cancellationToken.ThrowIfCancellationRequested(); - string accessToken = await Auth.ReadAccessTokenAsync(); - if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); - - // Check access token - cancellationToken.ThrowIfCancellationRequested(); - 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); - cancellationToken.ThrowIfCancellationRequested(); - JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"][0]; + 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"][0]; + } // Create unified playlist url PlaylistUrl = new Uri($"https://twitch.tv/{ID}"); @@ -72,12 +63,9 @@ namespace VDownload.Core.Services.Sources.Twitch // GET CHANNEL VIDEOS public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default) { - // Get access token cancellationToken.ThrowIfCancellationRequested(); - string accessToken = await Auth.ReadAccessTokenAsync(); - if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); - // Set pagination + // Set page id string pagination = ""; // Set array of videos @@ -92,30 +80,24 @@ namespace VDownload.Core.Services.Sources.Twitch List getStreamsTasks = new List(); do { - // Check access token - cancellationToken.ThrowIfCancellationRequested(); - 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 && !getAll ? numberOfVideos : 100; // Get response - client.QueryString.Add("user_id", ID); - client.QueryString.Add("first", count.ToString()); - client.QueryString.Add("after", pagination); - - cancellationToken.ThrowIfCancellationRequested(); - JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")); + JToken response = null; + using (WebClient client = await Client.Helix()) + { + client.QueryString.Add("user_id", ID); + client.QueryString.Add("first", count.ToString()); + client.QueryString.Add("after", pagination); + response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")); + } + // Set page id pagination = (string)response["pagination"]["cursor"]; - videosData = response["data"].ToArray(); + // Set videos data + videosData = response["data"].ToArray(); foreach (JToken videoData in videosData) { Vod video = new Vod((string)videoData["id"]); @@ -131,7 +113,7 @@ namespace VDownload.Core.Services.Sources.Twitch // Wait for all getStreams tasks await Task.WhenAll(getStreamsTasks); - // Set Videos parameter + // Set videos Videos = videos.ToArray(); } diff --git a/VDownload.Core/Services/Sources/Twitch/Clip.cs b/VDownload.Core/Services/Sources/Twitch/Clip.cs index 48499b4..a9a30d8 100644 --- a/VDownload.Core/Services/Sources/Twitch/Clip.cs +++ b/VDownload.Core/Services/Sources/Twitch/Clip.cs @@ -12,21 +12,14 @@ using System.Web; using VDownload.Core.Enums; using VDownload.Core.Exceptions; using VDownload.Core.Interfaces; -using VDownload.Core.Objects; +using VDownload.Core.Services.Sources.Twitch.Helpers; +using VDownload.Core.Structs; using Windows.Storage; namespace VDownload.Core.Services.Sources.Twitch { public class Clip : IVideoService { - #region CONSTANTS - - - - #endregion - - - #region CONSTRUCTORS public Clip(string id) @@ -42,13 +35,8 @@ namespace VDownload.Core.Services.Sources.Twitch public string ID { get; private set; } public Uri VideoUrl { 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 IBaseStream[] BaseStreams { get; private set; } + public Metadata Metadata { get; private set; } + public BaseStream[] BaseStreams { get; private set; } #endregion @@ -59,92 +47,83 @@ namespace VDownload.Core.Services.Sources.Twitch // GET CLIP METADATA public async Task GetMetadataAsync(CancellationToken cancellationToken = default) { - // Get access token cancellationToken.ThrowIfCancellationRequested(); - string accessToken = await Auth.ReadAccessTokenAsync(); - if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); - - // Check access token - cancellationToken.ThrowIfCancellationRequested(); - 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]; + 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")[0]; + } // Create unified video url VideoUrl = new Uri($"https://clips.twitch.tv/{ID}"); - // 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"]); + // Set metadata + Metadata = new Metadata() + { + 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) { - // Create client - WebClient client = new WebClient { Encoding = Encoding.UTF8 }; - client.Headers.Add("Client-ID", Auth.GQLApiClientID); - - // Get video streams cancellationToken.ThrowIfCancellationRequested(); - 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(); - + + // Get response + 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(); + } + // Init streams list - List streams = new 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) + BaseStream stream = new BaseStream() { - Height = height, - FrameRate = frameRate + Url = new Uri((string)streamData["sourceURL"]), + Height = int.Parse((string)streamData["quality"]), + FrameRate = (int)streamData["frameRate"], }; // Add stream streams.Add(stream); } - // Set Streams parameter + // Set streams BaseStreams = streams.ToArray(); } - public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { // Invoke DownloadingStarted event - DownloadingStarted?.Invoke(this, System.EventArgs.Empty); - - // Create client - WebClient client = new WebClient(); - client.Headers.Add("Client-Id", Auth.GQLApiClientID); + cancellationToken.ThrowIfCancellationRequested(); + DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); // Get video GQL access token - cancellationToken.ThrowIfCancellationRequested(); - 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"]; + 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 (client = new WebClient()) + using (WebClient client = new WebClient()) { - client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new ProgressChangedEventArgs(a.ProgressPercentage, null)); }; + 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(); @@ -153,20 +132,24 @@ namespace VDownload.Core.Services.Sources.Twitch await client.DownloadFileTaskAsync(baseStream.Url, rawFile.Path); } } - DownloadingCompleted?.Invoke(this, System.EventArgs.Empty); + DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true)); // Processing StorageFile outputFile = rawFile; - if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart > new TimeSpan(0) || trimEnd < Duration) + if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart != null || trimEnd != null) { cancellationToken.ThrowIfCancellationRequested(); outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}"); - MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd); - mediaProcessor.ProcessingStarted += ProcessingStarted; - mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged; - mediaProcessor.ProcessingCompleted += ProcessingCompleted; - cancellationToken.ThrowIfCancellationRequested(); - await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken); + + MediaProcessor mediaProcessor = new MediaProcessor(); + mediaProcessor.ProgressChanged += ProcessingProgressChanged; + + Task mediaProcessorTask; + if (trimStart == TimeSpan.Zero && trimEnd == Metadata.Duration) mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, cancellationToken: cancellationToken); + else if (trimStart == TimeSpan.Zero) mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trimStart: trimStart, cancellationToken: cancellationToken); + else if (trimEnd == Metadata.Duration) mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trimEnd: trimEnd, cancellationToken: cancellationToken); + else mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trimStart, trimEnd, cancellationToken); + await mediaProcessorTask; } // Return output file @@ -179,12 +162,8 @@ namespace VDownload.Core.Services.Sources.Twitch #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; + public event EventHandler DownloadingProgressChanged; + public event EventHandler ProcessingProgressChanged; #endregion } diff --git a/VDownload.Core/Services/Sources/Twitch/Auth.cs b/VDownload.Core/Services/Sources/Twitch/Helpers/Auth.cs similarity index 96% rename from VDownload.Core/Services/Sources/Twitch/Auth.cs rename to VDownload.Core/Services/Sources/Twitch/Helpers/Auth.cs index 8c13420..e545d4e 100644 --- a/VDownload.Core/Services/Sources/Twitch/Auth.cs +++ b/VDownload.Core/Services/Sources/Twitch/Helpers/Auth.cs @@ -1,16 +1,12 @@ 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 +namespace VDownload.Core.Services.Sources.Twitch.Helpers { public class Auth { diff --git a/VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs b/VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs new file mode 100644 index 0000000..4bd2e99 --- /dev/null +++ b/VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs @@ -0,0 +1,38 @@ +using System.Net; +using System.Threading.Tasks; +using VDownload.Core.Exceptions; + +namespace VDownload.Core.Services.Sources.Twitch.Helpers +{ + internal class Client + { + internal static async Task Helix() + { + // 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); + + // Return client + return client; + } + + internal static WebClient GQL() + { + // Create client + WebClient client = new WebClient(); + client.Headers.Add("Client-Id", Auth.GQLApiClientID); + + // Return client + return client; + } + } +} diff --git a/VDownload.Core/Services/Sources/Twitch/Vod.cs b/VDownload.Core/Services/Sources/Twitch/Vod.cs index a8c30f7..ce7d749 100644 --- a/VDownload.Core/Services/Sources/Twitch/Vod.cs +++ b/VDownload.Core/Services/Sources/Twitch/Vod.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Net; @@ -9,27 +8,15 @@ 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.Objects; +using VDownload.Core.Services.Sources.Twitch.Helpers; +using VDownload.Core.Structs; using Windows.Storage; namespace VDownload.Core.Services.Sources.Twitch { public class Vod : IVideoService { - #region CONSTANTS - - // 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) @@ -45,13 +32,8 @@ namespace VDownload.Core.Services.Sources.Twitch public string ID { get; private set; } public Uri VideoUrl { 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 IBaseStream[] BaseStreams { get; private set; } + public Metadata Metadata { get; private set; } + public BaseStream[] BaseStreams { get; private set; } #endregion @@ -62,25 +44,16 @@ namespace VDownload.Core.Services.Sources.Twitch // GET VOD METADATA public async Task GetMetadataAsync(CancellationToken cancellationToken = default) { - // Get access token cancellationToken.ThrowIfCancellationRequested(); - string accessToken = await Auth.ReadAccessTokenAsync(); - if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); - - // Check access token - cancellationToken.ThrowIfCancellationRequested(); - 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); - cancellationToken.ThrowIfCancellationRequested(); - JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0]; + JToken response = null; + using (WebClient client = await Client.Helix()) + { + client.QueryString.Add("id", ID); + cancellationToken.ThrowIfCancellationRequested(); + response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0]; + } // Set parameters GetMetadataAsync(response); @@ -90,83 +63,86 @@ namespace VDownload.Core.Services.Sources.Twitch // Create unified video url VideoUrl = new Uri($"https://www.twitch.tv/videos/{ID}"); - // Set parameters - 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")); + // Set metadata + Metadata = new Metadata() + { + 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")), + }; } // GET VOD STREAMS public async Task GetStreamsAsync(CancellationToken cancellationToken = default) { - // Create client - WebClient client = new WebClient(); - client.Headers.Add("Client-Id", Auth.GQLApiClientID); - - // Get video GQL access token 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"]; - // Get video streams - cancellationToken.ThrowIfCancellationRequested(); - string[] response = (await client.DownloadStringTaskAsync($"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken["value"]}&nauthsig={videoAccessToken["signature"]}&allow_source=true&player=twitchweb")).Split("\n"); + // Get response + string[] response = null; + using (WebClient client = Client.GQL()) + { + // Get video GQL access token + 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"]; + + // Get video streams + 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"); + } // Init streams list - List streams = new List(); + List streams = new List(); + + // Stream data line2 regular expression + Regex streamDataL2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""\S+,\S+"",RESOLUTION=\d+x(?\d+),VIDEO=""\w+""(,FRAME-RATE=(?\d+.\d+))?"); // 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 = line2.Groups["frame_rate"].Value != string.Empty ? (int)Math.Round(double.Parse(line2.Groups["frame_rate"].Value)) : 0; - string videoCodec = line2.Groups["video_codec"].Value; - string audioCodec = line2.Groups["audio_codec"].Value; + Match line2 = streamDataL2Regex.Match(response[i + 1]); // Create stream - Stream stream = new Stream(url, true, StreamType.AudioVideo) + BaseStream stream = new BaseStream() { - Width = width, - Height = height, - FrameRate = frameRate, - VideoCodec = videoCodec, - AudioCodec = audioCodec, + 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, }; // Add stream streams.Add(stream); } - // Set Streams parameter + // Set streams BaseStreams = streams.ToArray(); } // DOWNLOAD AND TRANSCODE VOD - public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { // Invoke DownloadingStarted event - DownloadingStarted?.Invoke(this, System.EventArgs.Empty); + cancellationToken.ThrowIfCancellationRequested(); + DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); // Get video chunks cancellationToken.ThrowIfCancellationRequested(); List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken); + // Changeable duration + TimeSpan duration = Metadata.Duration; + // Passive trim - if ((bool)Config.GetValue("twitch_vod_passive_trim")) (trimStart, trimEnd) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration); + if ((bool)Config.GetValue("twitch_vod_passive_trim") && trimStart != TimeSpan.Zero && trimEnd != duration) (trimStart, trimEnd, duration) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Metadata.Duration); // Download cancellationToken.ThrowIfCancellationRequested(); StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts"); - float chunksDownloaded = 0; + double chunksDownloaded = 0; Task downloadTask; Task writeTask; @@ -180,26 +156,25 @@ namespace VDownload.Core.Services.Sources.Twitch 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)); + DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(++chunksDownloaded * 100 / chunksList.Count)); } cancellationToken.ThrowIfCancellationRequested(); await WriteChunkToFileAsync(rawFile, downloadTask.Result); - DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null)); - - DownloadingCompleted?.Invoke(this, System.EventArgs.Empty); - + DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true)); // Processing cancellationToken.ThrowIfCancellationRequested(); 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; - cancellationToken.ThrowIfCancellationRequested(); - await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken); - + MediaProcessor mediaProcessor = new MediaProcessor(); + mediaProcessor.ProgressChanged += ProcessingProgressChanged; + + Task mediaProcessorTask; + if (trimStart == TimeSpan.Zero && trimEnd == duration) mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, cancellationToken: cancellationToken); + else if (trimStart == TimeSpan.Zero) mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trimStart: trimStart, cancellationToken: cancellationToken); + else if (trimEnd == duration) mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trimEnd: trimEnd, cancellationToken: cancellationToken); + else mediaProcessorTask = mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trimStart, trimEnd, cancellationToken); + await mediaProcessorTask; // Return output file return outputFile; @@ -214,21 +189,28 @@ namespace VDownload.Core.Services.Sources.Twitch // GET CHUNKS DATA FROM M3U8 PLAYLIST private static async Task> ExtractChunksFromM3U8Async(Uri streamUrl, CancellationToken cancellationToken = default) { - // Create client - WebClient client = new WebClient(); - client.Headers.Add("Client-Id", Auth.GQLApiClientID); - - // Get playlist cancellationToken.ThrowIfCancellationRequested(); - string response = await client.DownloadStringTaskAsync(streamUrl); + + // Get response + string response = null; + using (WebClient client = Client.GQL()) + { + response = await client.DownloadStringTaskAsync(streamUrl); + } // Create dictionary List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>(); + // Chunk data regular expression + Regex chunkDataRegex = new Regex(@"#EXTINF:(?\d+.\d+),\n(?\S+.ts)"); + + // Chunks location + string chunkLocationPath = streamUrl.AbsoluteUri.Replace(System.IO.Path.GetFileName(streamUrl.AbsoluteUri), ""); + // Pack data into dictionary - foreach (Match chunk in ChunkRegex.Matches(response)) + foreach (Match chunk in chunkDataRegex.Matches(response)) { - Uri chunkUrl = new Uri($"{streamUrl.AbsoluteUri.Replace(System.IO.Path.GetFileName(streamUrl.AbsoluteUri), "")}{chunk.Groups["filename"].Value}"); + Uri chunkUrl = new Uri($"{chunkLocationPath}{chunk.Groups["filename"].Value}"); TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value)); chunks.Add((chunkUrl, chunkDuration)); } @@ -238,7 +220,7 @@ namespace VDownload.Core.Services.Sources.Twitch } // PASSIVE TRIM - private static (TimeSpan TrimStart, TimeSpan TrimEnd) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration) + private static (TimeSpan NewTrimStart, TimeSpan NewTrimEnd, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration) { // Copy duration TimeSpan newDuration = duration; @@ -260,7 +242,7 @@ namespace VDownload.Core.Services.Sources.Twitch } // Return data - return (trimStart, trimEnd); + return (trimStart, trimEnd, newDuration); } // DOWNLOAD CHUNK @@ -322,12 +304,8 @@ namespace VDownload.Core.Services.Sources.Twitch #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; + public event EventHandler DownloadingProgressChanged; + public event EventHandler ProcessingProgressChanged; #endregion } diff --git a/VDownload.Core/Services/TaskId.cs b/VDownload.Core/Services/TaskId.cs index dbb4e59..6707ffa 100644 --- a/VDownload.Core/Services/TaskId.cs +++ b/VDownload.Core/Services/TaskId.cs @@ -7,21 +7,12 @@ namespace VDownload.Core.Services { #region CONSTANTS - // RANDOM - private static readonly Random Random = new Random(); - // ID SETTINGS private static readonly char[] IDChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); private static readonly int IDLength = 10; - #endregion - - - - #region PROPERTIES - - // USED IDS LIST - private static readonly List UsedIDs = new List(); + // IDS LIST + private static readonly List IDList = new List(); #endregion @@ -38,17 +29,17 @@ namespace VDownload.Core.Services id = ""; while (id.Length < IDLength) { - id += IDChars[Random.Next(0, IDChars.Length)]; + id += IDChars[new Random().Next(0, IDChars.Length)]; } - } while (UsedIDs.Contains(id)); - UsedIDs.Add(id); + } while (IDList.Contains(id)); + IDList.Add(id); return id; } // DISPOSE TASK ID public static void Dispose(string id) { - UsedIDs.Remove(id); + IDList.Remove(id); } #endregion diff --git a/VDownload.Core/Services/TimeSpanCustomFormat.cs b/VDownload.Core/Services/TimeSpanCustomFormat.cs new file mode 100644 index 0000000..003748b --- /dev/null +++ b/VDownload.Core/Services/TimeSpanCustomFormat.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VDownload.Core.Services +{ + public class TimeSpanCustomFormat + { + // (TH:)MM:SS + public static string ToOptTHBaseMMSS(TimeSpan timeSpan, params TimeSpan[] formatBase) + { + string formattedTimeSpan = string.Empty; + + int maxTHLength = 0; + if (Math.Floor(timeSpan.TotalHours) > 0) + { + maxTHLength = Math.Floor(timeSpan.TotalHours).ToString().Length; + foreach (TimeSpan format in formatBase) + { + int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0; + if (THLength > maxTHLength) maxTHLength = THLength; + } + formattedTimeSpan += $"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:"; + } + formattedTimeSpan += maxTHLength == 0 ? $"{timeSpan.Minutes}:" : $"{timeSpan.Minutes:00}:"; + formattedTimeSpan += $"{timeSpan.Seconds:00}"; + + return formattedTimeSpan; + } + + // ((TH:)MM:)SS + public static string ToOptTHMMBaseSS(TimeSpan timeSpan, params TimeSpan[] formatBase) + { + string formattedTimeSpan = string.Empty; + + int maxTHLength = 0; + if (Math.Floor(timeSpan.TotalHours) > 0) + { + maxTHLength = Math.Floor(timeSpan.TotalHours).ToString().Length; + foreach (TimeSpan format in formatBase) + { + int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0; + if (THLength > maxTHLength) maxTHLength = THLength; + } + formattedTimeSpan += $"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:"; + } + bool MM = false; + if (Math.Floor(timeSpan.TotalMinutes) > 0) + { + formattedTimeSpan += maxTHLength > 0 ? $"{timeSpan.Minutes:00}:" : $"{timeSpan.Minutes}:"; + MM = true; + } + formattedTimeSpan += MM ? $"{timeSpan.Seconds:00}:" : $"{timeSpan.Seconds}:"; + + return formattedTimeSpan; + } + } +} diff --git a/VDownload.Core/Structs/BaseStream.cs b/VDownload.Core/Structs/BaseStream.cs new file mode 100644 index 0000000..d728710 --- /dev/null +++ b/VDownload.Core/Structs/BaseStream.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VDownload.Core.Structs +{ + public struct BaseStream + { + public Uri Url { get; set; } + public int Height { get; set; } + public int FrameRate { get; set; } + } +} diff --git a/VDownload.Core/Structs/Metadata.cs b/VDownload.Core/Structs/Metadata.cs new file mode 100644 index 0000000..071c0e5 --- /dev/null +++ b/VDownload.Core/Structs/Metadata.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VDownload.Core.Structs +{ + public struct Metadata + { + public string Title { get; set; } + public string Author { get; set; } + public DateTime Date { get; set; } + public TimeSpan Duration { get; set; } + public long Views { get; set; } + public Uri Thumbnail { get; set; } + } +} diff --git a/VDownload.Core/EventArgs/VideoAddEventArgs.cs b/VDownload.Core/Structs/TaskData.cs similarity index 75% rename from VDownload.Core/EventArgs/VideoAddEventArgs.cs rename to VDownload.Core/Structs/TaskData.cs index 55e5e9a..74ddb4d 100644 --- a/VDownload.Core/EventArgs/VideoAddEventArgs.cs +++ b/VDownload.Core/Structs/TaskData.cs @@ -1,16 +1,15 @@ using System; using VDownload.Core.Enums; using VDownload.Core.Interfaces; -using VDownload.Core.Objects; using Windows.Storage; -namespace VDownload.Core.EventArgs +namespace VDownload.Core.Structs { - public class VideoAddEventArgs : System.EventArgs + public struct TaskData { public IVideoService VideoService { get; set; } public MediaType MediaType { get; set; } - public IBaseStream Stream { get; set; } + public BaseStream Stream { get; set; } public TimeSpan TrimStart { get; set; } public TimeSpan TrimEnd { get; set; } public string Filename { get; set; } diff --git a/VDownload.Core/VDownload.Core.csproj b/VDownload.Core/VDownload.Core.csproj index 650fe5e..8375529 100644 --- a/VDownload.Core/VDownload.Core.csproj +++ b/VDownload.Core/VDownload.Core.csproj @@ -124,24 +124,27 @@ - + - - + + - - + + + + + - + diff --git a/VDownload/Strings/en-US/Resources.resw b/VDownload/Strings/en-US/Resources.resw index 3d36421..267d523 100644 --- a/VDownload/Strings/en-US/Resources.resw +++ b/VDownload/Strings/en-US/Resources.resw @@ -182,7 +182,7 @@ The number in the numberbox indicades how many videos will be got from playlist. 90 - Load subscription + Load subscriptions 120 diff --git a/VDownload/Strings/en-US/ResourcesOld.resw b/VDownload/Strings/en-US/ResourcesOld.resw deleted file mode 100644 index 03adbcc..0000000 --- a/VDownload/Strings/en-US/ResourcesOld.resw +++ /dev/null @@ -1,348 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Cancel - - - Add - - - ADD PLAYLIST - - - Restore - - - {x} videos removed - - - Apply - - - Location - - - Playlist not found. Try again. - - - Search - - - Paste URL and click "Search" button - - - URL - - - Only audio - - - Normal - - - Only video - - - Media type - - - Quality - - - Trim - - - File - - - Location - - - Only audio - - - Cancel - - - Add - - - ADD VIDEO - - - Video not found. Try again. - - - Paste URL and click "Search" button - - - Start - - - Subscriptions - - - Media type - - - Quality - - - Download options - - - Trim - - - Only audio - - - Apply - - - Filename and extension - - - File - - - Location - - - Save options - - - File - - - Browse - - - Location - - - Only audio - - - Normal - - - Only video - - - If numberbox is set to 0, app will load all videos from the playlist. Otherwise, the program will load the specified number of videos. - - - - Twitch (Channels) - - - - Youtube (Playlists, Channels) - - - Supported websites, types of playlist and numberbox instruction - - - Search - - - Playlist URL - - - - Twitch (VODs, Clips) - - - - Youtube (Videos) - - - Supported websites and types of videos - - - Search - - - Video URL - - - Add playlist - - - 85 - - - Add video - - - 75 - - - Download All - - - 90 - - - Load subscriptions - - - 120 - - - Only audio - - - Audio & Video - - - Only video - - - Cancelled - - - Done in - - - seconds - - - Downloading - - - An error occured! - - - Internet connection error - - - Finalizing - - - Idle - - - Transcoding - - - Queued - - - Only audio - - \ No newline at end of file diff --git a/VDownload/VDownload.csproj b/VDownload/VDownload.csproj index d288260..9c17aa0 100644 --- a/VDownload/VDownload.csproj +++ b/VDownload/VDownload.csproj @@ -365,9 +365,6 @@ 2.8.0-prerelease.220118001 - - - diff --git a/VDownload/Views/Home/HomeMain.xaml b/VDownload/Views/Home/HomeMain.xaml index 171ea56..10be0d4 100644 --- a/VDownload/Views/Home/HomeMain.xaml +++ b/VDownload/Views/Home/HomeMain.xaml @@ -60,7 +60,7 @@ - + diff --git a/VDownload/Views/Home/HomeMain.xaml.cs b/VDownload/Views/Home/HomeMain.xaml.cs index 56ba3b6..441fdde 100644 --- a/VDownload/Views/Home/HomeMain.xaml.cs +++ b/VDownload/Views/Home/HomeMain.xaml.cs @@ -11,6 +11,7 @@ using VDownload.Core.EventArgs; using VDownload.Core.Exceptions; using VDownload.Core.Interfaces; using VDownload.Core.Services; +using VDownload.Core.Structs; using Windows.ApplicationModel.Resources; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -21,11 +22,29 @@ namespace VDownload.Views.Home { public sealed partial class HomeMain : Page { + #region CONSTANTS + + // RESOURCES + private static readonly ResourceDictionary ImageRes = new ResourceDictionary { Source = new Uri("ms-appx:///Resources/Icons.xaml") }; + + // SEARCHING STATUS CONTROLS + private static readonly Microsoft.UI.Xaml.Controls.ProgressRing HomeOptionsBarSearchingStatusProgressRing = new Microsoft.UI.Xaml.Controls.ProgressRing { Width = 15, Height = 15, Margin = new Thickness(5), IsActive = true }; + private static readonly Image HomeOptionsBarSearchingStatusErrorImage = new Image { Width = 15, Height = 15, Margin = new Thickness(5), Source = (SvgImageSource)ImageRes["ErrorIcon"] }; + + // TASKS LIST PLACEHOLDER + private static readonly HomeTasksListPlaceholder HomeTasksListPlaceholder = new HomeTasksListPlaceholder(); + + #endregion + + #region CONSTRUCTORS public HomeMain() { this.InitializeComponent(); + + // Set cancellation token + SearchingCancellationToken = new CancellationTokenSource(); } #endregion @@ -34,20 +53,13 @@ namespace VDownload.Views.Home #region PROPERTIES - // SEARCHING STATUS CONTROLS - private readonly Microsoft.UI.Xaml.Controls.ProgressRing HomeOptionsBarSearchingStatusProgressRing = new Microsoft.UI.Xaml.Controls.ProgressRing { Width = 15, Height = 15, Margin = new Thickness(5), IsActive = true }; - private readonly Image HomeOptionsBarSearchingStatusErrorImage = new Image { Width = 15, Height = 15, Margin = new Thickness(5), Source = (SvgImageSource)new ResourceDictionary { Source = new Uri("ms-appx:///Resources/Icons.xaml") }["ErrorIcon"] }; - // CANCELLATON TOKEN - private CancellationTokenSource SearchingCancellationToken = new CancellationTokenSource(); + private CancellationTokenSource SearchingCancellationToken { get; set; } - // HOME TASKS LIST PLACEHOLDER - private readonly HomeTasksListPlaceholder HomeTasksListPlaceholder = new HomeTasksListPlaceholder(); - - // HOME VIDEOS LIST - private static ContentControl HomeTasksListPlaceCurrent { get; set; } + // HOME TASKS LIST + private static ContentControl HomeTasksListCurrentParent = null; private static StackPanel HomeTasksList = null; - public static List TaskPanelsList = new List(); + public static List TasksList = new List(); #endregion @@ -58,20 +70,28 @@ namespace VDownload.Views.Home // ON NAVIGATED TO protected override void OnNavigatedTo(NavigationEventArgs e) { - HomeTasksListPlaceCurrent = HomeTasksListPlace; + // Set current panel + HomeTasksListCurrentParent = HomeTasksListParent; + + // Detach task panels from old task list if (HomeTasksList != null) HomeTasksList.Children.Clear(); + + // Create new task list HomeTasksList = new StackPanel { Spacing = 10 }; - if (TaskPanelsList.Count > 0) + + // Attach task panels to new task list + if (TasksList.Count > 0) { - foreach (HomeTaskPanel homeVideoPanel in TaskPanelsList) HomeTasksList.Children.Add(homeVideoPanel); - HomeTasksListPlaceCurrent.Content = HomeTasksList; + foreach (HomeTaskPanel homeVideoPanel in TasksList) HomeTasksList.Children.Add(homeVideoPanel); + HomeTasksListCurrentParent.Content = HomeTasksList; } else { - HomeTasksListPlaceCurrent.Content = HomeTasksListPlaceholder; + HomeTasksListCurrentParent.Content = HomeTasksListPlaceholder; } } + // ADD VIDEO BUTTON CHECKED private void HomeOptionsBarAddVideoButton_Checked(object sender, RoutedEventArgs e) { @@ -100,7 +120,7 @@ namespace VDownload.Views.Home HomeOptionsBarSearchingStatusControl.Content = HomeOptionsBarSearchingStatusProgressRing; // Parse url - (VideoSource Type, string ID) source = Source.GetVideoSource(e.Phrase); + (VideoSource Type, string ID) source = Source.GetVideoSource(e.Url); // Check url if (source.Type == VideoSource.Null) @@ -176,37 +196,13 @@ namespace VDownload.Views.Home HomeOptionBarAndAddingPanelRow.Height = new GridLength(1, GridUnitType.Star); HomeTasksListRow.Height = new GridLength(0); + // Open adding panel HomeVideoAddingPanel addingPanel = new HomeVideoAddingPanel(videoService); - addingPanel.VideoAddRequest += HomeVideoAddingPanel_VideoAddRequest; + addingPanel.TasksAddingRequested += HomeTasksAddingRequest; HomeAddingPanel.Content = addingPanel; } } - // ADD VIDEO REQUEST FROM VIDEO ADDING PANEL - private void HomeVideoAddingPanel_VideoAddRequest(object sender, VideoAddEventArgs e) - { - // Replace placeholder - HomeTasksListPlaceCurrent.Content = HomeTasksList; - - // Uncheck video button - HomeOptionsBarAddVideoButton.IsChecked = false; - - // Create video task - HomeTaskPanel taskPanel = new HomeTaskPanel(e.VideoService, e.MediaType, e.Stream, e.TrimStart, e.TrimEnd, e.Filename, e.Extension, e.Location, e.Schedule); - - taskPanel.TaskRemovingRequested += (s, a) => - { - // Remove task from tasks lists - TaskPanelsList.Remove(taskPanel); - HomeTasksList.Children.Remove(taskPanel); - if (TaskPanelsList.Count <= 0) HomeTasksListPlaceCurrent.Content = HomeTasksListPlaceholder; - }; - - // Add task to tasks lists - HomeTasksList.Children.Add(taskPanel); - TaskPanelsList.Add(taskPanel); - } - // ADD PLAYLIST BUTTON CHECKED private void HomeOptionsBarAddPlaylistButton_Checked(object sender, RoutedEventArgs e) @@ -236,7 +232,7 @@ namespace VDownload.Views.Home HomeOptionsBarSearchingStatusControl.Content = HomeOptionsBarSearchingStatusProgressRing; // Parse url - (PlaylistSource Type, string ID) source = Source.GetPlaylistSource(e.Phrase); + (PlaylistSource Type, string ID) source = Source.GetPlaylistSource(e.Url); // Check url if (source.Type == PlaylistSource.Null) @@ -256,7 +252,7 @@ namespace VDownload.Views.Home try { await playlistService.GetMetadataAsync(SearchingCancellationToken.Token); - await playlistService.GetVideosAsync(e.Count, SearchingCancellationToken.Token); + await playlistService.GetVideosAsync(e.VideosCount, SearchingCancellationToken.Token); } catch (OperationCanceledException) { @@ -290,7 +286,7 @@ namespace VDownload.Views.Home catch (WebException wex) { HomeOptionsBarSearchingStatusControl.Content = HomeOptionsBarSearchingStatusErrorImage; - if (wex.Response == null) + if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable) { ContentDialog internetAccessErrorDialog = new ContentDialog { @@ -310,83 +306,101 @@ namespace VDownload.Views.Home // Set UI HomeOptionBarAndAddingPanelRow.Height = new GridLength(1, GridUnitType.Star); HomeTasksListRow.Height = new GridLength(0); - + + // Open adding panel HomePlaylistAddingPanel addingPanel = new HomePlaylistAddingPanel(playlistService); - addingPanel.PlaylistAddRequest += HomeVideoAddingPanel_PlayListAddRequest; + addingPanel.TasksAddingRequested += HomeTasksAddingRequest; HomeAddingPanel.Content = addingPanel; } } - // ADD PLAYLIST REQUEST FROM PLAYLIST ADDING PANEL - private void HomeVideoAddingPanel_PlayListAddRequest(object sender, PlaylistAddEventArgs e) + + // TASK ADDING REQUEST + private void HomeTasksAddingRequest(object sender, TasksAddingRequestedEventArgs e) { // Replace placeholder - HomeTasksListPlaceCurrent.Content = HomeTasksList; + HomeTasksListCurrentParent.Content = HomeTasksList; - // Uncheck video button - HomeOptionsBarAddPlaylistButton.IsChecked = false; - - // Create video tasks - foreach (var video in e.Videos) + // Uncheck button + switch (e.RequestSource) { - HomeTaskPanel taskPanel = new HomeTaskPanel(video.VideoService, video.MediaType, video.Stream, video.TrimStart, video.TrimEnd, video.Filename, video.Extension, video.Location, video.Schedule); + case TaskAddingRequestSource.Video: HomeOptionsBarAddVideoButton.IsChecked = false; break; + case TaskAddingRequestSource.Playlist: HomeOptionsBarAddPlaylistButton.IsChecked = false; break; + } + + // Create video tasks + foreach (TaskData taskData in e.TaskData) + { + HomeTaskPanel taskPanel = new HomeTaskPanel(taskData); taskPanel.TaskRemovingRequested += (s, a) => { // Remove task from tasks lists - TaskPanelsList.Remove(taskPanel); + TasksList.Remove(taskPanel); HomeTasksList.Children.Remove(taskPanel); - if (TaskPanelsList.Count <= 0) HomeTasksListPlaceCurrent.Content = HomeTasksListPlaceholder; + if (TasksList.Count <= 0) HomeTasksListCurrentParent.Content = HomeTasksListPlaceholder; }; // Add task to tasks lists HomeTasksList.Children.Add(taskPanel); - TaskPanelsList.Add(taskPanel); + TasksList.Add(taskPanel); } } - // ADDING BUTTONS UNCHECKED - private void HomeOptionsBarAddingButtons_Unchecked(object sender, RoutedEventArgs e) + // TASK ADDING CANCELLED + private void HomeSearchingCancelled() { // Cancel searching operations SearchingCancellationToken.Cancel(); SearchingCancellationToken = new CancellationTokenSource(); + // Set grid dimensions HomeOptionBarAndAddingPanelRow.Height = GridLength.Auto; HomeTasksListRow.Height = new GridLength(1, GridUnitType.Star); + // Clear panels HomeAddingPanel.Content = null; HomeOptionsBarAddingControl.Content = null; HomeOptionsBarSearchingStatusControl.Content = null; } + // ADDING BUTTONS UNCHECKED + private void HomeOptionsBarAddingButtons_Unchecked(object sender, RoutedEventArgs e) + { + HomeSearchingCancelled(); + } + + // DOWNLOAD ALL BUTTON CLICKED private async void HomeOptionsBarDownloadAllButton_Click(object sender, RoutedEventArgs e) { - HomeTaskPanel[] idleTasks = TaskPanelsList.Where((HomeTaskPanel video) => video.TaskStatus == Core.Enums.TaskStatus.Idle).ToArray(); + HomeTaskPanel[] idleTasks = TasksList.Where((HomeTaskPanel video) => video.Status == Core.Enums.TaskStatus.Idle).ToArray(); if (idleTasks.Count() > 0) { bool delay = (bool)Config.GetValue("delay_task_when_queued_task_starts_on_metered_network"); - ContentDialogResult dialogResult = await new ContentDialog + if (NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection) { - Title = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogTitle"), - Content = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogDescription"), - PrimaryButtonText = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogStartAndDelayText"), - SecondaryButtonText = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogStartWithoutDelayText"), - CloseButtonText = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogCancel"), - }.ShowAsync(); - switch (dialogResult) - { - case ContentDialogResult.Primary: delay = true; break; - case ContentDialogResult.Secondary: delay = false; break; - case ContentDialogResult.None: return; + ContentDialogResult dialogResult = await new ContentDialog + { + Title = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogTitle"), + Content = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogDescription"), + PrimaryButtonText = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogStartAndDelayText"), + SecondaryButtonText = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogStartWithoutDelayText"), + CloseButtonText = ResourceLoader.GetForCurrentView().GetString("HomeDownloadAllButtonMeteredConnectionDialogCancel"), + }.ShowAsync(); + switch (dialogResult) + { + case ContentDialogResult.Primary: delay = true; break; + case ContentDialogResult.Secondary: delay = false; break; + case ContentDialogResult.None: return; + } } foreach (HomeTaskPanel videoPanel in idleTasks) { - await Task.Delay(50); + await Task.Delay(10); #pragma warning disable CS4014 videoPanel.Start(delay); @@ -404,7 +418,7 @@ namespace VDownload.Views.Home // WAIT IN QUEUE public static async Task WaitInQueue(bool delayWhenOnMeteredConnection, CancellationToken token) { - while ((TaskPanelsList.Where((HomeTaskPanel video) => video.TaskStatus == Core.Enums.TaskStatus.InProgress).Count() >= (int)Config.GetValue("max_active_video_task") || (delayWhenOnMeteredConnection && NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection)) && !token.IsCancellationRequested) + while ((TasksList.Where((HomeTaskPanel task) => task.Status == Core.Enums.TaskStatus.InProgress).Count() >= (int)Config.GetValue("max_active_video_task") || (delayWhenOnMeteredConnection && NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection)) && !token.IsCancellationRequested) { await Task.Delay(100); } diff --git a/VDownload/Views/Home/HomeOptionsBarAddPlaylistControl.xaml b/VDownload/Views/Home/HomeOptionsBarAddPlaylistControl.xaml index fd58012..72af936 100644 --- a/VDownload/Views/Home/HomeOptionsBarAddPlaylistControl.xaml +++ b/VDownload/Views/Home/HomeOptionsBarAddPlaylistControl.xaml @@ -23,7 +23,7 @@ - +