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; namespace Tesses.YouTubeDownloader2025Companion { public class Program { private const string VERSION="1.0.0.0-dev"; 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 = 10; 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) { 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); ctx.ResponseHeaders["Set-Cookie"]=[$"Session={res.Key}; SameSite=Lax; Expires={DateTimeOffset.FromUnixTimeSeconds(res.Expires).ToString("R")}; HttpOnly"]; await RedirectAsync(ctx,"/"); } } } } 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 { await AuthRedirectAsync(ctx); return; } } else { if(ctx.UrlPath != "/oobe") await RedirectAsync(ctx,"/oobe"); else await action(ctx); return; } }; } public static IEnumerable DefaultLinks() { yield return new PageLink("Home","home","/"); yield return new PageLink("Search","search","/search"); yield return new PageLink("Music","music_note","/music"); } 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 "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("/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 cfg = Config ?? new AuthConfig(); await ctx.SendTextAsync(Components.Shell("Home",(await File.ReadAllTextAsync("wwwroot/index.html")).Replace("@TYTD_URL@",HttpUtility.HtmlAttributeEncode($"{cfg.TYTDUrl.TrimEnd('/')}/")),DefaultLinks())); })); 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(); cfg.CompanionUrl = companion; cfg.TYTDUrl = tytd; cfg.Session = tytdLogin.Token; cfg.ServiceName = $"tytdcompanion-{service_name_unique}"; cfg.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/myartists", EnsureLoggedin(MyArtists)); routeServer.Add("/music/fix-song",EnsureLoggedin(FixIncorrectGet)); routeServer.Add("/music/fix-song",EnsureLoggedin(FixIncorrectPost),"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/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/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); Console.WriteLine("Wait expired"); await InstallExtension(); }).Wait(0); routeServer.StartServer(4255); finished=true; t.Wait(); } 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)).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((await db.GetAlbumsFromArtistAsync(artistId.ToString(),q,offset,count)).ToList()); } } 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)).ToList()); } private static async Task ApiAlbum(ServerContext ctx) { if(ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId)) using(var db = new Database()) { List tracks = new List(); foreach(var item in await db.GetTracksAsync(albumId.ToString())) { var artists = await db.GetContributingArtistsAsync(item.SongId); tracks.Add(new { item.SongId, item.SongName, item.Track, ContributingArtists = artists.ToList() }); } 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 }); } } private static async Task ApiSong(ServerContext ctx) { if(ctx.QueryParams.TryGetFirstGuid("albumId",out var albumId) && ctx.QueryParams.TryGetFirstGuid("songId",out var songId)) { 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; } } } await ctx.SendNotFoundAsync(); } private static async Task SettingsAsync(ServerContext ctx) { await ctx.SendTextAsync(Pages.Settings(ctx)); } 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"); } } 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; } string firstArtist = ""; string name = ""; foreach(var item in recording.ContributingArtists) { if(firstArtist.Length == 0) firstArtist = item.Artist.Name; name += item.Artist.Name; name += item.JoinPhrase; } name += $" - \"{albumName}\" - \"{recording.Name}\""; await foreach(var item in YouTubeMusicClient.SearchAsync(name, YouTubeMusicAPI.Models.Search.SearchCategory.Songs)) { 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=AudioOnly"); 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.AudioOnly))) { 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.AudioOnly); } } 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())); } 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(),$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(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 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.AudioOnly); //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 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()); } await ctx.SendTextAsync(html); } else { List entries=new List(); using(var db = new Database()) { await foreach(var item in db.GetMediaEntriesAsync(songId.ToString())) { entries.Add(item); } } if(entries.Count == 0) { ctx.SendText(Components.Shell("Song not found","

    Song not found

    ", DefaultLinks())); } else if(entries.Count == 1) { await RedirectAsync(ctx,$"/music/mysong?songId={HttpUtility.UrlEncode(songId.ToString())}&albumId={HttpUtility.UrlEncode(entries[0].AlbumId)}"); } } } } 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 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(),$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(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;}=""; } } }