algoNote

プログラミング関連の記録

ブログに競プロのレーティングを表示するようにした(その2)

以前の記事でプログのサイドバーに競プロのレーティング(と色)を表示するjavascriptを紹介しました。 意外にも多くの方に使って頂けているようで嬉しいです。

algon-320.hatenablog.com

以前紹介したスクリプトではAtCoderのレーティング取得がYQLに依存していましたが、 残念ながらYQLは2019年1月でサービス終了してしまいました。 それに伴って、AtCoderのレーティング取得ができなくなっていました。

この問題に対応するために、簡易的なAPIサーバー的なもの(Kyopro-Ratingsと命名)を作成し、Heroku上で動かすことにしました

※追記 2022/11/05
Herokuの無料枠の終了に合わせてHerokuに設置していたAPIサーバを停止します。 今後は以下のURLで@su8ruさんにホストしていただけることになりました。

http://kyopro-ratings.jp1.su8.run/json

既にブログなどに設置している方は、以下のように変更していただくことで引き続き利用できます。

-      xhr.open('GET', 'https://kyopro-ratings.herokuapp.com/json?' + query_str, true);
+      xhr.open('GET', 'http://kyopro-ratings.jp1.su8.run/json?' + query_str, true);

デモ

機能

旧バージョンと同じです。

  • レーティング
  • 色付け
  • ユーザーページへのリンク

Kyopro-Ratings

サーバー側でレーティングを取得して、jsonでフロント側に送っています。

github.com

スクレイピングしている関係上、高頻度でリクエストが来た場合にエラーを返すようになっています。とりあえず200ms以内に複数のリクエストが来た場合に弾くようにしました。

※追記 2020/05/05 一日に1回だけフェッチしてサーバ側でキャッシュするようにしました。200msの制限が無くなった代わりにレーティングの更新が最大1日遅れます。

ソースコード

こちらをサイドバーなどに設置しています。適当に改変して使ってください。
(htmlのtwitter・AOJの部分、jsのatcoder_user, codeforces_user, topcoder_algorithm_userの部分を自分のユーザー名に書き換えてください。)

なお、予告なくサーバーを停止するなどして動かなくなるかもしれませんので、その点はご了承ください。

<p>
  Twitter : <a id="twitter" href="https://twitter.com/algon_320" target="_blank"
    style="text-decoration:none;font-weight:bold;color:black;">@algon_320</a><br>
  AtCoder : <a id="atcoder_rating" target="_blank" style="text-decoration:none;font-weight:bold;">loading</a><br>
  Codeforces : <a id="codeforces_rating" target="_blank" style="text-decoration:none;font-weight:bold;">loading</a><br>
  TopCoder SRM : <a id="topcoder_algorithm_rating" target="_blank" style="text-decoration:none;font-weight:bold;">loading</a><br>
  AOJ : <a id="aoj" href="http://judge.u-aizu.ac.jp/onlinejudge/user.jsp?id=algon#1" target="_blank"
    style="text-decoration:none;font-weight:bold;color:black;">algon</a><br>
</p>

<script type="text/javascript">
  (function () {
    class User {
      constructor(service, handle) {
        this.service = service;
        this.handle = handle;
        this.rating = 0;
        this.color = '#000';  // デフォルトの色
      }
    }
    class Service {
      constructor(name, url) {
        this.name = name;
        this.url = url;
      }
    }

    let atcoder            = new Service('atcoder',            'https://atcoder.jp/user/');
    let codeforces         = new Service('codeforces',         'http://codeforces.com/profile/');
    let topcoder_algorithm = new Service('topcoder_algorithm', 'https://www.topcoder.com/members/');

    let atcoder_user            = new User(atcoder,            'algon');
    let codeforces_user         = new User(codeforces,         'algon_320');
    let topcoder_algorithm_user = new User(topcoder_algorithm, 'algon_320');

    let accounts = [atcoder_user, codeforces_user, topcoder_algorithm_user];

    // ユーザーページへのリンクのみ
    function set_html_without_rating(user) {
      let a = document.getElementById(user.service.name + '_rating');
      a.href = user.service.url + user.handle;
      a.innerHTML = user.handle;
      a.setAttribute('style', 'text-decoration:none;font-weight:bold;color:' + user.color);
    }
    // ユーザーページへのリンク + レーティング表示 + 色
    function set_html(user) {
      let a = document.getElementById(user.service.name + '_rating');
      a.href = user.service.url + user.handle;
      a.innerHTML = user.handle + ' (' + user.rating.toString() + ')';
      a.setAttribute('style', 'text-decoration:none;font-weight:bold;color:' + user.color);
    }

    function fetch_ratings() {
      let query_str = '';
      accounts.forEach(user => {
        query_str += user.service.name + "=" + user.handle + '&';
      });

      function error() {
        accounts.forEach(user => set_html_without_rating(user));
      }
      let xhr = new XMLHttpRequest();
      xhr.open('GET', 'http://kyopro-ratings.jp1.su8.run/json?' + query_str, true);
      xhr.onload = function (e) {
        if (xhr.readyState === 4) {
          if (xhr.status === 200) {
            json = JSON.parse(xhr.responseText);
            // console.log(json);
            if ('error' in json) {
              error();
            } else {
              accounts.forEach(user => {
                let service_name = user.service.name;
                if (json[service_name]['status'] == 'success') {
                  user.rating = json[service_name]['rating'];
                  user.color = json[service_name]['color'];
                  set_html(user);
                } else {
                  set_html_without_rating(user);
                }
              });
            }
          }
        }
      };
      xhr.onerror = function (e) { error(); };
      xhr.ontimeout = function (e) { error(); };
      xhr.timeout = 10000;
      xhr.send(null);
    }
    fetch_ratings();
  })();
</script>

はてなブログの場合、管理画面のデザイン→サイドバー→カスタマイズから設置することが出来ます。