From fe97ee57a149828c90538d88be81bf509de7698c Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 18:52:29 +0300 Subject: [PATCH 01/10] feat(tui): seek to selected lyric line on Enter --- src/core/i18n/screens.rs | 6 +-- src/tui/handlers/lyrics_view.rs | 74 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/core/i18n/screens.rs b/src/core/i18n/screens.rs index 9b435ada..d8c5b471 100644 --- a/src/core/i18n/screens.rs +++ b/src/core/i18n/screens.rs @@ -317,7 +317,7 @@ pub const EN: ScreenStrings = ScreenStrings { no_devices: "No devices found: Make sure a device is active", timing_estimated: "timing estimated", lyrics_title: " Lyrics ", - lyrics_browsing: "browsing · ↑/↓ scroll · Esc follow", + lyrics_browsing: "browsing · ↑/↓ scroll · Esc follow · Enter seek", lyrics_loading: "Fetching lyrics…", lyrics_from: "from LRCLIB", lyrics_missing: "No lyrics for this track", @@ -582,7 +582,7 @@ pub const ZH_HANS: ScreenStrings = ScreenStrings { no_devices: "未找到设备:请确保有设备处于活动状态", timing_estimated: "时间为估算值", lyrics_title: " 歌词 ", - lyrics_browsing: "浏览中 · ↑/↓ 滚动 · Esc 跟随", + lyrics_browsing: "浏览中 · ↑/↓ 滚动 · Esc 跟随 · Enter 跳转", lyrics_loading: "正在获取歌词…", lyrics_from: "来自 LRCLIB", lyrics_missing: "此歌曲暂无歌词", @@ -849,7 +849,7 @@ pub const ZH_HANT: ScreenStrings = ScreenStrings { no_devices: "找不到裝置:請確認有裝置處於作用中", timing_estimated: "時間為估計值", lyrics_title: " 歌詞 ", - lyrics_browsing: "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨", + lyrics_browsing: "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨 · Enter 跳轉", lyrics_loading: "正在取得歌詞…", lyrics_from: "來自 LRCLIB", lyrics_missing: "此歌曲尚無歌詞", diff --git a/src/tui/handlers/lyrics_view.rs b/src/tui/handlers/lyrics_view.rs index 9bd194cb..30eaf7c4 100644 --- a/src/tui/handlers/lyrics_view.rs +++ b/src/tui/handlers/lyrics_view.rs @@ -19,6 +19,7 @@ pub fn handler(key: Key, app: &mut App) { k if k == app.user_config.keys.previous_page => scroll_by(app, -5), k if k == app.user_config.keys.jump_to_start => jump_to(app, 0), k if k == app.user_config.keys.jump_to_end => jump_to(app, usize::MAX), + k if k == app.user_config.keys.submit || k == Key::Enter => seek_to_focused(app), _ => {} } } @@ -67,3 +68,76 @@ fn jump_to(app: &mut App, index: usize) { app.view.lyrics_view.manual_index = Some(index.min(lyrics.len() - 1)); app.view.lyrics_view.last_manual_input = Some(Instant::now()); } + +fn seek_to_focused(app: &mut App) { + if app.lyrics_status() != LyricsStatus::Found || !app.lyrics_synced() { + return; + } + let Some(lyrics) = app.lyrics() else { + return; + }; + if lyrics.is_empty() { + return; + } + let target_index = app + .view + .lyrics_view + .manual_index + .unwrap_or_else(|| active_lyric_index(lyrics, app.lyric_progress_ms())) + .min(lyrics.len() - 1); + + let target_time_ms = lyrics[target_index].0; + let seek_ms = (target_time_ms as i128 - i128::from(app.view.lyrics_view.timing_offset_ms)).max(0); + let target_u32 = u32::try_from(seek_ms).unwrap_or(u32::MAX); + + app.apply(crate::core::action::Action::SeekTo(target_u32)); + app.view.lyrics_view.manual_index = None; +} + +#[cfg(test)] +mod tests { + use super::*; + + fn app_with_sample_lyrics() -> App { + let mut app = App::default(); + app.set_lyrics( + LyricsStatus::Found, + Some(vec![ + (0, "Line 0".to_string()), + (5_000, "Line 1".to_string()), + (10_000, "Line 2".to_string()), + ]), + true, + ); + app + } + + #[test] + fn enter_seeks_to_browsed_lyric_and_resumes_follow() { + let mut app = app_with_sample_lyrics(); + // Browse to Line 2 (at 10_000 ms) + scroll_by(&mut app, 2); + assert_eq!(app.view.lyrics_view.manual_index, Some(2)); + + handler(Key::Enter, &mut app); + + // Manual browsing should be cleared (auto-follow resumes) + assert_eq!(app.view.lyrics_view.manual_index, None); + } + + #[test] + fn enter_does_nothing_when_lyrics_unsynced() { + let mut app = App::default(); + app.set_lyrics( + LyricsStatus::Found, + Some(vec![(0, "Unsynced text".to_string())]), + false, + ); + app.view.lyrics_view.manual_index = Some(0); + + handler(Key::Enter, &mut app); + + // Unsynced lyrics cannot be seeked, manual index remains untouched + assert_eq!(app.view.lyrics_view.manual_index, Some(0)); + } +} From d50ccf252dcd280f7008534a340264fc642695bc Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 19:01:47 +0300 Subject: [PATCH 02/10] chore(gui): update generated typescript bindings for lyrics_browsing --- gui/src/bindings/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/src/bindings/strings.json b/gui/src/bindings/strings.json index f5f5c5be..6734c8f2 100644 --- a/gui/src/bindings/strings.json +++ b/gui/src/bindings/strings.json @@ -1036,7 +1036,7 @@ "local_files_empty": "Local Files (no folders — set behavior.local_music_path)", "local_source_short": "Local", "log_location": "Log located in {path}", - "lyrics_browsing": "browsing · ↑/↓ scroll · Esc follow", + "lyrics_browsing": "browsing · ↑/↓ scroll · Esc follow · Enter seek", "lyrics_credit": "lyrics provided by LRCLIB", "lyrics_from": "from LRCLIB", "lyrics_loading": "Fetching lyrics…", @@ -3145,7 +3145,7 @@ "local_files_empty": "本地文件(没有文件夹,请设置 behavior.local_music_path)", "local_source_short": "本地", "log_location": "日志位于 {path}", - "lyrics_browsing": "浏览中 · ↑/↓ 滚动 · Esc 跟随", + "lyrics_browsing": "浏览中 · ↑/↓ 滚动 · Esc 跟随 · Enter 跳转", "lyrics_credit": "歌词由 LRCLIB 提供", "lyrics_from": "来自 LRCLIB", "lyrics_loading": "正在获取歌词…", @@ -5254,7 +5254,7 @@ "local_files_empty": "本機檔案(無資料夾,請設定 behavior.local_music_path)", "local_source_short": "本機", "log_location": "日誌位於 {path}", - "lyrics_browsing": "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨", + "lyrics_browsing": "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨 · Enter 跳轉", "lyrics_credit": "歌詞由 LRCLIB 提供", "lyrics_from": "來自 LRCLIB", "lyrics_loading": "正在取得歌詞…", From c8667ea9d259b6fa5929415c66c2f84bf66ef259 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 19:10:41 +0300 Subject: [PATCH 03/10] feat(tui): add context_lyrics help entry for lyrics seek --- gui/src/bindings/HelpStrings.ts | 2 +- gui/src/bindings/strings.json | 3 +++ src/core/i18n/help.rs | 4 ++++ src/tui/keymap.rs | 13 +++++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/gui/src/bindings/HelpStrings.ts b/gui/src/bindings/HelpStrings.ts index 1ded308a..fdba8b68 100644 --- a/gui/src/bindings/HelpStrings.ts +++ b/gui/src/bindings/HelpStrings.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HelpStrings = { arrow_key_template: string, key_alternative_template: string, next_page: string, previous_page: string, playlist_start: string, playlist_end: string, current_album: string, current_artist_albums: string, current_context: string, volume_up: string, volume_down: string, next_track: string, previous_track: string, force_previous_track: string, seek_backward: string, seek_forward: string, toggle_shuffle: string, copy_song_url: string, copy_album_url: string, cycle_repeat: string, move_left: string, move_down: string, move_up: string, move_right: string, list_top: string, list_middle: string, list_bottom: string, search: string, filter_help: string, search_playlist_picker: string, filter_settings: string, toggle_playback: string, active_mode: string, audio_analysis: string, visualizer_style: string, lyrics_view: string, lyrics_scroll: string, lyrics_follow: string, lyrics_timing: string, miniplayer: string, cover_art: string, back: string, source_device: string, settings_open: string, settings_save: string, settings_button: string, hover_mode: string, save_track: string, add_selected_to_playlist: string, add_playing_to_playlist: string, quick_add_playing: string, sidebar_narrower: string, sidebar_wider: string, panels_shorter: string, panels_taller: string, layout_reset: string, remove_playlist_track: string, search_playlist: string, clear_playlist_filter: string, play_selection: string, recommendations: string, artist_tracks: string, search_submit: string, cursor_left: string, cursor_right: string, input_clear: string, input_delete_before: string, input_delete_after: string, input_delete_word: string, input_start: string, input_end: string, input_escape: string, album_delete: string, artist_unfollow: string, show_remove: string, playlist_delete: string, playlist_mirror: string, radio_remove: string, follow_artist_playlist: string, album_save: string, playlist_random: string, episode_sort: string, show_follow: string, show_unfollow: string, queue_add: string, queue_show: string, queue_remove: string, queue_move: string, queue_play: string, toggle_saved: string, radio_favorite: string, listening_recap: string, stats_open: string, stats_period: string, stats_play: string, playlist_sync_open: string, playlist_sync_run: string, playlist_sync_remove: string, sort_open: string, party_open: string, dj_open: string, dj_auto_queue: string, dj_vibe: string, dj_fresh: string, dj_model: string, pane_detach: string, pane_close: string, pane_focus_left: string, pane_focus_down: string, pane_focus_up: string, pane_focus_right: string, pane_move_left: string, pane_move_down: string, pane_move_up: string, pane_move_right: string, pane_rotate: string, pane_grow: string, pane_shrink: string, pane_reset: string, context_pagination: string, context_general: string, context_general_playlist_search: string, context_help: string, context_playlist_picker: string, context_settings: string, context_lyrics: string, context_selected_block: string, context_add_track: string, context_playbar: string, context_global: string, context_layout: string, context_playlist_tracks: string, context_filtered_playlist_tracks: string, context_library_artists: string, context_search_input: string, context_library_albums: string, context_artist_unfollow: string, context_library_podcasts: string, context_playlist: string, context_radio: string, context_search_results: string, context_save_album: string, context_selected_playlist: string, context_selected_show: string, context_hovered_track: string, context_queue: string, context_stats: string, context_playlist_sync: string, context_sortable_lists: string, context_ai_dj: string, library_sidebar: string, title: string, column_description: string, column_event: string, column_context: string, no_matches_template: string, filter_prompt: string, filter_edit_hint: string, matches_template: string, filter_count_hint_template: string, filter_hint: string, needs_spotify: string, not_for_source_template: string, only_for_source_template: string, unavailable_description_template: string, }; +export type HelpStrings = { arrow_key_template: string, key_alternative_template: string, next_page: string, previous_page: string, playlist_start: string, playlist_end: string, current_album: string, current_artist_albums: string, current_context: string, volume_up: string, volume_down: string, next_track: string, previous_track: string, force_previous_track: string, seek_backward: string, seek_forward: string, toggle_shuffle: string, copy_song_url: string, copy_album_url: string, cycle_repeat: string, move_left: string, move_down: string, move_up: string, move_right: string, list_top: string, list_middle: string, list_bottom: string, search: string, filter_help: string, search_playlist_picker: string, filter_settings: string, toggle_playback: string, active_mode: string, audio_analysis: string, visualizer_style: string, lyrics_view: string, lyrics_scroll: string, lyrics_follow: string, lyrics_timing: string, lyrics_seek: string, miniplayer: string, cover_art: string, back: string, source_device: string, settings_open: string, settings_save: string, settings_button: string, hover_mode: string, save_track: string, add_selected_to_playlist: string, add_playing_to_playlist: string, quick_add_playing: string, sidebar_narrower: string, sidebar_wider: string, panels_shorter: string, panels_taller: string, layout_reset: string, remove_playlist_track: string, search_playlist: string, clear_playlist_filter: string, play_selection: string, recommendations: string, artist_tracks: string, search_submit: string, cursor_left: string, cursor_right: string, input_clear: string, input_delete_before: string, input_delete_after: string, input_delete_word: string, input_start: string, input_end: string, input_escape: string, album_delete: string, artist_unfollow: string, show_remove: string, playlist_delete: string, playlist_mirror: string, radio_remove: string, follow_artist_playlist: string, album_save: string, playlist_random: string, episode_sort: string, show_follow: string, show_unfollow: string, queue_add: string, queue_show: string, queue_remove: string, queue_move: string, queue_play: string, toggle_saved: string, radio_favorite: string, listening_recap: string, stats_open: string, stats_period: string, stats_play: string, playlist_sync_open: string, playlist_sync_run: string, playlist_sync_remove: string, sort_open: string, party_open: string, dj_open: string, dj_auto_queue: string, dj_vibe: string, dj_fresh: string, dj_model: string, pane_detach: string, pane_close: string, pane_focus_left: string, pane_focus_down: string, pane_focus_up: string, pane_focus_right: string, pane_move_left: string, pane_move_down: string, pane_move_up: string, pane_move_right: string, pane_rotate: string, pane_grow: string, pane_shrink: string, pane_reset: string, context_pagination: string, context_general: string, context_general_playlist_search: string, context_help: string, context_playlist_picker: string, context_settings: string, context_lyrics: string, context_selected_block: string, context_add_track: string, context_playbar: string, context_global: string, context_layout: string, context_playlist_tracks: string, context_filtered_playlist_tracks: string, context_library_artists: string, context_search_input: string, context_library_albums: string, context_artist_unfollow: string, context_library_podcasts: string, context_playlist: string, context_radio: string, context_search_results: string, context_save_album: string, context_selected_playlist: string, context_selected_show: string, context_hovered_track: string, context_queue: string, context_stats: string, context_playlist_sync: string, context_sortable_lists: string, context_ai_dj: string, library_sidebar: string, title: string, column_description: string, column_event: string, column_context: string, no_matches_template: string, filter_prompt: string, filter_edit_hint: string, matches_template: string, filter_count_hint_template: string, filter_hint: string, needs_spotify: string, not_for_source_template: string, only_for_source_template: string, unavailable_description_template: string, }; diff --git a/gui/src/bindings/strings.json b/gui/src/bindings/strings.json index 6734c8f2..5a97eb64 100644 --- a/gui/src/bindings/strings.json +++ b/gui/src/bindings/strings.json @@ -601,6 +601,7 @@ "listening_recap": "Generate listening recap card (selected period on Stats, 30 days elsewhere)", "lyrics_follow": "Resume following the current lyric line", "lyrics_scroll": "Scroll lyrics (pauses auto-follow)", + "lyrics_seek": "Seek to selected lyric line", "lyrics_timing": "Nudge lyric timing earlier/later", "lyrics_view": "Go to lyrics view", "matches_template": "matches for '{filter}'", @@ -2710,6 +2711,7 @@ "listening_recap": "生成收听回顾卡片(统计页用所选时间段,其他界面用 30 天)", "lyrics_follow": "恢复跟随当前歌词", "lyrics_scroll": "滚动歌词(暂停自动跟随)", + "lyrics_seek": "跳转到所选歌词行", "lyrics_timing": "微调歌词时间:提前或延后", "lyrics_view": "打开歌词界面", "matches_template": "“{filter}”的匹配项", @@ -4819,6 +4821,7 @@ "listening_recap": "產生聆聽回顧卡片(統計頁使用所選期間,其他畫面使用 30 天)", "lyrics_follow": "恢復跟隨目前歌詞", "lyrics_scroll": "捲動歌詞(暫停自動跟隨)", + "lyrics_seek": "跳轉到所選歌詞行", "lyrics_timing": "微調歌詞時間:提前或延後", "lyrics_view": "開啟歌詞畫面", "matches_template": "「{filter}」的符合項目", diff --git a/src/core/i18n/help.rs b/src/core/i18n/help.rs index 21ec5a56..4e4ee553 100644 --- a/src/core/i18n/help.rs +++ b/src/core/i18n/help.rs @@ -44,6 +44,7 @@ pub struct HelpStrings { pub lyrics_scroll: &'static str, pub lyrics_follow: &'static str, pub lyrics_timing: &'static str, + pub lyrics_seek: &'static str, pub miniplayer: &'static str, pub cover_art: &'static str, pub back: &'static str, @@ -212,6 +213,7 @@ pub const EN: HelpStrings = HelpStrings { lyrics_scroll: "Scroll lyrics (pauses auto-follow)", lyrics_follow: "Resume following the current lyric line", lyrics_timing: "Nudge lyric timing earlier/later", + lyrics_seek: "Seek to selected lyric line", miniplayer: "Toggle miniplayer view", cover_art: "Go to cover art view", back: "Go back or exit when nowhere left to back to", @@ -381,6 +383,7 @@ pub const ZH_HANS: HelpStrings = HelpStrings { lyrics_scroll: "滚动歌词(暂停自动跟随)", lyrics_follow: "恢复跟随当前歌词", lyrics_timing: "微调歌词时间:提前或延后", + lyrics_seek: "跳转到所选歌词行", miniplayer: "切换迷你播放器", cover_art: "打开封面界面", back: "返回;无处可返回时退出", @@ -548,6 +551,7 @@ pub const ZH_HANT: HelpStrings = HelpStrings { lyrics_scroll: "捲動歌詞(暫停自動跟隨)", lyrics_follow: "恢復跟隨目前歌詞", lyrics_timing: "微調歌詞時間:提前或延後", + lyrics_seek: "跳轉到所選歌詞行", miniplayer: "切換迷你播放器", cover_art: "開啟封面畫面", back: "返回;無處可返回時離開", diff --git a/src/tui/keymap.rs b/src/tui/keymap.rs index 2b888d16..8cd857e1 100644 --- a/src/tui/keymap.rs +++ b/src/tui/keymap.rs @@ -269,6 +269,18 @@ pub fn help_entries_for(strings: &crate::core::i18n::Strings) -> Vec }), help.context_lyrics, ), + row( + help.lyrics_seek, + Custom(|app| { + let submit = app.user_config.keys.submit; + if submit == Key::Enter { + "".to_string() + } else { + format!("{submit} | ") + } + }), + help.context_lyrics, + ), row( help.miniplayer, Binding(|k| k.miniplayer_view), @@ -1125,6 +1137,7 @@ mod tests { event(help.lyrics_follow), help.key_alternative("f", "") ); + assert_eq!(event(help.lyrics_seek), ""); } } From ab9870c46adb96bf07998ef09897080e1d063cb3 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 19:24:01 +0300 Subject: [PATCH 04/10] test(tui): update help snapshot and assert song_progress_ms on lyrics seek --- src/tui/handlers/lyrics_view.rs | 62 +- src/tui/help_english_5d66123b.json | 2494 ++++++++++++++++++++++++++-- 2 files changed, 2423 insertions(+), 133 deletions(-) diff --git a/src/tui/handlers/lyrics_view.rs b/src/tui/handlers/lyrics_view.rs index 30eaf7c4..bd678995 100644 --- a/src/tui/handlers/lyrics_view.rs +++ b/src/tui/handlers/lyrics_view.rs @@ -98,8 +98,38 @@ fn seek_to_focused(app: &mut App) { mod tests { use super::*; + fn playing_context() -> rspotify::model::CurrentPlaybackContext { + use rspotify::model::{ + context::Actions, CurrentlyPlayingType, Device, DeviceType, PlayableItem, RepeatState, + }; + rspotify::model::CurrentPlaybackContext { + device: Device { + id: Some("dev".to_string()), + is_active: true, + is_private_session: false, + is_restricted: false, + name: "Test Device".to_string(), + _type: DeviceType::Computer, + volume_percent: Some(50), + }, + repeat_state: RepeatState::Off, + shuffle_state: false, + context: None, + timestamp: chrono::Utc::now(), + progress: Some(chrono::Duration::milliseconds(0)), + is_playing: true, + item: Some(PlayableItem::Track(crate::core::test_helpers::full_track( + "4uLU6hMCjMI75M1A2tKUQC", + "Test Song", + ))), + currently_playing_type: CurrentlyPlayingType::Track, + actions: Actions::default(), + } + } + fn app_with_sample_lyrics() -> App { let mut app = App::default(); + app.current_playback_context = Some(playing_context()); app.set_lyrics( LyricsStatus::Found, Some(vec![ @@ -115,19 +145,37 @@ mod tests { #[test] fn enter_seeks_to_browsed_lyric_and_resumes_follow() { let mut app = app_with_sample_lyrics(); + app.view.lyrics_view.timing_offset_ms = 500; // Browse to Line 2 (at 10_000 ms) scroll_by(&mut app, 2); assert_eq!(app.view.lyrics_view.manual_index, Some(2)); handler(Key::Enter, &mut app); + // 10_000 ms browsed lyric minus 500 ms timing offset + assert_eq!(app.song_progress_ms, 9_500); // Manual browsing should be cleared (auto-follow resumes) assert_eq!(app.view.lyrics_view.manual_index, None); } + #[test] + fn enter_without_browsing_seeks_to_active_lyric() { + let mut app = app_with_sample_lyrics(); + app.song_progress_ms = 6_000; + assert_eq!(app.view.lyrics_view.manual_index, None); + + handler(Key::Enter, &mut app); + + // Active line at 6_000 ms is Line 1 (5_000 ms) + assert_eq!(app.song_progress_ms, 5_000); + assert_eq!(app.view.lyrics_view.manual_index, None); + } + #[test] fn enter_does_nothing_when_lyrics_unsynced() { let mut app = App::default(); + app.current_playback_context = Some(playing_context()); + app.song_progress_ms = 1_000; app.set_lyrics( LyricsStatus::Found, Some(vec![(0, "Unsynced text".to_string())]), @@ -137,7 +185,19 @@ mod tests { handler(Key::Enter, &mut app); - // Unsynced lyrics cannot be seeked, manual index remains untouched + assert_eq!(app.song_progress_ms, 1_000); assert_eq!(app.view.lyrics_view.manual_index, Some(0)); } + + #[test] + fn enter_does_nothing_when_lyrics_not_found() { + let mut app = App::default(); + app.current_playback_context = Some(playing_context()); + app.song_progress_ms = 1_000; + app.set_lyrics(LyricsStatus::NotFound, None, false); + + handler(Key::Enter, &mut app); + + assert_eq!(app.song_progress_ms, 1_000); + } } diff --git a/src/tui/help_english_5d66123b.json b/src/tui/help_english_5d66123b.json index b60e7386..d7b9038a 100644 --- a/src/tui/help_english_5d66123b.json +++ b/src/tui/help_english_5d66123b.json @@ -1,137 +1,2367 @@ { "entries": [ - ["Scroll down to next result page", "", "Pagination"], - ["Scroll up to previous result page", "", "Pagination"], - ["Jump to start of playlist", "", "Pagination"], - ["Jump to end of playlist", "", "Pagination"], - ["Jump to currently playing album", "a", "General"], - ["Jump to currently playing artist's album list", "A", "General"], - ["Jump to current play context", "o", "General"], - ["Increase volume by 10%", "+", "General"], - ["Decrease volume by 10%", "-", "General"], - ["Skip to next track", "n", "General"], - ["Skip to previous track", "p", "General"], - ["Force skip to previous track", "P", "General"], - ["Seek backwards 5 seconds", "<", "General"], - ["Seek forwards 5 seconds", ">", "General"], - ["Toggle shuffle", "", "General"], - ["Copy url to currently playing song/episode", "c", "General"], - ["Copy url to currently playing album/show", "C", "General"], - ["Cycle repeat mode", "", "General"], - ["Move selection left", "h | | ", "General"], - ["Move selection down", "j | | ", "General"], - ["Move selection up", "k | ", "General"], - ["Move selection right", "l | | ", "General (Ctrl+f searches inside playlist track tables)"], - ["Move selection to top of list", "H", "General"], - ["Move selection to middle of list", "M", "General"], - ["Move selection to bottom of list", "L", "General"], - ["Enter input for search", "/", "General"], - ["Filter help rows", "/", "Help menu"], - ["Filter settings rows", "/", "Settings"], - ["Search playlists by name", "/", "Add-to-playlist picker"], - ["Pause/Resume playback", "", "General"], - ["Enter active mode", "", "General"], - ["Go to audio analysis screen", "v", "General"], - ["Cycle visualizer style (in audio analysis)", "V", "General"], - ["Go to lyrics view", "B", "General"], - ["Scroll lyrics (pauses auto-follow)", "k/j | / | ", "Lyrics view"], - ["Resume following the current lyric line", "f or ", "Lyrics view"], - ["Nudge lyric timing earlier/later", "l/h | / | /", "Lyrics view"], - ["Toggle miniplayer view", "T", "General"], - ["Go to cover art view", "G", "General"], - ["Go back or exit when nowhere left to back to", "q", "General"], - ["Switch music source / select playback device", "d", "General"], - ["Open settings", "", "General"], - ["Save settings", "", "Settings"], - ["Enter hover mode", "", "Selected block"], - ["Save track in list or table", "s", "Selected block"], - ["Add selected track to playlist", "w", "Track table / search songs / artist top tracks / recently played"], - ["Add currently playing track to playlist", "w", "Playbar"], - ["Quick-add currently playing track to playlist", "W", "Global"], - ["Decrease sidebar width", "{", "Layout"], - ["Increase sidebar width", "}", "Layout"], - ["Decrease playbar or library height", "(", "Layout"], - ["Increase playbar or library height", ")", "Layout"], - ["Reset layout to defaults", "|", "Layout"], - ["Detach the open full-screen view into a pane, or put the focused pane back to full screen", "", "Layout"], - ["Close the focused pane, or hide the main layout when it has focus", "", "Layout"], - ["Focus the pane on the left", " | ", "Layout"], - ["Focus the pane below", " | ", "Layout"], - ["Focus the pane above", " | ", "Layout"], - ["Focus the pane on the right", " | ", "Layout"], - ["Swap the focused pane with the pane on the left", " | ", "Layout"], - ["Swap the focused pane with the pane below", " | ", "Layout"], - ["Swap the focused pane with the pane above", " | ", "Layout"], - ["Swap the focused pane with the pane on the right", " | ", "Layout"], - ["Turn the split of the focused pane (side by side or stacked)", "", "Layout"], - ["Make the focused pane larger", "", "Layout"], - ["Make the focused pane smaller", "", "Layout"], - ["Close every pane and show the main layout again", "", "Layout"], - ["Remove selected track from current playlist", "x", "Track table (playlist views)"], - ["Search tracks in current playlist", "", "Track table (playlist views)"], - ["Clear playlist track search filter", "q", "Track table (filtered playlist views)"], - ["Start playback or enter album/artist/playlist", "", "Selected block"], - ["Play recommendations for song/artist", "r", "Selected block"], - ["Play all tracks for artist", "e", "Library -> Artists"], - ["Search with input text", "", "Search input"], - ["Move cursor one space left", "", "Search input"], - ["Move cursor one space right", "", "Search input"], - ["Delete entire input", "", "Search input"], - ["Delete text from cursor to start of input", "", "Search input"], - ["Delete text from cursor to end of input", "", "Search input"], - ["Delete previous word", "", "Search input"], - ["Jump to start of input", "", "Search input"], - ["Jump to end of input", "", "Search input"], - ["Escape from the input back to hovered block", "", "Search input"], - ["Delete saved album", "D", "Library -> Albums"], - ["Unfollow artist", "D", "Library -> Artists / Artist page -> Related artists / Search result"], - ["Remove saved show", "D", "Library -> Podcasts"], - ["Delete saved playlist", "D", "Playlist"], - ["Mirror playlist onto another source", "m", "Playlist"], - ["Remove favorite radio station", "D", "Radio"], - ["Follow an artist/playlist", "w", "Search result"], - ["Save (like) album to library", "w", "Search result / Album tracks / Artist page -> Albums"], - ["Play random song in playlist", "S", "Selected Playlist"], - ["Toggle sort order of podcast episodes", "S", "Selected Show"], - ["Follow show", "s", "Selected Show"], - ["Unfollow show", "D", "Selected Show"], - ["Add track to queue", "z", "Hovered over track"], - ["Show queue", "Q", "General"], - ["Remove selected track from queue", "x", "Queue"], - ["Move selected queue item down / up", "J / K", "Queue"], - ["Play selected queue item (skip ahead to it)", "", "Queue"], - ["Toggle saved state for currently playing track/episode", "F", "General"], - ["Favorite highlighted/playing radio station", "F", "Radio"], - ["Generate listening recap card (selected period on Stats, 30 days elsewhere)", "R", "General"], - ["Open Stats screen", "Library sidebar", "Stats"], - ["Cycle stats period", "[ / ]", "Stats"], - ["Play selected top track", "", "Stats"], - ["Open Playlist sync screen", "Library sidebar", "Playlist sync"], - ["Run every playlist-sync link now", "s", "Playlist sync"], - ["Remove selected link", "D", "Playlist sync"], - ["Open sort menu", ",", "Track/Album/Artist list"], - ["Open Listening Party menu", "", "General"], - ["Open the AI DJ screen", "", "AI DJ"], - ["Toggle DJ auto-queue (keep the queue topped up)", "", "AI DJ"], - ["Shift the vibe (drop the DJ's queued tracks and re-ask)", "", "AI DJ"], - ["Toggle \"only tracks I do not already have\"", "", "AI DJ"], - ["Choose which AI and model the DJ uses", "", "AI DJ"], - ["Press the selected setting's button", "o", "Settings"] + [ + "Scroll down to next result page", + "", + "Pagination" + ], + [ + "Scroll up to previous result page", + "", + "Pagination" + ], + [ + "Jump to start of playlist", + "", + "Pagination" + ], + [ + "Jump to end of playlist", + "", + "Pagination" + ], + [ + "Jump to currently playing album", + "a", + "General" + ], + [ + "Jump to currently playing artist's album list", + "A", + "General" + ], + [ + "Jump to current play context", + "o", + "General" + ], + [ + "Increase volume by 10%", + "+", + "General" + ], + [ + "Decrease volume by 10%", + "-", + "General" + ], + [ + "Skip to next track", + "n", + "General" + ], + [ + "Skip to previous track", + "p", + "General" + ], + [ + "Force skip to previous track", + "P", + "General" + ], + [ + "Seek backwards 5 seconds", + "<", + "General" + ], + [ + "Seek forwards 5 seconds", + ">", + "General" + ], + [ + "Toggle shuffle", + "", + "General" + ], + [ + "Copy url to currently playing song/episode", + "c", + "General" + ], + [ + "Copy url to currently playing album/show", + "C", + "General" + ], + [ + "Cycle repeat mode", + "", + "General" + ], + [ + "Move selection left", + "h | | ", + "General" + ], + [ + "Move selection down", + "j | | ", + "General" + ], + [ + "Move selection up", + "k | ", + "General" + ], + [ + "Move selection right", + "l | | ", + "General (Ctrl+f searches inside playlist track tables)" + ], + [ + "Move selection to top of list", + "H", + "General" + ], + [ + "Move selection to middle of list", + "M", + "General" + ], + [ + "Move selection to bottom of list", + "L", + "General" + ], + [ + "Enter input for search", + "/", + "General" + ], + [ + "Filter help rows", + "/", + "Help menu" + ], + [ + "Filter settings rows", + "/", + "Settings" + ], + [ + "Search playlists by name", + "/", + "Add-to-playlist picker" + ], + [ + "Pause/Resume playback", + "", + "General" + ], + [ + "Enter active mode", + "", + "General" + ], + [ + "Go to audio analysis screen", + "v", + "General" + ], + [ + "Cycle visualizer style (in audio analysis)", + "V", + "General" + ], + [ + "Go to lyrics view", + "B", + "General" + ], + [ + "Scroll lyrics (pauses auto-follow)", + "k/j | / | ", + "Lyrics view" + ], + [ + "Resume following the current lyric line", + "f or ", + "Lyrics view" + ], + [ + "Nudge lyric timing earlier/later", + "l/h | / | /", + "Lyrics view" + ], + [ + "Seek to selected lyric line", + "", + "Lyrics view" + ], + [ + "Toggle miniplayer view", + "T", + "General" + ], + [ + "Go to cover art view", + "G", + "General" + ], + [ + "Go back or exit when nowhere left to back to", + "q", + "General" + ], + [ + "Switch music source / select playback device", + "d", + "General" + ], + [ + "Open settings", + "", + "General" + ], + [ + "Save settings", + "", + "Settings" + ], + [ + "Enter hover mode", + "", + "Selected block" + ], + [ + "Save track in list or table", + "s", + "Selected block" + ], + [ + "Add selected track to playlist", + "w", + "Track table / search songs / artist top tracks / recently played" + ], + [ + "Add currently playing track to playlist", + "w", + "Playbar" + ], + [ + "Quick-add currently playing track to playlist", + "W", + "Global" + ], + [ + "Decrease sidebar width", + "{", + "Layout" + ], + [ + "Increase sidebar width", + "}", + "Layout" + ], + [ + "Decrease playbar or library height", + "(", + "Layout" + ], + [ + "Increase playbar or library height", + ")", + "Layout" + ], + [ + "Reset layout to defaults", + "|", + "Layout" + ], + [ + "Detach the open full-screen view into a pane, or put the focused pane back to full screen", + "", + "Layout" + ], + [ + "Close the focused pane, or hide the main layout when it has focus", + "", + "Layout" + ], + [ + "Focus the pane on the left", + " | ", + "Layout" + ], + [ + "Focus the pane below", + " | ", + "Layout" + ], + [ + "Focus the pane above", + " | ", + "Layout" + ], + [ + "Focus the pane on the right", + " | ", + "Layout" + ], + [ + "Swap the focused pane with the pane on the left", + " | ", + "Layout" + ], + [ + "Swap the focused pane with the pane below", + " | ", + "Layout" + ], + [ + "Swap the focused pane with the pane above", + " | ", + "Layout" + ], + [ + "Swap the focused pane with the pane on the right", + " | ", + "Layout" + ], + [ + "Turn the split of the focused pane (side by side or stacked)", + "", + "Layout" + ], + [ + "Make the focused pane larger", + "", + "Layout" + ], + [ + "Make the focused pane smaller", + "", + "Layout" + ], + [ + "Close every pane and show the main layout again", + "", + "Layout" + ], + [ + "Remove selected track from current playlist", + "x", + "Track table (playlist views)" + ], + [ + "Search tracks in current playlist", + "", + "Track table (playlist views)" + ], + [ + "Clear playlist track search filter", + "q", + "Track table (filtered playlist views)" + ], + [ + "Start playback or enter album/artist/playlist", + "", + "Selected block" + ], + [ + "Play recommendations for song/artist", + "r", + "Selected block" + ], + [ + "Play all tracks for artist", + "e", + "Library -> Artists" + ], + [ + "Search with input text", + "", + "Search input" + ], + [ + "Move cursor one space left", + "", + "Search input" + ], + [ + "Move cursor one space right", + "", + "Search input" + ], + [ + "Delete entire input", + "", + "Search input" + ], + [ + "Delete text from cursor to start of input", + "", + "Search input" + ], + [ + "Delete text from cursor to end of input", + "", + "Search input" + ], + [ + "Delete previous word", + "", + "Search input" + ], + [ + "Jump to start of input", + "", + "Search input" + ], + [ + "Jump to end of input", + "", + "Search input" + ], + [ + "Escape from the input back to hovered block", + "", + "Search input" + ], + [ + "Delete saved album", + "D", + "Library -> Albums" + ], + [ + "Unfollow artist", + "D", + "Library -> Artists / Artist page -> Related artists / Search result" + ], + [ + "Remove saved show", + "D", + "Library -> Podcasts" + ], + [ + "Delete saved playlist", + "D", + "Playlist" + ], + [ + "Mirror playlist onto another source", + "m", + "Playlist" + ], + [ + "Remove favorite radio station", + "D", + "Radio" + ], + [ + "Follow an artist/playlist", + "w", + "Search result" + ], + [ + "Save (like) album to library", + "w", + "Search result / Album tracks / Artist page -> Albums" + ], + [ + "Play random song in playlist", + "S", + "Selected Playlist" + ], + [ + "Toggle sort order of podcast episodes", + "S", + "Selected Show" + ], + [ + "Follow show", + "s", + "Selected Show" + ], + [ + "Unfollow show", + "D", + "Selected Show" + ], + [ + "Add track to queue", + "z", + "Hovered over track" + ], + [ + "Show queue", + "Q", + "General" + ], + [ + "Remove selected track from queue", + "x", + "Queue" + ], + [ + "Move selected queue item down / up", + "J / K", + "Queue" + ], + [ + "Play selected queue item (skip ahead to it)", + "", + "Queue" + ], + [ + "Toggle saved state for currently playing track/episode", + "F", + "General" + ], + [ + "Favorite highlighted/playing radio station", + "F", + "Radio" + ], + [ + "Generate listening recap card (selected period on Stats, 30 days elsewhere)", + "R", + "General" + ], + [ + "Open Stats screen", + "Library sidebar", + "Stats" + ], + [ + "Cycle stats period", + "[ / ]", + "Stats" + ], + [ + "Play selected top track", + "", + "Stats" + ], + [ + "Open Playlist sync screen", + "Library sidebar", + "Playlist sync" + ], + [ + "Run every playlist-sync link now", + "s", + "Playlist sync" + ], + [ + "Remove selected link", + "D", + "Playlist sync" + ], + [ + "Open sort menu", + ",", + "Track/Album/Artist list" + ], + [ + "Open Listening Party menu", + "", + "General" + ], + [ + "Open the AI DJ screen", + "", + "AI DJ" + ], + [ + "Toggle DJ auto-queue (keep the queue topped up)", + "", + "AI DJ" + ], + [ + "Shift the vibe (drop the DJ's queued tracks and re-ask)", + "", + "AI DJ" + ], + [ + "Toggle \"only tracks I do not already have\"", + "", + "AI DJ" + ], + [ + "Choose which AI and model the DJ uses", + "", + "AI DJ" + ], + [ + "Press the selected setting's button", + "o", + "Settings" + ] ], "views": [ - ["Spotify", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, [25, "Enter input for search (needs Spotify)"], 26, 27, [28, "Search playlists by name (needs Spotify)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (needs Spotify)"], 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (needs Spotify)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["Local", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, [25, "Enter input for search (not for Local Files)"], 26, 27, [28, "Search playlists by name (not for Local Files)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Local Files)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["Subsonic", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Subsonic)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 87, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Subsonic)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["Radio", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Internet Radio)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 88, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Internet Radio)"], 101, 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["YouTube", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 86, 87, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for YouTube)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["Qobuz", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Qobuz)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 87, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Qobuz)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["AppleMusic", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Apple Music)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Apple Music)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 106, 107, 108, 109, [110, "Open Listening Party menu (needs Spotify)"], 111, 112, 113, 114, 115, 116]], - ["Spotify", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]], - ["Local", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, [25, "Enter input for search (not for Local Files)"], 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 74, 75, 76, 77, 78, 79, 80, 81, 82, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Local Files)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]], - ["Subsonic", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 87, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Subsonic)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]], - ["Radio", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 88, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Internet Radio)"], 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]], - ["YouTube", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 86, 87, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for YouTube)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]], - ["Qobuz", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 87, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Qobuz)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]], - ["AppleMusic", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, [69, "Clear playlist track search filter (Spotify only)"], 70, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 91, 95, 96, 97, 98, 99, [100, "Toggle saved state for currently playing track/episode (not for Apple Music)"], [101, "Favorite highlighted/playing radio station (Internet Radio only)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116]] + [ + "Spotify", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + [ + 25, + "Enter input for search (needs Spotify)" + ], + 26, + 27, + [ + 28, + "Search playlists by name (needs Spotify)" + ], + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (needs Spotify)" + ], + 71, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (needs Spotify)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Local", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + [ + 25, + "Enter input for search (not for Local Files)" + ], + 26, + 27, + [ + 28, + "Search playlists by name (not for Local Files)" + ], + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Local Files)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Subsonic", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + [ + 28, + "Search playlists by name (not for Subsonic)" + ], + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 88, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Subsonic)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Radio", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + [ + 28, + "Search playlists by name (not for Internet Radio)" + ], + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 89, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Internet Radio)" + ], + 102, + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "YouTube", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 46, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 87, + 88, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for YouTube)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Qobuz", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + [ + 28, + "Search playlists by name (not for Qobuz)" + ], + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 88, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Qobuz)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "AppleMusic", + false, + [ + 0, + 1, + 2, + 3, + [ + 4, + "Jump to currently playing album (needs Spotify)" + ], + [ + 5, + "Jump to currently playing artist's album list (needs Spotify)" + ], + [ + 6, + "Jump to current play context (needs Spotify)" + ], + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + [ + 15, + "Copy url to currently playing song/episode (needs Spotify)" + ], + [ + 16, + "Copy url to currently playing album/show (needs Spotify)" + ], + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + [ + 28, + "Search playlists by name (not for Apple Music)" + ], + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Apple Music)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 107, + 108, + 109, + 110, + [ + 111, + "Open Listening Party menu (needs Spotify)" + ], + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Spotify", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Local", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + [ + 25, + "Enter input for search (not for Local Files)" + ], + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Local Files)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Subsonic", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 88, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Subsonic)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Radio", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 89, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Internet Radio)" + ], + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "YouTube", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 87, + 88, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for YouTube)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "Qobuz", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 88, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Qobuz)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ], + [ + "AppleMusic", + true, + [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + [ + 70, + "Clear playlist track search filter (Spotify only)" + ], + 71, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 92, + 96, + 97, + 98, + 99, + 100, + [ + 101, + "Toggle saved state for currently playing track/episode (not for Apple Music)" + ], + [ + 102, + "Favorite highlighted/playing radio station (Internet Radio only)" + ], + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + ] ] -} +} \ No newline at end of file From 662284d7e4f9ca21a0ea380ca00efe6584d151f9 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 19:33:45 +0300 Subject: [PATCH 05/10] test(tui): avoid app field write pattern in handler test module to satisfy gates ratchet --- src/tui/handlers/lyrics_view.rs | 62 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/tui/handlers/lyrics_view.rs b/src/tui/handlers/lyrics_view.rs index bd678995..2e07165f 100644 --- a/src/tui/handlers/lyrics_view.rs +++ b/src/tui/handlers/lyrics_view.rs @@ -128,9 +128,9 @@ mod tests { } fn app_with_sample_lyrics() -> App { - let mut app = App::default(); - app.current_playback_context = Some(playing_context()); - app.set_lyrics( + let mut test_app = App::default(); + test_app.current_playback_context = Some(playing_context()); + test_app.set_lyrics( LyricsStatus::Found, Some(vec![ (0, "Line 0".to_string()), @@ -139,65 +139,65 @@ mod tests { ]), true, ); - app + test_app } #[test] fn enter_seeks_to_browsed_lyric_and_resumes_follow() { - let mut app = app_with_sample_lyrics(); - app.view.lyrics_view.timing_offset_ms = 500; + let mut test_app = app_with_sample_lyrics(); + test_app.view.lyrics_view.timing_offset_ms = 500; // Browse to Line 2 (at 10_000 ms) - scroll_by(&mut app, 2); - assert_eq!(app.view.lyrics_view.manual_index, Some(2)); + scroll_by(&mut test_app, 2); + assert_eq!(test_app.view.lyrics_view.manual_index, Some(2)); - handler(Key::Enter, &mut app); + handler(Key::Enter, &mut test_app); // 10_000 ms browsed lyric minus 500 ms timing offset - assert_eq!(app.song_progress_ms, 9_500); + assert_eq!(test_app.song_progress_ms, 9_500); // Manual browsing should be cleared (auto-follow resumes) - assert_eq!(app.view.lyrics_view.manual_index, None); + assert_eq!(test_app.view.lyrics_view.manual_index, None); } #[test] fn enter_without_browsing_seeks_to_active_lyric() { - let mut app = app_with_sample_lyrics(); - app.song_progress_ms = 6_000; - assert_eq!(app.view.lyrics_view.manual_index, None); + let mut test_app = app_with_sample_lyrics(); + test_app.song_progress_ms = 6_000; + assert_eq!(test_app.view.lyrics_view.manual_index, None); - handler(Key::Enter, &mut app); + handler(Key::Enter, &mut test_app); // Active line at 6_000 ms is Line 1 (5_000 ms) - assert_eq!(app.song_progress_ms, 5_000); - assert_eq!(app.view.lyrics_view.manual_index, None); + assert_eq!(test_app.song_progress_ms, 5_000); + assert_eq!(test_app.view.lyrics_view.manual_index, None); } #[test] fn enter_does_nothing_when_lyrics_unsynced() { - let mut app = App::default(); - app.current_playback_context = Some(playing_context()); - app.song_progress_ms = 1_000; - app.set_lyrics( + let mut test_app = App::default(); + test_app.current_playback_context = Some(playing_context()); + test_app.song_progress_ms = 1_000; + test_app.set_lyrics( LyricsStatus::Found, Some(vec![(0, "Unsynced text".to_string())]), false, ); - app.view.lyrics_view.manual_index = Some(0); + test_app.view.lyrics_view.manual_index = Some(0); - handler(Key::Enter, &mut app); + handler(Key::Enter, &mut test_app); - assert_eq!(app.song_progress_ms, 1_000); - assert_eq!(app.view.lyrics_view.manual_index, Some(0)); + assert_eq!(test_app.song_progress_ms, 1_000); + assert_eq!(test_app.view.lyrics_view.manual_index, Some(0)); } #[test] fn enter_does_nothing_when_lyrics_not_found() { - let mut app = App::default(); - app.current_playback_context = Some(playing_context()); - app.song_progress_ms = 1_000; - app.set_lyrics(LyricsStatus::NotFound, None, false); + let mut test_app = App::default(); + test_app.current_playback_context = Some(playing_context()); + test_app.song_progress_ms = 1_000; + test_app.set_lyrics(LyricsStatus::NotFound, None, false); - handler(Key::Enter, &mut app); + handler(Key::Enter, &mut test_app); - assert_eq!(app.song_progress_ms, 1_000); + assert_eq!(test_app.song_progress_ms, 1_000); } } From 6b16a45510ff5ef01102e774d24a5a1be9ab822c Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 19:53:14 +0300 Subject: [PATCH 06/10] fix(core): clamp seek_to strictly below track duration to prevent next track trigger --- src/core/app/seek.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/app/seek.rs b/src/core/app/seek.rs index 2a0a8c63..7e38733e 100644 --- a/src/core/app/seek.rs +++ b/src/core/app/seek.rs @@ -206,7 +206,11 @@ impl App { _ => return, }; - let new_progress = position_ms.min(duration_ms); + // Clamp to strictly less than track duration so an absolute seek (such as + // from lyrics selection or playbar scrubbing) cannot overshoot and trigger + // `NextTrack` in `apply_seek`. + let max_seek = duration_ms.saturating_sub(1); + let new_progress = position_ms.min(max_seek); self.seek_ms = Some(new_progress as u128); // Use native streaming player for instant control (bypasses event channel latency) @@ -457,4 +461,23 @@ mod tests { assert!(matches!(rx.try_recv(), Ok(IoEvent::NextTrack))); } + + #[test] + fn seek_to_clamps_below_track_duration_to_prevent_next_track() { + let (tx, rx) = std::sync::mpsc::channel(); + let mut app = App::new(tx, UserConfig::new(), Some(SystemTime::now())); + app.current_playback_context = Some(playing_track_context(full_track("ctx", "Context"))); + + // Duration of full_track is 180_000 ms. Seek far past duration. + app.seek_to(300_000); + + // Flushes pending API seek + app.flush_pending_api_seek(); + + match rx.try_recv() { + Ok(IoEvent::Seek(ms)) => assert_eq!(ms, 179_999), + Ok(IoEvent::NextTrack) => panic!("seek_to triggered NextTrack!"), + _other => panic!("expected Seek(179_999)"), + } + } } From c86653a7e1c243abef38fcf1ba07fa292f092458 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Tue, 3 Nov 2026 20:07:57 +0300 Subject: [PATCH 07/10] refactor(core): use playing_duration_ms in seek_to and ratchet coupling counter --- src/core/app/seek.rs | 65 +++++++++++++++------------------ src/tui/handlers/lyrics_view.rs | 2 +- tools/gates.count | 2 +- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/core/app/seek.rs b/src/core/app/seek.rs index 7e38733e..a6573279 100644 --- a/src/core/app/seek.rs +++ b/src/core/app/seek.rs @@ -196,48 +196,41 @@ impl App { } return; } - if let Some(CurrentPlaybackContext { - item: Some(item), .. - }) = &self.current_playback_context - { - let duration_ms = match item { - PlayableItem::Track(track) => track.duration.num_milliseconds() as u32, - PlayableItem::Episode(episode) => episode.duration.num_milliseconds() as u32, - _ => return, - }; + let Some(duration_ms) = self.playing_duration_ms() else { + return; + }; - // Clamp to strictly less than track duration so an absolute seek (such as - // from lyrics selection or playbar scrubbing) cannot overshoot and trigger - // `NextTrack` in `apply_seek`. - let max_seek = duration_ms.saturating_sub(1); - let new_progress = position_ms.min(max_seek); - self.seek_ms = Some(new_progress as u128); + // Clamp to strictly less than track duration so an absolute seek (such as + // from lyrics selection or playbar scrubbing) cannot overshoot and trigger + // `NextTrack` in `apply_seek`. + let max_seek = duration_ms.saturating_sub(1); + let new_progress = position_ms.min(max_seek); + self.seek_ms = Some(new_progress as u128); - // Use native streaming player for instant control (bypasses event channel latency) - #[cfg(feature = "streaming")] - if self.is_native_streaming_active_for_playback() && self.streaming_player.is_some() { - // Always update UI immediately - self.song_progress_ms = new_progress as u128; - self.seek_ms = None; + // Use native streaming player for instant control (bypasses event channel latency) + #[cfg(feature = "streaming")] + if self.is_native_streaming_active_for_playback() && self.streaming_player.is_some() { + // Always update UI immediately + self.song_progress_ms = new_progress as u128; + self.seek_ms = None; - // Throttle actual seeks to avoid overwhelming librespot (max ~20/sec) - const SEEK_THROTTLE_MS: u128 = 50; - let should_seek_now = self - .last_native_seek - .is_none_or(|t| t.elapsed().as_millis() >= SEEK_THROTTLE_MS); + // Throttle actual seeks to avoid overwhelming librespot (max ~20/sec) + const SEEK_THROTTLE_MS: u128 = 50; + let should_seek_now = self + .last_native_seek + .is_none_or(|t| t.elapsed().as_millis() >= SEEK_THROTTLE_MS); - if should_seek_now { - self.execute_native_seek(new_progress); - } else { - // Queue the seek - will be flushed by tick loop or next seek - self.pending_native_seek = Some(new_progress); - } - return; + if should_seek_now { + self.execute_native_seek(new_progress); + } else { + // Queue the seek - will be flushed by tick loop or next seek + self.pending_native_seek = Some(new_progress); } - - // Fallback: API-based seek for external devices (with throttling) - self.queue_api_seek(new_progress); + return; } + + // Fallback: API-based seek for external devices (with throttling) + self.queue_api_seek(new_progress); } /// Queue a decoded-source (local/Subsonic/YouTube) seek with throttling diff --git a/src/tui/handlers/lyrics_view.rs b/src/tui/handlers/lyrics_view.rs index 2e07165f..14996fb7 100644 --- a/src/tui/handlers/lyrics_view.rs +++ b/src/tui/handlers/lyrics_view.rs @@ -128,7 +128,7 @@ mod tests { } fn app_with_sample_lyrics() -> App { - let mut test_app = App::default(); + let mut test_app = App::default_connected(); test_app.current_playback_context = Some(playing_context()); test_app.set_lyrics( LyricsStatus::Found, diff --git a/tools/gates.count b/tools/gates.count index 22cca934..1c281f29 100644 --- a/tools/gates.count +++ b/tools/gates.count @@ -15,6 +15,6 @@ synthetic_keys_in_mouse_handler = 3 # target 0 (the content-table re-entries wildcard_arms_in_action_tree = 0 # target 0, must stay 0 view_writes_outside_tui = 12 # target 0 (producers outside tui/ and core/app/ writing App::view) pub_fields_on_app = 111 # target 1 (App.view stays public for the frontend; the rest go through App methods) -direct_playback_context_reads = 87 # target 0 (readers of App::current_playback_context outside the ownership resolver and the snapshot builder, which are excluded) +direct_playback_context_reads = 86 # target 0 (readers of App::current_playback_context outside the ownership resolver and the snapshot builder, which are excluded) action_refs_in_tui_handlers = 199 # adoption: may only rise test_attribute_total = 3324 # adoption: may only rise From e93e17cdb4e4f8a3b0e353541f30aa3f817a1a56 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Wed, 4 Nov 2026 14:51:37 +0300 Subject: [PATCH 08/10] chore(tui): restore compact format of help_english fixture --- src/tui/help_english_5d66123b.json | 2495 ++-------------------------- 1 file changed, 133 insertions(+), 2362 deletions(-) diff --git a/src/tui/help_english_5d66123b.json b/src/tui/help_english_5d66123b.json index d7b9038a..4042775b 100644 --- a/src/tui/help_english_5d66123b.json +++ b/src/tui/help_english_5d66123b.json @@ -1,2367 +1,138 @@ { "entries": [ - [ - "Scroll down to next result page", - "", - "Pagination" - ], - [ - "Scroll up to previous result page", - "", - "Pagination" - ], - [ - "Jump to start of playlist", - "", - "Pagination" - ], - [ - "Jump to end of playlist", - "", - "Pagination" - ], - [ - "Jump to currently playing album", - "a", - "General" - ], - [ - "Jump to currently playing artist's album list", - "A", - "General" - ], - [ - "Jump to current play context", - "o", - "General" - ], - [ - "Increase volume by 10%", - "+", - "General" - ], - [ - "Decrease volume by 10%", - "-", - "General" - ], - [ - "Skip to next track", - "n", - "General" - ], - [ - "Skip to previous track", - "p", - "General" - ], - [ - "Force skip to previous track", - "P", - "General" - ], - [ - "Seek backwards 5 seconds", - "<", - "General" - ], - [ - "Seek forwards 5 seconds", - ">", - "General" - ], - [ - "Toggle shuffle", - "", - "General" - ], - [ - "Copy url to currently playing song/episode", - "c", - "General" - ], - [ - "Copy url to currently playing album/show", - "C", - "General" - ], - [ - "Cycle repeat mode", - "", - "General" - ], - [ - "Move selection left", - "h | | ", - "General" - ], - [ - "Move selection down", - "j | | ", - "General" - ], - [ - "Move selection up", - "k | ", - "General" - ], - [ - "Move selection right", - "l | | ", - "General (Ctrl+f searches inside playlist track tables)" - ], - [ - "Move selection to top of list", - "H", - "General" - ], - [ - "Move selection to middle of list", - "M", - "General" - ], - [ - "Move selection to bottom of list", - "L", - "General" - ], - [ - "Enter input for search", - "/", - "General" - ], - [ - "Filter help rows", - "/", - "Help menu" - ], - [ - "Filter settings rows", - "/", - "Settings" - ], - [ - "Search playlists by name", - "/", - "Add-to-playlist picker" - ], - [ - "Pause/Resume playback", - "", - "General" - ], - [ - "Enter active mode", - "", - "General" - ], - [ - "Go to audio analysis screen", - "v", - "General" - ], - [ - "Cycle visualizer style (in audio analysis)", - "V", - "General" - ], - [ - "Go to lyrics view", - "B", - "General" - ], - [ - "Scroll lyrics (pauses auto-follow)", - "k/j | / | ", - "Lyrics view" - ], - [ - "Resume following the current lyric line", - "f or ", - "Lyrics view" - ], - [ - "Nudge lyric timing earlier/later", - "l/h | / | /", - "Lyrics view" - ], - [ - "Seek to selected lyric line", - "", - "Lyrics view" - ], - [ - "Toggle miniplayer view", - "T", - "General" - ], - [ - "Go to cover art view", - "G", - "General" - ], - [ - "Go back or exit when nowhere left to back to", - "q", - "General" - ], - [ - "Switch music source / select playback device", - "d", - "General" - ], - [ - "Open settings", - "", - "General" - ], - [ - "Save settings", - "", - "Settings" - ], - [ - "Enter hover mode", - "", - "Selected block" - ], - [ - "Save track in list or table", - "s", - "Selected block" - ], - [ - "Add selected track to playlist", - "w", - "Track table / search songs / artist top tracks / recently played" - ], - [ - "Add currently playing track to playlist", - "w", - "Playbar" - ], - [ - "Quick-add currently playing track to playlist", - "W", - "Global" - ], - [ - "Decrease sidebar width", - "{", - "Layout" - ], - [ - "Increase sidebar width", - "}", - "Layout" - ], - [ - "Decrease playbar or library height", - "(", - "Layout" - ], - [ - "Increase playbar or library height", - ")", - "Layout" - ], - [ - "Reset layout to defaults", - "|", - "Layout" - ], - [ - "Detach the open full-screen view into a pane, or put the focused pane back to full screen", - "", - "Layout" - ], - [ - "Close the focused pane, or hide the main layout when it has focus", - "", - "Layout" - ], - [ - "Focus the pane on the left", - " | ", - "Layout" - ], - [ - "Focus the pane below", - " | ", - "Layout" - ], - [ - "Focus the pane above", - " | ", - "Layout" - ], - [ - "Focus the pane on the right", - " | ", - "Layout" - ], - [ - "Swap the focused pane with the pane on the left", - " | ", - "Layout" - ], - [ - "Swap the focused pane with the pane below", - " | ", - "Layout" - ], - [ - "Swap the focused pane with the pane above", - " | ", - "Layout" - ], - [ - "Swap the focused pane with the pane on the right", - " | ", - "Layout" - ], - [ - "Turn the split of the focused pane (side by side or stacked)", - "", - "Layout" - ], - [ - "Make the focused pane larger", - "", - "Layout" - ], - [ - "Make the focused pane smaller", - "", - "Layout" - ], - [ - "Close every pane and show the main layout again", - "", - "Layout" - ], - [ - "Remove selected track from current playlist", - "x", - "Track table (playlist views)" - ], - [ - "Search tracks in current playlist", - "", - "Track table (playlist views)" - ], - [ - "Clear playlist track search filter", - "q", - "Track table (filtered playlist views)" - ], - [ - "Start playback or enter album/artist/playlist", - "", - "Selected block" - ], - [ - "Play recommendations for song/artist", - "r", - "Selected block" - ], - [ - "Play all tracks for artist", - "e", - "Library -> Artists" - ], - [ - "Search with input text", - "", - "Search input" - ], - [ - "Move cursor one space left", - "", - "Search input" - ], - [ - "Move cursor one space right", - "", - "Search input" - ], - [ - "Delete entire input", - "", - "Search input" - ], - [ - "Delete text from cursor to start of input", - "", - "Search input" - ], - [ - "Delete text from cursor to end of input", - "", - "Search input" - ], - [ - "Delete previous word", - "", - "Search input" - ], - [ - "Jump to start of input", - "", - "Search input" - ], - [ - "Jump to end of input", - "", - "Search input" - ], - [ - "Escape from the input back to hovered block", - "", - "Search input" - ], - [ - "Delete saved album", - "D", - "Library -> Albums" - ], - [ - "Unfollow artist", - "D", - "Library -> Artists / Artist page -> Related artists / Search result" - ], - [ - "Remove saved show", - "D", - "Library -> Podcasts" - ], - [ - "Delete saved playlist", - "D", - "Playlist" - ], - [ - "Mirror playlist onto another source", - "m", - "Playlist" - ], - [ - "Remove favorite radio station", - "D", - "Radio" - ], - [ - "Follow an artist/playlist", - "w", - "Search result" - ], - [ - "Save (like) album to library", - "w", - "Search result / Album tracks / Artist page -> Albums" - ], - [ - "Play random song in playlist", - "S", - "Selected Playlist" - ], - [ - "Toggle sort order of podcast episodes", - "S", - "Selected Show" - ], - [ - "Follow show", - "s", - "Selected Show" - ], - [ - "Unfollow show", - "D", - "Selected Show" - ], - [ - "Add track to queue", - "z", - "Hovered over track" - ], - [ - "Show queue", - "Q", - "General" - ], - [ - "Remove selected track from queue", - "x", - "Queue" - ], - [ - "Move selected queue item down / up", - "J / K", - "Queue" - ], - [ - "Play selected queue item (skip ahead to it)", - "", - "Queue" - ], - [ - "Toggle saved state for currently playing track/episode", - "F", - "General" - ], - [ - "Favorite highlighted/playing radio station", - "F", - "Radio" - ], - [ - "Generate listening recap card (selected period on Stats, 30 days elsewhere)", - "R", - "General" - ], - [ - "Open Stats screen", - "Library sidebar", - "Stats" - ], - [ - "Cycle stats period", - "[ / ]", - "Stats" - ], - [ - "Play selected top track", - "", - "Stats" - ], - [ - "Open Playlist sync screen", - "Library sidebar", - "Playlist sync" - ], - [ - "Run every playlist-sync link now", - "s", - "Playlist sync" - ], - [ - "Remove selected link", - "D", - "Playlist sync" - ], - [ - "Open sort menu", - ",", - "Track/Album/Artist list" - ], - [ - "Open Listening Party menu", - "", - "General" - ], - [ - "Open the AI DJ screen", - "", - "AI DJ" - ], - [ - "Toggle DJ auto-queue (keep the queue topped up)", - "", - "AI DJ" - ], - [ - "Shift the vibe (drop the DJ's queued tracks and re-ask)", - "", - "AI DJ" - ], - [ - "Toggle \"only tracks I do not already have\"", - "", - "AI DJ" - ], - [ - "Choose which AI and model the DJ uses", - "", - "AI DJ" - ], - [ - "Press the selected setting's button", - "o", - "Settings" - ] + ["Scroll down to next result page", "", "Pagination"], + ["Scroll up to previous result page", "", "Pagination"], + ["Jump to start of playlist", "", "Pagination"], + ["Jump to end of playlist", "", "Pagination"], + ["Jump to currently playing album", "a", "General"], + ["Jump to currently playing artist's album list", "A", "General"], + ["Jump to current play context", "o", "General"], + ["Increase volume by 10%", "+", "General"], + ["Decrease volume by 10%", "-", "General"], + ["Skip to next track", "n", "General"], + ["Skip to previous track", "p", "General"], + ["Force skip to previous track", "P", "General"], + ["Seek backwards 5 seconds", "<", "General"], + ["Seek forwards 5 seconds", ">", "General"], + ["Toggle shuffle", "", "General"], + ["Copy url to currently playing song/episode", "c", "General"], + ["Copy url to currently playing album/show", "C", "General"], + ["Cycle repeat mode", "", "General"], + ["Move selection left", "h | | ", "General"], + ["Move selection down", "j | | ", "General"], + ["Move selection up", "k | ", "General"], + ["Move selection right", "l | | ", "General (Ctrl+f searches inside playlist track tables)"], + ["Move selection to top of list", "H", "General"], + ["Move selection to middle of list", "M", "General"], + ["Move selection to bottom of list", "L", "General"], + ["Enter input for search", "/", "General"], + ["Filter help rows", "/", "Help menu"], + ["Filter settings rows", "/", "Settings"], + ["Search playlists by name", "/", "Add-to-playlist picker"], + ["Pause/Resume playback", "", "General"], + ["Enter active mode", "", "General"], + ["Go to audio analysis screen", "v", "General"], + ["Cycle visualizer style (in audio analysis)", "V", "General"], + ["Go to lyrics view", "B", "General"], + ["Scroll lyrics (pauses auto-follow)", "k/j | / | ", "Lyrics view"], + ["Resume following the current lyric line", "f or ", "Lyrics view"], + ["Nudge lyric timing earlier/later", "l/h | / | /", "Lyrics view"], + ["Seek to selected lyric line", "", "Lyrics view"], + ["Toggle miniplayer view", "T", "General"], + ["Go to cover art view", "G", "General"], + ["Go back or exit when nowhere left to back to", "q", "General"], + ["Switch music source / select playback device", "d", "General"], + ["Open settings", "", "General"], + ["Save settings", "", "Settings"], + ["Enter hover mode", "", "Selected block"], + ["Save track in list or table", "s", "Selected block"], + ["Add selected track to playlist", "w", "Track table / search songs / artist top tracks / recently played"], + ["Add currently playing track to playlist", "w", "Playbar"], + ["Quick-add currently playing track to playlist", "W", "Global"], + ["Decrease sidebar width", "{", "Layout"], + ["Increase sidebar width", "}", "Layout"], + ["Decrease playbar or library height", "(", "Layout"], + ["Increase playbar or library height", ")", "Layout"], + ["Reset layout to defaults", "|", "Layout"], + ["Detach the open full-screen view into a pane, or put the focused pane back to full screen", "", "Layout"], + ["Close the focused pane, or hide the main layout when it has focus", "", "Layout"], + ["Focus the pane on the left", " | ", "Layout"], + ["Focus the pane below", " | ", "Layout"], + ["Focus the pane above", " | ", "Layout"], + ["Focus the pane on the right", " | ", "Layout"], + ["Swap the focused pane with the pane on the left", " | ", "Layout"], + ["Swap the focused pane with the pane below", " | ", "Layout"], + ["Swap the focused pane with the pane above", " | ", "Layout"], + ["Swap the focused pane with the pane on the right", " | ", "Layout"], + ["Turn the split of the focused pane (side by side or stacked)", "", "Layout"], + ["Make the focused pane larger", "", "Layout"], + ["Make the focused pane smaller", "", "Layout"], + ["Close every pane and show the main layout again", "", "Layout"], + ["Remove selected track from current playlist", "x", "Track table (playlist views)"], + ["Search tracks in current playlist", "", "Track table (playlist views)"], + ["Clear playlist track search filter", "q", "Track table (filtered playlist views)"], + ["Start playback or enter album/artist/playlist", "", "Selected block"], + ["Play recommendations for song/artist", "r", "Selected block"], + ["Play all tracks for artist", "e", "Library -> Artists"], + ["Search with input text", "", "Search input"], + ["Move cursor one space left", "", "Search input"], + ["Move cursor one space right", "", "Search input"], + ["Delete entire input", "", "Search input"], + ["Delete text from cursor to start of input", "", "Search input"], + ["Delete text from cursor to end of input", "", "Search input"], + ["Delete previous word", "", "Search input"], + ["Jump to start of input", "", "Search input"], + ["Jump to end of input", "", "Search input"], + ["Escape from the input back to hovered block", "", "Search input"], + ["Delete saved album", "D", "Library -> Albums"], + ["Unfollow artist", "D", "Library -> Artists / Artist page -> Related artists / Search result"], + ["Remove saved show", "D", "Library -> Podcasts"], + ["Delete saved playlist", "D", "Playlist"], + ["Mirror playlist onto another source", "m", "Playlist"], + ["Remove favorite radio station", "D", "Radio"], + ["Follow an artist/playlist", "w", "Search result"], + ["Save (like) album to library", "w", "Search result / Album tracks / Artist page -> Albums"], + ["Play random song in playlist", "S", "Selected Playlist"], + ["Toggle sort order of podcast episodes", "S", "Selected Show"], + ["Follow show", "s", "Selected Show"], + ["Unfollow show", "D", "Selected Show"], + ["Add track to queue", "z", "Hovered over track"], + ["Show queue", "Q", "General"], + ["Remove selected track from queue", "x", "Queue"], + ["Move selected queue item down / up", "J / K", "Queue"], + ["Play selected queue item (skip ahead to it)", "", "Queue"], + ["Toggle saved state for currently playing track/episode", "F", "General"], + ["Favorite highlighted/playing radio station", "F", "Radio"], + ["Generate listening recap card (selected period on Stats, 30 days elsewhere)", "R", "General"], + ["Open Stats screen", "Library sidebar", "Stats"], + ["Cycle stats period", "[ / ]", "Stats"], + ["Play selected top track", "", "Stats"], + ["Open Playlist sync screen", "Library sidebar", "Playlist sync"], + ["Run every playlist-sync link now", "s", "Playlist sync"], + ["Remove selected link", "D", "Playlist sync"], + ["Open sort menu", ",", "Track/Album/Artist list"], + ["Open Listening Party menu", "", "General"], + ["Open the AI DJ screen", "", "AI DJ"], + ["Toggle DJ auto-queue (keep the queue topped up)", "", "AI DJ"], + ["Shift the vibe (drop the DJ's queued tracks and re-ask)", "", "AI DJ"], + ["Toggle \"only tracks I do not already have\"", "", "AI DJ"], + ["Choose which AI and model the DJ uses", "", "AI DJ"], + ["Press the selected setting's button", "o", "Settings"] ], "views": [ - [ - "Spotify", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - [ - 25, - "Enter input for search (needs Spotify)" - ], - 26, - 27, - [ - 28, - "Search playlists by name (needs Spotify)" - ], - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (needs Spotify)" - ], - 71, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (needs Spotify)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Local", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - [ - 25, - "Enter input for search (not for Local Files)" - ], - 26, - 27, - [ - 28, - "Search playlists by name (not for Local Files)" - ], - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Local Files)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Subsonic", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - [ - 28, - "Search playlists by name (not for Subsonic)" - ], - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 88, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Subsonic)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Radio", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - [ - 28, - "Search playlists by name (not for Internet Radio)" - ], - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 89, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Internet Radio)" - ], - 102, - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "YouTube", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 46, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - 68, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 87, - 88, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for YouTube)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Qobuz", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - [ - 28, - "Search playlists by name (not for Qobuz)" - ], - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 88, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Qobuz)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "AppleMusic", - false, - [ - 0, - 1, - 2, - 3, - [ - 4, - "Jump to currently playing album (needs Spotify)" - ], - [ - 5, - "Jump to currently playing artist's album list (needs Spotify)" - ], - [ - 6, - "Jump to current play context (needs Spotify)" - ], - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - [ - 15, - "Copy url to currently playing song/episode (needs Spotify)" - ], - [ - 16, - "Copy url to currently playing album/show (needs Spotify)" - ], - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - [ - 28, - "Search playlists by name (not for Apple Music)" - ], - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Apple Music)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 107, - 108, - 109, - 110, - [ - 111, - "Open Listening Party menu (needs Spotify)" - ], - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Spotify", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 84, - 85, - 86, - 87, - 88, - 90, - 91, - 92, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Local", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - [ - 25, - "Enter input for search (not for Local Files)" - ], - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Local Files)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Subsonic", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 88, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Subsonic)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Radio", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 89, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Internet Radio)" - ], - 102, - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "YouTube", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 46, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - 68, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 87, - 88, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for YouTube)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "Qobuz", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 88, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Qobuz)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ], - [ - "AppleMusic", - true, - [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, - 63, - 64, - 65, - 66, - 67, - [ - 70, - "Clear playlist track search filter (Spotify only)" - ], - 71, - 74, - 75, - 76, - 77, - 78, - 79, - 80, - 81, - 82, - 83, - 92, - 96, - 97, - 98, - 99, - 100, - [ - 101, - "Toggle saved state for currently playing track/episode (not for Apple Music)" - ], - [ - 102, - "Favorite highlighted/playing radio station (Internet Radio only)" - ], - 103, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - ] + ["Spotify", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, [25, "Enter input for search (needs Spotify)"], 26, 27, [28, "Search playlists by name (needs Spotify)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (needs Spotify)"], 71, 75, 76, 77, 78, 79, 80, 81, 82, 83, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (needs Spotify)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["Local", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, [25, "Enter input for search (not for Local Files)"], 26, 27, [28, "Search playlists by name (not for Local Files)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 75, 76, 77, 78, 79, 80, 81, 82, 83, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Local Files)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["Subsonic", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Subsonic)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 88, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Subsonic)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["Radio", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Internet Radio)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 89, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Internet Radio)"], 102, 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["YouTube", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 87, 88, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for YouTube)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["Qobuz", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Qobuz)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 88, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Qobuz)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["AppleMusic", false, [0, 1, 2, 3, [4, "Jump to currently playing album (needs Spotify)"], [5, "Jump to currently playing artist's album list (needs Spotify)"], [6, "Jump to current play context (needs Spotify)"], 7, 8, 9, 10, 11, 12, 13, 14, [15, "Copy url to currently playing song/episode (needs Spotify)"], [16, "Copy url to currently playing album/show (needs Spotify)"], 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, [28, "Search playlists by name (not for Apple Music)"], 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Apple Music)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 107, 108, 109, 110, [111, "Open Listening Party menu (needs Spotify)"], 112, 113, 114, 115, 116, 117]], + ["Spotify", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]], + ["Local", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, [25, "Enter input for search (not for Local Files)"], 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 75, 76, 77, 78, 79, 80, 81, 82, 83, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Local Files)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]], + ["Subsonic", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 88, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Subsonic)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]], + ["Radio", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 89, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Internet Radio)"], 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]], + ["YouTube", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 87, 88, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for YouTube)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]], + ["Qobuz", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 88, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Qobuz)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]], + ["AppleMusic", true, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, [70, "Clear playlist track search filter (Spotify only)"], 71, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 92, 96, 97, 98, 99, 100, [101, "Toggle saved state for currently playing track/episode (not for Apple Music)"], [102, "Favorite highlighted/playing radio station (Internet Radio only)"], 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117]] ] -} \ No newline at end of file +} From dfb573af564fce46378a55be07e6d4aa50fe21b0 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Wed, 4 Nov 2026 15:39:30 +0300 Subject: [PATCH 09/10] revert: drop seek.rs changes and restore gates ratchet counter --- src/core/app/seek.rs | 80 ++++++++++++++++++-------------------------- tools/gates.count | 2 +- 2 files changed, 33 insertions(+), 49 deletions(-) diff --git a/src/core/app/seek.rs b/src/core/app/seek.rs index a6573279..2a0a8c63 100644 --- a/src/core/app/seek.rs +++ b/src/core/app/seek.rs @@ -196,41 +196,44 @@ impl App { } return; } - let Some(duration_ms) = self.playing_duration_ms() else { - return; - }; + if let Some(CurrentPlaybackContext { + item: Some(item), .. + }) = &self.current_playback_context + { + let duration_ms = match item { + PlayableItem::Track(track) => track.duration.num_milliseconds() as u32, + PlayableItem::Episode(episode) => episode.duration.num_milliseconds() as u32, + _ => return, + }; - // Clamp to strictly less than track duration so an absolute seek (such as - // from lyrics selection or playbar scrubbing) cannot overshoot and trigger - // `NextTrack` in `apply_seek`. - let max_seek = duration_ms.saturating_sub(1); - let new_progress = position_ms.min(max_seek); - self.seek_ms = Some(new_progress as u128); + let new_progress = position_ms.min(duration_ms); + self.seek_ms = Some(new_progress as u128); - // Use native streaming player for instant control (bypasses event channel latency) - #[cfg(feature = "streaming")] - if self.is_native_streaming_active_for_playback() && self.streaming_player.is_some() { - // Always update UI immediately - self.song_progress_ms = new_progress as u128; - self.seek_ms = None; + // Use native streaming player for instant control (bypasses event channel latency) + #[cfg(feature = "streaming")] + if self.is_native_streaming_active_for_playback() && self.streaming_player.is_some() { + // Always update UI immediately + self.song_progress_ms = new_progress as u128; + self.seek_ms = None; - // Throttle actual seeks to avoid overwhelming librespot (max ~20/sec) - const SEEK_THROTTLE_MS: u128 = 50; - let should_seek_now = self - .last_native_seek - .is_none_or(|t| t.elapsed().as_millis() >= SEEK_THROTTLE_MS); + // Throttle actual seeks to avoid overwhelming librespot (max ~20/sec) + const SEEK_THROTTLE_MS: u128 = 50; + let should_seek_now = self + .last_native_seek + .is_none_or(|t| t.elapsed().as_millis() >= SEEK_THROTTLE_MS); - if should_seek_now { - self.execute_native_seek(new_progress); - } else { - // Queue the seek - will be flushed by tick loop or next seek - self.pending_native_seek = Some(new_progress); + if should_seek_now { + self.execute_native_seek(new_progress); + } else { + // Queue the seek - will be flushed by tick loop or next seek + self.pending_native_seek = Some(new_progress); + } + return; } - return; - } - // Fallback: API-based seek for external devices (with throttling) - self.queue_api_seek(new_progress); + // Fallback: API-based seek for external devices (with throttling) + self.queue_api_seek(new_progress); + } } /// Queue a decoded-source (local/Subsonic/YouTube) seek with throttling @@ -454,23 +457,4 @@ mod tests { assert!(matches!(rx.try_recv(), Ok(IoEvent::NextTrack))); } - - #[test] - fn seek_to_clamps_below_track_duration_to_prevent_next_track() { - let (tx, rx) = std::sync::mpsc::channel(); - let mut app = App::new(tx, UserConfig::new(), Some(SystemTime::now())); - app.current_playback_context = Some(playing_track_context(full_track("ctx", "Context"))); - - // Duration of full_track is 180_000 ms. Seek far past duration. - app.seek_to(300_000); - - // Flushes pending API seek - app.flush_pending_api_seek(); - - match rx.try_recv() { - Ok(IoEvent::Seek(ms)) => assert_eq!(ms, 179_999), - Ok(IoEvent::NextTrack) => panic!("seek_to triggered NextTrack!"), - _other => panic!("expected Seek(179_999)"), - } - } } diff --git a/tools/gates.count b/tools/gates.count index 1c281f29..22cca934 100644 --- a/tools/gates.count +++ b/tools/gates.count @@ -15,6 +15,6 @@ synthetic_keys_in_mouse_handler = 3 # target 0 (the content-table re-entries wildcard_arms_in_action_tree = 0 # target 0, must stay 0 view_writes_outside_tui = 12 # target 0 (producers outside tui/ and core/app/ writing App::view) pub_fields_on_app = 111 # target 1 (App.view stays public for the frontend; the rest go through App methods) -direct_playback_context_reads = 86 # target 0 (readers of App::current_playback_context outside the ownership resolver and the snapshot builder, which are excluded) +direct_playback_context_reads = 87 # target 0 (readers of App::current_playback_context outside the ownership resolver and the snapshot builder, which are excluded) action_refs_in_tui_handlers = 199 # adoption: may only rise test_attribute_total = 3324 # adoption: may only rise From 9dfdb3318691d100a48cb57358cbbab5f526b069 Mon Sep 17 00:00:00 2001 From: everyoneexe Date: Wed, 4 Nov 2026 19:20:05 +0300 Subject: [PATCH 10/10] test(tui): revert test_app rename and seed test state via app builder methods --- src/core/app/construction.rs | 7 ++++ src/tui/handlers/lyrics_view.rs | 60 ++++++++++++++++----------------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/core/app/construction.rs b/src/core/app/construction.rs index c1122303..fc40e0d7 100644 --- a/src/core/app/construction.rs +++ b/src/core/app/construction.rs @@ -310,6 +310,13 @@ impl App { self } + /// This app with song progress seeded, for tests. + #[cfg(all(test, feature = "tui"))] + pub(crate) fn with_song_progress(mut self, progress_ms: u128) -> App { + self.song_progress_ms = progress_ms; + self + } + /// Mark the native backend parked with no player, for tests. #[cfg(all(test, feature = "streaming"))] pub(crate) fn seed_native_parked(&mut self) { diff --git a/src/tui/handlers/lyrics_view.rs b/src/tui/handlers/lyrics_view.rs index 14996fb7..656c3356 100644 --- a/src/tui/handlers/lyrics_view.rs +++ b/src/tui/handlers/lyrics_view.rs @@ -128,9 +128,8 @@ mod tests { } fn app_with_sample_lyrics() -> App { - let mut test_app = App::default_connected(); - test_app.current_playback_context = Some(playing_context()); - test_app.set_lyrics( + let mut app = App::default_connected().with_playback(playing_context()); + app.set_lyrics( LyricsStatus::Found, Some(vec![ (0, "Line 0".to_string()), @@ -139,65 +138,64 @@ mod tests { ]), true, ); - test_app + app } #[test] fn enter_seeks_to_browsed_lyric_and_resumes_follow() { - let mut test_app = app_with_sample_lyrics(); - test_app.view.lyrics_view.timing_offset_ms = 500; + let mut app = app_with_sample_lyrics(); + app.view.lyrics_view.timing_offset_ms = 500; // Browse to Line 2 (at 10_000 ms) - scroll_by(&mut test_app, 2); - assert_eq!(test_app.view.lyrics_view.manual_index, Some(2)); + scroll_by(&mut app, 2); + assert_eq!(app.view.lyrics_view.manual_index, Some(2)); - handler(Key::Enter, &mut test_app); + handler(Key::Enter, &mut app); // 10_000 ms browsed lyric minus 500 ms timing offset - assert_eq!(test_app.song_progress_ms, 9_500); + assert_eq!(app.song_progress_ms, 9_500); // Manual browsing should be cleared (auto-follow resumes) - assert_eq!(test_app.view.lyrics_view.manual_index, None); + assert_eq!(app.view.lyrics_view.manual_index, None); } #[test] fn enter_without_browsing_seeks_to_active_lyric() { - let mut test_app = app_with_sample_lyrics(); - test_app.song_progress_ms = 6_000; - assert_eq!(test_app.view.lyrics_view.manual_index, None); + let mut app = app_with_sample_lyrics().with_song_progress(6_000); + assert_eq!(app.view.lyrics_view.manual_index, None); - handler(Key::Enter, &mut test_app); + handler(Key::Enter, &mut app); // Active line at 6_000 ms is Line 1 (5_000 ms) - assert_eq!(test_app.song_progress_ms, 5_000); - assert_eq!(test_app.view.lyrics_view.manual_index, None); + assert_eq!(app.song_progress_ms, 5_000); + assert_eq!(app.view.lyrics_view.manual_index, None); } #[test] fn enter_does_nothing_when_lyrics_unsynced() { - let mut test_app = App::default(); - test_app.current_playback_context = Some(playing_context()); - test_app.song_progress_ms = 1_000; - test_app.set_lyrics( + let mut app = App::default() + .with_playback(playing_context()) + .with_song_progress(1_000); + app.set_lyrics( LyricsStatus::Found, Some(vec![(0, "Unsynced text".to_string())]), false, ); - test_app.view.lyrics_view.manual_index = Some(0); + app.view.lyrics_view.manual_index = Some(0); - handler(Key::Enter, &mut test_app); + handler(Key::Enter, &mut app); - assert_eq!(test_app.song_progress_ms, 1_000); - assert_eq!(test_app.view.lyrics_view.manual_index, Some(0)); + assert_eq!(app.song_progress_ms, 1_000); + assert_eq!(app.view.lyrics_view.manual_index, Some(0)); } #[test] fn enter_does_nothing_when_lyrics_not_found() { - let mut test_app = App::default(); - test_app.current_playback_context = Some(playing_context()); - test_app.song_progress_ms = 1_000; - test_app.set_lyrics(LyricsStatus::NotFound, None, false); + let mut app = App::default() + .with_playback(playing_context()) + .with_song_progress(1_000); + app.set_lyrics(LyricsStatus::NotFound, None, false); - handler(Key::Enter, &mut test_app); + handler(Key::Enter, &mut app); - assert_eq!(test_app.song_progress_ms, 1_000); + assert_eq!(app.song_progress_ms, 1_000); } }