Target crosslang v0.0.11, add channel tags, descriptions and thumbnail downloading
Build and Deploy on Tag / 🔨 Build (push) Successful in 22s

This commit is contained in:
2026-09-30 04:03:51 -05:00
parent f73c45bb48
commit c993977e65
27 changed files with 331 additions and 90 deletions
+2 -2
View File
@@ -14,6 +14,6 @@
"project_dependencies": [
"..\/Tesses.YouTubeDownloader"
],
"version": "1.0.1.0-dev",
"version": "1.0.2.0-dev",
"compTime": "secure"
}
}
@@ -1,6 +1,6 @@
[
{
"route": "/api/v1/video.json",
"route": "/api/v1/video",
"method": "GET",
"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"
},
{
"route": "/api/v1/downloads.json",
"route": "/api/v1/downloads",
"method": "GET",
"queryParams": [
{
@@ -83,4 +83,9 @@ body.dark {
top: 50vh;
left: 50vw;
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">
<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 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>
@@ -40,6 +40,13 @@ func Components.Shell(title, html, page, $mypage)
classStr="",
badge=""
},
{
text = "Subscriptions",
icon = "subscriptions",
href = (/"subscriptions").MakeRelative(mypage).ToString(),
classStr="",
badge=""
},
{
text = "Plugins",
icon = "extension",
@@ -68,7 +75,7 @@ func Components.Shell(title, html, page, $mypage)
<head>
<meta charset="UTF-8">
<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={(/"theme.css").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)
{
var bell = tytd.GetSubscriptionBell(id);
+87 -18
View File
@@ -540,16 +540,30 @@ class TYTDApp {
}
return false;
}
else if(ctx.Path == "/api/v1/progress_ex.json")
else if(ctx.Path == "/api/v1/progress_ex" || ctx.Path == "/api/v1/progress_ex.json")
{
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetProgressEx());
return true;
}
else if(ctx.Path == "/api/v1/progress.json")
else if(ctx.Path == "/api/v1/progress" || ctx.Path == "/api/v1/progress.json")
{
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;
}
else if(ctx.Path == "/api/v1/add")
{
if(ctx.Method=="POST")
@@ -564,14 +578,14 @@ class TYTDApp {
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");
if(TypeOf(id)!="String") id = "";
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetVideo(id));
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 page = ctx.QueryParams.TryGetFirstInt("page");
@@ -582,7 +596,7 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetPlaylistContents(id,page-1,count));
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 page = ctx.QueryParams.TryGetFirstInt("page");
@@ -593,9 +607,9 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
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 count = ctx.QueryParams.TryGetFirstInt("count");
@@ -608,7 +622,33 @@ class TYTDApp {
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 type = ctx.QueryParams.TryGetFirst("type");
@@ -661,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");
if(TypeIsString(v))
@@ -684,7 +724,7 @@ class TYTDApp {
name=
*/
if(ctx.Method == "GET")
if(ctx.Method == "GET" || ctx.Method == "HEAD")
{
var name = ctx.QueryParams.TryGetFirst("name");
if(TypeOf(name) == "String")
@@ -716,7 +756,23 @@ class TYTDApp {
{
var idHas = TypeOf(j.id) == "String" && j.id != "";
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)"});
}
@@ -765,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")
{
var id = ctx.QueryParams.TryGetFirst("id");
var bell = ctx.QueryParams.TryGetFirst("bell");
var type = ctx.QueryParams.TryGetFirst("type");
var id = ctx.BodyParams.TryGetFirst("id");
var bell = ctx.BodyParams.TryGetFirst("bell");
var type = ctx.BodyParams.TryGetFirst("type");
switch(type)
{
@@ -781,9 +842,17 @@ class TYTDApp {
case "unsubscribe":
this.TYTD.SetSubscriptionBell(id, null);
break;
case "unsubscribe-subscriptions-page":
this.TYTD.SetSubscriptionBell(id, null);
ctx.WithHeader("HX-Location", "/subscriptions").WriteHeaders();
return true;
case "bell":
this.TYTD.SetSubscriptionBell(id, bell);
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));
return true;
@@ -797,7 +866,7 @@ class TYTDApp {
else if(ctx.Path == "/edit-personal-description")
{
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));
return true;
@@ -968,7 +1037,7 @@ class TYTDApp {
else if(ctx.Path == "/watch_videos")
{
if(ctx.Method=="GET")
if(ctx.Method=="GET" || ctx.Method == "HEAD")
{
const video_ids = ctx.QueryParams.TryGetFirst("video_ids");
@@ -1261,4 +1330,4 @@ func main(args)
Net.Http.ListenSimpleWithLoop(res,3255);
res.Close();
return 0;
}
}
@@ -72,5 +72,5 @@ func Pages.Admin(tytd,ctx)
</form>;
return Components.Shell("Admin Settings",html ,4);
return Components.Shell("Admin Settings",html ,5);
}
@@ -8,5 +8,5 @@ func Pages.Api()
</a>
</ul>
</null>;
return Components.Shell("Api",html,5);
return Components.Shell("Api",html,6);
}
@@ -21,7 +21,7 @@ func Pages.ApiV1()
</fieldset>
</each>
</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());
@@ -45,5 +45,5 @@ const html = <null>
</form>
</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 title = "N/A";
if(TypeOf(id) == "String")
{
id = TYTD.GetChannelId(id);
if(TypeIsString(id))
{
var res = tytd.GetChannelContents(id,page,20);
html = <null>
<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">
<h1>{res.authorName}</h1>
</div>
@@ -22,6 +30,11 @@ func Pages.ChannelInfo(tytd,ctx)
<raw(Components.Subscribe(tytd,id))>
</div>
</div>
<details>
<summary>Description <i>expand_more</i></summary>
<plink(res.description)>
</details>
<each(var item : res.items)>
<raw(Components.DownloadedVideo(item))>
</each>
@@ -35,5 +48,5 @@ func Pages.ChannelInfo(tytd,ctx)
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>
</null>;
return Components.Shell("Edit users",html, 4);
return Components.Shell("Edit users",html, 5);
}
func Pages.EditUser(user)
@@ -141,5 +141,5 @@ func Pages.EditUser(user)
</if>;
return Components.Shell("Edit user",html, 4);
return Components.Shell("Edit user",html, 5);
}
@@ -81,5 +81,5 @@ func Pages.NewUser(error)
</form>
</null>;
return Components.Shell("New User",html ,4);
return Components.Shell("New User",html ,5);
}
@@ -75,5 +75,5 @@ func Pages.DownloadPlugins(tytd,ctx)
</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>
</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>;
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 class="min">
<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)>
</div>
@@ -11,5 +11,5 @@ func Pages.WhoAmI(row)
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
</ul>
</null>;
return Components.Shell("Settings",html ,4);
return Components.Shell("Settings",html ,5);
}