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.Linq;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Gress;
using YoutubeDownloader.Core.Utils; using YoutubeDownloader.Core.Utils;
using YoutubeExplode; using YoutubeExplode;
using YoutubeExplode.Channels; using YoutubeExplode.Channels;
@ -122,33 +120,4 @@ public class QueryResolver(IReadOnlyList<Cookie>? initialCookies = null)
?? await TryResolveChannelAsync(query, cancellationToken) ?? await TryResolveChannelAsync(query, cancellationToken)
?? await ResolveSearchAsync(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; using YoutubeExplode.Videos;
namespace YoutubeDownloader.Core.Resolving; 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;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -21,6 +22,7 @@ namespace YoutubeDownloader.ViewModels.Components;
public partial class DashboardViewModel : ViewModelBase public partial class DashboardViewModel : ViewModelBase
{ {
private readonly ViewModelManager _viewModelManager; private readonly ViewModelManager _viewModelManager;
private readonly SnackbarManager _snackbarManager;
private readonly DialogManager _dialogManager; private readonly DialogManager _dialogManager;
private readonly SettingsService _settingsService; private readonly SettingsService _settingsService;
@ -28,24 +30,15 @@ public partial class DashboardViewModel : ViewModelBase
private readonly ResizableSemaphore _downloadSemaphore = new(); private readonly ResizableSemaphore _downloadSemaphore = new();
private readonly AutoResetProgressMuxer _progressMuxer; 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( public DashboardViewModel(
ViewModelManager viewModelManager, ViewModelManager viewModelManager,
SnackbarManager snackbarManager,
DialogManager dialogManager, DialogManager dialogManager,
SettingsService settingsService SettingsService settingsService
) )
{ {
_viewModelManager = viewModelManager; _viewModelManager = viewModelManager;
_snackbarManager = snackbarManager;
_dialogManager = dialogManager; _dialogManager = dialogManager;
_settingsService = settingsService; _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 ProgressContainer<Percentage> Progress { get; } = new();
public ObservableCollection<DownloadViewModel> Downloads { get; } = [];
public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1; 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; private bool CanShowAuthSetup() => !IsBusy;
[RelayCommand(CanExecute = nameof(CanShowAuthSetup))] [RelayCommand(CanExecute = nameof(CanShowAuthSetup))]
@ -179,18 +183,41 @@ public partial class DashboardViewModel : ViewModelBase
var resolver = new QueryResolver(_settingsService.LastAuthCookies); var resolver = new QueryResolver(_settingsService.LastAuthCookies);
var downloader = new VideoDownloader(_settingsService.LastAuthCookies); var downloader = new VideoDownloader(_settingsService.LastAuthCookies);
var result = await resolver.ResolveAsync( // Split queries by newlines
Query.Split( var queries = Query.Split(
"\n", '\n',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
),
progress
); );
// Single video // Process individual queries
if (result.Videos.Count == 1) 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( var downloadOptions = await downloader.GetDownloadOptionsAsync(
video.Id, video.Id,
@ -209,14 +236,14 @@ public partial class DashboardViewModel : ViewModelBase
Query = ""; Query = "";
} }
// Multiple videos // Multiple videos
else if (result.Videos.Count > 1) else if (queryResult.Videos.Count > 1)
{ {
var downloads = await _dialogManager.ShowDialogAsync( var downloads = await _dialogManager.ShowDialogAsync(
_viewModelManager.CreateDownloadMultipleSetupViewModel( _viewModelManager.CreateDownloadMultipleSetupViewModel(
result.Title, queryResult.Title,
result.Videos, queryResult.Videos,
// Pre-select videos if they come from a single query and not from search // Pre-select videos if they come from a single query and not from search
result.Kind queryResult.Kind
is not QueryResultKind.Search is not QueryResultKind.Search
and not QueryResultKind.Aggregate and not QueryResultKind.Aggregate
) )