From 1052b94a544b3669644945f4efcbc7fa489bbd59 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Tue, 28 Apr 2026 03:21:01 -0500 Subject: [PATCH] Make it more complete --- Components/Shell.cs | 8 +- Database.cs | 19 +- Models/AlbumQuery.cs | 3 + Models/MusicBrainzSongInfo.cs | 17 + Models/SongQuery.cs | 27 ++ Pages/Index.cs | 61 ++++ Pages/Log.cs | 2 +- Pages/Music/Albums/AlbumInfo.cs | 2 +- Pages/Music/Albums/AlbumListing.cs | 6 +- Pages/Music/Artists/ArtistAlbumListing.cs | 2 +- Pages/Music/Artists/ArtistListing.cs | 2 +- Pages/Music/MyAlbums/MyAlbumInfo.cs | 2 +- Pages/Music/MyAlbums/MyAlbumListing.cs | 2 +- Pages/Music/MyArtists/MyArtistAlbumListing.cs | 2 +- Pages/Music/MyArtists/MyArtistListing.cs | 2 +- Pages/Music/MySongs/MySong.cs | 2 +- Pages/Music/MySongs/MySongSelectAlbum.cs | 38 ++ Pages/Music/MySongs/MySongsListing.cs | 2 +- Pages/Music/Songs/SongListing.cs | 26 ++ Pages/Music/Songs/SongSelectAlbum.cs | 20 ++ Pages/Search.cs | 2 +- Pages/Settings.cs | 11 +- Program.cs | 330 ++++++++++++++++-- tytdplugin/src/server.tcross | 2 +- wwwroot/components/fix-song-index.sbnhtml | 23 ++ wwwroot/components/shell.sbnhtml | 20 +- wwwroot/index.html | 11 - wwwroot/pages/index.sbnhtml | 37 ++ .../pages/music/mysongs/song-albums.sbnhtml | 15 + wwwroot/pages/music/songs/song-albums.sbnhtml | 12 + wwwroot/pages/music/songs/song-list.sbnhtml | 31 ++ wwwroot/pages/oobe.html | 12 + wwwroot/pages/settings.sbnhtml | 39 +++ wwwroot/return-to-setup.html | 4 + 34 files changed, 712 insertions(+), 82 deletions(-) create mode 100644 Models/MusicBrainzSongInfo.cs create mode 100644 Models/SongQuery.cs create mode 100644 Pages/Index.cs create mode 100644 Pages/Music/Songs/SongListing.cs create mode 100644 Pages/Music/Songs/SongSelectAlbum.cs create mode 100644 wwwroot/components/fix-song-index.sbnhtml delete mode 100644 wwwroot/index.html create mode 100644 wwwroot/pages/index.sbnhtml create mode 100644 wwwroot/pages/music/mysongs/song-albums.sbnhtml create mode 100644 wwwroot/pages/music/songs/song-albums.sbnhtml create mode 100644 wwwroot/pages/music/songs/song-list.sbnhtml create mode 100644 wwwroot/return-to-setup.html diff --git a/Components/Shell.cs b/Components/Shell.cs index 9acd4ac..4d26084 100644 --- a/Components/Shell.cs +++ b/Components/Shell.cs @@ -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(); - } } } \ No newline at end of file diff --git a/Database.cs b/Database.cs index d9a681a..487ec66 100644 --- a/Database.cs +++ b/Database.cs @@ -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 CreateSessionTokenAsync(string username, bool expires = true) + public async Task 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> 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("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> GetAlbumsAsync(string q = "", long offset = 0, long count = 20) { return await conn.QueryAsync("SELECT * FROM albums WHERE AlbumName LIKE @Query ORDER BY AlbumReleaseDate, Id ASC LIMIT @Count OFFSET @Offset;", new DynamicParameters(new @@ -323,6 +334,8 @@ namespace Tesses.YouTubeDownloader2025Companion public string Key { get; set; } = ""; + public long Flags {get;set;} + public long Expires { get; set; } } } diff --git a/Models/AlbumQuery.cs b/Models/AlbumQuery.cs index 475a618..7690d5b 100644 --- a/Models/AlbumQuery.cs +++ b/Models/AlbumQuery.cs @@ -15,5 +15,8 @@ public class AlbumQuery { [JsonPropertyName("title")] public string Name {get;set;}=""; + + [JsonPropertyName("date")] + public string Date {get;set;}=""; } } diff --git a/Models/MusicBrainzSongInfo.cs b/Models/MusicBrainzSongInfo.cs new file mode 100644 index 0000000..eac3e4b --- /dev/null +++ b/Models/MusicBrainzSongInfo.cs @@ -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 ContributingArtists {get;set;}=new List(); + + [JsonPropertyName("releases")] + public List Albums {get;set;}=new(); +} \ No newline at end of file diff --git a/Models/SongQuery.cs b/Models/SongQuery.cs new file mode 100644 index 0000000..485965e --- /dev/null +++ b/Models/SongQuery.cs @@ -0,0 +1,27 @@ + +using System.Text.Json.Serialization; + +namespace Tesses.YouTubeDownloader2025Companion; + + +public class SongQuery +{ + [JsonPropertyName("recordings")] + public List Recordings {get;set;}=new List(); + + 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 ContributingArtists {get;set;}=new List(); + + } +} \ No newline at end of file diff --git a/Pages/Index.cs b/Pages/Index.cs new file mode 100644 index 0000000..121b5a8 --- /dev/null +++ b/Pages/Index.cs @@ -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 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)); + } + +} \ No newline at end of file diff --git a/Pages/Log.cs b/Pages/Log.cs index 09fbc80..da9c7c2 100644 --- a/Pages/Log.cs +++ b/Pages/Log.cs @@ -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)); } } \ No newline at end of file diff --git a/Pages/Music/Albums/AlbumInfo.cs b/Pages/Music/Albums/AlbumInfo.cs index 1b59757..ab49425 100644 --- a/Pages/Music/Albums/AlbumInfo.cs +++ b/Pages/Music/Albums/AlbumInfo.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/Albums/AlbumListing.cs b/Pages/Music/Albums/AlbumListing.cs index 88147fe..c59c526 100644 --- a/Pages/Music/Albums/AlbumListing.cs +++ b/Pages/Music/Albums/AlbumListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/Artists/ArtistAlbumListing.cs b/Pages/Music/Artists/ArtistAlbumListing.cs index b6adc60..872e897 100644 --- a/Pages/Music/Artists/ArtistAlbumListing.cs +++ b/Pages/Music/Artists/ArtistAlbumListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/Artists/ArtistListing.cs b/Pages/Music/Artists/ArtistListing.cs index c6fbaee..6480022 100644 --- a/Pages/Music/Artists/ArtistListing.cs +++ b/Pages/Music/Artists/ArtistListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/MyAlbums/MyAlbumInfo.cs b/Pages/Music/MyAlbums/MyAlbumInfo.cs index e8efd2d..a20331f 100644 --- a/Pages/Music/MyAlbums/MyAlbumInfo.cs +++ b/Pages/Music/MyAlbums/MyAlbumInfo.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/MyAlbums/MyAlbumListing.cs b/Pages/Music/MyAlbums/MyAlbumListing.cs index 0275122..d345457 100644 --- a/Pages/Music/MyAlbums/MyAlbumListing.cs +++ b/Pages/Music/MyAlbums/MyAlbumListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/MyArtists/MyArtistAlbumListing.cs b/Pages/Music/MyArtists/MyArtistAlbumListing.cs index 41b527e..8451589 100644 --- a/Pages/Music/MyArtists/MyArtistAlbumListing.cs +++ b/Pages/Music/MyArtists/MyArtistAlbumListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/MyArtists/MyArtistListing.cs b/Pages/Music/MyArtists/MyArtistListing.cs index 50d6b93..f3cae53 100644 --- a/Pages/Music/MyArtists/MyArtistListing.cs +++ b/Pages/Music/MyArtists/MyArtistListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/MySongs/MySong.cs b/Pages/Music/MySongs/MySong.cs index 5593e9e..017978f 100644 --- a/Pages/Music/MySongs/MySong.cs +++ b/Pages/Music/MySongs/MySong.cs @@ -49,7 +49,7 @@ namespace Tesses.YouTubeDownloader2025Companion Tytdurl = HttpUtility.HtmlAttributeEncode($"{tytdEndpoint.TrimEnd('/')}/watch?v={HttpUtility.UrlEncode(video.VideoId)}"), Dlbtn = downloaded ? $"download Download" : string.IsNullOrEmpty(video.VideoId) ? "file_download_off Unavailable" : "downloading Downloading", Hasvideo = !string.IsNullOrEmpty(video.VideoId) - }),Program.DefaultLinks()); + }),Program.DefaultLinks(2)); } } } \ No newline at end of file diff --git a/Pages/Music/MySongs/MySongSelectAlbum.cs b/Pages/Music/MySongs/MySongSelectAlbum.cs index e69de29..235d1ae 100644 --- a/Pages/Music/MySongs/MySongSelectAlbum.cs +++ b/Pages/Music/MySongs/MySongSelectAlbum.cs @@ -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 entries, Song song, List 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)); + } + } +} \ No newline at end of file diff --git a/Pages/Music/MySongs/MySongsListing.cs b/Pages/Music/MySongs/MySongsListing.cs index a35819c..e545d3d 100644 --- a/Pages/Music/MySongs/MySongsListing.cs +++ b/Pages/Music/MySongs/MySongsListing.cs @@ -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)); } } } \ No newline at end of file diff --git a/Pages/Music/Songs/SongListing.cs b/Pages/Music/Songs/SongListing.cs new file mode 100644 index 0000000..eff9dfc --- /dev/null +++ b/Pages/Music/Songs/SongListing.cs @@ -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 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)); + } + } +} \ No newline at end of file diff --git a/Pages/Music/Songs/SongSelectAlbum.cs b/Pages/Music/Songs/SongSelectAlbum.cs new file mode 100644 index 0000000..e933d81 --- /dev/null +++ b/Pages/Music/Songs/SongSelectAlbum.cs @@ -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 albums) + { + return Components.Shell($"{songName}'s Albums", song_albums.Render(new + { + Songname = songName, + Albums = albums + }), Program.DefaultLinks(2)); + } + } +} \ No newline at end of file diff --git a/Pages/Search.cs b/Pages/Search.cs index 04a187d..8fce7b9 100644 --- a/Pages/Search.cs +++ b/Pages/Search.cs @@ -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 SearchDialogAsync(ServerContext ctx) { diff --git a/Pages/Settings.cs b/Pages/Settings.cs index da2dfba..6402eee 100644 --- a/Pages/Settings.cs +++ b/Pages/Settings.cs @@ -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)); } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 6df6eb5..bac0f34 100644 --- a/Program.cs +++ b/Program.cs @@ -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 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 DefaultLinks() + public static IEnumerable 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 artists=new List(); + 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","

You are unauthorized to do that

", 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 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","

You are unauthorized to enter settings

",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","

You are unauthorized to enter settings

",DefaultLinks(3))); } private static async Task AddAlbum(ServerContext ctx) @@ -878,6 +999,7 @@ namespace Tesses.YouTubeDownloader2025Companion } + public static async Task 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($"

{HttpUtility.HtmlEncode($"SongId: {songId}, AlbumId: {albumId}, Url: {url}")}

"); + + + 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", "

Song not found

", DefaultLinks()); + html = Components.Shell("Song not found", "

Song not found

", DefaultLinks(2)); } await ctx.SendTextAsync(html); @@ -1405,22 +1580,41 @@ namespace Tesses.YouTubeDownloader2025Companion else { List entries = new List(); + List artists=new List(); + 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", "

Song not found

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

Song not found

", 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", "

Song not found

", 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($"/ws/2/recording/{HttpUtility.UrlEncode(id.ToString())}/?fmt=json&inc=artist-credits+releases"); + if(songResp is not null) + { + + + + List links = new List(); + + 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 links = new List(); + string q = ""; + int page = 1; + List songs = new List(); + if (ctx.QueryParams.TryGetFirst("q", out q)) + { + + if (!ctx.QueryParams.TryGetFirstInt32("page", out page)) + page = 1; + + var songQuery = await MusicBrainzRequestAsync($"/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())); } } } diff --git a/tytdplugin/src/server.tcross b/tytdplugin/src/server.tcross index c7c063d..2d3fc29 100644 --- a/tytdplugin/src/server.tcross +++ b/tytdplugin/src/server.tcross @@ -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)) diff --git a/wwwroot/components/fix-song-index.sbnhtml b/wwwroot/components/fix-song-index.sbnhtml new file mode 100644 index 0000000..adb95c8 --- /dev/null +++ b/wwwroot/components/fix-song-index.sbnhtml @@ -0,0 +1,23 @@ +
+
+
{{songname}}
+ +
+
+
+ + +
+
+
+ {{if !string.empty(searchurl) }} + + {{end}} +
+
+ +
+
\ No newline at end of file diff --git a/wwwroot/components/shell.sbnhtml b/wwwroot/components/shell.sbnhtml index d328d09..c936b13 100644 --- a/wwwroot/components/shell.sbnhtml +++ b/wwwroot/components/shell.sbnhtml @@ -22,7 +22,12 @@
    {{for item in pages}} + {{if item.active }} +
  • + + {{else}}
  • + {{end}} {{item.icon}} {{item.title}} {{item.badge}} @@ -36,20 +41,9 @@ -
    -
    - - -
  • Settings
  • -
  • Log
  • -
    -
    +
    TYTD Companion
    + -
    -
    TYTD Companion
    -
    {{body}} diff --git a/wwwroot/index.html b/wwwroot/index.html deleted file mode 100644 index 30d87b1..0000000 --- a/wwwroot/index.html +++ /dev/null @@ -1,11 +0,0 @@ -
    -
    - -
    - -
    \ No newline at end of file diff --git a/wwwroot/pages/index.sbnhtml b/wwwroot/pages/index.sbnhtml new file mode 100644 index 0000000..991b8ab --- /dev/null +++ b/wwwroot/pages/index.sbnhtml @@ -0,0 +1,37 @@ +
    +
    Logged in as {{username}}
    +
    +
    +

    {{videotitle}}

    +
    {{channeltitle}}
    +
    + + +
    + {{if needsattention}} +
    +
    +
    No YouTube Url found for
    + {{songtitle}} +
    +
    + +
    +
    + {{else}} + There are no songs that need your attention + {{end}} +
    \ No newline at end of file diff --git a/wwwroot/pages/music/mysongs/song-albums.sbnhtml b/wwwroot/pages/music/mysongs/song-albums.sbnhtml new file mode 100644 index 0000000..47c47dd --- /dev/null +++ b/wwwroot/pages/music/mysongs/song-albums.sbnhtml @@ -0,0 +1,15 @@ +

    {{songname }}'s albums

    + +{{for album in albums}} +
    +
    + album thumbnail +
    + + +
    +{{end}} diff --git a/wwwroot/pages/music/songs/song-albums.sbnhtml b/wwwroot/pages/music/songs/song-albums.sbnhtml new file mode 100644 index 0000000..dce3641 --- /dev/null +++ b/wwwroot/pages/music/songs/song-albums.sbnhtml @@ -0,0 +1,12 @@ +

    {{songname }}'s albums

    + +{{for album in albums}} +
    +
    + album thumbnail +
    + +
    +{{end}} diff --git a/wwwroot/pages/music/songs/song-list.sbnhtml b/wwwroot/pages/music/songs/song-list.sbnhtml new file mode 100644 index 0000000..28fd30a --- /dev/null +++ b/wwwroot/pages/music/songs/song-list.sbnhtml @@ -0,0 +1,31 @@ +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    + +{{current}} + +
    +
    +
    +
    \ No newline at end of file diff --git a/wwwroot/pages/oobe.html b/wwwroot/pages/oobe.html index d043e72..25de5fc 100644 --- a/wwwroot/pages/oobe.html +++ b/wwwroot/pages/oobe.html @@ -16,6 +16,18 @@ +
    + +

    diff --git a/wwwroot/pages/settings.sbnhtml b/wwwroot/pages/settings.sbnhtml index e69de29..f6a53a1 100644 --- a/wwwroot/pages/settings.sbnhtml +++ b/wwwroot/pages/settings.sbnhtml @@ -0,0 +1,39 @@ +
    +
    +
    + +
    +
    + +
    + + +
    + +
    +
    +
    +

    + Return to Initial Setup +

    +

    + WARNING: Please be quick if publicly exposed to the internet as the companion will lose it's security +

    +
    +
    + +
    +
    \ No newline at end of file diff --git a/wwwroot/return-to-setup.html b/wwwroot/return-to-setup.html new file mode 100644 index 0000000..969a916 --- /dev/null +++ b/wwwroot/return-to-setup.html @@ -0,0 +1,4 @@ +
    +

    Are you sure you want to return to initial setup?

    + +
    \ No newline at end of file