Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92c5582c98 | |||
| a3801f6698 |
@@ -1,6 +1,6 @@
|
||||
export interface NowPlayingResponse {
|
||||
success: boolean;
|
||||
nowPlaying: string;
|
||||
playingItem: PlaylistItem;
|
||||
isPaused: boolean;
|
||||
volume: number;
|
||||
isIdle: boolean;
|
||||
@@ -21,6 +21,10 @@ export interface PlaylistItem {
|
||||
metadata?: Metadata;
|
||||
}
|
||||
|
||||
export const getDisplayTitle = (item: PlaylistItem): string => {
|
||||
return item.metadata?.title || item.title || item.filename;
|
||||
}
|
||||
|
||||
export interface MetadataUpdateEvent {
|
||||
event: 'metadata_update';
|
||||
data: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import SongTable from './SongTable';
|
||||
import NowPlaying from './NowPlaying';
|
||||
import AddSongPanel from './AddSongPanel';
|
||||
import { API, PlaylistItem } from '../api/player';
|
||||
import { API, getDisplayTitle, PlaylistItem } from '../api/player';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
@@ -11,7 +11,7 @@ const App: React.FC = () => {
|
||||
const [volume, setVolume] = useState(100);
|
||||
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
|
||||
const [songs, setSongs] = useState<PlaylistItem[]>([]);
|
||||
const [_, setWs] = useState<WebSocket | null>(null);
|
||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
||||
|
||||
const fetchPlaylist = useCallback(async () => {
|
||||
const playlist = await API.getPlaylist();
|
||||
@@ -20,8 +20,8 @@ const App: React.FC = () => {
|
||||
|
||||
const fetchNowPlaying = useCallback(async () => {
|
||||
const nowPlaying = await API.getNowPlaying();
|
||||
setNowPlayingSong(nowPlaying.nowPlaying);
|
||||
setNowPlayingFileName(nowPlaying.currentFile);
|
||||
setNowPlayingSong(getDisplayTitle(nowPlaying.playingItem));
|
||||
setNowPlayingFileName(nowPlaying.playingItem.filename);
|
||||
setIsPlaying(!nowPlaying.isPaused);
|
||||
|
||||
if (!volumeSettingIsLocked) {
|
||||
@@ -29,11 +29,15 @@ const App: React.FC = () => {
|
||||
}
|
||||
}, [volumeSettingIsLocked]);
|
||||
|
||||
const handleAddURL = (url: string) => {
|
||||
const handleAddURL = async (url: string) => {
|
||||
const urlToAdd = url.trim();
|
||||
if (urlToAdd) {
|
||||
API.addToPlaylist(urlToAdd);
|
||||
await API.addToPlaylist(urlToAdd);
|
||||
fetchPlaylist();
|
||||
|
||||
if (!isPlaying) {
|
||||
await API.play();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,14 +104,25 @@ const App: React.FC = () => {
|
||||
fetchNowPlaying();
|
||||
}, [fetchPlaylist, fetchNowPlaying]);
|
||||
|
||||
// WebSocket connection
|
||||
// Update WebSocket connection
|
||||
useEffect(() => {
|
||||
const ws = API.subscribeToEvents(handleWebSocketEvent);
|
||||
setWs(ws);
|
||||
const websocket = API.subscribeToEvents(handleWebSocketEvent);
|
||||
setWs(websocket);
|
||||
|
||||
// Handle page visibility changes, so if the user navigates back to this tab, we reconnect the WebSocket
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible' && (!ws || ws.readyState === WebSocket.CLOSED)) {
|
||||
const newWs = API.subscribeToEvents(handleWebSocketEvent);
|
||||
setWs(newWs);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
if (websocket) {
|
||||
websocket.close();
|
||||
}
|
||||
};
|
||||
}, [handleWebSocketEvent]);
|
||||
|
||||
@@ -23,15 +23,14 @@ interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
const NowPlaying: React.FC<NowPlayingProps> = (props) => {
|
||||
return (
|
||||
<div className={classNames(props.className, 'bg-black/50 h-[150px] p-5')}>
|
||||
<div className={classNames(props.className, 'bg-black/50 h-fit p-5')}>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<div className="flex flex-row w-full h-full bg-black/50 rounded-lg p-5 items-center">
|
||||
<div className="flex-grow min-w-0">
|
||||
<div className="text-white text-lg font-bold truncate">{props.songName}</div>
|
||||
<div className="text-white text-sm truncate">{props.fileName}</div>
|
||||
<div className="flex flex-col md:flex-row w-full h-full bg-black/50 rounded-lg p-5 items-center gap-4">
|
||||
<div className="flex-grow min-w-0 w-full md:w-auto text-white">
|
||||
<div className="text-lg font-bold truncate text-center md:text-left">{props.songName}</div>
|
||||
<div className="text-sm truncate text-center md:text-left">{props.fileName}</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
|
||||
<div className="flex flex-row items-center gap-4 justify-center md:justify-end">
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<FaVolumeUp size={20} />
|
||||
<input
|
||||
@@ -42,7 +41,7 @@ const NowPlaying: React.FC<NowPlayingProps> = (props) => {
|
||||
onMouseDown={() => props.onVolumeWillChange(props.volume)}
|
||||
onMouseUp={() => props.onVolumeDidChange(props.volume)}
|
||||
onChange={(e) => props.onVolumeSettingChange(Number(e.target.value))}
|
||||
className="w-24 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full hover:[&::-webkit-slider-thumb]:bg-violet-300"
|
||||
className="fancy-slider w-48 md:w-24 h-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { FaPlay, FaVolumeUp, FaVolumeOff } from 'react-icons/fa';
|
||||
import { PlaylistItem } from '../api/player';
|
||||
import { getDisplayTitle, PlaylistItem } from '../api/player';
|
||||
|
||||
export enum PlayState {
|
||||
NotPlaying,
|
||||
@@ -36,7 +36,7 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
|
||||
};
|
||||
}, [showDeleteConfirm]);
|
||||
|
||||
const displayTitle = song.metadata?.title || song.title || song.filename;
|
||||
const displayTitle = getDisplayTitle(song);
|
||||
|
||||
return (
|
||||
<div className={classNames("flex flex-row w-full h-[100px] px-2 py-5 items-center border-b gap-2 transition-colors", {
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer components {
|
||||
.fancy-slider {
|
||||
@apply bg-gray-700 rounded-lg appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3
|
||||
[&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-white
|
||||
[&::-webkit-slider-thumb]:rounded-full
|
||||
hover:[&::-webkit-slider-thumb]:bg-violet-300;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,15 @@ interface LinkMetadata {
|
||||
siteName?: string;
|
||||
}
|
||||
|
||||
interface PlaylistItem {
|
||||
id: number;
|
||||
filename: string;
|
||||
title?: string;
|
||||
playing?: boolean;
|
||||
current?: boolean;
|
||||
metadata?: LinkMetadata;
|
||||
}
|
||||
|
||||
export class MediaPlayer {
|
||||
private playerProcess: ChildProcess;
|
||||
private socket: Socket;
|
||||
@@ -46,23 +55,43 @@ export class MediaPlayer {
|
||||
});
|
||||
}
|
||||
|
||||
public async getPlaylist(): Promise<any> {
|
||||
public async getPlaylist(): Promise<PlaylistItem[]> {
|
||||
return this.writeCommand("get_property", ["playlist"])
|
||||
.then((response) => {
|
||||
// Enhance playlist items with metadata
|
||||
const playlist = response.data;
|
||||
return playlist.map((item: any) => ({
|
||||
const playlist = response.data as PlaylistItem[];
|
||||
return playlist.map((item: PlaylistItem) => ({
|
||||
...item,
|
||||
metadata: this.metadata.get(item.filename) || {}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
public async getNowPlaying(): Promise<string> {
|
||||
return this.writeCommand("get_property", ["media-title"])
|
||||
.then((response) => {
|
||||
return response.data;
|
||||
});
|
||||
public async getNowPlaying(): Promise<PlaylistItem> {
|
||||
const playlist = await this.getPlaylist();
|
||||
const currentlyPlayingSong = playlist.find((item: PlaylistItem) => item.current);
|
||||
const fetchMediaTitle = async (): Promise<string> => {
|
||||
return (await this.writeCommand("get_property", ["media-title"])).data;
|
||||
};
|
||||
|
||||
if (currentlyPlayingSong !== undefined) {
|
||||
// Use media title if we don't have a title
|
||||
if (currentlyPlayingSong.title === undefined && currentlyPlayingSong.metadata?.title === undefined) {
|
||||
return {
|
||||
...currentlyPlayingSong,
|
||||
title: await fetchMediaTitle()
|
||||
};
|
||||
}
|
||||
|
||||
return currentlyPlayingSong;
|
||||
}
|
||||
|
||||
const mediaTitle = await fetchMediaTitle();
|
||||
return {
|
||||
id: 0,
|
||||
filename: mediaTitle,
|
||||
title: mediaTitle
|
||||
};
|
||||
}
|
||||
|
||||
public async getCurrentFile(): Promise<string> {
|
||||
|
||||
@@ -63,7 +63,7 @@ apiRouter.post("/previous", withErrorHandling(async (req, res) => {
|
||||
}));
|
||||
|
||||
apiRouter.get("/nowplaying", withErrorHandling(async (req, res) => {
|
||||
const nowPlaying = await mediaPlayer.getNowPlaying();
|
||||
const playingItem = await mediaPlayer.getNowPlaying();
|
||||
const currentFile = await mediaPlayer.getCurrentFile();
|
||||
const pauseState = await mediaPlayer.getPauseState();
|
||||
const volume = await mediaPlayer.getVolume();
|
||||
@@ -71,7 +71,7 @@ apiRouter.get("/nowplaying", withErrorHandling(async (req, res) => {
|
||||
|
||||
res.send(JSON.stringify({
|
||||
success: true,
|
||||
nowPlaying: nowPlaying,
|
||||
playingItem: playingItem,
|
||||
isPaused: pauseState,
|
||||
volume: volume,
|
||||
isIdle: idle,
|
||||
|
||||
Reference in New Issue
Block a user