Skip to content

Commit

Permalink
displaying progress
Browse files Browse the repository at this point in the history
  • Loading branch information
h0lg committed Oct 25, 2024
1 parent b94da22 commit 593590b
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 38 deletions.
24 changes: 23 additions & 1 deletion SubTubular/SearchProgress.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
namespace SubTubular;
using SubTubular.Extensions;

namespace SubTubular;

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

public override string ToString() =>
VideoLists.Select(list => list.Key.Describe() + " " + list.Value).Join(Environment.NewLine);

/// <summary>Represents the progress of a <see cref="CommandScope"/>.</summary>
public sealed class VideoList
{
public Status State { get; set; } = Status.queued;

/// <summary>Represents the progress of individual <see cref="Video.Id"/>s in a <see cref="CommandScope"/>s.</summary>
public Dictionary<string, Status>? Videos { get; set; }

public int AllJobs => Videos?.Count ?? 1; // default to one job for the VideoList itself
public int CompletedJobs => Videos?.Count(v => v.Value == Status.searched) ?? 0;

// used for display in UI
public override string ToString()
{
var videos = Videos?.Where(v => v.Value != State).GroupBy(v => v.Value).Select(g => $"{Display(g.Key)} " + g.Count()).Join(" | ");
return $"{Display(State)} {CompletedJobs}/{AllJobs}" + (videos.IsNullOrEmpty() ? null : (" - " + videos));
}
}

// used for display in UI
private static string Display(Status status) => status switch
{
Status.indexingAndSearching => "indexing and searching",
_ => status.ToString()
};

public enum Status { queued, loading, downloading, validated, refreshing, indexing, searching, indexingAndSearching, searched }
}

Expand Down
15 changes: 13 additions & 2 deletions SubTubular/Youtube.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,17 @@ private async IAsyncEnumerable<VideoSearchResult> SearchPlaylistAsync(SearchComm
var indexedVideoInfos = indexedVideoIds.ToDictionary(id => id, id => playlist.Videos[id]);

// search already indexed videos in one go - but on background task to start downloading and indexing videos in parallel
searches.Add(index.SearchAsync(command, CreateVideoLookup(progress), indexedVideoInfos, UpdatePlaylistVideosUploaded, cancellation));
progress?.Report(BatchProgress.Status.searching);
searches.Add(SearchIndexedVideos());

async IAsyncEnumerable<VideoSearchResult> SearchIndexedVideos()
{
foreach (var videoId in indexedVideoIds) progress?.Report(videoId, BatchProgress.Status.searching);

await foreach (var result in index.SearchAsync(command, CreateVideoLookup(progress), indexedVideoInfos, UpdatePlaylistVideosUploaded, cancellation))
yield return result;

foreach (var videoId in indexedVideoIds) progress?.Report(videoId, BatchProgress.Status.searched);
}
}

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

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

async Task UpdatePlaylistVideosUploaded(IEnumerable<Video> videos)
{
Expand Down
85 changes: 50 additions & 35 deletions Ui/App.fs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ module App =
| Search of bool
| SearchResult of VideoSearchResult
| SearchProgress of BatchProgress
| ProgressChanged of float
| SearchCompleted

| AttachedToVisualTreeChanged of VisualTreeAttachmentEventArgs
Expand Down Expand Up @@ -171,10 +172,12 @@ module App =
fun dispatch ->
async {
let command = mapToSearchCommand model
CommandValidator.PrevalidateSearchCommand command
let cacheFolder = Folder.GetPath Folders.cache
let dataStore = JsonFileDataStore cacheFolder
let youtube = Youtube(dataStore, VideoIndexRepository cacheFolder)
let dispatchProgress = Cmd.debounce 100 (fun progress -> SearchProgress progress)
command.SetProgressReporter(Progress<BatchProgress>(fun progress -> dispatchProgress progress |> List.iter (fun effect -> effect dispatch)))
CommandValidator.PrevalidateSearchCommand command
use cts = new CancellationTokenSource()
do! CommandValidator.ValidateScopesAsync(command, youtube, dataStore, cts.Token) |> Async.AwaitTask

Expand Down Expand Up @@ -310,7 +313,6 @@ module App =

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

| SearchProgress progress ->
let scopes = model.Scopes |> List.map(fun s ->
Expand All @@ -322,15 +324,20 @@ module App =
| _ -> failwith "quark"

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

{ model with Scopes = scopes }, Cmd.none

| ProgressChanged _value -> model, Cmd.none
| SearchCompleted -> { model with Searching = false }, Notify "search completed" |> Cmd.ofMsg

| AttachedToVisualTreeChanged args ->
let notifier = FabApplication.Current.WindowNotificationManager
notifier.Position <- NotificationPosition.BottomRight
{ model with Notifier = notifier }, Cmd.none

| Notify title -> model, notifyInfo model.Notifier title
| OpenUrl url -> model, (fun _ -> ShellCommands.OpenUri(url); Cmd.none)()
| CopyingToClipboard _args -> model, Cmd.none
Expand Down Expand Up @@ -474,37 +481,45 @@ module App =
Label "in"

for scope in model.Scopes do
Label(displayScope scope.Type)

TextBox(scope.Aliases, fun value -> AliasesUpdated(scope, value))
.watermark("by " + (if scope.Type = Scopes.videos then "space-separated IDs or URLs"
elif scope.Type = Scopes.playlist then "ID or URL"
else "handle, slug, user name, ID or URL"))

(HStack(5) {
Label "search top"
NumericUpDown(0, float UInt16.MaxValue, scope.Top, fun value -> TopChanged(scope, value))
.formatString("F0")
.tip(ToolTip("number of videos to search"))
Label "videos"
}).centerHorizontal().isVisible(scope.DisplaysSettings)

(HStack(5) {
Label "and look for new ones after"
NumericUpDown(0, float UInt16.MaxValue, scope.CacheHours, fun value -> CacheHoursChanged(scope, value))
.formatString("F0")
.tip(ToolTip("The info about which videos are in a playlist or channel is cached locally to speed up future searches."
+ " This controls after how many hours such a cache is considered stale."
+ Environment.NewLine + Environment.NewLine
+ "Note that this doesn't concern the video data caches,"
+ " which are not expected to change often and are stored until you explicitly clear them."))
Label "hours"
}).centerHorizontal().isVisible(scope.DisplaysSettings)

ToggleButton("", scope.DisplaysSettings, fun display -> DisplaySettingsChanged(scope, display))
.tip(ToolTip("display settings"))

Button("", RemoveScope scope).tip(ToolTip("remove this scope"))
VStack(5){
HStack(5){
Label(displayScope scope.Type)

TextBox(scope.Aliases, fun value -> AliasesUpdated(scope, value))
.watermark("by " + (if scope.Type = Scopes.videos then "space-separated IDs or URLs"
elif scope.Type = Scopes.playlist then "ID or URL"
else "handle, slug, user name, ID or URL"))

(HStack(5) {
Label "search top"
NumericUpDown(0, float UInt16.MaxValue, scope.Top, fun value -> TopChanged(scope, value))
.formatString("F0")
.tip(ToolTip("number of videos to search"))
Label "videos"
}).centerHorizontal().isVisible(scope.DisplaysSettings)

(HStack(5) {
Label "and look for new ones after"
NumericUpDown(0, float UInt16.MaxValue, scope.CacheHours, fun value -> CacheHoursChanged(scope, value))
.formatString("F0")
.tip(ToolTip("The info about which videos are in a playlist or channel is cached locally to speed up future searches."
+ " This controls after how many hours such a cache is considered stale."
+ Environment.NewLine + Environment.NewLine
+ "Note that this doesn't concern the video data caches,"
+ " which are not expected to change often and are stored until you explicitly clear them."))
Label "hours"
}).centerHorizontal().isVisible(scope.DisplaysSettings)

ToggleButton("", scope.DisplaysSettings, fun display -> DisplaySettingsChanged(scope, display))
.tip(ToolTip("display settings"))

Button("", RemoveScope scope).tip(ToolTip("remove this scope"))
}

if scope.Progress.IsSome then
ProgressBar(0, scope.Progress.Value.AllJobs, scope.Progress.Value.CompletedJobs, ProgressChanged)
.progressTextFormat(scope.Progress.Value.ToString()).showProgressText(true)
}
}

HStack(5){
Expand Down

0 comments on commit 593590b

Please sign in to comment.