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 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) { 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 OneUse { get; set; } = new ConcurrentDictionary(); 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) { if (InRateLimit || ShouldRateLimit()) await Task.Delay(30000); InRateLimit = false; await task(); } public static async Task RateLimitAsync(Func> task) { if (InRateLimit || ShouldRateLimit()) await Task.Delay(30000); InRateLimit = false; return await task(); } public static async IAsyncEnumerable RateLimitAsync(IAsyncEnumerable task) { await using var enumerator = task.GetAsyncEnumerator(); while (await RateLimitAsync(async () => await enumerator.MoveNextAsync())) { yield return enumerator.Current; } } public static async Task MusicBrainzRequestAsync(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(); } //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(); 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 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(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(); 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 artists=new List(); 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(); 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(); 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(); 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(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("""
  • Added
  • """); 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","

    You are unauthorized to do that

    ", 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 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 songs = new List(); foreach (var item in await db.GetSongsAsync(q, offset * count, count)) { List entries = new List(); 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 tracks = new List(); 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 entries = new List(); 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","

    You are unauthorized to enter settings

    ",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","

    You are unauthorized to enter settings

    ",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("Added"); } } private class ApiAddAlbumRequest { public Guid Id { get; set; } } private static async Task ApiAddAlbum(ServerContext ctx) { var req = await ctx.ReadJsonAsync(); 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 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($"/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 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 entries = new List(); 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(); 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 links = new List(); 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 links = new List(); var albumQuery = await MusicBrainzRequestAsync($"/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($"

    {HttpUtility.HtmlEncode($"SongId: {songId}, AlbumId: {albumId}, Url: {url}")}

    "); 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($"

    {HttpUtility.HtmlEncode($"SongId: {songId}, AlbumId: {albumId}, Url: {url}")}

    "); 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", "

    Song not found

    ", DefaultLinks(2)); } await ctx.SendTextAsync(html); } else { List entries = new List(); List artists=new List(); 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", "

    Song not found

    ", 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", "

    Song not found

    ", 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 links = new List(); 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()) ? $"download" : ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null ? "downloading" : "file_download_off"); else { html = "list"; 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 links = new List(); 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 links = new List(); 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($"/ws/2/recording/{HttpUtility.UrlEncode(id.ToString())}/?fmt=json&inc=artist-credits+releases"); if(songResp is not null) { List links = new List(); 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 links = new List(); string q = ""; int page = 1; List songs = new List(); if (ctx.QueryParams.TryGetFirst("q", out q)) { if (!ctx.QueryParams.TryGetFirstInt32("page", out page)) page = 1; var songQuery = await MusicBrainzRequestAsync($"/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 links = new List(); 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($"/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 links = new List(); 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(""); artistsString.Append(HttpUtility.HtmlEncode(item.ArtistName)); artistsString.Append(""); 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 ? $"download" : ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null ? "downloading" : "file_download_off") )); 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 links = new List(); var albumQuery = await MusicBrainzRequestAsync($"/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 links = new List(); 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($"/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 links = new List(); 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 eventables = new ConcurrentBag(); 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; } = ""; } } }