Add some api docs

This commit is contained in:
2025-11-20 18:20:07 -06:00
parent c71c73bb77
commit 9da78e364c
18 changed files with 599 additions and 125 deletions

View File

@@ -14,5 +14,5 @@
"project_dependencies": [ "project_dependencies": [
"..\/Tesses.YouTubeDownloader" "..\/Tesses.YouTubeDownloader"
], ],
"version": "1.0.0.0-prod" "version": "1.0.0.0-dev"
} }

View File

@@ -0,0 +1,47 @@
[
{
"route": "/api/v1/video.json",
"method": "GET",
"queryParams": [
{
"name": "v",
"type": "string",
"description": "The 11 character videoid"
}
],
"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",
"method": "GET",
"queryParams": [
{
"name": "q",
"type": "string?",
"description": "The search parameter"
},
{
"name": "type",
"type": "string?",
"description": "Whether you are searching for videos, playlists, channels or personal (defaults to videos)"
},
{
"name": "page",
"type": "long?",
"description": "The page to retreve, starts at 1 and defaults to 1"
},
{
"name": "count",
"type": "long?",
"description": "The entries to return, defaults to 20"
}
],
"description": "Query or browse the downloads"
},
{
"route": "/api/v1/database.db",
"method": "GET",
"queryParams": [],
"description": "Download the database"
}
]

View File

@@ -0,0 +1,44 @@
func Components.PersonalListDescription(tytd,name,editing)
{
var description = tytd.GetPersonalListDescription(name);
var first=true;
var description_with_br = "";
each(var txt : description.Split("\n"))
{
if(!first)
{
description_with_br += <br>;
}
description_with_br += Net.Http.HtmlEncode(txt);
first=false;
}
<return>
<if(editing)>
<true>
<form hx-post={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-swap="outerHTML">
<div class="row">
<div class="max">
<div class="field textarea label border">
<textarea name="description">{description}</textarea>
<label>Description</label>
</div>
</div>
<div class="min">
<button><i>save</i></button>
</div>
</div>
</form>
</true>
<false>
<div class="row" id="description">
<div class="max">
<p><raw(description_with_br)></p>
</div>
<div class="min">
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
</div>
</div>
</false>
</if>
</return>
}

View File

@@ -11,7 +11,7 @@ func Components.InstalledPlugin(item)
<div class="min"> <div class="min">
<if(item.pluginObject.Server != undefined && item.pluginObject.Server != null)> <if(item.pluginObject.Server != undefined && item.pluginObject.Server != null)>
<true> <true>
<a href={$"./plugin/{Net.Http.UrlPathEncode(item.pluginName)}/"}>{TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)}</a> <a class="underline" href={$"./plugin/{Net.Http.UrlPathEncode(item.pluginName)}/"}>{TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)}</a>
</true> </true>
<false> <false>
<span>{TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)}</span> <span>{TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)}</span>

View File

@@ -30,6 +30,12 @@ func Components.Shell(title, html, page, $mypage)
icon = "settings", icon = "settings",
href= (/"settings").MakeRelative(mypage).ToString(), href= (/"settings").MakeRelative(mypage).ToString(),
classStr="" classStr=""
},
{
text = "Api",
icon = "api",
href = (/"api").MakeRelative(mypage).ToString(),
classStr=""
} }
]; ];
pages[page].classStr = "active"; pages[page].classStr = "active";

View File

@@ -31,6 +31,16 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD)); ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD));
return true; return true;
} }
else if(ctx.Path == "/api")
{
ctx.WithMimeType("text/html").SendText(Pages.Api());
return true;
}
else if(ctx.Path == "/api-v1")
{
ctx.WithMimeType("text/html").SendText(Pages.ApiV1());
return true;
}
else if(ctx.Path == "/api/v1/download") else if(ctx.Path == "/api/v1/download")
{ {
var v = ctx.QueryParams.TryGetFirst("v"); var v = ctx.QueryParams.TryGetFirst("v");
@@ -44,12 +54,29 @@ class TYTDApp {
if(path != null) if(path != null)
{ {
var info = this.TYTD.GetVideo(v); var info = this.TYTD.GetVideo(v);
var filename = $"{info.title}-{info.videoId}.{path.GetExtension()}"; var filename = $"{info.title}-{info.videoId}-{res}{path.GetExtension()}";
var strm = this.TYTD.Storage.OpenFile(path,"rb"); var strm = this.TYTD.Storage.OpenFile(path,"rb");
ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm); ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm);
strm.Close(); strm.Close();
} }
} }
else if(ctx.Path == "/api/v1/progress.json")
{
var jo = {
CurrentVideoProgress = this.TYTD.CurrentVideoProgress,
CurrentVideo = this.TYTD.CurrentVideo
};
ctx.WithMimeType("application/json").SendJson(jo);
return true;
}
else if(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.json")
{ {
var id = ctx.QueryParams.TryGetFirst("id"); var id = ctx.QueryParams.TryGetFirst("id");
@@ -102,6 +129,105 @@ class TYTDApp {
ctx.WithMimeType("application/json").SendJson(result); ctx.WithMimeType("application/json").SendJson(result);
return true; return true;
} }
else if(ctx.Path == "/api/v1/database.db")
{
this.TYTD.SendDatabase(ctx);
return true;
}
else if(ctx.Path == "/api/v1/personal")
{
/*
GET /api/v1/personal
GET /api/v1/personal?name=NAME
DELETE /api/v1/personal
name=NAME
id=THEID //If this does not exist delete the entire list
POST /api/v1/personal
id=THEID or description=
name=
*/
if(ctx.Method == "GET")
{
var name = ctx.QueryParams.TryGetFirst("name");
if(TypeOf(name) == "String")
{
var page = ctx.QueryParams.TryGetFirstInt("page") ?? 1;
var count = ctx.QueryParams.TryGetFirstInt("count") ?? 20;
var json = {
description = this.TYTD.GetPersonalListDescription(name),
items = this.TYTD.GetPersonalListContents(name,page-1,count)
};
ctx.WithMimeType("application/json").SendJson(json);
return true;
}
else {
var json = {
items = this.TYTD.GetPersonalLists()
};
ctx.WithMimeType("application/json").SendJson(json);
return true;
}
}
else if(ctx.Method == "POST")
{
//{name="",id="",description=""}
var j = ctx.ReadJson();
if(TypeOf(j.name) == "String")
{
var idHas = TypeOf(j.id) == "String" && j.id != "";
var descHas = TypeOf(j.description) == "String" && j.description != "";
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)"});
}
else if(idHas)
{
this.TYTD.AddToPersonalList(j.name, j.id);
ctx.WithMimeType("application/json").SendJson({success=true});
}
else if(descHas)
{
this.TYTD.SetPersonalListDescription(j.name,j.description);
ctx.WithMimeType("application/json").SendJson({success=true});
}
else {
ctx.WithMimeType("application/json").SendJson({success=false,reason="You must provide either a description (to set the list description) or id (to add a item)"});
}
}
else {
ctx.WithMimeType("application/json").SendJson({success=false, reason="Need a name"});
}
return true;
}
else if(ctx.Method == "DELETE")
{
var j = ctx.ReadJson();
/*
public RemoveFromPersonalList(name,id)
public RemovePersonalList(name)
*/
if(TypeOf(j.name) == "String")
{
if(TypeOf(j.id) == "String" && j.id != "")
{
this.TYTD.RemoveFromPersonalList(j.name,j.id);
}
else {
this.TYTD.RemovePersonalList(j.name);
}
ctx.WithMimeType("application/json").SendJson({success=true});
}
else {
ctx.WithMimeType("application/json").SendJson({success=false, reason="Need a name"});
}
return true;
}
}
else if(ctx.Path == "/subscribe") else if(ctx.Path == "/subscribe")
{ {
var id = ctx.QueryParams.TryGetFirst("id"); var id = ctx.QueryParams.TryGetFirst("id");
@@ -125,6 +251,23 @@ class TYTDApp {
ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id)); ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id));
return true; return true;
} }
else if(ctx.Path == "/edit-personal-description")
{
var name = ctx.QueryParams.TryGetFirst("name");
if(ctx.Method == "GET")
{
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,true));
return true;
}
else if(ctx.Method == "POST")
{
var description =ctx.QueryParams.TryGetFirst("description");
this.TYTD.SetPersonalListDescription(name,description);
ctx.WithMimeType("text/html").SendText(Components.PersonalListDescription(this.TYTD,name,false));
return true;
}
}
else if(ctx.Path == "/downloads") else if(ctx.Path == "/downloads")
{ {
ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx)); ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx));

View File

@@ -0,0 +1,12 @@
func Pages.Api()
{
var html = <null>
<h1>Developers</h1>
<ul>
<a class="underline" href="./api-v1">
API v1
</a>
</ul>
</null>;
return Components.Shell("Api",html,4);
}

View File

@@ -0,0 +1,27 @@
func Pages.ApiV1()
{
var html = <null>
<h1>Api V1</h1>
<each(var r : apiv1_routes)>
<fieldset>
<legend>{r.method} {r.route}</legend>
<h6>Query Parameters</h6>
<each(var qp : r.queryParams)>
<div>{qp.name} ({qp.type}): {qp.description}</div>
</each>
<h6>Description</h6>
<plink(r.description)>
</fieldset>
</each>
</null>;
return Components.Shell("Api V1",html,4);
}
var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString());

View File

@@ -20,6 +20,9 @@ func Pages.List(tytd,ctx)
<h1>{name}</h1> <h1>{name}</h1>
</div> </div>
</div> </div>
<raw(Components.PersonalListDescription(tytd,name,false))>
<each(var item : res)> <each(var item : res)>
<raw(Components.DownloadedVideo(item))> <raw(Components.DownloadedVideo(item))>
</each> </each>

View File

@@ -19,7 +19,7 @@ func Pages.PlaylistInfo(tytd,ctx)
<h1>{res.title}</h1> <h1>{res.title}</h1>
</div> </div>
<div class="max"> <div class="max">
<a href={$"./channel?id={Net.Http.UrlEncode(res.channelId)}"}>{res.channelTitle}</a> <a class="underline" href={$"./channel?id={Net.Http.UrlEncode(res.channelId)}"}>{res.channelTitle}</a>
</div> </div>
</div> </div>
<each(var item : res.items)> <each(var item : res.items)>

View File

@@ -60,6 +60,8 @@ func Pages.Settings(tytd,ctx)
<footer> <footer>
<button>Save</button> <button>Save</button>
</footer> </footer>
<a class="button responsive" href="./api/v1/database.db"><i>download</i> Download Database</a>
</form>; </form>;
return Components.Shell("Settings",html ,3); return Components.Shell("Settings",html ,3);

View File

@@ -19,7 +19,7 @@ func Pages.VideoInfo(tytd,ctx)
</div> </div>
<div class="min"> <div class="min">
<h4>{vi.title}</h4> <h4>{vi.title}</h4>
<a href={$"./channel?id={Net.Http.UrlEncode(vi.channelId)}"}>{vi.author}</a> <a class="underline" href={$"./channel?id={Net.Http.UrlEncode(vi.channelId)}"}>{vi.author}</a>
<p>{vi.shortDescription}</p> <p>{vi.shortDescription}</p>
</div> </div>

View File

@@ -14,6 +14,6 @@
} }
], ],
"name": "Tesses.YouTubeDownloader", "name": "Tesses.YouTubeDownloader",
"version": "1.0.0.0-prod", "version": "1.0.0.0-dev",
"icon": "icon.png" "icon": "icon.png"
} }

View File

@@ -1,17 +1,38 @@
class TYTD.Downloader { class TYTD.Downloader {
/^
The storage vfs that TYTD accesses
^/
public Storage; public Storage;
/^
The directory of where the database is (should be the same physical folder as the Storage vfs if you want ffmpeg)
^/
public DatabaseDirectory; public DatabaseDirectory;
/^
Package manager object
^/
public PackageManager = new Tesses.CrossLang.PackageManager(); public PackageManager = new Tesses.CrossLang.PackageManager();
public Servers = Net.Http.MountableServer({Handle=(ctx)=>false});
/^
All the plugin webpages
^/
public Servers = Net.Http.MountableServer({Handle=(ctx)=>false});
/^
vfs: The storage vfs that TYTD accesses
dbDir: The directory of where the database is (should be the same physical folder as the Storage vfs if you want ffmpeg)
^/
public Downloader(vfs,dbDir) public Downloader(vfs,dbDir)
{ {
this.Storage = vfs; this.Storage = vfs;
this.DatabaseDirectory = dbDir; this.DatabaseDirectory = dbDir;
} }
/^
Download a video
id: video id or url
res: see Resolution
^/
public DownloadVideo(id,$res) public DownloadVideo(id,$res)
{ {
this.LOG($"Adding video: {TYTD.GetVideoId(id)}, original val: {id}");
switch(res) switch(res)
{ {
case Resolution.NoDownload: case Resolution.NoDownload:
@@ -48,7 +69,11 @@ class TYTD.Downloader {
break; break;
} }
} }
/^
Download a playlist
id: playlist id or url
res: see Resolution
^/
public DownloadPlaylist(id,$res) public DownloadPlaylist(id,$res)
{ {
var pid = TYTD.GetPlaylistId(id); var pid = TYTD.GetPlaylistId(id);
@@ -56,6 +81,7 @@ class TYTD.Downloader {
if(pid != null) if(pid != null)
{ {
this.LOG($"Adding playlist: https://www.youtube.com/playlist?list={pid}");
this.PlaylistQueue.Push(()=>{ this.PlaylistQueue.Push(()=>{
each(var item : this.QueryPlaylistItems(pid,true)) each(var item : this.QueryPlaylistItems(pid,true))
{ {
@@ -68,13 +94,18 @@ class TYTD.Downloader {
}); });
} }
} }
/^
Download a channel
id: channel id or url
res: see Resolution
^/
public DownloadChannel(id,$res) public DownloadChannel(id,$res)
{ {
var cid = TYTD.GetChannelId(id); var cid = TYTD.GetChannelId(id);
if(cid != null) if(cid != null)
{ {
this.LOG($"Adding channel: https://www.youtube.com/channel/{cid}");
this.PlaylistQueue.Push(()=>{ this.PlaylistQueue.Push(()=>{
each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
{ {
@@ -86,7 +117,9 @@ class TYTD.Downloader {
}); });
} }
} }
/^
Get the thumbnail of a plugin
^/
public GetPluginThumbnail(name) public GetPluginThumbnail(name)
{ {
each(var item : this.Plugins) each(var item : this.Plugins)
@@ -98,7 +131,11 @@ class TYTD.Downloader {
return embed("package_icon.png"); return embed("package_icon.png");
} }
/^
Download video, playlist or channel
url: id or url
res: see Resolution
^/
public DownloadItem(url, $res) public DownloadItem(url, $res)
{ {
var vid = TYTD.GetVideoId(url); var vid = TYTD.GetVideoId(url);
@@ -106,7 +143,7 @@ class TYTD.Downloader {
var cid = TYTD.GetChannelId(url); var cid = TYTD.GetChannelId(url);
if(vid != null && url.Length == 11) if(vid != null)
{ {
this.DownloadVideo(vid, res); this.DownloadVideo(vid, res);
} }
@@ -114,25 +151,32 @@ class TYTD.Downloader {
{ {
this.DownloadPlaylist(pid,res); this.DownloadPlaylist(pid,res);
} }
else if(vid != null)
{
this.DownloadVideo(vid, res);
}
else if(cid != null) else if(cid != null)
{ {
this.DownloadChannel(cid,res); this.DownloadChannel(cid,res);
} }
} }
/^
Redirect url to info page
^/
public PageRedirect(url) public PageRedirect(url)
{ {
var vid = TYTD.GetVideoId(url); var vid = TYTD.GetVideoId(url);
var pid = TYTD.GetPlaylistId(url);
if(vid != null && url.Length == 11) if(vid != null)
{ {
return $"./video?v={Net.Http.UrlEncode(vid)}"; return $"./video?v={Net.Http.UrlEncode(vid)}";
} }
else if(pid != null)
{
return $"./playlist?id={Net.Http.UrlEncode(pid)}";
}
else if(cid != null)
{
return $"./playlist?id={Net.Http.UrlEncode(cid)}";
}
return "./"; return "./";
} }
@@ -148,12 +192,18 @@ class TYTD.Downloader {
return $"{views/1000000000000}T views"; return $"{views/1000000000000}T views";
} }
/^
Get videos
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetVideos(query, offset, count) public GetVideos(query, offset, count)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%"); var q = Sqlite.Escape($"%{query}%");
var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.title LIKE {q} OR v.shortDescription LIKE {q}) LIMIT {count} OFFSET {offset*count};"); var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.title LIKE {q} OR v.shortDescription LIKE {q}) LIMIT {count} OFFSET {offset*count};");
@@ -169,6 +219,7 @@ class TYTD.Downloader {
else item.keywords = []; else item.keywords = [];
item.addDate = ParseLong(item.addDate); item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds); item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount); item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount);
@@ -176,7 +227,11 @@ class TYTD.Downloader {
} }
return res; return res;
} }
/^
Get playlists
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetPlaylists(query, offset, count) public GetPlaylists(query, offset, count)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
@@ -191,6 +246,11 @@ class TYTD.Downloader {
return res; return res;
} }
/^
Get playlist contents
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetPlaylistContents(id, offset, count) public GetPlaylistContents(id, offset, count)
{ {
id = TYTD.GetPlaylistId(id); id = TYTD.GetPlaylistId(id);
@@ -239,6 +299,8 @@ class TYTD.Downloader {
else else
item.keywords = []; item.keywords = [];
item.addDate = ParseLong(item.addDate); item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds); item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount); item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount);
@@ -250,6 +312,11 @@ class TYTD.Downloader {
items = res2 items = res2
}; };
} }
/^
Get channel contents
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetChannelContents(id, offset, count) public GetChannelContents(id, offset, count)
{ {
id = TYTD.GetChannelId(id); id = TYTD.GetChannelId(id);
@@ -283,6 +350,8 @@ class TYTD.Downloader {
else else
item.keywords = []; item.keywords = [];
item.addDate = ParseLong(item.addDate); item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds); item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount); item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount);
@@ -292,7 +361,11 @@ class TYTD.Downloader {
items = res items = res
}; };
} }
/^
Get channels
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetChannels(query, offset, count) public GetChannels(query, offset, count)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
@@ -307,7 +380,10 @@ class TYTD.Downloader {
return res; return res;
} }
/^
Get the video path
res: see Resolution
^/
public GetVideoPath(v,res) public GetVideoPath(v,res)
{ {
var id = TYTD.GetVideoId(v); var id = TYTD.GetVideoId(v);
@@ -343,16 +419,22 @@ class TYTD.Downloader {
} }
/^
Get video info
vid can be a video url from youtube or just the id
^/
public GetVideo(vid) public GetVideo(vid)
{ {
var id = TYTD.GetVideoId(vid); var id = TYTD.GetVideoId(vid);
if(id == null) return null; if(id == null) return null;
Console.WriteLine(id);
this.Muxex.Lock(); this.Muxex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};"); var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};");
var out = null; var out = null;
Console.WriteLine(res);
if(TypeOf(res) == "List" && res.Length == 1) out = res[0]; if(TypeOf(res) == "List" && res.Length == 1) out = res[0];
@@ -365,6 +447,8 @@ class TYTD.Downloader {
else else
out.keywords = []; out.keywords = [];
out.addDate = ParseLong(out.addDate); out.addDate = ParseLong(out.addDate);
out.addDateStr = new DateTime(out.addDate).ToString();
out.lengthSeconds = ParseLong(out.lengthSeconds); out.lengthSeconds = ParseLong(out.lengthSeconds);
out.viewCount = ParseLong(out.viewCount); out.viewCount = ParseLong(out.viewCount);
out.viewCountStr = this.Views2Str(out.viewCount); out.viewCountStr = this.Views2Str(out.viewCount);
@@ -374,6 +458,10 @@ class TYTD.Downloader {
/^
Get playlist info
id can be a playlist url from youtube or just the id
^/
public GetPlaylist(id) public GetPlaylist(id)
{ {
var id = TYTD.GetPlaylistId(vid); var id = TYTD.GetPlaylistId(vid);
@@ -392,6 +480,10 @@ class TYTD.Downloader {
return out; return out;
} }
/^
Get channel info
id can be a channel url from youtube or just the id
^/
public GetChannel(id) public GetChannel(id)
{ {
var id = TYTD.GetChannelId(vid); var id = TYTD.GetChannelId(vid);
@@ -432,6 +524,7 @@ class TYTD.Downloader {
this.Mutex.Unlock(); this.Mutex.Unlock();
return items; return items;
} }
/^ Set the description of a personal list ^/
public SetPersonalListDescription(name,description) public SetPersonalListDescription(name,description)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
@@ -441,6 +534,7 @@ class TYTD.Downloader {
Sqlite.Close(db); Sqlite.Close(db);
this.Mutex.Unlock(); this.Mutex.Unlock();
} }
/^ Get the description of a personal list ^/
public GetPersonalListDescription(name) public GetPersonalListDescription(name)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
@@ -452,10 +546,13 @@ class TYTD.Downloader {
if(TypeOf(res) == "List" && res.Length > 0) if(TypeOf(res) == "List" && res.Length > 0)
{ {
res[0].description; var d = res[0].description;
if(TypeOf(d)=="String")
return d;
} }
return ""; return "";
} }
/^ ^/
public GetPersonalListContents(name, offset, count) public GetPersonalListContents(name, offset, count)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
@@ -472,6 +569,8 @@ class TYTD.Downloader {
item.keywords = Json.Decode(item.keywords); item.keywords = Json.Decode(item.keywords);
else item.keywords = []; else item.keywords = [];
item.addDate = ParseLong(item.addDate); item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds); item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount); item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount);
@@ -505,11 +604,19 @@ class TYTD.Downloader {
this.Mutex.Lock(); this.Mutex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
//personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT) //personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT)
Sqlite.Exec(db, $"DELETE FROM personal_list_entries e WHERE (e.listName = {Sqlite.Escape(name)} AND e.videoId = {Sqlite.Escape(id)});"); Sqlite.Exec(db, $"DELETE FROM personal_list_entries WHERE (listName = {Sqlite.Escape(name)} AND videoId = {Sqlite.Escape(id)});");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public RemovePersonalList(name)
{
this.Mutex.Lock();
var db = this.OpenDB();
//personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT)
Sqlite.Exec(db, $"DELETE FROM personal_list_entries WHERE listName = {Sqlite.Escape(name)}; DELETE FROM personal_lists WHERE name = {Sqlite.Escape(name)};");
Sqlite.Close(db); Sqlite.Close(db);
this.Mutex.Unlock(); this.Mutex.Unlock();
} }
public SetSubscriptionBell(url, bell) public SetSubscriptionBell(url, bell)
{ {
var cid = TYTD.GetChannelId(url); var cid = TYTD.GetChannelId(url);
@@ -587,7 +694,9 @@ class TYTD.Downloader {
public Plugins = []; public Plugins = [];
/^
The mutex (for database)
^/
public Mutex = new Mutex(); public Mutex = new Mutex();
public Running=true; public Running=true;
@@ -595,6 +704,11 @@ class TYTD.Downloader {
private DownloaderThreadHandle; private DownloaderThreadHandle;
private Queue = new TYTD.Queue(); private Queue = new TYTD.Queue();
/^
Get Video Queue count
^/
public getVideoQueueCount() this.Queue.Count;
private PlaylistThreadHandle; private PlaylistThreadHandle;
@@ -660,7 +774,7 @@ class TYTD.Downloader {
try {info = Json.Decode(_exec.Info);} catch(ex) {} try {info = Json.Decode(_exec.Info);} catch(ex) {}
_pkg.info = info; _pkg.info = info;
_pkg.pluginName = TypeOf(info.short_name) ? info.short_name : name; _pkg.pluginName = TypeOf(info.short_name) == "String" ? info.short_name : name;
var reso = _exec.Resources; var reso = _exec.Resources;
var ico = _exec.Icon; var ico = _exec.Icon;
_pkg.pluginIcon = ico >= 0 && i < reso.Count ? reso[ico] : embed("package_icon.png"); _pkg.pluginIcon = ico >= 0 && i < reso.Count ? reso[ico] : embed("package_icon.png");
@@ -773,33 +887,35 @@ class TYTD.Downloader {
private lastSubPollTime = 0; private lastSubPollTime = 0;
private PlaylistThread() private PlaylistThread()
{ {
while(this.Running) while(this.Running)
{ {
var res = this.PlaylistQueue.Pop(); try {
var res = this.PlaylistQueue.Pop();
if(TypeOf(res) != "Null") if(TypeOf(res) != "Null")
{ {
res(); res();
} }
var currentTime = DateTime.NowEpoch; var currentTime = DateTime.NowEpoch;
var bt = this.Config.BellTimer; var bt = this.Config.BellTimer;
if((currentTime-this.lastSubPollTime) > bt) if((currentTime-this.lastSubPollTime) > bt)
{ {
this.lastSubPollTime = currentTime; this.lastSubPollTime = currentTime;
this.Mutex.Lock(); this.Mutex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
var res = Sqlite.Exec(db, "SELECT * FROM subscriptions;"); var res = Sqlite.Exec(db, "SELECT * FROM subscriptions;");
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);"); //Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
this.Mutex.Unlock(); this.Mutex.Unlock();
/* /*
/^ Disabled bell ^/ /^ Disabled bell ^/
static getDisabled() "Disabled"; static getDisabled() "Disabled";
/^ Download (Low quality) ^/ /^ Download (Low quality) ^/
static getDownloadLow() "DownloadLow"; static getDownloadLow() "DownloadLow";
/^ Download (High quality) ^/ /^ Download (High quality) ^/
@@ -813,98 +929,123 @@ class TYTD.Downloader {
static getBell() "Bell"; static getBell() "Bell";
*/ */
each(var sub : res) each(var sub : res)
{ {
var downloadRes = Resolution.NoDownload; var downloadRes = Resolution.NoDownload;
var notify = false; var notify = false;
switch(sub.bell) switch(sub.bell)
{ {
case SubscriptionBell.Bell: case SubscriptionBell.Bell:
notify=true; notify=true;
break; break;
case SubscriptionBell.BellLow: case SubscriptionBell.BellLow:
notify=true; notify=true;
case SubscriptionBell.DownloadLow: case SubscriptionBell.DownloadLow:
downloadRes = Resolution.LowVideo; downloadRes = Resolution.LowVideo;
break; break;
case SubscriptionBell.BellHigh: case SubscriptionBell.BellHigh:
notify = true; notify = true;
break; break;
case SubscriptionBell.DownloadHigh: case SubscriptionBell.DownloadHigh:
downloadRes = Resolution.MKV; downloadRes = Resolution.MKV;
break; break;
} }
if(!notify && downloadRes == Resolution.NoDownload) continue; if(!notify && downloadRes == Resolution.NoDownload) continue;
var cid = sub.channelId; var cid = sub.channelId;
var newVideos = []; var newVideos = [];
each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
{
each(var videoId : batch)
{ {
each(var videoId : batch)
{
if(this.GetVideo(videoId) == null) if(this.GetVideo(videoId) == null)
newVideos.Add(videoId); newVideos.Add(videoId);
}
} }
}
if(notify) if(notify)
{
each(var id : newVideos)
{ {
this.PutVideoInfoIfNotExists(id); each(var id : newVideos)
var res = this.GetVideo(id); {
if(res != null) this.PutVideoInfoIfNotExists(id);
this.Bell.Invoke(this,{ var res = this.GetVideo(id);
Video = res if(res != null)
}); this.Bell.Invoke(this,{
Video = res
});
}
} }
} if(downloadRes != Resolution.NoDownload)
if(downloadRes != Resolution.NoDownload)
{
each(var id : newVideos)
{ {
this.DownloadVideo(id,downloadRes); each(var id : newVideos)
{
this.DownloadVideo(id,downloadRes);
}
} }
}
}
} }
}
}catch(ex) {
try{
this.LOG($"Exception caught on playlist thread: {e}");
}catch(ex2){}
} }
} }
}
private LOGDATEFMT = "%Y%m%d_%H%M%S";
private LOGDATE = DateTime.Now;
/^
Log stuff
^/
public LOG(text)
{
this.Mutex.Lock();
this.Storage.CreateDirectory(/"Logs");
var strm = this.Storage.OpenFile(/"Logs"/$"{this.LOGDATE.ToString(this.LOGDATEFMT)}.log","a");
strm.WriteText($"[{DateTime.Now.ToString()}] {text}\n");
strm.Close();
this.Mutex.Unlock();
}
private DownloadThread() private DownloadThread()
{ {
while(this.Running) while(this.Running)
{ {
var res = this.Queue.Pop(); try {
var res = this.Queue.Pop();
if(TypeOf(res) != "Null") if(TypeOf(res) != "Null")
{ {
res.TYTD = this; res.TYTD = this;
res.Progress = (progress)=>{ res.Progress = (progress)=>{
this.CurrentVideoProgress = progress; this.CurrentVideoProgress = progress;
this.VideoProgress.Invoke(this, { this.VideoProgress.Invoke(this, {
Video = res.Video, Video = res.Video,
progress progress
});
};
this.CurrentVideo = res.Video;
this.VideoStarted.Invoke(this,{
Video = res.Video
}); });
};
this.CurrentVideo = res.Video;
this.VideoStarted.Invoke(this,{
Video = res.Video
});
res.Start(); res.Start();
this.VideoEnded.Invoke(this,{ this.VideoEnded.Invoke(this,{
Video = res.Video Video = res.Video
}); });
}
} catch(ex) {
try{
this.LOG($"Exception caught on download thread: {e}");
}catch(ex2){}
} }
} }
} }
@@ -971,12 +1112,19 @@ class TYTD.Downloader {
} }
return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg"); return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
} }
private OpenDB() /^
Open the database
^/
public OpenDB()
{ {
var dbFile = this.DatabaseDirectory / "tytd.db"; var dbFile = this.DatabaseDirectory / "tytd.db";
return Sqlite.Open(dbFile); return Sqlite.Open(dbFile);
} }
/^
Get whether package is installed
version must be the current version as a Version not string
returns 0 if not, 1 if installed or 2 if can update
^/
public PackageState(name, version) public PackageState(name, version)
{ {
each(var item : this.Plugins) each(var item : this.Plugins)
@@ -994,6 +1142,10 @@ class TYTD.Downloader {
this.PackageManager.DownloadPlugin(dir,name,version); this.PackageManager.DownloadPlugin(dir,name,version);
this.LoadPlugins(); this.LoadPlugins();
} }
/^
Install plugin
version must be a Version not a String
^/
public PackageInstall(name, version) public PackageInstall(name, version)
{ {
@@ -1013,7 +1165,9 @@ class TYTD.Downloader {
this.PackageDownload(name, version); this.PackageDownload(name, version);
} }
/^
Uninstall plugin
^/
public PackageUninstall(name) public PackageUninstall(name)
{ {
each(var item : this.Plugins) each(var item : this.Plugins)
@@ -1029,11 +1183,18 @@ class TYTD.Downloader {
} }
} }
} }
/^
Get the TYTD Tag
^/
public getTYTDTag() public getTYTDTag()
{ {
return this.Config.TYTDTag ?? "UnknownPC"; return this.Config.TYTDTag ?? "UnknownPC";
} }
/^
Download video info if it does not exist
pass in a video id or url
^/
public PutVideoInfoIfNotExists(vid) public PutVideoInfoIfNotExists(vid)
{ {
var id = TYTD.GetVideoId(vid); var id = TYTD.GetVideoId(vid);
@@ -1048,13 +1209,22 @@ class TYTD.Downloader {
} }
} }
/^
Put video info from info into database
^/
public PutVideoInfo(info) public PutVideoInfo(info)
{ {
this.Muxex.Lock(); this.Muxex.Lock();
var db = this.OpenDB(); var db = this.OpenDB();
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},{Sqlite.Escape(info.keywords.ToString())},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
Sqlite.Exec(db, d); var keywords = info.keywords;
if(TypeOf(keywords) != "List") keywords = [];
var keywordsStr = Sqlite.Escape(Json.Encode(keywords));
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)});";
Console.WriteLine(Sqlite.Exec(db, d));
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});"); 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(); this.Mutex.Unlock();
@@ -1108,6 +1278,7 @@ class TYTD.Downloader {
return config[0].value; return config[0].value;
} }
} }
private DownloadChannelThumbInternal(channelId, thumbnail_url) private DownloadChannelThumbInternal(channelId, thumbnail_url)
{ {
var id = TYTD.GetChannelId(channelId); var id = TYTD.GetChannelId(channelId);
@@ -1286,7 +1457,9 @@ class TYTD.Downloader {
} }
return {items}; return {items};
} }
/^
Make a video manifest request
^/
public ManifestRequest(vid) public ManifestRequest(vid)
{ {
var id = TYTD.GetVideoId(vid); var id = TYTD.GetVideoId(vid);
@@ -1509,7 +1682,6 @@ class TYTD.Downloader {
private rlm=new Muxex(); private rlm=new Muxex();
private RateLimit() private RateLimit()
{ {
this.rlm.Lock(); this.rlm.Lock();
@@ -1530,4 +1702,19 @@ class TYTD.Downloader {
this.lastRequest = curRequest; this.lastRequest = curRequest;
this.rlm.Unlock(); this.rlm.Unlock();
} }
} /^
Send the database as http response
^/
public SendDatabase(ctx)
{
this.Mutex.Lock();
try {
var strm = FS.Local.OpenFile(this.DatabaseDirectory/"tytd.db","rb");
ctx.SendStream(strm);
strm.Close();
}catch(ex) {
Console.WriteLine($"ERROR: {ex}");
}
this.Mutex.Unlock();
}
}

View File

@@ -27,6 +27,7 @@ class TYTD.AOVideoDownload : IVideoDownload {
var req = this.tytd.ManifestRequest(id).playerResponse; var req = this.tytd.ManifestRequest(id).playerResponse;
this.info.Title = req.videoDetails.title; this.info.Title = req.videoDetails.title;
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Audio");
this.info.Channel = req.videoDetails.author; this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId; this.info.ChannelId = req.videoDetails.channelId;

View File

@@ -35,6 +35,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload {
this.info.Title = req.videoDetails.title; this.info.Title = req.videoDetails.title;
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video/Audio");
this.info.Channel = req.videoDetails.author; this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId; this.info.ChannelId = req.videoDetails.channelId;

View File

@@ -27,6 +27,7 @@ class TYTD.SDVideoDownload : IVideoDownload {
var req = this.tytd.ManifestRequest(id).playerResponse; var req = this.tytd.ManifestRequest(id).playerResponse;
this.info.Title = req.videoDetails.title; this.info.Title = req.videoDetails.title;
tytd.LOG($"Downloading: {this.info.Title} with id: {id} LowVideo");
this.info.Channel = req.videoDetails.author; this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId; this.info.ChannelId = req.videoDetails.channelId;

View File

@@ -29,7 +29,7 @@ class TYTD.VOVideoDownload : IVideoDownload {
this.info.Title = req.videoDetails.title; this.info.Title = req.videoDetails.title;
this.info.Channel = req.videoDetails.author; this.info.Channel = req.videoDetails.author;
this.info.ChannelId = req.videoDetails.channelId; this.info.ChannelId = req.videoDetails.channelId;
tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video");
this.tytd.PutVideoInfo(req.videoDetails); this.tytd.PutVideoInfo(req.videoDetails);
var width = 0; var width = 0;