From 28b71385472ec406b69a35c0da7b9cccedbcb105 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Sat, 28 Feb 2026 06:32:39 -0600 Subject: [PATCH] Add pwa support --- README.md | 15 +- Tesses.YouTubeDownloader.Server/cross.json | 5 +- .../res/offline-progress.html | 4 + .../res/offline.html | 106 +++++++ .../res/offline.js | 54 ++++ .../res/service_worker.js | 42 +++ .../res/site.webmanifest | 73 +++++ .../res/tytd-1024.png | Bin 0 -> 23091 bytes .../res/{icon.png => tytd-128.png} | Bin .../res/tytd-192.png | Bin 0 -> 2907 bytes .../res/tytd-256.png | Bin 0 -> 4037 bytes .../res/tytd-384.png | Bin 0 -> 5881 bytes .../res/tytd-512.png | Bin 0 -> 8309 bytes .../components/personallistdescription.tcross | 8 + .../src/components/progress.tcross | 3 +- .../src/components/queuesz.tcross | 12 + .../src/components/shell.tcross | 24 +- .../src/main.tcross | 234 +++++++++++++++- .../src/pages/personal-from-yt-temp.tcross | 26 ++ .../src/YouTubeDownloader.tcross | 258 +++++++++++++++--- Tesses.YouTubeDownloader/src/ids.tcross | 46 ++++ .../videodownload/audioonlydownload.tcross | 2 +- .../videodownload/noconvertdownload.tcross | 2 +- .../src/videodownload/sdvideodownload.tcross | 2 +- .../videodownload/videoonlydownload.tcross | 2 +- 25 files changed, 870 insertions(+), 48 deletions(-) create mode 100644 Tesses.YouTubeDownloader.Server/res/offline-progress.html create mode 100644 Tesses.YouTubeDownloader.Server/res/offline.html create mode 100644 Tesses.YouTubeDownloader.Server/res/offline.js create mode 100644 Tesses.YouTubeDownloader.Server/res/service_worker.js create mode 100644 Tesses.YouTubeDownloader.Server/res/site.webmanifest create mode 100644 Tesses.YouTubeDownloader.Server/res/tytd-1024.png rename Tesses.YouTubeDownloader.Server/res/{icon.png => tytd-128.png} (100%) create mode 100644 Tesses.YouTubeDownloader.Server/res/tytd-192.png create mode 100644 Tesses.YouTubeDownloader.Server/res/tytd-256.png create mode 100644 Tesses.YouTubeDownloader.Server/res/tytd-384.png create mode 100644 Tesses.YouTubeDownloader.Server/res/tytd-512.png create mode 100644 Tesses.YouTubeDownloader.Server/src/components/queuesz.tcross create mode 100644 Tesses.YouTubeDownloader.Server/src/pages/personal-from-yt-temp.tcross diff --git a/README.md b/README.md index faf1e02..bf43c2b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ My web based YouTube downloader that I created in 2025 writen in [CrossLang](htt [Website](https://crosslang.tesseslanguage.com/software/webapps/tytd2025/) # Features -- Uses [SQLite3](https://www.sqlite.org/) for it's database (embedded into TessesFramework) +- PWA with the ability to add videos when server is down - Can download videos, playlists and channels (you need a channel url like this [https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A](https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A)) - Can subscribe to channels - Can create playlists (that are stored on the server) @@ -18,6 +18,19 @@ My web based YouTube downloader that I created in 2025 writen in [CrossLang](htt - Can download YouTube videos either Low quality (but doesn't) require [ffmpeg](https://ffmpeg.org/), you can also download individual streams (also doesn't need [ffmpeg](https://ffmpeg.org/)), or to MP4 (doesn't work on wii due to libx264 having illegal instruction), MKV (so no transcode), MP3 or FLAC (these do need [ffmpeg](https://ffmpeg.org/) in your PATH however) - Runs on the Wii using the [Wii Linux Continuation Project](https://wiibrew.org/wiki/Wii-Linux#Wii_Linux_Continuation_Project) (albeit extremely slowly, despite this that's where I run it) +# What this project uses (attribution) +I don't feel like storing their licenses in my project, so I link to their projects instead + +- [SQLite3](https://www.sqlite.org/) for it's database (embedded into TessesFramework) +- [BeerCSS](https://www.beercss.com/) for its webui, licensed under MIT +- [HTMX](https://htmx.org/) for the SPA experience +- [FFmpeg](https://ffmpeg.org/) if you convert the videos, uses the cli +- [YouTubeExplode](https://github.com/Tyrrrz/YoutubeExplode) for some of its json payloads +- [NewPipe](https://newpipe.net/) for some of its json payloads +- [Material Symbols Font](https://fonts.google.com/icons) (wget from beercss css files) +- [VideoJS](https://videojs.org/) for video player +- [CrossLang](https://crosslang.tesseslanguage.com/) My programming language + ## To Install Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html) diff --git a/Tesses.YouTubeDownloader.Server/cross.json b/Tesses.YouTubeDownloader.Server/cross.json index d710983..b546c35 100644 --- a/Tesses.YouTubeDownloader.Server/cross.json +++ b/Tesses.YouTubeDownloader.Server/cross.json @@ -1,5 +1,5 @@ { - "icon": "icon.png", + "icon": "tytd-128.png", "info": { "description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)", "maintainer": "Mike Nolan", @@ -14,5 +14,6 @@ "project_dependencies": [ "..\/Tesses.YouTubeDownloader" ], - "version": "1.0.0.0-dev" + "version": "1.0.0.1-prod", + "compTime": "secure" } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/offline-progress.html b/Tesses.YouTubeDownloader.Server/res/offline-progress.html new file mode 100644 index 0000000..a6161d3 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/offline-progress.html @@ -0,0 +1,4 @@ +
+

Cannot connect to the TYTD2025 server.

+ Refresh page to add to offline queue +
\ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/offline.html b/Tesses.YouTubeDownloader.Server/res/offline.html new file mode 100644 index 0000000..89d0294 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/offline.html @@ -0,0 +1,106 @@ + + + + + + TYTD2025 is Offline + + + + + + + +
+
+
+
+ tytd-logo +
+
+
+
+
+
+

Cannot connect to the TYTD2025 server.

+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +
+
+ + + arrow_drop_down +
+
+
+ +
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/offline.js b/Tesses.YouTubeDownloader.Server/res/offline.js new file mode 100644 index 0000000..9a26e66 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/offline.js @@ -0,0 +1,54 @@ +function addOffline(url,res) +{ + var videos = JSON.parse(localStorage.getItem("videos") ?? "[]"); + videos.push({ + url: url, + res: res + }); + localStorage.setItem('videos',JSON.stringify(videos)); +} + +async function syncOffline() +{ + const json = localStorage.getItem("videos") ?? "[]"; + if(json !== "[]") + { + const resp = await fetch('/api/v1/add',{ + body: json, + headers: { + 'Content-Type': 'application/json' + }, + method: "POST" + }); + if(resp.ok) + { + localStorage.removeItem('videos'); + } + } +} + +if(navigator.online) +{ + syncOffline(); +} + +window.addEventListener('online',()=>{ + syncOffline(); +}); + +async function getPersonalTempLink() +{ + const searchParams = new URLSearchParams(window.location.search); + const ent = searchParams.get("name"); + if(ent) + { + const resp=await fetch(`./api/v1/personal_tmp_link?name=${encodeURIComponent(ent)}`); + if(resp.ok) + { + const text = await resp.text(); + the_playlist_url.innerText = text; + the_playlist_url.href = text; + ui("#dialog"); + } + } +} diff --git a/Tesses.YouTubeDownloader.Server/res/service_worker.js b/Tesses.YouTubeDownloader.Server/res/service_worker.js new file mode 100644 index 0000000..1d32a87 --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/service_worker.js @@ -0,0 +1,42 @@ +const assets = ["<@ASSETS@>"]; + +const staticCacheName = "tytd-static-<@BUILD_TIME@>"; + +self.addEventListener('install',evt => { + evt.waitUntil( + caches.open(staticCacheName).then(cache =>{ + cache.addAll(assets); + }) + ); +}); + +self.addEventListener('activate',(evt)=>{ + evt.waitUntil( + caches.keys().then(keys => { + return Promise.all(keys.filter(key => key !== staticCacheName).map(key => caches.delete(key))); + }) + ); +}); + +self.addEventListener('fetch', evt => { + const uri = new URL(evt.request.url); + + evt.respondWith( + + caches.match(evt.request).then(cacheRes=>{ + return cacheRes || fetch(evt.request); + }).catch(()=>{ + if(uri.pathname === '/queue-size') + { + return new Response('?',{ + headers: { 'Content-Type': 'text/html' } + }); + } + if(uri.pathname === "/progress") + { + return caches.match("/offline-progress.html"); + } + return caches.match('/offline.html'); + }) + ); +}); \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/site.webmanifest b/Tesses.YouTubeDownloader.Server/res/site.webmanifest new file mode 100644 index 0000000..cb05eaa --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/res/site.webmanifest @@ -0,0 +1,73 @@ +{ + "short_name": "TYTD2025", + "name": "Tesses YouTubeDownloader 2025", + "start_url": "/", + "display": "standalone", + "theme_color": "#ffb2be", + "background_color": "#241e1f", + "description": "A web based YouTube archiver to preserve videos that might be removed", + "icons": [ + { + "src": "/tytd.svg", + "sizes": "192x192 256x256 384x384 512x512", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/tytd-128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "/tytd-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/tytd-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/tytd-384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "/tytd-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/tytd-1024.png", + "sizes": "1024x1024", + "type": "image/png" + } + ], + "shortcuts": [ + { + "name": "Open Downloads", + "short_name": "Downloads", + "description": "Open Downloads page", + "url": "/downloads" + }, + { + "name": "Open Installed Plugins", + "short_name": "Installed plugins", + "description": "Open the installed plugins page", + "url": "/plugins" + }, + { + "name": "Open Download Plugins", + "short_name": "Download plugins", + "description": "Open the download plugins page", + "url": "/plugins-download" + }, + { + "name": "Open Settings", + "short_name": "Settings", + "description": "Open settings page", + "url": "/settings" + } + ] +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/res/tytd-1024.png b/Tesses.YouTubeDownloader.Server/res/tytd-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..24b8cdd28fa76ac001aaf07ba62499b22eddc19a GIT binary patch literal 23091 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 HQ$iB}&5nh- literal 0 HcmV?d00001 diff --git a/Tesses.YouTubeDownloader.Server/res/icon.png b/Tesses.YouTubeDownloader.Server/res/tytd-128.png similarity index 100% rename from Tesses.YouTubeDownloader.Server/res/icon.png rename to Tesses.YouTubeDownloader.Server/res/tytd-128.png diff --git a/Tesses.YouTubeDownloader.Server/res/tytd-192.png b/Tesses.YouTubeDownloader.Server/res/tytd-192.png new file mode 100644 index 0000000000000000000000000000000000000000..7fa95394491018820f315256706de6c039890617 GIT binary patch literal 2907 zcmdT``#aN*7k}?!gjo$uxtCkTM^m|Otm1?CxE7L0j6Rji%%yFrZG0jnL_#t}eYz;O zF61(!T5M{3(I(3JvZ!sjHMfOteg1~;58v;3o^xK$dCqg5^E~G{uk*aJ{e0Zvs+&~- z0DyaXxCZ=0_Agg~{fq%4vC2PT9ns@((og(lAmA?w>Sv+`_jKKRkbFb-AuV=Ltp9q3 zpXasBs+g1r>&;C}jDPW=GkxnNZj-c=ZpDT*M58Q>O1Fw~khi_H1UaC6Uey6TuZ+^V6|FETwgx0zcRAc3Lo6qt^{GlQ_k)|lP7=~SWKherx zT3MD%?FL_oEDuBA#*q-CK^@>e2k_Vf!#zL)0hdAfC)Mc(qk!9+|3^!YJ}C9N#jh|{ zAp{Bh`xkXWw~KAfgzCX$rF*rYlMfhQ#~KD9#dt)~0ofM%HPEo=u|b z36h?gr;wardTDCpBvjogYd?yC{S>vPOgkB%kjj)Xen8J=6?GEU*K52~%d`<&34^@Kq!KZ0Wuv4J+} zYDP!qX9U2hGHOjm{jV>sK-QYL0t`dS#7)3?B@e-|k?G?z6}H*@p#lPY&$_jdPtReH<2>QBAMq zmID~Y%kLK-_K`(tsLgq0D=2S(#Fk?Wf2tnQi*s!7t_*x5U4=c>%mzxfwMbAfh zI!{kDt>MsgNza2T!E~hP*1o0v2`|Gp26W$)-<9WjtUM4Z6wt*n$91h{uNL&}VZ@B{ z?sUy+amVaEX%m50B|n*|{HK>mcHQ2=-$NFkbnFjb9*I28WvZS-|400ih_n} znwXQY@@OZJRxQ|!nS}MW*TI2ff+6O?U@sKUKL$~_tQphmm2XE;qD!pbv#CXwlaL9{dqq%WvM){mxg~em@=1LV8 zOFN6DyH%UOuYrPyie$SW@aUUe!nIv5&#|rgCsd>h%hBz$aWz=kJSV2|C7N!MQWz&m zjW}p15|Ih3!&P{?aT*~ruKuuvgSyB}Jzpl4vrCxl`w7NL8wik!Aw$JTWAtR;v*3@g z_1wIh@WO3JEkR};P*j%j%7n^zsf(gbIMKS}LT&!&ud_!T+qcvK$*FpTCE<`8SCv0^?sIUI#RbqRdzh0C zvg;y?I#vwtF0%T2{-N4-5#X4dkMd18ie2NUSItLm&d|POl3ue7;~x`z|F_NrnVXe+ zKypRtmQx{vy&~Br)dUC`MXr5U!h3!>t?kodhP0x>a*yzdk=uky4Sry@-=?2@%jZu5 z+`2oIHAT%Ox57pFBdbw8iWiL97N)7cWP!JXA%z`!>lzzuF_Z+ojC$II=FGvw3Af`} z-ph>-Ki%SpzZW)w!iuuqRQrGbN{mWZW3vcxLsrTUC~T*|D?b|D#ks5oePQ#Cy5bIr za8K+`&ahW@E1dm4rsuPJ<>Zxk4s;>h|16)uU67 z8aJX^>kLQ51dpA*f{q(ycIUQ{e>l8es&F_;CWh^Ufnwe^|@CSF29OM+Zob7vE}a zdORZ6do<%r-!3BFLZ5%d4*fZT7k)FK8EFx+oi>@ZXwH?kG?oMJx?lS^mOWqk*WP!r z*?1>G(y!nC5P6eqvXkVv^NrlH`+AB^YGrJ96baMYR{GE0fIQyo9_LSyodZ|OSFKDh z^FFs5f$OA+bPQ#_1<@q<*+i8Z5nF|#(yR>%t{vXHpX{%;(>?sNY8y6sz?N22_HeK^ zjNcd30rgWGSn-%Qd*&uYb0Tsil!8O=$!PdA7xR+E&l4UsFXFAC7Tv6hNM+lzoSuGa zWq8|zLXH#6KPkh;#EV3k9;GuG>uV#8`WWs#SN@Uh67y13XQrTo(^g-|xU!0!Ks9&g4&1}n$7Ay;WJx9J#OH8^T?FTs4)=J#B zwk-RsZ9>3OalSa1QJI444vCOL9e&pVjg%wCCt%ZMnXJiWeN$vaUPms-eH8q0#7f3B zI3lKq(k0L1gL*-`>(yp#>iXjk3t1T4;_;2|?lXJfW=NcqW?Oek~Oy|j864}$u$F*i(MEZXK DPogtI literal 0 HcmV?d00001 diff --git a/Tesses.YouTubeDownloader.Server/res/tytd-256.png b/Tesses.YouTubeDownloader.Server/res/tytd-256.png new file mode 100644 index 0000000000000000000000000000000000000000..79b9cdab8fb14805179bd69a49f0f1f22955aa95 GIT binary patch literal 4037 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 literal 0 HcmV?d00001 diff --git a/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross b/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross index 51a5c9a..f23e179 100644 --- a/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross +++ b/Tesses.YouTubeDownloader.Server/src/components/personallistdescription.tcross @@ -20,11 +20,19 @@ func Components.PersonalListDescription(tytd,name,editing) + +
The url
+
+ +
+
diff --git a/Tesses.YouTubeDownloader.Server/src/components/progress.tcross b/Tesses.YouTubeDownloader.Server/src/components/progress.tcross index ea20ea9..0c2fba0 100644 --- a/Tesses.YouTubeDownloader.Server/src/components/progress.tcross +++ b/Tesses.YouTubeDownloader.Server/src/components/progress.tcross @@ -1,4 +1,4 @@ -var progress=0; + func Components.Progress(tytd) { var vid = tytd.CurrentVideo; @@ -8,6 +8,5 @@ func Components.Progress(tytd)

{vid.Channel}

; - progress++; return html; } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/queuesz.tcross b/Tesses.YouTubeDownloader.Server/src/components/queuesz.tcross new file mode 100644 index 0000000..39b57dc --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/components/queuesz.tcross @@ -0,0 +1,12 @@ + +func Components.QueueSZ(tytd) +{ + var vid = tytd.CurrentVideo; + tytd.Mutex.Lock(); + + + var html = {tytd.VideoQueueCount}; + + tytd.Mutex.Unlock(); + return html; +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/components/shell.tcross b/Tesses.YouTubeDownloader.Server/src/components/shell.tcross index cda4c37..0bee664 100644 --- a/Tesses.YouTubeDownloader.Server/src/components/shell.tcross +++ b/Tesses.YouTubeDownloader.Server/src/components/shell.tcross @@ -1,11 +1,21 @@ func Components.Shell(title, html, page, $mypage) { + if(TypeOf(mypage) != "Path") mypage = /; var index = (/).MakeRelative(mypage).ToString(); if(index == "") index = "./"; + const service_worker_script_path = (/"service_worker.js").MakeRelative(mypage).ToString(); + const service_worker_script = $""; + var pages = [ { text="Home", @@ -47,7 +57,11 @@ func Components.Shell(title, html, page, $mypage) TYTD - {title} + + + + @@ -56,7 +70,15 @@ func Components.Shell(title, html, page, $mypage) {page.icon} + + + + ? + + +
{page.text}
+
@@ -69,7 +91,7 @@ func Components.Shell(title, html, page, $mypage)
- + ; } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader.Server/src/main.tcross b/Tesses.YouTubeDownloader.Server/src/main.tcross index 0c90b65..24cdada 100644 --- a/Tesses.YouTubeDownloader.Server/src/main.tcross +++ b/Tesses.YouTubeDownloader.Server/src/main.tcross @@ -1,3 +1,5 @@ +const BUILD_TIME = comptime DateTime.NowEpoch; + var TYTDResources = [ {path="/beer.min.css",value=embed("beer.min.css")}, {path="/beer.min.js",value=embed("beer.min.js")}, @@ -8,12 +10,31 @@ var TYTDResources = [ {path="/htmx.min.js",value=embed("htmx.min.js")}, {path="/favicon.ico",value=embed("favicon.ico")}, {path="/tytd.svg",value=embed("tytd.svg")}, + {path="/tytd-128.png",value=embed("tytd-128.png")}, + {path="/tytd-192.png",value=embed("tytd-192.png")}, + {path="/tytd-256.png",value=embed("tytd-256.png")}, + {path="/tytd-384.png",value=embed("tytd-384.png")}, + {path="/tytd-512.png",value=embed("tytd-512.png")}, + {path="/tytd-1024.png",value=embed("tytd-1024.png")}, {path="/loading-indicator.svg",value=embed("loading-indicator.svg")}, {path="/theme.css",value=embed("theme.css")}, {path="/video.min.js",value=embed("video.min.js")}, {path="/video-js.css",value=embed("video-js.css")}, - {path="/wavy.svg",value=embed("wavy.svg")} + {path="/wavy.svg",value=embed("wavy.svg")}, + {path="/site.webmanifest",value=embed("site.webmanifest")}, + {path="/offline-progress.html",value=embed("offline-progress.html")}, + {path="/offline.html",value=embed("offline.html")}, + {path="/offline.js", value=embed("offline.js")}, ]; + +const fileNames = []; +each(var item : TYTDResources) +{ + fileNames.Add(item.path); +} + +const service_worker_str = embed("service_worker.js").ToString().Replace("[\"<@ASSETS@>\"]",Json.Encode(fileNames)).Replace("<@BUILD_TIME@>",BUILD_TIME.ToString()); + var times=1; class TYTDApp { @@ -29,21 +50,66 @@ class TYTDApp { public TYTDApp() { - + Console.WriteLine($"Built at {new DateTime(BUILD_TIME).ToString()}"); var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir()); this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir())); this.TYTD.Start(); } - public Handle(ctx) { + if(ctx.Path == "/service_worker.js" || fileNames.Contains(ctx.Path)) + { + const inm=ctx.RequestHeaders.TryGetFirst("If-None-Match"); + if(TypeIsString(inm)) + { + const strs = inm.Split(", "); + each(var item : strs) + { + if(item == $"W/\"{BUILD_TIME}\"" || item == $"\"{BUILD_TIME}\"") + { + ctx.StatusCode = 304; + ctx.WriteHeaders(); + return true; + } + } + } + ctx.WithHeader("ETag",$"W/\"{BUILD_TIME}\""); + } + if(ctx.Path == "/service_worker.js") + { + ctx.WithMimeType(Net.Http.MimeType("service_worker.js")).SendText(service_worker_str); + return true; + } + if(ctx.Path == "/api/v1/auth") + { + if(ctx.Method == "POST") + { + const req=ctx.ReadJson(); + const result = this.TYTD.Auth(req.username, req.password); + if(result) + { + ctx.SendJson({ + success = true, + flags = result.flags + }); + } + else { + ctx.SendJson({ + success=false + }); + } + return true; + } + + return false; + } if(ctx.Path == "/api/v1/login") { if(ctx.Method == "POST") { const req=ctx.ReadJson(); - const result = this.TYTD.Login(req.username, req.password); + const result = this.TYTD.Login(req.username, req.password, false); if(result) { ctx.SendJson({ @@ -70,14 +136,17 @@ class TYTDApp { const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/"; const username = ctx.QueryParams.TryGetFirst("username") ?? ""; const password = ctx.QueryParams.TryGetFirst("password") ?? ""; + + if(ctx.Method == "POST") { - const result = this.TYTD.Login(username, password); + const result = this.TYTD.Login(username, password,true); if(result) { ctx.StatusCode = 303; - ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Strict").SendRedirect("/"); + var date = new DateTime(DateTime.NowEpoch + UserFlags.Expires); + ctx.WithHeader("Set-Cookie",$"Session={result}; SameSite=Lax; Expires={date.ToHttpDate()}; HttpOnly").SendRedirect(redirect); return true; } else incorrect=true; @@ -97,6 +166,25 @@ class TYTDApp { return true; } } + if(ctx.Path == "/progress") + { + ctx.WithMimeType("text/html").SendText( +
+

You have been logged out

+ Login +
+ ); + return true; + } + if(ctx.Path == "/sso") + { + const app = ctx.QueryParams.TryGetFirst("app") ?? ""; + const token = ctx.QueryParams.TryGetFirst("token") ?? ""; + const path = $"/sso?app={Net.Http.UrlEncode(app)}&token={Net.Http.UrlEncode(token)}"; + ctx.StatusCode = 307; + ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(path)}"); + return true; + } ctx.StatusCode = 307; ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}"); return true; @@ -208,7 +296,6 @@ class TYTDApp { return true; } } - if(ctx.Path == "/welcome") { const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings"; @@ -245,11 +332,49 @@ class TYTDApp { return true; } } + if(ctx.Path == "/") { ctx.WithMimeType("text/html").SendText(Pages.Index(this.TYTD)); return true; } + else if(ctx.Path == "/sso") + { + const app = ctx.QueryParams.TryGetFirst("app"); + const token = ctx.QueryParams.TryGetFirst("token"); + if(TypeIsString(app) && TypeIsString(token)) + { + + const sso = this.TYTD.GetSSO(app); + if(TypeIsDictionary(sso)) + { + const user = this.TYTD.WhoAmI(ctx); + if(TypeIsDictionary(user) && user.flags != 0) + { + const postJson = { + username = user.username, + flags = user.flags, + token = token, + sso_app_key = sso.sso_app_key + }; + + const resp = Net.Http.MakeRequest(sso.service_auth_post, { + Method = "POST", + Body = Net.Http.TextHttpRequestBody(postJson.ToString(),"application/json") + }); + + if(resp.StatusCode >= 200 && resp.StatusCode <= 299) + { + ctx.StatusCode = 307; + ctx.SendRedirect($"{sso.service_auth_redirect}{Net.Http.UrlEncode(token)}"); + return true; + } + } + } + } + ctx.StatusCode = 401; + return false; + } else if(ctx.Path == "/passwd") { var incorrect=false; @@ -265,6 +390,7 @@ class TYTDApp { ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect)); } + else if(ctx.Path == "/newuser") { var error = null; @@ -340,6 +466,36 @@ class TYTDApp { } return false; } + else if(ctx.Path == "/api/v1/register_sso") + { + if(ctx.Method!="POST") + { + ctx.WithMimeType("application/json").SendJson({ + success=false, + reason = "Method must be post", + type = "method" + }); + return true; + } + if(!UserFlags.IsAdmin(this.TYTD.IsLoggedIn(ctx))) + { + ctx.WithMimeType("application/json").SendJson({ + success=false, + reason = "You are either not logged in or not admin", + type ="auth" + }); + return true; + } + + const json = ctx.ReadJson(); + + const resp = this.TYTD.RegisterSSO(json); + + ctx.WithMimeType("application/json").SendJson( + resp + ); + return true; + } else if(ctx.Path == "/api/v1/download") { var v = ctx.QueryParams.TryGetFirst("v"); @@ -371,6 +527,20 @@ class TYTDApp { ctx.WithMimeType("application/json").SendJson(jo); return true; } + else if(ctx.Path == "/api/v1/add") + { + if(ctx.Method=="POST") + { + const json = ctx.ReadJson(); + each(var item : json) + { + this.TYTD.DownloadItem(item.url,item.res); + } + ctx.StatusCode=204; + ctx.WriteHeaders(); + return true; + } + } else if(ctx.Path == "/api/v1/video.json") { var id = ctx.QueryParams.TryGetFirst("v"); @@ -439,6 +609,20 @@ class TYTDApp { } return true; } + else if(ctx.Path == "/api/v1/personal_tmp_link") + { + const name = ctx.QueryParams.TryGetFirst("name"); + if(TypeIsString(name)) + { + const ents = this.TYTD.GetPersonalListTempUrl(name); + if(TypeIsString(ents)) + { + ctx.WithMimeType("text/plain").SendText(ents); + return true; + } + + } + } else if(ctx.Path == "/api/v1/personal") { /* @@ -714,6 +898,36 @@ class TYTDApp { ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx)); return true; } + + else if(ctx.Path == "/watch_videos") + { + const video_ids = ctx.QueryParams.TryGetFirst("video_ids"); + if(TypeIsString(video_ids)) + { + if(ctx.Method=="GET") + { + ctx.WithMimeType("text/html").SendText(Pages.YouTubeAnonyPlaylist(video_ids)); + return true; + } + if(ctx.Method=="POST") + { + const name = ctx.QueryParams.TryGetFirst("name"); + if(TypeIsString(name)) + { + const nameParts=video_ids.Split(","); + Console.WriteLine(nameParts); + each(var item : nameParts) + { + + this.TYTD.AddToPersonalList(name,item); + } + Console.WriteLine(name); + ctx.SendRedirect($"/list?name={Net.Http.UrlEncode(name)}",303); + return true; + } + } + } + } else if(ctx.Path == "/playlist") { ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx)); @@ -802,6 +1016,12 @@ class TYTDApp { ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD)); return true; } + else if(ctx.Path == "/queue-size") + { + + ctx.WithMimeType("text/html").SendText(Components.QueueSZ(this.TYTD)); + return true; + } else if(ctx.Path == "/plugins") { ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx)); diff --git a/Tesses.YouTubeDownloader.Server/src/pages/personal-from-yt-temp.tcross b/Tesses.YouTubeDownloader.Server/src/pages/personal-from-yt-temp.tcross new file mode 100644 index 0000000..710421c --- /dev/null +++ b/Tesses.YouTubeDownloader.Server/src/pages/personal-from-yt-temp.tcross @@ -0,0 +1,26 @@ +func Pages.YouTubeAnonyPlaylist(video_ids) +{ + const html = +
+
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
; + return Components.Shell("Create playlist",html ,1); +} \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross index 55e5f0d..f5096d3 100644 --- a/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross +++ b/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross @@ -35,6 +35,8 @@ class UserFlags { return false; } static getITTR() 35000; + + static getExpires() 86400 * 7; } class TYTD.Downloader { @@ -72,6 +74,22 @@ class TYTD.Downloader { ^/ public DownloadVideo(id,$res) { + const theVideoId = TYTD.GetVideoId(id); + if(TypeIsString(theVideoId)) + { + const ent = { + Id = theVideoId, + Resolution = res, + Cancel = false, + Type = "Video" + }; + this.BeforeQueued.Invoke(this, ent); + if(ent.Cancel) { + this.LOG("Adding video canceled: {theVideoId}"); + return; + } + } + this.LOG($"Adding video: {TYTD.GetVideoId(id)}, original val: {id}"); switch(res) { @@ -121,6 +139,19 @@ class TYTD.Downloader { if(pid != null) { + + const ent = { + Id = pid, + Resolution = res, + Cancel = false, + Type = "Playlist" + }; + this.BeforeQueued.Invoke(this, ent); + if(ent.Cancel) { + this.LOG("Adding playlist canceled: {pid}"); + return; + } + this.LOG($"Adding playlist: https://www.youtube.com/playlist?list={pid}"); this.PlaylistQueue.Push(()=>{ each(var item : this.QueryPlaylistItems(pid,true)) @@ -145,6 +176,18 @@ class TYTD.Downloader { if(cid != null) { + + const ent = { + Id = cid, + Resolution = res, + Cancel = false, + Type = "Channel" + }; + this.BeforeQueued.Invoke(this, ent); + if(ent.Cancel) { + this.LOG("Adding channel canceled: {cid}"); + return; + } this.LOG($"Adding channel: https://www.youtube.com/channel/{cid}"); this.PlaylistQueue.Push(()=>{ each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false)) @@ -182,6 +225,7 @@ class TYTD.Downloader { var pid = TYTD.GetPlaylistId(url); var cid = TYTD.GetChannelId(url); + var tmp = TYTD.GetYouTubeTempPlaylist(url); if(vid != null) { @@ -195,7 +239,13 @@ class TYTD.Downloader { { this.DownloadChannel(cid,res); } - + else if(tmp != null) + { + each(var item : tmp) + { + this.DownloadItem(item,res); + } + } } /^ Redirect url to info page @@ -204,6 +254,8 @@ class TYTD.Downloader { { var vid = TYTD.GetVideoId(url); var pid = TYTD.GetPlaylistId(url); + var cid = TYTD.GetChannelId(url); + var tmp = TYTD.GetYouTubeTempPlaylistRedirect(url); if(vid != null) { @@ -217,6 +269,10 @@ class TYTD.Downloader { { return $"./channel?id={Net.Http.UrlEncode(cid)}"; } + else if(tmp != null) + { + return tmp; + } return "./"; } @@ -590,6 +646,29 @@ class TYTD.Downloader { } return ""; } + public GetPersonalListTempUrl(name) + { + this.Muxex.Lock(); + var db = this.OpenDB(); + var items = []; + + var lists = Sqlite.Exec(db, $"SELECT * FROM personal_list_entries WHERE listName = {Sqlite.Escape(name)};"); + Sqlite.Close(db); + this.Mutex.Unlock(); + if(TypeIsList(lists)) + { + var url = $"https://www.youtube.com/watch_videos?video_ids="; + var first = true; + each(var item : lists) + { + if(!first) url += $",{item.videoId}"; + else url += item.videoId; + first=false; + } + return url; + } + return null; + } /^ ^/ public GetPersonalListContents(name, offset, count) { @@ -716,6 +795,8 @@ class TYTD.Downloader { public VideoProgress = new TYTD.Event(); + public BeforeQueued = new TYTD.Event(); + public CurrentVideo = { Title = "N/A", Channel = "N/A", @@ -930,6 +1011,7 @@ class TYTD.Downloader { while(this.Running) { try { + this.FlushExpired(); var res = this.PlaylistQueue.Pop(); if(TypeOf(res) != "Null") @@ -937,7 +1019,7 @@ class TYTD.Downloader { res(); } - var currentTime = DateTime.NowEpoch; + var currentTime = DateTime.NowEpoch ?? 0; var bt = this.Config.BellTimer; @@ -1057,26 +1139,27 @@ class TYTD.Downloader { if(TypeOf(res) != "Null") { - res.TYTD = this; - res.Progress = (progress)=>{ - this.CurrentVideoProgress = progress; - this.VideoProgress.Invoke(this, { - Video = res.Video, - progress + if(TypeIsDefined(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{ @@ -1297,7 +1380,10 @@ class TYTD.Downloader { Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS plugin_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, extension TEXT, key TEXT, value TEXT, UNIQUE(extension,key) ON CONFLICT REPLACE);"); 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 users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, password_salt TEXT, flags INTEGER);"); - Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key TEXT UNIQUE);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, accountId INTEGER, key TEXT UNIQUE, expires INTEGER);"); + Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS sso (id INTEGER PRIMARY KEY AUTOINCREMENT, service_name TEXT UNIQUE, service_pretty_name TEXT, sso_app_key TEXT UNIQUE, service_auth_post TEXT, service_auth_redirect TEXT);"); + Sqlite.Exec(db,"ALTER TABLE sessions ADD expires INTEGER;"); + Sqlite.Exec(db,"DELETE FROM sessions WHERE expires IS NULL;"); var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';"); if(TypeOf(config) == "List" && config.Length>0) { @@ -1798,7 +1884,6 @@ class TYTD.Downloader { const token = GetSessionToken(ctx); if(TypeIsString(token)) { - this.Mutex.Lock(); const db = this.OpenDB(); Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};"); @@ -1831,7 +1916,17 @@ class TYTD.Downloader { } return null; } + public FlushExpired() + { + this.Mutex.Lock(); + const db = this.OpenDB(); + const currentTime = DateTime.NowEpoch ?? 0; + const sessions = Sqlite.Exec(db, $"DELETE FROM sessions WHERE expires != 0 AND expires < {currentTime};"); + Sqlite.Close(db); + this.Mutex.Unlock(); + + } public IsLoggedIn(ctx) { this.Mutex.Lock(); @@ -1852,13 +1947,32 @@ class TYTD.Downloader { if(TypeIsString(sessionToken)) { const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(sessionToken)};"); + if(TypeIsList(res)) each(var item : res) { + const whenItExpires = ParseLong(item.expires); + const currentTime = DateTime.NowEpoch ?? 0; + if(whenItExpires != 0 && currentTime < whenItExpires && (whenItExpires - currentTime) < (UserFlags.Expires-3600)) + { + const expiry = currentTime + UserFlags.Expires; + Sqlite.Exec(db, $"UPDATE sessions SET expires = {expiry} WHERE key = {Sqlite.Escape(sessionToken)};"); + + ctx.WithHeader("Set-Cookie",$"Session={sessionToken}; SameSite=Lax; Expires={new DateTime(expiry).ToHttpDate()}; HttpOnly"); + } + else if(whenItExpires != 0 && currentTime >= whenItExpires) + { + + Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(sessionToken)};"); + item.flags = 0; + } Sqlite.Close(db); this.Mutex.Unlock(); + return ParseLong(item.flags) | 1; + + } } @@ -1867,9 +1981,26 @@ class TYTD.Downloader { return 0; } + public GetSSO(appname) + { + this.Mutex.Lock(); + const db = this.OpenDB(); + const res = Sqlite.Exec(db, $"SELECT * FROM sso WHERE service_name = {Sqlite.Escape(appname)}"); + Sqlite.Close(db); + this.Mutex.Unlock(); + if(TypeIsList(res)) + { + each(var item : res) + { + return item; + } + } + return null; + } + public WhoAmI(ctx) { - his.Mutex.Lock(); + this.Mutex.Lock(); const db = this.OpenDB(); const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;"); var noAccounts=true; @@ -1891,6 +2022,20 @@ class TYTD.Downloader { if(TypeIsList(res)) each(var item : res) { + const whenItExpires = ParseLong(item.expires); + const currentTime = DateTime.NowEpoch ?? 0; + if(whenItExpires != 0 && currentTime < whenItExpires && (whenItExpires - currentTime) < (UserFlags.Expires-3600)) + { + const expiry = currentTime + UserFlags.Expires; + Sqlite.Exec(db, $"UPDATE sessions SET expires = {expiry} WHERE key = {Sqlite.Escape(sessionToken)};"); + + ctx.WithHeader("Set-Cookie",$"Session={sessionToken}; SameSite=Lax; Expires={new DateTime(expiry).ToHttpDate()}"); + } + else if(whenItExpires != 0 && currentTime >= whenItExpires) + { + Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(sessionToken)};"); + item.flags = "0"; + } Sqlite.Close(db); this.Mutex.Unlock(); item.flags = ParseLong(item.flags); @@ -1905,9 +2050,9 @@ class TYTD.Downloader { public Passwd(ctx, oldPassword, newPassword, logout) { const whoami = this.WhoAmI(ctx); - if(TypeIsDictionary(user) && TypeIsString(item.password_salt)) + if(whoami.flags != 0 && TypeIsDictionary(whoami) && TypeIsString(whoami.password_salt)) { - var salt = Crypto.Base64Decode(item.password_salt); + var salt = Crypto.Base64Decode(whoami.password_salt); var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384); var hashStr = Crypto.Base64Encode(hash); @@ -1937,17 +2082,45 @@ class TYTD.Downloader { } } + return { success=false, reason = "Unable to login for some reason, maybe your token expired"}; } - public Login(username, password) + public Auth(username, password) { this.Mutex.Lock(); const db = this.OpenDB(); const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};"); + Sqlite.Close(db); + + this.Mutex.Unlock(); + if(TypeIsList(user)) + { + each(var item : user) + { + + var salt = Crypto.Base64Decode(item.password_salt); + var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384); + var hashStr = Crypto.Base64Encode(hash); + + if(item.password_hash == hashStr) + { + return {flags = ParseLong(item.flags)}; + } + + } + } + + return null; + } + public Login(username, password, doesExpire) + { + this.Mutex.Lock(); + const db = this.OpenDB(); + const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};"); + if(TypeIsList(user)) { each(var item : user) { - this.Mutex.Unlock(); var salt = Crypto.Base64Decode(item.password_salt); var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384); @@ -1956,18 +2129,22 @@ class TYTD.Downloader { if(item.password_hash == hashStr) { var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025"))); - this.Mutex.Lock(); - const dbCon = this.OpenDB(); - Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({item.id},{Sqlite.Escape(rand)});"); - Sqlite.Close(dbCon); + + const expires = doesExpire ? ((DateTime.NowEpoch??0) + UserFlags.Expires) : 0; + Sqlite.Exec(db, $"INSERT INTO sessions (accountId,key,expires) VALUES ({item.id},{Sqlite.Escape(rand)},{expires});"); + Sqlite.Close(db); this.Mutex.Unlock(); return rand; } + Sqlite.Close(db); + this.Mutex.Unlock(); return null; } } + Sqlite.Close(db); + this.Mutex.Unlock(); return null; } @@ -1992,6 +2169,25 @@ class TYTD.Downloader { return false; } - + public RegisterSSO(req) + { + this.Mutex.Lock(); + const db = this.OpenDB(); + /* + service_name TEXT UNIQUE, service_pretty_name TEXT, sso_app_key TEXT UNIQUE, service_auth_post TEXT, service_auth_redirect TEXT + */ + const resp = Sqlite.Exec(db, $"INSERT INTO sso (service_name, service_pretty_name, sso_app_key, service_auth_post, service_auth_redirect) VALUES ({Sqlite.Escape(req.service_name)},{Sqlite.Escape(req.service_pretty_name)},{Sqlite.Escape(req.sso_app_key)},{Sqlite.Escape(req.service_auth_post)}, {Sqlite.Escape(req.service_auth_redirect)});"); + Sqlite.Close(db); + this.Mutex.Unlock(); + if(TypeIsList(resp)) + { + return { success=true}; + } + else if(TypeIsString(resp)) + { + return { success = false, reason = resp , type="db"}; + } + return {success = false, reason = "Unknown", type ="db"}; + } } diff --git a/Tesses.YouTubeDownloader/src/ids.tcross b/Tesses.YouTubeDownloader/src/ids.tcross index e120754..7eba4c3 100644 --- a/Tesses.YouTubeDownloader/src/ids.tcross +++ b/Tesses.YouTubeDownloader/src/ids.tcross @@ -27,6 +27,7 @@ func TYTD.GetVideoId(v) return null; } + func TYTD.GetPlaylistId(pid) { func IsValidId(v) @@ -84,4 +85,49 @@ func TYTD.GetChannelId(cid) } return null; +} + +func TYTD.CreateYouTubeTempPlaylist(ids) +{ + var url = "https://www.youtube.com/watch_videos?video_ids="; + var first=true; + each(var item : ids) + { + if(!first) url += $"{url},{Net.Http.UrlEncode(item)}"; + else + url += Net.Http.UrlEncode(item); + first=false; + } + return url; +} +func TYTD.GetYouTubeTempPlaylistRedirect(url) +{ + if(url.Contains("/watch_videos?") && url.Contains("video_ids=")) + { + var queryPart = url.Split("?",true,2); + return $"/watch_videos?{queryPart[1]}"; + } + return null; +} +func TYTD.GetYouTubeTempPlaylist(url) +{ + if(url.Contains("/watch_videos?") && url.Contains("video_ids=")) + { + var queryPart = url.Split("?",true,2); + if(queryPart.Length == 2) + { + var queryParms =queryPart[1].Split("&"); + + each(var item : queryParms) + { + const vals = item.Split("=",true,2); + if(vals.Length == 2 && vals[0] == "video_ids") + { + return Net.Http.UrlDecode(vals[1]).Split(","); + + } + } + } + } + return null; } \ No newline at end of file diff --git a/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross index 6c7c4ca..89fe433 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/audioonlydownload.tcross @@ -22,7 +22,7 @@ class TYTD.AOVideoDownload : IVideoDownload { var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin"; if(this.tytd.Storage.FileExists(path)) { this.done = true; - return tytd; + return null; } var req = this.tytd.ManifestRequest(id).playerResponse; diff --git a/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross index 325be4e..9d600be 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/noconvertdownload.tcross @@ -29,7 +29,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload { var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin"; if(this.tytd.Storage.FileExists(path) && this.tytd.Storage.FileExists(pathA)) { this.done = true; - return tytd; + return null; } var req = this.tytd.ManifestRequest(id).playerResponse; diff --git a/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross index 7d247a1..b5a6e6a 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/sdvideodownload.tcross @@ -22,7 +22,7 @@ class TYTD.SDVideoDownload : IVideoDownload { var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4"; if(this.tytd.Storage.FileExists(path)) { this.done = true; - return tytd; + return null; } var req = this.tytd.ManifestRequest(id).playerResponse; diff --git a/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross b/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross index c20c545..2a48053 100644 --- a/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross +++ b/Tesses.YouTubeDownloader/src/videodownload/videoonlydownload.tcross @@ -22,7 +22,7 @@ class TYTD.VOVideoDownload : IVideoDownload { var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin"; if(this.tytd.Storage.FileExists(path)) { this.done = true; - return tytd; + return null; } var req = this.tytd.ManifestRequest(id).playerResponse;