summaryrefslogtreecommitdiffstats
path: root/mediaplayer/MpdEventHandler.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'mediaplayer/MpdEventHandler.cpp')
-rw-r--r--mediaplayer/MpdEventHandler.cpp309
1 files changed, 309 insertions, 0 deletions
diff --git a/mediaplayer/MpdEventHandler.cpp b/mediaplayer/MpdEventHandler.cpp
new file mode 100644
index 0000000..40d2999
--- /dev/null
+++ b/mediaplayer/MpdEventHandler.cpp
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDebug>
+#include "MpdEventHandler.h"
+
+MpdEventHandler::MpdEventHandler(QObject *parent) :
+ QObject(parent)
+{
+ // NOTE: Not specifying a timeout here as it is assumed to be long
+ // enough that we won't timeout in the brief intervals when
+ // switching in and out of idle mode.
+ struct mpd_connection *conn = mpd_connection_new(NULL, 0, 0);
+ if (!conn) {
+ qFatal("Could not create MPD connection");
+ }
+ if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) {
+ qFatal("%s", mpd_connection_get_error_message(conn));
+ }
+ m_mpd_conn = conn;
+}
+
+MpdEventHandler::~MpdEventHandler()
+{
+ mpd_connection_free(m_mpd_conn);
+}
+
+void MpdEventHandler::handleEvents(void)
+{
+ bool done = false;
+ enum mpd_idle mask = (enum mpd_idle)(MPD_IDLE_DATABASE|MPD_IDLE_QUEUE|MPD_IDLE_PLAYER);
+ enum mpd_idle events;
+ while (!done) {
+ // Wait for an event
+ events = mpd_run_idle_mask(m_mpd_conn, mask);
+
+ // NOTE: Realistically should be checking the result of
+ // mpd_connection_get_error here, but handling it will
+ // complicate things significantly.
+
+ // handle the events
+ if (events & MPD_IDLE_DATABASE) {
+ handleDatabaseEvent();
+ }
+ else if (events & MPD_IDLE_QUEUE) {
+ handleQueueEvent();
+ }
+ else if (events & MPD_IDLE_PLAYER) {
+ handlePlayerEvent();
+ }
+ }
+
+ qDebug() << "MpdEventHandler::handleEvents: exit";
+}
+
+void MpdEventHandler::handleDatabaseEvent(void)
+{
+ // Maybe clear queue here?
+ //mpd_run_delete_range(m_mpd_con, 0, UINT_MAX);
+
+ if(!mpd_run_add(m_mpd_conn, "/")) {
+ qWarning() << "mpd_run_add failed";
+ }
+}
+
+void MpdEventHandler::handleQueueEvent(void)
+{
+ QVariantList playlist;
+ bool done = false;
+ struct mpd_entity *entity;
+ mpd_send_list_queue_meta(m_mpd_conn);
+ do {
+ entity = mpd_recv_entity(m_mpd_conn);
+ if (!entity) {
+ enum mpd_error error = mpd_connection_get_error(m_mpd_conn);
+ if (error == MPD_ERROR_SUCCESS)
+ done = true;
+ break;
+ }
+ if (mpd_entity_get_type(entity) != MPD_ENTITY_TYPE_SONG) {
+ mpd_entity_free(entity);
+ continue;
+ }
+ const struct mpd_song *song = mpd_entity_get_song(entity);
+ QString title(mpd_song_get_tag(song, MPD_TAG_TITLE, 0));
+ QString artist(mpd_song_get_tag(song, MPD_TAG_ARTIST, 0));
+ QString album(mpd_song_get_tag(song, MPD_TAG_ALBUM, 0));
+ QString genre(mpd_song_get_tag(song, MPD_TAG_GENRE, 0));
+ QString uri(mpd_song_get_uri(song));
+ int pos = mpd_song_get_pos(song);
+ //qDebug() << "Queue[" << pos << "]: " << artist << " - " << title << " / " << album << ", genre " << genre;
+
+ QVariantMap track;
+ track["title"] = title;
+ track["artist"] = artist;
+ track["album"] = album;
+ track["genre"] = genre;
+ track["index"] = pos;
+ track["duration"] = mpd_song_get_duration_ms(song);
+ track["path"] = uri;
+ playlist.append(track);
+
+ mpd_entity_free(entity);
+ } while(!done);
+
+ if (done) {
+ QVariantMap metadata;
+ metadata["list"] = playlist;
+
+ emit playlistUpdate(metadata);
+ }
+}
+
+void MpdEventHandler::handlePlayerEvent(void)
+{
+ int pos = -1;
+ QString uri;
+ QVariantMap track;
+ QVariantMap metadata;
+ struct mpd_song* song = mpd_run_current_song(m_mpd_conn);
+ if (song) {
+ QString title(mpd_song_get_tag(song, MPD_TAG_TITLE, 0));
+ QString artist(mpd_song_get_tag(song, MPD_TAG_ARTIST, 0));
+ QString album(mpd_song_get_tag(song, MPD_TAG_ALBUM, 0));
+ QString genre(mpd_song_get_tag(song, MPD_TAG_GENRE, 0));
+ pos = mpd_song_get_pos(song);
+ uri = mpd_song_get_uri(song);
+ //qDebug() << "Current song[" << pos << "]: " << artist << " - " << title << " / " << album << ", genre " << genre;
+
+ track["title"] = title;
+ track["artist"] = artist;
+ track["album"] = album;
+ track["genre"] = genre;
+ track["index"] = pos;
+ track["duration"] = mpd_song_get_duration_ms(song);
+ track["path"] = uri;
+ mpd_song_free(song);
+
+ metadata["track"] = track;
+ }
+ // NOTE:
+ // It may make sense to check status first, and then try to handle the
+ // no song + stopped case at the end of the queue to trigger a move to
+ // the top of the playlist in the UI if that is deemed desirable (the
+ // old binding does not seem to). However, this may prove a bit
+ // complicated if triggering stop instead of pause is done to trigger
+ // corking in WirePlumber...
+
+ struct mpd_status *status = mpd_run_status(m_mpd_conn);
+
+ int elapsed_ms = mpd_status_get_elapsed_ms(status);
+ metadata["position"] = elapsed_ms;
+
+ int volume = mpd_status_get_volume(status);
+ metadata["volume"] = volume == -1 ? 0 : volume;
+
+ // NOTE: current UI client user does not care about paused vs stopped,
+ // and the old binding did not differentiate in its responses,
+ // so do not do so either for now.
+ enum mpd_state state = mpd_status_get_state(status);
+ QString status_name("stopped");
+ if (state == MPD_STATE_PLAY)
+ status_name = "playing";
+ metadata["status"] = status_name;
+
+ mpd_status_free(status);
+
+ // For UI
+ emit metadataUpdate(metadata);
+
+ // For backend state tracking
+ emit playbackStateUpdate(pos, elapsed_ms, (state == MPD_STATE_PLAY));
+
+ if (uri.size()) {
+ // Send album art to UI as a separate update.
+ // This avoids things being out of sync than delaying while
+ // the art is is read.
+ // NOTE: Some form of caching might be desirable here.
+ QByteArray buffer;
+ QString mime_type;
+ if (getSongArt(uri, buffer, mime_type) && mime_type.size()) {
+ QString image_base64(buffer.toBase64());
+ QString mime_type_header("data:");
+ mime_type_header += mime_type;
+ mime_type_header += ";base64,";
+ image_base64.prepend(mime_type_header);
+
+ // Re-use metadata map...
+ track["image"] = image_base64;
+ metadata["track"] = track;
+
+ // ...but clear out the ephemeral metadata
+ metadata.remove("position");
+ metadata.remove("status");
+ metadata.remove("volume");
+
+ // Update UI
+ emit metadataUpdate(metadata);
+ }
+ }
+}
+
+bool MpdEventHandler::getSongArt(const QString &path, QByteArray &buffer, QString &type)
+{
+ bool rc = false;
+ unsigned pic_offset = 0;
+ unsigned pic_size = 0;
+
+ QByteArray path_ba = path.toLocal8Bit();
+ const char *path_cstr = path_ba.data();
+
+ bool first = true;
+ do {
+ QString pic_offset_str = QString::number(pic_offset);
+ QByteArray pic_offset_ba = pic_offset_str.toLocal8Bit();
+ const char *pic_offset_cstr = pic_offset_ba.data();
+
+ if (!mpd_send_command(m_mpd_conn, "readpicture", path_cstr, pic_offset_cstr, NULL)) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS)
+ goto conn_error;
+ }
+ struct mpd_pair *pair = mpd_recv_pair_named(m_mpd_conn, "size");
+ if (!pair) {
+ if (first) {
+ // No art, exit
+ break;
+ }
+
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS)
+ goto conn_error;
+ }
+ pic_size = QString(pair->value).toInt();
+ mpd_return_pair(m_mpd_conn, pair);
+
+ pair = mpd_recv_pair_named(m_mpd_conn, "type");
+ if (!pair) {
+ // check for error
+ }
+ QString mime_type(pair->value);
+ mpd_return_pair(m_mpd_conn, pair);
+ if (first) {
+ if (mime_type.size()) {
+ type = mime_type;
+ } else {
+ break;
+ }
+ first = false;
+ }
+
+ pair = mpd_recv_pair_named(m_mpd_conn, "binary");
+ if (!pair) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS)
+ goto conn_error;
+ }
+
+ unsigned chunk_size = QString(pair->value).toInt();
+ mpd_return_pair(m_mpd_conn, pair);
+
+ if (!chunk_size)
+ break;
+
+ char *buf = new (std::nothrow) char[chunk_size];
+ if (!buf)
+ goto conn_error;
+
+ if (!mpd_recv_binary(m_mpd_conn, buf, chunk_size)) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS) {
+ delete[] buf;
+ goto conn_error;
+ }
+ }
+
+ if (!mpd_response_finish(m_mpd_conn)) {
+ if (mpd_connection_get_error(m_mpd_conn) != MPD_ERROR_SUCCESS) {
+ delete[] buf;
+ goto conn_error;
+ }
+ }
+
+ buffer.append(buf, chunk_size);
+ delete[] buf;
+
+ pic_offset += chunk_size;
+
+ } while (pic_offset < pic_size);
+
+conn_error:
+ if (pic_offset == pic_size) {
+ rc = true;
+ } else {
+ // Don't pass garbage to caller
+ buffer.clear();
+ }
+
+ return rc;
+}