@inherits FeedbackComponent @using LANCommander.Server.Services.Abstractions @using LANCommander.Server.Services.Models @using LANCommander.SDK.Enums @inject IMediaGrabberService MediaGrabberService @inject IMessageService MessageService @inject ILogger Logger } else { } } } else { } @if (CanLoadMore(grabberGroup)) {
} } @if (Loading && _groupedResults.Any()) {
} @if (!_groupedResults.Any() && !Loading) { } @if (_isDownloading) {
@_downloadStatus
} @code { [Parameter] public string Search { get; set; } [Parameter] public MediaType Type { get; set; } MediaGrabberResult Media { get; set; } MediaGrabberResult _preview { get; set; } bool MultiSelect { get; set; } HashSet _selectedKeys = new(); double Size { get; set; } = 200; bool Loading { get; set; } = true; bool _isDownloading; double _downloadPercent; string _downloadStatus = "Downloading..."; CancellationTokenSource _searchCts; List _allResults = new(); List _groupedResults = new(); HashSet _seenKeys = new(); HashSet _pagingProviders = new(); Dictionary _groupPages = new(); HashSet _exhaustedGroups = new(); string _loadingGroupId; string _selectedProvider = string.Empty; string[] _availableProviders = []; protected override async Task OnFirstAfterRenderAsync() { Type = Options.Type; Search = Options.Search; MultiSelect = Options.MultiSelect; _availableProviders = new[] { "All" }.Concat(MediaGrabberService.GetGrabberNames()).ToArray(); _selectedProvider = _availableProviders.First(); _pagingProviders = MediaGrabberService.GetPagingGrabberNames().ToHashSet(); await GetResults(Type, Search); } private async Task OnProviderChanged(string provider) { _selectedProvider = provider; if (!string.IsNullOrWhiteSpace(Search)) await GetResults(Type, Search); } private async Task GetResults(MediaType type, string search) { _searchCts?.Cancel(); _searchCts = new CancellationTokenSource(); var token = _searchCts.Token; try { Loading = true; _preview = null; _allResults.Clear(); _groupedResults.Clear(); _seenKeys.Clear(); _selectedKeys.Clear(); _groupPages.Clear(); _exhaustedGroups.Clear(); _loadingGroupId = null; await InvokeAsync(StateHasChanged); if (String.IsNullOrWhiteSpace(search)) return; var grabberName = _selectedProvider == "All" ? null : _selectedProvider; await StreamResults(type, search, grabberName, 0, null, token); Loading = false; await InvokeAsync(StateHasChanged); } catch (OperationCanceledException) { } catch (Exception ex) { MessageService.Error("An unknown error occurred when trying to get media results"); Logger.LogError(ex, "An unknown error occurred when trying to get media results"); Loading = false; } } private string GroupId(GroupedResults group) => $"{group.GrabberName}|{group.Group}"; private bool CanLoadMore(GroupedResults group) => !Loading && group.Results.Any() && _pagingProviders.Contains(group.GrabberName) && !_exhaustedGroups.Contains(GroupId(group)); private async Task LoadMoreGroup(GroupedResults group) { if (Loading || _loadingGroupId != null || String.IsNullOrWhiteSpace(Search)) return; var groupId = GroupId(group); _searchCts ??= new CancellationTokenSource(); var token = _searchCts.Token; try { _loadingGroupId = groupId; await InvokeAsync(StateHasChanged); var nextPage = _groupPages.GetValueOrDefault(groupId) + 1; _groupPages[groupId] = nextPage; // Only query the provider this group came from, and keep only this game's results. var added = await StreamResults(Type, Search, group.GrabberName, nextPage, group.Group, token); if (added == 0) _exhaustedGroups.Add(groupId); _loadingGroupId = null; await InvokeAsync(StateHasChanged); } catch (OperationCanceledException) { } catch (Exception ex) { _loadingGroupId = null; MessageService.Error("An unknown error occurred when trying to get more media results"); Logger.LogError(ex, "An unknown error occurred when trying to get more media results"); } } private async Task StreamResults(MediaType type, string search, string? grabberName, int page, string? onlyGroup, CancellationToken token) { var added = 0; await foreach (var batch in MediaGrabberService.SearchStreamAsync(type, search, grabberName, page, token)) { if (token.IsCancellationRequested) break; foreach (var result in batch) { if (onlyGroup != null && result.Group != onlyGroup) continue; result.UniqueKey = $"{result.GrabberName}:{result.Id}"; if (!_seenKeys.Add(result.UniqueKey)) continue; _allResults.Add(result); added++; } RebuildGroups(); await InvokeAsync(StateHasChanged); } return added; } private void RebuildGroups() { _groupedResults = _allResults .GroupBy(r => new { r.GrabberName, r.Group }) .Select(g => new GroupedResults { GrabberName = g.Key.GrabberName, Group = g.Key.Group, Results = g.DistinctBy(r => r.Id).ToList() }) .ToList(); } private void OnResultSelected(MediaGrabberResult result) { Media = result; if (MultiSelect) { if (!_selectedKeys.Add(result.UniqueKey)) _selectedKeys.Remove(result.UniqueKey); } } private bool AllSelected(GroupedResults group) => group.Results.Count > 0 && group.Results.All(r => _selectedKeys.Contains(r.UniqueKey)); private async Task ToggleSelectAll(GroupedResults group) { var keys = group.Results.Select(r => r.UniqueKey).ToList(); if (keys.All(_selectedKeys.Contains)) { foreach (var key in keys) _selectedKeys.Remove(key); } else { foreach (var key in keys) _selectedKeys.Add(key); } await InvokeAsync(StateHasChanged); } private async Task OnResultPicked(MediaGrabberResult result) { Media = result; await DownloadAndClose(); } private void OnImageSelected(string uniqueKey) { Media = _allResults.FirstOrDefault(r => r.UniqueKey == uniqueKey); } private void OnMultiImageSelectionChanged(HashSet selectedKeys) { _selectedKeys = selectedKeys; } private async Task OnImagePicked(string uniqueKey) { if (MultiSelect) { if (!_selectedKeys.Add(uniqueKey)) _selectedKeys.Remove(uniqueKey); await InvokeAsync(StateHasChanged); } else { OnImageSelected(uniqueKey); await DownloadAndClose(); } } private void PreviewVideo(MediaGrabberResult result) { _preview = _preview?.Id == result.Id ? null : result; } private static bool IsYouTubeUrl(string url) => url.Contains("youtube.com/") || url.Contains("youtu.be/"); private static string GetYouTubeEmbedUrl(string url) { var uri = new Uri(url); var query = System.Web.HttpUtility.ParseQueryString(uri.Query); var videoId = query["v"] ?? uri.Segments.LastOrDefault(); return $"https://www.youtube.com/embed/{videoId}?autoplay=1"; } public override async Task OnFeedbackOkAsync(ModalClosingEventArgs args) { args.Cancel = true; await DownloadAndClose(); } public override async Task OnFeedbackCancelAsync(ModalClosingEventArgs args) { _preview = null; await base.OnFeedbackCancelAsync(args); } private async Task DownloadAndClose() { if (MultiSelect) { await DownloadMultipleAndClose(); return; } if (Media == null) return; _preview = null; _isDownloading = true; _downloadPercent = 0; _downloadStatus = "Downloading..."; await InvokeAsync(StateHasChanged); try { var progress = new Progress(p => { _downloadPercent = p.Percent ?? 0; _downloadStatus = p.Status ?? "Downloading..."; InvokeAsync(StateHasChanged); }); var download = await MediaGrabberService.DownloadAsync(Media, progress); var result = new MediaGrabberDownloadResult { Result = Media, Download = download }; await base.OkCancelRefWithResult!.OnOk(result); await CloseFeedbackAsync(); } catch (Exception ex) { _isDownloading = false; await InvokeAsync(StateHasChanged); MessageService.Error("Download failed. Please try again."); Logger.LogError(ex, "Failed to download media"); } } private async Task DownloadMultipleAndClose() { var selectedResults = _allResults.Where(r => _selectedKeys.Contains(r.UniqueKey)).ToList(); if (!selectedResults.Any()) return; _preview = null; _isDownloading = true; _downloadPercent = 0; await InvokeAsync(StateHasChanged); try { var downloadResult = new MediaGrabberDownloadResult(); for (int i = 0; i < selectedResults.Count; i++) { var selected = selectedResults[i]; _downloadStatus = $"Downloading {i + 1} of {selectedResults.Count}..."; _downloadPercent = 0; await InvokeAsync(StateHasChanged); var progress = new Progress(p => { _downloadPercent = p.Percent ?? 0; InvokeAsync(StateHasChanged); }); var download = await MediaGrabberService.DownloadAsync(selected, progress); downloadResult.Results.Add(new MediaGrabberDownloadResultEntry { Result = selected, Download = download }); } await base.OkCancelRefWithResult!.OnOk(downloadResult); await CloseFeedbackAsync(); } catch (Exception ex) { _isDownloading = false; await InvokeAsync(StateHasChanged); MessageService.Error("Download failed. Please try again."); Logger.LogError(ex, "Failed to download media"); } } private class GroupedResults { public string GrabberName { get; set; } public string Group { get; set; } public List Results { get; set; } } }