Compare commits
8 Commits
02b10131f9
...
ede1f92981
| Author | SHA1 | Date | |
|---|---|---|---|
| ede1f92981 | |||
| 90f33d455f | |||
| ecb1496a54 | |||
| 39709662af | |||
| 29fdf6a2f3 | |||
| 44c87e1e50 | |||
| 8851f710fb | |||
| 28b7138547 |
15
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/)
|
[Website](https://crosslang.tesseslanguage.com/software/webapps/tytd2025/)
|
||||||
|
|
||||||
# Features
|
# 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 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 subscribe to channels
|
||||||
- Can create playlists (that are stored on the server)
|
- 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)
|
- 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)
|
- 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
|
## To Install
|
||||||
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
Install [crosslang](https://crosslang.tesseslanguage.com/downloads/index.html)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"icon": "icon.png",
|
"icon": "tytd-128.png",
|
||||||
"info": {
|
"info": {
|
||||||
"description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)",
|
"description": "Download YouTube Videos (using CrossLang, via web interface, great for homelabs)",
|
||||||
"maintainer": "Mike Nolan",
|
"maintainer": "Mike Nolan",
|
||||||
@@ -14,5 +14,6 @@
|
|||||||
"project_dependencies": [
|
"project_dependencies": [
|
||||||
"..\/Tesses.YouTubeDownloader"
|
"..\/Tesses.YouTubeDownloader"
|
||||||
],
|
],
|
||||||
"version": "1.0.0.0-dev"
|
"version": "1.0.0.6-dev",
|
||||||
|
"compTime": "secure"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div>
|
||||||
|
<h1>Cannot connect to the TYTD2025 server.</h1>
|
||||||
|
Refresh page to add to offline queue
|
||||||
|
</div>
|
||||||
106
Tesses.YouTubeDownloader.Server/res/offline.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TYTD2025 is Offline</title>
|
||||||
|
<link rel="stylesheet" href="/beer.min.css">
|
||||||
|
<link rel="stylesheet" href="/theme.css">
|
||||||
|
<script src="/offline.js" defer></script>
|
||||||
|
<script type="module" src="/beer.min.js" defer></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<img src="tytd-128.png" alt="tytd-logo">
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<h3>Cannot connect to the TYTD2025 server.</h3>
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<form>
|
||||||
|
|
||||||
|
<div class="field label border small fill">
|
||||||
|
<input id="url" type="text">
|
||||||
|
<label>Url or id</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="max">
|
||||||
|
<div class="field label suffix border fill">
|
||||||
|
<select id="res">
|
||||||
|
|
||||||
|
|
||||||
|
<option value="NoDownload">Don't Download</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="LowVideo" selected="">Low (muxed by YouTube)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="VideoOnly">Highest video (no audio)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="AudioOnly">Highest audio (no video)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="MP3">Convert to MP3</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="FLAC">Convert to FLAC</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="MP4">Convert to MP4</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="MKV">Mux to MKV (no transcoding)</option>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<option value="DontConvert">Don't convert or Mux</option>
|
||||||
|
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<label>Resolution</label>
|
||||||
|
<i>arrow_drop_down</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min">
|
||||||
|
<button id="btn">
|
||||||
|
<i>add</i>
|
||||||
|
Add when online
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
btn.onclick = (evt)=>{
|
||||||
|
evt.preventDefault();
|
||||||
|
addOffline(url.value, res.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
Tesses.YouTubeDownloader.Server/res/offline.js
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Tesses.YouTubeDownloader.Server/res/service_worker.js
Normal file
@@ -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('<span hx-trigger="every 5000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">?</span>',{
|
||||||
|
headers: { 'Content-Type': 'text/html' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(uri.pathname === "/progress")
|
||||||
|
{
|
||||||
|
return caches.match("/offline-progress.html");
|
||||||
|
}
|
||||||
|
return caches.match('/offline.html');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
73
Tesses.YouTubeDownloader.Server/res/site.webmanifest
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Tesses.YouTubeDownloader.Server/res/tytd-1024.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-192.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-256.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-384.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
Tesses.YouTubeDownloader.Server/res/tytd-512.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
@@ -20,11 +20,19 @@ func Components.PersonalListDescription(tytd,name,editing)
|
|||||||
</form>
|
</form>
|
||||||
</true>
|
</true>
|
||||||
<false>
|
<false>
|
||||||
|
<dialog id="dialog">
|
||||||
|
<h5>The url</h5>
|
||||||
|
<div><a id="the_playlist_url" href=""></a></div>
|
||||||
|
<nav class="right-align no-space">
|
||||||
|
<button data-ui="#dialog" class="transparent link">OK</button>
|
||||||
|
</nav>
|
||||||
|
</dialog>
|
||||||
<div class="row" id="description">
|
<div class="row" id="description">
|
||||||
<div class="max">
|
<div class="max">
|
||||||
<plink(description)>
|
<plink(description)>
|
||||||
</div>
|
</div>
|
||||||
<div class="min">
|
<div class="min">
|
||||||
|
<button onclick="getPersonalTempLink()"><i>link</i></button>
|
||||||
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
|
<button hx-get={$"./edit-personal-description?name={Net.Http.UrlEncode(name)}"} hx-target="#description" hx-swap="outerHTML"><i>edit</i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
var progress=0;
|
|
||||||
func Components.Progress(tytd)
|
func Components.Progress(tytd)
|
||||||
{
|
{
|
||||||
var vid = tytd.CurrentVideo;
|
var vid = tytd.CurrentVideo;
|
||||||
|
|
||||||
var html = <div hx-trigger="every 1500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
var html = <div hx-trigger="every 4500ms" hx-indicator="none" hx-get="./progress" hx-swap="outerHTML">
|
||||||
<h2><a hx-target="body" hx-push-url="true" hx-get={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"}>{vid.Title}</a></h2>
|
<h2><a hx-target="body" hx-push-url="true" hx-get={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"} href={$"./watch?v={Net.Http.UrlEncode(vid.VideoId)}"}>{vid.Title}</a></h2>
|
||||||
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"}>{vid.Channel}</a></h4>
|
<h4><a hx-target="body" hx-push-url="true" hx-get={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"} href={$"./channel?id={Net.Http.UrlEncode(vid.ChannelId)}"}>{vid.Channel}</a></h4>
|
||||||
<progress class="wavy light-green-text" value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
<progress class="wavy light-green-text" value={tytd.CurrentVideoProgress * 100.0} max="100" min="0"></progress>
|
||||||
</div>;
|
</div>;
|
||||||
progress++;
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
func Components.QueueSZ(tytd)
|
||||||
|
{
|
||||||
|
var vid = tytd.CurrentVideo;
|
||||||
|
tytd.Mutex.Lock();
|
||||||
|
|
||||||
|
|
||||||
|
var html = <span hx-trigger="every 10000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">{tytd.VideoQueueCount}</span>;
|
||||||
|
|
||||||
|
tytd.Mutex.Unlock();
|
||||||
|
return html;
|
||||||
|
}
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
func Components.Shell(title, html, page, $mypage)
|
func Components.Shell(title, html, page, $mypage)
|
||||||
{
|
{
|
||||||
|
|
||||||
if(TypeOf(mypage) != "Path")
|
if(TypeOf(mypage) != "Path")
|
||||||
mypage = /;
|
mypage = /;
|
||||||
|
|
||||||
var index = (/).MakeRelative(mypage).ToString();
|
var index = (/).MakeRelative(mypage).ToString();
|
||||||
if(index == "") index = "./";
|
if(index == "") index = "./";
|
||||||
|
|
||||||
|
const service_worker_script_path = (/"service_worker.js").MakeRelative(mypage).ToString();
|
||||||
|
const service_worker_script = $"<script>
|
||||||
|
if(\"serviceWorker\" in navigator) {123.ToChar()}
|
||||||
|
navigator.serviceWorker.register({Json.Encode(service_worker_script_path)})
|
||||||
|
.then(()=>console.log('Service worker registered'))
|
||||||
|
.catch(()=>console.log('service work not registered'));
|
||||||
|
{125.ToChar()}
|
||||||
|
</script>";
|
||||||
|
|
||||||
var pages = [
|
var pages = [
|
||||||
{
|
{
|
||||||
text="Home",
|
text="Home",
|
||||||
@@ -47,7 +57,11 @@ func Components.Shell(title, html, page, $mypage)
|
|||||||
<title>TYTD - {title}</title>
|
<title>TYTD - {title}</title>
|
||||||
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
|
<link rel="stylesheet" href={(/"beer.min.css").MakeRelative(mypage).ToString()}>
|
||||||
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
|
<link rel="stylesheet" href={(/"theme.css").MakeRelative(mypage).ToString()}>
|
||||||
|
<link rel="manifest" href={(/"site.webmanifest").MakeRelative(mypage).ToString()}>
|
||||||
|
|
||||||
<script src={(/"htmx.min.js").MakeRelative(mypage).ToString()} defer></script>
|
<script src={(/"htmx.min.js").MakeRelative(mypage).ToString()} defer></script>
|
||||||
|
|
||||||
|
<script src={(/"offline.js").MakeRelative(mypage).ToString()} defer></script>
|
||||||
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
|
<script type="module" src={(/"beer.min.js").MakeRelative(mypage).ToString()} defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body hx-indicator="#loading-indicator">
|
<body hx-indicator="#loading-indicator">
|
||||||
@@ -56,7 +70,15 @@ func Components.Shell(title, html, page, $mypage)
|
|||||||
<each(var page : pages)>
|
<each(var page : pages)>
|
||||||
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
|
<a hx-get={page.href} hx-target="body" hx-push-url="true" class={page.classStr}>
|
||||||
<i>{page.icon}</i>
|
<i>{page.icon}</i>
|
||||||
|
<if(page.text == "Home")>
|
||||||
|
<true>
|
||||||
|
<span hx-trigger="every 1000ms" hx-target="this" hx-push-url="false" hx-indicator="none" hx-get="./queue-size" hx-swap="outerHTML" class="badge">
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
</true>
|
||||||
|
</if>
|
||||||
<div>{page.text}</div>
|
<div>{page.text}</div>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</each>
|
</each>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -69,7 +91,7 @@ func Components.Shell(title, html, page, $mypage)
|
|||||||
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
|
<div class="htmx-indicator shape loading-indicator extra" id="loading-indicator">
|
||||||
<img class="responsive" src="./tytd.svg">
|
<img class="responsive" src="./tytd.svg">
|
||||||
</div>
|
</div>
|
||||||
|
<raw(service_worker_script)>
|
||||||
</body>
|
</body>
|
||||||
</html>;
|
</html>;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const BUILD_TIME = comptime DateTime.NowEpoch;
|
||||||
|
|
||||||
var TYTDResources = [
|
var TYTDResources = [
|
||||||
{path="/beer.min.css",value=embed("beer.min.css")},
|
{path="/beer.min.css",value=embed("beer.min.css")},
|
||||||
{path="/beer.min.js",value=embed("beer.min.js")},
|
{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="/htmx.min.js",value=embed("htmx.min.js")},
|
||||||
{path="/favicon.ico",value=embed("favicon.ico")},
|
{path="/favicon.ico",value=embed("favicon.ico")},
|
||||||
{path="/tytd.svg",value=embed("tytd.svg")},
|
{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="/loading-indicator.svg",value=embed("loading-indicator.svg")},
|
||||||
{path="/theme.css",value=embed("theme.css")},
|
{path="/theme.css",value=embed("theme.css")},
|
||||||
{path="/video.min.js",value=embed("video.min.js")},
|
{path="/video.min.js",value=embed("video.min.js")},
|
||||||
{path="/video-js.css",value=embed("video-js.css")},
|
{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;
|
var times=1;
|
||||||
|
|
||||||
class TYTDApp {
|
class TYTDApp {
|
||||||
@@ -29,21 +50,66 @@ class TYTDApp {
|
|||||||
|
|
||||||
public TYTDApp()
|
public TYTDApp()
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"Built at {new DateTime(BUILD_TIME).ToString()}");
|
||||||
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
|
var tytdfs = new SubdirFilesystem(FS.Local, GetTYTDDir());
|
||||||
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
|
this.TYTD = new TYTD.Downloader(tytdfs,FS.MakeFull(GetTYTDDir()));
|
||||||
this.TYTD.Start();
|
this.TYTD.Start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Handle(ctx)
|
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.Path == "/api/v1/login")
|
||||||
{
|
{
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
const req=ctx.ReadJson();
|
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)
|
if(result)
|
||||||
{
|
{
|
||||||
ctx.SendJson({
|
ctx.SendJson({
|
||||||
@@ -71,13 +137,16 @@ class TYTDApp {
|
|||||||
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
const username = ctx.QueryParams.TryGetFirst("username") ?? "";
|
||||||
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
const password = ctx.QueryParams.TryGetFirst("password") ?? "";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(ctx.Method == "POST")
|
if(ctx.Method == "POST")
|
||||||
{
|
{
|
||||||
const result = this.TYTD.Login(username, password);
|
const result = this.TYTD.Login(username, password,true);
|
||||||
if(result)
|
if(result)
|
||||||
{
|
{
|
||||||
ctx.StatusCode = 303;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
else incorrect=true;
|
else incorrect=true;
|
||||||
@@ -97,6 +166,25 @@ class TYTDApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(ctx.Path == "/progress")
|
||||||
|
{
|
||||||
|
ctx.WithMimeType("text/html").SendText(
|
||||||
|
<div>
|
||||||
|
<h1>You have been logged out</h1>
|
||||||
|
<a href="./login">Login</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
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.StatusCode = 307;
|
||||||
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
|
ctx.SendRedirect($"/login?redirect={Net.Http.UrlEncode(ctx.Path)}");
|
||||||
return true;
|
return true;
|
||||||
@@ -208,7 +296,6 @@ class TYTDApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ctx.Path == "/welcome")
|
if(ctx.Path == "/welcome")
|
||||||
{
|
{
|
||||||
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
|
const redirect = ctx.QueryParams.TryGetFirst("redirect") ?? "/settings";
|
||||||
@@ -245,11 +332,49 @@ class TYTDApp {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ctx.Path == "/")
|
if(ctx.Path == "/")
|
||||||
{
|
{
|
||||||
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 == "/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")
|
else if(ctx.Path == "/passwd")
|
||||||
{
|
{
|
||||||
var incorrect=false;
|
var incorrect=false;
|
||||||
@@ -265,6 +390,7 @@ class TYTDApp {
|
|||||||
|
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
ctx.WithMimeType("text/html").SendText(Pages.ChangePassword(redirect, incorrect));
|
||||||
}
|
}
|
||||||
|
|
||||||
else if(ctx.Path == "/newuser")
|
else if(ctx.Path == "/newuser")
|
||||||
{
|
{
|
||||||
var error = null;
|
var error = null;
|
||||||
@@ -340,6 +466,36 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
return false;
|
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")
|
else if(ctx.Path == "/api/v1/download")
|
||||||
{
|
{
|
||||||
var v = ctx.QueryParams.TryGetFirst("v");
|
var v = ctx.QueryParams.TryGetFirst("v");
|
||||||
@@ -371,6 +527,20 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("application/json").SendJson(jo);
|
ctx.WithMimeType("application/json").SendJson(jo);
|
||||||
return true;
|
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")
|
else if(ctx.Path == "/api/v1/video.json")
|
||||||
{
|
{
|
||||||
var id = ctx.QueryParams.TryGetFirst("v");
|
var id = ctx.QueryParams.TryGetFirst("v");
|
||||||
@@ -439,6 +609,30 @@ class TYTDApp {
|
|||||||
}
|
}
|
||||||
return true;
|
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/manifest.json")
|
||||||
|
{
|
||||||
|
const v = ctx.QueryParams.TryGetFirst("v");
|
||||||
|
if(TypeIsString(v))
|
||||||
|
{
|
||||||
|
const resp=this.TYTD.ManifestRequest(v);
|
||||||
|
ctx.WithMimeType("application/json").SendJson(resp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if(ctx.Path == "/api/v1/personal")
|
else if(ctx.Path == "/api/v1/personal")
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -714,6 +908,36 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.VideoInfo(this.TYTD,ctx));
|
||||||
return true;
|
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")
|
else if(ctx.Path == "/playlist")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.PlaylistInfo(this.TYTD,ctx));
|
||||||
@@ -802,6 +1026,12 @@ class TYTDApp {
|
|||||||
ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD));
|
ctx.WithMimeType("text/html").SendText(Components.Progress(this.TYTD));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if(ctx.Path == "/queue-size")
|
||||||
|
{
|
||||||
|
|
||||||
|
ctx.WithMimeType("text/html").SendText(Components.QueueSZ(this.TYTD));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
else if(ctx.Path == "/plugins")
|
else if(ctx.Path == "/plugins")
|
||||||
{
|
{
|
||||||
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));
|
ctx.WithMimeType("text/html").SendText(Pages.Plugins(this.TYTD,ctx));
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
func Pages.YouTubeAnonyPlaylist(video_ids)
|
||||||
|
{
|
||||||
|
const html = <null>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max"></div>
|
||||||
|
<div class="min">
|
||||||
|
<form method="POST" action="./watch_videos">
|
||||||
|
<input type="hidden" name="video_ids" value={video_ids}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max">
|
||||||
|
<div class="field label border">
|
||||||
|
<input type="text" name="name">
|
||||||
|
<label>Playlist Name</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min">
|
||||||
|
<button>Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="max"></div>
|
||||||
|
</div>
|
||||||
|
</null>;
|
||||||
|
return Components.Shell("Create playlist",html ,1);
|
||||||
|
}
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "Tesses.YouTubeDownloader",
|
"name": "Tesses.YouTubeDownloader",
|
||||||
"version": "1.0.0.0-dev",
|
"version": "1.0.0.6-dev",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
"context": {
|
"context": {
|
||||||
"client": {
|
"client": {
|
||||||
"clientName": "ANDROID",
|
"clientName": "ANDROID",
|
||||||
"clientVersion": "19.28.35",
|
"clientVersion": "21.03.36",
|
||||||
"clientScreen": "WATCH",
|
"clientScreen": "WATCH",
|
||||||
"platform": "MOBILE",
|
"platform": "MOBILE",
|
||||||
"osName": "Android",
|
"osName": "Android",
|
||||||
"osVersion": "15",
|
"osVersion": "16",
|
||||||
"androidSdkVersion": 35,
|
"androidSdkVersion": 36,
|
||||||
"hl": "en-GB",
|
"hl": "en-GB",
|
||||||
"gl": "US",
|
"gl": "US",
|
||||||
"utcOffsetMinutes": 0
|
"utcOffsetMinutes": 0
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
"context": {
|
"context": {
|
||||||
"client": {
|
"client": {
|
||||||
"clientName": "ANDROID",
|
"clientName": "ANDROID",
|
||||||
"clientVersion": "19.28.35",
|
"clientVersion": "21.03.36",
|
||||||
"clientScreen": "WATCH",
|
"clientScreen": "WATCH",
|
||||||
"platform": "MOBILE",
|
"platform": "MOBILE",
|
||||||
"visitorData": "VISITOR_DATA",
|
"visitorData": "VISITOR_DATA",
|
||||||
"osName": "Android",
|
"osName": "Android",
|
||||||
"osVersion": "15",
|
"osVersion": "16",
|
||||||
"androidSdkVersion": 35,
|
"androidSdkVersion": 36,
|
||||||
"hl": "en-GB",
|
"hl": "en-GB",
|
||||||
"gl": "US",
|
"gl": "US",
|
||||||
"utcOffsetMinutes": 0
|
"utcOffsetMinutes": 0
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class UserFlags {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
static getITTR() 35000;
|
static getITTR() 35000;
|
||||||
|
|
||||||
|
static getExpires() 86400 * 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TYTD.Downloader {
|
class TYTD.Downloader {
|
||||||
@@ -72,6 +74,22 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public DownloadVideo(id,$res)
|
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}");
|
this.LOG($"Adding video: {TYTD.GetVideoId(id)}, original val: {id}");
|
||||||
switch(res)
|
switch(res)
|
||||||
{
|
{
|
||||||
@@ -79,7 +97,7 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
var id = TYTD.GetVideoId(id);
|
var id = TYTD.GetVideoId(id);
|
||||||
if(id != null)
|
if(id != null)
|
||||||
PutVideoInfoIfNotExists(id);
|
this.PutVideoInfoIfNotExists(id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Resolution.LowVideo:
|
case Resolution.LowVideo:
|
||||||
@@ -121,6 +139,19 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
if(pid != null)
|
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.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))
|
||||||
@@ -145,6 +176,18 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
if(cid != null)
|
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.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))
|
||||||
@@ -182,6 +225,7 @@ class TYTD.Downloader {
|
|||||||
var pid = TYTD.GetPlaylistId(url);
|
var pid = TYTD.GetPlaylistId(url);
|
||||||
|
|
||||||
var cid = TYTD.GetChannelId(url);
|
var cid = TYTD.GetChannelId(url);
|
||||||
|
var tmp = TYTD.GetYouTubeTempPlaylist(url);
|
||||||
|
|
||||||
if(vid != null)
|
if(vid != null)
|
||||||
{
|
{
|
||||||
@@ -195,7 +239,13 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
this.DownloadChannel(cid,res);
|
this.DownloadChannel(cid,res);
|
||||||
}
|
}
|
||||||
|
else if(tmp != null)
|
||||||
|
{
|
||||||
|
each(var item : tmp)
|
||||||
|
{
|
||||||
|
this.DownloadItem(item,res);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/^
|
/^
|
||||||
Redirect url to info page
|
Redirect url to info page
|
||||||
@@ -204,6 +254,8 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
var vid = TYTD.GetVideoId(url);
|
var vid = TYTD.GetVideoId(url);
|
||||||
var pid = TYTD.GetPlaylistId(url);
|
var pid = TYTD.GetPlaylistId(url);
|
||||||
|
var cid = TYTD.GetChannelId(url);
|
||||||
|
var tmp = TYTD.GetYouTubeTempPlaylistRedirect(url);
|
||||||
|
|
||||||
if(vid != null)
|
if(vid != null)
|
||||||
{
|
{
|
||||||
@@ -217,6 +269,10 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
return $"./channel?id={Net.Http.UrlEncode(cid)}";
|
return $"./channel?id={Net.Http.UrlEncode(cid)}";
|
||||||
}
|
}
|
||||||
|
else if(tmp != null)
|
||||||
|
{
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
return "./";
|
return "./";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +646,29 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
return "";
|
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)
|
public GetPersonalListContents(name, offset, count)
|
||||||
{
|
{
|
||||||
@@ -716,6 +795,8 @@ class TYTD.Downloader {
|
|||||||
|
|
||||||
public VideoProgress = new TYTD.Event();
|
public VideoProgress = new TYTD.Event();
|
||||||
|
|
||||||
|
public BeforeQueued = new TYTD.Event();
|
||||||
|
|
||||||
public CurrentVideo = {
|
public CurrentVideo = {
|
||||||
Title = "N/A",
|
Title = "N/A",
|
||||||
Channel = "N/A",
|
Channel = "N/A",
|
||||||
@@ -930,6 +1011,7 @@ class TYTD.Downloader {
|
|||||||
while(this.Running)
|
while(this.Running)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
this.FlushExpired();
|
||||||
var res = this.PlaylistQueue.Pop();
|
var res = this.PlaylistQueue.Pop();
|
||||||
|
|
||||||
if(TypeOf(res) != "Null")
|
if(TypeOf(res) != "Null")
|
||||||
@@ -937,7 +1019,7 @@ class TYTD.Downloader {
|
|||||||
res();
|
res();
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentTime = DateTime.NowEpoch;
|
var currentTime = DateTime.NowEpoch ?? 0;
|
||||||
var bt = this.Config.BellTimer;
|
var bt = this.Config.BellTimer;
|
||||||
|
|
||||||
|
|
||||||
@@ -1057,7 +1139,7 @@ class TYTD.Downloader {
|
|||||||
if(TypeOf(res) != "Null")
|
if(TypeOf(res) != "Null")
|
||||||
{
|
{
|
||||||
|
|
||||||
res.TYTD = this;
|
if(TypeIsDefined(res.TYTD = this)){
|
||||||
res.Progress = (progress)=>{
|
res.Progress = (progress)=>{
|
||||||
this.CurrentVideoProgress = progress;
|
this.CurrentVideoProgress = progress;
|
||||||
this.VideoProgress.Invoke(this, {
|
this.VideoProgress.Invoke(this, {
|
||||||
@@ -1078,6 +1160,7 @@ class TYTD.Downloader {
|
|||||||
Video = res.Video
|
Video = res.Video
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
try{
|
try{
|
||||||
this.LOG($"Exception caught on download thread: {ex}");
|
this.LOG($"Exception caught on download thread: {ex}");
|
||||||
@@ -1258,11 +1341,62 @@ class TYTD.Downloader {
|
|||||||
if(e == null)
|
if(e == null)
|
||||||
{
|
{
|
||||||
var req = this.ManifestRequest(id);
|
var req = this.ManifestRequest(id);
|
||||||
|
|
||||||
this.PutVideoInfo(req.playerResponse.videoDetails);
|
this.PutVideoInfo(req.playerResponse.videoDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private DownloadCaptions(req)
|
||||||
|
{
|
||||||
|
const tracks = req.playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks;
|
||||||
|
|
||||||
|
if(TypeIsList(tracks))
|
||||||
|
{
|
||||||
|
each(var item : tracks)
|
||||||
|
{
|
||||||
|
if(!TypeIsString(item.languageCode)) continue;
|
||||||
|
if(!TypeIsString(item.baseUrl)) continue;
|
||||||
|
try {
|
||||||
|
var path = /"Streams"/id.Substring(0,4) / id.Substring(4) / item.languageCode;
|
||||||
|
if(!this.Storage.FileExists(path+".xml"))
|
||||||
|
{
|
||||||
|
var resp = Net.Http.MakeRequest(url,{FollowRedirects=true});
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path+".xml","wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!this.Storage.FileExists(path+".vtt"))
|
||||||
|
{
|
||||||
|
var resp = Net.Http.MakeRequest(url.Replace("fmt=srv3","fmt=vtt"),{FollowRedirects=true});
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path+".vtt","wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!this.Storage.FileExists(path+".srt"))
|
||||||
|
{
|
||||||
|
var resp = Net.Http.MakeRequest(url.Replace("fmt=srv3","fmt=srt"),{FollowRedirects=true});
|
||||||
|
|
||||||
|
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
|
||||||
|
{
|
||||||
|
const strm=this.Storage.OpenFile(path+".srt","wb");
|
||||||
|
resp.CopyToStream(strm);
|
||||||
|
strm.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(ex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/^
|
/^
|
||||||
Put video info from info into database
|
Put video info from info into database
|
||||||
^/
|
^/
|
||||||
@@ -1297,7 +1431,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 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 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 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';");
|
var config=Sqlite.Exec(db,"SELECT * FROM plugin_settings WHERE extension = '' AND key = 'settings';");
|
||||||
if(TypeOf(config) == "List" && config.Length>0)
|
if(TypeOf(config) == "List" && config.Length>0)
|
||||||
{
|
{
|
||||||
@@ -1320,7 +1457,7 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
this.Muxex.Lock();
|
this.Muxex.Lock();
|
||||||
var db = this.OpenDB();
|
var db = this.OpenDB();
|
||||||
Sqlite.Exec(db, $"INSERT INTO plugin_settings (extension,key,value) VALUES ({Sqlite.Escape(extension)},{Sqlite.Escape(key)},{Sqlite.Escape(value)});");
|
Sqlite.Exec(db, $"INSERT OR REPLACE INTO plugin_settings (extension,key,value) VALUES ({Sqlite.Escape(extension)},{Sqlite.Escape(key)},{Sqlite.Escape(value)});");
|
||||||
Sqlite.Close(db);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
}
|
}
|
||||||
@@ -1522,6 +1659,7 @@ class TYTD.Downloader {
|
|||||||
^/
|
^/
|
||||||
public ManifestRequest(vid)
|
public ManifestRequest(vid)
|
||||||
{
|
{
|
||||||
|
for(var tries=0; tries<5;tries++) {
|
||||||
var id = TYTD.GetVideoId(vid);
|
var id = TYTD.GetVideoId(vid);
|
||||||
if(id == null) return null;
|
if(id == null) return null;
|
||||||
TryDownloadVideoThumbnail(id,"0");
|
TryDownloadVideoThumbnail(id,"0");
|
||||||
@@ -1539,7 +1677,7 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
Key = "User-Agent",
|
Key = "User-Agent",
|
||||||
|
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
Value = "com.google.android.youtube/21.03.36 (Linux; U; Android 16; GB) gzip"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Body = Net.Http.TextHttpRequestBody(embed("request.json").ToString(),"application/json")
|
Body = Net.Http.TextHttpRequestBody(embed("request.json").ToString(),"application/json")
|
||||||
@@ -1557,7 +1695,7 @@ class TYTD.Downloader {
|
|||||||
RequestHeaders = [
|
RequestHeaders = [
|
||||||
{
|
{
|
||||||
Key = "User-Agent",
|
Key = "User-Agent",
|
||||||
Value = "com.google.android.youtube/19.28.35 (Linux; U; Android 15; GB) gzip"
|
Value = "com.google.android.youtube/21.03.36 (Linux; U; Android 16; GB) gzip"
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
@@ -1565,9 +1703,47 @@ class TYTD.Downloader {
|
|||||||
};
|
};
|
||||||
this.RateLimit();
|
this.RateLimit();
|
||||||
var response = Net.Http.MakeRequest(url,requestData);
|
var response = Net.Http.MakeRequest(url,requestData);
|
||||||
if(response.StatusCode < 200 || response.StatusCode > 299) return null;
|
|
||||||
return Json.Decode(response.ReadAsString());
|
|
||||||
|
|
||||||
|
if(response.StatusCode < 200 || response.StatusCode > 299) {
|
||||||
|
if(tries == 4)
|
||||||
|
{
|
||||||
|
const respText = response.ReadAsString();
|
||||||
|
throw new VideoDownloadError(id, $"StatusCode does not indicate success {response.StatusCode}\n{respText}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const respText = response.ReadAsString();
|
||||||
|
const jsonResp = Json.Decode(respText);
|
||||||
|
if(!TypeIsDictionary(jsonResp.playerResponse)) {
|
||||||
|
if(tries == 4)
|
||||||
|
{
|
||||||
|
throw new VideoDownloadError(id, $"Player response is not defined, StatusCode: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(TypeIsDictionary(jsonResp.playerResponse.playabilityStatus))
|
||||||
|
{
|
||||||
|
if(jsonResp.playerResponse.playabilityStatus.status == "ERROR")
|
||||||
|
{
|
||||||
|
throw new VideoDownloadError(id, jsonResp.playerResponse.playabilityStatus.reason);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new VideoDownloadError(id, "playabilityStatus is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!TypeIsDictionary(jsonResp.playerResponse.videoDetails))
|
||||||
|
{
|
||||||
|
throw new VideoDownloadError(id, "videoDetails is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!TypeIsList(jsonResp.playerResponse.streamingData.adaptiveFormats))
|
||||||
|
{
|
||||||
|
throw new VideoDownloadError(id, "adaptiveFormats is missing");
|
||||||
|
}
|
||||||
|
this.DownloadCaptions(jsonResp);
|
||||||
|
return jsonResp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1798,7 +1974,6 @@ class TYTD.Downloader {
|
|||||||
const token = GetSessionToken(ctx);
|
const token = GetSessionToken(ctx);
|
||||||
if(TypeIsString(token))
|
if(TypeIsString(token))
|
||||||
{
|
{
|
||||||
|
|
||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
const db = this.OpenDB();
|
const db = this.OpenDB();
|
||||||
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
|
Sqlite.Exec(db, $"DELETE FROM sessions WHERE key = {Sqlite.Escape(token)};");
|
||||||
@@ -1831,7 +2006,17 @@ class TYTD.Downloader {
|
|||||||
}
|
}
|
||||||
return null;
|
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)
|
public IsLoggedIn(ctx)
|
||||||
{
|
{
|
||||||
this.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
@@ -1853,12 +2038,31 @@ class TYTD.Downloader {
|
|||||||
{
|
{
|
||||||
const res = Sqlite.Exec(db, $"SELECT * FROM sessions s INNER JOIN users u ON s.accountId = u.id WHERE key = {Sqlite.Escape(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))
|
if(TypeIsList(res))
|
||||||
each(var item : 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);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
|
|
||||||
return ParseLong(item.flags) | 1;
|
return ParseLong(item.flags) | 1;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1867,9 +2071,27 @@ class TYTD.Downloader {
|
|||||||
return 0;
|
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)
|
public WhoAmI(ctx)
|
||||||
{
|
{
|
||||||
his.Mutex.Lock();
|
this.Mutex.Lock();
|
||||||
const db = this.OpenDB();
|
const db = this.OpenDB();
|
||||||
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
|
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
|
||||||
var noAccounts=true;
|
var noAccounts=true;
|
||||||
@@ -1891,6 +2113,20 @@ class TYTD.Downloader {
|
|||||||
if(TypeIsList(res))
|
if(TypeIsList(res))
|
||||||
each(var item : 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);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
item.flags = ParseLong(item.flags);
|
item.flags = ParseLong(item.flags);
|
||||||
@@ -1905,9 +2141,9 @@ class TYTD.Downloader {
|
|||||||
public Passwd(ctx, oldPassword, newPassword, logout)
|
public Passwd(ctx, oldPassword, newPassword, logout)
|
||||||
{
|
{
|
||||||
const whoami = this.WhoAmI(ctx);
|
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 hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
var hashStr = Crypto.Base64Encode(hash);
|
var hashStr = Crypto.Base64Encode(hash);
|
||||||
|
|
||||||
@@ -1937,17 +2173,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();
|
this.Mutex.Lock();
|
||||||
const db = this.OpenDB();
|
const db = this.OpenDB();
|
||||||
const user = Sqlite.Exec(db, $"SELECT * FROM users WHERE username = {Sqlite.Escape(username)};");
|
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))
|
if(TypeIsList(user))
|
||||||
{
|
{
|
||||||
each(var item : user)
|
each(var item : user)
|
||||||
{
|
{
|
||||||
this.Mutex.Unlock();
|
|
||||||
|
|
||||||
var salt = Crypto.Base64Decode(item.password_salt);
|
var salt = Crypto.Base64Decode(item.password_salt);
|
||||||
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
|
||||||
@@ -1956,18 +2220,22 @@ class TYTD.Downloader {
|
|||||||
if(item.password_hash == hashStr)
|
if(item.password_hash == hashStr)
|
||||||
{
|
{
|
||||||
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
|
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
|
||||||
this.Mutex.Lock();
|
|
||||||
const dbCon = this.OpenDB();
|
const expires = doesExpire ? ((DateTime.NowEpoch??0) + UserFlags.Expires) : 0;
|
||||||
Sqlite.Exec(dbCon, $"INSERT INTO sessions (accountId,key) VALUES ({item.id},{Sqlite.Escape(rand)});");
|
Sqlite.Exec(db, $"INSERT INTO sessions (accountId,key,expires) VALUES ({item.id},{Sqlite.Escape(rand)},{expires});");
|
||||||
Sqlite.Close(dbCon);
|
Sqlite.Close(db);
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
return rand;
|
return rand;
|
||||||
}
|
}
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
|
this.Mutex.Unlock();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sqlite.Close(db);
|
||||||
|
|
||||||
this.Mutex.Unlock();
|
this.Mutex.Unlock();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1992,6 +2260,25 @@ class TYTD.Downloader {
|
|||||||
return false;
|
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"};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
Tesses.YouTubeDownloader/src/error.tcross
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class VideoDownloadError {
|
||||||
|
public VideoDownloadError(id, error)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Id;
|
||||||
|
|
||||||
|
public Error;
|
||||||
|
|
||||||
|
public ToString()
|
||||||
|
{
|
||||||
|
return $"Download error {Error} with Id: {Id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ func TYTD.GetVideoId(v)
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TYTD.GetPlaylistId(pid)
|
func TYTD.GetPlaylistId(pid)
|
||||||
{
|
{
|
||||||
func IsValidId(v)
|
func IsValidId(v)
|
||||||
@@ -85,3 +86,48 @@ func TYTD.GetChannelId(cid)
|
|||||||
return null;
|
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;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ class TYTD.AOVideoDownload : IVideoDownload {
|
|||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
||||||
if(this.tytd.Storage.FileExists(path)) {
|
if(this.tytd.Storage.FileExists(path)) {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
return tytd;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TYTD.NoConvertVideoDownload : IVideoDownload {
|
|||||||
var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
var pathA = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ao.bin";
|
||||||
if(this.tytd.Storage.FileExists(path) && this.tytd.Storage.FileExists(pathA)) {
|
if(this.tytd.Storage.FileExists(path) && this.tytd.Storage.FileExists(pathA)) {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
return tytd;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TYTD.SDVideoDownload : IVideoDownload {
|
|||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4";
|
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"ytmux.mp4";
|
||||||
if(this.tytd.Storage.FileExists(path)) {
|
if(this.tytd.Storage.FileExists(path)) {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
return tytd;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TYTD.VOVideoDownload : IVideoDownload {
|
|||||||
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
var path = /"Streams"/id.Substring(0,4)/id.Substring(4)/"vo.bin";
|
||||||
if(this.tytd.Storage.FileExists(path)) {
|
if(this.tytd.Storage.FileExists(path)) {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
return tytd;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var req = this.tytd.ManifestRequest(id).playerResponse;
|
var req = this.tytd.ManifestRequest(id).playerResponse;
|
||||||
|
|||||||