[work]: settings system for genius_token setup

The last thing needed is the ui for searching songs
This commit is contained in:
Chris Cochrun 2026-04-19 15:27:26 -05:00
parent 2c72b9f6a2
commit cae76c8d72
3 changed files with 192 additions and 74 deletions

View file

@ -1,22 +1,18 @@
use std::collections::HashMap;
#[allow(unused)]
use crate::core::settings;
use crate::core::songs::{Song, VerseName};
use itertools::Itertools;
use miette::{IntoDiagnostic, Result, miette};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_till, take_till1, take_until};
use nom::character::complete::{
digit0, multispace0, newline, space0,
};
use nom::combinator::{complete, eof, peek, rest};
use nom::multi::{many0, many1, separated_list1};
use nom::sequence::{delimited, pair, preceded, terminated};
use nom::character::complete::{digit0, space0};
use nom::combinator::rest;
use nom::multi::many1;
use nom::sequence::{delimited, pair};
use nom::{IResult, Parser};
use reqwest::header;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Display;
#[derive(
Clone,
@ -56,6 +52,18 @@ pub enum Provider {
LyricsCom,
}
impl Display for Provider {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match self {
Provider::Genius { .. } => f.write_str("Genius"),
Provider::LyricsCom => f.write_str("Lyrics.com"),
}
}
}
impl From<OnlineSong> for Song {
fn from(online_song: OnlineSong) -> Self {
let map = if online_song.provider
@ -66,17 +74,18 @@ impl From<OnlineSong> for Song {
)
.ok()
} else {
None
let mut map = HashMap::new();
map.entry(VerseName::Verse { number: 1 })
.or_insert(online_song.lyrics);
Some(map)
};
let song = Self {
Self {
title: online_song.title,
author: Some(online_song.author),
verse_map: map,
..Default::default()
};
song
}
}
}
@ -86,8 +95,6 @@ fn parse_genius_lyrics(
) -> Result<HashMap<VerseName, String>> {
let (input, chunks) =
many1(pair(parse_verse_name, alt((take_until("["), rest))))
// separated_list1(pair(newline, newline), ident)
// many1(complete(take_until("[")))
.parse(lyrics)
.map_err(|e| e.to_owned())
.into_diagnostic()?;
@ -102,38 +109,12 @@ fn parse_genius_lyrics(
name = name.next();
}
map.entry(name).or_insert(lyric.trim().to_string());
map.entry(name).or_insert_with(|| lyric.trim().to_string());
}
Ok(map)
}
#[allow(unused)]
fn ident(input: &str) -> IResult<&str, &str> {
dbg!(&input);
preceded(space0, any).parse(input)
}
#[allow(unused)]
fn any(input: &str) -> IResult<&str, &str> {
dbg!(&input);
complete(take_until("\n\n")).parse(input)
}
#[allow(unused)]
fn block(input: &str) -> IResult<&str, &str> {
dbg!(&input);
terminated(ident, pair(newline, newline)).parse(input)
}
#[allow(unused)]
fn parse_verse(chunk: &str) -> IResult<&str, (VerseName, String)> {
let (input, verse_name) = parse_verse_name.parse(chunk)?;
let lyrics = input.trim().to_string();
Ok((input, (verse_name, lyrics)))
}
#[allow(unused)]
fn parse_verse_name(line: &str) -> IResult<&str, VerseName> {
let (input, (name, _, num, _, _)) = delimited(
(tag("["), space0),
@ -167,7 +148,7 @@ fn parse_verse_name(line: &str) -> IResult<&str, VerseName> {
}
pub async fn search_genius_links(
query: impl AsRef<str> + std::fmt::Display,
query: String,
auth_token: String,
) -> Result<Vec<OnlineSong>> {
// let Some(auth_token) = option_env!("GENIUS_TOKEN") else {
@ -273,7 +254,7 @@ pub async fn get_genius_lyrics(
);
let lyrics = lyrics.replace("<br>", "\n");
song.provider = Provider::Genius {
parsable: lyrics.contains("["),
parsable: lyrics.contains('['),
};
song.lyrics = lyrics;
Ok(song)
@ -391,7 +372,7 @@ mod test {
link: "https://genius.com/North-point-worship-death-was-arrested-lyrics".to_string(),
};
let hits = search_genius_links(
"Death was arrested",
"Death was arrested".to_string(),
env!("GENIUS_TOKEN").to_string(),
)
.await
@ -529,7 +510,6 @@ mod test {
Ok(())
}
#[allow(unreachable_code)]
#[test]
fn test_parse_song() -> Result<()> {
let song = r#"[Verse 1]
@ -588,24 +568,6 @@ Aw man, that was good"#;
let new_map = parse_genius_lyrics(&new_song)?;
dbg!(map);
dbg!(new_map);
panic!();
Ok(())
}
#[test]
fn test_block_parsing() -> Result<()> {
let chorus = r#"[Chorus]
I'm singing, "Hallelujah" (Hallelujah)
God is able, hallelujah (Hallelujah)
God is faithful, hallelujah
Lord, I'm gonna sing (Come on now, sing it)
Oh I'm singing, "Hallelujah" (Hallelujah)
God is able, hallelujah (Hallelujah)
God is faithful, hallelujah (God is so good)
Lord, I'm gonna sing (Sing it, Dave)
"#;
let _thing = block.parse(chorus).into_diagnostic()?;
Ok(())
}
}

View file

@ -176,6 +176,7 @@ struct App {
config_handler: Option<Config>,
obs_connection: String,
view_mode: ViewMode,
genius_token_hidden: bool,
}
#[allow(dead_code)]
@ -231,6 +232,8 @@ enum Message {
SetObsConnection(String),
ModifiersPressed(Modifiers),
ViewModeSwitch(ViewMode),
ShowGeniusToken,
SetGeniusToken(String),
}
#[allow(dead_code)]
@ -353,7 +356,10 @@ impl cosmic::Application for App {
let items: Arc<Vec<ServiceItem>> = Arc::new(vec![]);
let presenter = Presenter::with_items(items.clone());
let song_editor = SongEditor::new(Arc::clone(&fontdb));
let song_editor = SongEditor::new(
Arc::clone(&fontdb),
settings.genius_token.clone(),
);
// for item in items.iter() {
// nav_model.insert().text(item.title()).data(item.clone());
@ -433,6 +439,7 @@ impl cosmic::Application for App {
config_handler,
obs_connection: String::new(),
view_mode: ViewMode::Row,
genius_token_hidden: true,
};
let mut batch = vec![];
@ -826,6 +833,20 @@ impl cosmic::Application for App {
Message::SetObsUrl(self.obs_connection.clone()),
),
);
let genius_token = settings::item::builder("Token")
.control(
text_input::secure_input(
"",
self.settings
.genius_token
.clone()
.unwrap_or_default(),
Some(Message::ShowGeniusToken),
self.genius_token_hidden,
)
.select_on_focus(true)
.on_input(Message::SetGeniusToken),
);
let settings_column = column![
icon::from_name("dialog-close")
.symbolic(true)
@ -837,17 +858,25 @@ impl cosmic::Application for App {
.padding(space_s)
.align_right(Length::Fill)
.align_top(60),
horizontal().height(space_xxl),
settings::section()
.title("Obs Settings")
.add(obs_socket)
.add(apply_button)
.apply(container)
.center_x(Length::Fill)
.align_top(Length::Fill)
.padding([0, space_xxxl * 2])
.align_top(Length::Fill),
settings::section()
.title("Genius Auth Token")
.add(genius_token)
.apply(container)
.center_x(Length::Fill)
.align_top(Length::Fill),
horizontal().height(space_xxl),
]
.height(Length::Fill);
.spacing(space_s)
.height(Length::Fill)
.apply(container)
.padding(space_xxl);
let settings_container = settings_column
.apply(container)
.style(nav_bar_style)
@ -1560,9 +1589,31 @@ impl cosmic::Application for App {
Task::none()
}
Message::SetObsConnection(url) => {
if let Some(config_handler) =
self.config_handler.as_ref()
{
// todo!()
();
}
self.obs_connection = url;
Task::none()
}
Message::SetGeniusToken(token) => {
if let Some(config_handler) =
self.config_handler.as_ref()
{
self.settings.set_genius_token(
config_handler,
Some(token.clone()),
);
self.song_editor.genius_token = Some(token);
}
Task::none()
}
Message::ShowGeniusToken => {
self.genius_token_hidden = !self.genius_token_hidden;
Task::none()
}
Message::ModifiersPressed(modifiers) => {
if modifiers.is_empty() {
self.modifiers_pressed = None;

View file

@ -5,11 +5,13 @@ use std::io::{self};
use std::path::PathBuf;
use std::sync::Arc;
use cosmic::cosmic_config::CosmicConfigEntry;
use cosmic::dialog::file_chooser::FileFilter;
use cosmic::dialog::file_chooser::open::Dialog;
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::core::widget::tree;
use cosmic::iced::font::{Style, Weight};
use cosmic::iced::theme::Base;
use cosmic::iced::widget::scrollable::{
self as iced_scrollable, AbsoluteOffset, Direction, Scrollbar,
};
@ -18,6 +20,7 @@ use cosmic::iced::{
Background as ContainerBackground, Border, Color, Length,
Padding, Shadow, Vector, color, task,
};
use cosmic::widget::button::Catalog;
use cosmic::widget::color_picker::ColorPickerUpdate;
use cosmic::widget::grid::{self};
use cosmic::widget::space::{self, horizontal};
@ -36,7 +39,9 @@ use itertools::Itertools;
use tracing::{debug, error};
use crate::core::service_items::ServiceTrait;
use crate::core::settings;
use crate::core::slide::{Slide, TextAlignment};
use crate::core::song_search::{self, OnlineSong};
use crate::core::songs::{Song, VerseName};
use crate::ui::presenter::slide_view;
use crate::ui::slide_editor::SlideEditor;
@ -89,6 +94,9 @@ pub struct SongEditor {
shadow_color_model: ColorPickerModel,
shadow_tools_open: bool,
importing: bool,
search_input: String,
search_results: Option<Vec<OnlineSong>>,
pub genius_token: Option<String>,
}
#[allow(clippy::large_enum_variant)]
@ -143,6 +151,9 @@ pub enum Message {
UpdateShadowOffsetX(usize),
UpdateShadowOffsetY(usize),
ChangeFontWeight,
SearchUpdate(String),
SearchSong(String),
UpdateSearchResults(Result<Vec<OnlineSong>, String>),
}
#[derive(Debug, Clone)]
@ -182,7 +193,10 @@ impl Display for Face {
#[allow(clippy::cast_possible_truncation)]
impl SongEditor {
pub fn new(font_db: Arc<fontdb::Database>) -> Self {
pub fn new(
font_db: Arc<fontdb::Database>,
genius_token: Option<String>,
) -> Self {
let fonts = font_dir();
debug!(?fonts);
let fonts: Vec<Face> = font_db
@ -337,6 +351,9 @@ impl SongEditor {
),
shadow_tools_open: false,
importing: false,
search_input: String::new(),
search_results: None,
genius_token,
}
}
@ -923,6 +940,28 @@ impl SongEditor {
Message::OpenShadowTools => {
self.shadow_tools_open = !self.shadow_tools_open;
}
Message::SearchUpdate(query) => {
self.search_input = query;
}
Message::SearchSong(query) => {
return Action::Task(Task::perform(
song_search::search_genius_links(
query,
self.genius_token.clone().unwrap_or_default(),
),
|res| {
Message::UpdateSearchResults(
res.map_err(|e| e.to_string()),
)
},
));
}
Message::UpdateSearchResults(result) => match result {
Ok(songs) => self.search_results = Some(songs),
Err(e) => {
error!("Cannot find songs: {e}");
}
},
Message::None => (),
}
Action::None
@ -1852,7 +1891,73 @@ impl SongEditor {
}
pub fn import_view(&self) -> Element<Message> {
todo!("need to add an import view")
let search_bar =
cosmic::widget::search_input("", &self.search_input)
.on_input(Message::SearchUpdate)
.on_submit(Message::SearchSong)
.label("Search for Song");
let submit_button =
button::icon(icon::from_name("document-send-symbolic"))
.on_press(Message::SearchSong(
self.search_input.clone(),
));
let search_results =
self.search_results.as_ref().map_or_else(
|| space::horizontal().apply(container),
|songs| {
let songs: Vec<Element<Message>> = songs
.iter()
.map(|song| {
let title = text::heading(&song.title);
let author = text::body(&song.author);
let link = text::body(&song.link);
let provider =
text::body(song.provider.to_string())
.apply(container)
.style(|t| {
container::Style::default()
.color(
t.cosmic()
.palette
.accent_green,
)
.border(
Border::default()
.rounded(
t.cosmic()
.radius_s(
),
),
)
})
.padding(
theme::spacing().space_s,
);
row![
column![title, author, link].spacing(
theme::spacing().space_xxs
),
space::horizontal(),
provider
]
.padding(theme::spacing().space_s)
.apply(container)
.class(theme::Container::Card)
.into()
})
.collect();
column::with_children(songs)
.spacing(theme::spacing().space_s)
.apply(container)
},
);
let search_row =
row![search_bar, space::horizontal(), submit_button];
column![search_row, search_results].into()
}
pub const fn editing(&self) -> bool {
@ -2045,7 +2150,7 @@ impl Default for SongEditor {
fn default() -> Self {
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
Self::new(Arc::new(fontdb))
Self::new(Arc::new(fontdb), None)
}
}