Make it more complete

This commit is contained in:
2026-04-28 03:21:01 -05:00
parent 74c6d1126e
commit 1052b94a54
34 changed files with 712 additions and 82 deletions
+3 -5
View File
@@ -4,11 +4,13 @@ using System.Web;
namespace Tesses.YouTubeDownloader2025Companion
{
public class PageLink(string title, string icon, string url,string badge="") {
public class PageLink(string title, string icon, string url, bool active,string badge="") {
public string Title => HttpUtility.HtmlEncode(title);
public string Icon => HttpUtility.HtmlEncode(icon);
public string Url => HttpUtility.HtmlAttributeEncode(url);
public bool Active => active;
public string Badge => HttpUtility.HtmlEncode(badge);
}
public static partial class Components {
@@ -22,9 +24,5 @@ namespace Tesses.YouTubeDownloader2025Companion
});
}
internal static string Shell(string v1, string v2, object defaultLinks)
{
throw new NotImplementedException();
}
}
}
+16 -3
View File
@@ -22,7 +22,7 @@ namespace Tesses.YouTubeDownloader2025Companion
public void Init()
{
conn.Execute("CREATE TABLE IF NOT EXISTS sessions (Id INTEGER PRIMARY KEY AUTOINCREMENT, AccountName TEXT, Key TEXT UNIQUE, Expires INTEGER);");
conn.Execute("CREATE TABLE IF NOT EXISTS sessions (Id INTEGER PRIMARY KEY AUTOINCREMENT, AccountName TEXT, Key TEXT UNIQUE, Expires INTEGER, Flags INTEGER);");
conn.Execute("CREATE TABLE IF NOT EXISTS song_videos (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT UNIQUE, VideoId TEXT UNIQUE, VideoTitle TEXT);");
conn.Execute("CREATE TABLE IF NOT EXISTS songs (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT UNIQUE, SongName TEXT);");
conn.Execute("CREATE TABLE IF NOT EXISTS artists (Id INTEGER PRIMARY KEY AUTOINCREMENT, ArtistId TEXT UNIQUE, ArtistName TEXT, ArtistDisambiguation TEXT);");
@@ -87,14 +87,15 @@ namespace Tesses.YouTubeDownloader2025Companion
yield return ent;
}
}
public async Task<Session> CreateSessionTokenAsync(string username, bool expires = true)
public async Task<Session> CreateSessionTokenAsync(string username, long flags, bool expires = true)
{
Session session = new Session();
session.AccountName = username;
session.Expires = expires ? Program.NowEpoch + Expires : 0;
session.Flags = flags;
session.Key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Expires) VALUES (@AccountName,@Key,@Expires);", session);
await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Flags,Expires) VALUES (@AccountName,@Key,@Flags,@Expires);", session);
return session;
}
@@ -226,6 +227,16 @@ namespace Tesses.YouTubeDownloader2025Companion
ArtistId = artistId
}));
}
public async Task<IEnumerable<Song>> GetUnavailableSongsAsync(long offset=0, long count=20)
{
//SELECT * FROM songs s WHERE NOT EXISTS (SELECT * FROM song_videos v WHERE v.SongId = s.SongId);
return await conn.QueryAsync<Song>("SELECT * FROM songs s WHERE NOT EXISTS (SELECT * FROM song_videos v WHERE v.SongId = s.SongId) LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
{
Offset = offset,
Count = count
}));
}
public async Task<IEnumerable<Album>> GetAlbumsAsync(string q = "", long offset = 0, long count = 20)
{
return await conn.QueryAsync<Album>("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
@@ -323,6 +334,8 @@ namespace Tesses.YouTubeDownloader2025Companion
public string Key { get; set; } = "";
public long Flags {get;set;}
public long Expires { get; set; }
}
}
+3
View File
@@ -15,5 +15,8 @@ public class AlbumQuery {
[JsonPropertyName("title")]
public string Name {get;set;}="";
[JsonPropertyName("date")]
public string Date {get;set;}="";
}
}
+17
View File
@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
namespace Tesses.YouTubeDownloader2025Companion;
public class MusicBrainzSongInfo
{
[JsonPropertyName("title")]
public string Name {get;set;}="";
[JsonPropertyName("artist-credit")]
public List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists> ContributingArtists {get;set;}=new List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists>();
[JsonPropertyName("releases")]
public List<AlbumQuery.AlbumQueryItem> Albums {get;set;}=new();
}
+27
View File
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;
namespace Tesses.YouTubeDownloader2025Companion;
public class SongQuery
{
[JsonPropertyName("recordings")]
public List<SongQueryRecording> Recordings {get;set;}=new List<SongQueryRecording>();
public class SongQueryRecording
{
[JsonPropertyName("id")]
public Guid Id {get;set;}
[JsonPropertyName("title")]
public string Name {get;set;}="";
[JsonPropertyName("length")]
public long Length {get;set;}
[JsonPropertyName("artist-credit")]
public List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists> ContributingArtists {get;set;}=new List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists>();
}
}
+61
View File
@@ -0,0 +1,61 @@
using Tesses.WebServer;
using Scriban;
using System.Web;
using YoutubeExplode.Search;
using System.Collections.Concurrent;
using System.Security.Cryptography;
using Newtonsoft.Json;
using System.Text;
namespace Tesses.YouTubeDownloader2025Companion;
public static partial class Pages {
private static ReloadingScript fix_song_index = new ReloadingScript("wwwroot/components/fix-song-index.sbnhtml");
public static string FixSongIndex(string songId, string query)
{
return fix_song_index.Render(new {
Songid = HttpUtility.HtmlAttributeEncode(songId),
Searchurl = query == "" ? "" : HttpUtility.HtmlAttributeEncode($"/search_dialog?q={HttpUtility.UrlEncode(query)}"),
Songname = query == "" ? "" : HttpUtility.HtmlEncode(query)
});
}
private static ReloadingScript index_page = new ReloadingScript("wwwroot/pages/index.sbnhtml");
public static string Index(ServerContext ctx, AuthConfig cfg, Song? song, List<Artist> artists, string username)
{
string guid="";
StringBuilder builder=new();
if(song is not null)
{
guid = song.SongId;
int i = 0;
foreach (var artist in artists)
{
if (i == 0)
builder.Append($"{artist.ArtistName} - {song.SongName}");
else if (i == 1)
builder.Append($" feat {artist.ArtistName}");
else
builder.Append($", {artist.ArtistName}");
i++;
}
}
return Components.Shell("Home", index_page.Render(new
{
Videotitle = Program.VideoTitle,
Channeltitle = Program.ChannelTitle,
Tytdurl = cfg.TYTDUrl.TrimEnd('/'),
Needsattention = song is not null,
Songtitle = HttpUtility.HtmlEncode(builder.ToString()),
Fixsong = HttpUtility.HtmlAttributeEncode($"/fix-song-index?id={HttpUtility.UrlEncode(guid)}"),
Username = HttpUtility.HtmlEncode(username)
}),Program.DefaultLinks(0));
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ public static partial class Pages {
Prev = HttpUtility.HtmlAttributeEncode($"/log?page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/log?page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(4));
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Name = HttpUtility.HtmlEncode(name),
Songs = songLinks,
Addvals = HttpUtility.HtmlAttributeEncode(JsonConvert.SerializeObject(new{id}))
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+3 -3
View File
@@ -3,10 +3,10 @@ using Scriban;
using System.Web;
namespace Tesses.YouTubeDownloader2025Companion
{
public class AlbumLink(string name, string id, string thumbnail) {
public class AlbumLink(string name, string id) {
public string Albumname => HttpUtility.HtmlEncode(name);
public string Albumurl => HttpUtility.HtmlAttributeEncode($"/music/album?id={HttpUtility.UrlEncode(id)}");
public string Imgurl => HttpUtility.HtmlAttributeEncode(thumbnail);
public string Imgurl => HttpUtility.HtmlAttributeEncode($"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(id)}");
}
public static partial class Pages {
@@ -21,7 +21,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/albums?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Prev = HttpUtility.HtmlAttributeEncode($"/music/artist?id={HttpUtility.UrlEncode(id.ToString())}&page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/artist?id={HttpUtility.UrlEncode(id.ToString())}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Prev = HttpUtility.HtmlAttributeEncode($"/music/artists?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/artists?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Hasanysongs = hasAny,
Zipurl = HttpUtility.HtmlAttributeEncode($"/api/v1/album.zip?albumId={HttpUtility.UrlEncode(id)}"),
Artists = artists
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/myalbums?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
@@ -21,7 +21,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Prev = HttpUtility.HtmlAttributeEncode($"/music/myartist?q={HttpUtility.UrlEncode(query)}&id={HttpUtility.UrlEncode(id.ToString())}&page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/myartist?q={HttpUtility.UrlEncode(query)}&id={HttpUtility.UrlEncode(id.ToString())}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Prev = HttpUtility.HtmlAttributeEncode($"/music/myartists?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/myartists?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Tytdurl = HttpUtility.HtmlAttributeEncode($"{tytdEndpoint.TrimEnd('/')}/watch?v={HttpUtility.UrlEncode(video.VideoId)}"),
Dlbtn = downloaded ? $"<a class=\"button\" href=\"/api/v1/song?albumId={HttpUtility.UrlEncode(album.AlbumId)}&songId={HttpUtility.UrlEncode(song.SongId)}\"><i>download</i> Download</a>" : string.IsNullOrEmpty(video.VideoId) ? "<span><i>file_download_off</i> Unavailable</span>" : "<span><i>downloading</i> Downloading</span>",
Hasvideo = !string.IsNullOrEmpty(video.VideoId)
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using Tesses.WebServer;
using Scriban;
using System.Web;
using System.Text;
namespace Tesses.YouTubeDownloader2025Companion
{
public static partial class Pages {
private static ReloadingScript mysong_albums = new ReloadingScript("wwwroot/pages/music/mysongs/song-albums.sbnhtml");
public static string MySongAlbums(List<MediaEntry> entries, Song song, List<Artist> artists)
{
StringBuilder builder=new StringBuilder();
if(artists.Count > 0)
builder.Append($"{artists[0].ArtistName} - ");
builder.Append(song.SongName);
if(artists.Count > 1)
builder.Append($" feat. {string.Join(",",artists.Skip(1).Select(e=>e.ArtistName))}");
return Components.Shell($"{builder}'s Albums", mysong_albums.Render(new
{
Songname = builder.ToString(),
Albums = entries.Select(e =>
{
return new
{
Albumurl = $"/music/myalbum?id={HttpUtility.UrlEncode(e.AlbumId)}",
Albumname = HttpUtility.HtmlEncode(e.AlbumName),
Url = $"/music/mysong?albumId={HttpUtility.UrlEncode(e.AlbumId)}&songId={song.SongId}",
Imgurl = $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(e.AlbumId)}"
};
})
}), Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ namespace Tesses.YouTubeDownloader2025Companion
Prev = HttpUtility.HtmlAttributeEncode($"/music/mysongs?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/mysongs?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
}),Program.DefaultLinks());
}),Program.DefaultLinks(2));
}
}
}
+26
View File
@@ -0,0 +1,26 @@
using Tesses.WebServer;
using Scriban;
using System.Web;
namespace Tesses.YouTubeDownloader2025Companion
{
public class SongLink(string name, string id) {
public string Name => HttpUtility.HtmlEncode(name);
public string Url => HttpUtility.HtmlAttributeEncode($"/music/song?id={HttpUtility.UrlEncode(id)}");
}
public static partial class Pages {
private static ReloadingScript song_listing = new ReloadingScript("wwwroot/pages/music/songs/song-list.sbnhtml");
public static string SongListing(string query, List<SongLink> links, int page)
{
return Components.Shell($"Song search {query}", song_listing.Render(new {
Songs = links,
Q = HttpUtility.HtmlAttributeEncode(query),
Prev = HttpUtility.HtmlAttributeEncode($"/music/songs?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
Current = page,
Next = HttpUtility.HtmlAttributeEncode($"/music/songs?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
}),Program.DefaultLinks(2));
}
}
}
+20
View File
@@ -0,0 +1,20 @@
using Tesses.WebServer;
using Scriban;
using System.Web;
using System.Text;
namespace Tesses.YouTubeDownloader2025Companion
{
public static partial class Pages {
private static ReloadingScript song_albums = new ReloadingScript("wwwroot/pages/music/songs/song-albums.sbnhtml");
public static string SongAlbums(string songName, List<AlbumLink> albums)
{
return Components.Shell($"{songName}'s Albums", song_albums.Render(new
{
Songname = songName,
Albums = albums
}), Program.DefaultLinks(2));
}
}
}
+1 -1
View File
@@ -132,7 +132,7 @@ public static partial class Pages {
body = await Program.SearchResultHandler.SearchAsync(q,false);
}
return Components.Shell($"Search {q}",search_page.Render(new {body,q = HttpUtility.HtmlAttributeEncode(q)}),Program.DefaultLinks());
return Components.Shell($"Search {q}",search_page.Render(new {body,q = HttpUtility.HtmlAttributeEncode(q)}),Program.DefaultLinks(1));
}
public static async Task<string> SearchDialogAsync(ServerContext ctx)
{
+10 -1
View File
@@ -16,7 +16,16 @@ public static partial class Pages {
public static string Settings(ServerContext ctx)
{
return Components.Shell("Settings",settings_page.Render(new{}),Program.DefaultLinks());
bool tytdthumbnail = false;
lock(Program.MyLockObject)
{
tytdthumbnail = Program.Config?.ThumbnailsThroughTYTD ?? false;
}
return Components.Shell("Settings", settings_page.Render(new
{
Tytdthumbnail=tytdthumbnail
}),Program.DefaultLinks(3));
}
}
+295 -33
View File
@@ -16,14 +16,40 @@ using System.Text;
using YoutubeExplode.Search;
using YouTubeMusicAPI.Client;
using YouTubeMusicAPI.Models.Search;
using YouTubeMusicAPI.Pagination;
namespace Tesses.YouTubeDownloader2025Companion
{
public class Program
public static class Program
{
public static string GetName(this List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists> artists, string songName)
{
string prefix = "";
StringBuilder builder=new StringBuilder();
if(artists.Count > 0)
{
prefix = artists[0].JoinPhrase;
builder.Append($"{artists[0].Artist.Name} - ");
}
builder.Append(songName);
foreach(var aname in artists.Skip(1))
{
builder.Append(prefix);
builder.Append(aname.Artist.Name);
prefix = aname.JoinPhrase;
}
return builder.ToString();
}
public static string VideoTitle {get;set;}="N/A";
public static string ChannelTitle {get;set;}="N/A";
private const string VERSION = "1.0.0.0-dev";
public const long AdminFlag = 0b00000100;
public static async Task InstallExtension()
{
var cfg = Config;
@@ -124,7 +150,7 @@ namespace Tesses.YouTubeDownloader2025Companion
private static long LastRequestMinute = 0;
private static long RequestsInThisMinute = 0;
private const long RequestsPerMinute = 10;
private const long RequestsPerMinute = 15;
private static bool InRateLimit = false;
@@ -243,7 +269,7 @@ namespace Tesses.YouTubeDownloader2025Companion
{
using (var db = new Database())
{
var res = await db.CreateSessionTokenAsync(val.Username);
var res = await db.CreateSessionTokenAsync(val.Username, val.Flags);
ctx.ResponseHeaders["Set-Cookie"] = [$"Session={res.Key}; SameSite=Lax; Expires={DateTimeOffset.FromUnixTimeSeconds(res.Expires).ToString("R")}; HttpOnly"];
await RedirectAsync(ctx, "/");
@@ -251,6 +277,10 @@ namespace Tesses.YouTubeDownloader2025Companion
}
}
}
public static Database.Session? GetSession(this ServerContext ctx)
{
return ctx.Tag as Database.Session;
}
public static HttpActionAsync EnsureLoggedin(HttpActionAsync action)
{
return async (ctx) =>
@@ -330,11 +360,13 @@ namespace Tesses.YouTubeDownloader2025Companion
}
};
}
public static IEnumerable<PageLink> DefaultLinks()
public static IEnumerable<PageLink> DefaultLinks(int index)
{
yield return new PageLink("Home", "home", "/");
yield return new PageLink("Search", "search", "/search");
yield return new PageLink("Music", "music_note", "/music");
yield return new PageLink("Home", "home", "/", index == 0);
yield return new PageLink("Search", "search", "/search", index== 1);
yield return new PageLink("Music", "music_note", "/music", index==2);
yield return new PageLink("Settings","settings","/settings", index == 3);
yield return new PageLink("Log","data_table","/log", index == 4);
}
private class LoginRequest
{
@@ -384,6 +416,13 @@ namespace Tesses.YouTubeDownloader2025Companion
Console.WriteLine(JsonConvert.SerializeObject(webhook));
switch (webhook.Type)
{
case "VideoStarted":
{
VideoTitle = webhook.Video.Title;
ChannelTitle = webhook.Video.Channel;
}
break;
case "VideoFinished":
{
@@ -426,6 +465,7 @@ namespace Tesses.YouTubeDownloader2025Companion
}, "POST");
routeServer.Add("/log", EnsureLoggedin(async (ctx) => await ctx.SendTextAsync(await Pages.LogAsync(ctx))));
routeServer.Add("/settings", EnsureLoggedin(SettingsAsync));
routeServer.Add("/settings", EnsureLoggedin(SettingsPostAsync),"POST");
routeServer.Add("/auth", AuthAsync, "POST");
routeServer.Add("/login", LoginAsync);
routeServer.Add("/rescan", EnsureLoggedin(async (ctx) =>
@@ -447,8 +487,21 @@ namespace Tesses.YouTubeDownloader2025Companion
}), "POST");
routeServer.Add("/", EnsureLoggedin(async (ctx) =>
{
var session = ctx.GetSession();
var cfg = Config ?? new AuthConfig();
await ctx.SendTextAsync(Components.Shell("Home", (await File.ReadAllTextAsync("wwwroot/index.html")).Replace("@TYTD_URL@", HttpUtility.HtmlAttributeEncode($"{cfg.TYTDUrl.TrimEnd('/')}/")), DefaultLinks()));
Song? song = null;
List<Artist> artists=new List<Artist>();
using(var db = new Database())
{
song = (await db.GetUnavailableSongsAsync(0,1)).FirstOrDefault();
if(song is not null)
{
artists.AddRange(await db.GetContributingArtistsAsync(song.SongId));
}
}
await ctx.SendTextAsync(Pages.Index(ctx,cfg, song,artists, session?.AccountName ?? ""));
}));
routeServer.Add("/api/v1/login", async (ctx) =>
@@ -470,11 +523,11 @@ namespace Tesses.YouTubeDownloader2025Companion
using (var db = new Database())
{
var session = await db.CreateSessionTokenAsync(req.username, false);
var session = await db.CreateSessionTokenAsync(req.username, myresp.flags, false);
await ctx.SendJsonAsync(new
{
success = true,
token = session.Key
token = session.Key,
});
return;
}
@@ -487,6 +540,7 @@ namespace Tesses.YouTubeDownloader2025Companion
success = false
});
}, "POST");
routeServer.Add("/return-to-setup",EnsureLoggedin(ReturnToSetup),"POST");
routeServer.Add("/oobe", EnsureLoggedin(async (ctx) =>
{
await ctx.SendTextAsync(Components.Shell("Setup", await File.ReadAllTextAsync("wwwroot/pages/oobe.html"), []));
@@ -527,12 +581,14 @@ namespace Tesses.YouTubeDownloader2025Companion
{
if (sso_resp2.Success)
{
AuthConfig cfg = new AuthConfig();
cfg.CompanionUrl = companion;
cfg.TYTDUrl = tytd;
cfg.Session = tytdLogin.Token;
cfg.ServiceName = $"tytdcompanion-{service_name_unique}";
cfg.SSOAppKey = sso_app_key;
AuthConfig cfg = new AuthConfig
{
CompanionUrl = companion,
TYTDUrl = tytd,
Session = tytdLogin.Token,
ServiceName = $"tytdcompanion-{service_name_unique}",
SSOAppKey = sso_app_key
};
File.WriteAllText(AuthConfigPath, JsonConvert.SerializeObject(cfg));
lock (MyLockObject)
@@ -579,9 +635,14 @@ namespace Tesses.YouTubeDownloader2025Companion
routeServer.Add("/search_more", EnsureLoggedin(SearchMore));
routeServer.Add("/music", EnsureLoggedin(Music));
routeServer.Add("/music/artists", EnsureLoggedin(Artists));
routeServer.Add("/music/songs", EnsureLoggedin(Songs));
routeServer.Add("/music/song", EnsureLoggedin(Song));
routeServer.Add("/music/myartists", EnsureLoggedin(MyArtists));
routeServer.Add("/music/fix-song", EnsureLoggedin(FixIncorrectGet));
routeServer.Add("/music/fix-song", EnsureLoggedin(FixIncorrectPost), "POST");
routeServer.Add("/fix-song-index", EnsureLoggedin(FixSongIndexGet));
routeServer.Add("/fix-song-index", EnsureLoggedin(FixSongIndexPost), "POST");
routeServer.Add("/music/artist", EnsureLoggedin(Artist));
routeServer.Add("/music/myartist", EnsureLoggedin(MyArtist));
@@ -601,6 +662,7 @@ namespace Tesses.YouTubeDownloader2025Companion
routeServer.Add("/api/v1/albums", EnsureLoggedin(ApiAlbums));
routeServer.Add("/api/v1/album", EnsureLoggedin(ApiAlbum));
routeServer.Add("/api/v1/songs", EnsureLoggedin(ApiSongs));
routeServer.Add("/api/v1/unavailable", EnsureLoggedin(ApiUnavailable));
routeServer.Add("/api/v1/song", EnsureLoggedin(ApiSong));
routeServer.Add("/api/v1/album.zip", EnsureLoggedin(ApiAlbumZip));
bool finished = false;
@@ -618,7 +680,6 @@ namespace Tesses.YouTubeDownloader2025Companion
Task.Run(async () =>
{
await Task.Delay(10000);
Console.WriteLine("Wait expired");
await InstallExtension();
}).Wait(0);
routeServer.StartServer(4255);
@@ -626,6 +687,23 @@ namespace Tesses.YouTubeDownloader2025Companion
t.Wait();
}
private static async Task ReturnToSetup(ServerContext ctx)
{
var session = ctx.GetSession();
if(((session?.Flags ?? 0) & AdminFlag) != 0)
{
lock (MyLockObject)
{
Config = null;
}
await RedirectAsync(ctx,"/oobe");
}
else
{
await ctx.SendTextAsync(Components.Shell("Unauthorized","<h1>You are unauthorized to do that</h1>", DefaultLinks(3)));
}
}
private static async Task ApiAlbumZip(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
@@ -689,6 +767,22 @@ namespace Tesses.YouTubeDownloader2025Companion
}
}
private static async Task ApiUnavailable(ServerContext ctx)
{
if (!ctx.QueryParams.TryGetFirstInt64("offset", out var offset))
offset = 0;
if (!ctx.QueryParams.TryGetFirstInt64("count", out var count))
count = 20;
using (var db = new Database())
{
List<object> songs = [.. await db.GetUnavailableSongsAsync(offset * count, count)];
await ctx.SendJsonAsync(songs);
}
}
private static async Task ApiSongs(ServerContext ctx)
{
if (!ctx.QueryParams.TryGetFirst("q", out var q))
@@ -836,7 +930,34 @@ namespace Tesses.YouTubeDownloader2025Companion
}
private static async Task SettingsAsync(ServerContext ctx)
{
var userInfo = ctx.GetSession();
if(((userInfo?.Flags ?? 0) & AdminFlag) != 0)
await ctx.SendTextAsync(Pages.Settings(ctx));
else
await ctx.SendTextAsync(Components.Shell("Unauthorized","<h1>You are unauthorized to enter settings</h1>",DefaultLinks(3)));
}
private static async Task SettingsPostAsync(ServerContext ctx)
{
ctx.ParseBody();
var userInfo = ctx.GetSession();
if(((userInfo?.Flags ?? 0) & AdminFlag) != 0)
{
var tytdthumbnail = ctx.QueryParams.GetFirstBoolean("tytdthumbnail");
lock(MyLockObject)
{
var cfg = Config;
if(cfg is not null)
{
cfg.ThumbnailsThroughTYTD = tytdthumbnail;
File.WriteAllText(AuthConfigPath, JsonConvert.SerializeObject(cfg));
}
}
await RedirectAsync(ctx,"/settings");
}
else
await ctx.SendTextAsync(Components.Shell("Unauthorized","<h1>You are unauthorized to enter settings</h1>",DefaultLinks(3)));
}
private static async Task AddAlbum(ServerContext ctx)
@@ -878,6 +999,7 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public static async Task<VideoId?> GetVideoIdFromSong(AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording recording, string albumName)
{
using (var db = new Database())
@@ -886,19 +1008,9 @@ namespace Tesses.YouTubeDownloader2025Companion
if (vid.HasValue) return vid;
}
string firstArtist = "";
string name = "";
foreach (var item in recording.ContributingArtists)
{
if (firstArtist.Length == 0) firstArtist = item.Artist.Name;
name += item.Artist.Name;
name += item.JoinPhrase;
}
name += $" - \"{albumName}\" - \"{recording.Name}\"";
await foreach (var item in YouTubeMusicClient.SearchAsync(name, YouTubeMusicAPI.Models.Search.SearchCategory.Songs))
await foreach (var item in YouTubeMusicClient.SearchAsync(recording.ContributingArtists.GetName(recording.Name), YouTubeMusicAPI.Models.Search.SearchCategory.Songs))
{
if (item is SongSearchResult ssr)
{
@@ -1259,7 +1371,7 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public static async Task Music(ServerContext ctx)
{
await ctx.SendTextAsync(Components.Shell("Music", await File.ReadAllTextAsync("wwwroot/music.html"), DefaultLinks()));
await ctx.SendTextAsync(Components.Shell("Music", await File.ReadAllTextAsync("wwwroot/music.html"), DefaultLinks(2)));
}
public static async Task MyArtist(ServerContext ctx)
@@ -1296,7 +1408,7 @@ namespace Tesses.YouTubeDownloader2025Companion
{
foreach (var item in albumQuery.Albums)
{
links.Add(new AlbumLink(item.Name, item.Id.ToString(), $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
}
}
@@ -1309,6 +1421,40 @@ namespace Tesses.YouTubeDownloader2025Companion
}
*/
}
public static async Task FixSongIndexPost(ServerContext ctx)
{
ctx.ParseBody();
VideoId? myId;
if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId) && ctx.QueryParams.TryGetFirst("url", out var url) && (myId = VideoId.TryParse(url)).HasValue)
{
var info = await YouTubeClient.Videos.GetAsync(myId.Value);
using (var db = new Database())
{
VideoId? vid = await db.GetVideoIdAsync(songId.ToString());
if (vid.HasValue)
{
string path = $"data/MusicFromTYTD/{vid.Value.Value}.bin";
if (File.Exists(path)) File.Delete(path);
}
await db.UpdateVideoIdAsync(songId.ToString(), myId.Value.Value, info.Title);
await foreach (var rel in db.GetMediaEntriesAsync(songId.ToString()))
{
string path = rel.ToString();
if (File.Exists(path)) File.Delete(path);
}
}
AddVideo(myId.Value.Value, TYTDResolution.AudioOnly);
//await ctx.SendTextAsync($"<p>{HttpUtility.HtmlEncode($"SongId: {songId}, AlbumId: {albumId}, Url: {url}")}</p>");
await RedirectAsync(ctx, "/");
}
}
public static async Task FixIncorrectPost(ServerContext ctx)
{
ctx.ParseBody();
@@ -1342,6 +1488,35 @@ namespace Tesses.YouTubeDownloader2025Companion
}
}
public static async Task FixSongIndexGet(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var songId))
{
StringBuilder builder = new StringBuilder();
using (var db = new Database())
{
var song = await db.GetSongAsync(songId.ToString());
if (song is not null)
{
int i = 0;
foreach (var artist in await db.GetContributingArtistsAsync(songId.ToString()))
{
if (i == 0)
builder.Append($"{artist.ArtistName} - {song.SongName}");
else if (i == 1)
builder.Append($" feat {artist.ArtistName}");
else
builder.Append($", {artist.ArtistName}");
i++;
}
}
}
await ctx.SendTextAsync(Pages.FixSongIndex(songId.ToString(), builder.ToString()));
}
}
public static async Task FixIncorrectGet(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId) && ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
@@ -1397,7 +1572,7 @@ namespace Tesses.YouTubeDownloader2025Companion
html = Pages.MySong(song, album, video, cfg.TYTDUrl, File.Exists(file.ToString()), trk.Track, TimeSpan.FromSeconds(trk.Length / 1000), artists.ToList());
}
else
html = Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks());
html = Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks(2));
}
await ctx.SendTextAsync(html);
@@ -1405,22 +1580,41 @@ namespace Tesses.YouTubeDownloader2025Companion
else
{
List<MediaEntry> entries = new List<MediaEntry>();
List<Artist> artists=new List<Artist>();
Song? song = null;
using (var db = new Database())
{
await foreach (var item in db.GetMediaEntriesAsync(songId.ToString()))
{
entries.Add(item);
}
if(entries.Count > 1)
{
song = await db.GetSongAsync(songId.ToString());
foreach(var item in await db.GetContributingArtistsAsync(songId.ToString()))
{
artists.Add(item);
}
}
}
if (entries.Count == 0)
{
ctx.SendText(Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks()));
await ctx.SendTextAsync(Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks(2)));
}
else if (entries.Count == 1)
{
await RedirectAsync(ctx, $"/music/mysong?songId={HttpUtility.UrlEncode(songId.ToString())}&albumId={HttpUtility.UrlEncode(entries[0].AlbumId)}");
}
else
{
if(song is not null)
await ctx.SendTextAsync(Pages.MySongAlbums(entries, song, artists));
else
await ctx.SendTextAsync(Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks(2)));
}
}
}
@@ -1506,6 +1700,74 @@ namespace Tesses.YouTubeDownloader2025Companion
await ctx.SendTextAsync(Pages.MyAlbumListing(q, links, page));
}
public static async Task Song(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id",out var id))
{
var songResp = await MusicBrainzRequestAsync<MusicBrainzSongInfo>($"/ws/2/recording/{HttpUtility.UrlEncode(id.ToString())}/?fmt=json&inc=artist-credits+releases");
if(songResp is not null)
{
List<AlbumLink> links = new List<AlbumLink>();
foreach(var item in songResp.Albums)
{
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
}
await ctx.SendTextAsync(Pages.SongAlbums(songResp.ContributingArtists.GetName(songResp.Name),links));
return;
}
}
}
public static async Task Songs(ServerContext ctx)
{
List<ArtistLink> links = new List<ArtistLink>();
string q = "";
int page = 1;
List<SongLink> songs = new List<SongLink>();
if (ctx.QueryParams.TryGetFirst("q", out q))
{
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
var songQuery = await MusicBrainzRequestAsync<SongQuery>($"/ws/2/recording?query={HttpUtility.UrlEncode(q)}&fmt=json&offset={(page - 1) * 25}&limit=25");
if (songQuery is not null)
{
foreach(var item in songQuery.Recordings)
{
string prefix = "";
StringBuilder builder=new StringBuilder();
if(item.ContributingArtists.Count > 0)
{
prefix = item.ContributingArtists[0].JoinPhrase;
builder.Append($"{item.ContributingArtists[0].Artist.Name} - ");
}
builder.Append(item.Name);
foreach(var aname in item.ContributingArtists.Skip(1))
{
builder.Append(prefix);
builder.Append(aname.Artist.Name);
prefix = aname.JoinPhrase;
}
songs.Add(new SongLink(builder.ToString(),item.Id.ToString()));
}
}
}
await ctx.SendTextAsync(Pages.SongListing(q,songs,page));
}
public static async Task Artists(ServerContext ctx)
{
@@ -1645,7 +1907,7 @@ namespace Tesses.YouTubeDownloader2025Companion
{
foreach (var item in albumQuery.Albums)
{
links.Add(new AlbumLink(item.Name, item.Id.ToString(), $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ class PluginServer
return false;
}
}
else if(ctx.Path == "/artist-thumbnail")
else if(ctx.Path == "/album-thumbnail")
{
const id = ctx.QueryParams.TryGetFirst("id");
if(TypeIsString(id))
+23
View File
@@ -0,0 +1,23 @@
<article class="primary-container" id="songattention">
<form hx-post="/fix-song-index" hx-target="body">
<h6>{{songname}}</h6>
<input type="hidden" name="songId" value="{{songid}}">
<div class="row">
<div class="max">
<div class="field label border">
<input type="text" name="url" id='url'>
<label>Video Url</label>
</div>
</div>
<div class="min">
{{if !string.empty(searchurl) }}
<button hx-get="{{searchurl}}" hx-swap="innerHTML" hx-target="#search-dialog-results" type="button" hx-on::after-request="ui('#search-dialog')">Browse</button>
{{end}}
</div>
</div>
<nav>
<button>OK</button>
<button hx-get="/" hx-target="body">Cancel</button>
</nav>
</form>
</article>
+7 -13
View File
@@ -22,7 +22,12 @@
<div class="space"></div>
<ul class="list">
{{for item in pages}}
{{if item.active }}
<li class="wave round primary-container" hx-target="body" hx-push-url="true" hx-get="{{item.url}}">
{{else}}
<li class="wave round" hx-target="body" hx-push-url="true" hx-get="{{item.url}}">
{{end}}
<i>{{item.icon}}</i>
<span class="max">{{item.title}}</span>
<b>{{item.badge}}</b>
@@ -36,20 +41,9 @@
<button hx-on:click="ui('#leftpane')" class="circle transparent">
<i>menu</i>
</button>
<div class="max"></div>
<div>
<button class="circle transparent">
<i>more_vert</i>
</button>
<menu class="left no-wrap">
<li hx-target="body" hx-push-url="true" hx-get="/settings">Settings</li>
<li hx-target="body" hx-push-url="true" hx-get="/log">Log</li>
</menu>
</div>
<h6 class="max">TYTD Companion</h6>
</nav>
<div class="small-padding">
<h5>TYTD Companion</h5>
</div>
</header>
<main class="responsive">
{{body}}
-11
View File
@@ -1,11 +0,0 @@
<div class="row">
<div class="min">
<button hx-post="/rescan">
<i>sync</i>
Rescan TYTD for Music
</button>
</div>
<div class="min">
<a class="button" href="@TYTD_URL@"><i>exit_to_app</i> TYTD2025 Downloader</a>
</div>
</div>
+37
View File
@@ -0,0 +1,37 @@
<article class="primary-container">
<h5>Logged in as {{username}}</h5>
</article>
<article class="primary-container">
<h3>{{videotitle}}</h3>
<h5>{{channeltitle}}</h5>
</article>
<article class="primary-container">
<h3>Actions</h3>
<ul class="list border">
<li>
<a hx-post="/rescan">
<i>sync</i>
Rescan TYTD for Music
</a>
</li>
<li>
<a href="{{tytdurl}}"><i>exit_to_app</i> TYTD2025 Downloader</a>
</li>
</ul>
</article>
<article class="primary-container" id="songattention">
{{if needsattention}}
<div class="row">
<div class="max">
<h6>No YouTube Url found for</h6>
<span>{{songtitle}}</span>
</div>
<div class="min">
<button hx-swap="outerHTML" hx-target="#songattention" hx-get="{{fixsong}}" class="border round secondary">Set YouTube Url</button>
</div>
</div>
{{else}}
There are no songs that need your attention
{{end}}
</article>
@@ -0,0 +1,15 @@
<h1>{{songname }}'s albums</h1>
{{for album in albums}}
<div class="row">
<div class="min">
<img width="120" height="120" src="{{album.imgurl}}" alt="album thumbnail">
</div>
<div class="max">
<a href="{{album.albumurl}}">{{album.albumname}}</a>
</div>
<div class="min">
<a href="{{album.url}}">View Song</a>
</div>
</div>
{{end}}
@@ -0,0 +1,12 @@
<h1>{{songname }}'s albums</h1>
{{for album in albums}}
<div class="row">
<div class="min">
<img width="120" height="120" src="{{album.imgurl}}" alt="album thumbnail">
</div>
<div class="max">
<a href="{{album.albumurl}}">{{album.albumname}}</a>
</div>
</div>
{{end}}
@@ -0,0 +1,31 @@
<form hx-target="body" hx-push-url="true" hx-get="/music/songs">
<div class="row">
<div class="max">
<div class="field label border">
<input type="text" name="q" value="{{q}}">
<label>Search for songs</label>
</div>
</div>
<div class="min">
<button>Search</button>
</div>
</div>
</form>
<ul class="list border">
{{for song in songs}}
<li><a href="{{song.url}}">{{song.name}}</a></li>
{{end}}
</ul>
<div class="row">
<div class="max">
</div>
<div class="min">
<button class="round" hx-target="body" hx-push-url="true" hx-get="{{prev}}"><i>arrow_back</i></button>
<span>{{current}}</span>
<button class="round" hx-target="body" hx-push-url="true" hx-get="{{next}}"><i>arrow_forward</i></button>
</div>
<div class="max">
</div>
</div>
+12
View File
@@ -16,6 +16,18 @@
<input type="password" name="pass">
<label>TYTD Password</label>
</div>
<div class="field middle-align">
<nav>
<div class="max">
<h6>Thumbnail Passthrough</h6>
<div>Use TYTD For Downloading Thumbnails (Slower, but there if you want)</div>
</div>
<label class="switch">
<input type="checkbox" name="tytdthumbnail">
<span></span>
</label>
</nav>
</div>
<br>
<button>Finish</button>
+39
View File
@@ -0,0 +1,39 @@
<article class="primary-container">
<form method="POST" action="/settings">
<div class="field middle-align">
<nav>
<div class="max">
<h6>Thumbnail Passthrough</h6>
<div>Use TYTD For Downloading Thumbnails (Slower, but there if you want)</div>
</div>
<label class="switch">
{{if tytdthumbnail}}
<input type="checkbox" name="tytdthumbnail" checked>
{{else}}
<input type="checkbox" name="tytdthumbnail">
{{end}}
<span></span>
</label>
</nav>
</div>
<br>
<button>Save</button>
</form>
</article>
<article class="primary-container" id="return-to-setup">
<div class="row">
<div class="max">
<h4>
Return to Initial Setup
</h4>
<p>
<b>WARNING</b>: Please be quick if publicly exposed to the internet as the companion will lose it&apos;s security
</p>
</div>
<div class="min">
<button hx-swap="outerHTML" hx-get="/return-to-setup.html" hx-target="#return-to-setup">Return To Setup</button>
</div>
</article>
+4
View File
@@ -0,0 +1,4 @@
<article class="primary-container" id="return-to-setup">
<h4>Are you sure you want to return to initial setup?</h4>
<button hx-target="body" hx-post="/return-to-setup">Yes</button> <button hx-target="body" hx-get="/settings">No</button>
</article>