First commit
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
bin
|
||||||
|
obj
|
||||||
|
publish
|
||||||
|
TYTD
|
||||||
13
Dockerfile
Normal 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"]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bin
|
||||||
|
obj
|
||||||
31
Tesses.YouTubeDownloader.PluginTemplate/cross.json
Normal 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"
|
||||||
|
}
|
||||||
1
Tesses.YouTubeDownloader.PluginTemplate/src/main.tcross
Normal file
@@ -0,0 +1 @@
|
|||||||
|
func PluginInit() new Plugin();
|
||||||
29
Tesses.YouTubeDownloader.PluginTemplate/src/plugin.tcross
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Tesses.YouTubeDownloader.Server/cross.json
Normal 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"
|
||||||
|
}
|
||||||
1
Tesses.YouTubeDownloader.Server/res/beer.min.css
vendored
Normal file
1
Tesses.YouTubeDownloader.Server/res/beer.min.js
vendored
Normal file
BIN
Tesses.YouTubeDownloader.Server/res/favicon.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
1
Tesses.YouTubeDownloader.Server/res/htmx.min.js
vendored
Normal file
BIN
Tesses.YouTubeDownloader.Server/res/icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
19
Tesses.YouTubeDownloader.Server/res/loading-indicator.svg
Normal 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 |
BIN
Tesses.YouTubeDownloader.Server/res/material-symbols-sharp.woff2
Normal file
78
Tesses.YouTubeDownloader.Server/res/theme.css
Normal 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;
|
||||||
|
}
|
||||||
66
Tesses.YouTubeDownloader.Server/res/tytd.svg
Normal 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 |
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
33
Tesses.YouTubeDownloader.Server/src/components/add.tcross
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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 "";
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
func Components.MusicArtist(item)
|
||||||
|
{
|
||||||
|
return <a href={$"./music-artist?id={Net.Http.UrlEncode(item.id)}"}>{item.name}</a>;
|
||||||
|
}
|
||||||
@@ -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 "";
|
||||||
|
}
|
||||||
30
Tesses.YouTubeDownloader.Server/src/components/plugin.tcross
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
68
Tesses.YouTubeDownloader.Server/src/components/shell.tcross
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
374
Tesses.YouTubeDownloader.Server/src/main.tcross
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
34
Tesses.YouTubeDownloader.Server/src/pages/discover.tcross
Normal 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);
|
||||||
|
}
|
||||||
293
Tesses.YouTubeDownloader.Server/src/pages/downloads.tcross
Normal 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);
|
||||||
|
}
|
||||||
11
Tesses.YouTubeDownloader.Server/src/pages/index.tcross
Normal 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);
|
||||||
|
}
|
||||||
36
Tesses.YouTubeDownloader.Server/src/pages/list.tcross
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
79
Tesses.YouTubeDownloader.Server/src/pages/plugins-dl.tcross
Normal 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);
|
||||||
|
}
|
||||||
21
Tesses.YouTubeDownloader.Server/src/pages/plugins.tcross
Normal 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);
|
||||||
|
}
|
||||||
66
Tesses.YouTubeDownloader.Server/src/pages/settings.tcross
Normal 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);
|
||||||
|
}
|
||||||
30
Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross
Normal 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);
|
||||||
|
}
|
||||||
19
Tesses.YouTubeDownloader/cross.json
Normal 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"
|
||||||
|
}
|
||||||
BIN
Tesses.YouTubeDownloader/res/icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Tesses.YouTubeDownloader/res/package_icon.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
23
Tesses.YouTubeDownloader/res/request.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Tesses.YouTubeDownloader/res/request2.json
Normal 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
|
||||||
|
}
|
||||||
29
Tesses.YouTubeDownloader/src/Bell.tcross
Normal 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}
|
||||||
|
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Tesses.YouTubeDownloader/src/Event.tcross
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
127
Tesses.YouTubeDownloader/src/Music.tcross
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Tesses.YouTubeDownloader/src/Queue.tcross
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Tesses.YouTubeDownloader/src/Resolution.tcross
Normal 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}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Tesses.YouTubeDownloader/src/VideoDownload.tcross
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class IVideoDownload {
|
||||||
|
public setTYTD(tytd);
|
||||||
|
public setProgress(p);
|
||||||
|
public getVideo();
|
||||||
|
public abstract Start();
|
||||||
|
}
|
||||||
|
|
||||||
1550
Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross
Normal file
0
Tesses.YouTubeDownloader/src/data.tcross
Normal file
87
Tesses.YouTubeDownloader/src/ids.tcross
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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 |