Skip to content

Commit c44b313

Browse files
committed
feat: implement history sync
1 parent 8026bcd commit c44b313

File tree

3 files changed

+130
-14
lines changed

3 files changed

+130
-14
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ reqwest = { version = "0.12.7", features = ["json"] }
1616
serde_json = "1.0.128"
1717
serde_yaml = "0.9.34"
1818
tokio = { version = "1.40.0", features = ["full"] }
19+
regex = "1.10.3"

src/main.rs

Lines changed: 128 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,94 @@
11
use aw_client_rust::AwClient;
22
use aw_models::{Bucket, Event};
3-
use chrono::{TimeDelta, Utc};
4-
use dirs::config_dir;
3+
use chrono::{DateTime, Duration as ChronoDuration, NaiveDateTime, TimeDelta, Utc};
4+
use regex::Regex;
5+
use std::path::PathBuf;
56
use env_logger::Env;
67
use log::{info, warn};
78
use reqwest;
89
use serde_json::{Map, Value};
910
use serde_yaml;
1011
use std::env;
12+
use dirs::config_dir;
1113
use std::fs::{DirBuilder, File};
1214
use std::io::prelude::*;
1315
use std::thread::sleep;
1416
use tokio::time::{interval, Duration};
1517

18+
fn parse_time_string(time_str: &str) -> Option<ChronoDuration> {
19+
let re = Regex::new(r"^(\d+)([dhm])$").unwrap();
20+
if let Some(caps) = re.captures(time_str) {
21+
let amount: i64 = caps.get(1)?.as_str().parse().ok()?;
22+
let unit = caps.get(2)?.as_str();
23+
24+
match unit {
25+
"d" => Some(ChronoDuration::days(amount)),
26+
"h" => Some(ChronoDuration::hours(amount)),
27+
"m" => Some(ChronoDuration::minutes(amount)),
28+
_ => None,
29+
}
30+
} else {
31+
None
32+
}
33+
}
34+
35+
async fn sync_historical_data(
36+
client: &reqwest::Client,
37+
aw_client: &AwClient,
38+
username: &str,
39+
apikey: &str,
40+
from_time: ChronoDuration,
41+
) -> Result<(), Box<dyn std::error::Error>> {
42+
let from_timestamp = (Utc::now() - from_time).timestamp();
43+
let url = format!(
44+
"http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user={}&api_key={}&format=json&limit=200&from={}",
45+
username, apikey, from_timestamp
46+
);
47+
48+
let response = client.get(&url).send().await?;
49+
let v: Value = response.json().await?;
50+
51+
if let Some(tracks) = v["recenttracks"]["track"].as_array() {
52+
info!("Syncing {} historical tracks...", tracks.len());
53+
for track in tracks.iter().rev() {
54+
let mut event_data: Map<String, Value> = Map::new();
55+
56+
event_data.insert("title".to_string(), track["name"].to_owned());
57+
event_data.insert(
58+
"artist".to_string(),
59+
track["artist"]["#text"].to_owned(),
60+
);
61+
event_data.insert(
62+
"album".to_string(),
63+
track["album"]["#text"].to_owned(),
64+
);
65+
66+
// Get timestamp from the track
67+
if let Some(date) = track["date"]["uts"].as_str() {
68+
if let Ok(timestamp) = date.parse::<i64>() {
69+
// TODO: remove the deprecated from_utc and from_timestamp
70+
let event = Event {
71+
id: None,
72+
timestamp: DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(timestamp, 0), Utc),
73+
duration: TimeDelta::seconds(30),
74+
data: event_data,
75+
};
76+
77+
aw_client
78+
.insert_event("aw-watcher-lastfm", &event)
79+
.await
80+
.unwrap_or_else(|e| {
81+
warn!("Error inserting historical event: {:?}", e);
82+
});
83+
}
84+
}
85+
}
86+
info!("Historical sync completed!");
87+
}
88+
89+
Ok(())
90+
}
91+
1692
fn get_config_path() -> Option<std::path::PathBuf> {
1793
config_dir().map(|mut path| {
1894
path.push("activitywatch");
@@ -52,17 +128,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
52128

53129
let args: Vec<String> = env::args().collect();
54130
let mut port: u16 = 5600;
55-
if args.len() > 1 {
56-
for idx in 1..args.len() {
57-
if args[idx] == "--port" {
58-
port = args[idx + 1].parse().expect("Invalid port number");
59-
break;
131+
let mut sync_duration: Option<ChronoDuration> = None;
132+
133+
let mut idx = 1;
134+
while idx < args.len() {
135+
match args[idx].as_str() {
136+
"--port" => {
137+
if idx + 1 < args.len() {
138+
port = args[idx + 1].parse().expect("Invalid port number");
139+
idx += 2;
140+
} else {
141+
panic!("--port requires a value");
142+
}
60143
}
61-
if args[idx] == "--testing" {
144+
"--testing" => {
62145
port = 5699;
146+
idx += 1;
147+
}
148+
"--sync" => {
149+
if idx + 1 < args.len() {
150+
sync_duration = Some(parse_time_string(&args[idx + 1])
151+
.expect("Invalid sync duration format. Use format: 7d, 24h, or 30m"));
152+
idx += 2;
153+
} else {
154+
panic!("--sync requires a duration value (e.g., 7d, 24h, 30m)");
155+
}
63156
}
64-
if args[idx] == "--help" {
65-
println!("Usage: aw-watcher-lastfm-rust [--testing] [--port PORT] [--help]");
157+
"--help" => {
158+
println!("Usage: aw-watcher-lastfm-rust [--testing] [--port PORT] [--sync DURATION] [--help]");
159+
println!("\nOptions:");
160+
println!(" --testing Use testing port (5699)");
161+
println!(" --port PORT Specify custom port");
162+
println!(" --sync DURATION Sync historical data (format: 7d, 24h, 30m)");
163+
println!(" --help Show this help message");
164+
return Ok(());
165+
}
166+
_ => {
167+
println!("Unknown argument: {}", args[idx]);
66168
return Ok(());
67169
}
68170
}
@@ -75,10 +177,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
75177
env_logger::init_from_env(env);
76178

77179
if !config_path.exists() {
78-
DirBuilder::new()
79-
.recursive(true)
80-
.create(config_dir)
81-
.expect("Unable to create directory");
180+
if !config_dir.exists() {
181+
DirBuilder::new()
182+
.recursive(true)
183+
.create(&config_dir)
184+
.expect("Unable to create directory");
185+
}
82186
let mut file = File::create(&config_path).expect("Unable to create file");
83187
file.write_all(b"apikey: your-api-key\nusername: your_username\npolling_interval: 10")
84188
.expect("Unable to write to file");
@@ -138,6 +242,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
138242
.build()
139243
.unwrap();
140244

245+
// Handle historical sync if requested
246+
if let Some(duration) = sync_duration {
247+
info!("Starting historical sync...");
248+
match sync_historical_data(&client, &aw_client, &username, &apikey, duration).await {
249+
Ok(_) => info!("Historical sync completed successfully"),
250+
Err(e) => warn!("Error during historical sync: {:?}", e),
251+
}
252+
info!("Starting real-time tracking...");
253+
}
254+
141255
loop {
142256
interval.tick().await;
143257

0 commit comments

Comments
 (0)