Video adding and subscriptions finished
This commit is contained in:
14
VDownload.Core/Enums/DownloadTaskStatus.cs
Normal file
14
VDownload.Core/Enums/DownloadTaskStatus.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace VDownload.Core.Enums
|
||||
{
|
||||
public enum DownloadTaskStatus
|
||||
{
|
||||
Idle,
|
||||
Scheduled,
|
||||
Queued,
|
||||
Downloading,
|
||||
Processing,
|
||||
Finalizing,
|
||||
EndedSuccessfully,
|
||||
EndedUnsuccessfully,
|
||||
}
|
||||
}
|
||||
9
VDownload.Core/Enums/DownloadTasksAddingRequestSource.cs
Normal file
9
VDownload.Core/Enums/DownloadTasksAddingRequestSource.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace VDownload.Core.Enums
|
||||
{
|
||||
public enum DownloadTasksAddingRequestSource
|
||||
{
|
||||
Video,
|
||||
Playlist,
|
||||
Subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace VDownload.Core.Enums
|
||||
{
|
||||
public enum TaskStatus
|
||||
{
|
||||
Idle,
|
||||
Scheduled,
|
||||
Waiting,
|
||||
InProgress
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
24
VDownload.Core/EventArgs/DownloadTaskScheduledEventArgs.cs
Normal file
24
VDownload.Core/EventArgs/DownloadTaskScheduledEventArgs.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
66
VDownload.Core/Extensions/TimeSpanExtension.cs
Normal file
66
VDownload.Core/Extensions/TimeSpanExtension.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
|
||||
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region EVENT HANDLERS
|
||||
#region EVENTS
|
||||
|
||||
event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
|
||||
@@ -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<string, object> DefaultSettings = new Dictionary<string, object>()
|
||||
{
|
||||
{ "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<string, object> s in DefaultSettings)
|
||||
{
|
||||
SettingsContainer.Values[s.Key] = s.Value;
|
||||
ApplicationData.Current.LocalSettings.Values[s.Key] = s.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// REBUILD
|
||||
public static void Rebuild()
|
||||
{
|
||||
foreach (KeyValuePair<string, object> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
VDownload.Core/Services/DownloadTask.cs
Normal file
176
VDownload.Core/Services/DownloadTask.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
64
VDownload.Core/Services/DownloadTasksCollectionManagement.cs
Normal file
64
VDownload.Core/Services/DownloadTasksCollectionManagement.cs
Normal file
@@ -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<string, DownloadTask> ChangeableDownloadTasksCollection = new Dictionary<string, DownloadTask>();
|
||||
public static readonly ReadOnlyDictionary<string, DownloadTask> DownloadTasksCollection = new ReadOnlyDictionary<string, DownloadTask>(ChangeableDownloadTasksCollection);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public static string GenerateID()
|
||||
{
|
||||
char[] idChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
|
||||
int idLength = 10;
|
||||
|
||||
string id;
|
||||
do
|
||||
{
|
||||
id = "";
|
||||
while (id.Length < idLength)
|
||||
{
|
||||
id += idChars[new Random().Next(0, idChars.Length)];
|
||||
}
|
||||
} while (DownloadTasksCollection.Keys.Contains(id));
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public static void Add(DownloadTask downloadTask, string id)
|
||||
{
|
||||
ChangeableDownloadTasksCollection[id] = downloadTask;
|
||||
}
|
||||
|
||||
public static void Remove(string id)
|
||||
{
|
||||
ChangeableDownloadTasksCollection.Remove(id);
|
||||
}
|
||||
|
||||
public static async Task WaitInQueue(bool delayWhenOnMeteredConnection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
while ((ChangeableDownloadTasksCollection.Values.Where((DownloadTask task) => task.Status == Enums.DownloadTaskStatus.Downloading || task.Status == Enums.DownloadTaskStatus.Processing || task.Status == Enums.DownloadTaskStatus.Finalizing).Count() >= (int)Config.GetValue("max_active_video_task") || (delayWhenOnMeteredConnection && NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection)) && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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<double> transcodingTask = transcodingPreparated.TranscodeAsync();
|
||||
await transcodingTask.AsTask(cancellationToken, new Progress<double>((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<MediaClip> getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask();
|
||||
Task<BackgroundAudioTrack> 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<MediaEncodingProfile> 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<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType)
|
||||
@@ -164,7 +137,7 @@ namespace VDownload.Core.Services
|
||||
|
||||
|
||||
|
||||
#region EVENT HANDLERS
|
||||
#region EVENTS
|
||||
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProgressChanged;
|
||||
|
||||
|
||||
56
VDownload.Core/Services/OutputFile.cs
Normal file
56
VDownload.Core/Services/OutputFile.cs
Normal file
@@ -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<StorageFile> Create()
|
||||
{
|
||||
string filename = $"{Name}.{Extension.ToString().ToLower()}";
|
||||
CreationCollisionOption collisionOption = (bool)Config.GetValue("replace_output_file_if_exists") ? CreationCollisionOption.ReplaceExisting : CreationCollisionOption.GenerateUniqueName;
|
||||
return await(!(Location is null) ? Location.CreateFileAsync(filename, collisionOption) : DownloadsFolder.CreateFileAsync(filename, collisionOption));
|
||||
}
|
||||
|
||||
public string GetPath() => $@"{(Location != null ? Location.Path : $@"{UserDataPaths.GetDefault().Downloads}\VDownload")}\{Name}.{Extension.ToString().ToLower()}";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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<IVideo[]> GetNewVideosAsync()
|
||||
public async Task<IVideo[]> GetNewVideosAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Playlist.GetVideosAsync();
|
||||
await Playlist.GetVideosAsync(cancellationToken);
|
||||
return GetUnsavedVideos();
|
||||
}
|
||||
|
||||
public async Task<IVideo[]> GetNewVideosAndUpdateAsync()
|
||||
public async Task<IVideo[]> GetNewVideosAndUpdateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Playlist.GetVideosAsync();
|
||||
await Playlist.GetVideosAsync(cancellationToken);
|
||||
IVideo[] newVideos = GetUnsavedVideos();
|
||||
SavedVideos = Playlist.Videos;
|
||||
return newVideos;
|
||||
|
||||
@@ -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<StorageFile> 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);
|
||||
|
||||
@@ -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<string> IDList = new List<string>();
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
VDownload.Core/Sources/AuthorizationData.cs
Normal file
11
VDownload.Core/Sources/AuthorizationData.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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<Vod> videos = new List<Vod>();
|
||||
|
||||
// Get all
|
||||
bool getAll = numberOfVideos == 0;
|
||||
|
||||
// Get videos
|
||||
int count;
|
||||
JToken[] videosData;
|
||||
List<Task> getStreamsTasks = new List<Task>();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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<BaseStream> streams = new List<BaseStream>();
|
||||
|
||||
// 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<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
|
||||
public async Task<StorageFile> 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<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
@@ -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<TwitchAccessTokenValidationData> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ namespace VDownload.Core.Services.Sources.Twitch.Helpers
|
||||
{
|
||||
internal static async Task<WebClient> 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;
|
||||
}
|
||||
@@ -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<BaseStream> streams = new List<BaseStream>();
|
||||
|
||||
// Stream data line2 regular expression
|
||||
Regex streamDataL2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""\S+,\S+"",RESOLUTION=\d+x(?<height>\d+),VIDEO=""\w+""(,FRAME-RATE=(?<frame_rate>\d+.\d+))?");
|
||||
|
||||
// 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<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
|
||||
public async Task<StorageFile> 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<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> 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:(?<duration>\d+.\d+),\n(?<filename>\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<byte[]> 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<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
18
VDownload.Core/Structs/TrimData.cs
Normal file
18
VDownload.Core/Structs/TrimData.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
32
VDownload.Core/Structs/TwitchAccessTokenValidationData.cs
Normal file
32
VDownload.Core/Structs/TwitchAccessTokenValidationData.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -124,42 +124,50 @@
|
||||
<Compile Include="Enums\MediaFileExtension.cs" />
|
||||
<Compile Include="Enums\MediaType.cs" />
|
||||
<Compile Include="Enums\PlaylistSource.cs" />
|
||||
<Compile Include="Enums\SubscriptionStatus.cs" />
|
||||
<Compile Include="Enums\TaskAddingRequestSource.cs" />
|
||||
<Compile Include="Enums\DownloadTasksAddingRequestSource.cs" />
|
||||
<Compile Include="Enums\VideoFileExtension.cs" />
|
||||
<Compile Include="Enums\VideoSource.cs" />
|
||||
<Compile Include="Enums\TaskStatus.cs" />
|
||||
<Compile Include="Enums\DownloadTaskStatus.cs" />
|
||||
<Compile Include="EventArgs\DownloadTaskEndedUnsuccessfullyEventArgs.cs" />
|
||||
<Compile Include="EventArgs\DownloadTaskEndedSuccessfullyEventArgs.cs" />
|
||||
<Compile Include="EventArgs\DownloadTaskScheduledEventArgs.cs" />
|
||||
<Compile Include="EventArgs\ProgressChangedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\TasksAddingRequestedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\DownloadTasksAddingRequestedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\PlaylistSearchSuccessedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\SubscriptionLoadSuccessedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\VideoSearchSuccessedEventArgs.cs" />
|
||||
<Compile Include="Exceptions\MediaNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\SubscriptionExistsException.cs" />
|
||||
<Compile Include="Exceptions\TwitchAccessTokenNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\TwitchAccessTokenNotValidException.cs" />
|
||||
<Compile Include="Extensions\TimeSpanExtension.cs" />
|
||||
<Compile Include="Interfaces\IPlaylist.cs" />
|
||||
<Compile Include="Interfaces\IVideo.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Helpers\Client.cs" />
|
||||
<Compile Include="Services\DownloadTasksCollectionManagement.cs" />
|
||||
<Compile Include="Services\OutputFile.cs" />
|
||||
<Compile Include="Sources\AuthorizationData.cs" />
|
||||
<Compile Include="Sources\Twitch\Helpers\Client.cs" />
|
||||
<Compile Include="Services\Subscription.cs" />
|
||||
<Compile Include="Services\SubscriptionsCollectionManagement.cs" />
|
||||
<Compile Include="Services\TimeSpanCustomFormat.cs" />
|
||||
<Compile Include="Structs\BaseStream.cs" />
|
||||
<Compile Include="Structs\Metadata.cs" />
|
||||
<Compile Include="Structs\TaskData.cs" />
|
||||
<Compile Include="Services\DownloadTask.cs" />
|
||||
<Compile Include="Services\Config.cs" />
|
||||
<Compile Include="Services\MediaProcessor.cs" />
|
||||
<Compile Include="Services\Sources\Source.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Helpers\Auth.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Channel.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Clip.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Vod.cs" />
|
||||
<Compile Include="Services\TaskId.cs" />
|
||||
<Compile Include="Structs\TaskOptions.cs" />
|
||||
<Compile Include="Sources\Source.cs" />
|
||||
<Compile Include="Sources\Twitch\Helpers\Authorization.cs" />
|
||||
<Compile Include="Sources\Twitch\Channel.cs" />
|
||||
<Compile Include="Sources\Twitch\Clip.cs" />
|
||||
<Compile Include="Sources\Twitch\Vod.cs" />
|
||||
<Compile Include="Structs\TrimData.cs" />
|
||||
<Compile Include="Structs\TwitchAccessTokenValidationData.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||
<Version>6.2.13</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Connectivity">
|
||||
<Version>7.1.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>13.0.1</Version>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user