Projektdateien hinzufügen.

This commit is contained in:
Kevin Krüger
2023-07-24 12:00:34 +02:00
parent 656751e10b
commit 0d00a90942
210 changed files with 45049 additions and 0 deletions

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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
}
}

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}