mirror of
https://onedev.site.tesses.net/tytd2025
synced 2026-04-18 14:06:33 +00:00
Compare commits
35 Commits
abe5ce7fba
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 02b10131f9 | |||
| 8007eeb043 | |||
| 3919384b57 | |||
| c3ef57b8b6 | |||
| deea6b32a4 | |||
| 3cc23ff97d | |||
| 2626242fa1 | |||
| c8a59cd2e9 | |||
| 9c3d8ed2aa | |||
| c3144e7483 | |||
| 1502e599bd | |||
| 1eccb71437 | |||
| 269b654c2e | |||
| 8126b2a616 | |||
| 22086d488f | |||
| 7d6c783f26 | |||
| 8560529475 | |||
| dec21a7c5d | |||
| 0708b3b0cf | |||
| c64f2cca88 | |||
| e614f4841f | |||
| 9abff478b8 | |||
| 31b179c8a3 | |||
| 8e3aa313ff | |||
| e1b7216c69 | |||
| 67de5e2d6d | |||
| 95a3585648 | |||
| 148106f191 | |||
| 5106c5dc6b | |||
| d3d04bb971 | |||
| 478451034f | |||
| 1ab1c11582 | |||
| 43f6d4ac33 | |||
| d64052920e | |||
| 2eff24d67b |
@@ -2,6 +2,7 @@ FROM onedev.site.tesses.net/crosslang/crosslang:latest
|
|||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY Tesses.YouTubeDownloader.Server/bin/ /app
|
COPY Tesses.YouTubeDownloader.Server/bin/ /app
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
ENV TYTDDIR=/data
|
||||||
|
|
||||||
EXPOSE 3255
|
EXPOSE 3255
|
||||||
|
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -1,23 +1,58 @@
|
|||||||
# TYTD 2025
|
# TYTD 2025
|
||||||
A YouTube downloader writen in [CrossLang](https://crosslang.tesseslanguage.com/)
|
My web based YouTube downloader that I created in 2025 writen in [CrossLang](https://crosslang.tesseslanguage.com/), my own language
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Website](https://crosslang.tesseslanguage.com/software/webapps/tytd2025/)
|
||||||
|
|
||||||
|
# Features
|
||||||
|
- Uses [SQLite3](https://www.sqlite.org/) for it's database (embedded into TessesFramework)
|
||||||
|
- Can download videos, playlists and channels (you need a channel url like this [https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A](https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A))
|
||||||
|
- Can subscribe to channels
|
||||||
|
- Can create playlists (that are stored on the server)
|
||||||
|
- Search and browse your downloaded videos, playlists, or channels (the search is very basic though)
|
||||||
|
- User accounts
|
||||||
|
- Videos can be tagged based on your downloader's TYTD tag (to determine which instance downloaded it)
|
||||||
|
- Plugins from [CPKG](https://cpkg.tesseslangauge.com/) or any CPKG compliant server
|
||||||
|
- Can download YouTube videos either Low quality (but doesn't) require [ffmpeg](https://ffmpeg.org/), you can also download individual streams (also doesn't need [ffmpeg](https://ffmpeg.org/)), or to MP4 (doesn't work on wii due to libx264 having illegal instruction), MKV (so no transcode), MP3 or FLAC (these do need [ffmpeg](https://ffmpeg.org/) in your PATH however)
|
||||||
|
- Runs on the Wii using the [Wii Linux Continuation Project](https://wiibrew.org/wiki/Wii-Linux#Wii_Linux_Continuation_Project) (albeit extremely slowly, despite this that's where I run it)
|
||||||
|
|
||||||
|
|
||||||
## To Install
|
## To Install
|
||||||
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
||||||
|
|
||||||
from source:
|
from source:
|
||||||
```bash
|
```bash
|
||||||
cd Tesses.YouTubeDownloader.Server
|
git clone https://onedev.site.tesses.net/tytd2025
|
||||||
|
cd tytd2025/Tesses.YouTubeDownloader.Server
|
||||||
crosslang install-webapp
|
crosslang install-webapp
|
||||||
mkdir ~/tytd-work # or any directory
|
|
||||||
cd ~/tytd-work # or any directory (you must run the command in this folder every time you start the server)
|
|
||||||
crosslang webapp tytd2025 --port=3255
|
|
||||||
```
|
```
|
||||||
|
|
||||||
from package manager:
|
from package manager:
|
||||||
```bash
|
```bash
|
||||||
crosslang install-webapp Tesses.YouTubeDownloader.Server
|
crosslang install-webapp Tesses.YouTubeDownloader.Server
|
||||||
mkdir ~/tytd-work # or any directory
|
```
|
||||||
cd ~/tytd-work # or any directory (you must run the command in this folder every time you start the server)
|
|
||||||
|
|
||||||
|
## To Run
|
||||||
|
|
||||||
|
Save to videos:
|
||||||
|
```bash
|
||||||
|
crosslang webapp tytd2025 --port=3255
|
||||||
|
```
|
||||||
|
|
||||||
|
Save to a folder (unix):
|
||||||
|
```bash
|
||||||
|
# Replace /path/to/tytd with the folder you want
|
||||||
|
export TYTDDIR=/path/to/tytd
|
||||||
|
crosslang webapp tytd2025 --port=3255
|
||||||
|
```
|
||||||
|
|
||||||
|
Save to a folder (windows):
|
||||||
|
```batch
|
||||||
|
REM Replace C:\path\to\tytd with the folder you want
|
||||||
|
SET TYTDDIR=C:\path\to\tytd
|
||||||
crosslang webapp tytd2025 --port=3255
|
crosslang webapp tytd2025 --port=3255
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
@@ -24,8 +24,10 @@
|
|||||||
},
|
},
|
||||||
"template_project_dependencies": [
|
"template_project_dependencies": [
|
||||||
],
|
],
|
||||||
"type": "template"
|
"type": "template",
|
||||||
|
"template_icon": "icon.png"
|
||||||
},
|
},
|
||||||
"name": "Tesses.YouTubeDownloader.PluginTemplate",
|
"name": "Tesses.YouTubeDownloader.PluginTemplate",
|
||||||
"version": "1.0.0.0-prod"
|
"version": "1.0.0.0-prod",
|
||||||
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
BIN
Tesses.YouTubeDownloader.PluginTemplate/res/icon.png
Normal file
BIN
Tesses.YouTubeDownloader.PluginTemplate/res/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -2,7 +2,7 @@ class PluginServer
|
|||||||
{
|
{
|
||||||
public Handle(ctx)
|
public Handle(ctx)
|
||||||
{
|
{
|
||||||
ctx.WithMimeType().SendText(<div><h1>Hello, world from @%PROJECT_NAME<h1><p>Path: {ctx.Path}, OriginalPath: {ctx.OriginalPath}</p></div>);
|
ctx.WithMimeType().SendText(<div><h1>Hello, world from %PROJECT_NAME%<h1><p>Path: {ctx.Path}, OriginalPath: {ctx.OriginalPath}</p></div>);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,78 +1,86 @@
|
|||||||
:root,
|
:root,
|
||||||
body.light {
|
body.light {
|
||||||
--primary:#c00100;
|
--primary:#bc004b;
|
||||||
--on-primary:#ffffff;
|
--on-primary:#ffffff;
|
||||||
--primary-container:#ffdad4;
|
--primary-container:#ffd9de;
|
||||||
--on-primary-container:#410000;
|
--on-primary-container:#400014;
|
||||||
--secondary:#775651;
|
--secondary:#75565b;
|
||||||
--on-secondary:#ffffff;
|
--on-secondary:#ffffff;
|
||||||
--secondary-container:#ffdad4;
|
--secondary-container:#ffd9de;
|
||||||
--on-secondary-container:#2c1512;
|
--on-secondary-container:#2c1519;
|
||||||
--tertiary:#705c2e;
|
--tertiary:#795831;
|
||||||
--on-tertiary:#ffffff;
|
--on-tertiary:#ffffff;
|
||||||
--tertiary-container:#fbdfa6;
|
--tertiary-container:#ffddba;
|
||||||
--on-tertiary-container:#251a00;
|
--on-tertiary-container:#2b1700;
|
||||||
--error:#ba1a1a;
|
--error:#ba1a1a;
|
||||||
--on-error:#ffffff;
|
--on-error:#ffffff;
|
||||||
--error-container:#ffdad6;
|
--error-container:#ffdad6;
|
||||||
--on-error-container:#410002;
|
--on-error-container:#410002;
|
||||||
--background:#fffbff;
|
--background:#fffbff;
|
||||||
--on-background:#201a19;
|
--on-background:#201a1b;
|
||||||
--surface:#fff8f6;
|
--surface:#fff8f7;
|
||||||
--on-surface:#201a19;
|
--on-surface:#201a1b;
|
||||||
--surface-variant:#f5ddda;
|
--surface-variant:#f3dddf;
|
||||||
--on-surface-variant:#534341;
|
--on-surface-variant:#524345;
|
||||||
--outline:#857370;
|
--outline:#847375;
|
||||||
--outline-variant:#d8c2be;
|
--outline-variant:#d6c2c3;
|
||||||
--shadow:#000000;
|
--shadow:#000000;
|
||||||
--scrim:#000000;
|
--scrim:#000000;
|
||||||
--inverse-surface:#362f2e;
|
--inverse-surface:#362f2f;
|
||||||
--inverse-on-surface:#fbeeec;
|
--inverse-on-surface:#fbeeee;
|
||||||
--inverse-primary:#ffb4a8;
|
--inverse-primary:#ffb2be;
|
||||||
--surface-dim:#e4d7d5;
|
--surface-dim:#e3d7d8;
|
||||||
--surface-bright:#fff8f6;
|
--surface-bright:#fff8f7;
|
||||||
--surface-container-lowest:#ffffff;
|
--surface-container-lowest:#ffffff;
|
||||||
--surface-container-low:#fef1ee;
|
--surface-container-low:#fdf1f1;
|
||||||
--surface-container:#f8ebe9;
|
--surface-container:#f8ebeb;
|
||||||
--surface-container-high:#f3e5e3;
|
--surface-container-high:#f2e5e6;
|
||||||
--surface-container-highest:#ede0dd;
|
--surface-container-highest:#ece0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark {
|
body.dark {
|
||||||
--primary:#ffb4a8;
|
--primary:#ffb2be;
|
||||||
--on-primary:#690100;
|
--on-primary:#660025;
|
||||||
--primary-container:#930100;
|
--primary-container:#900038;
|
||||||
--on-primary-container:#ffdad4;
|
--on-primary-container:#ffd9de;
|
||||||
--secondary:#e7bdb6;
|
--secondary:#e5bdc2;
|
||||||
--on-secondary:#442925;
|
--on-secondary:#43292d;
|
||||||
--secondary-container:#5d3f3b;
|
--secondary-container:#5c3f43;
|
||||||
--on-secondary-container:#ffdad4;
|
--on-secondary-container:#ffd9de;
|
||||||
--tertiary:#dec48c;
|
--tertiary:#ebbf90;
|
||||||
--on-tertiary:#3e2e04;
|
--on-tertiary:#452b08;
|
||||||
--tertiary-container:#564419;
|
--tertiary-container:#5f411c;
|
||||||
--on-tertiary-container:#fbdfa6;
|
--on-tertiary-container:#ffddba;
|
||||||
--error:#ffb4ab;
|
--error:#ffb4ab;
|
||||||
--on-error:#690005;
|
--on-error:#690005;
|
||||||
--error-container:#93000a;
|
--error-container:#93000a;
|
||||||
--on-error-container:#ffb4ab;
|
--on-error-container:#ffb4ab;
|
||||||
--background:#201a19;
|
--background:#201a1b;
|
||||||
--on-background:#ede0dd;
|
--on-background:#ece0e0;
|
||||||
--surface:#181211;
|
--surface:#181213;
|
||||||
--on-surface:#ede0dd;
|
--on-surface:#ece0e0;
|
||||||
--surface-variant:#534341;
|
--surface-variant:#524345;
|
||||||
--on-surface-variant:#d8c2be;
|
--on-surface-variant:#d6c2c3;
|
||||||
--outline:#a08c89;
|
--outline:#9f8c8e;
|
||||||
--outline-variant:#534341;
|
--outline-variant:#524345;
|
||||||
--shadow:#000000;
|
--shadow:#000000;
|
||||||
--scrim:#000000;
|
--scrim:#000000;
|
||||||
--inverse-surface:#ede0dd;
|
--inverse-surface:#ece0e0;
|
||||||
--inverse-on-surface:#362f2e;
|
--inverse-on-surface:#362f2f;
|
||||||
--inverse-primary:#c00100;
|
--inverse-primary:#bc004b;
|
||||||
--surface-dim:#181211;
|
--surface-dim:#181213;
|
||||||
--surface-bright:#3f3736;
|
--surface-bright:#3f3738;
|
||||||
--surface-container-lowest:#120d0c;
|
--surface-container-lowest:#120d0d;
|
||||||
--surface-container-low:#201a19;
|
--surface-container-low:#201a1b;
|
||||||
--surface-container:#251e1d;
|
--surface-container:#241e1f;
|
||||||
--surface-container-high:#2f2827;
|
--surface-container-high:#2f2829;
|
||||||
--surface-container-highest:#3b3332;
|
--surface-container-highest:#3a3334;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 50vh;
|
||||||
|
left: 50vw;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
2018
Tesses.YouTubeDownloader.Server/res/video-js.css
Normal file
2018
Tesses.YouTubeDownloader.Server/res/video-js.css
Normal file
File diff suppressed because one or more lines are too long
53
Tesses.YouTubeDownloader.Server/res/video.min.js
vendored
Normal file
53
Tesses.YouTubeDownloader.Server/res/video.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
Tesses.YouTubeDownloader.Server/res/wavy.svg
Normal file
5
Tesses.YouTubeDownloader.Server/res/wavy.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="610 1101 1220 75">
|
||||||
|
<path fill="#D0BCFF" d="M1870.425 1173.746c-29.448 0-58.665-7.477-84.493-21.622-.03-.016-.061-.032-.09-.049-43.555-23.82-95.546-23.809-139.091.039a4.079 4.079 0 0 1-.113.062c-25.803 14.109-54.984 21.567-84.394 21.568-29.449 0-58.667-7.478-84.495-21.623-43.554-23.852-95.539-23.865-139.089-.052-.03.018-.062.034-.092.051-25.83 14.146-55.048 21.623-84.497 21.623-29.423 0-58.613-7.463-84.425-21.584l-.077-.042c-43.572-23.863-95.596-23.862-139.164-.001l-.085.046c-25.809 14.117-55.001 21.581-84.418 21.581h-.001c-29.45-.001-58.669-7.479-84.498-21.625-43.534-23.845-95.513-23.863-139.063-.058l-.108.06c-25.829 14.146-55.047 21.623-84.496 21.623-29.398 0-58.566-7.451-84.362-21.551-.048-.024-.095-.05-.142-.075-43.568-23.863-95.593-23.863-139.163 0l-.081.043c-25.811 14.12-55.001 21.583-84.421 21.583-29.45 0-58.668-7.478-84.497-21.624-7.521-4.118-10.278-13.554-6.159-21.074s13.554-10.277 21.074-6.158c43.57 23.862 95.593 23.864 139.163 0l.098-.053c25.807-14.114 54.991-21.573 84.406-21.573 29.398 0 58.567 7.451 84.364 21.55l.14.076c43.534 23.843 95.508 23.864 139.056.059l.113-.063c25.828-14.145 55.049-21.622 84.496-21.622h.002c29.449 0 58.668 7.478 84.497 21.625 43.57 23.863 95.595 23.862 139.165.001.021-.013.043-.024.065-.036 25.813-14.124 55.011-21.591 84.438-21.591 29.42 0 58.61 7.463 84.421 21.582l.082.045c43.539 23.847 95.521 23.862 139.074.049l.096-.053c25.829-14.146 55.047-21.623 84.496-21.623s58.667 7.477 84.496 21.623c43.58 23.867 95.604 23.866 139.172.005l.118-.064c25.803-14.108 54.98-21.564 84.389-21.564 29.448-.001 58.666 7.476 84.494 21.62l.114.063c43.549 23.807 95.528 23.791 139.063-.051 7.52-4.121 16.955-1.361 21.073 6.159 4.119 7.521 1.361 16.955-6.159 21.073-25.828 14.146-55.045 21.622-84.492 21.622z">
|
||||||
|
<animateTransform attributeName="transform" calcMode="linear" dur="1s" from="0 0" repeatCount="indefinite" to="312.5 0" type="translate"/>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -24,7 +24,6 @@ func Components.AddVideo(id)
|
|||||||
|
|
||||||
<button name="action" value="add"><i>add</i></button>
|
<button name="action" value="add"><i>add</i></button>
|
||||||
<button name="action" value="download"><i>download</i></button>
|
<button name="action" value="download"><i>download</i></button>
|
||||||
<button name="action" value="play"><i>play_arrow</i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>;
|
</form>;
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,6 @@ func Components.Add()
|
|||||||
<button name="action" value="add"><i>add</i></button>
|
<button name="action" value="add"><i>add</i></button>
|
||||||
<button name="action" value="info"><i>info</i></button>
|
<button name="action" value="info"><i>info</i></button>
|
||||||
<button name="action" value="download"><i>download</i></button>
|
<button name="action" value="download"><i>download</i></button>
|
||||||
<button name="action" value="play"><i>play_arrow</i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>;
|
</form>;
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ func Components.DiscoverEntry(item)
|
|||||||
<div class="max">
|
<div class="max">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<a target="_blank" href={$"./video?v={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
<a target="_blank" href={$"./watch?v={Net.Http.UrlEncode(item.id)}"}>{item.title}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<span>{item.views} • {item.uploaded}</span>
|
<span>{item.views} • {item.uploaded}</span>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func Components.DownloadedVideo(item)
|
|||||||
<div class="max">
|
<div class="max">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="min">
|
<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>
|
<a hx-get={$"./watch?v={Net.Http.UrlEncode(item.videoId)}"} hx-target="body" hx-push-url="true" href={$"./watch?v={Net.Http.UrlEncode(item.videoId)}"}>{item.title}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<span>{item.viewCountStr} (when downloaded)</span>
|
<span>{item.viewCountStr} (when downloaded)</span>
|
||||||
@@ -34,7 +34,7 @@ func Components.DownloadedPlaylist(item)
|
|||||||
<div class="max">
|
<div class="max">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="min">
|
<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>
|
<a hx-get={$"./playlist?list={Net.Http.UrlEncode(item.playlistId)}"} href={$"./playlist?list={Net.Http.UrlEncode(item.playlistId)}"} hx-target="body" hx-push-url="true">{item.title}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -2,16 +2,6 @@ func Components.PersonalListDescription(tytd,name,editing)
|
|||||||
{
|
{
|
||||||
var description = tytd.GetPersonalListDescription(name);
|
var description = tytd.GetPersonalListDescription(name);
|
||||||
var first=true;
|
var first=true;
|
||||||
var description_with_br = "";
|
|
||||||
each(var txt : description.Split("\n"))
|
|
||||||
{
|
|
||||||
if(!first)
|
|
||||||
{
|
|
||||||
description_with_br += <br>;
|
|
||||||
}
|
|
||||||
description_with_br += Net.Http.HtmlEncode(txt);
|
|
||||||
first=false;
|
|
||||||
}
|
|
||||||
<return>
|
<return>
|
||||||
<if(editing)>
|
<if(editing)>
|
||||||
<true>
|
<true>
|
||||||
@@ -32,7 +22,7 @@ func Components.PersonalListDescription(tytd,name,editing)
|
|||||||
<false>
|
<false>
|
||||||
<div class="row" id="description">
|
<div class="row" id="description">
|
||||||
<div class="max">
|
<div class="max">
|
||||||
<p><raw(description_with_br)></p>
|
<plink(description)>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
|
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ func Components.InstalledPlugin(item)
|
|||||||
<div class="max">
|
<div class="max">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<if(item.pluginObject.Server != undefined && item.pluginObject.Server != null)>
|
<if(TypeIsDefined(item.pluginObject.Server))>
|
||||||
<true>
|
<true>
|
||||||
<a class="underline" 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>
|
<a class="underline" 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>
|
</true>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ func Components.Progress(tytd)
|
|||||||
var vid = tytd.CurrentVideo;
|
var vid = tytd.CurrentVideo;
|
||||||
|
|
||||||
var html = <div hx-trigger="every 1500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
var html = <div hx-trigger="every 1500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
||||||
<h2>{vid.Title}</h2>
|
<h2><a hx-target="body" hx-push-url="true" hx-get={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"}>{vid.Title}</a></h2>
|
||||||
<h4>{vid.Channel}</h4>
|
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"}>{vid.Channel}</a></h4>
|
||||||
<progress value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
<progress class="wavy light-green-text" value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
||||||
</div>;
|
</div>;
|
||||||
progress++;
|
progress++;
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -62,12 +62,13 @@ func Components.Shell(title, html, page, $mypage)
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="responsive">
|
<main class="responsive">
|
||||||
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
|
|
||||||
<img class="responsive" src="./tytd.svg">
|
|
||||||
</div>
|
|
||||||
<raw(html)>
|
<raw(html)>
|
||||||
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
|
||||||
|
<img class="responsive" src="./tytd.svg">
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>;
|
</html>;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
func Components.ShellSimple(title, html, $htmx)
|
||||||
|
{
|
||||||
|
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">
|
||||||
|
<link rel="stylesheet" href="theme.css">
|
||||||
|
<script type="module" src="beer.min.js" defer></script>
|
||||||
|
<if(htmx)>
|
||||||
|
<true>
|
||||||
|
<script src="htmx.min.js" defer></script>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<main class="responsive">
|
||||||
|
<raw(html)>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>;
|
||||||
|
}
|
||||||
@@ -9,28 +9,313 @@ var TYTDResources = [
|
|||||||
{path="/favicon.ico",value=embed("favicon.ico")},
|
{path="/favicon.ico",value=embed("favicon.ico")},
|
||||||
{path="/tytd.svg",value=embed("tytd.svg")},
|
{path="/tytd.svg",value=embed("tytd.svg")},
|
||||||
{path="/loading-indicator.svg",value=embed("loading-indicator.svg")},
|
{path="/loading-indicator.svg",value=embed("loading-indicator.svg")},
|
||||||
{path="/theme.css",value=embed("theme.css")}
|
{path="/theme.css",value=embed("theme.css")},
|
||||||
|
{path="/video.min.js",value=embed("video.min.js")},
|
||||||
|
{path="/video-js.css",value=embed("video-js.css")},
|
||||||
|
{path="/wavy.svg",value=embed("wavy.svg")}
|
||||||
];
|
];
|
||||||
var times=1;
|
var times=1;
|
||||||
|
|
||||||
class TYTDApp {
|
class TYTDApp {
|
||||||
|
private OOBE_STATE = {
|
||||||
|
tag = "UnknownPC",
|
||||||
|
pollHours = 3,
|
||||||
|
pollMinutes = 0,
|
||||||
|
pollSeconds = 0,
|
||||||
|
enablePlugins = true
|
||||||
|
};
|
||||||
|
|
||||||
private TYTD;
|
private TYTD;
|
||||||
|
|
||||||
public TYTDApp()
|
public TYTDApp()
|
||||||
{
|
{
|
||||||
|
|
||||||
var tytdfs = new SubdirFilesystem(FS.Local, "TYTD");
|
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
|
||||||
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull("TYTD"));
|
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
|
||||||
this.TYTD.Start();
|
this.TYTD.Start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Handle(ctx)
|
public Handle(ctx)
|
||||||
{
|
{
|
||||||
|
if(ctx.Path == "/api/v1/login")
|
||||||
|
{
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
const req=ctx.ReadJson();
|
||||||
|
const result = this.TYTD.Login(req.username, req.password);
|
||||||
|
if(result)
|
||||||
|
{
|
||||||
|
ctx.SendJson({
|
||||||
|
success = true,
|
||||||
|
token = result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx.SendJson({
|
||||||
|
success=false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx.StatusCode=307;
|
||||||
|
ctx.SendRedirect("/login");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ctx.Path == "/login")
|
||||||
|
{
|
||||||
|
var incorrect=false;
|
||||||
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
||||||
|
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
||||||
|
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
||||||
|
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
const result = this.TYTD.Login(username, password);
|
||||||
|
if(result)
|
||||||
|
{
|
||||||
|
ctx.StatusCode = 303;
|
||||||
|
ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Strict").SendRedirect("/");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else incorrect=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.Login(redirect, incorrect));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const loggedIn = this.TYTD.IsLoggedIn(ctx);
|
||||||
|
if(!loggedIn)
|
||||||
|
{
|
||||||
|
each(var file : TYTDResources)
|
||||||
|
{
|
||||||
|
if(ctx.Path == file.path)
|
||||||
|
{
|
||||||
|
ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(this.TYTD.Config.OobeState == "oobe")
|
||||||
|
{
|
||||||
|
each(var file : TYTDResources)
|
||||||
|
{
|
||||||
|
if(ctx.Path == file.path)
|
||||||
|
{
|
||||||
|
ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ctx.Path == "/oobe")
|
||||||
|
{
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
const from = ctx.QueryParams.TryGetFirstInt("from");
|
||||||
|
switch(from)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
const enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins");
|
||||||
|
const tag = ctx.QueryParams.TryGetFirst("tag") ?? "UnknownPC";
|
||||||
|
const pollHours = ctx.QueryParams.TryGetFirstInt("pollHours") ?? 3;
|
||||||
|
const pollMinutes = ctx.QueryParams.TryGetFirstInt("pollMinutes") ?? 0;
|
||||||
|
const pollSeconds = ctx.QueryParams.TryGetFirstInt("pollSeconds") ?? 0;
|
||||||
|
|
||||||
|
|
||||||
|
this.OOBE_STATE.tag = tag;
|
||||||
|
this.OOBE_STATE.pollHours = pollHours;
|
||||||
|
this.OOBE_STATE.pollMinutes = pollMinutes;
|
||||||
|
|
||||||
|
this.OOBE_STATE.pollSeconds = pollSeconds;
|
||||||
|
|
||||||
|
this.OOBE_STATE.enablePlugins = enablePlugins;
|
||||||
|
|
||||||
|
const page = this.OOBE_STATE.enablePlugins ? "/oobe?page=2" : "/oobe?page=3";
|
||||||
|
ctx.StatusCode = 303;
|
||||||
|
ctx.SendRedirect(page);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
{
|
||||||
|
|
||||||
|
const createUser = (ctx.QueryParams.TryGetFirst("user") ?? "no")=="yes";
|
||||||
|
|
||||||
|
var seconds = this.OOBE_STATE.pollSeconds;
|
||||||
|
seconds += this.OOBE_STATE.pollMinutes * 60;
|
||||||
|
seconds += this.OOBE_STATE.pollHours * 3600;
|
||||||
|
this.TYTD.Config.TYTDTag = this.OOBE_STATE.tag;
|
||||||
|
this.TYTD.Config.BellTimer = seconds;
|
||||||
|
this.TYTD.Config.EnablePlugins = this.OOBE_STATE.enablePlugins;
|
||||||
|
this.TYTD.Config.OobeState = "welcome";
|
||||||
|
this.TYTD.SaveConfig();
|
||||||
|
|
||||||
|
|
||||||
|
if(createUser)
|
||||||
|
{
|
||||||
|
|
||||||
|
ctx.StatusCode = 303;
|
||||||
|
ctx.SendRedirect("/newuser");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
ctx.StatusCode = 303;
|
||||||
|
ctx.SendRedirect("/");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.ShellSimple("First Setup", <h1>Malformed data</h1>));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const page = ctx.QueryParams.TryGetFirstInt("page");
|
||||||
|
switch(page)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.OobePage1(this.OOBE_STATE));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.OobePage2(this.TYTD,ctx));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.OobePage3());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.ShellSimple("First Setup", <h1>Setup page not found</h1>));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(ctx.Path != "/package-manage" && ctx.Path != "/api/v1/plugin-thumbnail.png")
|
||||||
|
{
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect($"/oobe?page=1");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ctx.Path == "/welcome")
|
||||||
|
{
|
||||||
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
|
||||||
|
if(ctx.QueryParams.GetFirstBoolean("closePopup"))
|
||||||
|
{
|
||||||
|
if(this.TYTD.Config.OobeState == "welcome")
|
||||||
|
{
|
||||||
|
this.TYTD.Config.OobeState = "finished";
|
||||||
|
this.TYTD.SaveConfig();
|
||||||
|
}
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect(redirect);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.Welcome(redirect));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(this.TYTD.Config.OobeState == "welcome")
|
||||||
|
{
|
||||||
|
each(var file : TYTDResources)
|
||||||
|
{
|
||||||
|
if(ctx.Path == file.path)
|
||||||
|
{
|
||||||
|
ctx.WithMimeType(Net.Http.MimeType(file.path)).SendBytes(file.value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ctx.Path != "/newuser")
|
||||||
|
{
|
||||||
|
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect($"/welcome?redirect={Net.Http.UrlEncode(ctx.Path)}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if(ctx.Path == "/")
|
if(ctx.Path == "/")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
|
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/passwd")
|
||||||
|
{
|
||||||
|
var incorrect=false;
|
||||||
|
|
||||||
|
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
||||||
|
const newpassword = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
|
||||||
|
|
||||||
|
const confirm = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
this.TYTD.Passwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
||||||
|
}
|
||||||
|
else if(ctx.Path == "/newuser")
|
||||||
|
{
|
||||||
|
var error = null;
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
const username = ctx.QueryParams.TryGetFirst("username");
|
||||||
|
const password = ctx.QueryParams.TryGetFirst("password");
|
||||||
|
const confirm = ctx.QueryParams.TryGetFirst("confirm");
|
||||||
|
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin");
|
||||||
|
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins");
|
||||||
|
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins");
|
||||||
|
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB");
|
||||||
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
|
||||||
|
var flags = 0;
|
||||||
|
if(isAdmin) flags |= UserFlags.AdminFlag;
|
||||||
|
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
||||||
|
if(canManagePlugins) flags |= UserFlags.ManagePluginFlag;
|
||||||
|
if(canDownloadDB) flags |= UserFlags.DatabaseFlag;
|
||||||
|
|
||||||
|
|
||||||
|
if(password != confirm)
|
||||||
|
{
|
||||||
|
error = "Passwords do not match";
|
||||||
|
}
|
||||||
|
else if(!TypeIsString(password)) {
|
||||||
|
error = "Password is not defined";
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(!TypeIsString(username)) {
|
||||||
|
error = "Username not defined";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
error = this.TYTD.CreateAccount(ctx, username, password, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!TypeIsString(error))
|
||||||
|
{
|
||||||
|
ctx.StatusCode=303;
|
||||||
|
ctx.SendRedirect(redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.NewUser(error));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if(ctx.Path == "/logout")
|
||||||
|
{
|
||||||
|
this.TYTD.Logout(ctx);
|
||||||
|
ctx.StatusCode = 307;
|
||||||
|
ctx.SendRedirect("/");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api")
|
else if(ctx.Path == "/api")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Api());
|
ctx.WithMimeType("text/html").SendText(Pages.Api());
|
||||||
@@ -41,6 +326,20 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("text/html").SendText(Pages.ApiV1());
|
ctx.WithMimeType("text/html").SendText(Pages.ApiV1());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/embed")
|
||||||
|
{
|
||||||
|
var v = ctx.QueryParams.TryGetFirst("v");
|
||||||
|
if(TypeIsString(v))
|
||||||
|
{
|
||||||
|
const video = this.TYTD.GetVideo(v);
|
||||||
|
if(TypeIsDefined(video))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.VideoEmbed(v, video.title));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/download")
|
else if(ctx.Path == "/api/v1/download")
|
||||||
{
|
{
|
||||||
var v = ctx.QueryParams.TryGetFirst("v");
|
var v = ctx.QueryParams.TryGetFirst("v");
|
||||||
@@ -51,14 +350,16 @@ class TYTDApp {
|
|||||||
|
|
||||||
var path = this.TYTD.GetVideoPath(v,res);
|
var path = this.TYTD.GetVideoPath(v,res);
|
||||||
|
|
||||||
if(path != null)
|
if(path != null && this.TYTD.Storage.FileExists(path))
|
||||||
{
|
{
|
||||||
var info = this.TYTD.GetVideo(v);
|
var info = this.TYTD.GetVideo(v);
|
||||||
var filename = $"{info.title}-{info.videoId}-{res}{path.GetExtension()}";
|
var filename = $"{info.title}-{info.videoId}-{res}{path.GetExtension()}";
|
||||||
var strm = this.TYTD.Storage.OpenFile(path,"rb");
|
var strm = this.TYTD.Storage.OpenFile(path,"rb");
|
||||||
ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm);
|
ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm);
|
||||||
strm.Close();
|
strm.Close();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
else if(ctx.Path == "/api/v1/progress.json")
|
else if(ctx.Path == "/api/v1/progress.json")
|
||||||
{
|
{
|
||||||
@@ -131,7 +432,11 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path == "/api/v1/database.db")
|
else if(ctx.Path == "/api/v1/database.db")
|
||||||
{
|
{
|
||||||
this.TYTD.SendDatabase(ctx);
|
|
||||||
|
if(!this.TYTD.SendDatabase(ctx))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized","<h1>You are not authorized to download the database</h1>",3,/"api/v1/database.db"));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(ctx.Path == "/api/v1/personal")
|
else if(ctx.Path == "/api/v1/personal")
|
||||||
@@ -251,6 +556,12 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id));
|
ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/whoami")
|
||||||
|
{
|
||||||
|
const row = this.TYTD.WhoAmI(ctx);
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.WhoAmI(row));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/edit-personal-description")
|
else if(ctx.Path == "/edit-personal-description")
|
||||||
{
|
{
|
||||||
var name = ctx.QueryParams.TryGetFirst("name");
|
var name = ctx.QueryParams.TryGetFirst("name");
|
||||||
@@ -280,8 +591,98 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path == "/settings")
|
else if(ctx.Path == "/settings")
|
||||||
{
|
{
|
||||||
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.Settings(this.TYTD,ctx));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if(ctx.Path == "/edituser")
|
||||||
|
{
|
||||||
|
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,3));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const user = ctx.QueryParams.TryGetFirst("user");
|
||||||
|
|
||||||
|
if(TypeIsString(user))
|
||||||
|
{
|
||||||
|
var userObj = null;
|
||||||
|
const whoami = this.TYTD.WhoAmI(ctx);
|
||||||
|
if(ctx.Method == "POST")
|
||||||
|
{
|
||||||
|
if(whoami.username != user)
|
||||||
|
{
|
||||||
|
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin");
|
||||||
|
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins");
|
||||||
|
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins");
|
||||||
|
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB");
|
||||||
|
var flags = 0;
|
||||||
|
if(isAdmin) flags |= UserFlags.AdminFlag;
|
||||||
|
if(canUsePlugins) flags |= UserFlags.PluginFlag;
|
||||||
|
if(canManagePlugins) flags |= UserFlags.ManagePluginFlag;
|
||||||
|
if(canDownloadDB) flags |= UserFlags.DatabaseFlag;
|
||||||
|
|
||||||
|
|
||||||
|
this.TYTD.Mutex.Lock();
|
||||||
|
const db = this.TYTD.OpenDB();
|
||||||
|
Sqlite.Exec(db, $"UPDATE users SET flags = {flags} WHERE username = {Sqlite.Escape(user)}");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.TYTD.Mutex.Unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ctx.Method == "DELETE")
|
||||||
|
{
|
||||||
|
if(whoami.username != user)
|
||||||
|
{
|
||||||
|
this.TYTD.Mutex.Lock();
|
||||||
|
const db = this.TYTD.OpenDB();
|
||||||
|
Sqlite.Exec(db,$"DELETE FROM users WHERE username = {Sqlite.Escape(user)}");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.TYTD.Mutex.Unlock();
|
||||||
|
ctx.StatusCode = 200;
|
||||||
|
ctx.WithHeader("HX-Location", "/edituser");
|
||||||
|
ctx.SendText("Redirect");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.StatusCode = 401;
|
||||||
|
ctx.WriteHeaders();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(whoami.username != user)
|
||||||
|
{
|
||||||
|
this.TYTD.Mutex.Lock();
|
||||||
|
const db = this.TYTD.OpenDB();
|
||||||
|
userObj = Sqlite.Exec(db,$"SELECT * FROM users WHERE username = {Sqlite.Escape(user)}");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.TYTD.Mutex.Unlock();
|
||||||
|
if(TypeIsList(userObj) && userObj.Length == 1) userObj = userObj[0];
|
||||||
|
else userObj=null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.EditUser(userObj));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx.WithMimeType("text/html").SendText(Pages.EditUserList(this.TYTD, ctx));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if(ctx.Path == "/admin")
|
||||||
|
{
|
||||||
|
if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx)))
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,3));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
|
|
||||||
/*
|
/*
|
||||||
hours
|
hours
|
||||||
minutes
|
minutes
|
||||||
@@ -305,10 +706,10 @@ class TYTDApp {
|
|||||||
this.TYTD.SaveConfig();
|
this.TYTD.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Settings(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.Admin(this.TYTD,ctx));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(ctx.Path == "/video")
|
else if(ctx.Path == "/watch" || ctx.Path == "/video")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
||||||
return true;
|
return true;
|
||||||
@@ -408,11 +809,13 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path == "/plugins-download")
|
else if(ctx.Path == "/plugins-download")
|
||||||
{
|
{
|
||||||
|
if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false;
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.DownloadPlugins(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.DownloadPlugins(this.TYTD,ctx));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if(ctx.Path == "/package-manage" && ctx.Method == "POST")
|
else if(ctx.Path == "/package-manage" && ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
|
if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false;
|
||||||
var data = {
|
var data = {
|
||||||
name = ctx.QueryParams.TryGetFirst("name"),
|
name = ctx.QueryParams.TryGetFirst("name"),
|
||||||
version = ctx.QueryParams.TryGetFirst("version"),
|
version = ctx.QueryParams.TryGetFirst("version"),
|
||||||
@@ -483,7 +886,12 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
else if(ctx.Path.StartsWith("/plugin/"))
|
else if(ctx.Path.StartsWith("/plugin/"))
|
||||||
{
|
{
|
||||||
|
if(UserFlags.CanUsePlugins(this.TYTD.IsLoggedIn(ctx)))
|
||||||
return this.TYTD.Servers.Handle(ctx);
|
return this.TYTD.Servers.Handle(ctx);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
each(var file : TYTDResources)
|
each(var file : TYTDResources)
|
||||||
@@ -504,8 +912,53 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTYTDDir()
|
||||||
|
{
|
||||||
|
const dir = Env["TYTDDIR"];
|
||||||
|
if(TypeIsString(dir) && dir.Length > 0)
|
||||||
|
return dir;
|
||||||
|
return (Env.Videos / "TYTD2025").ToString();
|
||||||
|
}
|
||||||
|
|
||||||
func WebAppMain(args)
|
func WebAppMain(args)
|
||||||
{
|
{
|
||||||
|
if(args.Length == 2 && args[1] == "change-password")
|
||||||
|
{
|
||||||
|
Console.Write("Username: ");
|
||||||
|
const username = Console.ReadLine();
|
||||||
|
Console.Write("Password: ");
|
||||||
|
const echo = Console.Echo;
|
||||||
|
Console.Echo = false;
|
||||||
|
const password = Console.ReadLine();
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.Write("Confirm: ");
|
||||||
|
const confirm = Console.ReadLine();
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.Echo = true;
|
||||||
|
|
||||||
|
if(password != confirm)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Passwords do not match!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = Crypto.RandomBytes(32, "TYTD2025");
|
||||||
|
const hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
|
FS.Local.CreateDirectory(GetTYTDDir());
|
||||||
|
const db = Sqlite.Open(GetTYTDDir()/"tytd.db");
|
||||||
|
|
||||||
|
|
||||||
|
const res = Sqlite.Exec(db, $"UPDATE users SET password_hash = {Sqlite.Escape(Crypto.Base64Encode(hash))}, password_salt = {Sqlite.Escape(Crypto.Base64Encode(salt))} WHERE username = {Sqlite.Escape(username)};");
|
||||||
|
if(TypeIsList(res))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Changed password successfully");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Console.WriteLine($"Failed to change password: {res}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return new TYTDApp();
|
return new TYTDApp();
|
||||||
}
|
}
|
||||||
func main(args)
|
func main(args)
|
||||||
|
|||||||
76
Tesses.YouTubeDownloader.Server/src/pages/admin.tcross
Normal file
76
Tesses.YouTubeDownloader.Server/src/pages/admin.tcross
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
func Pages.Admin(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="./admin" 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>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<button>Save</button>
|
||||||
|
<a class="button" hx-push-url="true" hx-get="./newuser" hx-target="body" href="./newuser">New user</a>
|
||||||
|
|
||||||
|
<a class="button" hx-push-url="true" hx-get="./edituser" hx-target="body" href="./edituser">Edit user</a>
|
||||||
|
<a class="button" hx-push-url="true" hx-get="./settings" hx-target="body" href="./settings">Back to Settings</a>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</form>;
|
||||||
|
|
||||||
|
return Components.Shell("Admin Settings",html ,3);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
func Pages.ChangePassword(redirect,incorrect)
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const html = <null>
|
||||||
|
<if(incorrect)>
|
||||||
|
<true>
|
||||||
|
<blockquote>
|
||||||
|
<h5>The password could not be changed</h5>
|
||||||
|
</blockquote>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
|
<form method="POST" action="./passwd">
|
||||||
|
<input type="hidden" name="redirect" value={redirect}>
|
||||||
|
<article class="border medium no-padding center-align middle-align">
|
||||||
|
<div class="padding">
|
||||||
|
<h5>Change your password</h5>
|
||||||
|
<div class="medium-padding">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<label>Current password</label>
|
||||||
|
</div>
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="password" name="newpassword">
|
||||||
|
<label>New password</label>
|
||||||
|
</div>
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="password" name="confirm">
|
||||||
|
<label>Confirm new password</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-padding">
|
||||||
|
|
||||||
|
<button>Change password</button>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" name="logout" checked>
|
||||||
|
<span>Log me out on all devices</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</form>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.Shell("Change your password",html,3);
|
||||||
|
}
|
||||||
145
Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross
Normal file
145
Tesses.YouTubeDownloader.Server/src/pages/edituser.tcross
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
func Pages.EditUserList(tytd, ctx)
|
||||||
|
{
|
||||||
|
tytd.Mutex.Lock();
|
||||||
|
const db = tytd.OpenDB();
|
||||||
|
const users = Sqlite.Exec(db,"SELECT * FROM users;");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
tytd.Mutex.Unlock();
|
||||||
|
const myuser = tytd.WhoAmI(ctx);
|
||||||
|
const html = <null>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Find by username</legend>
|
||||||
|
<form method="GET" hx-get="./edituser" action="./edituser" hx-target="body" hx-push-url="true">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="text" name="user">
|
||||||
|
<label>Username</label>
|
||||||
|
</div>
|
||||||
|
<button>Edit User</button>
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Users</legend>
|
||||||
|
<ul class="list border">
|
||||||
|
<if(TypeIsList(users))>
|
||||||
|
<true>
|
||||||
|
<each(var item : users)>
|
||||||
|
<if(item.username != myuser.username)>
|
||||||
|
<true>
|
||||||
|
<li>
|
||||||
|
<a hx-get={$"./edituser?user={Net.Http.UrlEncode(item.username)}"} href={$"./edituser?user={Net.Http.UrlEncode(item.username)}"} hx-target="body" hx-push-url="true">{item.username}</a>
|
||||||
|
</li>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
|
</each>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.Shell("Edit users",html, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pages.EditUser(user)
|
||||||
|
{
|
||||||
|
user.flags = ParseLong(user.flags);
|
||||||
|
const html = <if(TypeIsDictionary(user))>
|
||||||
|
<true>
|
||||||
|
<h1>User: {user.username}</h1>
|
||||||
|
|
||||||
|
<form hx-post={$"./edituser?user={Net.Http.UrlEncode(user.username)}"} hx-target="body" hx-push-url="true">
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Is Admin</h6>
|
||||||
|
<div>Is an administrator (will enable all toggles)</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<if(user.flags & UserFlags.AdminFlag)>
|
||||||
|
<true>
|
||||||
|
<input type="checkbox" name="isAdmin" checked>
|
||||||
|
</true>
|
||||||
|
<false>
|
||||||
|
|
||||||
|
<input type="checkbox" name="isAdmin">
|
||||||
|
</false>
|
||||||
|
</if>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Plugins</h6>
|
||||||
|
<div>Can use plugins</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
|
||||||
|
<if(user.flags & UserFlags.PluginFlag)>
|
||||||
|
<true>
|
||||||
|
<input type="checkbox" name="canUsePlugins" checked>
|
||||||
|
</true>
|
||||||
|
<false>
|
||||||
|
|
||||||
|
<input type="checkbox" name="canUsePlugins">
|
||||||
|
</false>
|
||||||
|
</if>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Manage plugins</h6>
|
||||||
|
<div>Can manage plugins (will also enable Plugins)</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<if(user.flags & UserFlags.ManagePluginFlag)>
|
||||||
|
<true>
|
||||||
|
<input type="checkbox" name="canManagePlugins" checked>
|
||||||
|
</true>
|
||||||
|
<false>
|
||||||
|
|
||||||
|
<input type="checkbox" name="canManagePlugins">
|
||||||
|
</false>
|
||||||
|
</if>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Database Download</h6>
|
||||||
|
<div>Can download database</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<if(user.flags & UserFlags.DatabaseFlag)>
|
||||||
|
<true>
|
||||||
|
<input type="checkbox" name="canDownloadDB" checked>
|
||||||
|
</true>
|
||||||
|
<false>
|
||||||
|
|
||||||
|
<input type="checkbox" name="canDownloadDB">
|
||||||
|
</false>
|
||||||
|
</if>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button><i>save</i> Save</button>
|
||||||
|
<button class="error-container" hx-delete={$"./edituser?user={Net.Http.UrlEncode(user.username)}"} hx-confirm="Are you sure?"><i>delete_forever</i> Delete</button>
|
||||||
|
</form>
|
||||||
|
</true>
|
||||||
|
<false>
|
||||||
|
You can{"'"}t edit yourself
|
||||||
|
</false>
|
||||||
|
</if>;
|
||||||
|
|
||||||
|
|
||||||
|
return Components.Shell("Edit user",html, 3);
|
||||||
|
}
|
||||||
41
Tesses.YouTubeDownloader.Server/src/pages/embed.tcross
Normal file
41
Tesses.YouTubeDownloader.Server/src/pages/embed.tcross
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
func Pages.VideoEmbed(id, name)
|
||||||
|
{
|
||||||
|
const css = "<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>";
|
||||||
|
const srcTag = $"<source src=\"./api/v1/download?v={Net.Http.UrlEncode(id)}&inline=true\" type=\"video/mp4\"/>";
|
||||||
|
const html = <!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TYTD - {name}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./video-js.css">
|
||||||
|
<script src="./video.min.js" defer></script>
|
||||||
|
/* thanks https://github.com/videojs/video.js/discussions/8156#discussioncomment-5098465 */
|
||||||
|
<raw(css)>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video
|
||||||
|
id="my-video"
|
||||||
|
class="video-js"
|
||||||
|
controls="on"
|
||||||
|
preload="auto"
|
||||||
|
poster={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(id)}&res=default"}
|
||||||
|
data-setup="{\"fill\": true}">
|
||||||
|
<raw(srcTag)>
|
||||||
|
<p class="vjs-no-js">
|
||||||
|
To view this video please enable JavaScript, and consider upgrading to a
|
||||||
|
web browser that
|
||||||
|
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
|
||||||
|
</p>
|
||||||
|
</video>
|
||||||
|
</body>
|
||||||
|
</html>;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
35
Tesses.YouTubeDownloader.Server/src/pages/login.tcross
Normal file
35
Tesses.YouTubeDownloader.Server/src/pages/login.tcross
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
func Pages.Login(redirect,incorrect)
|
||||||
|
{
|
||||||
|
const html = <null>
|
||||||
|
<if(incorrect)>
|
||||||
|
<true>
|
||||||
|
<blockquote>
|
||||||
|
<h5>The login was incorrect, or does not exist</h5>
|
||||||
|
</blockquote>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
|
<form method="POST" action="./login">
|
||||||
|
<input type="hidden" name="redirect" value={redirect}>
|
||||||
|
<article class="border medium no-padding center-align middle-align">
|
||||||
|
<div class="padding">
|
||||||
|
<h5>Login to TYTD2025</h5>
|
||||||
|
<div class="medium-padding">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="text" name="username">
|
||||||
|
<label>Username</label>
|
||||||
|
</div>
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<label>Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="medium-padding">
|
||||||
|
<button>Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</form>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.ShellSimple("Login", html);
|
||||||
|
}
|
||||||
85
Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross
Normal file
85
Tesses.YouTubeDownloader.Server/src/pages/newaccount.tcross
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
func Pages.NewUser(error)
|
||||||
|
{
|
||||||
|
const html = <null>
|
||||||
|
<if(TypeIsString(error))>
|
||||||
|
<true>
|
||||||
|
<blockquote>
|
||||||
|
<h5>{error}</h5>
|
||||||
|
</blockquote>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
|
<form hx-post="./newuser" hx-target="body" hx-push-url="true">
|
||||||
|
<div class="padding">
|
||||||
|
<h5>Create new user</h5>
|
||||||
|
<div class="medium-padding">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="text" name="username">
|
||||||
|
<label>Username</label>
|
||||||
|
</div>
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<label>Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="password" name="confirm">
|
||||||
|
<label>Confirm Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Is Admin</h6>
|
||||||
|
<div>Is an administrator (will enable all toggles)</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" name="isAdmin">
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Plugins</h6>
|
||||||
|
<div>Can use plugins</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" name="canUsePlugins">
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Manage plugins</h6>
|
||||||
|
<div>Can manage plugins (will also enable Plugins)</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" name="canManagePlugins">
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="field middle-align">
|
||||||
|
<nav>
|
||||||
|
<div class="max">
|
||||||
|
<h6>Database Download</h6>
|
||||||
|
<div>Can download database</div>
|
||||||
|
</div>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" name="canDownloadDB">
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medium-padding">
|
||||||
|
<button>Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.Shell("New User",html ,3);
|
||||||
|
}
|
||||||
73
Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross
Normal file
73
Tesses.YouTubeDownloader.Server/src/pages/oobe/page1.tcross
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
func Pages.OobePage1(state)
|
||||||
|
{
|
||||||
|
|
||||||
|
const html= <null><nav>
|
||||||
|
<div class="center-align">
|
||||||
|
<button class="circle small">1</button>
|
||||||
|
<div class="small-margin">Settings</div>
|
||||||
|
</div>
|
||||||
|
<hr class="max">
|
||||||
|
<div class="center-align">
|
||||||
|
<button class="circle small" disabled>2</button>
|
||||||
|
<div class="small-margin">Plugins</div>
|
||||||
|
</div>
|
||||||
|
<hr class="max">
|
||||||
|
<div class="center-align">
|
||||||
|
<button class="circle small" disabled>3</button>
|
||||||
|
<div class="small-margin">Security</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<form action="./oobe" method="POST">
|
||||||
|
<input type="hidden" value="1" name="from">
|
||||||
|
<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(state.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={state.tag}>
|
||||||
|
<label>TYTD Tag (Name your instance)</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={state.pollHours}>
|
||||||
|
<label>Hours</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="number" name="minutes" value={state.pollMinutes}>
|
||||||
|
<label>Minutes</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="number" name="seconds" value={state.pollSeconds}>
|
||||||
|
<label>Seconds</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<button>Next</button>
|
||||||
|
</form>
|
||||||
|
</null>;
|
||||||
|
return Components.ShellSimple("Settings", html);
|
||||||
|
}
|
||||||
88
Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross
Normal file
88
Tesses.YouTubeDownloader.Server/src/pages/oobe/page2.tcross
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
func Pages.OobePage2(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>
|
||||||
|
<div class="center-align">
|
||||||
|
<a href="./oobe?page=1" class="button circle small">
|
||||||
|
<i>done</i>
|
||||||
|
</a>
|
||||||
|
<div class="small-margin">Settings</div>
|
||||||
|
</div>
|
||||||
|
<hr class="max">
|
||||||
|
<div class="center-align">
|
||||||
|
<button class="circle small">2</button>
|
||||||
|
<div class="small-margin">Plugins</div>
|
||||||
|
</div>
|
||||||
|
<hr class="max">
|
||||||
|
<div class="center-align">
|
||||||
|
<button class="circle small" disabled>3</button>
|
||||||
|
<div class="small-margin">Security</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<form hx-get="./oobe" hx-target="body" hx-push-url="true">
|
||||||
|
<input type="hidden" name="page" value="2">
|
||||||
|
<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>
|
||||||
|
<a class="button" href="./oobe?page=3">Next</a>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.ShellSimple("Download plugins",html ,true);
|
||||||
|
}
|
||||||
46
Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross
Normal file
46
Tesses.YouTubeDownloader.Server/src/pages/oobe/page3.tcross
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
func Pages.OobePage3()
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
var html= <null>
|
||||||
|
<nav>
|
||||||
|
<div class="center-align">
|
||||||
|
<a href="./oobe?page=1" class="button circle small">
|
||||||
|
<i>done</i>
|
||||||
|
</a>
|
||||||
|
<div class="small-margin">Settings</div>
|
||||||
|
</div>
|
||||||
|
<hr class="max">
|
||||||
|
<div class="center-align">
|
||||||
|
<a href="./oobe?page=2" class="button circle small">
|
||||||
|
<i>done</i>
|
||||||
|
</a>
|
||||||
|
<div class="small-margin">Plugins</div>
|
||||||
|
</div>
|
||||||
|
<hr class="max">
|
||||||
|
<div class="center-align">
|
||||||
|
<button class="circle small">3</button>
|
||||||
|
<div class="small-margin">Security</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<form method="POST" action="/oobe">
|
||||||
|
<input type="hidden" name="from" value="3">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Do you want to create an account</legend>
|
||||||
|
<nav class="vertical">
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="user" value="yes" checked>
|
||||||
|
<span>Yes create an account</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="user" value="no">
|
||||||
|
<span>No don{"'"}t create an account (anyone who has access to this interface can use this downloader)</span>
|
||||||
|
</label>
|
||||||
|
</nav>
|
||||||
|
</fieldset>
|
||||||
|
<button>Next</button>
|
||||||
|
</form>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.ShellSimple("Security",html);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
func Pages.PlaylistInfo(tytd,ctx)
|
func Pages.PlaylistInfo(tytd,ctx)
|
||||||
{
|
{
|
||||||
var id = ctx.QueryParams.TryGetFirst("id");
|
var id = ctx.QueryParams.TryGetFirst("list") ?? ctx.QueryParams.TryGetFirst("id");
|
||||||
var page = ctx.QueryParams.TryGetFirstInt("page");
|
var page = ctx.QueryParams.TryGetFirstInt("page");
|
||||||
if(TypeOf(page) != "Long") page = 1;
|
if(TypeOf(page) != "Long") page = 1;
|
||||||
page--;
|
page--;
|
||||||
@@ -27,7 +27,7 @@ func Pages.PlaylistInfo(tytd,ctx)
|
|||||||
</each>
|
</each>
|
||||||
<footer class="row center-align">
|
<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>
|
<button hx-get={$"./playlist?list={Net.Http.UrlEncode(id)}&page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./playlist?list={Net.Http.UrlEncode(id)}&type=videos&page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
|
||||||
|
|
||||||
</footer>
|
</footer>
|
||||||
</null>;
|
</null>;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
func Pages.Plugins(tytd,ctx)
|
func Pages.Plugins(tytd,ctx)
|
||||||
{
|
{
|
||||||
var html= <null><nav class="tabbed">
|
const userFlags = tytd.IsLoggedIn(ctx);
|
||||||
|
|
||||||
|
var html= <null><if(UserFlags.CanManagePlugins(userFlags))><true><nav class="tabbed">
|
||||||
<a class="active" hx-get="./plugins" hx-target="body" hx-push-url="true">
|
<a class="active" hx-get="./plugins" hx-target="body" hx-push-url="true">
|
||||||
<i>download_done</i>
|
<i>download_done</i>
|
||||||
<span>Installed</span>
|
<span>Installed</span>
|
||||||
@@ -10,12 +12,13 @@ func Pages.Plugins(tytd,ctx)
|
|||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
<each(var item : tytd.Plugins)>
|
<each(var item : tytd.Plugins)>
|
||||||
<raw(Components.InstalledPlugin(item))>
|
<raw(Components.InstalledPlugin(item))>
|
||||||
|
|
||||||
</each>
|
</each>
|
||||||
</null>;
|
</null>;
|
||||||
|
|
||||||
return Components.Shell("Installed plugins",html ,2);
|
return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,2);
|
||||||
}
|
}
|
||||||
@@ -1,68 +1,19 @@
|
|||||||
func Pages.Settings(tytd,ctx)
|
func Pages.Settings(tytd,ctx)
|
||||||
{
|
{
|
||||||
var totalSecs = tytd.Config.BellTimer ?? 10800;
|
|
||||||
var enablePlugins = tytd.Config.EnablePlugins ?? true;
|
const html= <fieldset>
|
||||||
var hours = totalSecs / 3600;
|
<legend>Actions</legend>
|
||||||
totalSecs -= hours * 3600;
|
<a class="button" hx-push-url="true" hx-get="./admin" hx-target="body" href="./admin">Admin</a>
|
||||||
var minutes = totalSecs / 60;
|
|
||||||
var seconds = totalSecs % 60;
|
|
||||||
|
|
||||||
|
<a class="button" hx-push-url="true" href="./api/v1/database.db">Download Database</a>
|
||||||
var html = <form hx-post="./settings" hx-target="body" >
|
|
||||||
<div class="field middle-align">
|
|
||||||
<nav>
|
<a class="button" hx-push-url="true" hx-get="./whoami" hx-target="body" href="./whoami">whoami</a>
|
||||||
<div class="max">
|
<a class="button" hx-push-url="true" hx-get="./passwd" hx-target="body" href="./passwd">Change password</a>
|
||||||
<h6>Enable plugins</h6>
|
|
||||||
<div>Plugins allow you to extend tytd, but they have full access</div>
|
<a class="button" href="./logout">Logout</a>
|
||||||
</div>
|
|
||||||
<label class="switch">
|
</fieldset>;
|
||||||
<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>
|
|
||||||
|
|
||||||
<a class="button responsive" href="./api/v1/database.db"><i>download</i> Download Database</a>
|
|
||||||
</form>;
|
|
||||||
|
|
||||||
return Components.Shell("Settings",html ,3);
|
return Components.Shell("Settings",html ,3);
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,11 @@ func Pages.VideoInfo(tytd,ctx)
|
|||||||
{
|
{
|
||||||
html = <div class="col">
|
html = <div class="col">
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<img src={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(vi.videoId)}"}>
|
<iframe src={$"./embed?v={Net.Http.UrlEncode(vi.videoId)}"} style="overflow: hidden;" width="640"
|
||||||
|
height="360" scrolling="no"></iframe>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<raw(Components.AddVideo($"https://www.youtube.com/watch?v={vi.videoId}"))>
|
<raw(Components.AddVideo($"https://www.youtube.com/watch?v={Net.Http.UrlEncode(vi.videoId)}"))>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
<raw(Components.AddToPersonalList(tytd,vi.videoId))>
|
<raw(Components.AddToPersonalList(tytd,vi.videoId))>
|
||||||
@@ -20,7 +21,7 @@ func Pages.VideoInfo(tytd,ctx)
|
|||||||
<div class="min">
|
<div class="min">
|
||||||
<h4>{vi.title}</h4>
|
<h4>{vi.title}</h4>
|
||||||
<a class="underline" href={$"./channel?id={Net.Http.UrlEncode(vi.channelId)}"}>{vi.author}</a>
|
<a class="underline" href={$"./channel?id={Net.Http.UrlEncode(vi.channelId)}"}>{vi.author}</a>
|
||||||
<p>{vi.shortDescription}</p>
|
<plink(vi.shortDescription)>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
29
Tesses.YouTubeDownloader.Server/src/pages/welcome.tcross
Normal file
29
Tesses.YouTubeDownloader.Server/src/pages/welcome.tcross
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
func Pages.Welcome(redirect)
|
||||||
|
{
|
||||||
|
const html = <null>
|
||||||
|
<h1>You are all set</h1>
|
||||||
|
<p>
|
||||||
|
This is Tesses YouTube Downloader 2025, a YouTube Downloader created in <a target="_blank" href="https://crosslang.tesseslanguage.com/">CrossLang</a>
|
||||||
|
|
||||||
|
<h4><i>home</i> Home</h4>
|
||||||
|
Once you click the <b>{"I'm Done"}</b> button, you will be able to add YouTube videos to the downloader by using <i>add</i>, you can download the video from the server using <i>download</i> or view metadata/watch the video using <i>info</i> (this also will allow you to view playlist/channel contents)
|
||||||
|
|
||||||
|
<h4><i>download</i> Downloads</h4>
|
||||||
|
You can search or browse <i>movie</i> Videos, <i>list</i> Playlists, <i>person</i> Channels or <i>edit_note</i> Personal Lists (Playlists but created in TYTD2025)
|
||||||
|
|
||||||
|
<h4><i>extension</i> Plugins</h4>
|
||||||
|
You can browse (and access if you click on extension name) for installed extensions in <i>download_done</i> Installed
|
||||||
|
<br>
|
||||||
|
You can search (and install, uninstall or upgrade) plugins on <a href="https://cpkg.tesseslanguage.com/">CPKG</a> or any other CPKG server (requires editing CrossLang config files) in <i>download</i> Download
|
||||||
|
|
||||||
|
<h4><i>settings</i> Settings</h4>
|
||||||
|
You can create users, download database, enable/disable plugin support, your downloader{"'"}s tag, and the interval between subscriptions
|
||||||
|
|
||||||
|
<h4><i>api</i> Api</h4>
|
||||||
|
For developers who want to make apps around the downloader (documentation not complete yet)
|
||||||
|
</p>
|
||||||
|
<a class="button" href={$"./welcome?redirect={Net.Http.UrlEncode(redirect)}&closePopup=true"}>{"I'm Done"}</a>
|
||||||
|
</null>;
|
||||||
|
|
||||||
|
return Components.ShellSimple("Welcome", html);
|
||||||
|
}
|
||||||
15
Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
Normal file
15
Tesses.YouTubeDownloader.Server/src/pages/whoami.tcross
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
func Pages.WhoAmI(row)
|
||||||
|
{
|
||||||
|
const html = <null>
|
||||||
|
<h5>Your username is: {row.username}</h5>
|
||||||
|
|
||||||
|
<h6>Permissions: </h6>
|
||||||
|
<ul>
|
||||||
|
<li>Is admin: {UserFlags.IsAdmin(row.flags)}</li>
|
||||||
|
<li>Can download database: {UserFlags.CanDownloadDB(row.flags)}</li>
|
||||||
|
<li>Can use plugins: {UserFlags.CanUsePlugins(row.flags)}</li>
|
||||||
|
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
|
||||||
|
</ul>
|
||||||
|
</null>;
|
||||||
|
return Components.Shell("Settings",html ,3);
|
||||||
|
}
|
||||||
@@ -1,4 +1,44 @@
|
|||||||
|
//DO NOT ADD A FLAG WITH ONES BIT SET AS THIS IS
|
||||||
|
//TO DENOTE USER IS LOGGED IN
|
||||||
|
class UserFlags {
|
||||||
|
static getAdminFlag() 0b00000100;
|
||||||
|
static getPluginFlag() 0b00000010;
|
||||||
|
static getDatabaseFlag() 0b00001000;
|
||||||
|
static getManagePluginFlag() 0b00100000;
|
||||||
|
static IsAdmin(flags)
|
||||||
|
{
|
||||||
|
if(flags & UserFlags.AdminFlag) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static CanDownloadDB(flags)
|
||||||
|
{
|
||||||
|
if(flags & UserFlags.AdminFlag) return true;
|
||||||
|
if(flags & UserFlags.DatabaseFlag) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static CanCreateUsers(flags)
|
||||||
|
{
|
||||||
|
if(flags & UserFlags.AdminFlag) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static CanUsePlugins(flags)
|
||||||
|
{
|
||||||
|
if(flags & UserFlags.AdminFlag) return true;
|
||||||
|
if(flags & UserFlags.ManagePluginFlag) return true;
|
||||||
|
if(flags & UserFlags.PluginFlag) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static CanManagePlugins(flags)
|
||||||
|
{
|
||||||
|
if(flags & UserFlags.AdminFlag) return true;
|
||||||
|
if(flags & UserFlags.ManagePluginFlag) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static getITTR() 35000;
|
||||||
|
}
|
||||||
|
|
||||||
class TYTD.Downloader {
|
class TYTD.Downloader {
|
||||||
|
|
||||||
/^
|
/^
|
||||||
The storage vfs that TYTD accesses
|
The storage vfs that TYTD accesses
|
||||||
^/
|
^/
|
||||||
@@ -167,15 +207,15 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
if(vid != null)
|
if(vid != null)
|
||||||
{
|
{
|
||||||
return $"./video?v={Net.Http.UrlEncode(vid)}";
|
return $"./watch?v={Net.Http.UrlEncode(vid)}";
|
||||||
}
|
}
|
||||||
else if(pid != null)
|
else if(pid != null)
|
||||||
{
|
{
|
||||||
return $"./playlist?id={Net.Http.UrlEncode(pid)}";
|
return $"./playlist?list={Net.Http.UrlEncode(pid)}";
|
||||||
}
|
}
|
||||||
else if(cid != null)
|
else if(cid != null)
|
||||||
{
|
{
|
||||||
return $"./playlist?id={Net.Http.UrlEncode(cid)}";
|
return $"./channel?id={Net.Http.UrlEncode(cid)}";
|
||||||
}
|
}
|
||||||
return "./";
|
return "./";
|
||||||
}
|
}
|
||||||
@@ -715,7 +755,8 @@ class TYTD.Downloader {
|
|||||||
public Config = {
|
public Config = {
|
||||||
TYTDTag = "UnknownPC",
|
TYTDTag = "UnknownPC",
|
||||||
BellTimer = 10800,
|
BellTimer = 10800,
|
||||||
EnablePlugins=true
|
EnablePlugins=true,
|
||||||
|
OobeState = "oobe"
|
||||||
};
|
};
|
||||||
public SaveConfig()
|
public SaveConfig()
|
||||||
{
|
{
|
||||||
@@ -775,7 +816,7 @@ class TYTD.Downloader {
|
|||||||
_pkg.pluginName = TypeOf(info.short_name) == "String" ? info.short_name : name;
|
_pkg.pluginName = TypeOf(info.short_name) == "String" ? info.short_name : name;
|
||||||
var reso = _exec.Resources;
|
var reso = _exec.Resources;
|
||||||
var ico = _exec.Icon;
|
var ico = _exec.Icon;
|
||||||
_pkg.pluginIcon = ico >= 0 && i < reso.Count ? reso[ico] : embed("package_icon.png");
|
_pkg.pluginIcon = TypeOf(ico) == "ByteArray" ? ico : embed("package_icon.png");
|
||||||
subdir.CreateDirectory(/"Files");
|
subdir.CreateDirectory(/"Files");
|
||||||
var d = {
|
var d = {
|
||||||
TYTD = {
|
TYTD = {
|
||||||
@@ -785,11 +826,11 @@ class TYTD.Downloader {
|
|||||||
GetChannelId = TYTD.GetChannelId,
|
GetChannelId = TYTD.GetChannelId,
|
||||||
Config = {
|
Config = {
|
||||||
GetAt = (key)=>{
|
GetAt = (key)=>{
|
||||||
return this._getPluginValue(info.short_name, key);
|
return this._getPluginValue(_pkg.pluginName, key);
|
||||||
},
|
},
|
||||||
SetAt = (key,value)=>{
|
SetAt = (key,value)=>{
|
||||||
var value=value.ToString();
|
var value=value.ToString();
|
||||||
this._setPluginValue(info.short_name,key,value);
|
this._setPluginValue(_pkg.pluginName,key,value);
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
Directory = new SubdirFilesystem(subdir, /"Files"),
|
Directory = new SubdirFilesystem(subdir, /"Files"),
|
||||||
@@ -812,11 +853,11 @@ class TYTD.Downloader {
|
|||||||
env.LoadFileWithDependencies(subdir, _exec);
|
env.LoadFileWithDependencies(subdir, _exec);
|
||||||
|
|
||||||
_pkg.pluginObject = d.PluginInit();
|
_pkg.pluginObject = d.PluginInit();
|
||||||
if(_pkg.pluginObject.Server != null && _pkg.pluginObject.Server != undefined)
|
if(TypeIsDefined(_pkg.pluginObject.Server))
|
||||||
{
|
{
|
||||||
var path = /"plugin"/_pkg.pluginName;
|
var path = /"plugin"/_pkg.pluginName;
|
||||||
|
|
||||||
this.Servers.Mount(path,_pkg.pluginObject.Server);
|
this.Servers.Mount(path.ToString(),_pkg.pluginObject.Server);
|
||||||
}
|
}
|
||||||
|
|
||||||
_pkg.pluginEnv = env;
|
_pkg.pluginEnv = env;
|
||||||
@@ -838,10 +879,10 @@ class TYTD.Downloader {
|
|||||||
var pkg2 = {name,version};
|
var pkg2 = {name,version};
|
||||||
func _close()
|
func _close()
|
||||||
{
|
{
|
||||||
if(pkg2.pluginObject.Server != null && pkg2.pluginObject.Server != undefined)
|
if(TypeIsDefined(pkg2.pluginObject.Server))
|
||||||
{
|
{
|
||||||
var path = /"plugin"/pkg2.pluginName;
|
var path = /"plugin"/pkg2.pluginName;
|
||||||
this.Servers.Unmount(path);
|
this.Servers.Unmount(path.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
pkg2.pluginObject.Close();
|
pkg2.pluginObject.Close();
|
||||||
@@ -908,6 +949,7 @@ class TYTD.Downloader {
|
|||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
var res = Sqlite.Exec(db, "SELECT * FROM subscriptions;");
|
var res = Sqlite.Exec(db, "SELECT * FROM subscriptions;");
|
||||||
|
Sqlite.Close(db);
|
||||||
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
|
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
|
||||||
|
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
@@ -944,7 +986,6 @@ class TYTD.Downloader {
|
|||||||
break;
|
break;
|
||||||
case SubscriptionBell.BellHigh:
|
case SubscriptionBell.BellHigh:
|
||||||
notify = true;
|
notify = true;
|
||||||
break;
|
|
||||||
case SubscriptionBell.DownloadHigh:
|
case SubscriptionBell.DownloadHigh:
|
||||||
downloadRes = Resolution.MKV;
|
downloadRes = Resolution.MKV;
|
||||||
break;
|
break;
|
||||||
@@ -952,7 +993,6 @@ class TYTD.Downloader {
|
|||||||
if(!notify && downloadRes == Resolution.NoDownload) continue;
|
if(!notify && downloadRes == Resolution.NoDownload) continue;
|
||||||
var cid = sub.channelId;
|
var cid = sub.channelId;
|
||||||
|
|
||||||
var newVideos = [];
|
|
||||||
|
|
||||||
each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
|
each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
|
||||||
{
|
{
|
||||||
@@ -960,36 +1000,34 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
|
|
||||||
if(this.GetVideo(videoId) == null)
|
if(this.GetVideo(videoId) == null)
|
||||||
|
{
|
||||||
newVideos.Add(videoId);
|
newVideos.Add(videoId);
|
||||||
|
if(notify)
|
||||||
|
{
|
||||||
|
this.PutVideoInfoIfNotExists(videoId);
|
||||||
|
var res = this.GetVideo(videoId);
|
||||||
|
if(res != null)
|
||||||
|
this.Bell.Invoke(this,{
|
||||||
|
Video = res
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(downloadRes != Resolution.NoDownload)
|
||||||
|
{
|
||||||
|
this.DownloadVideo(videoId,downloadRes);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(notify)
|
|
||||||
{
|
|
||||||
each(var id : newVideos)
|
|
||||||
{
|
|
||||||
this.PutVideoInfoIfNotExists(id);
|
|
||||||
var res = this.GetVideo(id);
|
|
||||||
if(res != null)
|
|
||||||
this.Bell.Invoke(this,{
|
|
||||||
Video = res
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(downloadRes != Resolution.NoDownload)
|
|
||||||
{
|
|
||||||
each(var id : newVideos)
|
|
||||||
{
|
|
||||||
this.DownloadVideo(id,downloadRes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}catch(ex) {
|
}catch(ex) {
|
||||||
try{
|
try{
|
||||||
this.LOG($"Exception caught on playlist thread: {e}");
|
this.LOG($"Exception caught on playlist thread: {ex}");
|
||||||
}catch(ex2){}
|
}catch(ex2){}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1042,7 +1080,7 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
try{
|
try{
|
||||||
this.LOG($"Exception caught on download thread: {e}");
|
this.LOG($"Exception caught on download thread: {ex}");
|
||||||
}catch(ex2){}
|
}catch(ex2){}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1093,8 +1131,26 @@ class TYTD.Downloader {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
try {
|
||||||
var url = $"https://s.ytimg.com/vi/{id}/{res}.jpg";
|
var url = $"https://s.ytimg.com/vi/{id}/{res}.jpg";
|
||||||
Net.Http.DownloadToFile(url,this.Storage, path);
|
const resp = Net.Http.MakeRequest(url,{FollowRedirects=true});
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path,"wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
const bytes = FS.ReadAllBytes(this.Storage,"/Streams/nullthumb.jpg");
|
||||||
|
FS.WriteAllBytes(this.Storage, path, bytes);
|
||||||
|
}
|
||||||
|
resp.Close(); //for other implementations
|
||||||
|
|
||||||
|
}catch(ex) {
|
||||||
|
return /"Streams"/"nullthumb.jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@@ -1240,6 +1296,8 @@ class TYTD.Downloader {
|
|||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_list_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, listName TEXT, videoId TEXT);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_list_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, listName TEXT, videoId TEXT);");
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS plugin_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, extension TEXT, key TEXT, value TEXT, UNIQUE(extension,key) ON CONFLICT REPLACE);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS plugin_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, extension TEXT, key TEXT, value TEXT, UNIQUE(extension,key) ON CONFLICT REPLACE);");
|
||||||
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
|
||||||
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, password_salt TEXT, flags INTEGER);");
|
||||||
|
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key TEXT UNIQUE);");
|
||||||
var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';");
|
var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';");
|
||||||
if(TypeOf(config) == "List" && config.Length>0)
|
if(TypeOf(config) == "List" && config.Length>0)
|
||||||
{
|
{
|
||||||
@@ -1249,6 +1307,10 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.Config.OobeState ??= "oobe";
|
||||||
|
|
||||||
|
|
||||||
Sqlite.Close(db);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
|
|
||||||
@@ -1551,7 +1613,10 @@ class TYTD.Downloader {
|
|||||||
this.RateLimit();
|
this.RateLimit();
|
||||||
var response = Net.Http.MakeRequest(url,requestData);
|
var response = Net.Http.MakeRequest(url,requestData);
|
||||||
if(response.StatusCode != 200) throw "Not success";
|
if(response.StatusCode != 200) throw "Not success";
|
||||||
var data = Json.Decode(response.ReadAsString());
|
const text = response.ReadAsString();
|
||||||
|
var data = Json.Decode(text);
|
||||||
|
|
||||||
|
|
||||||
var cr = data.contents.twoColumnWatchNextResults.playlist.playlist;
|
var cr = data.contents.twoColumnWatchNextResults.playlist.playlist;
|
||||||
if(cr == null || cr == undefined)
|
if(cr == null || cr == undefined)
|
||||||
{
|
{
|
||||||
@@ -1700,11 +1765,219 @@ class TYTD.Downloader {
|
|||||||
this.lastRequest = curRequest;
|
this.lastRequest = curRequest;
|
||||||
this.rlm.Unlock();
|
this.rlm.Unlock();
|
||||||
}
|
}
|
||||||
|
public GetSessionToken(ctx)
|
||||||
|
{
|
||||||
|
var cookie = ctx.RequestHeaders.TryGetFirst("Cookie");
|
||||||
|
if(TypeOf(cookie) == "String")
|
||||||
|
{
|
||||||
|
each(var part : cookie.Split("; "))
|
||||||
|
{
|
||||||
|
if(part.Length > 0)
|
||||||
|
{
|
||||||
|
var cookieKV = part.Split("=",true,2);
|
||||||
|
if(cookieKV.Length == 2 && cookieKV[0] == "Session")
|
||||||
|
{
|
||||||
|
return cookieKV[1];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var auth = ctx.RequestHeaders.TryGetFirst("Authorization");
|
||||||
|
if(TypeOf(auth) == "String")
|
||||||
|
{
|
||||||
|
auth=auth.Split(" ",true,2);
|
||||||
|
if(auth.Length < 2) return null;
|
||||||
|
if(auth[0] != "Bearer") return null;
|
||||||
|
return auth[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public Logout(ctx)
|
||||||
|
{
|
||||||
|
const token = GetSessionToken(ctx);
|
||||||
|
if(TypeIsString(token))
|
||||||
|
{
|
||||||
|
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreateAccount(ctx, username, password, flags)
|
||||||
|
{
|
||||||
|
var loggedIn = this.IsLoggedIn(ctx);
|
||||||
|
if(loggedIn == 0xFFFFFFFE) flags = 0xFFFFFFFE;
|
||||||
|
|
||||||
|
if(UserFlags.CanCreateUsers(loggedIn))
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
|
||||||
|
const salt = Crypto.RandomBytes(32, "TYTD2025");
|
||||||
|
const hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
|
const resp = Sqlite.Exec(db, $"INSERT INTO users (username, password_salt, password_hash, flags) VALUES ({Sqlite.Escape(username)}, {Sqlite.Escape(Crypto.Base64Encode(salt))}, {Sqlite.Escape(Crypto.Base64Encode(hash))}, {flags});");
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
|
||||||
|
if(TypeIsString(resp)) return resp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "You are not authorized to create user accounts";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IsLoggedIn(ctx)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
|
||||||
|
var noAccounts=true;
|
||||||
|
if(TypeOf(res) == "List" && res.Length != 0)
|
||||||
|
{
|
||||||
|
if(res[0].["COUNT(*)"] != "0")
|
||||||
|
noAccounts=false;
|
||||||
|
}
|
||||||
|
if(noAccounts) {
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return 0xFFFFFFFE;
|
||||||
|
}
|
||||||
|
const sessionToken = this.GetSessionToken(ctx);
|
||||||
|
if(TypeIsString(sessionToken))
|
||||||
|
{
|
||||||
|
const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
|
||||||
|
if(TypeIsList(res))
|
||||||
|
each(var item : res)
|
||||||
|
{
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return ParseLong(item.flags) | 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WhoAmI(ctx)
|
||||||
|
{
|
||||||
|
his.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
|
||||||
|
var noAccounts=true;
|
||||||
|
if(TypeOf(res) == "List" && res.Length != 0)
|
||||||
|
{
|
||||||
|
if(res[0].["COUNT(*)"] != "0")
|
||||||
|
noAccounts=false;
|
||||||
|
}
|
||||||
|
if(noAccounts) {
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return { flags = 0xFFFFFFFE, username = "N/A" };
|
||||||
|
}
|
||||||
|
const sessionToken = this.GetSessionToken(ctx);
|
||||||
|
if(TypeIsString(sessionToken))
|
||||||
|
{
|
||||||
|
const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};");
|
||||||
|
|
||||||
|
if(TypeIsList(res))
|
||||||
|
each(var item : res)
|
||||||
|
{
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
item.flags = ParseLong(item.flags);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return { flags = 0, username = "N/A" };
|
||||||
|
}
|
||||||
|
public Passwd(ctx, oldPassword, newPassword, logout)
|
||||||
|
{
|
||||||
|
const whoami = this.WhoAmI(ctx);
|
||||||
|
if(TypeIsDictionary(user) && TypeIsString(item.password_salt))
|
||||||
|
{
|
||||||
|
var salt = Crypto.Base64Decode(item.password_salt);
|
||||||
|
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
|
var hashStr = Crypto.Base64Encode(hash);
|
||||||
|
|
||||||
|
if(item.password_hash == hashStr)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const res = Sqlite.Exec(db, $"UPDATE users SET password_hash = {Sqlite.Escape(Crypto.Base64Encode(hash))}, password_salt = {Sqlite.Escape(Crypto.Base64Encode(salt))} WHERE username = {Sqlite.Escape(item.username)};");
|
||||||
|
if(TypeIsList(res))
|
||||||
|
{
|
||||||
|
if(logout)
|
||||||
|
{
|
||||||
|
Sqlite.Exec(db, $"DELETE FROM sessions WHERE accountId = {Sqlite.Escape(res.accountId)};");
|
||||||
|
}
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return {success=true};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return {success=false, reason = res};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public Login(username, password)
|
||||||
|
{
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const db = this.OpenDB();
|
||||||
|
const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};");
|
||||||
|
if(TypeIsList(user))
|
||||||
|
{
|
||||||
|
each(var item : user)
|
||||||
|
{
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
|
||||||
|
var salt = Crypto.Base64Decode(item.password_salt);
|
||||||
|
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
|
var hashStr = Crypto.Base64Encode(hash);
|
||||||
|
|
||||||
|
if(item.password_hash == hashStr)
|
||||||
|
{
|
||||||
|
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
|
||||||
|
this.Mutex.Lock();
|
||||||
|
const dbCon = this.OpenDB();
|
||||||
|
Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({item.id},{Sqlite.Escape(rand)});");
|
||||||
|
Sqlite.Close(dbCon);
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return rand;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Mutex.Unlock();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
/^
|
/^
|
||||||
Send the database as http response
|
Send the database as http response
|
||||||
^/
|
^/
|
||||||
public SendDatabase(ctx)
|
public SendDatabase(ctx)
|
||||||
{
|
{
|
||||||
|
if(UserFlags.CanDownloadDB(this.IsLoggedIn(ctx)))
|
||||||
|
{
|
||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
try {
|
try {
|
||||||
var strm = FS.Local.OpenFile(this.DatabaseDirectory/"tytd.db","rb");
|
var strm = FS.Local.OpenFile(this.DatabaseDirectory/"tytd.db","rb");
|
||||||
@@ -1714,5 +1987,11 @@ class TYTD.Downloader {
|
|||||||
Console.WriteLine($"ERROR: {ex}");
|
Console.WriteLine($"ERROR: {ex}");
|
||||||
}
|
}
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class TYTD.AOVideoDownload : IVideoDownload {
|
|||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
this.info.Title = req.videoDetails.title;
|
this.info.Title = req.videoDetails.title;
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Audio");
|
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Audio");
|
||||||
|
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Audio");
|
||||||
this.info.Channel = req.videoDetails.author;
|
this.info.Channel = req.videoDetails.author;
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
this.info.ChannelId = req.videoDetails.channelId;
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ class TYTD.AOVideoDownload : IVideoDownload {
|
|||||||
}
|
}
|
||||||
public Start()
|
public Start()
|
||||||
{
|
{
|
||||||
|
if(this.done) return;
|
||||||
for(var i = 0; i < 5; i++)
|
for(var i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
var req = {
|
var req = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload {
|
|||||||
|
|
||||||
private done=false;
|
private done=false;
|
||||||
|
|
||||||
|
|
||||||
public NoConvertVideoDownload(id)
|
public NoConvertVideoDownload(id)
|
||||||
{
|
{
|
||||||
this.info = {
|
this.info = {
|
||||||
@@ -36,6 +37,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload {
|
|||||||
|
|
||||||
this.info.Title = req.videoDetails.title;
|
this.info.Title = req.videoDetails.title;
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video/Audio");
|
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video/Audio");
|
||||||
|
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Video/Audio");
|
||||||
this.info.Channel = req.videoDetails.author;
|
this.info.Channel = req.videoDetails.author;
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
this.info.ChannelId = req.videoDetails.channelId;
|
||||||
|
|
||||||
@@ -220,5 +222,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.progress(1.0);
|
this.progress(1.0);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ class TYTD.SDVideoDownload : IVideoDownload {
|
|||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
this.info.Title = req.videoDetails.title;
|
this.info.Title = req.videoDetails.title;
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} LowVideo");
|
tytd.LOG($"Downloading: {this.info.Title} with id: {id} LowVideo");
|
||||||
|
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} LowVideo");
|
||||||
this.info.Channel = req.videoDetails.author;
|
this.info.Channel = req.videoDetails.author;
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
this.info.ChannelId = req.videoDetails.channelId;
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ class TYTD.SDVideoDownload : IVideoDownload {
|
|||||||
}
|
}
|
||||||
public Start()
|
public Start()
|
||||||
{
|
{
|
||||||
|
if(this.done) return;
|
||||||
for(var i = 0; i < 5; i++)
|
for(var i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
var req = {
|
var req = {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class TYTD.TranscodeAudio : IVideoDownload {
|
|||||||
private ncv;
|
private ncv;
|
||||||
private tytd;
|
private tytd;
|
||||||
private ext;
|
private ext;
|
||||||
|
private done;
|
||||||
|
|
||||||
public TranscodeAudio(id,ext)
|
public TranscodeAudio(id,ext)
|
||||||
{
|
{
|
||||||
@@ -30,7 +31,7 @@ class TYTD.TranscodeAudio : IVideoDownload {
|
|||||||
public Start()
|
public Start()
|
||||||
{
|
{
|
||||||
var id = this.id;
|
var id = this.id;
|
||||||
this.ncv.Start();
|
this.mcv.Start();
|
||||||
|
|
||||||
var p = new Process();
|
var p = new Process();
|
||||||
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class TYTD.TranscodeVideo : IVideoDownload {
|
|||||||
public Start()
|
public Start()
|
||||||
{
|
{
|
||||||
var id = this.id;
|
var id = this.id;
|
||||||
this.ncv.Start();
|
this.mcv.Start();
|
||||||
|
|
||||||
var p = new Process();
|
var p = new Process();
|
||||||
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
p.FileName = Env.GetRealExecutablePath("ffmpeg").ToString();
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class TYTD.VOVideoDownload : IVideoDownload {
|
|||||||
this.info.Channel = req.videoDetails.author;
|
this.info.Channel = req.videoDetails.author;
|
||||||
this.info.ChannelId = req.videoDetails.channelId;
|
this.info.ChannelId = req.videoDetails.channelId;
|
||||||
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
||||||
|
Console.WriteLine($"Downloading: {this.info.Title} with id: {id} Highest Video");
|
||||||
|
|
||||||
this.tytd.PutVideoInfo(req.videoDetails);
|
this.tytd.PutVideoInfo(req.videoDetails);
|
||||||
|
|
||||||
var width = 0;
|
var width = 0;
|
||||||
@@ -63,6 +65,7 @@ class TYTD.VOVideoDownload : IVideoDownload {
|
|||||||
}
|
}
|
||||||
public Start()
|
public Start()
|
||||||
{
|
{
|
||||||
|
if(this.done) return;
|
||||||
var req = {
|
var req = {
|
||||||
FollowRedirects = true,
|
FollowRedirects = true,
|
||||||
RequestHeaders = [
|
RequestHeaders = [
|
||||||
|
|||||||
Reference in New Issue
Block a user