record-daemon: refactor to record raw league data

This commit is contained in:
2026-03-27 13:42:12 +01:00
parent b09f669e73
commit d67d52fa86
7 changed files with 191 additions and 951 deletions

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{Result, TimelineError};
use crate::lqp::{GameEvent, RunePage, SummonerSpells};
use crate::lqp::GameEvent;
use crate::recording::RecordingResult;
/// A timestamped event in the timeline.
@@ -32,55 +32,36 @@ pub struct TimestampedEvent {
}
/// Metadata for a recording.
/// Stores raw API responses for maximum flexibility and future-proofing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingMetadata {
/// Unique recording ID.
pub id: Uuid,
/// Game ID if available.
pub game_id: Option<u64>,
/// Match ID if available.
pub match_id: Option<String>,
/// Champion played.
pub champion: Option<String>,
/// Skin name.
pub skin_name: Option<String>,
/// Queue type (ranked, normal, aram, etc.).
pub queue_type: Option<String>,
/// Queue ID.
pub queue_id: Option<u32>,
/// Game mode.
pub game_mode: Option<String>,
/// Map name.
pub map_name: Option<String>,
/// Player's summoner name.
pub summoner_name: Option<String>,
/// Player's PUUID.
/// Raw session data from `/lol-gameflow/v1/session`.
#[serde(default)]
pub puuid: Option<String>,
/// Team (100 = blue, 200 = red).
pub team: Option<u32>,
/// Whether the game was won.
pub victory: Option<bool>,
/// Final player stats.
pub final_stats: Option<GameFinalStats>,
/// Player's rune page at game start.
pub raw_session: Option<serde_json::Value>,
/// Raw summoner data from `/lol-summoner/v1/current-summoner`.
#[serde(default)]
pub runes: Option<RunePage>,
/// Player's summoner spells.
pub raw_summoner: Option<serde_json::Value>,
/// Raw champion select data from `/lol-champ-select/v1/session`.
#[serde(default)]
pub summoner_spells: Option<SummonerSpells>,
/// Raw end-of-game stats JSON from the API.
pub raw_champion_select: Option<serde_json::Value>,
/// Raw rune page data from `/lol-perks/v1/currentpage`.
#[serde(default)]
pub raw_rune_page: Option<serde_json::Value>,
/// Raw live client data from `/liveclientdata/allgamedata`.
#[serde(default)]
pub raw_live_client_data: Option<serde_json::Value>,
/// Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`.
#[serde(default)]
pub raw_end_game_stats: Option<serde_json::Value>,
/// All players in the game (puuid -> summoner name mapping).
#[serde(default)]
pub all_players: Vec<PlayerIdentityInfo>,
/// Recording start time.
pub start_time: DateTime<Utc>,
/// Recording end time.
pub end_time: Option<DateTime<Utc>>,
/// Recording duration.
// #[serde(with = "chrono::serde::seconds")]
pub duration: Duration,
/// Output file path.
pub file_path: Option<PathBuf>,
@@ -92,64 +73,16 @@ pub struct RecordingMetadata {
pub finalized: bool,
}
/// Player identity information for puuid to summoner name mapping.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerIdentityInfo {
/// Player's PUUID.
pub puuid: String,
/// Player's summoner name.
pub summoner_name: String,
/// Player's champion name.
#[serde(default)]
pub champion_name: Option<String>,
/// Player's team (100 or 200).
#[serde(default)]
pub team: Option<u32>,
}
/// Final game statistics for the player.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameFinalStats {
/// Kills.
pub kills: u32,
/// Deaths.
pub deaths: u32,
/// Assists.
pub assists: u32,
/// Creep score.
pub creep_score: u32,
/// Gold earned.
pub gold_earned: u32,
/// Damage dealt.
pub damage_dealt: u64,
/// Damage taken.
pub damage_taken: u64,
/// Vision score.
pub vision_score: f64,
/// Game duration in seconds.
pub game_duration: f64,
}
/// Update for recording metadata.
#[derive(Debug, Clone, Default)]
pub struct MetadataUpdate {
pub champion: Option<String>,
pub match_id: Option<String>,
pub skin_name: Option<String>,
pub queue_type: Option<String>,
pub queue_id: Option<u32>,
pub game_mode: Option<String>,
pub map_name: Option<String>,
pub summoner_name: Option<String>,
pub puuid: Option<String>,
pub team: Option<u32>,
pub victory: Option<bool>,
pub final_stats: Option<GameFinalStats>,
pub runes: Option<RunePage>,
pub summoner_spells: Option<SummonerSpells>,
pub game_id: Option<u64>,
pub raw_session: Option<serde_json::Value>,
pub raw_summoner: Option<serde_json::Value>,
pub raw_champion_select: Option<serde_json::Value>,
pub raw_rune_page: Option<serde_json::Value>,
pub raw_live_client_data: Option<serde_json::Value>,
pub raw_end_game_stats: Option<serde_json::Value>,
pub all_players: Vec<PlayerIdentityInfo>,
}
impl RecordingMetadata {
@@ -158,22 +91,12 @@ impl RecordingMetadata {
Self {
id: Uuid::new_v4(),
game_id: result.game_id,
match_id: None,
champion: result.champion.clone(),
skin_name: None,
queue_type: None,
queue_id: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_session: None,
raw_summoner: None,
raw_champion_select: None,
raw_rune_page: None,
raw_live_client_data: None,
raw_end_game_stats: None,
all_players: Vec::new(),
start_time: result.start_time,
end_time: Some(result.end_time),
duration: result.duration,
@@ -220,27 +143,17 @@ impl TimelineStore {
/// Start a new recording entry (called when recording begins).
/// Returns the recording ID for tracking events during recording.
pub fn start_recording_entry(&self, game_id: Option<u64>, champion: Option<String>) -> Uuid {
pub fn start_recording_entry(&self, game_id: Option<u64>, _champion: Option<String>) -> Uuid {
let id = Uuid::new_v4();
let metadata = RecordingMetadata {
id,
game_id,
match_id: None,
champion,
skin_name: None,
queue_type: None,
queue_id: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_session: None,
raw_summoner: None,
raw_champion_select: None,
raw_rune_page: None,
raw_live_client_data: None,
raw_end_game_stats: None,
all_players: Vec::new(),
start_time: Utc::now(),
end_time: None,
duration: Duration::zero(),
@@ -286,22 +199,12 @@ impl TimelineStore {
let metadata = RecordingMetadata {
id,
game_id: result.game_id,
match_id: None,
champion: result.champion.clone(),
skin_name: None,
queue_type: None,
queue_id: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_session: None,
raw_summoner: None,
raw_champion_select: None,
raw_rune_page: None,
raw_live_client_data: None,
raw_end_game_stats: None,
all_players: Vec::new(),
start_time: result.start_time,
end_time: Some(result.end_time),
duration: result.duration,
@@ -344,54 +247,27 @@ impl TimelineStore {
pub fn update_metadata(&self, recording_id: Uuid, update: MetadataUpdate) -> Result<()> {
let mut recordings = self.recordings.write();
if let Some(metadata) = recordings.get_mut(&recording_id) {
if let Some(champion) = update.champion {
metadata.champion = Some(champion);
if let Some(game_id) = update.game_id {
metadata.game_id = Some(game_id);
}
if let Some(match_id) = update.match_id {
metadata.match_id = Some(match_id);
if let Some(raw_session) = update.raw_session {
metadata.raw_session = Some(raw_session);
}
if let Some(skin_name) = update.skin_name {
metadata.skin_name = Some(skin_name);
if let Some(raw_summoner) = update.raw_summoner {
metadata.raw_summoner = Some(raw_summoner);
}
if let Some(queue_type) = update.queue_type {
metadata.queue_type = Some(queue_type);
if let Some(raw_champion_select) = update.raw_champion_select {
metadata.raw_champion_select = Some(raw_champion_select);
}
if let Some(queue_id) = update.queue_id {
metadata.queue_id = Some(queue_id);
if let Some(raw_rune_page) = update.raw_rune_page {
metadata.raw_rune_page = Some(raw_rune_page);
}
if let Some(game_mode) = update.game_mode {
metadata.game_mode = Some(game_mode);
}
if let Some(map_name) = update.map_name {
metadata.map_name = Some(map_name);
}
if let Some(summoner_name) = update.summoner_name {
metadata.summoner_name = Some(summoner_name);
}
if let Some(puuid) = update.puuid {
metadata.puuid = Some(puuid);
}
if let Some(team) = update.team {
metadata.team = Some(team);
}
if let Some(victory) = update.victory {
metadata.victory = Some(victory);
}
if let Some(final_stats) = update.final_stats {
metadata.final_stats = Some(final_stats);
}
if let Some(runes) = update.runes {
metadata.runes = Some(runes);
}
if let Some(summoner_spells) = update.summoner_spells {
metadata.summoner_spells = Some(summoner_spells);
if let Some(raw_live_client_data) = update.raw_live_client_data {
metadata.raw_live_client_data = Some(raw_live_client_data);
}
if let Some(raw_end_game_stats) = update.raw_end_game_stats {
metadata.raw_end_game_stats = Some(raw_end_game_stats);
}
if !update.all_players.is_empty() {
metadata.all_players = update.all_players;
}
}
drop(recordings);
self.persist_recording(recording_id)?;
@@ -429,21 +305,13 @@ impl TimelineStore {
end_time: metadata.end_time,
duration_secs: metadata.duration.num_seconds(),
events,
champion: metadata.champion.clone(),
skin_name: metadata.skin_name.clone(),
queue_type: metadata.queue_type.clone(),
queue_id: metadata.queue_id,
game_mode: metadata.game_mode.clone(),
map_name: metadata.map_name.clone(),
summoner_name: metadata.summoner_name.clone(),
puuid: metadata.puuid.clone(),
team: metadata.team,
victory: metadata.victory,
final_stats: metadata.final_stats.clone(),
runes: metadata.runes.clone(),
summoner_spells: metadata.summoner_spells.clone(),
game_id: metadata.game_id,
raw_session: metadata.raw_session.clone(),
raw_summoner: metadata.raw_summoner.clone(),
raw_champion_select: metadata.raw_champion_select.clone(),
raw_rune_page: metadata.raw_rune_page.clone(),
raw_live_client_data: metadata.raw_live_client_data.clone(),
raw_end_game_stats: metadata.raw_end_game_stats.clone(),
all_players: metadata.all_players.clone(),
})
}
@@ -480,21 +348,13 @@ impl TimelineStore {
end_time: metadata.end_time,
duration_secs: metadata.duration.num_seconds(),
events,
champion: metadata.champion,
skin_name: metadata.skin_name,
queue_type: metadata.queue_type,
queue_id: metadata.queue_id,
game_mode: metadata.game_mode,
map_name: metadata.map_name,
summoner_name: metadata.summoner_name,
puuid: metadata.puuid,
team: metadata.team,
victory: metadata.victory,
final_stats: metadata.final_stats,
runes: metadata.runes,
summoner_spells: metadata.summoner_spells,
game_id: metadata.game_id,
raw_session: metadata.raw_session,
raw_summoner: metadata.raw_summoner,
raw_champion_select: metadata.raw_champion_select,
raw_rune_page: metadata.raw_rune_page,
raw_live_client_data: metadata.raw_live_client_data,
raw_end_game_stats: metadata.raw_end_game_stats,
all_players: metadata.all_players,
};
let file_path = self.storage_dir.join(format!("{}.json", id));
@@ -518,40 +378,40 @@ impl TimelineStore {
if path.extension().map(|e| e == "json").unwrap_or(false) {
if let Ok(contents) = std::fs::read_to_string(&path) {
if let Ok(timeline) = serde_json::from_str::<super::Timeline>(&contents) {
let recording_id = timeline.recording_id;
let start_time = timeline.start_time;
let end_time = timeline.end_time;
let duration = timeline.duration();
let event_count = timeline.events.len();
let game_id = timeline.game_id;
let raw_session = timeline.raw_session;
let raw_summoner = timeline.raw_summoner;
let raw_champion_select = timeline.raw_champion_select;
let raw_rune_page = timeline.raw_rune_page;
let raw_live_client_data = timeline.raw_live_client_data;
let raw_end_game_stats = timeline.raw_end_game_stats;
let events = timeline.events;
let metadata = RecordingMetadata {
id: timeline.recording_id,
game_id: None,
match_id: None,
champion: None,
skin_name: None,
queue_type: None,
queue_id: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_end_game_stats: None,
all_players: Vec::new(),
start_time: timeline.start_time,
end_time: timeline.end_time,
duration: timeline.duration(),
id: recording_id,
game_id,
raw_session,
raw_summoner,
raw_champion_select,
raw_rune_page,
raw_live_client_data,
raw_end_game_stats,
start_time,
end_time,
duration,
file_path: None,
file_size: None,
event_count: timeline.events.len(),
event_count,
finalized: true,
};
self.recordings
.write()
.insert(timeline.recording_id, metadata);
self.timelines
.write()
.insert(timeline.recording_id, timeline.events);
self.recordings.write().insert(recording_id, metadata);
self.timelines.write().insert(recording_id, events);
}
}
}
@@ -597,6 +457,6 @@ mod tests {
assert_eq!(recordings.len(), 1);
let metadata = store.get_recording(id).unwrap();
assert_eq!(metadata.champion, Some("Ahri".to_string()));
assert_eq!(metadata.game_id, Some(12345));
}
}