Make it more complete
This commit is contained in:
+3
-5
@@ -4,11 +4,13 @@ using System.Web;
|
||||
|
||||
namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
public class PageLink(string title, string icon, string url,string badge="") {
|
||||
public class PageLink(string title, string icon, string url, bool active,string badge="") {
|
||||
public string Title => HttpUtility.HtmlEncode(title);
|
||||
public string Icon => HttpUtility.HtmlEncode(icon);
|
||||
public string Url => HttpUtility.HtmlAttributeEncode(url);
|
||||
|
||||
public bool Active => active;
|
||||
|
||||
public string Badge => HttpUtility.HtmlEncode(badge);
|
||||
}
|
||||
public static partial class Components {
|
||||
@@ -22,9 +24,5 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
});
|
||||
}
|
||||
|
||||
internal static string Shell(string v1, string v2, object defaultLinks)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -22,7 +22,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
|
||||
public void Init()
|
||||
{
|
||||
conn.Execute("CREATE TABLE IF NOT EXISTS sessions (Id INTEGER PRIMARY KEY AUTOINCREMENT, AccountName TEXT, Key TEXT UNIQUE, Expires INTEGER);");
|
||||
conn.Execute("CREATE TABLE IF NOT EXISTS sessions (Id INTEGER PRIMARY KEY AUTOINCREMENT, AccountName TEXT, Key TEXT UNIQUE, Expires INTEGER, Flags INTEGER);");
|
||||
conn.Execute("CREATE TABLE IF NOT EXISTS song_videos (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT UNIQUE, VideoId TEXT UNIQUE, VideoTitle TEXT);");
|
||||
conn.Execute("CREATE TABLE IF NOT EXISTS songs (Id INTEGER PRIMARY KEY AUTOINCREMENT, SongId TEXT UNIQUE, SongName TEXT);");
|
||||
conn.Execute("CREATE TABLE IF NOT EXISTS artists (Id INTEGER PRIMARY KEY AUTOINCREMENT, ArtistId TEXT UNIQUE, ArtistName TEXT, ArtistDisambiguation TEXT);");
|
||||
@@ -87,14 +87,15 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
yield return ent;
|
||||
}
|
||||
}
|
||||
public async Task<Session> CreateSessionTokenAsync(string username, bool expires = true)
|
||||
public async Task<Session> CreateSessionTokenAsync(string username, long flags, bool expires = true)
|
||||
{
|
||||
Session session = new Session();
|
||||
session.AccountName = username;
|
||||
session.Expires = expires ? Program.NowEpoch + Expires : 0;
|
||||
session.Flags = flags;
|
||||
session.Key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Expires) VALUES (@AccountName,@Key,@Expires);", session);
|
||||
await conn.ExecuteAsync("INSERT INTO sessions (AccountName,Key,Flags,Expires) VALUES (@AccountName,@Key,@Flags,@Expires);", session);
|
||||
|
||||
return session;
|
||||
}
|
||||
@@ -226,6 +227,16 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
ArtistId = artistId
|
||||
}));
|
||||
}
|
||||
public async Task<IEnumerable<Song>> GetUnavailableSongsAsync(long offset=0, long count=20)
|
||||
{
|
||||
//SELECT * FROM songs s WHERE NOT EXISTS (SELECT * FROM song_videos v WHERE v.SongId = s.SongId);
|
||||
return await conn.QueryAsync<Song>("SELECT * FROM songs s WHERE NOT EXISTS (SELECT * FROM song_videos v WHERE v.SongId = s.SongId) LIMIT @Count OFFSET @Offset;", new DynamicParameters(new
|
||||
{
|
||||
Offset = offset,
|
||||
Count = count
|
||||
}));
|
||||
}
|
||||
|
||||
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
|
||||
@@ -323,6 +334,8 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
|
||||
public string Key { get; set; } = "";
|
||||
|
||||
public long Flags {get;set;}
|
||||
|
||||
public long Expires { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,8 @@ public class AlbumQuery {
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Name {get;set;}="";
|
||||
|
||||
[JsonPropertyName("date")]
|
||||
public string Date {get;set;}="";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Tesses.YouTubeDownloader2025Companion;
|
||||
|
||||
|
||||
public class MusicBrainzSongInfo
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Name {get;set;}="";
|
||||
|
||||
[JsonPropertyName("artist-credit")]
|
||||
public List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists> ContributingArtists {get;set;}=new List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists>();
|
||||
|
||||
[JsonPropertyName("releases")]
|
||||
public List<AlbumQuery.AlbumQueryItem> Albums {get;set;}=new();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Tesses.YouTubeDownloader2025Companion;
|
||||
|
||||
|
||||
public class SongQuery
|
||||
{
|
||||
[JsonPropertyName("recordings")]
|
||||
public List<SongQueryRecording> Recordings {get;set;}=new List<SongQueryRecording>();
|
||||
|
||||
public class SongQueryRecording
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public Guid Id {get;set;}
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Name {get;set;}="";
|
||||
|
||||
[JsonPropertyName("length")]
|
||||
public long Length {get;set;}
|
||||
|
||||
[JsonPropertyName("artist-credit")]
|
||||
public List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists> ContributingArtists {get;set;}=new List<AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording.AlbumSongQueryMediaTrackRecordingArtists>();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Tesses.WebServer;
|
||||
using Scriban;
|
||||
using System.Web;
|
||||
using YoutubeExplode.Search;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
|
||||
|
||||
namespace Tesses.YouTubeDownloader2025Companion;
|
||||
|
||||
public static partial class Pages {
|
||||
|
||||
private static ReloadingScript fix_song_index = new ReloadingScript("wwwroot/components/fix-song-index.sbnhtml");
|
||||
public static string FixSongIndex(string songId, string query)
|
||||
{
|
||||
|
||||
return fix_song_index.Render(new {
|
||||
Songid = HttpUtility.HtmlAttributeEncode(songId),
|
||||
Searchurl = query == "" ? "" : HttpUtility.HtmlAttributeEncode($"/search_dialog?q={HttpUtility.UrlEncode(query)}"),
|
||||
Songname = query == "" ? "" : HttpUtility.HtmlEncode(query)
|
||||
});
|
||||
}
|
||||
private static ReloadingScript index_page = new ReloadingScript("wwwroot/pages/index.sbnhtml");
|
||||
|
||||
public static string Index(ServerContext ctx, AuthConfig cfg, Song? song, List<Artist> artists, string username)
|
||||
{
|
||||
string guid="";
|
||||
|
||||
StringBuilder builder=new();
|
||||
|
||||
if(song is not null)
|
||||
{
|
||||
guid = song.SongId;
|
||||
int i = 0;
|
||||
foreach (var artist in artists)
|
||||
{
|
||||
if (i == 0)
|
||||
builder.Append($"{artist.ArtistName} - {song.SongName}");
|
||||
else if (i == 1)
|
||||
builder.Append($" feat {artist.ArtistName}");
|
||||
else
|
||||
builder.Append($", {artist.ArtistName}");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return Components.Shell("Home", index_page.Render(new
|
||||
{
|
||||
Videotitle = Program.VideoTitle,
|
||||
Channeltitle = Program.ChannelTitle,
|
||||
Tytdurl = cfg.TYTDUrl.TrimEnd('/'),
|
||||
Needsattention = song is not null,
|
||||
Songtitle = HttpUtility.HtmlEncode(builder.ToString()),
|
||||
Fixsong = HttpUtility.HtmlAttributeEncode($"/fix-song-index?id={HttpUtility.UrlEncode(guid)}"),
|
||||
Username = HttpUtility.HtmlEncode(username)
|
||||
}),Program.DefaultLinks(0));
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ public static partial class Pages {
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/log?page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/log?page={page + 1}")
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(4));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Name = HttpUtility.HtmlEncode(name),
|
||||
Songs = songLinks,
|
||||
Addvals = HttpUtility.HtmlAttributeEncode(JsonConvert.SerializeObject(new{id}))
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ using Scriban;
|
||||
using System.Web;
|
||||
namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
public class AlbumLink(string name, string id, string thumbnail) {
|
||||
public class AlbumLink(string name, string id) {
|
||||
public string Albumname => HttpUtility.HtmlEncode(name);
|
||||
public string Albumurl => HttpUtility.HtmlAttributeEncode($"/music/album?id={HttpUtility.UrlEncode(id)}");
|
||||
public string Imgurl => HttpUtility.HtmlAttributeEncode(thumbnail);
|
||||
public string Imgurl => HttpUtility.HtmlAttributeEncode($"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(id)}");
|
||||
}
|
||||
|
||||
public static partial class Pages {
|
||||
@@ -21,7 +21,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/albums?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
|
||||
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/music/artist?id={HttpUtility.UrlEncode(id.ToString())}&page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/artist?id={HttpUtility.UrlEncode(id.ToString())}&page={page + 1}")
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/music/artists?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/artists?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Hasanysongs = hasAny,
|
||||
Zipurl = HttpUtility.HtmlAttributeEncode($"/api/v1/album.zip?albumId={HttpUtility.UrlEncode(id)}"),
|
||||
Artists = artists
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/myalbums?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
|
||||
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/music/myartist?q={HttpUtility.UrlEncode(query)}&id={HttpUtility.UrlEncode(id.ToString())}&page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/myartist?q={HttpUtility.UrlEncode(query)}&id={HttpUtility.UrlEncode(id.ToString())}&page={page + 1}")
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/music/myartists?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/myartists?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Tytdurl = HttpUtility.HtmlAttributeEncode($"{tytdEndpoint.TrimEnd('/')}/watch?v={HttpUtility.UrlEncode(video.VideoId)}"),
|
||||
Dlbtn = downloaded ? $"<a class=\"button\" href=\"/api/v1/song?albumId={HttpUtility.UrlEncode(album.AlbumId)}&songId={HttpUtility.UrlEncode(song.SongId)}\"><i>download</i> Download</a>" : string.IsNullOrEmpty(video.VideoId) ? "<span><i>file_download_off</i> Unavailable</span>" : "<span><i>downloading</i> Downloading</span>",
|
||||
Hasvideo = !string.IsNullOrEmpty(video.VideoId)
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Tesses.WebServer;
|
||||
using Scriban;
|
||||
using System.Web;
|
||||
using System.Text;
|
||||
namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
public static partial class Pages {
|
||||
|
||||
private static ReloadingScript mysong_albums = new ReloadingScript("wwwroot/pages/music/mysongs/song-albums.sbnhtml");
|
||||
|
||||
public static string MySongAlbums(List<MediaEntry> entries, Song song, List<Artist> artists)
|
||||
{
|
||||
StringBuilder builder=new StringBuilder();
|
||||
if(artists.Count > 0)
|
||||
builder.Append($"{artists[0].ArtistName} - ");
|
||||
builder.Append(song.SongName);
|
||||
|
||||
if(artists.Count > 1)
|
||||
builder.Append($" feat. {string.Join(",",artists.Skip(1).Select(e=>e.ArtistName))}");
|
||||
|
||||
|
||||
return Components.Shell($"{builder}'s Albums", mysong_albums.Render(new
|
||||
{
|
||||
Songname = builder.ToString(),
|
||||
Albums = entries.Select(e =>
|
||||
{
|
||||
return new
|
||||
{
|
||||
Albumurl = $"/music/myalbum?id={HttpUtility.UrlEncode(e.AlbumId)}",
|
||||
Albumname = HttpUtility.HtmlEncode(e.AlbumName),
|
||||
Url = $"/music/mysong?albumId={HttpUtility.UrlEncode(e.AlbumId)}&songId={song.SongId}",
|
||||
Imgurl = $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(e.AlbumId)}"
|
||||
};
|
||||
})
|
||||
}), Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/music/mysongs?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/mysongs?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
|
||||
}),Program.DefaultLinks());
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Tesses.WebServer;
|
||||
using Scriban;
|
||||
using System.Web;
|
||||
namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
public class SongLink(string name, string id) {
|
||||
public string Name => HttpUtility.HtmlEncode(name);
|
||||
public string Url => HttpUtility.HtmlAttributeEncode($"/music/song?id={HttpUtility.UrlEncode(id)}");
|
||||
|
||||
}
|
||||
|
||||
public static partial class Pages {
|
||||
|
||||
private static ReloadingScript song_listing = new ReloadingScript("wwwroot/pages/music/songs/song-list.sbnhtml");
|
||||
public static string SongListing(string query, List<SongLink> links, int page)
|
||||
{
|
||||
return Components.Shell($"Song search {query}", song_listing.Render(new {
|
||||
Songs = links,
|
||||
Q = HttpUtility.HtmlAttributeEncode(query),
|
||||
Prev = HttpUtility.HtmlAttributeEncode($"/music/songs?q={HttpUtility.UrlEncode(query)}&page={(page == 1 ? 1 : page - 1)}"),
|
||||
Current = page,
|
||||
Next = HttpUtility.HtmlAttributeEncode($"/music/songs?q={HttpUtility.UrlEncode(query)}&page={page + 1}")
|
||||
}),Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Tesses.WebServer;
|
||||
using Scriban;
|
||||
using System.Web;
|
||||
using System.Text;
|
||||
namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
public static partial class Pages {
|
||||
|
||||
private static ReloadingScript song_albums = new ReloadingScript("wwwroot/pages/music/songs/song-albums.sbnhtml");
|
||||
|
||||
public static string SongAlbums(string songName, List<AlbumLink> albums)
|
||||
{
|
||||
return Components.Shell($"{songName}'s Albums", song_albums.Render(new
|
||||
{
|
||||
Songname = songName,
|
||||
Albums = albums
|
||||
}), Program.DefaultLinks(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -132,7 +132,7 @@ public static partial class Pages {
|
||||
body = await Program.SearchResultHandler.SearchAsync(q,false);
|
||||
}
|
||||
|
||||
return Components.Shell($"Search {q}",search_page.Render(new {body,q = HttpUtility.HtmlAttributeEncode(q)}),Program.DefaultLinks());
|
||||
return Components.Shell($"Search {q}",search_page.Render(new {body,q = HttpUtility.HtmlAttributeEncode(q)}),Program.DefaultLinks(1));
|
||||
}
|
||||
public static async Task<string> SearchDialogAsync(ServerContext ctx)
|
||||
{
|
||||
|
||||
+10
-1
@@ -16,7 +16,16 @@ public static partial class Pages {
|
||||
|
||||
public static string Settings(ServerContext ctx)
|
||||
{
|
||||
return Components.Shell("Settings",settings_page.Render(new{}),Program.DefaultLinks());
|
||||
bool tytdthumbnail = false;
|
||||
lock(Program.MyLockObject)
|
||||
{
|
||||
tytdthumbnail = Program.Config?.ThumbnailsThroughTYTD ?? false;
|
||||
}
|
||||
|
||||
return Components.Shell("Settings", settings_page.Render(new
|
||||
{
|
||||
Tytdthumbnail=tytdthumbnail
|
||||
}),Program.DefaultLinks(3));
|
||||
}
|
||||
|
||||
}
|
||||
+296
-34
@@ -16,14 +16,40 @@ using System.Text;
|
||||
using YoutubeExplode.Search;
|
||||
using YouTubeMusicAPI.Client;
|
||||
using YouTubeMusicAPI.Models.Search;
|
||||
using YouTubeMusicAPI.Pagination;
|
||||
|
||||
|
||||
|
||||
namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
public class Program
|
||||
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;
|
||||
@@ -124,7 +150,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
private static long LastRequestMinute = 0;
|
||||
private static long RequestsInThisMinute = 0;
|
||||
|
||||
private const long RequestsPerMinute = 10;
|
||||
private const long RequestsPerMinute = 15;
|
||||
|
||||
private static bool InRateLimit = false;
|
||||
|
||||
@@ -243,7 +269,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
using (var db = new Database())
|
||||
{
|
||||
var res = await db.CreateSessionTokenAsync(val.Username);
|
||||
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, "/");
|
||||
@@ -251,6 +277,10 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}
|
||||
}
|
||||
}
|
||||
public static Database.Session? GetSession(this ServerContext ctx)
|
||||
{
|
||||
return ctx.Tag as Database.Session;
|
||||
}
|
||||
public static HttpActionAsync EnsureLoggedin(HttpActionAsync action)
|
||||
{
|
||||
return async (ctx) =>
|
||||
@@ -330,11 +360,13 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}
|
||||
};
|
||||
}
|
||||
public static IEnumerable<PageLink> DefaultLinks()
|
||||
public static IEnumerable<PageLink> DefaultLinks(int index)
|
||||
{
|
||||
yield return new PageLink("Home", "home", "/");
|
||||
yield return new PageLink("Search", "search", "/search");
|
||||
yield return new PageLink("Music", "music_note", "/music");
|
||||
yield return new PageLink("Home", "home", "/", 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
|
||||
{
|
||||
@@ -384,6 +416,13 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Console.WriteLine(JsonConvert.SerializeObject(webhook));
|
||||
switch (webhook.Type)
|
||||
{
|
||||
case "VideoStarted":
|
||||
{
|
||||
VideoTitle = webhook.Video.Title;
|
||||
|
||||
ChannelTitle = webhook.Video.Channel;
|
||||
}
|
||||
break;
|
||||
case "VideoFinished":
|
||||
{
|
||||
|
||||
@@ -426,6 +465,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}, "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) =>
|
||||
@@ -447,8 +487,21 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}), "POST");
|
||||
routeServer.Add("/", EnsureLoggedin(async (ctx) =>
|
||||
{
|
||||
var session = ctx.GetSession();
|
||||
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()));
|
||||
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) =>
|
||||
@@ -470,11 +523,11 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
|
||||
using (var db = new Database())
|
||||
{
|
||||
var session = await db.CreateSessionTokenAsync(req.username, false);
|
||||
var session = await db.CreateSessionTokenAsync(req.username, myresp.flags, false);
|
||||
await ctx.SendJsonAsync(new
|
||||
{
|
||||
success = true,
|
||||
token = session.Key
|
||||
token = session.Key,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -487,6 +540,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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"), []));
|
||||
@@ -527,12 +581,14 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
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;
|
||||
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)
|
||||
@@ -579,9 +635,14 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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));
|
||||
@@ -601,6 +662,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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;
|
||||
@@ -618,7 +680,6 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(10000);
|
||||
Console.WriteLine("Wait expired");
|
||||
await InstallExtension();
|
||||
}).Wait(0);
|
||||
routeServer.StartServer(4255);
|
||||
@@ -626,6 +687,23 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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))
|
||||
@@ -689,6 +767,22 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
|
||||
}
|
||||
}
|
||||
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))
|
||||
@@ -836,7 +930,34 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}
|
||||
private static async Task SettingsAsync(ServerContext ctx)
|
||||
{
|
||||
await ctx.SendTextAsync(Pages.Settings(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)
|
||||
@@ -878,6 +999,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static async Task<VideoId?> GetVideoIdFromSong(AlbumSongQuery.AlbumSongQueryMedia.AlbumSongQueryMediaTrack.AlbumSongQueryMediaTrackRecording recording, string albumName)
|
||||
{
|
||||
using (var db = new Database())
|
||||
@@ -886,19 +1008,9 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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))
|
||||
await foreach (var item in YouTubeMusicClient.SearchAsync(recording.ContributingArtists.GetName(recording.Name), YouTubeMusicAPI.Models.Search.SearchCategory.Songs))
|
||||
{
|
||||
if (item is SongSearchResult ssr)
|
||||
{
|
||||
@@ -1259,7 +1371,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}
|
||||
public static async Task Music(ServerContext ctx)
|
||||
{
|
||||
await ctx.SendTextAsync(Components.Shell("Music", await File.ReadAllTextAsync("wwwroot/music.html"), DefaultLinks()));
|
||||
await ctx.SendTextAsync(Components.Shell("Music", await File.ReadAllTextAsync("wwwroot/music.html"), DefaultLinks(2)));
|
||||
}
|
||||
|
||||
public static async Task MyArtist(ServerContext ctx)
|
||||
@@ -1296,7 +1408,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
foreach (var item in albumQuery.Albums)
|
||||
{
|
||||
links.Add(new AlbumLink(item.Name, item.Id.ToString(), $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
|
||||
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1309,6 +1421,40 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}
|
||||
*/
|
||||
}
|
||||
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.AudioOnly);
|
||||
//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();
|
||||
@@ -1342,6 +1488,35 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
}
|
||||
|
||||
}
|
||||
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))
|
||||
@@ -1397,7 +1572,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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());
|
||||
html = Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks(2));
|
||||
}
|
||||
|
||||
await ctx.SendTextAsync(html);
|
||||
@@ -1405,22 +1580,41 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
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)
|
||||
{
|
||||
ctx.SendText(Components.Shell("Song not found", "<h1>Song not found</h1>", DefaultLinks()));
|
||||
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)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1506,6 +1700,74 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -1645,7 +1907,7 @@ namespace Tesses.YouTubeDownloader2025Companion
|
||||
{
|
||||
foreach (var item in albumQuery.Albums)
|
||||
{
|
||||
links.Add(new AlbumLink(item.Name, item.Id.ToString(), $"/api/v1/album-thumbnail?id={HttpUtility.UrlEncode(item.Id.ToString())}"));
|
||||
links.Add(new AlbumLink(item.Name, item.Id.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class PluginServer
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if(ctx.Path == "/artist-thumbnail")
|
||||
else if(ctx.Path == "/album-thumbnail")
|
||||
{
|
||||
const id = ctx.QueryParams.TryGetFirst("id");
|
||||
if(TypeIsString(id))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<article class="primary-container" id="songattention">
|
||||
<form hx-post="/fix-song-index" hx-target="body">
|
||||
<h6>{{songname}}</h6>
|
||||
<input type="hidden" name="songId" value="{{songid}}">
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="text" name="url" id='url'>
|
||||
<label>Video Url</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min">
|
||||
{{if !string.empty(searchurl) }}
|
||||
<button hx-get="{{searchurl}}" hx-swap="innerHTML" hx-target="#search-dialog-results" type="button" hx-on::after-request="ui('#search-dialog')">Browse</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<button>OK</button>
|
||||
<button hx-get="/" hx-target="body">Cancel</button>
|
||||
</nav>
|
||||
</form>
|
||||
</article>
|
||||
@@ -22,7 +22,12 @@
|
||||
<div class="space"></div>
|
||||
<ul class="list">
|
||||
{{for item in pages}}
|
||||
{{if item.active }}
|
||||
<li class="wave round primary-container" hx-target="body" hx-push-url="true" hx-get="{{item.url}}">
|
||||
|
||||
{{else}}
|
||||
<li class="wave round" hx-target="body" hx-push-url="true" hx-get="{{item.url}}">
|
||||
{{end}}
|
||||
<i>{{item.icon}}</i>
|
||||
<span class="max">{{item.title}}</span>
|
||||
<b>{{item.badge}}</b>
|
||||
@@ -36,20 +41,9 @@
|
||||
<button hx-on:click="ui('#leftpane')" class="circle transparent">
|
||||
<i>menu</i>
|
||||
</button>
|
||||
<div class="max"></div>
|
||||
<div>
|
||||
<button class="circle transparent">
|
||||
<i>more_vert</i>
|
||||
</button>
|
||||
<menu class="left no-wrap">
|
||||
<li hx-target="body" hx-push-url="true" hx-get="/settings">Settings</li>
|
||||
<li hx-target="body" hx-push-url="true" hx-get="/log">Log</li>
|
||||
</menu>
|
||||
</div>
|
||||
<h6 class="max">TYTD Companion</h6>
|
||||
|
||||
</nav>
|
||||
<div class="small-padding">
|
||||
<h5>TYTD Companion</h5>
|
||||
</div>
|
||||
</header>
|
||||
<main class="responsive">
|
||||
{{body}}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="min">
|
||||
<button hx-post="/rescan">
|
||||
<i>sync</i>
|
||||
Rescan TYTD for Music
|
||||
</button>
|
||||
</div>
|
||||
<div class="min">
|
||||
<a class="button" href="@TYTD_URL@"><i>exit_to_app</i> TYTD2025 Downloader</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<article class="primary-container">
|
||||
<h5>Logged in as {{username}}</h5>
|
||||
</article>
|
||||
<article class="primary-container">
|
||||
<h3>{{videotitle}}</h3>
|
||||
<h5>{{channeltitle}}</h5>
|
||||
</article>
|
||||
<article class="primary-container">
|
||||
<h3>Actions</h3>
|
||||
<ul class="list border">
|
||||
<li>
|
||||
<a hx-post="/rescan">
|
||||
<i>sync</i>
|
||||
Rescan TYTD for Music
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{tytdurl}}"><i>exit_to_app</i> TYTD2025 Downloader</a>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="primary-container" id="songattention">
|
||||
{{if needsattention}}
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<h6>No YouTube Url found for</h6>
|
||||
<span>{{songtitle}}</span>
|
||||
</div>
|
||||
<div class="min">
|
||||
<button hx-swap="outerHTML" hx-target="#songattention" hx-get="{{fixsong}}" class="border round secondary">Set YouTube Url</button>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
There are no songs that need your attention
|
||||
{{end}}
|
||||
</article>
|
||||
@@ -0,0 +1,15 @@
|
||||
<h1>{{songname }}'s albums</h1>
|
||||
|
||||
{{for album in albums}}
|
||||
<div class="row">
|
||||
<div class="min">
|
||||
<img width="120" height="120" src="{{album.imgurl}}" alt="album thumbnail">
|
||||
</div>
|
||||
<div class="max">
|
||||
<a href="{{album.albumurl}}">{{album.albumname}}</a>
|
||||
</div>
|
||||
<div class="min">
|
||||
<a href="{{album.url}}">View Song</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,12 @@
|
||||
<h1>{{songname }}'s albums</h1>
|
||||
|
||||
{{for album in albums}}
|
||||
<div class="row">
|
||||
<div class="min">
|
||||
<img width="120" height="120" src="{{album.imgurl}}" alt="album thumbnail">
|
||||
</div>
|
||||
<div class="max">
|
||||
<a href="{{album.albumurl}}">{{album.albumname}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,31 @@
|
||||
<form hx-target="body" hx-push-url="true" hx-get="/music/songs">
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<div class="field label border">
|
||||
<input type="text" name="q" value="{{q}}">
|
||||
<label>Search for songs</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min">
|
||||
<button>Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ul class="list border">
|
||||
{{for song in songs}}
|
||||
<li><a href="{{song.url}}">{{song.name}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
</div>
|
||||
<div class="min">
|
||||
<button class="round" hx-target="body" hx-push-url="true" hx-get="{{prev}}"><i>arrow_back</i></button>
|
||||
<span>{{current}}</span>
|
||||
<button class="round" hx-target="body" hx-push-url="true" hx-get="{{next}}"><i>arrow_forward</i></button>
|
||||
</div>
|
||||
<div class="max">
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,6 +16,18 @@
|
||||
<input type="password" name="pass">
|
||||
<label>TYTD Password</label>
|
||||
</div>
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Thumbnail Passthrough</h6>
|
||||
<div>Use TYTD For Downloading Thumbnails (Slower, but there if you want)</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="tytdthumbnail">
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<br>
|
||||
<button>Finish</button>
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<article class="primary-container">
|
||||
<form method="POST" action="/settings">
|
||||
<div class="field middle-align">
|
||||
<nav>
|
||||
<div class="max">
|
||||
<h6>Thumbnail Passthrough</h6>
|
||||
<div>Use TYTD For Downloading Thumbnails (Slower, but there if you want)</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
{{if tytdthumbnail}}
|
||||
<input type="checkbox" name="tytdthumbnail" checked>
|
||||
{{else}}
|
||||
<input type="checkbox" name="tytdthumbnail">
|
||||
{{end}}
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
<br>
|
||||
<button>Save</button>
|
||||
</form>
|
||||
|
||||
|
||||
</article>
|
||||
|
||||
<article class="primary-container" id="return-to-setup">
|
||||
<div class="row">
|
||||
<div class="max">
|
||||
<h4>
|
||||
Return to Initial Setup
|
||||
</h4>
|
||||
<p>
|
||||
<b>WARNING</b>: Please be quick if publicly exposed to the internet as the companion will lose it's security
|
||||
</p>
|
||||
</div>
|
||||
<div class="min">
|
||||
<button hx-swap="outerHTML" hx-get="/return-to-setup.html" hx-target="#return-to-setup">Return To Setup</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,4 @@
|
||||
<article class="primary-container" id="return-to-setup">
|
||||
<h4>Are you sure you want to return to initial setup?</h4>
|
||||
<button hx-target="body" hx-post="/return-to-setup">Yes</button> <button hx-target="body" hx-get="/settings">No</button>
|
||||
</article>
|
||||
Reference in New Issue
Block a user