最近カラオケにハマっているKadoです。スマホアプリ上で選曲できるデンモクというアプリが本当に便利ですね。今回はそれに関連して、DAMの採点Aiデータを取得してみた、というネタを提供してみようと思います。
背景
もともとDAM★トモというWebアプリで、DAMで歌ったカラオケの履歴を閲覧することができます。しかし、画像をよく見ると、こんな事が書いてありました。
りれき(最新200件)
今回の採点Aiデータ取得を実施する直前は192件で、つまり次回歌ったら200をオーバーフロー
して、DAMのサーバーは崩壊します(大嘘)。
200件より先のデータが閲覧できないのは困る、と思い、今回はスクレイピングを用いて事前にデータを取得しておこう、と決断しました。
200より過去のデータはリスト取得不可
結論を言うと、やはり200以降のデータはリスト取得ができませんでした。しかし、scoringAiId
を指定して単体取得をすることができます。最悪scoringAiId
さえ別途で保存すれば後から取得できそうですね。
他にもやりたいこととして、
- 初めて歌った曲に「初」と付与したい
- 途中で
Reject
(中断)した曲を最高点数から除外したい - 日付ごと、曲・歌手ごとのソート、フィルタを実施したい
という思いがあり、前々からやりたかったスクレイピングでした。
DAMの採点Aiデータの取得API
公式ドキュメントがあるわけではないため、スクレイピングした結果で、自分でそれっぽく以下にまとめてみました。
前提
一応非公開でもセッション・トークンを付与することで閲覧可能になるが、現実的ではないので公開にしてやってみるのをおすすめします。
エンドポイント
https://www.clubdam.com/app/damtomo/scoring/GetScoringAiListXML.do
QueryParams
key | value |
---|---|
cdmCardNo | CLUB DAM CARD ID必須
|
scoringAiId | 採点AiのID(単体取得の場合) |
pageNo | ページNo(リスト取得の場合) |
detailFlg | detailを表示するか(1 で表示) |
「CLUB DAM CARD ID」は、サイト内に明示的に示されているわけではないので、ブラウザの開発者モードを開いて、ネットワークタブにてcdmCardId
を見つけましょう。
Response
xmlの結果
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="https://www.clubdam.com/app/damtomo/scoring/GetScoringAiListXML" type="2.2">
<result>
<status>OK</status>
<statusCode>0000</statusCode>
<message></message>
</result>
<data>
<page dataCount="192" pageCount="39" hasNext="1" hasPreview="0">1</page>
<cdmCardNo>{cdmCardNo}</cdmCardNo>
</data>
<list count="5">
<data>
<scoring scoringAiId="1413882" requestNo="5373-53" contentsName="もぐもぐYUMMY!" artistName="猫又おかゆ" dContentsName="もぐもぐYUMMY!" dArtistName="猫又おかゆ" lastPerformKey="0" scoringDateTime="20240615085712" favorite="0">90066</scoring>
</data>
<data>
<scoring scoringAiId="1413880" requestNo="1263-42" contentsName="カミサマ・ネコサマ" artistName="猫又おかゆ" dContentsName="カミサマ・ネコサマ" dArtistName="猫又おかゆ" lastPerformKey="0" scoringDateTime="20240615085148" favorite="0">82395</scoring>
</data>
<data>
<scoring scoringAiId="1413875" requestNo="2463-46" contentsName="あした天気になれ" artistName="中島みゆき" dContentsName="あした天気になれ" dArtistName="中島みゆき" lastPerformKey="0" scoringDateTime="20240615084616" favorite="0">88489</scoring>
</data>
<data>
<scoring scoringAiId="1413874" requestNo="1249-95" contentsName="NEXT COLOR PLANET" artistName="星街すいせい" dContentsName="NEXT COLOR PLANET" dArtistName="星街すいせい" lastPerformKey="0" scoringDateTime="20240615084122" favorite="0">89070</scoring>
</data>
<data>
<scoring scoringAiId="1413871" requestNo="1192-34" contentsName="いのち" artistName="AZKi" dContentsName="いのち" dArtistName="AZKi" lastPerformKey="0" scoringDateTime="20240615083629" favorite="0">85902</scoring>
</data>
</list>
</document>
今どきだとapplication/json
のイメージですが、DAMではxml
形式でデータが返るようです。
scoringAiId
を指定しない場合は、5個ずつデータを取得できる仕様です。
詳細取得の場合
詳細取得を実施する場合は、detailFlg
を1
にすることで実現できます。これは単体取得・リスト取得共に、同じように適用されます。
xml
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="https://www.clubdam.com/app/damtomo/scoring/GetScoringAiListXML" type="2.2">
<result>
<status>OK</status>
<statusCode>0000</statusCode>
<message></message>
</result>
<data>
<page dataCount="1" pageCount="1" hasNext="0" hasPreview="0">1</page>
<cdmCardNo>{cdmCardNo}</cdmCardNo>
</data>
<list count="1">
<data>
<scoring scoringAiId="1413882" requestNo="5373-53" contentsName="もぐもぐYUMMY!" artistName="猫又おかゆ" dContentsName="もぐもぐYUMMY!" dArtistName="猫又おかゆ" damserial="AT016103" dataKind="A00001" dataSize="" scoringEngineVersionNumber="" edyId="" clubDamCardNo="{cdmCardNo}" entryCount="1" topRecordNumber="" lastPerformKey="0" requestNoTray="5373" requestNoChapter="53" fadeout="0" analysisReportCommentNo="3701" radarChartPitch="86" radarChartStability="79" radarChartExpressive="65" radarChartVibratoLongtone="89" radarChartRhythm="83" spare1="" singingRangeHighest="70" singingRangeLowest="54" vocalRangeHighest="68" vocalRangeLowest="54" intonation="82" kobushiCount="6" shakuriCount="4" fallCount="0" timing="4" longtoneSkill="7" vibratoSkill="7" vibratoType="11" vibratoTotalSecond="22" vibratoCount="4" accentCount="0" hammeringOnCount="0" edgeVoiceCount="0" hiccupCount="0" aiSensitivityMeterAdd="64" aiSensitivityMeterDeduct="0" aiSensitivityPoints="64" aiSensitivityBonus="3453" nationalAverageTotalPoints="82183" nationalAveragePitch="73" nationalAverageStability="60" nationalAverageExpression="60" nationalAverageVibratoAndLongtone="53" nationalAverageRhythm="86" spare2="" intervalGraphPointsSection01="84" intervalGraphPointsSection02="91" intervalGraphPointsSection03="86" intervalGraphPointsSection04="85" intervalGraphPointsSection05="90" intervalGraphPointsSection06="88" intervalGraphPointsSection07="90" intervalGraphPointsSection08="94" intervalGraphPointsSection09="90" intervalGraphPointsSection10="96" intervalGraphPointsSection11="79" intervalGraphPointsSection12="86" intervalGraphPointsSection13="92" intervalGraphPointsSection14="90" intervalGraphPointsSection15="82" intervalGraphPointsSection16="95" intervalGraphPointsSection17="92" intervalGraphPointsSection18="92" intervalGraphPointsSection19="84" intervalGraphPointsSection20="81" intervalGraphPointsSection21="87" intervalGraphPointsSection22="82" intervalGraphPointsSection23="82" intervalGraphPointsSection24="78" intervalGraphIndexSection01="B'01" intervalGraphIndexSection02="B'01" intervalGraphIndexSection03="B'01" intervalGraphIndexSection04="B'10" intervalGraphIndexSection05="B'10" intervalGraphIndexSection06="B'10" intervalGraphIndexSection07="B'10" intervalGraphIndexSection08="B'10" intervalGraphIndexSection09="B'10" intervalGraphIndexSection10="B'00" intervalGraphIndexSection11="B'01" intervalGraphIndexSection12="B'01" intervalGraphIndexSection13="B'01" intervalGraphIndexSection14="B'10" intervalGraphIndexSection15="B'10" intervalGraphIndexSection16="B'10" intervalGraphIndexSection17="B'10" intervalGraphIndexSection18="B'10" intervalGraphIndexSection19="B'10" intervalGraphIndexSection20="B'10" intervalGraphIndexSection21="B'10" intervalGraphIndexSection22="B'10" intervalGraphIndexSection23="B'10" intervalGraphIndexSection24="B'10" maxTotalPoints="86276" scoringDateTime="20240615085712" aiSensitivityGraphAddPointsSection01="21" aiSensitivityGraphAddPointsSection02="40" aiSensitivityGraphAddPointsSection03="48" aiSensitivityGraphAddPointsSection04="31" aiSensitivityGraphAddPointsSection05="12" aiSensitivityGraphAddPointsSection06="35" aiSensitivityGraphAddPointsSection07="21" aiSensitivityGraphAddPointsSection08="42" aiSensitivityGraphAddPointsSection09="30" aiSensitivityGraphAddPointsSection10="12" aiSensitivityGraphAddPointsSection11="12" aiSensitivityGraphAddPointsSection12="56" aiSensitivityGraphAddPointsSection13="12" aiSensitivityGraphAddPointsSection14="22" aiSensitivityGraphAddPointsSection15="12" aiSensitivityGraphAddPointsSection16="40" aiSensitivityGraphAddPointsSection17="42" aiSensitivityGraphAddPointsSection18="55" aiSensitivityGraphAddPointsSection19="56" aiSensitivityGraphAddPointsSection20="27" aiSensitivityGraphAddPointsSection21="27" aiSensitivityGraphAddPointsSection22="42" aiSensitivityGraphAddPointsSection23="31" aiSensitivityGraphAddPointsSection24="22" aiSensitivityGraphDeductPointsSection01="0" aiSensitivityGraphDeductPointsSection02="0" aiSensitivityGraphDeductPointsSection03="0" aiSensitivityGraphDeductPointsSection04="0" aiSensitivityGraphDeductPointsSection05="0" aiSensitivityGraphDeductPointsSection06="0" aiSensitivityGraphDeductPointsSection07="0" aiSensitivityGraphDeductPointsSection08="0" aiSensitivityGraphDeductPointsSection09="0" aiSensitivityGraphDeductPointsSection10="0" aiSensitivityGraphDeductPointsSection11="0" aiSensitivityGraphDeductPointsSection12="0" aiSensitivityGraphDeductPointsSection13="0" aiSensitivityGraphDeductPointsSection14="0" aiSensitivityGraphDeductPointsSection15="0" aiSensitivityGraphDeductPointsSection16="0" aiSensitivityGraphDeductPointsSection17="0" aiSensitivityGraphDeductPointsSection18="0" aiSensitivityGraphDeductPointsSection19="0" aiSensitivityGraphDeductPointsSection20="0" aiSensitivityGraphDeductPointsSection21="0" aiSensitivityGraphDeductPointsSection22="0" aiSensitivityGraphDeductPointsSection23="0" aiSensitivityGraphDeductPointsSection24="0" aiSensitivityGraphIndexSection01="B'01" aiSensitivityGraphIndexSection02="B'01" aiSensitivityGraphIndexSection03="B'01" aiSensitivityGraphIndexSection04="B'10" aiSensitivityGraphIndexSection05="B'10" aiSensitivityGraphIndexSection06="B'10" aiSensitivityGraphIndexSection07="B'10" aiSensitivityGraphIndexSection08="B'10" aiSensitivityGraphIndexSection09="B'10" aiSensitivityGraphIndexSection10="B'00" aiSensitivityGraphIndexSection11="B'01" aiSensitivityGraphIndexSection12="B'01" aiSensitivityGraphIndexSection13="B'01" aiSensitivityGraphIndexSection14="B'10" aiSensitivityGraphIndexSection15="B'10" aiSensitivityGraphIndexSection16="B'10" aiSensitivityGraphIndexSection17="B'10" aiSensitivityGraphIndexSection18="B'10" aiSensitivityGraphIndexSection19="B'10" aiSensitivityGraphIndexSection20="B'10" aiSensitivityGraphIndexSection21="B'10" aiSensitivityGraphIndexSection22="B'10" aiSensitivityGraphIndexSection23="B'10" aiSensitivityGraphIndexSection24="B'10" favorite="0">90066</scoring>
</data>
</list>
</document>
詳細の取得にすると、恐ろしいくらいプロパティが付与されています。上記プロパティを全て取得することで、下記の画像のようなものが作成できます。
リストの「詳細」ボタンクリック | 詳細が表示される |
---|---|
実際のWebアプリ上でも、詳細をクリックすると都度APIをfetchし、データを反映しています。
他の採点もほぼ同等
他の採点で、DX-Gも試してみましたが、同じ構成だと思います(詳しくは見ていないです)。
活用例
データを取得すればあとは加工して見やすくしたり、分析したりするのが賢明かと思いますが、今回私がやってみたことを紹介します。
sqliteに保存する
一応単体としてはデータが残っているのでscoringAiIdだけ保存でも良さげですが、毎回fetchするのも申し訳ないので、ローカルでsqliteに保存してみます。
自分の得意なNext.jsのAPI Routesで実施してみると、以下のようなコードになります。
import { convertDamAiSummary, convertMeta } from "@/utils/convertData";
import { convertXmlToJson } from "@/utils/convertXmlToJson";
import { PrismaClient } from "@prisma/client";
import axios from "axios";
import { NextRequest, NextResponse } from "next/server";
const prisma = new PrismaClient();
export const GET = async (
req: NextRequest,
{ params }: { params: { cdmCardNo: string } }
) => {
// クエリパラメータを取得
const { searchParams } = new URL(req.url);
const pageNo = searchParams.get("pageNo");
const scoringAiId = searchParams.get("scoringAiId");
const detailFlg = searchParams.get("detailFlg");
const cdmCardNo = params.cdmCardNo;
if (!cdmCardNo) {
return NextResponse.json(
{ message: "cdmCardNo is required" },
{ status: 400 }
);
}
const url =
"https://www.clubdam.com/app/damtomo/scoring/GetScoringAiListXML.do";
const response = await axios.get(url, {
params: {
cdmCardNo,
pageNo: pageNo || undefined,
scoringAiId: scoringAiId || undefined,
detailFlg: detailFlg || undefined,
},
});
const data = response.data;
// xmlをjsonに変換(ライブラリを使用)
const resultJson = await convertXmlToJson(data);
// jsonをきれいなフォーマットに変更(独自の関数)
const convertedData = convertDamAiSummary(resultJson);
try {
const query = convertedData.map((damAiScore) => {
return prisma.damAiScores.upsert({
where: { scoringAiId: damAiScore.scoringAiId },
update: {},
create: {
...damAiScore,
score: Number(damAiScore.score),
},
});
});
await prisma.$transaction([...query]);
} catch (e) {
console.error(e);
}
// metaデータもきれいなフォーマットに変更
const meta = convertMeta(resultJson);
return NextResponse.json({ list: convertedData, meta });
};
const convertDamAiSummary = (data: any): IDamAiRecord[] => {
return data.document.list[0].data.map((d: any) => {
const scoring = d.scoring[0];
return {
score: scoring._,
...scoring.$,
};
});
};
export const convertMeta = (data: any): IMeta => {
const page = data.document.data[0].page[0];
return {
page: Number(page._),
dataCount: Number(page.$.dataCount),
pageCount: Number(page.$.pageCount),
hasNext: page.$.hasNext === "1",
hasPreview: page.$.hasPreview === "1",
};
};
ちなみに型定義ファイルはこんな感じ(ほぼstringですが本来はちゃんとした型があります)
interface IDamAiRecord {
score: string;
scoringAiId: string;
requestNo: string;
contentsName: string;
artistName: string;
dContentsName: string;
dArtistName: string;
damserial: string;
dataKind: string;
dataSize: string;
scoringEngineVersionNumber: string;
edyId: string;
clubDamCardNo: string;
entryCount: string;
topRecordNumber: string;
lastPerformKey: string;
requestNoTray: string;
requestNoChapter: string;
fadeout: string;
analysisReportCommentNo: string;
radarChartPitch: string;
radarChartStability: string;
radarChartExpressive: string;
radarChartVibratoLongtone: string;
radarChartRhythm: string;
spare1: string;
singingRangeHighest: string;
singingRangeLowest: string;
vocalRangeHighest: string;
vocalRangeLowest: string;
intonation: string;
kobushiCount: string;
shakuriCount: string;
fallCount: string;
timing: string;
longtoneSkill: string;
vibratoSkill: string;
vibratoType: string;
vibratoTotalSecond: string;
vibratoCount: string;
accentCount: string;
hammeringOnCount: string;
edgeVoiceCount: string;
hiccupCount: string;
aiSensitivityMeterAdd: string;
aiSensitivityMeterDeduct: string;
aiSensitivityPoints: string;
aiSensitivityBonus: string;
nationalAverageTotalPoints: string;
nationalAveragePitch: string;
nationalAverageStability: string;
nationalAverageExpression: string;
nationalAverageVibratoAndLongtone: string;
nationalAverageRhythm: string;
spare2: string;
intervalGraphPointsSection01: string;
intervalGraphPointsSection02: string;
intervalGraphPointsSection03: string;
intervalGraphPointsSection04: string;
intervalGraphPointsSection05: string;
intervalGraphPointsSection06: string;
intervalGraphPointsSection07: string;
intervalGraphPointsSection08: string;
intervalGraphPointsSection09: string;
intervalGraphPointsSection10: string;
intervalGraphPointsSection11: string;
intervalGraphPointsSection12: string;
intervalGraphPointsSection13: string;
intervalGraphPointsSection14: string;
intervalGraphPointsSection15: string;
intervalGraphPointsSection16: string;
intervalGraphPointsSection17: string;
intervalGraphPointsSection18: string;
intervalGraphPointsSection19: string;
intervalGraphPointsSection20: string;
intervalGraphPointsSection21: string;
intervalGraphPointsSection22: string;
intervalGraphPointsSection23: string;
intervalGraphPointsSection24: string;
intervalGraphIndexSection01: string;
intervalGraphIndexSection02: string;
intervalGraphIndexSection03: string;
intervalGraphIndexSection04: string;
intervalGraphIndexSection05: string;
intervalGraphIndexSection06: string;
intervalGraphIndexSection07: string;
intervalGraphIndexSection08: string;
intervalGraphIndexSection09: string;
intervalGraphIndexSection10: string;
intervalGraphIndexSection11: string;
intervalGraphIndexSection12: string;
intervalGraphIndexSection13: string;
intervalGraphIndexSection14: string;
intervalGraphIndexSection15: string;
intervalGraphIndexSection16: string;
intervalGraphIndexSection17: string;
intervalGraphIndexSection18: string;
intervalGraphIndexSection19: string;
intervalGraphIndexSection20: string;
intervalGraphIndexSection21: string;
intervalGraphIndexSection22: string;
intervalGraphIndexSection23: string;
intervalGraphIndexSection24: string;
maxTotalPoints: string;
scoringDateTime: string;
aiSensitivityGraphAddPointsSection01: string;
aiSensitivityGraphAddPointsSection02: string;
aiSensitivityGraphAddPointsSection03: string;
aiSensitivityGraphAddPointsSection04: string;
aiSensitivityGraphAddPointsSection05: string;
aiSensitivityGraphAddPointsSection06: string;
aiSensitivityGraphAddPointsSection07: string;
aiSensitivityGraphAddPointsSection08: string;
aiSensitivityGraphAddPointsSection09: string;
aiSensitivityGraphAddPointsSection10: string;
aiSensitivityGraphAddPointsSection11: string;
aiSensitivityGraphAddPointsSection12: string;
aiSensitivityGraphAddPointsSection13: string;
aiSensitivityGraphAddPointsSection14: string;
aiSensitivityGraphAddPointsSection15: string;
aiSensitivityGraphAddPointsSection16: string;
aiSensitivityGraphAddPointsSection17: string;
aiSensitivityGraphAddPointsSection18: string;
aiSensitivityGraphAddPointsSection19: string;
aiSensitivityGraphAddPointsSection20: string;
aiSensitivityGraphAddPointsSection21: string;
aiSensitivityGraphAddPointsSection22: string;
aiSensitivityGraphAddPointsSection23: string;
aiSensitivityGraphAddPointsSection24: string;
aiSensitivityGraphDeductPointsSection01: string;
aiSensitivityGraphDeductPointsSection02: string;
aiSensitivityGraphDeductPointsSection03: string;
aiSensitivityGraphDeductPointsSection04: string;
aiSensitivityGraphDeductPointsSection05: string;
aiSensitivityGraphDeductPointsSection06: string;
aiSensitivityGraphDeductPointsSection07: string;
aiSensitivityGraphDeductPointsSection08: string;
aiSensitivityGraphDeductPointsSection09: string;
aiSensitivityGraphDeductPointsSection10: string;
aiSensitivityGraphDeductPointsSection11: string;
aiSensitivityGraphDeductPointsSection12: string;
aiSensitivityGraphDeductPointsSection13: string;
aiSensitivityGraphDeductPointsSection14: string;
aiSensitivityGraphDeductPointsSection15: string;
aiSensitivityGraphDeductPointsSection16: string;
aiSensitivityGraphDeductPointsSection17: string;
aiSensitivityGraphDeductPointsSection18: string;
aiSensitivityGraphDeductPointsSection19: string;
aiSensitivityGraphDeductPointsSection20: string;
aiSensitivityGraphDeductPointsSection21: string;
aiSensitivityGraphDeductPointsSection22: string;
aiSensitivityGraphDeductPointsSection23: string;
aiSensitivityGraphDeductPointsSection24: string;
aiSensitivityGraphIndexSection01: string;
aiSensitivityGraphIndexSection02: string;
aiSensitivityGraphIndexSection03: string;
aiSensitivityGraphIndexSection04: string;
aiSensitivityGraphIndexSection05: string;
aiSensitivityGraphIndexSection06: string;
aiSensitivityGraphIndexSection07: string;
aiSensitivityGraphIndexSection08: string;
aiSensitivityGraphIndexSection09: string;
aiSensitivityGraphIndexSection10: string;
aiSensitivityGraphIndexSection11: string;
aiSensitivityGraphIndexSection12: string;
aiSensitivityGraphIndexSection13: string;
aiSensitivityGraphIndexSection14: string;
aiSensitivityGraphIndexSection15: string;
aiSensitivityGraphIndexSection16: string;
aiSensitivityGraphIndexSection17: string;
aiSensitivityGraphIndexSection18: string;
aiSensitivityGraphIndexSection19: string;
aiSensitivityGraphIndexSection20: string;
aiSensitivityGraphIndexSection21: string;
aiSensitivityGraphIndexSection22: string;
aiSensitivityGraphIndexSection23: string;
aiSensitivityGraphIndexSection24: string;
favorite: string;
}
interface IMeta {
page: number;
dataCount: number;
pageCount: number;
hasNext: boolean;
hasPreview: boolean;
}
基本的にはupsertを使用して、存在しない行だけを追加する方式を取っています。あまり賢いコードとは言えませんが、処理次第では、過去の採点Aiデータを一発で取得することも可能だと思います。
表として表示
やっていることとしてはDAMデンモクのサイトと一緒ですが、5個ずつの表示を一括表示にできます。
グラフとして表示
グラフにするとこんな感じです。今まで歌ってきたものの傾向を一括表示でき、様々なデータとして使えそうですね。
まとめ
まだスクレイピングをし始めて数日しか経っていないのでまともなシステム作れていませんが、かなり可能性が広がりますね。もう少し完成度を高めていい感じにいい感じのアプリが作れたらいいかな、と感じています。
余談
今回DAMのスクレイピングをしてみましたが、APIレスポンスが本当に爆速で驚きました。採点データは相当な人数のデータを記録していると思うので、それをここまで爆速にレスポンスを返すのは本当に驚きが止まりません。DAMデンモクのURLの拡張子が.do
であることから、Javaで動いているのは予想できましたが、早い動作にも直結しているのでしょうか。気になるところです。
ただそう思うとDAMの機械はなぜあんなにも大きくなったのかは未だに謎です。やはりリアルタイムで声を解析するのでRTX4080くらいのGPUとか積んでいるんですかね。それなら逆に小さいとすら思えてくるかも。そのうちDAMのリモコンもiPadとかに置き換わるのかしら。