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