this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add command to show current play status

+214 -1
+7 -1
src/error.rs
··· 1 1 use jacquard::{ 2 2 client::{AgentError, SessionStoreError}, 3 3 error::ClientError, 4 - types::string::AtStrError, 4 + types::{string::AtStrError, uri::UriError}, 5 5 }; 6 6 use jacquard_identity::resolver::IdentityError; 7 7 use jacquard_oauth::error::OAuthError; ··· 84 84 85 85 impl From<AgentError> for OnyxError { 86 86 fn from(err: AgentError) -> Self { 87 + OnyxError::Other(err.to_string().into()) 88 + } 89 + } 90 + 91 + impl From<UriError> for OnyxError { 92 + fn from(err: UriError) -> Self { 87 93 OnyxError::Other(err.to_string().into()) 88 94 } 89 95 }
+33
src/main.rs
··· 7 7 error::OnyxError, 8 8 parser::{ParsedArtist, ParsedTrack}, 9 9 scrobble::Scrobbler, 10 + status::StatusManager, 10 11 }; 11 12 use clap::{ 12 13 CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum, ··· 20 21 mod error; 21 22 mod parser; 22 23 mod scrobble; 24 + mod status; 23 25 24 26 fn args_styles() -> Styles { 25 27 Styles::styled() ··· 50 52 Scrobble { 51 53 #[command(subcommand)] 52 54 command: ScrobbleCommands, 55 + }, 56 + 57 + /// View and manage listening status 58 + Status { 59 + #[command(subcommand)] 60 + command: StatusCommands, 53 61 }, 54 62 } 55 63 ··· 160 168 enum LogFormat { 161 169 /// Use AudioScrobbler log format 162 170 AudioScrobbler, 171 + } 172 + 173 + #[derive(Subcommand, Debug)] 174 + enum StatusCommands { 175 + Show { 176 + /// Handle or DID to query 177 + #[arg(long)] 178 + handle: Option<String>, 179 + }, 163 180 } 164 181 165 182 fn get_auth() -> Result<Authenticator, OnyxError> { ··· 337 354 format!("deleted log: {}", log.to_str().unwrap()).dimmed() 338 355 ); 339 356 } 357 + } 358 + }, 359 + Commands::Status { command } => match command { 360 + StatusCommands::Show { handle } => { 361 + let ident = match handle { 362 + Some(s) => s, 363 + None => { 364 + let auth = get_auth()?; 365 + let session_info = auth.get_session_info()?; 366 + session_info.did 367 + } 368 + }; 369 + 370 + let status_man = StatusManager::new(&ident); 371 + let status = status_man.get_status().await?; 372 + status_man.display_status(&status); 340 373 } 341 374 }, 342 375 }
+174
src/status.rs
··· 1 + use chrono::{DateTime, FixedOffset}; 2 + use jacquard::{ 3 + client::{AgentSessionExt, BasicClient}, 4 + prelude::IdentityResolver, 5 + types::{ 6 + did::Did, 7 + string::{Datetime, Handle}, 8 + }, 9 + }; 10 + use jacquard_api::fm_teal::alpha::actor::status as fm_teal_status; 11 + use jacquard_identity::{JacquardResolver, PublicResolver}; 12 + use owo_colors::OwoColorize; 13 + use serde_json::json; 14 + 15 + use crate::error::OnyxError; 16 + 17 + #[derive(Debug)] 18 + pub struct ArtistStatus { 19 + pub artist_name: String, 20 + pub artist_mb_id: Option<String>, 21 + } 22 + 23 + #[derive(Debug)] 24 + pub struct TrackStatus { 25 + pub time: DateTime<FixedOffset>, 26 + pub expiry: Option<DateTime<FixedOffset>>, 27 + pub track_name: String, 28 + pub track_mb_id: Option<String>, 29 + pub recording_mb_id: Option<String>, 30 + pub duration: Option<i64>, 31 + pub artists: Vec<ArtistStatus>, 32 + pub release_name: Option<String>, 33 + pub release_mb_id: Option<String>, 34 + pub isrc: Option<String>, 35 + pub origin_url: Option<String>, 36 + pub music_service_base_domain: Option<String>, 37 + pub client_id: Option<String>, 38 + pub played_time: Option<DateTime<FixedOffset>>, 39 + } 40 + 41 + fn get_status_endpoint(did: String) -> String { 42 + format!("at://{}/fm.teal.alpha.actor.status/self", did) 43 + } 44 + 45 + pub struct StatusManager { 46 + pub ident: String, 47 + 48 + resolver: JacquardResolver, 49 + } 50 + 51 + impl StatusManager { 52 + pub fn new(ident: &str) -> Self { 53 + Self { 54 + ident: ident.to_owned(), 55 + resolver: PublicResolver::default(), 56 + } 57 + } 58 + 59 + async fn resolve_did(&self, ident: &str) -> Result<Did<'_>, OnyxError> { 60 + if let Ok(did) = ident.parse() { 61 + return Ok(did); 62 + } 63 + 64 + let handle = Handle::new(ident)?; 65 + let did = self.resolver.resolve_handle(&handle).await?; 66 + Ok(did) 67 + } 68 + 69 + pub async fn get_status(&self) -> Result<TrackStatus, OnyxError> { 70 + let did = self.resolve_did(&self.ident).await?; 71 + 72 + let endpoint = get_status_endpoint(did.to_string()); 73 + 74 + let uri = fm_teal_status::Status::uri(&endpoint)?; 75 + let agent = BasicClient::unauthenticated(); 76 + 77 + let response = agent 78 + .get_record::<fm_teal_status::StatusRecord>(&uri) 79 + .await?; 80 + 81 + let status_rec = response 82 + .into_output() 83 + .map_err(|e| OnyxError::Other(e.to_string().into()))? 84 + .value; 85 + 86 + let artists: Vec<ArtistStatus> = status_rec 87 + .item 88 + .artists 89 + .iter() 90 + .map(|a| ArtistStatus { 91 + artist_name: a.artist_name.to_string(), 92 + artist_mb_id: a.artist_mb_id.clone().map(|s| s.to_string()), 93 + }) 94 + .collect(); 95 + 96 + Ok(TrackStatus { 97 + time: *status_rec.time.as_ref(), 98 + expiry: status_rec.expiry.map(|t| *t.as_ref()), 99 + track_name: status_rec.item.track_name.to_string(), 100 + track_mb_id: status_rec.item.track_mb_id.map(|s| s.to_string()), 101 + recording_mb_id: status_rec.item.recording_mb_id.map(|s| s.to_string()), 102 + duration: status_rec.item.duration, 103 + artists, 104 + release_name: status_rec.item.release_name.map(|s| s.to_string()), 105 + release_mb_id: status_rec.item.release_mb_id.map(|s| s.to_string()), 106 + isrc: status_rec.item.isrc.map(|s| s.to_string()), 107 + origin_url: status_rec.item.origin_url.map(|s| s.to_string()), 108 + music_service_base_domain: status_rec 109 + .item 110 + .music_service_base_domain 111 + .map(|s| s.to_string()), 112 + client_id: status_rec 113 + .item 114 + .submission_client_agent 115 + .map(|s| s.to_string()), 116 + played_time: status_rec.item.played_time.map(|t| *t.as_ref()), 117 + }) 118 + } 119 + 120 + pub fn display_status(&self, status: &TrackStatus) { 121 + // if both track name and artists are blank, probably nothing's playing 122 + if status.track_name.is_empty() && status.artists.is_empty() { 123 + println!("{}", "nothing playing right now".dimmed()); 124 + return; 125 + } 126 + 127 + println!("{} {}", "track:".dimmed(), status.track_name.blue()); 128 + 129 + if !status.artists.is_empty() { 130 + print!("{} ", "artists:".dimmed()); 131 + 132 + for i in 0..status.artists.len() { 133 + print!("{}", status.artists[i].artist_name.magenta()); 134 + 135 + if i != status.artists.len() - 1 { 136 + print!("{} ", ",".dimmed()); 137 + } 138 + } 139 + 140 + println!(); 141 + } 142 + 143 + if let Some(release) = &status.release_name { 144 + println!("{} {}", "release:".dimmed(), release.red()); 145 + } 146 + 147 + if let Some(played_time) = &status.played_time { 148 + println!( 149 + "{} {}", 150 + "played:".dimmed(), 151 + played_time.format("%Y-%m-%d %H:%M:%S %:z").yellow() 152 + ); 153 + } 154 + 155 + if let Some(duration) = status.duration { 156 + let hours = duration / 3600; 157 + let minutes = (duration - (hours * 3600)) / 60; 158 + let seconds = duration - (minutes * 60); 159 + 160 + let mut duration_str = "".to_string(); 161 + if hours > 0 { 162 + duration_str = format!("{:02}:", hours); 163 + } 164 + if minutes > 0 || hours > 0 { 165 + duration_str = format!("{}{:02}:", duration_str, minutes); 166 + } 167 + if seconds > 0 || minutes > 0 || hours > 0 { 168 + duration_str = format!("{}{:02}", duration_str, seconds); 169 + } 170 + 171 + println!("{} {}", "duration:".dimmed(), duration_str.green()); 172 + } 173 + } 174 + }