ui/services/mpd.js

362 lines
7.7 KiB
JavaScript

import Gio from "gi://Gio";
Gio._promisify(Gio.DataInputStream.prototype, "read_line_async");
class Mpd extends Service {
static {
Service.register(
this,
{},
{
//TODO: parse some properties like duration into number?
partition: ["string", "r"],
volume: ["string", "r"],
repeat: ["string", "r"],
random: ["string", "r"],
single: ["string", "r"],
consume: ["string", "r"],
playlist: ["string", "r"],
playlistlength: ["string", "r"],
state: ["string", "r"],
song: ["string", "r"],
songid: ["string", "r"],
nextsong: ["string", "r"],
nextsongid: ["string", "r"],
elapsed: ["string", "r"],
duration: ["string", "r"],
bitrate: ["string", "r"],
mixrampdb: ["string", "r"],
audio: ["string", "r"],
file: ["string", "r"],
"Last-Modified": ["string", "r"],
Artist: ["string", "r"],
Title: ["string", "r"],
Album: ["string", "r"],
Pos: ["string", "r"],
Id: ["string", "r"],
},
);
}
#socket;
#inputStream;
#outputStream;
_decoder = new TextDecoder();
_encoder = new TextEncoder();
_messageHandlerQueue = [];
//TODO: more properties?
// Status
_partition;
_volume;
_repeat;
_random;
_single;
_consume;
_playlist;
_playlistlength;
_state;
_song;
_songid;
_nextsong;
_nextsongid;
_elapsed;
_duration;
_bitrate;
_mixrampdb;
_audio;
_file;
_LastModified;
_Artist;
_Title;
_Album;
_Pos;
_Id;
get partition() {
return this._partition;
}
get volume() {
return this._volume;
}
get repeat() {
return this._repeat;
}
get random() {
return this._random;
}
get single() {
return this._single;
}
get consume() {
return this._consume;
}
get playlist() {
return this._playlist;
}
get playlistlength() {
return this._playlistlength;
}
get state() {
return this._state;
}
get song() {
return this._song;
}
get songid() {
return this._songid;
}
get nextsong() {
return this._nextsong;
}
get nextsongid() {
return this._nextsongid;
}
get elapsed() {
return this._elapsed;
}
get duration() {
return this._duration;
}
get bitrate() {
return this._bitrate;
}
get mixrampdb() {
return this._mixrampdb;
}
get audio() {
return this._audio;
}
get file() {
return this._file;
}
get Last_Modified() {
return this._LastModified;
}
get Artist() {
return this._Artist;
}
get Title() {
return this._Title;
}
get Album() {
return this._Album;
}
get Pos() {
return this._Pos;
}
get Id() {
return this._Id;
}
constructor() {
super();
this._initSocket();
}
async _initSocket() {
try {
this.#socket = new Gio.SocketClient().connect_to_host(
"localhost",
6600,
null,
);
this.#inputStream = new Gio.DataInputStream({
base_stream: this.#socket.get_input_stream(),
});
this.#outputStream = new Gio.DataOutputStream({
base_stream: this.#socket.get_output_stream(),
});
this._watchSocket();
//init properties
//[TODO): init more properties?
this.send("status")
.then(this._updateProperties.bind(this))
.catch(logError);
this.send("currentsong")
.then(this._updateProperties.bind(this))
.catch(logError);
} catch (e) {
logError(e);
}
}
async _watchSocket() {
let bufferedLines = [];
while (true) {
const [rawData] = await this.#inputStream.read_line_async(0, null);
const data = this._decoder.decode(rawData);
if (data == null) continue;
bufferedLines.push(data);
const { response, remain } = this._parseResponse(bufferedLines);
bufferedLines = remain;
if (!response) continue;
switch (response.type) {
case "version":
console.log(`MPD Server Version ${response.payload}`);
break;
case "error":
this._handleMessage(new Error(response.payload), null);
break;
case "data":
this._handleMessage(null, response.payload);
break;
}
}
}
_parseResponse(lines) {
let response;
let beginLine = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const version = line.match(/^OK MPD (.+)/);
const error = line.match(/^ACK \[.*] {.*} (.+)/);
if (version) {
response = { type: "version", payload: version[1] };
beginLine = i + 1;
} else if (error) {
response = { type: "error", payload: error[1] };
beginLine = i + 1;
} else if (line === "OK") {
response = {
type: "data",
payload: lines.slice(beginLine, i).join("\n"),
};
beginLine = i + 1;
}
}
return { response, remain: lines.slice(beginLine) };
}
_handleMessage(err, msg) {
const { func } = this._messageHandlerQueue.shift();
func(err, msg);
if (this._messageHandlerQueue.length === 0) {
this._idle();
}
}
async send(data) {
data = data.trim();
const isIdle = data === "idle";
if (this._messageHandlerQueue[0]?.isIdle) {
this.#outputStream.write(this._encoder.encode("noidle\n"), null);
}
this.#outputStream.write(this._encoder.encode(`${data}\n`), null);
return new Promise((resolve, reject) => {
this._messageHandlerQueue.push({
isIdle,
func: (err, msg) => {
if (err != null) reject(err);
resolve(msg);
},
});
});
}
_idle() {
this.send("idle")
.then((msg) => {
for (const line of msg.split("\n")) {
const subsystem = /changed: (\w+)/.exec(line);
if (subsystem == null) continue;
//TODO: only update those things that could have
//changed using a switch over the subsystems
this.send("status")
.then(this._updateProperties.bind(this))
.catch(logError);
this.send("currentsong")
.then(this._updateProperties.bind(this))
.catch(logError);
/*
switch(subsystem[1]) {
case "player":
break;
}*/
}
})
.catch(logError);
}
_updateProperties(msg) {
for (const line of msg.split("\n")) {
const keyValue = line.match(/(.*): (.*)/);
if (keyValue == null) continue;
const deprecatedKeys = [
"time",
"Time", //deprecated
"Format", //same as audio
];
if (deprecatedKeys.includes(keyValue[1])) continue;
if (!this.hasOwnProperty(`_${keyValue[1]}`)) continue;
this.updateProperty(keyValue[1], keyValue[2]);
this.emit("changed");
}
}
setCrossfade = (seconds) => this.send(`crossfade ${seconds}`);
setVolume = (volume) => this.send(`setvol ${volume}`);
toggleShuffle = () => this.send(`random ${+this._random ? "0" : "1"}`);
toggleRepeat = () => this.send(`repeat ${+this._repeat ? "0" : "1"}`);
next = () => this.send("next");
playPause = () => this.send(`pause ${this._state === "pause" ? "0" : "1"}`);
pause = () => this.send("pause 1");
play = () => this.send("pause 0");
playSong = (songpos) => this.send(`play ${songpos}`);
playSongId = (songid) => this.send(`playid ${songid}`);
seekSong = (songpos, time) => this.send(`seek ${songpos} ${time}`);
seekSongId = (songid, time) => this.send(`seekid ${songid} ${time}`);
seekCur = (time) => this.send(`seekcur ${time}`);
previous = () => this.send("previous");
stop = () => this.send("stop");
clearQueue = () => this.send("clear");
}
const service = new Mpd;
export default service;