tytdlite/TYTDLite/Class1.cs

1038 lines
46 KiB
C#

/*
TYTD Lite: A YouTube Downloader website that archives videos when people download videos using it.
Copyright (C) 2024 Mike Nolan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Humanizer;
using LiteDB;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Scriban;
using Tesses.WebServer;
using YoutubeExplode;
using YoutubeExplode.Exceptions;
using YoutubeExplode.Videos;
using YoutubeExplode.Videos.Streams;
namespace TYTDLite
{
public class TYTDLiteServer
{
public ILiteDatabase Database {get;}
public ILiteCollection<SavedVideo> Videos => Database.GetCollection<SavedVideo>("Videos");
List<Job> Jobs {get;set;}=new List<Job>();
public string ServerPath {get;}
public RouteServer Server {get;}
public YoutubeClient YoutubeClient {get;}
public HttpClient HttpClient {get;}
Lazy<Template> template;
Lazy<Template> pageIndex;
Lazy<Template> pageDownload;
private Lazy<Template> pageInitDownload;
SemaphoreSlim semaphore=new SemaphoreSlim(1,1);
bool HasConverter => File.Exists(Path.Combine(ServerPath,"converter.txt"));
string ConverterPath => File.ReadAllText(Path.Combine(ServerPath,"converter.txt")).Replace("\r","").Replace("\n","");
public TYTDLiteServer(string serverPath,HttpClient client)
{
template = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/template.html")),true);
pageIndex = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/index.html")),true);
pageDownload = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/video_download.html")),true);
pageInitDownload = new Lazy<Template>(()=> Template.Parse(AssetProvider.ReadAllText("/video_init_download.html")),true);
ServerPath = serverPath;
Directory.CreateDirectory(ServerPath);
Database = new LiteDatabase(Path.Combine(ServerPath,"tytdlite.db"));
Server = new RouteServer(new AssetProvider());
HttpClient = client;
YoutubeClient = new YoutubeClient(client);
Server.Add("/",IndexAsync,"GET");
Server.Add("/thumbnail",ThumbnailAsync,"GET");
Server.Add("/download",DownloadAsync,"GET");
Server.Add("/video",VideoAsync,"GET");
Server.Add("/init_download",InitDownloadAsync,"GET");
Server.Add("/init_download_event",InitDownloadEventAsync,"GET");
Server.Add("/info.json",InfoAsync,"GET");
Server.Add("/count",CountAsync,"GET");
}
public static void MainFunc(string[] args)
{
int port = 4220;
string path = "data";
HttpClient client=null;
if(args.Length >= 1)
{
if(args[0] == "--help")
{
Console.WriteLine("TYTD Lite: A YouTube Downloader website that archives videos when people download videos using it.");
Console.WriteLine("Created by Tesses (Mike Nolan): https://tesses.net/");
Console.WriteLine("Released under the GPLv3 License, for more info go: https://www.gnu.org/licenses/");
Console.WriteLine("Thanks for using this software, consider buying me a coffee: https://www.buymeacoffee.com/tesses50");
Console.WriteLine();
Console.WriteLine("USAGE:");
Console.WriteLine("tytdlite");
Console.WriteLine("tytdlite <path>");
Console.WriteLine("tytdlite <path> <port>");
Console.WriteLine("tytdlite <path> <port> <ignorecert:true|false>");
return;
}
else
{
path = args[0];
if(args.Length >= 2)
{
if(int.TryParse(args[1],out var p)) port = p;
if(args.Length >= 3)
{
if(args[2] == "true")
{
System.Net.ServicePointManager.ServerCertificateValidationCallback += (a,b,c,d)=>true;
HttpClientHandler ch=new HttpClientHandler();
ch.ServerCertificateCustomValidationCallback = (a,b,c,d)=>true;
client = new HttpClient(ch);
}
}
}
}
}
TYTDLiteServer server=new TYTDLiteServer(path,client ?? new HttpClient());
server.Server.StartServer(port);
}
private async Task CountAsync(ServerContext ctx)
{
await ctx.SendJsonAsync(new{count = Videos.LongCount()});
}
private async Task InfoAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("v",out var v))
{
var video=VideoId.TryParse(v);
if(video.HasValue)
{
var (_v,rfy,off)=await GetSavedVideoAsync(video.Value);
await ctx.SendJsonAsync(new{Video=_v,RemovedFromYouTube=rfy,Offline=off});
}
else
await ctx.SendNotFoundAsync();
}else{
await ctx.SendNotFoundAsync();
}
}
private async Task InitDownloadAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("v",out var v) && ctx.QueryParams.TryGetFirst("res",out var res))
{
var video = VideoId.TryParse(v);
if(video.HasValue && reses.Contains(res))
{
await ctx.SendTextAsync(await template.Value.RenderAsync(new{body=await pageInitDownload.Value.RenderAsync(new{v,res})}));
}
}
}
string[] reses=new string[]{"PreMuxed","VideoOnly","AudioOnly","MKV","MP3","MP4"};
private async Task InitDownloadEventAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("v",out var v) && ctx.QueryParams.TryGetFirst("res",out var res))
{
var video = VideoId.TryParse(v);
if(video.HasValue && reses.Contains(res))
{
SendEvents evts=new SendEvents();
Task.Run(()=>ctx.ServerSentEvents(evts)).Wait(0);
try{
await this.PreformTask(video.Value,res,new Progress<double>(e=>{
try{
evts.SendEvent(new{type="progress",progress=e});
}catch(Exception){}
}));
evts.SendEvent(new{type="done",url=$"./video?v={video.Value.Value}&res={res}"});
}catch(Exception ex)
{
try{
evts.SendEvent(new{type="error",error=ex});
}catch(Exception){}
}
}
else
{
await ctx.SendExceptionAsync(new ArgumentException("Must provide video id \"v\" and \"res\" must be PreMuxed, VideoOnly, AudioOnly, MKV, MP3 or MP4"));
}
}
else
{
await ctx.SendExceptionAsync(new ArgumentException("Must provide video id \"v\" and \"res\" must be PreMuxed, VideoOnly, AudioOnly, MKV, MP3 or MP4"));
}
}
private async Task VideoAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("v",out var v) && ctx.QueryParams.TryGetFirst("res",out var res))
{
var video = VideoId.TryParse(v);
if(video.HasValue && reses.Contains(res))
{
await PreformTask(video.Value,res,new Progress<double>(e=>{
}));
var (svideo,_,_) = await GetSavedVideoAsync(video.Value);
switch(res)
{
case "PreMuxed":
{
string ext = svideo.PreMuxed != null ? svideo.PreMuxed.Container : "mp4";
await ctx.WithFileName(svideo.SafeFileName(ext),false).SendFileAsync(Path.Combine(ServerPath,"PreMuxed",$"{svideo.VideoId}.{ext}"));
}
break;
case "VideoOnly":
{
string ext = svideo.VideoOnly != null ? svideo.VideoOnly.Container : "mp4";
await ctx.WithFileName(svideo.SafeFileName(ext),false).SendFileAsync(Path.Combine(ServerPath,"VideoOnly",$"{svideo.VideoId}.{ext}"));
}
break;
case "AudioOnly":
{
string ext = svideo.AudioOnly != null ? svideo.AudioOnly.Container : "mp4";
await ctx.WithFileName(svideo.SafeFileName(ext),false).SendFileAsync(Path.Combine(ServerPath,"AudioOnly",$"{svideo.VideoId}.{ext}"));
}
break;
case "MP3":
{
await ctx.WithFileName(svideo.SafeFileName("mp3"),false).SendFileAsync(Path.Combine(ServerPath,"MP3",$"{svideo.VideoId}.mp3"));
}
break;
case "MP4":
{
await ctx.WithFileName(svideo.SafeFileName("mp4"),false).SendFileAsync(Path.Combine(ServerPath,"MP4",$"{svideo.VideoId}.mp4"));
}
break;
case "MKV":
{
await ctx.WithFileName(svideo.SafeFileName("mkv"),false).SendFileAsync(Path.Combine(ServerPath,"MKV",$"{svideo.VideoId}.mkv"));
}
break;
}
}
}
}
private async Task<(SavedVideo video,bool removedByYt,bool offline)> GetSavedVideoAsync(VideoId id)
{
string _id = id.Value;
bool removedByYt=false;
bool offline=false;
SavedVideo video=new SavedVideo();
await LockAsync(async()=>{
var _video=Videos.FindOne(e=>e.VideoId == _id);
if(_video != null)
{
video = _video;
DateTime now = DateTime.Now;
if(video.Expires < now)
{
try{
var streams = await YoutubeClient.Videos.Streams.GetManifestAsync(id);
video.PreMuxed.Stream = streams.GetMuxedStreams().GetWithHighestVideoQuality();
video.VideoOnly.Stream = streams.GetVideoOnlyStreams().GetWithHighestVideoQuality();
video.AudioOnly.Stream = streams.GetAudioOnlyStreams().GetWithHighestBitrate();
video.Expires = now.AddHours(6);
Videos.Update(video);
}
catch(VideoRequiresPurchaseException ex)
{
_=ex;
removedByYt=true;
}
catch(VideoUnavailableException ex)
{
_=ex;
removedByYt=true;
}
catch(VideoUnplayableException ex)
{
_=ex;
removedByYt=true;
}
catch(Exception ex)
{
_=ex;
offline=true;
}
}
}
else
{
try{
var now = DateTime.Now;
video = await YoutubeClient.Videos.GetAsync(id);
var streams = await YoutubeClient.Videos.Streams.GetManifestAsync(id);
video.PreMuxed.Stream = streams.GetMuxedStreams().GetWithHighestVideoQuality();
video.VideoOnly.Stream = streams.GetVideoOnlyStreams().GetWithHighestVideoQuality();
video.AudioOnly.Stream = streams.GetAudioOnlyStreams().GetWithHighestBitrate();
video.Expires = now.AddHours(6);
video.Id=Videos.Insert(video);
await DownloadThumbnailsAsync(id);
}catch(VideoRequiresPurchaseException ex)
{
_=ex;
removedByYt=true;
}
catch(VideoUnavailableException ex)
{
_=ex;
removedByYt=true;
}
catch(VideoUnplayableException ex)
{
_=ex;
removedByYt=true;
}
}
});
return (video,removedByYt,offline);
}
private async Task DownloadThumbnailsAsync(VideoId id)
{
async Task DLAsync(string resolution)
{
try{
string dir = Path.Combine(ServerPath,"Thumbnails",id.Value);
string filename = Path.Combine(dir,$"{resolution}.jpg");
Directory.CreateDirectory(dir);
if(!File.Exists(filename))
{
File.WriteAllBytes(filename,await HttpClient.GetByteArrayAsync($"https://s.ytimg.com/vi/{id.Value}/{resolution}.jpg"));
}
}catch(Exception ex){_=ex;}
}
await DLAsync("default");
await DLAsync("sddefault");
await DLAsync("mqdefault");
await DLAsync("hqdefault");
await DLAsync("maxresdefault");
}
private async Task ConvertAsync(string command,IProgress<TimeSpan> progress)
{
if(!HasConverter) return;
using(Process p=new Process())
{
p.StartInfo.CreateNoWindow = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.StartInfo.FileName = ConverterPath;
p.StartInfo.Arguments = command;
p.StartInfo.RedirectStandardError=true;
p.Start();
while(!p.HasExited)
{
var res=await p.StandardError.ReadLineAsync();
if(!string.IsNullOrEmpty(res))
{
int index = res.IndexOf("time=");
if(index > -1)
{
index+=5;
int index2 = res.IndexOf(' ',index);
string timeStr = index2 == -1 ? res.Substring(index) : res.Substring(index,index2-index);
if(!string.IsNullOrWhiteSpace(timeStr))
{
if(TimeSpan.TryParse(timeStr,out var ts))
{
progress?.Report(ts);
}
else
{
if(double.TryParse(timeStr,out var tsn)) //for avconv
{
progress?.Report(TimeSpan.FromSeconds(tsn));
}
}
}
}
}
}
}
}
private async Task DownloadPreMuxed(SavedVideo video,IProgress<double> progress=null)
{
string dir = Path.Combine(ServerPath,"PreMuxed");
string complete=Path.Combine(dir,$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
if(Directory.Exists(dir) && File.Exists(complete)) return;
//assume saved video has not expired
if(!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var killSwitch = video.Expires-DateTime.Now;
using(CancellationTokenSource src=new CancellationTokenSource())
{
src.CancelAfter(killSwitch);
var token = src.Token;
byte[] buffer=new byte[4096];
using(var destStrm = File.Open(incomplete,FileMode.OpenOrCreate,FileAccess.Write))
{
destStrm.Position = destStrm.Length;
using(var srcStrm = await YoutubeClient.Videos.Streams.GetAsync(video.PreMuxed.Stream,token))
{
long pos = destStrm.Position;
long len = srcStrm.Length;
if(pos > 0)
srcStrm.Position = pos;
int read=0;
do {
read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
if(token.IsCancellationRequested) return;
await destStrm.WriteAsync(buffer,0,read,token);
if(token.IsCancellationRequested) return;
pos+=read;
progress?.Report(pos / (double)len);
} while(read != 0 && !token.IsCancellationRequested);
if(token.IsCancellationRequested) return;
File.Move(incomplete,complete);
}
}
}
}
private async Task DownloadVideoOnly(SavedVideo video,IProgress<double> progress=null)
{
string dir = Path.Combine(ServerPath,"VideoOnly");
string complete=Path.Combine(dir,$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
if(Directory.Exists(dir) && File.Exists(complete)) return;
//assume saved video has not expired
if(!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var killSwitch = video.Expires - DateTime.Now;
using(CancellationTokenSource src=new CancellationTokenSource())
{
src.CancelAfter(killSwitch);
var token = src.Token;
byte[] buffer=new byte[4096];
using(var destStrm = File.Open(incomplete,FileMode.OpenOrCreate,FileAccess.Write))
{
destStrm.Position = destStrm.Length;
using(var srcStrm = await YoutubeClient.Videos.Streams.GetAsync(video.VideoOnly.Stream,token))
{
long pos = destStrm.Position;
long len = srcStrm.Length;
if(pos > 0)
srcStrm.Position = pos;
int read=0;
do {
read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
if(token.IsCancellationRequested) return;
await destStrm.WriteAsync(buffer,0,read,token);
if(token.IsCancellationRequested) return;
pos+=read;
progress?.Report(pos / (double)len);
} while(read != 0 && !token.IsCancellationRequested);
if(token.IsCancellationRequested) return;
File.Move(incomplete,complete);
}
}
}
}
private async Task DownloadAudioOnly(SavedVideo video,IProgress<double> progress=null)
{
string dir = Path.Combine(ServerPath,"AudioOnly");
string complete=Path.Combine(dir,$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
if(Directory.Exists(dir) && File.Exists(complete)) return;
//assume saved video has not expired
if(!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var killSwitch = video.Expires - DateTime.Now;
using(CancellationTokenSource src=new CancellationTokenSource())
{
src.CancelAfter(killSwitch);
var token = src.Token;
byte[] buffer=new byte[4096];
using(var destStrm = File.Open(incomplete,FileMode.OpenOrCreate,FileAccess.Write))
{
destStrm.Position = destStrm.Length;
using(var srcStrm = await YoutubeClient.Videos.Streams.GetAsync(video.AudioOnly.Stream,token))
{
long pos = destStrm.Position;
long len = srcStrm.Length;
if(pos > 0)
srcStrm.Position = pos;
int read=0;
do {
read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
if(token.IsCancellationRequested) return;
await destStrm.WriteAsync(buffer,0,read,token);
if(token.IsCancellationRequested) return;
pos+=read;
progress?.Report(pos / (double)len);
} while(read != 0 && !token.IsCancellationRequested);
if(token.IsCancellationRequested) return;
File.Move(incomplete,complete);
}
}
}
}
private async Task ConvertToMp3(SavedVideo video,IProgress<double> progress=null)
{
string dir = Path.Combine(ServerPath,"MP3");
string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.mp3");
string complete = Path.Combine(dir,$"{video.VideoId}.mp3");
string audioonly=Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
string premuxed = Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
if(!Directory.Exists(dir))
Directory.CreateDirectory(dir);
if(File.Exists(complete)) return;
await PreformTask(video.VideoId,"AudioOnly",new Progress<double>(e=>{
progress?.Report(e / 2);
}));
if(!File.Exists(audioonly))
{
await PreformTask(video.VideoId,"PreMuxed",new Progress<double>(e=>{
progress?.Report(e/2);
}));
audioonly = premuxed;
}
if(File.Exists(audioonly) && !File.Exists(complete))
{
if(File.Exists(incomplete))
File.Delete(incomplete);
await ConvertAsync($"-i \"{audioonly}\" -c:a libmp3lame -vn -preset ultrafast \"{incomplete}\"",new Progress<TimeSpan>((v)=>{
progress?.Report(( v.TotalSeconds/ video.Duration.TotalSeconds/2)+0.5);
}));
}
if(File.Exists(incomplete))
File.Move(incomplete,complete);
}
private async Task MuxToMkv(SavedVideo video,IProgress<double> progress=null)
{
string dir = Path.Combine(ServerPath,"MKV");
string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.mkv");
string complete = Path.Combine(dir,$"{video.VideoId}.mkv");
string audioonly=Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
string videoonly=Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
string premuxed = Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
if(File.Exists(complete)) return;
if(!Directory.Exists(dir))
Directory.CreateDirectory(dir);
await PreformTask(video.VideoId,"VideoOnly",new Progress<double>(e=>{
progress?.Report(e / 3);
}));
await PreformTask(video.VideoId,"AudioOnly",new Progress<double>(e=>{
progress?.Report((e/3)+0.3333);
}));
if(!File.Exists(audioonly))
{
await PreformTask(video.VideoId,"PreMuxed",new Progress<double>(e=>{
progress?.Report((e/3)+0.3333);
}));
audioonly = premuxed;
}
if(File.Exists(audioonly) && File.Exists(videoonly) && !File.Exists(complete))
{
if(File.Exists(incomplete))
File.Delete(incomplete);
await ConvertAsync($"-i \"{videoonly}\" -i \"{audioonly}\" -c copy -map 0:v -map 1:a -preset ultrafast \"{incomplete}\"",new Progress<TimeSpan>((v)=>{
progress?.Report(( v.TotalSeconds/video.Duration.TotalSeconds/3)+0.6666); //no we are not worshiping satan that is 2/3
}));
}
if(File.Exists(incomplete))
File.Move(incomplete,complete);
}
private async Task ConvertToMp4(SavedVideo video,IProgress<double> progress=null)
{
string dir = Path.Combine(ServerPath,"MP4");
string incomplete = Path.Combine(dir,$"{video.VideoId}incomplete.mp4");
string complete = Path.Combine(dir,$"{video.VideoId}.mp4");
string audioonly=Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}");
string videoonly=Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}");
string premuxed = Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}");
if(File.Exists(complete)) return;
if(!Directory.Exists(dir))
Directory.CreateDirectory(dir);
await PreformTask(video.VideoId,"VideoOnly",new Progress<double>(e=>{
progress?.Report(e / 3);
}));
await PreformTask(video.VideoId,"AudioOnly",new Progress<double>(e=>{
progress?.Report((e/3)+33.3333);
}));
if(!File.Exists(audioonly))
{
await PreformTask(video.VideoId,"PreMuxed",new Progress<double>(e=>{
progress?.Report((e/3)+33.3333);
}));
audioonly = premuxed;
}
if(File.Exists(audioonly) && File.Exists(videoonly) && !File.Exists(complete))
{
if(File.Exists(incomplete))
File.Delete(incomplete);
await ConvertAsync($"-i \"{videoonly}\" -i \"{audioonly}\" -c:v libx264 -c:a aac -map 0:v -map 1:a -preset ultrafast \"{incomplete}\"",new Progress<TimeSpan>((v)=>{
progress?.Report((v.TotalSeconds/video.Duration.TotalSeconds/3)+66.6666); //no we are not worshiping satan that is 2/3
}));
}
if(File.Exists(incomplete))
File.Move(incomplete,complete);
}
private async Task PreformTask(VideoId id, string resolution,IProgress<double> progress=null)
{
Job _j=null;
await LockAsync(async()=>{
foreach(var j in Jobs)
{
if(j.Id == id && j.JobType == resolution)
{
_j = j;
}
}
if(_j == null)
{
_j = new Job();
_j.Id = id;
_j.JobType = resolution;
_j.Slim = semaphore;
Jobs.Add(_j);
_j.JobTask = async(p)=>{
switch(resolution)
{
case "PreMuxed":
{
var (video,removedByYT,off) = await GetSavedVideoAsync(id);
if(off && !Directory.Exists(Path.Combine(ServerPath,"PreMuxed")) && !File.Exists(Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}")))
throw new VideoUnavailableException("We don't have access to YouTube");
if(removedByYT && !Directory.Exists(Path.Combine(ServerPath,"PreMuxed")) && !File.Exists(Path.Combine(ServerPath,"PreMuxed",$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}")))
throw new VideoUnavailableException("We dont have the video either");
await DownloadPreMuxed(video,p);
}
break;
case "AudioOnly":
{
var (video,removedByYT,off) = await GetSavedVideoAsync(id);
if(off && !Directory.Exists(Path.Combine(ServerPath,"AudioOnly")) && !File.Exists(Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}")))
throw new VideoUnavailableException("We don't have access to YouTube");
if(removedByYT && !Directory.Exists(Path.Combine(ServerPath,"AudioOnly")) && !File.Exists(Path.Combine(ServerPath,"AudioOnly",$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}")))
throw new VideoUnavailableException("We dont have the video either");
await DownloadAudioOnly(video,p);
}
break;
case "VideoOnly":
{
var (video,removedByYT,off) = await GetSavedVideoAsync(id);
if(off && !Directory.Exists(Path.Combine(ServerPath,"VideoOnly")) && !File.Exists(Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}")))
throw new VideoUnavailableException("We don't have access to YouTube");
if(removedByYT && !Directory.Exists(Path.Combine(ServerPath,"VideoOnly")) && !File.Exists(Path.Combine(ServerPath,"VideoOnly",$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}")))
throw new VideoUnavailableException("We dont have the video either");
await DownloadVideoOnly(video,p);
}
break;
case "MP3":
{
var (video,removedByYT,off) = await GetSavedVideoAsync(id);
if(!HasConverter && !Directory.Exists(Path.Combine(ServerPath,"MP3")) && !File.Exists(Path.Combine(ServerPath,"MP3",$"{id}.mp3")))
throw new OperationCanceledException("We don't have the converter.");
await ConvertToMp3(video,p);
}
break;
case "MP4":
{
var (video,removedByYT,off) = await GetSavedVideoAsync(id);
if(!HasConverter && !Directory.Exists(Path.Combine(ServerPath,"MP4")) && !File.Exists(Path.Combine(ServerPath,"MP4",$"{id}.mp4")))
throw new OperationCanceledException("We don't have the converter.");
await ConvertToMp4(video,p);
}
break;
case "MKV":
{
var (video,removedByYT,off) = await GetSavedVideoAsync(id);
if(!HasConverter && !Directory.Exists(Path.Combine(ServerPath,"MKV")) && !File.Exists(Path.Combine(ServerPath,"MKV",$"{id}.mkv")))
throw new OperationCanceledException("We don't have the converter.");
await MuxToMkv(video,p);
}
break;
}
};
_j.Run(Jobs);
}
await Task.CompletedTask;
});
if(_j == null) throw new NullReferenceException();
Action<double> update= (e)=>{
try{
progress?.Report(e);
}catch(Exception ex){_=ex;}
};
_j.Update+=update;
while(!_j.Done)
{
if(_j.CurrentException != null) throw _j.CurrentException;
}
_j.Update-=update;
}
private async Task DownloadAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("url",out var url))
{
VideoId? id = VideoId.TryParse(url);
object result = new {invalidurl=true};
if(id.HasValue)
{
(SavedVideo video,bool notavailable,bool offline)=await GetSavedVideoAsync(id.Value);
bool premuxed = (notavailable || offline) ? await StreamExistsAsync(video,"PreMuxed") : true;
bool audioonly = (notavailable || offline) ? await StreamExistsAsync(video,"AudioOnly") : true;
bool videoonly = (notavailable || offline) ? await StreamExistsAsync(video,"VideoOnly") : true;
bool converter=HasConverter; //change me
result = new {
offline,
notavailable,
premuxed,
audioonly,
videoonly,
converter,
v = id.Value.Value,
title = HttpUtility.HtmlEncode(video.VideoTitle),
channel = HttpUtility.HtmlEncode(video.ChannelTitle),
otherstats = $"{MakeWeb(video.Views)} View(s) {MakeWeb(video.Likes)} Like(s)<br>Uploaded: {video.UploadDate.Humanize()}<br>Length: {video.Duration.ToString()}",
desc=DescriptLinkUtils(video.Description ?? "").Replace("\n","<br>")
};
}
await ctx.SendTextAsync(await template.Value.RenderAsync(new{body=await pageDownload.Value.RenderAsync(result)}));
}
}
private static bool StartsWithAt(string str,int indexof,string startsWith)
{
if(str.Length-indexof < startsWith.Length) return false;
for(int i = 0;i<startsWith.Length;i++)
{
if(str[i+indexof] != startsWith[i]) return false;
}
return true;
}
internal static string DescriptLinkUtils(string url)
{
StringBuilder b=new StringBuilder();
for(int i = 0;i<url.Length;i++)
{
if(StartsWithAt(url,i,"http:") || StartsWithAt(url,i,"https:") || StartsWithAt(url,i,"ftp:") || StartsWithAt(url,i,"ftps:") || StartsWithAt(url,i,"sftp:") || StartsWithAt(url,i,"magnet:"))
{
StringBuilder b2=new StringBuilder();
for(;i<url.Length;i++)
{
if(url[i] == '\n' || url[i] == ' ')
{
break;
}
b2.Append(url[i]);
}
i--;
b.Append($"<a href=\"{HttpUtility.HtmlAttributeEncode(b2.ToString())}\">{HttpUtility.HtmlEncode(b2.ToString())}</a>");
}
else
{
b.Append(HttpUtility.HtmlEncode(url[i]));
}
}
return b.ToString();
}
private string MakeWeb(long no)
{
if(no < 1000) return $"{no}";
if(no < 1000000) return $"{(long)(no/1000)} k";
if(no < 1000000000) return $"{(long)(no/1000000)} M";
if(no < 1000000000000) return $"{(long)(no/1000000000)} B";
return $"{(long)(no/1000000000000)} T";
}
private async Task<bool> StreamExistsAsync(SavedVideo video, string res)
{
switch(res)
{
case "PreMuxed":
{
string dir = Path.Combine(ServerPath,"PreMuxed");
if(!Directory.Exists(dir)) return false;
return await Task.FromResult(File.Exists(Path.Combine(dir,$"{video.VideoId}.{(video.PreMuxed != null ? video.PreMuxed.Container : "mp4")}")));
}
case "VideoOnly":
{
string dir = Path.Combine(ServerPath,"VideoOnly");
if(!Directory.Exists(dir)) return false;
return await Task.FromResult(File.Exists(Path.Combine(dir,$"{video.VideoId}.{(video.VideoOnly != null ? video.VideoOnly.Container : "mp4")}")));
}
case "AudioOnly":
{
string dir = Path.Combine(ServerPath,"AudioOnly");
if(!Directory.Exists(dir)) return false;
return await Task.FromResult(File.Exists(Path.Combine(dir,$"{video.VideoId}.{(video.AudioOnly != null ? video.AudioOnly.Container : "mp4")}")));
}
}
return false;
}
private async Task LockAsync(Func<Task> task)
{
Exception ex2=null;
await semaphore.WaitAsync();
try{
await task();
}catch(Exception ex)
{
ex2 = ex;
}
semaphore.Release();
if(ex2 != null) throw ex2;
}
private async Task<byte[]> GetThumbnailAsync(VideoId id,string resolution)
{
string dir = Path.Combine(ServerPath,"Thumbnails",id.Value);
string filename = Path.Combine(dir,$"{resolution}.jpg");
await LockAsync(async()=>{
Directory.CreateDirectory(dir);
if(!File.Exists(filename))
{
File.WriteAllBytes(filename,await HttpClient.GetByteArrayAsync($"https://s.ytimg.com/vi/{id.Value}/{resolution}.jpg"));
}
});
return File.ReadAllBytes(filename);
}
private async Task ThumbnailAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("v",out var v))
{
if(!ctx.QueryParams.TryGetFirst("res",out var resolution))
{
resolution="sddefault";
}
VideoId? id = VideoId.TryParse(v);
if(id.HasValue)
{
await ctx.SendBytesAsync(await GetThumbnailAsync(id.Value,resolution),"image/jpeg");
}
}
}
private async Task IndexAsync(ServerContext ctx)
{
await ctx.SendTextAsync(await template.Value.RenderAsync(new{body=await pageIndex.Value.RenderAsync()}));
}
/*
public async Task<IDownloadHandle> GetDownloadHandle(VideoId id, Resolution resolution)
{
}*/
}
public class Job
{
public SemaphoreSlim Slim {get;set;}
public Exception CurrentException {get;set;}=null;
public void Run(List<Job> jobs)
{
Task.Run(async()=>{
try{
await JobTask(new Progress<double>(e=>{
Update?.Invoke(e);
}));
}catch(Exception ex)
{
CurrentException = ex;
await Slim.WaitAsync();
jobs.Remove(this);
Slim.Release();
}
await Slim.WaitAsync();
jobs.Remove(this);
Slim.Release();
Done = true;
}).Wait(0);
}
public Func<IProgress<double>,Task> JobTask {get;set;}
public event Action<double> Update;
public bool Done {get;set;}
public string JobType {get;set;}
public string Id {get;set;}
}
public class SavedStreamInfo
{
[BsonIgnore]
[JsonIgnore]
public IStreamInfo Stream {
get=>new SSI(this);
set{
Url=value.Url;
Container = value.Container.Name;
Size = value.Size.Bytes;
Bitrate = value.Bitrate.BitsPerSecond;
}
}
public string Container {get;set;}="";
public string Url {get;set;}="";
public long Size {get;set;}
public long Bitrate {get;set;}
private class SSI : IStreamInfo
{
SavedStreamInfo info;
public SSI(SavedStreamInfo info)
{
this.info = info;
}
public string Url => info.Url;
public Container Container => new Container(info.Container);
public FileSize Size => new FileSize(info.Size);
public Bitrate Bitrate => new Bitrate(info.Bitrate);
}
}
public class SavedVideo
{
public long Id {get;set;}
public string VideoId {get;set;}="";
public string VideoTitle {get;set;}="";
public string ChannelId {get;set;}="";
public string ChannelTitle {get;set;}="";
public string Description {get;set;}="";
public List<string> Tags {get;set;}=new List<string>();
public TimeSpan Duration {get;set;}=TimeSpan.Zero;
public long Likes {get;set;}
public long Dislikes {get;set;}
public long Views {get;set;}
public DateTime UploadDate {get;set;}=DateTime.MinValue;
public SavedStreamInfo PreMuxed {get;set;}=new SavedStreamInfo();
public SavedStreamInfo AudioOnly {get;set;}=new SavedStreamInfo();
public SavedStreamInfo VideoOnly {get;set;}=new SavedStreamInfo();
public DateTime Expires {get;set;}=DateTime.Now;
internal string SafeFileName(string ext)
{
const string BadChars = "<>?/\\*|^?\":";
StringBuilder b=new StringBuilder();
foreach(var c in VideoTitle)
{
if(BadChars.Contains(c)) continue;
if(char.IsControl(c)) continue;
b.Append(c);
}
b.Append($"-{VideoId}.{ext}");
return b.ToString();
}
public static implicit operator SavedVideo(Video video)
{
SavedVideo video1=new SavedVideo();
video1.VideoId = video.Id;
video1.VideoTitle = video.Title;
video1.ChannelId = video.Author.ChannelId;
video1.ChannelTitle = video.Author.ChannelTitle;
video1.Description=video.Description;
video1.Duration = video.Duration.HasValue ? video.Duration.Value : TimeSpan.Zero;
video1.UploadDate = video.UploadDate.Date;
video1.Likes = video.Engagement.LikeCount;
video1.Dislikes = video.Engagement.DislikeCount; //why do I bother
video1.Views = video.Engagement.ViewCount;
video1.Tags.AddRange(video.Keywords);
return video1;
}
}
}