// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using Humanizer; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osu.Framework.Bindables; namespace osu.Game.Database { /// /// An that has the ability to download models using an and /// import them into the store. /// /// The model type. /// The associated file join type. public abstract class DownloadableArchiveModelManager : ArchiveModelManager, IModelDownloader where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable where TFileModel : class, INamedFileInfo, new() { public IBindable>> DownloadBegan => downloadBegan; private readonly Bindable>> downloadBegan = new Bindable>>(); public IBindable>> DownloadFailed => downloadFailed; private readonly Bindable>> downloadFailed = new Bindable>>(); private readonly IAPIProvider api; private readonly List> currentDownloads = new List>(); private readonly MutableDatabaseBackedStoreWithFileIncludes modelStore; protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) : base(storage, contextFactory, modelStore, importHost) { this.api = api; this.modelStore = modelStore; } /// /// 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(TModel model, bool minimiseDownloadSize); /// /// Begin a download for the requested . /// /// The to be downloaded. /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. /// Whether the download was started. public bool Download(TModel 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 Import(notification, new ImportTask(filename)); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) downloadFailed.Value = new WeakReference>(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.Value = new WeakReference>(request); return true; void triggerFailure(Exception error) { currentDownloads.Remove(request); downloadFailed.Value = new WeakReference>(request); notification.State = ProgressNotificationState.Cancelled; if (!(error is OperationCanceledException)) Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!"); } } public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); /// /// Performs implementation specific comparisons to determine whether a given model is present in the local store. /// /// The whose existence needs to be checked. /// The usable items present in the store. /// Whether the exists. protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); public ArchiveDownloadRequest GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); private bool canDownload(TModel 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; } } } }