Add more apis

This commit is contained in:
2026-04-05 02:53:22 -05:00
parent c0b83e4f4f
commit 8454187b3d
2 changed files with 922 additions and 695 deletions
+50 -26
View File
@@ -34,7 +34,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task<IEnumerable<LogEntry>> GetLogEntriesAsync(int offset = 0, int count = 20)
{
return await conn.QueryAsync<LogEntry>("SELECT * FROM tytd_events ORDER BY Time DESC LIMIT @Count OFFSET @Offset",new DynamicParameters(new{
return await conn.QueryAsync<LogEntry>("SELECT * FROM tytd_events ORDER BY Time DESC LIMIT @Count OFFSET @Offset", new DynamicParameters(new
{
Offset = offset,
Count = count
}));
@@ -86,11 +87,11 @@ namespace Tesses.YouTubeDownloader2025Companion
yield return ent;
}
}
public async Task<Session> CreateSessionTokenAsync(string username)
public async Task<Session> CreateSessionTokenAsync(string username, bool expires = true)
{
Session session = new Session();
session.AccountName = username;
session.Expires = Program.NowEpoch + Expires;
session.Expires = expires ? Program.NowEpoch + Expires : 0;
session.Key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Expires) VALUES (@AccountName,@Key,@Expires);", session);
@@ -117,28 +118,36 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task AddVideoIdAsync(string songId, VideoId id, string title)
{
try {
try
{
await conn.ExecuteAsync($"INSERT INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new { SongId = songId, VideoId = id.Value, VideoTitle = title }));
}catch(Exception){}
}
catch (Exception) { }
}
public async Task UpdateVideoIdAsync(string songId, VideoId id, string title)
{
try {
try
{
await conn.ExecuteAsync($"INSERT OR REPLACE INTO song_videos (SongId, VideoId, VideoTitle) VALUES (@SongId,@VideoId,@VideoTitle);", new DynamicParameters(new { SongId = songId, VideoId = id.Value, VideoTitle = title }));
}catch(Exception){}
}
catch (Exception) { }
}
public async Task AddAlbumArtistAsync(string albumId, string artistId)
{
try {
try
{
await conn.ExecuteAsync($"INSERT INTO album_artists (AlbumId, ArtistId) VALUES (@AlbumId,@ArtistId);", new DynamicParameters(new { AlbumId = albumId, ArtistId = artistId }));
}catch(Exception){}
}
catch (Exception) { }
}
public async Task AddContributingArtistAsync(string songId, string artistId)
{
try {
try
{
await conn.ExecuteAsync($"INSERT INTO song_artists (SongId, ArtistId) VALUES (@SongId,@ArtistId);", new DynamicParameters(new { SongId = songId, ArtistId = artistId }));
}catch(Exception){}
}
catch (Exception) { }
}
public async Task<SongVideo?> GetSongVideoFromVideoIdAsync(VideoId id)
{
@@ -150,27 +159,35 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task AddArtistAsync(Artist artist)
{
try {
try
{
await conn.ExecuteAsync("INSERT INTO artists (ArtistId,ArtistName,ArtistDisambiguation) VALUES (@ArtistId,@ArtistName,@ArtistDisambiguation);", artist);
}catch(Exception) {}
}
catch (Exception) { }
}
public async Task AddAlbumAsync(Album album)
{
try {
try
{
await conn.ExecuteAsync("INSERT INTO albums (AlbumId,AlbumName,AlbumReleaseDate) VALUES (@AlbumId,@AlbumName,@AlbumReleaseDate);", album);
}catch(Exception) {}
}
catch (Exception) { }
}
public async Task AddSongAsync(Song song)
{
try {
try
{
await conn.ExecuteAsync("INSERT INTO songs (SongId,SongName) VALUES (@SongId,@SongName);", song);
}catch(Exception) {}
}
catch (Exception) { }
}
public async Task AddTrackAsync(AlbumTrack track)
{
try {
try
{
await conn.ExecuteAsync("INSERT INTO album_tracks (AlbumId,SongId,Track,Length) VALUES (@AlbumId,@SongId,@Track,@Length);", track);
}catch(Exception) {}
}
catch (Exception) { }
}
public async Task<Song?> GetSongAsync(string id)
{
@@ -183,7 +200,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task<IEnumerable<Artist>> GetArtistsAsync(string q = "", long offset = 0, long count = 20)
{
return await conn.QueryAsync<Artist>("SELECT * FROM artists WHERE ArtistName LIKE @Query LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{
return await conn.QueryAsync<Artist>("SELECT * FROM artists WHERE ArtistName LIKE @Query LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
{
Query = $"%{q}%",
Offset = offset,
Count = count
@@ -191,7 +209,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task<IEnumerable<Song>> GetSongsAsync(string q = "", long offset = 0, long count = 20)
{
return await conn.QueryAsync<Song>("SELECT * FROM songs WHERE SongName LIKE @Query LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{
return await conn.QueryAsync<Song>("SELECT * FROM songs WHERE SongName LIKE @Query LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
{
Query = $"%{q}%",
Offset = offset,
Count = count
@@ -199,7 +218,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task<IEnumerable<Album>> GetAlbumsFromArtistAsync(string artistId, string q = "", long offset = 0, long count = 20)
{
return await conn.QueryAsync<Album>("SELECT * FROM albums al INNER JOIN album_artists ar ON al.AlbumId = ar.AlbumId WHERE AlbumName LIKE @Query AND ar.ArtistId = @ArtistId ORDER BY al.AlbumReleaseDate, al.Id ASC LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{
return await conn.QueryAsync<Album>("SELECT * FROM albums al INNER JOIN album_artists ar ON al.AlbumId = ar.AlbumId WHERE AlbumName LIKE @Query AND ar.ArtistId = @ArtistId ORDER BY al.AlbumReleaseDate, al.Id ASC LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
{
Query = $"%{q}%",
Offset = offset,
Count = count,
@@ -208,7 +228,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task<IEnumerable<Album>> GetAlbumsAsync(string q = "", long offset = 0, long count = 20)
{
return await conn.QueryAsync<Album>("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;",new DynamicParameters(new{
return await conn.QueryAsync<Album>("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
{
Query = $"%{q}%",
Offset = offset,
Count = count
@@ -234,9 +255,11 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public async Task<AlbumTrack?> GetTrackAsync(string songId, string albumId)
{
return (await conn.QueryAsync<AlbumTrack>("SELECT * FROM album_tracks WHERE AlbumId = @AlbumId AND SongId = @SongId", new DynamicParameters(new{
return (await conn.QueryAsync<AlbumTrack>("SELECT * FROM album_tracks WHERE AlbumId = @AlbumId AND SongId = @SongId", new DynamicParameters(new
{
SongId = songId,
AlbumId = albumId}))).FirstOrDefault();
AlbumId = albumId
}))).FirstOrDefault();
}
public async Task<IEnumerable<AlbumSongTrack>> GetTracksAsync(string albumId)
{
@@ -281,7 +304,8 @@ namespace Tesses.YouTubeDownloader2025Companion
public async Task AddEventAsync(long epoch, Webhook webhook)
{
//conn.Execute("CREATE TABLE IF NOT EXISTS tytd_events (Id INTEGER PRIMARY KEY AUTOINCREMENT, Time INTEGER, Type TEXT, VideoTitle TEXT, VideoId TEXT, ChannelTitle TEXT, ChannelId TEXT);");
await conn.ExecuteAsync("INSERT INTO tytd_events (Time,Type,VideoTitle,VideoId,ChannelTitle,ChannelId) VALUES (@Time,@Type,@VideoTitle,@VideoId,@ChannelTitle,@ChannelId);", new DynamicParameters(new {
await conn.ExecuteAsync("INSERT INTO tytd_events (Time,Type,VideoTitle,VideoId,ChannelTitle,ChannelId) VALUES (@Time,@Type,@VideoTitle,@VideoId,@ChannelTitle,@ChannelId);", new DynamicParameters(new
{
Time = epoch,
Type = webhook.Type,
VideoTitle = webhook.Video.Title,
+236 -33
View File
@@ -82,7 +82,8 @@ namespace Tesses.YouTubeDownloader2025Companion
public static void AddVideo(VideoId id, TYTDResolution res = TYTDResolution.LowVideo)
{
EnqueueTask(async()=>{
EnqueueTask(async () =>
{
var cfg = Config ?? new AuthConfig();
HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, $"{cfg.TYTDUrl.TrimEnd('/')}/api/v1/add");
@@ -203,6 +204,13 @@ namespace Tesses.YouTubeDownloader2025Companion
}
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);
@@ -245,7 +253,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
public static HttpActionAsync EnsureLoggedin(HttpActionAsync action)
{
return async(ctx)=>{
return async (ctx) =>
{
AuthConfig? cfg;
lock (MyLockObject)
{
@@ -284,6 +293,27 @@ namespace Tesses.YouTubeDownloader2025Companion
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);
@@ -306,6 +336,16 @@ namespace Tesses.YouTubeDownloader2025Companion
yield return new PageLink("Search", "search", "/search");
yield return new PageLink("Music", "music_note", "/music");
}
private class LoginRequest
{
public string username { get; set; } = "";
public string password { get; set; } = "";
}
private class AuthResponse
{
public bool success { get; set; } = false;
public long flags { get; set; } = 0;
}
public static void Main(string[] args)
{
Directory.CreateDirectory("data/Config");
@@ -323,7 +363,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
RouteServer routeServer = new RouteServer(new StaticServer("wwwroot"));
routeServer.Add("/api/v1/webhook",async(ctx)=>{
routeServer.Add("/api/v1/webhook", async (ctx) =>
{
string auth = "";
ctx.RequestHeaders.TryGetFirst("Authorization", out auth);
@@ -332,7 +373,8 @@ namespace Tesses.YouTubeDownloader2025Companion
if (string.IsNullOrWhiteSpace(cfg.Session) || (authHdr.Length == 2 && authHdr[0] == "TYTD" && cfg.SSOAppKey == authHdr[1]))
{
try {
try
{
var webhook = await ctx.ReadJsonAsync<Webhook>();
if (webhook is not null)
{
@@ -346,7 +388,8 @@ namespace Tesses.YouTubeDownloader2025Companion
{
var vid = VideoId.TryParse(webhook.Video.VideoId);
EnqueueTask(async()=>{
EnqueueTask(async () =>
{
if (vid.HasValue)
await ProcessSongAsync(vid.Value);
});
@@ -354,7 +397,8 @@ namespace Tesses.YouTubeDownloader2025Companion
break;
case "Started":
{
EnqueueTask(async()=>{
EnqueueTask(async () =>
{
using (var db = new Database())
{
foreach (var item in await db.GetSongVideosAsync())
@@ -371,7 +415,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
}
} catch(Exception ex)
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
@@ -383,9 +428,11 @@ namespace Tesses.YouTubeDownloader2025Companion
routeServer.Add("/settings", EnsureLoggedin(SettingsAsync));
routeServer.Add("/auth", AuthAsync, "POST");
routeServer.Add("/login", LoginAsync);
routeServer.Add("/rescan",EnsureLoggedin(async(ctx)=>{
routeServer.Add("/rescan", EnsureLoggedin(async (ctx) =>
{
ctx.ParseBody();
EnqueueTask(async()=>{
EnqueueTask(async () =>
{
using (var db = new Database())
{
foreach (var item in await db.GetSongVideosAsync())
@@ -398,19 +445,59 @@ namespace Tesses.YouTubeDownloader2025Companion
});
await ctx.WithStatusCode(System.Net.HttpStatusCode.ExpectationFailed).WriteHeadersAsync();
}), "POST");
routeServer.Add("/",EnsureLoggedin(async(ctx)=>{
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)=>{
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, false);
await ctx.SendJsonAsync(new
{
success = true,
token = session.Key
});
return;
}
}
}
}
await ctx.SendJsonAsync(new
{
success = false
});
}, "POST");
routeServer.Add("/oobe", EnsureLoggedin(async (ctx) =>
{
await ctx.SendTextAsync(Components.Shell("Setup", await File.ReadAllTextAsync("wwwroot/pages/oobe.html"), []));
}));
routeServer.Add("/oobe",EnsureLoggedin(async(ctx)=>{
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 {
using var resp = await MyHttpClient.PostAsync($"{tytd.TrimEnd('/')}/api/v1/login", JsonContent.Create(new
{
username = user,
password = pass
}));
@@ -423,7 +510,8 @@ namespace Tesses.YouTubeDownloader2025Companion
{
var service_name_unique = RandomNumberGenerator.GetHexString(24);
var sso_app_key = RandomNumberGenerator.GetHexString(64);
var register_req = JsonContent.Create(new{
var register_req = JsonContent.Create(new
{
service_name = $"tytdcompanion-{service_name_unique}",
sso_app_key,
service_pretty_name = "TYTD Companion",
@@ -454,7 +542,8 @@ namespace Tesses.YouTubeDownloader2025Companion
await InstallExtension();
await RedirectAsync(ctx, "/");
return;
} else if(sso_resp2.Type != "db")
}
else if (sso_resp2.Type != "db")
{
throw new Exception($"Error of {sso_resp2.Type}, {sso_resp2.Reason}");
}
@@ -468,7 +557,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}), "POST");
routeServer.Add("/search", EnsureLoggedin(Search));
routeServer.Add("/search_dialog", EnsureLoggedin(SearchDialog));
routeServer.Add("/search_button",EnsureLoggedin(async(ctx)=>{
routeServer.Add("/search_button", EnsureLoggedin(async (ctx) =>
{
ctx.ParseBody();
if (ctx.QueryParams.TryGetFirst("type", out var type))
@@ -502,6 +592,7 @@ namespace Tesses.YouTubeDownloader2025Companion
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));
@@ -509,20 +600,23 @@ namespace Tesses.YouTubeDownloader2025Companion
routeServer.Add("/api/v1/artist", EnsureLoggedin(ApiArtist));
routeServer.Add("/api/v1/albums", EnsureLoggedin(ApiAlbums));
routeServer.Add("/api/v1/album", EnsureLoggedin(ApiAlbum));
routeServer.Add("/api/v1/songs", EnsureLoggedin(ApiSongs));
routeServer.Add("/api/v1/song", EnsureLoggedin(ApiSong));
routeServer.Add("/api/v1/album.zip", EnsureLoggedin(ApiAlbumZip));
bool finished = false;
RegisterEventable(SearchResultHandler);
RegisterEventable(tasks);
Task t = Task.Factory.StartNew(async()=>{
Task t = Task.Factory.StartNew(async () =>
{
while (!finished)
{
foreach (var item in eventables)
await item.EventAsync();
}
});
Task.Run(async()=>{
Task.Run(async () =>
{
await Task.Delay(10000);
Console.WriteLine("Wait expired");
await InstallExtension();
@@ -585,10 +679,51 @@ namespace Tesses.YouTubeDownloader2025Companion
count = 20;
using (var db = new Database())
await ctx.WithMimeType("application/json").SendJsonAsync((await db.GetAlbumsFromArtistAsync(artistId.ToString(),q,offset,count)).ToList());
await ctx.WithMimeType("application/json").SendJsonAsync(
new
{
Albums = (await db.GetAlbumsFromArtistAsync(artistId.ToString(), q, offset, count)).ToList(),
ArtistName = (await db.GetArtistAsync(artistId.ToString()))?.ArtistName ?? ""
}
);
}
}
private static async Task ApiSongs(ServerContext ctx)
{
if (!ctx.QueryParams.TryGetFirst("q", out var q))
q = "";
if (!ctx.QueryParams.TryGetFirstInt64("offset", out var offset))
offset = 0;
if (!ctx.QueryParams.TryGetFirstInt64("count", out var count))
count = 20;
using (var db = new Database())
{
List<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))
@@ -600,38 +735,62 @@ namespace Tesses.YouTubeDownloader2025Companion
count = 20;
using (var db = new Database())
await ctx.WithMimeType("application/json").SendJsonAsync((await db.GetAlbumsAsync(q,offset,count)).ToList());
await ctx.WithMimeType("application/json").SendJsonAsync((await db.GetAlbumsAsync(q, offset * 20, count)).ToList());
}
private static async Task ApiAlbum(ServerContext ctx)
{
if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
using (var db = new Database())
{
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);
tracks.Add(new {
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,
ContributingArtists = artists.ToList()
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 {
await ctx.WithMimeType("application/json").SendJsonAsync(new
{
Album = album,
AlbumArtists = albumArtists,
Tracks = tracks
Tracks = tracks,
HasAny = hasAny
});
}
}
private static async Task ApiSong(ServerContext ctx)
{
if(ctx.QueryParams.TryGetFirstGuid("albumId",out var albumId) && ctx.QueryParams.TryGetFirstGuid("songId",out var songId))
if (ctx.QueryParams.TryGetFirstGuid("songId", out var songId))
{
if (ctx.QueryParams.TryGetFirstGuid("albumId", out var albumId))
{
var inline = ctx.QueryParams.GetFirstBoolean("inline");
MediaEntry? mId = null;
@@ -653,7 +812,25 @@ namespace Tesses.YouTubeDownloader2025Companion
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();
}
@@ -667,13 +844,31 @@ namespace Tesses.YouTubeDownloader2025Companion
ctx.ParseBody();
if (ctx.QueryParams.TryGetFirstGuid("id", out var id))
{
EnqueueTask(async()=>{
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));
@@ -870,7 +1065,9 @@ namespace Tesses.YouTubeDownloader2025Companion
}
}
} else if(!string.IsNullOrWhiteSpace(songId)) {
}
else if (!string.IsNullOrWhiteSpace(songId))
{
AddVideo(value, TYTDResolution.AudioOnly);
}
@@ -914,7 +1111,8 @@ namespace Tesses.YouTubeDownloader2025Companion
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
else {
else
{
using (var res = await MyHttpClient.GetAsync(url))
{
@@ -925,7 +1123,8 @@ namespace Tesses.YouTubeDownloader2025Companion
}
}
else {
else
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
}
@@ -966,7 +1165,8 @@ namespace Tesses.YouTubeDownloader2025Companion
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
else {
else
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
}
@@ -995,7 +1195,8 @@ namespace Tesses.YouTubeDownloader2025Companion
using (var dest = File.Create(path))
await res.Content.CopyToAsync(dest);
}
else {
else
{
File.Copy("wwwroot/tytd-artist.jpg", path, true);
}
}
@@ -1185,7 +1386,8 @@ namespace Tesses.YouTubeDownloader2025Companion
var file = await db.GetMediaEntryAsync(albumId.ToString(), songId.ToString());
if (song is not null && album is not null && trk is not null && file is not null)
{
video ??= new SongVideo() {
video ??= new SongVideo()
{
SongId = songId.ToString(),
VideoTitle = "No Video",
@@ -1256,7 +1458,8 @@ namespace Tesses.YouTubeDownloader2025Companion
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 {
else
{
html = "<i>list</i>";
break;
}