Defer errors when processing multiple queries (#660)

This commit is contained in:
Oleksii Holub 2025-05-13 19:25:59 +03:00 committed by GitHub
parent e4bfb708dc
commit 3a19ecb343
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 78 additions and 60 deletions

View File

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Gress;
using YoutubeDownloader.Core.Utils;
using YoutubeExplode;
using YoutubeExplode.Channels;
@ -122,33 +120,4 @@ public class QueryResolver(IReadOnlyList<Cookie>? initialCookies = null)
?? await TryResolveChannelAsync(query, cancellationToken)
?? await ResolveSearchAsync(query, cancellationToken);
}
public async Task<QueryResult> ResolveAsync(
IReadOnlyList<string> queries,
IProgress<Percentage>? progress = null,
CancellationToken cancellationToken = default
)
{
if (queries.Count == 1)
return await ResolveAsync(queries.Single(), cancellationToken);
var videos = new List<IVideo>();
var videoIds = new HashSet<VideoId>();
var completed = 0;
foreach (var query in queries)
{
var result = await ResolveAsync(query, cancellationToken);
foreach (var video in result.Videos)
{
if (videoIds.Add(video.Id))
videos.Add(video);
}
progress?.Report(Percentage.FromFraction(1.0 * ++completed / queries.Count));
}
return new QueryResult(QueryResultKind.Aggregate, $"{queries.Count} queries", videos);
}
}

View File

@ -1,6 +1,28 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using YoutubeExplode.Videos;
namespace YoutubeDownloader.Core.Resolving;
public record QueryResult(QueryResultKind Kind, string Title, IReadOnlyList<IVideo> Videos);
public record QueryResult(QueryResultKind Kind, string Title, IReadOnlyList<IVideo> Videos)
{
public static QueryResult Aggregate(IReadOnlyList<QueryResult> results)
{
if (!results.Any())
throw new ArgumentException("Cannot aggregate empty results.", nameof(results));
return new QueryResult(
// Single query -> inherit kind, multiple queries -> aggregate
results.Count == 1
? results.Single().Kind
: QueryResultKind.Aggregate,
// Single query -> inherit title, multiple queries -> aggregate
results.Count == 1
? results.Single().Title
: $"{results.Count} queries",
// Combine all videos, deduplicate by ID
results.SelectMany(q => q.Videos).DistinctBy(v => v.Id).ToArray()
);
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
@ -21,6 +22,7 @@ namespace YoutubeDownloader.ViewModels.Components;
public partial class DashboardViewModel : ViewModelBase
{
private readonly ViewModelManager _viewModelManager;
private readonly SnackbarManager _snackbarManager;
private readonly DialogManager _dialogManager;
private readonly SettingsService _settingsService;
@ -28,24 +30,15 @@ public partial class DashboardViewModel : ViewModelBase
private readonly ResizableSemaphore _downloadSemaphore = new();
private readonly AutoResetProgressMuxer _progressMuxer;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsProgressIndeterminate))]
[NotifyCanExecuteChangedFor(nameof(ProcessQueryCommand))]
[NotifyCanExecuteChangedFor(nameof(ShowAuthSetupCommand))]
[NotifyCanExecuteChangedFor(nameof(ShowSettingsCommand))]
public partial bool IsBusy { get; set; }
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ProcessQueryCommand))]
public partial string? Query { get; set; }
public DashboardViewModel(
ViewModelManager viewModelManager,
SnackbarManager snackbarManager,
DialogManager dialogManager,
SettingsService settingsService
)
{
_viewModelManager = viewModelManager;
_snackbarManager = snackbarManager;
_dialogManager = dialogManager;
_settingsService = settingsService;
@ -67,12 +60,23 @@ public partial class DashboardViewModel : ViewModelBase
);
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsProgressIndeterminate))]
[NotifyCanExecuteChangedFor(nameof(ProcessQueryCommand))]
[NotifyCanExecuteChangedFor(nameof(ShowAuthSetupCommand))]
[NotifyCanExecuteChangedFor(nameof(ShowSettingsCommand))]
public partial bool IsBusy { get; set; }
public ProgressContainer<Percentage> Progress { get; } = new();
public ObservableCollection<DownloadViewModel> Downloads { get; } = [];
public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ProcessQueryCommand))]
public partial string? Query { get; set; }
public ObservableCollection<DownloadViewModel> Downloads { get; } = [];
private bool CanShowAuthSetup() => !IsBusy;
[RelayCommand(CanExecute = nameof(CanShowAuthSetup))]
@ -179,18 +183,41 @@ public partial class DashboardViewModel : ViewModelBase
var resolver = new QueryResolver(_settingsService.LastAuthCookies);
var downloader = new VideoDownloader(_settingsService.LastAuthCookies);
var result = await resolver.ResolveAsync(
Query.Split(
"\n",
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
),
progress
// Split queries by newlines
var queries = Query.Split(
'\n',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
);
// Single video
if (result.Videos.Count == 1)
// Process individual queries
var queryResults = new List<QueryResult>();
foreach (var (i, query) in queries.Index())
{
var video = result.Videos.Single();
try
{
queryResults.Add(await resolver.ResolveAsync(query));
}
// If it's not the only query in the list, don't interrupt the process
// and report the error via an async notification instead of a sync dialog.
// https://github.com/Tyrrrz/YoutubeDownloader/issues/563
catch (YoutubeExplodeException ex)
when (ex is VideoUnavailableException or PlaylistUnavailableException
&& queries.Length > 1
)
{
_snackbarManager.Notify(ex.Message);
}
progress.Report(Percentage.FromFraction((i + 1.0) / queries.Length));
}
// Aggregate results
var queryResult = QueryResult.Aggregate(queryResults);
// Single video result
if (queryResult.Videos.Count == 1)
{
var video = queryResult.Videos.Single();
var downloadOptions = await downloader.GetDownloadOptionsAsync(
video.Id,
@ -209,14 +236,14 @@ public partial class DashboardViewModel : ViewModelBase
Query = "";
}
// Multiple videos
else if (result.Videos.Count > 1)
else if (queryResult.Videos.Count > 1)
{
var downloads = await _dialogManager.ShowDialogAsync(
_viewModelManager.CreateDownloadMultipleSetupViewModel(
result.Title,
result.Videos,
queryResult.Title,
queryResult.Videos,
// Pre-select videos if they come from a single query and not from search
result.Kind
queryResult.Kind
is not QueryResultKind.Search
and not QueryResultKind.Aggregate
)