Add pwa support

This commit is contained in:
2026-02-28 06:32:39 -06:00
parent 02b10131f9
commit 28b7138547
25 changed files with 870 additions and 48 deletions

View File

@@ -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"};
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;