Make it a little faster, queue is in sqlite now, fix db bug and more
Some checks failed
Build and Deploy on Tag / 🔨 Build for everything else (push) Failing after 11s
Build and Deploy on Tag / 🔨 Build for PowerPC (push) Failing after 20s

This commit is contained in:
2026-06-25 15:35:39 -05:00
parent ede1f92981
commit c82957cc61
53 changed files with 661 additions and 888 deletions

View File

@@ -14,6 +14,6 @@
"project_dependencies": [
"..\/Tesses.YouTubeDownloader"
],
"version": "1.0.0.6-dev",
"version": "1.0.0.0-dev",
"compTime": "secure"
}

View File

@@ -12,6 +12,10 @@
</head>
<body>
<main>
<a href="./" class="button extend circle">
<i>home</i>
<span>Home</span>
</a>
<div class="row">
<div class="max"></div>
<div class="min">

View File

@@ -2,6 +2,13 @@ const assets = ["<@ASSETS@>"];
const staticCacheName = "tytd-static-<@BUILD_TIME@>";
async function ThrowOnBadGateway(req)
{
const resp = await fetch(req);
if(resp.status === 502) throw new Error("Bad Gateway");
return resp;
}
self.addEventListener('install',evt => {
evt.waitUntil(
caches.open(staticCacheName).then(cache =>{
@@ -19,24 +26,29 @@ self.addEventListener('activate',(evt)=>{
});
self.addEventListener('fetch', evt => {
const uri = new URL(evt.request.url);
evt.respondWith(
caches.match(evt.request).then(cacheRes=>{
return cacheRes || fetch(evt.request);
}).catch(()=>{
if(uri.pathname === '/queue-size')
console.warn('SW cacheRes:', cacheRes);
return cacheRes || ThrowOnBadGateway(evt.request);
}).catch((err)=>{
console.warn('SW Fallback:', err.message);
if(evt.request.headers.has('HX-Request'))
{
return new Response('<span hx-trigger="every 5000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">?</span>',{
headers: { 'Content-Type': 'text/html' }
});
return new Response("",{
status: 200,
headers: {
'HX-Redirect': "/offline.html"
}
});
}
if(uri.pathname === "/progress")
else
{
return caches.match("/offline-progress.html");
return caches.match('/offline.html');
}
return caches.match('/offline.html');
})
);
});
});

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ func Components.QueueSZ(tytd)
tytd.Mutex.Lock();
var html = <span hx-trigger="every 10000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">{tytd.VideoQueueCount}</span>;
var html = <b hx-trigger="every 10000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML">{tytd.VideoQueueCount}</b>;
tytd.Mutex.Unlock();
return html;

View File

@@ -21,34 +21,48 @@ func Components.Shell(title, html, page, $mypage)
text="Home",
icon="home",
href=index,
classStr=""
classStr="",
badge = ""
},
{
text="Queue",
icon="downloading",
href=(/"queue").MakeRelative(mypage).ToString(),,
classStr="",
badge=<b hx-trigger="every 1000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML">
?
</b>
},
{
text = "Downloads",
icon = "download",
href=(/"downloads").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
},
{
text = "Plugins",
icon = "extension",
href= (/"plugins").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
},
{
text = "Settings",
icon = "settings",
href= (/"settings").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
},
{
text = "Api",
icon = "api",
href = (/"api").MakeRelative(mypage).ToString(),
classStr=""
classStr="",
badge=""
}
];
pages[page].classStr = "active";
pages[page].classStr = " primary-container";
return <!DOCTYPE html>
<html lang="en">
<head>
@@ -65,8 +79,43 @@ func Components.Shell(title, html, page, $mypage)
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
</head>
<body hx-indicator="#loading-indicator">
<nav class="bottom">
<dialog class="left" id="leftpane">
<header>
<nav>
<img class="circle large" src="/tytd-128.png">
<h6 class="max">TYTD2025</h6>
<button hx-on:click="ui('#leftpane')" class="transparent circle large">
<i>close</i>
</button>
</nav>
</header>
<div class="space"></div>
<ul class="list">
<each(var page : pages)>
<li class={$"wave round{page.classStr}"} hx-target="body" hx-push-url="true" hx-get={page.href}>
<i>{page.icon}</i>
<span class="max">{page.text}</span>
<raw(page.badge)>
</li>
</each>
</ul>
</dialog>
<header class="fixed primary-container">
<nav>
<button hx-on:click="ui('#leftpane')" class="circle transparent">
<i>menu</i>
</button>
<h6 class="max">TYTD2025</h6>
</nav>
</header>
/*<nav class="bottom">
<each(var page : pages)>
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
<i>{page.icon}</i>
@@ -81,7 +130,7 @@ func Components.Shell(title, html, page, $mypage)
</a>
</each>
</nav>
</nav>*/
<main class="responsive">
<raw(html)>

View File

@@ -58,6 +58,11 @@ class TYTDApp {
}
public Handle(ctx)
{
if(ctx.Path == "/api/v1/resolutions.json")
{
ctx.SendJson(Resolution.Resolutions);
return true;
}
if(ctx.Path == "/service_worker.js" || fileNames.Contains(ctx.Path))
{
const inm=ctx.RequestHeaders.TryGetFirst("If-None-Match");
@@ -176,6 +181,10 @@ class TYTDApp {
);
return true;
}
if(ctx.Path == "/queue-size")
{
return false;
}
if(ctx.Path == "/sso")
{
const app = ctx.QueryParams.TryGetFirst("app") ?? "";
@@ -570,6 +579,21 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(this.TYTD.GetChannelContents(id,page-1,count));
return true;
}
else if(ctx.Path == "/api/v1/queue.json")
{
if(ctx.Method == "GET")
{
var page = ctx.QueryParams.TryGetFirstInt("page");
var count = ctx.QueryParams.TryGetFirstInt("count");
if(TypeOf(page)!="Long") page = 1;
if(TypeOf(count)!="Long") count = 20;
var result = { entries = TYTD.GetQueueItems(page-1,count)};
ctx.WithMimeType("application/json").SendJson(result);
return true;
}
}
else if(ctx.Path == "/api/v1/downloads.json")
{
var q = ctx.QueryParams.TryGetFirst("q");
@@ -605,7 +629,7 @@ class TYTDApp {
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"));
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized","<h1>You are not authorized to download the database</h1>",4,/"api/v1/database.db"));
}
return true;
}
@@ -773,6 +797,24 @@ class TYTDApp {
return true;
}
}
else if(ctx.Path == "/queue")
{
if(ctx.Method == "DELETE")
{
const id = ctx.QueryParams.TryGetFirstInt("id");
if(TypeIsLong(id))
{
this.TYTD.QueueRemoveItem(id);
}
else
{
this.TYTD.QueueClear();
}
}
ctx.WithMimeType("text/html").SendText(Pages.Queue(this.TYTD,ctx));
return true;
}
else if(ctx.Path == "/downloads")
{
ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx));
@@ -793,7 +835,7 @@ class TYTDApp {
{
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));
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,4));
return true;
}
const user = ctx.QueryParams.TryGetFirst("user");
@@ -871,7 +913,7 @@ class TYTDApp {
{
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));
ctx.WithMimeType("text/html").SendText(Components.Shell("Unauthorized",<h1>You can{"'"}t modify admin settings as you are not an admin</h1>,4));
return true;
}
if(ctx.Method == "POST")

View File

@@ -72,5 +72,5 @@ func Pages.Admin(tytd,ctx)
</form>;
return Components.Shell("Admin Settings",html ,3);
return Components.Shell("Admin Settings",html ,4);
}

View File

@@ -8,5 +8,5 @@ func Pages.Api()
</a>
</ul>
</null>;
return Components.Shell("Api",html,4);
return Components.Shell("Api",html,5);
}

View File

@@ -21,7 +21,7 @@ func Pages.ApiV1()
</fieldset>
</each>
</null>;
return Components.Shell("Api V1",html,4);
return Components.Shell("Api V1",html,5);
}
var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString());

View File

@@ -45,5 +45,5 @@ const html = <null>
</form>
</null>;
return Components.Shell("Change your password",html,3);
return Components.Shell("Change your password",html,4);
}

View File

@@ -35,5 +35,5 @@ func Pages.ChannelInfo(tytd,ctx)
title = res.authorName;
}
return Components.Shell($"Channel {title}",html,1);
return Components.Shell($"Channel {title}",html,2);
}

View File

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

View File

@@ -289,5 +289,5 @@ func Pages.Downloads(tytd,ctx)
return Components.Shell($"Downloads {q}",html,1);
return Components.Shell($"Downloads {q}",html,2);
}

View File

@@ -38,7 +38,7 @@ func Pages.EditUserList(tytd, ctx)
</fieldset>
</null>;
return Components.Shell("Edit users",html, 3);
return Components.Shell("Edit users",html, 4);
}
func Pages.EditUser(user)
@@ -141,5 +141,5 @@ func Pages.EditUser(user)
</if>;
return Components.Shell("Edit user",html, 3);
return Components.Shell("Edit user",html, 4);
}

View File

@@ -35,5 +35,5 @@ func Pages.List(tytd,ctx)
</null>;
return Components.Shell($"Personal List {name}",html,1);
return Components.Shell($"Personal List {name}",html,2);
}

View File

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

View File

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

View File

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

View File

@@ -81,5 +81,5 @@ func Pages.NewUser(error)
</form>
</null>;
return Components.Shell("New User",html ,3);
return Components.Shell("New User",html ,4);
}

View File

@@ -22,5 +22,5 @@ func Pages.YouTubeAnonyPlaylist(video_ids)
<div class="max"></div>
</div>
</null>;
return Components.Shell("Create playlist",html ,1);
return Components.Shell("Create playlist",html ,2);
}

View File

@@ -35,5 +35,5 @@ func Pages.PlaylistInfo(tytd,ctx)
title = res.authorName;
}
return Components.Shell($"Playlist {title}",html,1);
return Components.Shell($"Playlist {title}",html,2);
}

View File

@@ -75,5 +75,5 @@ func Pages.DownloadPlugins(tytd,ctx)
</null>;
return Components.Shell("Download plugins",html ,2);
return Components.Shell("Download plugins",html ,3);
}

View File

@@ -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> ,2);
return Components.Shell("Installed plugins", UserFlags.CanUsePlugins(userFlags) ? html : <h1>You can{"'"}t use plugins</h1> ,3);
}

View File

@@ -0,0 +1,71 @@
func Pages.Queue(tytd,ctx)
{
var page = ctx.QueryParams.TryGetFirstInt("page");
if(TypeOf(page) != "Long") page = 1;
page--;
enumerable func getitems()
{
each(var item : tytd.GetQueueItems(page,10))
{
tytd.PutVideoInfoIfNotExists(item.videoId);
const info = tytd.GetVideo(item.videoId);
yield {
Title = $"{info.title}-{item.videoId}-{item.resolution}",
VideoId = item.videoId,
Id = item.id,
ChannelId = info.channelId,
Channel = info.author
};
}
}
const html = <null>
<div class="row">
<div class="max">
</div>
<div class="min">
<button hx-delete="./queue" hx-target="body" hx-push-url="true" hx-confirm="Are you sure you want to clear the queue?">Clear All</button>
</div>
</div>
<each(var item : getitems())>
<div class="row">
<div class="min">
<img src={$"./api/v1/video-thumbnail?v={Net.Http.UrlEncode(item.VideoId)}"} width="144" width="120">
</div>
<div class="max">
<div class="col">
<div class="min">
{item.Title}
</div>
<div class="min">
<a hx-get={$"./channel?id={Net.Http.UrlEncode(item.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(item.ChannelId)}"} hx-target="body" hx-push-url="true">{item.Channel}</a>
</div>
</div>
</div>
<div class="min">
<button class="circle" hx-delete={$"./queue?page={page+1}"} hx-vals={{id = item.Id}} hx-target="body" hx-push-url="true" hx-confirm="Are you sure you want to delete this?"><i>delete</i></button>
</div>
</div>
</each>
<footer class="row center-align">
<button hx-get={$"./queue?page={page}"} hx-target="body" hx-push-url="true" >Prev</button>{page+1}<button hx-get={$"./queue?page={page+2}"} hx-target="body" hx-push-url="true" >Next</button>
</footer>
</null>;
return Components.Shell("Queue",html,1);
}

View File

@@ -15,5 +15,5 @@ func Pages.Settings(tytd,ctx)
</fieldset>;
return Components.Shell("Settings",html ,3);
return Components.Shell("Settings",html ,4);
}

View File

@@ -27,5 +27,5 @@ func Pages.VideoInfo(tytd,ctx)
</div>;
}
return Components.Shell(vi != null ? vi.title : "Could not find video", html, 1);
return Components.Shell(vi != null ? vi.title : "Could not find video", html, 2);
}

View File

@@ -11,5 +11,5 @@ func Pages.WhoAmI(row)
<li>Can manage plugins: {UserFlags.CanManagePlugins(row.flags)}</li>
</ul>
</null>;
return Components.Shell("Settings",html ,3);
return Components.Shell("Settings",html ,4);
}