// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Humanizer; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; namespace osu.Game.Database { public abstract class ModelDownloader : IModelDownloader where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable, T where T : class { public Action PostNotification { protected get; set; } public Action> DownloadBegan { get; set; } public Action> DownloadFailed { get; set; } private readonly IModelImporter importer; private readonly IAPIProvider api; protected readonly List> CurrentDownloads = new List>(); protected ModelDownloader(IModelImporter importer, IAPIProvider api, IIpcHost importHost = null) { this.importer = importer; this.api = api; } /// /// Creates the download request for this . /// /// The to be downloaded. /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. /// The request object. protected abstract ArchiveDownloadRequest CreateDownloadRequest(T model, bool minimiseDownloadSize); public bool Download(T model, bool minimiseDownloadSize = false) { if (!canDownload(model)) return false; var request = CreateDownloadRequest(model, minimiseDownloadSize); DownloadNotification notification = new DownloadNotification { Text = $"Downloading {request.Model}", }; request.DownloadProgressed += progress => { notification.State = ProgressNotificationState.Active; notification.Progress = progress; }; request.Success += filename => { Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. var imported = await importer.Import(notification, new ImportTask(filename)).ConfigureAwait(false); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) DownloadFailed?.Invoke(request); CurrentDownloads.Remove(request); }, TaskCreationOptions.LongRunning); }; request.Failure += triggerFailure; notification.CancelRequested += () => { request.Cancel(); return true; }; CurrentDownloads.Add(request); PostNotification?.Invoke(notification); api.PerformAsync(request); DownloadBegan?.Invoke(request); return true; void triggerFailure(Exception error) { CurrentDownloads.Remove(request); DownloadFailed?.Invoke(request); notification.State = ProgressNotificationState.Cancelled; if (!(error is OperationCanceledException)) Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!"); } } public abstract ArchiveDownloadRequest GetExistingDownload(T model); private bool canDownload(T model) => GetExistingDownload(model) == null && api != null; private class DownloadNotification : ProgressNotification { public override bool IsImportant => false; protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification { Activated = CompletionClickAction, Text = CompletionText }; private class SilencedProgressCompletionNotification : ProgressCompletionNotification { public override bool IsImportant => false; } } } }