Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acd31a9154 | |||
| 687a7fc555 |
@@ -25,7 +25,7 @@ export class FavoritesStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// In production (in a container), use /app/data
|
// In production (in a container), use /app/data
|
||||||
if (process.env.NODE_ENV === 'production') {
|
else if (process.env.NODE_ENV === 'production') {
|
||||||
storePath = path.resolve('/app/data');
|
storePath = path.resolve('/app/data');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,9 @@ export class FavoritesStore {
|
|||||||
console.error('Failed to create intermediate directory:', err);
|
console.error('Failed to create intermediate directory:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
return path.join(storePath, storeFilename);
|
const fullPath = path.join(storePath, storeFilename);
|
||||||
|
console.log("Favorites store path: " + fullPath);
|
||||||
|
return fullPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadFavorites() {
|
private async loadFavorites() {
|
||||||
|
|||||||
@@ -4,11 +4,21 @@ import { WebSocket } from "ws";
|
|||||||
import { getLinkPreview } from "link-preview-js";
|
import { getLinkPreview } from "link-preview-js";
|
||||||
import { PlaylistItem, LinkMetadata } from './types';
|
import { PlaylistItem, LinkMetadata } from './types';
|
||||||
import { FavoritesStore } from "./FavoritesStore";
|
import { FavoritesStore } from "./FavoritesStore";
|
||||||
|
|
||||||
interface PendingCommand {
|
interface PendingCommand {
|
||||||
resolve: (value: any) => void;
|
resolve: (value: any) => void;
|
||||||
reject: (reason: any) => void;
|
reject: (reason: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UserEvent {
|
||||||
|
PlaylistUpdate = "playlist_update",
|
||||||
|
NowPlayingUpdate = "now_playing_update",
|
||||||
|
VolumeUpdate = "volume_update",
|
||||||
|
FavoritesUpdate = "favorites_update",
|
||||||
|
MetadataUpdate = "metadata_update",
|
||||||
|
MPDUpdate = "mpd_update",
|
||||||
|
}
|
||||||
|
|
||||||
export class MediaPlayer {
|
export class MediaPlayer {
|
||||||
private playerProcess: ChildProcess;
|
private playerProcess: ChildProcess;
|
||||||
private socket: Socket;
|
private socket: Socket;
|
||||||
@@ -43,7 +53,7 @@ export class MediaPlayer {
|
|||||||
|
|
||||||
this.favoritesStore = new FavoritesStore();
|
this.favoritesStore = new FavoritesStore();
|
||||||
this.favoritesStore.onFavoritesChanged = (favorites) => {
|
this.favoritesStore.onFavoritesChanged = (favorites) => {
|
||||||
this.handleEvent("favorites_update", { favorites });
|
this.handleEvent(UserEvent.FavoritesUpdate, { favorites });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,31 +133,31 @@ export class MediaPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async play() {
|
public async play() {
|
||||||
return this.modify(() => this.writeCommand("set_property", ["pause", false]));
|
return this.modify(UserEvent.NowPlayingUpdate, () => this.writeCommand("set_property", ["pause", false]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pause() {
|
public async pause() {
|
||||||
return this.modify(() => this.writeCommand("set_property", ["pause", true]));
|
return this.modify(UserEvent.NowPlayingUpdate, () => this.writeCommand("set_property", ["pause", true]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async skip() {
|
public async skip() {
|
||||||
return this.modify(() => this.writeCommand("playlist-next", []));
|
return this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("playlist-next", []));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async skipTo(index: number) {
|
public async skipTo(index: number) {
|
||||||
return this.modify(() => this.writeCommand("playlist-play-index", [index]));
|
return this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("playlist-play-index", [index]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async previous() {
|
public async previous() {
|
||||||
return this.modify(() => this.writeCommand("playlist-prev", []));
|
return this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("playlist-prev", []));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deletePlaylistItem(index: number) {
|
public async deletePlaylistItem(index: number) {
|
||||||
return this.modify(() => this.writeCommand("playlist-remove", [index]));
|
return this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("playlist-remove", [index]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setVolume(volume: number) {
|
public async setVolume(volume: number) {
|
||||||
return this.modify(() => this.writeCommand("set_property", ["volume", volume]));
|
return this.modify(UserEvent.VolumeUpdate, () => this.writeCommand("set_property", ["volume", volume]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscribe(ws: WebSocket) {
|
public subscribe(ws: WebSocket) {
|
||||||
@@ -175,18 +185,18 @@ export class MediaPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadFile(url: string, mode: string) {
|
private async loadFile(url: string, mode: string) {
|
||||||
this.modify(() => this.writeCommand("loadfile", [url, mode]));
|
this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode]));
|
||||||
|
|
||||||
this.fetchMetadataAndNotify(url).catch(error => {
|
this.fetchMetadataAndNotify(url).catch(error => {
|
||||||
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async modify<T>(func: () => Promise<T>): Promise<T> {
|
private async modify<T>(event: UserEvent, func: () => Promise<T>): Promise<T> {
|
||||||
return func()
|
return func()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
// Notify all subscribers
|
// Notify all subscribers
|
||||||
this.handleEvent("user_modify", {});
|
this.handleEvent(event, {});
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -230,7 +240,7 @@ export class MediaPlayer {
|
|||||||
console.log(this.metadata.get(url));
|
console.log(this.metadata.get(url));
|
||||||
|
|
||||||
// Notify clients that metadata has been updated
|
// Notify clients that metadata has been updated
|
||||||
this.handleEvent("metadata_update", {
|
this.handleEvent(UserEvent.MetadataUpdate, {
|
||||||
url,
|
url,
|
||||||
metadata: this.metadata.get(url)
|
metadata: this.metadata.get(url)
|
||||||
});
|
});
|
||||||
@@ -272,7 +282,7 @@ export class MediaPlayer {
|
|||||||
this.pendingCommands.delete(response.request_id);
|
this.pendingCommands.delete(response.request_id);
|
||||||
}
|
}
|
||||||
} else if (response.event) {
|
} else if (response.event) {
|
||||||
this.handleEvent(response.event, response);
|
this.handleEvent(UserEvent.MPDUpdate, response);
|
||||||
} else {
|
} else {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ export interface SearchResponse {
|
|||||||
results: SearchResult[];
|
results: SearchResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ServerEvent {
|
||||||
|
PlaylistUpdate = "playlist_update",
|
||||||
|
NowPlayingUpdate = "now_playing_update",
|
||||||
|
VolumeUpdate = "volume_update",
|
||||||
|
FavoritesUpdate = "favorites_update",
|
||||||
|
MetadataUpdate = "metadata_update",
|
||||||
|
MPDUpdate = "mpd_update",
|
||||||
|
}
|
||||||
|
|
||||||
export const API = {
|
export const API = {
|
||||||
async getPlaylist(): Promise<PlaylistItem[]> {
|
async getPlaylist(): Promise<PlaylistItem[]> {
|
||||||
const response = await fetch('/api/playlist');
|
const response = await fetch('/api/playlist');
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import SongTable from './SongTable';
|
|||||||
import NowPlaying from './NowPlaying';
|
import NowPlaying from './NowPlaying';
|
||||||
import AddSongPanel from './AddSongPanel';
|
import AddSongPanel from './AddSongPanel';
|
||||||
import { TabView, Tab } from './TabView';
|
import { TabView, Tab } from './TabView';
|
||||||
import { API, getDisplayTitle, PlaylistItem } from '../api/player';
|
import { API, getDisplayTitle, PlaylistItem, ServerEvent } from '../api/player';
|
||||||
import { useEventWebSocket } from '../hooks/useEventWebsocket';
|
|
||||||
import { FaMusic, FaHeart } from 'react-icons/fa';
|
import { FaMusic, FaHeart } from 'react-icons/fa';
|
||||||
|
import useWebSocket from 'react-use-websocket';
|
||||||
|
|
||||||
enum Tabs {
|
enum Tabs {
|
||||||
Playlist = "playlist",
|
Playlist = "playlist",
|
||||||
@@ -96,14 +96,17 @@ const App: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchNowPlaying = useCallback(async () => {
|
const fetchNowPlaying = useCallback(async () => {
|
||||||
|
if (volumeSettingIsLocked) {
|
||||||
|
// We are actively changing the volume, which we do actually want to send events
|
||||||
|
// continuously to the server, but we don't want to refresh our state while doing that.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nowPlaying = await API.getNowPlaying();
|
const nowPlaying = await API.getNowPlaying();
|
||||||
setNowPlayingSong(getDisplayTitle(nowPlaying.playingItem));
|
setNowPlayingSong(getDisplayTitle(nowPlaying.playingItem));
|
||||||
setNowPlayingFileName(nowPlaying.playingItem.filename);
|
setNowPlayingFileName(nowPlaying.playingItem.filename);
|
||||||
setIsPlaying(!nowPlaying.isPaused);
|
setIsPlaying(!nowPlaying.isPaused);
|
||||||
|
setVolume(nowPlaying.volume);
|
||||||
if (!volumeSettingIsLocked) {
|
|
||||||
setVolume(nowPlaying.volume);
|
|
||||||
}
|
|
||||||
}, [volumeSettingIsLocked]);
|
}, [volumeSettingIsLocked]);
|
||||||
|
|
||||||
const handleAddURL = async (url: string) => {
|
const handleAddURL = async (url: string) => {
|
||||||
@@ -148,23 +151,41 @@ const App: React.FC = () => {
|
|||||||
await API.setVolume(volume);
|
await API.setVolume(volume);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWebSocketEvent = useCallback((event: any) => {
|
const handleWebSocketEvent = useCallback((message: MessageEvent) => {
|
||||||
|
const event = JSON.parse(message.data);
|
||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case 'user_modify':
|
case ServerEvent.PlaylistUpdate:
|
||||||
case 'end-file':
|
case ServerEvent.NowPlayingUpdate:
|
||||||
case 'playback-restart':
|
case ServerEvent.MetadataUpdate:
|
||||||
case 'metadata_update':
|
case ServerEvent.MPDUpdate:
|
||||||
fetchPlaylist();
|
fetchPlaylist();
|
||||||
fetchNowPlaying();
|
fetchNowPlaying();
|
||||||
break;
|
break;
|
||||||
case 'favorites_update':
|
case ServerEvent.VolumeUpdate:
|
||||||
|
if (!volumeSettingIsLocked) {
|
||||||
|
fetchNowPlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ServerEvent.FavoritesUpdate:
|
||||||
fetchFavorites();
|
fetchFavorites();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
|
}, [fetchPlaylist, fetchNowPlaying, fetchFavorites]);
|
||||||
|
|
||||||
// Use the hook
|
useWebSocket('/api/events', {
|
||||||
useEventWebSocket(handleWebSocketEvent);
|
onOpen: () => {
|
||||||
|
console.log('WebSocket connected');
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
console.log('WebSocket disconnected');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
},
|
||||||
|
onMessage: handleWebSocketEvent,
|
||||||
|
shouldReconnect: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
// Handle visibility changes
|
// Handle visibility changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { API } from '../api/player';
|
|
||||||
|
|
||||||
export const useEventWebSocket = (onEvent: (event: any) => void) => {
|
|
||||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
|
||||||
const [wsReconnectTimeout, setWsReconnectTimeout] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const connectWebSocket = useCallback(() => {
|
|
||||||
// Clear any existing reconnection timeout
|
|
||||||
if (wsReconnectTimeout) {
|
|
||||||
window.clearTimeout(wsReconnectTimeout);
|
|
||||||
setWsReconnectTimeout(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close existing websocket if it exists
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const websocket = API.subscribeToEvents(onEvent);
|
|
||||||
|
|
||||||
websocket.addEventListener('close', () => {
|
|
||||||
console.log('WebSocket closed. Attempting to reconnect...');
|
|
||||||
|
|
||||||
// Attempt to reconnect after 2 seconds
|
|
||||||
const timeout = window.setTimeout(() => {
|
|
||||||
connectWebSocket();
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
setWsReconnectTimeout(timeout);
|
|
||||||
});
|
|
||||||
|
|
||||||
websocket.addEventListener('error', (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
websocket.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
setWs(websocket);
|
|
||||||
}, [wsReconnectTimeout, onEvent]);
|
|
||||||
|
|
||||||
// Check WebSocket health periodically
|
|
||||||
useEffect(() => {
|
|
||||||
const healthCheck = setInterval(() => {
|
|
||||||
if (!ws || ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
|
|
||||||
console.log('WebSocket unhealthy, reconnecting...');
|
|
||||||
connectWebSocket();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(healthCheck);
|
|
||||||
};
|
|
||||||
}, [ws, connectWebSocket]);
|
|
||||||
|
|
||||||
// Initial WebSocket connection
|
|
||||||
useEffect(() => {
|
|
||||||
connectWebSocket();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
if (wsReconnectTimeout) {
|
|
||||||
window.clearTimeout(wsReconnectTimeout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [connectWebSocket]);
|
|
||||||
|
|
||||||
return ws;
|
|
||||||
};
|
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -10,7 +10,10 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"backend",
|
"backend",
|
||||||
"frontend"
|
"frontend"
|
||||||
]
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"react-use-websocket": "^4.13.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"name": "mpvqueue",
|
"name": "mpvqueue",
|
||||||
@@ -4685,6 +4688,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-use-websocket": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz",
|
||||||
|
"integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
|||||||
@@ -8,5 +8,8 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"backend",
|
"backend",
|
||||||
"frontend"
|
"frontend"
|
||||||
]
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"react-use-websocket": "^4.13.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user