Skip to main content

scrobble_scrubber/
json_logger.rs

1use 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/// JSON log entry for scrobble edit attempts
13#[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/// Detailed edit information extracted from ExactScrobbleEdit
23#[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/// Track metadata for before/after comparison
32#[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
40/// JSON logger that focuses on ClientEvent::EditAttempted events
41pub 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    /// Start listening for events and logging them
64    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    /// Process a single ScrubberEvent and log if it contains ClientEvent::EditAttempted
73    fn process_event(&self, event: &ScrubberEvent) -> Result<(), Box<dyn std::error::Error>> {
74        if !self.enabled {
75            return Ok(());
76        }
77
78        // Defensive check: Do not log during dry run mode
79        if self.config.dry_run {
80            log::trace!("JSON logger: Skipping log during dry run mode");
81            return Ok(());
82        }
83
84        // Only process ClientEvent::EditAttempted variants
85        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    /// Write a log entry to the file
122    fn write_log_entry(&self, entry: &EditLogEntry) -> Result<(), Box<dyn std::error::Error>> {
123        // Ensure directory exists
124        if let Some(parent) = Path::new(&self.log_file_path).parent() {
125            std::fs::create_dir_all(parent)?;
126        }
127
128        // Append JSON line to log file
129        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}