File indexing completed on 2025-02-23 04:34:22
0001 /** 0002 * \file mprisinterface.cpp 0003 * MPRIS D-Bus interface for audio player. 0004 * 0005 * \b Project: Kid3 0006 * \author Urs Fleisch 0007 * \date 09-Dec-2016 0008 * 0009 * Copyright (C) 2016-2024 Urs Fleisch 0010 * 0011 * This file is part of Kid3. 0012 * 0013 * Kid3 is free software; you can redistribute it and/or modify 0014 * it under the terms of the GNU General Public License as published by 0015 * the Free Software Foundation; either version 2 of the License, or 0016 * (at your option) any later version. 0017 * 0018 * Kid3 is distributed in the hope that it will be useful, 0019 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0020 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0021 * GNU General Public License for more details. 0022 * 0023 * You should have received a copy of the GNU General Public License 0024 * along with this program. If not, see <http://www.gnu.org/licenses/>. 0025 */ 0026 0027 #include "mprisinterface.h" 0028 0029 #ifdef HAVE_QTDBUS 0030 0031 #include <QDBusMessage> 0032 #include <QDBusConnection> 0033 #include <QDBusObjectPath> 0034 #include <QUrl> 0035 #include <QDir> 0036 #include <QTemporaryFile> 0037 #include <QCoreApplication> 0038 #include "audioplayer.h" 0039 #include "taggedfile.h" 0040 #include "trackdata.h" 0041 #include "pictureframe.h" 0042 0043 MprisInterface::MprisInterface(AudioPlayer* player) 0044 : QDBusAbstractAdaptor(player), m_audioPlayer(player) 0045 { 0046 } 0047 0048 QString MprisInterface::identity() const 0049 { 0050 return QLatin1String("Kid3"); 0051 } 0052 0053 QString MprisInterface::desktopEntry() const 0054 { 0055 // Organization domain is only set in the KDE application. 0056 return QCoreApplication::organizationDomain().isEmpty() 0057 ? QLatin1String("kid3-qt") : QLatin1String("kid3"); 0058 } 0059 0060 QStringList MprisInterface::supportedUriSchemes() const 0061 { 0062 return {QLatin1String("file")}; 0063 } 0064 0065 QStringList MprisInterface::supportedMimeTypes() const 0066 { 0067 return { 0068 QLatin1String("audio/mpeg"), 0069 QLatin1String("audio/ogg"), 0070 QLatin1String("application/ogg"), 0071 QLatin1String("audio/x-flac"), 0072 QLatin1String("audio/x-flac+ogg"), 0073 QLatin1String("audio/x-vorbis+ogg"), 0074 QLatin1String("audio/x-speex+ogg"), 0075 QLatin1String("audio/x-oggflac"), 0076 QLatin1String("audio/x-musepack"), 0077 QLatin1String("audio/aac"), 0078 QLatin1String("audio/mp4"), 0079 QLatin1String("audio/x-speex"), 0080 QLatin1String("audio/x-tta"), 0081 QLatin1String("audio/x-wavpack"), 0082 QLatin1String("audio/x-aiff"), 0083 QLatin1String("audio/x-it"), 0084 QLatin1String("audio/x-mod"), 0085 QLatin1String("audio/x-s3m"), 0086 QLatin1String("audio/x-ms-wma"), 0087 QLatin1String("audio/x-wav"), 0088 QLatin1String("audio/x-xm"), 0089 QLatin1String("audio/opus"), 0090 QLatin1String("audio/x-opus+ogg"), 0091 QLatin1String("audio/x-dsf") 0092 }; 0093 } 0094 0095 0096 MprisPlayerInterface::MprisPlayerInterface(AudioPlayer* player) 0097 : QDBusAbstractAdaptor(player), m_audioPlayer(player), 0098 m_hasPrevious(false), m_hasNext(false), 0099 m_hasFiles(m_audioPlayer->getFileCount() > 0), 0100 m_tempCoverArtFile(nullptr) 0101 { 0102 connect(m_audioPlayer, &AudioPlayer::stateChanged, 0103 this, &MprisPlayerInterface::onStateChanged); 0104 connect(m_audioPlayer, &AudioPlayer::trackChanged, 0105 this, &MprisPlayerInterface::onTrackChanged); 0106 connect(m_audioPlayer, &AudioPlayer::volumeChanged, 0107 this, &MprisPlayerInterface::onVolumeChanged); 0108 connect(m_audioPlayer, &AudioPlayer::fileCountChanged, 0109 this, &MprisPlayerInterface::onFileCountChanged); 0110 connect(m_audioPlayer, &AudioPlayer::currentPositionChanged, 0111 this, &MprisPlayerInterface::onCurrentPositionChanged); 0112 } 0113 0114 MprisPlayerInterface::~MprisPlayerInterface() 0115 { 0116 if (m_tempCoverArtFile) { 0117 m_tempCoverArtFile->deleteLater(); 0118 } 0119 } 0120 0121 void MprisPlayerInterface::Next() 0122 { 0123 m_audioPlayer->next(); 0124 } 0125 0126 void MprisPlayerInterface::Previous() 0127 { 0128 m_audioPlayer->previous(); 0129 } 0130 0131 void MprisPlayerInterface::Pause() 0132 { 0133 m_audioPlayer->pause(); 0134 } 0135 0136 void MprisPlayerInterface::PlayPause() 0137 { 0138 m_audioPlayer->playOrPause(); 0139 } 0140 0141 void MprisPlayerInterface::Stop() 0142 { 0143 m_audioPlayer->stop(); 0144 } 0145 0146 void MprisPlayerInterface::Play() 0147 { 0148 m_audioPlayer->play(); 0149 } 0150 0151 void MprisPlayerInterface::Seek(qlonglong offsetUs) 0152 { 0153 qlonglong posMs = m_audioPlayer->getCurrentPosition() + offsetUs / 1000; 0154 if (posMs < 0) { 0155 posMs = 0; 0156 } 0157 0158 if (qint64 duration = m_audioPlayer->getDuration(); 0159 duration < 0 || posMs <= duration) { 0160 m_audioPlayer->setCurrentPosition(posMs); 0161 } else { 0162 m_audioPlayer->next(); 0163 } 0164 } 0165 0166 void MprisPlayerInterface::SetPosition(const QDBusObjectPath& trackId, 0167 qlonglong positionUs) 0168 { 0169 if (trackId == getCurrentTrackId() && positionUs >= 0) { 0170 qlonglong posMs = positionUs / 1000; 0171 if (qlonglong duration = m_audioPlayer->getDuration(); 0172 duration < 0 || posMs <= duration) { 0173 m_audioPlayer->setCurrentPosition(posMs); 0174 } 0175 } 0176 } 0177 0178 void MprisPlayerInterface::OpenUri(const QString& uri) 0179 { 0180 m_audioPlayer->setFiles({QUrl(uri).toLocalFile()}); 0181 } 0182 0183 0184 QString MprisPlayerInterface::playbackStatus() const 0185 { 0186 QString status; 0187 switch (m_audioPlayer->getState()) { 0188 case AudioPlayer::PlayingState: 0189 status = QLatin1String("Playing"); 0190 break; 0191 case AudioPlayer::PausedState: 0192 status = QLatin1String("Paused"); 0193 break; 0194 case AudioPlayer::StoppedState: 0195 default: 0196 status = QLatin1String("Stopped"); 0197 break; 0198 } 0199 return status; 0200 } 0201 0202 QVariantMap MprisPlayerInterface::metadata() const 0203 { 0204 QVariantMap map; 0205 if (QString filePath = m_audioPlayer->getFileName(); !filePath.isEmpty()) { 0206 map.insert(QLatin1String("mpris:trackid"), 0207 QVariant::fromValue<QDBusObjectPath>(getCurrentTrackId())); 0208 qint64 duration = m_audioPlayer->getDuration(); 0209 map.insert(QLatin1String("xesam:url"), QUrl::fromLocalFile(filePath).toString()); 0210 if (TaggedFile* taggedFile = m_audioPlayer->getTaggedFile()) { 0211 // https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ 0212 taggedFile->readTags(false); 0213 const TrackData trackData(*taggedFile, Frame::TagVAll); 0214 0215 // Phonon often returns a duration of -1 or from the last track. 0216 // In such cases, get the duration from the tagged file and convert it to 0217 // milliseconds. 0218 if (qint64 seconds = taggedFile->getDuration(); 0219 (duration < 0 || duration / 1000 != seconds) && seconds > 0) { 0220 duration = seconds * 1000; 0221 } 0222 0223 QString artPath; 0224 QStringList albumArtists, artists, comments, composers, genres, lyricists; 0225 for (auto it = trackData.cbegin(); it != trackData.cend(); ++it) { 0226 switch (const Frame& frame = *it; frame.getType()) { 0227 case Frame::FT_Album: 0228 map.insert(QLatin1String("xesam:album"), frame.getValue()); 0229 break; 0230 case Frame::FT_AlbumArtist: 0231 albumArtists.append(frame.getValue()); 0232 break; 0233 case Frame::FT_Artist: 0234 artists.append(frame.getValue()); 0235 break; 0236 case Frame::FT_Lyrics: 0237 map.insert(QLatin1String("xesam:asText"), frame.getValue()); 0238 break; 0239 case Frame::FT_Bpm: 0240 if (int bpm = frame.getValue().toInt()) { 0241 map.insert(QLatin1String("xesam:audioBPM"), bpm); 0242 } 0243 break; 0244 case Frame::FT_Comment: 0245 comments.append(frame.getValue()); 0246 break; 0247 case Frame::FT_Composer: 0248 composers.append(frame.getValue()); 0249 break; 0250 case Frame::FT_Date: 0251 map.insert(QLatin1String("xesam:contentCreated"), frame.getValue()); 0252 break; 0253 case Frame::FT_Disc: 0254 if (int disc = frame.getValue().toInt()) { 0255 map.insert(QLatin1String("xesam:discNumber"), disc); 0256 } 0257 break; 0258 case Frame::FT_Genre: 0259 genres.append(frame.getValue()); 0260 break; 0261 case Frame::FT_Lyricist: 0262 lyricists.append(frame.getValue()); 0263 break; 0264 case Frame::FT_Title: 0265 map.insert(QLatin1String("xesam:title"), frame.getValue()); 0266 break; 0267 case Frame::FT_Track: 0268 if (int track = frame.getValue().toInt()) { 0269 map.insert(QLatin1String("xesam:tracknumber"), track); 0270 } 0271 break; 0272 case Frame::FT_Picture: 0273 if (artPath.isEmpty()) { 0274 if (QByteArray data; PictureFrame::getData(frame, data)) { 0275 if (m_tempCoverArtFile) 0276 m_tempCoverArtFile->deleteLater(); 0277 m_tempCoverArtFile = new QTemporaryFile; 0278 m_tempCoverArtFile->open(); 0279 m_tempCoverArtFile->write(data); 0280 artPath = m_tempCoverArtFile->fileName(); 0281 m_tempCoverArtFile->close(); 0282 } 0283 } 0284 break; 0285 default: 0286 break; 0287 } 0288 } 0289 if (artPath.isEmpty()) { 0290 artPath = findCoverArtInDirectory(taggedFile->getDirname()); 0291 } 0292 if (!albumArtists.isEmpty()) 0293 map.insert(QLatin1String("xesam:albumArtist"), albumArtists); 0294 if (!artists.isEmpty()) 0295 map.insert(QLatin1String("xesam:artist"), artists); 0296 if (!comments.isEmpty()) 0297 map.insert(QLatin1String("xesam:comment"), comments); 0298 if (!composers.isEmpty()) 0299 map.insert(QLatin1String("xesam:composer"), composers); 0300 if (!genres.isEmpty()) 0301 map.insert(QLatin1String("xesam:genre"), genres); 0302 if (!lyricists.isEmpty()) 0303 map.insert(QLatin1String("xesam:lyricist"), lyricists); 0304 if (!artPath.isEmpty()) 0305 map.insert(QLatin1String("mpris:artUrl"), 0306 QUrl::fromLocalFile(artPath).toString()); 0307 } 0308 if (duration >= 0) 0309 map.insert(QLatin1String("mpris:length"), duration * 1000); 0310 } 0311 return map; 0312 } 0313 0314 double MprisPlayerInterface::volume() const 0315 { 0316 return m_audioPlayer->getVolume() / 100.0; 0317 } 0318 0319 void MprisPlayerInterface::setVolume(double volume) 0320 { 0321 if (volume < 0.0) { 0322 volume = 0.0; 0323 } 0324 m_audioPlayer->setVolume(static_cast<int>(volume * 100.0)); 0325 } 0326 0327 qlonglong MprisPlayerInterface::position() const 0328 { 0329 return m_audioPlayer->getCurrentPosition() * 1000; 0330 } 0331 0332 bool MprisPlayerInterface::canGoNext() const 0333 { 0334 return m_hasNext; 0335 } 0336 0337 bool MprisPlayerInterface::canGoPrevious() const 0338 { 0339 return m_hasPrevious; 0340 } 0341 0342 bool MprisPlayerInterface::canPlay() const 0343 { 0344 return m_audioPlayer->getFileCount() > 0; 0345 } 0346 0347 bool MprisPlayerInterface::canPause() const 0348 { 0349 return m_audioPlayer->getFileCount() > 0; 0350 } 0351 0352 void MprisPlayerInterface::onStateChanged() 0353 { 0354 if (QString status = playbackStatus(); m_status != status) { 0355 m_status = status; 0356 sendPropertiesChangedSignal(QLatin1String("PlaybackStatus"), status); 0357 } 0358 } 0359 0360 void MprisPlayerInterface::onTrackChanged( 0361 const QString&, bool hasPrevious, bool hasNext) 0362 { 0363 if (m_hasPrevious != hasPrevious) { 0364 m_hasPrevious = hasPrevious; 0365 sendPropertiesChangedSignal(QLatin1String("CanGoPrevious"), m_hasPrevious); 0366 } 0367 if (m_hasNext != hasNext) { 0368 m_hasNext = hasNext; 0369 sendPropertiesChangedSignal(QLatin1String("CanGoNext"), m_hasNext); 0370 } 0371 sendPropertiesChangedSignal(QLatin1String("Metadata"), metadata()); 0372 } 0373 0374 void MprisPlayerInterface::onVolumeChanged() 0375 { 0376 sendPropertiesChangedSignal(QLatin1String("Volume"), volume()); 0377 } 0378 0379 void MprisPlayerInterface::onFileCountChanged(int count) 0380 { 0381 if (bool hasFiles = count > 0; m_hasFiles != hasFiles) { 0382 m_hasFiles = hasFiles; 0383 sendPropertiesChangedSignal(QLatin1String("CanPlay"), canPlay()); 0384 sendPropertiesChangedSignal(QLatin1String("CanPause"), canPause()); 0385 } 0386 } 0387 0388 void MprisPlayerInterface::onCurrentPositionChanged(qint64 position) 0389 { 0390 emit Seeked(position * 1000); 0391 } 0392 0393 void MprisPlayerInterface::sendPropertiesChangedSignal( 0394 const QString& name, const QVariant& value) 0395 { 0396 QVariantMap changedProps; 0397 changedProps.insert(name, value); 0398 QDBusConnection::sessionBus().send( 0399 QDBusMessage::createSignal( 0400 QLatin1String("/org/mpris/MediaPlayer2"), 0401 QLatin1String("org.freedesktop.DBus.Properties"), 0402 QLatin1String("PropertiesChanged")) 0403 << QLatin1String("org.mpris.MediaPlayer2.Player") 0404 << changedProps 0405 << QStringList()); 0406 } 0407 0408 QDBusObjectPath MprisPlayerInterface::getCurrentTrackId() const { 0409 int index = m_audioPlayer->getCurrentIndex(); 0410 if (index < 0) { 0411 return QDBusObjectPath(); 0412 } 0413 return QDBusObjectPath(QLatin1String("/org/kde/kid3/playlist/") 0414 + QString::number(index)); 0415 } 0416 0417 QString MprisPlayerInterface::findCoverArtInDirectory(const QString& dirPath) 0418 const 0419 { 0420 if (m_coverArtDirName != dirPath) { 0421 m_coverArtDirName = dirPath; 0422 QStringList files = QDir(dirPath).entryList( 0423 {QLatin1String("*.jpg"), QLatin1String("*.jpeg"), 0424 QLatin1String("*.png"), QLatin1String("*.webp")}, 0425 QDir::Files); 0426 m_coverArtFileName = !files.isEmpty() ? files.first() : QString(); 0427 } 0428 return !m_coverArtFileName.isEmpty() 0429 ? m_coverArtDirName + QLatin1Char('/') + m_coverArtFileName : QString(); 0430 } 0431 #endif