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 { /// /// Defines the /// 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 _jsonPaths = new List(); 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 /// /// Initializes a new instance of the class. /// /// The url /// The videos /// The reverse 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 /// /// Gets or sets a value indicating whether Canceled /// public bool Canceled { get; set; } = false; /// /// Gets or sets the PlayList /// public PlayList PlayList { get; set; } = null; #endregion Properties #region Methods /// /// The ErrorReadLine /// /// The process /// The line public void ErrorReadLine(Process process, string line) { _jsonPaths.Add($"[ERROR:{_currentVideoID}] {line}"); } /// /// The Next /// /// The 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; } /// /// The OutputReadLine /// /// The process /// The line 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; } } } /// /// The Stop /// public void Stop() { _youtubeDl.Kill(); _cts.Cancel(); this.Canceled = true; } /// /// The WaitForPlaylist /// /// The timeoutMS /// The 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 } } }