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