2019-01-24 17:43:03 +09:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 18:19:50 +09:00
2021-09-30 16:45:32 +09:00
using System ;
using System.Collections.Generic ;
2022-06-16 18:07:04 +09:00
using System.Diagnostics ;
2021-09-30 16:45:32 +09:00
using System.IO ;
using System.Linq ;
using System.Linq.Expressions ;
2022-06-16 18:07:04 +09:00
using System.Text ;
2021-09-30 16:45:32 +09:00
using System.Threading ;
using System.Threading.Tasks ;
using osu.Framework.Audio ;
2021-11-09 17:27:07 +09:00
using osu.Framework.Audio.Track ;
2022-06-16 18:07:04 +09:00
using osu.Framework.Extensions ;
2021-09-30 16:45:32 +09:00
using osu.Framework.IO.Stores ;
using osu.Framework.Platform ;
2020-10-16 14:39:02 +09:00
using osu.Framework.Testing ;
2022-06-16 18:07:04 +09:00
using osu.Game.Beatmaps.Formats ;
2021-09-30 16:45:32 +09:00
using osu.Game.Database ;
2022-06-16 18:07:04 +09:00
using osu.Game.Extensions ;
2021-09-30 16:45:32 +09:00
using osu.Game.IO.Archives ;
2021-11-22 15:52:55 +09:00
using osu.Game.Models ;
2021-09-30 16:45:32 +09:00
using osu.Game.Online.API ;
2021-11-04 18:02:44 +09:00
using osu.Game.Online.API.Requests.Responses ;
2021-09-30 16:45:32 +09:00
using osu.Game.Overlays.Notifications ;
using osu.Game.Rulesets ;
using osu.Game.Skinning ;
2022-02-16 23:12:13 +01:00
using osu.Game.Utils ;
2018-04-13 18:19:50 +09:00
2017-07-27 16:56:41 +09:00
namespace osu.Game.Beatmaps
{
/// <summary>
2021-09-30 15:43:49 +09:00
/// Handles general operations related to global beatmap management.
2017-07-27 16:56:41 +09:00
/// </summary>
2020-10-16 14:39:02 +09:00
[ExcludeFromDynamicCompile]
2022-07-04 18:13:53 +09:00
public class BeatmapManager : ModelManager < BeatmapSetInfo > , IModelImporter < BeatmapSetInfo > , IWorkingBeatmapCache
2017-07-27 16:56:41 +09:00
{
2021-11-09 17:27:07 +09:00
public ITrackStore BeatmapTrackStore { get ; }
2022-06-16 18:07:04 +09:00
private readonly BeatmapImporter beatmapImporter ;
2021-09-30 18:21:16 +09:00
2021-09-30 16:45:32 +09:00
private readonly WorkingBeatmapCache workingBeatmapCache ;
2022-07-04 18:13:53 +09:00
public Action < BeatmapSetInfo > ? ProcessBeatmap { private get ; set ; }
2021-12-15 14:26:38 +09:00
2022-06-16 18:07:04 +09:00
public BeatmapManager ( Storage storage , RealmAccess realm , RulesetStore rulesets , IAPIProvider ? api , AudioManager audioManager , IResourceStore < byte [ ] > gameResources , GameHost ? host = null ,
2022-06-20 18:39:53 +09:00
WorkingBeatmap ? defaultBeatmap = null , BeatmapDifficultyCache ? difficultyCache = null , bool performOnlineLookups = false )
2022-06-16 18:53:13 +09:00
: base ( storage , realm )
2017-07-27 16:56:41 +09:00
{
2021-12-14 19:47:11 +09:00
if ( performOnlineLookups )
2022-01-11 21:36:34 +09:00
{
if ( api = = null )
throw new ArgumentNullException ( nameof ( api ) , "API must be provided if online lookups are required." ) ;
2022-06-20 18:39:53 +09:00
if ( difficultyCache = = null )
throw new ArgumentNullException ( nameof ( difficultyCache ) , "Difficulty cache must be provided if online lookups are required." ) ;
2022-01-11 21:36:34 +09:00
}
2021-12-14 19:47:11 +09:00
2022-01-24 19:59:58 +09:00
var userResources = new RealmFileStore ( realm , storage ) . Store ;
2021-11-09 17:27:07 +09:00
BeatmapTrackStore = audioManager . GetTrackStore ( userResources ) ;
2022-07-04 18:13:53 +09:00
beatmapImporter = CreateBeatmapImporter ( storage , realm ) ;
beatmapImporter . ProcessBeatmap = obj = > ProcessBeatmap ? . Invoke ( obj ) ;
2022-06-16 19:48:18 +09:00
beatmapImporter . PostNotification = obj = > PostNotification ? . Invoke ( obj ) ;
2021-11-09 17:27:07 +09:00
workingBeatmapCache = CreateWorkingBeatmapCache ( audioManager , gameResources , userResources , defaultBeatmap , host ) ;
2018-07-19 13:41:09 +09:00
}
2022-06-16 18:07:04 +09:00
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache ( AudioManager audioManager , IResourceStore < byte [ ] > resources , IResourceStore < byte [ ] > storage , WorkingBeatmap ? defaultBeatmap ,
GameHost ? host )
2021-11-09 17:27:07 +09:00
{
return new WorkingBeatmapCache ( BeatmapTrackStore , audioManager , resources , storage , defaultBeatmap , host ) ;
}
2021-09-30 16:45:32 +09:00
2022-07-04 18:13:53 +09:00
protected virtual BeatmapImporter CreateBeatmapImporter ( Storage storage , RealmAccess realm ) = > new BeatmapImporter ( storage , realm ) ;
2021-09-30 16:45:32 +09:00
/// <summary>
2022-01-23 17:49:17 +01:00
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
/// with a single difficulty which is backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned usable <see cref="WorkingBeatmap"/>.
2021-09-30 16:45:32 +09:00
/// </summary>
2021-11-04 18:02:44 +09:00
public WorkingBeatmap CreateNew ( RulesetInfo ruleset , APIUser user )
2021-09-30 16:45:32 +09:00
{
var metadata = new BeatmapMetadata
{
2021-11-22 15:52:55 +09:00
Author = new RealmUser
{
OnlineID = user . OnlineID ,
Username = user . Username ,
}
2021-09-30 16:45:32 +09:00
} ;
2022-01-11 18:17:13 +09:00
var beatmapSet = new BeatmapSetInfo
2021-09-30 16:45:32 +09:00
{
2021-11-24 13:25:52 +09:00
Beatmaps =
2021-09-30 16:45:32 +09:00
{
2022-01-27 21:41:30 +01:00
new BeatmapInfo ( ruleset , new BeatmapDifficulty ( ) , metadata )
2021-09-30 16:45:32 +09:00
}
} ;
2022-01-11 18:17:13 +09:00
foreach ( BeatmapInfo b in beatmapSet . Beatmaps )
b . BeatmapSet = beatmapSet ;
2022-06-20 15:18:07 +09:00
var imported = beatmapImporter . ImportModel ( beatmapSet ) ;
2021-09-30 19:33:12 +09:00
2022-01-11 16:30:55 +09:00
if ( imported = = null )
throw new InvalidOperationException ( "Failed to import new beatmap" ) ;
return imported . PerformRead ( s = > GetWorkingBeatmap ( s . Beatmaps . First ( ) ) ) ;
2021-09-30 16:45:32 +09:00
}
2022-01-23 17:49:17 +01:00
/// <summary>
2022-02-14 20:56:05 +01:00
/// Add a new difficulty to the provided <paramref name="targetBeatmapSet"/> based on the provided <paramref name="referenceWorkingBeatmap"/>.
2022-01-23 17:49:17 +01:00
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
2022-02-14 20:56:05 +01:00
/// <remarks>
/// Contrary to <see cref="CopyExistingDifficulty"/>, this method does not preserve hitobjects and beatmap-level settings from <paramref name="referenceWorkingBeatmap"/>.
/// The created beatmap will have zero hitobjects and will have default settings (including difficulty settings), but will preserve metadata and existing timing points.
/// </remarks>
/// <param name="targetBeatmapSet">The <see cref="BeatmapSetInfo"/> to add the new difficulty to.</param>
/// <param name="referenceWorkingBeatmap">The <see cref="WorkingBeatmap"/> to use as a baseline reference when creating the new difficulty.</param>
/// <param name="rulesetInfo">The ruleset with which the new difficulty should be created.</param>
public virtual WorkingBeatmap CreateNewDifficulty ( BeatmapSetInfo targetBeatmapSet , WorkingBeatmap referenceWorkingBeatmap , RulesetInfo rulesetInfo )
2022-01-23 17:49:17 +01:00
{
2022-02-14 20:56:05 +01:00
var playableBeatmap = referenceWorkingBeatmap . GetPlayableBeatmap ( rulesetInfo ) ;
2022-01-23 17:49:17 +01:00
2022-02-16 23:12:13 +01:00
var newBeatmapInfo = new BeatmapInfo ( rulesetInfo , new BeatmapDifficulty ( ) , playableBeatmap . Metadata . DeepClone ( ) )
{
DifficultyName = NamingUtils . GetNextBestName ( targetBeatmapSet . Beatmaps . Select ( b = > b . DifficultyName ) , "New Difficulty" )
} ;
2022-02-14 20:56:05 +01:00
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo } ;
foreach ( var timingPoint in playableBeatmap . ControlPointInfo . TimingPoints )
newBeatmap . ControlPointInfo . Add ( timingPoint . Time , timingPoint . DeepClone ( ) ) ;
return addDifficultyToSet ( targetBeatmapSet , newBeatmap , referenceWorkingBeatmap . Skin ) ;
}
/// <summary>
/// Add a copy of the provided <paramref name="referenceWorkingBeatmap"/> to the provided <paramref name="targetBeatmapSet"/>.
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
/// and represented by the returned <see cref="WorkingBeatmap"/>.
/// </summary>
/// <remarks>
/// Contrary to <see cref="CreateNewDifficulty"/>, this method creates a nearly-exact copy of <paramref name="referenceWorkingBeatmap"/>
/// (with the exception of a few key properties that cannot be copied under any circumstance, like difficulty name, beatmap hash, or online status).
/// </remarks>
/// <param name="targetBeatmapSet">The <see cref="BeatmapSetInfo"/> to add the copy to.</param>
/// <param name="referenceWorkingBeatmap">The <see cref="WorkingBeatmap"/> to be copied.</param>
public virtual WorkingBeatmap CopyExistingDifficulty ( BeatmapSetInfo targetBeatmapSet , WorkingBeatmap referenceWorkingBeatmap )
{
var newBeatmap = referenceWorkingBeatmap . GetPlayableBeatmap ( referenceWorkingBeatmap . BeatmapInfo . Ruleset ) . Clone ( ) ;
2022-02-06 18:40:51 +01:00
BeatmapInfo newBeatmapInfo ;
2022-02-06 17:52:59 +01:00
2022-02-14 20:56:05 +01:00
newBeatmap . BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap . BeatmapInfo . Clone ( ) ;
// assign a new ID to the clone.
newBeatmapInfo . ID = Guid . NewGuid ( ) ;
2022-02-16 23:12:13 +01:00
// add "(copy)" suffix to difficulty name, and additionally ensure that it doesn't conflict with any other potentially pre-existing copies.
newBeatmapInfo . DifficultyName = NamingUtils . GetNextBestName (
targetBeatmapSet . Beatmaps . Select ( b = > b . DifficultyName ) ,
$"{newBeatmapInfo.DifficultyName} (copy)" ) ;
2022-02-14 20:56:05 +01:00
// clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.
newBeatmapInfo . Hash = string . Empty ;
// clear online properties.
newBeatmapInfo . OnlineID = - 1 ;
newBeatmapInfo . Status = BeatmapOnlineStatus . None ;
return addDifficultyToSet ( targetBeatmapSet , newBeatmap , referenceWorkingBeatmap . Skin ) ;
}
2022-01-23 17:49:17 +01:00
2022-02-14 20:56:05 +01:00
private WorkingBeatmap addDifficultyToSet ( BeatmapSetInfo targetBeatmapSet , IBeatmap newBeatmap , ISkin beatmapSkin )
{
2022-02-06 18:40:51 +01:00
// populate circular beatmap set info <-> beatmap info references manually.
2022-06-16 12:01:53 +02:00
// several places like `Save()` or `GetWorkingBeatmap()`
2022-02-06 18:40:51 +01:00
// rely on them being freely traversable in both directions for correct operation.
2022-02-14 20:56:05 +01:00
targetBeatmapSet . Beatmaps . Add ( newBeatmap . BeatmapInfo ) ;
newBeatmap . BeatmapInfo . BeatmapSet = targetBeatmapSet ;
2022-02-06 18:40:51 +01:00
2022-06-16 18:07:04 +09:00
Save ( newBeatmap . BeatmapInfo , newBeatmap , beatmapSkin ) ;
2022-01-23 18:56:19 +01:00
2022-02-06 17:52:59 +01:00
workingBeatmapCache . Invalidate ( targetBeatmapSet ) ;
2022-02-03 19:39:24 +09:00
return GetWorkingBeatmap ( newBeatmap . BeatmapInfo ) ;
2022-01-23 17:49:17 +01:00
}
2021-12-15 14:26:38 +09:00
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
public void Hide ( BeatmapInfo beatmapInfo )
2021-11-05 18:05:31 +09:00
{
2022-06-16 18:56:53 +09:00
Realm . Run ( r = >
2021-12-15 14:26:38 +09:00
{
2022-01-25 13:04:05 +09:00
using ( var transaction = r . BeginWrite ( ) )
2022-01-21 01:34:20 +09:00
{
if ( ! beatmapInfo . IsManaged )
2022-01-25 13:04:05 +09:00
beatmapInfo = r . Find < BeatmapInfo > ( beatmapInfo . ID ) ;
2022-01-11 22:55:00 +09:00
2022-01-21 01:34:20 +09:00
beatmapInfo . Hidden = true ;
transaction . Commit ( ) ;
}
} ) ;
2021-11-05 18:05:31 +09:00
}
2021-09-30 16:45:32 +09:00
/// <summary>
2021-12-15 14:26:38 +09:00
/// Restore a beatmap difficulty.
2021-09-30 16:45:32 +09:00
/// </summary>
2021-12-15 14:26:38 +09:00
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
public void Restore ( BeatmapInfo beatmapInfo )
2021-11-05 18:05:31 +09:00
{
2022-06-16 18:56:53 +09:00
Realm . Run ( r = >
2021-12-15 14:26:38 +09:00
{
2022-01-25 13:04:05 +09:00
using ( var transaction = r . BeginWrite ( ) )
2022-01-21 01:34:20 +09:00
{
if ( ! beatmapInfo . IsManaged )
2022-01-25 13:04:05 +09:00
beatmapInfo = r . Find < BeatmapInfo > ( beatmapInfo . ID ) ;
2022-01-11 22:55:00 +09:00
2022-01-21 01:34:20 +09:00
beatmapInfo . Hidden = false ;
transaction . Commit ( ) ;
}
} ) ;
2021-11-05 18:05:31 +09:00
}
2021-09-30 16:45:32 +09:00
2022-01-11 23:04:36 +09:00
public void RestoreAll ( )
{
2022-06-16 18:56:53 +09:00
Realm . Run ( r = >
2022-01-11 23:04:36 +09:00
{
2022-01-25 13:04:05 +09:00
using ( var transaction = r . BeginWrite ( ) )
2022-01-21 17:08:20 +09:00
{
2022-01-25 13:04:05 +09:00
foreach ( var beatmap in r . All < BeatmapInfo > ( ) . Where ( b = > b . Hidden ) )
2022-01-21 17:08:20 +09:00
beatmap . Hidden = false ;
2022-01-11 23:04:36 +09:00
2022-01-21 17:08:20 +09:00
transaction . Commit ( ) ;
}
} ) ;
2022-01-11 23:04:36 +09:00
}
2021-09-30 16:45:32 +09:00
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
2021-12-16 20:11:45 +09:00
public List < BeatmapSetInfo > GetAllUsableBeatmapSets ( )
2021-12-15 15:24:26 +09:00
{
2022-06-16 18:56:53 +09:00
return Realm . Run ( r = >
2022-01-21 17:33:26 +09:00
{
2022-01-25 13:04:05 +09:00
r . Refresh ( ) ;
return r . All < BeatmapSetInfo > ( ) . Where ( b = > ! b . DeletePending ) . Detach ( ) ;
2022-01-21 17:33:26 +09:00
} ) ;
2021-12-15 15:24:26 +09:00
}
2021-09-30 16:45:32 +09:00
/// <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>
2022-01-26 13:37:33 +09:00
public Live < BeatmapSetInfo > ? QueryBeatmapSet ( Expression < Func < BeatmapSetInfo , bool > > query )
2021-12-15 15:24:26 +09:00
{
2022-06-16 18:56:53 +09:00
return Realm . Run ( r = > r . All < BeatmapSetInfo > ( ) . FirstOrDefault ( query ) ? . ToLive ( Realm ) ) ;
2021-12-15 15:24:26 +09:00
}
2021-09-30 16:45:32 +09: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>
2022-06-16 18:56:53 +09:00
public BeatmapInfo ? QueryBeatmap ( Expression < Func < BeatmapInfo , bool > > query ) = > Realm . Run ( r = > r . All < BeatmapInfo > ( ) . FirstOrDefault ( query ) ? . Detach ( ) ) ;
2021-12-15 15:24:26 +09:00
2021-09-30 16:45:32 +09:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
2021-11-15 18:46:11 +09:00
public IWorkingBeatmap DefaultBeatmap = > workingBeatmapCache . DefaultBeatmap ;
2021-09-30 16:45:32 +09:00
2022-06-16 18:07:04 +09:00
/// <summary>
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
public virtual void Save ( BeatmapInfo beatmapInfo , IBeatmap beatmapContent , ISkin ? beatmapSkin = null )
{
var setInfo = beatmapInfo . BeatmapSet ;
Debug . Assert ( setInfo ! = null ) ;
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent . Difficulty . CopyTo ( beatmapInfo . Difficulty ) ;
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent . BeatmapInfo = beatmapInfo ;
using ( var stream = new MemoryStream ( ) )
{
using ( var sw = new StreamWriter ( stream , Encoding . UTF8 , 1024 , true ) )
new LegacyBeatmapEncoder ( beatmapContent , beatmapSkin ) . Encode ( sw ) ;
stream . Seek ( 0 , SeekOrigin . Begin ) ;
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = setInfo . Files . SingleOrDefault ( f = > string . Equals ( f . Filename , beatmapInfo . Path , StringComparison . OrdinalIgnoreCase ) ) ;
string targetFilename = createBeatmapFilenameFromMetadata ( beatmapInfo ) ;
// ensure that two difficulties from the set don't point at the same beatmap file.
if ( setInfo . Beatmaps . Any ( b = > b . ID ! = beatmapInfo . ID & & string . Equals ( b . Path , targetFilename , StringComparison . OrdinalIgnoreCase ) ) )
throw new InvalidOperationException ( $"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'." ) ;
if ( existingFileInfo ! = null )
DeleteFile ( setInfo , existingFileInfo ) ;
beatmapInfo . MD5Hash = stream . ComputeMD5Hash ( ) ;
beatmapInfo . Hash = stream . ComputeSHA2Hash ( ) ;
AddFile ( setInfo , stream , createBeatmapFilenameFromMetadata ( beatmapInfo ) ) ;
2022-07-08 02:31:33 +09:00
setInfo . Hash = beatmapImporter . ComputeHash ( setInfo ) ;
2022-06-24 18:27:47 +09:00
Realm . Write ( r = >
{
var liveBeatmapSet = r . Find < BeatmapSetInfo > ( setInfo . ID ) ;
2022-06-16 18:07:04 +09:00
2022-06-24 18:27:47 +09:00
setInfo . CopyChangesToRealm ( liveBeatmapSet ) ;
2022-06-16 18:07:04 +09:00
2022-07-04 18:13:53 +09:00
ProcessBeatmap ? . Invoke ( liveBeatmapSet ) ;
2022-06-24 18:27:47 +09:00
} ) ;
}
2022-06-20 18:57:57 +09:00
2022-06-24 18:27:47 +09:00
Debug . Assert ( beatmapInfo . BeatmapSet ! = null ) ;
2022-06-20 18:57:57 +09:00
2022-06-16 18:07:04 +09:00
static string createBeatmapFilenameFromMetadata ( BeatmapInfo beatmapInfo )
{
var metadata = beatmapInfo . Metadata ;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu" . GetValidArchiveContentFilename ( ) ;
}
}
2022-06-06 20:12:18 +09:00
public void DeleteAllVideos ( )
2022-05-18 01:09:58 -05:00
{
2022-06-16 18:56:53 +09:00
Realm . Write ( r = >
2022-05-18 01:09:58 -05:00
{
var items = r . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
2022-06-16 18:07:04 +09:00
DeleteVideos ( items . ToList ( ) ) ;
2022-05-18 01:09:58 -05:00
} ) ;
}
2022-06-16 18:26:13 +09:00
public void Delete ( Expression < Func < BeatmapSetInfo , bool > > ? filter = null , bool silent = false )
{
2022-06-16 18:56:53 +09:00
Realm . Run ( r = >
2022-06-16 18:26:13 +09:00
{
var items = r . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & ! s . Protected ) ;
if ( filter ! = null )
items = items . Where ( filter ) ;
2022-06-16 18:53:13 +09:00
Delete ( items . ToList ( ) , silent ) ;
2022-06-16 18:26:13 +09:00
} ) ;
}
2022-06-16 18:07:04 +09:00
/// <summary>
/// Delete videos from a list of beatmaps.
/// This will post notifications tracking progress.
/// </summary>
public void DeleteVideos ( List < BeatmapSetInfo > items , bool silent = false )
{
if ( items . Count = = 0 ) return ;
var notification = new ProgressNotification
{
Progress = 0 ,
2022-06-16 19:05:25 +09:00
Text = $"Preparing to delete all {HumanisedModelName} videos..." ,
2022-06-16 18:07:04 +09:00
CompletionText = "No videos found to delete!" ,
State = ProgressNotificationState . Active ,
} ;
if ( ! silent )
PostNotification ? . Invoke ( notification ) ;
int i = 0 ;
int deleted = 0 ;
foreach ( var b in items )
{
if ( notification . State = = ProgressNotificationState . Cancelled )
// user requested abort
return ;
var video = b . Files . FirstOrDefault ( f = > OsuGameBase . VIDEO_EXTENSIONS . Any ( ex = > f . Filename . EndsWith ( ex , StringComparison . Ordinal ) ) ) ;
if ( video ! = null )
{
DeleteFile ( b , video ) ;
deleted + + ;
2022-06-16 19:05:25 +09:00
notification . CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!" ;
2022-06-16 18:07:04 +09:00
}
2022-06-16 19:05:25 +09:00
notification . Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)" ;
2022-06-16 18:07:04 +09:00
notification . Progress = ( float ) + + i / items . Count ;
}
notification . State = ProgressNotificationState . Completed ;
}
2022-01-11 23:04:36 +09:00
public void UndeleteAll ( )
{
2022-06-16 18:56:53 +09:00
Realm . Run ( r = > Undelete ( r . All < BeatmapSetInfo > ( ) . Where ( s = > s . DeletePending ) . ToList ( ) ) ) ;
2022-01-11 23:04:36 +09:00
}
2021-09-30 16:45:32 +09:00
#region Implementation of ICanAcceptFiles
2022-06-16 18:07:04 +09:00
public Task Import ( params string [ ] paths ) = > beatmapImporter . Import ( paths ) ;
2021-09-30 16:45:32 +09:00
2022-06-16 18:07:04 +09:00
public Task Import ( params ImportTask [ ] tasks ) = > beatmapImporter . Import ( tasks ) ;
2021-09-30 16:45:32 +09:00
2022-06-16 18:07:04 +09:00
public Task < IEnumerable < Live < BeatmapSetInfo > > > Import ( ProgressNotification notification , params ImportTask [ ] tasks ) = > beatmapImporter . Import ( notification , tasks ) ;
2021-09-30 16:45:32 +09:00
2022-06-16 18:07:04 +09:00
public Task < Live < BeatmapSetInfo > ? > Import ( ImportTask task , bool batchImport = false , CancellationToken cancellationToken = default ) = >
beatmapImporter . Import ( task , batchImport , cancellationToken ) ;
2021-09-30 16:45:32 +09:00
2022-06-16 18:07:04 +09:00
public Live < BeatmapSetInfo > ? Import ( BeatmapSetInfo item , ArchiveReader ? archive = null , CancellationToken cancellationToken = default ) = >
2022-06-20 15:18:07 +09:00
beatmapImporter . ImportModel ( item , archive , false , cancellationToken ) ;
2021-09-30 16:45:32 +09:00
2022-06-16 18:07:04 +09:00
public IEnumerable < string > HandledExtensions = > beatmapImporter . HandledExtensions ;
2021-09-30 16:45:32 +09:00
#endregion
#region Implementation of IWorkingBeatmapCache
2022-06-24 21:02:14 +09: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="refetch">Whether to force a refetch from the database to ensure <see cref="BeatmapInfo"/> is up-to-date.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
2022-06-30 16:46:28 +09:00
public WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo ? beatmapInfo , bool refetch = false )
2022-01-19 00:49:16 +09:00
{
2022-06-30 16:46:28 +09:00
if ( beatmapInfo ! = null )
2022-01-19 00:49:16 +09:00
{
2022-06-30 16:46:28 +09:00
// Detached sets don't come with files.
// If we seem to be missing files, now is a good time to re-fetch.
if ( refetch | | beatmapInfo . IsManaged | | beatmapInfo . BeatmapSet ? . Files . Count = = 0 )
{
workingBeatmapCache . Invalidate ( beatmapInfo ) ;
2022-01-19 00:49:16 +09:00
2022-06-30 16:46:28 +09:00
Guid id = beatmapInfo . ID ;
beatmapInfo = Realm . Run ( r = > r . Find < BeatmapInfo > ( id ) ? . Detach ( ) ) ? ? beatmapInfo ;
}
2022-01-19 00:49:16 +09:00
2022-06-30 16:46:28 +09:00
Debug . Assert ( beatmapInfo . IsManaged ! = true ) ;
}
2022-06-24 19:04:29 +09:00
2022-06-24 17:46:46 +09:00
return workingBeatmapCache . GetWorkingBeatmap ( beatmapInfo ) ;
2022-01-19 00:49:16 +09:00
}
2021-09-30 16:45:32 +09:00
2022-06-24 21:02:14 +09:00
WorkingBeatmap IWorkingBeatmapCache . GetWorkingBeatmap ( BeatmapInfo beatmapInfo ) = > GetWorkingBeatmap ( beatmapInfo ) ;
2021-10-06 12:05:30 +09:00
void IWorkingBeatmapCache . Invalidate ( BeatmapSetInfo beatmapSetInfo ) = > workingBeatmapCache . Invalidate ( beatmapSetInfo ) ;
void IWorkingBeatmapCache . Invalidate ( BeatmapInfo beatmapInfo ) = > workingBeatmapCache . Invalidate ( beatmapInfo ) ;
2022-06-20 19:48:46 +09:00
public event Action < WorkingBeatmap > ? OnInvalidated
{
add = > workingBeatmapCache . OnInvalidated + = value ;
remove = > workingBeatmapCache . OnInvalidated - = value ;
}
2022-06-16 19:05:25 +09:00
public override bool IsAvailableLocally ( BeatmapSetInfo model ) = > Realm . Run ( realm = > realm . All < BeatmapSetInfo > ( ) . Any ( s = > s . OnlineID = = model . OnlineID ) ) ;
2021-09-30 16:45:32 +09:00
#endregion
2021-10-15 16:00:09 +09:00
#region Implementation of IPostImports < out BeatmapSetInfo >
2022-06-20 18:21:37 +09:00
public Action < IEnumerable < Live < BeatmapSetInfo > > > ? PresentImport
2021-10-15 16:00:09 +09:00
{
2022-06-20 18:21:37 +09:00
set = > beatmapImporter . PresentImport = value ;
2021-10-15 16:00:09 +09:00
}
#endregion
2022-06-16 19:05:25 +09:00
public override string HumanisedModelName = > "beatmap" ;
2021-09-30 16:45:32 +09:00
}
2017-07-27 16:56:41 +09:00
}