[LR2] 最終プレイ日時を記録できるようにし、最終プレイ日ソートとして活用する方法 後編 +タグ付けで選曲画面で最終プレイ日を確認する方法 [BeMusicSeeker]


前提条件

  • LR2(Lunatic Rave 2) + BeMusicSeeker という環境で遊んでいること

  • 「PowerShell  7」が使えること ※ スクリプトは7環境で作ったのでPowerShell 5.1だと動かない可能性が高いです

  • 以下の前編記事での作業を終わらせていること(sqlite3.exeは紛失していませんか?)

(Windows標準環境ならだれでも簡単に行えることを目指しましたがPowerShell 7はまだ普及していないようです…)

PowerShell 7について

おそらく世の中のWindowsでPowerShellを実行すると以下のような画面が出るでしょう。これは、PowerShellをバージョンアップしろという意味ではなく、新しく「PowerShell 7」を作ったのでそれを入れてくださいという意味です。現在はPowerShell 5系と7系は別のアプリとして共存していますが、さすがに今後開発されるPowerShell資産は7系が主流になるでしょう。

旧PowerShell5.1で出るメッセージ

PowerShell 7の入手方法

さすがにここに細かく書くのは話が逸れすぎるので、以下を参考に適当にググって入れてください。LR2とかbeatorajaの導入に比べたら簡単です。.ps1ファイルをダブルクリックで起動できる状態にしておくと便利だと思います。

おすすめはMicrosoftも推奨しているWingetを使う方法です。今どきのWindowsならWingetは使えるはずですが、ダメな場合は、以下を参照してください。「アプリ インストーラー」の導入に見えますが、wingetはアプリインストーラー内に含まれているものなのです。

それでもwingetが使えない場合は環境変数PATHがおかしくなっていると思うので、PATHに「%LOCALAPPDATA%\Microsoft\WindowsApps」を追加すればwingetが使えると思います。

最終プレイ日でソートするカスタムフォルダを作るPowerShellスクリプト

難易度表のレベル毎にカスタムフォルダを出力し、フォルダ内では最終プレイ日(lastplay)でソートされるものを自動で出力するスクリプトです。

ついでに、minbp順やmaxbpm順のフォルダも出力するようにしています。
不要な方はスクリプトを編集して削ってください。$ORDERSの要素を消したし足したりすればOKです。SQLが分かる人はカスタマイズしてみると良いでしょう。

スクリプト冒頭にある3つの定数は環境に合わせて書き換えてください
$SQLITE_PATH "sqlite3.exeの場所" ←前編でダウンロードしたやつです
$SONGDB_PATH = "song.dbの場所"
$OUT_DIR  = "カスタムフォルダの出力先"

2024-09-15: 「# 設定が有効か確認」の部分で設定値の不備を検出できていなかったので修正しました。本筋の動作に影響のある修正ではありません。

# これは「PowerShell 7」で動作確認したスクリプトです

# 設定用
$SQLITE_PATH = "D:\BIN\SQLite\sqlite-tools-win-x64-3460100\sqlite3.exe" # 環境変数PATHを設定しているなら"sqlite3"みたいな指定方法でもOK
$SONGDB_PATH = "D:\LR2beta3\LR2files\Database\song.db"
$OUT_DIR = "${PSScriptRoot}\カスタムフォルダ" #カスタムフォルダの出力先、.ps1ファイル自体の場所にある「カスタムフォルダ」

# どのようなソート順のカスタムフォルダを難易度表別に作っていきたいかの定義、難易度表のレベル毎にこの定義でソートされるカスタムフォルダを出力する
$ORDERS = @(
  @{SORT_NAME = "最終プレイ順"; ORDER_BY = "ORDER BY (SELECT lastplay FROM history WHERE hash = song.hash) ASC" },
  @{SORT_NAME = "minbp順"; ORDER_BY = "ORDER BY minbp ASC" },
  @{SORT_NAME = "maxbpm順"; ORDER_BY = "ORDER BY maxbpm" }
)

# 設定が有効か確認
try {
  @($SQLITE_PATH, $SONGDB_PATH) | ForEach-Object { Get-Command $_ -ErrorAction Stop } | Out-Null
  @($OUT_DIR) | ForEach-Object { if (!(Test-path -LiteralPath $_ -PathType Container)) { throw } } | Out-Null
}
catch {
  Write-Host "[Error] 設定値がおかしいです" -BackgroundColor Red
  Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"; Read-Host
  exit
}

# フォルダ名に使えない文字を削除する関数
function Remove-InvalidFileNameChars {
  param ($name)
  # Windowsでファイル名に使えない文字を削除
  return $name -replace '[\/:*?"<>|]', ''
}

# カスタムフォルダにするフォルダのリストを得るクエリ
$SQL_QUERY = @"
-- playlist.folder_orderを基にカスタムフォルダの出力順を求めるための共通テーブル式
WITH folder_order_index AS (
  SELECT
    playlist_id AS 'foi_playlist_id',
    json_each.value AS 'foi_folder',
    json_each.rowid AS 'fo_index'
  FROM
    playlist,
    json_each(playlist.folder_order)
)
SELECT
  playlist_id, -- BeMusicSeekerで作られる表のid
  folder,      -- 表内の各難易度のフォルダ
  name         -- 表の名前
FROM
  playlist_entry
  LEFT OUTER JOIN playlist USING(playlist_id)
  LEFT OUTER JOIN folder_order_index ON playlist_id = foi_playlist_id AND folder = foi_folder
WHERE
  is_removed = 0       -- BeMusicSeekerではこれで論理削除しているので、削除されていないもののみを対象とする
  AND md5 IS NOT NULL  -- bmsonや査定中の譜面の場合にNULLの場合がよくあるのでこれは除外する
GROUP BY playlist_id, folder
ORDER BY playlist_id, CASE WHEN fo_index IS NULL THEN 1000 ELSE fo_index END
;
"@

# フォルダ順定義数、実際にあるフォルダ数を取得するクエリ
# (フォルダ順の定義があったとしても、実際のフォルダ数の方が多い場合はどうしようかと考えたが、フォルダ順定義がある分はフォルダ順定義を使って、NULLの場合は後ろに来るようにすることにした)
$SQL_QUERY2 = @"
WITH folder_order_index AS (
  SELECT
    playlist_id AS 'foidx_id',
    json_each.value AS 'folder',
    json_each.rowid AS 'fo_index'
  FROM
    playlist,
    json_each(playlist.folder_order)
)
, folder_order_count AS (
  SELECT
    foidx_id AS 'playlist_id',
    count(*) AS 'focnt'
  FROM folder_order_index
  GROUP BY foidx_id
)
, folder_count AS (
  SELECT
    playlist_id, 
    count(*) AS 'fcnt' 
  FROM (SELECT DISTINCT playlist_id, folder FROM playlist_entry WHERE is_removed = 0 AND md5 IS NOT NULL)
  GROUP BY playlist_id
)
SELECT
  playlist_id, -- BeMusicSeekerで作られる表のid
  focnt,       -- フォルダ順定義数
  fcnt         -- フォルダ数
FROM
  playlist_entry
  LEFT OUTER JOIN playlist USING(playlist_id)
  LEFT OUTER JOIN folder_order_index USING(folder)
  LEFT OUTER JOIN folder_order_count USING(playlist_id)
  LEFT OUTER JOIN folder_count USING(playlist_id)
WHERE
  is_removed = 0       -- BeMusicSeekerではこれで論理削除しているので、削除されていないもののみを対象とする
  AND md5 IS NOT NULL  -- bmsonや査定中の譜面の場合にNULLの場合がよくあるのでこれは除外する
GROUP BY playlist_id
ORDER BY playlist_id
;
"@

# #COMMANDとは以下のSQLの{COMMAND}部分を指す(LR2のhistory.txt:992行目より)
# SELECT * FROM song LEFT JOIN score ON song.hash = score.hash WHERE {COMMAND}

# .lr2folderテンプレートの定義
$LR2FOLDER_TEMPLATE = @"
#COMMAND song.hash in (SELECT md5 FROM playlist_entry WHERE playlist_id = {playlist_id} AND folder = '{folder}' AND is_removed = 0) {order_by}
#MAXTRACKS 0
#CATEGORY {name} ({sort_name})
#TITLE {folder} ({sort_name})
#INFORMATION_A 
#INFORMATION_B 
"@

# 以下メイン処理

# SQLite3 コマンドを実行し、結果を取得
Write-Host "[Info] song.dbから表の情報を取得中"
[console]::OutputEncoding = [System.Text.Encoding]::UTF8 # sqliteからの出力をUTF8して解釈するための設定
$folders = $SQL_QUERY | & $SQLITE_PATH -csv -header $SONGDB_PATH | ConvertFrom-Csv
$focnt_fcnt = $SQL_QUERY2 | & $SQLITE_PATH -csv -header $SONGDB_PATH | ConvertFrom-Csv
$folderCount = $folders.Count

# $ORDERSに定義されている分カスタムフォルダを作っていく
foreach ($ORDER in $ORDERS) {
  # プレイリストループ
  $totalFolder_cnt = 0
  foreach ($table in $focnt_fcnt) {
    # プレイリスト内のフォルダを取り出す
    $tableFolders = $folders | Where-Object { $_.playlist_id -eq $table.playlist_id }
    # folder_orderが完全に未定義の場合、フォルダ名で自然順ソートする(自然順参考文献:https://qiita.com/Variablob/items/3299333c143c193756d8)
    if (!$table.focnt) {
      $tableFolders = $tableFolders | Sort-Object { [regex]::Replace($_.folder, '\d+', { $args[0].Value.PadLeft(20) }) }
    }
    # プレイリスト内ループ
    $lr2folder_cnt = 0
    foreach ($tableFolder in $tableFolders) {
      # "表の名前+ソート名"を出力先フォルダとする
      $tablePath = Join-Path -Path "${OUT_DIR}\$($ORDER.SORT_NAME)" -ChildPath (Remove-InvalidFileNameChars $tableFolder.name)
      # 表のフォルダが存在しない場合は作成する
      New-Item -Path $tablePath -ItemType Directory -Force | Out-Null
      # .lr2folderのファイル名を設定
      $fileName = "{0:D4}.lr2folder" -f $lr2folder_cnt
      # テンプレートのプレースホルダーを置換
      $lr2folderContent = $LR2FOLDER_TEMPLATE -replace "{playlist_id}", $tableFolder.playlist_id `
        -replace "{folder}", $tableFolder.folder `
        -replace "{name}", $tableFolder.name `
        -replace "{order_by}", $ORDER.ORDER_BY `
        -replace "{sort_name}", $ORDER.SORT_NAME
      # ファイルに保存
      $filePath = Join-Path -Path $tablePath -ChildPath $fileName
      $lr2folderContent | Out-File -LiteralPath $filePath -Encoding "shift_jis"
      $lr2folder_cnt++
      $totalFolder_cnt++
      Write-Host "[Info] ${totalFolder_cnt}/${folderCount} : $($ORDER.SORT_NAME) : Table : $($tableFolder.name), Folder : $($tableFolder.folder)"
    }
  }
}

Write-Host "[Info] カスタムフォルダの出力が完了しました"

Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"; Read-Host

タグ付けを利用して最終プレイ日を選曲画面で確認する方法

まず、多くのスキンではタグはそんなに目立つところには表示されていないと思うので以下のページを参考にスキンを弄ると良いと思います。

そして、以下はタグ付け用のSQLです。上述のHexさんのものを参考に、前編で用意したlastplayの情報も表示できるように改変してあります。
前編でも使用したDB Browser for SQLiteを使用すると良いでしょう。スコア用のDBはscoreという名前でATTACHされている前提です。前編での作業時に「LR2.sqbpro」としてATTACH状態のプロジェクトを保存していればそれを使ってください。保存していなかったら「ATTACH DATABASE 'スコアDBの場所.db' AS score;」を実行してからタグ付けSQLを実行してください。
実行後変更を書き込みボタンを押さないとsong.dbに書き込まれません。
既存のタグ情報は全て削除されるのでご注意ください。

--sqliteにメモリを使ってもらってディスクIOを減らす設定、1GBとしておく
PRAGMA mmap_size=1000000000;
--タグ付け
UPDATE song SET tag = (
  --難易度表搭載状況
  ifnull((
    SELECT
      group_concat(symbol||replace(folder,compat_prefix,''), ',')
    FROM
      playlist_entry INNER JOIN playlist USING(playlist_id)
    WHERE
      hash = md5
      AND is_removed = 0 --playlist_entryの削除フラグが立っていないものを対象とする
  ),'表外')
  ||'|'||
  --最終プレイ日
  ifnull((
    SELECT
      substr(date(history.lastplay,'unixepoch'), 3)  --日付の形式は'22-07-14'のように年を下2桁だけ表示する(1000-9999年まで機能)
    FROM
      history
    WHERE
      song.hash = history.hash
  ),'未')
);

私の選曲画面では以下のように表示されます。タグの末尾に24-09-13と最終プレイ日が表示されています。収録されている難易度表が多いことも一目でわかりますし、AI難易度表では★2になっていることなどもわかります。

選曲画面でのタグの様子

タグ付け用のPowerShellスクリプト

いちいちDB Browser for SQLiteからSQLを実行するのは面倒なのでタグ付け用のPowerShellスクリプトを用意しました。
スクリプト冒頭にある3つの定数は環境に合わせて書き換えてください。
2024-09-15: 「# 設定が有効か確認」の部分で設定値の不備を検出できていなかったので修正しました。本筋の動作に影響のある修正ではありません。

# これは「PowerShell 7」で動作確認したスクリプトです

# 設定用
$SQLITE_PATH = "D:\BIN\SQLite\sqlite-tools-win-x64-3460100\sqlite3.exe" # 環境変数PATHを設定しているなら"sqlite3"みたいな指定方法でもOK
$SONGDB_PATH = "D:\LR2beta3\LR2files\Database\song.db"
$SCOREDB_PATH = "D:\LR2beta3\LR2files\Database\Score\NEETED.db"

# 設定が有効か確認
try {
  @($SQLITE_PATH, $SONGDB_PATH, $SCOREDB_PATH) | ForEach-Object { Get-Command $_ -ErrorAction Stop } | Out-Null
}
catch {
  Write-Host "[Error] 設定値がおかしいです" -BackgroundColor Red
  Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"; Read-Host
  exit
}

# 難易度表を絞りたい場合(▽,▼,☆,★,★★,sl,st辺りのみにしたいなど)はWHERE句で制限すると良いでしょう
# AI難易度表(https://bms.hexlataia.xyz/tables/ai.html)を入れておくと便利でしょう
$SQL_QUERY = @"
--sqliteにメモリを使ってもらってディスクIOを減らす設定、1GBとしておく
PRAGMA mmap_size=1000000000;
--スコアDBをアタッチする
ATTACH DATABASE '${SCOREDB_PATH}' AS score;
--タグ付け
UPDATE song SET tag = (
  --難易度表搭載状況
  ifnull((
    SELECT
      group_concat(symbol||replace(folder,compat_prefix,''), ',')
    FROM
      playlist_entry INNER JOIN playlist USING(playlist_id)
    WHERE
      hash = md5
      AND is_removed = 0 --playlist_entryの削除フラグが立っていないものを対象とする
  ),'表外')
  ||'|'||
  --最終プレイ日
  ifnull((
    SELECT
      substr(date(history.lastplay,'unixepoch'), 3)  --日付の形式は'22-07-14'のように年を下2桁だけ表示する(1000-9999年まで機能)
    FROM
      history
    WHERE
      song.hash = history.hash
  ),'未')
);
DETACH DATABASE score; 
"@

Write-Host "タグ付け実行中…"

$executionTime = Measure-Command {
  $SQL_QUERY | & $SQLITE_PATH $SONGDB_PATH
}

Write-Host "$($executionTime.TotalMilliseconds)msでタグ付けが完了しました"

Write-Host "スクリプトが完了しました。Enterキーを押すとウィンドウを閉じます。"
Read-Host

タグ付け時の裏技

タグ付け時のgroup_concat()で結合される順番は、以下のようにORDER BYで設定することができます。例えば「後からいれたAI難易度表のシンボルを手前に持ってきたい」といったときに使えます。これをやらない場合、playlist_id順に結合された結果が得られることになります。
どの難易度表がどのplaylist_idかというのは各自の環境によって違うので、playlistテーブルを見てみると良いでしょう。

group_concat(
  symbol||replace(folder,compat_prefix,'')
  ORDER BY
    -表の並びを補正、☆の後に★が来るようになど
    CASE playlist_id
        WHEN 7   THEN 1
        WHEN 18  THEN 2
        WHEN 8   THEN 3
        WHEN 9   THEN 4
        WHEN 12  THEN 5
        WHEN 13  THEN 6
        WHEN 10  THEN 7
        WHEN 11  THEN 8
        WHEN 17  THEN 9
        WHEN 16  THEN 10
        WHEN 19  THEN 11
        WHEN 321 THEN 12
        ELSE playlist_id + 100 --並びが未定義の場合後ろに来るように+100
    END
)

この記事が気に入ったらサポートをしてみませんか?