1
1
use aw_client_rust:: AwClient ;
2
2
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 ;
5
6
use env_logger:: Env ;
6
7
use log:: { info, warn} ;
7
8
use reqwest;
8
9
use serde_json:: { Map , Value } ;
9
10
use serde_yaml;
10
11
use std:: env;
12
+ use dirs:: config_dir;
11
13
use std:: fs:: { DirBuilder , File } ;
12
14
use std:: io:: prelude:: * ;
13
15
use std:: thread:: sleep;
14
16
use tokio:: time:: { interval, Duration } ;
15
17
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
+
16
92
fn get_config_path ( ) -> Option < std:: path:: PathBuf > {
17
93
config_dir ( ) . map ( |mut path| {
18
94
path. push ( "activitywatch" ) ;
@@ -52,17 +128,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
52
128
53
129
let args: Vec < String > = env:: args ( ) . collect ( ) ;
54
130
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
+ }
60
143
}
61
- if args [ idx ] == "--testing" {
144
+ "--testing" => {
62
145
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
+ }
63
156
}
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 ! ( "\n Options:" ) ;
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] ) ;
66
168
return Ok ( ( ) ) ;
67
169
}
68
170
}
@@ -75,10 +177,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
75
177
env_logger:: init_from_env ( env) ;
76
178
77
179
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
+ }
82
186
let mut file = File :: create ( & config_path) . expect ( "Unable to create file" ) ;
83
187
file. write_all ( b"apikey: your-api-key\n username: your_username\n polling_interval: 10" )
84
188
. expect ( "Unable to write to file" ) ;
@@ -138,6 +242,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
138
242
. build ( )
139
243
. unwrap ( ) ;
140
244
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
+
141
255
loop {
142
256
interval. tick ( ) . await ;
143
257
0 commit comments