diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2024-03-15 22:10:06 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2024-03-15 22:10:06 +0100 |
| commit | 44e7b4de4073e6dc25681bb2fa6977bf5869689a (patch) | |
| tree | 164c8d8d980732e64d96fe6e788373108144d085 /src | |
| parent | b2096fb3958d9e9cdcca77931aadfb947cab24ee (diff) | |
Add macos sources
Diffstat (limited to 'src')
| -rw-r--r-- | src/configuration.rs | 98 | ||||
| -rw-r--r-- | src/main.rs | 28 | ||||
| -rw-r--r-- | src/sources/apple_music.rs | 71 | ||||
| -rw-r--r-- | src/sources/mod.rs | 66 | ||||
| -rw-r--r-- | src/sources/spotify.rs | 69 |
5 files changed, 304 insertions, 28 deletions
diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..f678461 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,98 @@ +use std::env; +use std::fs::{create_dir_all, write, File}; +use std::io::{Read, Result}; +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use serde_json; + +const CONFIG_ENV_VARIABLE: &str = "LYRICLI_CONFIG_DIRECTORY"; +const CONFIG_DEFAULT_LOCATION: &str = "XDG_CONFIG_HOME"; +const CONFIG_FALLBACK_LOCATION: &str = ".config"; +const CONFIG_SUBDIRECTORY: &str = ".config"; +const CONFIG_FILENAME: &str = "lyricli.conf"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Configuration { + enabled_sources: Vec<String> +} + +impl Configuration { + + pub fn new() -> Self { + + if let Some(configuration) = Configuration::read() { + return configuration; + } + + let configuration = Configuration::default(); + + // Write the defaults. + Configuration::write(&configuration).ok(); + configuration + } + + // Public API + + pub fn is_enabled(&self, source_name: &String) -> bool { + self.enabled_sources.contains(source_name) + } + + pub fn enable_source(&mut self, source_name: &String) -> Result<()> { + if !self.enabled_sources.contains(source_name) { + self.enabled_sources.push(source_name.to_owned()) + } + Configuration::write(self) + } + + pub fn disable_source(&mut self, source_name: &String) -> Result<()> { + self.enabled_sources.retain(|source| source != source_name); + Configuration::write(self) + } + + // Helpers + + fn default() -> Configuration { + Configuration { + enabled_sources: vec![ + "apple_music".to_string(), + "spotify".to_string() + ] + } + } + + fn read() -> Option<Configuration> { + let config_file_path = Configuration::file_path(); + + let mut config_file = File::open(&config_file_path).ok()?; + let mut config_contents = String::new(); + config_file.read_to_string(&mut config_contents).ok()?; + serde_json::from_str(&config_contents).ok()? + } + + fn write(configuration: &Configuration) -> Result<()> { + let config_file_path = Configuration::file_path(); + if let Ok(serialized_configuration) = serde_json::to_string(&configuration) { + write(&config_file_path, serialized_configuration)?; + } + Ok(()) + } + + fn file_path() -> PathBuf { + let config_directory = Configuration::directory(); + create_dir_all(&config_directory).ok(); + config_directory.join(CONFIG_FILENAME) + } + + fn directory() -> PathBuf { + match env::var(CONFIG_ENV_VARIABLE) { + Ok(directory) => PathBuf::from(directory), + Err(_) => match env::var(CONFIG_DEFAULT_LOCATION) { + Ok(directory) => PathBuf::from(directory), + Err(_) => match env::var("HOME") { + Ok(directory) => PathBuf::from(directory).join(CONFIG_FALLBACK_LOCATION), + Err(_) => panic!("Could not find required directory, {} or {} should be set and readable.", CONFIG_ENV_VARIABLE, CONFIG_DEFAULT_LOCATION), + }, + }, + }.join(CONFIG_SUBDIRECTORY) + } +} diff --git a/src/main.rs b/src/main.rs index 13b7e37..a611926 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ -// mod configuration; +mod configuration; mod lyrics_engine; mod sources; use std::io::{Result, Error, ErrorKind::Other}; -use clap::{Parser}; +use clap::Parser; +use configuration::Configuration; use sources::{enable, disable, get_track, reset, list}; use lyrics_engine::print_lyrics; @@ -45,22 +46,37 @@ pub struct Track { #[tokio::main] async fn main() -> Result<()> { + let mut configuration = Configuration::new(); let arguments = Arguments::parse(); if arguments.list_sources { - return list(); + let sources = list(); + for source in sources { + print!("{}", source); + if configuration.is_enabled(&source) { + print!(" (enabled)"); + } + println!(""); + } + return Ok(()); } if let Some(source_name) = arguments.enable_source { - return enable(source_name); + if !configuration.is_enabled(&source_name) { + enable(&source_name)?; + } + return configuration.enable_source(&source_name); } if let Some(source_name) = arguments.disable_source { - return disable(source_name); + if configuration.is_enabled(&source_name) { + disable(&source_name)?; + } + return configuration.disable_source(&source_name); } if let Some(source_name) = arguments.reset_source { - return reset(source_name); + return reset(&source_name); } let current_track: Track; diff --git a/src/sources/apple_music.rs b/src/sources/apple_music.rs new file mode 100644 index 0000000..cb36f5c --- /dev/null +++ b/src/sources/apple_music.rs @@ -0,0 +1,71 @@ +use std::ffi::CStr; +use std::io::Result; + +use cocoa::{base::nil, foundation::NSString}; +use objc::{class, msg_send, sel, sel_impl, runtime::Object}; +use objc_id::Id; + +use crate::Track; + +use super::LyricsSource; + +pub struct AppleMusic; + +impl AppleMusic { + pub fn new() -> Self { + AppleMusic + } +} + +impl LyricsSource for AppleMusic { + + fn name(&self) -> String { + "apple_music".to_string() + } + + fn current_track(&self) -> Option<Track> { + unsafe { + let app: Id<Object> = { + let cls = class!(SBApplication); + let bundle_identifier = NSString::alloc(nil).init_str("com.apple.Music"); + let app: *mut Object = msg_send![cls, applicationWithBundleIdentifier:bundle_identifier]; + Id::from_ptr(app) + }; + + if msg_send![app, isRunning] { + let current_track: *mut Object = msg_send![app, currentTrack]; + if !current_track.is_null() { + let name_raw: *mut Object = msg_send![current_track, name]; + let artist_raw: *mut Object = msg_send![current_track, artist]; + + let name_ptr: *const i8 = msg_send![name_raw, UTF8String]; + let artist_ptr: *const i8 = msg_send![artist_raw, UTF8String]; + + let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); + let artist = CStr::from_ptr(artist_ptr).to_string_lossy().into_owned(); + + + return Some(Track { + name, + artist + }) + } + } + } + None + } + + fn disable(&self) -> Result<()> { + Ok(()) + } + + fn enable(&self) -> Result<()> { + Ok(()) + } + + fn reset(&self) -> Result<()> { + Ok(()) + } +} + + diff --git a/src/sources/mod.rs b/src/sources/mod.rs index fb7af8f..78f7499 100644 --- a/src/sources/mod.rs +++ b/src/sources/mod.rs @@ -1,9 +1,10 @@ -use std::io::Result; +use std::io::{Result, Error, ErrorKind::Other}; + +#[cfg(target_os = "macos")] +mod apple_music; +#[cfg(target_os = "macos")] +mod spotify; -// #[cfg(target_os = "macos")] -// mod applie_music; -// #[cfg(target_os = "macos")] -// mod spotify; // #[cfg(not(target_os = "macos"))] // mod rhythmbox; // #[cfg(not(target_os = "macos"))] @@ -13,10 +14,10 @@ use std::io::Result; // #[cfg(not(target_os = "macos"))] // mod tauon; -// #[cfg(target_os = "macos")] -// use apple_music::AppleMusic; -// #[cfg(target_os = "macos")] -// use spotify::Spotify; +#[cfg(target_os = "macos")] +use apple_music::AppleMusic; +#[cfg(target_os = "macos")] +use spotify::Spotify; // #[cfg(not(target_os = "macos"))] // use rhythmbox::Rhythmbox; @@ -39,26 +40,47 @@ pub trait LyricsSource { fn reset(&self) -> Result<()>; } -pub fn list() -> Result<()> { - Ok(()) +pub fn list() -> Vec<String> { + available_sources().into_iter().map(|source| source.name()).collect() } -pub fn enable(source_name: String) -> Result<()> { - println!("Enabling {}", source_name); - Ok(()) +pub fn enable(source_name: &String) -> Result<()> { + let sources = available_sources(); + for source in sources { + if &source.name() == source_name { + return source.enable() + } + } + Err(Error::new(Other, "No such source was available.")) } -pub fn disable(source_name: String) -> Result<()> { - println!("Disabling {}", source_name); - Ok(()) +pub fn disable(source_name: &String) -> Result<()> { + let sources = available_sources(); + for source in sources { + if &source.name() == source_name { + return source.disable() + } + } + Err(Error::new(Other, "No such source was available.")) } -pub fn reset(source_name: String) -> Result<()> { - println!("Reset {}", source_name); - Ok(()) +pub fn reset(source_name: &String) -> Result<()> { + let sources = available_sources(); + for source in sources { + if &source.name() == source_name { + return source.reset() + } + } + Err(Error::new(Other, "No such source was available.")) } pub fn get_track() -> Option<Track> { + let sources = available_sources(); + for source in sources { + if let Some(track) = source.current_track() { + return Some(track); + } + } return None } @@ -66,8 +88,8 @@ pub fn available_sources() -> Vec<Box<dyn LyricsSource>> { let mut sources: Vec<Box<dyn LyricsSource>> = Vec::new(); #[cfg(target_os = "macos")] { - // sources.push(Box::new(AppleMusic::new())); - // sources.push(Box::new(Spotify::new())); + sources.push(Box::new(AppleMusic::new())); + sources.push(Box::new(Spotify::new())); } #[cfg(not(target_os = "macos"))] diff --git a/src/sources/spotify.rs b/src/sources/spotify.rs new file mode 100644 index 0000000..b23ea73 --- /dev/null +++ b/src/sources/spotify.rs @@ -0,0 +1,69 @@ +use std::ffi::CStr; +use std::io::Result; + +use cocoa::{base::nil, foundation::NSString}; +use objc::{class, msg_send, sel, sel_impl, runtime::Object}; +use objc_id::Id; + +use crate::Track; + +use super::LyricsSource; + +pub struct Spotify; + +impl Spotify { + pub fn new() -> Self { + Spotify + } +} + +impl LyricsSource for Spotify { + + fn name(&self) -> String { + "spotify".to_string() + } + + fn current_track(&self) -> Option<Track> { + unsafe { + let app: Id<Object> = { + let cls = class!(SBApplication); + let bundle_identifier = NSString::alloc(nil).init_str("com.spotify.Client"); + let app: *mut Object = msg_send![cls, applicationWithBundleIdentifier:bundle_identifier]; + Id::from_ptr(app) + }; + + if msg_send![app, isRunning] { + let current_track: *mut Object = msg_send![app, currentTrack]; + if !current_track.is_null() { + let name_raw: *mut Object = msg_send![current_track, name]; + let artist_raw: *mut Object = msg_send![current_track, artist]; + + let name_ptr: *const i8 = msg_send![name_raw, UTF8String]; + let artist_ptr: *const i8 = msg_send![artist_raw, UTF8String]; + + let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); + let artist = CStr::from_ptr(artist_ptr).to_string_lossy().into_owned(); + + + return Some(Track { + name, + artist + }) + } + } + } + None + } + + fn disable(&self) -> Result<()> { + Ok(()) + } + + fn enable(&self) -> Result<()> { + Ok(()) + } + + fn reset(&self) -> Result<()> { + Ok(()) + } +} |