// 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.Logging; 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) { if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res)) continue; if (res == null || shouldDiscardLookupResult(res, beatmapInfo)) { 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; } } private bool shouldDiscardLookupResult(OnlineBeatmapMetadata result, BeatmapInfo beatmapInfo) { if (beatmapInfo.OnlineID > 0 && result.BeatmapID != beatmapInfo.OnlineID) { Logger.Log($"Discarding metadata lookup result due to mismatching online ID (expected: {beatmapInfo.OnlineID} actual: {result.BeatmapID})", LoggingTarget.Database); return true; } if (beatmapInfo.OnlineID == -1 && result.MD5Hash != beatmapInfo.MD5Hash) { Logger.Log($"Discarding metadata lookup result due to mismatching hash (expected: {beatmapInfo.MD5Hash} actual: {result.MD5Hash})", LoggingTarget.Database); return true; } return false; } /// /// 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(); } } }