From 1709811458f4a0a87f014846ca2dbb571ef02d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Jul 2025 10:10:47 +0200 Subject: [PATCH] Add back-population operation for user tags --- .../LocalCachedBeatmapMetadataSource.cs | 8 +- .../Database/BackgroundDataStoreProcessor.cs | 93 +++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index d9c96403ba..08c4e2e418 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -46,7 +46,7 @@ namespace osu.Game.Beatmaps this.storage = storage; if (shouldFetchCache()) - prepareLocalCache(); + FetchCache(); } private bool shouldFetchCache() @@ -131,7 +131,7 @@ namespace osu.Game.Beatmaps return false; tryPurgeCache(); - prepareLocalCache(); + FetchCache(); return false; } @@ -165,7 +165,7 @@ namespace osu.Game.Beatmaps private SqliteConnection getConnection() => new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))); - private void prepareLocalCache() + public Task FetchCache() { bool isRefetch = storage.Exists(cache_database_name); @@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps } }; - Task.Run(async () => + return Task.Run(async () => { try { diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 4e813fa2c7..29d6ef2a77 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -23,6 +24,7 @@ using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Play; +using Realms; namespace osu.Game.Database { @@ -85,6 +87,7 @@ namespace osu.Game.Database convertLegacyTotalScoreToStandardised(); upgradeScoreRanks(); backpopulateMissingSubmissionAndRankDates(); + backpopulateUserTags(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -621,6 +624,96 @@ namespace osu.Game.Database completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount); } + private void backpopulateUserTags() + { + var localMetadataSource = new LocalCachedBeatmapMetadataSource(storage); + + if (!localMetadataSource.Available || localMetadataSource.GetCacheVersion() < 3) + { + Logger.Log(@"Local metadata cache has too low version to backpopulate user tags, attempting refetch..."); + localMetadataSource.FetchCache().WaitSafely(); + + if (!localMetadataSource.Available || localMetadataSource.GetCacheVersion() < 3) + { + Logger.Log(@"Local metadata cache refetch failed. Aborting user tags backpopulation."); + return; + } + } + + Logger.Log(@"Querying for beatmaps that do not have user tags"); + + // it is not an abnormal situation for a map not to have user tags. + // therefore there's some chance that this will run much too often and be annoying to users. + // if that turns out to be the case we may need a better way to debounce this (or just delete the backpopulation logic after some time has passed?) + HashSet beatmapIds = realmAccess.Run(r => new HashSet( + r.All() + .Filter($"{nameof(BeatmapInfo.Metadata)}.{nameof(BeatmapMetadata.UserTags)}.@count == 0 AND {nameof(BeatmapInfo.StatusInt)} IN {{ 1,2,4 }}") + .AsEnumerable() + .Select(b => b.ID))); + + if (beatmapIds.Count == 0) + return; + + Logger.Log($@"Found {beatmapIds.Count} beatmaps with missing user tags."); + + var notification = showProgressNotification(beatmapIds.Count, @"Populating missing user tags", @"beatmaps now have user tags."); + + int processedCount = 0; + int countOfBeatmapsThatReceivedTags = 0; + int failedCount = 0; + + foreach (var id in beatmapIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapIds.Count); + + sleepIfRequired(); + + try + { + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + bool succeeded = realmAccess.Write(r => + { + BeatmapInfo beatmap = r.Find(id)!; + + bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result); + + if (lookupSucceeded) + { + Debug.Assert(result != null); + beatmap.Metadata.UserTags.Clear(); + beatmap.Metadata.UserTags.AddRange(result.UserTags); + if (beatmap.Metadata.UserTags.Any()) + countOfBeatmapsThatReceivedTags++; + return true; + } + + Logger.Log(@$"Could not find {beatmap.GetDisplayString()} in local cache while backpopulating missing user tags"); + return false; + }); + + if (succeeded) + ++processedCount; + else + ++failedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log(@$"Failed to update ranked/submitted dates for beatmap set {id}: {e}"); + ++failedCount; + } + } + + completeNotification(notification, countOfBeatmapsThatReceivedTags, beatmapIds.Count, failedCount); + } + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) { if (notification == null)