More rich metadata with NowPlaying

This commit is contained in:
2025-02-15 22:15:59 -08:00
parent d465dbf6fb
commit a3801f6698
7 changed files with 72 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
export interface NowPlayingResponse { export interface NowPlayingResponse {
success: boolean; success: boolean;
nowPlaying: string; playingItem: PlaylistItem;
isPaused: boolean; isPaused: boolean;
volume: number; volume: number;
isIdle: boolean; isIdle: boolean;
@@ -21,6 +21,10 @@ export interface PlaylistItem {
metadata?: Metadata; metadata?: Metadata;
} }
export const getDisplayTitle = (item: PlaylistItem): string => {
return item.metadata?.title || item.title || item.filename;
}
export interface MetadataUpdateEvent { export interface MetadataUpdateEvent {
event: 'metadata_update'; event: 'metadata_update';
data: { data: {

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import SongTable from './SongTable'; import SongTable from './SongTable';
import NowPlaying from './NowPlaying'; import NowPlaying from './NowPlaying';
import AddSongPanel from './AddSongPanel'; import AddSongPanel from './AddSongPanel';
import { API, PlaylistItem } from '../api/player'; import { API, getDisplayTitle, PlaylistItem } from '../api/player';
const App: React.FC = () => { const App: React.FC = () => {
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
@@ -20,8 +20,8 @@ const App: React.FC = () => {
const fetchNowPlaying = useCallback(async () => { const fetchNowPlaying = useCallback(async () => {
const nowPlaying = await API.getNowPlaying(); const nowPlaying = await API.getNowPlaying();
setNowPlayingSong(nowPlaying.nowPlaying); setNowPlayingSong(getDisplayTitle(nowPlaying.playingItem));
setNowPlayingFileName(nowPlaying.currentFile); setNowPlayingFileName(nowPlaying.playingItem.filename);
setIsPlaying(!nowPlaying.isPaused); setIsPlaying(!nowPlaying.isPaused);
if (!volumeSettingIsLocked) { if (!volumeSettingIsLocked) {
@@ -29,11 +29,15 @@ const App: React.FC = () => {
} }
}, [volumeSettingIsLocked]); }, [volumeSettingIsLocked]);
const handleAddURL = (url: string) => { const handleAddURL = async (url: string) => {
const urlToAdd = url.trim(); const urlToAdd = url.trim();
if (urlToAdd) { if (urlToAdd) {
API.addToPlaylist(urlToAdd); await API.addToPlaylist(urlToAdd);
fetchPlaylist(); fetchPlaylist();
if (!isPlaying) {
await API.play();
}
} }
}; };

View File

@@ -23,15 +23,14 @@ interface NowPlayingProps extends HTMLAttributes<HTMLDivElement> {
const NowPlaying: React.FC<NowPlayingProps> = (props) => { const NowPlaying: React.FC<NowPlayingProps> = (props) => {
return ( 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-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 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"> <div className="flex-grow min-w-0 w-full md:w-auto text-white">
<div className="text-white text-lg font-bold truncate">{props.songName}</div> <div className="text-lg font-bold truncate text-center md:text-left">{props.songName}</div>
<div className="text-white text-sm truncate">{props.fileName}</div> <div className="text-sm truncate text-center md:text-left">{props.fileName}</div>
</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"> <div className="flex items-center gap-2 text-white">
<FaVolumeUp size={20} /> <FaVolumeUp size={20} />
<input <input
@@ -42,7 +41,7 @@ const NowPlaying: React.FC<NowPlayingProps> = (props) => {
onMouseDown={() => props.onVolumeWillChange(props.volume)} onMouseDown={() => props.onVolumeWillChange(props.volume)}
onMouseUp={() => props.onVolumeDidChange(props.volume)} onMouseUp={() => props.onVolumeDidChange(props.volume)}
onChange={(e) => props.onVolumeSettingChange(Number(e.target.value))} 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> </div>

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { FaPlay, FaVolumeUp, FaVolumeOff } from 'react-icons/fa'; import { FaPlay, FaVolumeUp, FaVolumeOff } from 'react-icons/fa';
import { PlaylistItem } from '../api/player'; import { getDisplayTitle, PlaylistItem } from '../api/player';
export enum PlayState { export enum PlayState {
NotPlaying, NotPlaying,
@@ -36,7 +36,7 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
}; };
}, [showDeleteConfirm]); }, [showDeleteConfirm]);
const displayTitle = song.metadata?.title || song.title || song.filename; const displayTitle = getDisplayTitle(song);
return ( return (
<div className={classNames("flex flex-row w-full h-[100px] px-2 py-5 items-center border-b gap-2 transition-colors", { <div className={classNames("flex flex-row w-full h-[100px] px-2 py-5 items-center border-b gap-2 transition-colors", {

View File

@@ -1 +1,11 @@
@import "tailwindcss"; @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;
}
}

View File

@@ -14,6 +14,15 @@ interface LinkMetadata {
siteName?: string; siteName?: string;
} }
interface PlaylistItem {
id: number;
filename: string;
title?: string;
playing?: boolean;
current?: boolean;
metadata?: LinkMetadata;
}
export class MediaPlayer { export class MediaPlayer {
private playerProcess: ChildProcess; private playerProcess: ChildProcess;
private socket: Socket; 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"]) return this.writeCommand("get_property", ["playlist"])
.then((response) => { .then((response) => {
// Enhance playlist items with metadata // Enhance playlist items with metadata
const playlist = response.data; const playlist = response.data as PlaylistItem[];
return playlist.map((item: any) => ({ return playlist.map((item: PlaylistItem) => ({
...item, ...item,
metadata: this.metadata.get(item.filename) || {} metadata: this.metadata.get(item.filename) || {}
})); }));
}); });
} }
public async getNowPlaying(): Promise<string> { public async getNowPlaying(): Promise<PlaylistItem> {
return this.writeCommand("get_property", ["media-title"]) const playlist = await this.getPlaylist();
.then((response) => { const currentlyPlayingSong = playlist.find((item: PlaylistItem) => item.current);
return response.data; 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> { public async getCurrentFile(): Promise<string> {

View File

@@ -63,7 +63,7 @@ apiRouter.post("/previous", withErrorHandling(async (req, res) => {
})); }));
apiRouter.get("/nowplaying", 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 currentFile = await mediaPlayer.getCurrentFile();
const pauseState = await mediaPlayer.getPauseState(); const pauseState = await mediaPlayer.getPauseState();
const volume = await mediaPlayer.getVolume(); const volume = await mediaPlayer.getVolume();
@@ -71,7 +71,7 @@ apiRouter.get("/nowplaying", withErrorHandling(async (req, res) => {
res.send(JSON.stringify({ res.send(JSON.stringify({
success: true, success: true,
nowPlaying: nowPlaying, playingItem: playingItem,
isPaused: pauseState, isPaused: pauseState,
volume: volume, volume: volume,
isIdle: idle, isIdle: idle,