Skip to main content

hitomi/
config.rs

1//! Configuration for `hitomi`
2
3use std::env;
4use std::fmt::{Display, Formatter};
5
6use anyhow::Result;
7use clap::Args;
8use derive_builder::Builder;
9use dialoguer::theme::ColorfulTheme;
10use dialoguer::{Input, Select};
11use reqwest::Url;
12use serde::{Deserialize, Serialize};
13use simplelog::{debug, info};
14
15use crate::db;
16use crate::plex::PlexClient;
17use crate::types::plex::plex_token::PlexToken;
18
19/// Represents the configuration file
20#[derive(Args, Builder, Clone, Debug, Deserialize, Serialize, PartialEq, sqlx::Type)]
21pub struct Config {
22    #[arg(long)]
23    plex_token: String,
24    #[arg(long)]
25    plex_url: String,
26    #[arg(long)]
27    primary_section_id: u32,
28}
29
30impl Default for Config {
31    fn default() -> Self {
32        Self {
33            plex_url: "http://127.0.0.1:32400".to_string(),
34            plex_token: "PLEX_TOKEN".to_string(),
35            primary_section_id: 0,
36        }
37    }
38}
39
40impl Config {
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    pub fn get_plex_url(&self) -> Result<Url> {
46        Ok(Url::parse(&self.plex_url)?)
47    }
48
49    pub fn get_plex_url_str(&self) -> String {
50        self.get_plex_url().unwrap().to_string()
51    }
52
53    pub fn get_plex_token(&self) -> Result<PlexToken> {
54        Ok(PlexToken::try_new(&self.plex_token)?)
55    }
56
57    pub fn get_primary_section_id(&self) -> u32 {
58        self.primary_section_id
59    }
60}
61
62/// Wizard used by user to create an initial configuration table
63pub async fn build_config_wizard() -> Result<Config> {
64    info!("Config table not populated. Checking for environment variables...");
65
66    let plex_url = if let Ok(plex_url) = env::var("PLEX_URL") {
67        plex_url
68    } else {
69        Input::<String>::with_theme(&ColorfulTheme::default())
70            .with_prompt("Enter your plex URL:")
71            .interact_text()?
72            .to_string()
73    };
74    let plex_url = Url::parse(&plex_url)?;
75
76    let plex_token = if let Ok(plex_token) = env::var("PLEX_TOKEN") {
77        plex_token
78    } else {
79        Input::<String>::with_theme(&ColorfulTheme::default())
80            .with_prompt("Enter your plex token:")
81            .interact_text()?
82            .to_string()
83    };
84    let plex_token = PlexToken::try_new(plex_token)?;
85
86    info!("Testing connection to plex. Please wait...");
87    match PlexClient::new_for_config(&plex_url, &plex_token).await {
88        Ok(_) => {
89            info!("Successfully connected to plex!");
90        }
91        Err(err) => {
92            panic!("Could not connect to plex:\n{err}")
93        }
94    };
95
96    let primary_section_id = if let Ok(id) = env::var("PRIMARY_SECTION_ID") {
97        id.parse::<u32>()
98    } else {
99        let plex = PlexClient::new_for_config(&plex_url, &plex_token).await?;
100        let sections = plex.get_music_sections();
101        let titles = sections
102            .iter()
103            .map(|x| x.get_title().to_owned())
104            .collect::<Vec<String>>();
105        let selection = Select::with_theme(&ColorfulTheme::default())
106            .with_prompt("Select your music library:")
107            .default(0)
108            .items(&titles)
109            .interact()?;
110        sections[selection].id().parse::<u32>()
111    }
112    .expect("Could not parse section id");
113
114    let config = ConfigBuilder::default()
115        .plex_url(plex_url.to_string())
116        .plex_token(plex_token.to_string())
117        .primary_section_id(primary_section_id)
118        .build()?;
119
120    db::config::save_config(&config).await?;
121
122    Ok(config)
123}
124
125pub async fn load_config() -> Result<Config> {
126    debug!("Loading config...");
127
128    if !db::config::have_config().await? {
129        info!("Config not found in database.");
130        return build_config_wizard().await;
131    }
132
133    let config = db::config::fetch_config().await?;
134
135    Ok(config)
136}
137
138impl Display for Config {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        let mut output = String::default();
141        output += &format!("Plex URL:       {}\n", self.get_plex_url_str());
142
143        write!(f, "{}", output)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use pretty_assertions::assert_eq;
151
152    const VALID_TOKEN: &str = "RWtuIcHBY-hq6HbSq3GY";
153    const VALID_URL: &str = "http://127.0.0.1:32400";
154
155    #[test]
156    fn test_valid_config() {
157        let config = ConfigBuilder::default()
158            .plex_token(VALID_TOKEN.to_string())
159            .plex_url(VALID_URL.to_string())
160            .primary_section_id(1)
161            .build()
162            .unwrap();
163
164        let valid_token = PlexToken::try_new(VALID_TOKEN).unwrap();
165        assert_eq!(config.get_plex_token().unwrap(), valid_token);
166
167        let valid_url = Url::parse(VALID_URL).unwrap();
168        assert_eq!(config.get_plex_url().unwrap(), valid_url);
169    }
170
171    #[test]
172    #[should_panic]
173    fn test_invalid_config_token() {
174        let config = ConfigBuilder::default()
175            .plex_token("rucpkuXGIn/1ZlqJPBVaYZQduMJWX5yWGQan20nOpFokXbGviXonA==".to_string())
176            .plex_url(VALID_URL.to_string())
177            .primary_section_id(1)
178            .build()
179            .unwrap();
180
181        config.get_plex_token().unwrap();
182    }
183
184    #[test]
185    #[should_panic]
186    fn test_invalid_config_url() {
187        let config = ConfigBuilder::default()
188            .plex_token(VALID_TOKEN.to_string())
189            .plex_url("It dawned on her that others could make her happier, but only she could make herself happy.".to_string())
190            .primary_section_id(1)
191            .build()
192            .unwrap();
193
194        config.get_plex_url().unwrap();
195    }
196}