This repository has been archived on 2026-03-14. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Eco/VideoBrowser/Core/PlayListReader.cs
2023-07-24 12:00:34 +02:00

306 lines
10 KiB
C#

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