maistats는 maimaidx-eng.com 데이터를 수집하고, 곡 메타데이터와 개인 플레이 기록을 각각 분리해서 제공하는 monorepo입니다. 저장소에는 Rust 서버 3개와 Vite/React 프런트엔드 1개가 들어 있습니다.
핵심 배포 모델은 다음과 같습니다.
maistats-song-info: 개발자가 공용으로 호스팅maistats-discord-bot: 개발자가 공용으로 호스팅apps/maistats: 개발자가 공용으로 호스팅maistats-record-collector: 각 사용자가 자기 SEGA ID로 직접 호스팅
즉, 곡 정보는 공유되고 플레이 기록은 사용자별 self-hosted collector에서만 관리됩니다.
공용 곡 정보 서버입니다.
- 내부
songdb서브시스템으로 곡 목록, 버전, 내부 레벨, 재킷 이미지를 준비합니다. - 내부 레벨은 maimai DX NET의 레벨별 곡 목록 페이지를 읽어 매일 다시 추론합니다.
data/song_data/data.json을 메모리로 로드해 API로 제공합니다.- SongDB 관련 env가 설정돼 있으면 시작 시 업데이트를 시도하고, 이후 매일 07:30 KST에 다시 갱신합니다.
- 대표 엔드포인트:
GET /healthGET /health/readyGET /api/songsGET /api/songs/versionsPOST /api/songs/metadataGET /api/cover/:image_name
개인 플레이 기록 수집 서버입니다.
- 사용자의 SEGA ID로 로그인합니다.
- 프로세스별 임시 cookie store를 사용해 인증 세션을 유지합니다.
- SQLite를 사용하며 런타임에
sqlx::migrate!()로 마이그레이션을 적용합니다. - 시작 시 점수 시드를 보장하고
playerData를 읽은 뒤, 플레이 횟수 변화가 있으면 recent를 동기화합니다. - 이후 10분마다 백그라운드 polling을 수행합니다.
- 유지보수 시간대에는 초기 동기화를 건너뜁니다.
- 대표 엔드포인트:
GET /healthGET /health/readyGET /api/playerGET /api/scores/ratedGET /api/songs/scoresGET /api/recentGET /api/todayGET /api/rating/targets
공용 Discord 봇입니다.
- 전역 Song Info 서버를 참조합니다.
- 각 Discord 사용자는
/register <url>로 자기 Record Collector URL을 등록합니다. - 봇 자체 SQLite에
Discord user -> record collector URL매핑을 저장합니다. - 시작 시 slash command를 전역 등록하고, 개발자 계정에만 startup 요약 DM을 보냅니다.
- 주요 커맨드:
/how-to-use/register/mai-score/mai-song-info/mai-recent/mai-today
공용 웹 프런트엔드입니다.
- Vite + React 기반입니다.
- 기본적으로 Song Info 서버와 Record Collector 서버를 각각 다른 origin으로 붙습니다.
- Settings 화면에서 연결 URL을 직접 바꿀 수 있어, 공용 프런트엔드에서 각자 self-hosted collector에 붙는 방식으로 사용할 수 있습니다.
- 주요 화면:
- Scores
- Rating
- Playlogs
- Random Picker
- Settings
.
|-- apps/maistats/ # Vite + React frontend
|-- maistats-song-info/ # shared song metadata server
|-- maistats-record-collector/ # per-user self-hosted record server
|-- maistats-discord-bot/ # shared Discord bot
`-- crates/
|-- maimai-auth/ # maimaidx-eng.com auth helpers
|-- maimai-parsers/ # HTML parsers
`-- models/ # shared API/domain/storage models
- Rust stable
- Node.js 20+
- npm
- SEGA ID 계정
- Discord Bot Token 및 개발자 Discord User ID
- Song Info 갱신까지 사용할 경우 SongDB 관련 인증 정보
루트 .env는 Rust 서비스들이 공용으로 사용합니다.
cp .env.example .env
기본 항목:
- Record Collector
SEGA_IDSEGA_PASSWORDRECORD_COLLECTOR_PORTDATA_DIRDATABASE_URL
- Song Info
SONG_INFO_PORTSONG_DATA_PATH
- Discord Bot
DISCORD_BOT_TOKENDISCORD_DEV_USER_IDSONG_INFO_SERVER_URLDISCORD_BOT_DATABASE_URL
- SongDB updater
MAIMAI_INTL_SEGA_IDMAIMAI_INTL_SEGA_PASSWORDMAIMAI_JP_SEGA_IDMAIMAI_JP_SEGA_PASSWORDUSER_AGENT
프런트엔드는 별도 .env를 사용합니다.
cp apps/maistats/.env.example apps/maistats/.env
SONG_INFO_SERVER_URLRECORD_COLLECTOR_SERVER_URL
프런트엔드의 RECORD_COLLECTOR_SERVER_URL은 기본값일 뿐입니다. 실제 배포에서는 사용자가 Settings에서 자기 collector URL로 덮어쓰는 흐름을 전제로 합니다.
의존성 설치:
npm ci
cargo run -p maistats-song-info
기본 주소: http://localhost:3001
cargo run -p maistats-record-collector
기본 주소: http://localhost:3000
처음 실행 시 data/를 만들고 DB 마이그레이션을 적용합니다. 인증이나 초기 동기화가 실패해도 서버는 뜨므로, API 상태 확인과 디버깅을 분리해서 진행할 수 있습니다.
cargo run -p maistats-discord-bot
Discord 사용자는 /register <record-collector-url>로 자신의 collector를 연결해야 합니다.
npm run dev:maistats
기본 주소: http://localhost:5174
권장 운영 형태는 다음과 같습니다.
- 개발자가
maistats-song-info,maistats-discord-bot,apps/maistats를 공용으로 운영합니다. - 각 사용자는 자신의 환경에서
maistats-record-collector를 띄웁니다. - Discord에서는
/register로 collector URL을 등록합니다. - 웹에서는 Settings에서 collector URL을 입력해 같은 공용 프런트엔드에 연결합니다.
이 구조 덕분에 곡 정보와 UI는 한 곳에서 관리하면서도, 플레이 기록 DB와 로그인 세션은 각 사용자 환경에 남길 수 있습니다.
- Record Collector
- SQLite DB: 기본값
data/maimai.sqlite3 - 임시 cookie file: OS temp 디렉터리 아래
maistats-cookies-<pid>.json
- SQLite DB: 기본값
- Song Info
- 곡 JSON: 기본값
data/song_data/data.json - 재킷 이미지: 기본값
data/song_data/
- 곡 JSON: 기본값
- Discord Bot
- 봇 DB: 기본값
data/maistats-discord-bot.sqlite3
- 봇 DB: 기본값
data/와 .env는 커밋하지 않습니다.
cargo fmt --all
cargo clippy --all -- -D warnings
cargo test
npm run build:maistats
- Rust 서비스는 각각 독립 바이너리로 배포합니다.
- 프런트엔드
apps/maistats는 Cloudflare용 Vite 설정을 사용합니다. - CI/배포 워크플로는
.github/workflows/아래에 있습니다.