diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da4ad96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.svclog +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml +*.azurePubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +packages/ +## TODO: If the tool you use requires repositories.config, also uncomment the next line +!packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +![Ss]tyle[Cc]op.targets +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml + +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac desktop service store files +.DS_Store + +_NCrunch* + +Tesses.YouTubeDownloader.Net6/AudioOnly/ +Tesses.YouTubeDownloader.Net6/VideoOnly/ +Tesses.YouTubeDownloader.Net6/Muxed/ +Tesses.YouTubeDownloader.Net6/Info/ +Tesses.YouTubeDownloader.Net6/Thumbnails/ +Tesses.YouTubeDownloader.Net6/StreamInfo/ +Tesses.YouTubeDownloader.Net6/PreMuxed/ +Tesses.YouTubeDownloader.Net6/config/ +Tesses.YouTubeDownloader.Net6/Playlist/ +Tesses.YouTubeDownloader.Net6/Channel/ +Tesses.YouTubeDownloader.Net6/Subscriptions/ +push diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..80cd92e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env + +WORKDIR /app +RUN git clone https://gitlab.tesses.net/tesses50/tytd_proxy . +WORKDIR /app/TYTDProxy +# Restore as distinct layers +RUN dotnet restore +# Build and publish a release +RUN dotnet publish -c Release -o /app/out + +# Build runtime image +FROM mcr.microsoft.com/dotnet/runtime:6.0 +WORKDIR /app +EXPOSE 3252 +COPY --from=build-env /app/out . +ENTRYPOINT ["dotnet", "TYTDProxy.dll","--docker"] diff --git a/LICENSE.Tesses.WebServer b/LICENSE.Tesses.WebServer new file mode 100644 index 0000000..4bd7b72 --- /dev/null +++ b/LICENSE.Tesses.WebServer @@ -0,0 +1,24 @@ +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 diff --git a/TYTDProxy/Program.cs b/TYTDProxy/Program.cs new file mode 100644 index 0000000..9b41df3 --- /dev/null +++ b/TYTDProxy/Program.cs @@ -0,0 +1,183 @@ +using System.Net; +using Tesses.WebServer; +using YoutubeExplode; +using YoutubeExplode.Search; +using YoutubeExplode.Videos; +using YoutubeExplode.Videos.Streams; +public class TYTDProxy +{ + static YoutubeClient Youtube = new YoutubeClient(); + public static async Task Main(string[] args) + { + + using (var token = new CancellationTokenSource()) + { + Console.CancelKeyPress += (sender,e)=>{ + token.Cancel(); + }; + StaticServer static_server=new StaticServer("WebSite"); + RouteServer api_server=new RouteServer(); + RouteApi(api_server); + MountableServer mountable_server=new MountableServer(static_server); + mountable_server.Mount("/api/",api_server); + + HttpServerListener listener=new HttpServerListener(new System.Net.IPEndPoint(System.Net.IPAddress.Any,3253),mountable_server); + await listener.ListenAsync(token.Token); + } + } + + public static void RouteApi(RouteServer api) + { + api.Add("/Video.json",Video); + api.Add("/Video.json",Video,"POST"); + api.Add("/PreMuxed",PreMuxed); + api.Add("/VideoOnly",VideoOnly); + api.Add("/AudioOnly",AudioOnly); + api.Add("/PreMuxed",PreMuxed,"POST"); + api.Add("/VideoOnly",VideoOnly,"POST"); + api.Add("/AudioOnly",AudioOnly,"POST"); + api.Add("/DownloadYTStream",DownloadYTStream,"POST"); + api.Add("/Search.json",Search,"POST"); + api.Add("/Search.json",Search); + } + public class StreamResult + { + public StreamResult(IStreamInfo info) + { + url = info.Url; + ext=info.Container.Name; + size = info.Size.Bytes; + } + public string url {get;set;} + + public string ext {get;set;} + + public long size {get;set;} + } + public class VideoResult + { + public VideoResult(StreamResult muxed,StreamResult video_only,StreamResult audio_only,SavedVideo video) + { + this.muxed=muxed; + this.video_only=video_only; + this.audio_only = audio_only; + this.video_info=video; + } + public StreamResult muxed {get;set;} + + public StreamResult video_only {get;set;} + + public StreamResult audio_only {get;set;} + + public SavedVideo video_info {get;set;} + } + public static async Task Search(ServerContext ctx) + { + string q; + if(ctx.QueryParams.TryGetFirst("q",out q)){ + if(ctx.Method == "POST") ctx.ParseBody(); + if(ctx.Method != "POST") q=WebUtility.UrlDecode(q); + { + List searchResults=new List(); + await foreach(var item in Youtube.Search.GetVideosAsync(q)) + { + searchResults.Add(item); + } + + await ctx.SendJsonAsync(searchResults); + } + } + } + public static async Task PreMuxed(ServerContext ctx) + { + string v; + if(ctx.QueryParams.TryGetFirst("v",out v)){ + if(ctx.Method == "POST") ctx.ParseBody(); + if(ctx.Method != "POST") v=WebUtility.UrlDecode(v); + var vid=await Youtube.Videos.GetAsync(v); + var res= await Youtube.Videos.Streams.GetManifestAsync(v); + var bestMuxed=res.GetMuxedStreams().GetWithHighestVideoQuality(); + System.Net.Mime.ContentDisposition contentDisposition=new System.Net.Mime.ContentDisposition(); + contentDisposition.FileName = vid.Title; + await ctx.SendStreamAsync(await Youtube.Videos.Streams.GetAsync(bestMuxed),$"file.{bestMuxed.Container.Name}"); + } + } + public static async Task VideoOnly(ServerContext ctx) + { + string v; + if(ctx.QueryParams.TryGetFirst("v",out v)){ + if(ctx.Method == "POST") ctx.ParseBody(); + if(ctx.Method != "POST") v=WebUtility.UrlDecode(v); + var vid=await Youtube.Videos.GetAsync(v); + var res= await Youtube.Videos.Streams.GetManifestAsync(v); + var bestVideoOnly=res.GetVideoOnlyStreams().GetWithHighestVideoQuality(); + System.Net.Mime.ContentDisposition contentDisposition=new System.Net.Mime.ContentDisposition(); + contentDisposition.FileName = vid.Title; + await ctx.SendStreamAsync(await Youtube.Videos.Streams.GetAsync(bestVideoOnly),$"file.{bestVideoOnly.Container.Name}"); + } + } + public static async Task AudioOnly(ServerContext ctx) + { + string v; + if(ctx.QueryParams.TryGetFirst("v",out v)){ + if(ctx.Method == "POST") ctx.ParseBody(); + if(ctx.Method != "POST") v=WebUtility.UrlDecode(v); + var vid=await Youtube.Videos.GetAsync(v); + var res= await Youtube.Videos.Streams.GetManifestAsync(v); + var bestAudioOnly=res.GetAudioOnlyStreams().GetWithHighestBitrate(); + System.Net.Mime.ContentDisposition contentDisposition=new System.Net.Mime.ContentDisposition(); + contentDisposition.FileName = vid.Title; + await ctx.SendStreamAsync(await Youtube.Videos.Streams.GetAsync(bestAudioOnly),$"file.{bestAudioOnly.Container.Name}"); + } + } + public class DummyStr : IStreamInfo + { + public DummyStr(string url,long len) + { + this.url = url; + this.len=len; + + } + long len; + string url; + public string Url => url; + + public Container Container => Container.Mp4; + public FileSize Size => new FileSize(len); + + public Bitrate Bitrate => new Bitrate(420000); + } + public static async Task DownloadYTStream(ServerContext ctx) + { + string v; + string lenTxt; + if(ctx.QueryParams.TryGetFirst("url",out v)){ + if(ctx.QueryParams.TryGetFirst("flen",out lenTxt)){ + if(ctx.Method == "POST") ctx.ParseBody(); + if(ctx.Method != "POST") v=WebUtility.UrlDecode(v); + long len; + if(long.TryParse(lenTxt,out len)){ + + await ctx.SendStreamAsync(await Youtube.Videos.Streams.GetAsync(new DummyStr(v,len)),HeyRed.Mime.MimeTypesMap.GetMimeType("file.bin")); + } + } + } + } + public static async Task Video(ServerContext ctx) + { + string v; + if(ctx.QueryParams.TryGetFirst("v",out v)){ + if(ctx.Method == "POST") ctx.ParseBody(); + if(ctx.Method != "POST") v=WebUtility.UrlDecode(v); + + var res= await Youtube.Videos.Streams.GetManifestAsync(v); + var bestMuxed=new StreamResult(res.GetMuxedStreams().GetWithHighestVideoQuality()); + var bestVideoOnly=new StreamResult(res.GetVideoOnlyStreams().GetWithHighestVideoQuality()); + var bestAudioOnly=new StreamResult(res.GetAudioOnlyStreams().GetWithHighestBitrate()); + SavedVideo video = new SavedVideo(await Youtube.Videos.GetAsync(v)); + VideoResult result = new VideoResult(bestMuxed,bestVideoOnly,bestAudioOnly,video); + await ctx.SendJsonAsync(result); + + } + } +} \ No newline at end of file diff --git a/TYTDProxy/SavedVideo.cs b/TYTDProxy/SavedVideo.cs new file mode 100644 index 0000000..aba4ee7 --- /dev/null +++ b/TYTDProxy/SavedVideo.cs @@ -0,0 +1,88 @@ +using System.Text; +using YoutubeExplode.Common; +using YoutubeExplode.Videos; + +public class SavedVideo + { + public SavedVideo() + { + Id = ""; + Title = ""; + AuthorChannelId = ""; + AuthorTitle = ""; + Description = ""; + Keywords = new string[0]; + Likes = 0; + Dislikes = 0; + Views = 0; + Duration = TimeSpan.Zero; + UploadDate=new DateTime(1992,8,20); + AddDate=DateTime.Now; + LegacyVideo=false; + } + public SavedVideo(Video video) + { + Id=video.Id; + Title = video.Title; + AuthorChannelId = video.Author.ChannelId; + AuthorTitle = video.Author.ChannelTitle; + Description = video.Description; + Keywords=video.Keywords.ToArray(); + Likes=video.Engagement.LikeCount; + Dislikes = video.Engagement.DislikeCount; + Views = video.Engagement.ViewCount; + Duration = video.Duration.Value; + UploadDate = video.UploadDate.DateTime; + AddDate=DateTime.Now; + LegacyVideo=false; + } + public bool LegacyVideo {get;set;} + + + public Video ToVideo() + { + List thumbnails=new List(); + thumbnails.Add(new Thumbnail($"https://s.ytimg.com/vi/{Id}/default.jpg",new YoutubeExplode.Common.Resolution(120,90))); + thumbnails.Add(new Thumbnail($"https://s.ytimg.com/vi/{Id}/hqdefault.jpg",new YoutubeExplode.Common.Resolution(480,360))); + thumbnails.Add(new Thumbnail($"https://s.ytimg.com/vi/{Id}/mqdefault.jpg",new YoutubeExplode.Common.Resolution(320,180))); + + return new Video(Id,Title,new YoutubeExplode.Common.Author(AuthorChannelId,AuthorTitle),new DateTimeOffset(UploadDate),Description,Duration,thumbnails,Keywords.ToList(),new Engagement(Views,Likes,Dislikes)); + } + + + + public DateTime AddDate {get;set;} + public string Title { get; set; } + public DateTime UploadDate { get; set; } + public string[] Keywords { get; set; } + public string Id { get; set; } + public string AuthorTitle { get; set; } + public string AuthorChannelId { get; set; } + + public string Description { get; set; } + + public TimeSpan Duration { get; set; } + + public long Views { get; set; } + public long Likes { get; set; } + public long Dislikes { get; set; } + + + + public override string ToString() + { + StringBuilder b=new StringBuilder(); + b.AppendLine($"Title: {Title}"); + b.AppendLine($"AuthorTitle: {AuthorTitle}"); + DateTime date=UploadDate; + + b.AppendLine($"Upload Date: {date.ToShortDateString()}"); + + b.AppendLine($"Likes: {Likes}, Dislikes: {Dislikes}, Views: {Views}"); + b.AppendLine($"Duration: {Duration.ToString()}"); + b.AppendLine($"Tags: {string.Join(", ",Keywords)}"); + b.AppendLine("Description:"); + b.AppendLine(Description); + return b.ToString(); + } + } \ No newline at end of file diff --git a/TYTDProxy/TYTDProxy.csproj b/TYTDProxy/TYTDProxy.csproj new file mode 100644 index 0000000..419ef18 --- /dev/null +++ b/TYTDProxy/TYTDProxy.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/TYTDProxy/WebSite/index.html b/TYTDProxy/WebSite/index.html new file mode 100644 index 0000000..d96d18e --- /dev/null +++ b/TYTDProxy/WebSite/index.html @@ -0,0 +1,17 @@ + + + + + + + YouTube Downloader + + +

Download YouTube Videos

+ +
+ + +
+ + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3894aa2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '2' +services: + tytd: + build: Dockerfile + image: tytd-proxy-ogc + ports: + - 3253:3253 + volumes: + - tytd-data:/data +volumes: + tytd-data: