diff --git a/VDownload.Core/Enums/DownloadTaskStatus.cs b/VDownload.Core/Enums/DownloadTaskStatus.cs new file mode 100644 index 0000000..0997584 --- /dev/null +++ b/VDownload.Core/Enums/DownloadTaskStatus.cs @@ -0,0 +1,14 @@ +namespace VDownload.Core.Enums +{ + public enum DownloadTaskStatus + { + Idle, + Scheduled, + Queued, + Downloading, + Processing, + Finalizing, + EndedSuccessfully, + EndedUnsuccessfully, + } +} diff --git a/VDownload.Core/Enums/DownloadTasksAddingRequestSource.cs b/VDownload.Core/Enums/DownloadTasksAddingRequestSource.cs new file mode 100644 index 0000000..4d86b43 --- /dev/null +++ b/VDownload.Core/Enums/DownloadTasksAddingRequestSource.cs @@ -0,0 +1,9 @@ +namespace VDownload.Core.Enums +{ + public enum DownloadTasksAddingRequestSource + { + Video, + Playlist, + Subscriptions, + } +} diff --git a/VDownload.Core/Enums/SubscriptionStatus.cs b/VDownload.Core/Enums/SubscriptionStatus.cs deleted file mode 100644 index 41745b0..0000000 --- a/VDownload.Core/Enums/SubscriptionStatus.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace VDownload.Core.Enums -{ - public enum SubscriptionStatus - { - Added, - Loaded, - Ready - } -} diff --git a/VDownload.Core/Enums/TaskAddingRequestSource.cs b/VDownload.Core/Enums/TaskAddingRequestSource.cs deleted file mode 100644 index b2058c1..0000000 --- a/VDownload.Core/Enums/TaskAddingRequestSource.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/Enums/TaskStatus.cs b/VDownload.Core/Enums/TaskStatus.cs deleted file mode 100644 index 419a012..0000000 --- a/VDownload.Core/Enums/TaskStatus.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace VDownload.Core.Enums -{ - public enum TaskStatus - { - Idle, - Scheduled, - Waiting, - InProgress - } -} diff --git a/VDownload.Core/EventArgs/DownloadTaskEndedSuccessfullyEventArgs.cs b/VDownload.Core/EventArgs/DownloadTaskEndedSuccessfullyEventArgs.cs new file mode 100644 index 0000000..9315429 --- /dev/null +++ b/VDownload.Core/EventArgs/DownloadTaskEndedSuccessfullyEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace VDownload.Core.EventArgs +{ + public class DownloadTaskEndedSuccessfullyEventArgs : System.EventArgs + { + #region CONSTRUCTORS + + public DownloadTaskEndedSuccessfullyEventArgs(TimeSpan elapsedTime) + { + ElapsedTime = elapsedTime; + } + + #endregion + + + + #region PROPERTIES + + public TimeSpan ElapsedTime { get; private set; } + + #endregion + } +} diff --git a/VDownload.Core/EventArgs/DownloadTaskEndedUnsuccessfullyEventArgs.cs b/VDownload.Core/EventArgs/DownloadTaskEndedUnsuccessfullyEventArgs.cs new file mode 100644 index 0000000..1d71fab --- /dev/null +++ b/VDownload.Core/EventArgs/DownloadTaskEndedUnsuccessfullyEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace VDownload.Core.EventArgs +{ + public class DownloadTaskEndedUnsuccessfullyEventArgs : System.EventArgs + { + #region CONSTRUCTORS + + public DownloadTaskEndedUnsuccessfullyEventArgs(Exception exception) + { + Exception = exception; + } + + #endregion + + + + #region PROPERTIES + + public Exception Exception { get; private set; } + + #endregion + } +} diff --git a/VDownload.Core/EventArgs/DownloadTaskScheduledEventArgs.cs b/VDownload.Core/EventArgs/DownloadTaskScheduledEventArgs.cs new file mode 100644 index 0000000..e5d4b7e --- /dev/null +++ b/VDownload.Core/EventArgs/DownloadTaskScheduledEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace VDownload.Core.EventArgs +{ + public class DownloadTaskScheduledEventArgs : System.EventArgs + { + #region CONSTRUCTORS + + public DownloadTaskScheduledEventArgs(DateTime scheduledFor) + { + ScheduledFor = scheduledFor; + } + + #endregion + + + + #region PROPERTIES + + public DateTime ScheduledFor { get; private set; } + + #endregion + } +} diff --git a/VDownload.Core/EventArgs/DownloadTasksAddingRequestedEventArgs.cs b/VDownload.Core/EventArgs/DownloadTasksAddingRequestedEventArgs.cs new file mode 100644 index 0000000..cadb7ff --- /dev/null +++ b/VDownload.Core/EventArgs/DownloadTasksAddingRequestedEventArgs.cs @@ -0,0 +1,27 @@ +using VDownload.Core.Enums; +using VDownload.Core.Structs; + +namespace VDownload.Core.EventArgs +{ + public class DownloadTasksAddingRequestedEventArgs : System.EventArgs + { + #region CONSTRUCTORS + + public DownloadTasksAddingRequestedEventArgs(DownloadTask[] downloadTasks, DownloadTasksAddingRequestSource requestSource) + { + DownloadTasks = downloadTasks; + RequestSource = requestSource; + } + + #endregion + + + + #region PROPERTIES + + public DownloadTask[] DownloadTasks { get; private set; } + public DownloadTasksAddingRequestSource RequestSource { get; private set; } + + #endregion + } +} diff --git a/VDownload.Core/EventArgs/PlaylistSearchSuccessedEventArgs.cs b/VDownload.Core/EventArgs/PlaylistSearchSuccessedEventArgs.cs index 8fb7175..12f6232 100644 --- a/VDownload.Core/EventArgs/PlaylistSearchSuccessedEventArgs.cs +++ b/VDownload.Core/EventArgs/PlaylistSearchSuccessedEventArgs.cs @@ -4,6 +4,21 @@ namespace VDownload.Core.EventArgs { public class PlaylistSearchSuccessedEventArgs : System.EventArgs { - public IPlaylist PlaylistService { get; set; } + #region CONSTRUCTORS + + public PlaylistSearchSuccessedEventArgs(IPlaylist playlist) + { + Playlist = playlist; + } + + #endregion + + + + #region PROPERTIES + + public IPlaylist Playlist { get; private set; } + + #endregion } } diff --git a/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs b/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs index d4dfffa..ddaccae 100644 --- a/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs +++ b/VDownload.Core/EventArgs/ProgressChangedEventArgs.cs @@ -16,8 +16,8 @@ #region PROPERTIES - public double Progress { get; set; } - public bool IsCompleted { get; set; } + public double Progress { get; private set; } + public bool IsCompleted { get; private set; } #endregion } diff --git a/VDownload.Core/EventArgs/SubscriptionLoadSuccessedEventArgs.cs b/VDownload.Core/EventArgs/SubscriptionLoadSuccessedEventArgs.cs new file mode 100644 index 0000000..aacad34 --- /dev/null +++ b/VDownload.Core/EventArgs/SubscriptionLoadSuccessedEventArgs.cs @@ -0,0 +1,24 @@ +using VDownload.Core.Interfaces; + +namespace VDownload.Core.EventArgs +{ + public class SubscriptionLoadSuccessedEventArgs : System.EventArgs + { + #region CONSTRUCTORS + + public SubscriptionLoadSuccessedEventArgs(IVideo[] videos) + { + Videos = videos; + } + + #endregion + + + + #region PROPERTIES + + public IVideo[] Videos { get; private set; } + + #endregion + } +} diff --git a/VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs b/VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs deleted file mode 100644 index 8da4118..0000000 --- a/VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -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/VideoSearchSuccessedEventArgs.cs b/VDownload.Core/EventArgs/VideoSearchSuccessedEventArgs.cs index bd1bb9a..8936ea5 100644 --- a/VDownload.Core/EventArgs/VideoSearchSuccessedEventArgs.cs +++ b/VDownload.Core/EventArgs/VideoSearchSuccessedEventArgs.cs @@ -4,6 +4,21 @@ namespace VDownload.Core.EventArgs { public class VideoSearchSuccessedEventArgs : System.EventArgs { + #region CONSTRUCTORS + + public VideoSearchSuccessedEventArgs(IVideo video) + { + Video = video; + } + + #endregion + + + + #region PROPERTIES + public IVideo Video { get; set; } + + #endregion } } diff --git a/VDownload.Core/Extensions/TimeSpanExtension.cs b/VDownload.Core/Extensions/TimeSpanExtension.cs new file mode 100644 index 0000000..6a83f57 --- /dev/null +++ b/VDownload.Core/Extensions/TimeSpanExtension.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Text; + +namespace VDownload.Core.Extensions +{ + public static class TimeSpanExtension + { + // To string (TH:)MM:SS + public static string ToStringOptTHBaseMMSS(this TimeSpan timeSpan, params TimeSpan[] formatBase) + { + StringBuilder formattedTimeSpan = new StringBuilder(); + + int maxTHLength = 0; + foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan })) + { + int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0; + if (THLength > maxTHLength) + { + maxTHLength = THLength; + } + } + if (maxTHLength > 0) + { + formattedTimeSpan.Append($"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:"); + } + + formattedTimeSpan.Append(maxTHLength == 0 ? $"{timeSpan.Minutes}:" : $"{timeSpan.Minutes:00}:"); + + formattedTimeSpan.Append($"{timeSpan.Seconds:00}"); + + return formattedTimeSpan.ToString(); + } + + // To string ((TH:)MM:)SS + public static string ToStringOptTHMMBaseSS(this TimeSpan timeSpan, params TimeSpan[] formatBase) + { + StringBuilder formattedTimeSpan = new StringBuilder(); + + int maxTHLength = 0; + foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan })) + { + int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0; + if (THLength > maxTHLength) + { + maxTHLength = THLength; + } + } + if (maxTHLength > 0) + { + formattedTimeSpan.Append($"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:"); + } + + bool MM = false; + if (Math.Floor(timeSpan.TotalMinutes) > 0 || maxTHLength > 0) + { + formattedTimeSpan.Append(maxTHLength > 0 ? $"{timeSpan.Minutes:00}:" : $"{timeSpan.Minutes}:"); + MM = true; + } + + formattedTimeSpan.Append(MM ? $"{timeSpan.Seconds:00}" : $"{timeSpan.Seconds}"); + + return formattedTimeSpan.ToString(); + } + } +} diff --git a/VDownload.Core/Interfaces/IPlaylist.cs b/VDownload.Core/Interfaces/IPlaylist.cs index 46e7c19..a69e88b 100644 --- a/VDownload.Core/Interfaces/IPlaylist.cs +++ b/VDownload.Core/Interfaces/IPlaylist.cs @@ -9,9 +9,8 @@ namespace VDownload.Core.Interfaces { #region PROPERTIES - // PLAYLIST PROPERTIES - string ID { get; } PlaylistSource Source { get; } + string ID { get; } Uri Url { get; } string Name { get; } IVideo[] Videos { get; } @@ -22,10 +21,8 @@ namespace VDownload.Core.Interfaces #region METHODS - // GET PLAYLIST METADATA Task GetMetadataAsync(CancellationToken cancellationToken = default); - // GET VIDEOS FROM PLAYLIST Task GetVideosAsync(CancellationToken cancellationToken = default); Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default); diff --git a/VDownload.Core/Interfaces/IVideo.cs b/VDownload.Core/Interfaces/IVideo.cs index f97b2dd..e57b638 100644 --- a/VDownload.Core/Interfaces/IVideo.cs +++ b/VDownload.Core/Interfaces/IVideo.cs @@ -12,11 +12,15 @@ namespace VDownload.Core.Interfaces { #region PROPERTIES - // VIDEO PROPERTIES VideoSource Source { get; } string ID { get; } Uri Url { get; } - Metadata Metadata { get; } + string Title { get; } + string Author { get; } + DateTime Date { get; } + TimeSpan Duration { get; } + long Views { get; } + Uri Thumbnail { get; } BaseStream[] BaseStreams { get; } #endregion @@ -25,20 +29,17 @@ namespace VDownload.Core.Interfaces #region METHODS - // GET VIDEO METADATA Task GetMetadataAsync(CancellationToken cancellationToken = default); - // GET VIDEO STREAMS Task GetStreamsAsync(CancellationToken cancellationToken = default); - // DOWNLOAD VIDEO - Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default); + Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default); #endregion - #region EVENT HANDLERS + #region EVENTS event EventHandler DownloadingProgressChanged; event EventHandler ProcessingProgressChanged; diff --git a/VDownload.Core/Services/Config.cs b/VDownload.Core/Services/Config.cs index ab1fb61..d276d4f 100644 --- a/VDownload.Core/Services/Config.cs +++ b/VDownload.Core/Services/Config.cs @@ -10,10 +10,6 @@ namespace VDownload.Core.Services { #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 }, @@ -45,37 +41,33 @@ namespace VDownload.Core.Services - #region METHODS + #region PUBLIC METHODS - // GET VALUE public static object GetValue(string key) { - return SettingsContainer.Values[key]; + return ApplicationData.Current.LocalSettings.Values[key]; } - // SET VALUE public static void SetValue(string key, object value) { - SettingsContainer.Values[key] = value; + ApplicationData.Current.LocalSettings.Values[key] = value; } - // SET DEFAULT public static void SetDefault() { foreach (KeyValuePair s in DefaultSettings) { - SettingsContainer.Values[s.Key] = s.Value; + ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value; } } - // REBUILD public static void Rebuild() { foreach (KeyValuePair s in DefaultSettings) { - if (!SettingsContainer.Values.ContainsKey(s.Key)) + if (!ApplicationData.Current.LocalSettings.Values.ContainsKey(s.Key)) { - SettingsContainer.Values[s.Key] = s.Value; + ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value; } } } diff --git a/VDownload.Core/Services/DownloadTask.cs b/VDownload.Core/Services/DownloadTask.cs new file mode 100644 index 0000000..4a38e61 --- /dev/null +++ b/VDownload.Core/Services/DownloadTask.cs @@ -0,0 +1,176 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using VDownload.Core.Enums; +using VDownload.Core.EventArgs; +using VDownload.Core.Interfaces; +using VDownload.Core.Services; +using Windows.ApplicationModel.ExtendedExecution; +using Windows.Storage; +using Windows.Storage.AccessCache; + +namespace VDownload.Core.Structs +{ + public class DownloadTask + { + #region CONSTRUCTORS + + public DownloadTask(string id, IVideo video, MediaType mediaType, BaseStream selectedStream, TrimData trim, OutputFile file, double schedule) + { + Id = id; + Video = video; + MediaType = mediaType; + SelectedStream = selectedStream; + Trim = trim; + File = file; + Schedule = schedule; + + Status = DownloadTaskStatus.Idle; + CancellationTokenSource = new CancellationTokenSource(); + } + + #endregion + + + + #region PROPERTIES + + public string Id { get; set; } + public IVideo Video { get; set; } + public MediaType MediaType { get; set; } + public BaseStream SelectedStream { get; set; } + public TrimData Trim { get; set; } + public OutputFile File { get; set; } + public double Schedule { get; set; } + public DownloadTaskStatus Status { get; private set; } + public CancellationTokenSource CancellationTokenSource { get; private set; } + + public DateTime ScheduledFor { get; private set; } + public double DownloadingProgress { get; private set; } + public double ProcessingProgress { get; private set; } + public TimeSpan ElapsedTime { get; private set; } + public Exception Exception { get; private set; } + + #endregion + + + + #region METHODS + + public async Task Run(bool delayWhenOnMeteredConnection) + { + StatusChanged.Invoke(this, System.EventArgs.Empty); + + CancellationTokenSource = new CancellationTokenSource(); + + if (Schedule > 0) + { + ScheduledFor = DateTime.Now.AddMinutes(Schedule); + Status = DownloadTaskStatus.Scheduled; + StatusChanged.Invoke(this, System.EventArgs.Empty); + while (DateTime.Now < ScheduledFor && !CancellationTokenSource.Token.IsCancellationRequested) await Task.Delay(100); + } + + Status = DownloadTaskStatus.Queued; + StatusChanged.Invoke(this, System.EventArgs.Empty); + await DownloadTasksCollectionManagement.WaitInQueue(delayWhenOnMeteredConnection, CancellationTokenSource.Token); + + if (!CancellationTokenSource.Token.IsCancellationRequested) + { + DownloadingProgress = 0; + Status = DownloadTaskStatus.Downloading; + StatusChanged.Invoke(this, System.EventArgs.Empty); + + StorageFolder tempFolder; + if ((bool)Config.GetValue("custom_temp_location") && StorageApplicationPermissions.FutureAccessList.ContainsItem("custom_temp_location")) + { + tempFolder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync("custom_temp_location"); + } + else + { + tempFolder = ApplicationData.Current.TemporaryFolder; + } + tempFolder = await tempFolder.CreateFolderAsync(Id); + + bool endedWithError = false; + + try + { + CancellationTokenSource.Token.ThrowIfCancellationRequested(); + + Stopwatch taskStopwatch = Stopwatch.StartNew(); + + ExtendedExecutionSession session = new ExtendedExecutionSession { Reason = ExtendedExecutionReason.Unspecified }; + await session.RequestExtensionAsync(); + CancellationTokenSource.Token.ThrowIfCancellationRequested(); + + Video.DownloadingProgressChanged += DownloadingProgressChanged; + Video.ProcessingProgressChanged += ProcessingProgressChanged; + StorageFile tempOutputFile = await Video.DownloadAndTranscodeAsync(tempFolder, SelectedStream, File.Extension, MediaType, Trim, CancellationTokenSource.Token); + + session.Dispose(); + + Status = DownloadTaskStatus.Finalizing; + StatusChanged.Invoke(this, System.EventArgs.Empty); + + StorageFile outputFile = await File.Create(); + + CancellationTokenSource.Token.ThrowIfCancellationRequested(); + await tempOutputFile.MoveAndReplaceAsync(outputFile); + + taskStopwatch.Stop(); + + ElapsedTime = taskStopwatch.Elapsed; + Status = DownloadTaskStatus.EndedSuccessfully; + StatusChanged.Invoke(this, System.EventArgs.Empty); + } + catch (Exception ex) + { + endedWithError = true; + Exception = ex; + Status = DownloadTaskStatus.EndedUnsuccessfully; + StatusChanged.Invoke(this, System.EventArgs.Empty); + } + finally + { + if (!endedWithError || (bool)Config.GetValue("delete_task_temp_when_ended_with_error")) + { + // Delete temporary files + await tempFolder.DeleteAsync(); + } + } + } + else + { + Exception = new OperationCanceledException(CancellationTokenSource.Token); + Status = DownloadTaskStatus.EndedUnsuccessfully; + StatusChanged.Invoke(this, System.EventArgs.Empty); + } + } + + private void DownloadingProgressChanged(object sender, ProgressChangedEventArgs e) + { + DownloadingProgress = e.Progress; + Status = DownloadTaskStatus.Downloading; + StatusChanged.Invoke(this, System.EventArgs.Empty); + } + + private void ProcessingProgressChanged(object sender, ProgressChangedEventArgs e) + { + ProcessingProgress = e.Progress; + Status = DownloadTaskStatus.Processing; + StatusChanged.Invoke(this, System.EventArgs.Empty); + } + + #endregion + + + + #region EVENT + + public event EventHandler StatusChanged; + + #endregion + } +} diff --git a/VDownload.Core/Services/DownloadTasksCollectionManagement.cs b/VDownload.Core/Services/DownloadTasksCollectionManagement.cs new file mode 100644 index 0000000..7f8ab4a --- /dev/null +++ b/VDownload.Core/Services/DownloadTasksCollectionManagement.cs @@ -0,0 +1,64 @@ +using Microsoft.Toolkit.Uwp.Connectivity; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using VDownload.Core.Structs; + +namespace VDownload.Core.Services +{ + public static class DownloadTasksCollectionManagement + { + #region PROPERTIES + + private static readonly Dictionary ChangeableDownloadTasksCollection = new Dictionary(); + public static readonly ReadOnlyDictionary DownloadTasksCollection = new ReadOnlyDictionary(ChangeableDownloadTasksCollection); + + #endregion + + + + #region PUBLIC METHODS + + public static string GenerateID() + { + char[] idChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); + int idLength = 10; + + string id; + do + { + id = ""; + while (id.Length < idLength) + { + id += idChars[new Random().Next(0, idChars.Length)]; + } + } while (DownloadTasksCollection.Keys.Contains(id)); + + return id; + } + + public static void Add(DownloadTask downloadTask, string id) + { + ChangeableDownloadTasksCollection[id] = downloadTask; + } + + public static void Remove(string id) + { + ChangeableDownloadTasksCollection.Remove(id); + } + + public static async Task WaitInQueue(bool delayWhenOnMeteredConnection, CancellationToken cancellationToken = default) + { + while ((ChangeableDownloadTasksCollection.Values.Where((DownloadTask task) => task.Status == Enums.DownloadTaskStatus.Downloading || task.Status == Enums.DownloadTaskStatus.Processing || task.Status == Enums.DownloadTaskStatus.Finalizing).Count() >= (int)Config.GetValue("max_active_video_task") || (delayWhenOnMeteredConnection && NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection)) && !cancellationToken.IsCancellationRequested) + { + await Task.Delay(100); + } + } + + #endregion + } +} diff --git a/VDownload.Core/Services/MediaProcessor.cs b/VDownload.Core/Services/MediaProcessor.cs index d75caef..966a350 100644 --- a/VDownload.Core/Services/MediaProcessor.cs +++ b/VDownload.Core/Services/MediaProcessor.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using VDownload.Core.Enums; +using VDownload.Core.Structs; using Windows.Foundation; using Windows.Media.Editing; using Windows.Media.MediaProperties; @@ -14,105 +15,83 @@ namespace VDownload.Core.Services { public class MediaProcessor { - #region STANDARD METHODS + #region PUBLIC METHODS - // SINGLE AUDIO & VIDEO FILE PROCESSING - public async Task Run(StorageFile mediaFile, MediaFileExtension extension, MediaType mediaType, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) + public async Task Run(StorageFile mediaFile, MediaFileExtension extension, MediaType mediaType, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default) { - // 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 = (MediaVideoProcessingAlgorithm)Config.GetValue("media_transcoding_algorithm"), }; - if (trimStart != null) mediaTranscoder.TrimStartTime = (TimeSpan)trimStart; - if (trimEnd != null) mediaTranscoder.TrimStopTime = (TimeSpan)trimEnd; + if (trim.Start != null) mediaTranscoder.TrimStartTime = trim.Start; + if (trim.End != null) mediaTranscoder.TrimStopTime = trim.End; - // Start transcoding operation using (IRandomAccessStream openedOutputFile = await outputFile.OpenAsync(FileAccessMode.ReadWrite)) { - // 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) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(percent)); })); cancellationToken.ThrowIfCancellationRequested(); - // Finalizing await openedOutputFile.FlushAsync(); transcodingTask.Close(); } - // 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, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) + public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default) { - // Invoke event at start cancellationToken.ThrowIfCancellationRequested(); ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); - // Init editor MediaComposition mediaEditor = new MediaComposition(); - // Add media files cancellationToken.ThrowIfCancellationRequested(); Task getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask(); Task getAudioFileTask = BackgroundAudioTrack.CreateFromFileAsync(audioFile).AsTask(); await Task.WhenAll(getVideoFileTask, getAudioFileTask); MediaClip videoElement = getVideoFileTask.Result; - if (trimStart != null) videoElement.TrimTimeFromStart = (TimeSpan)trimStart; - if (trimEnd != null) videoElement.TrimTimeFromEnd = (TimeSpan)trimEnd; + if (trim.Start != null) videoElement.TrimTimeFromStart = trim.Start; + if (trim.End != null) videoElement.TrimTimeFromEnd = trim.End; BackgroundAudioTrack audioElement = getAudioFileTask.Result; - if (trimStart != null) audioElement.TrimTimeFromStart = (TimeSpan)trimStart; - if (trimEnd != null) audioElement.TrimTimeFromEnd = (TimeSpan)trimEnd; + if (trim.Start != null) audioElement.TrimTimeFromStart = trim.Start; + if (trim.End != null) audioElement.TrimTimeFromEnd = trim.End; 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) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(progress)); }; cancellationToken.ThrowIfCancellationRequested(); await renderOperation.AsTask(cancellationToken); - // Invoke event at end ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true)); } - - // AUDIO FILE PROCESSING - public async Task Run(StorageFile audioFile, AudioFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) + public async Task Run(StorageFile audioFile, AudioFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default) { - await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, outputFile, trimStart, trimEnd, cancellationToken); + await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, outputFile, trim, cancellationToken); } - - // VIDEO FILE PROCESSING - public async Task Run(StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default) + public async Task Run(StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TrimData trim, CancellationToken cancellationToken = default) { - await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, outputFile, trimStart, trimEnd, cancellationToken); + await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, outputFile, trim, cancellationToken); } #endregion - #region LOCAL METHODS + #region PRIVATE METHODS - // GET ENCODING PROFILE private static async Task GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType) { - // Create profile object MediaEncodingProfile profile; - // Set extension switch (extension) { default: @@ -127,7 +106,6 @@ namespace VDownload.Core.Services case MediaFileExtension.WMA: profile = MediaEncodingProfile.CreateWma(AudioEncodingQuality.High); break; } - // Set video parameters if (mediaType != MediaType.OnlyAudio) { var videoData = await videoFile.Properties.GetVideoPropertiesAsync(); @@ -135,16 +113,12 @@ namespace VDownload.Core.Services 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(); @@ -152,7 +126,6 @@ namespace VDownload.Core.Services profile.SetAudioTracks(audioTracks.AsEnumerable()); } - // Return profile return profile; } private static async Task GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType) @@ -164,7 +137,7 @@ namespace VDownload.Core.Services - #region EVENT HANDLERS + #region EVENTS public event EventHandler ProgressChanged; diff --git a/VDownload.Core/Services/OutputFile.cs b/VDownload.Core/Services/OutputFile.cs new file mode 100644 index 0000000..9aa2850 --- /dev/null +++ b/VDownload.Core/Services/OutputFile.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VDownload.Core.Enums; +using Windows.Storage; + +namespace VDownload.Core.Services +{ + public class OutputFile + { + #region CONSTRUCTORS + + public OutputFile(string name, MediaFileExtension extension, StorageFolder location) + { + Name = name; + Extension = extension; + Location = location; + } + + public OutputFile(string name, MediaFileExtension extension) + { + Name = name; + Extension = extension; + Location = null; + } + + #endregion + + + + #region PROPERTIES + + public string Name { get; private set; } + public MediaFileExtension Extension { get; private set; } + public StorageFolder Location { get; private set; } + + #endregion + + + + #region PUBLIC METHODS + + public async Task Create() + { + string filename = $"{Name}.{Extension.ToString().ToLower()}"; + CreationCollisionOption collisionOption = (bool)Config.GetValue("replace_output_file_if_exists") ? CreationCollisionOption.ReplaceExisting : CreationCollisionOption.GenerateUniqueName; + return await(!(Location is null) ? Location.CreateFileAsync(filename, collisionOption) : DownloadsFolder.CreateFileAsync(filename, collisionOption)); + } + + public string GetPath() => $@"{(Location != null ? Location.Path : $@"{UserDataPaths.GetDefault().Downloads}\VDownload")}\{Name}.{Extension.ToString().ToLower()}"; + + #endregion + } +} diff --git a/VDownload.Core/Services/Subscription.cs b/VDownload.Core/Services/Subscription.cs index 0b65bc3..3b38836 100644 --- a/VDownload.Core/Services/Subscription.cs +++ b/VDownload.Core/Services/Subscription.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using VDownload.Core.Interfaces; @@ -32,15 +33,15 @@ namespace VDownload.Core.Services #region PUBLIC METHODS - public async Task GetNewVideosAsync() + public async Task GetNewVideosAsync(CancellationToken cancellationToken = default) { - await Playlist.GetVideosAsync(); + await Playlist.GetVideosAsync(cancellationToken); return GetUnsavedVideos(); } - public async Task GetNewVideosAndUpdateAsync() + public async Task GetNewVideosAndUpdateAsync(CancellationToken cancellationToken = default) { - await Playlist.GetVideosAsync(); + await Playlist.GetVideosAsync(cancellationToken); IVideo[] newVideos = GetUnsavedVideos(); SavedVideos = Playlist.Videos; return newVideos; diff --git a/VDownload.Core/Services/SubscriptionsCollectionManagement.cs b/VDownload.Core/Services/SubscriptionsCollectionManagement.cs index 8a9f54f..36470d0 100644 --- a/VDownload.Core/Services/SubscriptionsCollectionManagement.cs +++ b/VDownload.Core/Services/SubscriptionsCollectionManagement.cs @@ -27,7 +27,7 @@ namespace VDownload.Core.Services public static async Task<(Subscription Subscription, StorageFile SubscriptionFile)[]> GetSubscriptionsAsync() { List<(Subscription Subscription, StorageFile SubscriptionFile)> subscriptions = new List<(Subscription Subscription,StorageFile SubscriptionFile)> (); - StorageFolder subscriptionsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists); + StorageFolder subscriptionsFolder = await SubscriptionFolderLocation.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists); BinaryFormatter formatter = new BinaryFormatter(); foreach (StorageFile file in await subscriptionsFolder.GetFilesAsync()) { @@ -43,7 +43,7 @@ namespace VDownload.Core.Services public static async Task CreateSubscriptionFileAsync(Subscription subscription) { - StorageFolder subscriptionsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists); + StorageFolder subscriptionsFolder = await SubscriptionFolderLocation.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists); try { StorageFile subscriptionFile = await subscriptionsFolder.CreateFileAsync($"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}.{SubscriptionFileExtension}", CreationCollisionOption.FailIfExists); diff --git a/VDownload.Core/Services/TaskId.cs b/VDownload.Core/Services/TaskId.cs deleted file mode 100644 index 38e0267..0000000 --- a/VDownload.Core/Services/TaskId.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace VDownload.Core.Services -{ - public static class TaskId - { - #region CONSTANTS - - // ID SETTINGS - private static readonly char[] IDChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); - private static readonly int IDLength = 10; - - // IDS LIST - private static readonly List IDList = new List(); - - #endregion - - - - #region METHODS - - // GET TASK ID - public static string Get() - { - string id; - do - { - id = ""; - while (id.Length < IDLength) - { - id += IDChars[new Random().Next(0, IDChars.Length)]; - } - } while (IDList.Contains(id)); - IDList.Add(id); - return id; - } - - // DISPOSE TASK ID - public static void Dispose(string id) - { - IDList.Remove(id); - } - - #endregion - } -} diff --git a/VDownload.Core/Services/TimeSpanCustomFormat.cs b/VDownload.Core/Services/TimeSpanCustomFormat.cs deleted file mode 100644 index 85dad19..0000000 --- a/VDownload.Core/Services/TimeSpanCustomFormat.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; - -namespace VDownload.Core.Services -{ - public static class TimeSpanCustomFormat - { - // (TH:)MM:SS - public static string ToOptTHBaseMMSS(TimeSpan timeSpan, params TimeSpan[] formatBase) - { - string formattedTimeSpan = string.Empty; - - int maxTHLength = 0; - foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan })) - { - int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0; - if (THLength > maxTHLength) maxTHLength = THLength; - } - 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; - foreach (TimeSpan format in formatBase.Concat(new TimeSpan[] { timeSpan })) - { - int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0; - if (THLength > maxTHLength) maxTHLength = THLength; - } - formattedTimeSpan += $"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:"; - - bool MM = false; - if (Math.Floor(timeSpan.TotalMinutes) > 0 || maxTHLength > 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/Sources/AuthorizationData.cs b/VDownload.Core/Sources/AuthorizationData.cs new file mode 100644 index 0000000..7c0bbce --- /dev/null +++ b/VDownload.Core/Sources/AuthorizationData.cs @@ -0,0 +1,11 @@ +using Windows.Storage; + +namespace VDownload.Core.Sources +{ + internal static class AuthorizationData + { + internal static StorageFolder FolderLocation = ApplicationData.Current.LocalCacheFolder; + internal static string FolderName = "AuthData"; + internal static string FilesExtension = "auth"; + } +} diff --git a/VDownload.Core/Services/Sources/Source.cs b/VDownload.Core/Sources/Source.cs similarity index 100% rename from VDownload.Core/Services/Sources/Source.cs rename to VDownload.Core/Sources/Source.cs diff --git a/VDownload.Core/Services/Sources/Twitch/Channel.cs b/VDownload.Core/Sources/Twitch/Channel.cs similarity index 83% rename from VDownload.Core/Services/Sources/Twitch/Channel.cs rename to VDownload.Core/Sources/Twitch/Channel.cs index 95a2f0d..97e5612 100644 --- a/VDownload.Core/Services/Sources/Twitch/Channel.cs +++ b/VDownload.Core/Sources/Twitch/Channel.cs @@ -21,8 +21,9 @@ namespace VDownload.Core.Services.Sources.Twitch public Channel(string id) { - ID = id; Source = PlaylistSource.TwitchChannel; + ID = id; + Url = new Uri($"https://twitch.tv/{ID}"); } #endregion @@ -31,25 +32,23 @@ namespace VDownload.Core.Services.Sources.Twitch #region PROPERTIES - public string ID { get; private set; } public PlaylistSource Source { get; private set; } + public string ID { get; private set; } + private string UniqueID { get; set; } public Uri Url { get; private set; } public string Name { get; private set; } public IVideo[] Videos { get; private set; } - private string UniqueUserID { get; set; } #endregion - #region STANDARD METHODS + #region PUBLIC METHODS - // GET CHANNEL METADATA public async Task GetMetadataAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // Get response JToken response = null; using (WebClient client = await Client.Helix()) { @@ -59,52 +58,38 @@ namespace VDownload.Core.Services.Sources.Twitch else throw new MediaNotFoundException($"Twitch Channel (ID: {ID}) was not found"); } - // Create unified playlist url - Url = new Uri($"https://twitch.tv/{ID}"); - - // Set parameters - UniqueUserID = (string)response["id"]; + UniqueID = (string)response["id"]; Name = (string)response["display_name"]; } - // GET CHANNEL VIDEOS public async Task GetVideosAsync(CancellationToken cancellationToken = default) => await GetVideosAsync(0, cancellationToken); public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // Set page id string pagination = ""; - // Set array of videos List videos = new List(); - // Get all bool getAll = numberOfVideos == 0; - - // Get videos int count; JToken[] videosData; List getStreamsTasks = new List(); do { - // Set number of videos to get in this iteration count = numberOfVideos < 100 && !getAll ? numberOfVideos : 100; - // Get response JToken response = null; using (WebClient client = await Client.Helix()) { - client.QueryString.Add("user_id", UniqueUserID); + client.QueryString.Add("user_id", UniqueID); client.QueryString.Add("first", count.ToString()); client.QueryString.Add("after", pagination); response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")); } - // Set page id pagination = (string)response["pagination"]["cursor"]; - // Set videos data videosData = response["data"].ToArray(); foreach (JToken videoData in videosData) { @@ -118,10 +103,8 @@ namespace VDownload.Core.Services.Sources.Twitch } while ((getAll || numberOfVideos > 0) && count == videosData.Length); - // Wait for all getStreams tasks await Task.WhenAll(getStreamsTasks); - // Set videos Videos = videos.ToArray(); } diff --git a/VDownload.Core/Services/Sources/Twitch/Clip.cs b/VDownload.Core/Sources/Twitch/Clip.cs similarity index 73% rename from VDownload.Core/Services/Sources/Twitch/Clip.cs rename to VDownload.Core/Sources/Twitch/Clip.cs index 9539074..b95f17e 100644 --- a/VDownload.Core/Services/Sources/Twitch/Clip.cs +++ b/VDownload.Core/Sources/Twitch/Clip.cs @@ -25,8 +25,9 @@ namespace VDownload.Core.Services.Sources.Twitch public Clip(string id) { - ID = id; Source = VideoSource.TwitchClip; + ID = id; + Url = new Uri($"https://clips.twitch.tv/{ID}"); } #endregion @@ -38,21 +39,24 @@ namespace VDownload.Core.Services.Sources.Twitch public VideoSource Source { get; private set; } public string ID { get; private set; } public Uri Url { get; private set; } - public Metadata Metadata { get; private set; } + public string Title { get; private set; } + public string Author { get; private set; } + public DateTime Date { get; private set; } + public TimeSpan Duration { get; private set; } + public long Views { get; private set; } + public Uri Thumbnail { get; private set; } public BaseStream[] BaseStreams { get; private set; } #endregion - #region STANDARD METHODS + #region PUBLIC METHODS - // GET CLIP METADATA public async Task GetMetadataAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // Get response JToken response = null; using (WebClient client = await Client.Helix()) { @@ -62,39 +66,28 @@ namespace VDownload.Core.Services.Sources.Twitch else throw new MediaNotFoundException($"Twitch Clip (ID: {ID}) was not found"); } - // Create unified video url - Url = new Uri($"https://clips.twitch.tv/{ID}"); - - // 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"]), - }; + Title = (string)response["title"]; + Author = (string)response["broadcaster_name"]; + Date = Convert.ToDateTime(response["created_at"]); + Duration = TimeSpan.FromSeconds((double)response["duration"]); + Views = (long)response["view_count"]; + Thumbnail = new Uri((string)response["thumbnail_url"]); } public async Task GetStreamsAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // 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(); - // Parse response foreach (JToken streamData in response) { - // Create stream BaseStream stream = new BaseStream() { Url = new Uri((string)streamData["sourceURL"]), @@ -102,21 +95,17 @@ namespace VDownload.Core.Services.Sources.Twitch FrameRate = (int)streamData["frameRate"], }; - // Add stream streams.Add(stream); } - // Set streams BaseStreams = streams.ToArray(); } - public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default) { - // Invoke DownloadingStarted event cancellationToken.ThrowIfCancellationRequested(); DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); - // Get video GQL access token JToken videoAccessToken = null; using (WebClient client = Client.GQL()) { @@ -141,7 +130,7 @@ namespace VDownload.Core.Services.Sources.Twitch // Processing StorageFile outputFile = rawFile; - if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart != null || trimEnd != null) + if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trim.Start != null || trim.End != null) { cancellationToken.ThrowIfCancellationRequested(); outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}"); @@ -149,15 +138,9 @@ namespace VDownload.Core.Services.Sources.Twitch 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; + await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken); } - // Return output file return outputFile; } @@ -165,7 +148,7 @@ namespace VDownload.Core.Services.Sources.Twitch - #region EVENT HANDLERS + #region EVENTS public event EventHandler DownloadingProgressChanged; public event EventHandler ProcessingProgressChanged; diff --git a/VDownload.Core/Services/Sources/Twitch/Helpers/Auth.cs b/VDownload.Core/Sources/Twitch/Helpers/Authorization.cs similarity index 67% rename from VDownload.Core/Services/Sources/Twitch/Helpers/Auth.cs rename to VDownload.Core/Sources/Twitch/Helpers/Authorization.cs index 1ea85fa..6e9d480 100644 --- a/VDownload.Core/Services/Sources/Twitch/Helpers/Auth.cs +++ b/VDownload.Core/Sources/Twitch/Helpers/Authorization.cs @@ -4,11 +4,13 @@ using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; +using VDownload.Core.Sources; +using VDownload.Core.Structs; using Windows.Storage; namespace VDownload.Core.Services.Sources.Twitch.Helpers { - public static class Auth + public static class Authorization { #region CONSTANTS @@ -16,7 +18,6 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers public readonly static string GQLApiClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko"; 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[] { @@ -34,8 +35,8 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers { try { - StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.GetFolderAsync("AuthData"); - StorageFile authDataFile = await authDataFolder.GetFileAsync("Twitch.auth"); + StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName); + StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}"); return await FileIO.ReadTextAsync(authDataFile); } @@ -47,8 +48,8 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers public static async Task SaveAccessTokenAsync(string accessToken) { - StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync("AuthData", CreationCollisionOption.OpenIfExists); - StorageFile authDataFile = await authDataFolder.CreateFileAsync("Twitch.auth", CreationCollisionOption.ReplaceExisting); + StorageFolder authDataFolder = await AuthorizationData.FolderLocation.CreateFolderAsync(AuthorizationData.FolderName, CreationCollisionOption.OpenIfExists); + StorageFile authDataFile = await authDataFolder.CreateFileAsync($"Twitch.{AuthorizationData.FilesExtension}", CreationCollisionOption.ReplaceExisting); await FileIO.WriteTextAsync(authDataFile, accessToken); } @@ -57,15 +58,15 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers { try { - StorageFolder authDataFolder = await ApplicationData.Current.LocalCacheFolder.GetFolderAsync("AuthData"); - StorageFile authDataFile = await authDataFolder.GetFileAsync("Twitch.auth"); + StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName); + StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}"); await authDataFile.DeleteAsync(); } catch (FileNotFoundException) { } } - public static async Task<(bool IsValid, string Login, DateTime? ExpirationDate)> ValidateAccessTokenAsync(string accessToken) + public static async Task ValidateAccessTokenAsync(string accessToken) { WebClient client = new WebClient { Encoding = Encoding.UTF8 }; client.Headers.Add("Authorization", $"Bearer {accessToken}"); @@ -77,17 +78,20 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers string login = response["login"].ToString(); DateTime? expirationDate = DateTime.Now.AddSeconds(long.Parse(response["expires_in"].ToString())); - return (true, login, expirationDate); + return new TwitchAccessTokenValidationData(accessToken, true, login, expirationDate); } - catch (WebException wex) + catch (WebException ex) { - if (wex.Response != null) + if (ex.Response is null) { - JObject wexInfo = JObject.Parse(new StreamReader(wex.Response.GetResponseStream()).ReadToEnd()); - if ((int)wexInfo["status"] == 401) return (false, null, null); + throw; + } + else + { + JObject exInfo = JObject.Parse(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd()); + if ((int)exInfo["status"] == 401) return new TwitchAccessTokenValidationData(accessToken, false, null, null); else throw; } - else throw; } } diff --git a/VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs b/VDownload.Core/Sources/Twitch/Helpers/Client.cs similarity index 69% rename from VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs rename to VDownload.Core/Sources/Twitch/Helpers/Client.cs index e93e5bd..d461b6b 100644 --- a/VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs +++ b/VDownload.Core/Sources/Twitch/Helpers/Client.cs @@ -8,15 +8,15 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers { internal static async Task Helix() { - string accessToken = await Auth.ReadAccessTokenAsync(); + string accessToken = await Authorization.ReadAccessTokenAsync(); if (accessToken == null) throw new TwitchAccessTokenNotFoundException(); - var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken); + var twitchAccessTokenValidation = await Authorization.ValidateAccessTokenAsync(accessToken); if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException(); WebClient client = new WebClient(); client.Headers.Add("Authorization", $"Bearer {accessToken}"); - client.Headers.Add("Client-Id", Auth.ClientID); + client.Headers.Add("Client-Id", Authorization.ClientID); return client; } @@ -24,7 +24,7 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers internal static WebClient GQL() { WebClient client = new WebClient(); - client.Headers.Add("Client-Id", Auth.GQLApiClientID); + client.Headers.Add("Client-Id", Authorization.GQLApiClientID); return client; } diff --git a/VDownload.Core/Services/Sources/Twitch/Vod.cs b/VDownload.Core/Sources/Twitch/Vod.cs similarity index 73% rename from VDownload.Core/Services/Sources/Twitch/Vod.cs rename to VDownload.Core/Sources/Twitch/Vod.cs index 347e1ca..5525b96 100644 --- a/VDownload.Core/Services/Sources/Twitch/Vod.cs +++ b/VDownload.Core/Sources/Twitch/Vod.cs @@ -24,8 +24,9 @@ namespace VDownload.Core.Services.Sources.Twitch public Vod(string id) { - ID = id; Source = VideoSource.TwitchVod; + ID = id; + Url = new Uri($"https://www.twitch.tv/videos/{ID}"); } #endregion @@ -37,21 +38,24 @@ namespace VDownload.Core.Services.Sources.Twitch public VideoSource Source { get; private set; } public string ID { get; private set; } public Uri Url { get; private set; } - public Metadata Metadata { get; private set; } + public string Title { get; private set; } + public string Author { get; private set; } + public DateTime Date { get; private set; } + public TimeSpan Duration { get; private set; } + public long Views { get; private set; } + public Uri Thumbnail { get; private set; } public BaseStream[] BaseStreams { get; private set; } #endregion - #region STANDARD METHODS + #region PUBLIC METHODS - // GET VOD METADATA public async Task GetMetadataAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // Get response JToken response = null; using (WebClient client = await Client.Helix()) { @@ -69,57 +73,40 @@ namespace VDownload.Core.Services.Sources.Twitch } } - // Set parameters GetMetadataAsync(response); } internal void GetMetadataAsync(JToken response) { - // Create unified video url - Url = new Uri($"https://www.twitch.tv/videos/{ID}"); - - // 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")), - }; + 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) { cancellationToken.ThrowIfCancellationRequested(); - // 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(); - // 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 = streamDataL2Regex.Match(response[i + 1]); - // Create stream BaseStream stream = new BaseStream() { Url = new Uri(response[i + 2]), @@ -127,30 +114,22 @@ namespace VDownload.Core.Services.Sources.Twitch 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 BaseStreams = streams.ToArray(); } - // DOWNLOAD AND TRANSCODE VOD - public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) + public async Task DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default) { - // Invoke DownloadingStarted event - cancellationToken.ThrowIfCancellationRequested(); - DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0)); - - // Get video chunks cancellationToken.ThrowIfCancellationRequested(); + DownloadingProgressChanged.Invoke(this, new EventArgs.ProgressChangedEventArgs(0)); List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken); - // Changeable duration - TimeSpan duration = Metadata.Duration; + TimeSpan duration = Duration; // Passive trim - if ((bool)Config.GetValue("twitch_vod_passive_trim") && trimStart != TimeSpan.Zero && trimEnd != duration) (trimStart, trimEnd, duration) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Metadata.Duration); + if ((bool)Config.GetValue("twitch_vod_passive_trim") && trim.Start != TimeSpan.Zero && trim.End != duration) (trim, duration) = PassiveVideoTrim(chunksList, trim, Duration); // Download cancellationToken.ThrowIfCancellationRequested(); @@ -183,12 +162,7 @@ namespace VDownload.Core.Services.Sources.Twitch 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; + await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken); // Return output file return outputFile; @@ -198,30 +172,24 @@ namespace VDownload.Core.Services.Sources.Twitch - #region LOCAL METHODS + #region PRIVATE METHODS - // GET CHUNKS DATA FROM M3U8 PLAYLIST private static async Task> ExtractChunksFromM3U8Async(Uri streamUrl, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // 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), ""); + string chunkLocationPath = streamUrl.AbsoluteUri.Replace(Path.GetFileName(streamUrl.AbsoluteUri), ""); - // Pack data into dictionary foreach (Match chunk in chunkDataRegex.Matches(response)) { Uri chunkUrl = new Uri($"{chunkLocationPath}{chunk.Groups["filename"].Value}"); @@ -229,37 +197,30 @@ namespace VDownload.Core.Services.Sources.Twitch chunks.Add((chunkUrl, chunkDuration)); } - // Return chunks data return chunks; } - // PASSIVE TRIM - private static (TimeSpan NewTrimStart, TimeSpan NewTrimEnd, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration) + private static (TrimData Trim, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TrimData trim, TimeSpan duration) { - // Copy duration TimeSpan newDuration = duration; - // Trim at start - while (chunksList[0].ChunkDuration <= trimStart) + while (chunksList[0].ChunkDuration <= trim.Start) { - trimStart = trimStart.Subtract(chunksList[0].ChunkDuration); - trimEnd = trimEnd.Subtract(chunksList[0].ChunkDuration); + trim.Start = trim.Start.Subtract(chunksList[0].ChunkDuration); + trim.End = trim.End.Subtract(chunksList[0].ChunkDuration); newDuration = newDuration.Subtract(chunksList[0].ChunkDuration); chunksList.RemoveAt(0); } - // Trim at end - while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trimEnd)) + while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trim.End)) { newDuration = newDuration.Subtract(chunksList.Last().ChunkDuration); chunksList.RemoveAt(chunksList.Count - 1); } - // Return data - return (trimStart, trimEnd, newDuration); + return (trim, newDuration); } - // DOWNLOAD CHUNK private static async Task DownloadChunkAsync(Uri chunkUrl, CancellationToken cancellationToken = default) { int retriesCount = 0; @@ -285,12 +246,11 @@ namespace VDownload.Core.Services.Sources.Twitch } } - // 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)) + using (var stream = new FileStream(file.Path, FileMode.Append)) { stream.Write(chunk, 0, chunk.Length); stream.Close(); @@ -298,7 +258,6 @@ namespace VDownload.Core.Services.Sources.Twitch }); } - // PARSE DURATION private static TimeSpan ParseDuration(string duration) { char[] separators = { 'h', 'm', 's' }; @@ -316,7 +275,7 @@ namespace VDownload.Core.Services.Sources.Twitch - #region EVENT HANDLERS + #region EVENTS public event EventHandler DownloadingProgressChanged; public event EventHandler ProcessingProgressChanged; diff --git a/VDownload.Core/Structs/BaseStream.cs b/VDownload.Core/Structs/BaseStream.cs index 879811d..321204e 100644 --- a/VDownload.Core/Structs/BaseStream.cs +++ b/VDownload.Core/Structs/BaseStream.cs @@ -9,8 +9,20 @@ namespace VDownload.Core.Structs [Serializable] public struct BaseStream { + #region PROPERTIES + public Uri Url { get; set; } public int Height { get; set; } public int FrameRate { get; set; } + + #endregion + + + + #region METHODS + + public override string ToString() => $"{Height}p{(FrameRate > 0 ? FrameRate.ToString() : "N/A")}"; + + #endregion } } diff --git a/VDownload.Core/Structs/Metadata.cs b/VDownload.Core/Structs/Metadata.cs deleted file mode 100644 index bc81de4..0000000 --- a/VDownload.Core/Structs/Metadata.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace VDownload.Core.Structs -{ - [Serializable] - public struct 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/Structs/TaskData.cs b/VDownload.Core/Structs/TaskData.cs deleted file mode 100644 index a3da46d..0000000 --- a/VDownload.Core/Structs/TaskData.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using VDownload.Core.Enums; -using VDownload.Core.Interfaces; -using Windows.Storage; - -namespace VDownload.Core.Structs -{ - public struct TaskData - { - public IVideo VideoService { get; set; } - public TaskOptions TaskOptions { get; set; } - } -} diff --git a/VDownload.Core/Structs/TaskOptions.cs b/VDownload.Core/Structs/TaskOptions.cs deleted file mode 100644 index 8b83f51..0000000 --- a/VDownload.Core/Structs/TaskOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using VDownload.Core.Enums; -using Windows.Storage; - -namespace VDownload.Core.Structs -{ - public struct TaskOptions - { - public MediaType MediaType { get; set; } - public BaseStream Stream { get; set; } - public TimeSpan TrimStart { get; set; } - public TimeSpan TrimEnd { get; set; } - public string Filename { get; set; } - public MediaFileExtension Extension { get; set; } - public StorageFolder Location { get; set; } - public double Schedule { get; set; } - } -} diff --git a/VDownload.Core/Structs/TrimData.cs b/VDownload.Core/Structs/TrimData.cs new file mode 100644 index 0000000..2ba586f --- /dev/null +++ b/VDownload.Core/Structs/TrimData.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 TrimData + { + #region PROPERTIES + + public TimeSpan Start { get; set; } + public TimeSpan End { get; set; } + + #endregion + } +} diff --git a/VDownload.Core/Structs/TwitchAccessTokenValidationData.cs b/VDownload.Core/Structs/TwitchAccessTokenValidationData.cs new file mode 100644 index 0000000..b0e3ee9 --- /dev/null +++ b/VDownload.Core/Structs/TwitchAccessTokenValidationData.cs @@ -0,0 +1,32 @@ +using System; + +namespace VDownload.Core.Structs +{ + public struct TwitchAccessTokenValidationData + { + #region CONSTRUCTORS + + public TwitchAccessTokenValidationData(string accessToken, bool isValid, string login, DateTime? expirationDate) + { + AccessToken = accessToken; + IsValid = isValid; + Login = login; + ExpirationDate = expirationDate; + } + + public static TwitchAccessTokenValidationData Null = new TwitchAccessTokenValidationData(string.Empty, false, string.Empty, null); + + #endregion + + + + #region PROPERTIES + + public string AccessToken { get; private set; } + public bool IsValid { get; private set; } + public string Login { get; private set; } + public DateTime? ExpirationDate { get; private set; } + + #endregion + } +} diff --git a/VDownload.Core/VDownload.Core.csproj b/VDownload.Core/VDownload.Core.csproj index b3afa2d..5c1eb71 100644 --- a/VDownload.Core/VDownload.Core.csproj +++ b/VDownload.Core/VDownload.Core.csproj @@ -124,42 +124,50 @@ - - + - + + + + - + + + - + + + + - - - + - - - - - - - + + + + + + + 6.2.13 + + 7.1.2 + 13.0.1 diff --git a/VDownload/Assets/Icons/StateWaitingDark.svg b/VDownload/Assets/Icons/StateQueuedDark.svg similarity index 100% rename from VDownload/Assets/Icons/StateWaitingDark.svg rename to VDownload/Assets/Icons/StateQueuedDark.svg diff --git a/VDownload/Assets/Icons/StateWaitingLight.svg b/VDownload/Assets/Icons/StateQueuedLight.svg similarity index 100% rename from VDownload/Assets/Icons/StateWaitingLight.svg rename to VDownload/Assets/Icons/StateQueuedLight.svg diff --git a/VDownload/Views/Settings/SettingsMain.xaml b/VDownload/Controls/PlaceholderableStackPanel.xaml similarity index 56% rename from VDownload/Views/Settings/SettingsMain.xaml rename to VDownload/Controls/PlaceholderableStackPanel.xaml index 9a1c1f3..15c0593 100644 --- a/VDownload/Views/Settings/SettingsMain.xaml +++ b/VDownload/Controls/PlaceholderableStackPanel.xaml @@ -1,14 +1,13 @@ - + d:DesignHeight="300" + d:DesignWidth="400"> - - - - + + diff --git a/VDownload/Controls/PlaceholderableStackPanel.xaml.cs b/VDownload/Controls/PlaceholderableStackPanel.xaml.cs new file mode 100644 index 0000000..b736e9c --- /dev/null +++ b/VDownload/Controls/PlaceholderableStackPanel.xaml.cs @@ -0,0 +1,100 @@ +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; + +namespace VDownload.Controls +{ + public sealed partial class PlaceholderableStackPanel : UserControl + { + #region CONSTRUCTORS + + public PlaceholderableStackPanel() + { + InitializeComponent(); + + IsPlaceholderActive = true; + StackPanel.Children.Add(Placeholder); + StackPanel.VerticalAlignment = VerticalAlignment.Center; + } + + #endregion + + + + #region PROPERTIES + + private UIElement _Placeholder = new Grid(); + public UIElement Placeholder + { + get => _Placeholder; + set + { + _Placeholder = value; + if (IsPlaceholderActive) + { + StackPanel.Children.Clear(); + StackPanel.Children.Add(_Placeholder); + StackPanel.VerticalAlignment = VerticalAlignment.Center; + } + } + } + public double Spacing + { + get => StackPanel.Spacing; + set => StackPanel.Spacing = value; + } + + public bool IsPlaceholderActive { get; private set; } + + #endregion + + + + #region PUBLIC METHODS + + public void Add(UIElement item) + { + if (IsPlaceholderActive) + { + StackPanel.Children.Clear(); + IsPlaceholderActive = false; + StackPanel.VerticalAlignment = VerticalAlignment.Stretch; + } + StackPanel.Children.Add(item); + } + + public void Remove(UIElement item) + { + StackPanel.Children.Remove(item); + if (StackPanel.Children.Count == 0) + { + StackPanel.Children.Add(_Placeholder); + StackPanel.VerticalAlignment = VerticalAlignment.Center; + IsPlaceholderActive = true; + } + } + + public void Clear() + { + StackPanel.Children.Clear(); + StackPanel.Children.Add(_Placeholder); + StackPanel.VerticalAlignment = VerticalAlignment.Center; + IsPlaceholderActive = true; + } + + public UIElement[] GetAllItems() => IsPlaceholderActive ? StackPanel.Children.ToArray() : new UIElement[0]; + + #endregion + } +} diff --git a/VDownload/Resources/Icons.xaml b/VDownload/Resources/Icons.xaml index 9e237bd..eab70bc 100644 --- a/VDownload/Resources/Icons.xaml +++ b/VDownload/Resources/Icons.xaml @@ -14,7 +14,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/VDownload/Strings/en-US/DialogResources.resw b/VDownload/Strings/en-US/DialogResources.resw index 2cd07aa..33f7103 100644 --- a/VDownload/Strings/en-US/DialogResources.resw +++ b/VDownload/Strings/en-US/DialogResources.resw @@ -117,9 +117,84 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Cancel + OK + + You are on the metered connection now. You can delay tasks until the network changes. Do you want to start the tasks? + + + Yes (With delay) + + + Yes (Without delay) + + + Metered connection detected + + + You are on the metered connection now. You can delay task until the network changes. Do you want to start the task? + + + Yes (With delay) + + + Yes (Without delay) + + + Metered connection detected + + + Playlist search error + + + Unable to connect to servers. Check your internet connection. + + + To get information about Twitch playlists (Channels), you have to link your Twitch account with VDownload. Go to Sources page to sign in. + + + There is a problem with linked Twitch account. Check Twitch login status in Sources page. + + + Subscriptions loading error + + + Unable to connect to servers. Check your internet connection. + + + Subscribed playlist not found. Name: + + + To get information about Twitch videos and playlists, you have to link your Twitch account with VDownload. Go to Sources page to sign in. + + + There is a problem with linked Twitch account. Check Twitch login status in Sources page. + + + Video search error + + + Unable to connect to servers. Check your internet connection. + + + To get information about Twitch videos (VODs and Clips), you have to link your Twitch account with VDownload. Go to Sources page to sign in. + + + There is a problem with linked Twitch account. Check Twitch login status in Sources page. + + + Login to Twitch failed + + + Unable to connect to servers. Check your internet connection. + + + Unknown error. + Playlist adding error diff --git a/VDownload/Strings/en-US/Resources.resw b/VDownload/Strings/en-US/Resources.resw index aab47f9..0e91ebb 100644 --- a/VDownload/Strings/en-US/Resources.resw +++ b/VDownload/Strings/en-US/Resources.resw @@ -117,304 +117,211 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - OK + + Audio & Video - + + Only audio + + + Only video + + + Author + + + Enter regular expression + + + Date + + + Duration + + + Hidden + + + Filter + + + Restore + + + Removed + + + Title + + + Enter regular expression + + + Views + + Downloading options - - File options - - - File - - - Browse - - - Location - - + Media type - + Quality - - Number of minutes to start the task (after clicking downloading button). - - - Schedule - - - Task options - - + Trim - - No + + Location - - You are on the metered connection now. You can delay tasks until the network changes. Do you want to start the tasks? + + Browse - - Yes (With delay) + + File - - Yes (Without delay) + + File options - - Metered connection detected + + Task options - + + Number of minutes to start the task (after clicking downloading button). + + + Schedule + + + Click "Video/Playlist search" button to start + + + Internet connection error + + + downloading ended successfully + + + Video downloading ended successfully + + + an error occured + + + Video downloading ended unsuccessfully + + + Cancelled + + + Done + + + Downloading + + + An error occured + + + Finalizing + + + Idle + + + Processing + + + Queued + + + Scheduled + + Download all - + 90 - + Load subscriptions - + 120 - + Playlist search - + 95 - + - Twitch (Channel) Number of videos got from playlist The number in the numberbox indicades how many videos will be got from playlist. 0 = all. - + Supported sources - + Search - + Playlist URL - - Playlist search error - - - Unable to connect to servers. Check your internet connection. - - - To get information about Twitch playlists (Channels), you have to link your Twitch account with VDownload. Go to Sources page to sign in. - - - There is a problem with linked Twitch account. Check Twitch login status in Sources page. - - + Video search - + 90 - - - Twitch (VODs, Clips) - - - Supported sources - - - Search - - - Video URL - - - Video search error - - - Unable to connect to servers. Check your internet connection. - - - To get information about Twitch videos (VODs and Clips), you have to link your Twitch account with VDownload. Go to Sources page to sign in. - - - There is a problem with linked Twitch account. Check Twitch login status in Sources page. - - + Apply - - Browse - - - Location - - - Number of minutes to start the task (after clicking downloading button). - - - Schedule - - + Apply to all options - - Author - - - Enter regular expression - - - Date - - - Duration - - - Apply - - - Hidden - - - Filter - - - Restore - - - Removed - - - Title - - - Enter regular expression - - - Views - - - Downloading options - - - File options - - - File - - - Browse - - + Location - - Media type + + Browse - - Quality - - + Number of minutes to start the task (after clicking downloading button). - + Schedule - - Task options + + Loading new videos - - Trim + + - Twitch (VODs, Clips) - - Cancelled + + Supported sources - - Done + + Search - - Downloading + + Video URL - - An error occured + + About - - Finalizing - - - Idle - - - Processing - - - Scheduled - - - Queued - - - No - - - You are on the metered connection now. You can delay task until the network changes. Do you want to start the task? - - - Yes (With delay) - - - Yes (Without delay) - - - Metered connection detected - - - Click "Video/Playlist search" button to start - - + Home - + Sources - + Subscriptions - - Audio & Video - - - Only audio - - - Only video - - - downloading ended successfully - - - Video downloading ended successfully - - - an error occured - - - Video downloading ended unsuccessfully - - - Unable to connect to Twitch servers. Check your internet connection. - - - Unknown error - - - Login to Twitch failed - Sources @@ -424,12 +331,12 @@ The number in the numberbox indicades how many videos will be got from playlist. Twitch - - Your Twitch access token has expired. Please sign in. - Unable to connect to Twitch servers. Check your internet connection. + + Your Twitch access token is invalid (maybe expired?). Please sign in to get new access token. + Logged in as @@ -440,7 +347,7 @@ The number in the numberbox indicades how many videos will be got from playlist. Loading... - Log out + Sign out Sign in @@ -460,7 +367,16 @@ The number in the numberbox indicades how many videos will be got from playlist. Syncing... - - Internet connection error + + Subscriptions + + + Found videos + + + Load + + + Settings \ No newline at end of file diff --git a/VDownload/VDownload.csproj b/VDownload/VDownload.csproj index c2e64f1..0f18545 100644 --- a/VDownload/VDownload.csproj +++ b/VDownload/VDownload.csproj @@ -119,41 +119,50 @@ App.xaml + + PlaceholderableStackPanel.xaml + - - AboutMain.xaml + + MainPage.xaml - - HomeAddingVideoOptions.xaml + + SerialVideoAddingControl.xaml - - HomeOptionsBarPlaylistSearch.xaml + + VideoAddingOptionsControl.xaml - - HomeOptionsBarVideoSearch.xaml + + SubscriptionsLoadControl.xaml + + + PlaylistSearchControl.xaml + + + VideoSearchControl.xaml SettingControl.xaml - - HomeMain.xaml + + MainPage.xaml - - HomePlaylistAddingPanel.xaml + + SubscriptionsAddingPanel.xaml - - HomeSerialAddingVideoPanel.xaml + + PlaylistAddingPanel.xaml - - HomeTasksListPlaceholder.xaml + + SerialVideoAddingVideoControl.xaml - - HomeVideoAddingPanel.xaml + + VideoAddingPanel.xaml - - HomeTaskPanel.xaml + + DownloadTaskControl.xaml MainPage.xaml @@ -161,8 +170,8 @@ MainPage.xaml - - SettingsMain.xaml + + MainPage.xaml SubscriptionPanel.xaml @@ -214,8 +223,8 @@ - - + + @@ -281,6 +290,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -297,19 +310,27 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + + Designer + MSBuild:Compile + + MSBuild:Compile Designer - + + MSBuild:Compile + Designer + + Designer MSBuild:Compile @@ -317,27 +338,27 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile - + + MSBuild:Compile + Designer + + Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - - Designer - MSBuild:Compile - - + Designer MSBuild:Compile @@ -349,7 +370,7 @@ Designer MSBuild:Compile - + Designer MSBuild:Compile diff --git a/VDownload/Views/About/AboutMain.xaml b/VDownload/Views/About/MainPage.xaml similarity index 90% rename from VDownload/Views/About/AboutMain.xaml rename to VDownload/Views/About/MainPage.xaml index 7841cc4..0540ae9 100644 --- a/VDownload/Views/About/AboutMain.xaml +++ b/VDownload/Views/About/MainPage.xaml @@ -1,5 +1,5 @@  /// An empty page that can be used on its own or navigated to within a Frame. /// - public sealed partial class AboutMain : Page + public sealed partial class MainPage : Page { - public AboutMain() + public MainPage() { this.InitializeComponent(); } diff --git a/VDownload/Views/Home/Controls/HomeTaskPanel.xaml b/VDownload/Views/Home/Controls/DownloadTaskControl.xaml similarity index 79% rename from VDownload/Views/Home/Controls/HomeTaskPanel.xaml rename to VDownload/Views/Home/Controls/DownloadTaskControl.xaml index 29a5346..6534f11 100644 --- a/VDownload/Views/Home/Controls/HomeTaskPanel.xaml +++ b/VDownload/Views/Home/Controls/DownloadTaskControl.xaml @@ -1,5 +1,5 @@  - - + + @@ -60,12 +60,12 @@ - - - + + + - - - + + + diff --git a/VDownload/Views/Home/Controls/DownloadTaskControl.xaml.cs b/VDownload/Views/Home/Controls/DownloadTaskControl.xaml.cs new file mode 100644 index 0000000..c74096a --- /dev/null +++ b/VDownload/Views/Home/Controls/DownloadTaskControl.xaml.cs @@ -0,0 +1,248 @@ +using Microsoft.Toolkit.Uwp.Connectivity; +using Microsoft.Toolkit.Uwp.Notifications; +using System; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using VDownload.Core.Enums; +using VDownload.Core.Extensions; +using VDownload.Core.Services; +using VDownload.Core.Structs; +using Windows.ApplicationModel.ExtendedExecution; +using Windows.ApplicationModel.Resources; +using Windows.Storage; +using Windows.Storage.AccessCache; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; + +namespace VDownload.Views.Home.Controls +{ + public sealed partial class DownloadTaskControl : UserControl + { + #region CONSTANTS + + private static readonly ResourceDictionary IconsRes = new ResourceDictionary { Source = new Uri("ms-appx:///Resources/Icons.xaml") }; + private static readonly ResourceDictionary ImagesRes = new ResourceDictionary { Source = new Uri("ms-appx:///Resources/Images.xaml") }; + + #endregion + + + + #region CONSTRUCTORS + + public DownloadTaskControl(DownloadTask downloadTask) + { + this.InitializeComponent(); + + DownloadTask = downloadTask; + DownloadTask.StatusChanged += UpdateStatus; + + ThumbnailImage = DownloadTask.Video.Thumbnail != null ? new BitmapImage { UriSource = DownloadTask.Video.Thumbnail } : (BitmapImage)ImagesRes["UnknownThumbnailImage"]; + + SourceImage = new BitmapIcon { UriSource = new Uri($"ms-appx:///Assets/Sources/{DownloadTask.Video.GetType().Namespace.Split(".").Last()}.png"), ShowAsMonochrome = false }; + + TimeSpan newDuration = DownloadTask.Trim.End.Subtract(DownloadTask.Trim.Start); + StringBuilder durationBuilder = new StringBuilder(newDuration.ToStringOptTHBaseMMSS()); + if (DownloadTask.Video.Duration > newDuration) + { + durationBuilder.Append($" ({DownloadTask.Trim.Start.ToStringOptTHBaseMMSS(DownloadTask.Trim.End)} - {DownloadTask.Trim.End.ToStringOptTHBaseMMSS(DownloadTask.Trim.Start)})"); + } + Duration = durationBuilder.ToString(); + + StringBuilder mediaTypeQualityBuilder = new StringBuilder(ResourceLoader.GetForCurrentView().GetString($"Base_MediaType_{DownloadTask.MediaType}Text")); + if (DownloadTask.MediaType != MediaType.OnlyAudio) + { + mediaTypeQualityBuilder.Append($" ({DownloadTask.SelectedStream})"); + } + MediaTypeQuality = mediaTypeQualityBuilder.ToString(); + + File = DownloadTask.File.GetPath(); + + UpdateStatus(this, EventArgs.Empty); + } + + #endregion + + + + #region PROPERTIES + + private DownloadTask DownloadTask { get; set; } + + private ImageSource ThumbnailImage { get; set; } + private IconElement SourceImage { get; set; } + private string Title { get; set; } + private string Author { get; set; } + private string Duration { get; set; } + private string MediaTypeQuality { get; set; } + private string File { get; set; } + + #endregion + + + + #region EVENT HANDLERS + + private async void StartStopButton_Click(object sender, RoutedEventArgs e) + { + if (DownloadTask.Status != DownloadTaskStatus.Idle) + { + DownloadTask.CancellationTokenSource.Cancel(); + } + else + { + bool delay = (bool)Config.GetValue("delay_task_when_queued_task_starts_on_metered_network"); + if (NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection) + { + ContentDialogResult dialogResult = await new ContentDialog + { + Title = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Start_MeteredConnection_Title"), + Content = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Start_MeteredConnection_Content"), + PrimaryButtonText = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Start_MeteredConnection_StartWithDelayButtonText"), + SecondaryButtonText = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Start_MeteredConnection_StartWithoutDelayButtonText1"), + CloseButtonText = ResourceLoader.GetForCurrentView().GetString("Base_CancelButtonText"), + }.ShowAsync(); + switch (dialogResult) + { + case ContentDialogResult.Primary: delay = true; break; + case ContentDialogResult.Secondary: delay = false; break; + case ContentDialogResult.None: return; + } + } + await DownloadTask.Run(delay); + } + } + + private void RemoveButton_Click(object sender, RoutedEventArgs e) + { + if (DownloadTask.Status != DownloadTaskStatus.Idle) + { + DownloadTask.CancellationTokenSource.Cancel(); + } + RemovingRequested.Invoke(this, EventArgs.Empty); + } + + private async void SourceButton_Click(object sender, RoutedEventArgs e) + { + await Windows.System.Launcher.LaunchUriAsync(DownloadTask.Video.Url); + } + + private void UpdateStatus(object sender, EventArgs e) + { + if (DownloadTask.Status == DownloadTaskStatus.Idle || DownloadTask.Status == DownloadTaskStatus.EndedSuccessfully || DownloadTask.Status == DownloadTaskStatus.EndedUnsuccessfully) + { + StartStopButton.Icon = new SymbolIcon(Symbol.Download); + } + else + { + StartStopButton.Icon = new SymbolIcon(Symbol.Stop); + } + + if (DownloadTask.Status == DownloadTaskStatus.Scheduled) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateScheduledIcon"]; + StateText.Text = $"{ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Scheduled")} ({DownloadTask.ScheduledFor.ToString(CultureInfo.InstalledUICulture.DateTimeFormat.ShortDatePattern)} {DownloadTask.ScheduledFor.ToString(CultureInfo.InstalledUICulture.DateTimeFormat.ShortTimePattern)})"; + StateProgressBar.Visibility = Visibility.Collapsed; + } + else if (DownloadTask.Status == DownloadTaskStatus.Queued) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateQueuedIcon"]; + StateText.Text = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Queued"); + StateProgressBar.Visibility = Visibility.Visible; + StateProgressBar.IsIndeterminate = true; + } + else if (DownloadTask.Status == DownloadTaskStatus.Downloading) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateDownloadingIcon"]; + StateText.Text = $"{ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Downloading")} ({Math.Round(DownloadTask.DownloadingProgress)}%)"; + StateProgressBar.Visibility = Visibility.Visible; + StateProgressBar.IsIndeterminate = false; + StateProgressBar.Value = DownloadTask.DownloadingProgress; + } + else if (DownloadTask.Status == DownloadTaskStatus.Processing) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateProcessingIcon"]; + StateText.Text = $"{ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Processing")} ({Math.Round(DownloadTask.ProcessingProgress)}%)"; + StateProgressBar.Visibility = Visibility.Visible; + StateProgressBar.IsIndeterminate = false; + StateProgressBar.Value = DownloadTask.ProcessingProgress; + } + else if (DownloadTask.Status == DownloadTaskStatus.Finalizing) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateFinalizingIcon"]; + StateText.Text = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Finalizing"); + StateProgressBar.Visibility = Visibility.Visible; + StateProgressBar.IsIndeterminate = true; + } + else if (DownloadTask.Status == DownloadTaskStatus.EndedSuccessfully) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateDoneIcon"]; + StateText.Text = $"{ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Done")} ({DownloadTask.ElapsedTime.ToStringOptTHBaseMMSS()})"; + StateProgressBar.Visibility = Visibility.Collapsed; + + if ((bool)Config.GetValue("show_notification_when_task_ended_successfully")) + { + new ToastContentBuilder() + .AddText(ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Notification_EndedSuccessfully_Header")) + .AddText($"\"{Title}\" - {ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Notification_EndedSuccessfully_Description")}") + .Show(); + } + } + else if (DownloadTask.Status == DownloadTaskStatus.EndedUnsuccessfully) + { + if (DownloadTask.Exception is OperationCanceledException) + { + StateIcon.Source = (SvgImageSource)IconsRes["StateCancelledIcon"]; + StateText.Text = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Cancelled"); + StateProgressBar.Visibility = Visibility.Collapsed; + } + else + { + string errorInfo; + if (DownloadTask.Exception is WebException) + { + if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable) errorInfo = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Error_InternetNotAvailable"); + else throw DownloadTask.Exception; + } + else + { + throw DownloadTask.Exception; + } + StateIcon.Source = (SvgImageSource)IconsRes["StateErrorIcon"]; + StateText.Text = $"{ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Error")} ({errorInfo})"; + StateProgressBar.Visibility = Visibility.Collapsed; + + if ((bool)Config.GetValue("show_notification_when_task_ended_unsuccessfully")) + { + new ToastContentBuilder() + .AddText(ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Notification_EndedUnsuccessfully_Header")) + .AddText($"\"{Title}\" - {ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_Notification_EndedUnsuccessfully_Description")} ({errorInfo})") + .Show(); + } + } + } + else + { + StateIcon.Source = (SvgImageSource)IconsRes["StateIdleIcon"]; + StateText.Text = ResourceLoader.GetForCurrentView().GetString("Home_DownloadTaskControl_State_Idle"); + StateProgressBar.Visibility = Visibility.Collapsed; + } + } + + #endregion + + + + #region EVENT HANDLERS + + public event EventHandler RemovingRequested; + + #endregion + } +} diff --git a/VDownload/Views/Home/Controls/HomeAddingVideoOptions.xaml b/VDownload/Views/Home/Controls/HomeAddingVideoOptions.xaml deleted file mode 100644 index c71806a..0000000 --- a/VDownload/Views/Home/Controls/HomeAddingVideoOptions.xaml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -