using Tesses.WebServer; using Scriban; using System.Web; using YoutubeExplode.Search; using System.Collections.Concurrent; using System.Security.Cryptography; using Newtonsoft.Json; namespace Tesses.YouTubeDownloader2025Companion; public class SearchResultHandler : IEventable { private class SearchResultObj(IAsyncEnumerator> e,bool isDialog) { public long expires = Program.NowEpoch + (3600*6); public IAsyncEnumerator> enumerator = e; public bool isDialog = isDialog; } private ConcurrentDictionary ents=new ConcurrentDictionary(); public SearchResultHandler() { } private class SearchResult(string tytdUrl, string title, string id) { public string Url => HttpUtility.HtmlAttributeEncode($"{tytdUrl.TrimEnd('/')}/watch?v={id}"); public string Imgurl => HttpUtility.HtmlAttributeEncode($"/api/v1/video-thumbnail?v={id}"); public string Title => HttpUtility.HtmlEncode(title); public string Dialogok => HttpUtility.HtmlAttributeEncode($"searchDialogSubmit('{HttpUtility.JavaScriptStringEncode(id)}')"); public string Addbtnjson => HttpUtility.HtmlAttributeEncode(JsonConvert.SerializeObject(new{type="add",value=id})); } public async Task SearchAsync(string query,bool isDialog) { var obj = Program.YouTubeClient.Search.GetResultBatchesAsync(query, YoutubeExplode.Search.SearchFilter.Video); SearchResultObj ro = new SearchResultObj(obj.GetAsyncEnumerator(),isDialog); string id = RandomNumberGenerator.GetHexString(32); while(!ents.TryAdd(id,ro)) { id = RandomNumberGenerator.GetHexString(32); } return await MoreAsync(id,ro); } private static ReloadingScript results_component = new ReloadingScript("wwwroot/components/search-results.sbnhtml"); private static ReloadingScript results_dialog_component = new ReloadingScript("wwwroot/components/search-results-dialog.sbnhtml"); private async Task MoreAsync(string id,SearchResultObj o) { if(o.expires <= Program.NowEpoch) { return """

The search has expired

"""; } if(await o.enumerator.MoveNextAsync()) { var tytd = Program.Config ?? new AuthConfig(); List results = new List(); foreach(var ent in o.enumerator.Current.Items) if(ent is VideoSearchResult item) results.Add(new SearchResult(tytd.TYTDUrl, item.Title, item.Id)); return o.isDialog ? results_dialog_component.Render(new{results, moreurl = HttpUtility.HtmlAttributeEncode($"/search_more?id={id}")}) : results_component.Render(new { results, moreurl = HttpUtility.HtmlAttributeEncode($"/search_more?id={id}") }); } return """

There are no more results

"""; } public async Task MoreAsync(string id) { if(ents.TryGetValue(id, out var v)) { return await MoreAsync(id,v); } return """

There are no more results

"""; } public async Task EventAsync() { try { List expired = new List(); var time = Program.NowEpoch; foreach(var item in ents) { if(item.Value.expires <= time) expired.Add(item.Key); } foreach(var expire in expired) ents.Remove(expire,out var e); }catch{ } await Task.CompletedTask; } } public static partial class Components { public static async Task SearchMoreAsync(ServerContext ctx) { if(ctx.QueryParams.TryGetFirst("id",out var id)) { return await Program.SearchResultHandler.MoreAsync(id); } return """

No continuation id provided

"""; } } public static partial class Pages { private static ReloadingScript search_page = new ReloadingScript("wwwroot/pages/search.sbnhtml"); public static async Task SearchAsync(ServerContext ctx) { string q=""; string body = ""; if(ctx.QueryParams.TryGetFirst("q",out q)) { body = await Program.SearchResultHandler.SearchAsync(q,false); } return Components.Shell($"Search {q}",search_page.Render(new {body,q = HttpUtility.HtmlAttributeEncode(q)}),Program.DefaultLinks(1)); } public static async Task SearchDialogAsync(ServerContext ctx) { string q=""; string body = ""; if(ctx.QueryParams.TryGetFirst("q",out q)) { body = await Program.SearchResultHandler.SearchAsync(q,true); } return body; } }