// 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.Net; using System.Threading.Tasks; using Humanizer; using osu.Framework.Logging; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; namespace osu.Game.Database { public abstract class ModelDownloader : IModelDownloader where TModel : class, IHasGuidPrimaryKey, ISoftDelete, IEquatable, T where T : class { public Action? PostNotification { protected get; set; } public event Action>? DownloadBegan; public event Action>? DownloadFailed; private readonly IModelImporter importer; private readonly IAPIProvider? api; protected readonly List> CurrentDownloads = new List>(); protected ModelDownloader(IModelImporter importer, IAPIProvider? api) { 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) => Download(model, minimiseDownloadSize, null); public void DownloadAsUpdate(TModel originalModel, bool minimiseDownloadSize) => Download(originalModel, minimiseDownloadSize, originalModel); protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel) { if (!canDownload(model)) return false; var request = CreateDownloadRequest(model, minimiseDownloadSize); DownloadNotification notification = new DownloadNotification { Text = $"Downloading {request.Model.GetDisplayString()}", }; request.DownloadProgressed += progress => { notification.State = ProgressNotificationState.Active; notification.Progress = progress; }; request.Success += filename => { Task.Factory.StartNew(async () => { bool importSuccessful; if (originalModel != null) importSuccessful = (await importer.ImportAsUpdate(notification, new ImportTask(filename), originalModel)) != null; else importSuccessful = (await importer.Import(notification, new ImportTask(filename))).Any(); // for now a failed import will be marked as a failed download for simplicity. if (!importSuccessful) 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)) { if (error is WebException webException && webException.Message == @"TooManyRequests") { notification.Close(false); PostNotification?.Invoke(new TooManyDownloadsNotification()); } else 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; } } } }