5 Commits
Author SHA1 Message Date
tesses50 c993977e65 Target crosslang v0.0.11, add channel tags, descriptions and thumbnail downloading
Build and Deploy on Tag / 🔨 Build (push) Successful in 22s
2026-09-30 04:03:51 -05:00
tesses50 f73c45bb48 Target crosslang v0.0.10, add /api/v1/progress_ex.json
Build and Deploy on Tag / 🔨 Build (push) Successful in 22s
2026-09-10 03:29:30 -05:00
tesses50 dc270e6785 Target crosslang v0.0.10, add /api/v1/progress_ex.json
Build and Deploy on Tag / 🔨 Build (push) Successful in 20s
2026-09-10 03:15:05 -05:00
tesses50 456577f2f7 Fix for crosslang v0.0.10
Build and Deploy on Tag / 🔨 Build (push) Has been cancelled
2026-09-02 12:32:59 -05:00
tesses50 027f601887 Fix for crosslang v0.0.10 2026-09-02 12:31:31 -05:00
33 changed files with 470 additions and 178 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ env:
jobs: jobs:
build-packages: build-packages:
name: "🔨 Build" name: "🔨 Build"
runs-on: fw13-ubuntu-latest runs-on: msi-walmart
container: container:
image: "git.tesses.org/tesses50/linux-x64:latest" image: "git.tesses.org/tesses50/linux-x64:latest"
@@ -56,4 +56,4 @@ jobs:
+2 -1
View File
@@ -5,4 +5,5 @@ TYTD
artifacts artifacts
*.pkg.tar.zst *.pkg.tar.zst
*.deb *.deb
*.rpm *.rpm
tmp/
+1 -1
View File
@@ -11,4 +11,4 @@ ENV TYTDDIR=/data
EXPOSE 3255 EXPOSE 3255
ENTRYPOINT ["crossvm","/app/tytd2025.crvm","--port=3255"] ENTRYPOINT ["crosslang","vm","/app/tytd2025.crvm","--port=3255"]
+4 -4
View File
@@ -1,5 +1,5 @@
# TYTD 2025 # TYTD 2025
My web based YouTube downloader that I created in 2025 writen in [CrossLang](https://crosslang.tesseslanguage.com/), my own language My web based YouTube downloader that I created in 2025 written in [CrossLang](https://crosslang.tesseslanguage.com/), my own language
![The Image](https://crosslang.tesseslanguage.com/images/tytdscreenshot.jpg) ![The Image](https://crosslang.tesseslanguage.com/images/tytdscreenshot.jpg)
@@ -14,8 +14,8 @@ My web based YouTube downloader that I created in 2025 writen in [CrossLang](htt
- Search and browse your downloaded videos, playlists, or channels (the search is very basic though) - Search and browse your downloaded videos, playlists, or channels (the search is very basic though)
- User accounts - User accounts
- Videos can be tagged based on your downloader's TYTD tag (to determine which instance downloaded it) - 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 - Plugins from [CPKG](https://cpkg.tesseslanguage.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) - Can download YouTube videos either Low quality (but doesn't) require [ffmpeg](https://ffmpeg.org/), MP3, FLAC or AMV (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) - 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)
# What this project uses (attribution) # What this project uses (attribution)
@@ -37,7 +37,7 @@ Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
from source: from source:
```bash ```bash
git clone https://onedev.site.tesses.net/tytd2025 git clone https://git.tesses.org/tesses50/tytd2025
cd tytd2025/Tesses.YouTubeDownloader.Server cd tytd2025/Tesses.YouTubeDownloader.Server
crosslang install-webapp crosslang install-webapp
``` ```
@@ -28,6 +28,6 @@
"template_icon": "icon.png" "template_icon": "icon.png"
}, },
"name": "Tesses.YouTubeDownloader.PluginTemplate", "name": "Tesses.YouTubeDownloader.PluginTemplate",
"version": "1.0.0.0-dev", "version": "1.0.1.0-dev",
"icon": "icon.png" "icon": "icon.png"
} }
+2 -2
View File
@@ -14,6 +14,6 @@
"project_dependencies": [ "project_dependencies": [
"..\/Tesses.YouTubeDownloader" "..\/Tesses.YouTubeDownloader"
], ],
"version": "1.0.0.0-dev", "version": "1.0.2.0-dev",
"compTime": "secure" "compTime": "secure"
} }
@@ -1,6 +1,6 @@
[ [
{ {
"route": "/api/v1/video.json", "route": "/api/v1/video",
"method": "GET", "method": "GET",
"queryParams": [ "queryParams": [
{ {
@@ -12,7 +12,7 @@
"description": "Returns json object like https://tesses.net/apps/tytd/2025/video-example.json or null if video doesn't exist" "description": "Returns json object like https://tesses.net/apps/tytd/2025/video-example.json or null if video doesn't exist"
}, },
{ {
"route": "/api/v1/downloads.json", "route": "/api/v1/downloads",
"method": "GET", "method": "GET",
"queryParams": [ "queryParams": [
{ {
@@ -83,4 +83,9 @@ body.dark {
top: 50vh; top: 50vh;
left: 50vw; left: 50vw;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
}
img.circle.supersized {
width: 160px; /* or whatever size you need */
height: 160px;
} }
@@ -49,7 +49,7 @@ func Components.DownloadedChannel(item)
{ {
var res = <div class="row"> var res = <div class="row">
<div class="min"> <div class="min">
<img src={$"./api/v1/channel-thumbnail?id={Net.Http.UrlEncode(item.channelId)}"} width="144" width="120"> <img class="circle supersized" src={$"./api/v1/channel-thumbnail?id={Net.Http.UrlEncode(item.channelId)}"}>
</div> </div>
<div class="max"> <div class="max">
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} hx-target="body" hx-push-url="true">{item.title}</a> <a hx-get={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} hx-target="body" hx-push-url="true">{item.title}</a>
@@ -1,12 +1,12 @@
func Components.Progress(tytd) func Components.Progress(tytd)
{ {
var vid = tytd.CurrentVideo; var progress = tytd.GetProgress();
var html = <div hx-trigger="every 4500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML"> var html = <div hx-trigger="every 4500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
<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> <h2><a hx-target="body" hx-push-url="true" hx-get={$"./watch?v={Net.Http.UrlEncode(progress.CurrentVideo.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(progress.CurrentVideo.VideoId)}"}>{progress.CurrentVideo.Title}</a></h2>
<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> <h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(progress.CurrentVideo.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(progress.CurrentVideo.ChannelId)}"}>{progress.CurrentVideo.Channel}</a></h4>
<progress class="wavy light-green-text" value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress> <progress class="wavy light-green-text" value={progress.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
</div>; </div>;
return html; return html;
} }
@@ -40,6 +40,13 @@ func Components.Shell(title, html, page, $mypage)
classStr="", classStr="",
badge="" badge=""
}, },
{
text = "Subscriptions",
icon = "subscriptions",
href = (/"subscriptions").MakeRelative(mypage).ToString(),
classStr="",
badge=""
},
{ {
text = "Plugins", text = "Plugins",
icon = "extension", icon = "extension",
@@ -68,7 +75,7 @@ func Components.Shell(title, html, page, $mypage)
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TYTD - {title}</title> <title>{title} - TYTD2025</title>
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}> <link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}> <link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
<link rel="manifest" href={(/"site.webmanifest").MakeRelative(mypage).ToString()}> <link rel="manifest" href={(/"site.webmanifest").MakeRelative(mypage).ToString()}>
@@ -1,3 +1,34 @@
func Components.SubscribePageBell(tytd, id)
{
var bell = tytd.GetSubscriptionBell(id);
var bellLogo = "notifications_off";
switch(bell)
{
case SubscriptionBell.DownloadLow:
case SubscriptionBell.DownloadHigh:
bellLogo = "download";
break;
case SubscriptionBell.Bell:
case SubscriptionBell.BellLow:
case SubscriptionBell.BellHigh:
bellLogo="notifications_active";
break;
}
<return>
<div class="min" id={$"subscribe-btn-{Net.Http.HtmlEncode(id)}"}>
<button class="circle">
<i>{bellLogo}</i>
<menu class="left no-wrap">
<each(var b : SubscriptionBell.Bells)>
<li hx-post="./subscribe" hx-swap="outerHTML" hx-target={$"#subscribe-btn-{Net.Http.HtmlEncode(id)}"} hx-vals={Json.Encode({id,type="bell-subscriptions-page",bell=b.value})}>{b.name}</li>
</each>
</menu>
</button>
</div>
</return>
}
func Components.Subscribe(tytd,id) func Components.Subscribe(tytd,id)
{ {
var bell = tytd.GetSubscriptionBell(id); var bell = tytd.GetSubscriptionBell(id);
+170 -79
View File
@@ -139,8 +139,8 @@ class TYTDApp {
{ {
var incorrect=false; var incorrect=false;
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/"; const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
const username = ctx.QueryParams.TryGetFirst("username") ?? ""; const username = ctx.BodyParams.TryGetFirst("username") ?? "";
const password = ctx.QueryParams.TryGetFirst("password") ?? ""; const password = ctx.BodyParams.TryGetFirst("password") ?? "";
@@ -218,16 +218,16 @@ class TYTDApp {
{ {
if(ctx.Method == "POST") if(ctx.Method == "POST")
{ {
const from = ctx.QueryParams.TryGetFirstInt("from"); const from = ctx.BodyParams.TryGetFirstInt("from");
switch(from) switch(from)
{ {
case 1: case 1:
{ {
const enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins"); const enablePlugins = ctx.BodyParams.GetFirstBoolean("enablePlugins");
const tag = ctx.QueryParams.TryGetFirst("tag") ?? "UnknownPC"; const tag = ctx.BodyParams.TryGetFirst("tag") ?? "UnknownPC";
const pollHours = ctx.QueryParams.TryGetFirstInt("pollHours") ?? 3; const pollHours = ctx.BodyParams.TryGetFirstInt("pollHours") ?? 3;
const pollMinutes = ctx.QueryParams.TryGetFirstInt("pollMinutes") ?? 0; const pollMinutes = ctx.BodyParams.TryGetFirstInt("pollMinutes") ?? 0;
const pollSeconds = ctx.QueryParams.TryGetFirstInt("pollSeconds") ?? 0; const pollSeconds = ctx.BodyParams.TryGetFirstInt("pollSeconds") ?? 0;
this.OOBE_STATE.tag = tag; this.OOBE_STATE.tag = tag;
@@ -248,7 +248,7 @@ class TYTDApp {
case 3: case 3:
{ {
const createUser = (ctx.QueryParams.TryGetFirst("user") ?? "no")=="yes"; const createUser = (ctx.BodyParams.TryGetFirst("user") ?? "no")=="yes";
var seconds = this.OOBE_STATE.pollSeconds; var seconds = this.OOBE_STATE.pollSeconds;
seconds += this.OOBE_STATE.pollMinutes * 60; seconds += this.OOBE_STATE.pollMinutes * 60;
@@ -386,18 +386,38 @@ class TYTDApp {
} }
else if(ctx.Path == "/passwd") else if(ctx.Path == "/passwd")
{ {
var incorrect=false; var result = {success=true};
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
const newpassword = ctx.QueryParams.TryGetFirst("newpassword") ?? "";
const confirm = ctx.QueryParams.TryGetFirst("newpassword") ?? ""; const redirect = ctx.BodyParams.TryGetFirst("redirect") ?? "/login";
const password = ctx.BodyParams.TryGetFirst("password") ?? "";
const newpassword = ctx.BodyParams.TryGetFirst("newpassword") ?? "";
const confirm = ctx.BodyParams.TryGetFirst("confirm") ?? "";
const logout = ctx.BodyParams.GetFirstBoolean("logout");
if(ctx.Method == "POST") if(ctx.Method == "POST")
{ {
this.TYTD.Passwd(); //public Passwd(ctx, oldPassword, newPassword, logout)
if(password != newpassword)
{
result = {success = false, reason = "Passwords do not match"};
}
else
result = this.TYTD.Passwd(ctx, password, newpassword, logout);
if(result.success)
{
ctx.StatusCode = 307;
ctx.SendRedirect(redirect);
return true;
}
} }
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, result.success ? null : result.reason));
return true;
} }
else if(ctx.Path == "/newuser") else if(ctx.Path == "/newuser")
@@ -405,14 +425,14 @@ class TYTDApp {
var error = null; var error = null;
if(ctx.Method == "POST") if(ctx.Method == "POST")
{ {
const username = ctx.QueryParams.TryGetFirst("username"); const username = ctx.BodyParams.TryGetFirst("username");
const password = ctx.QueryParams.TryGetFirst("password"); const password = ctx.BodyParams.TryGetFirst("password");
const confirm = ctx.QueryParams.TryGetFirst("confirm"); const confirm = ctx.BodyParams.TryGetFirst("confirm");
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin"); const isAdmin = ctx.BodyParams.GetFirstBoolean("isAdmin");
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins"); const canUsePlugins = ctx.BodyParams.GetFirstBoolean("canUsePlugins");
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins"); const canManagePlugins = ctx.BodyParams.GetFirstBoolean("canManagePlugins");
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB"); const canDownloadDB = ctx.BodyParams.GetFirstBoolean("canDownloadDB");
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/";
var flags = 0; var flags = 0;
if(isAdmin) flags |= UserFlags.AdminFlag; if(isAdmin) flags |= UserFlags.AdminFlag;
if(canUsePlugins) flags |= UserFlags.PluginFlag; if(canUsePlugins) flags |= UserFlags.PluginFlag;
@@ -434,12 +454,6 @@ class TYTDApp {
else { else {
error = this.TYTD.CreateAccount(ctx, username, password, flags); 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)); ctx.WithMimeType("text/html").SendText(Pages.NewUser(error));
return true; return true;
@@ -526,14 +540,28 @@ class TYTDApp {
} }
return false; return false;
} }
else if(ctx.Path == "/api/v1/progress.json") else if(ctx.Path == "/api/v1/progress_ex" || ctx.Path == "/api/v1/progress_ex.json")
{ {
var jo = { ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgressEx());
CurrentVideoProgress = this.TYTD.CurrentVideoProgress, return true;
CurrentVideo = this.TYTD.CurrentVideo }
else if(ctx.Path == "/api/v1/progress" || ctx.Path == "/api/v1/progress.json")
}; {
ctx.WithMimeType("application/json").SendJson(jo); ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgress());
return true;
}
else if(ctx.Path == "/api/v1/server")
{
ctx.WithMimeType("application/json").SendJson(
{
Service = "tytd2025",
TYTD = {
CrossLangVersion = VM.RuntimeVersion.ToString(),
TYTDTag = this.TYTD.Config.TYTDTag,
DownloaderVersion = Resolution.getResolutions.File.Version.ToString()
}
}
);
return true; return true;
} }
else if(ctx.Path == "/api/v1/add") else if(ctx.Path == "/api/v1/add")
@@ -550,14 +578,14 @@ class TYTDApp {
return true; return true;
} }
} }
else if(ctx.Path == "/api/v1/video.json") else if(ctx.Path == "/api/v1/video" || ctx.Path == "/api/v1/video.json")
{ {
var id = ctx.QueryParams.TryGetFirst("v"); var id = ctx.QueryParams.TryGetFirst("v");
if(TypeOf(id)!="String") id = ""; if(TypeOf(id)!="String") id = "";
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetVideo(id)); ctx.WithMimeType("application/json").SendJson(this.TYTD.GetVideo(id));
return true; return true;
} }
else if(ctx.Path == "/api/v1/playlist.json") else if(ctx.Path == "/api/v1/playlist" || ctx.Path == "/api/v1/playlist.json")
{ {
var id = ctx.QueryParams.TryGetFirst("id"); var id = ctx.QueryParams.TryGetFirst("id");
var page = ctx.QueryParams.TryGetFirstInt("page"); var page = ctx.QueryParams.TryGetFirstInt("page");
@@ -568,7 +596,7 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetPlaylistContents(id,page-1,count)); ctx.WithMimeType("application/json").SendJson(this.TYTD.GetPlaylistContents(id,page-1,count));
return true; return true;
} }
else if(ctx.Path == "/api/v1/channel.json") else if(ctx.Path == "/api/v1/channel" || ctx.Path == "/api/v1/channel.json")
{ {
var id = ctx.QueryParams.TryGetFirst("id"); var id = ctx.QueryParams.TryGetFirst("id");
var page = ctx.QueryParams.TryGetFirstInt("page"); var page = ctx.QueryParams.TryGetFirstInt("page");
@@ -579,9 +607,9 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count)); ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
return true; return true;
} }
else if(ctx.Path == "/api/v1/queue.json") else if(ctx.Path == "/api/v1/queue" || ctx.Path == "/api/v1/queue.json")
{ {
if(ctx.Method == "GET") if(ctx.Method == "GET" || ctx.Method == "HEAD")
{ {
var page = ctx.QueryParams.TryGetFirstInt("page"); var page = ctx.QueryParams.TryGetFirstInt("page");
var count = ctx.QueryParams.TryGetFirstInt("count"); var count = ctx.QueryParams.TryGetFirstInt("count");
@@ -594,7 +622,33 @@ class TYTDApp {
return true; return true;
} }
} }
else if(ctx.Path == "/api/v1/downloads.json") else if(ctx.Path == "/api/v1/subscriptions")
{
if(ctx.Method == "GET" || ctx.Method == "HEAD")
{
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetSubscriptions());
}
else if(ctx.Method == "POST")
{
const json = ctx.ParseJson();
// bell: null => unsubscribe; absent => default to BellLow
const bell = json.bell == undefined ? SubscriptionBell.BellLow : json.bell;
const id = json.id ?? "";
this.TYTD.SetSubscriptionBell(id, bell);
ctx.StatusCode = 204;
ctx.WriteHeaders();
return true;
}
}
else if(ctx.Path == "/api/v1/downloads" || ctx.Path == "/api/v1/downloads.json")
{ {
var q = ctx.QueryParams.TryGetFirst("q"); var q = ctx.QueryParams.TryGetFirst("q");
var type = ctx.QueryParams.TryGetFirst("type"); var type = ctx.QueryParams.TryGetFirst("type");
@@ -647,7 +701,7 @@ class TYTDApp {
} }
} }
else if(ctx.Path == "/api/v1/manifest.json") else if(ctx.Path == "/api/v1/manifest" || ctx.Path == "/api/v1/manifest.json")
{ {
const v = ctx.QueryParams.TryGetFirst("v"); const v = ctx.QueryParams.TryGetFirst("v");
if(TypeIsString(v)) if(TypeIsString(v))
@@ -670,7 +724,7 @@ class TYTDApp {
name= name=
*/ */
if(ctx.Method == "GET") if(ctx.Method == "GET" || ctx.Method == "HEAD")
{ {
var name = ctx.QueryParams.TryGetFirst("name"); var name = ctx.QueryParams.TryGetFirst("name");
if(TypeOf(name) == "String") if(TypeOf(name) == "String")
@@ -702,7 +756,23 @@ class TYTDApp {
{ {
var idHas = TypeOf(j.id) == "String" && j.id != ""; var idHas = TypeOf(j.id) == "String" && j.id != "";
var descHas = TypeOf(j.description) == "String" && j.description != ""; var descHas = TypeOf(j.description) == "String" && j.description != "";
if(idHas && descHas) var mode = TypeIsString(j.mode) ? j.mode : "add";
if(mode == "delete")
{
if(descHas)
{
ctx.WithMimeType("application/json").SendJson({success=false,reason="You provided a description which doesn't make sense when deleting something."});
}
else if(idHas)
{
this.TYTD.RemoveFromPersonalList(j.name,j.id);
}
else {
this.TYTD.RemovePersonalList(j.name);
}
}
else if(idHas && descHas)
{ {
ctx.WithMimeType("application/json").SendJson({success=false,reason="You provided both a description (to set the list description) and id (to add a item) (you can only provide one)"}); ctx.WithMimeType("application/json").SendJson({success=false,reason="You provided both a description (to set the list description) and id (to add a item) (you can only provide one)"});
} }
@@ -751,12 +821,17 @@ class TYTDApp {
} }
} }
else if(ctx.Path == "/subscriptions")
{
ctx.WithMimeType("text/html").SendText(Pages.Subscriptions(this.TYTD));
return true;
}
else if(ctx.Path == "/subscribe") else if(ctx.Path == "/subscribe")
{ {
var id = ctx.QueryParams.TryGetFirst("id"); var id = ctx.BodyParams.TryGetFirst("id");
var bell = ctx.QueryParams.TryGetFirst("bell"); var bell = ctx.BodyParams.TryGetFirst("bell");
var type = ctx.QueryParams.TryGetFirst("type"); var type = ctx.BodyParams.TryGetFirst("type");
switch(type) switch(type)
{ {
@@ -767,9 +842,17 @@ class TYTDApp {
case "unsubscribe": case "unsubscribe":
this.TYTD.SetSubscriptionBell(id, null); this.TYTD.SetSubscriptionBell(id, null);
break; break;
case "unsubscribe-subscriptions-page":
this.TYTD.SetSubscriptionBell(id, null);
ctx.WithHeader("HX-Location", "/subscriptions").WriteHeaders();
return true;
case "bell": case "bell":
this.TYTD.SetSubscriptionBell(id, bell); this.TYTD.SetSubscriptionBell(id, bell);
break; break;
case "bell-subscriptions-page":
this.TYTD.SetSubscriptionBell(id, bell);
ctx.WithMimeType("text/html").SendText(Components.SubscribePageBell(this.TYTD,id));
return true;
} }
ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id)); ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id));
return true; return true;
@@ -783,14 +866,14 @@ class TYTDApp {
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");
if(ctx.Method == "GET") if(ctx.Method == "GET" || ctx.Method == "HEAD")
{ {
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,true)); ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,true));
return true; return true;
} }
else if(ctx.Method == "POST") else if(ctx.Method == "POST")
{ {
var description =ctx.QueryParams.TryGetFirst("description"); var description =ctx.BodyParams.TryGetFirst("description");
this.TYTD.SetPersonalListDescription(name,description); this.TYTD.SetPersonalListDescription(name,description);
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,false)); ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,false));
@@ -848,10 +931,10 @@ class TYTDApp {
{ {
if(whoami.username != user) if(whoami.username != user)
{ {
const isAdmin = ctx.QueryParams.GetFirstBoolean("isAdmin"); const isAdmin = ctx.BodyParams.GetFirstBoolean("isAdmin");
const canUsePlugins = ctx.QueryParams.GetFirstBoolean("canUsePlugins"); const canUsePlugins = ctx.BodyParams.GetFirstBoolean("canUsePlugins");
const canManagePlugins = ctx.QueryParams.GetFirstBoolean("canManagePlugins"); const canManagePlugins = ctx.BodyParams.GetFirstBoolean("canManagePlugins");
const canDownloadDB = ctx.QueryParams.GetFirstBoolean("canDownloadDB"); const canDownloadDB = ctx.BodyParams.GetFirstBoolean("canDownloadDB");
var flags = 0; var flags = 0;
if(isAdmin) flags |= UserFlags.AdminFlag; if(isAdmin) flags |= UserFlags.AdminFlag;
if(canUsePlugins) flags |= UserFlags.PluginFlag; if(canUsePlugins) flags |= UserFlags.PluginFlag;
@@ -926,11 +1009,11 @@ class TYTDApp {
tag tag
enablePlugins enablePlugins
*/ */
var enablePlugins = ctx.QueryParams.GetFirstBoolean("enablePlugins"); var enablePlugins = ctx.BodyParams.GetFirstBoolean("enablePlugins");
var tag = ctx.QueryParams.TryGetFirst("tag"); var tag = ctx.BodyParams.TryGetFirst("tag");
var hours = ctx.QueryParams.TryGetFirstInt("hours"); var hours = ctx.BodyParams.TryGetFirstInt("hours");
var minutes = ctx.QueryParams.TryGetFirstInt("minutes"); var minutes = ctx.BodyParams.TryGetFirstInt("minutes");
var seconds = ctx.QueryParams.TryGetFirstInt("seconds"); var seconds = ctx.BodyParams.TryGetFirstInt("seconds");
if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long") if(TypeOf(tag) == "String" && TypeOf(hours) == "Long" && TypeOf(minutes) == "Long" && TypeOf(seconds) == "Long")
{ {
@@ -953,32 +1036,40 @@ class TYTDApp {
else if(ctx.Path == "/watch_videos") else if(ctx.Path == "/watch_videos")
{ {
const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
if(TypeIsString(video_ids)) if(ctx.Method=="GET" || ctx.Method == "HEAD")
{ {
if(ctx.Method=="GET") const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
if(TypeIsString(video_ids))
{ {
ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids)); ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids));
return true; return true;
} }
if(ctx.Method=="POST") }
if(ctx.Method=="POST")
{
const video_ids = ctx.BodyParams.TryGetFirst("video_ids");
if(TypeIsString(video_ids))
{ {
const name = ctx.QueryParams.TryGetFirst("name"); const name = ctx.BodyParams.TryGetFirst("name");
if(TypeIsString(name)) if(TypeIsString(name))
{ {
const nameParts=video_ids.Split(","); const nameParts=video_ids.Split(",");
Console.WriteLine(nameParts); //Console.WriteLine(nameParts);
each(var item : nameParts) each(var item : nameParts)
{ {
this.TYTD.AddToPersonalList(name,item); this.TYTD.AddToPersonalList(name,item);
} }
Console.WriteLine(name); //Console.WriteLine(name);
ctx.SendRedirect($"/list?name={Net.Http.UrlEncode(name)}",303); ctx.SendRedirect($"/list?name={Net.Http.UrlEncode(name)}",303);
return true; return true;
} }
} }
} }
} }
else if(ctx.Path == "/playlist") else if(ctx.Path == "/playlist")
{ {
@@ -992,9 +1083,9 @@ class TYTDApp {
} }
else if(ctx.Path == "/add") else if(ctx.Path == "/add")
{ {
var url = ctx.QueryParams.TryGetFirst("url"); var url = ctx.BodyParams.TryGetFirst("url");
var action =ctx.QueryParams.TryGetFirst("action"); var action =ctx.BodyParams.TryGetFirst("action");
var res = ctx.QueryParams.TryGetFirst("res"); var res = ctx.BodyParams.TryGetFirst("res");
if(action == "add") if(action == "add")
{ {
this.TYTD.DownloadItem(url,res); this.TYTD.DownloadItem(url,res);
@@ -1022,9 +1113,9 @@ class TYTDApp {
} }
else if(ctx.Path == "/add-video") else if(ctx.Path == "/add-video")
{ {
var url = ctx.QueryParams.TryGetFirst("url"); var url = ctx.BodyParams.TryGetFirst("url");
var action =ctx.QueryParams.TryGetFirst("action"); var action =ctx.BodyParams.TryGetFirst("action");
var res = ctx.QueryParams.TryGetFirst("res"); var res = ctx.BodyParams.TryGetFirst("res");
if(action == "add") if(action == "add")
{ {
this.TYTD.DownloadItem(url,res); this.TYTD.DownloadItem(url,res);
@@ -1045,10 +1136,10 @@ class TYTDApp {
} }
else if(ctx.Path == "/add-to-list") else if(ctx.Path == "/add-to-list")
{ {
var id = ctx.QueryParams.TryGetFirst("id"); var id = ctx.BodyParams.TryGetFirst("id");
if(TypeOf(id) != "String") id = ""; if(TypeOf(id) != "String") id = "";
var name = ctx.QueryParams.TryGetFirst("name"); var name = ctx.BodyParams.TryGetFirst("name");
if(TypeOf(name) != "String") name = ""; if(TypeOf(name) != "String") name = "";
if(name == "") if(name == "")
@@ -1089,9 +1180,9 @@ class TYTDApp {
{ {
if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false; if(!UserFlags.CanManagePlugins(this.TYTD.IsLoggedIn(ctx))) return false;
var data = { var data = {
name = ctx.QueryParams.TryGetFirst("name"), name = ctx.BodyParams.TryGetFirst("name"),
version = ctx.QueryParams.TryGetFirst("version"), version = ctx.BodyParams.TryGetFirst("version"),
action = ctx.QueryParams.TryGetFirst("action") action = ctx.BodyParams.TryGetFirst("action")
}; };
var mustConfirm = false; var mustConfirm = false;
@@ -1239,4 +1330,4 @@ func main(args)
Net.Http.ListenSimpleWithLoop(res,3255); Net.Http.ListenSimpleWithLoop(res,3255);
res.Close(); res.Close();
return 0; return 0;
} }
@@ -72,5 +72,5 @@ func Pages.Admin(tytd,ctx)
</form>; </form>;
return Components.Shell("Admin Settings",html ,4); return Components.Shell("Admin Settings",html ,5);
} }
@@ -8,5 +8,5 @@ func Pages.Api()
</a> </a>
</ul> </ul>
</null>; </null>;
return Components.Shell("Api",html,5); return Components.Shell("Api",html,6);
} }
@@ -21,7 +21,7 @@ func Pages.ApiV1()
</fieldset> </fieldset>
</each> </each>
</null>; </null>;
return Components.Shell("Api V1",html,5); return Components.Shell("Api V1",html,6);
} }
var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString()); var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString());
@@ -1,14 +1,14 @@
func Pages.ChangePassword(redirect,incorrect) func Pages.ChangePassword(redirect,error)
{ {
const html = <null> const html = <null>
<if(incorrect)> <if(TypeIsString(error))>
<true> <true>
<blockquote> <blockquote>
<h5>The password could not be changed</h5> <h5>{error}</h5>
</blockquote> </blockquote>
</true> </true>
</if> </if>
@@ -45,5 +45,5 @@ const html = <null>
</form> </form>
</null>; </null>;
return Components.Shell("Change your password",html,4); return Components.Shell("Change your password",html,5);
} }
@@ -8,13 +8,21 @@ func Pages.ChannelInfo(tytd,ctx)
var html = <h1>Could not find channel</h1>; var html = <h1>Could not find channel</h1>;
var title = "N/A"; var title = "N/A";
if(TypeOf(id) == "String")
{
id = TYTD.GetChannelId(id); id = TYTD.GetChannelId(id);
if(TypeIsString(id))
{
var res = tytd.GetChannelContents(id,page,20); var res = tytd.GetChannelContents(id,page,20);
html = <null> html = <null>
<div class="row"> <div class="row">
<div class="min">
<img class="circle supersized" src={$"./api/v1/channel-thumbnail?id={Net.Http.UrlEncode(id)}"} width="144">
</div>
<div class="max"> <div class="max">
<h1>{res.authorName}</h1> <h1>{res.authorName}</h1>
</div> </div>
@@ -22,6 +30,11 @@ func Pages.ChannelInfo(tytd,ctx)
<raw(Components.Subscribe(tytd,id))> <raw(Components.Subscribe(tytd,id))>
</div> </div>
</div> </div>
<details>
<summary>Description <i>expand_more</i></summary>
<plink(res.description)>
</details>
<each(var item : res.items)> <each(var item : res.items)>
<raw(Components.DownloadedVideo(item))> <raw(Components.DownloadedVideo(item))>
</each> </each>
@@ -35,5 +48,5 @@ func Pages.ChannelInfo(tytd,ctx)
title = res.authorName; title = res.authorName;
} }
return Components.Shell($"Channel {title}",html,2); return Components.Shell(title,html,2);
} }
@@ -38,7 +38,7 @@ func Pages.EditUserList(tytd, ctx)
</fieldset> </fieldset>
</null>; </null>;
return Components.Shell("Edit users",html, 4); return Components.Shell("Edit users",html, 5);
} }
func Pages.EditUser(user) func Pages.EditUser(user)
@@ -141,5 +141,5 @@ func Pages.EditUser(user)
</if>; </if>;
return Components.Shell("Edit user",html, 4); return Components.Shell("Edit user",html, 5);
} }
@@ -81,5 +81,5 @@ func Pages.NewUser(error)
</form> </form>
</null>; </null>;
return Components.Shell("New User",html ,4); return Components.Shell("New User",html ,5);
} }
@@ -51,7 +51,7 @@ func Pages.OobePage2(tytd,ctx)
<div class="small-margin">Security</div> <div class="small-margin">Security</div>
</div> </div>
</nav> </nav>
<form hx-get="./oobe" hx-target="body" hx-push-url="true"> <form hx-post="./oobe" hx-target="body" hx-push-url="true">
<input type="hidden" name="page" value="2"> <input type="hidden" name="page" value="2">
<div class="field suffix border round"> <div class="field suffix border round">
<select name="server"> <select name="server">
@@ -75,5 +75,5 @@ func Pages.DownloadPlugins(tytd,ctx)
</null>; </null>;
return Components.Shell("Download plugins",html ,3); return Components.Shell("Download plugins",html ,4);
} }
@@ -20,5 +20,5 @@ func Pages.Plugins(tytd,ctx)
</each> </each>
</null>; </null>;
return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,3); return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,4);
} }
@@ -15,5 +15,5 @@ func Pages.Settings(tytd,ctx)
</fieldset>; </fieldset>;
return Components.Shell("Settings",html ,4); return Components.Shell("Settings",html ,5);
} }
@@ -0,0 +1,22 @@
func Pages.Subscriptions(tytd)
{
const html = <null>
<h1>Subscriptions</h1>
<each(var item : tytd.GetSubscriptions())>
<div class="row">
<div class="min">
<img class="circle supersized" src={$"./api/v1/channel-thumbnail?id={Net.Http.UrlEncode(item.channelId)}"}>
</div>
<div class="max">
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.channelId)}"} hx-target="body" hx-push-url="true">{item.title}</a>
</div>
<div class="min">
<button hx-post="./subscribe" hx-target="body" hx-push-url="true" hx-vals={Json.Encode({id=item.channelId,type="unsubscribe-subscriptions-page"})}>Unsubscribe</button>
</div>
<raw(Components.SubscribePageBell(tytd,item.channelId))>
</div>
</each>
</null>;
return Components.Shell("Subscriptions",html ,3);
}
@@ -20,7 +20,17 @@ func Pages.VideoInfo(tytd,ctx)
</div> </div>
<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> <div class="row">
<div class="min">
<img class="circle" src={$"./api/v1/channel-thumbnail?id={Net.Http.UrlEncode(vi.channelId)}"} width="144">
</div>
<div class="min">
<a class="underline" href={$"./channel?id={Net.Http.UrlEncode(vi.channelId)}"}>{vi.author}</a>
</div>
<div class="min">
<raw(Components.Subscribe(tytd,vi.channelId))>
</div>
</div>
<plink(vi.shortDescription)> <plink(vi.shortDescription)>
</div> </div>
@@ -11,5 +11,5 @@ func Pages.WhoAmI(row)
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li> <li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
</ul> </ul>
</null>; </null>;
return Components.Shell("Settings",html ,4); return Components.Shell("Settings",html ,5);
} }
+3 -3
View File
@@ -2,7 +2,7 @@
"info": { "info": {
"description": "Download YouTube Videos (using CrossLang)", "description": "Download YouTube Videos (using CrossLang)",
"maintainer": "Mike Nolan", "maintainer": "Mike Nolan",
"repo":"https://onedev.site.tesses.net/tytd2025", "repo":"https://git.tesses.org/tytd2025",
"homepage": "https://tesses.net/apps/tytd/2025/", "homepage": "https://tesses.net/apps/tytd/2025/",
"type": "lib", "type": "lib",
"license": "GPLv3-or-later" "license": "GPLv3-or-later"
@@ -14,6 +14,6 @@
} }
], ],
"name": "Tesses.YouTubeDownloader", "name": "Tesses.YouTubeDownloader",
"version": "1.0.0.0-dev", "version": "1.0.2.0-dev",
"icon": "icon.png" "icon": "icon.png"
} }
+4 -4
View File
@@ -20,10 +20,10 @@ class SubscriptionBell
{name="Disabled",value=SubscriptionBell.Disabled}, {name="Disabled",value=SubscriptionBell.Disabled},
{name="Notify",value=SubscriptionBell.Bell}, {name="Notify",value=SubscriptionBell.Bell},
{name="Notify and Download (Low)", value=SubscriptionBell.BellLow}, {name="Notify and Download (Low)", value=SubscriptionBell.BellLow},
{name="Notify and Download (High)", value=SubscriptionBell.BellHigh}, // {name="Notify and Download (High)", value=SubscriptionBell.BellHigh},
{name="Download (Low)", value=SubscriptionBell.DownloadLow}, {name="Download (Low)", value=SubscriptionBell.DownloadLow}
{name="Download (High)", value=SubscriptionBell.DownloadHigh} // ,{name="Download (High)", value=SubscriptionBell.DownloadHigh}
]; ];
} }
} }
@@ -131,7 +131,7 @@ class TYTD.Downloader {
}; };
this.BeforeQueued.Invoke(this, ent); this.BeforeQueued.Invoke(this, ent);
if(ent.Cancel) { if(ent.Cancel) {
this.LOG("Adding video canceled: {theVideoId}"); this.LOG($"Adding video canceled: {theVideoId}");
return; return;
} }
} }
@@ -199,6 +199,7 @@ class TYTD.Downloader {
if(cid != null) if(cid != null)
{ {
DownloadChannelMetadataIfNotExists(cid);
const ent = { const ent = {
Id = cid, Id = cid,
@@ -451,23 +452,30 @@ class TYTD.Downloader {
public GetChannelContents(id, offset, count) public GetChannelContents(id, offset, count)
{ {
id = TYTD.GetChannelId(id); id = TYTD.GetChannelId(id);
//title TEXT, description TEXT, tags TEXT
if(id == null) return { if(id == null) return {
authorName = "N/A", authorName = "N/A",
description = "",
tags = [],
items = [] items = []
}; };
this.Mutex.Lock(); this.Mutex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
var authorName = ""; var authorName = "";
var description = "";
var tags = [];
var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.channelId = {Sqlite.Escape(id)}) LIMIT {count} OFFSET {offset*count};"); var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.channelId = {Sqlite.Escape(id)}) LIMIT {count} OFFSET {offset*count};");
if(TypeOf(res) != "List" || res.Length == 0) { var res2 = Sqlite.Exec(db,$"SELECT * FROM channels c WHERE c.channelId = {Sqlite.Escape(id)};");
var res2 = Sqlite.Exec(db,"SELECT * FROM channels c WHERE c.channelId = {Sqlite.Escape(id)};");
if(TypeOf(res2) == "List" && res2.Length > 0) if(TypeOf(res2) == "List" && res2.Length > 0)
{ {
authorName = res2[0].title; authorName = res2[0].title;
} description = res2[0].description ?? "";
tags = Json.Decode(res2[0].tags ?? "[]") ?? [];
} }
else { else if(TypeOf(res) == "List" && res.Length > 0)
{
authorName = res[0].author; authorName = res[0].author;
} }
Sqlite.Close(db); Sqlite.Close(db);
@@ -489,6 +497,8 @@ class TYTD.Downloader {
} }
return { return {
authorName, authorName,
description,
tags,
items = res items = res
}; };
} }
@@ -508,6 +518,12 @@ class TYTD.Downloader {
this.Mutex.Unlock(); this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res; if(TypeOf(res) != "List") throw res;
each(var item in res)
{
item.description ??= "";
item.tags = Json.Decode(item.tags ?? "[]");
}
return res; return res;
} }
@@ -525,26 +541,21 @@ class TYTD.Downloader {
{ {
case Resolution.LowVideo: case Resolution.LowVideo:
return dir / "ytmux.mp4"; return dir / "ytmux.mp4";
break;
case Resolution.VideoOnly: case Resolution.VideoOnly:
return dir / "vo.bin"; return dir / "vo.bin";
break;
case Resolution.AudioOnly: case Resolution.AudioOnly:
return dir / "ao.bin"; return dir / "ao.bin";
break;
case Resolution.MP4: case Resolution.MP4:
return dir / "conv.mp4"; return dir / "conv.mp4";
break;
case Resolution.MKV: case Resolution.MKV:
return dir / "conv.mkv"; return dir / "conv.mkv";
break;
case Resolution.MP3: case Resolution.MP3:
return dir / "conv.mp3"; return dir / "conv.mp3";
break;
case Resolution.FLAC: case Resolution.FLAC:
return dir / "conv.flac"; return dir / "conv.flac";
break; case Resolution.AMV:
return dir / "conv.amv";
} }
throw $"Could not get file path for format {res}"; throw $"Could not get file path for format {res}";
} }
@@ -816,11 +827,20 @@ class TYTD.Downloader {
if(TypeOf(res) == "List" && res.Count > 0) return res[0].bell; if(TypeOf(res) == "List" && res.Count > 0) return res[0].bell;
return null; return null;
} }
public GetSubscriptions()
{
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db,"SELECT subscriptions.channelId, subscriptions.bell, channels.title FROM subscriptions INNER JOIN channels ON subscriptions.channelId = channels.channelId;");
Sqlite.Close(db);
this.Mutex.Unlock();
return res;
}
public GetSubscriptionUrls() public GetSubscriptionUrls()
{ {
var cid = TYTD.GetChannelId(url);
if(cid == null) return [];
//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.Lock(); this.Mutex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
@@ -849,14 +869,14 @@ class TYTD.Downloader {
public BeforeQueued = new TYTD.Event(); public BeforeQueued = new TYTD.Event();
public CurrentVideo = { private CurrentVideo = {
Title = "N/A", Title = "N/A",
Channel = "N/A", Channel = "N/A",
VideoId = "", VideoId = "",
ChannelId = "" ChannelId = ""
}; };
public CurrentVideoProgress = 0.0; private CurrentVideoProgress = 0.0;
public VideoEnded = new TYTD.Event(); public VideoEnded = new TYTD.Event();
@@ -874,6 +894,30 @@ class TYTD.Downloader {
private DownloaderThreadHandle; private DownloaderThreadHandle;
public GetProgress() {
this.Mutex.Lock();
const progress = {CurrentVideoProgress,CurrentVideo };
this.Mutex.Unlock();
return progress;
}
public GetProgressEx() {
this.Mutex.Lock();
const db = OpenDB();
const resultVideos = Sqlite.Exec(db, "SELECT COUNT(*) FROM videos;");
const resultQueue = Sqlite.Exec(db, "SELECT COUNT(*) FROM queue;");
Sqlite.Close(db);
const videoCount = TypeIsList(resultVideos) ? (ParseLong(resultVideos[0].["COUNT(*)"])??0) : 0;
const queueCount = TypeIsList(resultQueue) ? (ParseLong(resultQueue[0].["COUNT(*)"])??0) : 0;
const progress = {CurrentVideoProgress,CurrentVideo,QueueCount=queueCount, VideoCount=videoCount};
this.Mutex.Unlock();
return progress;
}
/^ /^
Get Video Queue count Get Video Queue count
^/ ^/
@@ -932,6 +976,11 @@ class TYTD.Downloader {
{ {
var id = TYTD.GetChannelId(id); var id = TYTD.GetChannelId(id);
if(id == null) return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); if(id == null) return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
const videoThumb = /"ChannelThumbnails"/id.Substring(0,4)/id.Substring(4) + ".jpg";
if(Storage.FileExists(videoThumb)) return FS.ReadAllBytes(Storage, videoThumb);
this.Mutex.Lock(); this.Mutex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;"); var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;");
@@ -1056,6 +1105,7 @@ class TYTD.Downloader {
} }
public Start() public Start()
{ {
this.Storage.CreateDirectory("ChannelThumbnails");
this.Storage.CreateDirectory(/"Streams"); this.Storage.CreateDirectory(/"Streams");
this.Storage.CreateDirectory(/"Plugins"); this.Storage.CreateDirectory(/"Plugins");
if(!this.Storage.FileExists(/"Streams"/"nullthumb.jpg")) if(!this.Storage.FileExists(/"Streams"/"nullthumb.jpg"))
@@ -1209,20 +1259,27 @@ class TYTD.Downloader {
var res = PopQueue(); var res = PopQueue();
//Console.WriteLine(res); //Console.WriteLine(res);
if(TypeOf(res) != "Null") if(TypeOf(res) != "Null")
{ {
if(TypeIsDefined(res.TYTD = this)){ if(TypeIsDefined(res.TYTD = this)){
res.Progress = (progress)=>{ res.Progress = (progress)=>{
this.Mutex.Lock();
this.CurrentVideoProgress = progress; this.CurrentVideoProgress = progress;
this.Mutex.Unlock();
this.VideoProgress.Invoke(this, { this.VideoProgress.Invoke(this, {
Video = res.Video, Video = res.Video,
progress progress
}); });
}; };
this.Mutex.Lock();
this.CurrentVideo = res.Video; this.CurrentVideo = res.Video;
this.Mutex.Unlock();
this.VideoStarted.Invoke(this,{ this.VideoStarted.Invoke(this,{
Video = res.Video Video = res.Video
@@ -1488,8 +1545,85 @@ class TYTD.Downloader {
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{keywordsStr},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});"; var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{keywordsStr},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
Sqlite.Exec(db, d); Sqlite.Exec(db, d);
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});");
Sqlite.Close(db); Sqlite.Close(db);
this.Mutex.Unlock();
DownloadChannelMetadataIfNotExists(info.channelId);
}
private DownloadChannelMetadataIfNotExists(id)
{
this.Mutex.Lock();
var db = this.OpenDB();
const results=Sqlite.Exec(db, $"SELECT * FROM channels WHERE channelId = {Sqlite.Escape(id)};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeIsList(results) && results.Length > 0) return;
try {
const result = {Id = id ,Tags = [], Title = "", Thumbnail = "", Description=""};
const str= Net.Http.DownloadToString($"https://www.youtube.com/channel/{id}");
const html = Net.Http.ParseHtml(str);
const headers = html.FindNodes("meta");
each(var item in headers)
{
const content = item.Attributes["content"] ?? null;
const property = item.Attributes["property"] ?? null;
if(TypeIsString(content) && TypeIsString(property))
{
switch(property)
{
case "og:image":
result.Thumbnail = content;
break;
case "og:title":
result.Title = content;
break;
case "og:description":
result.Description = content;
break;
case "og:video:tag":
result.Tags.Add(content);
break;
}
}
}
PutChannelMetadataIfNotExists(result);
}catch(ex) {
try{
this.LOG($"Failed to get info on getting metadata for channel {ex}");
}catch(ex2){}
}
}
private PutChannelMetadataIfNotExists(metadata)
{
const videoThumb = /"ChannelThumbnails"/metadata.Id.Substring(0,4)/metadata.Id.Substring(4) + ".jpg";
if(!Storage.FileExists(videoThumb))
{
Storage.CreateDirectory(videoThumb.GetParent());
const resp = Net.Http.MakeRequest(metadata.Thumbnail,{FollowRedirects=true});
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
const strm=this.Storage.OpenFile(videoThumb,"wb");
resp.CopyToStream(strm);
strm.Close();
}
}
this.Mutex.Lock();
var db = this.OpenDB();
//CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT, description TEXT, tags TEXT);
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title,description, tags) VALUES ({Sqlite.Escape(metadata.Id)}, {Sqlite.Escape(metadata.Title)}, {Sqlite.Escape(metadata.Description)}, {Sqlite.Escape(Json.Encode(metadata.Tags))});");
Sqlite.Close(db);
this.Mutex.Unlock(); this.Mutex.Unlock();
} }
@@ -1499,7 +1633,7 @@ class TYTD.Downloader {
var db = this.OpenDB(); var db = this.OpenDB();
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);"); Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);"); Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT);"); Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT, description TEXT, tags TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId INTEGER, videoId TEXT);"); Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId INTEGER, videoId TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT);"); Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description 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 personal_list_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, listName TEXT, videoId TEXT);");
@@ -1552,34 +1686,7 @@ class TYTD.Downloader {
} }
} }
private DownloadChannelThumbInternal(channelId, thumbnail_url)
{
var id = TYTD.GetChannelId(channelId);
var path = /"ChannelThumbnails"/id.Substring(2,4)/id.Substring(6);
this.Storage.CreateDirectory(path.GetParent());
if(!(this.Storage.FileExists(path + ".webp") || this.Storage.FileExists(path+".jpg")))
{
if(thumbnail_url.StartsWith("//")) thumbnail_url = $"https:{thumbnail_url}";
var dl = Net.Http.MakeRequest(thumbnail_url);
if(dl.StatusCode >= 200 && dl.StatusCode <= 299)
{
var ct=dl.ResponseHeaders.TryGetFirst("Content-Type");
if(TypeOf(ct) == "String")
{
var s = ct.Split("; ",true,2);
var ext = ".jpg";
if(s[0] == "image/webp")
ext = ".webp";
var dest = this.Storage.OpenFile(path+ext,"wb");
dl.CopyToStream(dest);
dest.Close();
}
}
}
}
private DiscoverInternal(q,continuation,params) private DiscoverInternal(q,continuation,params)
{ {
if(continuation == undefined) var continuation = null; if(continuation == undefined) var continuation = null;
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 1.0.2
Target crosslang v0.0.11, add channel tags, descriptions and thumbnail downloading
## 1.0.1
Target crosslang v0.0.10, add /api/v1/progress_ex.json
## 1.0.0
Make it a little faster, queue is in sqlite now, fix db bug and more
-4
View File
@@ -1,4 +0,0 @@
# Changelog
## 1.0.0
Make it a little faster, queue is in sqlite now, fix db bug and more
+1 -1
View File
@@ -9,7 +9,7 @@
name: "tytd2025" name: "tytd2025"
arch: "any" arch: "any"
platform: "linux" platform: "linux"
version: "1.0.0" version: "1.0.2"
section: "default" section: "default"
priority: "extra" priority: "extra"