2018-01-05 19:21:19 +08:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
2017-07-27 15:56:41 +08:00
// 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 ;
2017-10-25 16:03:10 +08:00
using System.Linq.Expressions ;
2017-10-11 03:29:16 +08:00
using System.Threading.Tasks ;
2017-10-18 17:27:17 +08:00
using Microsoft.EntityFrameworkCore ;
2017-07-27 15:56:41 +08:00
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 ;
2017-10-11 03:29:16 +08:00
using osu.Game.Database ;
2017-11-05 19:03:58 +08:00
using osu.Game.Graphics ;
2018-01-04 23:42:48 +08:00
using osu.Game.Graphics.Textures ;
2017-07-27 15:56:41 +08:00
using osu.Game.IO ;
using osu.Game.IPC ;
2017-10-11 03:29:16 +08:00
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
2017-07-28 15:55:58 +08:00
using osu.Game.Overlays.Notifications ;
2017-07-27 15:56:41 +08:00
using osu.Game.Rulesets ;
2017-11-19 12:46:51 +08:00
using osu.Game.Utils ;
2017-11-30 04:28:02 +08:00
using osu.Game.Storyboards ;
2017-07-27 15:56:41 +08:00
namespace osu.Game.Beatmaps
{
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
public class BeatmapManager
{
/// <summary>
/// Fired when a new <see cref="BeatmapSetInfo"/> becomes available in the database.
/// </summary>
public event Action < BeatmapSetInfo > BeatmapSetAdded ;
2017-08-31 14:49:56 +08:00
/// <summary>
/// Fired when a single difficulty has been hidden.
/// </summary>
public event Action < BeatmapInfo > BeatmapHidden ;
2017-07-27 15:56:41 +08:00
/// <summary>
/// Fired when a <see cref="BeatmapSetInfo"/> is removed from the database.
/// </summary>
public event Action < BeatmapSetInfo > BeatmapSetRemoved ;
2017-08-31 14:49:56 +08:00
/// <summary>
/// Fired when a single difficulty has been restored.
/// </summary>
public event Action < BeatmapInfo > BeatmapRestored ;
2017-11-05 19:03:58 +08:00
/// <summary>
/// Fired when a beatmap download begins.
/// </summary>
public event Action < DownloadBeatmapSetRequest > BeatmapDownloadBegan ;
2017-07-27 15:56:41 +08:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
public WorkingBeatmap DefaultBeatmap { private get ; set ; }
private readonly Storage storage ;
2017-10-17 14:00:27 +08:00
private BeatmapStore createBeatmapStore ( Func < OsuDbContext > context )
{
var store = new BeatmapStore ( context ) ;
store . BeatmapSetAdded + = s = > BeatmapSetAdded ? . Invoke ( s ) ;
store . BeatmapSetRemoved + = s = > BeatmapSetRemoved ? . Invoke ( s ) ;
store . BeatmapHidden + = b = > BeatmapHidden ? . Invoke ( b ) ;
store . BeatmapRestored + = b = > BeatmapRestored ? . Invoke ( b ) ;
return store ;
}
private readonly Func < OsuDbContext > createContext ;
2017-07-27 15:56:41 +08:00
private readonly FileStore files ;
private readonly RulesetStore rulesets ;
private readonly BeatmapStore beatmaps ;
2017-09-09 01:07:28 +08:00
private readonly APIAccess api ;
2017-09-09 15:14:27 +08:00
private readonly List < DownloadBeatmapSetRequest > currentDownloads = new List < DownloadBeatmapSetRequest > ( ) ;
2017-09-09 01:07:28 +08:00
2017-07-27 15:56:41 +08:00
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private BeatmapIPCChannel ipc ;
2017-07-31 17:03:55 +08:00
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
public Action < Notification > PostNotification { private get ; set ; }
2017-08-01 14:12:12 +08:00
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func < Storage > GetStableStorage { private get ; set ; }
2018-01-04 23:42:48 +08:00
private void refreshImportContext ( )
{
lock ( importContextLock )
{
importContext ? . Value ? . Dispose ( ) ;
importContext = new Lazy < OsuDbContext > ( ( ) = >
{
var c = createContext ( ) ;
c . Database . AutoTransactionsEnabled = false ;
return c ;
} ) ;
}
}
2017-10-17 14:00:27 +08:00
public BeatmapManager ( Storage storage , Func < OsuDbContext > context , RulesetStore rulesets , APIAccess api , IIpcHost importHost = null )
2017-07-27 15:56:41 +08:00
{
2017-10-17 14:00:27 +08:00
createContext = context ;
2018-01-04 23:42:48 +08:00
refreshImportContext ( ) ;
2017-10-18 17:27:17 +08:00
2017-10-17 14:00:27 +08:00
beatmaps = createBeatmapStore ( context ) ;
files = new FileStore ( context , storage ) ;
2017-07-27 15:56:41 +08:00
2017-10-17 16:08:42 +08:00
this . storage = files . Storage ;
2017-07-27 15:56:41 +08:00
this . rulesets = rulesets ;
2017-09-09 01:07:28 +08:00
this . api = api ;
2017-07-27 15:56:41 +08:00
if ( importHost ! = null )
ipc = new BeatmapIPCChannel ( importHost , this ) ;
2017-10-17 18:58:33 +08:00
beatmaps . Cleanup ( ) ;
2017-07-27 15:56:41 +08:00
}
/// <summary>
2017-07-28 15:55:58 +08:00
/// Import one or more <see cref="BeatmapSetInfo"/> from filesystem <paramref name="paths"/>.
/// This will post a notification tracking import progress.
2017-07-27 15:56:41 +08:00
/// </summary>
2017-07-28 15:55:58 +08:00
/// <param name="paths">One or more beatmap locations on disk.</param>
2017-07-27 15:56:41 +08:00
public void Import ( params string [ ] paths )
{
2017-07-28 15:55:58 +08:00
var notification = new ProgressNotification
{
Text = "Beatmap import is initialising..." ,
2017-12-18 18:14:07 +08:00
CompletionText = "Import successful!" ,
2017-07-28 15:55:58 +08:00
Progress = 0 ,
State = ProgressNotificationState . Active ,
} ;
2017-07-31 17:03:55 +08:00
PostNotification ? . Invoke ( notification ) ;
2017-07-28 15:55:58 +08:00
int i = 0 ;
2017-07-27 15:56:41 +08:00
foreach ( string path in paths )
{
2017-07-28 15:55:58 +08:00
if ( notification . State = = ProgressNotificationState . Cancelled )
// user requested abort
return ;
2017-07-27 15:56:41 +08:00
try
{
2017-07-28 15:55:58 +08:00
notification . Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}" ;
2017-07-27 15:56:41 +08:00
using ( ArchiveReader reader = getReaderFrom ( path ) )
Import ( reader ) ;
2017-07-28 15:55:58 +08:00
notification . Progress = ( float ) + + i / paths . Length ;
2017-07-27 15:56:41 +08:00
// 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
{
2017-07-28 14:08:56 +08:00
if ( File . Exists ( path ) )
File . Delete ( path ) ;
2017-07-27 15:56:41 +08:00
}
catch ( Exception e )
{
2017-07-28 14:08:56 +08:00
Logger . Error ( e , $@"Could not delete original file after import ({Path.GetFileName(path)})" ) ;
2017-07-27 15:56:41 +08:00
}
}
catch ( Exception e )
{
e = e . InnerException ? ? e ;
2017-11-22 18:35:25 +08:00
Logger . Error ( e , $@"Could not import beatmap set ({Path.GetFileName(path)})" ) ;
2018-01-04 23:42:48 +08:00
refreshImportContext ( ) ;
2017-07-27 15:56:41 +08:00
}
}
2017-07-28 15:55:58 +08:00
notification . State = ProgressNotificationState . Completed ;
2017-07-27 15:56:41 +08:00
}
2018-01-04 23:42:48 +08:00
private readonly object importContextLock = new object ( ) ;
private Lazy < OsuDbContext > importContext ;
2017-10-17 19:29:47 +08:00
2017-07-27 15:56:41 +08:00
/// <summary>
/// Import a beatmap from an <see cref="ArchiveReader"/>.
/// </summary>
/// <param name="archiveReader">The beatmap to be imported.</param>
public BeatmapSetInfo Import ( ArchiveReader archiveReader )
{
2017-07-28 11:46:54 +08:00
// let's only allow one concurrent import at a time for now.
2018-01-04 23:42:48 +08:00
lock ( importContextLock )
2017-10-17 14:00:27 +08:00
{
2017-10-18 17:27:17 +08:00
var context = importContext . Value ;
2017-08-02 13:18:35 +08:00
2017-10-23 13:53:13 +08:00
using ( var transaction = context . BeginTransaction ( ) )
2017-10-17 14:00:27 +08:00
{
// create local stores so we can isolate and thread safely, and share a context/transaction.
2017-10-17 16:08:42 +08:00
var iFiles = new FileStore ( ( ) = > context , storage ) ;
var iBeatmaps = createBeatmapStore ( ( ) = > context ) ;
BeatmapSetInfo set = importToStorage ( iFiles , iBeatmaps , archiveReader ) ;
if ( set . ID = = 0 )
{
iBeatmaps . Add ( set ) ;
context . SaveChanges ( ) ;
}
2017-10-17 14:00:27 +08:00
2017-10-22 15:17:40 +08:00
context . SaveChanges ( transaction ) ;
2017-10-17 14:00:27 +08:00
return set ;
}
}
2017-07-27 15:56:41 +08:00
}
/// <summary>
/// Import a beatmap from a <see cref="BeatmapSetInfo"/>.
/// </summary>
/// <param name="beatmapSetInfo">The beatmap to be imported.</param>
public void Import ( BeatmapSetInfo beatmapSetInfo )
{
// If we have an ID then we already exist in the database.
2017-10-14 13:28:25 +08:00
if ( beatmapSetInfo . ID ! = 0 ) return ;
2017-07-27 15:56:41 +08:00
2017-10-17 14:00:27 +08:00
createBeatmapStore ( createContext ) . Add ( beatmapSetInfo ) ;
2017-07-27 15:56:41 +08:00
}
2017-09-09 01:07:28 +08:00
/// <summary>
2017-09-09 03:34:55 +08:00
/// Downloads a beatmap.
2017-09-09 01:07:28 +08:00
/// </summary>
2017-09-09 03:34:55 +08:00
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to be downloaded.</param>
2017-11-05 19:03:58 +08:00
/// <param name="noVideo">Whether the beatmap should be downloaded without video. Defaults to false.</param>
public void Download ( BeatmapSetInfo beatmapSetInfo , bool noVideo = false )
2017-09-09 01:07:28 +08:00
{
2017-09-09 15:14:27 +08:00
var existing = GetExistingDownload ( beatmapSetInfo ) ;
2017-09-09 12:55:28 +08:00
2017-11-05 19:03:58 +08:00
if ( existing ! = null | | api = = null ) return ;
2017-09-09 12:55:28 +08:00
2017-11-05 19:03:58 +08:00
if ( ! api . LocalUser . Value . IsSupporter )
{
PostNotification ? . Invoke ( new SimpleNotification
{
Icon = FontAwesome . fa_superpowers ,
Text = "You gotta be a supporter to download for now 'yo"
} ) ;
return ;
}
2017-09-09 01:07:28 +08:00
2017-12-18 18:14:07 +08:00
var downloadNotification = new ProgressNotification
2017-09-09 01:07:28 +08:00
{
2017-12-19 18:34:23 +08:00
CompletionText = $"Imported {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}!" ,
2017-10-06 05:23:26 +08:00
Text = $"Downloading {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}" ,
2017-09-09 01:07:28 +08:00
} ;
2017-11-05 19:03:58 +08:00
var request = new DownloadBeatmapSetRequest ( beatmapSetInfo , noVideo ) ;
2017-09-09 01:07:28 +08:00
request . DownloadProgressed + = progress = >
{
downloadNotification . State = ProgressNotificationState . Active ;
downloadNotification . Progress = progress ;
} ;
request . Success + = data = >
{
2017-10-25 10:49:00 +08:00
downloadNotification . Text = $"Importing {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}" ;
2017-09-09 01:07:28 +08:00
2017-10-25 10:43:30 +08:00
Task . Factory . StartNew ( ( ) = >
{
// This gets scheduled back to the update thread, but we want the import to run in the background.
using ( var stream = new MemoryStream ( data ) )
using ( var archive = new OszArchiveReader ( stream ) )
Import ( archive ) ;
2017-10-25 10:49:00 +08:00
downloadNotification . State = ProgressNotificationState . Completed ;
2018-01-22 12:17:03 +08:00
currentDownloads . Remove ( request ) ;
2017-10-25 10:43:30 +08:00
} , TaskCreationOptions . LongRunning ) ;
2017-09-09 01:07:28 +08:00
} ;
2018-01-22 12:25:49 +08:00
request . Failure + = error = >
2017-09-09 01:07:28 +08:00
{
2018-01-22 12:25:49 +08:00
if ( error is OperationCanceledException ) return ;
2017-09-09 01:07:28 +08:00
downloadNotification . State = ProgressNotificationState . Completed ;
2018-01-22 12:25:49 +08:00
Logger . Error ( error , "Beatmap download failed!" ) ;
2017-09-09 13:33:25 +08:00
currentDownloads . Remove ( request ) ;
2017-09-09 01:07:28 +08:00
} ;
downloadNotification . CancelRequested + = ( ) = >
{
2017-09-09 12:21:37 +08:00
request . Cancel ( ) ;
2017-09-09 13:33:25 +08:00
currentDownloads . Remove ( request ) ;
2017-09-09 02:25:20 +08:00
downloadNotification . State = ProgressNotificationState . Cancelled ;
2017-09-09 01:07:28 +08:00
return true ;
} ;
2017-09-09 13:33:25 +08:00
currentDownloads . Add ( request ) ;
2017-09-09 01:07:28 +08:00
PostNotification ? . Invoke ( downloadNotification ) ;
// don't run in the main api queue as this is a long-running task.
2017-10-25 10:42:55 +08:00
Task . Factory . StartNew ( ( ) = > request . Perform ( api ) , TaskCreationOptions . LongRunning ) ;
2017-11-05 19:03:58 +08:00
BeatmapDownloadBegan ? . Invoke ( request ) ;
2017-09-09 01:07:28 +08:00
}
/// <summary>
2017-09-09 02:25:20 +08:00
/// Get an existing download request if it exists.
2017-09-09 01:07:28 +08:00
/// </summary>
2017-09-09 02:25:20 +08:00
/// <param name="beatmap">The <see cref="BeatmapSetInfo"/> whose download request is wanted.</param>
/// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns>
2017-10-14 13:28:25 +08:00
public DownloadBeatmapSetRequest GetExistingDownload ( BeatmapSetInfo beatmap ) = > currentDownloads . Find ( d = > d . BeatmapSet . OnlineBeatmapSetID = = beatmap . OnlineBeatmapSetID ) ;
2017-09-09 01:07:28 +08:00
2017-07-27 15:56:41 +08:00
/// <summary>
/// Delete a beatmap from the manager.
/// Is a no-op for already deleted beatmaps.
/// </summary>
2017-08-30 20:12:46 +08:00
/// <param name="beatmapSet">The beatmap set to delete.</param>
2017-07-27 15:56:41 +08:00
public void Delete ( BeatmapSetInfo beatmapSet )
{
2018-01-04 23:42:48 +08:00
lock ( importContextLock )
2017-10-18 17:27:17 +08:00
{
var context = importContext . Value ;
2017-07-27 15:56:41 +08:00
2017-10-23 13:53:13 +08:00
using ( var transaction = context . BeginTransaction ( ) )
2017-10-18 17:27:17 +08:00
{
context . ChangeTracker . AutoDetectChangesEnabled = false ;
// re-fetch the beatmap set on the import context.
2017-11-20 17:55:48 +08:00
// ReSharper disable once AccessToModifiedClosure
2017-10-18 17:27:17 +08:00
beatmapSet = context . BeatmapSetInfo . Include ( s = > s . Files ) . ThenInclude ( f = > f . FileInfo ) . First ( s = > s . ID = = beatmapSet . ID ) ;
// create local stores so we can isolate and thread safely, and share a context/transaction.
var iFiles = new FileStore ( ( ) = > context , storage ) ;
var iBeatmaps = createBeatmapStore ( ( ) = > context ) ;
if ( iBeatmaps . Delete ( beatmapSet ) )
{
if ( ! beatmapSet . Protected )
iFiles . Dereference ( beatmapSet . Files . Select ( f = > f . FileInfo ) . ToArray ( ) ) ;
}
context . ChangeTracker . AutoDetectChangesEnabled = true ;
2017-10-22 15:17:40 +08:00
context . SaveChanges ( transaction ) ;
2017-10-18 17:27:17 +08:00
}
}
2017-07-27 15:56:41 +08:00
}
2017-12-18 17:55:07 +08:00
public void UndeleteAll ( )
{
2017-12-21 21:33:16 +08:00
var deleteMaps = QueryBeatmapSets ( bs = > bs . DeletePending ) . ToList ( ) ;
2017-12-18 17:55:07 +08:00
2017-12-21 21:33:16 +08:00
if ( ! deleteMaps . Any ( ) ) return ;
2017-12-18 17:55:07 +08:00
var notification = new ProgressNotification
{
2017-12-21 20:01:14 +08:00
CompletionText = "Restored all deleted beatmaps!" ,
2017-12-18 17:55:07 +08:00
Progress = 0 ,
State = ProgressNotificationState . Active ,
} ;
PostNotification ? . Invoke ( notification ) ;
int i = 0 ;
2017-12-21 21:33:16 +08:00
foreach ( var bs in deleteMaps )
2017-12-18 17:55:07 +08:00
{
if ( notification . State = = ProgressNotificationState . Cancelled )
// user requested abort
return ;
2017-12-21 21:33:16 +08:00
notification . Text = $"Restoring ({i} of {deleteMaps.Count})" ;
notification . Progress = ( float ) + + i / deleteMaps . Count ;
2017-12-18 17:55:07 +08:00
Undelete ( bs ) ;
}
notification . State = ProgressNotificationState . Completed ;
}
2017-11-30 17:58:32 +08:00
public void Undelete ( BeatmapSetInfo beatmapSet )
{
2017-12-10 18:31:37 +08:00
if ( beatmapSet . Protected )
2017-12-08 19:50:04 +08:00
return ;
2018-01-04 23:42:48 +08:00
lock ( importContextLock )
2017-11-30 17:58:32 +08:00
{
var context = importContext . Value ;
using ( var transaction = context . BeginTransaction ( ) )
{
context . ChangeTracker . AutoDetectChangesEnabled = false ;
var iFiles = new FileStore ( ( ) = > context , storage ) ;
var iBeatmaps = createBeatmapStore ( ( ) = > context ) ;
2017-11-30 18:02:53 +08:00
undelete ( iBeatmaps , iFiles , beatmapSet ) ;
2017-11-30 17:58:32 +08:00
2017-10-18 17:27:17 +08:00
context . ChangeTracker . AutoDetectChangesEnabled = true ;
2017-10-22 15:17:40 +08:00
context . SaveChanges ( transaction ) ;
2017-10-18 17:27:17 +08:00
}
}
2017-07-27 15:56:41 +08:00
}
2017-08-30 20:12:46 +08:00
/// <summary>
2017-08-31 14:49:56 +08:00
/// Delete a beatmap difficulty.
2017-08-30 20:12:46 +08:00
/// </summary>
2017-08-31 14:49:56 +08:00
/// <param name="beatmap">The beatmap difficulty to hide.</param>
2017-10-18 12:48:15 +08:00
public void Hide ( BeatmapInfo beatmap ) = > beatmaps . Hide ( beatmap ) ;
2017-08-31 14:49:56 +08:00
/// <summary>
/// Restore a beatmap difficulty.
/// </summary>
/// <param name="beatmap">The beatmap difficulty to restore.</param>
2017-10-18 12:48:15 +08:00
public void Restore ( BeatmapInfo beatmap ) = > beatmaps . Restore ( beatmap ) ;
2017-08-30 20:12:46 +08:00
2017-07-27 15:56:41 +08:00
/// <summary>
/// Returns a <see cref="BeatmapSetInfo"/> to a usable state if it has previously been deleted but not yet purged.
/// Is a no-op for already usable beatmaps.
/// </summary>
2017-10-30 21:24:11 +08:00
/// <param name="beatmaps">The store to restore beatmaps from.</param>
/// <param name="files">The store to restore beatmap files from.</param>
2017-07-27 15:56:41 +08:00
/// <param name="beatmapSet">The beatmap to restore.</param>
2017-10-17 16:08:42 +08:00
private void undelete ( BeatmapStore beatmaps , FileStore files , BeatmapSetInfo beatmapSet )
2017-07-27 15:56:41 +08:00
{
2017-10-18 12:48:15 +08:00
if ( ! beatmaps . Undelete ( beatmapSet ) ) return ;
2017-07-27 15:56:41 +08:00
2017-07-31 20:48:03 +08:00
if ( ! beatmapSet . Protected )
2017-08-01 16:37:21 +08:00
files . Reference ( beatmapSet . Files . Select ( f = > f . FileInfo ) . ToArray ( ) ) ;
2017-07-27 15:56:41 +08:00
}
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
/// <param name="beatmapInfo">The beatmap to lookup.</param>
/// <param name="previous">The currently loaded <see cref="WorkingBeatmap"/>. Allows for optimisation where elements are shared with the new beatmap.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
public WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo beatmapInfo , WorkingBeatmap previous = null )
{
2017-12-13 17:09:14 +08:00
if ( beatmapInfo ? . BeatmapSet = = null | | beatmapInfo = = DefaultBeatmap ? . BeatmapInfo )
2017-07-27 15:56:41 +08:00
return DefaultBeatmap ;
2017-10-06 05:23:26 +08:00
if ( beatmapInfo . Metadata = = null )
beatmapInfo . Metadata = beatmapInfo . BeatmapSet . Metadata ;
2017-07-27 15:56:41 +08:00
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap ( files . Store , beatmapInfo ) ;
previous ? . TransferTo ( working ) ;
return working ;
}
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
2017-10-25 22:56:05 +08:00
public BeatmapSetInfo QueryBeatmapSet ( Expression < Func < BeatmapSetInfo , bool > > query ) = > beatmaps . BeatmapSets . AsNoTracking ( ) . FirstOrDefault ( query ) ;
2017-07-27 15:56:41 +08:00
2017-08-31 14:49:56 +08:00
/// <summary>
/// Refresh an existing instance of a <see cref="BeatmapSetInfo"/> from the store.
/// </summary>
/// <param name="beatmapSet">A stale instance.</param>
/// <returns>A fresh instance.</returns>
2017-10-14 13:28:25 +08:00
public BeatmapSetInfo Refresh ( BeatmapSetInfo beatmapSet ) = > QueryBeatmapSet ( s = > s . ID = = beatmapSet . ID ) ;
2017-08-31 14:49:56 +08:00
2017-07-27 15:56:41 +08:00
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
2017-10-25 21:12:20 +08:00
public IEnumerable < BeatmapSetInfo > QueryBeatmapSets ( Expression < Func < BeatmapSetInfo , bool > > query ) = > beatmaps . BeatmapSets . AsNoTracking ( ) . Where ( query ) ;
2017-07-27 15:56:41 +08:00
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
2017-10-25 22:56:05 +08:00
public BeatmapInfo QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmaps . Beatmaps . AsNoTracking ( ) . FirstOrDefault ( query ) ;
2017-07-27 15:56:41 +08:00
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
2017-10-25 21:12:20 +08:00
public IEnumerable < BeatmapInfo > QueryBeatmaps ( Expression < Func < BeatmapInfo , bool > > query ) = > beatmaps . Beatmaps . AsNoTracking ( ) . Where ( query ) ;
2017-07-27 15:56:41 +08:00
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the beatmap content.</param>
/// <returns>A reader giving access to the beatmap's content.</returns>
private ArchiveReader getReaderFrom ( string path )
{
2017-11-19 12:46:51 +08:00
if ( ZipUtils . IsZipArchive ( path ) )
2017-10-18 17:41:04 +08:00
// ReSharper disable once InconsistentlySynchronizedField
2017-07-27 15:56:41 +08:00
return new OszArchiveReader ( storage . GetStream ( path ) ) ;
2017-10-11 03:29:16 +08:00
return new LegacyFilesystemReader ( path ) ;
2017-07-27 15:56:41 +08:00
}
/// <summary>
/// Import a beamap into our local <see cref="FileStore"/> storage.
/// If the beatmap is already imported, the existing instance will be returned.
/// </summary>
2017-10-30 21:24:11 +08:00
/// <param name="files">The store to import beatmap files to.</param>
/// <param name="beatmaps">The store to import beatmaps to.</param>
2017-07-27 15:56:41 +08:00
/// <param name="reader">The beatmap archive to be read.</param>
/// <returns>The imported beatmap, or an existing instance if it is already present.</returns>
2017-10-17 16:08:42 +08:00
private BeatmapSetInfo importToStorage ( FileStore files , BeatmapStore beatmaps , ArchiveReader reader )
2017-07-27 15:56:41 +08:00
{
2017-08-10 12:27:13 +08:00
// let's make sure there are actually .osu files to import.
2017-08-13 13:40:05 +08:00
string mapName = reader . Filenames . FirstOrDefault ( f = > f . EndsWith ( ".osu" ) ) ;
if ( string . IsNullOrEmpty ( mapName ) )
2017-08-10 12:27:13 +08:00
throw new InvalidOperationException ( "No beatmap files found in the map folder." ) ;
2017-07-27 15:56:41 +08:00
// 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 ) ;
2017-07-27 16:38:40 +08:00
var hash = hashable . ComputeSHA2Hash ( ) ;
2017-07-27 15:56:41 +08:00
// check if this beatmap has already been imported and exit early if so.
2017-10-18 12:48:15 +08:00
var beatmapSet = beatmaps . BeatmapSets . FirstOrDefault ( b = > b . Hash = = hash ) ;
2017-08-01 08:38:43 +08:00
2017-07-27 15:56:41 +08:00
if ( beatmapSet ! = null )
{
2017-10-17 16:08:42 +08:00
undelete ( beatmaps , files , beatmapSet ) ;
2017-09-19 17:34:58 +08:00
// ensure all files are present and accessible
foreach ( var f in beatmapSet . Files )
{
if ( ! storage . Exists ( f . FileInfo . StoragePath ) )
using ( Stream s = reader . GetStream ( f . Filename ) )
files . Add ( s , false ) ;
}
2017-10-17 16:08:42 +08:00
// todo: delete any files which shouldn't exist any more.
2017-07-27 15:56:41 +08:00
return beatmapSet ;
}
2017-07-31 20:48:03 +08:00
List < BeatmapSetFileInfo > fileInfos = new List < BeatmapSetFileInfo > ( ) ;
2017-07-27 15:56:41 +08:00
// import files to manager
foreach ( string file in reader . Filenames )
using ( Stream s = reader . GetStream ( file ) )
2017-07-31 20:48:03 +08:00
fileInfos . Add ( new BeatmapSetFileInfo
{
Filename = file ,
FileInfo = files . Add ( s )
} ) ;
2017-07-27 15:56:41 +08:00
BeatmapMetadata metadata ;
2017-08-10 14:49:34 +08:00
using ( var stream = new StreamReader ( reader . GetStream ( mapName ) ) )
2017-12-02 05:05:01 +08:00
metadata = Decoder . GetDecoder ( stream ) . DecodeBeatmap ( stream ) . Metadata ;
2017-07-27 15:56:41 +08:00
2017-11-19 00:45:07 +08:00
// check if a set already exists with the same online id.
2017-12-08 13:59:32 +08:00
if ( metadata . OnlineBeatmapSetID ! = null )
beatmapSet = beatmaps . BeatmapSets . FirstOrDefault ( b = > b . OnlineBeatmapSetID = = metadata . OnlineBeatmapSetID ) ;
if ( beatmapSet = = null )
beatmapSet = new BeatmapSetInfo
{
OnlineBeatmapSetID = metadata . OnlineBeatmapSetID ,
Beatmaps = new List < BeatmapInfo > ( ) ,
Hash = hash ,
Files = fileInfos ,
Metadata = metadata
} ;
2017-07-27 15:56:41 +08:00
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 ;
2017-12-02 05:05:01 +08:00
var decoder = Decoder . GetDecoder ( sr ) ;
2017-12-01 02:16:13 +08:00
Beatmap beatmap = decoder . DecodeBeatmap ( sr ) ;
2017-07-27 15:56:41 +08:00
beatmap . BeatmapInfo . Path = name ;
2017-07-27 16:38:40 +08:00
beatmap . BeatmapInfo . Hash = ms . ComputeSHA2Hash ( ) ;
2017-08-08 23:17:53 +08:00
beatmap . BeatmapInfo . MD5Hash = ms . ComputeMD5Hash ( ) ;
2017-07-27 15:56:41 +08:00
2017-12-08 14:53:59 +08:00
var existing = beatmaps . Beatmaps . FirstOrDefault ( b = > b . Hash = = beatmap . BeatmapInfo . Hash | | beatmap . BeatmapInfo . OnlineBeatmapID ! = null & & b . OnlineBeatmapID = = beatmap . BeatmapInfo . OnlineBeatmapID ) ;
2017-07-27 15:56:41 +08:00
2017-11-19 00:45:07 +08:00
if ( existing = = null )
{
2017-11-24 02:46:58 +08:00
// Exclude beatmap-metadata if it's equal to beatmapset-metadata
if ( metadata . Equals ( beatmap . Metadata ) )
beatmap . BeatmapInfo . Metadata = null ;
2017-11-19 00:45:07 +08:00
RulesetInfo ruleset = rulesets . GetRuleset ( beatmap . BeatmapInfo . RulesetID ) ;
2017-10-16 16:02:31 +08:00
2017-11-19 00:45:07 +08:00
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap . BeatmapInfo . Ruleset = ruleset ;
beatmap . BeatmapInfo . StarDifficulty = ruleset ? . CreateInstance ( ) ? . CreateDifficultyCalculator ( beatmap ) . Calculate ( ) ? ? 0 ;
2017-07-27 15:56:41 +08:00
2017-11-19 00:45:07 +08:00
beatmapSet . Beatmaps . Add ( beatmap . BeatmapInfo ) ;
}
2017-07-27 15:56:41 +08:00
}
}
return beatmapSet ;
}
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
2017-10-16 11:56:58 +08:00
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( )
2017-07-27 15:56:41 +08:00
{
2017-10-18 12:48:15 +08:00
return beatmaps . BeatmapSets . Where ( s = > ! s . DeletePending ) . ToList ( ) ;
2017-07-27 15:56:41 +08:00
}
protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
private readonly IResourceStore < byte [ ] > store ;
public BeatmapManagerWorkingBeatmap ( IResourceStore < byte [ ] > store , BeatmapInfo beatmapInfo )
: base ( beatmapInfo )
{
this . store = store ;
}
protected override Beatmap GetBeatmap ( )
{
try
{
using ( var stream = new StreamReader ( store . GetStream ( getPathForFile ( BeatmapInfo . Path ) ) ) )
{
2017-12-02 05:05:01 +08:00
Decoder decoder = Decoder . GetDecoder ( stream ) ;
2017-12-01 02:16:13 +08:00
return decoder . DecodeBeatmap ( stream ) ;
2017-07-27 15:56:41 +08:00
}
}
2017-10-16 21:04:22 +08:00
catch
{
return null ;
}
2017-07-27 15:56:41 +08:00
}
2017-09-27 20:27:01 +08:00
private string getPathForFile ( string filename ) = > BeatmapSetInfo . Files . First ( f = > string . Equals ( f . Filename , filename , StringComparison . InvariantCultureIgnoreCase ) ) . FileInfo . StoragePath ;
2017-07-27 15:56:41 +08:00
protected override Texture GetBackground ( )
{
if ( Metadata ? . BackgroundFile = = null )
return null ;
try
{
2018-01-04 23:42:48 +08:00
return new LargeTextureStore ( new RawTextureLoaderStore ( store ) ) . Get ( getPathForFile ( Metadata . BackgroundFile ) ) ;
2017-07-27 15:56:41 +08:00
}
2017-10-16 21:04:22 +08:00
catch
{
return null ;
}
2017-07-27 15:56:41 +08:00
}
protected override Track GetTrack ( )
{
try
{
var trackData = store . GetStream ( getPathForFile ( Metadata . AudioFile ) ) ;
return trackData = = null ? null : new TrackBass ( trackData ) ;
}
2017-10-16 21:04:22 +08:00
catch
{
return new TrackVirtual ( ) ;
}
2017-07-27 15:56:41 +08:00
}
2017-10-09 16:18:11 +08:00
2017-10-12 17:50:12 +08:00
protected override Waveform GetWaveform ( ) = > new Waveform ( store . GetStream ( getPathForFile ( Metadata . AudioFile ) ) ) ;
2017-11-30 04:28:02 +08:00
protected override Storyboard GetStoryboard ( )
{
try
{
2018-01-11 13:53:41 +08:00
using ( var beatmap = new StreamReader ( store . GetStream ( getPathForFile ( BeatmapInfo . Path ) ) ) )
{
Decoder decoder = Decoder . GetDecoder ( beatmap ) ;
if ( BeatmapSetInfo ? . StoryboardFile = = null )
return decoder . GetStoryboardDecoder ( ) . DecodeStoryboard ( beatmap ) ;
using ( var storyboard = new StreamReader ( store . GetStream ( getPathForFile ( BeatmapSetInfo . StoryboardFile ) ) ) )
return decoder . GetStoryboardDecoder ( ) . DecodeStoryboard ( beatmap , storyboard ) ;
}
2017-11-30 04:28:02 +08:00
}
catch
{
return new Storyboard ( ) ;
}
}
2017-07-27 15:56:41 +08:00
}
2017-07-28 11:46:38 +08:00
2017-12-25 23:37:50 +08:00
public bool StableInstallationAvailable = > GetStableStorage ? . Invoke ( ) ! = null ;
2017-08-01 14:12:12 +08:00
/// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary>
2017-12-25 23:37:50 +08:00
public async Task ImportFromStable ( )
2017-07-28 11:46:38 +08:00
{
2017-08-01 14:12:12 +08:00
var stable = GetStableStorage ? . Invoke ( ) ;
2017-07-28 11:46:38 +08:00
2017-08-01 14:12:12 +08:00
if ( stable = = null )
2017-07-28 11:46:38 +08:00
{
2017-08-01 14:12:12 +08:00
Logger . Log ( "No osu!stable installation available!" , LoggingTarget . Information , LogLevel . Error ) ;
2017-07-28 11:46:38 +08:00
return ;
}
2017-12-25 23:37:50 +08:00
await Task . Factory . StartNew ( ( ) = > Import ( stable . GetDirectories ( "Songs" ) ) , TaskCreationOptions . LongRunning ) ;
2017-07-28 11:46:38 +08:00
}
2017-07-31 17:52:59 +08:00
public void DeleteAll ( )
{
2017-10-18 17:27:17 +08:00
var maps = GetAllUsableBeatmapSets ( ) ;
2017-07-31 17:52:59 +08:00
2017-10-18 17:27:17 +08:00
if ( maps . Count = = 0 ) return ;
2017-07-31 17:52:59 +08:00
var notification = new ProgressNotification
{
Progress = 0 ,
2017-12-18 18:14:07 +08:00
CompletionText = "Deleted all beatmaps!" ,
2017-07-31 17:52:59 +08:00
State = ProgressNotificationState . Active ,
} ;
PostNotification ? . Invoke ( notification ) ;
int i = 0 ;
foreach ( var b in maps )
{
if ( notification . State = = ProgressNotificationState . Cancelled )
// user requested abort
return ;
2017-10-18 17:27:17 +08:00
notification . Text = $"Deleting ({i} of {maps.Count})" ;
notification . Progress = ( float ) + + i / maps . Count ;
2017-07-31 17:52:59 +08:00
Delete ( b ) ;
}
notification . State = ProgressNotificationState . Completed ;
}
2017-07-27 15:56:41 +08:00
}
}