Bring TYTD Back

This commit is contained in:
Mike Nolan 2024-10-19 00:17:43 -05:00
commit 3d1a64770a
17 changed files with 408 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin/
obj/
Creds.priv.cs

17
BringTYTDBack.csproj Normal file
View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SSH.NET" Version="2024.1.0" />
<PackageReference Include="Tesses.WebServer" Version="1.0.4.3" />
<PackageReference Include="Tesses.WebServer.EasyServer" Version="1.0.1" />
<PackageReference Include="YouTubeExplode" Version="6.4.3" />
</ItemGroup>
</Project>

9
Creds.cs Normal file
View File

@ -0,0 +1,9 @@
public static partial class Creds
{
public static string IP => _ip;
public static string Username => _username;
public static string Password => _password;
public static string DirectoryOnServer => _dir;
}

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ADD . /src
WORKDIR /src
RUN dotnet publish -c Release -o /out
FROM mcr.microsoft.com/dotnet/runtime:8.0
EXPOSE 3252
COPY --from=build /out /out
COPY --from=build /src/wwwroot /out/wwwroot
WORKDIR /out
CMD ["/out/BringTYTDBack"]
RUN chmod +x /out/BringTYTDBack

166
Program.cs Normal file
View File

@ -0,0 +1,166 @@
using System.Collections.Concurrent;
using Renci.SshNet;
using YoutubeExplode;
using YoutubeExplode.Videos;
using Tesses.WebServer;
using static Creds;
using System.Diagnostics.CodeAnalysis;
using YoutubeExplode.Playlists;
using YoutubeExplode.Channels;
YoutubeClient ytc=new YoutubeClient();
ConcurrentStack<VideoId> videoIds=new ConcurrentStack<VideoId>();
bool isRunning=true;
async Task AddVideo(VideoId id)
{
SshClient client = new SshClient(IP,Username,Password);
await client.ConnectAsync(default);
using(var res=client.CreateCommand($"echo {DirectoryOnServer}/*-{id.Value}.mp4"))
{
await res.ExecuteAsync();
if(res.Result == $"{DirectoryOnServer}/*-{id.Value}.mp4\n")
{
using(var res2=client.CreateCommand($"cd {DirectoryOnServer} && tytd {id.Value}"))
{
await res2.ExecuteAsync();
Console.WriteLine($"Downloaded: https://www.youtube.com/watch?v={id.Value}");
_=res2.Result;
}
}
else
{
Console.WriteLine($"Video already exists: https://www.youtube.com/watch?v={id.Value}");
}
}
}
async Task AddItem(string url)
{
VideoId? vId = VideoId.TryParse(url);
PlaylistId? pId = PlaylistId.TryParse(url);
ChannelId? cId = ChannelId.TryParse(url);
UserName? username = UserName.TryParse(url);
ChannelSlug? slug = ChannelSlug.TryParse(url);
ChannelHandle? handle = ChannelHandle.TryParse(url);
if(url.Length == 11 && vId.HasValue)
{
videoIds.Push(vId.Value);
}
else if(pId.HasValue)
{
await foreach(var v in ytc.Playlists.GetVideosAsync(pId.Value))
{
videoIds.Push(v.Id);
}
}
else if(vId.HasValue)
{
videoIds.Push(vId.Value);
}
else if(cId.HasValue)
{
await foreach(var c in ytc.Channels.GetUploadsAsync(cId.Value))
{
videoIds.Push(c.Id);
}
}
else if(username.HasValue)
{
cId = (await ytc.Channels.GetByUserAsync(username.Value)).Id;
await foreach(var c in ytc.Channels.GetUploadsAsync(cId.Value))
{
videoIds.Push(c.Id);
}
}
else if(slug.HasValue)
{
cId = (await ytc.Channels.GetBySlugAsync(slug.Value)).Id;
await foreach(var c in ytc.Channels.GetUploadsAsync(cId.Value))
{
videoIds.Push(c.Id);
}
}
else if(handle.HasValue)
{
cId = (await ytc.Channels.GetByHandleAsync(handle.Value)).Id;
await foreach(var c in ytc.Channels.GetUploadsAsync(cId.Value))
{
videoIds.Push(c.Id);
}
}
}
Task.Factory.StartNew(async()=>{
while(isRunning)
{
if(videoIds.TryPop(out var res))
{
await AddVideo(res);
}
}
},TaskCreationOptions.LongRunning).Wait(0);
MountableServer msvr=new MountableServer(new StaticServer("wwwroot"));
msvr.Mount("/api/AddItemRes/1/",new TYTDServer(AddItem));
msvr.Mount("/api/AddItem/",new TYTDServer(AddItem));
msvr.Mount("/api/AddVideoRes/1/",new TYTDServer(videoIds));
msvr.Mount("/api/AddVideo/",new TYTDServer(videoIds));
RouteServer routeServer=new RouteServer(msvr);
routeServer.Add("/itemsInQueue",async(ctx)=>{
await ctx.SendJsonAsync(videoIds.Count);
});
routeServer.Add("/add",async(ctx)=>{
if(ctx.QueryParams.TryGetFirst("v",out var v))
{
await AddItem(v);
}
await ctx.SendRedirectAsync("/");
});
routeServer.StartServer(3252);
public class TYTDServer : Server
{
bool onlyVideos;
ConcurrentStack<VideoId>? stk;
Func<string,Task>? cb;
public TYTDServer(ConcurrentStack<VideoId> stk)
{
onlyVideos=true;
this.stk = stk;
}
public TYTDServer(Func<string,Task> cb)
{
onlyVideos=false;
this.cb = cb;
}
public override async Task GetAsync(ServerContext ctx)
{
if(onlyVideos)
{
string url=ctx.UrlPath.Substring(1);
stk?.Push(url);
await ctx.SendTextAsync("<script>window.history.back();</script>");
}
else
{
if(cb == null) {
await ctx.SendTextAsync("<script>window.history.back();</script>");
return;
}
string url=ctx.UrlPath.Substring(1);
await cb(url);
await ctx.SendTextAsync("<script>window.history.back();</script>");
}
}
}

31
README.md Normal file
View File

@ -0,0 +1,31 @@
Bring TYTD Back
===============
I wanted to bring [TYTD](https://gitea.site.tesses.net/tesses50/tytd) back so I did somewhat
This project requires [tytd-cli](https://gitea.site.tesses.net/tesses50/tytd-cli-cpp) to be installed on a ssh (password protected server, could use private key if you modify Program.cs async Task AddVideo), the tytd-cli must be installed on PATH
To Configure you must add a file called Creds.priv.cs with the following content
```cs
public static partial class Creds
{
static string _ip = "YOUR_SSH_SERVER";
static string _username = "YOUR_SSH_USERNAME";
static string _password = "YOUR_SSH_PASSWORD";
static string _dir = "YOUR_SSH_PATH";
}
```
# WITHOUT DOCKER
then compile the project using
```bash
dotnet publish -c Release -o out --self-contained -r linux-x64 -p:PublishSingleFile=true -p:PublishReadyToRun=true
```
then copy wwwroot to out
# WITH DOCKER
```bash
docker build -t bring-tytd-back:latest .
docker run -p 3252:3252 -d bring-tytd-back:latest
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

1
wwwroot/beer.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
wwwroot/beer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
wwwroot/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

47
wwwroot/index.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TYTD</title>
<link rel="stylesheet" href="./beer.min.css">
<link rel="stylesheet" href="./theme.css">
</head>
<body>
<article class="medium middle-align center-align">
<div>
<i class="extra">download</i>
<h5>Add a video, playlist or channel to my TYTD</h5>
<div class="space"></div>
<form action="./add">
<nav class="no-space">
<div class="max field border left-round">
<input type="url" name="v">
</div>
<button type="submit" class="large right-round">Add</button>
</nav>
</form>
</div>
</article>
<article>
There are <span id="noItems">0</span> item(s) in the queue
</article>
<script src="./beer.min.js"></script>
<script defer>
const d = document.getElementById('noItems');
setInterval(()=>{
fetch('./itemsInQueue').then(e=>e.text()).then(e=>{
d.innerText = e;
});
},3000);
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

21
wwwroot/site.webmanifest Normal file
View File

@ -0,0 +1,21 @@
{
"name": "Tesses YouTube Downloader",
"short_name": "Tesses YouTube Downloader",
"start_url": "./",
"scope": ".",
"icons": [
{
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ff0000",
"background_color": "#ffffff",
"display": "standalone"
}

96
wwwroot/theme.css Normal file
View File

@ -0,0 +1,96 @@
:root,html,
body {
--primary:#ffb4a8;
--on-primary:#690000;
--primary-container:#930100;
--on-primary-container:#ffdad4;
--secondary:#e7bdb6;
--on-secondary:#442925;
--secondary-container:#5d3f3b;
--on-secondary-container:#ffdad4;
--tertiary:#dec48c;
--on-tertiary:#3e2e04;
--tertiary-container:#564419;
--on-tertiary-container:#fbdfa6;
--error:#ffb4ab;
--on-error:#690005;
--error-container:#93000a;
--on-error-container:#ffb4ab;
--background:#201a19;
--on-background:#ede0dd;
--surface:#181211;
--on-surface:#ede0dd;
--surface-variant:#534341;
--on-surface-variant:#d8c2be;
--outline:#a08c89;
--outline-variant:#534341;
--shadow:#000000;
--scrim:#000000;
--inverse-surface:#ede0dd;
--inverse-on-surface:#362f2e;
--inverse-primary:#c00100;
--surface-dim:#181211;
--surface-bright:#3f3736;
--surface-container-lowest:#120d0c;
--surface-container-low:#201a19;
--surface-container:#251e1d;
--surface-container-high:#2f2827;
--surface-container-highest:#3b3332;
}
@media (prefers-color-scheme: light) {
:root,html,
body {
--primary:#c00100;
--on-primary:#ffffff;
--primary-container:#ffdad4;
--on-primary-container:#410000;
--secondary:#775651;
--on-secondary:#ffffff;
--secondary-container:#ffdad4;
--on-secondary-container:#2c1512;
--tertiary:#705c2e;
--on-tertiary:#ffffff;
--tertiary-container:#fbdfa6;
--on-tertiary-container:#251a00;
--error:#ba1a1a;
--on-error:#ffffff;
--error-container:#ffdad6;
--on-error-container:#410002;
--background:#fffbff;
--on-background:#201a19;
--surface:#fff8f6;
--on-surface:#201a19;
--surface-variant:#f5ddda;
--on-surface-variant:#534341;
--outline:#857370;
--outline-variant:#d8c2be;
--shadow:#000000;
--scrim:#000000;
--inverse-surface:#362f2e;
--inverse-on-surface:#fbeeec;
--inverse-primary:#ffb4a8;
--surface-dim:#e4d7d5;
--surface-bright:#fff8f6;
--surface-container-lowest:#ffffff;
--surface-container-low:#fef1ee;
--surface-container:#f8ebe9;
--surface-container-high:#f3e5e3;
--surface-container-highest:#ede0dd;
}
}
.horizonal
{
display: flex;
flex-direction: column;
}
.vertical
{
display: flex;
flex-direction: row;
}
.filler
{
flex-grow: 1;
}