Files
tytdcompanion/Program.cs
T

1967 lines
84 KiB
C#

using Newtonsoft.Json;
using Tesses.WebServer;
//using YouTubeMusicAPI.Models.Info;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Collections.Concurrent;
using System.Web;
using YoutubeExplode;
//using YouTubeMusicAPI.Client;
using YoutubeExplode.Videos;
using Newtonsoft.Json.Linq;
using System.ComponentModel;
using System.Net.Mime;
using System.Data.Common;
using System.Text;
using YoutubeExplode.Search;
using YouTubeMusicAPI.Client;
using YouTubeMusicAPI.Models.Search;
using YouTubeMusicAPI.Pagination;
namespace Tesses.YouTubeDownloader2025Companion
{
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;
if (cfg is not null)
{
object jsonPayload = !string.IsNullOrWhiteSpace(cfg.SSOAppKey) ? new { url = cfg.CompanionUrl, token = cfg.SSOAppKey } : new { url = cfg.CompanionUrl };
using HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Put, $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/config");
msg.Content = JsonContent.Create(jsonPayload);
if (!string.IsNullOrWhiteSpace(cfg.Session))
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res = await MyHttpClient.SendAsync(msg))
{
if (res.StatusCode == System.Net.HttpStatusCode.NoContent)
{
return;
}
else if (res.StatusCode == System.Net.HttpStatusCode.NotFound)
{
using HttpRequestMessage msg2 = new HttpRequestMessage(HttpMethod.Post, $"{cfg.TYTDUrl.TrimEnd('/')}/package-manage");
msg2.Content = new System.Net.Http.StringContent($"action=install&name=Tesses.YouTubeDownloader.Plugins.TYTDCompanion&version={VERSION}", new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded"));
if (!string.IsNullOrWhiteSpace(cfg.Session))
{
msg2.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res2 = await MyHttpClient.SendAsync(msg2))
{
if (res2.IsSuccessStatusCode)
{
using HttpRequestMessage msg3 = new HttpRequestMessage(HttpMethod.Put, $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/config");
msg3.Content = JsonContent.Create(jsonPayload);
if (!string.IsNullOrWhiteSpace(cfg.Session))
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res3 = await MyHttpClient.SendAsync(msg3))
{
if (res3.StatusCode == System.Net.HttpStatusCode.NoContent)
{
return;
}
}
}
}
}
}
}
}
public static void AddVideo(VideoId id, TYTDResolution res = TYTDResolution.LowVideo)
{
EnqueueTask(async () =>
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/add");
msg.Content = JsonContent.Create(new object[] { new { url = $"https://www.youtube.com/watch?v={id.Value}", res = res.ToString() } });
if (!string.IsNullOrWhiteSpace(cfg.Session))
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res = await MyHttpClient.SendAsync(msg))
{
if (res.IsSuccessStatusCode)
Console.WriteLine($"Added video: https://www.youtube.com/watch?v={id.Value}");
}
});
}
public static void EnqueueTask(Func<Task> task)
{
tasks.Enqueue(task);
}
static TaskList tasks = new TaskList();
public static long NowEpoch
{
get => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
public static SearchResultHandler SearchResultHandler = new SearchResultHandler();
public static ConcurrentDictionary<string, OneUseTokenData?> OneUse { get; set; } = new ConcurrentDictionary<string, OneUseTokenData?>();
public const string AuthConfigPath = "data/Config/auth.json";
public static object MyLockObject = new object();
public static AuthConfig? Config = null;
public static HttpClient MyHttpClient = new HttpClient();
public static YoutubeClient YouTubeClient = new YoutubeClient();
public static YouTubeMusicClient YouTubeMusicClient = new YouTubeMusicClient();
private static long LastRequestMinute = 0;
private static long RequestsInThisMinute = 0;
private const long RequestsPerMinute = 15;
private static bool InRateLimit = false;
private static bool ShouldRateLimit()
{
var curTime = Program.NowEpoch;
lock (MyLockObject)
{
var sec = curTime % 60;
var minute = (curTime - sec) / 60;
if (LastRequestMinute != minute)
{
RequestsInThisMinute = 1;
LastRequestMinute = minute;
}
else
{
RequestsInThisMinute++;
if (RequestsInThisMinute >= RequestsPerMinute)
{
InRateLimit = true;
RequestsInThisMinute = 0;
return true;
}
}
}
return false;
}
public static async Task RateLimitAsync(Func<Task> task)
{
if (InRateLimit || ShouldRateLimit()) await Task.Delay(30000);
InRateLimit = false;
await task();
}
public static async Task<T> RateLimitAsync<T>(Func<Task<T>> task)
{
if (InRateLimit || ShouldRateLimit()) await Task.Delay(30000);
InRateLimit = false;
return await task();
}
public static async IAsyncEnumerable<T> RateLimitAsync<T>(IAsyncEnumerable<T> task)
{
await using var enumerator = task.GetAsyncEnumerator();
while (await RateLimitAsync(async () => await enumerator.MoveNextAsync()))
{
yield return enumerator.Current;
}
}
public static async Task<T?> MusicBrainzRequestAsync<T>(string url)
{
var req = new HttpRequestMessage(HttpMethod.Get, $"https://musicbrainz.org{url}");
req.Headers.Add("User-Agent", "TYTDCompanion/1.0 (tesses@tesses.net)");
using var resp = await MyHttpClient.SendAsync(req);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadFromJsonAsync<T>();
}
//public static YouTubeMusicClient YouTubeMusicClient = new YouTubeMusicClient();
public static async Task RedirectAsync(ServerContext ctx, string url)
{
if (ctx.RequestHeaders.GetFirstBoolean("HX-Request"))
{
ctx.ResponseHeaders["HX-Redirect"] = [url];
await ctx.WithStatusCode(System.Net.HttpStatusCode.NoContent).WriteHeadersAsync();
}
else
{
ctx.ResponseHeaders["Location"] = [url];
await ctx.WithStatusCode(System.Net.HttpStatusCode.SeeOther).WriteHeadersAsync();
}
}
public static async Task AuthRedirectAsync(ServerContext ctx)
{
if (ctx.UrlPath.StartsWith("/api/v1/"))
{
ctx.StatusCode = 401;
await ctx.WriteHeadersAsync();
return;
}
while (true)
{
string tmpToken = RandomNumberGenerator.GetHexString(24);
if (OneUse.TryAdd(tmpToken, null))
{
var cfg = Config ?? new AuthConfig();
await RedirectAsync(ctx, $"{cfg.TYTDUrl.TrimEnd('/')}/sso?app={HttpUtility.UrlEncode(cfg.ServiceName)}&token={HttpUtility.UrlEncode(tmpToken)}");
return;
}
}
}
public static async Task AuthAsync(ServerContext ctx)
{
var json = await ctx.ReadJsonAsync<OneUseTokenData>();
var cfg = Config ?? new AuthConfig();
if (json is not null && json.SSOAppKey == cfg.SSOAppKey && OneUse.TryUpdate(json.Token, json, null))
{
ctx.WithStatusCode(System.Net.HttpStatusCode.OK).SendText("OK", "text/plain");
return;
}
ctx.StatusCode = 401;
await ctx.SendTextAsync("BAD");
}
public static async Task LoginAsync(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirst("token", out var token) && OneUse.TryRemove(token, out var val))
{
if (val is not null)
{
using (var db = new Database())
{
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, "/");
}
}
}
}
public static Database.Session? GetSession(this ServerContext ctx)
{
return ctx.Tag as Database.Session;
}
public static HttpActionAsync EnsureLoggedin(HttpActionAsync action)
{
return async (ctx) =>
{
AuthConfig? cfg;
lock (MyLockObject)
{
cfg = Config;
}
if (cfg is not null && cfg.Session != "")
{
if (ctx.UrlPath == "/oobe")
{
await RedirectAsync(ctx, "/");
return;
}
if (ctx.RequestHeaders.TryGetFirst("Cookie", out var cookie))
{
var cookies = cookie.Split("; ", StringSplitOptions.None);
foreach (var item in cookies)
{
var myCookie = item.Split('=', 2);
if (myCookie[0] == "Session")
{
using (var db = new Database())
{
var session = await db.GetSessionAsync(ctx, myCookie[1]);
if (session is not null)
{
ctx.Tag = session;
await action(ctx);
return;
}
}
}
}
await AuthRedirectAsync(ctx);
return;
}
else if (ctx.RequestHeaders.TryGetFirst("Authorization", out var auth))
{
var authParts = auth.Split(" ", 2);
if (authParts.Length == 2 && authParts[0] == "Bearer")
{
using (var db = new Database())
{
var session = await db.GetSessionAsync(ctx, authParts[1]);
if (session is not null)
{
ctx.Tag = session;
await action(ctx);
return;
}
}
}
ctx.StatusCode = 401;
await ctx.WriteHeadersAsync();
return;
}
else
{
await AuthRedirectAsync(ctx);
return;
}
}
else
{
if (ctx.UrlPath != "/oobe")
await RedirectAsync(ctx, "/oobe");
else
await action(ctx);
return;
}
};
}
public static IEnumerable<PageLink> DefaultLinks(int index)
{
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
{
public string username { get; set; } = "";
public string password { get; set; } = "";
}
private class AuthResponse
{
public bool success { get; set; } = false;
public long flags { get; set; } = 0;
}
public static void Main(string[] args)
{
Directory.CreateDirectory("data/Config");
using (var myDb = new Database()) myDb.Init();
Directory.CreateDirectory("data/Music");
Directory.CreateDirectory("data/Temp");
Directory.CreateDirectory("data/MusicFromTYTD");
Directory.CreateDirectory("data/Thumbnails/Video");
Directory.CreateDirectory("data/Thumbnails/Albums");
Directory.CreateDirectory("data/Thumbnails/Artists");
if (File.Exists(AuthConfigPath))
{
Config = JsonConvert.DeserializeObject<AuthConfig>(File.ReadAllText(AuthConfigPath));
}
RouteServer routeServer = new RouteServer(new StaticServer("wwwroot"));
routeServer.Add("/api/v1/webhook", async (ctx) =>
{
string auth = "";
ctx.RequestHeaders.TryGetFirst("Authorization", out auth);
var authHdr = auth.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var cfg = Config ?? new AuthConfig();
if (string.IsNullOrWhiteSpace(cfg.Session) || (authHdr.Length == 2 && authHdr[0] == "TYTD" && cfg.SSOAppKey == authHdr[1]))
{
try
{
var webhook = await ctx.ReadJsonAsync<Webhook>();
if (webhook is not null)
{
var epoch = NowEpoch;
using (var db = new Database())
await db.AddEventAsync(epoch, webhook);
Console.WriteLine(JsonConvert.SerializeObject(webhook));
switch (webhook.Type)
{
case "VideoStarted":
{
VideoTitle = webhook.Video.Title;
ChannelTitle = webhook.Video.Channel;
}
break;
case "VideoFinished":
{
var vid = VideoId.TryParse(webhook.Video.VideoId);
EnqueueTask(async () =>
{
if (vid.HasValue)
await ProcessSongAsync(vid.Value);
});
}
break;
case "Started":
{
EnqueueTask(async () =>
{
using (var db = new Database())
{
foreach (var item in await db.GetSongVideosAsync())
{
var vid = VideoId.TryParse(item.VideoId);
if (vid.HasValue)
await ProcessSongAsync(vid.Value);
}
}
});
}
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
ctx.StatusCode = 204;
await ctx.WriteHeadersAsync();
}, "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) =>
{
ctx.ParseBody();
EnqueueTask(async () =>
{
using (var db = new Database())
{
foreach (var item in await db.GetSongVideosAsync())
{
var vid = VideoId.TryParse(item.VideoId);
if (vid.HasValue)
await ProcessSongAsync(vid.Value);
}
}
});
await ctx.WithStatusCode(System.Net.HttpStatusCode.ExpectationFailed).WriteHeadersAsync();
}), "POST");
routeServer.Add("/", EnsureLoggedin(async (ctx) =>
{
var session = ctx.GetSession();
var cfg = Config ?? new AuthConfig();
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) =>
{
ctx.StatusCode = 200;
var cfg = Program.Config ?? new AuthConfig();
var req = await ctx.ReadJsonAsync<LoginRequest>();
if (req is not null)
{
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/auth");
msg.Content = new StringContent(JsonConvert.SerializeObject(req), Encoding.UTF8, "application/json");
using var resp = await Program.MyHttpClient.SendAsync(msg);
if (resp.IsSuccessStatusCode)
{
var myresp = await resp.Content.ReadFromJsonAsync<AuthResponse>();
if (myresp is not null && myresp.success)
{
//we succeeded
using (var db = new Database())
{
var session = await db.CreateSessionTokenAsync(req.username, myresp.flags, false);
await ctx.SendJsonAsync(new
{
success = true,
token = session.Key,
});
return;
}
}
}
}
await ctx.SendJsonAsync(new
{
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"), []));
}));
routeServer.Add("/oobe", EnsureLoggedin(async (ctx) =>
{
ctx.ParseBody();
if (ctx.QueryParams.TryGetFirst("companion", out var companion) && ctx.QueryParams.TryGetFirst("tytd", out var tytd) && ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("pass", out var pass))
{
using var resp = await MyHttpClient.PostAsync($"{tytd.TrimEnd('/')}/api/v1/login", JsonContent.Create(new
{
username = user,
password = pass
}));
if (resp.IsSuccessStatusCode)
{
var tytdLogin = await resp.Content.ReadFromJsonAsync<TYTDLoginResponse>();
if (tytdLogin is not null && tytdLogin.Success)
{
for (int i = 0; i < 10; i++)
{
var service_name_unique = RandomNumberGenerator.GetHexString(24);
var sso_app_key = RandomNumberGenerator.GetHexString(64);
var register_req = JsonContent.Create(new
{
service_name = $"tytdcompanion-{service_name_unique}",
sso_app_key,
service_pretty_name = "TYTD Companion",
service_auth_post = $"{companion.TrimEnd('/')}/auth",
service_auth_redirect = $"{companion.TrimEnd('/')}/login?token="
});
var sso_req = new HttpRequestMessage(HttpMethod.Post, $"{tytd.TrimEnd('/')}/api/v1/register_sso");
sso_req.Content = register_req;
sso_req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tytdLogin.Token);
using var sso_resp = await MyHttpClient.SendAsync(sso_req);
var sso_resp2 = JsonConvert.DeserializeObject<SSOResp>(await sso_resp.Content.ReadAsStringAsync());
if (sso_resp2 is not null)
{
if (sso_resp2.Success)
{
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)
{
Config = cfg;
}
await InstallExtension();
await RedirectAsync(ctx, "/");
return;
}
else if (sso_resp2.Type != "db")
{
throw new Exception($"Error of {sso_resp2.Type}, {sso_resp2.Reason}");
}
}
}
//MyHttpClient.SendAsync()
}
}
}
}), "POST");
routeServer.Add("/search", EnsureLoggedin(Search));
routeServer.Add("/search_dialog", EnsureLoggedin(SearchDialog));
routeServer.Add("/search_button", EnsureLoggedin(async (ctx) =>
{
ctx.ParseBody();
if (ctx.QueryParams.TryGetFirst("type", out var type))
{
if (type == "add" && ctx.QueryParams.TryGetFirst("value", out var value))
{
VideoId? id = VideoId.TryParse(value);
if (id.HasValue)
{
AddVideo(id.Value);
await ctx.SendTextAsync("""<li>Added</li>""");
return;
}
}
}
await ctx.SendTextAsync("BAD");
}), "POST");
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));
routeServer.Add("/music/albums", EnsureLoggedin(Albums));
routeServer.Add("/music/myalbums", EnsureLoggedin(MyAlbums));
routeServer.Add("/music/album", EnsureLoggedin(Album));
routeServer.Add("/music/myalbum", EnsureLoggedin(MyAlbum));
routeServer.Add("/music/mysongs", EnsureLoggedin(MySongs));
routeServer.Add("/music/mysong", EnsureLoggedin(MySong));
routeServer.Add("/music/album/add", EnsureLoggedin(AddAlbum), "POST");
routeServer.Add("/api/v1/album", EnsureLoggedin(ApiAddAlbum), "POST");
routeServer.Add("/api/v1/video-thumbnail", EnsureLoggedin(VideoThumbnail));
routeServer.Add("/api/v1/artist-thumbnail", EnsureLoggedin(ArtistThumbnail));
routeServer.Add("/api/v1/album-thumbnail", EnsureLoggedin(AlbumThumbnail));
routeServer.Add("/api/v1/artists", EnsureLoggedin(ApiArtists));
routeServer.Add("/api/v1/artist", EnsureLoggedin(ApiArtist));
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;
RegisterEventable(SearchResultHandler);
RegisterEventable(tasks);
Task t = Task.Factory.StartNew(async () =>
{
while (!finished)
{
foreach (var item in eventables)
await item.EventAsync();
}
});
Task.Run(async () =>
{
await Task.Delay(10000);
await InstallExtension();
}).Wait(0);
routeServer.StartServer(4255);
finished = true;
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))
{
using (var db = new Database())
{
var album = await db.GetAlbumAsync(albumId.ToString());
var artist = (await db.GetAlbumArtistsAsync(albumId.ToString())).FirstOrDefault();
if (album is not null && artist is not null)
{
string path = $"data/Music/{ServerContext.FixFileName(artist.ArtistName)}/{ServerContext.FixFileName(album.AlbumName)}";
if (Directory.Exists(path))
{
ctx.WithFileName($"{ServerContext.FixFileName(artist.ArtistName)} - {ServerContext.FixFileName(album.AlbumName)}.zip", false);
using (var resp = ctx.GetResponseStream())
{
await System.IO.Compression.ZipFile.CreateFromDirectoryAsync(path, resp, System.IO.Compression.CompressionLevel.Fastest, true);
}
return;
}
}
await ctx.SendNotFoundAsync();
}
}
}
private static async Task ApiArtists(ServerContext ctx)
{
if (!ctx.QueryParams.TryGetFirst("q", out var q))
q = "";
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())
await ctx.WithMimeType("application/json").SendJsonAsync((await db.GetArtistsAsync(q, offset * count, count)).ToList());
}
private static async Task ApiArtist(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("artistId", out var artistId))
{
if (!ctx.QueryParams.TryGetFirst("q", out var q))
q = "";
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())
await ctx.WithMimeType("application/json").SendJsonAsync(
new
{
Albums = (await db.GetAlbumsFromArtistAsync(artistId.ToString(), q, offset * count, count)).ToList(),
ArtistName = (await db.GetArtistAsync(artistId.ToString()))?.ArtistName ?? ""
}
);
}
}
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))
q = "";
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 = new List<object>();
foreach (var item in await db.GetSongsAsync(q, offset * count, count))
{
List<object> entries = new List<object>();
await foreach (var entry in db.GetMediaEntriesAsync(item.SongId))
{
entries.Add(new
{
Entry = entry,
RelativePathOnServer = entry.ToString(),
Exists = File.Exists(entry.ToString())
});
}
songs.Add(new
{
Song = item,
MediaEntries = entries
});
}
await ctx.SendJsonAsync(songs);
}
}
private static async Task ApiAlbums(ServerContext ctx)
{
if (!ctx.QueryParams.TryGetFirst("q", out var q))
q = "";
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())
await ctx.WithMimeType("application/json").SendJsonAsync((await db.GetAlbumsAsync(q, offset * count, count)).ToList());
}
private static async Task ApiAlbum(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
using (var db = new Database())
{
bool hasAny = false;
List<object> tracks = new List<object>();
foreach (var item in await db.GetTracksAsync(albumId.ToString()))
{
var artists = await db.GetContributingArtistsAsync(item.SongId);
var me = await db.GetMediaEntryAsync(albumId.ToString(), item.SongId);
string status = "Unavailable";
if (me is not null)
{
bool hasSong = File.Exists(me.ToString());
if (hasSong)
{
status = "Downloaded";
}
else if ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null)
{
status = "Downloading";
}
if (hasSong) hasAny = true;
}
tracks.Add(new
{
item.SongId,
item.SongName,
item.Track,
item.Length,
LengthString = TimeSpan.FromSeconds(item.Length / 1000).ToString("m\\:ss"),
ContributingArtists = artists.ToList(),
Status = status
});
}
var album = await db.GetAlbumAsync(albumId.ToString());
var albumArtists = await db.GetAlbumArtistsAsync(albumId.ToString());
await ctx.WithMimeType("application/json").SendJsonAsync(new
{
Album = album,
AlbumArtists = albumArtists,
Tracks = tracks,
HasAny = hasAny
});
}
}
private static async Task ApiSong(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId))
{
if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
{
var inline = ctx.QueryParams.GetFirstBoolean("inline");
MediaEntry? mId = null;
using (var db = new Database())
mId = await db.GetMediaEntryAsync(albumId.ToString(), songId.ToString());
if (mId is not null)
{
string path = mId.ToString();
if (File.Exists(path))
{
ContentDisposition disposition = new ContentDisposition();
disposition.FileName = Path.GetFileName(path);
disposition.Inline = inline;
disposition.ModificationDate = File.GetLastWriteTime(path);
disposition.CreationDate = File.GetCreationTime(path);
ctx.ResponseHeaders["Content-Disposition"] = [disposition.ToString()];
await ctx.SendFileAsync(path);
return;
}
}
}
else
{
using (var db = new Database())
{
List<object> entries = new List<object>();
await foreach (var item in db.GetMediaEntriesAsync(songId.ToString()))
{
entries.Add(new
{
Entry = item,
RelativePathOnServer = item.ToString(),
Exists = File.Exists(item.ToString())
});
}
await ctx.SendJsonAsync(entries);
}
}
}
await ctx.SendNotFoundAsync();
}
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)
{
ctx.ParseBody();
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
EnqueueTask(async () =>
{
await AddAlbumAsync(id, true);
});
await ctx.SendTextAsync("<span>Added</span>");
}
}
private class ApiAddAlbumRequest
{
public Guid Id { get; set; }
}
private static async Task ApiAddAlbum(ServerContext ctx)
{
var req = await ctx.ReadJsonAsync<ApiAddAlbumRequest>();
if (req is not null)
{
EnqueueTask(async () =>
{
await AddAlbumAsync(req.Id, true);
});
ctx.StatusCode = 204;
await ctx.WriteHeadersAsync();
}
}
public static async Task AddArtistAsync(AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists artist)
{
await RateLimitAsync(async () => await EnsureArtistThumbnail(artist.Artist.Id));
using (var db = new Database())
await db.AddArtistAsync(new YouTubeDownloader2025Companion.Artist() { ArtistDisambiguation = artist.Artist.Disambiguation, ArtistName = artist.Artist.Name, ArtistId = artist.Artist.Id.ToString() });
}
public static async Task<VideoId?> GetVideoIdFromSong(AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording recording, string albumName)
{
using (var db = new Database())
{
VideoId? vid = await db.GetVideoIdAsync(recording.Id.ToString());
if (vid.HasValue) return vid;
}
await foreach (var item in YouTubeMusicClient.SearchAsync(recording.ContributingArtists.GetName(recording.Name), YouTubeMusicAPI.Models.Search.SearchCategory.Songs))
{
if (item is SongSearchResult ssr)
{
string theAlbumName = (ssr.Album?.Name) ?? "";
VideoId? id;
if (albumName.Replace(" ", "").Equals(theAlbumName.Replace(" ", ""), StringComparison.OrdinalIgnoreCase) && recording.Name.Replace(" ", "").Equals(ssr.Name.Replace(" ", ""), StringComparison.OrdinalIgnoreCase) && (id = VideoId.TryParse(ssr.Id)).HasValue)
{
var video = await RateLimitAsync(async () => await YouTubeClient.Videos.GetAsync(id.Value));
Console.WriteLine($"Added: {video.Title} with id: {video.Url}, Channel: {video.Author.ChannelTitle}");
using (var db = new Database())
await db.AddVideoIdAsync(recording.Id.ToString(), video.Id, video.Title);
return video.Id;
}
}
}
/* await foreach(var item in RateLimitAsync(YouTubeClient.Search.GetResultBatchesAsync(name,YoutubeExplode.Search.SearchFilter.Video)))
{
foreach(var vid in item.Items)
{
if(vid is VideoSearchResult r)
{
if(!r.Author.ChannelTitle.EndsWith("Topic", StringComparison.OrdinalIgnoreCase)) continue;
if(!r.Title.Contains(recording.Name,StringComparison.OrdinalIgnoreCase)) continue;
Console.WriteLine($"Added: {vid.Title} with id: {vid.Url}, Channel: {r.Author.ChannelTitle}");
using(var db = new Database())
await db.AddVideoIdAsync(recording.Id.ToString(),vid.Url, vid.Title);
return vid.Url;
}
}
}*/
return null;
}
public static async Task AddAlbumAsync(Guid id, bool addTracks = false)
{
await RateLimitAsync(async () => await EnsureAlbumThumbnail(id));
var results = await RateLimitAsync(async () => await MusicBrainzRequestAsync<AlbumSongQuery>($"/ws/2/release/{HttpUtility.UrlEncode(id.ToString())}/?inc=recordings+artist-credits&fmt=json"));
if (results is not null)
{
using (var db = new Database())
await db.AddAlbumAsync(new YouTubeDownloader2025Companion.Album() { AlbumId = id.ToString(), AlbumName = results.Title, AlbumReleaseDate = results.Date });
foreach (var artist in results.Artists)
{
using (var db = new Database())
await db.AddAlbumArtistAsync(id.ToString(), artist.Artist.Id.ToString());
await AddArtistAsync(artist);
}
if (addTracks)
{
foreach (var media in results.Media)
{
foreach (var track in media.Tracks)
{
foreach (var artist in track.Recording.ContributingArtists)
{
using (var db = new Database())
await db.AddContributingArtistAsync(track.Recording.Id.ToString(), artist.Artist.Id.ToString());
await AddArtistAsync(artist);
}
using (var db = new Database())
{
await db.AddSongAsync(new Song() { SongId = track.Recording.Id.ToString(), SongName = track.Recording.Name });
await db.AddTrackAsync(new AlbumTrack() { AlbumId = id.ToString(), SongId = track.Recording.Id.ToString(), Track = track.Track, Length = track.Length });
}
VideoId? vid = await GetVideoIdFromSong(track.Recording, results.Title);
if (vid.HasValue)
await ProcessSongAsync(vid.Value);
}
}
}
}
}
private static async Task<bool> VideoExistsOnTYTD(VideoId id, TYTDResolution res)
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Head, $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/download?v={id.Value}&res={res}");
if (!string.IsNullOrWhiteSpace(cfg.Session))
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res0 = await MyHttpClient.SendAsync(msg))
{
return res0.IsSuccessStatusCode;
}
}
public static async Task DownloadSongFromTYTD(VideoId id)
{
string videoPath = Path.Combine("data", "MusicFromTYTD", $"{id.Value}");
if (!File.Exists(videoPath + ".bin"))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/download?v={id.Value}&res=LowVideo");
if (!string.IsNullOrWhiteSpace(cfg.Session))
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res0 = await MyHttpClient.SendAsync(msg))
{
res0.EnsureSuccessStatusCode();
using (var f = File.Create(videoPath + ".part"))
{
await res0.Content.CopyToAsync(f);
}
File.Move(videoPath + ".part", videoPath + ".bin");
}
}
}
private static async Task ProcessSongAsync(VideoId value)
{
string? songId = null;
using (var db = new Database())
{
songId = await db.GetSongIdFromVideoId(value);
}
string videoPath = Path.Combine("data", "MusicFromTYTD", $"{value.Value}.bin");
if (!string.IsNullOrWhiteSpace(songId) && (File.Exists(videoPath) || await VideoExistsOnTYTD(value, TYTDResolution.LowVideo)))
{
await DownloadSongFromTYTD(value);
List<MediaEntry> entries = new List<MediaEntry>();
using (var db = new Database())
{
if (!string.IsNullOrWhiteSpace(songId))
{
await foreach (var item in db.GetMediaEntriesAsync(songId))
{
entries.Add(item);
}
}
}
if (File.Exists(videoPath))
{
foreach (var item in entries)
{
await item.ImportAsync(videoPath);
}
}
}
else if (!string.IsNullOrWhiteSpace(songId))
{
AddVideo(value, TYTDResolution.LowVideo);
}
}
public static async Task EnsureArtistThumbnail(Guid id)
{
string path = $"data/Thumbnails/Artists/{id}.jpg";
if (!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
if (cfg.ThumbnailsThroughTYTD)
{
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/artist-thumbnail?id={id.ToString()}");
if (!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res = await MyHttpClient.SendAsync(msg))
{
res.EnsureSuccessStatusCode();
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
}
else
{
using var resp = await MyHttpClient.GetAsync($"https://www.theaudiodb.com/api/v1/json/123/artist-mb.php?i={HttpUtility.UrlEncode(id.ToString())}");
resp.EnsureSuccessStatusCode();
var respJson = await resp.Content.ReadFromJsonAsync<AudioDBArtistJsonResponse>();
if (respJson is not null && respJson.Artists is not null && respJson.Artists.Count > 0)
{
string url = respJson.Artists[0].Thumbnail;
if (string.IsNullOrWhiteSpace(url))
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
else
{
using (var res = await MyHttpClient.GetAsync(url))
{
res.EnsureSuccessStatusCode();
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
}
}
else
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
}
}
}
private static async Task ArtistThumbnail(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
string path = $"data/Thumbnails/Artists/{id}.jpg";
await EnsureArtistThumbnail(id);
if (File.Exists(path))
{
await ctx.SendFileAsync(path);
}
else
{
await ctx.SendFileAsync("wwwroot/tytd-artist.jpg");
}
}
}
public static async Task EnsureAlbumThumbnail(Guid id)
{
string path = $"data/Thumbnails/Albums/{id.ToString()}.jpg";
if (!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, cfg.ThumbnailsThroughTYTD ? $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/artist-thumbnail?id={HttpUtility.UrlEncode(id.ToString())}" : $"https://coverartarchive.org/release/{HttpUtility.UrlEncode(id.ToString())}/front");
if (!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res = await MyHttpClient.SendAsync(msg))
{
if (res.IsSuccessStatusCode)
{
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
else
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
}
}
}
private static async Task AlbumThumbnail(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
string path = $"data/Thumbnails/Albums/{id.ToString()}.jpg";
if (!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, cfg.ThumbnailsThroughTYTD ? $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/artist-thumbnail?id={HttpUtility.UrlEncode(id.ToString())}" : $"https://coverartarchive.org/release/{HttpUtility.UrlEncode(id.ToString())}/front");
if (!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res = await MyHttpClient.SendAsync(msg))
{
if (res.IsSuccessStatusCode)
{
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
else
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
}
}
if (File.Exists(path))
{
await ctx.SendFileAsync(path);
}
}
}
private static async Task VideoThumbnail(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirst("v", out var v))
{
// TYTD/api/v1/video-thumbnail?v=il9nqWw9W3Y
VideoId? id = VideoId.TryParse(v);
if (id.HasValue)
{
string path = $"data/Thumbnails/Video/{id.Value}.jpg";
if (!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, cfg.ThumbnailsThroughTYTD ? $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/video-thumbnail?v={id.Value.Value}&res=default" : $"https://s.ytimg.com/vi/{id.Value.Value}/default.jpg");
if (!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session);
}
using (var res = await MyHttpClient.SendAsync(msg))
{
res.EnsureSuccessStatusCode();
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
}
if (File.Exists(path))
{
await ctx.SendFileAsync(path);
}
}
}
}
public static async Task SearchMore(ServerContext ctx)
{
await ctx.SendTextAsync(await Components.SearchMoreAsync(ctx));
}
public static async Task Search(ServerContext ctx)
{
await ctx.SendTextAsync(await Pages.SearchAsync(ctx));
}
public static async Task SearchDialog(ServerContext ctx)
{
await ctx.SendTextAsync(await Pages.SearchDialogAsync(ctx));
}
public static async Task Music(ServerContext ctx)
{
await ctx.SendTextAsync(Components.Shell("Music", await File.ReadAllTextAsync("wwwroot/music.html"), DefaultLinks(2)));
}
public static async Task MyArtist(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
string q = "";
int page = 1;
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
if (!ctx.QueryParams.TryGetFirst("q", out q))
q = "";
List<MyAlbumLink> links = new List<MyAlbumLink>();
using (var db = new Database())
links.AddRange((await db.GetAlbumsFromArtistAsync(id.ToString(), q, (page - 1) * 20)).Select(e => new MyAlbumLink(e.AlbumName, e.AlbumId, $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(e.AlbumId)}")));
await ctx.SendTextAsync(Pages.MyArtistAlbumListing(id, q, links, page));
}
}
public static async Task Artist(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
if (!ctx.QueryParams.TryGetFirstInt32("page", out var page))
page = 1;
List<AlbumLink> links = new List<AlbumLink>();
var albumQuery = await MusicBrainzRequestAsync<AlbumQuery>($"/ws/2/release?artist={HttpUtility.UrlEncode(id.ToString())}&fmt=json&offset={(page - 1) * 25}&limit=25&type=album");
if (albumQuery is not null)
{
foreach (var item in albumQuery.Albums)
{
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
}
}
await ctx.SendTextAsync(Pages.ArtistAlbumListing(id, links, page));
}
/*
if(ctx.QueryParams.TryGetFirst("id",out var id))
{
await ctx.SendTextAsync(Pages.ArtistInfo(await YouTubeMusicClient.GetArtistInfoAsync(id)));
}
*/
}
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.LowVideo);
//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();
VideoId? myId;
if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId) && ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId) && 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.LowVideo);
//await ctx.SendTextAsync($"<p>{HttpUtility.HtmlEncode($"SongId: {songId}, AlbumId: {albumId}, Url: {url}")}</p>");
await RedirectAsync(ctx, $"/music/mysong?songId={HttpUtility.UrlEncode(songId.ToString())}&albumId={HttpUtility.UrlEncode(albumId.ToString())}");
}
}
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))
{
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.IsntCorrect(songId.ToString(), albumId.ToString(), builder.ToString()));
}
}
public static async Task MySong(ServerContext ctx)
{
var cfg = Config ?? new AuthConfig();
if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId))
{
if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
{
string html;
using (var db = new Database())
{
var song = await db.GetSongAsync(songId.ToString());
var album = await db.GetAlbumAsync(albumId.ToString());
var trk = await db.GetTrackAsync(songId.ToString(), albumId.ToString());
var video = await db.GetSongVideoFromSongIdAsync(songId.ToString());
var file = await db.GetMediaEntryAsync(albumId.ToString(), songId.ToString());
if (song is not null && album is not null && trk is not null && file is not null)
{
video ??= new SongVideo()
{
SongId = songId.ToString(),
VideoTitle = "No Video",
};
var artists = await db.GetContributingArtistsAsync(songId.ToString());
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(2));
}
await ctx.SendTextAsync(html);
}
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)
{
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)));
}
}
}
}
public static async Task MySongs(ServerContext ctx)
{
string q = "";
int page = 1;
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
if (!ctx.QueryParams.TryGetFirst("q", out q))
q = "";
List<MySongLink> links = new List<MySongLink>();
using (var db = new Database())
{
foreach (var item in await db.GetSongsAsync(q, (page - 1) * 20))
{
int i = 0;
StringBuilder builder = new StringBuilder();
foreach (var artist in await db.GetContributingArtistsAsync(item.SongId))
{
if (i == 0)
builder.Append($"{artist.ArtistName} - {item.SongName}");
else if (i == 1)
builder.Append($" feat {artist.ArtistName}");
else
builder.Append($", {artist.ArtistName}");
i++;
}
string? html = null;
await foreach (var album in db.GetMediaEntriesAsync(item.SongId))
{
if (html is null) html = File.Exists(album.ToString()) ? $"<a class=\"transparent\" href=\"/api/v1/song?albumId={HttpUtility.UrlEncode(album.AlbumId)}&songId={HttpUtility.UrlEncode(item.SongId)}\"><i>download</i></a>" : ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null ? "<i>downloading</i>" : "<i>file_download_off</i>");
else
{
html = "<i>list</i>";
break;
}
}
links.Add(new MySongLink(builder.ToString(), item.SongId, html ?? ""));
}
//links.AddRange(().Select(e=>new MyArtistLink(e.ArtistName,e.ArtistId,$"/api/v1/artist-thumbnail?id={HttpUtility.UrlEncode(e.ArtistId)}")));
}
await ctx.SendTextAsync(Pages.MySongListing(q, links, page));
}
public static async Task MyArtists(ServerContext ctx)
{
string q = "";
int page = 1;
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
if (!ctx.QueryParams.TryGetFirst("q", out q))
q = "";
List<MyArtistLink> links = new List<MyArtistLink>();
using (var db = new Database())
links.AddRange((await db.GetArtistsAsync(q, (page - 1) * 20)).Select(e => new MyArtistLink(e.ArtistName, e.ArtistId, $"/api/v1/artist-thumbnail?id={HttpUtility.UrlEncode(e.ArtistId)}")));
await ctx.SendTextAsync(Pages.MyArtistListing(q, links, page));
}
public static async Task MyAlbums(ServerContext ctx)
{
string q = "";
int page = 1;
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
if (!ctx.QueryParams.TryGetFirst("q", out q))
q = "";
List<MyAlbumLink> links = new List<MyAlbumLink>();
using (var db = new Database())
links.AddRange((await db.GetAlbumsAsync(q, (page - 1) * 20)).Select(e => new MyAlbumLink(e.AlbumName, e.AlbumId, $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(e.AlbumId)}")));
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)
{
List<ArtistLink> links = new List<ArtistLink>();
string q = "";
int page = 1;
if (ctx.QueryParams.TryGetFirst("q", out q))
{
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
var artistQuery = await MusicBrainzRequestAsync<ArtistQuery>($"/ws/2/artist?query={HttpUtility.UrlEncode(q)}&fmt=json&offset={(page - 1) * 25}&limit=25");
if (artistQuery is not null)
{
foreach (var item in artistQuery.Artists)
{
links.Add(new ArtistLink(item.Name, item.Id.ToString(), $"/api/v1/artist-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
}
}
/*await foreach(var item in (q,10,0,true))
{
Console.WriteLine(item.Item.Name);
}*/
}
await ctx.SendTextAsync(Pages.ArtistListing(q, links, page));
}
public static async Task MyAlbum(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
string albumName = "";
bool hasAny = false;
StringBuilder artistsString = new StringBuilder();
List<MyAlbumSongLink> links = new List<MyAlbumSongLink>();
using (var db = new Database())
{
var album = await db.GetAlbumAsync(id.ToString());
if (album is not null)
{
albumName = album.AlbumName;
}
var artists = await db.GetAlbumArtistsAsync(id.ToString());
bool firstArtist = true;
foreach (var item in artists)
{
if (!firstArtist)
artistsString.Append(", ");
string artistEncoded = HttpUtility.UrlEncode(item.ArtistId);
artistsString.Append("<a hx-target=\"body\" hx-push-url=\"true\" hx-get=\"/music/myartist?id=");
artistsString.Append(artistEncoded);
artistsString.Append("\" href=\"/music/myartist?id=");
artistsString.Append(artistEncoded);
artistsString.Append("\">");
artistsString.Append(HttpUtility.HtmlEncode(item.ArtistName));
artistsString.Append("</a>");
firstArtist = false;
}
var tracks = await db.GetTracksAsync(id.ToString());
//hasAnySongs
foreach (var item in tracks)
{
var me = await db.GetMediaEntryAsync(id.ToString(), item.SongId);
if (me is not null)
{
bool hasSong = File.Exists(me.ToString());
links.Add(new MyAlbumSongLink(me.TrackIndex, item.SongName, id.ToString(), item.SongId, TimeSpan.FromSeconds(item.Length / 1000),
hasSong ? $"<a class=\"transparent\" href=\"/api/v1/song?albumId={HttpUtility.UrlEncode(id.ToString())}&songId={HttpUtility.UrlEncode(item.SongId)}\"><i>download</i></a>" : ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null ? "<i>downloading</i>" : "<i>file_download_off</i>")
));
if (hasSong) hasAny = true;
}
}
}
/*if(albumQuery is not null)
{
albumName = albumQuery.Title;
var first = albumQuery.Media.FirstOrDefault();
if(first is not null)
foreach(var item in first.Tracks)
{
links.Add(new AlbumSongLink(item.Track,item.Recording.Name,item.Recording.Id.ToString(),TimeSpan.FromSeconds(item.Length/1000)));
}
}*/
await ctx.SendTextAsync(Pages.MyAlbumInfo(albumName, id.ToString(), links, hasAny, artistsString.ToString()));
}
}
public static async Task Album(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
List<AlbumSongLink> links = new List<AlbumSongLink>();
var albumQuery = await MusicBrainzRequestAsync<AlbumSongQuery>($"/ws/2/release/{HttpUtility.UrlEncode(id.ToString())}/?inc=recordings&fmt=json");
string albumName = "";
if (albumQuery is not null)
{
albumName = albumQuery.Title;
var first = albumQuery.Media.FirstOrDefault();
if (first is not null)
foreach (var item in first.Tracks)
{
links.Add(new AlbumSongLink(item.Track, item.Recording.Name, id.ToString(), item.Recording.Id.ToString(), TimeSpan.FromSeconds(item.Length / 1000)));
}
}
await ctx.SendTextAsync(Pages.AlbumInfo(albumName, id.ToString(), links));
}
}
public static async Task Albums(ServerContext ctx)
{
List<AlbumLink> links = new List<AlbumLink>();
string q = "";
int page = 1;
if (ctx.QueryParams.TryGetFirst("q", out q))
{
if (!ctx.QueryParams.TryGetFirstInt32("page", out page))
page = 1;
var albumQuery = await MusicBrainzRequestAsync<AlbumQuery>($"/ws/2/release?query={HttpUtility.UrlEncode(q)}&fmt=json&offset={(page - 1) * 25}&limit=25");
if (albumQuery is not null)
{
foreach (var item in albumQuery.Albums)
{
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
}
}
}
await ctx.SendTextAsync(Pages.AlbumListing(q, links, page));
/*
List<AlbumLink> links = new List<AlbumLink>();
string q = "";
if(ctx.QueryParams.TryGetFirst("q",out q))
{
await foreach(var item in YouTubeMusicClient.SearchAsync(q,YouTubeMusicAPI.Models.Search.SearchCategory.Albums))
{
string url="";
foreach(var thumb in item.Thumbnails)
{
if(thumb.Width == 226) url = thumb.Url;
}
links.Add(new AlbumLink(item.Name,item.Id,url));
}
}
await ctx.SendTextAsync(Pages.AlbumListing(q,links));
*/
}
static ConcurrentBag<IEventable> eventables = new ConcurrentBag<IEventable>();
public static void RegisterEventable(IEventable evt)
{
eventables.Add(evt);
}
public class OneUseTokenData
{
/*
username = user.username,
flags = user.flags,
token = token,
sso_app_key = sso.sso_app_key
*/
[JsonProperty("username")]
public string Username { get; set; } = "";
[JsonProperty("flags")]
public long Flags { get; set; }
[JsonProperty("token")]
public string Token { get; set; } = "";
[JsonProperty("sso_app_key")]
public string SSOAppKey { get; set; } = "";
}
}
}