Skip to content

Commit

Permalink
Merge pull request ppy#11705 from nanaya/score-index
Browse files Browse the repository at this point in the history
Add api endpoint to list latest scores
  • Loading branch information
notbakaneko authored Dec 5, 2024
2 parents ae59132 + b0c22d7 commit 3e0b5ab
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 3 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ CLIENT_CHECK_VERSION=false
# SCORES_EXPERIMENTAL_RANK_AS_EXTRA=false
# SCORES_PROCESSING_QUEUE=osu-queue:score-statistics
# SCORES_SUBMISSION_ENABLED=1
# SCORE_INDEX_MAX_ID_DISTANCE=10_000_000

# BANCHO_BOT_USER_ID=

Expand Down
90 changes: 89 additions & 1 deletion app/Http/Controllers/ScoresController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace App\Http\Controllers;

use App\Enums\Ruleset;
use App\Models\Beatmap;
use App\Models\Score\Best\Model as ScoreBest;
use App\Models\ScoreReplayStats;
use App\Models\Solo\Score as SoloScore;
Expand All @@ -22,8 +23,9 @@ public function __construct()
parent::__construct();

$this->middleware('auth', ['except' => [
'show',
'download',
'index',
'show',
]]);

$this->middleware('require-scopes:public');
Expand Down Expand Up @@ -117,6 +119,92 @@ public function download($rulesetOrSoloId, $id = null)
}, $this->makeReplayFilename($score), $responseHeaders);
}

/**
* Get Scores
*
* Returns submitted scores. Up to 1000 scores will be returned in order of oldest to latest.
* Most recent scores will be returned if `cursor_string` parameter is not specified.
*
* Obtaining new scores that arrived after the last request can be done by passing `cursor_string`
* parameter from the previous request.
*
* ---
*
* ### Response Format
*
* Field | Type | Notes
* ------------- | ----------------------------- | -----
* scores | [Score](#score)[] | |
* cursor_string | [CursorString](#cursorstring) | Same value as the request will be returned if there's no new scores
*
* @group Scores
*
* @queryParam ruleset The [Ruleset](#ruleset) to get scores for.
* @queryParam cursor_string Next set of scores
*/
public function index()
{
$params = \Request::all();
$cursor = cursor_from_params($params);
$isOldScores = false;
if (isset($cursor['id']) && ($idFromCursor = get_int($cursor['id'])) !== null) {
$currentMaxId = SoloScore::max('id');
$idDistance = $currentMaxId - $idFromCursor;
if ($idDistance > $GLOBALS['cfg']['osu']['scores']['index_max_id_distance']) {
abort(422, 'cursor is too old');
}
$isOldScores = $idDistance > 10_000;
}

$rulesetId = null;
if (isset($params['ruleset'])) {
$rulesetId = Beatmap::modeInt(get_string($params['ruleset']));

if ($rulesetId === null) {
abort(422, 'invalid ruleset parameter');
}
}

return \Cache::remember(
'score_index:'.($rulesetId ?? '').':'.json_encode($cursor),
$isOldScores ? 600 : 5,
function () use ($cursor, $isOldScores, $rulesetId) {
$cursorHelper = SoloScore::makeDbCursorHelper('old');
$scoresQuery = SoloScore::forListing()->limit(1_000);
if ($rulesetId !== null) {
$scoresQuery->where('ruleset_id', $rulesetId);
}
if ($cursor === null || $cursorHelper->prepare($cursor) === null) {
// fetch the latest scores when no or invalid cursor is specified
// and reverse result to match the other query (latest score last)
$scores = array_reverse($scoresQuery->orderByDesc('id')->get()->all());
} else {
$scores = $scoresQuery->cursorSort($cursorHelper, $cursor)->get()->all();
}

if ($isOldScores) {
$filteredScores = $scores;
} else {
$filteredScores = [];
foreach ($scores as $score) {
// only return up to but not including the earliest unprocessed scores
if ($score->isProcessed()) {
$filteredScores[] = $score;
} else {
break;
}
}
}

return [
'scores' => json_collection($filteredScores, new ScoreTransformer(ScoreTransformer::TYPE_SOLO)),
// return previous cursor if no result, assuming there's no new scores yet
...cursor_for_response($cursorHelper->next($filteredScores) ?? $cursor),
];
},
);
}

public function show($rulesetOrSoloId, $legacyId = null)
{
if ($legacyId === null) {
Expand Down
28 changes: 27 additions & 1 deletion app/Models/Solo/Score.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@
*/
class Score extends Model implements Traits\ReportableInterface
{
use Traits\Reportable, Traits\WithWeightedPp;
use Traits\Reportable, Traits\WithDbCursorHelper, Traits\WithWeightedPp;

const DEFAULT_SORT = 'old';

const SORTS = [
'old' => [['column' => 'id', 'order' => 'ASC']],
];

public $timestamps = false;

Expand Down Expand Up @@ -163,6 +169,13 @@ public function scopeDefault(Builder $query): Builder
return $query->whereHas('beatmap.beatmapset');
}

public function scopeForListing(Builder $query): Builder
{
return $query->where('ranked', true)
->leftJoinRelation('processHistory')
->select([$query->qualifyColumn('*'), 'processed_version']);
}

public function scopeForRuleset(Builder $query, string $ruleset): Builder
{
return $query->where('ruleset_id', Beatmap::MODES[$ruleset]);
Expand Down Expand Up @@ -364,6 +377,19 @@ public function isPerfectLegacyCombo(): ?bool
return null;
}

public function isProcessed(): bool
{
if ($this->legacy_score_id !== null) {
return true;
}

if (array_key_exists('processed_version', $this->attributes)) {
return $this->attributes['processed_version'] !== null;
}

return $this->processHistory !== null;
}

public function legacyScore(): ?LegacyScore\Best\Model
{
$id = $this->legacy_score_id;
Expand Down
2 changes: 1 addition & 1 deletion app/Transformers/ScoreTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function transformSolo(MultiplayerScoreLink|ScoreModel|SoloScore $score)
if ($score instanceof SoloScore) {
$extraAttributes['classic_total_score'] = $score->getClassicTotalScore();
$extraAttributes['preserve'] = $score->preserve;
$extraAttributes['processed'] = $score->legacy_score_id !== null || $score->processHistory !== null;
$extraAttributes['processed'] = $score->isProcessed();
$extraAttributes['ranked'] = $score->ranked;
}

Expand Down
1 change: 1 addition & 0 deletions config/osu.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
'es_cache_duration' => 60 * (get_float(env('SCORES_ES_CACHE_DURATION')) ?? 0.5), // in minutes, converted to seconds
'experimental_rank_as_default' => get_bool(env('SCORES_EXPERIMENTAL_RANK_AS_DEFAULT')) ?? false,
'experimental_rank_as_extra' => get_bool(env('SCORES_EXPERIMENTAL_RANK_AS_EXTRA')) ?? false,
'index_max_id_distance' => get_int(env('SCORE_INDEX_MAX_ID_DISTANCE')) ?? 10_000_000,
'processing_queue' => presence(env('SCORES_PROCESSING_QUEUE')) ?? 'osu-queue:score-statistics',
'submission_enabled' => get_bool(env('SCORES_SUBMISSION_ENABLED')) ?? true,
],
Expand Down
2 changes: 2 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,8 @@
Route::get('{rulesetOrScore}/{score}/download', 'ScoresController@download')->middleware(ThrottleRequests::getApiThrottle('scores_download'))->name('download-legacy');

Route::get('{rulesetOrScore}/{score?}', 'ScoresController@show')->name('show');

Route::get('/', 'ScoresController@index');
});

// Beatmapsets
Expand Down
16 changes: 16 additions & 0 deletions tests/api_routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,22 @@
"public"
]
},
{
"uri": "api/v2/scores",
"methods": [
"GET",
"HEAD"
],
"controller": "App\\Http\\Controllers\\ScoresController@index",
"middlewares": [
"App\\Http\\Middleware\\ThrottleRequests:1200,1,api:",
"App\\Http\\Middleware\\RequireScopes",
"App\\Http\\Middleware\\RequireScopes:public"
],
"scopes": [
"public"
]
},
{
"uri": "api/v2/beatmapsets/search",
"methods": [
Expand Down

0 comments on commit 3e0b5ab

Please sign in to comment.