mirror of
https://onedev.site.tesses.net/tytd2025
synced 2026-02-08 17:45:45 +00:00
1550 lines
41 KiB
Plaintext
1550 lines
41 KiB
Plaintext
class TYTD.Downloader {
|
|
public Storage;
|
|
public DatabaseDirectory;
|
|
public PackageManager = new Tesses.CrossLang.PackageManager();
|
|
|
|
public Servers = Net.Http.MountableServer({Handle=(ctx)=>false});
|
|
|
|
public Downloader(vfs,dbDir)
|
|
{
|
|
this.Storage = vfs;
|
|
this.DatabaseDirectory = dbDir;
|
|
}
|
|
public DownloadVideo(id,$res)
|
|
{
|
|
switch(res)
|
|
{
|
|
case Resolution.NoDownload:
|
|
{
|
|
var id = TYTD.GetVideoId(id);
|
|
if(id != null)
|
|
PutVideoInfoIfNotExists(id);
|
|
}
|
|
break;
|
|
case Resolution.LowVideo:
|
|
this.Queue.Push(new TYTD.SDVideoDownload(id));
|
|
break;
|
|
case Resolution.VideoOnly:
|
|
this.Queue.Push(new TYTD.VOVideoDownload(id));
|
|
break;
|
|
|
|
case Resolution.AudioOnly:
|
|
this.Queue.Push(new TYTD.AOVideoDownload(id));
|
|
break;
|
|
case Resolution.MP4:
|
|
this.Queue.Push(new TYTD.TranscodeVideo(id,".mp4"));
|
|
break;
|
|
case Resolution.MKV:
|
|
this.Queue.Push(new TYTD.TranscodeVideo(id,".mkv"));
|
|
break;
|
|
case Resolution.MP3:
|
|
this.Queue.Push(new TYTD.TranscodeAudio(id,".mp3"));
|
|
break;
|
|
case Resolution.FLAC:
|
|
this.Queue.Push(new TYTD.TranscodeAudio(id,".flac"));
|
|
break;
|
|
case Resolution.DontConvert:
|
|
this.Queue.Push(new TYTD.NoConvertVideoDownload(id));
|
|
break;
|
|
}
|
|
}
|
|
|
|
public DownloadPlaylist(id,$res)
|
|
{
|
|
var pid = TYTD.GetPlaylistId(id);
|
|
|
|
|
|
if(pid != null)
|
|
{
|
|
this.PlaylistQueue.Push(()=>{
|
|
each(var item : this.QueryPlaylistItems(pid,true))
|
|
{
|
|
each(var vid : item)
|
|
{
|
|
|
|
DownloadVideo(vid, res);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public DownloadChannel(id,$res)
|
|
{
|
|
var cid = TYTD.GetChannelId(id);
|
|
|
|
if(cid != null)
|
|
{
|
|
this.PlaylistQueue.Push(()=>{
|
|
each(var item : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
|
|
{
|
|
each(var vid : item)
|
|
{
|
|
DownloadVideo(vid, res);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public GetPluginThumbnail(name)
|
|
{
|
|
each(var item : this.Plugins)
|
|
{
|
|
if(item.pluginName == name)
|
|
return item.pluginIcon;
|
|
}
|
|
|
|
return embed("package_icon.png");
|
|
}
|
|
|
|
|
|
public DownloadItem(url, $res)
|
|
{
|
|
var vid = TYTD.GetVideoId(url);
|
|
var pid = TYTD.GetPlaylistId(url);
|
|
|
|
var cid = TYTD.GetChannelId(url);
|
|
|
|
if(vid != null && url.Length == 11)
|
|
{
|
|
this.DownloadVideo(vid, res);
|
|
}
|
|
else if(pid != null)
|
|
{
|
|
this.DownloadPlaylist(pid,res);
|
|
}
|
|
else if(vid != null)
|
|
{
|
|
this.DownloadVideo(vid, res);
|
|
}
|
|
else if(cid != null)
|
|
{
|
|
this.DownloadChannel(cid,res);
|
|
}
|
|
|
|
}
|
|
|
|
public PageRedirect(url)
|
|
{
|
|
var vid = TYTD.GetVideoId(url);
|
|
|
|
if(vid != null && url.Length == 11)
|
|
{
|
|
return $"./video?v={Net.Http.UrlEncode(vid)}";
|
|
}
|
|
return "./";
|
|
}
|
|
|
|
public GetDownloadPath(url)
|
|
{
|
|
//Sqlite.Exec(db,"CREATE TABLE IF NOT EXISTS downloads (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, name TEXT, mime TEXT);");
|
|
this.Mutex.Lock();
|
|
var db = this.OpenDB();
|
|
var res = Sqlite.Exec(db, $"SELECT * FROM downloads WHERE url = {Sqlite.Escape(url)};");
|
|
|
|
Sqlite.Close(db);
|
|
this.Mutex.Unlock();
|
|
|
|
if(TypeOf(res) == "List" && res.Length > 0)
|
|
{
|
|
return /"Downloads"/$"{res[0].id}.bin";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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 GetVideos(query, offset, count)
|
|
{
|
|
this.Muxex.Lock();
|
|
var db = this.OpenDB();
|
|
var q = Sqlite.Escape($"%{query}%");
|
|
var res = Sqlite.Exec(db, $"SELECT * FROM videos v WHERE (v.title LIKE {q} OR v.shortDescription LIKE {q}) LIMIT {count} OFFSET {offset*count};");
|
|
|
|
|
|
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.lengthSeconds = ParseLong(item.lengthSeconds);
|
|
item.viewCount = ParseLong(item.viewCount);
|
|
item.viewCountStr = this.Views2Str(item.viewCount);
|
|
|
|
}
|
|
return res;
|
|
}
|
|
|
|
public GetPlaylists(query, offset, count)
|
|
{
|
|
this.Muxex.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;
|
|
}
|
|
public GetPlaylistContents(id, offset, count)
|
|
{
|
|
id = TYTD.GetPlaylistId(id);
|
|
if(id == null) return {
|
|
title = "N/A",
|
|
channelId="",
|
|
channelTitle="N/A",
|
|
items = []
|
|
};
|
|
this.Muxex.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 downloads (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, name TEXT, mime 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.lengthSeconds = ParseLong(item.lengthSeconds);
|
|
item.viewCount = ParseLong(item.viewCount);
|
|
item.viewCountStr = this.Views2Str(item.viewCount);
|
|
}
|
|
return {
|
|
channelTitle,
|
|
title,
|
|
channelId,
|
|
items = res2
|
|
};
|
|
}
|
|
public GetChannelContents(id, offset, count)
|
|
{
|
|
id = TYTD.GetChannelId(id);
|
|
if(id == null) return {
|
|
authorName = "N/A",
|
|
items = []
|
|
};
|
|
this.Muxex.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.lengthSeconds = ParseLong(item.lengthSeconds);
|
|
item.viewCount = ParseLong(item.viewCount);
|
|
item.viewCountStr = this.Views2Str(item.viewCount);
|
|
}
|
|
return {
|
|
authorName,
|
|
items = res
|
|
};
|
|
}
|
|
|
|
public GetChannels(query, offset, count)
|
|
{
|
|
this.Muxex.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;
|
|
}
|
|
|
|
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";
|
|
break;
|
|
case Resolution.VideoOnly:
|
|
return dir / "vo.bin";
|
|
break;
|
|
|
|
case Resolution.AudioOnly:
|
|
return dir / "ao.bin";
|
|
break;
|
|
case Resolution.MP4:
|
|
return dir / "conv.mp4";
|
|
break;
|
|
case Resolution.MKV:
|
|
return dir / "conv.mkv";
|
|
break;
|
|
case Resolution.MP3:
|
|
return dir / "conv.mp3";
|
|
break;
|
|
case Resolution.FLAC:
|
|
return dir / "conv.flac";
|
|
break;
|
|
}
|
|
throw $"Could not get file path for format {res}";
|
|
}
|
|
|
|
|
|
|
|
public GetVideo(vid)
|
|
{
|
|
var id = TYTD.GetVideoId(vid);
|
|
if(id == null) return null;
|
|
this.Muxex.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.lengthSeconds = ParseLong(out.lengthSeconds);
|
|
out.viewCount = ParseLong(out.viewCount);
|
|
out.viewCountStr = this.Views2Str(out.viewCount);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
|
|
public GetPlaylist(id)
|
|
{
|
|
var id = TYTD.GetPlaylistId(vid);
|
|
if(id == null) return null;
|
|
this.Muxex.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;
|
|
}
|
|
|
|
public GetChannel(id)
|
|
{
|
|
var id = TYTD.GetChannelId(vid);
|
|
if(id == null) return null;
|
|
this.Muxex.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.Muxex.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;
|
|
}
|
|
public SetPersonalListDescription(name,description)
|
|
{
|
|
this.Muxex.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();
|
|
}
|
|
public GetPersonalListDescription(name)
|
|
{
|
|
this.Muxex.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)
|
|
{
|
|
res[0].description;
|
|
}
|
|
return "";
|
|
}
|
|
public GetPersonalListContents(name, offset, count)
|
|
{
|
|
this.Muxex.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.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 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 e WHERE (e.listName = {Sqlite.Escape(name)} AND e.videoId = {Sqlite.Escape(id)});");
|
|
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 CurrentVideo = {
|
|
Title = "N/A",
|
|
Channel = "N/A",
|
|
VideoId = "",
|
|
ChannelId = ""
|
|
};
|
|
|
|
public CurrentVideoProgress = 0.0;
|
|
|
|
public VideoEnded = new TYTD.Event();
|
|
|
|
public Bell = new TYTD.Event();
|
|
|
|
public Plugins = [];
|
|
|
|
|
|
|
|
public Mutex = new Mutex();
|
|
|
|
public Running=true;
|
|
|
|
private DownloaderThreadHandle;
|
|
|
|
private Queue = new TYTD.Queue();
|
|
|
|
private PlaylistThreadHandle;
|
|
|
|
private PlaylistQueue = new TYTD.Queue();
|
|
|
|
public Config = {
|
|
TYTDTag = "UnknownPC",
|
|
BellTimer = 10800,
|
|
EnablePlugins=true
|
|
};
|
|
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;");
|
|
|
|
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;");
|
|
|
|
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) ? info.short_name : name;
|
|
var reso = _exec.Resources;
|
|
var ico = _exec.Icon;
|
|
_pkg.pluginIcon = ico >= 0 && i < reso.Count ? reso[ico] : embed("package_icon.png");
|
|
subdir.CreateDirectory(/"Files");
|
|
var d = {
|
|
TYTD = {
|
|
Downloader = this,
|
|
GetVideoId = TYTD.GetVideoId,
|
|
GetPlaylistId = TYTD.GetPlaylistId,
|
|
GetChannelId = TYTD.GetChannelId,
|
|
Config = {
|
|
GetAt = (key)=>{
|
|
return this._getPluginValue(info.short_name, key);
|
|
},
|
|
SetAt = (key,value)=>{
|
|
var value=value.ToString();
|
|
this._setPluginValue(info.short_name,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(_pkg.pluginObject.Server != null && _pkg.pluginObject.Server != undefined)
|
|
{
|
|
var path = /"plugin"/_pkg.pluginName;
|
|
|
|
this.Servers.Mount(path,_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(pkg2.pluginObject.Server != null && pkg2.pluginObject.Server != undefined)
|
|
{
|
|
var path = /"plugin"/pkg2.pluginName;
|
|
this.Servers.Unmount(path);
|
|
}
|
|
|
|
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(/"Downloads");
|
|
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)
|
|
{
|
|
var res = this.PlaylistQueue.Pop();
|
|
|
|
if(TypeOf(res) != "Null")
|
|
{
|
|
res();
|
|
}
|
|
|
|
var currentTime = DateTime.NowEpoch;
|
|
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.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;
|
|
break;
|
|
case SubscriptionBell.DownloadHigh:
|
|
downloadRes = Resolution.MKV;
|
|
break;
|
|
}
|
|
if(!notify && downloadRes == Resolution.NoDownload) continue;
|
|
var cid = sub.channelId;
|
|
|
|
var newVideos = [];
|
|
|
|
each(var batch : this.QueryPlaylistItems($"UU{cid.Substring(2)}",false))
|
|
{
|
|
each(var videoId : batch)
|
|
{
|
|
|
|
if(this.GetVideo(videoId) == null)
|
|
newVideos.Add(videoId);
|
|
}
|
|
}
|
|
|
|
if(notify)
|
|
{
|
|
each(var id : newVideos)
|
|
{
|
|
this.PutVideoInfoIfNotExists(id);
|
|
var res = this.GetVideo(id);
|
|
if(res != null)
|
|
this.Bell.Invoke(this,{
|
|
Video = res
|
|
});
|
|
|
|
}
|
|
}
|
|
if(downloadRes != Resolution.NoDownload)
|
|
{
|
|
each(var id : newVideos)
|
|
{
|
|
this.DownloadVideo(id,downloadRes);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private DownloadThread()
|
|
{
|
|
while(this.Running)
|
|
{
|
|
var res = this.Queue.Pop();
|
|
|
|
if(TypeOf(res) != "Null")
|
|
{
|
|
|
|
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
|
|
});
|
|
|
|
res.Start();
|
|
|
|
this.VideoEnded.Invoke(this,{
|
|
Video = res.Video
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
var url = $"https://s.ytimg.com/vi/{id}/{res}.jpg";
|
|
Net.Http.DownloadToFile(url,this.Storage, path);
|
|
|
|
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");
|
|
}
|
|
private OpenDB()
|
|
{
|
|
var dbFile = this.DatabaseDirectory / "tytd.db";
|
|
return Sqlite.Open(dbFile);
|
|
}
|
|
|
|
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();
|
|
}
|
|
public PackageInstall(name, version)
|
|
{
|
|
|
|
each(var item : this.Plugins)
|
|
{
|
|
if(item.name == name)
|
|
{
|
|
|
|
if(item.version >= version)
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
this.PackageDownload(name, version);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
public getTYTDTag()
|
|
{
|
|
return this.Config.TYTDTag ?? "UnknownPC";
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public PutVideoInfo(info)
|
|
{
|
|
this.Muxex.Lock();
|
|
var db = this.OpenDB();
|
|
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{Sqlite.Escape(info.keywords.ToString())},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
|
|
Sqlite.Exec(db, d);
|
|
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.Muxex.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);");
|
|
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) {
|
|
|
|
}
|
|
}
|
|
Sqlite.Close(db);
|
|
this.Mutex.Unlock();
|
|
|
|
|
|
}
|
|
private _setPluginValue(extension,key,value)
|
|
{
|
|
this.Muxex.Lock();
|
|
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.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};
|
|
}
|
|
|
|
public ManifestRequest(vid)
|
|
{
|
|
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/19.28.35 (Linux; U; Android 15; 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/19.28.35 (Linux; U; Android 15; GB) gzip"
|
|
},
|
|
|
|
],
|
|
Body = Net.Http.TextHttpRequestBody(embed("request2.json").ToString().Replace("VIDEO_ID_HERE", id).Replace("VISITOR_DATA",visitor),"application/json")
|
|
};
|
|
this.RateLimit();
|
|
var response = Net.Http.MakeRequest(url,requestData);
|
|
if(response.StatusCode < 200 || response.StatusCode > 299) return null;
|
|
return Json.Decode(response.ReadAsString());
|
|
|
|
}
|
|
|
|
|
|
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";
|
|
var data = Json.Decode(response.ReadAsString());
|
|
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.Muxex.Lock();
|
|
var db = this.OpenDB();
|
|
var d = $"INSERT INTO videos (videoId,title,lengthSeconds,keywords,channelId,shortDescription,viewCount,author,addDate,tytdTag) VALUES ({Sqlite.Escape(info.videoId)},{Sqlite.Escape(info.title)},{info.lengthSeconds},{Sqlite.Escape(info.keywords.ToString())},{Sqlite.Escape(info.channelId)},{Sqlite.Escape(info.shortDescription)},{info.viewCount},{Sqlite.Escape(info.author)},{DateTime.NowEpoch},{Sqlite.Escape(this.TYTDTag)});";
|
|
Sqlite.Exec(db, d);
|
|
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.Muxex.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.Muxex.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 Muxex();
|
|
|
|
|
|
private RateLimit()
|
|
{
|
|
this.rlm.Lock();
|
|
var curRequest = DateTime.NowEpoch;
|
|
if((curRequest - this.lastRequest) > 60)
|
|
{
|
|
this.requests = 0;
|
|
}
|
|
|
|
this.requests++;
|
|
if(this.requests > 5)
|
|
{
|
|
DateTime.Sleep(25000);
|
|
this.requests=0;
|
|
curRequest = DateTime.NowEpoch;
|
|
}
|
|
|
|
this.lastRequest = curRequest;
|
|
this.rlm.Unlock();
|
|
}
|
|
} |