Files
tytdcompanion/Program.cs
T
2026-03-15 22:15:34 -05:00

1501 lines
66 KiB
C#

using Newtonsoft.Json;
using Tesses.WebServer;
//using YouTubeMusicAPI.Models.Info;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Collections.Concurrent;
using System.Web;
using YoutubeExplode;
//using YouTubeMusicAPI.Client;
using YoutubeExplode.Videos;
using Newtonsoft.Json.Linq;
using System.ComponentModel;
using System.Net.Mime;
using System.Data.Common;
using System.Text;
using YoutubeExplode.Search;
using YouTubeMusicAPI.Client;
using YouTubeMusicAPI.Models.Search;
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> task)
{
tasks.Enqueue(task);
}
static TaskList tasks = new TaskList();
public static long NowEpoch
{
get => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
public static SearchResultHandler SearchResultHandler = new SearchResultHandler();
public static ConcurrentDictionary<string,OneUseTokenData?> OneUse {get;set;}=new ConcurrentDictionary<string,OneUseTokenData?>();
public const string AuthConfigPath = "data/Config/auth.json";
public static object MyLockObject=new object();
public static AuthConfig? Config =null;
public static HttpClient MyHttpClient =new HttpClient();
public static YoutubeClient YouTubeClient = new YoutubeClient();
public static YouTubeMusicClient YouTubeMusicClient = new YouTubeMusicClient();
private static long LastRequestMinute = 0;
private static long RequestsInThisMinute = 0;
private const long RequestsPerMinute = 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> task)
{
if(InRateLimit || ShouldRateLimit()) await Task.Delay(30000);
InRateLimit = false;
await task();
}
public static async Task<T> RateLimitAsync<T>(Func<Task<T>> task)
{
if(InRateLimit || ShouldRateLimit()) await Task.Delay(30000);
InRateLimit = false;
return await task();
}
public static async IAsyncEnumerable<T> RateLimitAsync<T>(IAsyncEnumerable<T> task)
{
await using var enumerator = task.GetAsyncEnumerator();
while(await RateLimitAsync(async ()=>await enumerator.MoveNextAsync()))
{
yield return enumerator.Current;
}
}
public static async Task<T?> MusicBrainzRequestAsync<T>(string url)
{
var req = new HttpRequestMessage(HttpMethod.Get,$"https://musicbrainz.org{url}");
req.Headers.Add("User-Agent","TYTDCompanion/1.0 (tesses@tesses.net)");
using var resp = await MyHttpClient.SendAsync(req);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadFromJsonAsync<T>();
}
//public static YouTubeMusicClient YouTubeMusicClient = new YouTubeMusicClient();
public static async Task RedirectAsync(ServerContext ctx, string url)
{
if(ctx.RequestHeaders.GetFirstBoolean("HX-Request"))
{
ctx.ResponseHeaders["HX-Redirect"] = [url];
await ctx.WithStatusCode(System.Net.HttpStatusCode.NoContent).WriteHeadersAsync();
}
else
{
ctx.ResponseHeaders["Location"] = [url];
await ctx.WithStatusCode(System.Net.HttpStatusCode.SeeOther).WriteHeadersAsync();
}
}
public static async Task AuthRedirectAsync(ServerContext ctx)
{
while(true)
{
string tmpToken = RandomNumberGenerator.GetHexString(24);
if(OneUse.TryAdd(tmpToken,null))
{
var cfg = Config ?? new AuthConfig();
await RedirectAsync(ctx,$"{cfg.TYTDUrl.TrimEnd('/')}/sso?app={HttpUtility.UrlEncode(cfg.ServiceName)}&token={HttpUtility.UrlEncode(tmpToken)}");
return;
}
}
}
public static async Task AuthAsync(ServerContext ctx)
{
var json = await ctx.ReadJsonAsync<OneUseTokenData>();
var cfg = Config ?? new AuthConfig();
if(json is not null && json.SSOAppKey == cfg.SSOAppKey && OneUse.TryUpdate(json.Token,json,null))
{
ctx.WithStatusCode(System.Net.HttpStatusCode.OK).SendText("OK","text/plain");
return;
}
ctx.StatusCode=401;
await ctx.SendTextAsync("BAD");
}
public static async Task LoginAsync(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("token",out var token) && OneUse.TryRemove(token,out var val))
{
if(val is not null)
{
using(var db = new Database())
{
var res = await db.CreateSessionTokenAsync(val.Username);
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<PageLink> 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<AuthConfig>(File.ReadAllText(AuthConfigPath));
}
RouteServer routeServer=new RouteServer(new StaticServer("wwwroot"));
routeServer.Add("/api/v1/webhook",async(ctx)=>{
string auth = "";
ctx.RequestHeaders.TryGetFirst("Authorization",out auth);
var authHdr = auth.Split(' ',2,StringSplitOptions.RemoveEmptyEntries);
var cfg = Config ?? new AuthConfig();
if(string.IsNullOrWhiteSpace(cfg.Session) || (authHdr.Length == 2 && authHdr[0] == "TYTD" && cfg.SSOAppKey == authHdr[1]))
{
try {
var webhook = await ctx.ReadJsonAsync<Webhook>();
if(webhook is not null)
{
var epoch = NowEpoch;
using(var db = new Database())
await db.AddEventAsync(epoch, webhook);
Console.WriteLine(JsonConvert.SerializeObject(webhook));
switch(webhook.Type)
{
case "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<TYTDLoginResponse>();
if(tytdLogin is not null && tytdLogin.Success)
{
for(int i = 0; i < 10; i++)
{
var service_name_unique = RandomNumberGenerator.GetHexString(24);
var sso_app_key = RandomNumberGenerator.GetHexString(64);
var register_req = JsonContent.Create(new{
service_name = $"tytdcompanion-{service_name_unique}",
sso_app_key,
service_pretty_name = "TYTD Companion",
service_auth_post = $"{companion.TrimEnd('/')}/auth",
service_auth_redirect = $"{companion.TrimEnd('/')}/login?token="
});
var sso_req=new HttpRequestMessage(HttpMethod.Post,$"{tytd.TrimEnd('/')}/api/v1/register_sso");
sso_req.Content = register_req;
sso_req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",tytdLogin.Token);
using var sso_resp = await MyHttpClient.SendAsync(sso_req);
var sso_resp2 = JsonConvert.DeserializeObject<SSOResp>(await sso_resp.Content.ReadAsStringAsync());
if(sso_resp2 is not null)
{
if(sso_resp2.Success)
{
AuthConfig cfg=new AuthConfig();
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("""<li>Added</li>""");
return;
}
}
}
await ctx.SendTextAsync("BAD");
}),"POST");
routeServer.Add("/search_more",EnsureLoggedin(SearchMore));
routeServer.Add("/music",EnsureLoggedin(Music));
routeServer.Add("/music/artists", EnsureLoggedin(Artists));
routeServer.Add("/music/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<object> tracks = new List<object>();
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("<span>Added</span>");
}
}
public static async Task AddArtistAsync(AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists artist)
{
await RateLimitAsync(async()=>await EnsureArtistThumbnail(artist.Artist.Id));
using(var db=new Database())
await db.AddArtistAsync(new YouTubeDownloader2025Companion.Artist() { ArtistDisambiguation =artist.Artist.Disambiguation , ArtistName = artist.Artist.Name, ArtistId = artist.Artist.Id.ToString() });
}
public static async Task<VideoId?> GetVideoIdFromSong(AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording recording, string albumName)
{
using(var db = new Database())
{
VideoId? vid = await db.GetVideoIdAsync(recording.Id.ToString());
if(vid.HasValue) return vid;
}
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<AlbumSongQuery>($"/ws/2/release/{HttpUtility.UrlEncode(id.ToString())}/?inc=recordings+artist-credits&fmt=json"));
if(results is not null)
{
using(var db=new Database())
await db.AddAlbumAsync(new YouTubeDownloader2025Companion.Album() { AlbumId = id.ToString(), AlbumName = results.Title, AlbumReleaseDate = results.Date });
foreach(var artist in results.Artists)
{
using(var db=new Database())
await db.AddAlbumArtistAsync(id.ToString(), artist.Artist.Id.ToString());
await AddArtistAsync(artist);
}
if(addTracks)
{
foreach(var media in results.Media)
{
foreach(var track in media.Tracks)
{
foreach(var artist in track.Recording.ContributingArtists)
{
using(var db=new Database())
await db.AddContributingArtistAsync(track.Recording.Id.ToString(), artist.Artist.Id.ToString());
await AddArtistAsync(artist);
}
using(var db=new Database())
{
await db.AddSongAsync(new Song() { SongId = track.Recording.Id.ToString(), SongName = track.Recording.Name });
await db.AddTrackAsync(new AlbumTrack() { AlbumId = id.ToString(), SongId = track.Recording.Id.ToString(), Track = track.Track, Length = track.Length});
}
VideoId? vid = await GetVideoIdFromSong(track.Recording, results.Title);
if(vid.HasValue)
await ProcessSongAsync(vid.Value);
}
}
}
}
}
private static async Task<bool> VideoExistsOnTYTD(VideoId id, TYTDResolution res)
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Head,$"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/download?v={id.Value}&res={res}");
if(!string.IsNullOrWhiteSpace(cfg.Session))
{
msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session);
}
using(var res0 = await MyHttpClient.SendAsync(msg))
{
return res0.IsSuccessStatusCode;
}
}
public static async Task DownloadSongFromTYTD(VideoId id)
{
string videoPath = Path.Combine("data","MusicFromTYTD",$"{id.Value}");
if(!File.Exists(videoPath+".bin"))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get,$"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/download?v={id.Value}&res=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<MediaEntry> entries = new List<MediaEntry>();
using(var db = new Database())
{
if(!string.IsNullOrWhiteSpace(songId))
{
await foreach(var item in db.GetMediaEntriesAsync(songId))
{
entries.Add(item);
}
}
}
if(File.Exists(videoPath))
{
foreach(var item in entries)
{
await item.ImportAsync(videoPath);
}
}
} else if(!string.IsNullOrWhiteSpace(songId)) {
AddVideo(value, TYTDResolution.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<AudioDBArtistJsonResponse>();
if(respJson is not null && respJson.Artists is not null && respJson.Artists.Count > 0)
{
string url = respJson.Artists[0].Thumbnail;
if(string.IsNullOrWhiteSpace(url))
{
File.Copy("wwwroot/tytd-artist.jpg",path,true);
}
else {
using(var res = await MyHttpClient.GetAsync(url))
{
res.EnsureSuccessStatusCode();
using(var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
}
}
else {
File.Copy("wwwroot/tytd-artist.jpg",path,true);
}
}
}
}
private static async Task ArtistThumbnail(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id",out var id))
{
string path = $"data/Thumbnails/Artists/{id}.jpg";
await EnsureArtistThumbnail(id);
if(File.Exists(path))
{
await ctx.SendFileAsync(path);
}
else
{
await ctx.SendFileAsync("wwwroot/tytd-artist.jpg");
}
}
}
public static async Task EnsureAlbumThumbnail(Guid id)
{
string path = $"data/Thumbnails/Albums/{id.ToString()}.jpg";
if(!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get,cfg.ThumbnailsThroughTYTD ? $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/artist-thumbnail?id={HttpUtility.UrlEncode(id.ToString())}" : $"https://coverartarchive.org/release/{HttpUtility.UrlEncode(id.ToString())}/front");
if(!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session);
}
using(var res = await MyHttpClient.SendAsync(msg))
{
if(res.IsSuccessStatusCode)
{
using(var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
else {
File.Copy("wwwroot/tytd-artist.jpg",path,true);
}
}
}
}
private static async Task AlbumThumbnail(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
string path = $"data/Thumbnails/Albums/{id.ToString()}.jpg";
if(!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get,cfg.ThumbnailsThroughTYTD ? $"{cfg.TYTDUrl.TrimEnd('/')}/plugin/tytdcompanion/artist-thumbnail?id={HttpUtility.UrlEncode(id.ToString())}" : $"https://coverartarchive.org/release/{HttpUtility.UrlEncode(id.ToString())}/front");
if(!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session);
}
using(var res = await MyHttpClient.SendAsync(msg))
{
if(res.IsSuccessStatusCode)
{
using(var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
else {
File.Copy("wwwroot/tytd-artist.jpg",path,true);
}
}
}
if(File.Exists(path))
{
await ctx.SendFileAsync(path);
}
}
}
private static async Task VideoThumbnail(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirst("v", out var v))
{
// TYTD/api/v1/video-thumbnail?v=il9nqWw9W3Y
VideoId? id = VideoId.TryParse(v);
if(id.HasValue)
{
string path = $"data/Thumbnails/Video/{id.Value}.jpg";
if(!File.Exists(path))
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get,cfg.ThumbnailsThroughTYTD ? $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/video-thumbnail?v={id.Value.Value}&res=default" : $"https://s.ytimg.com/vi/{id.Value.Value}/default.jpg");
if(!string.IsNullOrWhiteSpace(cfg.Session) && cfg.ThumbnailsThroughTYTD)
{
msg.Headers.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",cfg.Session);
}
using(var res = await MyHttpClient.SendAsync(msg))
{
res.EnsureSuccessStatusCode();
using(var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
}
if(File.Exists(path))
{
await ctx.SendFileAsync(path);
}
}
}
}
public static async Task SearchMore(ServerContext ctx)
{
await ctx.SendTextAsync(await Components.SearchMoreAsync(ctx));
}
public static async Task Search(ServerContext ctx)
{
await ctx.SendTextAsync(await Pages.SearchAsync(ctx));
}
public static async Task SearchDialog(ServerContext ctx)
{
await ctx.SendTextAsync(await Pages.SearchDialogAsync(ctx));
}
public static async Task Music(ServerContext ctx)
{
await ctx.SendTextAsync(Components.Shell("Music",await File.ReadAllTextAsync("wwwroot/music.html"),DefaultLinks()));
}
public static async Task MyArtist(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id",out var id))
{
string q = "";
int page = 1;
if(!ctx.QueryParams.TryGetFirstInt32("page",out page))
page=1;
if(!ctx.QueryParams.TryGetFirst("q",out q))
q="";
List<MyAlbumLink> links=new List<MyAlbumLink>();
using(var db=new Database())
links.AddRange((await db.GetAlbumsFromArtistAsync(id.ToString(),q,(page-1)*20)).Select(e=>new MyAlbumLink(e.AlbumName,e.AlbumId,$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(e.AlbumId)}")));
await ctx.SendTextAsync(Pages.MyArtistAlbumListing(id,q,links,page));
}
}
public static async Task Artist(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id",out var id))
{
if(!ctx.QueryParams.TryGetFirstInt32("page",out var page))
page=1;
List<AlbumLink> links = new List<AlbumLink>();
var albumQuery = await MusicBrainzRequestAsync<AlbumQuery>($"/ws/2/release?artist={HttpUtility.UrlEncode(id.ToString())}&fmt=json&offset={(page-1)*25}&limit=25&type=album");
if(albumQuery is not null)
{
foreach(var item in albumQuery.Albums)
{
links.Add(new AlbumLink(item.Name,item.Id.ToString(),$"/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($"<p>{HttpUtility.HtmlEncode($"SongId: {songId}, AlbumId: {albumId}, Url: {url}")}</p>");
await RedirectAsync(ctx,$"/music/mysong?songId={HttpUtility.UrlEncode(songId.ToString())}&albumId={HttpUtility.UrlEncode(albumId.ToString())}");
}
}
public static async Task FixIncorrectGet(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("songId",out var songId) && ctx.QueryParams.TryGetFirstGuid("albumId",out var albumId))
{
StringBuilder builder = new StringBuilder();
using(var db = new Database())
{
var song = await db.GetSongAsync(songId.ToString());
if(song is not null)
{
int i = 0;
foreach(var artist in await db.GetContributingArtistsAsync(songId.ToString()))
{
if(i == 0)
builder.Append($"{artist.ArtistName} - {song.SongName}");
else if(i == 1)
builder.Append($" feat {artist.ArtistName}");
else
builder.Append($", {artist.ArtistName}");
i++;
}
}
}
await ctx.SendTextAsync(Pages.IsntCorrect(songId.ToString(),albumId.ToString(),builder.ToString()));
}
}
public static async Task MySong(ServerContext ctx)
{
var cfg = Config ?? new AuthConfig();
if(ctx.QueryParams.TryGetFirstGuid("songId",out var songId))
{
if(ctx.QueryParams.TryGetFirstGuid("albumId",out var albumId))
{
string html;
using(var db = new Database())
{
var song = await db.GetSongAsync(songId.ToString());
var album = await db.GetAlbumAsync(albumId.ToString());
var trk = await db.GetTrackAsync(songId.ToString(), albumId.ToString());
var video = await db.GetSongVideoFromSongIdAsync(songId.ToString());
var file = await db.GetMediaEntryAsync(albumId.ToString(),songId.ToString());
if(song is not null && album is not null && trk is not null && file is not null)
{
video ??= new SongVideo() {
SongId = songId.ToString(),
VideoTitle = "No Video",
};
var artists = await db.GetContributingArtistsAsync(songId.ToString());
html =Pages.MySong(song,album,video,cfg.TYTDUrl,File.Exists(file.ToString()),trk.Track,TimeSpan.FromSeconds(trk.Length/1000),artists.ToList());
}
else
html = Components.Shell("Song not found","<h1>Song not found</h1>", DefaultLinks());
}
await ctx.SendTextAsync(html);
}
else
{
List<MediaEntry> entries=new List<MediaEntry>();
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","<h1>Song not found</h1>", 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<MySongLink> links=new List<MySongLink>();
using(var db=new Database())
{
foreach(var item in await db.GetSongsAsync(q,(page-1)*20))
{
int i=0;
StringBuilder builder = new StringBuilder();
foreach(var artist in await db.GetContributingArtistsAsync(item.SongId))
{
if(i == 0)
builder.Append($"{artist.ArtistName} - {item.SongName}");
else if(i == 1)
builder.Append($" feat {artist.ArtistName}");
else
builder.Append($", {artist.ArtistName}");
i++;
}
string? html = null;
await foreach(var album in db.GetMediaEntriesAsync(item.SongId))
{
if(html is null) html = File.Exists(album.ToString()) ? $"<a class=\"transparent\" href=\"/api/v1/song?albumId={HttpUtility.UrlEncode(album.AlbumId)}&songId={HttpUtility.UrlEncode(item.SongId)}\"><i>download</i></a>" : ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null ? "<i>downloading</i>" : "<i>file_download_off</i>");
else {
html="<i>list</i>";
break;
}
}
links.Add(new MySongLink(builder.ToString(),item.SongId,html ?? ""));
}
//links.AddRange(().Select(e=>new MyArtistLink(e.ArtistName,e.ArtistId,$"/api/v1/artist-thumbnail?id={HttpUtility.UrlEncode(e.ArtistId)}")));
}
await ctx.SendTextAsync(Pages.MySongListing(q,links,page));
}
public static async Task MyArtists(ServerContext ctx)
{
string q = "";
int page = 1;
if(!ctx.QueryParams.TryGetFirstInt32("page",out page))
page=1;
if(!ctx.QueryParams.TryGetFirst("q",out q))
q="";
List<MyArtistLink> links=new List<MyArtistLink>();
using(var db=new Database())
links.AddRange((await db.GetArtistsAsync(q,(page-1)*20)).Select(e=>new MyArtistLink(e.ArtistName,e.ArtistId,$"/api/v1/artist-thumbnail?id={HttpUtility.UrlEncode(e.ArtistId)}")));
await ctx.SendTextAsync(Pages.MyArtistListing(q,links,page));
}
public static async Task MyAlbums(ServerContext ctx)
{
string q = "";
int page = 1;
if(!ctx.QueryParams.TryGetFirstInt32("page",out page))
page=1;
if(!ctx.QueryParams.TryGetFirst("q",out q))
q="";
List<MyAlbumLink> links=new List<MyAlbumLink>();
using(var db=new Database())
links.AddRange((await db.GetAlbumsAsync(q,(page-1)*20)).Select(e=>new MyAlbumLink(e.AlbumName,e.AlbumId,$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(e.AlbumId)}")));
await ctx.SendTextAsync(Pages.MyAlbumListing(q,links,page));
}
public static async Task Artists(ServerContext ctx)
{
List<ArtistLink> links = new List<ArtistLink>();
string q = "";
int page = 1;
if(ctx.QueryParams.TryGetFirst("q",out q))
{
if(!ctx.QueryParams.TryGetFirstInt32("page",out page))
page=1;
var artistQuery = await MusicBrainzRequestAsync<ArtistQuery>($"/ws/2/artist?query={HttpUtility.UrlEncode(q)}&fmt=json&offset={(page-1)*25}&limit=25");
if(artistQuery is not null)
{
foreach(var item in artistQuery.Artists)
{
links.Add(new ArtistLink(item.Name,item.Id.ToString(),$"/api/v1/artist-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
}
}
/*await foreach(var item in (q,10,0,true))
{
Console.WriteLine(item.Item.Name);
}*/
}
await ctx.SendTextAsync(Pages.ArtistListing(q,links,page));
}
public static async Task MyAlbum(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id",out var id))
{
string albumName = "";
bool hasAny=false;
StringBuilder artistsString = new StringBuilder();
List<MyAlbumSongLink> links = new List<MyAlbumSongLink>();
using(var db = new Database())
{
var album = await db.GetAlbumAsync(id.ToString());
if(album is not null)
{
albumName = album.AlbumName;
}
var artists = await db.GetAlbumArtistsAsync(id.ToString());
bool firstArtist = true;
foreach(var item in artists)
{
if(!firstArtist)
artistsString.Append(", ");
string artistEncoded = HttpUtility.UrlEncode(item.ArtistId);
artistsString.Append("<a hx-target=\"body\" hx-push-url=\"true\" hx-get=\"/music/myartist?id=");
artistsString.Append(artistEncoded);
artistsString.Append("\" href=\"/music/myartist?id=");
artistsString.Append(artistEncoded);
artistsString.Append("\">");
artistsString.Append(HttpUtility.HtmlEncode(item.ArtistName));
artistsString.Append("</a>");
firstArtist=false;
}
var tracks = await db.GetTracksAsync(id.ToString());
//hasAnySongs
foreach(var item in tracks)
{
var me=await db.GetMediaEntryAsync(id.ToString(),item.SongId);
if(me is not null)
{
bool hasSong = File.Exists(me.ToString());
links.Add(new MyAlbumSongLink(me.TrackIndex,item.SongName,id.ToString(),item.SongId,TimeSpan.FromSeconds(item.Length/1000),
hasSong ? $"<a class=\"transparent\" href=\"/api/v1/song?albumId={HttpUtility.UrlEncode(id.ToString())}&songId={HttpUtility.UrlEncode(item.SongId)}\"><i>download</i></a>" : ((await db.GetSongVideoFromSongIdAsync(item.SongId)) is not null ? "<i>downloading</i>" : "<i>file_download_off</i>")
));
if(hasSong) hasAny=true;
}
}
}
/*if(albumQuery is not null)
{
albumName = albumQuery.Title;
var first = albumQuery.Media.FirstOrDefault();
if(first is not null)
foreach(var item in first.Tracks)
{
links.Add(new AlbumSongLink(item.Track,item.Recording.Name,item.Recording.Id.ToString(),TimeSpan.FromSeconds(item.Length/1000)));
}
}*/
await ctx.SendTextAsync(Pages.MyAlbumInfo(albumName, id.ToString(), links,hasAny, artistsString.ToString()));
}
}
public static async Task Album(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("id",out var id))
{
List<AlbumSongLink> links = new List<AlbumSongLink>();
var albumQuery = await MusicBrainzRequestAsync<AlbumSongQuery>($"/ws/2/release/{HttpUtility.UrlEncode(id.ToString())}/?inc=recordings&fmt=json");
string albumName = "";
if(albumQuery is not null)
{
albumName = albumQuery.Title;
var first = albumQuery.Media.FirstOrDefault();
if(first is not null)
foreach(var item in first.Tracks)
{
links.Add(new AlbumSongLink(item.Track,item.Recording.Name,id.ToString(),item.Recording.Id.ToString(),TimeSpan.FromSeconds(item.Length/1000)));
}
}
await ctx.SendTextAsync(Pages.AlbumInfo(albumName, id.ToString(), links));
}
}
public static async Task Albums(ServerContext ctx)
{
List<AlbumLink> links = new List<AlbumLink>();
string q = "";
int page = 1;
if(ctx.QueryParams.TryGetFirst("q",out q))
{
if(!ctx.QueryParams.TryGetFirstInt32("page",out page))
page=1;
var albumQuery = await MusicBrainzRequestAsync<AlbumQuery>($"/ws/2/release?query={HttpUtility.UrlEncode(q)}&fmt=json&offset={(page-1)*25}&limit=25");
if(albumQuery is not null)
{
foreach(var item in albumQuery.Albums)
{
links.Add(new AlbumLink(item.Name,item.Id.ToString(),$"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
}
}
}
await ctx.SendTextAsync(Pages.AlbumListing(q,links,page));
/*
List<AlbumLink> links = new List<AlbumLink>();
string q = "";
if(ctx.QueryParams.TryGetFirst("q",out q))
{
await foreach(var item in YouTubeMusicClient.SearchAsync(q,YouTubeMusicAPI.Models.Search.SearchCategory.Albums))
{
string url="";
foreach(var thumb in item.Thumbnails)
{
if(thumb.Width == 226) url = thumb.Url;
}
links.Add(new AlbumLink(item.Name,item.Id,url));
}
}
await ctx.SendTextAsync(Pages.AlbumListing(q,links));
*/
}
static ConcurrentBag<IEventable> eventables=new ConcurrentBag<IEventable>();
public static void RegisterEventable(IEventable evt)
{
eventables.Add(evt);
}
public class OneUseTokenData
{
/*
username = user.username,
flags = user.flags,
token = token,
sso_app_key = sso.sso_app_key
*/
[JsonProperty("username")]
public string Username {get;set;}="";
[JsonProperty("flags")]
public long Flags {get;set;}
[JsonProperty("token")]
public string Token {get;set;}="";
[JsonProperty("sso_app_key")]
public string SSOAppKey {get;set;}="";
}
}
}