2021-09-30 13:46:01 +08: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.
2022-06-17 15:37:17 +08:00
#nullable disable
2021-09-30 13:46:01 +08:00
using System ;
2021-11-24 11:16:08 +08:00
using System.Diagnostics ;
2021-09-30 13:46:01 +08:00
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using Microsoft.Data.Sqlite ;
using osu.Framework.Development ;
using osu.Framework.IO.Network ;
using osu.Framework.Logging ;
using osu.Framework.Platform ;
using osu.Framework.Testing ;
using osu.Framework.Threading ;
using osu.Game.Database ;
using osu.Game.Online.API ;
using osu.Game.Online.API.Requests ;
using SharpCompress.Compressors ;
using SharpCompress.Compressors.BZip2 ;
namespace osu.Game.Beatmaps
{
2021-09-30 13:46:07 +08:00
/// <summary>
/// A component which handles population of online IDs for beatmaps using a two part lookup procedure.
/// </summary>
/// <remarks>
2021-10-01 00:51:29 +08:00
/// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>) will be downloaded if not already present locally.
2021-09-30 13:46:07 +08:00
/// This will always be checked before doing a second online query to get required metadata.
/// </remarks>
2021-09-30 13:46:01 +08:00
[ExcludeFromDynamicCompile]
public class BeatmapOnlineLookupQueue : IDisposable
{
private readonly IAPIProvider api ;
private readonly Storage storage ;
private const int update_queue_request_concurrency = 4 ;
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler ( update_queue_request_concurrency , nameof ( BeatmapOnlineLookupQueue ) ) ;
private FileWebRequest cacheDownloadRequest ;
private const string cache_database_name = "online.db" ;
public BeatmapOnlineLookupQueue ( IAPIProvider api , Storage storage )
{
this . api = api ;
this . storage = storage ;
// avoid downloading / using cache for unit tests.
if ( ! DebugUtils . IsNUnitRunning & & ! storage . Exists ( cache_database_name ) )
prepareLocalCache ( ) ;
}
2022-01-14 12:19:00 +08:00
public void Update ( BeatmapSetInfo beatmapSet )
2022-01-13 15:13:30 +08:00
{
foreach ( var b in beatmapSet . Beatmaps )
lookup ( beatmapSet , b ) ;
}
2021-09-30 13:46:01 +08:00
public Task UpdateAsync ( BeatmapSetInfo beatmapSet , CancellationToken cancellationToken )
{
return Task . WhenAll ( beatmapSet . Beatmaps . Select ( b = > UpdateAsync ( beatmapSet , b , cancellationToken ) ) . ToArray ( ) ) ;
}
// todo: expose this when we need to do individual difficulty lookups.
2021-10-02 23:55:29 +08:00
protected Task UpdateAsync ( BeatmapSetInfo beatmapSet , BeatmapInfo beatmapInfo , CancellationToken cancellationToken )
= > Task . Factory . StartNew ( ( ) = > lookup ( beatmapSet , beatmapInfo ) , cancellationToken , TaskCreationOptions . HideScheduler | TaskCreationOptions . RunContinuationsAsynchronously , updateScheduler ) ;
2021-09-30 13:46:01 +08:00
2021-10-02 23:55:29 +08:00
private void lookup ( BeatmapSetInfo set , BeatmapInfo beatmapInfo )
2021-09-30 13:46:01 +08:00
{
2021-10-02 23:55:29 +08:00
if ( checkLocalCache ( set , beatmapInfo ) )
2021-09-30 13:46:01 +08:00
return ;
if ( api ? . State . Value ! = APIState . Online )
return ;
2021-10-02 23:55:29 +08:00
var req = new GetBeatmapRequest ( beatmapInfo ) ;
2021-09-30 13:46:01 +08:00
try
{
// intentionally blocking to limit web request concurrency
api . Perform ( req ) ;
2022-01-13 15:13:30 +08:00
if ( req . CompletionState = = APIRequestCompletionState . Failed )
{
logForModel ( set , $"Online retrieval failed for {beatmapInfo}" ) ;
beatmapInfo . OnlineID = - 1 ;
2022-01-14 12:20:51 +08:00
return ;
2022-01-13 15:13:30 +08:00
}
2021-10-05 13:28:56 +08:00
var res = req . Response ;
2021-09-30 13:46:01 +08:00
if ( res ! = null )
{
2021-10-02 23:55:29 +08:00
beatmapInfo . Status = res . Status ;
2021-11-24 11:16:08 +08:00
Debug . Assert ( beatmapInfo . BeatmapSet ! = null ) ;
2021-11-24 17:42:47 +08:00
beatmapInfo . BeatmapSet . Status = res . BeatmapSet ? . Status ? ? BeatmapOnlineStatus . None ;
2021-11-12 16:50:31 +08:00
beatmapInfo . BeatmapSet . OnlineID = res . OnlineBeatmapSetID ;
2022-07-19 18:37:04 +08:00
beatmapInfo . BeatmapSet . DateRanked = res . BeatmapSet ? . Ranked ;
beatmapInfo . BeatmapSet . DateSubmitted = res . BeatmapSet ? . Submitted ;
2022-07-19 18:39:51 +08:00
2022-06-20 17:59:08 +08:00
beatmapInfo . OnlineMD5Hash = res . MD5Hash ;
2022-07-19 18:39:51 +08:00
beatmapInfo . LastOnlineUpdate = res . LastUpdated ;
2021-11-12 16:45:05 +08:00
beatmapInfo . OnlineID = res . OnlineID ;
2021-09-30 13:46:01 +08:00
2021-11-24 11:16:08 +08:00
beatmapInfo . Metadata . Author . OnlineID = res . AuthorID ;
2021-09-30 13:46:01 +08:00
2021-10-21 18:14:31 +08:00
logForModel ( set , $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}." ) ;
2021-09-30 13:46:01 +08:00
}
}
catch ( Exception e )
{
2021-10-02 23:55:29 +08:00
logForModel ( set , $"Online retrieval failed for {beatmapInfo} ({e.Message})" ) ;
2022-01-13 15:13:30 +08:00
beatmapInfo . OnlineID = - 1 ;
2021-09-30 13:46:01 +08:00
}
}
private void prepareLocalCache ( )
{
string cacheFilePath = storage . GetFullPath ( cache_database_name ) ;
string compressedCacheFilePath = $"{cacheFilePath}.bz2" ;
cacheDownloadRequest = new FileWebRequest ( compressedCacheFilePath , $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}" ) ;
cacheDownloadRequest . Failed + = ex = >
{
File . Delete ( compressedCacheFilePath ) ;
File . Delete ( cacheFilePath ) ;
Logger . Log ( $"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}" , LoggingTarget . Database ) ;
} ;
cacheDownloadRequest . Finished + = ( ) = >
{
try
{
using ( var stream = File . OpenRead ( cacheDownloadRequest . Filename ) )
using ( var outStream = File . OpenWrite ( cacheFilePath ) )
using ( var bz2 = new BZip2Stream ( stream , CompressionMode . Decompress , false ) )
bz2 . CopyTo ( outStream ) ;
// set to null on completion to allow lookups to begin using the new source
cacheDownloadRequest = null ;
}
catch ( Exception ex )
{
Logger . Log ( $"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}" , LoggingTarget . Database ) ;
File . Delete ( cacheFilePath ) ;
}
finally
{
File . Delete ( compressedCacheFilePath ) ;
}
} ;
2022-05-09 11:15:54 +08:00
Task . Run ( async ( ) = >
{
try
{
await cacheDownloadRequest . PerformAsync ( ) ;
}
catch
{
// Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway.
}
} ) ;
2021-09-30 13:46:01 +08:00
}
2021-10-02 23:55:29 +08:00
private bool checkLocalCache ( BeatmapSetInfo set , BeatmapInfo beatmapInfo )
2021-09-30 13:46:01 +08:00
{
// download is in progress (or was, and failed).
if ( cacheDownloadRequest ! = null )
return false ;
// database is unavailable.
if ( ! storage . Exists ( cache_database_name ) )
return false ;
2021-10-02 23:55:29 +08:00
if ( string . IsNullOrEmpty ( beatmapInfo . MD5Hash )
& & string . IsNullOrEmpty ( beatmapInfo . Path )
2021-11-22 13:55:41 +08:00
& & beatmapInfo . OnlineID < = 0 )
2021-09-30 13:46:01 +08:00
return false ;
try
{
using ( var db = new SqliteConnection ( DatabaseContextFactory . CreateDatabaseConnectionString ( "online.db" , storage ) ) )
{
db . Open ( ) ;
using ( var cmd = db . CreateCommand ( ) )
{
2022-07-19 18:37:04 +08:00
cmd . CommandText =
"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path" ;
2021-09-30 13:46:01 +08:00
2021-10-02 23:55:29 +08:00
cmd . Parameters . Add ( new SqliteParameter ( "@MD5Hash" , beatmapInfo . MD5Hash ) ) ;
2021-11-22 13:55:41 +08:00
cmd . Parameters . Add ( new SqliteParameter ( "@OnlineID" , beatmapInfo . OnlineID ) ) ;
2021-10-02 23:55:29 +08:00
cmd . Parameters . Add ( new SqliteParameter ( "@Path" , beatmapInfo . Path ) ) ;
2021-09-30 13:46:01 +08:00
using ( var reader = cmd . ExecuteReader ( ) )
{
if ( reader . Read ( ) )
{
2021-11-24 17:42:47 +08:00
var status = ( BeatmapOnlineStatus ) reader . GetByte ( 2 ) ;
2021-09-30 13:46:01 +08:00
2021-10-02 23:55:29 +08:00
beatmapInfo . Status = status ;
2021-11-24 11:16:08 +08:00
Debug . Assert ( beatmapInfo . BeatmapSet ! = null ) ;
2021-10-02 23:55:29 +08:00
beatmapInfo . BeatmapSet . Status = status ;
2021-11-12 16:50:31 +08:00
beatmapInfo . BeatmapSet . OnlineID = reader . GetInt32 ( 0 ) ;
2022-07-19 18:37:04 +08:00
// TODO: DateSubmitted and DateRanked are not provided by local cache.
2021-11-12 16:45:05 +08:00
beatmapInfo . OnlineID = reader . GetInt32 ( 1 ) ;
2021-11-24 11:16:08 +08:00
beatmapInfo . Metadata . Author . OnlineID = reader . GetInt32 ( 3 ) ;
2022-07-19 18:39:51 +08:00
2022-06-20 17:59:08 +08:00
beatmapInfo . OnlineMD5Hash = reader . GetString ( 4 ) ;
2022-07-19 18:39:51 +08:00
beatmapInfo . LastOnlineUpdate = reader . GetDateTimeOffset ( 5 ) ;
2021-09-30 13:46:01 +08:00
2021-10-02 23:55:29 +08:00
logForModel ( set , $"Cached local retrieval for {beatmapInfo}." ) ;
2021-09-30 13:46:01 +08:00
return true ;
}
}
}
}
}
catch ( Exception ex )
{
2021-10-02 23:55:29 +08:00
logForModel ( set , $"Cached local retrieval for {beatmapInfo} failed with {ex}." ) ;
2021-09-30 13:46:01 +08:00
}
return false ;
}
private void logForModel ( BeatmapSetInfo set , string message ) = >
2021-11-24 11:16:08 +08:00
RealmArchiveModelImporter < BeatmapSetInfo > . LogForModel ( set , $"[{nameof(BeatmapOnlineLookupQueue)}] {message}" ) ;
2021-09-30 13:46:01 +08:00
public void Dispose ( )
{
cacheDownloadRequest ? . Dispose ( ) ;
updateScheduler ? . Dispose ( ) ;
}
}
}