// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Linq; using Microsoft.EntityFrameworkCore; using osu.Game.Database; namespace osu.Game.Beatmaps { /// <summary> /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// </summary> public class BeatmapStore : DatabaseBackedStore { public event Action<BeatmapSetInfo> BeatmapSetAdded; public event Action<BeatmapSetInfo> BeatmapSetRemoved; public event Action<BeatmapInfo> BeatmapHidden; public event Action<BeatmapInfo> BeatmapRestored; public BeatmapStore(Func<OsuDbContext> factory) : base(factory) { } /// <summary> /// Add a <see cref="BeatmapSetInfo"/> to the database. /// </summary> /// <param name="beatmapSet">The beatmap to add.</param> public void Add(BeatmapSetInfo beatmapSet) { var context = GetContext(); foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) { // If we detect a new metadata object it'll be attached to the current context so it can be reused // to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local) // of the corresponding table (.Set<BeatmapMetadata>()) for matching entries to our criteria. var contextMetadata = context.Set<BeatmapMetadata>().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata)); if (contextMetadata != null) beatmap.Metadata = contextMetadata; else context.BeatmapMetadata.Attach(beatmap.Metadata); } context.BeatmapSetInfo.Attach(beatmapSet); context.SaveChanges(); BeatmapSetAdded?.Invoke(beatmapSet); } /// <summary> /// Delete a <see cref="BeatmapSetInfo"/> from the database. /// </summary> /// <param name="beatmapSet">The beatmap to delete.</param> /// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns> public bool Delete(BeatmapSetInfo beatmapSet) { var context = GetContext(); Refresh(ref beatmapSet, BeatmapSets); if (beatmapSet.DeletePending) return false; beatmapSet.DeletePending = true; context.SaveChanges(); BeatmapSetRemoved?.Invoke(beatmapSet); return true; } /// <summary> /// Restore a previously deleted <see cref="BeatmapSetInfo"/>. /// </summary> /// <param name="beatmapSet">The beatmap to restore.</param> /// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns> public bool Undelete(BeatmapSetInfo beatmapSet) { var context = GetContext(); Refresh(ref beatmapSet, BeatmapSets); if (!beatmapSet.DeletePending) return false; beatmapSet.DeletePending = false; context.SaveChanges(); BeatmapSetAdded?.Invoke(beatmapSet); return true; } /// <summary> /// Hide a <see cref="BeatmapInfo"/> in the database. /// </summary> /// <param name="beatmap">The beatmap to hide.</param> /// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns> public bool Hide(BeatmapInfo beatmap) { var context = GetContext(); Refresh(ref beatmap, Beatmaps); if (beatmap.Hidden) return false; beatmap.Hidden = true; context.SaveChanges(); BeatmapHidden?.Invoke(beatmap); return true; } /// <summary> /// Restore a previously hidden <see cref="BeatmapInfo"/>. /// </summary> /// <param name="beatmap">The beatmap to restore.</param> /// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns> public bool Restore(BeatmapInfo beatmap) { var context = GetContext(); Refresh(ref beatmap, Beatmaps); if (!beatmap.Hidden) return false; beatmap.Hidden = false; context.SaveChanges(); BeatmapRestored?.Invoke(beatmap); return true; } public override void Cleanup() { var context = GetContext(); var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Metadata); // metadata is M-N so we can't rely on cascades context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); // cascades down to beatmaps. context.BeatmapSetInfo.RemoveRange(purgeable); context.SaveChanges(); } public IQueryable<BeatmapSetInfo> BeatmapSets => GetContext().BeatmapSetInfo .Include(s => s.Metadata) .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Files).ThenInclude(f => f.FileInfo); public IQueryable<BeatmapInfo> Beatmaps => GetContext().BeatmapInfo .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) .Include(b => b.Metadata) .Include(b => b.Ruleset) .Include(b => b.BaseDifficulty); } }