scrobble_scrubber/
json_logger.rs1use chrono::{DateTime, Utc};
2use lastfm_edit::ClientEvent;
3use serde::{Deserialize, Serialize};
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::path::Path;
7use tokio::sync::broadcast;
8
9use crate::config::ScrubberConfig;
10use crate::events::{ScrubberEvent, ScrubberEventType};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct EditLogEntry {
15 pub timestamp: DateTime<Utc>,
16 pub success: bool,
17 pub error_message: Option<String>,
18 pub duration_ms: u64,
19 pub edit: EditDetails,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct EditDetails {
25 pub timestamp: u64,
26 pub edit_all: bool,
27 pub original: TrackMetadata,
28 pub new: TrackMetadata,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct TrackMetadata {
34 pub track_name: String,
35 pub artist_name: String,
36 pub album_name: String,
37 pub album_artist_name: String,
38}
39
40pub struct JsonLogger {
42 log_file_path: String,
43 enabled: bool,
44 receiver: broadcast::Receiver<ScrubberEvent>,
45 config: ScrubberConfig,
46}
47
48impl JsonLogger {
49 pub fn new(
50 log_file_path: String,
51 enabled: bool,
52 receiver: broadcast::Receiver<ScrubberEvent>,
53 config: ScrubberConfig,
54 ) -> Self {
55 Self {
56 log_file_path,
57 enabled,
58 receiver,
59 config,
60 }
61 }
62
63 pub async fn run(&mut self) {
65 while let Ok(event) = self.receiver.recv().await {
66 if let Err(e) = self.process_event(&event) {
67 log::warn!("Failed to log event: {e}");
68 }
69 }
70 }
71
72 fn process_event(&self, event: &ScrubberEvent) -> Result<(), Box<dyn std::error::Error>> {
74 if !self.enabled {
75 return Ok(());
76 }
77
78 if self.config.dry_run {
80 log::trace!("JSON logger: Skipping log during dry run mode");
81 return Ok(());
82 }
83
84 if let ScrubberEventType::ClientEvent(ClientEvent::EditAttempted {
86 edit,
87 success,
88 error_message,
89 duration_ms,
90 }) = &event.event_type
91 {
92 let log_entry = EditLogEntry {
93 timestamp: event.timestamp,
94 success: *success,
95 error_message: error_message.clone(),
96 duration_ms: *duration_ms,
97 edit: EditDetails {
98 timestamp: edit.timestamp,
99 edit_all: edit.edit_all,
100 original: TrackMetadata {
101 track_name: edit.track_name_original.clone(),
102 artist_name: edit.artist_name_original.clone(),
103 album_name: edit.album_name_original.clone(),
104 album_artist_name: edit.album_artist_name_original.clone(),
105 },
106 new: TrackMetadata {
107 track_name: edit.track_name.clone(),
108 artist_name: edit.artist_name.clone(),
109 album_name: edit.album_name.clone(),
110 album_artist_name: edit.album_artist_name.clone(),
111 },
112 },
113 };
114
115 self.write_log_entry(&log_entry)?;
116 }
117
118 Ok(())
119 }
120
121 fn write_log_entry(&self, entry: &EditLogEntry) -> Result<(), Box<dyn std::error::Error>> {
123 if let Some(parent) = Path::new(&self.log_file_path).parent() {
125 std::fs::create_dir_all(parent)?;
126 }
127
128 let mut file = OpenOptions::new()
130 .create(true)
131 .append(true)
132 .open(&self.log_file_path)?;
133
134 let json_line = serde_json::to_string(entry)?;
135 writeln!(file, "{json_line}")?;
136
137 log::trace!("Logged edit attempt to {}", self.log_file_path);
138 Ok(())
139 }
140}