Skip to content

Commit 097fcfd

Browse files
committed
displaying progress
1 parent 08bb283 commit 097fcfd

File tree

3 files changed

+86
-38
lines changed

3 files changed

+86
-38
lines changed

Gui/App.fs

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ module App =
8686
| Search of bool
8787
| SearchResult of VideoSearchResult
8888
| SearchProgress of BatchProgress
89+
| ProgressChanged of float
8990
| SearchCompleted
9091

9192
| AttachedToVisualTreeChanged of VisualTreeAttachmentEventArgs
@@ -169,10 +170,12 @@ module App =
169170
fun dispatch ->
170171
async {
171172
let command = mapToSearchCommand model
172-
CommandValidator.PrevalidateSearchCommand command
173173
let cacheFolder = Folder.GetPath Folders.cache
174174
let dataStore = JsonFileDataStore cacheFolder
175175
let youtube = Youtube(dataStore, VideoIndexRepository cacheFolder)
176+
let dispatchProgress = Cmd.debounce 100 (fun progress -> SearchProgress progress)
177+
command.SetProgressReporter(Progress<BatchProgress>(fun progress -> dispatchProgress progress |> List.iter (fun effect -> effect dispatch)))
178+
CommandValidator.PrevalidateSearchCommand command
176179
use cts = new CancellationTokenSource()
177180
do! CommandValidator.ValidateScopesAsync(command, youtube, dataStore, cts.Token) |> Async.AwaitTask
178181

@@ -308,7 +311,6 @@ module App =
308311

309312
| Search on -> { model with Searching = on; SearchResults = [] }, (if on then searchCmd model else Cmd.none)
310313
| SearchResult result -> { model with SearchResults = result::model.SearchResults }, Cmd.none
311-
| SearchCompleted -> { model with Searching = false }, Notify "search completed" |> Cmd.ofMsg
312314

313315
| SearchProgress progress ->
314316
let scopes = model.Scopes |> List.map(fun s ->
@@ -320,15 +322,20 @@ module App =
320322
| _ -> failwith "quark"
321323

322324
let scopeProgress = progress.VideoLists |> Seq.tryFind (fun pair -> equals s pair.Key) |> Option.map (fun pair -> pair.Value)
323-
if scopeProgress.IsSome then { s with Progress = scopeProgress } else s )
325+
if scopeProgress.IsSome
326+
then { s with Progress = scopeProgress }
327+
else s )
324328

325329
{ model with Scopes = scopes }, Cmd.none
326330

331+
| ProgressChanged _value -> model, Cmd.none
332+
| SearchCompleted -> { model with Searching = false }, notifyInfo model.Notifier "search completed"
333+
327334
| AttachedToVisualTreeChanged args ->
328335
let notifier = FabApplication.Current.WindowNotificationManager
329336
notifier.Position <- NotificationPosition.BottomRight
330337
{ model with Notifier = notifier }, Cmd.none
331-
338+
332339
| Notify title -> model, notifyInfo model.Notifier title
333340
| OpenUrl url -> model, (fun _ -> ShellCommands.OpenUri(url); Cmd.none)()
334341
| CopyingToClipboard _args -> model, Cmd.none
@@ -461,37 +468,45 @@ module App =
461468
Label "in"
462469

463470
for scope in model.Scopes do
464-
Label(displayScope scope.Type)
465-
466-
TextBox(scope.Aliases, fun value -> AliasesUpdated(scope, value))
467-
.watermark("by " + (if scope.Type = Scopes.videos then "space-separated IDs or URLs"
468-
elif scope.Type = Scopes.playlist then "ID or URL"
469-
else "handle, slug, user name, ID or URL"))
470-
471-
(HStack(5) {
472-
Label "search top"
473-
NumericUpDown(0, float UInt16.MaxValue, scope.Top, fun value -> TopChanged(scope, value))
474-
.formatString("F0")
475-
.tip(ToolTip("number of videos to search"))
476-
Label "videos"
477-
}).centerHorizontal().isVisible(scope.DisplaysSettings)
478-
479-
(HStack(5) {
480-
Label "and look for new ones after"
481-
NumericUpDown(0, float UInt16.MaxValue, scope.CacheHours, fun value -> CacheHoursChanged(scope, value))
482-
.formatString("F0")
483-
.tip(ToolTip("The info about which videos are in a playlist or channel is cached locally to speed up future searches."
484-
+ " This controls after how many hours such a cache is considered stale."
485-
+ Environment.NewLine + Environment.NewLine
486-
+ "Note that this doesn't concern the video data caches,"
487-
+ " which are not expected to change often and are stored until you explicitly clear them."))
488-
Label "hours"
489-
}).centerHorizontal().isVisible(scope.DisplaysSettings)
490-
491-
ToggleButton("", scope.DisplaysSettings, fun display -> DisplaySettingsChanged(scope, display))
492-
.tip(ToolTip("display settings"))
493-
494-
Button("", RemoveScope scope).tip(ToolTip("remove this scope"))
471+
VStack(5){
472+
HStack(5){
473+
Label(displayScope scope.Type)
474+
475+
TextBox(scope.Aliases, fun value -> AliasesUpdated(scope, value))
476+
.watermark("by " + (if scope.Type = Scopes.videos then "space-separated IDs or URLs"
477+
elif scope.Type = Scopes.playlist then "ID or URL"
478+
else "handle, slug, user name, ID or URL"))
479+
480+
(HStack(5) {
481+
Label "search top"
482+
NumericUpDown(0, float UInt16.MaxValue, scope.Top, fun value -> TopChanged(scope, value))
483+
.formatString("F0")
484+
.tip(ToolTip("number of videos to search"))
485+
Label "videos"
486+
}).centerHorizontal().isVisible(scope.DisplaysSettings)
487+
488+
(HStack(5) {
489+
Label "and look for new ones after"
490+
NumericUpDown(0, float UInt16.MaxValue, scope.CacheHours, fun value -> CacheHoursChanged(scope, value))
491+
.formatString("F0")
492+
.tip(ToolTip("The info about which videos are in a playlist or channel is cached locally to speed up future searches."
493+
+ " This controls after how many hours such a cache is considered stale."
494+
+ Environment.NewLine + Environment.NewLine
495+
+ "Note that this doesn't concern the video data caches,"
496+
+ " which are not expected to change often and are stored until you explicitly clear them."))
497+
Label "hours"
498+
}).centerHorizontal().isVisible(scope.DisplaysSettings)
499+
500+
ToggleButton("", scope.DisplaysSettings, fun display -> DisplaySettingsChanged(scope, display))
501+
.tip(ToolTip("display settings"))
502+
503+
Button("", RemoveScope scope).tip(ToolTip("remove this scope"))
504+
}
505+
506+
if scope.Progress.IsSome then
507+
ProgressBar(0, scope.Progress.Value.AllJobs, scope.Progress.Value.CompletedJobs, ProgressChanged)
508+
.progressTextFormat(scope.Progress.Value.ToString()).showProgressText(true)
509+
}
495510
}
496511

497512
HStack(5){

SubTubular/SearchProgress.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,42 @@
1-
namespace SubTubular;
1+
using SubTubular.Extensions;
2+
3+
namespace SubTubular;
24

35
/// <summary>Tracks the progress of distinct <see cref="CommandScope"/>s in an <see cref="OutputCommand"/>.</summary>
46
public sealed class BatchProgress
57
{
68
public Dictionary<CommandScope, VideoList> VideoLists { get; set; } = [];
79

10+
public override string ToString() =>
11+
VideoLists.Select(list => list.Key.Describe() + " " + list.Value).Join(Environment.NewLine);
12+
813
/// <summary>Represents the progress of a <see cref="CommandScope"/>.</summary>
914
public sealed class VideoList
1015
{
1116
public Status State { get; set; } = Status.queued;
1217

1318
/// <summary>Represents the progress of individual <see cref="Video.Id"/>s in a <see cref="CommandScope"/>s.</summary>
1419
public Dictionary<string, Status>? Videos { get; set; }
20+
21+
public int AllJobs => Videos?.Count ?? 1; // default to one job for the VideoList itself
22+
public int CompletedJobs => Videos?.Count(v => v.Value == Status.searched) ?? 0;
23+
24+
// used for display in UI
25+
public override string ToString()
26+
{
27+
var videos = Videos?.Where(v => v.Value != State).GroupBy(v => v.Value).Select(g => $"{Display(g.Key)} " + g.Count()).Join(" | ");
28+
return $"{Display(State)} {CompletedJobs}/{AllJobs}" + (videos.IsNullOrEmpty() ? null : (" - " + videos));
29+
}
1530
}
1631

1732
public enum Status { queued, loading, downloading, validated, refreshing, indexing, searching, indexingAndSearching, searched }
33+
34+
// used for display in UI
35+
private static string Display(Status status) => status switch
36+
{
37+
Status.indexingAndSearching => "indexing and searching",
38+
_ => status.ToString()
39+
};
1840
}
1941

2042
/// <summary>A <see cref="VideoListProgress"/> factory for the <see cref="BatchProgress.VideoLists"/> of <paramref name="batchProgress"/>

SubTubular/Youtube.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,17 @@ private async IAsyncEnumerable<VideoSearchResult> SearchPlaylistAsync(SearchComm
7070
var indexedVideoInfos = indexedVideoIds.ToDictionary(id => id, id => playlist.Videos[id]);
7171

7272
// search already indexed videos in one go - but on background task to start downloading and indexing videos in parallel
73-
searches.Add(index.SearchAsync(command, CreateVideoLookup(progress), indexedVideoInfos, UpdatePlaylistVideosUploaded, cancellation));
74-
progress?.Report(BatchProgress.Status.searching);
73+
searches.Add(SearchIndexedVideos());
74+
75+
async IAsyncEnumerable<VideoSearchResult> SearchIndexedVideos()
76+
{
77+
foreach (var videoId in indexedVideoIds) progress?.Report(videoId, BatchProgress.Status.searching);
78+
79+
await foreach (var result in index.SearchAsync(command, CreateVideoLookup(progress), indexedVideoInfos, UpdatePlaylistVideosUploaded, cancellation))
80+
yield return result;
81+
82+
foreach (var videoId in indexedVideoIds) progress?.Report(videoId, BatchProgress.Status.searched);
83+
}
7584
}
7685

7786
var unIndexedVideoIds = videoIds.Except(indexedVideoIds).ToArray();
@@ -80,7 +89,9 @@ private async IAsyncEnumerable<VideoSearchResult> SearchPlaylistAsync(SearchComm
8089
if (unIndexedVideoIds.Length > 0) searches.Add(SearchUnindexedVideos(command,
8190
unIndexedVideoIds, index, progress, cancellation, UpdatePlaylistVideosUploaded));
8291

92+
progress?.Report(BatchProgress.Status.searching);
8393
await foreach (var result in searches.Parallelize(cancellation)) yield return result;
94+
progress?.Report(BatchProgress.Status.searched);
8495

8596
async Task UpdatePlaylistVideosUploaded(IEnumerable<Video> videos)
8697
{

0 commit comments

Comments
 (0)