initial commit

This commit is contained in:
2025-02-15 00:37:32 -08:00
commit 91bc10c395
5 changed files with 398 additions and 0 deletions

169
src/MediaPlayer.ts Normal file
View File

@@ -0,0 +1,169 @@
import { ChildProcess, spawn } from "child_process";
import { Socket } from "net";
import { WebSocket } from "ws";
interface PendingCommand {
resolve: (value: any) => void;
reject: (reason: any) => void;
}
export class MediaPlayer {
private playerProcess: ChildProcess;
private socket: Socket;
private eventSubscribers: WebSocket[] = [];
private pendingCommands: Map<number, PendingCommand> = new Map();
private requestId: number = 1;
private dataBuffer: string = '';
constructor() {
console.log("Starting player process");
this.playerProcess = spawn("mpv", [
"--no-video",
"--no-terminal",
"--idle=yes",
"--input-ipc-server=/tmp/mpv-socket"
]);
this.socket = new Socket();
this.playerProcess.on("spawn", () => {
console.log("Player process spawned, opening socket");
setTimeout(() => {
this.connectToSocket();
}, 200);
});
}
public async getPlaylist(): Promise<any> {
return this.writeCommand("get_property", ["playlist"])
.then((response) => {
return response.data;
});
}
public async getNowPlaying(): Promise<string> {
return this.writeCommand("get_property", ["media-title"])
.then((response) => {
return response.data;
});
}
public async getPauseState(): Promise<boolean> {
return this.writeCommand("get_property", ["pause"])
.then((response) => {
return response.data;
});
}
public async getVolume(): Promise<number> {
return this.writeCommand("get_property", ["volume"])
.then((response) => {
return response.data;
});
}
public async setVolume(volume: number) {
return this.writeCommand("set_property", ["volume", volume]);
}
public async getIdle(): Promise<boolean> {
return this.writeCommand("get_property", ["idle"])
.then((response) => {
return response.data;
});
}
public async append(url: string) {
return this.writeCommand("loadfile", [url, "append-play"]);
}
public async play() {
return this.writeCommand("set_property", ["pause", false]);
}
public async pause() {
return this.writeCommand("set_property", ["pause", true]);
}
public async deletePlaylistItem(index: number) {
return this.writeCommand("playlist-remove", [index]);
}
public subscribe(ws: WebSocket) {
this.eventSubscribers.push(ws);
}
public unsubscribe(ws: WebSocket) {
this.eventSubscribers = this.eventSubscribers.filter(subscriber => subscriber !== ws);
}
private async writeCommand(command: string, args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
const id = this.requestId++;
const commandObject = JSON.stringify({
command: [command, ...args],
request_id: id
});
this.pendingCommands.set(id, { resolve, reject });
this.socket.write(commandObject + '\n');
// Add timeout to prevent hanging promises
setTimeout(() => {
if (this.pendingCommands.has(id)) {
const pending = this.pendingCommands.get(id);
if (pending) {
pending.reject(new Error('Command timed out'));
this.pendingCommands.delete(id);
}
}
}, 5000);
});
}
private connectToSocket() {
this.socket.connect("/tmp/mpv-socket");
this.socket.on("data", data => this.receiveData(data.toString()));
}
private handleEvent(event: string, data: any) {
console.log("Event [" + event + "]: " + JSON.stringify(data, null, 2));
// Notify all subscribers
this.eventSubscribers.forEach(subscriber => {
subscriber.send(JSON.stringify({ event, data }));
});
}
private receiveData(data: string) {
this.dataBuffer += data;
const lines = this.dataBuffer.split('\n');
// Keep last incomplete line in the buffer
this.dataBuffer = lines.pop() || '';
for (const line of lines) {
if (line.trim().length > 0) {
try {
const response = JSON.parse(line);
if (response.request_id) {
const pending = this.pendingCommands.get(response.request_id);
if (pending) {
pending.resolve(response);
this.pendingCommands.delete(response.request_id);
}
} else if (response.event) {
this.handleEvent(response.event, response.data);
} else {
console.log(response);
}
} catch (error) {
console.error('Error parsing JSON:', error);
}
}
}
}
}

93
src/server.ts Normal file
View File

@@ -0,0 +1,93 @@
import express from "express";
import expressWs from "express-ws";
import { MediaPlayer } from "./MediaPlayer";
interface PlaylistAppendRequest {
url: string;
}
const app = express();
expressWs(app);
app.use(express.json());
const router = express.Router();
const mediaPlayer = new MediaPlayer();
router.get("/playlist", async (req, res) => {
const playlist = await mediaPlayer.getPlaylist();
res.send(playlist);
});
router.post("/playlist", async (req, res) => {
try {
const { url } = req.body as PlaylistAppendRequest;
await mediaPlayer.append(url);
res.send(JSON.stringify({ success: true }));
} catch (error: any) {
res.status(500)
.send(JSON.stringify({ success: false, error: error.message }));
}
});
router.delete("/playlist/:index", async (req, res) => {
const { index } = req.params as { index: string };
await mediaPlayer.deletePlaylistItem(parseInt(index));
res.send(JSON.stringify({ success: true }));
});
router.post("/play", async (req, res) => {
try {
await mediaPlayer.play();
res.send(JSON.stringify({ success: true }));
} catch (error: any) {
res.status(500)
.send(JSON.stringify({ success: false, error: error.message }));
}
});
router.post("/pause", async (req, res) => {
try {
await mediaPlayer.pause();
res.send(JSON.stringify({ success: true }));
} catch (error: any) {
res.status(500)
.send(JSON.stringify({ success: false, error: error.message }));
}
});
router.get("/nowplaying", async (req, res) => {
const nowPlaying = await mediaPlayer.getNowPlaying();
const pauseState = await mediaPlayer.getPauseState();
const volume = await mediaPlayer.getVolume();
const idle = await mediaPlayer.getIdle();
res.send(JSON.stringify({
success: true,
nowPlaying: nowPlaying,
isPaused: pauseState,
volume: volume,
isIdle: idle
}));
});
router.post("/volume", async (req, res) => {
const { volume } = req.body as { volume: number };
await mediaPlayer.setVolume(volume);
res.send(JSON.stringify({ success: true }));
});
router.ws("/events", (ws, req) => {
console.log(req.query);
mediaPlayer.subscribe(ws);
ws.on("close", () => {
mediaPlayer.unsubscribe(ws);
});
});
app.use("/", router);
app.listen(3000, () => {
console.log("Server is running on port 3000");
});