@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/CHANGELOG.md +2 -0
- package/dist/cli.js.map +1004 -0
- package/dist/package.json +1 -1
- package/dist/source/components/common/ShortcutsBar.js +11 -2
- package/dist/source/hooks/usePlayer.d.ts +1 -0
- package/dist/source/services/player-state/player-state.service.d.ts +1 -0
- package/dist/source/services/player-state/player-state.service.js +1 -0
- package/dist/source/services/web/web-server-manager.js +1 -0
- package/dist/source/services/web/web-streaming.service.js +3 -1
- package/dist/source/stores/player.store.d.ts +1 -0
- package/dist/source/stores/player.store.js +65 -0
- package/dist/source/types/actions.d.ts +4 -0
- package/dist/source/types/player.types.d.ts +3 -2
- package/dist/source/utils/constants.d.ts +1 -0
- package/dist/source/utils/constants.js +1 -0
- package/dist/source/utils/icons.d.ts +1 -0
- package/dist/source/utils/icons.js +2 -0
- package/dist/youtube-music-cli +0 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -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"];
|
package/dist/youtube-music-cli
CHANGED
|
Binary file
|