First commit

This commit is contained in:
2025-10-15 00:07:35 -05:00
commit 5e354c05be
69 changed files with 4598 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
bin
obj
publish
TYTD

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM onedev.site.tesses.net/crosslang/crosslangextras/crosslangextras:latest AS build
WORKDIR /src/
RUN git clone https://onedev.site.tesses.net/tytd2025 && cd tytd2025/Tesses.YouTubeDownloader.Server && crosslang build
FROM onedev.site.tesses.net/crosslang/crosslang:latest
COPY --from=build /src/Tesses.YouTubeDownloader.Server/bin /app
WORKDIR /data
EXPOSE 3255
ENTRYPOINT ["crossvm","--port=3255","/app/Tesses.YouTubeDownloader.Server.crvm"]

View File

@@ -0,0 +1,2 @@
bin
obj

View File

@@ -0,0 +1,31 @@
{
"dependencies": [
],
"info": {
"description": "Plugin Template for TYTD2025",
"maintainer": "Mike Nolan",
"repo":"https://onedev.site.tesses.net/tytd2025",
"homepage": "https://tesses.net/apps/tytd/2025/",
"short_name": "tytd2025plugin",
"short_name_pretty": "Tesses YouTube Downloader 2025 Plugin Template",
"license": "GPLv3",
"template_extra_text_ftles": [
],
"template_ignored_files": [
"bin",
"obj"
],
"template_info": {
"type": "lib",
"plugin_host": "tytd2025",
"short_name": "changeme",
"short_name_pretty": "Change Me",
"license": "GPLv3"
},
"template_project_dependencies": [
],
"type": "template"
},
"name": "Tesses.YouTubeDownloader.PluginTemplate",
"version": "1.0.0.0-prod"
}

View File

@@ -0,0 +1 @@
func PluginInit() new Plugin();

View File

@@ -0,0 +1,29 @@
/^
%PROJECT_NAME Class
^/
class Plugin {
public Plugin()
{
/*
You can access these anywhere in the plugin
TYTD.GetVideoId(id): get the youtube video id from url or id
TYTD.GetPlaylistId(id): get the youtube playlist id from url or id
TYTD.GetChannelId(id): get the youtube channel id from url or id
TYTD.Config[key]: get a string setting for this plugin
TYTD.Config[key] = value: set a string setting for this plugin (is presistant)
TYTD.Config.Directory: a crosslang SubdirFilesystem for the Files directory in plugin directory
TYTD.Config.DirectoryPath: same directory but the actual path of it for FS.Local
Resolution, SubscriptionBell, TYTD.Downloader (the instance, you can't create your own instance of it): See https://cpkg.tesseslanguage.com/package_docs?name=Tesses.YoutubeDownloader&version=1.0.0.0-prod
*/
Console.WriteLine("Run your initialization here");
}
public Server = new PluginServer();
public Close()
{
Console.WriteLine("Do any finishing work");
}
}

View File

@@ -0,0 +1,8 @@
class PluginServer
{
public Handle(ctx)
{
ctx.WithMimeType().SendText(<div><h1>Hello, world from @%PROJECT_NAME<h1><p>Path: {ctx.Path}, OriginalPath: {ctx.OriginalPath}</p></div>);
return true;
}
}

View File

@@ -0,0 +1,18 @@
{
"icon": "icon.png",
"info": {
"description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)",
"maintainer": "Mike Nolan",
"repo":"https://onedev.site.tesses.net/tytd2025",
"homepage": "https://tesses.net/apps/tytd/2025/",
"license": "GPLv3",
"short_name": "tytd2025",
"short_name_pretty": "Tesses YouTube Downloader 2025",
"type": "webapp"
},
"name": "Tesses.YouTubeDownloader.Server",
"project_dependencies": [
"..\/Tesses.YouTubeDownloader"
],
"version": "1.0.0.0-prod"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,19 @@
<svg width="100%" height="100%" viewBox="4 4 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="#D0BCFF">
<animate attributeName="d" dur="5s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8" keyTimes="0; 0.14; 0.14; 0.29; 0.29; 0.43; 0.43; 0.57; 0.57; 0.71; 0.71; 0.86; 0.86; 1" values="M20.9 10.4 21.4 9.5 21.9 8.7 22.5 7.8 23.2 7.2 24.2 7 25.1 7.4 25.7 8.1 26.2 9 26.8 9.8 27.3 10.6 28.1 11.2 29 11.3 30 11 30.9 10.6 31.8 10.3 32.8 9.9 33.7 10 34.5 10.6 34.9 11.5 34.8 12.5 34.8 13.5 34.7 14.5 34.7 15.5 35.2 16.3 36 16.8 37 17.1 37.9 17.3 38.9 17.5 39.8 17.9 40.4 18.7 40.5 19.7 40 20.5 39.4 21.3 38.7 22 38.1 22.8 37.6 23.7 37.7 24.6 38.3 25.5 38.9 26.2 39.6 27 40.2 27.7 40.5 28.7 40.3 29.6 39.5 30.3 38.6 30.6 37.6 30.8 36.7 31 35.7 31.3 35 31.9 34.6 32.8 34.7 33.8 34.8 34.8 34.9 35.8 34.8 36.8 34.3 37.6 33.4 38.1 32.4 38 31.5 37.6 30.6 37.2 29.7 36.9 28.7 36.6 27.8 36.9 27.1 37.6 26.6 38.5 26.1 39.3 25.5 40.2 24.8 40.8 23.8 41 22.9 40.6 22.3 39.9 21.8 39 21.2 38.2 20.7 37.4 19.9 36.8 19 36.7 18 37 17.1 37.4 16.2 37.7 15.2 38.1 14.3 38 13.5 37.4 13.1 36.5 13.2 35.5 13.2 34.5 13.3 33.5 13.3 32.5 12.8 31.7 12 31.2 11 31 10.1 30.7 9.1 30.5 8.2 30.1 7.6 29.3 7.5 28.3 8 27.5 8.7 26.7 9.3 26 9.9 25.2 10.4 24.3 10.3 23.4 9.7 22.5 9.1 21.8 8.4 21 7.8 20.3 7.5 19.3 7.7 18.4 8.5 17.7 9.4 17.4 10.4 17.2 11.3 17 12.3 16.7 13 16.1 13.4 15.2 13.3 14.2 13.2 13.2 13.1 12.2 13.2 11.2 13.7 10.4 14.6 9.9 15.6 10 16.5 10.4 17.4 10.8 18.3 11.1 19.3 11.4 20.2 11.1Z;
M20.3 8.6 21.1 8 22 7.6 23 7.3 23 7.3 24 7.2 25 7.3 25.9 7.5 26.8 8 27.6 8.6 28.4 9.1 28.4 9.1 29.3 9.6 30.3 9.8 31.3 9.9 32.3 10 33.3 10.2 34.2 10.6 34.2 10.6 35 11.2 35.7 11.9 36.3 12.7 36.7 13.6 36.9 14.6 37.2 15.5 37.2 15.5 37.6 16.5 38.2 17.3 38.9 18 39.6 18.7 40.2 19.5 40.6 20.4 40.6 20.4 40.9 21.3 41 22.3 40.9 23.3 40.6 24.3 40.2 25.2 39.8 26.1 39.8 26.1 39.5 27 39.4 28 39.5 29 39.6 30 39.5 31 39.3 32 39.3 32 38.9 32.9 38.3 33.7 37.6 34.4 36.8 35 35.9 35.4 35 35.8 35 35.8 34.1 36.3 33.4 37 32.9 37.9 32.3 38.7 31.6 39.4 30.8 40 30.8 40 29.9 40.4 28.9 40.7 27.9 40.8 27 40.7 26 40.4 25 40.1 25 40.1 24 40 23 40.1 22.1 40.4 21.1 40.7 20.1 40.8 19.1 40.7 19.1 40.7 18.2 40.5 17.3 40 16.4 39.5 15.7 38.8 15.2 37.9 14.6 37.1 14.6 37.1 13.9 36.4 13.1 35.8 12.2 35.4 11.3 35 10.5 34.4 9.7 33.8 9.7 33.8 9.1 32.9 8.7 32 8.5 31.1 8.4 30.1 8.5 29.1 8.6 28.1 8.6 28.1 8.5 27.1 8.3 26.1 7.8 25.2 7.4 24.3 7.1 23.4 7 22.4 7 22.4 7.1 21.4 7.3 20.4 7.8 19.5 8.3 18.7 9.1 18 9.8 17.3 9.8 17.3 10.4 16.5 10.8 15.6 11 14.6 11.3 13.7 11.7 12.8 12.2 11.9 12.2 11.9 12.9 11.2 13.8 10.7 14.7 10.2 15.6 10 16.6 9.9 17.6 9.8 17.6 9.8 18.6 9.6 19.5 9.2Z;
M18.6 9.6 19.5 9.2 20.3 8.6 21.1 8 22 7.6 23 7.3 24 7.2 25 7.3 25.9 7.5 26.8 8 27.6 8.6 28.4 9.1 29.3 9.6 30.3 9.8 31.3 9.9 32.3 10 33.3 10.2 34.2 10.6 35 11.2 35.7 11.9 36.3 12.7 36.7 13.6 36.9 14.6 37.2 15.5 37.6 16.4 38.2 17.3 38.9 18 39.6 18.7 40.2 19.5 40.6 20.4 40.9 21.3 41 22.3 40.9 23.3 40.6 24.3 40.2 25.2 39.8 26.1 39.5 27 39.4 28 39.5 29 39.6 30 39.5 31 39.3 32 38.9 32.9 38.3 33.7 37.6 34.4 36.8 35 35.9 35.4 35 35.8 34.1 36.3 33.4 37 32.9 37.9 32.3 38.7 31.6 39.4 30.8 40 29.9 40.4 28.9 40.7 27.9 40.8 27 40.7 26 40.4 25 40.1 24 40 23 40.1 22.1 40.4 21.1 40.7 20.1 40.8 19.1 40.7 18.2 40.5 17.3 40 16.4 39.5 15.7 38.8 15.2 37.9 14.6 37.1 13.9 36.4 13.1 35.8 12.2 35.4 11.3 35 10.5 34.4 9.7 33.8 9.1 32.9 8.7 32 8.5 31.1 8.4 30.1 8.5 29.1 8.6 28.1 8.5 27.1 8.3 26.1 7.8 25.2 7.4 24.3 7.1 23.4 7 22.4 7.1 21.4 7.3 20.4 7.8 19.5 8.3 18.7 9.1 18 9.8 17.3 10.4 16.5 10.8 15.6 11 14.6 11.3 13.7 11.7 12.8 12.2 11.9 12.9 11.2 13.8 10.7 14.7 10.2 15.6 10 16.6 9.9 17.6 9.8Z;
M18.6 9.9 19.5 9.4 20.3 8.8 21.1 8.2 22 7.8 23 7.6 23.9 7.5 24.9 7.6 25.9 7.8 26.8 8.2 27.6 8.7 28.5 9.3 29.3 9.9 30.1 10.5 30.9 11 31.7 11.6 32.5 12.2 33.3 12.8 33.7 13.1 34.1 13.3 34.9 13.9 35.7 14.5 36.6 15 37.4 15.6 38.2 16.2 39 16.8 39.7 17.5 40.2 18.3 40.7 19.2 40.9 20.1 41 21.1 40.9 22.1 40.7 23.1 40.3 24 40 24.9 39.7 25.9 39.4 26.8 39 27.8 38.7 28.7 38.4 29.6 38.1 30.6 37.8 31.5 37.5 32.5 37.2 33.4 36.9 34.4 36.6 35.3 36.2 36.2 35.7 37.1 35 37.8 34.3 38.4 33.4 38.9 32.5 39.3 31.5 39.5 30.5 39.5 30 39.5 29.5 39.5 28.5 39.5 27.5 39.5 26.5 39.5 25.5 39.4 24.5 39.4 23.6 39.4 22.6 39.4 21.6 39.5 20.6 39.5 19.6 39.5 18.6 39.5 17.6 39.5 16.6 39.5 15.6 39.3 14.7 39 13.8 38.5 13.1 37.9 12.4 37.2 11.9 36.3 11.5 35.4 11.2 34.5 10.9 33.5 10.6 32.6 10.3 31.6 10 30.7 9.7 29.7 9.3 28.8 9 27.9 8.7 26.9 8.4 26 8 25 7.7 24.1 7.4 23.2 7.1 22.2 7.1 21.7 7 21.2 7.1 20.2 7.3 19.3 7.7 18.4 8.3 17.5 8.9 16.8 9.7 16.2 10.5 15.6 11.4 15.1 12.2 14.5 13 14 13.8 13.4 14.6 12.8 15.4 12.3 16.2 11.7 17 11.1 17.8 10.5Z;
M15.4 12.3 16.2 11.7 17 11.1 17.8 10.5 18.6 9.9 19.5 9.4 20.3 8.8 21.1 8.3 22 7.8 23 7.6 23.9 7.5 24.9 7.6 25.9 7.8 26.8 8.2 27.6 8.7 28.5 9.3 29.3 9.9 30.1 10.5 30.9 11 31.7 11.6 32.5 12.2 33.3 12.8 34.1 13.3 34.9 13.9 35.7 14.5 36.6 15 37.4 15.6 38.2 16.2 39 16.8 39.7 17.5 40.2 18.3 40.7 19.2 40.9 20.1 41 21.1 40.9 22.1 40.7 23.1 40.3 24 40 24.9 39.7 25.9 39.4 26.8 39 27.8 38.7 28.7 38.4 29.6 38.1 30.6 37.8 31.5 37.5 32.5 37.2 33.4 36.9 34.4 36.6 35.3 36.2 36.2 35.7 37.1 35 37.8 34.3 38.4 33.4 38.9 32.5 39.3 31.5 39.4 30.5 39.5 29.5 39.5 28.5 39.5 27.5 39.5 26.5 39.5 25.5 39.4 24.5 39.4 23.6 39.4 22.6 39.4 21.6 39.5 20.6 39.5 19.6 39.5 18.6 39.5 17.6 39.5 16.6 39.5 15.6 39.3 14.7 39 13.8 38.5 13.1 37.9 12.4 37.2 11.9 36.3 11.5 35.4 11.2 34.5 10.9 33.5 10.6 32.6 10.3 31.6 10 30.7 9.7 29.7 9.3 28.8 9 27.9 8.7 26.9 8.4 26 8 25 7.7 24.1 7.4 23.2 7.1 22.2 7 21.2 7.1 20.2 7.3 19.3 7.7 18.4 8.3 17.5 8.9 16.8 9.7 16.2 10.5 15.6 11.4 15.1 12.2 14.5 13 14 13.8 13.4 14.6 12.8Z;
M17 12.8 17.7 12.1 18.5 11.5 19.3 10.9 20.1 10.5 20.2 10.4 21.1 10 22 9.7 23 9.4 24 9.2 25 9 26 9 27 9 27.6 9.1 28 9.1 28.9 9.3 29.9 9.6 30.9 9.9 31.8 10.3 32.6 10.8 33.5 11.3 34.3 11.9 34.6 12.2 35 12.6 35.7 13.3 36.4 14.1 36.9 14.9 37.4 15.8 37.9 16.6 38.3 17.6 38.6 18.5 38.6 18.7 38.8 19.5 38.9 20.5 39 21.5 39 22.5 38.9 23.5 38.7 24.5 38.5 25.4 38.2 26.3 38.2 26.4 37.8 27.3 37.4 28.2 36.8 29.1 36.2 29.9 35.6 30.6 34.9 31.3 34.2 32 33.8 32.5 33.5 32.8 32.8 33.5 32.1 34.2 31.4 34.9 30.7 35.6 29.9 36.2 29.1 36.8 28.2 37.3 27.9 37.5 27.4 37.8 26.4 38.2 25.5 38.5 24.5 38.7 23.5 38.9 22.5 39 21.5 39 20.5 38.9 20.4 38.9 19.5 38.8 18.6 38.6 17.6 38.3 16.7 37.9 15.8 37.5 14.9 37 14.1 36.4 13.4 35.8 13.3 35.8 12.6 35.1 12 34.3 11.3 33.5 10.8 32.7 10.3 31.8 9.9 30.9 9.6 30 9.4 29.3 9.3 29 9.1 28 9 27 9 26 9 25 9.2 24 9.4 23 9.6 22.1 9.8 21.7 10 21.1 10.4 20.2 10.9 19.4 11.4 18.5 12.1 17.8 12.7 17 13.4 16.3 14.2 15.6 14.2 15.5 14.9 14.9 15.6 14.2 16.3 13.5Z;
M33.5 11.3 34.3 11.9 35 12.6 35.3 12.8 35.7 13.3 36.4 14.1 36.9 14.9 37.4 15.8 37.9 16.6 38.3 17.6 38.3 17.7 38.6 18.5 38.8 19.5 38.9 20.5 39 21.5 39 22.5 38.9 23.5 38.9 23.5 38.7 24.5 38.5 25.4 38.2 26.4 37.8 27.3 37.4 28.2 36.9 28.9 36.8 29.1 36.2 29.9 35.6 30.6 34.9 31.3 34.2 32 33.5 32.8 33.1 33.2 32.8 33.5 32.1 34.2 31.4 34.9 30.7 35.6 29.9 36.2 29.1 36.8 28.7 37 28.2 37.3 27.4 37.8 26.4 38.2 25.5 38.5 24.5 38.7 23.5 38.9 23.3 38.9 22.5 39 21.5 39 20.5 38.9 19.5 38.8 18.6 38.6 17.6 38.3 17.6 38.3 16.7 37.9 15.8 37.5 14.9 37 14.1 36.4 13.3 35.8 12.7 35.2 12.6 35.1 12 34.3 11.3 33.5 10.8 32.7 10.3 31.8 9.9 30.9 9.7 30.3 9.6 29.9 9.3 29 9.1 28 9 27 9 26 9 25 9.1 24.5 9.2 24 9.4 23 9.6 22.1 10 21.1 10.4 20.2 10.9 19.4 11.1 19.1 11.5 18.5 12.1 17.7 12.7 17 13.5 16.3 14.2 15.6 14.9 14.9 14.9 14.8 15.6 14.2 16.3 13.5 17 12.8 17.7 12.1 18.5 11.5 19.3 11 19.3 10.9 20.2 10.4 21.1 10 22 9.7 23 9.4 24 9.2 24.7 9.1 25 9 26 9 27 9 28 9.1 28.9 9.3 29.9 9.6 30.4 9.7 30.9 9.9 31.8 10.3 32.6 10.8Z;
M33.2 11.1 34.2 11.2 35.1 11.4 36 11.9 36.6 12.7 36.8 13.7 36.9 14.7 36.9 15.7 37 16.7 37.1 17.7 37.3 18.6 37.9 19.4 38.5 20.2 39.2 20.9 39.8 21.7 40.5 22.4 40.9 23.3 41 24.3 40.7 25.2 40.1 26 39.4 26.8 38.8 27.5 38.1 28.3 37.5 29.1 37.1 30 37 31 37 31.9 36.9 32.9 36.8 33.9 36.7 34.9 36.2 35.8 35.5 36.4 34.5 36.8 33.6 36.8 32.6 36.9 31.6 37 30.6 37.1 29.6 37.2 28.8 37.7 28 38.4 27.3 39 26.5 39.7 25.8 40.3 24.9 40.8 23.9 41 23 40.8 22.1 40.2 21.4 39.6 20.6 38.9 19.9 38.3 19.1 37.6 18.3 37.2 17.3 37 16.3 37 15.3 36.9 14.3 36.8 13.3 36.7 12.4 36.4 11.7 35.7 11.3 34.8 11.2 33.8 11.1 32.8 11 31.8 11 30.8 10.9 29.9 10.4 29 9.8 28.2 9.1 27.5 8.5 26.7 7.8 26 7.3 25.1 7 24.2 7.1 23.2 7.6 22.3 8.2 21.6 8.9 20.8 9.5 20.1 10.2 19.3 10.7 18.5 10.9 17.5 11 16.6 11.1 15.6 11.1 14.6 11.2 13.6 11.5 12.6 12.1 11.9 13 11.4 13.9 11.2 14.9 11.1 15.9 11 16.9 11 17.9 10.9 18.8 10.6 19.6 10 20.4 9.3 21.1 8.6 21.9 8 22.6 7.4 23.6 7 24.6 7.1 25.5 7.5 26.2 8.1 27 8.7 27.7 9.4 28.5 10 29.3 10.6 30.2 10.9 31.2 11 32.2 11.1Z;
M27.7 9.4 28.5 10 29.3 10.6 30.2 10.9 31.2 11 32.2 11.1 33.2 11.1 34.2 11.2 35.1 11.4 36 11.9 36.6 12.7 36.8 13.7 36.9 14.7 36.9 15.7 37 16.7 37.1 17.7 37.3 18.6 37.9 19.4 38.5 20.2 39.2 20.9 39.8 21.7 40.5 22.4 40.9 23.3 41 24.3 40.7 25.2 40.1 26 39.4 26.8 38.8 27.5 38.1 28.3 37.5 29.1 37.1 30 37 31 37 31.9 36.9 32.9 36.8 33.9 36.7 34.9 36.2 35.8 35.5 36.4 34.5 36.8 33.6 36.9 32.6 36.9 31.6 37 30.6 37.1 29.6 37.2 28.8 37.7 28 38.4 27.3 39 26.5 39.7 25.8 40.3 24.9 40.8 23.9 41 23 40.8 22.1 40.2 21.4 39.6 20.6 38.9 19.9 38.3 19.1 37.6 18.3 37.2 17.3 37 16.3 37 15.3 36.9 14.3 36.8 13.3 36.7 12.4 36.4 11.7 35.7 11.3 34.8 11.2 33.8 11.1 32.8 11 31.8 11 30.8 10.9 29.9 10.4 29 9.8 28.2 9.1 27.5 8.5 26.7 7.8 26 7.3 25.1 7 24.2 7.1 23.2 7.6 22.3 8.2 21.6 8.9 20.8 9.5 20.1 10.2 19.3 10.7 18.5 10.9 17.5 11 16.5 11.1 15.6 11.1 14.6 11.2 13.6 11.5 12.6 12.1 11.9 13 11.4 13.9 11.2 14.9 11.1 15.9 11 16.9 11 17.9 10.9 18.8 10.6 19.6 10 20.4 9.3 21.1 8.6 21.9 8 22.6 7.4 23.6 7 24.6 7.1 25.5 7.5 26.2 8.1 27 8.7Z;
M27.9 10.6 28.8 10.3 29.8 10.1 30.8 10 31.8 10.1 32.7 10.3 33.7 10.6 34.6 11.1 34.8 11.3 35.4 11.7 36.1 12.4 36.7 13.1 37.2 14 37.6 14.9 37.9 15.9 38 16.9 38 17.9 37.8 18.8 37.5 19.8 37.1 20.7 36.8 21.6 36.5 22.6 36.4 23.6 36.4 24.4 36.4 24.6 36.5 25.5 36.8 26.5 37.2 27.4 37.6 28.3 37.8 29.3 38 30.3 38 31.3 37.8 32.3 37.6 33.2 37.2 34.1 36.6 35 36 35.7 35.3 36.4 34.4 37 34.1 37.2 33.6 37.4 32.6 37.8 31.6 37.9 30.6 38 29.7 37.9 28.7 37.7 27.8 37.3 26.8 36.9 25.9 36.6 24.9 36.4 23.9 36.4 22.9 36.4 22 36.6 21 37 20.1 37.4 20.1 37.4 19.2 37.7 18.2 37.9 17.2 38 16.2 37.9 15.3 37.7 14.3 37.4 13.4 36.9 12.6 36.3 11.9 35.6 11.3 34.9 10.8 34 10.4 33.1 10.1 32.1 10 31.1 10 30.6 10 30.1 10.2 29.2 10.5 28.2 10.9 27.3 11.2 26.4 11.5 25.4 11.6 24.4 11.6 23.4 11.5 22.5 11.2 21.5 10.8 20.6 10.4 19.7 10.2 18.7 10 17.7 10 16.7 10 16.6 10.2 15.7 10.4 14.8 10.8 13.9 11.4 13 12 12.3 12.7 11.6 13.6 11 14.4 10.6 15.4 10.2 16.4 10.1 17.4 10 18.3 10.1 19.3 10.3 20.2 10.7 20.9 11 21.2 11.1 22.1 11.4 23.1 11.6 24.1 11.6 25.1 11.6 26 11.4 27 11Z;
M36 35.7 35.3 36.4 34.4 37 33.6 37.4 32.6 37.8 31.6 37.9 30.6 38 29.7 37.9 28.7 37.7 27.8 37.3 26.8 36.9 25.9 36.6 24.9 36.4 23.9 36.4 22.9 36.4 22 36.6 21 37 20.1 37.4 19.2 37.7 18.2 37.9 17.2 38 16.2 37.9 15.3 37.7 14.3 37.4 13.4 36.9 12.6 36.3 11.9 35.6 11.3 34.9 10.8 34 10.4 33.1 10.1 32.1 10 31.1 10 30.2 10.2 29.2 10.5 28.2 10.9 27.3 11.2 26.4 11.5 25.4 11.6 24.4 11.6 23.4 11.5 22.5 11.2 21.5 10.8 20.6 10.4 19.7 10.2 18.7 10 17.7 10 16.7 10.2 15.7 10.4 14.8 10.8 13.9 11.4 13 12 12.3 12.7 11.6 13.6 11 14.4 10.6 15.4 10.2 16.4 10.1 17.4 10 18.3 10.1 19.3 10.3 20.2 10.7 21.2 11.1 22.1 11.4 23.1 11.6 24.1 11.6 25.1 11.6 26 11.4 27 11 27.9 10.6 28.8 10.3 29.8 10.1 30.8 10 31.8 10.1 32.7 10.3 33.7 10.6 34.6 11.1 35.4 11.7 36.1 12.4 36.7 13.1 37.2 14 37.6 14.9 37.9 15.9 38 16.9 38 17.8 37.8 18.8 37.5 19.8 37.1 20.7 36.8 21.6 36.5 22.6 36.4 23.6 36.4 24.6 36.5 25.5 36.8 26.5 37.2 27.4 37.6 28.3 37.8 29.3 38 30.3 38 31.3 37.8 32.3 37.6 33.2 37.2 34.1 36.6 35Z;
M32.1 32.1 31.4 32.8 30.7 33.5 29.9 34.1 29.1 34.7 28.3 35.3 27.6 35.8 27.5 35.8 26.6 36.4 25.8 36.8 24.9 37.3 24 37.7 23.1 38 22.1 38.3 21.2 38.6 20.2 38.8 19.2 38.9 18.2 39 17.2 39 16.6 38.9 16.3 38.9 15.3 38.7 14.3 38.4 13.4 38 12.5 37.5 11.7 36.9 11.1 36.3 10.5 35.5 10 34.6 9.6 33.7 9.3 32.7 9.1 31.7 9.1 31.4 9 30.8 9 29.8 9.1 28.8 9.2 27.8 9.4 26.8 9.7 25.9 10 24.9 10.3 24 10.7 23.1 11.2 22.2 11.6 21.4 12.2 20.5 12.2 20.4 12.7 19.7 13.3 18.9 13.9 18.1 14.5 17.3 15.2 16.6 15.9 15.9 16.6 15.2 17.3 14.5 18.1 13.9 18.9 13.3 19.7 12.7 20.4 12.2 20.5 12.2 21.4 11.6 22.2 11.2 23.1 10.7 24 10.3 24.9 10 25.9 9.7 26.8 9.4 27.8 9.2 28.8 9.1 29.8 9 30.8 9 31.4 9.1 31.7 9.1 32.7 9.3 33.7 9.6 34.6 10 35.5 10.5 36.3 11.1 36.9 11.7 37.5 12.5 38 13.4 38.4 14.3 38.7 15.3 38.9 16.3 38.9 16.6 39 17.2 39 18.2 38.9 19.2 38.8 20.2 38.6 21.2 38.3 22.1 38 23.1 37.7 24 37.3 24.9 36.8 25.8 36.4 26.6 35.8 27.5 35.8 27.6 35.3 28.3 34.7 29.1 34.1 29.9 33.5 30.7 32.8 31.4Z;
M24.3 10.2 24.9 10 25.9 9.7 26.8 9.4 27.1 9.4 27.8 9.2 28.8 9.1 29.8 9 29.9 9 30.8 9 31.7 9.1 32.7 9.3 32.8 9.3 33.7 9.6 34.6 10 35.5 10.5 35.5 10.5 36.3 11.1 36.9 11.7 37.5 12.5 37.5 12.5 38 13.4 38.4 14.3 38.7 15.2 38.7 15.3 38.9 16.3 39 17.2 39 18.1 39 18.2 38.9 19.2 38.8 20.2 38.6 20.9 38.6 21.2 38.3 22.1 38 23.1 37.8 23.7 37.7 24 37.3 24.9 36.8 25.8 36.5 26.4 36.4 26.6 35.8 27.5 35.3 28.3 35 28.8 34.7 29.1 34.1 29.9 33.5 30.7 33.1 31.1 32.8 31.4 32.1 32.1 31.4 32.8 31.1 33.1 30.7 33.5 29.9 34.1 29.1 34.7 28.8 35 28.3 35.3 27.5 35.8 26.6 36.4 26.4 36.5 25.8 36.8 24.9 37.3 24 37.7 23.7 37.8 23.1 38 22.1 38.3 21.2 38.6 20.9 38.6 20.2 38.8 19.2 38.9 18.2 39 18.1 39 17.2 39 16.3 38.9 15.3 38.7 15.2 38.7 14.3 38.4 13.4 38 12.5 37.5 12.5 37.5 11.7 36.9 11.1 36.3 10.5 35.5 10.5 35.5 10 34.6 9.6 33.7 9.3 32.8 9.3 32.7 9.1 31.7 9 30.8 9 29.9 9 29.8 9.1 28.8 9.2 27.8 9.4 27.1 9.4 26.8 9.7 25.9 10 24.9 10.2 24.3 10.3 24 10.7 23.1 11.2 22.2 11.5 21.6 11.6 21.4 12.2 20.5 12.7 19.7 13 19.2 13.3 18.9 13.9 18.1 14.5 17.3 14.9 16.9 15.2 16.6 15.9 15.9 16.6 15.2 16.9 14.9 17.3 14.5 18.1 13.9 18.9 13.3 19.2 13 19.7 12.7 20.5 12.2 21.4 11.6 21.6 11.5 22.2 11.2 23.1 10.7 24 10.3Z;
M22.5 7.8 23.2 7.2 24.2 7 25.1 7.4 25.7 8.1 26.2 9 26.8 9.8 27.3 10.6 28.1 11.2 29 11.3 30 11 30.9 10.6 31.8 10.3 32.8 9.9 33.7 10 34.5 10.6 34.9 11.5 34.8 12.5 34.8 13.5 34.7 14.5 34.7 15.5 35.2 16.3 36 16.8 37 17 37.9 17.3 38.9 17.5 39.8 17.9 40.4 18.7 40.5 19.7 40 20.5 39.3 21.3 38.7 22 38.1 22.8 37.6 23.7 37.7 24.6 38.3 25.4 38.9 26.2 39.6 27 40.2 27.7 40.5 28.6 40.3 29.6 39.5 30.3 38.6 30.6 37.6 30.8 36.7 31 35.7 31.3 35 31.9 34.6 32.8 34.7 33.8 34.8 34.8 34.8 35.8 34.8 36.8 34.3 37.6 33.4 38.1 32.4 38 31.5 37.6 30.6 37.2 29.7 36.9 28.7 36.6 27.8 36.9 27.1 37.6 26.6 38.5 26.1 39.3 25.5 40.2 24.8 40.8 23.8 41 22.9 40.6 22.3 39.9 21.8 39 21.2 38.2 20.7 37.4 19.9 36.8 19 36.7 18 37 17.1 37.4 16.2 37.7 15.2 38.1 14.3 38 13.5 37.4 13.1 36.5 13.2 35.5 13.2 34.5 13.3 33.5 13.3 32.5 12.8 31.7 12 31.2 11 31 10.1 30.7 9.1 30.5 8.2 30.1 7.6 29.3 7.5 28.3 8 27.5 8.7 26.7 9.3 26 9.9 25.2 10.4 24.3 10.3 23.4 9.7 22.6 9.1 21.8 8.4 21 7.8 20.3 7.5 19.4 7.7 18.4 8.5 17.7 9.4 17.4 10.4 17.2 11.3 17 12.3 16.7 13 16.1 13.4 15.2 13.3 14.2 13.2 13.2 13.2 12.2 13.2 11.2 13.7 10.4 14.6 9.9 15.6 10 16.5 10.4 17.4 10.8 18.3 11.1 19.3 11.4 20.2 11.1 20.9 10.4 21.4 9.5 21.9 8.7Z"></animate>
<animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="5s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8; 0.5 0.2 0 0.8" keyTimes="0; 0.14; 0.29; 0.43; 0.57; 0.71; 0.86; 1" values="0 24 24; 154 24 24; 309 24 24; 463 24 24; 617 24 24; 771 24 24; 926 24 24; 1080 24 24"></animateTransform>
</path>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,78 @@
:root,
body.light {
--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;
}
body.dark {
--primary:#ffb4a8;
--on-primary:#690100;
--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;
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="tytd.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.1438552"
inkscape:cx="396.90338"
inkscape:cy="561.25985"
inkscape:window-width="2560"
inkscape:window-height="1528"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ff0000;stroke-width:0.224175"
id="rect113"
width="120"
height="84"
x="40"
y="58"
ry="18.792341" />
<path
style="display:inline;fill:#00AA00;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M
100 140
130 106
105 106
105 60
95 60
95 106
70 106
100 140
"
id="path510"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,38 @@
func Components.AddToPersonalList(tytd,id)
{
return <form hx-post="./add-to-list" hx-swap="outerHTML">
<input type="hidden" name="id" value={id}>
<div class="row">
<div class="max">
<div class="field label suffix border fill">
<select name="name">
<option value="" selected>Create personal list</option>
<each(var item : tytd.GetPersonalLists())>
<option value={item.name}>Add to {item.name}</option>
</each>
</select>
<label>Personal List</label>
<i>arrow_drop_down</i>
</div>
</div>
<button name="action" value="add"><i>add</i></button>
</div>
</form>;
}
func Components.CreateNewList(tytd,id)
{
return <form hx-post="./add-to-list" hx-swap="outerHTML">
<input type="hidden" name="id" value={id}>
<div class="row">
<div class="field label border max">
<input type="text" name="name">
<label>List Name</label>
</div>
<button name="action" value="add"><i>add</i></button>
</div>
</form>;
}

View File

@@ -0,0 +1,30 @@
func Components.AddVideo(id)
{
return <form hx-post="./add-video" hx-swap="outerHTML">
<input type="hidden" name="url" value={id}>
<div class="row">
<div class="max">
<div class="field label suffix border fill">
<select name="res">
<each(var item : Resolution.Resolutions)>
<if(item.default)>
<true>
<option value={item.value} selected>{item.name}</option>
</true>
<false>
<option value={item.value}>{item.name}</option>
</false>
</if>
</each>
</select>
<label>Resolution</label>
<i>arrow_drop_down</i>
</div>
</div>
<button name="action" value="add"><i>add</i></button>
<button name="action" value="download"><i>download</i></button>
<button name="action" value="play"><i>play_arrow</i></button>
</div>
</form>;
}

View File

@@ -0,0 +1,33 @@
func Components.Add()
{
return <form hx-post="./add" hx-swap="outerHTML">
<div class="field border fill">
<input type="text" name="url">
</div>
<div class="row">
<div class="max">
<div class="field label suffix border fill">
<select name="res">
<each(var item : Resolution.Resolutions)>
<if(item.default)>
<true>
<option value={item.value} selected>{item.name}</option>
</true>
<false>
<option value={item.value}>{item.name}</option>
</false>
</if>
</each>
</select>
<label>Resolution</label>
<i>arrow_drop_down</i>
</div>
</div>
<button name="action" value="add"><i>add</i></button>
<button name="action" value="info"><i>info</i></button>
<button name="action" value="download"><i>download</i></button>
<button name="action" value="play"><i>play_arrow</i></button>
</div>
</form>;
}

View File

@@ -0,0 +1,62 @@
func Components.DiscoverEntry(item)
{
switch(item.type)
{
case "video":
return
<div class="row">
<div class="min">
<img src={$"./video-thumbnail?v={Net.Http.UrlEncode(item.id)}"} width="144" width="120">
</div>
<div class="max">
<div class="col">
<div class="min">
<a target="_blank" href={$"./video?v={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
</div>
<div class="min">
<span>{item.views} • {item.uploaded}</span>
</div>
<div class="min">
<each(var part : item.author)>
<if(TypeOf(part.navigationEndpoint) == "Dictionary")>
<true>
<a target="_blank" href={$"./channel?id={Net.Http.UrlEncode(part.navigationEndpoint.browseEndpoint.browseId)}"}>{part.text}</a>
</true>
<false>
<span>{part.text}</span>
</false>
</if>
</each>
</div>
</div>
</div>
</div>;
break;
case "channel":
return
<div class="row">
<div class="min">
<img src={$"./channel-thumbnail?id={Net.Http.UrlEncode(item.id)}"} width="144" width="144">
</div>
<div class="max">
<div class="col">
<div class="min">
<a target="_blank" href={$"./channel?id={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
</div>
<div class="min">
<span>{item.subs}</span>
</div>
<div class ="min">
<p class="min">{item.description}</p>
</div>
</div>
</div>
</div>;
break;
}
return "";
}

View File

@@ -0,0 +1,60 @@
func Components.DownloadedVideo(item)
{
return
<div class="row">
<div class="min">
<img src={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(item.videoId)}"} width="144" width="120">
</div>
<div class="max">
<div class="col">
<div class="min">
<a hx-get={$"./video?v={Net.Http.UrlEncode(item.videoId)}"} hx-target="body" hx-push-url="true" href={$"./video?v={Net.Http.UrlEncode(item.videoId)}"}>{item.title}</a>
</div>
<div class="min">
<span>{item.viewCountStr} (when downloaded)</span>
</div>
<div class="min">
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} hx-target="body" hx-push-url="true">{item.author}</a>
</div>
</div>
</div>
</div>;
}
func Components.DownloadedPlaylist(item)
{
var res = <div class="row">
<div class="min">
<img src={$"./api/v1/playlist-thumbnail?id={Net.Http.UrlEncode(item.playlistId)}"} width="144" width="120">
</div>
<div class="max">
<div class="col">
<div class="min">
<a hx-get={$"./playlist?id={Net.Http.UrlEncode(item.playlistId)}"} href={$"./playlist?id={Net.Http.UrlEncode(item.playlistId)}"} hx-target="body" hx-push-url="true">{item.title}</a>
</div>
<div class="min">
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} hx-target="body" hx-push-url="true">{item.channelTitle}</a>
</div>
</div>
</div>
</div>;
return res;
}
func Components.DownloadedChannel(item)
{
var res = <div class="row">
<div class="min">
<img src={$"./api/v1/channel-thumbnail?id={Net.Http.UrlEncode(item.channelId)}"} width="144" width="120">
</div>
<div class="max">
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} hx-target="body" hx-push-url="true">{item.title}</a>
</div>
</div>;
return res;
}

View File

@@ -0,0 +1,11 @@
func Components.MusicAlbum(item)
{
return <div class="row">
<div class="min">
<img src={$"./album-art?id={Net.Http.UrlEncode(item.id)}"} width="200" height="200">
</div>
<div class="min">
<a href={$"./music-album?id={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
</div>
</div>;
}

View File

@@ -0,0 +1,4 @@
func Components.MusicArtist(item)
{
return <a href={$"./music-artist?id={Net.Http.UrlEncode(item.id)}"}>{item.name}</a>;
}

View File

@@ -0,0 +1,77 @@
func Components.PackageItem(tytd,item)
{
var html = <div class="row">
<div class="min">
<img width="64" height="64" src={item.thumb} alt="Package thumbnail">
</div>
<div class="max">
<div class="col">
<div class="min">
<a target="_blank" href={item.url}>{item.name}</a>
</div>
<div class="min">
<span>{item.version}</span>
</div>
</div>
</div>
<div class="min">
<raw(Components.InstallButton(tytd,item.name,item.version))>
</div>
</div>;
return html;
}
func Components.InstallButton(tytd, name, version, $confirm)
{
var id = Crypto.Base64Encode(new ByteArray($"{name}-{version}")).Replace("=","");
var version = Version.Parse(version);
if(version != null)
{
var state = tytd.PackageState(name,version);
return <div id={id}>
<if(state == 0)>
<true>
<button hx-post="./package-manage" hx-vals={Json.Encode({action="install",name=name,version=version.ToString()})} hx-target={$"#{id}"} hx-swap="outerHTML">Install</button>
</true>
<false>
<if(confirm)>
<true>
<div class="row">
<div class="min">
<button hx-post="./package-manage" hx-vals={Json.Encode({action="confirm",name=name,version=version.ToString()})} hx-target={$"#{id}"} hx-swap="outerHTML">Yes Uninstall</button>
</div>
<div class="min">
<button hx-post="./package-manage" hx-vals={Json.Encode({action="cancel",name=name,version=version.ToString()})} hx-target={$"#{id}"} hx-swap="outerHTML">Cancel</button>
</div>
</div>
</true>
<false>
<if(state == 1)>
<true>
<button hx-post="./package-manage" hx-vals={Json.Encode({action="uninstall",name=name,version=version.ToString()})} hx-target={$"#{id}"} hx-swap="outerHTML">Uninstall</button>
</true>
<false>
<nav class="group split">
<button hx-post="./package-manage" hx-vals={Json.Encode({action="install",name=name,version=version.ToString()})} hx-target={$"#{id}"} hx-swap="outerHTML" class="left-round">
<i>update</i>
<span>Update</span>
</button>
<button hx-post="./package-manage" hx-vals={Json.Encode({action="uninstall",name=name,version=version.ToString()})} hx-target={$"#{id}"} hx-swap="outerHTML" class="right-round square">
<i>delete</i>
</button>
</nav>
</false>
</if>
</false>
</if>
</false>
</if>
</div>;
}
return "";
}

View File

@@ -0,0 +1,30 @@
func Components.InstalledPlugin(item)
{
var html = <div class="row">
<div class="min">
<img width="64" height="64" src={$"./api/v1/plugin-thumbnail.png?name={Net.Http.UrlEncode(item.pluginName)}"} alt="Package thumbnail">
</div>
<div class="max">
<div class="col">
<div class="min">
<if(item.pluginObject.Server != undefined && item.pluginObject.Server != null)>
<true>
<a href={$"./plugin/{Net.Http.UrlPathEncode(item.pluginName)}/"}>{TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)}</a>
</true>
<false>
<span>{TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)}</span>
</false>
</if>
</div>
<div class="min">
<span>{item.version}</span>
</div>
</div>
</div>
</div>;
return html;
}

View File

@@ -0,0 +1,13 @@
var progress=0;
func Components.Progress(tytd)
{
var vid = tytd.CurrentVideo;
var html = <div hx-trigger="every 1500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
<h2>{vid.Title}</h2>
<h4>{vid.Channel}</h4>
<progress value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
</div>;
progress++;
return html;
}

View File

@@ -0,0 +1,68 @@
func Components.Shell(title, html, page, $mypage)
{
if(TypeOf(mypage) != "Path")
mypage = /;
var index = (/).MakeRelative(mypage).ToString();
if(index == "") index = "./";
var pages = [
{
text="Home",
icon="home",
href=index,
classStr=""
},
{
text = "Downloads",
icon = "download",
href=(/"downloads").MakeRelative(mypage).ToString(),
classStr=""
},
{
text = "Plugins",
icon = "extension",
href= (/"plugins").MakeRelative(mypage).ToString(),
classStr=""
},
{
text = "Settings",
icon = "settings",
href= (/"settings").MakeRelative(mypage).ToString(),
classStr=""
}
];
pages[page].classStr = "active";
return <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TYTD - {title}</title>
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
<script src={(/"htmx.min.js").MakeRelative(mypage).ToString()} defer></script>
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
</head>
<body hx-indicator="#loading-indicator">
<nav class="bottom">
<each(var page : pages)>
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
<i>{page.icon}</i>
<div>{page.text}</div>
</a>
</each>
</nav>
<main class="responsive">
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
<img class="responsive" src="./tytd.svg">
</div>
<raw(html)>
</main>
</body>
</html>;
}

View File

@@ -0,0 +1,46 @@
func Components.Subscribe(tytd,id)
{
var bell = tytd.GetSubscriptionBell(id);
var subscribed= bell != null;
var bellLogo = "notifications_off";
switch(bell)
{
case SubscriptionBell.DownloadLow:
case SubscriptionBell.DownloadHigh:
bellLogo = "download";
break;
case SubscriptionBell.Bell:
case SubscriptionBell.BellLow:
case SubscriptionBell.BellHigh:
bellLogo="notifications_active";
break;
}
<return>
<div id="subscribe-btn">
<if(subscribed)>
<true>
<div class="row">
<div class="min">
<button hx-post="./subscribe" hx-swap="outerHTML" hx-target="#subscribe-btn" hx-vals={Json.Encode({id,type="unsubscribe"})}>Unsubscribe</button>
</div>
<div class="min">
<button class="circle">
<i>{bellLogo}</i>
<menu class="left no-wrap">
<each(var b : SubscriptionBell.Bells)>
<li hx-post="./subscribe" hx-swap="outerHTML" hx-target="#subscribe-btn" hx-vals={Json.Encode({id,type="bell",bell=b.value})}>{b.name}</li>
</each>
</menu>
</button>
</div>
</div>
</true>
<false>
<button hx-post="./subscribe" hx-swap="outerHTML" hx-target="#subscribe-btn" hx-vals={Json.Encode({id,type="subscribe"})}>Subscribe</button>
</false>
</if>
</div>
</return>
}

View File

@@ -0,0 +1,374 @@
var TYTDResources = [
{path="/beer.min.css",value=embed("beer.min.css")},
{path="/beer.min.js",value=embed("beer.min.js")},
{path="/material-symbols-outlined.woff2",value=embed("material-symbols-outlined.woff2")},
{path="/material-symbols-rounded.woff2",value=embed("material-symbols-rounded.woff2")},
{path="/material-symbols-sharp.woff2",value=embed("material-symbols-sharp.woff2")},
{path="/material-symbols-subset.woff2",value=embed("material-symbols-subset.woff2")},
{path="/htmx.min.js",value=embed("htmx.min.js")},
{path="/favicon.ico",value=embed("favicon.ico")},
{path="/tytd.svg",value=embed("tytd.svg")},
{path="/loading-indicator.svg",value=embed("loading-indicator.svg")},
{path="/theme.css",value=embed("theme.css")}
];
var times=1;
class TYTDApp {
private TYTD;
public TYTDApp()
{
var tytdfs = new SubdirFilesystem(FS.Local, "TYTD");
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull("TYTD"));
this.TYTD.Start();
}
public Handle(ctx)
{
if(ctx.Path == "/")
{
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
return true;
}
else if(ctx.Path == "/api/v1/download")
{
var v = ctx.QueryParams.TryGetFirst("v");
var inline = ctx.QueryParams.GetFirstBoolean("inline");
var res = ctx.QueryParams.TryGetFirst("res");
if(TypeOf(res) != "String") res = Resolution.LowVideo;
var path = this.TYTD.GetVideoPath(v,res);
if(path != null)
{
var info = this.TYTD.GetVideo(v);
var filename = $"{info.title}-{info.videoId}.{path.GetExtension()}";
var strm = this.TYTD.Storage.OpenFile(path,"rb");
ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm);
strm.Close();
}
}
else if(ctx.Path == "/api/v1/playlist.json")
{
var id = ctx.QueryParams.TryGetFirst("id");
var page = ctx.QueryParams.TryGetFirstInt("page");
var count = ctx.QueryParams.TryGetFirstInt("count");
if(TypeOf(page)!="Long") page = 1;
if(TypeOf(count)!="Long") count = 20;
if(TypeOf(id)!="String") id = "";
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetPlaylistContents(id,page-1,count));
return true;
}
else if(ctx.Path == "/api/v1/channel.json")
{
var id = ctx.QueryParams.TryGetFirst("id");
var page = ctx.QueryParams.TryGetFirstInt("page");
var count = ctx.QueryParams.TryGetFirstInt("count");
if(TypeOf(page)!="Long") page = 1;
if(TypeOf(count)!="Long") count = 20;
if(TypeOf(id)!="String") id = "";
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
return true;
}
else if(ctx.Path == "/api/v1/downloads.json")
{
var q = ctx.QueryParams.TryGetFirst("q");
var type = ctx.QueryParams.TryGetFirst("type");
var page = ctx.QueryParams.TryGetFirstInt("page");
var count = ctx.QueryParams.TryGetFirstInt("count");
if(TypeOf(page)!="Long") page = 1;
if(TypeOf(count)!="Long") count = 20;
if(TypeOf(type)!="String") type = "videos";
if(TypeOf(q)!="String") q = "";
var result = { entries = [] };
switch(type)
{
case "videos":
result.entries = this.TYTD.GetVideos(q,page-1,count);
break;
case "playlists":
result.entries = this.TYTD.GetPlaylists(q,page-1,count);
break;
case "channels":
result.entries = this.TYTD.GetChannels(q,page-1,count);
break;
}
ctx.WithMimeType("application/json").SendJson(result);
return true;
}
else if(ctx.Path == "/subscribe")
{
var id = ctx.QueryParams.TryGetFirst("id");
var bell = ctx.QueryParams.TryGetFirst("bell");
var type = ctx.QueryParams.TryGetFirst("type");
switch(type)
{
case "subscribe":
this.TYTD.SetSubscriptionBell(id, SubscriptionBell.BellLow);
break;
case "unsubscribe":
this.TYTD.SetSubscriptionBell(id, null);
break;
case "bell":
this.TYTD.SetSubscriptionBell(id, bell);
break;
}
ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id));
return true;
}
else if(ctx.Path == "/downloads")
{
ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/list")
{
ctx.WithMimeType("text/html").SendText(Pages.List(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/settings")
{
if(ctx.Method == "POST")
{
/*
hours
minutes
seconds
tag
enablePlugins
*/
var enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins");
var tag = ctx.QueryParams.TryGetFirst("tag");
var hours = ctx.QueryParams.TryGetFirstInt("hours");
var minutes = ctx.QueryParams.TryGetFirstInt("minutes");
var seconds = ctx.QueryParams.TryGetFirstInt("seconds");
if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long")
{
seconds += minutes * 60;
seconds += hours * 3600;
this.TYTD.Config.TYTDTag = tag;
this.TYTD.Config.BellTimer = seconds;
this.TYTD.Config.EnablePlugins = enablePlugins;
this.TYTD.SaveConfig();
}
}
ctx.WithMimeType("text/html").SendText(Pages.Settings(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/video")
{
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/playlist")
{
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/channel")
{
ctx.WithMimeType("text/html").SendText(Pages.ChannelInfo(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/add")
{
var url = ctx.QueryParams.TryGetFirst("url");
var action =ctx.QueryParams.TryGetFirst("action");
var res = ctx.QueryParams.TryGetFirst("res");
if(action == "add")
{
this.TYTD.DownloadItem(url,res);
}
else if(action == "info")
{
ctx.ResponseHeaders.SetValue("HX-Redirect", TYTD.PageRedirect(url));
}
else if(action == "download")
{
ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}");
}
else if(action == "play")
{
ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}&inline=true");
}
ctx.WithMimeType("text/html").SendText(Components.Add());
return true;
}
else if(ctx.Path == "/add-video")
{
var url = ctx.QueryParams.TryGetFirst("url");
var action =ctx.QueryParams.TryGetFirst("action");
var res = ctx.QueryParams.TryGetFirst("res");
if(action == "add")
{
this.TYTD.DownloadItem(url,res);
}
else if(action == "download")
{
ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}");
}
else if(action == "play")
{
ctx.ResponseHeaders.SetValue("HX-Redirect", $"./api/v1/download?v={Net.Http.UrlEncode(url)}&res={Net.Http.UrlEncode(res)}&inline=true");
}
ctx.WithMimeType("text/html").SendText(Components.AddVideo(url));
return true;
}
else if(ctx.Path == "/add-to-list")
{
var id = ctx.QueryParams.TryGetFirst("id");
if(TypeOf(id) != "String") id = "";
var name = ctx.QueryParams.TryGetFirst("name");
if(TypeOf(name) != "String") name = "";
if(name == "")
{
ctx.WithMimeType("text/html").SendText(Components.CreateNewList(this.TYTD,id));
}
else {
this.TYTD.AddToPersonalList(name,id);
ctx.WithMimeType("text/html").SendText(Components.AddToPersonalList(this.TYTD,id));
}
return true;
}
else if(ctx.Path == "/progress")
{
ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD));
return true;
}
else if(ctx.Path == "/plugins")
{
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/plugins-download")
{
ctx.WithMimeType("text/html").SendText(Pages.DownloadPlugins(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/package-manage" && ctx.Method == "POST")
{
var data = {
name = ctx.QueryParams.TryGetFirst("name"),
version = ctx.QueryParams.TryGetFirst("version"),
action = ctx.QueryParams.TryGetFirst("action")
};
var mustConfirm = false;
switch(data.action)
{
case "install":
{
var v = Version.Parse(data.version);
if(v != null)
this.TYTD.PackageInstall(data.name,v);
}
break;
case "uninstall":
{
mustConfirm = true;
}
break;
case "confirm":
{
this.TYTD.PackageUninstall(data.name);
}
break;
}
ctx.WithMimeType("text/html").SendText(Components.InstallButton(this.TYTD, data.name, data.version, mustConfirm));
return true;
}
else if(ctx.Path == "/api/v1/video-thumbnail")
{
var v = ctx.QueryParams.TryGetFirst("v");
var res = ctx.QueryParams.TryGetFirst("res");
if(TypeOf(res) != "String") res = "0";
var thu = this.TYTD.GetVideoThumbnail(v,res);
if(thu == null) return false;
ctx.WithMimeType("image/jpg").SendBytes(thu);
return true;
}
else if(ctx.Path == "/api/v1/playlist-thumbnail")
{
var id = ctx.QueryParams.TryGetFirst("id");
var res = ctx.QueryParams.TryGetFirst("res");
if(TypeOf(res) != "String") res = "0";
var thu = this.TYTD.GetPlaylistThumbnail(id,res);
if(thu == null) return false;
ctx.WithMimeType("image/jpg").SendBytes(thu);
return true;
}
else if(ctx.Path == "/api/v1/channel-thumbnail")
{
var id = ctx.QueryParams.TryGetFirst("id");
var res = ctx.QueryParams.TryGetFirst("res");
if(TypeOf(res) != "String") res = "0";
var thu = this.TYTD.GetChannelThumbnail(id,res);
if(thu == null) return false;
ctx.WithMimeType("image/jpg").SendBytes(thu);
return true;
}
else if(ctx.Path == "/api/v1/plugin-thumbnail.png")
{
var name = ctx.QueryParams.TryGetFirst("name");
ctx.WithMimeType().SendBytes(this.TYTD.GetPluginThumbnail(name));
return true;
}
else if(ctx.Path.StartsWith("/plugin/"))
{
return this.TYTD.Servers.Handle(ctx);
}
else {
each(var file : TYTDResources)
{
if(ctx.Path == file.path)
{
ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value);
return true;
}
}
}
return false;
}
public Close()
{
this.TYTD.Stop();
}
}
func WebAppMain(args)
{
return new TYTDApp();
}
func main(args)
{
var res = WebAppMain(args);
Net.Http.ListenSimpleWithLoop(res,3255);
res.Close();
return 0;
}

View File

@@ -0,0 +1,39 @@
func Pages.ChannelInfo(tytd,ctx)
{
var id = ctx.QueryParams.TryGetFirst("id");
var page = ctx.QueryParams.TryGetFirstInt("page");
if(TypeOf(page) != "Long") page = 1;
page--;
var html = <h1>Could not find channel</h1>;
var title = "N/A";
if(TypeOf(id) == "String")
{
id = TYTD.GetChannelId(id);
var res = tytd.GetChannelContents(id,page,20);
html = <null>
<div class="row">
<div class="max">
<h1>{res.authorName}</h1>
</div>
<div class="min">
<raw(Components.Subscribe(tytd,id))>
</div>
</div>
<each(var item : res.items)>
<raw(Components.DownloadedVideo(item))>
</each>
<footer class="row center-align">
<button hx-get={$"./channel?id={Net.Http.UrlEncode(id)}&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./channel?id={Net.Http.UrlEncode(id)}&type=videos&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
title = res.authorName;
}
return Components.Shell($"Channel {title}",html,1);
}

View File

@@ -0,0 +1,34 @@
func Pages.Discover(tytd,ctx)
{
var q = ctx.QueryParams.TryGetFirst("q");
var res = null;
var q2 = "";
if(TypeOf(q) == "String")
{
q2 = q;
res = tytd.Discover(q);
}
var html = <null>
<form hx-get="./discover" hx-target="body" hx-push-url="true">
<div class="field large prefix round fill active">
<i class="front">search</i>
<input name="q" value={q2}>
</div>
</form>
<if(res != null)>
<true>
<ul>
<each(var item : res.items)>
<li><raw(Components.DiscoverEntry(item))></li>
</each>
</ul>
<button class="responsive">More</button>
</true></null>;
return Components.Shell($"Discover {q2}",html,1);
}

View File

@@ -0,0 +1,293 @@
func Pages.Downloads(tytd,ctx)
{
var type = ctx.QueryParams.TryGetFirst("type");
var q = ctx.QueryParams.TryGetFirst("q");
var page = ctx.QueryParams.TryGetFirstInt("page");
if(TypeOf(page) != "Long") page = 1;
page--;
if(TypeOf(q) != "String")
{
q = "";
}
if(TypeOf(type) != "String")
{
type = "videos";
}
var html ="";
switch(type) {
case "videos":
{
var res = tytd.GetVideos(q,page,20);
html = <null>
<form hx-get="./downloads" hx-target="body" hx-push-url="true" class="s m">
<div class="row">
<div class="max">
<div class="field label suffix border round fill">
<select name="type">
<option value="videos" selected>Videos</option>
<option value="playlists">Playlists</option>
<option value="channels">Channels</option>
<option value="personal">Personal List</option>
</select>
<label>Type</label>
<i>arrow_drop_down</i>
</div>
</div>
<div class="min">
<button>Change</button>
</div>
</div>
</form>
<nav class="tabbed l">
<a class="active" hx-get="./downloads?type=videos" hx-target="body" hx-push-url="true">
<i>movie</i>
<span>Videos</span>
</a>
<a hx-get="./downloads?type=playlists" hx-target="body" hx-push-url="true">
<i>list</i>
<span>Playlists</span>
</a>
<a hx-get="./downloads?type=channels" hx-target="body" hx-push-url="true">
<i>person</i>
<span>Channels</span>
</a>
<a hx-get="./downloads?type=personal" hx-target="body" hx-push-url="true">
<i>edit_note</i>
<span>Personal Lists</span>
</a>
</nav>
<form hx-get="./downloads" hx-target="body" hx-push-url="true">
<div class="field large prefix round fill active">
<i class="front">search</i>
<input name="q" value={q}>
<input name="type" type="hidden" value="videos">
</div>
</form>
<each(var item : res)>
<raw(Components.DownloadedVideo(item))>
</each>
<footer class="row center-align">
<button hx-get={$"./downloads?q={Net.Http.UrlEncode(q)}&type=videos&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./downloads?q={Net.Http.UrlEncode(q)}&type=videos&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
}
break;
case "playlists":
{
var res = tytd.GetPlaylists(q,page,20);
html = <null>
<form hx-get="./downloads" hx-target="body" hx-push-url="true" class="s m">
<div class="row">
<div class="max">
<div class="field label suffix border round fill">
<select name="type">
<option value="videos">Videos</option>
<option value="playlists" selected>Playlists</option>
<option value="channels">Channels</option>
<option value="personal">Personal List</option>
</select>
<label>Type</label>
<i>arrow_drop_down</i>
</div>
</div>
<div class="min">
<button>Change</button>
</div>
</div>
</form>
<nav class="tabbed l">
<a hx-get="./downloads?type=videos" hx-target="body" hx-push-url="true">
<i>movie</i>
<span>Videos</span>
</a>
<a class="active" hx-get="./downloads?type=playlists" hx-target="body" hx-push-url="true">
<i>list</i>
<span>Playlists</span>
</a>
<a hx-get="./downloads?type=channels" hx-target="body" hx-push-url="true">
<i>person</i>
<span>Channels</span>
</a>
<a hx-get="./downloads?type=personal" hx-target="body" hx-push-url="true">
<i>edit_note</i>
<span>Personal Lists</span>
</a>
</nav>
<form hx-get="./downloads" hx-target="body" hx-push-url="true">
<div class="field large prefix round fill active">
<i class="front">search</i>
<input name="q" value={q}>
<input name="type" type="hidden" value="playlists">
</div>
</form>
<each(var item : res)>
<raw(Components.DownloadedPlaylist(item))>
</each>
<footer class="row center-align">
<button hx-get={$"./downloads?q={Net.Http.UrlEncode(q)}&type=playlists&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./downloads?q={Net.Http.UrlEncode(q)}&type=playlists&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
}
break;
case "channels":
{
var res = tytd.GetChannels(q,page,20);
html = <null>
<form hx-get="./downloads" hx-target="body" hx-push-url="true" class="s m">
<div class="row">
<div class="max">
<div class="field label suffix border round fill">
<select name="type">
<option value="videos">Videos</option>
<option value="playlists">Playlists</option>
<option value="channels" selected>Channels</option>
<option value="personal">Personal List</option>
</select>
<label>Type</label>
<i>arrow_drop_down</i>
</div>
</div>
<div class="min">
<button>Change</button>
</div>
</div>
</form>
<nav class="tabbed l">
<a hx-get="./downloads?type=videos" hx-target="body" hx-push-url="true">
<i>movie</i>
<span>Videos</span>
</a>
<a hx-get="./downloads?type=playlists" hx-target="body" hx-push-url="true">
<i>list</i>
<span>Playlists</span>
</a>
<a class="active" hx-get="./downloads?type=channels" hx-target="body" hx-push-url="true">
<i>person</i>
<span>Channels</span>
</a>
<a hx-get="./downloads?type=personal" hx-target="body" hx-push-url="true">
<i>edit_note</i>
<span>Personal Lists</span>
</a>
</nav>
<form hx-get="./downloads" hx-target="body" hx-push-url="true">
<div class="field large prefix round fill active">
<i class="front">search</i>
<input name="q" value={q}>
<input name="type" type="hidden" value="channels">
</div>
</form>
<each(var item : res)>
<raw(Components.DownloadedChannel(item))>
</each>
<footer class="row center-align">
<button hx-get={$"./downloads?q={Net.Http.UrlEncode(q)}&type=channels&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./downloads?q={Net.Http.UrlEncode(q)}&type=channels&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
}
break;
case "personal":
{
var res = tytd.GetPersonalLists();
html = <null>
<form hx-get="./downloads" hx-target="body" hx-push-url="true" class="s m">
<div class="row">
<div class="max">
<div class="field label suffix border round fill">
<select name="type">
<option value="videos">Videos</option>
<option value="playlists">Playlists</option>
<option value="channels">Channels</option>
<option value="personal" selected>Personal List</option>
</select>
<label>Type</label>
<i>arrow_drop_down</i>
</div>
</div>
<div class="min">
<button>Change</button>
</div>
</div>
</form>
<nav class="tabbed l">
<a hx-get="./downloads?type=videos" hx-target="body" hx-push-url="true">
<i>movie</i>
<span>Videos</span>
</a>
<a hx-get="./downloads?type=playlists" hx-target="body" hx-push-url="true">
<i>list</i>
<span>Playlists</span>
</a>
<a hx-get="./downloads?type=channels" hx-target="body" hx-push-url="true">
<i>person</i>
<span>Channels</span>
</a>
<a class="active" hx-get="./downloads?type=personal" hx-target="body" hx-push-url="true">
<i>edit_note</i>
<span>Personal Lists</span>
</a>
</nav>
<each(var item : res)>
<if(TypeOf(item.firstVideo) == "String")>
<true>
<div class="row">
<div class="min">
<img src={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(item.firstVideo)}"} width="144" width="120">
</div>
<div class="max">
<a href={$"./list?name={Net.Http.UrlEncode(item.name)}"}>{item.name}</a>
</div>
</div>
</true>
<false>
<div><a href={$"./list?name={Net.Http.UrlEncode(item.name)}"}>{item.name}</a></div>
</false>
</if>
</each>
</null>;
}
break;
}
return Components.Shell($"Downloads {q}",html,1);
}

View File

@@ -0,0 +1,11 @@
func Pages.Index(tytd)
{
var html = <null>
<raw(Components.Add())>
<raw(Components.Progress(tytd))>
</null>;
return Components.Shell("Home",html,0);
}

View File

@@ -0,0 +1,36 @@
func Pages.List(tytd,ctx)
{
var name = ctx.QueryParams.TryGetFirst("name");
var page = ctx.QueryParams.TryGetFirstInt("page");
if(TypeOf(page) != "Long") page = 1;
page--;
if(TypeOf(name) != "String")
{
name = "";
}
var res = tytd.GetPersonalListContents(name, page, 20);
var html = <null>
<div class="row">
<div class="min">
<button hx-get="./downloads?type=personal" hx-target="body" hx-push-url="true"><i>arrow_back</i></button>
</div>
<div class="max">
<h1>{name}</h1>
</div>
</div>
<each(var item : res)>
<raw(Components.DownloadedVideo(item))>
</each>
<footer class="row center-align">
<button hx-get={$"./downloads?name={Net.Http.UrlEncode(name)}&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./downloads?name={Net.Http.UrlEncode(name)}&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
return Components.Shell($"Personal List {name}",html,1);
}

View File

@@ -0,0 +1,25 @@
func Pages.Artist(music,ctx)
{
var id = ctx.QueryParams.TryGetFirst("id");
var res = music.GetArtistsAlbums(id);
if(TypeOf(id) != "String")
{
return Components.Shell("Error",<h1>Artist id not specified</h1>,pages);
}
var html = <null>
<h1>{res.name}</h1>
<ul>
<each(var item : res.items)>
<li><raw(Components.MusicAlbum(item))></li>
</each>
</ul>
</null>;
return Components.Shell($"Artist {res.name}",html,2);
}

View File

@@ -0,0 +1,33 @@
func Pages.Artists(music,ctx)
{
var q = ctx.QueryParams.TryGetFirst("q");
var res = null;
var q2 = "";
if(TypeOf(q) == "String")
{
q2 = q;
res = music.GetArtists(q);
}
var html = <null>
<form hx-get="./music-artists" hx-target="body" hx-push-url="true">
<div class="field large prefix round fill active">
<i class="front">search</i>
<input name="q" value={q2}>
</div>
</form>
<if(res != null)>
<true>
<ul>
<each(var item : res)>
<li><raw(Components.MusicArtist(item))></li>
</each>
</ul>
</true></null>;
return Components.Shell($"Search Artists {q2}",html,2);
}

View File

@@ -0,0 +1,8 @@
func Pages.Music(ctx)
{
var html = <ul>
<div><a href="./music-artists">Artists</a></div>
<div><a href="./music-downloaded">My Music</a></div>
</ul>;
return Components.Shell("Music",html,2);
}

View File

@@ -0,0 +1,39 @@
func Pages.PlaylistInfo(tytd,ctx)
{
var id = ctx.QueryParams.TryGetFirst("id");
var page = ctx.QueryParams.TryGetFirstInt("page");
if(TypeOf(page) != "Long") page = 1;
page--;
var html = <h1>Could not find playlist</h1>;
var title = "N/A";
if(TypeOf(id) == "String")
{
id = TYTD.GetPlaylistId(id);
var res = tytd.GetPlaylistContents(id,page,20);
html = <null>
<div class="col">
<div class="max">
<h1>{res.title}</h1>
</div>
<div class="max">
<a href={$"./channel?id={Net.Http.UrlEncode(res.channelId)}"}>{res.channelTitle}</a>
</div>
</div>
<each(var item : res.items)>
<raw(Components.DownloadedVideo(item))>
</each>
<footer class="row center-align">
<button hx-get={$"./playlist?id={Net.Http.UrlEncode(id)}&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./playlist?id={Net.Http.UrlEncode(id)}&type=videos&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
title = res.authorName;
}
return Components.Shell($"Playlist {title}",html,1);
}

View File

@@ -0,0 +1,79 @@
func Pages.DownloadPlugins(tytd,ctx)
{
var q = ctx.QueryParams.TryGetFirst("q");
if(TypeOf(q) != "String")
{
q = "";
}
var server = ctx.QueryParams.TryGetFirst("server");
if(TypeOf(server) != "String") server = "https://cpkg.tesseslanguage.com/";
var items=[];
var items2 = [];
each(var item : tytd.PackageManager.Search(q,{server,type="lib",pluginHost="tytd2025"}))
{
items2.Add({
name = item.packageName,
version = item.version,
url = $"{server}/package?name={Net.Http.UrlEncode(item.packageName)}",
thumb = $"{server}/api/v1/package_icon.png?name={Net.Http.UrlEncode(item.packageName)}&version={Net.Http.UrlEncode(item.version)}"
});
}
each(var item : tytd.PackageManager.GetPackageServers())
{
items.Add({
active = items.Count == 0,
url = item
});
}
var html= <null><nav class="tabbed">
<a hx-get="./plugins" hx-target="body" hx-push-url="true">
<i>download_done</i>
<span>Installed</span>
</a>
<a hx-get="./plugins-download" hx-target="body" class="active" hx-push-url="true">
<i>download</i>
<span>Download</span>
</a>
</nav>
<form hx-get="./plugins-download" hx-target="body" hx-push-url="true">
<div class="field suffix border round">
<select name="server">
<each(var item : items)>
<if(item.active)>
<true>
<option value={item.url} selected>{item.url}</option>
</true>
<false>
<option value={item.url}>{item.url}</option>
</false>
</if>
</each>
</select>
<i>arrow_drop_down</i>
</div>
<div class="row no-space">
<div class="field border left-round max">
<input type="text" name="q" value={q}>
</div>
<button type="submit" class="large right-round min">Search</button>
</div>
</form>
<each(var item : items2)>
<raw(Components.PackageItem(tytd,item))>
</each>
</null>;
return Components.Shell("Download plugins",html ,2);
}

View File

@@ -0,0 +1,21 @@
func Pages.Plugins(tytd,ctx)
{
var html= <null><nav class="tabbed">
<a class="active" hx-get="./plugins" hx-target="body" hx-push-url="true">
<i>download_done</i>
<span>Installed</span>
</a>
<a hx-get="./plugins-download" hx-target="body" hx-push-url="true">
<i>download</i>
<span>Download</span>
</a>
</nav>
<each(var item : tytd.Plugins)>
<raw(Components.InstalledPlugin(item))>
</each>
</null>;
return Components.Shell("Installed plugins",html ,2);
}

View File

@@ -0,0 +1,66 @@
func Pages.Settings(tytd,ctx)
{
var totalSecs = tytd.Config.BellTimer ?? 10800;
var enablePlugins = tytd.Config.EnablePlugins ?? true;
var hours = totalSecs / 3600;
totalSecs -= hours * 3600;
var minutes = totalSecs / 60;
var seconds = totalSecs % 60;
var html = <form hx-post="./settings" hx-target="body" >
<div class="field middle-align">
<nav>
<div class="max">
<h6>Enable plugins</h6>
<div>Plugins allow you to extend tytd, but they have full access</div>
</div>
<label class="switch">
<if(enablePlugins)>
<true>
<input name="enablePlugins" type="checkbox" checked>
</true>
<false>
<input name="enablePlugins" type="checkbox">
</false>
</if>
<span></span>
</label>
</nav>
</div>
<div class="field label border">
<input type="text" name="tag" value={tytd.TYTDTag}>
<label>TYTD Tag</label>
</div>
<fieldset>
<legend>Subscriber Poll Rate</legend>
<div class="row">
<div class="max">
<div class="field label border">
<input type="number" name="hours" value={hours}>
<label>Hours</label>
</div>
</div>
<div class="max">
<div class="field label border">
<input type="number" name="minutes" value={minutes}>
<label>Minutes</label>
</div>
</div>
<div class="max">
<div class="field label border">
<input type="number" name="seconds" value={seconds}>
<label>Seconds</label>
</div>
</div>
</div>
</fieldset>
<footer>
<button>Save</button>
</footer>
</form>;
return Components.Shell("Settings",html ,3);
}

View File

@@ -0,0 +1,30 @@
func Pages.VideoInfo(tytd,ctx)
{
var vid = ctx.QueryParams.TryGetFirst("v");
tytd.PutVideoInfoIfNotExists(vid);
var vi = tytd.GetVideo(vid);
var html = <h1>Could not find video</h1>;
if(vi != null)
{
html = <div class="col">
<div class="min">
<img src={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(vi.videoId)}"}>
</div>
<div class="min">
<raw(Components.AddVideo($"https://www.youtube.com/watch?v={vi.videoId}"))>
</div>
<div class="min">
<raw(Components.AddToPersonalList(tytd,vi.videoId))>
</div>
<div class="min">
<h4>{vi.title}</h4>
<a href={$"./channel?id={Net.Http.UrlEncode(vi.channelId)}"}>{vi.author}</a>
<p>{vi.shortDescription}</p>
</div>
</div>;
}
return Components.Shell(vi != null ? vi.title : "Could not find video", html, 1);
}

View File

@@ -0,0 +1,19 @@
{
"info": {
"description": "Download YouTube Videos (using CrossLang)",
"maintainer": "Mike Nolan",
"repo":"https://onedev.site.tesses.net/tytd2025",
"homepage": "https://tesses.net/apps/tytd/2025/",
"type": "lib",
"license": "GPLv3"
},
"dependencies": [
{
"name": "Tesses.CrossLang.BuildEssentials",
"version": "1.0.0.0-prod"
}
],
"name": "Tesses.YouTubeDownloader",
"version": "1.0.0.0-prod",
"icon": "icon.png"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,23 @@
{
"context": {
"client": {
"clientName": "ANDROID",
"clientVersion": "19.28.35",
"clientScreen": "WATCH",
"platform": "MOBILE",
"osName": "Android",
"osVersion": "15",
"androidSdkVersion": 35,
"hl": "en-GB",
"gl": "US",
"utcOffsetMinutes": 0
},
"request": {
"internalExperimentFlags": [],
"useSsl": true
},
"user": {
"lockedSafetyMode": false
}
}
}

View File

@@ -0,0 +1,31 @@
{
"context": {
"client": {
"clientName": "ANDROID",
"clientVersion": "19.28.35",
"clientScreen": "WATCH",
"platform": "MOBILE",
"visitorData": "VISITOR_DATA",
"osName": "Android",
"osVersion": "15",
"androidSdkVersion": 35,
"hl": "en-GB",
"gl": "US",
"utcOffsetMinutes": 0
},
"request": {
"internalExperimentFlags": [],
"useSsl": true
},
"user": {
"lockedSafetyMode": false
}
},
"playerRequest": {
"videoId": "VIDEO_ID_HERE",
"cpn": "kDqPxSobYp3UbLvx",
"contentCheckOk": true,
"racyCheckOk": true
},
"disablePlayerResponse": false
}

View File

@@ -0,0 +1,29 @@
class SubscriptionBell
{
/^ Disabled bell ^/
static getDisabled() "Disabled";
/^ Download (Low quality) ^/
static getDownloadLow() "DownloadLow";
/^ Download (High quality) ^/
static getDownloadHigh() "DownloadHigh";
/^ Download (and notify) (Low quality) ^/
static getBellLow() "BellLow";
/^ Download (and notify) (High quality) ^/
static getBellHigh() "BellHigh";
/^ Notify ^/
static getBell() "Bell";
static getBells()
{
return [
{name="Disabled",value=SubscriptionBell.Disabled},
{name="Notify",value=SubscriptionBell.Bell},
{name="Notify and Download (Low)", value=SubscriptionBell.BellLow},
{name="Notify and Download (High)", value=SubscriptionBell.BellHigh},
{name="Download (Low)", value=SubscriptionBell.DownloadLow},
{name="Download (High)", value=SubscriptionBell.DownloadHigh}
];
}
}

View File

@@ -0,0 +1,27 @@
class TYTD.Event {
private _events = [];
public operator+(e)
{
if (!_events.Contains(e))
{
this._events.Add(e);
}
return this;
}
public operator-(e)
{
this._events.Remove(e);
return this;
}
public Invoke($$args)
{
each(var e : this._events)
{
e.Call(args);
}
}
}

View File

@@ -0,0 +1,127 @@
class TYTD.Music
{
public getUserAgent() $"TYTDMusic/{this.getUserAgent.File.Version.Major}.{this.getUserAgent.File.Version.Minor}.{this.getUserAgent.File.Version.Patch} ( tesses@tesses.net )";
public TYTD;
public Music(tytd)
{
this.TYTD = tytd;
TYTD.Storage.CreateDirectory(/"Album Arts");
}
public GetArtists(query)
{
var req = {
RequestHeaders = [
{
Key = "User-Agent",
Value = this.UserAgent
}
]
};
var resp = Net.Http.MakeRequest($"https://musicbrainz.org/ws/2/artist?query={Net.Http.UrlEncode(query)}&fmt=json",req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
var jo = Json.Decode(resp.ReadAsString());
var items = [];
each(var item : jo.artists)
{
items.Add({
id = item.id,
type = item.type,
name = item.name
});
}
return items;
}
return null;
}
public GetArtistsAlbums(artist_id)
{
var req = {
RequestHeaders = [
{
Key = "User-Agent",
Value = this.UserAgent
}
]
};
var resp = Net.Http.MakeRequest($"https://musicbrainz.org/ws/2/artist/{Net.Http.UrlPathEncode(artist_id)}?inc=artist-credits+releases&fmt=json",req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
var jo = Json.Decode(resp.ReadAsString());
var items = [];
each(var item : jo.releases)
{
items.Add({
id = item.id,
packaging = item.packaging,
title = item.title,
date = item.date
});
}
return {items,name=jo.name};
}
return null;
}
public GetAlbumArts(album_id)
{
var req = {
RequestHeaders = [
{
Key = "User-Agent",
Value = UserAgent
}
],
FollowRedirects=true
};
var url = $"https://coverartarchive.org/release/{Net.Http.UrlPathEncode(album_id)}";
var resp = Net.Http.MakeRequest(url,req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
return Json.Decode(resp.ReadAsString());
}
return null;
}
public GetAlbumArt(album_id, res)
{
var path = /"Album Arts"/$"{album_id}-{res}.jpg";
if(TYTD.Storage.FileExists(path)) return FS.ReadAllBytes(TYTD.Storage,path);
var arts = GetAlbumArts(album_id);
if(arts == null) return null;
each(var img : arts.images)
{
if(img.front)
{
var url = res == "full" ? img.image : img.thumbnails.[res];
var req = {
RequestHeaders = [
{
Key = "User-Agent",
Value = UserAgent
}
],
FollowRedirects=true
};
var resp = Net.Http.MakeRequest(url.Replace("http://","https://"),req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
var strm = TYTD.Storage.OpenFile(path,"wb");
resp.CopyToStream(strm);
strm.Close();
return FS.ReadAllBytes(TYTD.Storage,path);
}
}
}
return null;
}
}

View File

@@ -0,0 +1,35 @@
class TYTD.Queue
{
private ls = [];
private mtx = new Muxex();
public getCount()
{
this.mtx.Lock();
var c = this.ls.Count;
this.mtx.Unlock();
return c;
}
public Pop()
{
var item = null;
this.mtx.Lock();
if(this.ls.Count > 0)
{
item = ls[ls.Count-1];
this.ls.RemoveAt(ls.Count-1);
}
this.mtx.Unlock();
return item;
}
public Push(val)
{
this.mtx.Lock();
this.ls.Add(val);
this.mtx.Unlock();
}
}

View File

@@ -0,0 +1,36 @@
class Resolution
{
/^ Used to fetch metadata only ^/
static getNoDownload() "NoDownload";
/^ Get the video/audio mp4 (muxed by YouTube) ^/
static getLowVideo() "LowVideo";
/^ Get the highest video stream ^/
static getVideoOnly() "VideoOnly";
/^ Get the highest audio stream ^/
static getAudioOnly() "AudioOnly";
/^ Get the highest audio stream (and convert to mp3) ^/
static getMP3() "MP3";
/^ Get the highest audio stream (and convert to flac) ^/
static getFLAC() "FLAC";
/^ Get the highest video and then audio stream (and convert to mp4) ^/
static getMP4() "MP4";
/^ Get the highest video and then audio stream (and mux to a mkv file) ^/
static getMKV() "MKV";
/^ Get the highest video and then audio stream (dont convert or mux) ^/
static getDontConvert() "DontConvert";
static getResolutions()
{
return [
{name="Don't Download", value=Resolution.NoDownload},
{name="Low (muxed by YouTube)", value=Resolution.LowVideo,default=true},
{name="Highest video (no audio)", value=Resolution.VideoOnly},
{name="Highest audio (no video)", value=Resolution.AudioOnly},
{name="Convert to MP3", value=Resolution.MP3},
{name="Convert to FLAC", value=Resolution.FLAC},
{name="Convert to MP4", value=Resolution.MP4},
{name="Mux to MKV (no transcoding)",value=Resolution.MKV},
{name="Don't convert or Mux",value=Resolution.DontConvert}
];
}
}

View File

@@ -0,0 +1,7 @@
class IVideoDownload {
public setTYTD(tytd);
public setProgress(p);
public getVideo();
public abstract Start();
}

File diff suppressed because it is too large Load Diff

View File

View File

@@ -0,0 +1,87 @@
func TYTD.GetVideoId(v)
{
func IsValidId(_v)
{
if(_v.Count != 11) return false;
each(var item : _v)
{
if(!(item.IsLetter() || item.IsDigit() || item == '-' || item == '_')) return false;
}
return true;
}
if(TypeOf(v) != "String") return null;
if(IsValidId(v)) return v;
each(var __re : ["youtube\\..+?/watch.*?v=(.*?)(?:&|/|$)","youtu\\.be/(.*?)(?:\\?|&|/|$)","youtube\\..+?/embed/(.*?)(?:\\?|&|/|$)","youtube\\..+?/shorts/(.*?)(?:\\?|&|/|$)","youtube\\..+?/live/(.*?)(?:\\?|&|/|$)"])
{
var __r = new Regex(__re);
var __s = __r.Search(v);
if(__s.Count == 2)
{
__r=__s[1].Text;
if(IsValidId(__r))
{
return __r;
}
}
}
return null;
}
func TYTD.GetPlaylistId(pid)
{
func IsValidId(v)
{
if(v.Count < 2) return false;
each(var item : v)
{
if(!(item.IsLetter() || item.IsDigit() || item == '-' || item == '_')) return false;
}
return true;
}
if(TypeOf(pid) != "String") return null;
if(IsValidId(pid)) return pid;
each(var __re : ["youtube\\..+?/playlist.*?list=(.*?)(?:&|/|$)","youtube\\..+?/watch.*?list=(.*?)(?:&|/|$)","youtu\\.be/.*?/.*?list=(.*?)(?:&|/|$)","youtube\\..+?/embed/.*?/.*?list=(.*?)(?:&|/|$)"])
{
var __r = new Regex(__re);
var __s = __r.Search(pid);
if(__s.Count == 2)
{
__r=__s[1].Text;
if(IsValidId(__r))
{
return __r;
}
}
}
return null;
}
func TYTD.GetChannelId(cid)
{
func IsValidId(v)
{
if(v.Count != 24) return false;
if(!v.StartsWith("UC")) return false;
each(var item : v)
{
if(!(item.IsLetter() || item.IsDigit() || item == '-' || item == '_')) return false;
}
return true;
}
if(TypeOf(cid) != "String") return null;
if(IsValidId(cid)) return cid;
each(var __re : ["youtube\\..+?/channel/(.*?)(?:\\?|&|/|$)"])
{
var __r = new Regex(__re);
var __s = __r.Search(cid);
if(__s.Count == 2)
{
__r=__s[1].Text;
if(IsValidId(__r))
{
return __r;
}
}
}
return null;
}

View File

@@ -0,0 +1,119 @@
class TYTD.AOVideoDownload : IVideoDownload {
private info;
private tytd;
private progress;
private video_stream_url;
private done=false;
public AOVideoDownload(id)
{
this.info = {
Title = "",
Channel = "",
VideoId = TYTD.GetVideoId(id),
ChannelId = ""
};
}
public setTYTD(tytd)
{
this.tytd = tytd;
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
if(this.tytd.Storage.FileExists(path)) {
this.done = true;
return tytd;
}
var req = this.tytd.ManifestRequest(id).playerResponse;
this.info.Title = req.videoDetails.title;
this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId;
this.tytd.PutVideoInfo(req.videoDetails);
var sampleRate = 0;
var bitrate = 0;
var url = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) != "Long")
{
item.audioSampleRate = ParseLong(item.audioSampleRate);
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
{
url = item.url;
sampleRate = item.audioSampleRate;
bitrate = item.bitrate;
}
}
}
this.video_stream_url = url;
return tytd;
}
public setProgress(p)
{
this.progress = p;
}
public getVideo()
{
return this.info;
}
public Start()
{
for(var i = 0; i < 5; i++)
{
var req = {
FollowRedirects = true,
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
}
]
};
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
this.tytd.Storage.CreateDirectory(path.GetParent());
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
var src = resp.ReadAsStream();
Helpers.CopyToProgress(src,strm,this.progress,100.0);
strm.Close();
src.Close();
this.tytd.Storage.MoveFile(path+".part",path);
break;
}else {
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
var sampleRate = 0;
var bitrate = 0;
var url = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) != "Long")
{
item.audioSampleRate = ParseLong(item.audioSampleRate);
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
{
url = item.url;
sampleRate = item.audioSampleRate;
bitrate = item.bitrate;
}
}
}
this.video_stream_url = url;
}
}
}
}

View File

@@ -0,0 +1,223 @@
class TYTD.NoConvertVideoDownload : IVideoDownload {
private info;
private tytd;
private progress;
private video_stream_url;
private audio_stream_url;
private done=false;
public NoConvertVideoDownload(id)
{
this.info = {
Title = "",
Channel = "",
VideoId = TYTD.GetVideoId(id),
ChannelId = ""
};
}
public getVideo()
{
return this.info;
}
public setTYTD(tytd)
{
this.tytd = tytd;
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
if(this.tytd.Storage.FileExists(path) && this.tytd.Storage.FileExists(pathA)) {
this.done = true;
return tytd;
}
var req = this.tytd.ManifestRequest(id).playerResponse;
this.info.Title = req.videoDetails.title;
this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId;
this.tytd.PutVideoInfo(req.videoDetails);
var width = 0;
var height = 0;
var sampleRate = 0;
var bitrate = 0;
var vurl = "";
var aurl = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) == "Long")
{
if(item.width >= width && item.height >= height)
{
vurl = item.url;
width = item.width;
height = item.height;
}
}
else {
item.audioSampleRate = ParseLong(item.audioSampleRate);
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
{
aurl = item.url;
sampleRate = item.audioSampleRate;
bitrate = item.bitrate;
}
}
}
this.video_stream_url = vurl;
this.audio_stream_url = aurl;
return tytd;
}
public setProgress(p)
{
this.progress = p;
}
public Start()
{
if(this.done) return;
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
if(!this.tytd.Storage.FileExists(path))
{
for(var i = 0; i < 5; i++)
{
var req = {
FollowRedirects = true,
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
}
]
};
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
this.tytd.Storage.CreateDirectory(path.GetParent());
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
var src = resp.ReadAsStream();
Helpers.CopyToProgress(src,strm,(p)=>{
this.progress(p/2);
},100.0);
strm.Close();
src.Close();
this.tytd.Storage.MoveFile(path+".part",path);
break;
}
else {
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
var width = 0;
var height = 0;
var sampleRate = 0;
var bitrate = 0;
var vurl = "";
var aurl = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) == "Long")
{
if(item.width >= width && item.height >= height)
{
vurl = item.url;
width = item.width;
height = item.height;
}
}
else {
item.audioSampleRate = ParseLong(item.audioSampleRate);
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
{
aurl = item.url;
sampleRate = item.audioSampleRate;
bitrate = item.bitrate;
}
}
}
this.video_stream_url = vurl;
this.audio_stream_url = aurl;
}
}
}
if(!this.tytd.Storage.FileExists(pathA))
{
for(var i = 0; i < 5; i++)
{
var req = {
FollowRedirects = true,
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
}
]
};
var resp = Net.Http.MakeRequest(this.audio_stream_url,req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
this.tytd.Storage.CreateDirectory(pathA.GetParent());
var strm = this.tytd.Storage.OpenFile(pathA+".part","wb");
var src = resp.ReadAsStream();
Helpers.CopyToProgress(src,strm,(p)=>{
this.progress((p/2)+0.5);
},100.0);
strm.Close();
src.Close();
this.tytd.Storage.MoveFile(pathA+".part",pathA);
break;
}
else {
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
var width = 0;
var height = 0;
var sampleRate = 0;
var bitrate = 0;
var vurl = "";
var aurl = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) == "Long")
{
if(item.width >= width && item.height >= height)
{
vurl = item.url;
width = item.width;
height = item.height;
}
}
else {
item.audioSampleRate = ParseLong(item.audioSampleRate);
if(item.audioSampleRate >= sampleRate && item.bitrate >= bitrate)
{
aurl = item.url;
sampleRate = item.audioSampleRate;
bitrate = item.bitrate;
}
}
}
this.video_stream_url = vurl;
this.audio_stream_url = aurl;
}
}
}
this.progress(1.0);
}
}

View File

@@ -0,0 +1,83 @@
class TYTD.SDVideoDownload : IVideoDownload {
private info;
private tytd;
private progress;
private video_stream_url;
private done=false;
public SDVideoDownload(id)
{
this.info = {
Title = "",
Channel = "",
VideoId = TYTD.GetVideoId(id),
ChannelId = ""
};
}
public setTYTD(tytd)
{
this.tytd = tytd;
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4";
if(this.tytd.Storage.FileExists(path)) {
this.done = true;
return tytd;
}
var req = this.tytd.ManifestRequest(id).playerResponse;
this.info.Title = req.videoDetails.title;
this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId;
this.tytd.PutVideoInfo(req.videoDetails);
this.video_stream_url = req.streamingData.formats[0].url;
return tytd;
}
public setProgress(p)
{
this.progress = p;
this.progress(0.0);
}
public getVideo()
{
return this.info;
}
public Start()
{
for(var i = 0; i < 5; i++)
{
var req = {
FollowRedirects = true,
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
}
]
};
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4";
this.tytd.Storage.CreateDirectory(path.GetParent());
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
var src = resp.ReadAsStream();
Helpers.CopyToProgress(src,strm,this.progress,100.0);
strm.Close();
src.Close();
this.tytd.Storage.MoveFile(path+".part",path);
break;
}
else {
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
this.video_stream_url = req.streamingData.formats[0].url;
}
}
}
}

View File

@@ -0,0 +1,54 @@
class TYTD.TranscodeAudio : IVideoDownload {
private id;
private ncv;
private tytd;
private ext;
public TranscodeAudio(id,ext)
{
this.id = id;
this.ext = ext;
this.ncv = new TYTD.AOVideoDownload(id);
}
public setTYTD(tytd)
{
this.tytd = tytd;
return this.ncv.TYTD = tytd;
}
public setProgress(p)
{
return this.ncv.Progress = p;
}
public getVideo()
{
return this.ncv.Video;
}
public Start()
{
var id = this.id;
this.ncv.Start();
var p = new Process();
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
var ao = dir / "ao.bin";
var out = dir / $"conv{this.ext}";
if(FS.Local.FileExists(out)) return;
var args=["-y","-i",ao.ToString()];
args.Add("-preset");
args.Add("ultrafast");
args.Add(out.ToString());
p.Arguments = args;
if(p.Start())
p.Join();
}
}

View File

@@ -0,0 +1,63 @@
class TYTD.TranscodeVideo : IVideoDownload {
private id;
private ncv;
private tytd;
private ext;
public TranscodeVideo(id,ext)
{
this.id = id;
this.ext = ext;
this.ncv = new TYTD.NoConvertVideoDownload(id);
}
public setTYTD(tytd)
{
this.tytd = tytd;
return this.ncv.TYTD = tytd;
}
public setProgress(p)
{
return this.ncv.Progress = p;
}
public getVideo()
{
return this.ncv.Video;
}
public Start()
{
var id = this.id;
this.ncv.Start();
var p = new Process();
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
var dir = this.tytd.DatabaseDirectory / "Streams"/this.id.Substring(0,4)/this.id.Substring(4);
var vo = dir / "vo.bin";
var ao = dir / "ao.bin";
var out = dir / $"conv{this.ext}";
if(FS.Local.FileExists(out)) return;
var args=["-y","-i",vo.ToString(),"-i",ao.ToString()];
if(this.ext == ".mkv")
{
args.Add("-c");
args.Add("copy");
}
args.Add("-map");
args.Add("0:v");
args.Add("-map");
args.Add("1:a");
args.Add("-preset");
args.Add("ultrafast");
args.Add(out.ToString());
p.Arguments = args;
if(p.Start())
p.Join();
}
}

View File

@@ -0,0 +1,112 @@
class TYTD.VOVideoDownload : IVideoDownload {
private info;
private tytd;
private progress;
private video_stream_url;
private done=false;
public VOVideoDownload(id)
{
this.info = {
Title = "",
Channel = "",
VideoId = TYTD.GetVideoId(id),
ChannelId = ""
};
}
public setTYTD(tytd)
{
this.tytd = tytd;
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
if(this.tytd.Storage.FileExists(path)) {
this.done = true;
return tytd;
}
var req = this.tytd.ManifestRequest(id).playerResponse;
this.info.Title = req.videoDetails.title;
this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId;
this.tytd.PutVideoInfo(req.videoDetails);
var width = 0;
var height = 0;
var url = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) == "Long")
{
if(item.width >= width && item.height >= height)
{
url = item.url;
width = item.width;
height = item.height;
}
}
}
this.video_stream_url = url;
return tytd;
}
public setProgress(p)
{
this.progress = p;
}
public getVideo()
{
return this.info;
}
public Start()
{
var req = {
FollowRedirects = true,
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
}
]
};
var resp = Net.Http.MakeRequest(this.video_stream_url,req);
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
var id = this.info.VideoId;
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
this.tytd.Storage.CreateDirectory(path.GetParent());
var strm = this.tytd.Storage.OpenFile(path+".part","wb");
var src = resp.ReadAsStream();
Helpers.CopyToProgress(src,strm,this.progress,100.0);
strm.Close();
src.Close();
this.tytd.Storage.MoveFile(path+".part",path);
}
else {
var req = this.tytd.ManifestRequest(this.info.VideoId).playerResponse;
var width = 0;
var height = 0;
var url = "";
each(var item : req.streamingData.adaptiveFormats)
{
if(TypeOf(item.height) == "Long")
{
if(item.width >= width && item.height >= height)
{
url = item.url;
width = item.width;
height = item.height;
}
}
}
this.video_stream_url = url;
}
}
}

10
docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
services:
tytd:
image: onedev.site.tesses.net/tytd2025:latest
volumes:
- tytd-data:/data
ports:
- "3255:3255"
volumes:
tytd-data:

65
tytd-musik.svg Normal file
View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg531"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="tytd-musik.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview533"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.698625"
inkscape:cx="377.95276"
inkscape:cy="377.95276"
inkscape:window-width="2560"
inkscape:window-height="1528"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs528" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#FF0000;stroke-width:0.264583"
id="path587"
cx="100"
cy="100"
rx="42"
ry="42" />
<path
style="display:inline;fill:#00AA00;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M
100 140
130 106
105 106
105 60
95 60
95 106
70 106
100 140
"
id="path510"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

66
tytd.svg Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="200mm"
height="200mm"
viewBox="0 0 200 200"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="tytd.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.1438552"
inkscape:cx="396.90338"
inkscape:cy="561.25985"
inkscape:window-width="2560"
inkscape:window-height="1528"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ff0000;stroke-width:0.224175"
id="rect113"
width="120"
height="84"
x="40"
y="58"
ry="18.792341" />
<path
style="display:inline;fill:#00AA00;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M
100 140
130 106
105 106
105 60
95 60
95 106
70 106
100 140
"
id="path510"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB