// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; using Ionic.Zip; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.IO; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using SQLite.Net; using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Beatmaps { /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// public class BeatmapManager { /// /// Fired when a new becomes available in the database. /// public event Action BeatmapSetAdded; /// /// Fired when a is removed from the database. /// public event Action BeatmapSetRemoved; /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// public WorkingBeatmap DefaultBeatmap { private get; set; } private readonly Storage storage; private readonly FileStore files; private readonly RulesetStore rulesets; private readonly Action postNotification; private readonly BeatmapStore beatmaps; // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private BeatmapIPCChannel ipc; public BeatmapManager(Storage storage, FileStore files, SQLiteConnection connection, RulesetStore rulesets, IIpcHost importHost = null, Action postNotification = null) { beatmaps = new BeatmapStore(connection); beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); this.storage = storage; this.files = files; this.rulesets = rulesets; this.postNotification = postNotification; if (importHost != null) ipc = new BeatmapIPCChannel(importHost, this); } /// /// Import one or more from filesystem . /// This will post a notification tracking import progress. /// /// One or more beatmap locations on disk. public void Import(params string[] paths) { var notification = new ProgressNotification { Text = "Beatmap import is initialising...", Progress = 0, State = ProgressNotificationState.Active, }; postNotification?.Invoke(notification); int i = 0; foreach (string path in paths) { if (notification.State == ProgressNotificationState.Cancelled) // user requested abort return; try { notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; using (ArchiveReader reader = getReaderFrom(path)) Import(reader); notification.Progress = (float)++i / paths.Length; // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with beatmaps from default storage. // Also, not always a single file, i.e. for LegacyFilesystemReader // TODO: Add a check to prevent files from storage to be deleted. try { if (File.Exists(path)) File.Delete(path); } catch (Exception e) { Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); } } catch (Exception e) { e = e.InnerException ?? e; Logger.Error(e, @"Could not import beatmap set"); } } notification.State = ProgressNotificationState.Completed; } private readonly object importLock = new object(); /// /// Import a beatmap from an . /// /// The beatmap to be imported. public BeatmapSetInfo Import(ArchiveReader archiveReader) { // let's only allow one concurrent import at a time for now. lock (importLock) { BeatmapSetInfo set = importToStorage(archiveReader); Import(set); return set; } } /// /// Import a beatmap from a . /// /// The beatmap to be imported. public void Import(BeatmapSetInfo beatmapSetInfo) { // If we have an ID then we already exist in the database. if (beatmapSetInfo.ID != 0) return; lock (beatmaps) beatmaps.Add(beatmapSetInfo); } /// /// Delete a beatmap from the manager. /// Is a no-op for already deleted beatmaps. /// /// The beatmap to delete. public void Delete(BeatmapSetInfo beatmapSet) { lock (beatmaps) if (!beatmaps.Delete(beatmapSet)) return; if (!beatmapSet.Protected) files.Dereference(beatmapSet.Files); } /// /// Returns a to a usable state if it has previously been deleted but not yet purged. /// Is a no-op for already usable beatmaps. /// /// The beatmap to restore. public void Undelete(BeatmapSetInfo beatmapSet) { lock (beatmaps) if (!beatmaps.Undelete(beatmapSet)) return; files.Reference(beatmapSet.Files); } /// /// Retrieve a instance for the provided /// /// The beatmap to lookup. /// The currently loaded . Allows for optimisation where elements are shared with the new beatmap. /// A instance correlating to the provided . public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null) { if (beatmapInfo == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) return DefaultBeatmap; lock (beatmaps) beatmaps.Populate(beatmapInfo); if (beatmapInfo.BeatmapSet == null) throw new InvalidOperationException($@"Beatmap set {beatmapInfo.BeatmapSetInfoID} is not in the local database."); if (beatmapInfo.Metadata == null) beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo); previous?.TransferTo(working); return working; } /// /// Reset the manager to an empty state. /// public void Reset() { lock (beatmaps) beatmaps.Reset(); } /// /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Func query) { lock (beatmaps) { BeatmapSetInfo set = beatmaps.Query().FirstOrDefault(query); if (set != null) beatmaps.Populate(set); return set; } } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. public List QueryBeatmapSets(Expression> query) { lock (beatmaps) return beatmaps.QueryAndPopulate(query); } /// /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapInfo QueryBeatmap(Func query) { lock (beatmaps) { BeatmapInfo set = beatmaps.Query().FirstOrDefault(query); if (set != null) beatmaps.Populate(set); return set; } } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. public List QueryBeatmaps(Expression> query) { lock (beatmaps) return beatmaps.QueryAndPopulate(query); } /// /// Creates an from a valid storage path. /// /// A file or folder path resolving the beatmap content. /// A reader giving access to the beatmap's content. private ArchiveReader getReaderFrom(string path) { if (ZipFile.IsZipFile(path)) return new OszArchiveReader(storage.GetStream(path)); else return new LegacyFilesystemReader(path); } /// /// Import a beamap into our local storage. /// If the beatmap is already imported, the existing instance will be returned. /// /// The beatmap archive to be read. /// The imported beatmap, or an existing instance if it is already present. private BeatmapSetInfo importToStorage(ArchiveReader reader) { // for now, concatenate all .osu files in the set to create a unique hash. MemoryStream hashable = new MemoryStream(); foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) using (Stream s = reader.GetStream(file)) s.CopyTo(hashable); var hash = hashable.ComputeSHA2Hash(); // check if this beatmap has already been imported and exit early if so. var beatmapSet = beatmaps.QueryAndPopulate().FirstOrDefault(b => b.Hash == hash); if (beatmapSet != null) { Undelete(beatmapSet); return beatmapSet; } List fileInfos = new List(); // import files to manager foreach (string file in reader.Filenames) using (Stream s = reader.GetStream(file)) fileInfos.Add(files.Add(s, file)); BeatmapMetadata metadata; using (var stream = new StreamReader(reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))))) metadata = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata; beatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, Beatmaps = new List(), Hash = hash, Files = fileInfos, Metadata = metadata }; var mapNames = reader.Filenames.Where(f => f.EndsWith(".osu")); foreach (var name in mapNames) { using (var raw = reader.GetStream(name)) using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit using (var sr = new StreamReader(ms)) { raw.CopyTo(ms); ms.Position = 0; var decoder = BeatmapDecoder.GetDecoder(sr); Beatmap beatmap = decoder.Decode(sr); beatmap.BeatmapInfo.Path = name; beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); // TODO: Diff beatmap metadata with set metadata and leave it here if necessary beatmap.BeatmapInfo.Metadata = null; // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.Ruleset = rulesets.Query().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID); beatmap.BeatmapInfo.StarDifficulty = rulesets.Query().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID)?.CreateInstance()?.CreateDifficultyCalculator(beatmap) .Calculate() ?? 0; beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); } } return beatmapSet; } /// /// Returns a list of all usable s. /// /// Whether returned objects should be pre-populated with all data. /// A list of available . public List GetAllUsableBeatmapSets(bool populate = true) { lock (beatmaps) { if (populate) return beatmaps.QueryAndPopulate(b => !b.DeletePending).ToList(); else return beatmaps.Query(b => !b.DeletePending).ToList(); } } protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap { private readonly IResourceStore store; public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) : base(beatmapInfo) { this.store = store; } protected override Beatmap GetBeatmap() { try { Beatmap beatmap; BeatmapDecoder decoder; using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) { decoder = BeatmapDecoder.GetDecoder(stream); beatmap = decoder.Decode(stream); } if (beatmap == null || BeatmapSetInfo.StoryboardFile == null) return beatmap; using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) decoder.Decode(stream, beatmap); return beatmap; } catch { return null; } } private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => f.Filename == filename).StoragePath; protected override Texture GetBackground() { if (Metadata?.BackgroundFile == null) return null; try { return new TextureStore(new RawTextureLoaderStore(store), false).Get(getPathForFile(Metadata.BackgroundFile)); } catch { return null; } } protected override Track GetTrack() { try { var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); return trackData == null ? null : new TrackBass(trackData); } catch { return new TrackVirtual(); } } } public void ImportFromStable() { string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!", "Songs"); if (!Directory.Exists(stableInstallPath)) stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu", "Songs"); if (!Directory.Exists(stableInstallPath)) { Logger.Log("Couldn't find an osu!stable installation!", LoggingTarget.Information, LogLevel.Error); return; } Import(Directory.GetDirectories(stableInstallPath)); } } }