Defer errors when processing multiple queries (#660)
This commit is contained in:
parent
e4bfb708dc
commit
3a19ecb343
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user