diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index 9cb21d3aa..fff7df151 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -20,7 +20,7 @@ mpv.start().catch((error: any) => { mpv.on('status', (status: any) => { if (status.property === 'playlist-pos') { if (status.value !== 0) { - getMainWindow()?.webContents.send('renderer-player-set-queue-next'); + getMainWindow()?.webContents.send('renderer-player-auto-next'); } } }); @@ -88,7 +88,7 @@ ipcMain.on('player-seek-to', async (_event, time: number) => { await mpv.goToPosition(time); }); -// Sets the queue to the given data. Used when manually starting a song or using the next/prev buttons +// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons ipcMain.on('player-set-queue', async (_event, data: PlayerData) => { if (data.queue.current) { await mpv.load(data.queue.current.streamUrl, 'replace'); @@ -99,8 +99,26 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData) => { } }); -// Sets the next song in the queue when reaching the end of the queue +// Replaces the queue in position 1 to the given data ipcMain.on('player-set-queue-next', async (_event, data: PlayerData) => { + const size = await mpv.getPlaylistSize(); + + if (size > 1) { + await mpv.playlistRemove(1); + } + + if (data.queue.next) { + await mpv.load(data.queue.next.streamUrl, 'append'); + } +}); + +// Sets the next song in the queue when reaching the end of the queue +ipcMain.on('player-auto-next', async (_event, data: PlayerData) => { + // Always keep the current song as position 0 in the mpv queue + // This allows us to easily set update the next song in the queue without + // disturbing the currently playing song + await mpv.playlistRemove(0); + if (data.queue.next) { await mpv.load(data.queue.next.streamUrl, 'append'); } diff --git a/src/main/preload.ts b/src/main/preload.ts index ce4066d50..13fbcd190 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -3,6 +3,9 @@ import { PlayerData } from '../renderer/store'; contextBridge.exposeInMainWorld('electron', { ipcRenderer: { + PLAYER_AUTO_NEXT(data: PlayerData) { + ipcRenderer.send('player-auto-next', data); + }, PLAYER_CURRENT_TIME() { ipcRenderer.send('player-current-time'); }, @@ -39,6 +42,11 @@ contextBridge.exposeInMainWorld('electron', { PLAYER_VOLUME(value: number) { ipcRenderer.send('player-volume', value); }, + RENDERER_PLAYER_AUTO_NEXT( + cb: (event: IpcRendererEvent, data: any) => void + ) { + ipcRenderer.on('renderer-player-auto-next', cb); + }, RENDERER_PLAYER_CURRENT_TIME( cb: (event: IpcRendererEvent, data: any) => void ) { @@ -50,11 +58,6 @@ contextBridge.exposeInMainWorld('electron', { RENDERER_PLAYER_PLAY(cb: (event: IpcRendererEvent, data: any) => void) { ipcRenderer.on('renderer-player-play', cb); }, - RENDERER_PLAYER_SET_QUEUE_NEXT( - cb: (event: IpcRendererEvent, data: any) => void - ) { - ipcRenderer.on('renderer-player-set-queue-next', cb); - }, RENDERER_PLAYER_STOP(cb: (event: IpcRendererEvent, data: any) => void) { ipcRenderer.on('renderer-player-stop', cb); }, diff --git a/src/renderer/components/virtual-grid/GridCardControls.tsx b/src/renderer/components/virtual-grid/GridCardControls.tsx index b536f0ed2..b1d855be2 100644 --- a/src/renderer/components/virtual-grid/GridCardControls.tsx +++ b/src/renderer/components/virtual-grid/GridCardControls.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { UnstyledButton, UnstyledButtonProps } from '@mantine/core'; +import { Button, UnstyledButton, UnstyledButtonProps } from '@mantine/core'; import { motion } from 'framer-motion'; import { RiPlayFill } from 'react-icons/ri'; import styled from 'styled-components'; +import { Play } from '../../../types'; type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>; @@ -13,7 +14,7 @@ const PlayButton = styled(UnstyledButton)` justify-content: center; width: 50px; height: 50px; - background-color: var(--primary-color); + border: 1px solid var(--primary-color); border-radius: 50%; cursor: default; opacity: 0.8; @@ -78,13 +79,43 @@ export const GridCardControls = ({ id: itemData[cardControls.idProperty], type: cardControls.type, }, + play: Play.NOW, }); }} > - + + + + ); }; diff --git a/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx b/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx index c51b27ac0..6da2ea7b3 100644 --- a/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx +++ b/src/renderer/components/virtual-grid/VirtualGridWrapper.tsx @@ -27,7 +27,7 @@ export const VirtualGridWrapper = ({ refInstance: Ref; rowCount: number; }) => { - const { handlePlayQueueAdd } = usePlayQueueHandler(); + const handlePlayQueueAdd = usePlayQueueHandler(); const memo = useMemo( () => ({ diff --git a/src/renderer/features/library/routes/LibraryAlbumsRoute.tsx b/src/renderer/features/library/routes/LibraryAlbumsRoute.tsx index 980794a03..abc190e7b 100644 --- a/src/renderer/features/library/routes/LibraryAlbumsRoute.tsx +++ b/src/renderer/features/library/routes/LibraryAlbumsRoute.tsx @@ -111,7 +111,7 @@ export const LibraryAlbumsRoute = () => { cardControls={{ endpoint: albumsApi.getAlbum, idProperty: 'id', - type: Item.Album, + type: Item.ALBUM, }} cardRows={[ { diff --git a/src/renderer/features/player/hooks/usePlayQueueHandler.ts b/src/renderer/features/player/hooks/usePlayQueueHandler.ts index b865dda2e..83621d617 100644 --- a/src/renderer/features/player/hooks/usePlayQueueHandler.ts +++ b/src/renderer/features/player/hooks/usePlayQueueHandler.ts @@ -1,5 +1,5 @@ -import { useQueryClient } from 'react-query'; import { Item, Play } from '../../../../types'; +import { albumsApi } from '../../../api/albumsApi'; import { usePlayerStore } from '../../../store'; import { getJellyfinStreamUrl, @@ -8,54 +8,23 @@ import { } from '../../../utils'; import { mpvPlayer } from '../utils/mpvPlayer'; -const getEndpoint = (item: Item) => { +const getEndpointByItemType = (item: Item) => { switch (item) { - case Item.Album: - return 'getAlbum'; - case Item.Artist: - return 'getArtistSongs'; - case Item.Playlist: - return 'getPlaylist'; + case Item.ALBUM: + return albumsApi.getAlbum; default: - return 'getAlbum'; + return albumsApi.getAlbum; } }; export const usePlayQueueHandler = () => { - const queryClient = useQueryClient(); - const addQueue = usePlayerStore((state) => state.add); - - // const dispatchSongsToQueue = useCallback( - // (entries: Song[], play: Play) => { - // const filteredSongs = filterPlayQueue(config.playback.filters, entries); - - // if (play === Play.Play) { - // if (filteredSongs.entries.length > 0) { - // } else { - // } - // } - - // if (play === Play.Next || play === Play.Later) { - // if (filteredSongs.entries.length > 0) { - // } - // } - - // notifyToast( - // 'info', - // getPlayedSongsNotification({ - // ...filteredSongs.count, - // type: play === Play.Play ? 'play' : 'add', - // }) - // ); - // }, - // [config.playback.filters, dispatch] - // ); + const addToQueue = usePlayerStore((state) => state.addToQueue); const handlePlayQueueAdd = async (options: { byData?: any[]; byItemType?: { endpoint: (params: Record) => any; - id: string; + id: number; type: Item; }; play: Play; @@ -71,11 +40,13 @@ export const usePlayQueueHandler = () => { ); if (deviceId) { - const data = await options.byItemType.endpoint({ + const endpoint = getEndpointByItemType(options.byItemType.type); + + const { data } = await endpoint({ id: options.byItemType.id, }); - const songs = data.data.songs.map((song) => { + const songs = data.songs.map((song) => { const auth = getServerFolderAuth(serverUrl, song.serverFolderId); if (auth) { @@ -106,30 +77,16 @@ export const usePlayQueueHandler = () => { }; }); - const pData = addQueue(songs); - mpvPlayer.setQueue(pData); - } + const playerData = addToQueue(songs, options.play); - // const data = await apiController({ - // args: { id: options.byItemType.id, musicFolder: options.musicFolder }, - // endpoint: - // options.byItemType.endpoint || getEndpoint(options.byItemType.item), - // serverType: config.serverType, - // }); - // if (options.byItemType.item === Item.Album) { - // queryClient.setQueryData(['album', options.byItemType.id], data); - // } else if (options.byItemType.item === Item.Artist) { - // queryClient.setQueryData(['artistSongs', options.byItemType.id], data); - // } else if (options.byItemType.item === Item.Playlist) { - // queryClient.setQueryData(['playlist', options.byItemType.id], data); - // } - // if (data?.song) { - // dispatchSongsToQueue(data.song, options.play); - // } else { - // dispatchSongsToQueue(data, options.play); - // } + if (options.play === Play.NEXT || options.play === Play.LAST) { + mpvPlayer.setQueueNext(playerData); + } else { + mpvPlayer.setQueue(playerData); + } + } } }; - return { handlePlayQueueAdd }; + return handlePlayQueueAdd; }; diff --git a/src/renderer/features/player/utils/mpvPlayer.ts b/src/renderer/features/player/utils/mpvPlayer.ts index 8e74da1c9..89a8ea8c7 100644 --- a/src/renderer/features/player/utils/mpvPlayer.ts +++ b/src/renderer/features/player/utils/mpvPlayer.ts @@ -1,5 +1,10 @@ +// Other files: +// main/features/core/player/index.ts +// main/preload.ts +// renderer/preload.d.ts + import isElectron from 'is-electron'; -import { PlayerData, usePlayerStore } from 'renderer/store'; +import { PlayerData, usePlayerStore } from '../../../store'; const ipc = isElectron() ? window.electron.ipcRenderer : null; @@ -19,6 +24,8 @@ const setQueue = (data: PlayerData) => ipc?.PLAYER_SET_QUEUE(data); const setQueueNext = (data: PlayerData) => ipc?.PLAYER_SET_QUEUE_NEXT(data); +const playerAutoNext = (data: PlayerData) => ipc?.PLAYER_AUTO_NEXT(data); + const seek = (seconds: number) => ipc?.PLAYER_SEEK(seconds); const seekTo = (seconds: number) => ipc?.PLAYER_SEEK_TO(seconds); @@ -42,10 +49,10 @@ ipc?.RENDERER_PLAYER_STOP(() => setPause()); ipc?.RENDERER_PLAYER_CURRENT_TIME((_event, time) => setCurrentTime(time)); -ipc?.RENDERER_PLAYER_SET_QUEUE_NEXT(() => { +ipc?.RENDERER_PLAYER_AUTO_NEXT(() => { const playerData = autoNext(); if (playerData.queue.next) { - setQueueNext(playerData); + playerAutoNext(playerData); } }); @@ -55,6 +62,7 @@ export const mpvPlayer = { next, pause, play, + playerAutoNext, previous, seek, seekTo, diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index f2f567d3f..8dba1e8d2 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -5,6 +5,7 @@ declare global { interface Window { electron: { ipcRenderer: { + PLAYER_AUTO_NEXT(data: PlayerData): void; PLAYER_CURRENT_TIME(): void; PLAYER_MUTE(): void; PLAYER_NEXT(): void; @@ -17,6 +18,9 @@ declare global { PLAYER_SET_QUEUE_NEXT(data: PlayerData): void; PLAYER_STOP(): void; PLAYER_VOLUME(value: number): void; + RENDERER_PLAYER_AUTO_NEXT( + cb: (event: IpcRendererEvent, data: any) => void + ): void; RENDERER_PLAYER_CURRENT_TIME( cb: (event: IpcRendererEvent, data: any) => void ): void; @@ -26,9 +30,6 @@ declare global { RENDERER_PLAYER_PLAY( cb: (event: IpcRendererEvent, data: any) => void ): void; - RENDERER_PLAYER_SET_QUEUE_NEXT( - cb: (event: IpcRendererEvent, data: any) => void - ): void; RENDERER_PLAYER_STOP( cb: (event: IpcRendererEvent, data: any) => void ): void; diff --git a/src/renderer/store/usePlayerStore.ts b/src/renderer/store/usePlayerStore.ts index 4f24d96b6..323ca9113 100644 --- a/src/renderer/store/usePlayerStore.ts +++ b/src/renderer/store/usePlayerStore.ts @@ -1,8 +1,10 @@ +/* eslint-disable prefer-destructuring */ /* eslint-disable @typescript-eslint/no-unused-vars */ import produce from 'immer'; import create from 'zustand'; import { devtools } from 'zustand/middleware'; import { + Play, CrossfadeStyle, PlaybackStyle, PlaybackType, @@ -55,7 +57,7 @@ export interface PlayerData { } export interface PlayerSlice extends PlayerState { - add: (songs: Song[]) => PlayerData; + addToQueue: (songs: Song[], type: Play) => PlayerData; autoNext: () => PlayerData; getPlayerData: () => PlayerData; next: () => PlayerData; @@ -70,15 +72,37 @@ export interface PlayerSlice extends PlayerState { export const usePlayerStore = create()( devtools((set, get) => ({ - add: (songs) => { - set( - produce((state) => { - state.queue.default = songs; - state.current.time = 0; - state.current.player = 1; - state.current.song = state.queue.default[state.current.index]; - }) - ); + addToQueue: (songs, type) => { + if (type === Play.NOW) { + set( + produce((state) => { + state.queue.default = songs; + state.current.time = 0; + state.current.player = 1; + state.current.index = 0; + state.current.song = songs[0]; + }) + ); + } else if (type === Play.LAST) { + set( + produce((state) => { + state.queue.default = [...get().queue.default, ...songs]; + }) + ); + } else if (type === Play.NEXT) { + const queue = get().queue.default; + const currentIndex = get().current.index; + + set( + produce((state) => { + state.queue.default = [ + ...queue.slice(0, currentIndex + 1), + ...songs, + ...queue.slice(currentIndex + 1), + ]; + }) + ); + } return get().getPlayerData(); }, diff --git a/src/types.ts b/src/types.ts index b753efeac..7e2c8517e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,12 +11,12 @@ export enum ServerType { } export enum Item { - Album = 'album', - Artist = 'artist', - Folder = 'folder', - Genre = 'genre', - Music = 'music', - Playlist = 'playlist', + ALBUM = 'album', + ARTIST = 'artist', + FOLDER = 'folder', + GENRE = 'genre', + PLAYLIST = 'playlist', + SONG = 'song', } export enum PlayerStatus { @@ -31,9 +31,9 @@ export enum PlayerRepeat { } export enum Play { - Later = 'later', - Next = 'next', - Now = 'play', + LAST = 'last', + NEXT = 'next', + NOW = 'now', } export enum CrossfadeStyle { @@ -55,8 +55,6 @@ export enum PlaybackType { Web = 'web', } -// export type ServerType = Server.Subsonic | Server.Jellyfin; - export type APIEndpoints = | 'getPlaylist' | 'getPlaylists' @@ -126,7 +124,7 @@ export interface Album { songCount: number; starred?: string; title: string; - type: Item.Album; + type: Item.ALBUM; uniqueId: string; userRating?: number; year?: number; @@ -142,7 +140,7 @@ export interface Artist { info?: ArtistInfo; starred?: string; title: string; - type?: Item.Artist; + type?: Item.ARTIST; uniqueId?: string; userRating?: number; } @@ -160,7 +158,7 @@ export interface Folder { image: string; isDir?: boolean; title: string; - type: Item.Folder; + type: Item.FOLDER; uniqueId: string; } @@ -169,7 +167,7 @@ export interface Genre { id: string; songCount?: number; title: string; - type?: Item.Genre; + type?: Item.GENRE; uniqueId?: string; } @@ -186,7 +184,7 @@ export interface Playlist { song?: Song[]; songCount?: number; title: string; - type: Item.Playlist; + type: Item.PLAYLIST; uniqueId: string; } @@ -216,7 +214,7 @@ export interface Song { suffix?: string; title: string; track?: number; - type?: Item.Music; + type?: Item.SONG; uniqueId?: string; userRating?: number; year?: number;