1use 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#[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
62pub 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}