From 9da78e364c4012ee8399ee766916d4f336133471 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Thu, 20 Nov 2025 18:20:07 -0600 Subject: [PATCH] Add some api docs --- Tesses.YouTubeDownloader.Server/cross.json | 2 +- .../res/apiv1_routes.json | 47 ++ .../components/personallistdescription.tcross | 44 ++ .../src/components/plugin.tcross | 2 +- .../src/components/shell.tcross | 6 + .../src/main.tcross | 145 +++++- .../src/pages/api.tcross | 12 + .../src/pages/api/api-v1.tcross | 27 ++ .../src/pages/list.tcross | 3 + .../src/pages/playlist/info.tcross | 2 +- .../src/pages/settings.tcross | 2 + .../src/pages/video/info.tcross | 2 +- Tesses.YouTubeDownloader/cross.json | 2 +- .../src/YouTubeDownloader.tcross | 423 +++++++++++++----- .../videodownload/audioonlydownload.tcross | 1 + .../videodownload/noconvertdownload.tcross | 1 + .../src/videodownload/sdvideodownload.tcross | 1 + .../videodownload/videoonlydownload.tcross | 2 +- 18 files changed, 599 insertions(+), 125 deletions(-) create mode 100644 Tesses.YouTubeDownloader.Server/res/apiv1_routes.json create mode 100644 Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/api.tcross create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/api/api-v1.tcross diff --git a/Tesses.YouTubeDownloader.Server/cross.json b/Tesses.YouTubeDownloader.Server/cross.json index f024001..d710983 100644 --- a/Tesses.YouTubeDownloader.Server/cross.json +++ b/Tesses.YouTubeDownloader.Server/cross.json @@ -14,5 +14,5 @@ "project_dependencies": [ "..\/Tesses.YouTubeDownloader" ], - "version": "1.0.0.0-prod" + "version": "1.0.0.0-dev" } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/apiv1_routes.json b/Tesses.YouTubeDownloader.Server/res/apiv1_routes.json new file mode 100644 index 0000000..b147ce0 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/apiv1_routes.json @@ -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" + } +] \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross b/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross new file mode 100644 index 0000000..224f4eb --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross @@ -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 +=
; + } + description_with_br += Net.Http.HtmlEncode(txt); + first=false; + } + + + +
+
+
+
+ + +
+
+
+ +
+
+
+
+ +
+
+

+
+
+ +
+
+
+ +
+} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross b/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross index dda889c..77360e7 100644 --- a/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross +++ b/Tesses.YouTubeDownloader.Server/src/components/plugin.tcross @@ -11,7 +11,7 @@ func Components.InstalledPlugin(item)
- {TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)} + {TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)} {TypeOf(item.info.short_name_pretty) == "String" ? item.info.short_name_pretty : (TypeOf(item.info.short_name) == "String" ? item.info.short_name : item.name)} diff --git a/Tesses.YouTubeDownloader.Server/src/components/shell.tcross b/Tesses.YouTubeDownloader.Server/src/components/shell.tcross index 73a40ad..288adde 100644 --- a/Tesses.YouTubeDownloader.Server/src/components/shell.tcross +++ b/Tesses.YouTubeDownloader.Server/src/components/shell.tcross @@ -30,6 +30,12 @@ func Components.Shell(title, html, page, $mypage) icon = "settings", href= (/"settings").MakeRelative(mypage).ToString(), classStr="" + }, + { + text = "Api", + icon = "api", + href = (/"api").MakeRelative(mypage).ToString(), + classStr="" } ]; pages[page].classStr = "active"; diff --git a/Tesses.YouTubeDownloader.Server/src/main.tcross b/Tesses.YouTubeDownloader.Server/src/main.tcross index 02e4868..c7e3b34 100644 --- a/Tesses.YouTubeDownloader.Server/src/main.tcross +++ b/Tesses.YouTubeDownloader.Server/src/main.tcross @@ -31,6 +31,16 @@ class TYTDApp { ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD)); 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") { var v = ctx.QueryParams.TryGetFirst("v"); @@ -44,12 +54,29 @@ class TYTDApp { if(path != null) { 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"); ctx.WithMimeType(Net.Http.MimeType(path.ToString())).WithContentDisposition(filename,inline).SendStream(strm); 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") { var id = ctx.QueryParams.TryGetFirst("id"); @@ -102,6 +129,105 @@ class TYTDApp { ctx.WithMimeType("application/json").SendJson(result); 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") { var id = ctx.QueryParams.TryGetFirst("id"); @@ -125,6 +251,23 @@ class TYTDApp { ctx.WithMimeType("text/html").SendText(Components.Subscribe(this.TYTD,id)); 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") { ctx.WithMimeType("text/html").SendText(Pages.Downloads(this.TYTD,ctx)); diff --git a/Tesses.YouTubeDownloader.Server/src/pages/api.tcross b/Tesses.YouTubeDownloader.Server/src/pages/api.tcross new file mode 100644 index 0000000..8cdbe40 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/api.tcross @@ -0,0 +1,12 @@ +func Pages.Api() +{ + var html = +

Developers

+ +
; + return Components.Shell("Api",html,4); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/pages/api/api-v1.tcross b/Tesses.YouTubeDownloader.Server/src/pages/api/api-v1.tcross new file mode 100644 index 0000000..80b147a --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/api/api-v1.tcross @@ -0,0 +1,27 @@ +func Pages.ApiV1() +{ + var html = +

Api V1

+ + +
+ + {r.method} {r.route} + +
Query Parameters
+ +
{qp.name} ({qp.type}): {qp.description}
+ + +
Description
+ + + + +
+ +
; + return Components.Shell("Api V1",html,4); +} + +var apiv1_routes = Json.Decode(embed("apiv1_routes.json").ToString()); diff --git a/Tesses.YouTubeDownloader.Server/src/pages/list.tcross b/Tesses.YouTubeDownloader.Server/src/pages/list.tcross index a7cd1b2..bb2659b 100644 --- a/Tesses.YouTubeDownloader.Server/src/pages/list.tcross +++ b/Tesses.YouTubeDownloader.Server/src/pages/list.tcross @@ -20,6 +20,9 @@ func Pages.List(tytd,ctx)

{name}

+ + + diff --git a/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross b/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross index 773783c..8aec628 100644 --- a/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross +++ b/Tesses.YouTubeDownloader.Server/src/pages/playlist/info.tcross @@ -19,7 +19,7 @@ func Pages.PlaylistInfo(tytd,ctx)

{res.title}

diff --git a/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross b/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross index ab0be33..5d70167 100644 --- a/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross +++ b/Tesses.YouTubeDownloader.Server/src/pages/settings.tcross @@ -60,6 +60,8 @@ func Pages.Settings(tytd,ctx)
+ + download Download Database ; return Components.Shell("Settings",html ,3); diff --git a/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross b/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross index d367cc4..525bc01 100644 --- a/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross +++ b/Tesses.YouTubeDownloader.Server/src/pages/video/info.tcross @@ -19,7 +19,7 @@ func Pages.VideoInfo(tytd,ctx)

{vi.title}

- {vi.author} + {vi.author}

{vi.shortDescription}

diff --git a/Tesses.YouTubeDownloader/cross.json b/Tesses.YouTubeDownloader/cross.json index 4b6a5ed..737909a 100644 --- a/Tesses.YouTubeDownloader/cross.json +++ b/Tesses.YouTubeDownloader/cross.json @@ -14,6 +14,6 @@ } ], "name": "Tesses.YouTubeDownloader", - "version": "1.0.0.0-prod", + "version": "1.0.0.0-dev", "icon": "icon.png" } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross index 712e7ca..850ed06 100644 --- a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross +++ b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross @@ -1,17 +1,38 @@ class TYTD.Downloader { + /^ + The storage vfs that TYTD accesses + ^/ 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; + /^ + Package manager object + ^/ 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) { this.Storage = vfs; this.DatabaseDirectory = dbDir; } + /^ + Download a video + id: video id or url + res: see Resolution + ^/ public DownloadVideo(id,$res) { + this.LOG($"Adding video: {TYTD.GetVideoId(id)}, original val: {id}"); switch(res) { case Resolution.NoDownload: @@ -48,7 +69,11 @@ class TYTD.Downloader { break; } } - + /^ + Download a playlist + id: playlist id or url + res: see Resolution + ^/ public DownloadPlaylist(id,$res) { var pid = TYTD.GetPlaylistId(id); @@ -56,6 +81,7 @@ class TYTD.Downloader { if(pid != null) { + this.LOG($"Adding playlist: https://www.youtube.com/playlist?list={pid}"); this.PlaylistQueue.Push(()=>{ 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) { var cid = TYTD.GetChannelId(id); if(cid != null) { + this.LOG($"Adding channel: https://www.youtube.com/channel/{cid}"); this.PlaylistQueue.Push(()=>{ 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) { each(var item : this.Plugins) @@ -98,7 +131,11 @@ class TYTD.Downloader { return embed("package_icon.png"); } - + /^ + Download video, playlist or channel + url: id or url + res: see Resolution + ^/ public DownloadItem(url, $res) { var vid = TYTD.GetVideoId(url); @@ -106,7 +143,7 @@ class TYTD.Downloader { var cid = TYTD.GetChannelId(url); - if(vid != null && url.Length == 11) + if(vid != null) { this.DownloadVideo(vid, res); } @@ -114,25 +151,32 @@ class TYTD.Downloader { { this.DownloadPlaylist(pid,res); } - else if(vid != null) - { - this.DownloadVideo(vid, res); - } else if(cid != null) { this.DownloadChannel(cid,res); } } - + /^ + Redirect url to info page + ^/ public PageRedirect(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)}"; } + else if(pid != null) + { + return $"./playlist?id={Net.Http.UrlEncode(pid)}"; + } + else if(cid != null) + { + return $"./playlist?id={Net.Http.UrlEncode(cid)}"; + } return "./"; } @@ -148,12 +192,18 @@ class TYTD.Downloader { 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) { this.Muxex.Lock(); var db = this.OpenDB(); + 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};"); @@ -169,6 +219,7 @@ class TYTD.Downloader { else item.keywords = []; item.addDate = ParseLong(item.addDate); + item.addDateStr = new DateTime(item.addDate).ToString(); item.lengthSeconds = ParseLong(item.lengthSeconds); item.viewCount = ParseLong(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount); @@ -176,7 +227,11 @@ class TYTD.Downloader { } 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) { this.Muxex.Lock(); @@ -191,6 +246,11 @@ class TYTD.Downloader { 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) { id = TYTD.GetPlaylistId(id); @@ -239,6 +299,8 @@ class TYTD.Downloader { else item.keywords = []; item.addDate = ParseLong(item.addDate); + + item.addDateStr = new DateTime(item.addDate).ToString(); item.lengthSeconds = ParseLong(item.lengthSeconds); item.viewCount = ParseLong(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount); @@ -250,6 +312,11 @@ class TYTD.Downloader { 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) { id = TYTD.GetChannelId(id); @@ -283,6 +350,8 @@ class TYTD.Downloader { else item.keywords = []; item.addDate = ParseLong(item.addDate); + + item.addDateStr = new DateTime(item.addDate).ToString(); item.lengthSeconds = ParseLong(item.lengthSeconds); item.viewCount = ParseLong(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount); @@ -292,7 +361,11 @@ class TYTD.Downloader { 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) { this.Muxex.Lock(); @@ -307,7 +380,10 @@ class TYTD.Downloader { return res; } - + /^ + Get the video path + res: see Resolution + ^/ public GetVideoPath(v,res) { 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) { var id = TYTD.GetVideoId(vid); + if(id == null) return null; + Console.WriteLine(id); this.Muxex.Lock(); var db = this.OpenDB(); var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};"); var out = null; + Console.WriteLine(res); if(TypeOf(res) == "List" && res.Length == 1) out = res[0]; @@ -365,6 +447,8 @@ class TYTD.Downloader { else out.keywords = []; out.addDate = ParseLong(out.addDate); + + out.addDateStr = new DateTime(out.addDate).ToString(); out.lengthSeconds = ParseLong(out.lengthSeconds); out.viewCount = ParseLong(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) { var id = TYTD.GetPlaylistId(vid); @@ -392,6 +480,10 @@ class TYTD.Downloader { return out; } + /^ + Get channel info + id can be a channel url from youtube or just the id + ^/ public GetChannel(id) { var id = TYTD.GetChannelId(vid); @@ -432,6 +524,7 @@ class TYTD.Downloader { this.Mutex.Unlock(); return items; } + /^ Set the description of a personal list ^/ public SetPersonalListDescription(name,description) { this.Muxex.Lock(); @@ -441,6 +534,7 @@ class TYTD.Downloader { Sqlite.Close(db); this.Mutex.Unlock(); } + /^ Get the description of a personal list ^/ public GetPersonalListDescription(name) { this.Muxex.Lock(); @@ -452,10 +546,13 @@ class TYTD.Downloader { if(TypeOf(res) == "List" && res.Length > 0) { - res[0].description; + var d = res[0].description; + if(TypeOf(d)=="String") + return d; } return ""; } + /^ ^/ public GetPersonalListContents(name, offset, count) { this.Muxex.Lock(); @@ -472,6 +569,8 @@ class TYTD.Downloader { item.keywords = Json.Decode(item.keywords); else item.keywords = []; item.addDate = ParseLong(item.addDate); + + item.addDateStr = new DateTime(item.addDate).ToString(); item.lengthSeconds = ParseLong(item.lengthSeconds); item.viewCount = ParseLong(item.viewCount); item.viewCountStr = this.Views2Str(item.viewCount); @@ -505,11 +604,19 @@ class TYTD.Downloader { 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 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); this.Mutex.Unlock(); } - public SetSubscriptionBell(url, bell) { var cid = TYTD.GetChannelId(url); @@ -587,7 +694,9 @@ class TYTD.Downloader { public Plugins = []; - + /^ + The mutex (for database) + ^/ public Mutex = new Mutex(); public Running=true; @@ -595,6 +704,11 @@ class TYTD.Downloader { private DownloaderThreadHandle; private Queue = new TYTD.Queue(); + /^ + Get Video Queue count + ^/ + + public getVideoQueueCount() this.Queue.Count; private PlaylistThreadHandle; @@ -660,7 +774,7 @@ class TYTD.Downloader { try {info = Json.Decode(_exec.Info);} catch(ex) {} _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 ico = _exec.Icon; _pkg.pluginIcon = ico >= 0 && i < reso.Count ? reso[ico] : embed("package_icon.png"); @@ -773,33 +887,35 @@ class TYTD.Downloader { private lastSubPollTime = 0; private PlaylistThread() { + while(this.Running) { - var res = this.PlaylistQueue.Pop(); + try { + var res = this.PlaylistQueue.Pop(); - if(TypeOf(res) != "Null") - { - res(); - } + if(TypeOf(res) != "Null") + { + res(); + } - var currentTime = DateTime.NowEpoch; - var bt = this.Config.BellTimer; + var currentTime = DateTime.NowEpoch; + var bt = this.Config.BellTimer; - if((currentTime-this.lastSubPollTime) > bt) - { - this.lastSubPollTime = currentTime; + if((currentTime-this.lastSubPollTime) > bt) + { + this.lastSubPollTime = currentTime; - this.Mutex.Lock(); - var db = this.OpenDB(); - 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);"); + this.Mutex.Lock(); + var db = this.OpenDB(); + 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);"); - this.Mutex.Unlock(); - /* - /^ Disabled bell ^/ - static getDisabled() "Disabled"; + this.Mutex.Unlock(); + /* + /^ Disabled bell ^/ + static getDisabled() "Disabled"; /^ Download (Low quality) ^/ static getDownloadLow() "DownloadLow"; /^ Download (High quality) ^/ @@ -813,98 +929,123 @@ class TYTD.Downloader { static getBell() "Bell"; */ - each(var sub : res) - { + each(var sub : res) + { - var downloadRes = Resolution.NoDownload; - var notify = false; - switch(sub.bell) - { - case SubscriptionBell.Bell: - notify=true; - break; - case SubscriptionBell.BellLow: - notify=true; - case SubscriptionBell.DownloadLow: - downloadRes = Resolution.LowVideo; - break; - case SubscriptionBell.BellHigh: - notify = true; - break; - case SubscriptionBell.DownloadHigh: - downloadRes = Resolution.MKV; - break; - } - if(!notify && downloadRes == Resolution.NoDownload) continue; - var cid = sub.channelId; + var downloadRes = Resolution.NoDownload; + var notify = false; + switch(sub.bell) + { + case SubscriptionBell.Bell: + notify=true; + break; + case SubscriptionBell.BellLow: + notify=true; + case SubscriptionBell.DownloadLow: + downloadRes = Resolution.LowVideo; + break; + case SubscriptionBell.BellHigh: + notify = true; + break; + case SubscriptionBell.DownloadHigh: + downloadRes = Resolution.MKV; + break; + } + if(!notify && downloadRes == Resolution.NoDownload) continue; + var cid = sub.channelId; - var newVideos = []; + var newVideos = []; - each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) - { - each(var videoId : batch) + each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) { + each(var videoId : batch) + { - if(this.GetVideo(videoId) == null) - newVideos.Add(videoId); + if(this.GetVideo(videoId) == null) + newVideos.Add(videoId); + } } - } - if(notify) - { - each(var id : newVideos) + if(notify) { - this.PutVideoInfoIfNotExists(id); - var res = this.GetVideo(id); - if(res != null) - this.Bell.Invoke(this,{ - Video = res - }); - + each(var id : newVideos) + { + this.PutVideoInfoIfNotExists(id); + var res = this.GetVideo(id); + if(res != null) + this.Bell.Invoke(this,{ + Video = res + }); + } } - } - if(downloadRes != Resolution.NoDownload) - { - each(var id : newVideos) + if(downloadRes != Resolution.NoDownload) { - 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() { 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.Progress = (progress)=>{ - this.CurrentVideoProgress = progress; - this.VideoProgress.Invoke(this, { - Video = res.Video, - progress + res.TYTD = this; + res.Progress = (progress)=>{ + this.CurrentVideoProgress = progress; + this.VideoProgress.Invoke(this, { + Video = res.Video, + 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,{ - Video = res.Video - }); + this.VideoEnded.Invoke(this,{ + 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"); } - private OpenDB() + /^ + Open the database + ^/ + public OpenDB() { var dbFile = this.DatabaseDirectory / "tytd.db"; 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) { each(var item : this.Plugins) @@ -994,6 +1142,10 @@ class TYTD.Downloader { this.PackageManager.DownloadPlugin(dir,name,version); this.LoadPlugins(); } + /^ + Install plugin + version must be a Version not a String + ^/ public PackageInstall(name, version) { @@ -1013,7 +1165,9 @@ class TYTD.Downloader { this.PackageDownload(name, version); } - + /^ + Uninstall plugin + ^/ public PackageUninstall(name) { each(var item : this.Plugins) @@ -1029,11 +1183,18 @@ class TYTD.Downloader { } } } - + /^ + Get the TYTD Tag + ^/ public getTYTDTag() { return this.Config.TYTDTag ?? "UnknownPC"; } + /^ + Download video info if it does not exist + pass in a video id or url + + ^/ public PutVideoInfoIfNotExists(vid) { var id = TYTD.GetVideoId(vid); @@ -1048,13 +1209,22 @@ class TYTD.Downloader { } } - + /^ + Put video info from info into database + ^/ public PutVideoInfo(info) { this.Muxex.Lock(); 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.Close(db); this.Mutex.Unlock(); @@ -1108,6 +1278,7 @@ class TYTD.Downloader { return config[0].value; } } + private DownloadChannelThumbInternal(channelId, thumbnail_url) { var id = TYTD.GetChannelId(channelId); @@ -1286,7 +1457,9 @@ class TYTD.Downloader { } return {items}; } - + /^ + Make a video manifest request + ^/ public ManifestRequest(vid) { var id = TYTD.GetVideoId(vid); @@ -1509,7 +1682,6 @@ class TYTD.Downloader { private rlm=new Muxex(); - private RateLimit() { this.rlm.Lock(); @@ -1530,4 +1702,19 @@ class TYTD.Downloader { this.lastRequest = curRequest; this.rlm.Unlock(); } -} \ No newline at end of file + /^ + 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(); + } +} diff --git a/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross index f8c210c..6664536 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross @@ -27,6 +27,7 @@ class TYTD.AOVideoDownload : IVideoDownload { var req = this.tytd.ManifestRequest(id).playerResponse; 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.ChannelId = req.videoDetails.channelId; diff --git a/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross index 7427311..8e73760 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross @@ -35,6 +35,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload { 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.ChannelId = req.videoDetails.channelId; diff --git a/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross index dd90dca..df92191 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross @@ -27,6 +27,7 @@ class TYTD.SDVideoDownload : IVideoDownload { var req = this.tytd.ManifestRequest(id).playerResponse; this.info.Title = req.videoDetails.title; + tytd.LOG($"Downloading: {this.info.Title} with id: {id} LowVideo"); this.info.Channel = req.videoDetails.author; this.info.ChannelId = req.videoDetails.channelId; diff --git a/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross index 4e85768..32b948a 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross @@ -29,7 +29,7 @@ class TYTD.VOVideoDownload : IVideoDownload { this.info.Title = req.videoDetails.title; this.info.Channel = req.videoDetails.author; this.info.ChannelId = req.videoDetails.channelId; - + tytd.LOG($"Downloading: {this.info.Title} with id: {id} Highest Video"); this.tytd.PutVideoInfo(req.videoDetails); var width = 0;