Files
tytd2025/Tesses.YouTubeDownloader/src/YouTubeDownloader.tcross
Mike Nolan f73c45bb48
All checks were successful
Build and Deploy on Tag / 🔨 Build (push) Successful in 22s
Target crosslang v0.0.10, add /api/v1/progress_ex.json
2026-09-10 03:29:30 -05:00

2387 lines
63 KiB
Plaintext

//DO NOT ADD A FLAG WITH ONES BIT SET AS THIS IS
//TO DENOTE USER IS LOGGED IN
class UserFlags {
static getAdminFlag() 0b00000100;
static getPluginFlag() 0b00000010;
static getDatabaseFlag() 0b00001000;
static getManagePluginFlag() 0b00100000;
static IsAdmin(flags)
{
if(flags & UserFlags.AdminFlag) return true;
return false;
}
static CanDownloadDB(flags)
{
if(flags & UserFlags.AdminFlag) return true;
if(flags & UserFlags.DatabaseFlag) return true;
return false;
}
static CanCreateUsers(flags)
{
if(flags & UserFlags.AdminFlag) return true;
return false;
}
static CanUsePlugins(flags)
{
if(flags & UserFlags.AdminFlag) return true;
if(flags & UserFlags.ManagePluginFlag) return true;
if(flags & UserFlags.PluginFlag) return true;
return false;
}
static CanManagePlugins(flags)
{
if(flags & UserFlags.AdminFlag) return true;
if(flags & UserFlags.ManagePluginFlag) return true;
return false;
}
static getITTR() 35000;
static getExpires() 86400 * 7;
}
class TYTD.Downloader {
/^
The storage vfs that TYTD accesses
^/
public Storage;
/^
The directory of where the database is (should be the same physical folder as the Storage vfs if you want ffmpeg)
^/
public DatabaseDirectory;
/^
Package manager object
^/
public PackageManager = new Tesses.CrossLang.PackageManager();
/^
All the plugin webpages
^/
public Servers = Net.Http.MountableServer({Handle=(ctx)=>false});
/^
vfs: The storage vfs that TYTD accesses
dbDir: The directory of where the database is (should be the same physical folder as the Storage vfs if you want ffmpeg)
^/
public Downloader(vfs,dbDir)
{
this.Storage = vfs;
this.DatabaseDirectory = dbDir;
}
private PopQueue()
{
Mutex.Lock();
const db = OpenDB();
var res = Sqlite.Exec(db,"SELECT * FROM queue ORDER BY id DESC LIMIT 1;");
if(TypeIsList(res) && res.Length > 0)
{
Sqlite.Exec(db,$"DELETE FROM queue WHERE id = {Sqlite.Escape(res[0].id)};");
res = res[0];
}
else { res = null;}
Sqlite.Close(db);
Mutex.Unlock();
if(TypeIsDictionary(res))
{
switch(res.resolution)
{
case Resolution.NoDownload:
{
this.PutVideoInfoIfNotExists(res.videoId);
return null;
}
case Resolution.MP3:
return new TYTD.TranscodeAudio(res.videoId,".mp3");
case Resolution.FLAC:
return new TYTD.TranscodeAudio(res.videoId,".flac");
case Resolution.AMV:
return new TYTD.TranscodeAMV(res.videoId);
default:
return new TYTD.SDVideoDownload(res.videoId);
}
}
return null;
}
/^
Download a video
id: video id or url
res: see Resolution
^/
public DownloadVideo(id,$res)
{
const theVideoId = TYTD.GetVideoId(id);
if(TypeIsString(theVideoId))
{
if(!TypeIsDefined(res)) res = Resolution.LowVideo;
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;
}
}
else {
this.LOG($"Malformed video id {id}");
return;
}
this.LOG($"Adding video: https://www.youtube.com/watch?v={theVideoId}");
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT, resolution TEXT);");
Mutex.Lock();
const db = OpenDB();
Sqlite.Exec(db, $"INSERT INTO queue (videoId, resolution) VALUES ({Sqlite.Escape(theVideoId)},{Sqlite.Escape(res)});");
Sqlite.Close(db);
Mutex.Unlock();
}
/^
Download a playlist
id: playlist id or url
res: see Resolution
^/
public DownloadPlaylist(id,$res)
{
var pid = TYTD.GetPlaylistId(id);
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))
{
each(var vid : item)
{
DownloadVideo(vid, res);
}
}
});
}
}
/^
Download a channel
id: channel id or url
res: see Resolution
^/
public DownloadChannel(id,$res)
{
var cid = TYTD.GetChannelId(id);
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))
{
each(var vid : item)
{
DownloadVideo(vid, res);
}
}
});
}
}
/^
Get the thumbnail of a plugin
^/
public GetPluginThumbnail(name)
{
each(var item : this.Plugins)
{
if(item.pluginName == name)
return item.pluginIcon;
}
return embed("package_icon.png");
}
/^
Download video, playlist or channel
url: id or url
res: see Resolution
^/
public DownloadItem(url, $res)
{
var vid = TYTD.GetVideoId(url);
var pid = TYTD.GetPlaylistId(url);
var cid = TYTD.GetChannelId(url);
var tmp = TYTD.GetYouTubeTempPlaylist(url);
if(vid != null)
{
this.DownloadVideo(vid, res);
}
else if(pid != null)
{
this.DownloadPlaylist(pid,res);
}
else if(cid != null)
{
this.DownloadChannel(cid,res);
}
else if(tmp != null)
{
each(var item : tmp)
{
this.DownloadItem(item,res);
}
}
}
/^
Redirect url to info page
^/
public PageRedirect(url)
{
var vid = TYTD.GetVideoId(url);
var pid = TYTD.GetPlaylistId(url);
var cid = TYTD.GetChannelId(url);
var tmp = TYTD.GetYouTubeTempPlaylistRedirect(url);
if(vid != null)
{
return $"./watch?v={Net.Http.UrlEncode(vid)}";
}
else if(pid != null)
{
return $"./playlist?list={Net.Http.UrlEncode(pid)}";
}
else if(cid != null)
{
return $"./channel?id={Net.Http.UrlEncode(cid)}";
}
else if(tmp != null)
{
return tmp;
}
return "./";
}
private Views2Str(views)
{
if(views == 1) return "1 view";
if(views < 1000) return $"{views} views";
if(views < 1000000) return $"{views/1000}K views";
if(views < 1000000000) return $"{views/1000000}M views";
if(views < 1000000000000) return $"{views/1000000000}B views";
return $"{views/1000000000000}T views";
}
public GetQueueItems(offset, count)
{
this.Mutex.Lock();
var db = this.OpenDB();
const vals = Sqlite.Exec(db,$"SELECT * FROM queue ORDER BY id DESC LIMIT {count} OFFSET {offset*count};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeIsList(vals))
return vals;
return [];
}
/^
Get videos
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetVideos(query, offset, count)
{
this.Mutex.Lock();
var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%");
var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.title LIKE {q} OR v.shortDescription LIKE {q}) LIMIT {count} OFFSET {offset*count};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res;
each(var item : res)
{
if(item.keywords != "undefined")
item.keywords = Json.Decode(item.keywords);
else item.keywords = [];
item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount);
}
return res;
}
/^
Get playlists
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetPlaylists(query, offset, count)
{
this.Mutex.Lock();
var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%");
var res = Sqlite.Exec(db, $"SELECT * FROM playlists v WHERE (v.title LIKE {q}) LIMIT {count} OFFSET {offset*count};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res;
return res;
}
/^
Get playlist contents
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetPlaylistContents(id, offset, count)
{
id = TYTD.GetPlaylistId(id);
if(id == null) return {
title = "N/A",
channelId="",
channelTitle="N/A",
items = []
};
this.Mutex.Lock();
var db = this.OpenDB();
var channelTitle = "";
var title = "";
var channelId = "";
/*
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId INTEGER, videoId TEXT);");
*/
var res = Sqlite.Exec(db, $"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};");
var res2 = null;
if(TypeOf(res) == "List" && res.Length > 0)
{
channelId = res[0].channelId;
channelTitle = res[0].channelTitle;
title = res[0].title;
var id = res[0].id;
res2 = Sqlite.Exec(db, $"SELECT * FROM playlist_entries e INNER JOIN videos v ON e.videoId = v.videoId WHERE e.playlistId = {id};");
}
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res;
if(TypeOf(res2) != "List") throw res2;
each(var item : res2)
{
if(item.keywords != "undefined")
item.keywords = Json.Decode(item.keywords);
else
item.keywords = [];
item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount);
}
return {
channelTitle,
title,
channelId,
items = res2
};
}
/^
Get channel contents
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetChannelContents(id, offset, count)
{
id = TYTD.GetChannelId(id);
if(id == null) return {
authorName = "N/A",
items = []
};
this.Mutex.Lock();
var db = this.OpenDB();
var authorName = "";
var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.channelId = {Sqlite.Escape(id)}) LIMIT {count} OFFSET {offset*count};");
if(TypeOf(res) != "List" || res.Length == 0) {
var res2 = Sqlite.Exec(db,"SELECT * FROM channels c WHERE c.channelId = {Sqlite.Escape(id)};");
if(TypeOf(res2) == "List" && res2.Length > 0)
{
authorName = res2[0].title;
}
}
else {
authorName = res[0].author;
}
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res;
each(var item : res)
{
if(item.keywords != "undefined")
item.keywords = Json.Decode(item.keywords);
else
item.keywords = [];
item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount);
}
return {
authorName,
items = res
};
}
/^
Get channels
set offset to the page (starting at 0)
set count to how many items per page
^/
public GetChannels(query, offset, count)
{
this.Mutex.Lock();
var db = this.OpenDB();
var q = Sqlite.Escape($"%{query}%");
var res = Sqlite.Exec(db, $"SELECT * FROM channels v WHERE (v.title LIKE {q}) LIMIT {count} OFFSET {offset*count};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) != "List") throw res;
return res;
}
/^
Get the video path
res: see Resolution
^/
public GetVideoPath(v,res)
{
var id = TYTD.GetVideoId(v);
if(id == null) throw "No id specified";
var dir = /"Streams"/id.Substring(0,4)/id.Substring(4);
switch(res)
{
case Resolution.LowVideo:
return dir / "ytmux.mp4";
case Resolution.VideoOnly:
return dir / "vo.bin";
case Resolution.AudioOnly:
return dir / "ao.bin";
case Resolution.MP4:
return dir / "conv.mp4";
case Resolution.MKV:
return dir / "conv.mkv";
case Resolution.MP3:
return dir / "conv.mp3";
case Resolution.FLAC:
return dir / "conv.flac";
case Resolution.AMV:
return dir / "conv.amv";
}
throw $"Could not get file path for format {res}";
}
/^
Get video info
vid can be a video url from youtube or just the id
^/
public GetVideo(vid)
{
var id = TYTD.GetVideoId(vid);
if(id == null) return null;
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db, $"SELECT * FROM videos WHERE videoId = {Sqlite.Escape(id)};");
var out = null;
if(TypeOf(res) == "List" && res.Length == 1) out = res[0];
Sqlite.Close(db);
this.Mutex.Unlock();
if(out != null)
{
if(out != "undefined")
out.keywords = Json.Decode(out.keywords);
else
out.keywords = [];
out.addDate = ParseLong(out.addDate);
out.addDateStr = new DateTime(out.addDate).ToString();
out.lengthSeconds = ParseLong(out.lengthSeconds);
out.viewCount = ParseLong(out.viewCount);
out.viewCountStr = this.Views2Str(out.viewCount);
}
return out;
}
/^
Get playlist info
id can be a playlist url from youtube or just the id
^/
public GetPlaylist(id)
{
var id = TYTD.GetPlaylistId(vid);
if(id == null) return null;
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db, $"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};");
var out = null;
if(TypeOf(res) == "List" && res.Length == 1) out = res[0];
Sqlite.Close(db);
this.Mutex.Unlock();
return out;
}
/^
Get channel info
id can be a channel url from youtube or just the id
^/
public GetChannel(id)
{
var id = TYTD.GetChannelId(vid);
if(id == null) return null;
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db, $"SELECT * FROM channels WHERE channelId = {Sqlite.Escape(id)};");
var out = null;
if(TypeOf(res) == "List" && res.Length == 1) out = res[0];
Sqlite.Close(db);
this.Mutex.Unlock();
return out;
}
/^ Get the list of personal list names^/
public GetPersonalLists()
{
this.Mutex.Lock();
var db = this.OpenDB();
var items = [];
var lists = Sqlite.Exec(db, "SELECT * FROM personal_lists;");
if(TypeOf(lists) == "List")
{
each(var item : lists)
{
items.Add(item);
var res2=Sqlite.Exec(db, $"SELECT * FROM personal_list_entries WHERE listName = {Sqlite.Escape(item.name)} LIMIT 1;");
if(TypeOf(res2) == "List" && res2.Length > 0) item.firstVideo = res2[0].videoId;
}
}
Sqlite.Close(db);
this.Mutex.Unlock();
return items;
}
/^ Set the description of a personal list ^/
public SetPersonalListDescription(name,description)
{
this.Mutex.Lock();
var db = this.OpenDB();
Sqlite.Exec(db, $"UPDATE personal_lists SET description = {Sqlite.Escape(description)} WHERE name = {Sqlite.Escape(name)};");
Sqlite.Close(db);
this.Mutex.Unlock();
}
/^ Get the description of a personal list ^/
public GetPersonalListDescription(name)
{
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db, $"SELECT * FROM personal_lists WHERE name = {Sqlite.Escape(name)};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) == "List" && res.Length > 0)
{
var d = res[0].description;
if(TypeOf(d)=="String")
return d;
}
return "";
}
public GetPersonalListTempUrl(name)
{
this.Mutex.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)
{
this.Mutex.Lock();
var db = this.OpenDB();
var items = [];
var lists = Sqlite.Exec(db, $"SELECT * FROM personal_list_entries e INNER JOIN videos v ON e.videoId = v.videoId WHERE e.listName = {Sqlite.Escape(name)} LIMIT {count} OFFSET {offset*count};");
if(TypeOf(lists) == "List")
{
each(var item : lists)
{
if(item.keywords != "undefined")
item.keywords = Json.Decode(item.keywords);
else item.keywords = [];
item.addDate = ParseLong(item.addDate);
item.addDateStr = new DateTime(item.addDate).ToString();
item.lengthSeconds = ParseLong(item.lengthSeconds);
item.viewCount = ParseLong(item.viewCount);
item.viewCountStr = this.Views2Str(item.viewCount);
items.Add(item);
}
}
Sqlite.Close(db);
this.Mutex.Unlock();
return items;
}
public QueueRemoveItem(id)
{
this.Mutex.Lock();
const db = OpenDB();
Sqlite.Exec(db,$"DELETE FROM queue WHERE id = {Sqlite.Escape(id)};");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public QueueClear()
{
this.Mutex.Lock();
const db = OpenDB();
Sqlite.Exec(db,$"DELETE FROM queue;");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public AddToPersonalList(name, id)
{
id = TYTD.GetVideoId(id);
if(id == null) return;
this.Mutex.Lock();
var db = this.OpenDB();
//personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT)
Sqlite.Exec(db, $"INSERT INTO personal_lists (name) VALUES ({Sqlite.Escape(name)});");
Sqlite.Exec(db, $"INSERT INTO personal_list_entries (listName,videoId) VALUES ({Sqlite.Escape(name)},{Sqlite.Escape(id)});");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public RemoveFromPersonalList(name,id)
{
id = TYTD.GetVideoId(id);
if(id == null) return;
this.Mutex.Lock();
var db = this.OpenDB();
//personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT)
Sqlite.Exec(db, $"DELETE FROM personal_list_entries WHERE (listName = {Sqlite.Escape(name)} AND videoId = {Sqlite.Escape(id)});");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public RemovePersonalList(name)
{
this.Mutex.Lock();
var db = this.OpenDB();
//personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT)
Sqlite.Exec(db, $"DELETE FROM personal_list_entries WHERE listName = {Sqlite.Escape(name)}; DELETE FROM personal_lists WHERE name = {Sqlite.Escape(name)};");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public SetSubscriptionBell(url, bell)
{
var cid = TYTD.GetChannelId(url);
if(cid == null) return;
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
this.Mutex.Lock();
var db = this.OpenDB();
if(bell == null)
Sqlite.Exec(db,$"DELETE FROM subscriptions WHERE channelId = {Sqlite.Escape(cid)};");
else
Sqlite.Exec(db,$"INSERT INTO subscriptions (channelId,bell) VALUES ({Sqlite.Escape(cid)},{Sqlite.Escape(bell)});");
Sqlite.Close(db);
this.Mutex.Unlock();
}
public GetSubscriptionBell(url)
{
var cid = TYTD.GetChannelId(url);
if(cid == null) return null;
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db,$"SELECT * FROM subscriptions WHERE channelId = {Sqlite.Escape(cid)};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(res) == "List" && res.Count > 0) return res[0].bell;
return null;
}
public GetSubscriptionUrls()
{
var cid = TYTD.GetChannelId(url);
if(cid == null) return [];
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db,"SELECT * FROM subscriptions;");
Sqlite.Close(db);
this.Mutex.Unlock();
var list=[];
if(TypeOf(res) == "List" && res.Count > 0) {
each(var i : res)
{
list.Add(i.channelId);
}
}
return list;
}
public RemoveSubscription(url)
{
SetSubscriptionBell(url,null);
}
public VideoStarted = new TYTD.Event();
public VideoProgress = new TYTD.Event();
public BeforeQueued = new TYTD.Event();
private CurrentVideo = {
Title = "N/A",
Channel = "N/A",
VideoId = "",
ChannelId = ""
};
private CurrentVideoProgress = 0.0;
public VideoEnded = new TYTD.Event();
public Bell = new TYTD.Event();
public Plugins = [];
/^
The mutex (for database)
^/
public Mutex = new Mutex();
public Running=true;
private DownloaderThreadHandle;
public GetProgress() {
this.Mutex.Lock();
const progress = {CurrentVideoProgress,CurrentVideo };
this.Mutex.Unlock();
return progress;
}
public GetProgressEx() {
this.Mutex.Lock();
const db = OpenDB();
const resultVideos = Sqlite.Exec(db, "SELECT COUNT(*) FROM videos;");
const resultQueue = Sqlite.Exec(db, "SELECT COUNT(*) FROM queue;");
Sqlite.Close(db);
const videoCount = TypeIsList(resultVideos) ? (ParseLong(resultVideos[0].["COUNT(*)"])??0) : 0;
const queueCount = TypeIsList(resultQueue) ? (ParseLong(resultQueue[0].["COUNT(*)"])??0) : 0;
const progress = {CurrentVideoProgress,CurrentVideo,QueueCount=queueCount, VideoCount=videoCount};
this.Mutex.Unlock();
return progress;
}
/^
Get Video Queue count
^/
public getVideoQueueCount() {
Mutex.Lock();
const db = OpenDB();
const res = Sqlite.Exec(db, "SELECT COUNT(*) FROM queue;");
Sqlite.Close(db);
Mutex.Unlock();
const queueCount = res[0].["COUNT(*)"];
if(TypeIsString(queueCount))
{
return ParseLong(queueCount);
}
return 0;
}
private PlaylistThreadHandle;
private PlaylistQueue = new TYTD.Queue();
public Config = {
TYTDTag = "UnknownPC",
BellTimer = 10800,
EnablePlugins=true,
OobeState = "oobe"
};
public SaveConfig()
{
this._setPluginValue("","settings", Json.Encode(this.Config));
}
public GetPlaylistThumbnail(id,res)
{
var id = TYTD.GetPlaylistId(id);
if(id == null) return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
this.Mutex.Lock();
var db = this.OpenDB();
var _res = Sqlite.Exec(db,$"SELECT * FROM playlists p INNER JOIN playlist_entries e ON p.id = e.playlistId WHERE p.playlistId = {Sqlite.Escape(id)} LIMIT 1;");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(_res) == "List" && _res.Length > 0)
{
var vid = _res[0].videoId;
return FS.ReadAllBytes(this.Storage,TryDownloadVideoThumbnail(vid,res));
}
return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
}
public GetChannelThumbnail(id,res)
{
var id = TYTD.GetChannelId(id);
if(id == null) return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
this.Mutex.Lock();
var db = this.OpenDB();
var _res = Sqlite.Exec(db,$"SELECT * FROM videos WHERE channelId = {Sqlite.Escape(id)} LIMIT 1;");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(_res) == "List" && _res.Length > 0)
{
var vid = _res[0].videoId;
return FS.ReadAllBytes(this.Storage,TryDownloadVideoThumbnail(vid,res));
}
return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
}
private LoadPlugin(path)
{
//{name,version, pluginEnv, pluginObject, pluginIcon, pluginName, info}
var strm = this.Storage.OpenFile(path,"rb");
var exec = VM.LoadExecutable(strm);
strm.Close();
var name = exec.Name;
var version = exec.Version;
func loadExec(_pkg, _exec, _path)
{
var subdir = new SubdirFilesystem(this.Storage,_path.GetParent());
var info = {};
try {info = Json.Decode(_exec.Info);} catch(ex) {}
_pkg.info = info;
_pkg.pluginName = TypeOf(info.short_name) == "String" ? info.short_name : name;
var reso = _exec.Resources;
var ico = _exec.Icon;
_pkg.pluginIcon = TypeOf(ico) == "ByteArray" ? ico : embed("package_icon.png");
subdir.CreateDirectory(/"Files");
var d = {
TYTD = {
Downloader = this,
GetVideoId = TYTD.GetVideoId,
GetPlaylistId = TYTD.GetPlaylistId,
GetChannelId = TYTD.GetChannelId,
Config = {
GetAt = (key)=>{
return this._getPluginValue(_pkg.pluginName, key);
},
SetAt = (key,value)=>{
var value=value.ToString();
this._setPluginValue(_pkg.pluginName,key,value);
return value;
},
Directory = new SubdirFilesystem(subdir, /"Files"),
DirectoryPath = this.DatabaseDirectory / "Plugins" / _path.GetParent().GetFileName() / "Files"
}
},
Resolution = Resolution,
SubscriptionBell = SubscriptionBell
};
var env = VM.CreateEnvironment(d);
try{
env.RegisterEverything();
}catch(ex) Console.WriteLine(ex);
env.LockRegister();
env.LoadFileWithDependencies(subdir, _exec);
_pkg.pluginObject = d.PluginInit();
if(TypeIsDefined(_pkg.pluginObject.Server))
{
var path = /"plugin"/_pkg.pluginName;
this.Servers.Mount(path.ToString(),_pkg.pluginObject.Server);
}
_pkg.pluginEnv = env;
}
each(var pkg : this.Plugins)
{
if(pkg.name == name) {
if(pkg.version >= version) return;
pkg.Close();
pkg.version = version;
loadExec(pkg, exec, path);
return;
}
}
var pkg2 = {name,version};
func _close()
{
if(TypeIsDefined(pkg2.pluginObject.Server))
{
var path = /"plugin"/pkg2.pluginName;
this.Servers.Unmount(path.ToString());
}
pkg2.pluginObject.Close();
}
pkg2.Close = _close;
loadExec(pkg2, exec, path);
this.Plugins.Add(pkg2);
}
public LoadPlugins()
{
if(this.Config.EnablePlugins)
{
var dir = /"Plugins";
each(var item : this.Storage.EnumeratePaths(dir))
{
if(this.Storage.DirectoryExists(item))
{
LoadPlugin(item/$"{item.GetFileName()}.crvm");
}
}
}
}
public Start()
{
this.Storage.CreateDirectory(/"Streams");
this.Storage.CreateDirectory(/"Plugins");
if(!this.Storage.FileExists(/"Streams"/"nullthumb.jpg"))
{
var resp=Net.Http.MakeRequest("https://s.ytimg.com/vi/0/0.jpg");
var strm = this.Storage.OpenFile(/"Streams"/"nullthumb.jpg","wb");
resp.CopyToStream(strm);
resp.Close();
}
this.InitDatabase();
this.DownloadThreadHandle= new Thread(this.DownloadThread);
this.PlaylistThreadHandle = new Thread(this.PlaylistThread);
this.LoadPlugins();
}
private lastSubPollTime = 0;
private PlaylistThread()
{
while(this.Running)
{
try {
this.FlushExpired();
var res = this.PlaylistQueue.Pop();
if(TypeOf(res) != "Null")
{
res();
}
var currentTime = DateTime.NowEpoch ?? 0;
var bt = this.Config.BellTimer;
if((currentTime-this.lastSubPollTime) > bt)
{
this.lastSubPollTime = currentTime;
this.Mutex.Lock();
var db = this.OpenDB();
var res = Sqlite.Exec(db, "SELECT * FROM subscriptions;");
Sqlite.Close(db);
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS subscriptions (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE ON CONFLICT REPLACE, bell TEXT);");
this.Mutex.Unlock();
/*
/^ Disabled bell ^/
static getDisabled() "Disabled";
/^ Download (Low quality) ^/
static getDownloadLow() "DownloadLow";
/^ Download (High quality) ^/
static getDownloadHigh() "DownloadHigh";
/^ Download (and notify) (Low quality) ^/
static getBellLow() "BellLow";
/^ Download (and notify) (High quality) ^/
static getBellHigh() "BellHigh";
/^ Notify ^/
static getBell() "Bell";
*/
each(var sub : res)
{
var downloadRes = Resolution.NoDownload;
var notify = false;
switch(sub.bell)
{
case SubscriptionBell.Bell:
notify=true;
break;
case SubscriptionBell.BellLow:
notify=true;
case SubscriptionBell.DownloadLow:
downloadRes = Resolution.LowVideo;
break;
case SubscriptionBell.BellHigh:
notify = true;
case SubscriptionBell.DownloadHigh:
downloadRes = Resolution.MKV;
break;
}
if(!notify && downloadRes == Resolution.NoDownload) continue;
var cid = sub.channelId;
each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
{
each(var videoId : batch)
{
if(this.GetVideo(videoId) == null)
{
newVideos.Add(videoId);
if(notify)
{
this.PutVideoInfoIfNotExists(videoId);
var res = this.GetVideo(videoId);
if(res != null)
this.Bell.Invoke(this,{
Video = {
VideoId = res.videoId,
ChannelId = res.channelId,
Title = res.title,
Channel = res.author
}
});
}
if(downloadRes != Resolution.NoDownload)
{
this.DownloadVideo(videoId,downloadRes);
}
}
}
}
}
}
}catch(ex) {
try{
this.LOG($"Exception caught on playlist thread: {ex}");
}catch(ex2){}
}
}
}
private LOGDATEFMT = "%Y%m%d_%H%M%S";
private LOGDATE = DateTime.Now;
/^
Log stuff
^/
public LOG(text)
{
this.Mutex.Lock();
this.Storage.CreateDirectory(/"Logs");
var strm = this.Storage.OpenFile(/"Logs"/$"{this.LOGDATE.ToString(this.LOGDATEFMT)}.log","a");
strm.WriteText($"[{DateTime.Now.ToString()}] {text}\n");
strm.Close();
this.Mutex.Unlock();
}
private DownloadThread()
{
while(this.Running)
{
try {
var res = PopQueue();
//Console.WriteLine(res);
if(TypeOf(res) != "Null")
{
if(TypeIsDefined(res.TYTD = this)){
res.Progress = (progress)=>{
this.Mutex.Lock();
this.CurrentVideoProgress = progress;
this.Mutex.Unlock();
this.VideoProgress.Invoke(this, {
Video = res.Video,
progress
});
};
this.Mutex.Lock();
this.CurrentVideo = res.Video;
this.Mutex.Unlock();
this.VideoStarted.Invoke(this,{
Video = res.Video
});
res.Start();
this.VideoEnded.Invoke(this,{
Video = res.Video
});
}
}
} catch(ex) {
try{
this.LOG($"Exception caught on download thread: {ex}");
}catch(ex2){}
}
}
}
public Stop()
{
each(var item : this.Plugins)
{
item.Close();
}
this.Running = false;
this.PlaylistThreadHandle.Join();
this.DownloaderThreadHandle.Join();
}
/*
public GetChannelThumbnail(channelId)
{
var id = TYTD.GetChannelId(channelId);
var path = /"ChannelThumbnails"/id.Substring(2,4)/id.Substring(6);
if(this.Storage.FileExists(path+".webp"))
{
return {
data = FS.ReadAllBytes(this.Storage,path+".webp"),
mime = "image/webp"
};
}
else if(this.Storage.FileExists(path+".jpg"))
{
return {
data = FS.ReadAllBytes(this.Storage,path+".jpg"),
mime = "image/jpeg"
};
}
return null;
}*/
public TryDownloadVideoThumbnail(v, res)
{
var id = TYTD.GetVideoId(v);
if(TypeOf(id) == "String")
{
var path = /"Streams"/id.Substring(0,4) / id.Substring(4) / $"{res}.jpg";
this.Storage.CreateDirectory(path.GetParent());
if(this.Storage.FileExists(path))
{
return path;
}
else {
try {
var url = $"https://s.ytimg.com/vi/{id}/{res}.jpg";
const resp = Net.Http.MakeRequest(url,{FollowRedirects=true});
if(resp.StatusCode >= 200 && resp.StatusCode <= 299)
{
const strm=this.Storage.OpenFile(path,"wb");
resp.CopyToStream(strm);
strm.Close();
}
else {
const bytes = FS.ReadAllBytes(this.Storage,"/Streams/nullthumb.jpg");
FS.WriteAllBytes(this.Storage, path, bytes);
}
resp.Close(); //for other implementations
}catch(ex) {
return /"Streams"/"nullthumb.jpg";
}
return path;
}
}
return null;
}
public GetVideoThumbnail(v, res)
{
var thumb = TryDownloadVideoThumbnail(v,res);
if(thumb != null)
{
return FS.ReadAllBytes(this.Storage, thumb);
}
return FS.ReadAllBytes(this.Storage,/"Streams"/"nullthumb.jpg");
}
/^
Open the database
^/
public OpenDB()
{
var dbFile = this.DatabaseDirectory / "tytd.db";
return Sqlite.Open(dbFile);
}
/^
Get whether package is installed
version must be the current version as a Version not string
returns 0 if not, 1 if installed or 2 if can update
^/
public PackageState(name, version)
{
each(var item : this.Plugins)
{
if(item.name == name) {
if(item.version < version) return 2;
return 1;
}
}
return 0;
}
private PackageDownload(name,version)
{
var dir = new SubdirFilesystem(this.Storage,/"Plugins");
this.PackageManager.DownloadPlugin(dir,name,version);
this.LoadPlugins();
}
/^
Install plugin
version must be a Version not a String
^/
public PackageInstall(name, version)
{
each(var item : this.Plugins)
{
if(item.name == name)
{
if(item.version >= version)
{
return;
}
}
}
this.PackageDownload(name, version);
}
/^
Uninstall plugin
^/
public PackageUninstall(name)
{
each(var item : this.Plugins)
{
if(item.name == name)
{
this.Plugins.Remove(item);
item.Close();
this.Storage.DeleteDirectoryRecurse(/"Plugins"/item.pluginName);
break;
}
}
}
/^
Get the TYTD Tag
^/
public getTYTDTag()
{
return this.Config.TYTDTag ?? "UnknownPC";
}
/^
Download video info if it does not exist
pass in a video id or url
^/
public PutVideoInfoIfNotExists(vid)
{
var id = TYTD.GetVideoId(vid);
if(id != null)
{
var e = this.GetVideo(id);
if(e == null)
{
var req = this.ManifestRequest(id);
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
^/
public PutVideoInfo(info)
{
this.Mutex.Lock();
var db = this.OpenDB();
var keywords = info.keywords;
if(TypeOf(keywords) != "List") keywords = [];
var keywordsStr = Sqlite.Escape(Json.Encode(keywords));
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{keywordsStr},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
Sqlite.Exec(db, d);
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});");
Sqlite.Close(db);
this.Mutex.Unlock();
}
private InitDatabase()
{
this.Mutex.Lock();
var db = this.OpenDB();
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS videos (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT UNIQUE, title TEXT, lengthSeconds INTEGER, keywords TEXT, channelId TEXT, shortDescription TEXT, viewCount INTEGER, author TEXT, addDate INTEGER, tytdTag TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlists (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId TEXT UNIQUE,channelId TEXT,channelTitle TEXT, title TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS channels (id INTEGER PRIMARY KEY AUTOINCREMENT, channelId TEXT UNIQUE, title TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS playlist_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, playlistId INTEGER, videoId TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_lists (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT);");
Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS personal_list_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, listName TEXT, videoId TEXT);");
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, 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,"CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY AUTOINCREMENT, videoId TEXT, resolution 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)
{
try {
this.Config = Json.Decode(config[0].value);
}catch(ex) {
}
}
this.Config.OobeState ??= "oobe";
Sqlite.Close(db);
this.Mutex.Unlock();
}
private _setPluginValue(extension,key,value)
{
this.Mutex.Lock();
var db = this.OpenDB();
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);
this.Mutex.Unlock();
}
private _getPluginValue(extension,key)
{
this.Mutex.Lock();
var db = this.OpenDB();
var config=Sqlite.Exec(db,$"SELECT * FROM plugin_settings WHERE extension = {Sqlite.Escape(extension)} AND key = {Sqlite.Escape(key)};");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeOf(config) == "List" && config.Length>0)
{
return config[0].value;
}
}
private DownloadChannelThumbInternal(channelId, thumbnail_url)
{
var id = TYTD.GetChannelId(channelId);
var path = /"ChannelThumbnails"/id.Substring(2,4)/id.Substring(6);
this.Storage.CreateDirectory(path.GetParent());
if(!(this.Storage.FileExists(path + ".webp") || this.Storage.FileExists(path+".jpg")))
{
if(thumbnail_url.StartsWith("//")) thumbnail_url = $"https:{thumbnail_url}";
var dl = Net.Http.MakeRequest(thumbnail_url);
if(dl.StatusCode >= 200 && dl.StatusCode <= 299)
{
var ct=dl.ResponseHeaders.TryGetFirst("Content-Type");
if(TypeOf(ct) == "String")
{
var s = ct.Split("; ",true,2);
var ext = ".jpg";
if(s[0] == "image/webp")
ext = ".webp";
var dest = this.Storage.OpenFile(path+ext,"wb");
dl.CopyToStream(dest);
dest.Close();
}
}
}
}
private DiscoverInternal(q,continuation,params)
{
if(continuation == undefined) var continuation = null;
var jo = {
query = q,
params,
continuation,
request = {
internalExperimentFlags=[],
useSsl=true
},
user = {
lockedSafetyMode=false
},
context = {
client = {
clientName = "WEB",
clientVersion = "2.20250710.09.00",
hl = "en-US",
gl = "US",
platform = "DESKTOP",
originalUrl = "https://www.youtube.com",
utcOffsetMinutes = 0
}
}
};
var url = $"https://www.youtube.com/youtubei/v1/search?key=AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w&prettyPrint=false";
var requestData = {
Method = "POST",
RequestHeaders = [
{
Key = "User-Agent",
Value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"
},
{
Key="Cookie",
Value = "SOCS: CAISEwgDEgk2NzM5OTg2ODUaAmVuIAEaBgiA6p23Bg"
},
{
Key="Referer",
Value="https://www.youtube.com"
},
{
Key="Origin",
Value="https://www.youtube.com"
},
{
Key="X-YouTube-Client-Version",
Value="2.20250710.09.00"
},
{
Key="X-YouTube-Client-Name",
Value="1"
}
],
Body = Net.Http.TextHttpRequestBody(jo.ToString(),"application/json")
};
var resp = Net.Http.MakeRequest(url,requestData);
if(resp.StatusCode < 200 || resp.StatusCode > 299) return null;
var jo2 = Json.Decode(resp.ReadAsString());
var o = jo2.contents;
if(TypeOf(o) != "Dictionary") o = jo2.onResponseReceivedCommands;
return o;
}
public DiscoverVideosBasic(q,$continuation)
{
var o = this.DiscoverInternal(q,continuation,"EgIQAQ==");
if(o == null) return null;
var videos = Dictionary.FindByKey(o,"videoRenderer");
var items=[];
each(var item : videos)
{
items.Add({
id = item.videoId,
title = item.title.runs[0].text
});
}
return items;
}
public Discover(q,$continuation)
{
var o = this.DiscoverInternal(q,continuation,null);
if(o == null) return null;
var videos = Dictionary.FindByKey(o,"videoRenderer");
var playlists = Dictionary.FindByKey(o,"lockupViewModel");
if(TypeOf(playlists) != "List" || playlists.Length == 0) playlists = Dictionary.FindByKey(o,"playlistRenderer");
var channels = Dictionary.FindByKey(o,"channelRenderer");
var items=[];
for(var i = 0; i < videos.Length; i++)
{
TryDownloadVideoThumbnail(videos[i].videoId,"0");
items.Add({
id = videos[i].videoId,
title = videos[i].title.runs[0].text,
type = "video",
author = videos[i].ownerText.runs,
views = videos[i].viewCountText.simpleText,
uploaded = videos[i].publishedTimeText.simpleText
});
}
for(var i = 0; i < playlists.Length; i++)
{
items.Add({
item = playlists[i],
type="playlist"
});
}
for(var i = 0; i < channels.Length; i++)
{
var thumbnail_url = "";
var thumbnail_width = 0;
var thumbnail_height = 0;
var description = "";
each(var item : channels[i].thumbnail.thumbnails)
{
if(item.width > thumbnail_width && item.height > thumbnail_height)
{
thumbnail_url = item.url;
thumbnail_width = item.width;
thumbnail_height = item.height;
}
}
each(var item : channels[i].descriptionSnippet.runs)
{
description += item.text;
}
var channelId = channels[i].channelId;
this.DownloadChannelThumbInternal(channelId, thumbnail_url);
items.Add({
id=channelId,
title = channels[i].title.simpleText,
type="channel",
subs = channels[i].videoCountText.simpleText,
description
});
}
return {items};
}
/^
Make a video manifest request
^/
public ManifestRequest(vid)
{
for(var tries=0; tries<5;tries++) {
var id = TYTD.GetVideoId(vid);
if(id == null) return null;
TryDownloadVideoThumbnail(id,"0");
TryDownloadVideoThumbnail(id,"1");
TryDownloadVideoThumbnail(id,"2");
TryDownloadVideoThumbnail(id,"3");
TryDownloadVideoThumbnail(id,"sddefault");
TryDownloadVideoThumbnail(id,"hqdefault");
TryDownloadVideoThumbnail(id,"mqdefault");
TryDownloadVideoThumbnail(id,"default");
TryDownloadVideoThumbnail(id,"maxresdefault");
var requestData = {
Method = "POST",
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/21.03.36 (Linux; U; Android 16; GB) gzip"
}
],
Body = Net.Http.TextHttpRequestBody(embed("request.json").ToString(),"application/json")
};
this.RateLimit();
var response = Net.Http.MakeRequest("https://youtubei.googleapis.com/youtubei/v1/visitor_id?prettyPrint=false",requestData);
if(response.StatusCode != 200) throw "Not success";
var data = Json.Decode(response.ReadAsString());
var visitor = data.responseContext.visitorData;
response.Close();
var url = "https://youtubei.googleapis.com/youtubei/v1/reel/reel_item_watch?prettyPrint=false&t=dQOGvBU_R4ke&id=4eeZWTqq5VE&$fields=playerResponse";
requestData = {
Method = "POST",
RequestHeaders = [
{
Key = "User-Agent",
Value = "com.google.android.youtube/21.03.36 (Linux; U; Android 16; GB) gzip"
},
],
Body = Net.Http.TextHttpRequestBody(embed("request2.json").ToString().Replace("VIDEO_ID_HERE", id).Replace("VISITOR_DATA",visitor),"application/json")
};
var response = Net.Http.MakeRequest(url,requestData);
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))
{
Console.WriteLine("YEY");
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;
}
}
private enumerable QueryPlaylistItems(id, isPlaylist)
{
var url = "https://www.youtube.com/youtubei/v1/next?key=AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w";
func makeRequest(videoId,index,visitorData)
{
var retriesCount = 5;
for(var retriesRemaining = retriesCount; ; retriesRemaining--)
{
var json = {
playlistId = id,
videoId = videoId,
playlistIndex = index,
context = {
client = {
clientName = "WEB",
clientVersion = "2.20210408.08.00",
hl = "en",
gl = "US",
utcOffsetMinutes = 0,
visitorData = visitorData
}
}
};
var requestData = {
Method = "POST",
RequestHeaders = [
{
Key = "User-Agent",
Value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"
},
{
Key = "Cookie",
Value = "SOCS: CAISEwgDEgk4MTM4MzYzNTIaAmVuIAEaBgiApPzGBg"
}
],
Body = Net.Http.TextHttpRequestBody(json.ToString(),"application/json")
};
this.RateLimit();
var response = Net.Http.MakeRequest(url,requestData);
if(response.StatusCode != 200) throw "Not success";
const text = response.ReadAsString();
var data = Json.Decode(text);
var cr = data.contents.twoColumnWatchNextResults.playlist.playlist;
if(cr == null || cr == undefined)
{
if(index > 0 && visitorData != null && retriesRemaining > 0)
continue;
if(index <= 0 && visitorData == null && retriesRemaining >= retriesCount)
{
Net.Http.MakeRequest($"https://youtube.com/playlist?list={id}");
continue;
}
throw $"Playlist '{id}' is not available.";
}
var items = [];
each(var item : cr.contents)
{
if(item.playlistPanelVideoRenderer != null && item.playlistPanelVideoRenderer != undefined)
{
items.Add({
videoId = item.playlistPanelVideoRenderer.videoId,
index = item.playlistPanelVideoRenderer.navigationEndpoint.watchEndpoint.index
});
}
}
return {
title = cr.title,
channelTitle = cr.shortBylineText.runs[0].text,
channelId = cr.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId,
videos = items,
visitorData = data.responseContext.visitorData
};
}
}
var first = true;
/*this.Mutex.Lock();
var db = this.OpenDB();
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{Sqlite.Escape(info.keywords.ToString())},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
Sqlite.Exec(db, d);
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.author)});");
Sqlite.Close(db);
this.Mutex.Unlock();*/
var encounteredIds = [];
var lastVideoId=null;
var lastVideoIndex = 0;
var visitorData = null;
var dbRow = null;
do(true)
{
var resp = makeRequest(lastVideoId,lastVideoIndex,visitorData);
/*
return {
title = cr.title,
channelTitle = cr.shortBylineText.runs[0].text,
channelId = cr.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId,
videos = items,
visitorData = data.responseContext.visitorData
};
*/
if(first) {
this.Mutex.Lock();
var db = this.OpenDB();
if(isPlaylist)
{
Sqlite.Exec(db,$"INSERT INTO playlists (playlistId,channelId,channelTitle,title) VALUES ({Sqlite.Escape(id)},{Sqlite.Escape(resp.channelId)},{Sqlite.Escape(resp.channelTitle)},{Sqlite.Escape(resp.title)});");
var res = Sqlite.Exec(db,$"SELECT * FROM playlists WHERE playlistId = {Sqlite.Escape(id)};");
if(TypeOf(res) == "List" && res.Count > 0) dbRow=ParseLong(res[0].id);
if(TypeOf(dbRow) == "Long")
Sqlite.Exec(db,$"DELETE FROM playlist_entries WHERE playlistId = {dbRow};");
}
Sqlite.Exec(db, $"INSERT INTO channels (channelId,title) VALUES ({Sqlite.Escape(resp.channelId)},{Sqlite.Escape(resp.channelTitle)});");
Sqlite.Close(db);
this.Mutex.Unlock();
first=false;
}
var ids = [];
each(var itm : resp.videos)
{
var vid = itm.videoId;
var vidx = itm.index;
lastVideoId = vid;
lastVideoIndex = vidx;
if(encounteredIds.IndexOf(vid) > -1) continue;
encounteredIds.Add(vid);
ids.Add(vid);
if(TypeOf(dbRow) == "Long")
{
this.Mutex.Lock();
var db = this.OpenDB();
Sqlite.Exec(db,$"INSERT INTO playlist_entries (playlistId,videoId) VALUES ({dbRow},{Sqlite.Escape(vid)});");
Sqlite.Close(db);
this.Mutex.Lock();
}
}
if(ids.Count == 0) break;
yield ids;
if(visitorData == null || visitorData == undefined)
visitorData = resp.visitorData;
}
}
private lastRequest = 0;
private requests = 0;
private rlm=new Mutex();
private RateLimit()
{
this.rlm.Lock();
var curRequest = DateTime.NowEpoch;
if((curRequest - this.lastRequest) > 10)
{
this.requests = 0;
}
this.requests++;
if(this.requests >= 1)
{
DateTime.Sleep(1500);
this.requests=0;
curRequest = DateTime.NowEpoch;
}
this.lastRequest = curRequest;
this.rlm.Unlock();
}
public GetSessionToken(ctx)
{
var cookie = ctx.RequestHeaders.TryGetFirst("Cookie");
if(TypeOf(cookie) == "String")
{
each(var part : cookie.Split("; "))
{
if(part.Length > 0)
{
var cookieKV = part.Split("=",true,2);
if(cookieKV.Length == 2 && cookieKV[0] == "Session")
{
return cookieKV[1];
}
}
}
}
var auth = ctx.RequestHeaders.TryGetFirst("Authorization");
if(TypeOf(auth) == "String")
{
auth=auth.Split(" ",true,2);
if(auth.Length < 2) return null;
if(auth[0] != "Bearer") return null;
return auth[1];
}
return null;
}
public Logout(ctx)
{
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)};");
Sqlite.Close(db);
this.Mutex.Unlock();
}
}
public CreateAccount(ctx, username, password, flags)
{
var loggedIn = this.IsLoggedIn(ctx);
if(loggedIn == 0xFFFFFFFE) flags = 0xFFFFFFFE;
if(UserFlags.CanCreateUsers(loggedIn))
{
this.Mutex.Lock();
const db = this.OpenDB();
const salt = Crypto.RandomBytes(32, "TYTD2025");
const hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
const resp = Sqlite.Exec(db, $"INSERT INTO users (username, password_salt, password_hash, flags) VALUES ({Sqlite.Escape(username)}, {Sqlite.Escape(Crypto.Base64Encode(salt))}, {Sqlite.Escape(Crypto.Base64Encode(hash))}, {flags});");
Sqlite.Close(db);
this.Mutex.Unlock();
if(TypeIsString(resp)) return resp;
}
else {
return "You are not authorized to create user accounts";
}
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();
const db = this.OpenDB();
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
var noAccounts=true;
if(TypeOf(res) == "List" && res.Length != 0)
{
if(res[0].["COUNT(*)"] != "0")
noAccounts=false;
}
if(noAccounts) {
Sqlite.Close(db);
this.Mutex.Unlock();
return 0xFFFFFFFE;
}
const sessionToken = this.GetSessionToken(ctx);
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;
}
}
Sqlite.Close(db);
this.Mutex.Unlock();
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)
{
this.Mutex.Lock();
const db = this.OpenDB();
const res=Sqlite.Exec(db, "SELECT COUNT(*) FROM users;");
var noAccounts=true;
if(TypeOf(res) == "List" && res.Length != 0)
{
if(res[0].["COUNT(*)"] != "0")
noAccounts=false;
}
if(noAccounts) {
Sqlite.Close(db);
this.Mutex.Unlock();
return { flags = 0xFFFFFFFE, username = "N/A" };
}
const sessionToken = this.GetSessionToken(ctx);
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()}");
}
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);
return item;
}
}
Sqlite.Close(db);
this.Mutex.Unlock();
return { flags = 0, username = "N/A" };
}
public Passwd(ctx, oldPassword, newPassword, logout)
{
const whoami = this.WhoAmI(ctx);
if(whoami.flags != 0 && TypeIsDictionary(whoami) && TypeIsString(whoami.password_salt))
{
var salt = Crypto.Base64Decode(whoami.password_salt);
var hash = Crypto.PBKDF2(password, salt, UserFlags.ITTR,64,384);
var hashStr = Crypto.Base64Encode(hash);
if(item.password_hash == hashStr)
{
this.Mutex.Lock();
const db = this.OpenDB();
const res = Sqlite.Exec(db, $"UPDATE users SET password_hash = {Sqlite.Escape(Crypto.Base64Encode(hash))}, password_salt = {Sqlite.Escape(Crypto.Base64Encode(salt))} WHERE username = {Sqlite.Escape(item.username)};");
if(TypeIsList(res))
{
if(logout)
{
Sqlite.Exec(db, $"DELETE FROM sessions WHERE accountId = {Sqlite.Escape(res.accountId)};");
}
Sqlite.Close(db);
this.Mutex.Unlock();
return {success=true};
}
else
{
Sqlite.Close(db);
this.Mutex.Unlock();
return {success=false, reason = res};
}
}
}
return { success=false, reason = "Unable to login for some reason, maybe your token expired"};
}
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)
{
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)
{
var rand = Net.Http.UrlEncode(Crypto.Base64Encode(Crypto.RandomBytes(32, "TYTD2025")));
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;
}
/^
Send the database as http response
^/
public SendDatabase(ctx)
{
if(UserFlags.CanDownloadDB(this.IsLoggedIn(ctx)))
{
this.Mutex.Lock();
try {
var strm = FS.Local.OpenFile(this.DatabaseDirectory/"tytd.db","rb");
ctx.SendStream(strm);
strm.Close();
}catch(ex) {
Console.WriteLine($"ERROR: {ex}");
}
this.Mutex.Unlock();
return true;
}
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"};
}
}