/* * 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 #include #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); if (title.isEmpty()) { // If there's no tag, use the filename QFileInfo fi(uri); title = fi.fileName(); } //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); if (title.isEmpty()) { // If there's no tag, use the filename QFileInfo fi(uri); title = fi.fileName(); } //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; }