@involvex/youtube-music-cli 0.0.2 → 0.0.3

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.
Files changed (56) hide show
  1. package/dist/source/components/common/ShortcutsBar.js +3 -1
  2. package/dist/source/components/config/KeybindingsLayout.d.ts +1 -0
  3. package/dist/source/components/config/KeybindingsLayout.js +107 -0
  4. package/dist/source/components/layouts/ExploreLayout.d.ts +1 -0
  5. package/dist/source/components/layouts/ExploreLayout.js +72 -0
  6. package/dist/source/components/layouts/LyricsLayout.d.ts +1 -0
  7. package/dist/source/components/layouts/LyricsLayout.js +89 -0
  8. package/dist/source/components/layouts/MainLayout.js +39 -1
  9. package/dist/source/components/layouts/MiniPlayerLayout.d.ts +1 -0
  10. package/dist/source/components/layouts/MiniPlayerLayout.js +19 -0
  11. package/dist/source/components/layouts/SearchLayout.js +10 -3
  12. package/dist/source/components/layouts/TrendingLayout.d.ts +1 -0
  13. package/dist/source/components/layouts/TrendingLayout.js +59 -0
  14. package/dist/source/components/player/NowPlaying.js +21 -1
  15. package/dist/source/components/player/PlayerControls.js +4 -2
  16. package/dist/source/components/search/SearchBar.js +4 -1
  17. package/dist/source/components/search/SearchHistory.d.ts +5 -0
  18. package/dist/source/components/search/SearchHistory.js +35 -0
  19. package/dist/source/components/settings/Settings.js +74 -11
  20. package/dist/source/config/themes.config.js +60 -0
  21. package/dist/source/hooks/usePlayer.d.ts +5 -0
  22. package/dist/source/hooks/usePlaylist.d.ts +2 -1
  23. package/dist/source/hooks/usePlaylist.js +8 -2
  24. package/dist/source/hooks/useSleepTimer.d.ts +9 -0
  25. package/dist/source/hooks/useSleepTimer.js +48 -0
  26. package/dist/source/services/cache/cache.service.d.ts +14 -0
  27. package/dist/source/services/cache/cache.service.js +67 -0
  28. package/dist/source/services/config/config.service.d.ts +2 -0
  29. package/dist/source/services/config/config.service.js +17 -0
  30. package/dist/source/services/discord/discord-rpc.service.d.ts +17 -0
  31. package/dist/source/services/discord/discord-rpc.service.js +95 -0
  32. package/dist/source/services/lyrics/lyrics.service.d.ts +22 -0
  33. package/dist/source/services/lyrics/lyrics.service.js +93 -0
  34. package/dist/source/services/mpris/mpris.service.d.ts +20 -0
  35. package/dist/source/services/mpris/mpris.service.js +78 -0
  36. package/dist/source/services/notification/notification.service.d.ts +14 -0
  37. package/dist/source/services/notification/notification.service.js +57 -0
  38. package/dist/source/services/player/player.service.d.ts +3 -0
  39. package/dist/source/services/player/player.service.js +20 -3
  40. package/dist/source/services/scrobbling/scrobbling.service.d.ts +23 -0
  41. package/dist/source/services/scrobbling/scrobbling.service.js +115 -0
  42. package/dist/source/services/sleep-timer/sleep-timer.service.d.ts +16 -0
  43. package/dist/source/services/sleep-timer/sleep-timer.service.js +45 -0
  44. package/dist/source/services/youtube-music/api.d.ts +6 -0
  45. package/dist/source/services/youtube-music/api.js +102 -2
  46. package/dist/source/stores/navigation.store.js +6 -0
  47. package/dist/source/stores/player.store.d.ts +5 -0
  48. package/dist/source/stores/player.store.js +141 -24
  49. package/dist/source/types/actions.d.ts +13 -0
  50. package/dist/source/types/config.types.d.ts +15 -1
  51. package/dist/source/types/navigation.types.d.ts +3 -2
  52. package/dist/source/types/player.types.d.ts +3 -2
  53. package/dist/source/utils/constants.d.ts +9 -0
  54. package/dist/source/utils/constants.js +9 -0
  55. package/dist/youtube-music-cli.exe +0 -0
  56. package/package.json +5 -2
@@ -4,10 +4,15 @@ import { createContext, useContext, useReducer, useEffect, useRef, } from 'react
4
4
  import { getPlayerService } from "../services/player/player.service.js";
5
5
  import { loadPlayerState, savePlayerState, } from "../services/player-state/player-state.service.js";
6
6
  import { logger } from "../services/logger/logger.service.js";
7
+ import { getNotificationService } from "../services/notification/notification.service.js";
8
+ import { getScrobblingService } from "../services/scrobbling/scrobbling.service.js";
9
+ import { getDiscordRpcService } from "../services/discord/discord-rpc.service.js";
10
+ import { getMprisService } from "../services/mpris/mpris.service.js";
7
11
  const initialState = {
8
12
  currentTrack: null,
9
13
  isPlaying: false,
10
14
  volume: 70,
15
+ speed: 1.0,
11
16
  progress: 0,
12
17
  duration: 0,
13
18
  queue: [],
@@ -104,6 +109,16 @@ function playerReducer(state, action) {
104
109
  playerService.setVolume(newVolume);
105
110
  return { ...state, volume: newVolume };
106
111
  }
112
+ case 'VOLUME_FINE_UP': {
113
+ const newVolume = Math.min(100, state.volume + 1);
114
+ playerService.setVolume(newVolume);
115
+ return { ...state, volume: newVolume };
116
+ }
117
+ case 'VOLUME_FINE_DOWN': {
118
+ const newVolume = Math.max(0, state.volume - 1);
119
+ playerService.setVolume(newVolume);
120
+ return { ...state, volume: newVolume };
121
+ }
107
122
  case 'TOGGLE_SHUFFLE':
108
123
  return { ...state, shuffle: !state.shuffle };
109
124
  case 'TOGGLE_REPEAT':
@@ -160,6 +175,11 @@ function playerReducer(state, action) {
160
175
  return { ...state, isLoading: action.loading };
161
176
  case 'SET_ERROR':
162
177
  return { ...state, error: action.error, isLoading: false };
178
+ case 'SET_SPEED': {
179
+ const clampedSpeed = Math.max(0.25, Math.min(4.0, action.speed));
180
+ playerService.setSpeed(clampedSpeed);
181
+ return { ...state, speed: clampedSpeed };
182
+ }
163
183
  case 'RESTORE_STATE':
164
184
  logger.info('PlayerReducer', 'RESTORE_STATE', {
165
185
  hasTrack: !!action.currentTrack,
@@ -189,6 +209,15 @@ function PlayerManager() {
189
209
  const progressIntervalRef = useRef(null);
190
210
  const musicService = getMusicService();
191
211
  const playerService = getPlayerService();
212
+ // Initialize MPRIS (Linux only, no-ops on other platforms)
213
+ useEffect(() => {
214
+ void getMprisService().initialize({
215
+ onPlay: () => dispatch({ category: 'RESUME' }),
216
+ onPause: () => dispatch({ category: 'PAUSE' }),
217
+ onNext: () => dispatch({ category: 'NEXT' }),
218
+ onPrevious: () => dispatch({ category: 'PREVIOUS' }),
219
+ });
220
+ }, [dispatch]);
192
221
  // Register event handler for mpv IPC events
193
222
  useEffect(() => {
194
223
  let lastProgressUpdate = 0;
@@ -260,29 +289,76 @@ function PlayerManager() {
260
289
  });
261
290
  const loadAndPlayTrack = async () => {
262
291
  dispatch({ category: 'SET_LOADING', loading: true });
263
- try {
264
- logger.debug('PlayerManager', 'Starting playback with mpv', {
265
- videoId: track.videoId,
266
- volume: state.volume,
267
- });
268
- // Pass YouTube URL directly to mpv (it handles stream extraction via yt-dlp)
269
- const youtubeUrl = `https://www.youtube.com/watch?v=${track.videoId}`;
270
- await playerService.play(youtubeUrl, {
271
- volume: state.volume,
272
- });
273
- logger.info('PlayerManager', 'Playback started successfully');
274
- dispatch({ category: 'SET_LOADING', loading: false });
275
- }
276
- catch (error) {
277
- logger.error('PlayerManager', 'Failed to load track', {
278
- error: error instanceof Error ? error.message : String(error),
279
- stack: error instanceof Error ? error.stack : undefined,
280
- track: { title: track.title, videoId: track.videoId },
281
- });
282
- dispatch({
283
- category: 'SET_ERROR',
284
- error: error instanceof Error ? error.message : 'Failed to load track',
285
- });
292
+ const MAX_RETRIES = 3;
293
+ const RETRY_DELAY_MS = 1500;
294
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
295
+ try {
296
+ logger.debug('PlayerManager', 'Starting playback with mpv', {
297
+ videoId: track.videoId,
298
+ volume: state.volume,
299
+ attempt,
300
+ });
301
+ // Pass YouTube URL directly to mpv (it handles stream extraction via yt-dlp)
302
+ const youtubeUrl = `https://www.youtube.com/watch?v=${track.videoId}`;
303
+ const config = getConfigService();
304
+ const artists = track.artists?.map(a => a.name).join(', ') ?? 'Unknown';
305
+ // Fire desktop notification if enabled (only on first attempt)
306
+ if (attempt === 1 && config.get('notifications')) {
307
+ const notificationService = getNotificationService();
308
+ notificationService.setEnabled(true);
309
+ void notificationService.notifyTrackChange(track.title, artists);
310
+ }
311
+ // Discord Rich Presence
312
+ if (config.get('discordRichPresence')) {
313
+ const discord = getDiscordRpcService();
314
+ discord.setEnabled(true);
315
+ void discord.connect().then(() => discord.updateActivity({
316
+ title: track.title,
317
+ artist: artists,
318
+ startTimestamp: Date.now(),
319
+ }));
320
+ }
321
+ // MPRIS (Linux)
322
+ const mpris = getMprisService();
323
+ mpris.updateTrack({
324
+ title: track.title,
325
+ artist: artists,
326
+ duration: (track.duration ?? 0) * 1_000_000,
327
+ }, true);
328
+ await playerService.play(youtubeUrl, {
329
+ volume: state.volume,
330
+ audioNormalization: config.get('audioNormalization') ?? false,
331
+ proxy: config.get('proxy'),
332
+ });
333
+ logger.info('PlayerManager', 'Playback started successfully', {
334
+ attempt,
335
+ });
336
+ dispatch({ category: 'SET_LOADING', loading: false });
337
+ return; // Success
338
+ }
339
+ catch (error) {
340
+ logger.error('PlayerManager', 'Failed to load track', {
341
+ error: error instanceof Error ? error.message : String(error),
342
+ track: { title: track.title, videoId: track.videoId },
343
+ attempt,
344
+ });
345
+ if (attempt < MAX_RETRIES) {
346
+ logger.info('PlayerManager', 'Retrying playback', {
347
+ attempt,
348
+ nextAttempt: attempt + 1,
349
+ delayMs: RETRY_DELAY_MS,
350
+ });
351
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
352
+ }
353
+ else {
354
+ dispatch({
355
+ category: 'SET_ERROR',
356
+ error: error instanceof Error
357
+ ? `${error.message} (after ${MAX_RETRIES} attempts)`
358
+ : 'Failed to load track',
359
+ });
360
+ }
361
+ }
286
362
  }
287
363
  };
288
364
  void loadAndPlayTrack();
@@ -301,6 +377,34 @@ function PlayerManager() {
301
377
  }
302
378
  return undefined;
303
379
  }, [state.isPlaying, state.currentTrack, dispatch]);
380
+ // Scrobble when >50% of track has been played
381
+ const scrobbledRef = useRef(null);
382
+ useEffect(() => {
383
+ if (state.currentTrack &&
384
+ state.duration > 0 &&
385
+ state.progress / state.duration > 0.5 &&
386
+ scrobbledRef.current !== state.currentTrack.videoId) {
387
+ scrobbledRef.current = state.currentTrack.videoId;
388
+ const config = getConfigService();
389
+ const scrobblingConfig = config.get('scrobbling');
390
+ if (scrobblingConfig) {
391
+ const scrobbler = getScrobblingService();
392
+ scrobbler.configure(scrobblingConfig);
393
+ const artist = state.currentTrack.artists?.[0]?.name ?? 'Unknown';
394
+ void scrobbler.scrobble({
395
+ title: state.currentTrack.title,
396
+ artist,
397
+ duration: state.duration,
398
+ });
399
+ }
400
+ }
401
+ if (state.currentTrack &&
402
+ scrobbledRef.current !== state.currentTrack.videoId &&
403
+ state.progress < 1) {
404
+ // New track started — reset so we can scrobble again
405
+ scrobbledRef.current = null;
406
+ }
407
+ }, [state.progress, state.duration, state.currentTrack]);
304
408
  // Handle play/pause state
305
409
  useEffect(() => {
306
410
  if (!state.isPlaying) {
@@ -441,6 +545,12 @@ export function PlayerProvider({ children }) {
441
545
  logger.debug('PlayerActions', 'volumeDown called');
442
546
  dispatch({ category: 'VOLUME_DOWN' });
443
547
  },
548
+ volumeFineUp: () => {
549
+ dispatch({ category: 'VOLUME_FINE_UP' });
550
+ },
551
+ volumeFineDown: () => {
552
+ dispatch({ category: 'VOLUME_FINE_DOWN' });
553
+ },
444
554
  toggleShuffle: () => dispatch({ category: 'TOGGLE_SHUFFLE' }),
445
555
  toggleRepeat: () => dispatch({ category: 'TOGGLE_REPEAT' }),
446
556
  setQueue: (queue) => dispatch({ category: 'SET_QUEUE', queue }),
@@ -448,7 +558,14 @@ export function PlayerProvider({ children }) {
448
558
  removeFromQueue: (index) => dispatch({ category: 'REMOVE_FROM_QUEUE', index }),
449
559
  clearQueue: () => dispatch({ category: 'CLEAR_QUEUE' }),
450
560
  setQueuePosition: (position) => dispatch({ category: 'SET_QUEUE_POSITION', position }),
451
- }), [dispatch]);
561
+ setSpeed: (speed) => dispatch({ category: 'SET_SPEED', speed }),
562
+ speedUp: () => {
563
+ dispatch({ category: 'SET_SPEED', speed: (state.speed ?? 1.0) + 0.25 });
564
+ },
565
+ speedDown: () => {
566
+ dispatch({ category: 'SET_SPEED', speed: (state.speed ?? 1.0) - 0.25 });
567
+ },
568
+ }), [dispatch, state.speed]);
452
569
  const contextValue = useMemo(() => ({
453
570
  state,
454
571
  dispatch, // Needed by PlayerManager
@@ -32,6 +32,12 @@ export interface VolumeUpAction {
32
32
  export interface VolumeDownAction {
33
33
  readonly category: 'VOLUME_DOWN';
34
34
  }
35
+ export interface VolumeFineUpAction {
36
+ readonly category: 'VOLUME_FINE_UP';
37
+ }
38
+ export interface VolumeFineDownAction {
39
+ readonly category: 'VOLUME_FINE_DOWN';
40
+ }
35
41
  export interface ToggleShuffleAction {
36
42
  readonly category: 'TOGGLE_SHUFFLE';
37
43
  }
@@ -86,6 +92,10 @@ export interface RestoreStateAction {
86
92
  shuffle: boolean;
87
93
  repeat: 'off' | 'all' | 'one';
88
94
  }
95
+ export interface SetSpeedAction {
96
+ readonly category: 'SET_SPEED';
97
+ speed: number;
98
+ }
89
99
  export interface NavigateAction {
90
100
  readonly category: 'NAVIGATE';
91
101
  view: string;
@@ -117,3 +127,6 @@ export interface SetSearchLimitAction {
117
127
  readonly category: 'SET_SEARCH_LIMIT';
118
128
  limit: number;
119
129
  }
130
+ export interface TogglePlayerModeAction {
131
+ readonly category: 'TOGGLE_PLAYER_MODE';
132
+ }
@@ -6,14 +6,28 @@ export interface KeybindingConfig {
6
6
  description: string;
7
7
  }
8
8
  export interface Config {
9
- theme: 'dark' | 'light' | 'midnight' | 'matrix' | 'custom';
9
+ theme: 'dark' | 'light' | 'midnight' | 'matrix' | 'dracula' | 'nord' | 'solarized' | 'catppuccin' | 'custom';
10
10
  volume: number;
11
11
  keybindings: Record<string, KeybindingConfig>;
12
12
  playlists: Playlist[];
13
13
  history: string[];
14
+ searchHistory: string[];
14
15
  favorites: string[];
15
16
  repeat: RepeatMode;
16
17
  shuffle: boolean;
17
18
  customTheme?: Theme;
18
19
  streamQuality?: 'low' | 'medium' | 'high';
20
+ audioNormalization?: boolean;
21
+ notifications?: boolean;
22
+ scrobbling?: {
23
+ lastfm?: {
24
+ apiKey?: string;
25
+ sessionKey?: string;
26
+ };
27
+ listenbrainz?: {
28
+ token?: string;
29
+ };
30
+ };
31
+ discordRichPresence?: boolean;
32
+ proxy?: string;
19
33
  }
@@ -1,4 +1,4 @@
1
- import type { NavigateAction, GoBackAction, SetSearchQueryAction, SetSearchCategoryAction, SetSelectedResultAction, SetSelectedPlaylistAction, SetHasSearchedAction, SetSearchLimitAction } from './actions.ts';
1
+ import type { NavigateAction, GoBackAction, SetSearchQueryAction, SetSearchCategoryAction, SetSelectedResultAction, SetSelectedPlaylistAction, SetHasSearchedAction, SetSearchLimitAction, TogglePlayerModeAction } from './actions.ts';
2
2
  export interface NavigationState {
3
3
  currentView: string;
4
4
  previousView: string | null;
@@ -10,5 +10,6 @@ export interface NavigationState {
10
10
  hasSearched: boolean;
11
11
  searchLimit: number;
12
12
  history: string[];
13
+ playerMode: 'full' | 'mini';
13
14
  }
14
- export type NavigationAction = NavigateAction | GoBackAction | SetSearchQueryAction | SetSearchCategoryAction | SetSelectedResultAction | SetSelectedPlaylistAction | SetHasSearchedAction | SetSearchLimitAction;
15
+ export type NavigationAction = NavigateAction | GoBackAction | SetSearchQueryAction | SetSearchCategoryAction | SetSelectedResultAction | SetSelectedPlaylistAction | SetHasSearchedAction | SetSearchLimitAction | TogglePlayerModeAction;
@@ -1,9 +1,10 @@
1
- import type { PlayAction, PauseAction, ResumeAction, StopAction, NextAction, PreviousAction, SeekAction, SetVolumeAction, VolumeUpAction, VolumeDownAction, ToggleShuffleAction, ToggleRepeatAction, SetQueueAction, AddToQueueAction, RemoveFromQueueAction, ClearQueueAction, SetQueuePositionAction, UpdateProgressAction, SetDurationAction, TickAction, SetLoadingAction, SetErrorAction, RestoreStateAction } from './actions.ts';
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';
2
2
  import type { Track } from './youtube-music.types.ts';
3
3
  export interface PlayerState {
4
4
  currentTrack: Track | null;
5
5
  isPlaying: boolean;
6
6
  volume: number;
7
+ speed: number;
7
8
  progress: number;
8
9
  duration: number;
9
10
  queue: Track[];
@@ -13,4 +14,4 @@ export interface PlayerState {
13
14
  isLoading: boolean;
14
15
  error: string | null;
15
16
  }
16
- export type PlayerAction = PlayAction | PauseAction | ResumeAction | StopAction | NextAction | PreviousAction | SeekAction | SetVolumeAction | VolumeUpAction | VolumeDownAction | ToggleShuffleAction | ToggleRepeatAction | SetQueueAction | AddToQueueAction | RemoveFromQueueAction | ClearQueueAction | SetQueuePositionAction | UpdateProgressAction | SetDurationAction | TickAction | SetLoadingAction | SetErrorAction | RestoreStateAction;
17
+ 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;
@@ -5,6 +5,7 @@ export declare const CONFIG_FILE: string;
5
5
  export declare const VIEW: {
6
6
  readonly PLAYER: "player";
7
7
  readonly SEARCH: "search";
8
+ readonly SEARCH_HISTORY: "search_history";
8
9
  readonly PLAYLISTS: "playlists";
9
10
  readonly ARTIST: "artist";
10
11
  readonly ALBUM: "album";
@@ -13,6 +14,10 @@ export declare const VIEW: {
13
14
  readonly SETTINGS: "settings";
14
15
  readonly CONFIG: "config";
15
16
  readonly PLUGINS: "plugins";
17
+ readonly LYRICS: "lyrics";
18
+ readonly KEYBINDINGS: "keybindings";
19
+ readonly TRENDING: "trending";
20
+ readonly EXPLORE: "explore";
16
21
  };
17
22
  export declare const SEARCH_TYPE: {
18
23
  readonly ALL: "all";
@@ -33,10 +38,14 @@ export declare const KEYBINDINGS: {
33
38
  readonly PREVIOUS: readonly ["b", "left"];
34
39
  readonly VOLUME_UP: readonly ["="];
35
40
  readonly VOLUME_DOWN: readonly ["-"];
41
+ readonly VOLUME_FINE_UP: readonly ["shift+="];
42
+ readonly VOLUME_FINE_DOWN: readonly ["shift+-"];
36
43
  readonly SHUFFLE: readonly ["s"];
37
44
  readonly REPEAT: readonly ["r"];
38
45
  readonly SEEK_FORWARD: readonly ["shift+right"];
39
46
  readonly SEEK_BACKWARD: readonly ["shift+left"];
47
+ readonly SPEED_UP: readonly [">"];
48
+ readonly SPEED_DOWN: readonly ["<"];
40
49
  readonly UP: readonly ["up", "k"];
41
50
  readonly DOWN: readonly ["down", "j"];
42
51
  readonly SELECT: readonly ["enter", "return"];
@@ -10,6 +10,7 @@ export const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
10
10
  export const VIEW = {
11
11
  PLAYER: 'player',
12
12
  SEARCH: 'search',
13
+ SEARCH_HISTORY: 'search_history',
13
14
  PLAYLISTS: 'playlists',
14
15
  ARTIST: 'artist',
15
16
  ALBUM: 'album',
@@ -18,6 +19,10 @@ export const VIEW = {
18
19
  SETTINGS: 'settings',
19
20
  CONFIG: 'config',
20
21
  PLUGINS: 'plugins',
22
+ LYRICS: 'lyrics',
23
+ KEYBINDINGS: 'keybindings',
24
+ TRENDING: 'trending',
25
+ EXPLORE: 'explore',
21
26
  };
22
27
  // Search types
23
28
  export const SEARCH_TYPE = {
@@ -42,10 +47,14 @@ export const KEYBINDINGS = {
42
47
  PREVIOUS: ['b', 'left'],
43
48
  VOLUME_UP: ['='], // Only '=' without shift, since '+' requires shift and causes issues
44
49
  VOLUME_DOWN: ['-'], // Only '-' without shift
50
+ VOLUME_FINE_UP: ['shift+='], // Fine-grained +1 step
51
+ VOLUME_FINE_DOWN: ['shift+-'], // Fine-grained -1 step
45
52
  SHUFFLE: ['s'],
46
53
  REPEAT: ['r'],
47
54
  SEEK_FORWARD: ['shift+right'],
48
55
  SEEK_BACKWARD: ['shift+left'],
56
+ SPEED_UP: ['>'],
57
+ SPEED_DOWN: ['<'],
49
58
  // Navigation
50
59
  UP: ['up', 'k'],
51
60
  DOWN: ['down', 'j'],
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@involvex/youtube-music-cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "- A Commandline music player for youtube-music",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,8 @@
32
32
  "test": "prettier --check . && xo && ava",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
35
- "clean": "rimraf dist"
35
+ "clean": "rimraf dist",
36
+ "release": "powershell -File scripts/release.ps1"
36
37
  },
37
38
  "xo": {
38
39
  "extends": "xo-react",
@@ -59,6 +60,7 @@
59
60
  "ink-text-input": "^6.0.0",
60
61
  "jiti": "^2.6.1",
61
62
  "meow": "^14.0.0",
63
+ "node-notifier": "^10.0.1",
62
64
  "node-youtube-music": "^0.10.3",
63
65
  "play-sound": "^1.1.6",
64
66
  "react": "^19.2.4",
@@ -68,6 +70,7 @@
68
70
  "devDependencies": {
69
71
  "@eslint/js": "^10.0.1",
70
72
  "@sindresorhus/tsconfig": "^8.1.0",
73
+ "@types/node-notifier": "^8.0.5",
71
74
  "@types/react": "^19.2.14",
72
75
  "@vdemedes/prettier-config": "^2.0.1",
73
76
  "ava": "^6.4.1",