Repository: project-primera/ongeki-score
Pull request #921: feat: 履歴ベースのスコア取得
Author: login: soramame0256
Labels: name: Type: Feature
description: New Feature
name: Scope: Application
description: Edited Application files
name: Scope: Bookmarklet
description: Edited bookmarklet files
Created: 2025-04-18T09:32:14Z | Merged: 2025-05-24T18:37:20Z

## 内容
履歴から更新するべき曲を取得してそれのみを更新するようにする。

## 実装概要
履歴件数が=50のとき最終更新日時よりももっとも古い履歴の日時が新しければ全曲取得 そうでなければ最終更新日時より新しい履歴を持つ曲のみをフィルターする形で更新を行う。
また履歴件数が<50であればもっとも古い履歴の日時にかかわらず最終更新日時よりも新しい履歴を持つ曲のみをフィルターする形で更新を行う。
また、最終更新からプレイが行われていない状態で更新が行われた場合は履歴にかかわらず全曲取得を行う。
## 結果
closes #877

## 備考
構造計画等は理解しきれていないので違反している可能性があります。
命名等はできる限り参照しましたがもし意に反した命名があれば申し訳ないです。
またgetScore.tsの782行目について変更を行いましたが、問題があるようでしたらprを落としていただけると幸いです。
Windows版Chrome 135.0.7049.96で動作することは確認しましたが、環境の都合でスマホでの検証ができていません。
申し訳ないです。

Linked issue #877: [ブックマークレット] 楽曲情報取得の高速化
プレイ履歴から取得すれば全件走らせなくてもいいはず

## 実装草案

DBに前回のlast_updatedをDBに保存 一番最後の履歴と比較して50曲超えてたら全件取得

Diff:
diff --git a/OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php b/OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php
index 0298da8a..67eec83d 100644
--- a/OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php
+++ b/OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php
@@ -6,6 +6,7 @@
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Http\Request;
 use App\Facades\Slack;
+use App\UserStatus;
 use Carbon\Carbon;
 use RuntimeException;
 
@@ -16,6 +17,7 @@ public function GetUpdateStatus(Request $request){
         $result['name'] = null;
         $result['message'] = [];
         $result['hash'] = '';
+        $result['updateAt'] = null;
 
         if(Auth::user() === null){
             $result['message'][] = "ログイン情報が取得できませんでした。ブックマークレットの再生成をお試しください。";
@@ -40,6 +42,14 @@ public function GetUpdateStatus(Request $request){
             \App\Facades\Slack::Warning($content, $fileContent, $fields, "success");
             return $result;
         }
+        $userStatus = new UserStatus();
+        $status = $userStatus -> getRecentUserData(Auth::id());
+        // 最終更新日時を取得
+        if(count($status) === 0){
+            $result['updateAt'] = '1970-01-01 00:00:01';
+        }else {
+            $result['updateAt'] = $status[0]->updated_at;
+        }
         $result['message'] = 'ok';
         $result['hash'] = hash('sha256', $result['id'] . '_' . microtime(true));
 
diff --git a/OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts b/OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts
index e4242073..cb90e396 100644
--- a/OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts
+++ b/OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts
@@ -176,11 +176,17 @@ import * as qs from 'qs';
     class ScoreData {
         private songInfos = new Array<SongInfo>();
         private sameNameList = null;
+        // 空の場合は全曲取得のフラグとして扱う
+        private songToUpload = [];
 
         public async GetArrayLength() {
             return this.songInfos.length;
         }
 
+        public async GetUploadArrayLength() {
+            return this.songToUpload.length;
+        }
+
         public async GetPartialArray($page = 0) {
             return this.songInfos.slice($page * 50, $page * 50 + 50);
         }
@@ -206,6 +212,43 @@ import * as qs from 'qs';
             });
         }
 
+        public async getScoreHtmlFromRecent(timestamp: Date) {
+            try {
+                const response = await axios.get(NET_URL + '/record/playlog/');
+                await this.checkNewScoreFromRecent(response.data, timestamp);
+            } catch (error) {
+                throw new Error("プレイ履歴からのスコア取得に失敗しました。" + error);
+            }
+        }
+
+        private async checkNewScoreFromRecent(html: string, timestamp: Date) {
+            if (this.sameNameList === null) {
+                this.sameNameList = await SameNameMusicList.get();
+            }
+            var parseHTML = $.parseHTML(html);
+            var $innerContainer3 = $(parseHTML).find(".container3").children(".m_10")
+            // 50曲存在するなら50曲目をとって、更新日時が最終更新日時以降なら全曲取得に変更
+            if ($innerContainer3.length === 50) {
+                let recentUpdate = new Date($innerContainer3.last().find("span.f_r.h_10").text());
+                if (recentUpdate > timestamp) {
+                    this.songToUpload = [];
+                    return;
+                }
+            }
+            $innerContainer3.each((key, value) => {
+                if ($(value).hasClass("m_10")){
+                    if (new Date($(value).find("span.f_r.h_10").text()) > timestamp) {
+                        var $elem = $(value).find(".m_5.l_h_10.break");
+                        // 履歴から取得した曲名には改行コードとタブ制御文字が含まれているので削除
+                        var songTitle = $elem.text().replace(/\n/g,"").replace(/\t/g,"");
+                        if (this.songToUpload.indexOf(songTitle) === -1) {
+                            this.songToUpload.push(songTitle);
+                        }
+                    }
+                }
+            });
+        }
+
         private async parseScoreData(html: string, difficulty: Difficulty) {
             if (this.sameNameList === null) {
                 this.sameNameList = await SameNameMusicList.get();
@@ -231,6 +274,14 @@ import * as qs from 'qs';
             let level = +($(element).find(".score_level").text().replace("+", ".5"));
             let artist = '';
 
+            // プレイ履歴から取得した曲があるかチェック
+            if (this.songToUpload.length !== 0) {
+                // プレイ履歴に存在しない曲ならスキップする
+                if (this.songToUpload.indexOf(name) === -1) {
+                    return;
+                }
+            }
+
             if (this.sameNameList.indexOf(name) !== -1) {
                 console.log("曲名が重複している楽曲名: " + name + ' / Lv' + level + ' / ' + genre + ' / ' + difficulty);
                 await sleep(SLEEP_MSEC);
@@ -558,6 +609,7 @@ import * as qs from 'qs';
         let userId: number = 0;
         let hash: string = "";
         let name: string = "";
+        let lastUpdate: Date = new Date();
 
         try {
             // メンテナンスチェック
@@ -574,6 +626,7 @@ import * as qs from 'qs';
                 userId = result.data.id;
                 hash = result.data.hash;
                 name = result.data.name;
+                lastUpdate = new Date(result.data.updateAt);
 
             }).catch(function (error) {
                     throw new Error(error + "<br>スコアツールサーバーへの接続に失敗しました。<br><br>まずは以下の手順をお試しください。<br>1)ブックマークレットの再生成を行い、現在ご利用のブックマークレットに上書きして再度実行してください。<br>２）メンテナンス情報をご確認ください。情報については<a href='https://twitter.com/ongeki_score' target='_blank' style='color:#222'>Twitter@ongeki_score</a>にてお知らせします。");
@@ -618,6 +671,16 @@ import * as qs from 'qs';
                 echo(await getTime() + "スタンダードプランの為、レーティング対象曲情報取得をスキップします。");
             }
 
+            echo(await getTime() + "更新対象曲を取得します。");
+            await scoreData.getScoreHtmlFromRecent(lastUpdate);
+            if (await scoreData.GetUploadArrayLength() === 0) {
+                echo(await getTime() + "前回更新からのプレイ曲数が50曲を超えているか、プレイが行われていないため全件取得を行います。");
+            } else {
+                echo(await getTime() + "前回更新からの曲のみを取得します。<br>全件取得を行いたい場合はもう一度実行してください。");
+                console.log("前回更新からのプレイ曲数: " + await scoreData.GetUploadArrayLength() + "曲");
+            }
+            await sleep(SLEEP_MSEC);
+
             echo(await getTime() + "Lunaticのスコアデータを取得します。");
             scoreData.Clear();
             await scoreData.GetDifficultyScoreData(Difficulty.Lunatic);
@@ -667,7 +730,7 @@ import * as qs from 'qs';
             await sleep(SLEEP_MSEC);
 
             echo(await getTime() + "Basicのスコアデータを取得します。");
-            scoreData = new ScoreData;
+            scoreData.Clear();
             await scoreData.GetDifficultyScoreData(Difficulty.Basic);
             echo(await getTime() + "スコアデータを送信します。");
             length = Math.ceil(await scoreData.GetArrayLength() / 50);


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces functionality to selectively update scores based on play history. It adds new methods in ScoreData to track songs for upload, changes the logic for filtering songs by their last-play timestamps, and updates the API response in the UserController to include update time information.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

| File | Description |
| ---- | ----------- |
| OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts | Added methods for processing recent play history and managing an upload song list; updated logic for partial score data retrieval. |
| OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php | Enhanced API response by including an updateAt field based on the user status. |


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts:165**
* [nitpick] Consider renaming 'songToUpload' to 'songsToUpload' to better reflect that it holds multiple song titles.
```
private songToUpload = [];
```
**OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php:20**
* [nitpick] Consider renaming 'updateAt' to 'updatedAt' for clarity and to align with common timestamp naming conventions.
```
$result['updateAt'] = null;
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T07:58:36Z
user: login: slime-hatena
body: 動作確認もいい感じでした！
超デカい変更、本当にありがとうございます！
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-24T18:36:40Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refines score fetching logic by updating song filtering based on recent play history and enhancing the update status API response.  
- Updated getScore.ts to selectively update scores using recent play history filtering.  
- Enhanced UserController.php to include a last update timestamp in the API response.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

| File | Description |
| ---- | ----------- |
| OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts | Added new methods and logic to filter score updates based on play history timestamps and added detailed logging. |
| OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php | Updated API response to include an update timestamp and retrieved recent user data. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T18:37:26Z

Review comments:
diff_hunk: @@ -191,6 +197,43 @@ import * as qs from 'qs';
             });
         }
 
+        public async getScoreHtmlFromRecent(timestamp: Date) {
+            await axios.get(NET_URL + '/record/playlog/'
+            ).then(async (response) => {
+                await this.checkNewScoreFromRecent(response.data, timestamp);
+            }).catch(function (error) {
+                throw new Error("プレイ履歴からのスコア取得に失敗しました。" + error)
+            })
path: OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts
user: login: Copilot
body: Consider refactoring this function to use a try/catch block with async/await throughout, instead of mixing .then() and .catch() with async/await.
```suggestion
            try {
                const response = await axios.get(NET_URL + '/record/playlog/');
                await this.checkNewScoreFromRecent(response.data, timestamp);
            } catch (error) {
                throw new Error("プレイ履歴からのスコア取得に失敗しました。" + error);
            }
```
created_at: 2025-05-09T07:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,6 +212,43 @@ import * as qs from 'qs';
             });
         }
 
+        public async getScoreHtmlFromRecent(timestamp: Date) {
+            try {
+                const response = await axios.get(NET_URL + '/record/playlog/');
+                await this.checkNewScoreFromRecent(response.data, timestamp);
+            } catch (error) {
+                throw new Error("プレイ履歴からのスコア取得に失敗しました。" + error);
path: OngekiScoreLog/resources/assets/js/bookmarklets/getScore.ts
user: login: Copilot
body: Consider checking if the error is an instance of Error and using its message property (e.g. error.message) for a clearer error message.
```suggestion
                const errorMessage = error instanceof Error ? error.message : String(error);
                throw new Error("プレイ履歴からのスコア取得に失敗しました。" + errorMessage);
```
created_at: 2025-05-24T18:37:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +42,14 @@ public function GetUpdateStatus(Request $request){
             \App\Facades\Slack::Warning($content, $fileContent, $fields, "success");
             return $result;
         }
+        $userStatus = new UserStatus();
+        $status = $userStatus -> getRecentUserData(Auth::id());
+        // 最終更新日時を取得
+        if(count($status) === 0){
+            $result['updateAt'] = '1970-01-01 00:00:01';
path: OngekiScoreLog/app/Http/Controllers/api/v2/UserController.php
user: login: Copilot
body: Using a hardcoded default date can be error-prone; consider extracting this magic constant into a well-documented configuration or constant.
```suggestion
            $result['updateAt'] = self::DEFAULT_UPDATE_AT;
```
created_at: 2025-05-24T18:37:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: slime-hatena
created_at: 2025-05-09T07:59:12Z
body: ありがとうございます！ 手が空いたタイミングで確認いたします！
author_association: MEMBER
user: login: slime-hatena
created_at: 2025-05-09T08:27:01Z
body: todo: #924, #925 を先にマージ

author_association: MEMBER
user: login: WorldRobertProject
created_at: 2025-05-10T11:39:22Z
body: そういえばスコアが正常に登録されない系の不具合を修正した場合、全取得が行われる条件を満たすか該当曲のスコアを更新するまでスコアが修正されないと思います。
一応二回取得すれば全取得する作りではあるのですが、スコアの最終更新日時よりツールの最終更新日時の方が後の場合は全取得するとか、何らかの対策が必要だと思います。
author_association: CONTRIBUTOR
user: login: soramame0256
created_at: 2025-05-12T01:02:50Z
body: 確かに周知だけだと足りませんしね。
今後のことを考えると強制全取得は実装した方がよさそうです。
ただ更新日時の取得をどうするかというのが少し問題になりそう...?
一応[githubのreleasesのapi](https://api.github.com/repos/projectprimera/ongeki-score/releases/latest)叩けば取得はできそうなんですけど未認証の場合のrate limitがip毎に60/hourなので少し不安です...

追記
曲情報の更新でも起こる可能性がある都合でそういうAPIを別途用意したほうがいいかも知れませんね。
ただdb変更が必要になるので少し微妙かも…
author_association: CONTRIBUTOR
user: login: slime-hatena
created_at: 2025-05-13T04:42:15Z
body: 強制全取得するボタンを入れる が一番いいかなぁと思ってます。
ユーザーさんが更新に気づかない可能性は大ですが、まぁそこは運用とお問い合わせでカバーかなと。
ここらへんは後で様子見ながら必要か判断しておきます！
author_association: MEMBER
user: login: soramame0256
created_at: 2025-05-13T12:14:26Z
body: デバッグ用にsongToUploadを外部から参照できるようにしていたんですけどsongInfosと同様lengthのみを外部から参照可能なように変更しました。
機能に問題はありません。
今更な変更なのは申し訳ないです
author_association: CONTRIBUTOR