Compare commits

...

10 Commits

Author SHA1 Message Date
Mike Nolan 2ef439d162 Fix ReplaceList 2023-02-23 21:23:50 -06:00
Mike Nolan e498ba2fdc Fix some stuff 2023-02-23 20:59:20 -06:00
Mike Nolan 03408687a8 Fix some stuff 2023-02-23 20:59:09 -06:00
Mike Nolan 53b6f8b27f Remove crappy blazor class and add throttle to SSE 2023-02-17 12:34:18 -06:00
Mike Nolan 15cf9a6606 Fix TYTDClient for blazor 2023-02-16 11:44:34 -06:00
Mike Nolan e1046c33b5 Fix TYTDClient for blazor 2023-02-16 11:43:41 -06:00
Mike Nolan 26c9960003 Add GetRealUrlOrPathAsync for TYTDClient 2023-02-16 10:51:46 -06:00
Mike Nolan 4a03e3398e Now GPL 2023-02-16 08:48:50 -06:00
Mike Nolan 6a403b7d15 Add Email Extra and add support for YouTube Handles and Slugs 2023-01-10 11:03:44 -06:00
Mike Nolan c471162cf0 Personal playlists use get methods 2022-11-01 03:55:28 -05:00
25 changed files with 1413 additions and 730 deletions

View File

@ -1,22 +0,0 @@
Copyright (c) 2008-2011, Kenneth Bell
Copyright (c) 2014, Quamotion
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
DiscUtils: https://github.com/DiscUtils/DiscUtils

View File

@ -1,26 +0,0 @@
The MIT License (MIT)
Copyright (c) Darko Jurić
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Licensing for JSON.NET dependency also applies: https://www.newtonsoft.com/json
Licensing for SimpleHttp also applies: https://github.com/dajuric/simple-http
I Used same license hince I used some of dajurics code in Tesses.WebServer

View File

@ -1,25 +0,0 @@
Copyright (c) 2017-2022, Alexandre Mutel
All rights reserved.
Redistribution and use in source and binary forms, with or without modification
, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Zio: https://github.com/xoofx/zio

File diff suppressed because it is too large Load Diff

View File

@ -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<Task<SmtpClient>> new_client;
public SMTPDownloader(Func<Task<SmtpClient>> 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<VideoStartedEventArgs> VideoStarted;
public event EventHandler<VideoProgressEventArgs> VideoProgress;
public event EventHandler<VideoFinishedEventArgs> VideoFinished;
public event EventHandler<TYTDErrorEventArgs> Error;
public event EventHandler<BellEventArgs> 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<ListContentItem> 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<ListContentItem> 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<Subscription> 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<ListContentItem> 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();
}
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\Tesses.YouTubeDownloader\Tesses.YouTubeDownloader.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="3.4.3" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<IsPackable>true</IsPackable>
<PackageId>Tesses.YouTubeDownloader.MailKit</PackageId>
<Author>Mike Nolan</Author>
<Company>Tesses</Company>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<FileVersion>1.0.0</FileVersion>
<Description>A YouTube Downloader</Description>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageTags>YoutubeExplode, YouTube, YouTubeDownloader</PackageTags>
<RepositoryUrl>https://gitlab.tesses.cf/tesses50/tytd</RepositoryUrl>
</PropertyGroup>
</Project>

View File

@ -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();

View File

@ -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");

View File

@ -1,4 +1,6 @@
{
"Url": "http://10.137.42.142:3252/",
"LocalFiles": "/media/mike/PhotoDrive/wii-vids/working"
}
"Url": "http://192.168.0.142:3252/",
"LocalFiles": "/media/mike/BackupPlus4/Videos/TYTD_working",
"AddComplete": false,
"AddPlaylistsToStorage": true
}

View File

@ -17,9 +17,11 @@ namespace Tesses.YouTubeDownloader.Net6
server.RootServer.Server=new StaticServer("WebSite");
currentDirectory.CanDownload=true;
HttpServerListener listener=new HttpServerListener(new System.Net.IPEndPoint(System.Net.IPAddress.Any,3252),server.InnerServer);
currentDirectory.StartLoop();
TYTDStorage.FFmpeg ="/usr/bin/ffmpeg";
Console.WriteLine("Almost Ready to Listen");
await listener.ListenAsync(CancellationToken.None);
}

View File

@ -16,6 +16,7 @@ using Tesses.WebServer.Swagme;
namespace Tesses.Extensions
{
public static class Extensions
{
public static string Substring(this string value,string str)
@ -31,8 +32,37 @@ namespace Tesses.Extensions
namespace Tesses.YouTubeDownloader.Server
{
using Tesses.YouTubeDownloader;
internal class EventSender
{
DateTime lastScan = DateTime.Now;
TimeSpan ts;
public EventSender(TimeSpan interval)
{
this.ts = interval;
}
public bool CanScan
{
get{
return DateTime.Now - lastScan >= ts;
}
}
public void SendEvent(Action action)
{
if(CanScan)
{
lastScan = DateTime.Now;
action();
}
}
public void Sent()
{
lastScan = DateTime.Now;
}
}
internal static class B64
{
@ -405,7 +435,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 +536,15 @@ internal static class B64
await ctx.SendStreamAsync(s);
}
}*/
if(path.StartsWith("/GetFiles/DownloadsInfo") || path.StartsWith("/GetFiles/DownloadsInfo/"))
{
List<string> urls=new List<string>();
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<string> urls=new List<string>();
@ -552,9 +604,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 +925,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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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","<a href=\"https://tesses.net/apps/tytd/2022/progress.php\">More Info</a>"),"Getting status");
@ -870,13 +939,13 @@ internal static class B64
/*Subscriptions*/
AddBoth("/Subscribe",Subscribe,new SwagmeDocumentation("/Subscribe?id=UC4QobU6STFB0P71PMvOGN5A&conf=NotifyAndDownload","Subscribe to youtuber","id: Channel Id<br>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<br>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<br>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<br>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<br>&emsp;Id: Channel Id<br>&emsp;BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions");
AddBoth("/subscriptions",Subscriptions,new SwagmeDocumentation("Get subscriptions","Returned Json array<br>&emsp;Id: Channel Id<br>&emsp;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<br>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<br>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<br>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<br>&emsp;Id: Channel Id<br>&emsp;BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions");
AddBoth("/subscriptions",Subscriptions,new SwagmeDocumentation("Get subscriptions","Returned Json array<br>&emsp;Id: Channel Id<br>&emsp;BellInfo: 0=DoNothing, 1=GetInfo, 3=Notify, 5=Download, 7=NotifyAndDownload"),"Subscriptions");
/*Personal Lists*/
@ -901,7 +970,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:<br>&emsp;true: Restart download<br>&emsp;false: Cancel Download"));
AddBoth("/Thumbnail",Thumbnail,new SwagmeDocumentation("/Thumbnail?v=PzUKeGZiEl0&res=maxresdefault","Get Thumbnail for video","v: Video Id<br>res: YouTube Thumbnail Resolution <a href=\"https://www.binarymoon.co.uk/2014/03/using-youtube-thumbnails/\">List of resolutions</a>"),"Other");
/*
public async Task AddToPersonalPlaylistAsync(string name, IEnumerable<(VideoId Id, Resolution Resolution)> items)
{
@ -922,6 +991,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)
{
@ -935,6 +1018,13 @@ internal static class B64
}
public async Task Event(ServerContext ctx)
{
double interval=0;
string intervalStr;
if(ctx.QueryParams.TryGetFirst("interval",out intervalStr))
{
if(!double.TryParse(intervalStr,out interval)) interval=0;
}
IStorage storage=Downloader as IStorage;
if(storage != null){
@ -957,7 +1047,10 @@ internal static class B64
p.Video=e.VideoInfo;
evts.SendEvent(p);
};
EventSender s = new EventSender(TimeSpan.FromSeconds(interval));
storage.VideoProgress += (sender,e)=>{
bool wasFirst= first;
ProgressItem p=new ProgressItem();
p.StartEvent=false;
p.StopEvent=false;
@ -969,7 +1062,13 @@ internal static class B64
p.Video=e.VideoInfo;
first=false;
evts.SendEvent(p);
if(wasFirst || s.CanScan)
{
s.Sent();
evts.SendEvent(p);
}
};
storage.VideoFinished +=(sender,e)=>{
ProgressItem p=new ProgressItem();
@ -1073,9 +1172,9 @@ internal static class B64
}
await ctx.RedirectBackAsync();
}
public async Task ReplaceList(ServerContext ctx)
public async Task ReplaceList(ServerContext ctx)
{
ctx.ParseBody();
//this is for personal playlists
string name;
if(ctx.QueryParams.TryGetFirst("name",out name)){
@ -1496,7 +1595,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<Resolution>(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<Resolution>(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;

View File

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Tesses.WebServer" Version="1.0.3.8" />
<PackageReference Include="Tesses.WebServer.Swagme" Version="1.0.1" />
<PackageReference Include="Tesses.WebServer.Swagme" Version="1.0.2" />
</ItemGroup>
<PropertyGroup>
@ -16,11 +16,11 @@
<PackageId>Tesses.YouTubeDownloader.Server</PackageId>
<Author>Mike Nolan</Author>
<Company>Tesses</Company>
<Version>1.1.8</Version>
<AssemblyVersion>1.1.8</AssemblyVersion>
<FileVersion>1.1.8</FileVersion>
<Version>2.0.1.2</Version>
<AssemblyVersion>2.0.1.2</AssemblyVersion>
<FileVersion>2.0.1.2</FileVersion>
<Description>Adds WebServer to TYTD</Description>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageTags>YoutubeExplode, YouTube, YouTubeDownloader</PackageTags>
<RepositoryUrl>https://gitlab.tesses.cf/tesses50/tytd</RepositoryUrl>

View File

@ -64,7 +64,7 @@ namespace Tesses.YouTubeDownloader
//Console.WriteLine("IN FUNC");
//Console.WriteLine("DIR");
if(storage.BestStreamInfoExists(id))
if(await storage.BestStreamInfoExistsAsync(id))
{
//Console.WriteLine("STREAMS");
BestStreamsSerialized serialization=await storage.GetBestStreamInfoAsync(id);
@ -83,7 +83,7 @@ namespace Tesses.YouTubeDownloader
{
if(storage.BestStreamInfoExists(id))
if(await storage.BestStreamInfoExistsAsync(id))
{
BestStreamsSerialized serialization=await storage.GetBestStreamInfoAsync(id);
if(DateTime.Now < serialization.Expires || !expire_check)
@ -99,7 +99,7 @@ namespace Tesses.YouTubeDownloader
DateTime expires=DateTime.Now.AddHours(6);
try{
if(storage.VideoInfoExists(id))
if(await storage.VideoInfoExistsAsync(id))
{
var video = await storage.GetVideoInfoAsync(id);
if(video.VideoFrozen)

View File

@ -131,10 +131,11 @@ namespace Tesses.YouTubeDownloader
{
begin_download:
//await Task.Delay(this.GetLoggerProperties().DownloadMediaDelay);
try{
if(video.DownloadFrom == "YouTube")
{
switch (resolution)
{
case Resolution.Mux:
@ -184,8 +185,8 @@ namespace Tesses.YouTubeDownloader
private async Task DownloadFileAsync(SavedVideo video, CancellationToken token, IProgress<double> 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;

View File

@ -23,10 +23,12 @@ namespace Tesses.YouTubeDownloader
event EventHandler<BellEventArgs> 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();

View File

@ -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);

View File

@ -15,7 +15,13 @@ namespace Tesses.YouTubeDownloader
{
public interface ITYTDBase : IPersonalPlaylistGet
{
Task<bool> BestStreamInfoExistsAsync(VideoId id);
Task<bool> VideoInfoExistsAsync(VideoId id);
Task<bool> PlaylistInfoExistsAsync(PlaylistId id);
Task<bool> ChannelInfoExistsAsync(ChannelId id);
Task<bool> ThumbnailExistsAsync(VideoId videoId,string res);
bool ThumbnailExists(VideoId videoId,string res);
Task<byte[]> ReadThumbnailAsync(VideoId videoId,string res,CancellationToken token=default);
IAsyncEnumerable<string> GetDownloadUrlsAsync();
IAsyncEnumerable<SavedVideo> GetDownloadsAsync();
Task<SavedVideo> GetDownloadInfoAsync(string url);

View File

@ -111,11 +111,16 @@ namespace Tesses.YouTubeDownloader
public bool AddDateInLog {get;set;}
public bool LogEntriesRightWhenAdded {get;set;}=true;
//public TimeSpan DownloadMediaDelay {get;set;}=new TimeSpan(0,0,5);
//public TimeSpan GetInfoDelay {get;set;}=new TimeSpan(0,0,5);
}
public class Logger
{
private string _video_log_file;
private string _filename;
private TYTDStorage _storage;
@ -126,8 +131,9 @@ namespace Tesses.YouTubeDownloader
_storage=storage;
string dateTime = DateTime.Now.ToString("yyyyMMdd_hhmmss");
string dateTime = DateTime.Now.ToString("yyyyMMdd_HHmmss");
_filename = $"config/logs/{dateTime}.log";
_video_log_file = $"config/addlog/{dateTime}.log";
}
private void WriteStdErr(string message)
{
@ -162,6 +168,30 @@ namespace Tesses.YouTubeDownloader
Write(message,writeToConsole,isError,log);
});
}
public void WriteVideoLog(string url)
{
if(!_storage.GetLoggerProperties().LogEntriesRightWhenAdded) return;
lock(TYTDStorage.o){
try{
using(var strm = _storage.OpenOrCreateAsync(_video_log_file).GetAwaiter().GetResult())
{
if(!strm.CanSeek) return;
strm.Seek(0,SeekOrigin.End);
using(var sw = new StreamWriter(strm))
{
DateTime dat=DateTime.Now;
sw.WriteLine($"[{dat.ToShortDateString()} at {dat.ToLongTimeString()}]: {url}");
}
} }
catch(Exception ex)
{
_=ex;
}
//mtx.ReleaseMutex();
}
}
public void Write(string message,bool writeToConsole=false, bool isError=false,bool log=true)
{

View File

@ -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<SavedChannel> 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<SavedChannel> DownloadThumbnails(TYTDStorage storage,YoutubeExplode.Channels.Channel channel)

View File

@ -310,7 +310,8 @@ namespace Tesses.YouTubeDownloader
{
foreach(var item in Videos)
{
if(await baseCls.FileExistsAsync($"Info/{item}.json"))
VideoId? id = VideoId.TryParse(item);
if(id.HasValue && await baseCls.VideoExistsAsync(id.Value))
{
yield return await baseCls.GetVideoInfoAsync(item);
}
@ -366,4 +367,4 @@ namespace Tesses.YouTubeDownloader
public string Title { get; set; }
public string TYTDTag {get;set;}
}
}
}

View File

@ -17,6 +17,23 @@ namespace Tesses.YouTubeDownloader
public abstract partial class TYTDStorage : TYTDBase, IStorage
{
public new virtual async Task<byte[]> 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();
@ -72,9 +89,10 @@ namespace Tesses.YouTubeDownloader
}
public async Task WriteAllBytesAsync(string path,byte[] data,CancellationToken token=default(CancellationToken))
{
MemoryStream ms = new MemoryStream(data);
using(var s=await CreateAsync(path))
{
await s.WriteAsync(data,0,data.Length,token);
await ms.CopyToAsync(s,4096,token);
}
}
public EventHandler<ConsoleWriteEventArgs> ConsoleWrite;
@ -110,8 +128,8 @@ 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";
if(!FileExists(file))
string file = info.DownloadFrom.StartsWith("NormalDownload,Length=") ? $"DownloadsInfo/{HashDownloadUrl(info.Id)}.json" : $"Info/{info.Id}.json";
if(!await FileExistsAsync(file))
{
await WriteAllTextAsync(file,JsonConvert.SerializeObject(info));
}
@ -127,7 +145,7 @@ namespace Tesses.YouTubeDownloader
public virtual async Task WriteChannelInfoAsync(SavedChannel info)
{
string file = $"Channel/{info.Id}.json";
if(!FileExists(file))
if(!await FileExistsAsync(file))
{
await WriteAllTextAsync(file,JsonConvert.SerializeObject(info));
}
@ -136,6 +154,7 @@ namespace Tesses.YouTubeDownloader
{
lock(Temporary)
{
GetLogger().WriteVideoLog($"https://www.youtube.com/playlist?list={id.Value}");
Temporary.Add( new PlaylistMediaContext(id,resolution));
}
@ -146,14 +165,35 @@ namespace Tesses.YouTubeDownloader
{
lock(Temporary)
{
GetLogger().WriteVideoLog($"https://www.youtube.com/channel/{id.Value}");
Temporary.Add(new ChannelMediaContext(id,resolution));
}
await Task.FromResult(0);
}
public async Task AddSlugAsync(ChannelSlug slug,Resolution resolution=Resolution.PreMuxed)
{
lock(Temporary)
{
GetLogger().WriteVideoLog($"https://www.youtube.com/c/{slug.Value}");
Temporary.Add(new ChannelMediaContext(slug,resolution));
}
await Task.FromResult(0);
}
public async Task AddHandleAsync(ChannelHandle handle,Resolution resolution=Resolution.PreMuxed)
{
lock(Temporary)
{
GetLogger().WriteVideoLog($"https://www.youtube.com/@{handle.Value}");
Temporary.Add(new ChannelMediaContext(handle,resolution));
}
await Task.FromResult(0);
}
public async Task AddUserAsync(UserName name,Resolution resolution=Resolution.PreMuxed)
{
lock(Temporary)
{
GetLogger().WriteVideoLog($"https://www.youtube.com/user/{name.Value}");
Temporary.Add(new ChannelMediaContext(name,resolution));
}
await Task.FromResult(0);
@ -163,6 +203,7 @@ namespace Tesses.YouTubeDownloader
{
lock(Temporary)
{
GetLogger().WriteVideoLog($"https://www.youtube.com/watch?v={videoId.Value}");
Temporary.Add(new VideoMediaContext(videoId,res));
}
await Task.FromResult(0);
@ -171,6 +212,7 @@ namespace Tesses.YouTubeDownloader
{
lock(Temporary)
{
GetLogger().WriteVideoLog(url);
Temporary.Add(new NormalDownloadMediaContext(url,download));
}
await Task.FromResult(0);
@ -207,7 +249,7 @@ namespace Tesses.YouTubeDownloader
CreateDirectoryIfNotExist("Channel");
CreateDirectoryIfNotExist("Playlist");
CreateDirectoryIfNotExist("Subscriptions");
CreateDirectoryIfNotExist("VideoOnly");
CreateDirectoryIfNotExist("VideoOnly");
CreateDirectoryIfNotExist("AudioOnly");
CreateDirectoryIfNotExist("Muxed");
CreateDirectoryIfNotExist("PreMuxed");
@ -215,14 +257,19 @@ namespace Tesses.YouTubeDownloader
CreateDirectoryIfNotExist("Thumbnails");
CreateDirectoryIfNotExist("config");
CreateDirectoryIfNotExist("config/logs");
CreateDirectoryIfNotExist("FileInfo");
CreateDirectoryIfNotExist("Download");
CreateDirectoryIfNotExist("config/addlog");
CreateDirectoryIfNotExist("DownloadsInfo");
CreateDirectoryIfNotExist("Downloads");
CreateDirectoryIfNotExist("StreamInfo");
CreateDirectoryIfNotExist("PersonalPlaylist");
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 +279,113 @@ namespace Tesses.YouTubeDownloader
});
thread1.Start();
}
private async IAsyncEnumerable<SavedVideo> GetDownloadsLegacyAsync()
{
await foreach(var item in EnumerateFilesAsync("FileInfo"))
{
if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal))
{
var res= JsonConvert.DeserializeObject<SavedVideo>(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);

View File

@ -12,20 +12,41 @@ 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<bool> 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<byte[]> 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");
}
public virtual async Task<bool> PersonalPlaylistExistsAsync(string name)
{
return await FileExistsAsync($"PersonalPlaylist/{name}.json");
}
public virtual async IAsyncEnumerable<ListContentItem> GetPersonalPlaylistContentsAsync(string playlist)
{
if(!PersonalPlaylistExists(playlist)) yield break;
if(!await PersonalPlaylistExistsAsync(playlist)) yield break;
var ls=JsonConvert.DeserializeObject<List<ListContentItem>>(await ReadAllTextAsync($"PersonalPlaylist/{playlist}.json"));
foreach(var item in ls)
{
@ -120,17 +141,14 @@ namespace Tesses.YouTubeDownloader
}
public async Task<byte[]> ReadAllBytesAsync(string path,CancellationToken token=default(CancellationToken))
{using(var strm = await OpenReadAsync(path))
{
MemoryStream memoryStream=new MemoryStream();
using(var strm = await OpenReadAsync(path))
{
byte[] data=new byte[strm.Length];
await strm.ReadAsync(data,0,data.Length,token);
if(token.IsCancellationRequested)
{
return new byte[0];
}
return data;
await strm.CopyToAsync(memoryStream,4096,token);
}
return memoryStream.ToArray();
}
public virtual async IAsyncEnumerable<string> GetPlaylistIdsAsync()
@ -179,21 +197,23 @@ namespace Tesses.YouTubeDownloader
public virtual async IAsyncEnumerable<SavedVideo> 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<SavedVideo>(await ReadAllTextAsync(item));
}
}
}
public virtual async IAsyncEnumerable<string> 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<BestStreamInfo.BestStreamsSerialized> GetBestStreamInfoAsync(VideoId id)
{
return JsonConvert.DeserializeObject<BestStreamInfo.BestStreamsSerialized>(await ReadAllTextAsync($"StreamInfo/{id.Value}.json"));
@ -201,18 +221,35 @@ namespace Tesses.YouTubeDownloader
public virtual bool BestStreamInfoExists(VideoId id)
{
return FileExists($"StreamInfo/{id.Value}.json");
}
public virtual async Task<bool> BestStreamInfoExistsAsync(VideoId id)
{
return await FileExistsAsync($"StreamInfo/{id.Value}.json");
}
public virtual async Task<SavedVideo> 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<SavedVideo>(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 async Task<bool> DownloadExistsAsync(string url)
{
string enc=$"DownloadsInfo/{HashDownloadUrl(url)}.json";
return await FileExistsAsync(enc);
}
public virtual bool DownloadExists(string url)
{
string enc=$"FileInfo/{B64.Base64UrlEncodes(url)}.json";
string enc=$"DownloadsInfo/{HashDownloadUrl(url)}.json";
return FileExists(enc);
}
@ -220,14 +257,26 @@ namespace Tesses.YouTubeDownloader
{
return FileExists($"Playlist/{id}.json");
}
public virtual async Task<bool> PlaylistInfoExistsAsync(PlaylistId id)
{
return await FileExistsAsync($"Playlist/{id}.json");
}
public virtual bool VideoInfoExists(VideoId id)
{
return FileExists($"Info/{id}.json");
}
public virtual async Task<bool> VideoInfoExistsAsync(VideoId id)
{
return await FileExistsAsync($"Info/{id}.json");
}
public virtual bool ChannelInfoExists(ChannelId id)
{
return FileExists($"Channel/{id}.json");
}
public virtual async Task<bool> ChannelInfoExistsAsync(ChannelId id)
{
return await FileExistsAsync($"Channel/{id}.json");
}
public virtual async Task<SavedPlaylist> GetPlaylistInfoAsync(PlaylistId id)
{
return JsonConvert.DeserializeObject<SavedPlaylist>(await ReadAllTextAsync($"Playlist/{id}.json"));
@ -282,6 +331,8 @@ namespace Tesses.YouTubeDownloader
return data;
}
}
public class ListContentItem
@ -397,6 +448,20 @@ namespace Tesses.YouTubeDownloader
}
public static class TYTDManager
{
public static async Task<bool> 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<SearchResult> SearchYouTubeAsync(this IStorage storage, string query,bool getMediaInfo=true)
{
await foreach(var vid in storage.YoutubeClient.Search.GetResultsAsync(query))
@ -419,7 +484,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;
}
/// <summary>
/// Add Video, Playlist, Channel Or Username
@ -438,10 +504,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 +534,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);
@ -866,6 +942,7 @@ namespace Tesses.YouTubeDownloader
ExtraData GetExtraData();
IAsyncEnumerable<ListContentItem> GetPersonalPlaylistContentsAsync(string name);
bool PersonalPlaylistExists(string name);
Task<bool> PersonalPlaylistExistsAsync(string name);
}
public interface IPersonalPlaylistSet : IPersonalPlaylistGet
{

View File

@ -16,6 +16,7 @@ using System.Net;
using System.Diagnostics.CodeAnalysis;
using YoutubeExplode.Utils.Extensions;
using System.Net.Http.Headers;
using System.Web;
namespace Tesses.YouTubeDownloader
{
@ -194,7 +195,23 @@ internal class SegmentedHttpStream : Stream
}
public class TYTDClient : TYTDBase,IDownloader
{
ExtraData data=null;
public override async Task<byte[]> 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 string Url {get{return url;}}
public TYTDClient(string url)
{
client=new HttpClient();
@ -240,7 +257,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));
@ -298,11 +315,16 @@ internal class SegmentedHttpStream : Stream
}
}
}
public override async Task<(string Path,bool Delete)> GetRealUrlOrPathAsync(string path)
{
return await Task.FromResult(($"{url}api/Storage/File/{path.TrimStart('/')}",false));
}
public async IAsyncEnumerable<Subscription> GetSubscriptionsAsync()
{
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));
@ -392,7 +414,7 @@ internal class SegmentedHttpStream : Stream
}
return false;
}
private async Task<IReadOnlyList<(SavedVideo Video, Resolution Resolution)>> GetQueueListAsync()
public async Task<IReadOnlyList<(SavedVideo Video, Resolution Resolution)>> GetQueueListAsync()
{
try{
@ -405,7 +427,7 @@ internal class SegmentedHttpStream : Stream
return new List<(SavedVideo video,Resolution resolution)>();
}
private async Task<SavedVideoProgress> GetProgressAsync()
public async Task<SavedVideoProgress> GetProgressAsync()
{
try{
@ -452,27 +474,18 @@ internal class SegmentedHttpStream : Stream
public async Task AddToPersonalPlaylistAsync(string name, IEnumerable<ListContentItem> items)
{
Dictionary<string,string> values=new Dictionary<string, string>
{
{ "name", name},
{ "data", JsonConvert.SerializeObject(items.ToArray())}
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync($"{url}api/v2/AddToList",content);
var resposeStr = await response.Content.ReadAsStringAsync();
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<ListContentItem> items)
{
//ReplaceList
Dictionary<string,string> values=new Dictionary<string, string>
{
{ "name", name},
{ "data", JsonConvert.SerializeObject(items.ToArray())}
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync($"{url}api/v2/ReplaceList",content);
var resposeStr = await response.Content.ReadAsStringAsync();
var content = new MultipartFormDataContent();
content.Add(new StringContent(name), "name");
content.Add(new StringContent(JsonConvert.SerializeObject(items.ToList())), "data");
await client.PostAsync($"{url}api/v2/ReplaceList",content);
}
public async Task RemoveItemFromPersonalPlaylistAsync(string name, VideoId id)
@ -505,7 +518,7 @@ internal class SegmentedHttpStream : Stream
try{
client.GetStringAsync($"{url}api/v2/DeleteList?name={WebUtility.UrlEncode(name)}").GetAwaiter().GetResult();
Task.Run(()=>client.GetStringAsync($"{url}api/v2/DeleteList?name={WebUtility.UrlEncode(name)}")).GetAwaiter().GetResult();
}catch(Exception ex)
{
@ -542,11 +555,12 @@ internal class SegmentedHttpStream : Stream
_error.Invoke(this,new TYTDErrorEventArgs("jNQXAC9IVRw",ex));
}
}
public void ResetEvents()
public void ResetEvents()
{
if(src != null) {
src.Cancel();
src.Dispose();
}
if(hadBeenListeningToEvents)
@ -656,7 +670,7 @@ internal class SegmentedHttpStream : Stream
}
if(item.BellEvent)
{
bell=true;
}
progress.Length = item.Length;
progress.ProgressRaw=item.Percent;
@ -701,17 +715,61 @@ internal class SegmentedHttpStream : Stream
}
private void _startEventStream()
{
Task.Run(_startEventStreamAsync).Wait(0);
Task.Run(_startEventStreamAsync).Wait(0);
}
public override ExtraData GetExtraData()
public ExtraData GetExtraDataOnce()
{
return Task.Run<ExtraData>(async()=>{
if(data ==null){
data= Task.Run<ExtraData>(async()=>{
string text= await client.GetStringAsync($"{url}api/v2/extra_data.json");
return JsonConvert.DeserializeObject<ExtraData>(text);
}).GetAwaiter().GetResult();
}
return data;
}
public async Task<ExtraData> GetExtraDataOnceAsync()
{
if(data ==null)
{
string text= await client.GetStringAsync($"{url}api/v2/extra_data.json");
data = JsonConvert.DeserializeObject<ExtraData>(text);
}
return data;
}
public async Task<ExtraData> GetExtraDataAsync()
{
string text= await client.GetStringAsync($"{url}api/v2/extra_data.json");
return JsonConvert.DeserializeObject<ExtraData>(text);
}
public override ExtraData GetExtraData()
{
return Task.Run<ExtraData>(async()=>{
string text= await client.GetStringAsync($"{url}api/v2/extra_data.json");
return JsonConvert.DeserializeObject<ExtraData>(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));
}
}
}
}

View File

@ -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<bool> 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,7 +889,91 @@ 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<byte[]> 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<bool> 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 async Task<bool> VideoInfoExistsAsync(VideoId id)
{
if(Storage ==null) return await Task.FromResult(false);
return await Storage.VideoInfoExistsAsync(id);
}
public async Task<bool> PlaylistInfoExistsAsync(PlaylistId id)
{
if(Storage ==null) return await Task.FromResult(false);
return await Storage.PlaylistInfoExistsAsync(id);
}
public async Task<bool> ChannelInfoExistsAsync(ChannelId id)
{
if(Storage ==null) return await Task.FromResult(false);
return await Storage.ChannelInfoExistsAsync(id);
}
public async Task<bool> PersonalPlaylistExistsAsync(string name)
{
if(Storage ==null) return await Task.FromResult(false);
return await Storage.PersonalPlaylistExistsAsync(name);
}
public async Task<bool> BestStreamInfoExistsAsync(VideoId id)
{
if(Storage ==null) return await Task.FromResult(false);
return await Storage.BestStreamInfoExistsAsync(id);
}
}

View File

@ -7,11 +7,11 @@
<PackageId>Tesses.YouTubeDownloader</PackageId>
<Author>Mike Nolan</Author>
<Company>Tesses</Company>
<Version>1.2.5</Version>
<AssemblyVersion>1.2.5</AssemblyVersion>
<FileVersion>1.2.5</FileVersion>
<Version>2.0.2.4</Version>
<AssemblyVersion>2.0.2.4</AssemblyVersion>
<FileVersion>2.0.2.4</FileVersion>
<Description>A YouTube Downloader</Description>
<PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageTags>YoutubeExplode, YouTube, YouTubeDownloader</PackageTags>
<RepositoryUrl>https://gitlab.tesses.cf/tesses50/tytd</RepositoryUrl>
@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="YouTubeExplode" Version="6.1.2" />
<PackageReference Include="YouTubeExplode" Version="6.2.5" />
</ItemGroup>
</Project>