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