// 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.Diagnostics; using System.Linq; using osu.Framework.Platform; using osu.Game.Online.API; namespace osu.Game.Beatmaps { /// /// A component which handles population of online IDs for beatmaps using a two part lookup procedure. /// public class BeatmapUpdaterMetadataLookup : IDisposable { private readonly IOnlineBeatmapMetadataSource apiMetadataSource; private readonly IOnlineBeatmapMetadataSource localCachedMetadataSource; public BeatmapUpdaterMetadataLookup(IAPIProvider api, Storage storage) : this(new APIBeatmapMetadataSource(api), new LocalCachedBeatmapMetadataSource(storage)) { } internal BeatmapUpdaterMetadataLookup(IOnlineBeatmapMetadataSource apiMetadataSource, IOnlineBeatmapMetadataSource localCachedMetadataSource) { this.apiMetadataSource = apiMetadataSource; this.localCachedMetadataSource = localCachedMetadataSource; } /// /// Queue an update for a beatmap set. /// /// /// This may happen during initial import, or at a later stage in response to a user action or server event. /// /// The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed). /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible. public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch) { var lookupResults = new List(); foreach (var beatmapInfo in beatmapSet.Beatmaps) { // note that these lookups DO NOT ACTUALLY FULLY GUARANTEE that the beatmap is what it claims it is, // i.e. the correctness of this lookup should be treated as APPROXIMATE AT WORST. // this is because the beatmap filename is used as a fallback in some scenarios where the MD5 of the beatmap may mismatch. // this is considered to be an acceptable casualty so that things can continue to work as expected for users in some rare scenarios // (stale beatmap files in beatmap packs, beatmap mirror desyncs). // however, all this means that other places such as score submission ARE EXPECTED TO VERIFY THE MD5 OF THE BEATMAP AGAINST THE ONLINE ONE EXPLICITLY AGAIN. // // additionally note that the online ID stored to the map is EXPLICITLY NOT USED because some users in a silly attempt to "fix" things for themselves on stable // would reuse online IDs of already submitted beatmaps, which means that information is pretty much expected to be bogus in a nonzero number of beatmapsets. if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res)) continue; if (res == null) { beatmapInfo.ResetOnlineInfo(); lookupResults.Add(null); // mark lookup failure continue; } lookupResults.Add(res); beatmapInfo.OnlineID = res.BeatmapID; beatmapInfo.OnlineMD5Hash = res.MD5Hash; beatmapInfo.LastOnlineUpdate = res.LastUpdated; Debug.Assert(beatmapInfo.BeatmapSet != null); beatmapInfo.BeatmapSet.OnlineID = res.BeatmapSetID; // Some metadata should only be applied if there's no local changes. if (beatmapInfo.MatchesOnlineVersion) { beatmapInfo.Status = res.BeatmapStatus; beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; } } if (beatmapSet.Beatmaps.All(b => b.MatchesOnlineVersion) && lookupResults.All(r => r != null) && lookupResults.Select(r => r!.BeatmapSetID).Distinct().Count() == 1) { var representative = lookupResults.First()!; beatmapSet.Status = representative.BeatmapSetStatus ?? BeatmapOnlineStatus.None; beatmapSet.DateRanked = representative.DateRanked; beatmapSet.DateSubmitted = representative.DateSubmitted; } } /// /// Attempts to retrieve the for the given . /// /// The beatmap to perform the online lookup for. /// Whether online sources should be preferred for the lookup. /// The result of the lookup. Can be if no matching beatmap was found (or the lookup failed). /// /// if any of the metadata sources were available and returned a valid . /// if none of the metadata sources were available, or if there was insufficient data to return a valid . /// /// /// There are two cases wherein this method will return : /// /// If neither the local cache or the API are available to query. /// If the API is not available to query, and a positive match was not made in the local cache. /// /// In either case, the online ID read from the .osu file will be preserved, which may not necessarily be what we want. /// TODO: reconsider this if/when a better flow for queueing online retrieval is implemented. /// private bool tryLookup(BeatmapInfo beatmapInfo, bool preferOnlineFetch, out OnlineBeatmapMetadata? result) { bool useLocalCache = !apiMetadataSource.Available || !preferOnlineFetch; if (useLocalCache && localCachedMetadataSource.TryLookup(beatmapInfo, out result)) return true; if (apiMetadataSource.TryLookup(beatmapInfo, out result)) return true; result = null; return false; } public void Dispose() { apiMetadataSource.Dispose(); localCachedMetadataSource.Dispose(); } } }