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
+1 -1
View File
@@ -14,7 +14,7 @@ env:
jobs:
build-packages:
name: "🔨 Build"
runs-on: fw13-ubuntu-latest
runs-on: msi-walmart
container:
image: "git.tesses.org/tesses50/linux-x64:latest"
+1
View File
@@ -6,3 +6,4 @@ artifacts
*.pkg.tar.zst
*.deb
*.rpm
tmp/
+1 -1
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": [
{
@@ -84,3 +84,8 @@ body.dark {
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);
+85 -16
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,11 +821,16 @@ 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");
@@ -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>
<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);
}
+2 -2
View File
@@ -2,7 +2,7 @@
"info": {
"description": "Download YouTube Videos (using CrossLang)",
"maintainer": "Mike Nolan",
"repo":"https://onedev.site.tesses.net/tytd2025",
"repo":"https://git.tesses.org/tytd2025",
"homepage": "https://tesses.net/apps/tytd/2025/",
"type": "lib",
"license": "GPLv3-or-later"
@@ -14,6 +14,6 @@
}
],
"name": "Tesses.YouTubeDownloader",
"version": "1.0.1.0-dev",
"version": "1.0.2.0-dev",
"icon": "icon.png"
}
+3 -3
View File
@@ -20,9 +20,9 @@ class SubscriptionBell
{name="Disabled",value=SubscriptionBell.Disabled},
{name="Notify",value=SubscriptionBell.Bell},
{name="Notify and Download (Low)", value=SubscriptionBell.BellLow},
{name="Notify and Download (High)", value=SubscriptionBell.BellHigh},
{name="Download (Low)", value=SubscriptionBell.DownloadLow},
{name="Download (High)", value=SubscriptionBell.DownloadHigh}
// {name="Notify and Download (High)", value=SubscriptionBell.BellHigh},
{name="Download (Low)", value=SubscriptionBell.DownloadLow}
// ,{name="Download (High)", value=SubscriptionBell.DownloadHigh}
];
}
@@ -131,7 +131,7 @@ class TYTD.Downloader {
};
this.BeforeQueued.Invoke(this, ent);
if(ent.Cancel) {
this.LOG("Adding video canceled: {theVideoId}");
this.LOG($"Adding video canceled: {theVideoId}");
return;
}
}
@@ -199,6 +199,7 @@ class TYTD.Downloader {
if(cid != null)
{
DownloadChannelMetadataIfNotExists(cid);
const ent = {
Id = cid,
@@ -451,23 +452,30 @@ class TYTD.Downloader {
public GetChannelContents(id, offset, count)
{
id = TYTD.GetChannelId(id);
//title TEXT, description TEXT, tags TEXT
if(id == null) return {
authorName = "N/A",
description = "",
tags = [],
items = []
};
this.Mutex.Lock();
var db = this.OpenDB();
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};");
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)
{
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;
}
Sqlite.Close(db);
@@ -489,6 +497,8 @@ class TYTD.Downloader {
}
return {
authorName,
description,
tags,
items = res
};
}
@@ -509,6 +519,12 @@ class TYTD.Downloader {
this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res;
each(var item in res)
{
item.description ??= "";
item.tags = Json.Decode(item.tags ?? "[]");
}
return res;
}
/^
@@ -812,10 +828,19 @@ class TYTD.Downloader {
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()
{
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);");
this.Mutex.Lock();
var db = this.OpenDB();
@@ -951,6 +976,11 @@ class TYTD.Downloader {
{
var id = TYTD.GetChannelId(id);
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();
var db = this.OpenDB();
var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;");
@@ -1075,6 +1105,7 @@ class TYTD.Downloader {
}
public Start()
{
this.Storage.CreateDirectory("ChannelThumbnails");
this.Storage.CreateDirectory(/"Streams");
this.Storage.CreateDirectory(/"Plugins");
if(!this.Storage.FileExists(/"Streams"/"nullthumb.jpg"))
@@ -1514,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)});";
Sqlite.Exec(db, d);
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});");
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();
}
@@ -1525,7 +1633,7 @@ class TYTD.Downloader {
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 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 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);");
@@ -1578,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)
{
if(continuation == undefined) var continuation = null;
+2
View File
@@ -1,5 +1,7 @@
# 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 -1
View File
@@ -9,7 +9,7 @@
name: "tytd2025"
arch: "any"
platform: "linux"
version: "1.0.1"
version: "1.0.2"
section: "default"
priority: "extra"