Skip to content
Commits on Source (7)
image: microsoft/dotnet:2.1-sdk
stages:
- test_and_build
test_and_build:
stage: test_and_build
script:
- curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
- chmod a+rx /usr/local/bin/youtube-dl
- dotnet restore
- dotnet test test/NYoutubeDL.Tests/NYoutubeDL.Tests.csproj
- dotnet build -c Release src/NYoutubeDL/NYoutubeDL.csproj
- dotnet pack -c Release src/NYoutubeDL/NYoutubeDL.csproj
artifacts:
paths:
- src/NYoutubeDL/bin/Release/
MIT License
Copyright (c) 2017 BrianAllred
Copyright (c) 2018 BrianAllred
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
......
# NYoutubeDL
**CircleCI:** [![CircleCI](https://circleci.com/gh/BrianAllred/NYoutubeDL.svg?style=svg)](https://circleci.com/gh/BrianAllred/NYoutubeDL)
[![pipeline status](https://gitlab.com/BrianAllred/NYoutubeDL/badges/master/pipeline.svg)](https://gitlab.com/BrianAllred/NYoutubeDL/commits/master)
A simple youtube-dl library for C#.
......
version: 2
jobs:
build:
docker:
- image: microsoft/dotnet:2.1-sdk
steps:
- run:
name: Install dependencies
command: |
curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
chmod a+rx /usr/local/bin/youtube-dl
- checkout
- run:
name: Restore package dependencies
command: |
dotnet restore
- run:
name: Run unit tests
working_directory: test/NYoutubeDL.Tests
command: |
dotnet test
- run:
name: Package release
working_directory: src/NYoutubeDL
command: |
dotnet build -c Release
dotnet pack -c Release
- store_artifacts:
path: src/NYoutubeDL/bin/Release
\ No newline at end of file
......@@ -104,18 +104,35 @@ namespace NYoutubeDL.Helpers
/// <returns>
/// A Task representing waiting for the process to end.
/// </returns>
internal static Task WaitForExitAsync(this Process process,
internal static async Task WaitForExitAsync(this Process process,
CancellationToken cancellationToken = default(CancellationToken))
{
var tcs = new TaskCompletionSource<object>();
process.EnableRaisingEvents = true;
process.Exited += (sender, args) => tcs.TrySetResult(null);
if (cancellationToken != default(CancellationToken))
var tcs = new TaskCompletionSource<bool>();
void Process_Exited(object sender, EventArgs e)
{
cancellationToken.Register(tcs.SetCanceled);
tcs.TrySetResult(true);
}
return tcs.Task;
process.EnableRaisingEvents = true;
process.Exited += Process_Exited;
try
{
if (process.HasExited)
{
return;
}
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
{
await tcs.Task;
}
}
finally
{
process.Exited -= Process_Exited;
}
}
}
}
\ No newline at end of file
......@@ -46,6 +46,8 @@ namespace NYoutubeDL.Models
protected const string VIDEOSTRING = "video";
protected const string DOWNLOADSTRING = "[download]";
private string downloadRate;
private string eta;
......
......@@ -77,7 +77,7 @@ namespace NYoutubeDL.Models
internal override void ParseOutput(object sender, string output)
{
if (output.Contains(VIDEOSTRING) && output.Contains(OFSTRING))
if (output.Contains(DOWNLOADSTRING) && output.Contains(VIDEOSTRING) && output.Contains(OFSTRING))
{
Regex regex = new Regex(".*?(\\d+)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
Match match = regex.Match(output);
......
......@@ -3,18 +3,18 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>0.9.0</Version>
<Version>0.10.1</Version>
<Authors>Brian Allred</Authors>
<Company />
<Product />
<Description>A simple youtube-dl library for C#.</Description>
<Copyright>Copyright Brian Allred 2018</Copyright>
<PackageLicenseUrl>https://opensource.org/licenses/MIT</PackageLicenseUrl>
<PackageProjectUrl>http://github.com/brianallred/nyoutubedl</PackageProjectUrl>
<PackageProjectUrl>http://gitlab.com/brianallred/nyoutubedl</PackageProjectUrl>
<PackageTags>youtube-dl</PackageTags>
<NeutralLanguage></NeutralLanguage>
<AssemblyVersion>0.9.0.0</AssemblyVersion>
<FileVersion>0.9.0.0</FileVersion>
<AssemblyVersion>0.10.1.0</AssemblyVersion>
<FileVersion>0.10.1.0</FileVersion>
</PropertyGroup>
<ItemGroup>
......
......@@ -66,7 +66,7 @@ namespace NYoutubeDL.Options
[Option] internal readonly BoolOption writeAnnotations = new BoolOption("--write-annotations");
[Option] internal readonly BoolOption writeDescription = new BoolOption("--write-desription");
[Option] internal readonly BoolOption writeDescription = new BoolOption("--write-description");
[Option] internal readonly BoolOption writeInfoJson = new BoolOption("--write-info-json");
......
......@@ -41,21 +41,42 @@ namespace NYoutubeDL.Services
/// <param name="ydl">
/// The client
/// </param>
internal static async Task DownloadAsync(this YoutubeDL ydl)
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
internal static async Task DownloadAsync(this YoutubeDL ydl, CancellationToken cancellationToken)
{
cancellationToken.Register(() =>
{
Cancel(ydl);
});
if (!ydl.isGettingInfo)
{
ydl.IsDownloading = true;
}
if (ydl.processStartInfo == null)
{
await PreparationService.PrepareDownloadAsync(ydl);
ydl.isGettingInfo = true;
await PreparationService.PrepareDownloadAsync(ydl, cancellationToken);
if (ydl.processStartInfo == null)
{
throw new NullReferenceException();
}
ydl.isGettingInfo = false;
}
SetupDownload(ydl);
SetupDownload(ydl, cancellationToken);
await ydl.process.WaitForExitAsync();
await ydl.process?.WaitForExitAsync(cancellationToken);
if (!ydl.isGettingInfo)
{
ydl.IsDownloading = false;
}
}
/// <summary>
......@@ -64,21 +85,42 @@ namespace NYoutubeDL.Services
/// <param name="ydl">
/// The client
/// </param>
internal static void Download(this YoutubeDL ydl)
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
internal static void Download(this YoutubeDL ydl, CancellationToken cancellationToken)
{
cancellationToken.Register(() =>
{
Cancel(ydl);
});
if (!ydl.isGettingInfo)
{
ydl.IsDownloading = true;
}
if (ydl.processStartInfo == null)
{
PreparationService.PrepareDownload(ydl);
ydl.isGettingInfo = true;
PreparationService.PrepareDownload(ydl, cancellationToken);
if (ydl.processStartInfo == null)
{
throw new NullReferenceException();
}
ydl.isGettingInfo = false;
}
SetupDownload(ydl);
SetupDownload(ydl, cancellationToken);
ydl.process.WaitForExit();
ydl.process?.WaitForExit();
if (!ydl.isGettingInfo)
{
ydl.IsDownloading = false;
}
}
/// <summary>
......@@ -90,10 +132,13 @@ namespace NYoutubeDL.Services
/// <param name="url">
/// The video / playlist URL to download
/// </param>
internal static async Task DownloadAsync(this YoutubeDL ydl, string url)
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
internal static async Task DownloadAsync(this YoutubeDL ydl, string url, CancellationToken cancellationToken)
{
ydl.VideoUrl = url;
await DownloadAsync(ydl);
await DownloadAsync(ydl, cancellationToken);
}
/// <summary>
......@@ -105,20 +150,28 @@ namespace NYoutubeDL.Services
/// <param name="url">
/// The video / playlist URL to download
/// </param>
internal static void Download(this YoutubeDL ydl, string url)
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
internal static void Download(this YoutubeDL ydl, string url, CancellationToken cancellationToken)
{
ydl.VideoUrl = url;
Download(ydl);
Download(ydl, cancellationToken);
}
private static void SetupDownload(YoutubeDL ydl)
private static void SetupDownload(YoutubeDL ydl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
ydl.process = new Process { StartInfo = ydl.processStartInfo, EnableRaisingEvents = true };
ydl.stdOutputTokenSource = new CancellationTokenSource();
ydl.stdErrorTokenSource = new CancellationTokenSource();
ydl.process.Exited += (sender, args) => ydl.KillProcess();
ydl.process.Exited += (sender, args) => ydl.KillStandardEventHandlers();
// Note that synchronous calls are needed in order to process the output line by line.
// Asynchronous output reading results in batches of output lines coming in all at once.
......@@ -134,6 +187,24 @@ namespace NYoutubeDL.Services
}
ydl.process.Start();
ydl.downloadProcessID = ydl.process.Id;
}
private static void Cancel(YoutubeDL ydl, int count = 0)
{
try
{
ydl.StopProcess();
}
catch (Exception ex)
{
Console.WriteLine($"\n\n{ex}\n\n");
}
finally
{
ydl.isGettingInfo = false;
ydl.IsDownloading = false;
}
}
}
}
\ No newline at end of file
......@@ -37,15 +37,18 @@ namespace NYoutubeDL.Services
internal static class InfoService
{
/// <summary>
/// Asynchronously retrieve video / playlist information
/// Asynchronously retrieve video / playlist information
/// </summary>
/// <param name="ydl">
/// The client
/// The client
/// </param>
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
/// <returns>
/// An object containing the download information
/// An object containing the download information
/// </returns>
internal static async Task<DownloadInfo> GetDownloadInfoAsync(this YoutubeDL ydl)
internal static async Task<DownloadInfo> GetDownloadInfoAsync(this YoutubeDL ydl, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(ydl.VideoUrl))
{
......@@ -69,7 +72,10 @@ namespace NYoutubeDL.Services
// Local function for easier event handling
void ParseInfoJson(object sender, string output)
{
infos.Add(DownloadInfo.CreateDownloadInfo(output));
if (!cancellationToken.IsCancellationRequested)
{
infos.Add(DownloadInfo.CreateDownloadInfo(output));
}
}
ydl.StandardOutputEvent += ParseInfoJson;
......@@ -78,13 +84,18 @@ namespace NYoutubeDL.Services
PreparationService.SetupPrepare(ydl);
// Download the info
await DownloadService.DownloadAsync(ydl);
await DownloadService.DownloadAsync(ydl, cancellationToken);
while (ydl.ProcessRunning || infos.Count == 0)
while ((!ydl.process.HasExited || infos.Count == 0) && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(1);
}
if (cancellationToken.IsCancellationRequested)
{
return null;
}
// Set the info object
ydl.Info = infos.Count > 1 ? new MultiDownloadInfo(infos) : infos[0];
......@@ -107,15 +118,18 @@ namespace NYoutubeDL.Services
}
/// <summary>
/// Synchronously retrieve video / playlist information
/// Synchronously retrieve video / playlist information
/// </summary>
/// <param name="ydl">
/// The client
/// The client
/// </param>
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
/// <returns>
/// An object containing the download information
/// An object containing the download information
/// </returns>
internal static DownloadInfo GetDownloadInfo(this YoutubeDL ydl)
internal static DownloadInfo GetDownloadInfo(this YoutubeDL ydl, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(ydl.VideoUrl))
{
......@@ -148,13 +162,18 @@ namespace NYoutubeDL.Services
PreparationService.SetupPrepare(ydl);
// Download the info
DownloadService.Download(ydl);
DownloadService.Download(ydl, cancellationToken);
while (ydl.ProcessRunning || infos.Count == 0)
while ((!ydl.process.HasExited || infos.Count == 0) && !cancellationToken.IsCancellationRequested)
{
Thread.Sleep(1);
}
if (cancellationToken.IsCancellationRequested)
{
return null;
}
// Set the info object
ydl.Info = infos.Count > 1 ? new MultiDownloadInfo(infos) : infos[0];
......@@ -177,40 +196,46 @@ namespace NYoutubeDL.Services
}
/// <summary>
/// Asynchronously retrieve video / playlist information
/// Asynchronously retrieve video / playlist information
/// </summary>
/// <param name="ydl">
/// The client
/// The client
/// </param>
/// <param name="url">
/// URL of video / playlist
/// URL of video / playlist
/// </param>
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
/// <returns>
/// An object containing the download information
/// An object containing the download information
/// </returns>
internal static async Task<DownloadInfo> GetDownloadInfoAsync(this YoutubeDL ydl, string url)
internal static async Task<DownloadInfo> GetDownloadInfoAsync(this YoutubeDL ydl, string url, CancellationToken cancellationToken)
{
ydl.VideoUrl = url;
await GetDownloadInfoAsync(ydl);
await GetDownloadInfoAsync(ydl, cancellationToken);
return ydl.Info;
}
/// <summary>
/// Synchronously retrieve video / playlist information
/// Synchronously retrieve video / playlist information
/// </summary>
/// <param name="ydl">
/// The client
/// The client
/// </param>
/// <param name="url">
/// URL of video / playlist
/// URL of video / playlist
/// </param>
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
/// <returns>
/// An object containing the download information
/// An object containing the download information
/// </returns>
internal static DownloadInfo GetDownloadInfo(this YoutubeDL ydl, string url)
internal static DownloadInfo GetDownloadInfo(this YoutubeDL ydl, string url, CancellationToken cancellationToken)
{
ydl.VideoUrl = url;
return GetDownloadInfo(ydl);
return GetDownloadInfo(ydl, cancellationToken);
}
private static void SetInfoOptions(YoutubeDL ydl)
......
......@@ -24,6 +24,7 @@ namespace NYoutubeDL.Services
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Models;
......@@ -40,14 +41,17 @@ namespace NYoutubeDL.Services
/// <param name="ydl">
/// The client.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
/// <returns>
/// The youtube-dl command that will be executed
/// </returns>
internal static async Task<string> PrepareDownloadAsync(this YoutubeDL ydl)
internal static async Task<string> PrepareDownloadAsync(this YoutubeDL ydl, CancellationToken cancellationToken)
{
if (ydl.Info == null)
{
ydl.Info = await InfoService.GetDownloadInfoAsync(ydl) ?? new DownloadInfo();
ydl.Info = await InfoService.GetDownloadInfoAsync(ydl, cancellationToken) ?? new DownloadInfo();
}
SetupPrepare(ydl);
......@@ -61,14 +65,17 @@ namespace NYoutubeDL.Services
/// <param name="ydl">
/// The client.
/// </param>
/// <param name="cancellationToken">
/// The cancellation token
/// </param>
/// <returns>
/// The youtube-dl command that will be executed
/// </returns>
internal static string PrepareDownload(this YoutubeDL ydl)
internal static string PrepareDownload(this YoutubeDL ydl, CancellationToken cancellationToken)
{
if (ydl.Info == null)
{
ydl.Info = InfoService.GetDownloadInfo(ydl) ?? new DownloadInfo();
ydl.Info = InfoService.GetDownloadInfo(ydl, cancellationToken) ?? new DownloadInfo();
}
SetupPrepare(ydl);
......
......@@ -42,6 +42,9 @@ namespace NYoutubeDL
/// </summary>
public class YoutubeDL
{
/// <summary>
/// The semaphore
/// </summary>
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
/// <summary>
......@@ -66,11 +69,27 @@ namespace NYoutubeDL
/// </summary>
internal CancellationTokenSource stdOutputTokenSource;
/// <summary>
/// Cancellation token used to stop downloads.
/// </summary>
internal CancellationTokenSource downloadTokenSource;
/// <summary>
/// Whether the client is currently downloading video info
/// </summary>
internal bool isGettingInfo;
/// <summary>
/// Process ID of the youtube-dl process
/// </summary>
internal int downloadProcessID;
/// <summary>
/// Creates a new YoutubeDL client
/// </summary>
public YoutubeDL()
{
downloadTokenSource = new CancellationTokenSource();
}
/// <summary>
......@@ -79,7 +98,7 @@ namespace NYoutubeDL
/// <param name="path">
/// Path of youtube-dl binary
/// </param>
public YoutubeDL(string path)
public YoutubeDL(string path) : this()
{
this.YoutubeDlPath = path;
}
......@@ -97,21 +116,7 @@ namespace NYoutubeDL
/// <summary>
/// Returns whether the download process is actively running
/// </summary>
public bool ProcessRunning
{
get
{
try
{
return !this.process.HasExited;
}
catch (Exception)
{
}
return false;
}
}
public bool IsDownloading { get; internal set; }
/// <summary>
/// Gets the complete command that was run by Download().
......@@ -142,7 +147,7 @@ namespace NYoutubeDL
public async Task DownloadAsync()
{
await this.semaphore.WaitAsync();
await DownloadService.DownloadAsync(this);
await DownloadService.DownloadAsync(this, downloadTokenSource.Token);
this.semaphore.Release();
}
......@@ -154,7 +159,7 @@ namespace NYoutubeDL
public async Task DownloadAsync(string videoUrl)
{
await this.semaphore.WaitAsync();
await DownloadService.DownloadAsync(this, videoUrl);
await DownloadService.DownloadAsync(this, videoUrl, downloadTokenSource.Token);
this.semaphore.Release();
}
......@@ -165,7 +170,7 @@ namespace NYoutubeDL
public void Download()
{
this.semaphore.Wait();
DownloadService.Download(this);
DownloadService.Download(this, downloadTokenSource.Token);
this.semaphore.Release();
}
......@@ -177,7 +182,7 @@ namespace NYoutubeDL
public void Download(string videoUrl)
{
this.semaphore.Wait();
DownloadService.Download(this, videoUrl);
DownloadService.Download(this, videoUrl, downloadTokenSource.Token);
this.semaphore.Release();
}
......@@ -190,7 +195,7 @@ namespace NYoutubeDL
public async Task<DownloadInfo> GetDownloadInfoAsync()
{
await this.semaphore.WaitAsync();
DownloadInfo info = await InfoService.GetDownloadInfoAsync(this);
DownloadInfo info = await InfoService.GetDownloadInfoAsync(this, downloadTokenSource.Token);
this.semaphore.Release();
return info;
}
......@@ -207,7 +212,7 @@ namespace NYoutubeDL
public async Task<DownloadInfo> GetDownloadInfoAsync(string url)
{
await this.semaphore.WaitAsync();
DownloadInfo info = await InfoService.GetDownloadInfoAsync(this, url);
DownloadInfo info = await InfoService.GetDownloadInfoAsync(this, url, downloadTokenSource.Token);
this.semaphore.Release();
return info;
}
......@@ -221,7 +226,7 @@ namespace NYoutubeDL
public DownloadInfo GetDownloadInfo()
{
this.semaphore.Wait();
DownloadInfo info = InfoService.GetDownloadInfo(this);
DownloadInfo info = InfoService.GetDownloadInfo(this, downloadTokenSource.Token);
this.semaphore.Release();
return info;
}
......@@ -238,16 +243,60 @@ namespace NYoutubeDL
public DownloadInfo GetDownloadInfo(string url)
{
this.semaphore.Wait();
DownloadInfo info = InfoService.GetDownloadInfo(this, url);
DownloadInfo info = InfoService.GetDownloadInfo(this, url, downloadTokenSource.Token);
this.semaphore.Release();
return info;
}
/// <summary>
/// Prepares the arguments to pass into downloader
/// </summary>
/// <returns>
/// The string of arguments built from the options
/// </returns>
public async Task<string> PrepareDownloadAsync()
{
await this.semaphore.WaitAsync();
string args = await PreparationService.PrepareDownloadAsync(this, downloadTokenSource.Token);
this.semaphore.Release();
return args;
}
/// <summary>
/// Prepares the arguments to pass into downloader
/// </summary>
/// <returns>
/// The string of arguments built from the options
/// </returns>
public string PrepareDownload()
{
this.semaphore.Wait();
string args = PreparationService.PrepareDownload(this, downloadTokenSource.Token);
this.semaphore.Release();
return args;
}
public void CancelDownload()
{
try
{
this.downloadTokenSource.Cancel();
}
catch(Exception ex)
{
Console.WriteLine($"\n\n{ex}\n\n");
}
finally
{
this.downloadTokenSource = new CancellationTokenSource();
}
}
/// <summary>
/// Kills the process and associated threads.
/// We catch these exceptions in case the objects have already been disposed of.
/// </summary>
public void KillProcess()
internal void KillStandardEventHandlers()
{
try
{
......@@ -268,32 +317,35 @@ namespace NYoutubeDL
}
}
/// <summary>
/// Prepares the arguments to pass into downloader
/// </summary>
/// <returns>
/// The string of arguments built from the options
/// </returns>
public async Task<string> PrepareDownloadAsync()
internal void StopProcess()
{
await this.semaphore.WaitAsync();
string args = await PreparationService.PrepareDownloadAsync(this);
this.semaphore.Release();
return args;
}
try
{
if (this.process != null)
{
if (!this.process.HasExited)
{
this.process.Kill();
}
/// <summary>
/// Prepares the arguments to pass into downloader
/// </summary>
/// <returns>
/// The string of arguments built from the options
/// </returns>
public string PrepareDownload()
{
this.semaphore.Wait();
string args = PreparationService.PrepareDownload(this);
this.semaphore.Release();
return args;
this.process.Dispose();
}
}
catch (Exception)
{
try
{
Process processById = Process.GetProcessById(this.downloadProcessID);
if (processById != null && processById.StartTime != null && processById.ProcessName.IndexOf("youtube-dl", StringComparison.OrdinalIgnoreCase) >= 0)
{
processById.Kill();
processById.Dispose();
}
}
catch (Exception)
{
}
}
}
/// <summary>
......