diff --git a/app/Http/Controllers/ScoresController.php b/app/Http/Controllers/ScoresController.php index d91ba1c89ad..083cc3aa4f8 100644 --- a/app/Http/Controllers/ScoresController.php +++ b/app/Http/Controllers/ScoresController.php @@ -7,6 +7,7 @@ use App\Enums\Ruleset; use App\Models\Score\Best\Model as ScoreBest; +use App\Models\ScoreReplayStats; use App\Models\Solo\Score as SoloScore; use App\Transformers\ScoreTransformer; use App\Transformers\UserCompactTransformer; @@ -58,6 +59,8 @@ public function download($rulesetOrSoloId, $id = null) ::where('score_id', $id) ->where('replay', true) ->firstOrFail(); + + $soloScore = SoloScore::firstWhere('legacy_score_id', $score->getKey()); } $file = $score->getReplayFile(); @@ -89,6 +92,12 @@ public function download($rulesetOrSoloId, $id = null) ->firstOrCreate([], ['play_count' => 0]) ->incrementInstance('play_count'); } + + if ($soloScore !== null) { + ScoreReplayStats + ::createOrFirst(['score_id' => $soloScore->getKey()], ['user_id' => $soloScore->user_id]) + ->incrementInstance('watch_count'); + } } } diff --git a/app/Models/ScoreReplayStats.php b/app/Models/ScoreReplayStats.php new file mode 100644 index 00000000000..25dc7d59d17 --- /dev/null +++ b/app/Models/ScoreReplayStats.php @@ -0,0 +1,14 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +namespace App\Models; + +class ScoreReplayStats extends Model +{ + public $incrementing = false; + protected $primaryKey = 'score_id'; +} diff --git a/database/migrations/2024_10_04_082317_create_score_replay_stats.php b/database/migrations/2024_10_04_082317_create_score_replay_stats.php new file mode 100644 index 00000000000..0fbd51aa755 --- /dev/null +++ b/database/migrations/2024_10_04_082317_create_score_replay_stats.php @@ -0,0 +1,30 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + public function up(): void + { + Schema::create('score_replay_stats', function (Blueprint $table) { + $table->bigInteger('score_id')->unsigned()->primary(); + $table->bigInteger('user_id')->unsigned(); + $table->integer('watch_count')->unsigned()->default(0); + $table->index(['user_id', 'watch_count']); + $table->index('watch_count'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('score_replay_stats'); + } +}; diff --git a/tests/Controllers/ScoresControllerTest.php b/tests/Controllers/ScoresControllerTest.php index 95fe1b834b3..9aed65cdb09 100644 --- a/tests/Controllers/ScoresControllerTest.php +++ b/tests/Controllers/ScoresControllerTest.php @@ -7,6 +7,7 @@ use App\Models\OAuth\Client; use App\Models\Score\Best\Osu; +use App\Models\ScoreReplayStats; use App\Models\Solo\Score as SoloScore; use App\Models\User; use App\Models\UserStatistics; @@ -17,6 +18,7 @@ class ScoresControllerTest extends TestCase { private Osu $score; + private SoloScore $soloScore; private User $user; private User $otherUser; @@ -25,6 +27,11 @@ private static function getLegacyScoreReplayViewCount(Osu $score): int return $score->replayViewCount()->first()?->play_count ?? 0; } + private static function getScoreReplayViewCount(SoloScore $score): int + { + return ScoreReplayStats::find($score->getKey())?->watch_count ?? 0; + } + private static function getUserReplaysWatchedCount(Osu|SoloScore $score): int { $month = format_month_column(new \DateTime()); @@ -40,6 +47,7 @@ private static function getUserReplayPopularity(Osu|SoloScore $score): int public function testDownloadApiSameUser() { $this->expectCountChange(fn () => static::getLegacyScoreReplayViewCount($this->score), 0); + $this->expectCountChange(fn () => static::getScoreReplayViewCount($this->soloScore), 0); $this->expectCountChange(fn () => static::getUserReplayPopularity($this->score), 0); $this->expectCountChange(fn () => static::getUserReplaysWatchedCount($this->score), 0); @@ -73,6 +81,7 @@ public function testDownloadApiSoloScoreSameUser() public function testDownload() { $this->expectCountChange(fn () => static::getLegacyScoreReplayViewCount($this->score), 0); + $this->expectCountChange(fn () => static::getScoreReplayViewCount($this->soloScore), 0); $this->expectCountChange(fn () => static::getUserReplayPopularity($this->score), 0); $this->expectCountChange(fn () => static::getUserReplaysWatchedCount($this->score), 0); @@ -89,6 +98,7 @@ public function testDownload() public function testDownloadApi(): void { $this->expectCountChange(fn () => static::getLegacyScoreReplayViewCount($this->score), 1); + $this->expectCountChange(fn () => static::getScoreReplayViewCount($this->soloScore), 1); $this->expectCountChange(fn () => static::getUserReplayPopularity($this->score), 1); $this->expectCountChange(fn () => static::getUserReplaysWatchedCount($this->score), 1); @@ -112,6 +122,7 @@ public function testDownloadApiTwiceNoCountChange(): void ->assertSuccessful(); $this->expectCountChange(fn () => static::getLegacyScoreReplayViewCount($this->score), 0); + $this->expectCountChange(fn () => static::getScoreReplayViewCount($this->soloScore), 0); $this->expectCountChange(fn () => static::getUserReplayPopularity($this->score), 0); $this->expectCountChange(fn () => static::getUserReplaysWatchedCount($this->score), 0); @@ -239,6 +250,12 @@ protected function setUp(): void UserStatistics\Osu::factory()->create(['user_id' => $this->user->user_id]); $this->score = Osu::factory()->withReplay()->create(['user_id' => $this->user->user_id]); + $this->soloScore = SoloScore::factory()->create([ + 'beatmap_id' => $this->score->beatmap_id, + 'data' => $this->score->data, + 'legacy_score_id' => $this->score->getKey(), + 'user_id' => $this->score->user_id, + ]); } private function actAsPasswordClientUser(User $user): static