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
+296 -34
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)
{
await ctx.SendTextAsync(Pages.Settings(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()));
}
}
}