1 /*
2  *      Copyright (C) 2019 Jean-Luc Barriere
3  *
4  *  This file is part of Noson-App
5  *
6  *  Noson is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  Noson is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with Noson.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 #include "mpris2.h"
22 
23 #include <QGuiApplication>
24 #include <QDBusConnection>
25 #include <QDebug>
26 
27 #include "mpris2_root.h"
28 #include "mpris2_player.h"
29 #include "player.h"
30 #include "tools.h"
31 
32 #define MPRIS_OBJECT_PATH     "/org/mpris/MediaPlayer2"
33 #define DBUS_MEDIAPLAYER_SVC  "org.mpris.MediaPlayer2"
34 #define DBUS_FREEDESKTOP_SVC  "org.freedesktop.DBus.Properties"
35 
36 using namespace nosonapp;
37 
Mpris2(Player * app,QObject * parent)38 Mpris2::Mpris2(Player* app, QObject* parent)
39 : QObject(parent)
40 , m_player(app)
41 , m_registered(false)
42 , m_identity()
43 , m_serviceName()
44 , m_servicePath()
45 , m_metadata()
46 {
47   new Mpris2Root(this);
48   new Mpris2Player(this);
49 
50   if (m_player)
51   {
52     QObject::connect(m_player, SIGNAL(connectedChanged(int)), SLOT(connectionStateChanged(int)));
53     QObject::connect(m_player, SIGNAL(playbackStateChanged(int)), SLOT(playbackStateChanged(int)));
54     QObject::connect(m_player, SIGNAL(renderingGroupChanged(int)), SLOT(volumeChanged(int)));
55     QObject::connect(m_player, SIGNAL(playModeChanged(int)), SLOT(playModeChanged(int)));
56     QObject::connect(m_player, SIGNAL(sourceChanged(int)), SLOT(currentTrackChanged(int)));
57     initDBusService(m_player->pid());
58   }
59 }
60 
~Mpris2()61 Mpris2::~Mpris2()
62 {
63   if (m_registered)
64     QDBusConnection::sessionBus().unregisterService(m_serviceName);
65 }
66 
connectionStateChanged(int pid)67 void Mpris2::connectionStateChanged(int pid)
68 {
69   initDBusService(pid);
70 }
71 
playbackStateChanged(int pid)72 void Mpris2::playbackStateChanged(int pid)
73 {
74   emitPlayerNotification("CanPlay", CanPlay());
75   emitPlayerNotification("CanPause", CanPause());
76   emitPlayerNotification("PlaybackStatus", PlaybackStatus());
77   if (m_player->playbackState() == "PLAYING")
78     emitPlayerNotification("CanSeek", CanSeek());
79 }
80 
volumeChanged(int pid)81 void Mpris2::volumeChanged(int pid)
82 {
83   emitPlayerNotification("Volume", Volume());
84 }
85 
playModeChanged(int pid)86 void Mpris2::playModeChanged(int pid)
87 {
88   emitPlayerNotification("Shuffle", Shuffle());
89   emitPlayerNotification("LoopStatus", LoopStatus());
90   emitPlayerNotification("CanGoNext", CanGoNext());
91   emitPlayerNotification("CanGoPrevious", CanGoPrevious());
92 }
93 
initDBusService(int pid)94 void Mpris2::initDBusService(int pid)
95 {
96   if (m_registered)
97     QDBusConnection::sessionBus().unregisterService(m_serviceName);
98   m_registered = false;
99 
100   if (!m_player->connected())
101     return;
102 
103   // Try to make a friendly name for the player according to the Dbus specs.
104   // A valid name must only contain the ASCII characters "[A-Z][a-z][0-9]_" and
105   // must not begin with a digit.
106   QString zoneId;
107   QString zone_ = normalizedString(m_player->zoneShortName().split('+').front());
108   foreach (QChar c, zone_)
109   {
110     switch (c.category())
111     {
112     case QChar::Letter_Lowercase:
113     case QChar::Letter_Uppercase:
114     case QChar::Number_DecimalDigit:
115       zoneId.append(c);
116       break;
117     default:
118       zoneId.append('_');
119     }
120   }
121 
122 
123   m_identity = QString("%1.%2").arg(QGuiApplication::applicationDisplayName(), zoneId);
124 
125   m_servicePath = QString("/%1/%2")
126           .arg(QString(QGuiApplication::applicationName()).replace('.', '/'), zoneId);
127 
128   m_serviceName = QString(DBUS_MEDIAPLAYER_SVC ".%1.%2")
129           .arg(QGuiApplication::applicationDisplayName(), zoneId);
130 
131   if (!QDBusConnection::sessionBus().registerService(m_serviceName))
132   {
133     qWarning() << "Failed to register" << m_serviceName << "on the session bus";
134     return;
135   }
136   m_registered = true;
137   QDBusConnection::sessionBus().registerObject(MPRIS_OBJECT_PATH, this);
138 
139   m_metadata = QVariantMap();
140   currentTrackChanged(pid);
141   playbackStateChanged(pid);
142   playModeChanged(pid);
143   emitPlayerNotification("Volume", Volume());
144 
145   qDebug() << "Succeeded to register" << m_serviceName << "on the session bus";
146 }
147 
emitPlayerNotification(const QString & name,const QVariant & val)148 void Mpris2::emitPlayerNotification(const QString& name, const QVariant& val)
149 {
150   emitNotification(name, val, DBUS_MEDIAPLAYER_SVC ".Player");
151 }
152 
emitNotification(const QString & name,const QVariant & val,const QString & mprisEntity)153 void Mpris2::emitNotification(const QString& name, const QVariant& val, const QString& mprisEntity)
154 {
155   QDBusMessage msg = QDBusMessage::createSignal(MPRIS_OBJECT_PATH, DBUS_FREEDESKTOP_SVC, "PropertiesChanged");
156   QVariantMap map;
157   map.insert(name, val);
158   QVariantList args = QVariantList() << mprisEntity << map << QStringList();
159   msg.setArguments(args);
160   QDBusConnection::sessionBus().send(msg);
161 }
162 
Identity() const163 QString Mpris2::Identity() const
164 {
165   return m_identity;
166 }
167 
desktopEntryAbsolutePath() const168 QString Mpris2::desktopEntryAbsolutePath() const
169 {
170   QString appId = DesktopEntry();
171   QStringList xdg_data_dirs = QString(getenv("XDG_DATA_DIRS")).split(":");
172   xdg_data_dirs.append("/usr/local/share/");
173   xdg_data_dirs.append("/usr/share/");
174 
175   for (const QString& directory : xdg_data_dirs)
176   {
177     QString path = QString("%1/applications/%2.desktop")
178             .arg(directory, appId);
179     if (QFile::exists(path))
180       return path;
181   }
182   return QString();
183 }
184 
DesktopEntry() const185 QString Mpris2::DesktopEntry() const
186 {
187   return QGuiApplication::applicationName().toLower();
188 }
189 
SupportedUriSchemes() const190 QStringList Mpris2::SupportedUriSchemes() const
191 {
192   static QStringList res = QStringList()
193           << "file"
194           << "http";
195   return res;
196 }
197 
SupportedMimeTypes() const198 QStringList Mpris2::SupportedMimeTypes() const
199 {
200   static QStringList res = QStringList()
201           << "audio/aac"
202           << "audio/mp3"
203           << "audio/flac"
204           << "audio/ogg"
205           << "application/ogg"
206           << "audio/x-mp3"
207           << "audio/x-flac"
208           << "application/x-ogg";
209   return res;
210 }
211 
Raise()212 void Mpris2::Raise()
213 {
214 }
215 
Quit()216 void Mpris2::Quit()
217 {
218 }
219 
PlaybackStatus() const220 QString Mpris2::PlaybackStatus() const
221 {
222   QString state = m_player->playbackState();
223   if (state == "PLAYING")
224     return "Playing";
225   if (state == "PAUSED_PLAYBACK")
226     return "Paused";
227   return "Stopped";
228 }
229 
LoopStatus() const230 QString Mpris2::LoopStatus() const
231 {
232   QString mode = m_player->playMode();
233   if (mode == "SHUFFLE")
234     return "Playlist";
235   if (mode == "REPEAT_ALL")
236     return "Playlist";
237   if (mode == "REPEAT_ONE")
238     return "Track";
239   return "None";
240 }
241 
SetLoopStatus(const QString & value)242 void Mpris2::SetLoopStatus(const QString& value)
243 {
244   QString mode = m_player->playMode();
245   if ((value == "None" && (mode == "REPEAT_ALL" || mode == "SHUFFLE" || mode == "REPEAT_ONE")) ||
246           (value == "Playlist" && (mode == "NORMAL" || mode == "SHUFFLE_NOREPEAT")))
247     m_player->toggleRepeat();
248 }
249 
Rate() const250 double Mpris2::Rate() const
251 {
252   return 1.0;
253 }
254 
SetRate(double rate)255 void Mpris2::SetRate(double rate)
256 {
257   if (rate == 0)
258     m_player->pause();
259 }
260 
Shuffle() const261 bool Mpris2::Shuffle() const
262 {
263   QString mode = m_player->playMode();
264   return (mode == "SHUFFLE" || mode == "SHUFFLE_NOREPEAT");
265 }
266 
SetShuffle(bool enable)267 void Mpris2::SetShuffle(bool enable)
268 {
269   QString mode = m_player->playMode();
270   if ((mode == "SHUFFLE" || mode == "SHUFFLE_NOREPEAT") != enable)
271     m_player->toggleShuffle();
272 }
273 
Metadata() const274 QVariantMap Mpris2::Metadata() const
275 {
276   return m_metadata;
277 }
278 
makeTrackId(int index) const279 QString Mpris2::makeTrackId(int index) const
280 {
281   return QString("%1/track/%2").arg(m_servicePath).arg(QString::number(index));
282 }
283 
currentTrackChanged(int pid)284 void Mpris2::currentTrackChanged(int pid)
285 {
286   emitPlayerNotification("CanPlay", CanPlay());
287   emitPlayerNotification("CanPause", CanPause());
288   emitPlayerNotification("CanGoNext", CanGoNext());
289   emitPlayerNotification("CanGoPrevious", CanGoPrevious());
290   emitPlayerNotification("CanSeek", CanSeek());
291 
292   m_metadata = QVariantMap();
293   addMetadata("mpris:trackid", makeTrackId(m_player->currentIndex()), &m_metadata);
294   addMetadata("mpris:length", (qint64)(1000000L * m_player->currentTrackDuration()), &m_metadata);
295   addMetadata("mpris:artUrl", m_player->currentMetaArt(), &m_metadata);
296   addMetadata("xesam:title", m_player->currentMetaTitle(), &m_metadata);
297   addMetadata("xesam:album", m_player->currentMetaAlbum(), &m_metadata);
298   addMetadataAsList("xesam:artist", m_player->currentMetaArtist(), &m_metadata);
299 
300   emitPlayerNotification("Metadata", m_metadata);
301 }
302 
Volume() const303 double Mpris2::Volume() const
304 {
305   return (double) (m_player->volumeMaster()) / 100.0;
306 }
307 
SetVolume(double value)308 void Mpris2::SetVolume(double value)
309 {
310   m_player->setVolumeGroup(value * 100.0);
311 }
312 
Position() const313 qlonglong Mpris2::Position() const
314 {
315   return 1000000L * m_player->currentTrackPosition();
316 }
317 
MaximumRate() const318 double Mpris2::MaximumRate() const
319 {
320   return 1.0;
321 }
322 
MinimumRate() const323 double Mpris2::MinimumRate() const
324 {
325   return 1.0;
326 }
327 
CanGoNext() const328 bool Mpris2::CanGoNext() const
329 {
330   return (m_player->currentTrackDuration() > 0 && m_player->numberOfTracks() > (m_player->currentIndex() + 1));
331 }
332 
CanGoPrevious() const333 bool Mpris2::CanGoPrevious() const
334 {
335   return (m_player->currentTrackDuration() > 0 && m_player->currentIndex() > 0);
336 }
337 
CanPlay() const338 bool Mpris2::CanPlay() const
339 {
340   return true;
341 }
342 
CanPause() const343 bool Mpris2::CanPause() const
344 {
345   return true;
346 }
347 
CanSeek() const348 bool Mpris2::CanSeek() const
349 {
350   switch (m_player->currentProtocol())
351   {
352   case 1: // x-rincon-stream
353   case 2: // x-rincon-mp3radio
354   case 5: // x-sonos-htastream
355   case 14: // http-get
356   case 17: // http
357     return false;
358   default:
359     return (m_player->currentTrackDuration() > 0);
360   }
361 }
362 
CanControl() const363 bool Mpris2::CanControl() const
364 {
365   return true;
366 }
367 
Next()368 void Mpris2::Next()
369 {
370   if (CanGoNext())
371     m_player->next();
372 }
373 
Previous()374 void Mpris2::Previous()
375 {
376   if (CanGoPrevious())
377     m_player->previous();
378 }
379 
Pause()380 void Mpris2::Pause()
381 {
382   if (CanPause() && m_player->playbackState() == "PLAYING")
383     m_player->pause();
384 }
385 
PlayPause()386 void Mpris2::PlayPause()
387 {
388   if (CanPause())
389   {
390     QString state = m_player->playbackState();
391     if (state == "PLAYING")
392       m_player->pause();
393     else if (state == "STOPPED" || state == "PAUSED_PLAYBACK")
394       m_player->play();
395   }
396 }
397 
Stop()398 void Mpris2::Stop()
399 {
400   m_player->stop();
401 }
402 
Play()403 void Mpris2::Play()
404 {
405   if (CanPlay())
406   {
407     m_player->play();
408   }
409 }
410 
Seek(qlonglong offset)411 void Mpris2::Seek(qlonglong offset)
412 {
413   if (CanSeek())
414     m_player->seekTime(m_player->currentTrackPosition() + offset / 1000000L);
415 }
416 
SetPosition(const QDBusObjectPath & trackId,qlonglong offset)417 void Mpris2::SetPosition(const QDBusObjectPath& trackId, qlonglong offset)
418 {
419   if (CanSeek() && trackId.path() == makeTrackId(m_player->currentIndex()) && offset >= 0)
420     m_player->seekTime(offset / 1000000L);
421 }
422 
OpenUri(const QString & uri)423 void Mpris2::OpenUri(const QString& uri)
424 {
425   Q_UNUSED(uri);
426 }
427