using Newtonsoft.Json; using YoutubeExplode.Videos.Streams; using System.Linq; using System; using System.Threading.Tasks; using YoutubeExplode.Videos; using System.Threading; using YoutubeExplode.Exceptions; using System.Collections.Generic; using System.IO; using YoutubeExplode.Channels; using YoutubeExplode.Playlists; using System.Net.Http; using System.Net; using System.Diagnostics.CodeAnalysis; using YoutubeExplode.Utils.Extensions; using System.Net.Http.Headers; using System.Web; namespace Tesses.YouTubeDownloader { //From YouTubeExplode internal static class Helpers { public static async ValueTask HeadAsync( this HttpClient http, string requestUri, CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Head, requestUri); return await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken ); } public static async ValueTask GetStreamAsync( this HttpClient http, string requestUri, long? from = null, long? to = null, bool ensureSuccess = true, CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); request.Headers.Range = new RangeHeaderValue(from, to); var response = await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken ); if (ensureSuccess) response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(); } public static async ValueTask TryGetContentLengthAsync( this HttpClient http, string requestUri, bool ensureSuccess = true, CancellationToken cancellationToken = default) { using var response = await http.HeadAsync(requestUri, cancellationToken); if (ensureSuccess) response.EnsureSuccessStatusCode(); return response.Content.Headers.ContentLength; } } // Special abstraction that works around YouTube's stream throttling // and provides seeking support. // From YouTubeExplode internal class SegmentedHttpStream : Stream { private readonly HttpClient _http; private readonly string _url; private readonly long? _segmentSize; private Stream _segmentStream; private long _actualPosition; [ExcludeFromCodeCoverage] public override bool CanRead => true; [ExcludeFromCodeCoverage] public override bool CanSeek => true; [ExcludeFromCodeCoverage] public override bool CanWrite => false; public override long Length { get; } public override long Position { get; set; } public SegmentedHttpStream(HttpClient http, string url, long length, long? segmentSize) { _url = url; _http = http; Length = length; _segmentSize = segmentSize; } private void ResetSegmentStream() { _segmentStream?.Dispose(); _segmentStream = null; } private async ValueTask ResolveSegmentStreamAsync( CancellationToken cancellationToken = default) { if (_segmentStream != null) return _segmentStream; var from = Position; var to = _segmentSize != null ? Position + _segmentSize - 1 : null; var stream = await _http.GetStreamAsync(_url, from, to, true, cancellationToken); return _segmentStream = stream; } public async ValueTask PreloadAsync(CancellationToken cancellationToken = default) => await ResolveSegmentStreamAsync(cancellationToken); public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { while (true) { // Check if consumer changed position between reads if (_actualPosition != Position) ResetSegmentStream(); // Check if finished reading (exit condition) if (Position >= Length) return 0; var stream = await ResolveSegmentStreamAsync(cancellationToken); var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken); _actualPosition = Position += bytesRead; if (bytesRead != 0) return bytesRead; // Reached the end of the segment, try to load the next one ResetSegmentStream(); } } [ExcludeFromCodeCoverage] public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); [ExcludeFromCodeCoverage] public override long Seek(long offset, SeekOrigin origin) => Position = origin switch { SeekOrigin.Begin => offset, SeekOrigin.Current => Position + offset, SeekOrigin.End => Length + offset, _ => throw new ArgumentOutOfRangeException(nameof(origin)) }; [ExcludeFromCodeCoverage] public override void Flush() => throw new NotSupportedException(); [ExcludeFromCodeCoverage] public override void SetLength(long value) => throw new NotSupportedException(); [ExcludeFromCodeCoverage] public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { ResetSegmentStream(); } } } public class TYTDClient : TYTDBase,IDownloader { string url; public TYTDClient(string url) { client=new HttpClient(); this.url = url.TrimEnd('/') + '/'; } public TYTDClient(HttpClient clt,string url) { client = clt; this.url = url.TrimEnd('/') + '/'; } public TYTDClient(HttpClient clt, Uri uri) { client=clt; this.url = url.ToString().TrimEnd('/') + '/'; } public TYTDClient(Uri uri) { client=new HttpClient(); this.url = url.ToString().TrimEnd('/') + '/'; } HttpClient client; public async Task AddChannelAsync(ChannelId id, Resolution resolution = Resolution.PreMuxed) { try{ await client.GetStringAsync($"{url}api/v2/AddChannel?v={id.Value}&res={resolution.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task AddPlaylistAsync(PlaylistId id, Resolution resolution = Resolution.PreMuxed) { try{ await client.GetStringAsync($"{url}api/v2/AddPlaylist?v={id.Value}&res={resolution.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task AddUserAsync(UserName userName, Resolution resolution = Resolution.PreMuxed) { try{ await client.GetStringAsync($"{url}api/v2/AddUser?v={userName.Value}&res={resolution.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task AddVideoAsync(VideoId id, Resolution resolution = Resolution.PreMuxed) { try{ await client.GetStringAsync($"{url}api/v2/AddVideo?v={id.Value}&res={resolution.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task AddFileAsync(string url,bool download=true) { try{ await client.GetStringAsync($"{url}api/v2/AddFile?url={WebUtility.UrlEncode(url)}&download={download}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public override async Task DirectoryExistsAsync(string path) { try{ string v=await client.GetStringAsync($"{url}api/Storage/DirectoryExists/{path}"); return v=="true"; }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } return false; } public async override IAsyncEnumerable EnumerateDirectoriesAsync(string path) { List items=null; try{ string v=await client.GetStringAsync($"{url}api/Storage/GetDirectory/{path}"); items=JsonConvert.DeserializeObject>(v); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } if(items==null) { yield break; }else{ foreach(var item in items) { yield return await Task.FromResult(item); } } } public async IAsyncEnumerable GetSubscriptionsAsync() { string v="[]"; try{ v=await client.GetStringAsync("{url}api/v2/subscriptions"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } foreach(var item in JsonConvert.DeserializeObject>(v)) { yield return await Task.FromResult(item); } } public async Task UnsubscribeAsync(ChannelId id) { try{ string v=await client.GetStringAsync($"{url}api/v2/unsubscribe?id={id.Value}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task SubscribeAsync(ChannelId id,ChannelBellInfo bellInfo = ChannelBellInfo.NotifyAndDownload) { try{ string v=await client.GetStringAsync($"{url}api/v2/subscribe?id={id.Value}&conf={bellInfo.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task SubscribeAsync(UserName name,ChannelBellInfo info=ChannelBellInfo.NotifyAndDownload) { try{ string v=await client.GetStringAsync($"{url}api/v2/subscribe?id={ WebUtility.UrlEncode(name.Value)}&conf={info.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task ResubscribeAsync(ChannelId id,ChannelBellInfo info=ChannelBellInfo.NotifyAndDownload) { try{ string v=await client.GetStringAsync($"{url}api/v2/resubscribe?id={id.Value}&conf={info.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async override IAsyncEnumerable EnumerateFilesAsync(string path) { List items=null; try{ string v=await client.GetStringAsync($"{url}api/Storage/GetFiles/{path}"); items=JsonConvert.DeserializeObject>(v); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } if(items==null) { yield break; }else{ foreach(var item in items) { yield return await Task.FromResult(item); } } } public async override Task FileExistsAsync(string path) { try{ string v=await client.GetStringAsync($"{url}api/Storage/FileExists/{path}"); return v=="true"; }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } return false; } private async Task> GetQueueListAsync() { try{ string v=await client.GetStringAsync($"{url}api/v2/QueueList"); return JsonConvert.DeserializeObject>(v); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } return new List<(SavedVideo video,Resolution resolution)>(); } private async Task GetProgressAsync() { try{ string v=await client.GetStringAsync($"{url}api/v2/Progress"); return JsonConvert.DeserializeObject(v); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } return null; } public SavedVideoProgress GetProgress() { if(hadBeenListeningToEvents) { return progress; }else{ return Task.Run(GetProgressAsync).GetAwaiter().GetResult(); } } public IReadOnlyList<(SavedVideo Video, Resolution Resolution)> GetQueueList() { return Task.Run(GetQueueListAsync).GetAwaiter().GetResult(); } public async override Task OpenReadAsync(string path) { try{ var strmLen= await client.TryGetContentLengthAsync($"{url}api/Storage/File/{path}",true); SegmentedHttpStream v=new SegmentedHttpStream(client,$"{url}api/Storage/File/{path}",strmLen.GetValueOrDefault(),null); return v; }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } return Stream.Null; } public async Task AddToPersonalPlaylistAsync(string name, IEnumerable items) { foreach(var item in items) { var response = await client.GetStringAsync($"{url}api/v2/AddToList?name={WebUtility.UrlEncode(name)}&v={item.Id}&res={item.Resolution.ToString()}"); } } public async Task ReplacePersonalPlaylistAsync(string name, IEnumerable items) { DeletePersonalPlaylist(name); await AddToPersonalPlaylistAsync(name,items); } public async Task RemoveItemFromPersonalPlaylistAsync(string name, VideoId id) { try{ await client.GetStringAsync($"{url}api/v2/DeleteFromList?name={WebUtility.UrlEncode(name)}&v={id.Value}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public async Task SetResolutionForItemInPersonalPlaylistAsync(string name, VideoId id, Resolution resolution) { try{ await client.GetStringAsync($"{url}api/v2/SetResolutionInList?name={WebUtility.UrlEncode(name)}&v={id.Value}&res={resolution.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public void DeletePersonalPlaylist(string name) { try{ client.GetStringAsync($"{url}api/v2/DeleteList?name={WebUtility.UrlEncode(name)}").GetAwaiter().GetResult(); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public void CancelDownload(bool restart = false) { try{ client.GetStringAsync($"{url}api/v2/CancelDownload?restart={restart}").GetAwaiter().GetResult(); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } SavedVideoProgress progress=new SavedVideoProgress(); CancellationTokenSource src=new CancellationTokenSource(); bool hadBeenListeningToEvents=false; private async Task _startEventStreamAsync() { try{ src=new CancellationTokenSource(); hadBeenListeningToEvents=true; var sse=await client.GetSSEClientAsync($"{url}api/v2/event"); sse.Event += sse_Event; await sse.ReadEventsAsync(src.Token); }catch(Exception ex){ _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } public void ResetEvents() { if(src != null) { src.Cancel(); src.Dispose(); } if(hadBeenListeningToEvents) { hadBeenListeningToEvents=false; _startEventStream(); } } public void CancelEvents() { if(src!= null){ src.Cancel(); src.Dispose(); } src=null; hadBeenListeningToEvents=false; } private event EventHandler _started_video; private event EventHandler _ended_video; private event EventHandler _progress_video; private event EventHandler _error; private event EventHandler _bell; public event EventHandler VideoStarted { add{ _started_video += value; _startEventStream(); } remove { _started_video-=value; } } public event EventHandler VideoProgress { add{ _progress_video += value; _startEventStream(); } remove { _progress_video -=value; } } public event EventHandler VideoFinished { add { _ended_video+=value; _startEventStream(); } remove { _ended_video -= value; } } public event EventHandler Error { add { _error += value; _startEventStream(); } remove { _error-=value; } } public event EventHandler Bell { add { _bell+=value; _startEventStream(); } remove { _bell-=value; } } private void sse_Event(object sender,SSEEvent evt) { bool started=false;bool ended=false;bool error=false;bool bell=false; try{ ProgressItem item = evt.ParseJson(); if(item != null) { if(item.Video != null) { progress.Video = item.Video; } if(item.StartEvent) { started = true; } if(item.StopEvent) { ended =true; } if(item.BellEvent) { } progress.Length = item.Length; progress.ProgressRaw=item.Percent; progress.Progress = ((int)(item.Percent * 100) % 101); if(started) { VideoStartedEventArgs evt0=new VideoStartedEventArgs(); evt0.EstimatedLength=item.Length; evt0.Resolution=Resolution.PreMuxed; evt0.VideoInfo=progress.Video; _started_video?.Invoke(this,evt0); }else if(ended) { VideoFinishedEventArgs evt0=new VideoFinishedEventArgs(); evt0.Resolution=Resolution.PreMuxed; evt0.VideoInfo=progress.Video; _ended_video?.Invoke(this,evt0); }else if(error) { TYTDErrorEventArgs evt0=new TYTDErrorEventArgs(item.Id,new TYTDException(item.Message,item.Error,item.ExceptionName)); _error?.Invoke(this,evt0); }else if(bell) { BellEventArgs evt0=new BellEventArgs(); evt0.Id = item.Id; _bell?.Invoke(this,evt0); } else{ VideoProgressEventArgs evt0=new VideoProgressEventArgs(); evt0.Progress = item.Percent; evt0.VideoInfo = progress.Video; _progress_video?.Invoke(this,evt0); } } } catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); } } private void _startEventStream() { Task.Run(_startEventStreamAsync).Wait(0); } public override ExtraData GetExtraData() { return Task.Run(async()=>{ string text= await client.GetStringAsync($"{url}api/v2/extra_data.json"); return JsonConvert.DeserializeObject(text); }).GetAwaiter().GetResult(); } } }