osu leaderboard

This commit is contained in:
flash 2016-12-11 19:51:45 +01:00
parent 9b6175c13c
commit 244d1177df
10 changed files with 495 additions and 5 deletions

View file

@ -25,10 +25,11 @@ class CronCommand extends ChainedCommand
}
public $commands = [
PremiumPurgeCommand::class,
PurgeInactiveUsersCommand::class,
RebuildForumCacheCommand::class,
SessionPurgeCommand::class,
PremiumPurgeCommand::class,
StatusCheckCommand::class,
RebuildForumCacheCommand::class,
OsuLeaderboardUpdateCommand::class,
];
}

View file

@ -0,0 +1,75 @@
<?php
/**
* Holds the osu-leaderboard-update command controller.
* @package Sakura
*/
namespace Sakura\Console\Command;
use Carbon\Carbon;
use CLIFramework\Command;
use Sakura\DB;
use Sakura\OsuLeaderboard;
use Sakura\User;
/**
* Connects to the osu!api and stores new data.
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class OsuLeaderboardUpdateCommand extends Command
{
/**
* A quick description of this command.
* @return string.
*/
public function brief(): string
{
return 'Connects to the osu!api and stores new data.';
}
/**
* Command.
*/
public function execute(): void
{
$this->getLogger()->writeln('Updating osu! leaderboard...');
$api_key = config('osu.api_key');
if ($api_key === null) {
$this->getLogger()->writeln("Your api key isn't set in the config, get one at https://osu.ppy.sh/p/api.");
return;
}
$game_modes = [
'osu!',
'osu!taiko',
'osu!catch',
'osu!mania',
];
$osulb = new OsuLeaderboard($api_key);
$start = Carbon::now();
$users = DB::table('users')
->whereNotNull('user_osu')
->get(['user_id']);
foreach ($game_modes as $mode => $name) {
$this->getLogger()->writeln(PHP_EOL . "=> {$name}");
foreach ($users as $user) {
$user = User::construct($user->user_id);
$user_display = $user->osu . ($user->username !== $user->osu ? " ({$user->username})" : '');
$this->getLogger()->writeln("==> Updating {$name} scores for {$user_display}...");
$osulb->update($user->osu, $user, $mode);
}
}
$this->getLogger()->writeln(PHP_EOL . "Purging inactive data...");
$osulb->purge($start);
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* Holds the osu! specific stuff controller.
* @package Sakura
*/
namespace Sakura\Controllers;
use Sakura\CurrentSession;
use Sakura\DB;
use Sakura\Notification;
/**
* Notification stuff.
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class OsuController extends Controller
{
private const MODES = [
'osu!standard',
'osu!taiko',
'osu!catch',
'osu!mania',
];
public function leaderboard(): string
{
return view('osu/leaderboard', ['gamemodes' => static::MODES]);
}
public function leaderboardData(): string
{
$mode = $_GET['mode'] ?? -1;
if (!array_key_exists($mode, static::MODES)) {
return $this->json(['error' => 'Invalid game mode.']);
}
return $this->json(DB::table('osu_leaderboard')
->where('osu_game_mode', $mode)
->orderBy('osu_rank')
->get());
}
}

111
app/OsuLeaderboard.php Normal file
View file

@ -0,0 +1,111 @@
<?php
/**
* Holds the osu!leaderboard backend.
* @package Sakura
*/
namespace Sakura;
use Carbon\Carbon;
use Illuminate\Database\Query\Builder;
use stdClass;
/**
* osu!leaderboard backend.
* @package Sakura
* @author Julian van de Groep <me@flash.moe>
*/
class OsuLeaderboard
{
private const ENDPOINT = "https://osu.ppy.sh/api/";
protected $apiKey;
public function __construct(string $apiKey)
{
$this->apiKey = $apiKey;
}
public function purge(Carbon $before): void
{
DB::table('osu_leaderboard')
->where('updated_on', '<', $before)
->delete();
}
public function update($osu, User $user, int $mode = 0): void
{
// skip if the user hasn't been online for a month
if ($user->lastOnline < time() - 30 * 24 * 60 * 60) {
return;
}
$osu_user = $this->apiGetUser($osu, $mode);
if (!$osu_user || $osu_user->pp_raw < 1) {
return;
}
$exists = DB::table('osu_leaderboard')
->where(is_int($osu) ? 'osu_user_id' : 'osu_username', $osu)
->where('user_id', $user->id)
->where('osu_game_mode', $mode)
->count() > 1;
$data = [
'user_id' => $user->id,
'username' => $user->username,
'osu_user_id' => intval($osu_user->user_id),
'osu_username' => $osu_user->username,
'osu_game_mode' => $mode,
'osu_count_300' => intval($osu_user->count300),
'osu_count_100' => intval($osu_user->count100),
'osu_count_50' => intval($osu_user->count50),
'osu_count_ss' => intval($osu_user->count_rank_ss),
'osu_count_s' => intval($osu_user->count_rank_s),
'osu_count_a' => intval($osu_user->count_rank_a),
'osu_play_count' => intval($osu_user->playcount),
'osu_score_ranked' => intval($osu_user->ranked_score),
'osu_score_total' => intval($osu_user->total_score),
'osu_rank' => intval($osu_user->pp_rank),
'osu_country' => $osu_user->country,
'osu_rank_country' => intval($osu_user->pp_country_rank),
'osu_performance' => floatval($osu_user->pp_raw),
'osu_accuracy' => floatval($osu_user->accuracy),
'osu_level' => floatval($osu_user->level),
'updated_on' => Carbon::now(),
];
if ($exists) {
DB::table('osu_leaderboard')
->where('osu_user_id', $data->osu_user_id)
->where('user_id', $data->user_id)
->where('osu_game_mode', $data->mode)
->update($data);
} else {
DB::table('osu_leaderboard')
->insert($data);
}
}
private function apiGetUser($user, int $mode = 0): ?stdClass
{
$result = json_decode(
Net::request(static::ENDPOINT . "get_user?k={$this->apiKey}&u={$user}&m={$mode}")
);
if ($result === false || count($result) < 1) {
return null;
}
return $result[0];
}
}

View file

@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Sakura\DB;
class OsuLeaderboardTables extends Migration
{
/**
* Run the migrations.
* @return void
*/
public function up()
{
$schema = DB::getSchemaBuilder();
$schema->create('osu_leaderboard', function (Blueprint $table) {
$table->integer('user_id');
$table->string('username');
$table->integer('osu_user_id');
$table->string('osu_username');
$table->tinyInteger('osu_game_mode');
$table->integer('osu_count_300');
$table->integer('osu_count_100');
$table->integer('osu_count_50');
$table->integer('osu_count_ss');
$table->integer('osu_count_s');
$table->integer('osu_count_a');
$table->integer('osu_play_count');
$table->bigInteger('osu_score_ranked');
$table->bigInteger('osu_score_total');
$table->integer('osu_rank');
$table->char('osu_country', 2);
$table->integer('osu_rank_country');
$table->float('osu_performance');
$table->float('osu_accuracy');
$table->float('osu_level');
$table->timestamp('updated_on');
});
}
/**
* Reverse the migrations.
* @return void
*/
public function down()
{
$schema = DB::getSchemaBuilder();
$schema->drop('osu_leaderboard');
}
}

View file

@ -0,0 +1,61 @@
.osu-leaderboard {
&__container {
display: flex;
align-items: center;
}
&__ranking {
font-size: 40px;
min-width: 100px;
text-align: center;
text-shadow: 1px 1px 1px #222;
&--rank-1 {
color: #D5CEA6;
}
&--rank-2 {
color: #C8D7DA;
}
&--rank-3 {
color: #C19E67;
}
}
&__avatar {
width: 120px;
height: 120px;
}
&__info {
padding-left: 5px;
}
&__username {
font-family: @cute-font;
font-size: 20px;
}
&__stats {
font-family: @cute-font;
font-size: 16px;
}
&__ss,
&__s,
&__a {
font-style: italic;
font-weight: bold;
text-shadow: 1px 1px .5px #222;
}
&__ss,
&__s {
color: #D5CEA6;
}
&__a {
color: #299A0B;
}
}

View file

@ -25,6 +25,7 @@
@import "bem/link";
@import "bem/members";
@import "bem/news";
@import "bem/osu-leaderboard";
@import "bem/post";
@import "bem/profile";
@import "bem/settings";

View file

@ -176,20 +176,21 @@
<li class="footer__item"><a class="footer__link" href="{{ route('main.index') }}">Home</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('news.category') }}">News</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('main.search') }}">Search</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('info.contact') }}">Contact</a></li>
<li class="footer__item"><a class="footer__link" href="https://sakura.flash.moe">Changelog</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('premium.index') }}">Support us</a></li>
</ul>
<ul class="footer__section">
<li class="footer__item footer__item--head">Community</li>
<li class="footer__item"><a class="footer__link" href="{{ route('forums.index') }}">Forums</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('osu.leaderboard') }}">osu! leaderboard</a></li>
<li class="footer__item"><a class="footer__link" href="https://twitter.com/_flashii">Twitter</a></li>
<li class="footer__item"><a class="footer__link" href="https://youtube.com/user/flashiinet">YouTube</a></li>
<li class="footer__item"><a class="footer__link" href="https://steamcommunity.com/groups/flashiinet">Steam</a></li>
<li class="footer__item"><a class="footer__link" href="https://github.com/aitemu">GitHub</a></li>
<li class="footer__item"><a class="footer__link" href="https://github.com/flashii">GitHub</a></li>
</ul>
<ul class="footer__section">
<li class="footer__item footer__item--head">Information</li>
<li class="footer__item"><a class="footer__link" href="{{ route('info.contact') }}">Contact</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('info.rules') }}">Rules</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('status.index') }}">Server Status</a></li>
<li class="footer__item"><a class="footer__link" href="{{ route('info.terms') }}">Terms of Service</a></li>

View file

@ -0,0 +1,130 @@
{% extends 'master.twig' %}
{% set title = 'osu! leaderboard' %}
{% block js %}
<script>
var osuModes = {{ gamemodes|json_encode|raw }};
function yuunoOsuFetch(mode) {
var client = new Sakura.AJAX,
leaderboard = Sakura.DOM.ID('osu-lb'),
loader = Sakura.DOM.ID('osu-lb-loading');
leaderboard.style.display = 'none';
loader.style.display = null;
client.SetUrl("{{ route('osu.leaderboard.data') }}?mode=" + mode);
client.AddCallback(200, function () {
var result = client.JSON();
if (result.error) {
var diag = new Sakura.Dialogue;
diag.Title = "osu! leaderboard";
diag.Text = result.error;
diag.AddCallback(Sakura.DialogueButton.Ok, function () {
yuunoOsuSwitch(osuModes[0]);
this.Close();
});
diag.Display();
} else {
yuunoOsuBuildList(result);
}
});
client.Start(Sakura.HTTPMethod.GET);
}
function yuunoOsuBuildList(list) {
var numberFormat = new Intl.NumberFormat(navigator.language),
leaderboard = Sakura.DOM.ID('osu-lb'),
loader = Sakura.DOM.ID('osu-lb-loading');
leaderboard.innerHTML = '';
for (var rank in list) {
var score = list[rank],
dispRank = +rank + 1,
container = Sakura.DOM.Create('div', 'osu-leaderboard__container', 'osu-lb-ranking-' + rank),
ranking = Sakura.DOM.Create('div', 'osu-leaderboard__ranking osu-leaderboard__ranking--rank-' + dispRank),
avatar = Sakura.DOM.Create('div', 'avatar avatar--border osu-leaderboard__avatar'),
info = Sakura.DOM.Create('div', 'osu-leaderboard__info'),
username = Sakura.DOM.Create('a', 'osu-leaderboard__username'),
stats = Sakura.DOM.Create('div', 'osu-leaderboard__stats'),
scores = Sakura.DOM.Create('div', 'osu-leaderboard__scores'),
level = Sakura.DOM.Create('div', 'osu-leaderboard__level');
ranking.innerText = '#' + numberFormat.format(dispRank);
Sakura.DOM.Append(container, ranking);
avatar.style.backgroundImage = "url('https://a.ppy.sh/" + score.osu_user_id + "')";
Sakura.DOM.Append(container, avatar);
username.innerText = score.osu_username + (score.osu_username !== score.username ? " (" + score.username + ")" : '');
username.href = "https://osu.ppy.sh/u/" + score.osu_user_id;
username.target = "_blank";
Sakura.DOM.Append(info, username);
stats.innerHTML = numberFormat.format(Math.round(score.osu_performance)) + "pp"
+ " #" + numberFormat.format(score.osu_rank) + " (<img src='/images/flags/" + score.osu_country.toLowerCase() + ".png' alt='" + score.osu_country + "'>"
+ " #" + numberFormat.format(score.osu_rank_country) + ")"
+ " " + numberFormat.format(Math.round(score.osu_accuracy * 100) / 100) + '%';
Sakura.DOM.Append(info, stats);
scores.innerText = "scores // ranked " + numberFormat.format(score.osu_score_ranked) + " // total " + numberFormat.format(score.osu_score_total);
Sakura.DOM.Append(info, scores);
level.innerHTML = "level "
+ numberFormat.format(Math.floor(score.osu_level))
+ " // <span class='osu-leaderboard__ss'>SS</span> "
+ numberFormat.format(score.osu_count_ss)
+ " / <span class='osu-leaderboard__s'>S</span> "
+ numberFormat.format(score.osu_count_s)
+ " / <span class='osu-leaderboard__a'>A</span> "
+ numberFormat.format(score.osu_count_a);
Sakura.DOM.Append(info, level);
Sakura.DOM.Append(container, info);
Sakura.DOM.Append(leaderboard, container);
}
loader.style.display = 'none';
leaderboard.style.display = null;
}
function yuunoOsuReload() {
yuunoOsuFetch(osuModes.indexOf(window.location.hash.substring(1)));
}
function yuunoOsuSwitch(mode) {
window.location.hash = '#' + mode;
}
window.addEventListener("hashchange", yuunoOsuReload);
window.addEventListener("load", function () {
if (!window.location.hash.substring(1)) {
yuunoOsuSwitch(osuModes[0]);
} else {
yuunoOsuReload();
}
});
</script>
{% endblock %}
{% block content %}
<div class="content">
<div class="content__header">osu! leaderboard</div>
<div style="text-align: center">
{% for mode in gamemodes %}
<h1 onclick="yuunoOsuSwitch('{{ mode }}')" style="cursor: pointer; display: inline-block; margin: 20px 10px">{{ mode }}</h1>
{% endfor %}
</div>
<div id="osu-lb-loading" style="text-align: center; margin: 2em 0">
<h1>Loading...</h1>
<h1 class="fa fa-spinner fa-spin fa-4x"></h1>
</div>
<div id="osu-lb" style="display: none"></div>
</div>
{% endblock %}

View file

@ -39,7 +39,7 @@ Router::group(['before' => 'maintenance'], function () {
'mcp' => 'manage.index',
'mcptest' => 'manage.index',
//'report' => 'report.something',
//'osu' => 'eventual link to flashii team',
'osu' => 'osu.leaderboard',
'everlastingness' => 'https://gist.github.com/fe207557385f0700d719a97b5fab647f',
'fuckingdone' => 'https://gist.github.com/4fac58c40be8a71dbfde50eca149575d',
];
@ -176,6 +176,12 @@ Router::group(['before' => 'maintenance'], function () {
Router::post('/purchase', 'PremiumController@purchase', 'premium.purchase');
});
// osu!
Router::group(['prefix' => 'osu'], function () {
Router::get('/leaderboard', 'OsuController@leaderboard', 'osu.leaderboard');
Router::get('/leaderboard/data', 'OsuController@leaderboardData', 'osu.leaderboard.data');
});
// Helpers
Router::group(['prefix' => 'helper'], function () {
// BBcode