Skip to content

Commit

Permalink
Lyrics support (#535)
Browse files Browse the repository at this point in the history
* Cache lyrics and add ability to skip to position in song via lyrics

* Better formatting for lyrics and fix initialization

* Add music note button to show lyrics.

* Toggle lyrics

---------

Co-authored-by: SO9010 <[email protected]>
Co-authored-by: Jackson Goode <[email protected]>
  • Loading branch information
3 people authored Oct 7, 2024
1 parent ecc0115 commit 33fa7b9
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 39 deletions.
2 changes: 1 addition & 1 deletion psst-gui/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn add_windows_icon() {
res.compile().expect("Could not attach exe icon");

fn load_images() -> Vec<IcoFrame<'static>> {
let sizes = vec![32, 64, 128, 256];
let sizes = [32, 64, 128, 256];
sizes
.iter()
.map(|s| {
Expand Down
2 changes: 2 additions & 0 deletions psst-gui/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub const LOG_OUT: Selector = Selector::new("app.log-out");
pub const NAVIGATE: Selector<Nav> = Selector::new("app.navigates");
pub const NAVIGATE_BACK: Selector<usize> = Selector::new("app.navigate-back");
pub const NAVIGATE_REFRESH: Selector = Selector::new("app.navigate-refresh");
pub const TOGGLE_LYRICS: Selector = Selector::new("app.toggle-lyrics");

// Playback state
pub const PLAYBACK_LOADING: Selector<ItemId> = Selector::new("app.playback-loading");
Expand All @@ -53,6 +54,7 @@ pub const PLAY_STOP: Selector = Selector::new("app.play-stop");
pub const ADD_TO_QUEUE: Selector<(QueueEntry, PlaybackItem)> = Selector::new("app.add-to-queue");
pub const PLAY_QUEUE_BEHAVIOR: Selector<QueueBehavior> = Selector::new("app.play-queue-behavior");
pub const PLAY_SEEK: Selector<f64> = Selector::new("app.play-seek");
pub const SKIP_TO_POSITION: Selector<u64> = Selector::new("app.skip-to-position");

// Sorting control
pub const SORT_BY_DATE_ADDED: Selector = Selector::new("app.sort-by-date-added");
Expand Down
4 changes: 2 additions & 2 deletions psst-gui/src/controller/nav.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use druid::widget::{prelude::*, Controller};

use crate::{
cmd,
data::{AppState, Nav, SpotifyUrl},
ui::{album, artist, library, playlist, recommend, search, show},
};
use druid::widget::{prelude::*, Controller};

pub struct NavController;

impl NavController {
fn load_route_data(&self, ctx: &mut EventCtx, data: &mut AppState) {
match &data.nav {
Nav::Home => {}
Nav::Lyrics => {}
Nav::SavedTracks => {
if !data.library.saved_tracks.is_resolved() {
ctx.submit_command(library::LOAD_TRACKS);
Expand Down
22 changes: 21 additions & 1 deletion psst-gui/src/controller/playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ use souvlaki::{

use crate::{
cmd,
data::{AppState, Config, Playback, PlaybackOrigin, PlaybackState, QueueBehavior, QueueEntry},
data::Nav,
data::{
AppState, Config, NowPlaying, Playback, PlaybackOrigin, PlaybackState, QueueBehavior,
QueueEntry,
},
ui::lyrics,
};

pub struct PlaybackController {
Expand Down Expand Up @@ -310,6 +315,12 @@ impl PlaybackController {
},
}));
}

fn update_lyrics(&self, ctx: &mut EventCtx, data: &AppState, now_playing: &NowPlaying) {
if matches!(data.nav, Nav::Lyrics) {
ctx.submit_command(lyrics::SHOW_LYRICS.with(now_playing.clone()));
}
}
}

impl<W> Controller<AppState, W> for PlaybackController
Expand Down Expand Up @@ -348,6 +359,9 @@ where
data.start_playback(queued.item, queued.origin, progress.to_owned());
self.update_media_control_playback(&data.playback);
self.update_media_control_metadata(&data.playback);
if let Some(now_playing) = &data.playback.now_playing {
self.update_lyrics(ctx, data, now_playing);
}
} else {
log::warn!("played item not found in playback queue");
}
Expand Down Expand Up @@ -436,6 +450,12 @@ where
}
ctx.set_handled();
}
Event::Command(cmd) if cmd.is(cmd::SKIP_TO_POSITION) => {
let location = cmd.get_unchecked(cmd::SKIP_TO_POSITION);
self.seek(Duration::from_millis(location.clone()));

ctx.set_handled();
}
// Keyboard shortcuts.
Event::KeyDown(key) if key.code == Code::Space => {
self.pause_or_resume();
Expand Down
4 changes: 3 additions & 1 deletion psst-gui/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub use crate::data::{
search::{Search, SearchResults, SearchTopic},
show::{Episode, EpisodeId, EpisodeLink, Show, ShowDetail, ShowEpisodes, ShowLink},
slider_scroll_scale::SliderScrollScale,
track::{AudioAnalysis, Track, TrackId},
track::{AudioAnalysis, Track, TrackId, TrackLines},
user::{PublicUser, UserProfile},
utils::{Cached, Float64, Image, Page},
};
Expand Down Expand Up @@ -83,6 +83,7 @@ pub struct AppState {
pub alerts: Vector<Alert>,
pub finder: Finder,
pub added_queue: Vector<QueueEntry>,
pub lyrics: Promise<Vector<TrackLines>>,
}

impl AppState {
Expand Down Expand Up @@ -164,6 +165,7 @@ impl AppState {
common_ctx,
alerts: Vector::new(),
finder: Finder::new(),
lyrics: Promise::Empty,
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions psst-gui/src/data/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::RecommendationsRequest;
#[derive(Copy, Clone, Debug, Data, PartialEq, Eq, Hash)]
pub enum Route {
Home,
Lyrics,
SavedTracks,
SavedAlbums,
SavedShows,
Expand All @@ -26,6 +27,7 @@ pub enum Route {
pub enum Nav {
#[default]
Home,
Lyrics,
SavedTracks,
SavedAlbums,
SavedShows,
Expand All @@ -41,6 +43,7 @@ impl Nav {
pub fn route(&self) -> Route {
match self {
Nav::Home => Route::Home,
Nav::Lyrics => Route::Lyrics,
Nav::SavedTracks => Route::SavedTracks,
Nav::SavedAlbums => Route::SavedAlbums,
Nav::SavedShows => Route::SavedShows,
Expand All @@ -56,6 +59,7 @@ impl Nav {
pub fn title(&self) -> String {
match self {
Nav::Home => "Home".to_string(),
Nav::Lyrics => "Lyrics".to_string(),
Nav::SavedTracks => "Saved Tracks".to_string(),
Nav::SavedAlbums => "Saved Albums".to_string(),
Nav::SavedShows => "Saved Podcasts".to_string(),
Expand All @@ -71,6 +75,7 @@ impl Nav {
pub fn full_title(&self) -> String {
match self {
Nav::Home => "Home".to_string(),
Nav::Lyrics => "Lyrics".to_string(),
Nav::SavedTracks => "Saved Tracks".to_string(),
Nav::SavedAlbums => "Saved Albums".to_string(),
Nav::SavedShows => "Saved Shows".to_string(),
Expand Down
9 changes: 9 additions & 0 deletions psst-gui/src/data/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct Track {
pub popularity: Option<u32>,
#[serde(skip)]
pub track_pos: usize,
pub lyrics: Option<Vector<TrackLines>>,
}

impl Track {
Expand Down Expand Up @@ -74,6 +75,14 @@ impl Track {
}
}

#[derive(Clone, Debug, Data, Lens, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TrackLines {
pub start_time_ms: String,
pub words: String,
pub end_time_ms: String,
}

#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, Deserialize, Serialize)]
#[serde(try_from = "String")]
#[serde(into = "String")]
Expand Down
52 changes: 24 additions & 28 deletions psst-gui/src/ui/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ pub fn home_widget() -> impl Widget<AppState> {
}

fn simple_title_label(title: &str) -> impl Widget<AppState> {
Flex::column()
.with_default_spacer()
.with_child(Label::new(title)
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0))
Flex::column().with_default_spacer().with_child(
Label::new(title)
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0)),
)
}

pub fn made_for_you() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -64,7 +63,7 @@ pub fn made_for_you() -> impl Widget<AppState> {
}

pub fn recommended_stations() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -82,27 +81,24 @@ pub fn recommended_stations() -> impl Widget<AppState> {

fn uniquely_yours_results_widget() -> impl Widget<WithCtx<MixedView>> {
Either::new(
|results: &WithCtx<MixedView>, _| {
results.data.playlists.is_empty()
},
|results: &WithCtx<MixedView>, _| results.data.playlists.is_empty(),
Empty,
Flex::column().with_default_spacer()
.with_child(Label::new("Uniquely yours")
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0))
).with_child(
Scroll::new(
Flex::row()
.with_child(playlist_results_widget())
Flex::column()
.with_default_spacer()
.with_child(
Label::new("Uniquely yours")
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0)),
)
.align_left(),
),
.with_child(
Scroll::new(Flex::row().with_child(playlist_results_widget())).align_left(),
),
)
}

pub fn uniquely_yours() -> impl Widget<AppState> {
Async::new(spinner_widget, uniquely_yours_results_widget, || {Empty})
Async::new(spinner_widget, uniquely_yours_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -119,7 +115,7 @@ pub fn uniquely_yours() -> impl Widget<AppState> {
}

pub fn user_top_mixes() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -136,7 +132,7 @@ pub fn user_top_mixes() -> impl Widget<AppState> {
}

pub fn best_of_artists() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -153,7 +149,7 @@ pub fn best_of_artists() -> impl Widget<AppState> {
}

pub fn your_shows() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -170,7 +166,7 @@ pub fn your_shows() -> impl Widget<AppState> {
}

pub fn jump_back_in() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -187,7 +183,7 @@ pub fn jump_back_in() -> impl Widget<AppState> {
}

pub fn shows_that_you_might_like() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand Down
Loading

0 comments on commit 33fa7b9

Please sign in to comment.