@involvex/youtube-music-cli 0.0.46 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@ import { ICONS } from "../../utils/icons.js";
10
10
  const FLASH_DURATION_MS = 300;
11
11
  export default function ShortcutsBar() {
12
12
  const { theme } = useTheme();
13
- const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, } = usePlayer();
13
+ const { state: playerState, pause, resume, next, previous, volumeUp, volumeDown, volumeFineUp, volumeFineDown, toggleShuffle, toggleRepeat, toggleAutoplay, } = usePlayer();
14
14
  const [flashState, setFlashState] = useState({});
15
15
  const flash = (key) => {
16
16
  setFlashState(prev => ({ ...prev, [key]: true }));
@@ -62,6 +62,10 @@ export default function ShortcutsBar() {
62
62
  flash('repeat');
63
63
  toggleRepeat();
64
64
  });
65
+ useKeyBinding(KEYBINDINGS.AUTOPLAY_TOGGLE, () => {
66
+ flash('autoplay');
67
+ toggleAutoplay();
68
+ });
65
69
  // Note: SETTINGS keybinding handled by MainLayout to avoid double-dispatch
66
70
  const shuffleColor = flashState['shuffle']
67
71
  ? theme.colors.success
@@ -76,5 +80,10 @@ export default function ShortcutsBar() {
76
80
  const volumeColor = flashState['volume']
77
81
  ? theme.colors.success
78
82
  : theme.colors.primary;
79
- return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: shortcutColor('playPause'), children: [playerState.isPlaying ? ICONS.PAUSE : ICONS.PLAY_PAUSE_ON, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: shortcutColor('prev'), children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: shortcutColor('next'), children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: shuffleColor, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: repeatColor, children: [playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL, ' ', "[R]"] }), ' ', "| ", _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: shuffleColor, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: repeatColor, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [+/-]"] }), ' ', _jsxs(Text, { color: volumeColor, children: [playerState.volume, "%"] })] })] }));
83
+ const autoplayColor = flashState['autoplay']
84
+ ? theme.colors.success
85
+ : playerState.autoplay
86
+ ? theme.colors.primary
87
+ : theme.colors.dim;
88
+ return (_jsxs(Box, { borderStyle: "single", borderColor: theme.colors.dim, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.colors.dim, children: [_jsxs(Text, { color: shortcutColor('playPause'), children: [playerState.isPlaying ? ICONS.PAUSE : ICONS.PLAY_PAUSE_ON, " [Space]"] }), ' ', "| ", _jsxs(Text, { color: shortcutColor('prev'), children: [ICONS.PREV, " [B/\u2190]"] }), " |", ' ', _jsxs(Text, { color: shortcutColor('next'), children: [ICONS.NEXT, " [N/\u2192]"] }), " |", ' ', _jsxs(Text, { color: shuffleColor, children: [ICONS.SHUFFLE, " [Shift+S]"] }), " |", ' ', _jsxs(Text, { color: repeatColor, children: [playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL, ' ', "[R]"] }), ' ', "| ", _jsxs(Text, { color: autoplayColor, children: [ICONS.AUTOPLAY, " [Shift+A]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.PLAYLIST, " [Shift+P]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.DOWNLOAD, " [Shift+D]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.SEARCH, " [/]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.HELP, " [?]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.BG_PLAY, " [Shift+Q]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.RESUME, " [Shift+R]"] }), " |", ' ', _jsxs(Text, { color: theme.colors.text, children: [ICONS.QUIT, " [Q]"] })] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: shuffleColor, children: ICONS.SHUFFLE }), ' ', _jsx(Text, { color: repeatColor, children: playerState.repeat === 'one' ? ICONS.REPEAT_ONE : ICONS.REPEAT_ALL }), ' ', _jsx(Text, { color: autoplayColor, children: ICONS.AUTOPLAY }), ' ', _jsxs(Text, { color: theme.colors.dim, children: [ICONS.VOLUME, " [+/-]"] }), ' ', _jsxs(Text, { color: volumeColor, children: [playerState.volume, "%"] })] })] }));
80
89
  }
@@ -17,6 +17,7 @@ export declare function usePlayer(): {
17
17
  volumeFineDown: () => void;
18
18
  toggleShuffle: () => void;
19
19
  toggleRepeat: () => void;
20
+ toggleAutoplay: () => void;
20
21
  setQueue: (queue: Track[]) => void;
21
22
  addToQueue: (track: Track) => void;
22
23
  removeFromQueue: (index: number) => void;
@@ -8,6 +8,7 @@ export interface PersistedPlayerState {
8
8
  volume: number;
9
9
  shuffle: boolean;
10
10
  repeat: 'off' | 'all' | 'one';
11
+ autoplay?: boolean;
11
12
  lastUpdated: string;
12
13
  }
13
14
  /**
@@ -15,6 +15,7 @@ const defaultState = {
15
15
  volume: 70,
16
16
  shuffle: false,
17
17
  repeat: 'off',
18
+ autoplay: true,
18
19
  lastUpdated: new Date().toISOString(),
19
20
  };
20
21
  /**
@@ -21,6 +21,7 @@ class WebServerManager {
21
21
  queuePosition: 0,
22
22
  repeat: 'off',
23
23
  shuffle: false,
24
+ autoplay: true,
24
25
  isLoading: false,
25
26
  error: null,
26
27
  playRequestId: 0,
@@ -164,6 +164,7 @@ class WebStreamingService {
164
164
  'queuePosition',
165
165
  'repeat',
166
166
  'shuffle',
167
+ 'autoplay',
167
168
  'isLoading',
168
169
  'error',
169
170
  ];
@@ -186,9 +187,10 @@ class WebStreamingService {
186
187
  * Update and broadcast player state (throttled)
187
188
  */
188
189
  onStateChange(state) {
189
- this.prevState = { ...state };
190
190
  const now = Date.now();
191
+ // Compute delta against OLD prevState BEFORE updating it
191
192
  const delta = this.computeDelta(state);
193
+ this.prevState = { ...state };
192
194
  // Skip if no changes
193
195
  if (Object.keys(delta).length === 0) {
194
196
  return;
@@ -18,6 +18,7 @@ type PlayerContextValue = {
18
18
  volumeFineDown: () => void;
19
19
  toggleShuffle: () => void;
20
20
  toggleRepeat: () => void;
21
+ toggleAutoplay: () => void;
21
22
  setQueue: (queue: Track[]) => void;
22
23
  addToQueue: (track: Track) => void;
23
24
  removeFromQueue: (index: number) => void;
@@ -21,6 +21,7 @@ const initialState = {
21
21
  queuePosition: 0,
22
22
  repeat: 'off',
23
23
  shuffle: false,
24
+ autoplay: true,
24
25
  isLoading: false,
25
26
  error: null,
26
27
  playRequestId: 0,
@@ -150,6 +151,8 @@ export function playerReducer(state, action) {
150
151
  }
151
152
  case 'TOGGLE_SHUFFLE':
152
153
  return { ...state, shuffle: !state.shuffle };
154
+ case 'TOGGLE_AUTOPLAY':
155
+ return { ...state, autoplay: !state.autoplay };
153
156
  case 'TOGGLE_REPEAT':
154
157
  const repeatModes = ['off', 'all', 'one'];
155
158
  const currentIndex = repeatModes.indexOf(state.repeat);
@@ -225,6 +228,7 @@ export function playerReducer(state, action) {
225
228
  volume: action.volume,
226
229
  shuffle: action.shuffle,
227
230
  repeat: action.repeat,
231
+ autoplay: action.autoplay ?? true,
228
232
  isPlaying: false, // Don't auto-play restored state
229
233
  };
230
234
  default:
@@ -547,6 +551,62 @@ function PlayerManager() {
547
551
  state.shuffle,
548
552
  dispatch,
549
553
  ]);
554
+ // Smart autoplay: fetch suggestions when near end of queue
555
+ const fetchedForRef = useRef(null);
556
+ const isFetchingAutoplayRef = useRef(false);
557
+ useEffect(() => {
558
+ if (!state.autoplay || !state.currentTrack || !state.isPlaying) {
559
+ return;
560
+ }
561
+ // Already looping — no need to feed the queue
562
+ if (state.repeat === 'all' || (state.shuffle && state.queue.length > 1)) {
563
+ return;
564
+ }
565
+ // Still enough tracks ahead — wait until we're close to the end
566
+ const tracksAhead = state.queue.length - state.queuePosition - 1;
567
+ if (tracksAhead > 5)
568
+ return;
569
+ // Already fetched for this track or a fetch is in flight
570
+ if (fetchedForRef.current === state.currentTrack.videoId ||
571
+ isFetchingAutoplayRef.current) {
572
+ return;
573
+ }
574
+ const trackId = state.currentTrack.videoId;
575
+ const trackTitle = state.currentTrack.title;
576
+ isFetchingAutoplayRef.current = true;
577
+ fetchedForRef.current = trackId;
578
+ musicService
579
+ .getSuggestions(trackId)
580
+ .then(suggestions => {
581
+ for (const track of suggestions) {
582
+ dispatch({ category: 'ADD_TO_QUEUE', track });
583
+ }
584
+ logger.info('PlayerManager', 'Autoplay: added suggestions', {
585
+ count: suggestions.length,
586
+ basedOn: trackTitle,
587
+ });
588
+ })
589
+ .catch((error) => {
590
+ isFetchingAutoplayRef.current = false;
591
+ fetchedForRef.current = null; // Allow retry on next track change
592
+ logger.warn('PlayerManager', 'Autoplay: failed to fetch suggestions', {
593
+ error: error instanceof Error ? error.message : String(error),
594
+ });
595
+ })
596
+ .finally(() => {
597
+ isFetchingAutoplayRef.current = false;
598
+ });
599
+ }, [
600
+ state.autoplay,
601
+ state.currentTrack,
602
+ state.isPlaying,
603
+ state.repeat,
604
+ state.shuffle,
605
+ state.queue.length,
606
+ state.queuePosition,
607
+ musicService,
608
+ dispatch,
609
+ ]);
550
610
  return null;
551
611
  }
552
612
  export function PlayerProvider({ children }) {
@@ -574,6 +634,7 @@ export function PlayerProvider({ children }) {
574
634
  volume: persistedState.volume,
575
635
  shuffle: persistedState.shuffle,
576
636
  repeat: persistedState.repeat,
637
+ autoplay: persistedState.autoplay ?? true,
577
638
  });
578
639
  }
579
640
  });
@@ -596,6 +657,7 @@ export function PlayerProvider({ children }) {
596
657
  volume: state.volume,
597
658
  shuffle: state.shuffle,
598
659
  repeat: state.repeat,
660
+ autoplay: state.autoplay,
599
661
  });
600
662
  },
601
663
  // Debounce progress updates (5s), immediate for track/queue changes
@@ -613,6 +675,7 @@ export function PlayerProvider({ children }) {
613
675
  state.volume,
614
676
  state.shuffle,
615
677
  state.repeat,
678
+ state.autoplay,
616
679
  ]);
617
680
  // Save immediately on unmount/quit
618
681
  useEffect(() => {
@@ -627,6 +690,7 @@ export function PlayerProvider({ children }) {
627
690
  volume: currentState.volume,
628
691
  shuffle: currentState.shuffle,
629
692
  repeat: currentState.repeat,
693
+ autoplay: currentState.autoplay,
630
694
  });
631
695
  };
632
696
  process.on('beforeExit', handleExit);
@@ -695,6 +759,7 @@ export function PlayerProvider({ children }) {
695
759
  },
696
760
  toggleShuffle: () => dispatch({ category: 'TOGGLE_SHUFFLE' }),
697
761
  toggleRepeat: () => dispatch({ category: 'TOGGLE_REPEAT' }),
762
+ toggleAutoplay: () => dispatch({ category: 'TOGGLE_AUTOPLAY' }),
698
763
  setQueue: (queue) => dispatch({ category: 'SET_QUEUE', queue }),
699
764
  addToQueue: (track) => dispatch({ category: 'ADD_TO_QUEUE', track }),
700
765
  removeFromQueue: (index) => dispatch({ category: 'REMOVE_FROM_QUEUE', index }),
@@ -44,6 +44,9 @@ export interface ToggleShuffleAction {
44
44
  export interface ToggleRepeatAction {
45
45
  readonly category: 'TOGGLE_REPEAT';
46
46
  }
47
+ export interface ToggleAutoplayAction {
48
+ readonly category: 'TOGGLE_AUTOPLAY';
49
+ }
47
50
  export interface SetQueueAction {
48
51
  readonly category: 'SET_QUEUE';
49
52
  queue: Track[];
@@ -91,6 +94,7 @@ export interface RestoreStateAction {
91
94
  volume: number;
92
95
  shuffle: boolean;
93
96
  repeat: 'off' | 'all' | 'one';
97
+ autoplay?: boolean;
94
98
  }
95
99
  export interface SetSpeedAction {
96
100
  readonly category: 'SET_SPEED';
@@ -1,4 +1,4 @@
1
- import type { PlayAction, PauseAction, ResumeAction, StopAction, NextAction, PreviousAction, SeekAction, SetVolumeAction, VolumeUpAction, VolumeDownAction, VolumeFineUpAction, VolumeFineDownAction, ToggleShuffleAction, ToggleRepeatAction, SetQueueAction, AddToQueueAction, RemoveFromQueueAction, ClearQueueAction, SetQueuePositionAction, UpdateProgressAction, SetDurationAction, TickAction, SetLoadingAction, SetErrorAction, RestoreStateAction, SetSpeedAction } from './actions.ts';
1
+ import type { PlayAction, PauseAction, ResumeAction, StopAction, NextAction, PreviousAction, SeekAction, SetVolumeAction, VolumeUpAction, VolumeDownAction, VolumeFineUpAction, VolumeFineDownAction, ToggleShuffleAction, ToggleRepeatAction, ToggleAutoplayAction, SetQueueAction, AddToQueueAction, RemoveFromQueueAction, ClearQueueAction, SetQueuePositionAction, UpdateProgressAction, SetDurationAction, TickAction, SetLoadingAction, SetErrorAction, RestoreStateAction, SetSpeedAction } from './actions.ts';
2
2
  import type { Track } from './youtube-music.types.ts';
3
3
  export interface PlayerState {
4
4
  currentTrack: Track | null;
@@ -11,8 +11,9 @@ export interface PlayerState {
11
11
  queuePosition: number;
12
12
  repeat: 'off' | 'all' | 'one';
13
13
  shuffle: boolean;
14
+ autoplay: boolean;
14
15
  isLoading: boolean;
15
16
  error: string | null;
16
17
  playRequestId: number;
17
18
  }
18
- export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | VolumeFineUpAction | VolumeFineDownAction | ToggleShuffleAction | ToggleRepeatAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction | SetSpeedAction;
19
+ export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | VolumeFineUpAction | VolumeFineDownAction | ToggleShuffleAction | ToggleRepeatAction | ToggleAutoplayAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction | SetSpeedAction;
@@ -49,6 +49,7 @@ export declare const KEYBINDINGS: {
49
49
  readonly VOLUME_FINE_DOWN: readonly ["shift+-"];
50
50
  readonly SHUFFLE: readonly ["shift+s"];
51
51
  readonly REPEAT: readonly ["r"];
52
+ readonly AUTOPLAY_TOGGLE: readonly ["shift+a"];
52
53
  readonly GAPLESS_TOGGLE: readonly ["shift+g"];
53
54
  readonly CROSSFADE_CYCLE: readonly ["shift+c"];
54
55
  readonly EQUALIZER_CYCLE: readonly ["shift+e"];
@@ -85,6 +85,7 @@ export const KEYBINDINGS = {
85
85
  VOLUME_FINE_DOWN: ['shift+-'], // Fine-grained -1 step
86
86
  SHUFFLE: ['shift+s'],
87
87
  REPEAT: ['r'],
88
+ AUTOPLAY_TOGGLE: ['shift+a'],
88
89
  GAPLESS_TOGGLE: ['shift+g'],
89
90
  CROSSFADE_CYCLE: ['shift+c'],
90
91
  EQUALIZER_CYCLE: ['shift+e'],
@@ -16,4 +16,5 @@ export declare const ICONS: {
16
16
  readonly RESUME: "⟳";
17
17
  readonly BG_PLAY: "○";
18
18
  readonly VOLUME: "♪";
19
+ readonly AUTOPLAY: "∞";
19
20
  };
@@ -23,4 +23,6 @@ export const ICONS = {
23
23
  BG_PLAY: '○', // U+25CB
24
24
  // Status
25
25
  VOLUME: '♪', // U+266A
26
+ // Autoplay / radio
27
+ AUTOPLAY: '∞', // U+221E
26
28
  };
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",