From 6a403b7d153f58902f1a03f3841937f5f81dc4cc Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Tue, 10 Jan 2023 11:03:44 -0600 Subject: [PATCH] Add Email Extra and add support for YouTube Handles and Slugs --- .../Tesses.YouTubeDownloader.MailKit/SMTP.cs | 298 ++++++++++++++++++ .../Tesses.YouTubeDownloader.MailKit.csproj | 28 ++ .../Config.cs | 3 + .../Program.cs | 1 + .../proxy.json | 8 +- Tesses.YouTubeDownloader.Server/Class1.cs | 125 +++++++- .../Tesses.YouTubeDownloader.Server.csproj | 6 +- Tesses.YouTubeDownloader/DownloadLoop.cs | 4 +- Tesses.YouTubeDownloader/IDownloader.cs | 4 +- Tesses.YouTubeDownloader/IStorage.cs | 1 + Tesses.YouTubeDownloader/ITYTDBase.cs | 3 + Tesses.YouTubeDownloader/PreMediaContext.cs | 37 ++- Tesses.YouTubeDownloader/TYTD.cs | 148 ++++++++- Tesses.YouTubeDownloader/TYTDBase.cs | 80 ++++- Tesses.YouTubeDownloader/TYTDClient.cs | 56 +++- .../TYTDIDownloaderStorageProxy.cs | 84 ++++- .../Tesses.YouTubeDownloader.csproj | 8 +- 17 files changed, 839 insertions(+), 55 deletions(-) create mode 100644 Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/SMTP.cs create mode 100644 Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/Tesses.YouTubeDownloader.MailKit.csproj diff --git a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/SMTP.cs b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/SMTP.cs new file mode 100644 index 0000000..4e525c0 --- /dev/null +++ b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/SMTP.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using YoutubeExplode.Channels; +using YoutubeExplode.Playlists; +using YoutubeExplode.Videos; +using MailKit.Net.Smtp; +using MimeKit; +using MailKit; +using MailKit.Net.Imap; +using System.IO; +using System.Threading; + +namespace Tesses.YouTubeDownloader.MailKit +{ + public sealed class SMTPDownloader : IDownloader + { + string fromEmail; + string toEmail; + Func> new_client; + public SMTPDownloader(Func> client,string fromEmail,string toEmail) + { + this.new_client = client; + this.fromEmail=fromEmail; + this.toEmail=toEmail; + } + + public SMTPDownloader(string smtpServer,string fromEmail,string toEmail,string username,string password,int port=587,global::MailKit.Security.SecureSocketOptions options=global::MailKit.Security.SecureSocketOptions.StartTls) + { + this.new_client = async ()=>{ + var clt= new SmtpClient(); + await clt.ConnectAsync(smtpServer,port,options); + await clt.AuthenticateAsync(username,password); + return clt; + }; + this.fromEmail=fromEmail; + this.toEmail = toEmail; + } + + public event EventHandler VideoStarted; + public event EventHandler VideoProgress; + public event EventHandler VideoFinished; + public event EventHandler Error; + public event EventHandler Bell; + + private async Task AddItemToAsync(string url) + { + using(var clt = await new_client()) + { + var messageToSend = new MimeMessage(); + messageToSend.From.Add(MailboxAddress.Parse(fromEmail)); + messageToSend.To.Add(MailboxAddress.Parse(toEmail)); + messageToSend.Body = new TextPart(MimeKit.Text.TextFormat.Plain){Text=url}; + messageToSend.Subject="Sent By Tesses.YouTubeDownloader.MailKit"; + await clt.SendAsync(messageToSend); + await clt.DisconnectAsync(true); + } + } + + public async Task AddChannelAsync(ChannelId id, Resolution resolution = Resolution.PreMuxed) + { + await AddItemToAsync($"https://www.youtube.com/channel/{id.Value}"); + } + + public async Task AddFileAsync(string url, bool download = true) + { + await Task.CompletedTask; + } + + public async Task AddPlaylistAsync(PlaylistId id, Resolution resolution = Resolution.PreMuxed) + { + await AddItemToAsync($"https://www.youtube.com/playlist?list={id.Value}"); + } + + public async Task AddToPersonalPlaylistAsync(string name, IEnumerable items) + { + await Task.CompletedTask; + } + + public async Task AddUserAsync(UserName userName, Resolution resolution = Resolution.PreMuxed) + { + await AddItemToAsync($"https://www.youtube.com/user/{userName.Value}"); + } + + public async Task AddVideoAsync(VideoId id, Resolution resolution = Resolution.PreMuxed) + { + await AddItemToAsync($"https://www.youtube.com/watch?v={id.Value}"); + } + public async Task AddSlugAsync(ChannelSlug slug,Resolution resolution=Resolution.PreMuxed) + { + await AddItemToAsync($"https://www.youtube.com/c/{slug.Value}"); + } + public async Task AddHandleAsync(ChannelHandle handle,Resolution resolution =Resolution.PreMuxed) + { + await AddItemToAsync($"https://www.youtube.com/@{handle.Value}"); + } + public void CancelDownload(bool restart = false) + { + + } + + public void DeletePersonalPlaylist(string name) + { + + } + + public ExtraData GetExtraData() + { + + return new ExtraData(); + } + + public async IAsyncEnumerable GetPersonalPlaylistContentsAsync(string name) + { + await Task.CompletedTask; + yield break; + } + + public SavedVideoProgress GetProgress() + { + return new SavedVideoProgress(); + } + + public IReadOnlyList<(SavedVideo Video, Resolution Resolution)> GetQueueList() + { + return new List<(SavedVideo Video,Resolution Resolution)>(); + } + + public async IAsyncEnumerable GetSubscriptionsAsync() + { + await Task.CompletedTask; + yield break; + } + + public bool PersonalPlaylistExists(string name) + { + return false; + } + + public async Task RemoveItemFromPersonalPlaylistAsync(string name, VideoId id) + { + await Task.CompletedTask; + } + + public async Task ReplacePersonalPlaylistAsync(string name, IEnumerable items) + { + await Task.CompletedTask; + } + + public async Task ResubscribeAsync(ChannelId id, ChannelBellInfo info = ChannelBellInfo.NotifyAndDownload) + { + await Task.CompletedTask; + } + + public async Task SetResolutionForItemInPersonalPlaylistAsync(string name, VideoId id, Resolution resolution) + { + await Task.CompletedTask; + } + + public async Task SubscribeAsync(ChannelId id, ChannelBellInfo bellInfo = ChannelBellInfo.NotifyAndDownload) + { + await Task.CompletedTask; + } + + public async Task SubscribeAsync(UserName name, ChannelBellInfo info = ChannelBellInfo.NotifyAndDownload) + { + await Task.CompletedTask; + } + + public async Task UnsubscribeAsync(ChannelId id) + { + await Task.CompletedTask; + } + } + + public sealed class IMAPDownloader + { + IDownloader downloader; + string imapServer; + int imapPort; + bool ssl; + string username; + string password; + string[] folders; + public IMAPDownloader(IDownloader downloader,string imapServer,int imapPort,bool ssl,string username,string password,params string[] folders) + { + this.downloader=downloader; + this.imapServer =imapServer; + this.imapPort = imapPort; + this.ssl = ssl; + this.username = username; + this.password = password; + this.folders = folders; + } + + public void Scan() + { + using (var client = new ImapClient()) + { + client.Connect(imapServer, imapPort,ssl); + client.Authenticate(username,password); + foreach (var folder_name in folders) + { + var folder = client.GetFolder(folder_name); + folder.Open(FolderAccess.ReadOnly); + foreach(var item in folder) + { + using (var ms = new MemoryStream()) + { + item.Body.WriteTo(ms); + ms.Position = 0; + using (var sr = new StreamReader(ms)) + { + string line; + while((line=sr.ReadLine()) != null) + { + try + { + Task.Run(async () => await downloader.AddItemAsync(line)).Wait(); + } + catch(Exception ex) + { + _ = ex; + } + } + } + } + + } + } + client.Disconnect(true); + + } + } + + public async Task ScanAsync(CancellationToken token=default) + { + using (var client = new ImapClient()) + { + await client.ConnectAsync(imapServer, imapPort,ssl,token); + await client.AuthenticateAsync(username,password,token); + foreach (var folder_name in folders) + { + if(token.IsCancellationRequested) break; + var folder = await client.GetFolderAsync(folder_name,token); + await folder.OpenAsync(FolderAccess.ReadOnly,token); + foreach(var item in folder) + { + if(token.IsCancellationRequested) break; + using (var ms = new MemoryStream()) + { + item.Body.WriteTo(ms); + ms.Position = 0; + using (var sr = new StreamReader(ms)) + { + string line; + while((line=await sr.ReadLineAsync()) != null) + { + if(token.IsCancellationRequested) break; + try + { + await downloader.AddItemAsync(line); + } + catch(Exception ex) + { + _ = ex; + } + } + } + } + + } + } + await client.DisconnectAsync(true,token); + + } + } + + public void ScanLoop(TimeSpan interval,CancellationToken token=default) + { + Thread t = new Thread(async()=>{ + try{ + while(!token.IsCancellationRequested) + { + await ScanAsync(token); + await Task.Delay(interval.Milliseconds,token); + } + }catch(Exception ex) + { + _=ex; + } + }); + t.Start(); + } + + } + +} diff --git a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/Tesses.YouTubeDownloader.MailKit.csproj b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/Tesses.YouTubeDownloader.MailKit.csproj new file mode 100644 index 0000000..cf5ff2f --- /dev/null +++ b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.MailKit/Tesses.YouTubeDownloader.MailKit.csproj @@ -0,0 +1,28 @@ + + + + + + + + + + + + netstandard2.0 + 8.0 + true + Tesses.YouTubeDownloader.MailKit + Mike Nolan + Tesses + 1.0.0 + 1.0.0 + 1.0.0 + A YouTube Downloader + LGPL-3.0-only + true + YoutubeExplode, YouTube, YouTubeDownloader + https://gitlab.tesses.cf/tesses50/tytd + + + diff --git a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Config.cs b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Config.cs index 8f2ad8c..99f794a 100644 --- a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Config.cs +++ b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Config.cs @@ -6,11 +6,14 @@ public class TYTDConfiguration { Url = "http://127.0.0.1:3252/"; LocalFiles=Environment.CurrentDirectory; + AddComplete = true; } public string Url {get;set;} public string LocalFiles {get;set;} + public bool AddComplete {get;set;} + public static TYTDConfiguration Load() { if(!File.Exists("proxy.json")) return new TYTDConfiguration(); diff --git a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Program.cs b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Program.cs index 5eee806..d8cc238 100644 --- a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Program.cs +++ b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/Program.cs @@ -12,6 +12,7 @@ TYTDClient client=new TYTDClient(c,config.Url); TYTDDownloaderStorageProxy proxy=new TYTDDownloaderStorageProxy(); proxy.Storage = currentDirectory; proxy.Downloader=client; +proxy.AddIfCompletedInStorage = config.AddComplete; TYTDServer server=new TYTDServer(proxy); server.RootServer.Server=new StaticServer("WebSite"); diff --git a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/proxy.json b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/proxy.json index 8b424a3..23e06d7 100644 --- a/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/proxy.json +++ b/Tesses.YouTubeDownloader.Extras/Tesses.YouTubeDownloader.ServerProxy/proxy.json @@ -1,4 +1,6 @@ { - "Url": "http://10.137.42.142:3252/", - "LocalFiles": "/media/mike/PhotoDrive/wii-vids/working" -} \ No newline at end of file + "Url": "http://192.168.0.142:3252/", + "LocalFiles": "/media/mike/BackupPlus4/Videos/TYTD_working", + "AddComplete": false, + "AddPlaylistsToStorage": true +} diff --git a/Tesses.YouTubeDownloader.Server/Class1.cs b/Tesses.YouTubeDownloader.Server/Class1.cs index eac6299..2e4d6ee 100644 --- a/Tesses.YouTubeDownloader.Server/Class1.cs +++ b/Tesses.YouTubeDownloader.Server/Class1.cs @@ -405,7 +405,20 @@ internal static class B64 await ctx.SendJsonAsync(data2.ToLegacy()); } else*/ - if(path.StartsWith("/File/FileInfo/")) + if(path.StartsWith("/File/DownloadsInfo/")) + { + string file = Path.GetFileNameWithoutExtension(WebUtility.UrlDecode(path.Substring("/File/DownloadsInfo/"))); + string url = B64.Base64UrlDecodes(file); + if(baseCtl.DownloadExists(url)) + { + var obj=await baseCtl.GetDownloadInfoAsync(url); + await ctx.SendJsonAsync(obj); + }else{ + ctx.StatusCode = 404; + ctx.NetworkStream.Close(); + } + } + else if(path.StartsWith("/File/FileInfo/")) { string file = Path.GetFileNameWithoutExtension(WebUtility.UrlDecode(path.Substring("/File/FileInfo/"))); string url = B64.Base64UrlDecodes(file); @@ -493,6 +506,15 @@ internal static class B64 await ctx.SendStreamAsync(s); } }*/ + if(path.StartsWith("/GetFiles/DownloadsInfo") || path.StartsWith("/GetFiles/DownloadsInfo/")) + { + List urls=new List(); + await foreach(var url in baseCtl.GetDownloadUrlsAsync()) + { + urls.Add($"{TYTDBase.HashDownloadUrl(url)}.json"); + } + await ctx.SendJsonAsync(urls); + } else if(path.StartsWith("/GetFiles/FileInfo") || path.StartsWith("/GetFiles/FileInfo/")) { List urls=new List(); @@ -552,9 +574,23 @@ internal static class B64 await ctx.SendTextAsync( "false","text/plain"); } } + else if(path.StartsWith("/FileExists/DownloadsInfo/")) + { + string file = Path.GetFileNameWithoutExtension(WebUtility.UrlDecode(path.Substring("/FileExists/DownloadsInfo/"))); + await foreach(var item in baseCtl.GetDownloadUrlsAsync()) + { + if(TYTDBase.HashDownloadUrl(item) == file) + { + await ctx.SendTextAsync( "true","text/plain"); + return; + } + } + await ctx.SendTextAsync( "false","text/plain"); + + } else if(path.StartsWith("/FileExists/FileInfo/")) { - string file = Path.GetFileNameWithoutExtension(WebUtility.UrlDecode(path.Substring("/FileExists/StreamInfo/"))); + string file = Path.GetFileNameWithoutExtension(WebUtility.UrlDecode(path.Substring("/FileExists/FileInfo/"))); string url = B64.Base64Decodes(file); if(baseCtl.DownloadExists(url)) @@ -859,10 +895,13 @@ internal static class B64 /*Adding items*/ AddBoth("/AddVideo",AddVideo,new SwagmeDocumentation("/AddVideo?v=jNQXAC9IVRw&res=PreMuxed","Add youtube video","v: Video Id Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); AddBoth("/AddPlaylist",AddPlaylist,new SwagmeDocumentation("/AddPlaylist?id=PLgXAgLm6Kre7M3c8G2OlQTG-PETLHs4Vd&res=PreMuxed","Add youtube playlist","id: Playlist Id Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); - AddBoth("/AddChannel",AddChannel,new SwagmeDocumentation("/AddChannel?id=UC4QobU6STFB0P71PMvOGN5A&res=PreMuxed","Add youtube channel","id: YouTube Channel Id Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); - AddBoth("/AddUser",AddUser,new SwagmeDocumentation("/AddUser?id=jawed&res=PreMuxed","Add youtube user","id: YouTube Channel Name Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); + AddBoth("/AddChannel",AddChannel,new SwagmeDocumentation("/AddChannel?id=UC4QobU6STFB0P71PMvOGN5A&res=PreMuxed","Add youtube channel","id: YouTube Channel Id Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); + AddBoth("/AddUser",AddUser,new SwagmeDocumentation("/AddUser?id=jawed&res=PreMuxed","Add youtube user","id: YouTube Channel Name Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); + AddBoth("/AddSlug",AddSlug,new SwagmeDocumentation("/AddSlug?id=https%3A%2F%2Fwww.youtube.com%2Fc%2FLinusTechTips&res=PreMuxed","Add youtube channel by slug","id: YouTube Channel with slug Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); + AddBoth("/AddHandle",AddHandle,new SwagmeDocumentation("/AddHandle?id=@tesses50&res=PreMuxed","Add youtube channel by handle","id: YouTube Channel with handle Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); + AddBoth("/AddItem",AddItem,new SwagmeDocumentation("/AddItem?v=jNQXAC9IVRw&res=PreMuxed","Add any type of item","v: Media Id Or encodeUriComponent Url
res: NoDownload=Do not download clip but just info, Mux=Requires conversion, PreMuxed=Does not require conversion, AudioOnly=Best audio only, VideoOnly=Best video only"),"Adding items"); - AddBoth("/AddFile",AddFile,new SwagmeDocumentation("/AddFile?url=https%3A%2F%2Ftesses.cf%2Fimages%2Frvl.jpg&download=true","Add normal file download","url: Url to file
download: whether to download file"),"Adding items"); + AddBoth("/AddFile",AddFile,new SwagmeDocumentation("/AddFile?url=https%3A%2F%2Ftesses.cf%2Fimages%2Frvl.jpg&download=true","Add normal file download","url: Url to file
download: whether to download file"),"Adding items"); /*Getting status*/ AddBoth("/event",Event,new SwagmeDocumentation("Server sent events","Returns events with json"),"Getting status"); AddBoth("/Progress",ProgressFunc,new SwagmeDocumentation("Get video progress","More Info"),"Getting status"); @@ -870,13 +909,13 @@ internal static class B64 /*Subscriptions*/ AddBoth("/Subscribe",Subscribe,new SwagmeDocumentation("/Subscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=NotifyAndDownload","Subscribe to youtuber","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); - AddBoth("/subscribe",Subscribe,new SwagmeDocumentation("/subscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=NotifyAndDownload","Subscribe to youtuber","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); - AddBoth("/Resubscribe",Resubscribe,new SwagmeDocumentation("/Resubscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=Download","Change subscription settings","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); - AddBoth("/resubscribe",Resubscribe,new SwagmeDocumentation("/resubscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=Download","Change subscription settings","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); - AddBoth("/Unsubscribe",Unsubscribe,new SwagmeDocumentation("/Unsubscribe?id=UC4QobU6STFB0P71PMvOGN5A","Unsubscribe from youtuber","id: Channel Id"),"Subscriptions"); - AddBoth("/unsubscribe",Unsubscribe,new SwagmeDocumentation("/unsubscribe?id=UC4QobU6STFB0P71PMvOGN5A","Unsubscribe from youtuber","id: Channel Id"),"Subscriptions"); - AddBoth("/Subscriptions",Subscriptions,new SwagmeDocumentation("Get subscriptions","Returned Json array
 Id: Channel Id
 BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions"); - AddBoth("/subscriptions",Subscriptions,new SwagmeDocumentation("Get subscriptions","Returned Json array
 Id: Channel Id
 BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions"); + AddBoth("/subscribe",Subscribe,new SwagmeDocumentation("/subscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=NotifyAndDownload","Subscribe to youtuber","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); + AddBoth("/Resubscribe",Resubscribe,new SwagmeDocumentation("/Resubscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=Download","Change subscription settings","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); + AddBoth("/resubscribe",Resubscribe,new SwagmeDocumentation("/resubscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=Download","Change subscription settings","id: Channel Id
conf: DoNothing=Disable, GetInfo=Just Get info, Notify=Bell event, Download=Download video no bell event, NotifyAndDownload=Download video with bell event"),"Subscriptions"); + AddBoth("/Unsubscribe",Unsubscribe,new SwagmeDocumentation("/Unsubscribe?id=UC4QobU6STFB0P71PMvOGN5A","Unsubscribe from youtuber","id: Channel Id"),"Subscriptions"); + AddBoth("/unsubscribe",Unsubscribe,new SwagmeDocumentation("/unsubscribe?id=UC4QobU6STFB0P71PMvOGN5A","Unsubscribe from youtuber","id: Channel Id"),"Subscriptions"); + AddBoth("/Subscriptions",Subscriptions,new SwagmeDocumentation("Get subscriptions","Returned Json array
 Id: Channel Id
 BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions"); + AddBoth("/subscriptions",Subscriptions,new SwagmeDocumentation("Get subscriptions","Returned Json array
 Id: Channel Id
 BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions"); /*Personal Lists*/ @@ -901,7 +940,7 @@ internal static class B64 AddBoth("/extra_data.json",ExtraData,new SwagmeDocumentation("Get extra info about downloader","Get extra data such as TYTD Tag, item count in queue and tytd version")); AddBoth("/CancelDownload",Cancel,new SwagmeDocumentation("/CancelDownload?restart=true","Cancel or Restart download","restart:
 true: Restart download
 false: Cancel Download")); - + AddBoth("/Thumbnail",Thumbnail,new SwagmeDocumentation("/Thumbnail?v=PzUKeGZiEl0&res=maxresdefault","Get Thumbnail for video","v: Video Id
res: YouTube Thumbnail Resolution List of resolutions"),"Other"); /* public async Task AddToPersonalPlaylistAsync(string name, IEnumerable<(VideoId Id, Resolution Resolution)> items) { @@ -922,6 +961,20 @@ internal static class B64 { throw new NotImplementedException(); }*/ + } + public async Task Thumbnail(ServerContext ctx) + { + string v=""; + string res=""; + VideoId? id=null; + ITYTDBase baseCtl = Downloader as ITYTDBase; + if(ctx.QueryParams.TryGetFirst("v",out v) && ctx.QueryParams.TryGetFirst("res",out res) && (id=VideoId.TryParse(v)).HasValue && baseCtl != null) + { + await ctx.SendBytesAsync(await baseCtl.ReadThumbnailAsync(id.Value,res),"image/jpg"); + }else{ + await ctx.SendTextAsync("Expected v=YouTubeVideoId\r\nres=a youtube thumbnail resolution\r\nAnd Downloader must implement ITYTDBase","text/plain"); + } + } public async Task ExtraData(ServerContext ctx) { @@ -1496,7 +1549,51 @@ internal static class B64 } await ctx.RedirectBackAsync(); } - + public async Task AddHandle(ServerContext ctx) + { + string id; + if(ctx.QueryParams.TryGetFirst("id",out id)) + { + Resolution resolution=Resolution.PreMuxed; + string res; + if(ctx.QueryParams.TryGetFirst("res",out res)) + { + if(!Enum.TryParse(res,out resolution)) + { + resolution=Resolution.PreMuxed; + } + } + ChannelHandle? id1=ChannelHandle.TryParse(id); + if(id1.HasValue) + { + await Downloader.AddHandleAsync(id1.Value,resolution); + } + } + await ctx.RedirectBackAsync(); + } + + public async Task AddSlug(ServerContext ctx) + { + string id; + if(ctx.QueryParams.TryGetFirst("id",out id)) + { + Resolution resolution=Resolution.PreMuxed; + string res; + if(ctx.QueryParams.TryGetFirst("res",out res)) + { + if(!Enum.TryParse(res,out resolution)) + { + resolution=Resolution.PreMuxed; + } + } + ChannelSlug? id1=ChannelSlug.TryParse(id); + if(id1.HasValue) + { + await Downloader.AddSlugAsync(id1.Value,resolution); + } + } + await ctx.RedirectBackAsync(); + } public async Task AddChannel(ServerContext ctx) { string id; diff --git a/Tesses.YouTubeDownloader.Server/Tesses.YouTubeDownloader.Server.csproj b/Tesses.YouTubeDownloader.Server/Tesses.YouTubeDownloader.Server.csproj index be0fced..10666a9 100644 --- a/Tesses.YouTubeDownloader.Server/Tesses.YouTubeDownloader.Server.csproj +++ b/Tesses.YouTubeDownloader.Server/Tesses.YouTubeDownloader.Server.csproj @@ -16,9 +16,9 @@ Tesses.YouTubeDownloader.Server Mike Nolan Tesses - 1.1.8 - 1.1.8 - 1.1.8 + 2.0.1 + 2.0.1 + 2.0.1 Adds WebServer to TYTD LGPL-3.0-only true diff --git a/Tesses.YouTubeDownloader/DownloadLoop.cs b/Tesses.YouTubeDownloader/DownloadLoop.cs index 715899b..2745e87 100644 --- a/Tesses.YouTubeDownloader/DownloadLoop.cs +++ b/Tesses.YouTubeDownloader/DownloadLoop.cs @@ -184,8 +184,8 @@ namespace Tesses.YouTubeDownloader private async Task DownloadFileAsync(SavedVideo video, CancellationToken token, IProgress progress, bool report) { - string incomplete_file_path = $"Download/{B64.Base64UrlEncodes(video.Id)}-incomplete.part"; - string file_path = $"Download/{B64.Base64UrlEncodes(video.Id)}.bin"; + string incomplete_file_path = $"Downloads/{HashDownloadUrl(video.Id)}-incomplete.part"; + string file_path = $"Downloads/{HashDownloadUrl(video.Id)}.bin"; string url = video.Id; bool canSeek=false; long length=0; diff --git a/Tesses.YouTubeDownloader/IDownloader.cs b/Tesses.YouTubeDownloader/IDownloader.cs index cb5798d..ef80738 100644 --- a/Tesses.YouTubeDownloader/IDownloader.cs +++ b/Tesses.YouTubeDownloader/IDownloader.cs @@ -23,10 +23,12 @@ namespace Tesses.YouTubeDownloader event EventHandler Bell; void CancelDownload(bool restart=false); + Task AddVideoAsync(VideoId id,Resolution resolution=Resolution.PreMuxed); Task AddPlaylistAsync(PlaylistId id,Resolution resolution=Resolution.PreMuxed); Task AddChannelAsync(ChannelId id,Resolution resolution=Resolution.PreMuxed); - + Task AddHandleAsync(ChannelHandle handle,Resolution resolution=Resolution.PreMuxed); + Task AddSlugAsync(ChannelSlug handle,Resolution resolution=Resolution.PreMuxed); Task AddUserAsync(UserName userName,Resolution resolution=Resolution.PreMuxed); Task AddFileAsync(string url,bool download=true); IReadOnlyList<(SavedVideo Video,Resolution Resolution)> GetQueueList(); diff --git a/Tesses.YouTubeDownloader/IStorage.cs b/Tesses.YouTubeDownloader/IStorage.cs index 800e8df..f60c905 100644 --- a/Tesses.YouTubeDownloader/IStorage.cs +++ b/Tesses.YouTubeDownloader/IStorage.cs @@ -15,6 +15,7 @@ namespace Tesses.YouTubeDownloader { public interface IStorage : IWritable, IDownloader, ITYTDBase { + Task WriteThumbnailAsync(VideoId videoId,string res,byte[] data,CancellationToken token=default); void WaitTillMediaContentQueueEmpty(); Task WriteBestStreamInfoAsync(VideoId id,BestStreamInfo.BestStreamsSerialized serialized); diff --git a/Tesses.YouTubeDownloader/ITYTDBase.cs b/Tesses.YouTubeDownloader/ITYTDBase.cs index 073d495..52a7937 100644 --- a/Tesses.YouTubeDownloader/ITYTDBase.cs +++ b/Tesses.YouTubeDownloader/ITYTDBase.cs @@ -16,6 +16,9 @@ namespace Tesses.YouTubeDownloader public interface ITYTDBase : IPersonalPlaylistGet { + Task ThumbnailExistsAsync(VideoId videoId,string res); + bool ThumbnailExists(VideoId videoId,string res); + Task ReadThumbnailAsync(VideoId videoId,string res,CancellationToken token=default); IAsyncEnumerable GetDownloadUrlsAsync(); IAsyncEnumerable GetDownloadsAsync(); Task GetDownloadInfoAsync(string url); diff --git a/Tesses.YouTubeDownloader/PreMediaContext.cs b/Tesses.YouTubeDownloader/PreMediaContext.cs index 9d50124..20c7419 100644 --- a/Tesses.YouTubeDownloader/PreMediaContext.cs +++ b/Tesses.YouTubeDownloader/PreMediaContext.cs @@ -18,6 +18,16 @@ namespace Tesses.YouTubeDownloader internal class ChannelMediaContext : IMediaContext { + public ChannelMediaContext(ChannelSlug slug,Resolution resolution) + { + this.slug=slug; + this.Resolution = resolution; + } + public ChannelMediaContext(ChannelHandle handle,Resolution resolution) + { + this.handle=handle; + this.Resolution = resolution; + } public ChannelMediaContext(ChannelId id,Resolution resolution) { Id=id; @@ -29,9 +39,12 @@ namespace Tesses.YouTubeDownloader Resolution=resolution; } Resolution Resolution; - UserName name1; + UserName? name1; ChannelId? Id; //made me nullable + ChannelHandle handle; + ChannelSlug? slug; + public async Task GetChannel(TYTDStorage storage) { SavedChannel channel; @@ -54,8 +67,8 @@ namespace Tesses.YouTubeDownloader var j=await storage.GetChannelInfoAsync(Id.Value); return j; } - }else{ - var c=await storage.YoutubeClient.Channels.GetByUserAsync(name1); + }else if(name1.HasValue){ + var c=await storage.YoutubeClient.Channels.GetByUserAsync(name1.Value); channel=await DownloadThumbnails(storage,c); //string path=$"Channel/{c.Id.Value}.json"; if(!storage.ChannelInfoExists(c.Id.Value)) @@ -64,6 +77,24 @@ namespace Tesses.YouTubeDownloader } return channel; + }else if(slug.HasValue) + { + var c = await storage.YoutubeClient.Channels.GetBySlugAsync(slug.Value); + channel = await DownloadThumbnails(storage,c); + if(!storage.ChannelInfoExists(c.Id.Value)) + { + await storage.WriteChannelInfoAsync(channel); + } + return channel; + } + else{ + var c = await storage.YoutubeClient.Channels.GetByHandleAsync(handle); + channel = await DownloadThumbnails(storage,c); + if(!storage.ChannelInfoExists(c.Id.Value)) + { + await storage.WriteChannelInfoAsync(channel); + } + return channel; } } private async Task DownloadThumbnails(TYTDStorage storage,YoutubeExplode.Channels.Channel channel) diff --git a/Tesses.YouTubeDownloader/TYTD.cs b/Tesses.YouTubeDownloader/TYTD.cs index 8572ece..0231e00 100644 --- a/Tesses.YouTubeDownloader/TYTD.cs +++ b/Tesses.YouTubeDownloader/TYTD.cs @@ -17,6 +17,23 @@ namespace Tesses.YouTubeDownloader public abstract partial class TYTDStorage : TYTDBase, IStorage { + public new virtual async Task ReadThumbnailAsync(VideoId videoId,string res,CancellationToken token=default) + { + CreateDirectoryIfNotExist($"Thumbnails/{videoId.Value}"); + if(await ThumbnailExistsAsync(videoId,res)) + { + return await ReadAllBytesAsync($"Thumbnails/{videoId.Value}/{res}.jpg",token); + }else{ + var result= await HttpClient.GetByteArrayAsync($"https://s.ytimg.com/vi/{videoId.Value}/{res}.jpg"); + await WriteThumbnailAsync(videoId,res,result,token); + return result; + } + } + public virtual async Task WriteThumbnailAsync(VideoId videoId,string res,byte[] data,CancellationToken token=default) + { + CreateDirectoryIfNotExist($"Thumbnails/{videoId.Value}"); + await WriteAllBytesAsync($"Thumbnails/{videoId.Value}/{res}.jpg",data,token); + } public override ExtraData GetExtraData() { ExtraData data=new ExtraData(); @@ -110,7 +127,7 @@ namespace Tesses.YouTubeDownloader public virtual async Task WriteVideoInfoAsync(SavedVideo info) { - string file = info.DownloadFrom.StartsWith("NormalDownload,Length=") ? $"FileInfo/{B64.Base64UrlEncodes(info.Id)}.json" : $"Info/{info.Id}.json"; + string file = info.DownloadFrom.StartsWith("NormalDownload,Length=") ? $"DownloadsInfo/{HashDownloadUrl(info.Id)}.json" : $"Info/{info.Id}.json"; if(!FileExists(file)) { await WriteAllTextAsync(file,JsonConvert.SerializeObject(info)); @@ -150,6 +167,22 @@ namespace Tesses.YouTubeDownloader } await Task.FromResult(0); } + public async Task AddSlugAsync(ChannelSlug slug,Resolution resolution=Resolution.PreMuxed) + { + lock(Temporary) + { + Temporary.Add(new ChannelMediaContext(slug,resolution)); + } + await Task.FromResult(0); + } + public async Task AddHandleAsync(ChannelHandle handle,Resolution resolution=Resolution.PreMuxed) + { + lock(Temporary) + { + Temporary.Add(new ChannelMediaContext(handle,resolution)); + } + await Task.FromResult(0); + } public async Task AddUserAsync(UserName name,Resolution resolution=Resolution.PreMuxed) { lock(Temporary) @@ -215,14 +248,16 @@ namespace Tesses.YouTubeDownloader CreateDirectoryIfNotExist("Thumbnails"); CreateDirectoryIfNotExist("config"); CreateDirectoryIfNotExist("config/logs"); - CreateDirectoryIfNotExist("FileInfo"); - CreateDirectoryIfNotExist("Download"); + CreateDirectoryIfNotExist("DownloadsInfo"); + CreateDirectoryIfNotExist("Downloads"); CreateDirectoryIfNotExist("StreamInfo"); CreateDirectoryIfNotExist("PersonalPlaylist"); } public void StartLoop(CancellationToken token = default(CancellationToken)) { CreateDirectories(); + if(DirectoryExists("Download") && DirectoryExists("FileInfo")) + Task.Run(MigrateDownloads).Wait(); Thread thread0=new Thread(()=>{ DownloadLoop(token).Wait(); }); @@ -232,6 +267,113 @@ namespace Tesses.YouTubeDownloader }); thread1.Start(); } + private async IAsyncEnumerable GetDownloadsLegacyAsync() + { + await foreach(var item in EnumerateFilesAsync("FileInfo")) + { + if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) + { + var res= JsonConvert.DeserializeObject(await ReadAllTextAsync(item)); + DeleteFile(item); + yield return res; + } + } + + } + private async Task MigrateDownloads() + { + await GetLogger().WriteAsync("Migrating File Downloads (Please Don't close TYTD)",true); + + await foreach(var dl in GetDownloadsLegacyAsync()) + { + await MoveLegacyDownload(dl); + } + int files=0; + await foreach(var f in EnumerateDirectoriesAsync("FileInfo")) + { + files++; + break; + } + if(files==0) + { + await foreach(var f in EnumerateFilesAsync("FileInfo")) + { + files++; + break; + } + } + if(files>0) + { + MoveDirectory("FileInfo","DownloadsInfoLegacy"); + await GetLogger().WriteAsync("WARNING: there were still files/folders in FileInfo so they are stored in DownloadsInfoLegacy",true); + }else{ + DeleteDirectory("FileInfo"); + } + files=0; + await foreach(var f in EnumerateDirectoriesAsync("Download")) + { + files++; + break; + } + if(files==0) + { + await foreach(var f in EnumerateFilesAsync("Download")) + { + files++; + break; + } + } + if(files>0) + { + MoveDirectory("Download","DownloadsLegacy"); + await GetLogger().WriteAsync("WARNING: there were still files/folders in Download so they are stored in DownloadsLegacy",true); + }else{ + DeleteDirectory("Download"); + } + + + await GetLogger().WriteAsync("Migrating Downloads Complete",true); + } + private async Task MoveLegacyDownload(SavedVideo dl) + { + await WriteVideoInfoAsync(dl); + string old_incomplete_file_path = $"Download/{B64.Base64UrlEncodes(dl.Id)}-incomplete.part"; + string old_file_path = $"Download/{B64.Base64UrlEncodes(dl.Id)}.bin"; + + string incomplete_file_path = $"Downloads/{HashDownloadUrl(dl.Id)}-incomplete.part"; + string file_path = $"Downloads/{HashDownloadUrl(dl.Id)}.bin"; + + bool complete = FileExists(old_file_path); + bool missing = !complete && !FileExists(old_incomplete_file_path); + string alreadyStr =""; + if(!missing) + { + if(complete) + { + //migrate complete + if(!FileExists(file_path)) + { + RenameFile(old_file_path,file_path); + }else{ + alreadyStr="Already "; + } + } + else + { + //migrate incomplete + if(!FileExists(incomplete_file_path)) + { + RenameFile(old_incomplete_file_path,incomplete_file_path); + }else{ + alreadyStr="Already "; + } + } + } + + + await GetLogger().WriteAsync($"{alreadyStr}Migrated {(missing? "missing" : (complete ? "complete" : "incomplete"))} download: {dl.Title} with Url: {dl.Id}",true); + + } internal void ThrowError(TYTDErrorEventArgs e) { Error?.Invoke(this,e); diff --git a/Tesses.YouTubeDownloader/TYTDBase.cs b/Tesses.YouTubeDownloader/TYTDBase.cs index 7d37245..a8926cf 100644 --- a/Tesses.YouTubeDownloader/TYTDBase.cs +++ b/Tesses.YouTubeDownloader/TYTDBase.cs @@ -12,13 +12,30 @@ using System.IO; using YoutubeExplode.Playlists; using YoutubeExplode.Channels; using YoutubeExplode.Search; +using System.Security.Cryptography; +using System.Text; namespace Tesses.YouTubeDownloader { - public abstract class TYTDBase : ITYTDBase + public abstract class TYTDBase : ITYTDBase { - + public virtual async Task ThumbnailExistsAsync(VideoId videoId,string res) + { + return await FileExistsAsync($"Thumbnails/{videoId.Value}/{res}.jpg"); + } + public virtual bool ThumbnailExists(VideoId videoId,string res) + { + return FileExists($"Thumbnails/{videoId.Value}/{res}.jpg"); + } + public virtual async Task ReadThumbnailAsync(VideoId videoId,string res,CancellationToken token=default) + { + if(await ThumbnailExistsAsync(videoId,res)) + { + return await ReadAllBytesAsync($"Thumbnails/{videoId.Value}/{res}.jpg",token); + } + return new byte[0]; + } public virtual bool PersonalPlaylistExists(string name) { return FileExists($"PersonalPlaylist/{name}.json"); @@ -179,21 +196,23 @@ namespace Tesses.YouTubeDownloader public virtual async IAsyncEnumerable GetDownloadsAsync() { - await foreach(var item in GetDownloadUrlsAsync()) + await foreach(var item in EnumerateFilesAsync("DownloadsInfo")) { - yield return await GetDownloadInfoAsync(item); + if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) + { + yield return JsonConvert.DeserializeObject(await ReadAllTextAsync(item)); + + } } } public virtual async IAsyncEnumerable GetDownloadUrlsAsync() { - await foreach(var item in EnumerateFilesAsync("FileInfo")) + await foreach(var item in GetDownloadsAsync()) { - if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) - { - yield return B64.Base64UrlDecodes(Path.GetFileNameWithoutExtension(item)); - } + yield return item.Id; } } + public virtual async Task GetBestStreamInfoAsync(VideoId id) { return JsonConvert.DeserializeObject(await ReadAllTextAsync($"StreamInfo/{id.Value}.json")); @@ -204,15 +223,23 @@ namespace Tesses.YouTubeDownloader } public virtual async Task GetDownloadInfoAsync(string url) { - string enc=$"FileInfo/{B64.Base64UrlEncodes(url)}.json"; + //string enc=$"FileInfo/{B64.Base64UrlEncodes(url)}.json"; + string enc=$"DownloadsInfo/{HashDownloadUrl(url)}.json"; return JsonConvert.DeserializeObject(await ReadAllTextAsync(enc)); } + public static string HashDownloadUrl(string url) + { + using(var sha1=SHA1.Create()){ + + return B64.Base64UrlEncode(sha1.ComputeHash(Encoding.UTF8.GetBytes(url))); + } + } public virtual bool DownloadExists(string url) { - string enc=$"FileInfo/{B64.Base64UrlEncodes(url)}.json"; + string enc=$"DownloadsInfo/{HashDownloadUrl(url)}.json"; return FileExists(enc); } @@ -282,6 +309,8 @@ namespace Tesses.YouTubeDownloader return data; } + + } public class ListContentItem @@ -397,6 +426,20 @@ namespace Tesses.YouTubeDownloader } public static class TYTDManager { + + public static async Task VideoExistsAsync(this ITYTDBase baseCtl,VideoId id,Resolution resolution=Resolution.PreMuxed) + { + return (await GetVideoPathAsync(baseCtl,id,resolution)).Exists; + } + public static async Task<(bool Exists,string Path)> GetVideoPathAsync(this ITYTDBase baseCtl,VideoId id,Resolution resolution=Resolution.PreMuxed) + { + if(!baseCtl.VideoInfoExists(id) || !baseCtl.BestStreamInfoExists(id)) return (false,""); + var video=await baseCtl.GetVideoInfoAsync(id); + var name = await BestStreams.GetPathResolution(baseCtl,video,resolution); + if(string.IsNullOrWhiteSpace(name)) return (false,""); + + return (await baseCtl.FileExistsAsync(name),name); + } public static async IAsyncEnumerable SearchYouTubeAsync(this IStorage storage, string query,bool getMediaInfo=true) { await foreach(var vid in storage.YoutubeClient.Search.GetResultsAsync(query)) @@ -419,7 +462,8 @@ namespace Tesses.YouTubeDownloader } public static string GetDownloadFile(string url) { - return $"Download/{B64.Base64UrlEncodes(url)}.bin"; + string file_path = $"Downloads/{TYTDBase.HashDownloadUrl(url)}.bin"; + return file_path; } /// /// Add Video, Playlist, Channel Or Username @@ -438,10 +482,13 @@ namespace Tesses.YouTubeDownloader public static async Task AddItemAsync(this IDownloader downloader,string url,Resolution resolution=Resolution.PreMuxed) { - VideoId? vid = VideoId.TryParse(url); + VideoId? vid = VideoId.TryParse(url); PlaylistId? pid = PlaylistId.TryParse(url); ChannelId? cid = ChannelId.TryParse(url); + ChannelSlug? slug = ChannelSlug.TryParse(url); + ChannelHandle? handle = ChannelHandle.TryParse(url); UserName? user = UserName.TryParse(url); + if (url.Length == 11) { @@ -465,6 +512,13 @@ namespace Tesses.YouTubeDownloader { await downloader.AddChannelAsync(cid.Value, resolution); } + else if(handle.HasValue) + { + await downloader.AddHandleAsync(handle.Value, resolution); + }else if(slug.HasValue) + { + await downloader.AddSlugAsync(slug.Value,resolution); + } else if (user.HasValue) { await downloader.AddUserAsync(user.Value, resolution); diff --git a/Tesses.YouTubeDownloader/TYTDClient.cs b/Tesses.YouTubeDownloader/TYTDClient.cs index e85d88e..4a57a12 100644 --- a/Tesses.YouTubeDownloader/TYTDClient.cs +++ b/Tesses.YouTubeDownloader/TYTDClient.cs @@ -195,6 +195,21 @@ internal class SegmentedHttpStream : Stream } public class TYTDClient : TYTDBase,IDownloader { + ExtraData data=null; + public override async Task ReadThumbnailAsync(VideoId videoId, string res, CancellationToken token = default) + { + if(CanReadThumbnailFromUrl()) + { + return await client.GetByteArrayAsync($"{url}api/v2/Thumbnail?v={videoId.Value}&res={res}"); + } + return await base.ReadThumbnailAsync(videoId,res,token); + } + private bool CanReadThumbnailFromUrl() + { + var data=GetExtraDataOnce(); + Version v=new Version(data.TYTDServerVersion); + return v.Major >= 2; + } string url; public TYTDClient(string url) { @@ -241,7 +256,7 @@ internal class SegmentedHttpStream : Stream public async Task AddUserAsync(UserName userName, Resolution resolution = Resolution.PreMuxed) { try{ - await client.GetStringAsync($"{url}api/v2/AddUser?v={userName.Value}&res={resolution.ToString()}"); + await client.GetStringAsync($"{url}api/v2/AddUser?id={userName.Value}&res={resolution.ToString()}"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); @@ -303,7 +318,7 @@ internal class SegmentedHttpStream : Stream { string v="[]"; try{ - v=await client.GetStringAsync("{url}api/v2/subscriptions"); + v=await client.GetStringAsync($"{url}api/v2/subscriptions"); }catch(Exception ex) { _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); @@ -694,14 +709,43 @@ internal class SegmentedHttpStream : Stream Task.Run(_startEventStreamAsync).Wait(0); } - - public override ExtraData GetExtraData() + public ExtraData GetExtraDataOnce() { - return Task.Run(async()=>{ + if(data ==null){ + data= Task.Run(async()=>{ string text= await client.GetStringAsync($"{url}api/v2/extra_data.json"); return JsonConvert.DeserializeObject(text); }).GetAwaiter().GetResult(); + } + return data; + } + 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(); + + + } + + public async Task AddHandleAsync(ChannelHandle handle, Resolution resolution = Resolution.PreMuxed) + { + try{ + await client.GetStringAsync($"{url}api/v2/AddHandle?id={handle.Value}&res={resolution.ToString()}"); + }catch(Exception ex) + { + _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); + } + } + public async Task AddSlugAsync(ChannelSlug handle, Resolution resolution = Resolution.PreMuxed) + { + try{ + await client.GetStringAsync($"{url}api/v2/AddSlug?id={handle.Value}&res={resolution.ToString()}"); + }catch(Exception ex) + { + _error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex)); + } } - } } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/TYTDIDownloaderStorageProxy.cs b/Tesses.YouTubeDownloader/TYTDIDownloaderStorageProxy.cs index 43aab4f..db80adf 100644 --- a/Tesses.YouTubeDownloader/TYTDIDownloaderStorageProxy.cs +++ b/Tesses.YouTubeDownloader/TYTDIDownloaderStorageProxy.cs @@ -15,6 +15,8 @@ namespace Tesses.YouTubeDownloader { public class TYTDDownloaderStorageProxy : IStorage { + public bool AddIfCompletedInStorage {get;set;}=true; + public bool ThumbnailsFromDownloader {get;set;}=true; public IDownloader Downloader {get{return _downloader;} set { SetDownloader(value); @@ -482,12 +484,22 @@ namespace Tesses.YouTubeDownloader yield return await Task.FromResult(item); } } - + private async Task SkipVideoAsync(VideoId id,Resolution resolution) + { + if(AddIfCompletedInStorage) return false; + if(await this.VideoExistsAsync(id,resolution)) + { + return true; + } + return false; + } public async Task AddVideoAsync(VideoId id, Resolution resolution = Resolution.PreMuxed) { + if(await SkipVideoAsync(id,resolution)) return; if(Downloader != null) await Downloader.AddVideoAsync(id,resolution); } + public async Task AddFileAsync(string url,bool download=true) { if(Downloader != null) @@ -511,7 +523,16 @@ namespace Tesses.YouTubeDownloader if(Downloader != null) await Downloader.AddUserAsync(userName,resolution); } - + public async Task AddHandleAsync(ChannelHandle handle, Resolution resolution = Resolution.PreMuxed) + { + if(Downloader != null) + await Downloader.AddHandleAsync(handle,resolution); + } + public async Task AddSlugAsync(ChannelSlug slug, Resolution resolution = Resolution.PreMuxed) + { + if(Downloader != null) + await Downloader.AddSlugAsync(slug,resolution); + } public IReadOnlyList<(SavedVideo Video, Resolution Resolution)> GetQueueList() { if(Downloader == null) return new List<(SavedVideo Video,Resolution Resolution)>(); @@ -868,8 +889,65 @@ namespace Tesses.YouTubeDownloader public ExtraData GetExtraData() { - throw new NotImplementedException(); + return Downloader.GetExtraData(); } + + public bool ThumbnailExists(VideoId videoId, string res) + { + bool exists=false; + if(ThumbnailsFromDownloader) + { + DownloaderAsITYTDBase((s)=>{ + exists = s.ThumbnailExists(videoId,res); + }); + }else{ + StorageAsStorage((s)=>{ + exists = s.ThumbnailExists(videoId,res); + }); + } + return exists; + } + + public async Task ReadThumbnailAsync(VideoId videoId, string res,CancellationToken token=default) + { + byte[] data=new byte[0]; + if(ThumbnailsFromDownloader) + { + await DownloaderAsITYTDBaseAsync(async (s)=>{ + data = await s.ReadThumbnailAsync(videoId,res,token); + }); + }else{ + await StorageAsStorageAsync(async(s)=>{ + data = await s.ReadThumbnailAsync(videoId,res,token); + }); + } + return data; + } + + public async Task WriteThumbnailAsync(VideoId videoId, string res, byte[] data, CancellationToken token = default) + { + await StorageAsStorageAsync(async(s)=>{ + await s.WriteThumbnailAsync(videoId,res,data,token); + }); + } + + public async Task ThumbnailExistsAsync(VideoId videoId, string res) + { + bool exists=false; + if(ThumbnailsFromDownloader) + { + await DownloaderAsITYTDBaseAsync(async(s)=>{ + exists = await s.ThumbnailExistsAsync(videoId,res); + }); + }else{ + await StorageAsStorageAsync(async(s)=>{ + exists = await s.ThumbnailExistsAsync(videoId,res); + }); + } + return exists; + } + + } public class DownloaderMigration diff --git a/Tesses.YouTubeDownloader/Tesses.YouTubeDownloader.csproj b/Tesses.YouTubeDownloader/Tesses.YouTubeDownloader.csproj index 50bfd83..71840a2 100644 --- a/Tesses.YouTubeDownloader/Tesses.YouTubeDownloader.csproj +++ b/Tesses.YouTubeDownloader/Tesses.YouTubeDownloader.csproj @@ -7,9 +7,9 @@ Tesses.YouTubeDownloader Mike Nolan Tesses - 1.2.6 - 1.2.6 - 1.2.6 + 2.0.1 + 2.0.1 + 2.0.1 A YouTube Downloader LGPL-3.0-only true @@ -20,7 +20,7 @@ - +