record-daemon: add game start and end metadata
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m9s

This commit is contained in:
2026-03-24 18:16:53 +01:00
parent fc7ba40b30
commit f90e549b1e
9 changed files with 1878 additions and 98 deletions

View File

@@ -38,8 +38,28 @@ pub struct RecordingMetadata {
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>,
/// 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>,
/// Recording start time.
pub start_time: DateTime<Utc>,
/// Recording end time.
@@ -48,7 +68,7 @@ pub struct RecordingMetadata {
// #[serde(with = "chrono::serde::seconds")]
pub duration: Duration,
/// Output file path.
pub file_path: PathBuf,
pub file_path: Option<PathBuf>,
/// File size in bytes.
pub file_size: Option<u64>,
/// Number of events.
@@ -57,17 +77,66 @@ pub struct RecordingMetadata {
pub finalized: bool,
}
/// 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 team: Option<u32>,
pub victory: Option<bool>,
pub final_stats: Option<GameFinalStats>,
}
impl RecordingMetadata {
/// Create metadata from a recording result.
pub fn from_result(result: &RecordingResult) -> Self {
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,
team: None,
victory: None,
final_stats: None,
start_time: result.start_time,
end_time: Some(result.end_time),
duration: result.duration,
file_path: result.path.clone(),
file_path: Some(result.path.clone()),
file_size: result.file_size(),
event_count: 0,
finalized: false,
@@ -108,17 +177,84 @@ impl TimelineStore {
}
}
/// Add a new recording.
/// 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 {
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,
team: None,
victory: None,
final_stats: None,
start_time: Utc::now(),
end_time: None,
duration: Duration::zero(),
file_path: None,
file_size: None,
event_count: 0,
finalized: false,
};
self.recordings.write().insert(id, metadata);
self.timelines.write().insert(id, Vec::new());
id
}
/// Finalize a recording with the recording result.
/// Called when recording stops.
pub fn finalize_recording(&self, recording_id: Uuid, result: RecordingResult) -> Result<()> {
let mut recordings = self.recordings.write();
let metadata = recordings
.get_mut(&recording_id)
.ok_or(TimelineError::RecordingNotFound(recording_id))?;
// Update with final recording data
metadata.start_time = result.start_time;
metadata.end_time = Some(result.end_time);
metadata.duration = result.duration;
metadata.file_path = Some(result.path.clone());
metadata.file_size = result.file_size();
metadata.finalized = true;
// Persist to disk
drop(recordings);
self.persist_recording(recording_id)?;
Ok(())
}
/// Add a new recording (legacy method for backwards compatibility).
pub fn add_recording(&self, result: RecordingResult) -> Result<Uuid> {
let id = Uuid::new_v4();
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,
team: None,
victory: None,
final_stats: None,
start_time: result.start_time,
end_time: Some(result.end_time),
duration: result.duration,
file_path: result.path.clone(),
file_path: Some(result.path.clone()),
file_size: result.file_size(),
event_count: 0,
finalized: true,
@@ -153,6 +289,49 @@ impl TimelineStore {
Ok(())
}
/// Update metadata for a recording.
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(match_id) = update.match_id {
metadata.match_id = Some(match_id);
}
if let Some(skin_name) = update.skin_name {
metadata.skin_name = Some(skin_name);
}
if let Some(queue_type) = update.queue_type {
metadata.queue_type = Some(queue_type);
}
if let Some(queue_id) = update.queue_id {
metadata.queue_id = Some(queue_id);
}
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(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);
}
}
drop(recordings);
self.persist_recording(recording_id)?;
Ok(())
}
/// Get all recordings.
pub fn get_all_recordings(&self) -> Result<Vec<RecordingMetadata>> {
let recordings = self.recordings.read();
@@ -184,6 +363,16 @@ 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(),
team: metadata.team,
victory: metadata.victory,
final_stats: metadata.final_stats.clone(),
})
}
@@ -220,6 +409,16 @@ 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,
team: metadata.team,
victory: metadata.victory,
final_stats: metadata.final_stats,
};
let file_path = self.storage_dir.join(format!("{}.json", id));
@@ -246,11 +445,21 @@ impl TimelineStore {
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,
team: None,
victory: None,
final_stats: None,
start_time: timeline.start_time,
end_time: timeline.end_time,
duration: timeline.duration(),
file_path: PathBuf::new(),
file_path: None,
file_size: None,
event_count: timeline.events.len(),
finalized: true,