aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2024-03-15 22:10:06 +0100
committerRuben Beltran del Rio <git@r.bdr.sh>2024-03-15 22:10:06 +0100
commit44e7b4de4073e6dc25681bb2fa6977bf5869689a (patch)
tree164c8d8d980732e64d96fe6e788373108144d085 /src
parentb2096fb3958d9e9cdcca77931aadfb947cab24ee (diff)
Add macos sources
Diffstat (limited to 'src')
-rw-r--r--src/configuration.rs98
-rw-r--r--src/main.rs28
-rw-r--r--src/sources/apple_music.rs71
-rw-r--r--src/sources/mod.rs66
-rw-r--r--src/sources/spotify.rs69
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(())
+ }
+}