diff --git a/Database.cs b/Database.cs index 5a700bf..d9a681a 100644 --- a/Database.cs +++ b/Database.cs @@ -13,11 +13,11 @@ namespace Tesses.YouTubeDownloader2025Companion { public class Database : IDisposable, IAsyncDisposable { - public const long Expires = 86400*7; + public const long Expires = 86400 * 7; SQLiteConnection conn; public Database() { - conn=new SQLiteConnection("Data Source=data/Config/database.db"); + conn = new SQLiteConnection("Data Source=data/Config/database.db"); } public void Init() @@ -32,9 +32,10 @@ namespace Tesses.YouTubeDownloader2025Companion conn.Execute("CREATE TABLE IF NOT EXISTS song_artists (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT, ArtistId TEXT, UNIQUE(SongId, ArtistId));"); conn.Execute("CREATE TABLE IF NOT EXISTS tytd_events (Id INTEGER PRIMARY KEY AUTOINCREMENT, Time INTEGER, Type TEXT, VideoTitle TEXT, VideoId TEXT, ChannelTitle TEXT, ChannelId TEXT);"); } - public async Task> GetLogEntriesAsync(int offset=0, int count=20) + public async Task> GetLogEntriesAsync(int offset = 0, int count = 20) { - return await conn.QueryAsync("SELECT * FROM tytd_events ORDER BY Time DESC LIMIT @Count OFFSET @Offset",new DynamicParameters(new{ + return await conn.QueryAsync("SELECT * FROM tytd_events ORDER BY Time DESC LIMIT @Count OFFSET @Offset", new DynamicParameters(new + { Offset = offset, Count = count })); @@ -42,65 +43,65 @@ namespace Tesses.YouTubeDownloader2025Companion public async Task GetMediaEntryAsync(string albumId, string songId) { var song = await GetSongAsync(songId); - if(song is null) return null; + if (song is null) return null; var album = await GetAlbumAsync(albumId); - if(album is null) return null; - var track = (await conn.QueryAsync("SELECT * FROM album_tracks WHERE SongId = @SongId AND AlbumId = @AlbumId;" ,new DynamicParameters(new{SongId = songId, AlbumId = albumId}))).FirstOrDefault(); - if(track is null) return null; + if (album is null) return null; + var track = (await conn.QueryAsync("SELECT * FROM album_tracks WHERE SongId = @SongId AND AlbumId = @AlbumId;", new DynamicParameters(new { SongId = songId, AlbumId = albumId }))).FirstOrDefault(); + if (track is null) return null; MediaEntry ent = new MediaEntry(); ent.TrackIndex = track.Track; ent.AlbumId = album.AlbumId; ent.SongName = song.SongName; - ent.AlbumArtist = (await GetAlbumArtistsAsync(track.AlbumId)).Select(e=>e.ArtistName).FirstOrDefault() ?? "Unknown Artist"; + ent.AlbumArtist = (await GetAlbumArtistsAsync(track.AlbumId)).Select(e => e.ArtistName).FirstOrDefault() ?? "Unknown Artist"; ent.AlbumName = album.AlbumName; - ent.ContributingArtists.AddRange((await GetContributingArtistsAsync(songId)).Select(e=>e.ArtistName)); + ent.ContributingArtists.AddRange((await GetContributingArtistsAsync(songId)).Select(e => e.ArtistName)); return ent; } public async IAsyncEnumerable GetMediaEntriesAsync(string songId) { var song = await GetSongAsync(songId); - if(song is null) yield break; + if (song is null) yield break; List contributingArtists = new List(); - contributingArtists.AddRange((await GetContributingArtistsAsync(songId)).Select(e=>e.ArtistName)); + contributingArtists.AddRange((await GetContributingArtistsAsync(songId)).Select(e => e.ArtistName)); - foreach(var track_info in await conn.QueryAsync("SELECT * FROM album_tracks WHERE SongId = @SongId;" ,new DynamicParameters(new{SongId = songId}))) + foreach (var track_info in await conn.QueryAsync("SELECT * FROM album_tracks WHERE SongId = @SongId;", new DynamicParameters(new { SongId = songId }))) { - MediaEntry ent=new MediaEntry(); + MediaEntry ent = new MediaEntry(); ent.TrackIndex = track_info.Track; ent.AlbumId = track_info.AlbumId; ent.SongName = song.SongName; ent.ContributingArtists.AddRange(contributingArtists); - ent.AlbumArtist = (await GetAlbumArtistsAsync(track_info.AlbumId)).Select(e=>e.ArtistName).FirstOrDefault() ?? "Unknown Artist"; - + ent.AlbumArtist = (await GetAlbumArtistsAsync(track_info.AlbumId)).Select(e => e.ArtistName).FirstOrDefault() ?? "Unknown Artist"; + + - var album = await GetAlbumAsync(track_info.AlbumId); - if(album is not null) + if (album is not null) { ent.AlbumName = album.AlbumName; - + } else ent.AlbumName = "Unknown Album"; yield return ent; } } - public async Task CreateSessionTokenAsync(string username) + public async Task CreateSessionTokenAsync(string username, bool expires = true) { - Session session=new Session(); + Session session = new Session(); session.AccountName = username; - session.Expires = Program.NowEpoch + Expires; + session.Expires = expires ? Program.NowEpoch + Expires : 0; session.Key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); - - await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Expires) VALUES (@AccountName,@Key,@Expires);",session); + + await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Expires) VALUES (@AccountName,@Key,@Expires);", session); return session; } public async Task GetVideoIdAsync(string id) { var video = await GetSongVideoFromSongIdAsync(id); - if(video is not null) + if (video is not null) { return VideoId.TryParse(video.VideoId); } @@ -108,8 +109,8 @@ namespace Tesses.YouTubeDownloader2025Companion } public async Task GetSongIdFromVideoId(VideoId id) { - var video = await GetSongVideoFromVideoIdAsync(id); - if(video is not null) + var video = await GetSongVideoFromVideoIdAsync(id); + if (video is not null) { return video.SongId; } @@ -117,98 +118,118 @@ namespace Tesses.YouTubeDownloader2025Companion } public async Task AddVideoIdAsync(string songId, VideoId id, string title) { - try { - await conn.ExecuteAsync($"INSERT INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new{SongId=songId,VideoId = id.Value, VideoTitle = title})); - }catch(Exception){} + try + { + await conn.ExecuteAsync($"INSERT INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new { SongId = songId, VideoId = id.Value, VideoTitle = title })); + } + catch (Exception) { } } public async Task UpdateVideoIdAsync(string songId, VideoId id, string title) { - try { + try + { - await conn.ExecuteAsync($"INSERT OR REPLACE INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new{SongId=songId,VideoId = id.Value, VideoTitle = title})); - }catch(Exception){} + await conn.ExecuteAsync($"INSERT OR REPLACE INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new { SongId = songId, VideoId = id.Value, VideoTitle = title })); + } + catch (Exception) { } } public async Task AddAlbumArtistAsync(string albumId, string artistId) { - try { - await conn.ExecuteAsync($"INSERT INTO album_artists (AlbumId, ArtistId) VALUES (@AlbumId,@ArtistId);", new DynamicParameters(new{AlbumId=albumId,ArtistId = artistId})); - }catch(Exception){} + try + { + await conn.ExecuteAsync($"INSERT INTO album_artists (AlbumId, ArtistId) VALUES (@AlbumId,@ArtistId);", new DynamicParameters(new { AlbumId = albumId, ArtistId = artistId })); + } + catch (Exception) { } } public async Task AddContributingArtistAsync(string songId, string artistId) { - try { - await conn.ExecuteAsync($"INSERT INTO song_artists (SongId, ArtistId) VALUES (@SongId,@ArtistId);", new DynamicParameters(new{SongId=songId,ArtistId = artistId})); - }catch(Exception){} + try + { + await conn.ExecuteAsync($"INSERT INTO song_artists (SongId, ArtistId) VALUES (@SongId,@ArtistId);", new DynamicParameters(new { SongId = songId, ArtistId = artistId })); + } + catch (Exception) { } } public async Task GetSongVideoFromVideoIdAsync(VideoId id) { - return (await conn.QueryAsync($"SELECT * FROM song_videos WHERE VideoId = @VideoId;", new DynamicParameters(new{VideoId = id.Value}))).FirstOrDefault(); + return (await conn.QueryAsync($"SELECT * FROM song_videos WHERE VideoId = @VideoId;", new DynamicParameters(new { VideoId = id.Value }))).FirstOrDefault(); } public async Task GetSongVideoFromSongIdAsync(string id) { - return (await conn.QueryAsync($"SELECT * FROM song_videos WHERE SongId = @SongId;", new DynamicParameters(new{SongId = id}))).FirstOrDefault(); + return (await conn.QueryAsync($"SELECT * FROM song_videos WHERE SongId = @SongId;", new DynamicParameters(new { SongId = id }))).FirstOrDefault(); } public async Task AddArtistAsync(Artist artist) { - try { - await conn.ExecuteAsync("INSERT INTO artists (ArtistId,ArtistName,ArtistDisambiguation) VALUES (@ArtistId,@ArtistName,@ArtistDisambiguation);",artist); - }catch(Exception) {} + try + { + await conn.ExecuteAsync("INSERT INTO artists (ArtistId,ArtistName,ArtistDisambiguation) VALUES (@ArtistId,@ArtistName,@ArtistDisambiguation);", artist); + } + catch (Exception) { } } public async Task AddAlbumAsync(Album album) { - try { - await conn.ExecuteAsync("INSERT INTO albums (AlbumId,AlbumName,AlbumReleaseDate) VALUES (@AlbumId,@AlbumName,@AlbumReleaseDate);",album); - }catch(Exception) {} + try + { + await conn.ExecuteAsync("INSERT INTO albums (AlbumId,AlbumName,AlbumReleaseDate) VALUES (@AlbumId,@AlbumName,@AlbumReleaseDate);", album); + } + catch (Exception) { } } public async Task AddSongAsync(Song song) { - try { - await conn.ExecuteAsync("INSERT INTO songs (SongId,SongName) VALUES (@SongId,@SongName);",song); - }catch(Exception) {} + try + { + await conn.ExecuteAsync("INSERT INTO songs (SongId,SongName) VALUES (@SongId,@SongName);", song); + } + catch (Exception) { } } public async Task AddTrackAsync(AlbumTrack track) { - try { - await conn.ExecuteAsync("INSERT INTO album_tracks (AlbumId,SongId,Track,Length) VALUES (@AlbumId,@SongId,@Track,@Length);",track); - }catch(Exception) {} + try + { + await conn.ExecuteAsync("INSERT INTO album_tracks (AlbumId,SongId,Track,Length) VALUES (@AlbumId,@SongId,@Track,@Length);", track); + } + catch (Exception) { } } public async Task GetSongAsync(string id) { - return (await conn.QueryAsync($"SELECT * FROM songs WHERE SongId = @SongId;", new DynamicParameters(new{SongId = id}))).FirstOrDefault(); + return (await conn.QueryAsync($"SELECT * FROM songs WHERE SongId = @SongId;", new DynamicParameters(new { SongId = id }))).FirstOrDefault(); } public async Task> GetContributingArtistsAsync(string songId) { - return await conn.QueryAsync($"SELECT * FROM song_artists s INNER JOIN artists a ON s.ArtistId = a.ArtistId WHERE s.SongId = @SongId ORDER BY s.Id ASC;", new DynamicParameters(new{SongId = songId})); - + return await conn.QueryAsync($"SELECT * FROM song_artists s INNER JOIN artists a ON s.ArtistId = a.ArtistId WHERE s.SongId = @SongId ORDER BY s.Id ASC;", new DynamicParameters(new { SongId = songId })); + } - public async Task> GetArtistsAsync(string q="", long offset=0, long count=20) + public async Task> GetArtistsAsync(string q = "", long offset = 0, long count = 20) { - return await conn.QueryAsync("SELECT * FROM artists WHERE ArtistName LIKE @Query LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ + return await conn.QueryAsync("SELECT * FROM artists WHERE ArtistName LIKE @Query LIMIT @Count OFFSET @Offset;", new DynamicParameters(new + { Query = $"%{q}%", Offset = offset, Count = count })); } - public async Task> GetSongsAsync(string q="", long offset=0, long count=20) + public async Task> GetSongsAsync(string q = "", long offset = 0, long count = 20) { - return await conn.QueryAsync("SELECT * FROM songs WHERE SongName LIKE @Query LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ + return await conn.QueryAsync("SELECT * FROM songs WHERE SongName LIKE @Query LIMIT @Count OFFSET @Offset;", new DynamicParameters(new + { Query = $"%{q}%", Offset = offset, Count = count })); } - public async Task> GetAlbumsFromArtistAsync(string artistId, string q = "",long offset=0, long count=20) + public async Task> GetAlbumsFromArtistAsync(string artistId, string q = "", long offset = 0, long count = 20) { - return await conn.QueryAsync("SELECT * FROM albums al INNER JOIN album_artists ar ON al.AlbumId = ar.AlbumId WHERE AlbumName LIKE @Query AND ar.ArtistId = @ArtistId ORDER BY al.AlbumReleaseDate, al.Id ASC LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ + return await conn.QueryAsync("SELECT * FROM albums al INNER JOIN album_artists ar ON al.AlbumId = ar.AlbumId WHERE AlbumName LIKE @Query AND ar.ArtistId = @ArtistId ORDER BY al.AlbumReleaseDate, al.Id ASC LIMIT @Count OFFSET @Offset;", new DynamicParameters(new + { Query = $"%{q}%", Offset = offset, Count = count, ArtistId = artistId })); } - public async Task> GetAlbumsAsync(string q="", long offset=0, long count=20) + public async Task> GetAlbumsAsync(string q = "", long offset = 0, long count = 20) { - return await conn.QueryAsync("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{ + return await conn.QueryAsync("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;", new DynamicParameters(new + { Query = $"%{q}%", Offset = offset, Count = count @@ -217,48 +238,50 @@ namespace Tesses.YouTubeDownloader2025Companion public async Task> GetSongVideosAsync() { return await conn.QueryAsync($"SELECT * FROM song_videos;"); - + } public async Task> GetAlbumArtistsAsync(string albumId) { - return await conn.QueryAsync($"SELECT * FROM album_artists s INNER JOIN artists a ON s.ArtistId = a.ArtistId WHERE s.AlbumId = @AlbumId;", new DynamicParameters(new{AlbumId = albumId})); - + return await conn.QueryAsync($"SELECT * FROM album_artists s INNER JOIN artists a ON s.ArtistId = a.ArtistId WHERE s.AlbumId = @AlbumId;", new DynamicParameters(new { AlbumId = albumId })); + } public async Task GetArtistAsync(string id) { - return (await conn.QueryAsync($"SELECT * FROM artists WHERE ArtistId = @ArtistId;", new DynamicParameters(new{ArtistId = id}))).FirstOrDefault(); + return (await conn.QueryAsync($"SELECT * FROM artists WHERE ArtistId = @ArtistId;", new DynamicParameters(new { ArtistId = id }))).FirstOrDefault(); } public async Task GetAlbumAsync(string id) { - return (await conn.QueryAsync("SELECT * FROM albums WHERE AlbumId = @AlbumId;", new DynamicParameters(new{AlbumId = id}))).FirstOrDefault(); + return (await conn.QueryAsync("SELECT * FROM albums WHERE AlbumId = @AlbumId;", new DynamicParameters(new { AlbumId = id }))).FirstOrDefault(); } public async Task GetTrackAsync(string songId, string albumId) { - return (await conn.QueryAsync("SELECT * FROM album_tracks WHERE AlbumId = @AlbumId AND SongId = @SongId", new DynamicParameters(new{ + return (await conn.QueryAsync("SELECT * FROM album_tracks WHERE AlbumId = @AlbumId AND SongId = @SongId", new DynamicParameters(new + { SongId = songId, - AlbumId = albumId}))).FirstOrDefault(); + AlbumId = albumId + }))).FirstOrDefault(); } public async Task> GetTracksAsync(string albumId) { - return await conn.QueryAsync("SELECT * FROM songs s INNER JOIN album_tracks t ON s.SongId = t.SongId WHERE t.AlbumId = @AlbumId ORDER BY t.Track ASC;",new DynamicParameters(new{AlbumId = albumId})); + return await conn.QueryAsync("SELECT * FROM songs s INNER JOIN album_tracks t ON s.SongId = t.SongId WHERE t.AlbumId = @AlbumId ORDER BY t.Track ASC;", new DynamicParameters(new { AlbumId = albumId })); } - public async Task GetSessionAsync(ServerContext ctx,string sessionToken) + public async Task GetSessionAsync(ServerContext ctx, string sessionToken) { var time = Program.NowEpoch; - var res = (await conn.QueryAsync("SELECT * FROM sessions WHERE Key = @Key;", new DynamicParameters(new{Key=sessionToken}))).FirstOrDefault(); - - if(res is not null) + var res = (await conn.QueryAsync("SELECT * FROM sessions WHERE Key = @Key;", new DynamicParameters(new { Key = sessionToken }))).FirstOrDefault(); + + if (res is not null) { var expires = res.Expires; - if(expires != 0 && time < expires && (expires - time) < (Expires - 3600)) + if (expires != 0 && time < expires && (expires - time) < (Expires - 3600)) { long expiry = time + Expires; res.Expires = expiry; - await conn.ExecuteAsync("UPDATE sessions SET Expires = @Expires WHERE Key = @Key;",res); - - ctx.ResponseHeaders["Set-Cookie"]=[$"Session={res.Key}; SameSite=Lax; Expires={DateTimeOffset.FromUnixTimeSeconds(expiry).ToString("R")}; HttpOnly"]; + await conn.ExecuteAsync("UPDATE sessions SET Expires = @Expires WHERE Key = @Key;", res); + + ctx.ResponseHeaders["Set-Cookie"] = [$"Session={res.Key}; SameSite=Lax; Expires={DateTimeOffset.FromUnixTimeSeconds(expiry).ToString("R")}; HttpOnly"]; } - else if(expires != 0 && time >= expires) + else if (expires != 0 && time >= expires) { await conn.ExecuteAsync("DELETE FROM sessions WHERE Key = @Key;", res); return null; @@ -266,7 +289,7 @@ namespace Tesses.YouTubeDownloader2025Companion } return res; } - + public void Dispose() { conn.Dispose(); @@ -281,7 +304,8 @@ namespace Tesses.YouTubeDownloader2025Companion public async Task AddEventAsync(long epoch, Webhook webhook) { //conn.Execute("CREATE TABLE IF NOT EXISTS tytd_events (Id INTEGER PRIMARY KEY AUTOINCREMENT, Time INTEGER, Type TEXT, VideoTitle TEXT, VideoId TEXT, ChannelTitle TEXT, ChannelId TEXT);"); - await conn.ExecuteAsync("INSERT INTO tytd_events (Time,Type,VideoTitle,VideoId,ChannelTitle,ChannelId) VALUES (@Time,@Type,@VideoTitle,@VideoId,@ChannelTitle,@ChannelId);", new DynamicParameters(new { + await conn.ExecuteAsync("INSERT INTO tytd_events (Time,Type,VideoTitle,VideoId,ChannelTitle,ChannelId) VALUES (@Time,@Type,@VideoTitle,@VideoId,@ChannelTitle,@ChannelId);", new DynamicParameters(new + { Time = epoch, Type = webhook.Type, VideoTitle = webhook.Video.Title, @@ -293,14 +317,14 @@ namespace Tesses.YouTubeDownloader2025Companion public class Session { - public long Id {get;set;} + public long Id { get; set; } - public string AccountName {get;set;}=""; + public string AccountName { get; set; } = ""; - public string Key {get;set;}=""; + public string Key { get; set; } = ""; - public long Expires {get;set;} + public long Expires { get; set; } } } -} \ No newline at end of file +} diff --git a/Program.cs b/Program.cs index d6c547a..5c8e136 100644 --- a/Program.cs +++ b/Program.cs @@ -21,79 +21,80 @@ using YouTubeMusicAPI.Models.Search; namespace Tesses.YouTubeDownloader2025Companion { - public class Program + public class Program { - private const string VERSION="1.0.0.0-dev"; + private const string VERSION = "1.0.0.0-dev"; public static async Task InstallExtension() { var cfg = Config; - if(cfg is not null) + 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"); - + 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)) + if (!string.IsNullOrWhiteSpace(cfg.Session)) { - msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session); + msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); } - using(var res = await MyHttpClient.SendAsync(msg)) + using (var res = await MyHttpClient.SendAsync(msg)) { - if(res.StatusCode == System.Net.HttpStatusCode.NoContent) + if (res.StatusCode == System.Net.HttpStatusCode.NoContent) { return; } - else if(res.StatusCode == System.Net.HttpStatusCode.NotFound) + 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)) + 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); - - } + msg2.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); - using(var res2 = await MyHttpClient.SendAsync(msg2)) + } + + using (var res2 = await MyHttpClient.SendAsync(msg2)) { - if(res2.IsSuccessStatusCode) + if (res2.IsSuccessStatusCode) { - using HttpRequestMessage msg3 = new HttpRequestMessage(HttpMethod.Put,$"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/config"); - + using HttpRequestMessage msg3 = new HttpRequestMessage(HttpMethod.Put, $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/config"); + msg3.Content = JsonContent.Create(jsonPayload); - if(!string.IsNullOrWhiteSpace(cfg.Session)) + if (!string.IsNullOrWhiteSpace(cfg.Session)) { - msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session); + msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); } - using(var res3 = await MyHttpClient.SendAsync(msg3)) + using (var res3 = await MyHttpClient.SendAsync(msg3)) { - if(res3.StatusCode == System.Net.HttpStatusCode.NoContent) + if (res3.StatusCode == System.Net.HttpStatusCode.NoContent) { return; - } + } } - } + } } } } - + } } public static void AddVideo(VideoId id, TYTDResolution res = TYTDResolution.LowVideo) { - EnqueueTask(async()=>{ + 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)) + 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); + msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); } - using(var res = await MyHttpClient.SendAsync(msg)) + using (var res = await MyHttpClient.SendAsync(msg)) { - if(res.IsSuccessStatusCode) + if (res.IsSuccessStatusCode) Console.WriteLine($"Added video: https://www.youtube.com/watch?v={id.Value}"); } }); @@ -107,14 +108,14 @@ namespace Tesses.YouTubeDownloader2025Companion { 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 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(); @@ -130,11 +131,11 @@ namespace Tesses.YouTubeDownloader2025Companion private static bool ShouldRateLimit() { var curTime = Program.NowEpoch; - lock(MyLockObject) + lock (MyLockObject) { var sec = curTime % 60; - var minute = (curTime - sec)/60; - if(LastRequestMinute != minute) + var minute = (curTime - sec) / 60; + if (LastRequestMinute != minute) { RequestsInThisMinute = 1; LastRequestMinute = minute; @@ -142,7 +143,7 @@ namespace Tesses.YouTubeDownloader2025Companion else { RequestsInThisMinute++; - if(RequestsInThisMinute >= RequestsPerMinute) + if (RequestsInThisMinute >= RequestsPerMinute) { InRateLimit = true; RequestsInThisMinute = 0; @@ -155,30 +156,30 @@ namespace Tesses.YouTubeDownloader2025Companion public static async Task RateLimitAsync(Func task) { - if(InRateLimit || ShouldRateLimit()) await Task.Delay(30000); + 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); + 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())) + 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)"); + 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(); @@ -188,9 +189,9 @@ namespace Tesses.YouTubeDownloader2025Companion public static async Task RedirectAsync(ServerContext ctx, string url) { - if(ctx.RequestHeaders.GetFirstBoolean("HX-Request")) + if (ctx.RequestHeaders.GetFirstBoolean("HX-Request")) { - + ctx.ResponseHeaders["HX-Redirect"] = [url]; await ctx.WithStatusCode(System.Net.HttpStatusCode.NoContent).WriteHeadersAsync(); } @@ -203,13 +204,20 @@ namespace Tesses.YouTubeDownloader2025Companion } public static async Task AuthRedirectAsync(ServerContext ctx) { - while(true) + 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)) + 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)}"); + await RedirectAsync(ctx, $"{cfg.TYTDUrl.TrimEnd('/')}/sso?app={HttpUtility.UrlEncode(cfg.ServiceName)}&token={HttpUtility.UrlEncode(tmpToken)}"); return; } } @@ -218,72 +226,94 @@ namespace Tesses.YouTubeDownloader2025Companion { 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)) + + 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"); + ctx.WithStatusCode(System.Net.HttpStatusCode.OK).SendText("OK", "text/plain"); return; } - ctx.StatusCode=401; + 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 (ctx.QueryParams.TryGetFirst("token", out var token) && OneUse.TryRemove(token, out var val)) { - if(val is not null) + if (val is not null) { - using(var db = new Database()) + 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,"/"); + 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)=>{ + return async (ctx) => + { AuthConfig? cfg; - lock(MyLockObject) + lock (MyLockObject) { cfg = Config; } - if(cfg is not null && cfg.Session != "") + if (cfg is not null && cfg.Session != "") { - if(ctx.UrlPath == "/oobe") + if (ctx.UrlPath == "/oobe") { - await RedirectAsync(ctx,"/"); + await RedirectAsync(ctx, "/"); return; } - - if(ctx.RequestHeaders.TryGetFirst("Cookie",out var cookie)) + + if (ctx.RequestHeaders.TryGetFirst("Cookie", out var cookie)) { var cookies = cookie.Split("; ", StringSplitOptions.None); - foreach(var item in cookies) + foreach (var item in cookies) { - var myCookie = item.Split('=',2); - if(myCookie[0] == "Session") + var myCookie = item.Split('=', 2); + if (myCookie[0] == "Session") { - using(var db = new Database()) + using (var db = new Database()) { - var session = await db.GetSessionAsync(ctx,myCookie[1]); - if(session is not null) + 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); @@ -292,24 +322,34 @@ namespace Tesses.YouTubeDownloader2025Companion } else { - if(ctx.UrlPath != "/oobe") - await RedirectAsync(ctx,"/oobe"); + if (ctx.UrlPath != "/oobe") + await RedirectAsync(ctx, "/oobe"); else - await action(ctx); + 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"); + yield return new PageLink("Home", "home", "/"); + yield return new PageLink("Search", "search", "/search"); + yield return new PageLink("Music", "music_note", "/music"); + } + 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(); + using (var myDb = new Database()) myDb.Init(); Directory.CreateDirectory("data/Music"); Directory.CreateDirectory("data/Temp"); Directory.CreateDirectory("data/MusicFromTYTD"); @@ -317,166 +357,216 @@ namespace Tesses.YouTubeDownloader2025Companion Directory.CreateDirectory("data/Thumbnails/Video"); Directory.CreateDirectory("data/Thumbnails/Albums"); Directory.CreateDirectory("data/Thumbnails/Artists"); - if(File.Exists(AuthConfigPath)) + if (File.Exists(AuthConfigPath)) { Config = JsonConvert.DeserializeObject(File.ReadAllText(AuthConfigPath)); } - RouteServer routeServer=new RouteServer(new StaticServer("wwwroot")); - routeServer.Add("/api/v1/webhook",async(ctx)=>{ + 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": - { + ctx.RequestHeaders.TryGetFirst("Authorization", out auth); - var vid = VideoId.TryParse(webhook.Video.VideoId); - EnqueueTask(async()=>{ - if(vid.HasValue) - await ProcessSongAsync(vid.Value); - }); - } - break; - case "Started": + 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) { - EnqueueTask(async()=>{ - using(var db = new Database()) - { - foreach(var item in await db.GetSongVideosAsync()) + case "VideoFinished": { - var vid = VideoId.TryParse(item.VideoId); - if(vid.HasValue) - await ProcessSongAsync(vid.Value); + + 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; } - break; + } - } - } catch(Exception ex) + 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)=>{ + }, "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); - } - } + 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)=>{ + }), "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())); + 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)) + 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) { - using var resp = await MyHttpClient.PostAsync($"{tytd.TrimEnd('/')}/api/v1/login",JsonContent.Create(new { + 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, false); + await ctx.SendJsonAsync(new + { + success = true, + token = session.Key + }); + return; + } + } + } + } + + await ctx.SendJsonAsync(new + { + success = false + }); + }, "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) + if (resp.IsSuccessStatusCode) { - var tytdLogin =await resp.Content.ReadFromJsonAsync(); - if(tytdLogin is not null && tytdLogin.Success) - { - for(int i = 0; i < 10; i++) + 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{ + 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"); + 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); + 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 is not null) { - if(sso_resp2.Success) + if (sso_resp2.Success) { - AuthConfig cfg=new AuthConfig(); + 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) + + File.WriteAllText(AuthConfigPath, JsonConvert.SerializeObject(cfg)); + lock (MyLockObject) { Config = cfg; } await InstallExtension(); - await RedirectAsync(ctx,"/"); + await RedirectAsync(ctx, "/"); return; - } else if(sso_resp2.Type != "db") + } + 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)=>{ - + }), "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)) + 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) + if (id.HasValue) { AddVideo(id.Value); await ctx.SendTextAsync("""
  • Added
  • """); @@ -485,13 +575,13 @@ namespace Tesses.YouTubeDownloader2025Companion } } await ctx.SendTextAsync("BAD"); - }),"POST"); - routeServer.Add("/search_more",EnsureLoggedin(SearchMore)); - routeServer.Add("/music",EnsureLoggedin(Music)); + }), "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/fix-song", EnsureLoggedin(FixIncorrectGet)); + routeServer.Add("/music/fix-song", EnsureLoggedin(FixIncorrectPost), "POST"); routeServer.Add("/music/artist", EnsureLoggedin(Artist)); routeServer.Add("/music/myartist", EnsureLoggedin(MyArtist)); @@ -501,59 +591,63 @@ namespace Tesses.YouTubeDownloader2025Companion 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; + 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/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) + Task t = Task.Factory.StartNew(async () => + { + while (!finished) { - foreach(var item in eventables) + foreach (var item in eventables) await item.EventAsync(); } }); - Task.Run(async()=>{ + Task.Run(async () => + { await Task.Delay(10000); Console.WriteLine("Wait expired"); await InstallExtension(); }).Wait(0); routeServer.StartServer(4255); - finished=true; + finished = true; t.Wait(); } private static async Task ApiAlbumZip(ServerContext ctx) { - if(ctx.QueryParams.TryGetFirstGuid("albumId",out var albumId)) + if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId)) { - using(var db = new Database()) + 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) + 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)) + if (Directory.Exists(path)) { - ctx.WithFileName($"{ServerContext.FixFileName(artist.ArtistName)} - {ServerContext.FixFileName(album.AlbumName)}.zip",false); - using(var resp = ctx.GetResponseStream()) + 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); + await System.IO.Compression.ZipFile.CreateFromDirectoryAsync(path, resp, System.IO.Compression.CompressionLevel.Fastest, true); } return; } - - + + } await ctx.SendNotFoundAsync(); } @@ -562,98 +656,181 @@ namespace Tesses.YouTubeDownloader2025Companion private static async Task ApiArtists(ServerContext ctx) { - if(!ctx.QueryParams.TryGetFirst("q",out var q)) + if (!ctx.QueryParams.TryGetFirst("q", out var q)) q = ""; - if(!ctx.QueryParams.TryGetFirstInt64("offset",out var offset)) + if (!ctx.QueryParams.TryGetFirstInt64("offset", out var offset)) offset = 0; - if(!ctx.QueryParams.TryGetFirstInt64("count",out var count)) + 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()); + 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.TryGetFirstGuid("artistId", out var artistId)) { - if(!ctx.QueryParams.TryGetFirst("q",out var q)) + if (!ctx.QueryParams.TryGetFirst("q", out var q)) q = ""; - - if(!ctx.QueryParams.TryGetFirstInt64("offset",out var offset)) + + if (!ctx.QueryParams.TryGetFirstInt64("offset", out var offset)) offset = 0; - if(!ctx.QueryParams.TryGetFirstInt64("count",out var count)) + 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()); - + + using (var db = new Database()) + await ctx.WithMimeType("application/json").SendJsonAsync( + new + { + Albums = (await db.GetAlbumsFromArtistAsync(artistId.ToString(), q, offset, count)).ToList(), + ArtistName = (await db.GetArtistAsync(artistId.ToString()))?.ArtistName ?? "" + } + ); + + } + } + 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)) + if (!ctx.QueryParams.TryGetFirst("q", out var q)) q = ""; - - if(!ctx.QueryParams.TryGetFirstInt64("offset",out var offset)) + + if (!ctx.QueryParams.TryGetFirstInt64("offset", out var offset)) offset = 0; - if(!ctx.QueryParams.TryGetFirstInt64("count",out var count)) + 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()); + + using (var db = new Database()) + await ctx.WithMimeType("application/json").SendJsonAsync((await db.GetAlbumsAsync(q, offset * 20, 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())) + if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId)) + using (var db = new Database()) { - var artists = await db.GetContributingArtistsAsync(item.SongId); - tracks.Add(new { - item.SongId, - item.SongName, - item.Track, - ContributingArtists = artists.ToList() + 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 }); + } - - 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)) + if (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) + if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId)) { - string path = mId.ToString(); - if(File.Exists(path)) + 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) { - 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; + 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(); } @@ -665,128 +842,146 @@ namespace Tesses.YouTubeDownloader2025Companion private static async Task AddAlbum(ServerContext ctx) { ctx.ParseBody(); - if(ctx.QueryParams.TryGetFirstGuid("id",out var id)) + if (ctx.QueryParams.TryGetFirstGuid("id", out var id)) { - EnqueueTask(async()=>{ - await AddAlbumAsync(id,true); + 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() }); - + 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()) + using (var db = new Database()) { VideoId? vid = await db.GetVideoIdAsync(recording.Id.ToString()); - if(vid.HasValue) return vid; + if (vid.HasValue) return vid; } - + string firstArtist = ""; string name = ""; - foreach(var item in recording.ContributingArtists) + foreach (var item in recording.ContributingArtists) { - if(firstArtist.Length == 0) firstArtist = item.Artist.Name; + 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) + + 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) + 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)); + 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()) + 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}"); + /* 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; - using(var db = new Database()) - await db.AddVideoIdAsync(recording.Id.ToString(),vid.Url, vid.Title); - return vid.Url; - } - - - } - }*/ + 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) + 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) + 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()) + 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) + + foreach (var artist in results.Artists) { - using(var db=new Database()) + using (var db = new Database()) await db.AddAlbumArtistAsync(id.ToString(), artist.Artist.Id.ToString()); await AddArtistAsync(artist); } - if(addTracks) + if (addTracks) { - foreach(var media in results.Media) + foreach (var media in results.Media) { - foreach(var track in media.Tracks) + foreach (var track in media.Tracks) { - - foreach(var artist in track.Recording.ContributingArtists) + + foreach (var artist in track.Recording.ContributingArtists) { - using(var db=new Database()) + using (var db = new Database()) await db.AddContributingArtistAsync(track.Recording.Id.ToString(), artist.Artist.Id.ToString()); await AddArtistAsync(artist); - + } - using(var db=new Database()) + 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}); + 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) + if (vid.HasValue) await ProcessSongAsync(vid.Value); } } @@ -796,39 +991,39 @@ namespace Tesses.YouTubeDownloader2025Companion 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; - } + 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")) + 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)) + 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); + msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); } - using(var res0 = await MyHttpClient.SendAsync(msg)) + using (var res0 = await MyHttpClient.SendAsync(msg)) { res0.EnsureSuccessStatusCode(); - using(var f = File.Create(videoPath+".part")) + using (var f = File.Create(videoPath + ".part")) { await res0.Content.CopyToAsync(f); } - File.Move(videoPath+".part",videoPath + ".bin"); + File.Move(videoPath + ".part", videoPath + ".bin"); } } } @@ -836,113 +1031,117 @@ namespace Tesses.YouTubeDownloader2025Companion private static async Task ProcessSongAsync(VideoId value) { string? songId = null; - using(var db = new Database()) + 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))) + 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()) + using (var db = new Database()) { - - if(!string.IsNullOrWhiteSpace(songId)) + + if (!string.IsNullOrWhiteSpace(songId)) { - await foreach(var item in db.GetMediaEntriesAsync(songId)) + await foreach (var item in db.GetMediaEntriesAsync(songId)) { entries.Add(item); } } } - - if(File.Exists(videoPath)) + + if (File.Exists(videoPath)) { - foreach(var item in entries) + foreach (var item in entries) { await item.ImportAsync(videoPath); } } - - } else if(!string.IsNullOrWhiteSpace(songId)) { + + } + 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)) + if (!File.Exists(path)) + { + var cfg = Config ?? new AuthConfig(); + if (cfg.ThumbnailsThroughTYTD) { - 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) { - 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)) { - msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session); + + File.Copy("wwwroot/tytd-artist.jpg", path, true); } - using(var res = await MyHttpClient.SendAsync(msg)) + else { - res.EnsureSuccessStatusCode(); - using(var dest = File.Create(path)) - await res.Content.CopyToAsync(dest); + + using (var res = await MyHttpClient.GetAsync(url)) + { + 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); - } + File.Copy("wwwroot/tytd-artist.jpg", path, true); } } - + } + } private static async Task ArtistThumbnail(ServerContext ctx) { - if(ctx.QueryParams.TryGetFirstGuid("id",out var id)) + if (ctx.QueryParams.TryGetFirstGuid("id", out var id)) { string path = $"data/Thumbnails/Artists/{id}.jpg"; await EnsureArtistThumbnail(id); - if(File.Exists(path)) + if (File.Exists(path)) { await ctx.SendFileAsync(path); } - else + else { await ctx.SendFileAsync("wwwroot/tytd-artist.jpg"); } @@ -951,96 +1150,98 @@ namespace Tesses.YouTubeDownloader2025Companion public static async Task EnsureAlbumThumbnail(Guid id) { string path = $"data/Thumbnails/Albums/{id.ToString()}.jpg"; - if(!File.Exists(path)) + 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) { - 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); - } - } - + 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)) + if (ctx.QueryParams.TryGetFirstGuid("id", out var id)) { - + string path = $"data/Thumbnails/Albums/{id.ToString()}.jpg"; - if(!File.Exists(path)) + 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) + 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); + msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); } - using(var res = await MyHttpClient.SendAsync(msg)) + using (var res = await MyHttpClient.SendAsync(msg)) { - if(res.IsSuccessStatusCode) + if (res.IsSuccessStatusCode) { - using(var dest = File.Create(path)) + using (var dest = File.Create(path)) await res.Content.CopyToAsync(dest); } - else { - File.Copy("wwwroot/tytd-artist.jpg",path,true); + else + { + File.Copy("wwwroot/tytd-artist.jpg", path, true); } } } - if(File.Exists(path)) + if (File.Exists(path)) { await ctx.SendFileAsync(path); } - + } - + } private static async Task VideoThumbnail(ServerContext ctx) { - if(ctx.QueryParams.TryGetFirst("v", out var v)) + if (ctx.QueryParams.TryGetFirst("v", out var v)) { - // TYTD/api/v1/video-thumbnail?v=il9nqWw9W3Y + // TYTD/api/v1/video-thumbnail?v=il9nqWw9W3Y VideoId? id = VideoId.TryParse(v); - if(id.HasValue) + if (id.HasValue) { string path = $"data/Thumbnails/Video/{id.Value}.jpg"; - if(!File.Exists(path)) + 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) + 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); + msg.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cfg.Session); } - using(var res = await MyHttpClient.SendAsync(msg)) + using (var res = await MyHttpClient.SendAsync(msg)) { res.EnsureSuccessStatusCode(); - using(var dest = File.Create(path)) + using (var dest = File.Create(path)) await res.Content.CopyToAsync(dest); } } - if(File.Exists(path)) + if (File.Exists(path)) { await ctx.SendFileAsync(path); } - + } } } @@ -1058,47 +1259,47 @@ namespace Tesses.YouTubeDownloader2025Companion } public static async Task Music(ServerContext ctx) { - await ctx.SendTextAsync(Components.Shell("Music",await File.ReadAllTextAsync("wwwroot/music.html"),DefaultLinks())); + await ctx.SendTextAsync(Components.Shell("Music", await File.ReadAllTextAsync("wwwroot/music.html"), DefaultLinks())); } public static async Task MyArtist(ServerContext ctx) { - if(ctx.QueryParams.TryGetFirstGuid("id",out var id)) + 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(); + if (!ctx.QueryParams.TryGetFirstInt32("page", out page)) + page = 1; + if (!ctx.QueryParams.TryGetFirst("q", out q)) + q = ""; - 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)}"))); + 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)); + 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.TryGetFirstGuid("id", out var id)) { - if(!ctx.QueryParams.TryGetFirstInt32("page",out var page)) - page=1; - + 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) + 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) + foreach (var item in albumQuery.Albums) { - links.Add(new AlbumLink(item.Name,item.Id.ToString(),$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}")); + links.Add(new AlbumLink(item.Name, item.Id.ToString(), $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}")); } } - + await ctx.SendTextAsync(Pages.ArtistAlbumListing(id, links, page)); } /* @@ -1112,23 +1313,23 @@ namespace Tesses.YouTubeDownloader2025Companion { 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) + 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()) + using (var db = new Database()) { - VideoId? vid = await db.GetVideoIdAsync(songId.ToString()); - if(vid.HasValue) + 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); + 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())) + 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); + if (File.Exists(path)) File.Delete(path); } } @@ -1137,89 +1338,90 @@ namespace Tesses.YouTubeDownloader2025Companion //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())}"); + 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)) + if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId) && ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId)) { - StringBuilder builder = new StringBuilder(); - - using(var db = new Database()) + StringBuilder builder = new StringBuilder(); + + using (var db = new Database()) { var song = await db.GetSongAsync(songId.ToString()); - if(song is not null) + if (song is not null) { int i = 0; - foreach(var artist in await db.GetContributingArtistsAsync(songId.ToString())) + foreach (var artist in await db.GetContributingArtistsAsync(songId.ToString())) { - if(i == 0) + if (i == 0) builder.Append($"{artist.ArtistName} - {song.SongName}"); - else if(i == 1) + else if (i == 1) builder.Append($" feat {artist.ArtistName}"); - else + else builder.Append($", {artist.ArtistName}"); i++; - } + } } } - await ctx.SendTextAsync(Pages.IsntCorrect(songId.ToString(),albumId.ToString(),builder.ToString())); + 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("songId", out var songId)) { - if(ctx.QueryParams.TryGetFirstGuid("albumId",out var albumId)) + if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId)) { string html; - using(var db = new Database()) + 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) + 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() { + 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()); + 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()); + 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()) + List entries = new List(); + using (var db = new Database()) { - await foreach(var item in db.GetMediaEntriesAsync(songId.ToString())) + await foreach (var item in db.GetMediaEntriesAsync(songId.ToString())) { entries.Add(item); } } - if(entries.Count == 0) + if (entries.Count == 0) { - ctx.SendText(Components.Shell("Song not found","

    Song not found

    ", DefaultLinks())); + ctx.SendText(Components.Shell("Song not found", "

    Song not found

    ", DefaultLinks())); } - else if(entries.Count == 1) + else if (entries.Count == 1) { - await RedirectAsync(ctx,$"/music/mysong?songId={HttpUtility.UrlEncode(songId.ToString())}&albumId={HttpUtility.UrlEncode(entries[0].AlbumId)}"); + await RedirectAsync(ctx, $"/music/mysong?songId={HttpUtility.UrlEncode(songId.ToString())}&albumId={HttpUtility.UrlEncode(entries[0].AlbumId)}"); } - + } } } @@ -1227,100 +1429,101 @@ namespace Tesses.YouTubeDownloader2025Companion { 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(); + if (!ctx.QueryParams.TryGetFirstInt32("page", out page)) + page = 1; + if (!ctx.QueryParams.TryGetFirst("q", out q)) + q = ""; - using(var db=new Database()) + List links = new List(); + + using (var db = new Database()) { - foreach(var item in await db.GetSongsAsync(q,(page-1)*20)) + foreach (var item in await db.GetSongsAsync(q, (page - 1) * 20)) { - int i=0; + int i = 0; StringBuilder builder = new StringBuilder(); - foreach(var artist in await db.GetContributingArtistsAsync(item.SongId)) + foreach (var artist in await db.GetContributingArtistsAsync(item.SongId)) { - if(i == 0) + if (i == 0) builder.Append($"{artist.ArtistName} - {item.SongName}"); - else if(i == 1) + else if (i == 1) builder.Append($" feat {artist.ArtistName}"); - else + else builder.Append($", {artist.ArtistName}"); i++; - } + } string? html = null; - await foreach(var album in db.GetMediaEntriesAsync(item.SongId)) + 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"; + 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.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)); + 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(); + if (!ctx.QueryParams.TryGetFirstInt32("page", out page)) + page = 1; + if (!ctx.QueryParams.TryGetFirst("q", out q)) + q = ""; - 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)}"))); + 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)); + 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(); + if (!ctx.QueryParams.TryGetFirstInt32("page", out page)) + page = 1; + if (!ctx.QueryParams.TryGetFirst("q", out q)) + q = ""; - 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)}"))); + 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)); + 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.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) + 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) + 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())}")); + 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)) @@ -1329,22 +1532,22 @@ namespace Tesses.YouTubeDownloader2025Companion }*/ } - await ctx.SendTextAsync(Pages.ArtistListing(q,links,page)); - + await ctx.SendTextAsync(Pages.ArtistListing(q, links, page)); + } public static async Task MyAlbum(ServerContext ctx) { - if(ctx.QueryParams.TryGetFirstGuid("id",out var id)) + if (ctx.QueryParams.TryGetFirstGuid("id", out var id)) { string albumName = ""; - bool hasAny=false; + bool hasAny = false; StringBuilder artistsString = new StringBuilder(); List links = new List(); - using(var db = new Database()) + using (var db = new Database()) { var album = await db.GetAlbumAsync(id.ToString()); - if(album is not null) + if (album is not null) { albumName = album.AlbumName; } @@ -1352,13 +1555,13 @@ namespace Tesses.YouTubeDownloader2025Companion bool firstArtist = true; - foreach(var item in artists) + foreach (var item in artists) { - if(!firstArtist) + if (!firstArtist) artistsString.Append(", "); - + string artistEncoded = HttpUtility.UrlEncode(item.ArtistId); - + artistsString.Append(""); - firstArtist=false; + firstArtist = false; } var tracks = await db.GetTracksAsync(id.ToString()); - + //hasAnySongs - foreach(var item in tracks) + foreach (var item in tracks) { - var me=await db.GetMediaEntryAsync(id.ToString(),item.SongId); - if(me is not null) + 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), + 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 (hasSong) hasAny = true; } } } - + /*if(albumQuery is not null) { albumName = albumQuery.Title; @@ -1397,57 +1600,57 @@ namespace Tesses.YouTubeDownloader2025Companion 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())); + + 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)) + 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) + + 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))); - } + 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.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) + 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) + foreach (var item in albumQuery.Albums) { - links.Add(new AlbumLink(item.Name,item.Id.ToString(),$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}")); + links.Add(new AlbumLink(item.Name, item.Id.ToString(), $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}")); } } } - await ctx.SendTextAsync(Pages.AlbumListing(q,links,page)); + await ctx.SendTextAsync(Pages.AlbumListing(q, links, page)); /* List links = new List(); string q = ""; @@ -1468,7 +1671,7 @@ namespace Tesses.YouTubeDownloader2025Companion */ } - static ConcurrentBag eventables=new ConcurrentBag(); + static ConcurrentBag eventables = new ConcurrentBag(); public static void RegisterEventable(IEventable evt) { @@ -1484,18 +1687,18 @@ namespace Tesses.YouTubeDownloader2025Companion sso_app_key = sso.sso_app_key */ [JsonProperty("username")] - public string Username {get;set;}=""; + public string Username { get; set; } = ""; [JsonProperty("flags")] - public long Flags {get;set;} + public long Flags { get; set; } [JsonProperty("token")] - public string Token {get;set;}=""; + public string Token { get; set; } = ""; [JsonProperty("sso_app_key")] - public string SSOAppKey {get;set;}=""; + public string SSOAppKey { get; set; } = ""; } } - -} \ No newline at end of file + +}