Projektdateien hinzufügen.
This commit is contained in:
26
VideoBrowser/Core/Commands.cs
Normal file
26
VideoBrowser/Core/Commands.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Commands" />
|
||||
/// </summary>
|
||||
public static class Commands
|
||||
{
|
||||
#region Constants
|
||||
|
||||
public const string Authentication = " -u {0} -p {1}";
|
||||
|
||||
public const string Download = " -o \"{0}\" --hls-prefer-native -f {1} {2}";
|
||||
|
||||
public const string GetJsonInfo = " -o \"{0}\\%(title)s\" --no-playlist --skip-download --restrict-filenames --write-info-json \"{1}\"{2}";
|
||||
|
||||
public const string GetJsonInfoBatch = " -o \"{0}\\%(id)s_%(title)s\" --no-playlist --skip-download --restrict-filenames --write-info-json {1}";
|
||||
|
||||
public const string TwoFactor = " -2 {0}";
|
||||
|
||||
public const string Update = " -U";
|
||||
|
||||
public const string Version = " --version";
|
||||
|
||||
#endregion Constants
|
||||
}
|
||||
}
|
||||
445
VideoBrowser/Core/DownloadOperation.cs
Normal file
445
VideoBrowser/Core/DownloadOperation.cs
Normal file
@@ -0,0 +1,445 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using VideoBrowser.Common;
|
||||
using VideoBrowser.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DownloadOperation" />
|
||||
/// </summary>
|
||||
public class DownloadOperation : Operation
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const string RegexAddToFilename = @"^(\w:.*\\.*)(\..*)$";
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly bool _combine;
|
||||
|
||||
private bool _downloadSuccessful;
|
||||
|
||||
private bool _processing;
|
||||
|
||||
private FileDownloader downloader;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DownloadOperation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="format">The format<see cref="VideoFormat"/></param>
|
||||
/// <param name="output">The output<see cref="string"/></param>
|
||||
public DownloadOperation(VideoFormat format,
|
||||
string output)
|
||||
: this(format)
|
||||
{
|
||||
this.Input = format.DownloadUrl;
|
||||
this.Output = output;
|
||||
this.Title = Path.GetFileName(this.Output);
|
||||
|
||||
downloader.Files.Add(new FileDownload(this.Output, this.Input));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DownloadOperation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="video">The video<see cref="VideoFormat"/></param>
|
||||
/// <param name="audio">The audio<see cref="VideoFormat"/></param>
|
||||
/// <param name="output">The output<see cref="string"/></param>
|
||||
public DownloadOperation(VideoFormat video,
|
||||
VideoFormat audio,
|
||||
string output)
|
||||
: this(video)
|
||||
{
|
||||
_combine = true;
|
||||
this.Input = $"{audio.DownloadUrl}|{video.DownloadUrl}";
|
||||
this.Output = output;
|
||||
this.FileSize += audio.FileSize;
|
||||
this.Title = Path.GetFileName(this.Output);
|
||||
|
||||
this.Formats.Add(audio);
|
||||
|
||||
var regex = new Regex(RegexAddToFilename);
|
||||
|
||||
downloader.Files.Add(new FileDownload(regex.Replace(this.Output, "$1_audio$2"),
|
||||
audio.DownloadUrl,
|
||||
true));
|
||||
downloader.Files.Add(new FileDownload(regex.Replace(this.Output, "$1_video$2"),
|
||||
video.DownloadUrl,
|
||||
true));
|
||||
|
||||
// Delete _audio and _video files in case they exists from a previous attempt
|
||||
FileHelper.DeleteFiles(downloader.Files[0].Path,
|
||||
downloader.Files[1].Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents a default instance of the <see cref="DownloadOperation"/> class from being created.
|
||||
/// </summary>
|
||||
private DownloadOperation()
|
||||
{
|
||||
downloader = new FileDownloader();
|
||||
// Attach events
|
||||
downloader.Canceled += downloader_Canceled;
|
||||
downloader.Completed += downloader_Completed;
|
||||
downloader.FileDownloadFailed += downloader_FileDownloadFailed;
|
||||
downloader.CalculatedTotalFileSize += downloader_CalculatedTotalFileSize;
|
||||
downloader.ProgressChanged += downloader_ProgressChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents a default instance of the <see cref="DownloadOperation"/> class from being created.
|
||||
/// </summary>
|
||||
/// <param name="format">The format<see cref="VideoFormat"/></param>
|
||||
private DownloadOperation(VideoFormat format)
|
||||
: this()
|
||||
{
|
||||
this.ReportsProgress = true;
|
||||
this.Duration = format.VideoInfo.Duration;
|
||||
this.FileSize = format.FileSize;
|
||||
this.Link = format.VideoInfo.Url;
|
||||
this.Thumbnail = format.VideoInfo.ThumbnailUrl;
|
||||
|
||||
this.Formats.Add(format);
|
||||
this.Video = format.VideoInfo;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Formats
|
||||
/// </summary>
|
||||
public List<VideoFormat> Formats { get; } = new List<VideoFormat>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Video
|
||||
/// </summary>
|
||||
public VideoInfo Video { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The CanOpen
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool CanOpen() => this.IsSuccessful;
|
||||
|
||||
/// <summary>
|
||||
/// The CanPause
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool CanPause()
|
||||
{
|
||||
// Only downloader can pause.
|
||||
return downloader?.CanPause == true && this.IsWorking;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The CanResume
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool CanResume()
|
||||
{
|
||||
// Only downloader can resume.
|
||||
return downloader?.CanResume == true && (this.IsPaused || this.IsQueued);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The CanStop
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool CanStop() => this.IsPaused || this.IsWorking || this.IsQueued;
|
||||
|
||||
/// <summary>
|
||||
/// The Dispose
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
|
||||
downloader?.Dispose();
|
||||
downloader = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Open
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool Open()
|
||||
{
|
||||
try
|
||||
{
|
||||
Controls.CefSharpBrowser.Helpers.ProcessHelper.Start(this.Output);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OpenContainingFolder
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool OpenContainingFolder()
|
||||
{
|
||||
try
|
||||
{
|
||||
Controls.CefSharpBrowser.Helpers.ProcessHelper.OpenContainedFolder(this.Output);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pause
|
||||
/// </summary>
|
||||
public override void Pause()
|
||||
{
|
||||
downloader.Pause();
|
||||
this.Status = OperationStatus.Paused;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Queue
|
||||
/// </summary>
|
||||
public override void Queue()
|
||||
{
|
||||
downloader.Pause();
|
||||
this.Status = OperationStatus.Queued;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Stop
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/></returns>
|
||||
public override bool Stop()
|
||||
{
|
||||
// Stop downloader if still running.
|
||||
if (downloader?.CanStop == true)
|
||||
{
|
||||
downloader.Stop();
|
||||
}
|
||||
|
||||
// Don't set status to canceled if already successful.
|
||||
if (!this.IsSuccessful)
|
||||
{
|
||||
this.Status = OperationStatus.Canceled;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ResumeInternal
|
||||
/// </summary>
|
||||
protected override void ResumeInternal()
|
||||
{
|
||||
downloader.Resume();
|
||||
this.Status = OperationStatus.Working;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The WorkerCompleted
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="RunWorkerCompletedEventArgs"/></param>
|
||||
protected override void WorkerCompleted(RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The WorkerDoWork
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="DoWorkEventArgs"/></param>
|
||||
protected override void WorkerDoWork(DoWorkEventArgs e)
|
||||
{
|
||||
downloader.Start();
|
||||
while (downloader?.IsBusy == true)
|
||||
{
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
|
||||
if (_combine && _downloadSuccessful)
|
||||
{
|
||||
var audio = downloader.Files[0].Path;
|
||||
var video = downloader.Files[1].Path;
|
||||
|
||||
this.ReportProgress(-1, new Dictionary<string, object>()
|
||||
{
|
||||
{ nameof(Progress), 0 }
|
||||
});
|
||||
this.ReportProgress(ProgressMax, null);
|
||||
|
||||
try
|
||||
{
|
||||
FFMpegResult<bool> result;
|
||||
|
||||
this.ReportProgress(-1, new Dictionary<string, object>()
|
||||
{
|
||||
{ nameof(ProgressText), "Combining..." }
|
||||
});
|
||||
|
||||
result = FFMpeg.Combine(video, audio, this.Output, delegate (int percentage)
|
||||
{
|
||||
// Combine progress
|
||||
this.ReportProgress(percentage, null);
|
||||
});
|
||||
|
||||
if (result.Value)
|
||||
{
|
||||
e.Result = OperationStatus.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Result = OperationStatus.Failed;
|
||||
this.ErrorsInternal.AddRange(result.Errors);
|
||||
}
|
||||
|
||||
// Cleanup the separate audio and video files
|
||||
FileHelper.DeleteFiles(audio, video);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.WriteException(ex);
|
||||
e.Result = OperationStatus.Failed;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Result = this.Status;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The WorkerProgressChanged
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="ProgressChangedEventArgs"/></param>
|
||||
protected override void WorkerProgressChanged(ProgressChangedEventArgs e)
|
||||
{
|
||||
if (e.UserState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Used to set multiple properties
|
||||
if (e.UserState is Dictionary<string, object>)
|
||||
{
|
||||
foreach (var pair in (e.UserState as Dictionary<string, object>))
|
||||
{
|
||||
this.GetType().GetProperty(pair.Key).SetValue(this, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_CalculatedTotalFileSize
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/></param>
|
||||
private void downloader_CalculatedTotalFileSize(object sender, EventArgs e)
|
||||
{
|
||||
this.FileSize = downloader.TotalSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_Canceled
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/></param>
|
||||
private void downloader_Canceled(object sender, EventArgs e)
|
||||
{
|
||||
if (this.Status == OperationStatus.Failed)
|
||||
{
|
||||
this.Status = OperationStatus.Canceled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_Completed
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/></param>
|
||||
private void downloader_Completed(object sender, EventArgs e)
|
||||
{
|
||||
// Set status to successful if no download(s) failed.
|
||||
if (this.Status != OperationStatus.Failed)
|
||||
{
|
||||
if (_combine)
|
||||
{
|
||||
_downloadSuccessful = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Status = OperationStatus.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_FileDownloadFailed
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="FileDownloadFailedEventArgs"/></param>
|
||||
private void downloader_FileDownloadFailed(object sender, FileDownloadFailedEventArgs e)
|
||||
{
|
||||
// If one or more files fail, whole operation failed. Might handle it more
|
||||
// elegantly in the future.
|
||||
this.Status = OperationStatus.Failed;
|
||||
downloader.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_ProgressChanged
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/></param>
|
||||
private void downloader_ProgressChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (_processing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_processing = true;
|
||||
|
||||
var speed = string.Format(new ByteFormatProvider(), "{0:s}", downloader.Speed);
|
||||
var longETA = FileHelper.GetETA(downloader.Speed, downloader.TotalSize, downloader.TotalProgress);
|
||||
var ETA = longETA == 0 ? "" : " " + TimeSpan.FromMilliseconds(longETA * 1000).ToString(@"hh\:mm\:ss");
|
||||
|
||||
this.ETA = ETA;
|
||||
this.Speed = speed;
|
||||
this.Progress = downloader.TotalProgress;
|
||||
this.ReportProgress((int)downloader.TotalPercentage(), null);
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
215
VideoBrowser/Core/DownloadQueueHandler.cs
Normal file
215
VideoBrowser/Core/DownloadQueueHandler.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="DownloadQueueHandler" />.
|
||||
/// </summary>
|
||||
public static class DownloadQueueHandler
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private static bool _stop;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DownloadingCount.
|
||||
/// </summary>
|
||||
private static int DownloadingCount => Queue.Count(o => IsDownloaderType(o) && o.CanPause());
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether LimitDownloads.
|
||||
/// </summary>
|
||||
public static bool LimitDownloads { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MaxDownloads.
|
||||
/// </summary>
|
||||
public static int MaxDownloads { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Queue.
|
||||
/// </summary>
|
||||
public static List<Operation> Queue { get; } = new List<Operation>();
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The Add.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation<see cref="Operation"/>.</param>
|
||||
public static void Add(Operation operation)
|
||||
{
|
||||
operation.Completed += Operation_Completed;
|
||||
operation.Resumed += Operation_Resumed;
|
||||
operation.StatusChanged += Operation_StatusChanged;
|
||||
|
||||
Queue.Add(operation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GetQueued.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Operation[]"/>.</returns>
|
||||
public static Operation[] GetQueued()
|
||||
{
|
||||
return Queue.Where(o => IsDownloaderType(o) && o.Status == OperationStatus.Queued).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GetWorking.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Operation[]"/>.</returns>
|
||||
public static Operation[] GetWorking()
|
||||
{
|
||||
return Queue.Where(o => IsDownloaderType(o) && o.Status == OperationStatus.Working).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Remove.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation<see cref="Operation"/>.</param>
|
||||
public static void Remove(Operation operation)
|
||||
{
|
||||
operation.Completed -= Operation_Completed;
|
||||
operation.Resumed -= Operation_Resumed;
|
||||
operation.StatusChanged -= Operation_StatusChanged;
|
||||
|
||||
Queue.Remove(operation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The StartWatching.
|
||||
/// </summary>
|
||||
/// <param name="maxDownloads">The maxDownloads<see cref="int"/>.</param>
|
||||
public static void StartWatching(int maxDownloads)
|
||||
{
|
||||
MaxDownloads = maxDownloads;
|
||||
MainLoop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Stop.
|
||||
/// </summary>
|
||||
public static void Stop()
|
||||
{
|
||||
_stop = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The IsDownloaderType.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation<see cref="Operation"/>.</param>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
private static bool IsDownloaderType(Operation operation)
|
||||
{
|
||||
return operation is DownloadOperation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The MainLoop.
|
||||
/// </summary>
|
||||
private static async void MainLoop()
|
||||
{
|
||||
while (!_stop)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
var queued = GetQueued();
|
||||
|
||||
// If downloads isn't limited, start all queued operations
|
||||
if (!LimitDownloads)
|
||||
{
|
||||
if (queued.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var operation in queued)
|
||||
{
|
||||
if (operation.HasStarted)
|
||||
{
|
||||
operation.ResumeQuiet();
|
||||
}
|
||||
else
|
||||
{
|
||||
operation.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (DownloadingCount < MaxDownloads)
|
||||
{
|
||||
// Number of operations to start
|
||||
var count = Math.Min(MaxDownloads - DownloadingCount, queued.Length);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (queued[i].HasStarted)
|
||||
{
|
||||
queued[i].ResumeQuiet();
|
||||
}
|
||||
else
|
||||
{
|
||||
queued[i].Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (DownloadingCount > MaxDownloads)
|
||||
{
|
||||
// Number of operations to pause
|
||||
var count = DownloadingCount - MaxDownloads;
|
||||
var working = GetWorking();
|
||||
|
||||
for (var i = DownloadingCount - 1; i > (MaxDownloads - 1); i--)
|
||||
{
|
||||
working[i].Queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Operation_Completed.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/>.</param>
|
||||
/// <param name="e">The e<see cref="OperationEventArgs"/>.</param>
|
||||
private static void Operation_Completed(object sender, OperationEventArgs e)
|
||||
{
|
||||
Remove((sender as Operation));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Operation_Resumed.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/>.</param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/>.</param>
|
||||
private static void Operation_Resumed(object sender, EventArgs e)
|
||||
{
|
||||
// User resumed operation, prioritize this operation over other queued
|
||||
var operation = sender as Operation;
|
||||
|
||||
// Move operation to top of queue, since pausing happens from the bottom.
|
||||
// I.E. this operation will only paused if absolutely necessary.
|
||||
Queue.Remove(operation);
|
||||
Queue.Insert(0, operation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Operation_StatusChanged.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/>.</param>
|
||||
/// <param name="e">The e<see cref="StatusChangedEventArgs"/>.</param>
|
||||
private static void Operation_StatusChanged(object sender, StatusChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
20
VideoBrowser/Core/EventHandlers.cs
Normal file
20
VideoBrowser/Core/EventHandlers.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
#region Delegates
|
||||
|
||||
/// <summary>
|
||||
/// The OperationEventHandler
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="OperationEventArgs"/></param>
|
||||
public delegate void OperationEventHandler(object sender, OperationEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// The StatusChangedEventHandler
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="StatusChangedEventArgs"/></param>
|
||||
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);
|
||||
|
||||
#endregion Delegates
|
||||
}
|
||||
86
VideoBrowser/Core/FileDownload.cs
Normal file
86
VideoBrowser/Core/FileDownload.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FileDownload" />
|
||||
/// </summary>
|
||||
public class FileDownload
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileDownload"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path<see cref="string"/></param>
|
||||
/// <param name="url">The url<see cref="string"/></param>
|
||||
public FileDownload(string path, string url)
|
||||
{
|
||||
this.Path = path;
|
||||
this.Url = url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileDownload"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path<see cref="string"/></param>
|
||||
/// <param name="url">The url<see cref="string"/></param>
|
||||
/// <param name="alwaysCleanupOnCancel">The alwaysCleanupOnCancel<see cref="bool"/></param>
|
||||
public FileDownload(string path, string url, bool alwaysCleanupOnCancel)
|
||||
: this(path, url)
|
||||
{
|
||||
this.AlwaysCleanupOnCancel = alwaysCleanupOnCancel;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether AlwaysCleanupOnCancel
|
||||
/// </summary>
|
||||
public bool AlwaysCleanupOnCancel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Directory
|
||||
/// </summary>
|
||||
public string Directory => System.IO.Path.GetDirectoryName(this.Path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Exception
|
||||
/// </summary>
|
||||
public Exception Exception { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether IsFinished
|
||||
/// </summary>
|
||||
public bool IsFinished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Name
|
||||
/// </summary>
|
||||
public string Name => System.IO.Path.GetFileName(this.Path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Path
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Progress
|
||||
/// </summary>
|
||||
public long Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TotalFileSize
|
||||
/// </summary>
|
||||
public long TotalFileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Url
|
||||
/// </summary>
|
||||
public string Url { get; set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
29
VideoBrowser/Core/FileDownloadEventArgs.cs
Normal file
29
VideoBrowser/Core/FileDownloadEventArgs.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FileDownloadEventArgs" />
|
||||
/// </summary>
|
||||
public class FileDownloadEventArgs : EventArgs
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileDownloadEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileDownload">The fileDownload<see cref="FileDownload"/></param>
|
||||
public FileDownloadEventArgs(FileDownload fileDownload) => this.FileDownload = fileDownload;
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FileDownload
|
||||
/// </summary>
|
||||
public FileDownload FileDownload { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
39
VideoBrowser/Core/FileDownloadFailedEventArgs.cs
Normal file
39
VideoBrowser/Core/FileDownloadFailedEventArgs.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FileDownloadFailedEventArgs" />
|
||||
/// </summary>
|
||||
public class FileDownloadFailedEventArgs : EventArgs
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileDownloadFailedEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception<see cref="Exception"/></param>
|
||||
/// <param name="fileDownload">The fileDownload<see cref="FileDownload"/></param>
|
||||
public FileDownloadFailedEventArgs(Exception exception, FileDownload fileDownload)
|
||||
{
|
||||
this.Exception = exception;
|
||||
this.FileDownload = fileDownload;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Exception
|
||||
/// </summary>
|
||||
public Exception Exception { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FileDownload
|
||||
/// </summary>
|
||||
public FileDownload FileDownload { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
688
VideoBrowser/Core/FileDownloader.cs
Normal file
688
VideoBrowser/Core/FileDownloader.cs
Normal file
@@ -0,0 +1,688 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using VideoBrowser.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FileDownloader" />
|
||||
/// </summary>
|
||||
public class FileDownloader : IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly bool _disposed = false;
|
||||
|
||||
private BackgroundWorker _downloader;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileDownloader"/> class.
|
||||
/// </summary>
|
||||
public FileDownloader()
|
||||
{
|
||||
_downloader = new BackgroundWorker()
|
||||
{
|
||||
WorkerReportsProgress = true,
|
||||
WorkerSupportsCancellation = true
|
||||
};
|
||||
|
||||
_downloader.DoWork += downloader_DoWork;
|
||||
_downloader.ProgressChanged += downloader_ProgressChanged;
|
||||
_downloader.RunWorkerCompleted += downloader_RunWorkerCompleted;
|
||||
|
||||
this.DeleteUnfinishedFilesOnCancel = true;
|
||||
this.Files = new List<FileDownload>();
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Delegates
|
||||
|
||||
/// <summary>
|
||||
/// The FileDownloadEventHandler
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="FileDownloadEventArgs"/></param>
|
||||
public delegate void FileDownloadEventHandler(object sender, FileDownloadEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// The FileDownloadFailedEventHandler
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="FileDownloadFailedEventArgs"/></param>
|
||||
public delegate void FileDownloadFailedEventHandler(object sender, FileDownloadFailedEventArgs e);
|
||||
|
||||
#endregion Delegates
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Defines the CalculatedTotalFileSize
|
||||
/// </summary>
|
||||
public event EventHandler CalculatedTotalFileSize;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Canceled
|
||||
/// </summary>
|
||||
public event EventHandler Canceled;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Completed
|
||||
/// </summary>
|
||||
public event EventHandler Completed;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the FileDownloadComplete
|
||||
/// </summary>
|
||||
public event FileDownloadEventHandler FileDownloadComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the FileDownloadFailed
|
||||
/// </summary>
|
||||
public event FileDownloadFailedEventHandler FileDownloadFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the FileDownloadSucceeded
|
||||
/// </summary>
|
||||
public event FileDownloadEventHandler FileDownloadSucceeded;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Paused
|
||||
/// </summary>
|
||||
public event EventHandler Paused;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the ProgressChanged
|
||||
/// </summary>
|
||||
public event EventHandler ProgressChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Resumed
|
||||
/// </summary>
|
||||
public event EventHandler Resumed;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Started
|
||||
/// </summary>
|
||||
public event EventHandler Started;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Stopped
|
||||
/// </summary>
|
||||
public event EventHandler Stopped;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// Defines the BackgroundEvents
|
||||
/// </summary>
|
||||
private enum BackgroundEvents
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the CalculatedTotalFileSize
|
||||
/// </summary>
|
||||
CalculatedTotalFileSize,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the FileDownloadComplete
|
||||
/// </summary>
|
||||
FileDownloadComplete,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the FileDownloadSucceeded
|
||||
/// </summary>
|
||||
FileDownloadSucceeded,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the ProgressChanged
|
||||
/// </summary>
|
||||
ProgressChanged
|
||||
}
|
||||
|
||||
#endregion Enums
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether CanPause
|
||||
/// </summary>
|
||||
public bool CanPause => this.IsBusy && !this.IsPaused && !_downloader.CancellationPending;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether CanResume
|
||||
/// </summary>
|
||||
public bool CanResume => this.IsBusy && this.IsPaused && !_downloader.CancellationPending;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether CanStart
|
||||
/// </summary>
|
||||
public bool CanStart => !this.IsBusy;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether CanStop
|
||||
/// </summary>
|
||||
public bool CanStop => this.IsBusy && !_downloader.CancellationPending;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CurrentFile
|
||||
/// </summary>
|
||||
public FileDownload CurrentFile { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether DeleteUnfinishedFilesOnCancel
|
||||
/// </summary>
|
||||
public bool DeleteUnfinishedFilesOnCancel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Files
|
||||
/// </summary>
|
||||
public List<FileDownload> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsBusy
|
||||
/// </summary>
|
||||
public bool IsBusy { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsPaused
|
||||
/// </summary>
|
||||
public bool IsPaused { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PackageSize
|
||||
/// </summary>
|
||||
public int PackageSize { get; set; } = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Speed
|
||||
/// </summary>
|
||||
public int Speed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TotalProgress
|
||||
/// </summary>
|
||||
public long TotalProgress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TotalSize
|
||||
/// </summary>
|
||||
public long TotalSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether WasCanceled
|
||||
/// </summary>
|
||||
public bool WasCanceled { get; set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pause
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
if (!this.IsBusy || this.IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.IsPaused = true;
|
||||
this.OnPaused();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Resume
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
if (!this.IsBusy || !this.IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.IsPaused = false;
|
||||
this.OnResumed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Start
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
this.IsBusy = true;
|
||||
this.WasCanceled = false;
|
||||
this.TotalProgress = 0;
|
||||
|
||||
_downloader.RunWorkerAsync();
|
||||
|
||||
this.OnStarted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Stop
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
this.IsBusy = false;
|
||||
this.IsPaused = false;
|
||||
this.WasCanceled = true;
|
||||
|
||||
_downloader.CancelAsync();
|
||||
|
||||
this.OnCanceled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The TotalPercentage
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="double"/></returns>
|
||||
public double TotalPercentage()
|
||||
{
|
||||
return Math.Round((double)this.TotalProgress / this.TotalSize * 100, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Dispose
|
||||
/// </summary>
|
||||
/// <param name="disposing">The disposing<see cref="bool"/></param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Free other state (managed objects)
|
||||
_downloader.Dispose();
|
||||
_downloader.DoWork -= downloader_DoWork;
|
||||
_downloader.ProgressChanged -= downloader_ProgressChanged;
|
||||
_downloader.RunWorkerCompleted -= downloader_RunWorkerCompleted;
|
||||
}
|
||||
|
||||
// Free your own state (unmanaged objects)
|
||||
// Set large fields to null
|
||||
this.Files = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnCalculatedTotalFileSize
|
||||
/// </summary>
|
||||
protected void OnCalculatedTotalFileSize() => this.CalculatedTotalFileSize?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnCanceled
|
||||
/// </summary>
|
||||
protected void OnCanceled() => this.Canceled?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnCompleted
|
||||
/// </summary>
|
||||
protected void OnCompleted() => this.Completed?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnFileDownloadComplete
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="FileDownloadEventArgs"/></param>
|
||||
protected void OnFileDownloadComplete(FileDownloadEventArgs e) => this.FileDownloadComplete?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The OnFileDownloadFailed
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception<see cref="Exception"/></param>
|
||||
/// <param name="fileDownload">The fileDownload<see cref="FileDownload"/></param>
|
||||
protected void OnFileDownloadFailed(Exception exception, FileDownload fileDownload) => this.FileDownloadFailed?.Invoke(this, new FileDownloadFailedEventArgs(exception, fileDownload));
|
||||
|
||||
/// <summary>
|
||||
/// The OnFileDownloadSucceeded
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="FileDownloadEventArgs"/></param>
|
||||
protected void OnFileDownloadSucceeded(FileDownloadEventArgs e) => this.FileDownloadSucceeded?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The OnPaused
|
||||
/// </summary>
|
||||
protected void OnPaused() => this.Paused?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnProgressChanged
|
||||
/// </summary>
|
||||
protected void OnProgressChanged() => this.ProgressChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnResumed
|
||||
/// </summary>
|
||||
protected void OnResumed() => this.Resumed?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnStarted
|
||||
/// </summary>
|
||||
protected void OnStarted() => this.Started?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The OnStopped
|
||||
/// </summary>
|
||||
protected void OnStopped() => this.Stopped?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The CalculateTotalFileSize
|
||||
/// </summary>
|
||||
private void CalculateTotalFileSize()
|
||||
{
|
||||
this.TotalSize = 0;
|
||||
|
||||
foreach (var file in this.Files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var webReq = WebRequest.Create(file.Url);
|
||||
var webResp = webReq.GetResponse();
|
||||
|
||||
this.TotalSize += webResp.ContentLength;
|
||||
|
||||
webResp.Close();
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
_downloader.ReportProgress(-1, BackgroundEvents.CalculatedTotalFileSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The CleanupFiles
|
||||
/// </summary>
|
||||
private void CleanupFiles()
|
||||
{
|
||||
if (this.Files == null)
|
||||
{
|
||||
Logger.Info("Clean up files is canceled, Files property is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
var files = this.Files;
|
||||
new Thread(delegate ()
|
||||
{
|
||||
var dict = new Dictionary<string, int>();
|
||||
var keys = new List<string>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (file.AlwaysCleanupOnCancel || !file.IsFinished)
|
||||
{
|
||||
dict.Add(file.Path, 0);
|
||||
keys.Add(file.Path);
|
||||
}
|
||||
}
|
||||
|
||||
while (dict.Count > 0)
|
||||
{
|
||||
foreach (string key in keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(key))
|
||||
{
|
||||
File.Delete(key);
|
||||
}
|
||||
|
||||
// Remove file from dictionary since it either got deleted
|
||||
// or it doesn't exist anymore.
|
||||
dict.Remove(key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (dict[key] == 10)
|
||||
{
|
||||
dict.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
dict[key]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
}).Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_DoWork
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="DoWorkEventArgs"/></param>
|
||||
private void downloader_DoWork(object sender, DoWorkEventArgs e)
|
||||
{
|
||||
this.CalculateTotalFileSize();
|
||||
foreach (var file in this.Files)
|
||||
{
|
||||
this.CurrentFile = file;
|
||||
if (!Directory.Exists(file.Directory))
|
||||
{
|
||||
Directory.CreateDirectory(file.Directory);
|
||||
}
|
||||
|
||||
this.DownloadFile();
|
||||
|
||||
this.RaiseEventFromBackground(BackgroundEvents.FileDownloadComplete,
|
||||
new FileDownloadEventArgs(file));
|
||||
|
||||
if (_downloader.CancellationPending)
|
||||
{
|
||||
this.CleanupFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_ProgressChanged
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="ProgressChangedEventArgs"/></param>
|
||||
private void downloader_ProgressChanged(object sender, ProgressChangedEventArgs e)
|
||||
{
|
||||
if (e.UserState is Exception)
|
||||
{
|
||||
this.CurrentFile.Exception = e.UserState as Exception;
|
||||
this.OnFileDownloadFailed(e.UserState as Exception, this.CurrentFile);
|
||||
}
|
||||
else if (e.UserState is object[])
|
||||
{
|
||||
var obj = (object[])e.UserState;
|
||||
if (obj[0] is BackgroundEvents)
|
||||
{
|
||||
this.RaiseEvent((BackgroundEvents)obj[0], (EventArgs)obj[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The downloader_RunWorkerCompleted
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="RunWorkerCompletedEventArgs"/></param>
|
||||
private void downloader_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
this.IsBusy = this.IsPaused = false;
|
||||
if (!this.WasCanceled)
|
||||
{
|
||||
this.OnCompleted();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.OnCanceled();
|
||||
}
|
||||
|
||||
this.OnStopped();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The DownloadFile
|
||||
/// </summary>
|
||||
private void DownloadFile()
|
||||
{
|
||||
long totalSize = 0;
|
||||
|
||||
var readBytes = new byte[this.PackageSize];
|
||||
int currentPackageSize;
|
||||
var speedTimer = new Stopwatch();
|
||||
Exception exception = null;
|
||||
|
||||
long existLen = 0;
|
||||
|
||||
if (File.Exists(this.CurrentFile.Path))
|
||||
{
|
||||
existLen = new FileInfo(this.CurrentFile.Path).Length;
|
||||
}
|
||||
|
||||
FileStream writer = existLen > 0
|
||||
? new FileStream(this.CurrentFile.Path, FileMode.Append, FileAccess.Write)
|
||||
: new FileStream(this.CurrentFile.Path, FileMode.Create, FileAccess.Write);
|
||||
HttpWebRequest webReq;
|
||||
HttpWebResponse webResp = null;
|
||||
|
||||
try
|
||||
{
|
||||
webReq = (HttpWebRequest)WebRequest.Create(this.CurrentFile.Url);
|
||||
webReq.Method = "HEAD";
|
||||
webResp = (HttpWebResponse)webReq.GetResponse();
|
||||
|
||||
if (webResp.ContentLength == existLen)
|
||||
{
|
||||
totalSize = existLen;
|
||||
}
|
||||
else
|
||||
{
|
||||
webResp.Close();
|
||||
webReq = (HttpWebRequest)WebRequest.Create(this.CurrentFile.Url);
|
||||
webReq.AddRange(existLen);
|
||||
webResp = (HttpWebResponse)webReq.GetResponse();
|
||||
totalSize = existLen + webResp.ContentLength;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
webResp?.Close();
|
||||
webResp?.Dispose();
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
this.CurrentFile.TotalFileSize = totalSize;
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
_downloader.ReportProgress(0, exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.CurrentFile.Progress = existLen;
|
||||
this.TotalProgress += existLen;
|
||||
|
||||
long prevSize = 0;
|
||||
var speedInterval = 100;
|
||||
var stream = webResp.GetResponseStream();
|
||||
|
||||
while (this.CurrentFile.Progress < totalSize && !_downloader.CancellationPending)
|
||||
{
|
||||
while (this.IsPaused)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
speedTimer.Start();
|
||||
currentPackageSize = stream.Read(readBytes, 0, this.PackageSize);
|
||||
|
||||
this.CurrentFile.Progress += currentPackageSize;
|
||||
this.TotalProgress += currentPackageSize;
|
||||
|
||||
// Raise ProgressChanged event
|
||||
this.RaiseEventFromBackground(BackgroundEvents.ProgressChanged, EventArgs.Empty);
|
||||
|
||||
writer.Write(readBytes, 0, currentPackageSize);
|
||||
|
||||
if (speedTimer.Elapsed.TotalMilliseconds >= speedInterval)
|
||||
{
|
||||
var downloadedBytes = writer.Length - prevSize;
|
||||
prevSize = writer.Length;
|
||||
|
||||
this.Speed = (int)downloadedBytes * (speedInterval == 100 ? 10 : 1);
|
||||
|
||||
// Only update speed once a second after initial update
|
||||
speedInterval = 1000;
|
||||
|
||||
speedTimer.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
speedTimer.Stop();
|
||||
stream.Close();
|
||||
writer.Close();
|
||||
webResp.Close();
|
||||
webResp.Dispose();
|
||||
|
||||
if (!_downloader.CancellationPending)
|
||||
{
|
||||
this.CurrentFile.IsFinished = true;
|
||||
this.RaiseEventFromBackground(BackgroundEvents.FileDownloadSucceeded,
|
||||
new FileDownloadEventArgs(this.CurrentFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The RaiseEvent
|
||||
/// </summary>
|
||||
/// <param name="evt">The evt<see cref="BackgroundEvents"/></param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/></param>
|
||||
private void RaiseEvent(BackgroundEvents evt, EventArgs e)
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case BackgroundEvents.CalculatedTotalFileSize:
|
||||
this.OnCalculatedTotalFileSize();
|
||||
break;
|
||||
|
||||
case BackgroundEvents.FileDownloadComplete:
|
||||
this.OnFileDownloadComplete((FileDownloadEventArgs)e);
|
||||
break;
|
||||
|
||||
case BackgroundEvents.FileDownloadSucceeded:
|
||||
this.OnFileDownloadSucceeded((FileDownloadEventArgs)e);
|
||||
break;
|
||||
|
||||
case BackgroundEvents.ProgressChanged:
|
||||
this.OnProgressChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The RaiseEventFromBackground
|
||||
/// </summary>
|
||||
/// <param name="evt">The evt<see cref="BackgroundEvents"/></param>
|
||||
/// <param name="e">The e<see cref="EventArgs"/></param>
|
||||
private void RaiseEventFromBackground(BackgroundEvents evt, EventArgs e) => _downloader.ReportProgress(-1, new object[] { evt, e });
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
43
VideoBrowser/Core/FileSizeUpdateEventArgs.cs
Normal file
43
VideoBrowser/Core/FileSizeUpdateEventArgs.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
#region Delegates
|
||||
|
||||
/// <summary>
|
||||
/// The FileSizeUpdateHandler
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="FileSizeUpdateEventArgs"/></param>
|
||||
public delegate void FileSizeUpdateHandler(object sender, FileSizeUpdateEventArgs e);
|
||||
|
||||
#endregion Delegates
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="FileSizeUpdateEventArgs" />
|
||||
/// </summary>
|
||||
public class FileSizeUpdateEventArgs : EventArgs
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileSizeUpdateEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="videoFormat">The videoFormat<see cref="VideoFormat"/></param>
|
||||
public FileSizeUpdateEventArgs(VideoFormat videoFormat)
|
||||
{
|
||||
this.VideoFormat = videoFormat;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the VideoFormat
|
||||
/// </summary>
|
||||
public VideoFormat VideoFormat { get; set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
30
VideoBrowser/Core/NoneUrlHandler.cs
Normal file
30
VideoBrowser/Core/NoneUrlHandler.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="NoneUrlHandler" />
|
||||
/// </summary>
|
||||
public class NoneUrlHandler : UrlHandlerBase
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NoneUrlHandler"/> class.
|
||||
/// </summary>
|
||||
public NoneUrlHandler() : base(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The ParseFullUrl
|
||||
/// </summary>
|
||||
protected override void ParseFullUrl()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
643
VideoBrowser/Core/Operation.cs
Normal file
643
VideoBrowser/Core/Operation.cs
Normal file
@@ -0,0 +1,643 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using VideoBrowser.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Operation" />.
|
||||
/// </summary>
|
||||
public abstract class Operation : IDisposable, INotifyPropertyChanged
|
||||
{
|
||||
#region Constants
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time to wait for progress updates in milliseconds.
|
||||
/// </summary>
|
||||
protected const int ProgressDelay = 500;
|
||||
|
||||
protected const int ProgressMax = 100;
|
||||
|
||||
protected const int ProgressMin = 0;
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly string _progressText = string.Empty;
|
||||
|
||||
private readonly string _progressTextOverride = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Store running operations that can be stopped automatically when closing application.
|
||||
/// </summary>
|
||||
public static List<Operation> Running = new List<Operation>();
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
private long _duration;
|
||||
|
||||
private string _eta;
|
||||
|
||||
private long _fileSize;
|
||||
|
||||
private string _input;
|
||||
|
||||
private string _link;
|
||||
|
||||
private string _output;
|
||||
|
||||
private long _progress;
|
||||
|
||||
private int _progressPercentage = 0;
|
||||
|
||||
private bool _reportsProgress = false;
|
||||
|
||||
private string _speed;
|
||||
|
||||
private OperationStatus _status = OperationStatus.Queued;
|
||||
|
||||
private string _thumbnail;
|
||||
|
||||
private string _title;
|
||||
|
||||
private BackgroundWorker _worker;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Operation"/> class.
|
||||
/// </summary>
|
||||
protected Operation()
|
||||
{
|
||||
_worker = new BackgroundWorker()
|
||||
{
|
||||
WorkerReportsProgress = true,
|
||||
WorkerSupportsCancellation = true
|
||||
};
|
||||
_worker.DoWork += Worker_DoWork;
|
||||
_worker.ProgressChanged += Worker_ProgressChanged;
|
||||
_worker.RunWorkerCompleted += Worker_Completed;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the operation is complete.
|
||||
/// </summary>
|
||||
public event OperationEventHandler Completed;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the ProgressChanged.
|
||||
/// </summary>
|
||||
public event ProgressChangedEventHandler ProgressChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the PropertyChanged.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the ReportsProgressChanged.
|
||||
/// </summary>
|
||||
public event EventHandler ReportsProgressChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Resumed.
|
||||
/// </summary>
|
||||
public event EventHandler Resumed;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Started.
|
||||
/// </summary>
|
||||
public event EventHandler Started;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the StatusChanged.
|
||||
/// </summary>
|
||||
public event StatusChangedEventHandler StatusChanged;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether CancellationPending.
|
||||
/// </summary>
|
||||
public bool CancellationPending => _worker?.CancellationPending == true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Duration.
|
||||
/// </summary>
|
||||
public long Duration { get => _duration; set => this.Set(this.PropertyChanged, ref _duration, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Errors
|
||||
/// Gets a human readable list of errors caused by the operation..
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> Errors => new ReadOnlyCollection<string>(ErrorsInternal);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Errors1.
|
||||
/// </summary>
|
||||
public List<string> Errors1 { get => ErrorsInternal; set => ErrorsInternal = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ETA.
|
||||
/// </summary>
|
||||
public string ETA { get => _eta; set => this.Set(this.PropertyChanged, ref _eta, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Exception.
|
||||
/// </summary>
|
||||
public Exception Exception { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FileSize.
|
||||
/// </summary>
|
||||
public long FileSize
|
||||
{
|
||||
get => _fileSize;
|
||||
set
|
||||
{
|
||||
_fileSize = value;
|
||||
this.OnPropertyChanged();
|
||||
this.OnPropertyChangedExplicit(nameof(ProgressText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether HasStarted.
|
||||
/// </summary>
|
||||
public bool HasStarted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Input
|
||||
/// Gets the input file or download url..
|
||||
/// </summary>
|
||||
public string Input { get => _input; set => this.Set(this.PropertyChanged, ref _input, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsBusy.
|
||||
/// </summary>
|
||||
public bool IsBusy => _worker?.IsBusy == true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsCanceled.
|
||||
/// </summary>
|
||||
public bool IsCanceled => this.Status == OperationStatus.Canceled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsDone
|
||||
/// Returns True if Operation is done, regardless of result..
|
||||
/// </summary>
|
||||
public bool IsDone => this.Status == OperationStatus.Canceled
|
||||
|| this.Status == OperationStatus.Failed
|
||||
|| this.Status == OperationStatus.Success;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsPaused.
|
||||
/// </summary>
|
||||
public bool IsPaused => this.Status == OperationStatus.Paused;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsQueued.
|
||||
/// </summary>
|
||||
public bool IsQueued => this.Status == OperationStatus.Queued;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsSuccessful.
|
||||
/// </summary>
|
||||
public bool IsSuccessful => this.Status == OperationStatus.Success;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsWorking.
|
||||
/// </summary>
|
||||
public bool IsWorking => this.Status == OperationStatus.Working;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Link.
|
||||
/// </summary>
|
||||
public string Link { get => _link; set => this.Set(this.PropertyChanged, ref _link, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Output
|
||||
/// Gets the output file..
|
||||
/// </summary>
|
||||
public string Output { get => _output; set => this.Set(this.PropertyChanged, ref _output, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Progress.
|
||||
/// </summary>
|
||||
public long Progress
|
||||
{
|
||||
get => _progress;
|
||||
set
|
||||
{
|
||||
_progress = value;
|
||||
this.OnPropertyChanged();
|
||||
this.OnPropertyChangedExplicit(nameof(ProgressText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ProgressPercentage
|
||||
/// Gets the operation progress, as a double between 0-100..
|
||||
/// </summary>
|
||||
public int ProgressPercentage
|
||||
{
|
||||
get => _progressPercentage;
|
||||
set
|
||||
{
|
||||
_progressPercentage = value;
|
||||
this.OnPropertyChanged();
|
||||
this.OnPropertyChangedExplicit(nameof(ProgressText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ProgressText.
|
||||
/// </summary>
|
||||
public string ProgressText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether ReportsProgress.
|
||||
/// </summary>
|
||||
public bool ReportsProgress
|
||||
{
|
||||
get => _reportsProgress;
|
||||
set
|
||||
{
|
||||
_reportsProgress = value;
|
||||
this.OnReportsProgressChanged(EventArgs.Empty);
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Speed.
|
||||
/// </summary>
|
||||
public string Speed
|
||||
{
|
||||
get => _speed;
|
||||
set
|
||||
{
|
||||
_speed = value;
|
||||
this.OnPropertyChanged();
|
||||
this.OnPropertyChangedExplicit(nameof(ProgressText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Status
|
||||
/// Gets the operation status..
|
||||
/// </summary>
|
||||
public OperationStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
var oldStatus = _status;
|
||||
|
||||
_status = value;
|
||||
|
||||
this.OnStatusChanged(new StatusChangedEventArgs(this, value, oldStatus));
|
||||
this.OnPropertyChanged();
|
||||
|
||||
// Send Changed notification to following properties
|
||||
foreach (string property in new string[] {
|
||||
nameof(IsCanceled),
|
||||
nameof(IsDone),
|
||||
nameof(IsPaused),
|
||||
nameof(IsSuccessful),
|
||||
nameof(IsWorking) })
|
||||
{
|
||||
this.OnPropertyChangedExplicit(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Thumbnail.
|
||||
/// </summary>
|
||||
public string Thumbnail { get => _thumbnail; set => this.Set(this.PropertyChanged, ref _thumbnail, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Title.
|
||||
/// </summary>
|
||||
public string Title { get => _title; set => this.Set(this.PropertyChanged, ref _title, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Operation's arguments..
|
||||
/// </summary>
|
||||
protected Dictionary<string, object> Arguments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ErrorsInternal
|
||||
/// Gets or sets a editable list of errors..
|
||||
/// </summary>
|
||||
protected List<string> ErrorsInternal { get; set; } = new List<string>();
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether 'Open' method is supported and available at the moment.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
public virtual bool CanOpen() => false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether 'Pause' method is supported and available at the moment.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
public virtual bool CanPause() => false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether 'Resume' method is supported and available at the moment.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
public virtual bool CanResume() => false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether 'Stop' method is supported and available at the moment.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
public virtual bool CanStop() => false;
|
||||
|
||||
/// <summary>
|
||||
/// The Dispose.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChanged.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The propertyName<see cref="string"/>.</param>
|
||||
public void OnPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
this.OnPropertyChangedExplicit(propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChangedExplicit.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The propertyName<see cref="string"/>.</param>
|
||||
public void OnPropertyChangedExplicit(string propertyName)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the output file.
|
||||
/// </summary>
|
||||
/// <returns>.</returns>
|
||||
public virtual bool Open()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the containing folder of the output file(s).
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
public virtual bool OpenContainingFolder()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the operation if supported & available.
|
||||
/// </summary>
|
||||
public virtual void Pause()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues the operation. Used to pause and put the operation back to being queued.
|
||||
/// </summary>
|
||||
public virtual void Queue()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the operation if supported & available.
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
if (!this.HasStarted)
|
||||
{
|
||||
this.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ResumeInternal();
|
||||
}
|
||||
|
||||
this.Resumed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the operation if supported & available, but does not fire the Resumed event.
|
||||
/// </summary>
|
||||
public void ResumeQuiet()
|
||||
{
|
||||
this.ResumeInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the operation.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_worker.RunWorkerAsync(this.Arguments);
|
||||
|
||||
Operation.Running.Add(this);
|
||||
|
||||
this.Status = OperationStatus.Working;
|
||||
this.OnStarted(EventArgs.Empty);
|
||||
|
||||
this.HasStarted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the operation if supported & available.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
||||
public virtual bool Stop()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The CancelAsync.
|
||||
/// </summary>
|
||||
protected void CancelAsync() => _worker?.CancelAsync();
|
||||
|
||||
/// <summary>
|
||||
/// The Complete.
|
||||
/// </summary>
|
||||
protected void Complete()
|
||||
{
|
||||
this.ReportsProgress = true;
|
||||
|
||||
if (this.Status == OperationStatus.Success)
|
||||
{
|
||||
this.ProgressPercentage = ProgressMax;
|
||||
}
|
||||
|
||||
this.OnPropertyChangedExplicit(nameof(ProgressText));
|
||||
OnCompleted(new OperationEventArgs(null, this.Status));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnCompleted.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="OperationEventArgs"/>.</param>
|
||||
protected virtual void OnCompleted(OperationEventArgs e) => this.Completed?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The OnProgressChanged.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="ProgressChangedEventArgs"/>.</param>
|
||||
protected virtual void OnProgressChanged(ProgressChangedEventArgs e) => this.ProgressChanged?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The OnReportsProgressChanged.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="EventArgs"/>.</param>
|
||||
protected virtual void OnReportsProgressChanged(EventArgs e) => this.ReportsProgressChanged?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The OnStarted.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="EventArgs"/>.</param>
|
||||
protected virtual void OnStarted(EventArgs e) => this.Started?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The OnStatusChanged.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="StatusChangedEventArgs"/>.</param>
|
||||
protected virtual void OnStatusChanged(StatusChangedEventArgs e) => this.StatusChanged?.Invoke(this, e);
|
||||
|
||||
/// <summary>
|
||||
/// The ReportProgress.
|
||||
/// </summary>
|
||||
/// <param name="percentProgress">The percentProgress<see cref="int"/>.</param>
|
||||
/// <param name="userState">The userState<see cref="object"/>.</param>
|
||||
protected void ReportProgress(int percentProgress, object userState) => _worker?.ReportProgress(percentProgress, userState);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the operation if supported & available.
|
||||
/// </summary>
|
||||
protected virtual void ResumeInternal() => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// The WorkerCompleted.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="RunWorkerCompletedEventArgs"/>.</param>
|
||||
protected abstract void WorkerCompleted(RunWorkerCompletedEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// The WorkerDoWork.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="DoWorkEventArgs"/>.</param>
|
||||
protected abstract void WorkerDoWork(DoWorkEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// The WorkerProgressChanged.
|
||||
/// </summary>
|
||||
/// <param name="e">The e<see cref="ProgressChangedEventArgs"/>.</param>
|
||||
protected abstract void WorkerProgressChanged(ProgressChangedEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// The Dispose.
|
||||
/// </summary>
|
||||
/// <param name="disposing">The disposing<see cref="bool"/>.</param>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_worker.DoWork -= Worker_DoWork;
|
||||
_worker.ProgressChanged -= Worker_ProgressChanged;
|
||||
_worker.RunWorkerCompleted -= Worker_Completed;
|
||||
this.Completed = null;
|
||||
|
||||
if (_worker != null)
|
||||
{
|
||||
_worker.Dispose();
|
||||
_worker = null;
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Worker_Completed.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/>.</param>
|
||||
/// <param name="e">The e<see cref="RunWorkerCompletedEventArgs"/>.</param>
|
||||
private void Worker_Completed(object sender, RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
Operation.Running.Remove(this);
|
||||
|
||||
if (e.Error != null)
|
||||
{
|
||||
this.Exception = e.Error;
|
||||
this.Status = OperationStatus.Failed;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Status = (OperationStatus)e.Result;
|
||||
}
|
||||
|
||||
this.Complete();
|
||||
this.WorkerCompleted(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Worker_DoWork.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/>.</param>
|
||||
/// <param name="e">The e<see cref="DoWorkEventArgs"/>.</param>
|
||||
private void Worker_DoWork(object sender, DoWorkEventArgs e) => WorkerDoWork(e);
|
||||
|
||||
/// <summary>
|
||||
/// The Worker_ProgressChanged.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/>.</param>
|
||||
/// <param name="e">The e<see cref="ProgressChangedEventArgs"/>.</param>
|
||||
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
|
||||
{
|
||||
if (e.ProgressPercentage >= 0 && e.ProgressPercentage <= 100)
|
||||
{
|
||||
this.ProgressPercentage = e.ProgressPercentage;
|
||||
}
|
||||
|
||||
this.WorkerProgressChanged(e);
|
||||
this.OnProgressChanged(e);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
40
VideoBrowser/Core/OperationEventArgs.cs
Normal file
40
VideoBrowser/Core/OperationEventArgs.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="OperationEventArgs" />
|
||||
/// </summary>
|
||||
public class OperationEventArgs : EventArgs
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OperationEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The item<see cref="ListViewItem"/></param>
|
||||
/// <param name="status">The status<see cref="OperationStatus"/></param>
|
||||
public OperationEventArgs(ListViewItem item, OperationStatus status)
|
||||
{
|
||||
this.Item = item;
|
||||
this.Status = status;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Item
|
||||
/// </summary>
|
||||
public ListViewItem Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Status
|
||||
/// </summary>
|
||||
public OperationStatus Status { get; set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
47
VideoBrowser/Core/OperationStatus.cs
Normal file
47
VideoBrowser/Core/OperationStatus.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// Defines the OperationStatus
|
||||
/// </summary>
|
||||
public enum OperationStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the Canceled
|
||||
/// </summary>
|
||||
Canceled,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Failed
|
||||
/// </summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the None
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Paused
|
||||
/// </summary>
|
||||
Paused,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Success
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Queued
|
||||
/// </summary>
|
||||
Queued,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Working
|
||||
/// </summary>
|
||||
Working
|
||||
}
|
||||
|
||||
#endregion Enums
|
||||
}
|
||||
69
VideoBrowser/Core/PlayList.cs
Normal file
69
VideoBrowser/Core/PlayList.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="PlayList" />
|
||||
/// </summary>
|
||||
public class PlayList
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlayList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The id<see cref="string"/></param>
|
||||
/// <param name="name">The name<see cref="string"/></param>
|
||||
/// <param name="onlineCount">The onlineCount<see cref="int"/></param>
|
||||
public PlayList(string id, string name, int onlineCount)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
this.OnlineCount = onlineCount;
|
||||
this.Videos = new List<VideoInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlayList"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The id<see cref="string"/></param>
|
||||
/// <param name="name">The name<see cref="string"/></param>
|
||||
/// <param name="onlineCount">The onlineCount<see cref="int"/></param>
|
||||
/// <param name="videos">The videos<see cref="List{VideoInfo}"/></param>
|
||||
public PlayList(string id, string name, int onlineCount, List<VideoInfo> videos)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
this.OnlineCount = onlineCount;
|
||||
this.Videos = videos;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the play list ID.
|
||||
/// </summary>
|
||||
public string Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playlist name.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the OnlineCount
|
||||
/// Gets the expected video count. Expected because some videos might not be included because of errors.
|
||||
/// Look at 'Playlist.Videos' property for actual count.
|
||||
/// </summary>
|
||||
public int OnlineCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the videos in the playlist. Videos with errors not included, for example country restrictions.
|
||||
/// </summary>
|
||||
public List<VideoInfo> Videos { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
306
VideoBrowser/Core/PlayListReader.cs
Normal file
306
VideoBrowser/Core/PlayListReader.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using VideoBrowser.Common;
|
||||
using VideoBrowser.Helpers;
|
||||
|
||||
namespace YouTube_Downloader_DLL.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="PlaylistReader" />
|
||||
/// </summary>
|
||||
public class PlaylistReader
|
||||
{
|
||||
#region Constants
|
||||
|
||||
public const string CmdPlaylistInfo = " -i -o \"{0}\\playlist-{1}\\%(playlist_index)s-%(title)s\" --restrict-filenames --skip-download --write-info-json{2}{3} \"{4}\"";
|
||||
|
||||
public const string CmdPlaylistRange = " --playlist-items {0}";
|
||||
|
||||
public const string CmdPlaylistReverse = " --playlist-reverse";
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly string _arguments;
|
||||
|
||||
private readonly string _playlist_id;
|
||||
|
||||
private readonly string _url;
|
||||
|
||||
private CancellationTokenSource _cts = new CancellationTokenSource();
|
||||
|
||||
private string _currentVideoID;
|
||||
|
||||
private int _currentVideoPlaylistIndex = -1;
|
||||
|
||||
private int _index = 0;
|
||||
|
||||
private List<string> _jsonPaths = new List<string>();
|
||||
|
||||
private bool _processFinished = false;
|
||||
|
||||
private Regex _regexPlaylistIndex = new Regex(@"\[download\]\s\w*\s\w*\s(\d+)", RegexOptions.Compiled);
|
||||
|
||||
private Regex _regexPlaylistInfo = new Regex(@"^\[youtube:playlist\] playlist (.*):.*Downloading\s+(\d+)\s+.*$", RegexOptions.Compiled);
|
||||
|
||||
private Regex _regexVideoID = new Regex(@"\[youtube\]\s(.*):", RegexOptions.Compiled);
|
||||
|
||||
private Regex _regexVideoJson = new Regex(@"^\[info\].*JSON.*:\s(.*)$", RegexOptions.Compiled);
|
||||
|
||||
private Process _youtubeDl;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaylistReader"/> class.
|
||||
/// </summary>
|
||||
/// <param name="url">The url<see cref="string"/></param>
|
||||
/// <param name="videos">The videos<see cref="int[]"/></param>
|
||||
/// <param name="reverse">The reverse<see cref="bool"/></param>
|
||||
public PlaylistReader(string url, int[] videos, bool reverse)
|
||||
{
|
||||
var json_dir = AppEnvironment.GetJsonDirectory();
|
||||
_playlist_id = YoutubeHelper.GetPlaylistId(url);
|
||||
var range = string.Empty;
|
||||
|
||||
if (videos != null && videos.Length > 0)
|
||||
{
|
||||
// Make sure the video indexes is sorted, otherwise reversing wont do anything
|
||||
Array.Sort(videos);
|
||||
range = string.Format(CmdPlaylistRange, string.Join(",", videos));
|
||||
}
|
||||
|
||||
var reverseS = reverse ? CmdPlaylistReverse : string.Empty;
|
||||
_arguments = string.Format(CmdPlaylistInfo, json_dir, _playlist_id, range, reverseS, url);
|
||||
_url = url;
|
||||
|
||||
YoutubeDl.LogHeader(_arguments);
|
||||
|
||||
_youtubeDl = ProcessHelper.StartProcess(YoutubeDl.YouTubeDlPath,
|
||||
_arguments,
|
||||
OutputReadLine,
|
||||
ErrorReadLine,
|
||||
null);
|
||||
_youtubeDl.Exited += delegate
|
||||
{
|
||||
_processFinished = true;
|
||||
YoutubeDl.LogFooter();
|
||||
};
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Canceled
|
||||
/// </summary>
|
||||
public bool Canceled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PlayList
|
||||
/// </summary>
|
||||
public PlayList PlayList { get; set; } = null;
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The ErrorReadLine
|
||||
/// </summary>
|
||||
/// <param name="process">The process<see cref="Process"/></param>
|
||||
/// <param name="line">The line<see cref="string"/></param>
|
||||
public void ErrorReadLine(Process process, string line)
|
||||
{
|
||||
_jsonPaths.Add($"[ERROR:{_currentVideoID}] {line}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Next
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="VideoInfo"/></returns>
|
||||
public VideoInfo Next()
|
||||
{
|
||||
var attempts = 0;
|
||||
string jsonPath = null;
|
||||
VideoInfo video = null;
|
||||
|
||||
while (!_processFinished)
|
||||
{
|
||||
if (_jsonPaths.Count > _index)
|
||||
{
|
||||
jsonPath = _jsonPaths[_index];
|
||||
_index++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's the end of the stream finish up the process.
|
||||
if (jsonPath == null)
|
||||
{
|
||||
if (!_youtubeDl.HasExited)
|
||||
{
|
||||
_youtubeDl.WaitForExit();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sometimes youtube-dl is slower to create the json file, try a couple times
|
||||
while (attempts < 10)
|
||||
{
|
||||
if (_cts.IsCancellationRequested)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (jsonPath.StartsWith("[ERROR"))
|
||||
{
|
||||
var match = new Regex(@"\[ERROR:(.*)]\s(.*)").Match(jsonPath);
|
||||
video = new VideoInfo
|
||||
{
|
||||
Id = match.Groups[1].Value,
|
||||
Failure = true,
|
||||
FailureReason = match.Groups[2].Value
|
||||
};
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
video = new VideoInfo(jsonPath)
|
||||
{
|
||||
PlaylistIndex = _currentVideoPlaylistIndex
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
attempts++;
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (video == null)
|
||||
{
|
||||
throw new FileNotFoundException("File not found.", jsonPath);
|
||||
}
|
||||
|
||||
this.PlayList.Videos.Add(video);
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OutputReadLine
|
||||
/// </summary>
|
||||
/// <param name="process">The process<see cref="Process"/></param>
|
||||
/// <param name="line">The line<see cref="string"/></param>
|
||||
public void OutputReadLine(Process process, string line)
|
||||
{
|
||||
Match m;
|
||||
|
||||
if (line.StartsWith("[youtube:playlist]"))
|
||||
{
|
||||
if ((m = _regexPlaylistInfo.Match(line)).Success)
|
||||
{
|
||||
// Get the playlist info
|
||||
var name = m.Groups[1].Value;
|
||||
var onlineCount = int.Parse(m.Groups[2].Value);
|
||||
|
||||
this.PlayList = new PlayList(_playlist_id, name, onlineCount);
|
||||
}
|
||||
}
|
||||
else if (line.StartsWith("[info]"))
|
||||
{
|
||||
// New json found, break & create a VideoInfo instance
|
||||
if ((m = _regexVideoJson.Match(line)).Success)
|
||||
{
|
||||
_jsonPaths.Add(m.Groups[1].Value.Trim());
|
||||
}
|
||||
}
|
||||
else if (line.StartsWith("[download]"))
|
||||
{
|
||||
if ((m = _regexPlaylistIndex.Match(line)).Success)
|
||||
{
|
||||
var i = -1;
|
||||
if (int.TryParse(m.Groups[1].Value, out i))
|
||||
{
|
||||
_currentVideoPlaylistIndex = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"PlaylistReader: Couldn't parse '{m.Groups[1].Value}' to integer for '{nameof(_currentVideoPlaylistIndex)}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (line.StartsWith("[youtube]"))
|
||||
{
|
||||
if ((m = _regexVideoID.Match(line)).Success)
|
||||
{
|
||||
_currentVideoID = m.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Stop
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_youtubeDl.Kill();
|
||||
_cts.Cancel();
|
||||
|
||||
this.Canceled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The WaitForPlaylist
|
||||
/// </summary>
|
||||
/// <param name="timeoutMS">The timeoutMS<see cref="int"/></param>
|
||||
/// <returns>The <see cref="PlayList"/></returns>
|
||||
public PlayList WaitForPlaylist(int timeoutMS = 30000)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
Exception exception = null;
|
||||
|
||||
sw.Start();
|
||||
|
||||
while (this.PlayList == null)
|
||||
{
|
||||
if (_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(50);
|
||||
|
||||
if (sw.ElapsedMilliseconds > timeoutMS)
|
||||
{
|
||||
exception = new TimeoutException("Couldn't get Play list information.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception != null)
|
||||
throw exception;
|
||||
|
||||
return this.PlayList;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
}
|
||||
46
VideoBrowser/Core/StatusChangedEventArgs.cs
Normal file
46
VideoBrowser/Core/StatusChangedEventArgs.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="StatusChangedEventArgs" />
|
||||
/// </summary>
|
||||
public class StatusChangedEventArgs : EventArgs
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StatusChangedEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation<see cref="Operation"/></param>
|
||||
/// <param name="newStatus">The newStatus<see cref="OperationStatus"/></param>
|
||||
/// <param name="oldStatus">The oldStatus<see cref="OperationStatus"/></param>
|
||||
public StatusChangedEventArgs(Operation operation, OperationStatus newStatus, OperationStatus oldStatus)
|
||||
{
|
||||
this.Operation = operation;
|
||||
this.NewStatus = newStatus;
|
||||
this.OldStatus = oldStatus;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the NewStatus
|
||||
/// </summary>
|
||||
public OperationStatus NewStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the OldStatus
|
||||
/// </summary>
|
||||
public OperationStatus OldStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Operation
|
||||
/// </summary>
|
||||
public Operation Operation { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
}
|
||||
}
|
||||
102
VideoBrowser/Core/UrlHandlerBase.cs
Normal file
102
VideoBrowser/Core/UrlHandlerBase.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using VideoBrowser.Extensions;
|
||||
using VideoBrowser.If;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="UrlHandlerBase" />
|
||||
/// </summary>
|
||||
public abstract class UrlHandlerBase : IUrlHandler, INotifyPropertyChanged
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private string _fullUrl;
|
||||
|
||||
private bool _isDownloadable;
|
||||
|
||||
private bool _isPlayList;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UrlHandlerBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="domainName">The domainName<see cref="string"/></param>
|
||||
protected UrlHandlerBase(string domainName)
|
||||
{
|
||||
this.DomainName = domainName;
|
||||
this.PropertyChanged += this.OnPropertyChanged;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Defines the PropertyChanged
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DomainName
|
||||
/// </summary>
|
||||
public string DomainName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FullUrl
|
||||
/// </summary>
|
||||
public string FullUrl { get => _fullUrl; set => this.Set(this.PropertyChanged, ref _fullUrl, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether IsDownloadable
|
||||
/// </summary>
|
||||
public bool IsDownloadable { get => _isDownloadable; set => this.Set(this.PropertyChanged, ref _isDownloadable, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether IsPlayList
|
||||
/// </summary>
|
||||
public bool IsPlayList { get => _isPlayList; set => this.Set(this.PropertyChanged, ref _isPlayList, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the VideoUrlTypes
|
||||
/// </summary>
|
||||
public UrlTypes VideoUrlTypes => throw new NotImplementedException();
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The ParseFullUrl
|
||||
/// </summary>
|
||||
protected abstract void ParseFullUrl();
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChanged
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="PropertyChangedEventArgs"/></param>
|
||||
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(this.FullUrl):
|
||||
this.ParseFullUrl();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
74
VideoBrowser/Core/UrlHandlerProvider.cs
Normal file
74
VideoBrowser/Core/UrlHandlerProvider.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using VideoBrowser.If;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="UrlHandlerProvider" />
|
||||
/// </summary>
|
||||
public class UrlHandlerProvider
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UrlHandlerProvider"/> class.
|
||||
/// </summary>
|
||||
public UrlHandlerProvider()
|
||||
{
|
||||
this.UrlHandlerDict = new Dictionary<string, IUrlHandler>();
|
||||
this.AddHandler(new YoutubeUrlHandler());
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the NoneUrlHandler
|
||||
/// </summary>
|
||||
internal static IUrlHandler NoneUrlHandler { get; } = new NoneUrlHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UrlHandlerDict
|
||||
/// </summary>
|
||||
private IDictionary<string, IUrlHandler> UrlHandlerDict { get; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The GetUrlHandler
|
||||
/// </summary>
|
||||
/// <param name="url">The url<see cref="string"/></param>
|
||||
/// <returns>The <see cref="IUrlHandler"/></returns>
|
||||
internal IUrlHandler GetUrlHandler(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var handlerPair in this.UrlHandlerDict)
|
||||
{
|
||||
if (url.Contains(handlerPair.Key))
|
||||
{
|
||||
return handlerPair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return NoneUrlHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The AddHandler
|
||||
/// </summary>
|
||||
/// <param name="handler">The handler<see cref="IUrlHandler"/></param>
|
||||
private void AddHandler(IUrlHandler handler)
|
||||
{
|
||||
this.UrlHandlerDict.Add(handler.DomainName, handler);
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
112
VideoBrowser/Core/UrlReader.cs
Normal file
112
VideoBrowser/Core/UrlReader.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using VideoBrowser.Extensions;
|
||||
using VideoBrowser.If;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="UrlReader" />
|
||||
/// </summary>
|
||||
public class UrlReader : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private string _url;
|
||||
|
||||
private IUrlHandler _urlHandler;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UrlReader"/> class.
|
||||
/// </summary>
|
||||
internal UrlReader()
|
||||
{
|
||||
this.UrlHandlerProvider = new UrlHandlerProvider();
|
||||
this.UrlHandler = UrlHandlerProvider.NoneUrlHandler;
|
||||
this.PropertyChanged += this.OnPropertyChanged;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Defines the PropertyChanged
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Url
|
||||
/// </summary>
|
||||
public string Url { get => this._url; set => this.Set(this.PropertyChanged, ref this._url, value); }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UrlHandler
|
||||
/// Gets or sets the UrlHandler
|
||||
/// </summary>
|
||||
public IUrlHandler UrlHandler
|
||||
{
|
||||
get => this._urlHandler;
|
||||
private set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.Set(this.PropertyChanged, ref this._urlHandler, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UrlHandlerProvider
|
||||
/// </summary>
|
||||
public UrlHandlerProvider UrlHandlerProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether IsDownloadable
|
||||
/// </summary>
|
||||
internal bool IsDownloadable => this.UrlHandler != null ? this.UrlHandler.IsDownloadable : false;
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.PropertyChanged -= this.OnPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChanged
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender<see cref="object"/></param>
|
||||
/// <param name="e">The e<see cref="PropertyChangedEventArgs"/></param>
|
||||
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(this.Url):
|
||||
this.UrlHandler = this.UrlHandlerProvider.GetUrlHandler(this.Url);
|
||||
this.UrlHandler.FullUrl = this.Url;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
30
VideoBrowser/Core/UrlTypes.cs
Normal file
30
VideoBrowser/Core/UrlTypes.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Video Url Types based on the website specification.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum UrlTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the None or the current URL is not complete or error.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the URL is Video that can be downloaded.
|
||||
/// </summary>
|
||||
Video = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Defines the URL is a PlayList that has many videos.
|
||||
/// </summary>
|
||||
PlayList = 2
|
||||
}
|
||||
|
||||
#endregion Enums
|
||||
}
|
||||
280
VideoBrowser/Core/VideoFormat.cs
Normal file
280
VideoBrowser/Core/VideoFormat.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="VideoFormat" />
|
||||
/// </summary>
|
||||
public class VideoFormat : INotifyPropertyChanged
|
||||
{
|
||||
#region Fields
|
||||
|
||||
internal long _fileSize;
|
||||
|
||||
private WebRequest request;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VideoFormat"/> class.
|
||||
/// </summary>
|
||||
/// <param name="videoInfo">The videoInfo<see cref="VideoInfo"/></param>
|
||||
/// <param name="token">The token<see cref="JToken"/></param>
|
||||
public VideoFormat(VideoInfo videoInfo, JToken token)
|
||||
{
|
||||
this.AudioBitRate = -1;
|
||||
this.VideoInfo = videoInfo;
|
||||
this.DeserializeJson(token);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Defines the PropertyChanged
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ACodec
|
||||
/// </summary>
|
||||
public string ACodec { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio bit rate. Returns -1 if not defined.
|
||||
/// </summary>
|
||||
public double AudioBitRate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether AudioOnly
|
||||
/// Gets whether the format is audio only.
|
||||
/// </summary>
|
||||
public bool AudioOnly { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether DASH
|
||||
/// Gets whether format is a DASH format.
|
||||
/// </summary>
|
||||
public bool DASH { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download url.
|
||||
/// </summary>
|
||||
public string DownloadUrl { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file extension, excluding the period.
|
||||
/// </summary>
|
||||
public string Extension { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file size as bytes count.
|
||||
/// </summary>
|
||||
public long FileSize
|
||||
{
|
||||
get { return _fileSize; }
|
||||
private set
|
||||
{
|
||||
_fileSize = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the format text.
|
||||
/// </summary>
|
||||
public string Format { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the format ID.
|
||||
/// </summary>
|
||||
public string FormatID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the frames per second. Null if not defined.
|
||||
/// </summary>
|
||||
public string FPS { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether HasAudioAndVideo
|
||||
/// </summary>
|
||||
public bool HasAudioAndVideo => !this.AudioOnly && !this.VideoOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the format title, displaying some basic information.
|
||||
/// </summary>
|
||||
public string Title => this.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the VCodec
|
||||
/// </summary>
|
||||
public string VCodec { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated VideoInfo.
|
||||
/// </summary>
|
||||
public VideoInfo VideoInfo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether VideoOnly
|
||||
/// </summary>
|
||||
public bool VideoOnly { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Aborts request for file size.
|
||||
/// </summary>
|
||||
public void AbortUpdateFileSize()
|
||||
{
|
||||
if (request != null)
|
||||
{
|
||||
request.Abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChanged
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The propertyName<see cref="string"/></param>
|
||||
public void OnPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
this.OnPropertyChangedExplicit(propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChangedExplicit
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The propertyName<see cref="string"/></param>
|
||||
public void OnPropertyChangedExplicit(string propertyName)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ToString
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="string"/></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var text = string.Empty;
|
||||
if (this.AudioOnly)
|
||||
{
|
||||
text = this.AudioBitRate > -1
|
||||
? string.Format("Audio Only - {0} kbps (.{1})", this.AudioBitRate, this.Extension)
|
||||
: string.Format("Audio Only (.{0})", this.Extension);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fps = !string.IsNullOrEmpty(this.FPS) ? $" - {this.FPS}fps" : string.Empty;
|
||||
text = string.Format("{0}{1} (.{2})", this.Format, fps, this.Extension);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a WebRequest to update the file size.
|
||||
/// </summary>
|
||||
public async void UpdateFileSizeAsync()
|
||||
{
|
||||
if (this.FileSize > 0)
|
||||
{
|
||||
// Probably already got the file size from .json file.
|
||||
this.VideoInfo.OnFileSizeUpdated(this);
|
||||
return;
|
||||
}
|
||||
|
||||
WebResponse response = null;
|
||||
try
|
||||
{
|
||||
request = WebRequest.Create(this.DownloadUrl);
|
||||
request.Method = "HEAD";
|
||||
response = await request.GetResponseAsync();
|
||||
|
||||
var bytes = response.ContentLength;
|
||||
this.FileSize = bytes;
|
||||
this.VideoInfo.OnFileSizeUpdated(this);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
Console.WriteLine("Canceled update file size");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
Console.WriteLine("Update file size error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (response != null)
|
||||
{
|
||||
response.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The DeserializeJson
|
||||
/// </summary>
|
||||
/// <param name="token">The token<see cref="JToken"/></param>
|
||||
private void DeserializeJson(JToken token)
|
||||
{
|
||||
this.ACodec = token["acodec"]?.ToString();
|
||||
this.VCodec = token["vcodec"]?.ToString();
|
||||
|
||||
this.DownloadUrl = token["url"].ToString();
|
||||
|
||||
var formatnote = token.SelectToken("format_note");
|
||||
if (formatnote != null)
|
||||
{
|
||||
this.DASH = token["format_note"].ToString().ToLower().Contains("dash");
|
||||
}
|
||||
|
||||
this.Extension = token["ext"].ToString();
|
||||
this.Format = Regex.Match(token["format"].ToString(), @".*-\s([^\(\n]*)").Groups[1].Value.Trim();
|
||||
this.FormatID = token["format_id"].ToString();
|
||||
|
||||
// Check if format is audio only or video only
|
||||
this.AudioOnly = this.Format.Contains("audio only");
|
||||
this.VideoOnly = this.ACodec == "none";
|
||||
|
||||
// Check for abr token (audio bit rate?)
|
||||
var abr = token.SelectToken("abr");
|
||||
if (abr != null)
|
||||
{
|
||||
this.AudioBitRate = double.Parse(abr.ToString());
|
||||
}
|
||||
|
||||
// Check for filesize token
|
||||
var filesize = token.SelectToken("filesize");
|
||||
if (filesize != null && !string.IsNullOrEmpty(filesize.ToString()))
|
||||
{
|
||||
this.FileSize = long.Parse(filesize.ToString());
|
||||
}
|
||||
|
||||
// Check for 60fps videos. If there is no 'fps' token, default to 30fps.
|
||||
var fps = token.SelectToken("fps", false);
|
||||
|
||||
this.FPS = fps == null || fps.ToString() == "null" ? string.Empty : fps.ToString();
|
||||
this.UpdateFileSizeAsync();
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
253
VideoBrowser/Core/VideoInfo.cs
Normal file
253
VideoBrowser/Core/VideoInfo.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="VideoInfo" />
|
||||
/// </summary>
|
||||
public class VideoInfo : INotifyPropertyChanged
|
||||
{
|
||||
#region Fields
|
||||
|
||||
internal long _duration = 0;
|
||||
|
||||
internal string _thumbnailUrl = string.Empty;
|
||||
|
||||
internal string _title = string.Empty;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VideoInfo"/> class.
|
||||
/// </summary>
|
||||
public VideoInfo()
|
||||
{
|
||||
this.Formats = new List<VideoFormat>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VideoInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="json_file">The json_file<see cref="string"/></param>
|
||||
public VideoInfo(string json_file)
|
||||
: this()
|
||||
{
|
||||
this.DeserializeJson(json_file);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when one of the format's file size has been updated.
|
||||
/// </summary>
|
||||
public event FileSizeUpdateHandler FileSizeUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the PropertyChanged
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
#endregion Events
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Duration
|
||||
/// Gets the video duration in seconds.
|
||||
/// </summary>
|
||||
public long Duration
|
||||
{
|
||||
get => _duration;
|
||||
set
|
||||
{
|
||||
_duration = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Failure
|
||||
/// Gets or sets whether there was a failure retrieving video information.
|
||||
/// </summary>
|
||||
public bool Failure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason for failure retrieving video information.
|
||||
/// </summary>
|
||||
public string FailureReason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Formats
|
||||
/// Gets all the available formats.
|
||||
/// </summary>
|
||||
public List<VideoFormat> Formats { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video Id.
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playlist index. Default value is -1.
|
||||
/// </summary>
|
||||
public int PlaylistIndex { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether RequiresAuthentication
|
||||
/// Gets or sets whether authentication is required to get video information.
|
||||
/// </summary>
|
||||
public bool RequiresAuthentication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ThumbnailUrl
|
||||
/// Gets the video thumbnail url.
|
||||
/// </summary>
|
||||
public string ThumbnailUrl
|
||||
{
|
||||
get => _thumbnailUrl;
|
||||
set
|
||||
{
|
||||
_thumbnailUrl = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Title
|
||||
/// Gets the video title.
|
||||
/// </summary>
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video url.
|
||||
/// </summary>
|
||||
public string Url { get; private set; }
|
||||
|
||||
/////// <summary>
|
||||
/////// Gets the video source (Twitch/YouTube).
|
||||
/////// </summary>
|
||||
////public VideoSource VideoSource { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Aborts all the requests for file size for each video format.
|
||||
/// </summary>
|
||||
public void AbortUpdateFileSizes()
|
||||
{
|
||||
foreach (VideoFormat format in this.Formats)
|
||||
{
|
||||
format.AbortUpdateFileSize();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The DeserializeJson
|
||||
/// </summary>
|
||||
/// <param name="json_file">The json_file<see cref="string"/></param>
|
||||
public void DeserializeJson(string json_file)
|
||||
{
|
||||
var raw_json = ReadJSON(json_file);
|
||||
var json = JObject.Parse(raw_json);
|
||||
|
||||
this.Duration = json.Value<long>("duration");
|
||||
this.Title = json.Value<string>("fulltitle");
|
||||
this.Id = json.Value<string>("id");
|
||||
|
||||
var displayId = json.Value<string>("display_id");
|
||||
this.ThumbnailUrl = string.Format("https://i.ytimg.com/vi/{0}/mqdefault.jpg", displayId);
|
||||
this.Url = json.Value<string>("webpage_url");
|
||||
|
||||
foreach (JToken token in (JArray)json["formats"])
|
||||
{
|
||||
this.Formats.Add(new VideoFormat(this, token));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnFileSizeUpdated
|
||||
/// </summary>
|
||||
/// <param name="videoFormat">The videoFormat<see cref="VideoFormat"/></param>
|
||||
internal void OnFileSizeUpdated(VideoFormat videoFormat)
|
||||
{
|
||||
this.FileSizeUpdated?.Invoke(this, new FileSizeUpdateEventArgs(videoFormat));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ReadJSON
|
||||
/// </summary>
|
||||
/// <param name="json_file">The json_file<see cref="string"/></param>
|
||||
/// <returns>The <see cref="string"/></returns>
|
||||
private static string ReadJSON(string json_file)
|
||||
{
|
||||
var json = string.Empty;
|
||||
|
||||
// Should try for about 10 seconds. */
|
||||
var attempts = 0;
|
||||
var maxAttempts = 20;
|
||||
|
||||
while ((attempts++) <= maxAttempts)
|
||||
{
|
||||
try
|
||||
{
|
||||
json = File.ReadAllText(json_file);
|
||||
break;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
if (ex is FileNotFoundException || ex.Message.EndsWith("because it is being used by another process."))
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChanged
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The propertyName<see cref="string"/></param>
|
||||
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
OnPropertyChangedExplicit(propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The OnPropertyChangedExplicit
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The propertyName<see cref="string"/></param>
|
||||
private void OnPropertyChangedExplicit(string propertyName)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
72
VideoBrowser/Core/YoutubeAuthentication.cs
Normal file
72
VideoBrowser/Core/YoutubeAuthentication.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="YoutubeAuthentication" />
|
||||
/// </summary>
|
||||
public class YoutubeAuthentication
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="YoutubeAuthentication"/> class.
|
||||
/// </summary>
|
||||
/// <param name="username">The username<see cref="string"/></param>
|
||||
/// <param name="password">The password<see cref="string"/></param>
|
||||
/// <param name="twoFactor">The twoFactor<see cref="string"/></param>
|
||||
public YoutubeAuthentication(string username, string password, string twoFactor)
|
||||
{
|
||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
throw new Exception($"{this.GetType().Name}: {nameof(username)} and {nameof(password)} can't be empty or null.");
|
||||
}
|
||||
|
||||
this.Username = username;
|
||||
this.Password = password;
|
||||
this.TwoFactor = twoFactor;
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Password
|
||||
/// </summary>
|
||||
public string Password { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TwoFactor
|
||||
/// </summary>
|
||||
public string TwoFactor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Username
|
||||
/// </summary>
|
||||
public string Username { get; private set; }
|
||||
|
||||
#endregion Properties
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The ToCmdArgument
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="string"/></returns>
|
||||
public string ToCmdArgument()
|
||||
{
|
||||
var twoFactor = this.TwoFactor;
|
||||
if (!string.IsNullOrEmpty(twoFactor))
|
||||
{
|
||||
twoFactor = string.Format(Commands.TwoFactor, twoFactor);
|
||||
}
|
||||
|
||||
return string.Format(Commands.Authentication,
|
||||
this.Username,
|
||||
this.Password) + twoFactor;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
236
VideoBrowser/Core/YoutubeDl.cs
Normal file
236
VideoBrowser/Core/YoutubeDl.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using VideoBrowser.Common;
|
||||
using VideoBrowser.Extensions;
|
||||
using VideoBrowser.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="YoutubeDl" />.
|
||||
/// </summary>
|
||||
public static class YoutubeDl
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const string ErrorSignIn = "YouTube said: Please sign in to view this video.";
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Fields
|
||||
|
||||
public static string YouTubeDlPath = Path.Combine(AppEnvironment.AppBinaryDirectory, "youtube-dl.exe");
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets current youtube-dl version.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="string"/>.</returns>
|
||||
public static string GetVersion()
|
||||
{
|
||||
string version = string.Empty;
|
||||
ProcessHelper.StartProcess(YouTubeDlPath, Commands.Version,
|
||||
delegate (Process process, string line)
|
||||
{
|
||||
// Only one line gets printed, so assume any non-empty line is the version
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
version = line.Trim();
|
||||
},
|
||||
null, null).WaitForExit();
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="VideoInfo"/> of the given video.
|
||||
/// </summary>
|
||||
/// <param name="url">The url to the video.</param>
|
||||
/// <param name="authentication">The authentication<see cref="YoutubeAuthentication"/>.</param>
|
||||
/// <returns>The <see cref="VideoInfo"/>.</returns>
|
||||
public static VideoInfo GetVideoInfo(string url,
|
||||
YoutubeAuthentication authentication = null)
|
||||
{
|
||||
string json_dir = AppEnvironment.GetJsonDirectory();
|
||||
string json_file = string.Empty;
|
||||
string arguments = string.Format(Commands.GetJsonInfo,
|
||||
json_dir,
|
||||
url,
|
||||
authentication == null ? string.Empty : authentication.ToCmdArgument());
|
||||
VideoInfo video = new VideoInfo();
|
||||
|
||||
LogHeader(arguments);
|
||||
|
||||
ProcessHelper.StartProcess(YouTubeDlPath, arguments,
|
||||
delegate (Process process, string line)
|
||||
{
|
||||
line = line.Trim();
|
||||
|
||||
if (line.StartsWith("[info] Writing video description metadata as JSON to:"))
|
||||
{
|
||||
// Store file path
|
||||
json_file = line.Substring(line.IndexOf(":") + 1).Trim();
|
||||
}
|
||||
else if (line.Contains("Refetching age-gated info webpage"))
|
||||
{
|
||||
video.RequiresAuthentication = true;
|
||||
}
|
||||
},
|
||||
delegate (Process process, string error)
|
||||
{
|
||||
error = error.Trim();
|
||||
|
||||
if (error.Contains(ErrorSignIn))
|
||||
{
|
||||
video.RequiresAuthentication = true;
|
||||
}
|
||||
else if (error.StartsWith("ERROR:"))
|
||||
{
|
||||
video.Failure = true;
|
||||
video.FailureReason = error.Substring("ERROR: ".Length);
|
||||
}
|
||||
}, null)
|
||||
.WaitForExit();
|
||||
|
||||
if (!video.Failure && !video.RequiresAuthentication)
|
||||
{
|
||||
video.DeserializeJson(json_file);
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GetVideoInfoBatchAsync.
|
||||
/// </summary>
|
||||
/// <param name="urls">The urls<see cref="ICollection{string}"/>.</param>
|
||||
/// <param name="videoReady">The videoReady<see cref="Action{VideoInfo}"/>.</param>
|
||||
/// <param name="authentication">The authentication<see cref="YoutubeAuthentication"/>.</param>
|
||||
/// <returns>The <see cref="Task"/>.</returns>
|
||||
public static async Task GetVideoInfoBatchAsync(ICollection<string> urls,
|
||||
Action<VideoInfo> videoReady,
|
||||
YoutubeAuthentication authentication = null)
|
||||
{
|
||||
string json_dir = AppEnvironment.GetJsonDirectory();
|
||||
string arguments = string.Format(Commands.GetJsonInfoBatch, json_dir, string.Join(" ", urls));
|
||||
var videos = new OrderedDictionary();
|
||||
var jsonFiles = new Dictionary<string, string>();
|
||||
var findVideoID = new Regex(@"(?:\]|ERROR:)\s(.{11}):", RegexOptions.Compiled);
|
||||
var findVideoIDJson = new Regex(@":\s.*\\(.{11})_", RegexOptions.Compiled);
|
||||
|
||||
LogHeader(arguments);
|
||||
await Task.Run(() =>
|
||||
{
|
||||
ProcessHelper.StartProcess(YouTubeDlPath, arguments,
|
||||
(Process process, string line) =>
|
||||
{
|
||||
line = line.Trim();
|
||||
Match m;
|
||||
string id;
|
||||
VideoInfo video = null;
|
||||
|
||||
if ((m = findVideoID.Match(line)).Success)
|
||||
{
|
||||
id = findVideoID.Match(line).Groups[1].Value;
|
||||
video = videos.Get<VideoInfo>(id, new VideoInfo() { Id = id });
|
||||
}
|
||||
|
||||
if (line.StartsWith("[info] Writing video description metadata as JSON to:"))
|
||||
{
|
||||
id = findVideoIDJson.Match(line).Groups[1].Value;
|
||||
var jsonFile = line.Substring(line.IndexOf(":") + 1).Trim();
|
||||
jsonFiles.Put(id, jsonFile);
|
||||
|
||||
video = videos[id] as VideoInfo;
|
||||
video.DeserializeJson(jsonFile);
|
||||
videoReady(video);
|
||||
}
|
||||
else if (line.Contains("Refetching age-gated info webpage"))
|
||||
{
|
||||
video.RequiresAuthentication = true;
|
||||
}
|
||||
},
|
||||
(Process process, string error) =>
|
||||
{
|
||||
error = error.Trim();
|
||||
var id = findVideoID.Match(error).Groups[1].Value;
|
||||
var video = videos.Get<VideoInfo>(id, new VideoInfo() { Id = id });
|
||||
|
||||
if (error.Contains(ErrorSignIn))
|
||||
{
|
||||
video.RequiresAuthentication = true;
|
||||
}
|
||||
else if (error.StartsWith("ERROR:"))
|
||||
{
|
||||
video.Failure = true;
|
||||
video.FailureReason = error.Substring("ERROR: ".Length);
|
||||
}
|
||||
}, null)
|
||||
.WaitForExit();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes log footer to log.
|
||||
/// </summary>
|
||||
public static void LogFooter()
|
||||
{
|
||||
// Write log footer to stream.
|
||||
// Possibly write elapsed time and/or error in future.
|
||||
Logger.Info("-" + Environment.NewLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes log header to log.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments to log in header.</param>
|
||||
/// <param name="caller">The caller<see cref="string"/>.</param>
|
||||
public static void LogHeader(string arguments, [CallerMemberName]string caller = "")
|
||||
{
|
||||
Logger.Info("[" + DateTime.Now + "]");
|
||||
Logger.Info("version: " + GetVersion());
|
||||
Logger.Info("caller: " + caller);
|
||||
Logger.Info("cmd: " + arguments.Trim());
|
||||
Logger.Info(string.Empty);
|
||||
Logger.Info("OUTPUT");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Update.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Task{string}"/>.</returns>
|
||||
public static async Task<string> Update()
|
||||
{
|
||||
var returnMsg = string.Empty;
|
||||
var versionRegex = new Regex(@"(\d{4}\.\d{2}\.\d{2})");
|
||||
|
||||
await Task.Run(delegate
|
||||
{
|
||||
ProcessHelper.StartProcess(YouTubeDlPath, Commands.Update,
|
||||
delegate (Process process, string line)
|
||||
{
|
||||
Match m;
|
||||
if ((m = versionRegex.Match(line)).Success)
|
||||
returnMsg = m.Groups[1].Value;
|
||||
},
|
||||
delegate (Process process, string line)
|
||||
{
|
||||
returnMsg = "Failed";
|
||||
}, null)
|
||||
.WaitForExit();
|
||||
});
|
||||
|
||||
return returnMsg;
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
40
VideoBrowser/Core/YoutubeUrlHandler.cs
Normal file
40
VideoBrowser/Core/YoutubeUrlHandler.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace VideoBrowser.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="YoutubeUrlHandler" />
|
||||
/// </summary>
|
||||
public class YoutubeUrlHandler : UrlHandlerBase
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="YoutubeUrlHandler"/> class.
|
||||
/// </summary>
|
||||
internal YoutubeUrlHandler() : base("youtube.com")
|
||||
{
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// The ParseFullUrl
|
||||
/// </summary>
|
||||
protected override void ParseFullUrl()
|
||||
{
|
||||
this.IsDownloadable = false;
|
||||
if (!this.FullUrl.Contains(this.DomainName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.FullUrl.Contains(@"youtube.com/watch?v="))
|
||||
{
|
||||
this.IsDownloadable = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Methods
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user