1 /* This file is part of Clementine.
2    Copyright 2010-2012, David Sansome <me@davidsansome.com>
3    Copyright 2010-2011, Paweł Bara <keirangtp@gmail.com>
4    Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
5    Copyright 2013, Arnaud Bienner <arnaud.bienner@gmail.com>
6    Copyright 2013, TTSDA <ttsda@ttsda.cc>
7    Copyright 2013, Aggelos Biboudis <biboudis@gmail.com>
8    Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
9 
10    Clementine is free software: you can redistribute it and/or modify
11    it under the terms of the GNU General Public License as published by
12    the Free Software Foundation, either version 3 of the License, or
13    (at your option) any later version.
14 
15    Clementine is distributed in the hope that it will be useful,
16    but WITHOUT ANY WARRANTY; without even the implied warranty of
17    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18    GNU General Public License for more details.
19 
20    You should have received a copy of the GNU General Public License
21    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
22 */
23 
24 #include "mpris2.h"
25 
26 #include <algorithm>
27 
28 #include <QApplication>
29 #include <QDBusConnection>
30 #include <QtConcurrentRun>
31 
32 #include "config.h"
33 #include "core/application.h"
34 #include "core/logging.h"
35 #include "core/mpris_common.h"
36 #include "core/mpris2_player.h"
37 #include "core/mpris2_playlists.h"
38 #include "core/mpris2_root.h"
39 #include "core/mpris2_tracklist.h"
40 #include "core/player.h"
41 #include "core/timeconstants.h"
42 #include "covers/currentartloader.h"
43 #include "engines/enginebase.h"
44 #include "playlist/playlist.h"
45 #include "playlist/playlistmanager.h"
46 #include "playlist/playlistsequence.h"
47 #include "ui/mainwindow.h"
48 
operator <<(QDBusArgument & arg,const MprisPlaylist & playlist)49 QDBusArgument& operator<<(QDBusArgument& arg, const MprisPlaylist& playlist) {
50   arg.beginStructure();
51   arg << playlist.id << playlist.name << playlist.icon;
52   arg.endStructure();
53   return arg;
54 }
55 
operator >>(const QDBusArgument & arg,MprisPlaylist & playlist)56 const QDBusArgument& operator>>(const QDBusArgument& arg,
57                                 MprisPlaylist& playlist) {
58   arg.beginStructure();
59   arg >> playlist.id >> playlist.name >> playlist.icon;
60   arg.endStructure();
61   return arg;
62 }
63 
operator <<(QDBusArgument & arg,const MaybePlaylist & playlist)64 QDBusArgument& operator<<(QDBusArgument& arg, const MaybePlaylist& playlist) {
65   arg.beginStructure();
66   arg << playlist.valid;
67   arg << playlist.playlist;
68   arg.endStructure();
69   return arg;
70 }
71 
operator >>(const QDBusArgument & arg,MaybePlaylist & playlist)72 const QDBusArgument& operator>>(const QDBusArgument& arg,
73                                 MaybePlaylist& playlist) {
74   arg.beginStructure();
75   arg >> playlist.valid >> playlist.playlist;
76   arg.endStructure();
77   return arg;
78 }
79 
80 namespace mpris {
81 
82 const char* Mpris2::kMprisObjectPath = "/org/mpris/MediaPlayer2";
83 const char* Mpris2::kServiceName = "org.mpris.MediaPlayer2.clementine";
84 const char* Mpris2::kFreedesktopPath = "org.freedesktop.DBus.Properties";
85 
Mpris2(Application * app,QObject * parent)86 Mpris2::Mpris2(Application* app, QObject* parent) : QObject(parent), app_(app) {
87   new Mpris2Root(this);
88   new Mpris2TrackList(this);
89   new Mpris2Player(this);
90   new Mpris2Playlists(this);
91 
92   if (!QDBusConnection::sessionBus().registerService(kServiceName)) {
93     qLog(Warning) << "Failed to register" << QString(kServiceName)
94                   << "on the session bus";
95     return;
96   }
97 
98   QDBusConnection::sessionBus().registerObject(kMprisObjectPath, this);
99 
100   connect(app_->current_art_loader(), SIGNAL(ArtLoaded(Song, QString, QImage)),
101           SLOT(ArtLoaded(Song, QString)));
102 
103   connect(app_->player()->engine(), SIGNAL(StateChanged(Engine::State)),
104           SLOT(EngineStateChanged(Engine::State)));
105   connect(app_->player(), SIGNAL(VolumeChanged(int)), SLOT(VolumeChanged()));
106   connect(app_->player(), SIGNAL(Seeked(qlonglong)), SIGNAL(Seeked(qlonglong)));
107 
108   connect(app_->playlist_manager(), SIGNAL(PlaylistManagerInitialized()),
109           SLOT(PlaylistManagerInitialized()));
110   connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)),
111           SLOT(CurrentSongChanged(Song)));
112   connect(app_->playlist_manager(), SIGNAL(PlaylistChanged(Playlist*)),
113           SLOT(PlaylistChanged(Playlist*)));
114   connect(app_->playlist_manager(), SIGNAL(CurrentChanged(Playlist*)),
115           SLOT(PlaylistCollectionChanged(Playlist*)));
116 }
117 
118 // when PlaylistManager gets it ready, we connect PlaylistSequence with this
PlaylistManagerInitialized()119 void Mpris2::PlaylistManagerInitialized() {
120   connect(app_->playlist_manager()->sequence(),
121           SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)),
122           SLOT(ShuffleModeChanged()));
123   connect(app_->playlist_manager()->sequence(),
124           SIGNAL(RepeatModeChanged(PlaylistSequence::RepeatMode)),
125           SLOT(RepeatModeChanged()));
126 }
127 
EngineStateChanged(Engine::State newState)128 void Mpris2::EngineStateChanged(Engine::State newState) {
129   if (newState != Engine::Playing && newState != Engine::Paused) {
130     last_metadata_ = QVariantMap();
131     EmitNotification("Metadata");
132   }
133 
134   EmitNotification("CanPlay");
135   EmitNotification("CanPause");
136   EmitNotification("PlaybackStatus", PlaybackStatus(newState));
137   if (newState == Engine::Playing)
138     EmitNotification("CanSeek", CanSeek(newState));
139 }
140 
VolumeChanged()141 void Mpris2::VolumeChanged() { EmitNotification("Volume"); }
142 
ShuffleModeChanged()143 void Mpris2::ShuffleModeChanged() { EmitNotification("Shuffle"); }
144 
RepeatModeChanged()145 void Mpris2::RepeatModeChanged() {
146   EmitNotification("LoopStatus");
147   EmitNotification("CanGoNext", CanGoNext());
148   EmitNotification("CanGoPrevious", CanGoPrevious());
149 }
150 
EmitNotification(const QString & name,const QVariant & val)151 void Mpris2::EmitNotification(const QString& name, const QVariant& val) {
152   EmitNotification(name, val, "org.mpris.MediaPlayer2.Player");
153 }
154 
EmitNotification(const QString & name,const QVariant & val,const QString & mprisEntity)155 void Mpris2::EmitNotification(const QString& name, const QVariant& val,
156                               const QString& mprisEntity) {
157   QDBusMessage msg = QDBusMessage::createSignal(
158       kMprisObjectPath, kFreedesktopPath, "PropertiesChanged");
159   QVariantMap map;
160   map.insert(name, val);
161   QVariantList args = QVariantList() << mprisEntity << map << QStringList();
162   msg.setArguments(args);
163   QDBusConnection::sessionBus().send(msg);
164 }
165 
EmitNotification(const QString & name)166 void Mpris2::EmitNotification(const QString& name) {
167   QVariant value;
168   if (name == "PlaybackStatus")
169     value = PlaybackStatus();
170   else if (name == "LoopStatus")
171     value = LoopStatus();
172   else if (name == "Shuffle")
173     value = Shuffle();
174   else if (name == "Metadata")
175     value = Metadata();
176   else if (name == "Volume")
177     value = Volume();
178   else if (name == "Position")
179     value = Position();
180   else if (name == "CanGoNext")
181     value = CanGoNext();
182   else if (name == "CanGoPrevious")
183     value = CanGoPrevious();
184   else if (name == "CanSeek")
185     value = CanSeek();
186   else if (name == "CanPlay")
187     value = CanPlay();
188   else if (name == "CanPause")
189     value = CanPause();
190 
191   if (value.isValid()) EmitNotification(name, value);
192 }
193 
194 // ------------------Root Interface--------------- //
195 
CanQuit() const196 bool Mpris2::CanQuit() const { return true; }
197 
CanRaise() const198 bool Mpris2::CanRaise() const { return true; }
199 
HasTrackList() const200 bool Mpris2::HasTrackList() const { return true; }
201 
Identity() const202 QString Mpris2::Identity() const { return QCoreApplication::applicationName(); }
203 
DesktopEntryAbsolutePath() const204 QString Mpris2::DesktopEntryAbsolutePath() const {
205   QStringList xdg_data_dirs = QString(getenv("XDG_DATA_DIRS")).split(":");
206   xdg_data_dirs.append("/usr/local/share/");
207   xdg_data_dirs.append("/usr/share/");
208 
209   for (const QString& directory : xdg_data_dirs) {
210     QString path = QString("%1/applications/%2.desktop").arg(
211         directory, QApplication::applicationName().toLower());
212     if (QFile::exists(path)) return path;
213   }
214   return QString();
215 }
216 
DesktopEntry() const217 QString Mpris2::DesktopEntry() const {
218   return QApplication::applicationName().toLower();
219 }
220 
SupportedUriSchemes() const221 QStringList Mpris2::SupportedUriSchemes() const {
222   static QStringList res = QStringList() << "file"
223                                          << "http"
224                                          << "cdda"
225                                          << "smb"
226                                          << "sftp";
227   return res;
228 }
229 
SupportedMimeTypes() const230 QStringList Mpris2::SupportedMimeTypes() const {
231   static QStringList res = QStringList() << "application/ogg"
232                                          << "application/x-ogg"
233                                          << "application/x-ogm-audio"
234                                          << "audio/aac"
235                                          << "audio/mp4"
236                                          << "audio/mpeg"
237                                          << "audio/mpegurl"
238                                          << "audio/ogg"
239                                          << "audio/vnd.rn-realaudio"
240                                          << "audio/vorbis"
241                                          << "audio/x-ape"
242                                          << "audio/x-flac"
243                                          << "audio/x-mp3"
244                                          << "audio/x-mpeg"
245                                          << "audio/x-mpegurl"
246                                          << "audio/x-ms-wma"
247                                          << "audio/x-musepack"
248                                          << "audio/x-oggflac"
249                                          << "audio/x-pn-realaudio"
250                                          << "audio/x-scpls"
251                                          << "audio/x-speex"
252                                          << "audio/x-vorbis"
253                                          << "audio/x-vorbis+ogg"
254                                          << "audio/x-wav"
255                                          << "video/x-ms-asf"
256                                          << "x-content/audio-player";
257   return res;
258 }
259 
Raise()260 void Mpris2::Raise() { emit RaiseMainWindow(); }
261 
Quit()262 void Mpris2::Quit() { qApp->quit(); }
263 
PlaybackStatus() const264 QString Mpris2::PlaybackStatus() const {
265   return PlaybackStatus(app_->player()->GetState());
266 }
267 
PlaybackStatus(Engine::State state) const268 QString Mpris2::PlaybackStatus(Engine::State state) const {
269   switch (state) {
270     case Engine::Playing:
271       return "Playing";
272     case Engine::Paused:
273       return "Paused";
274     default:
275       return "Stopped";
276   }
277 }
278 
LoopStatus() const279 QString Mpris2::LoopStatus() const {
280   if (!app_->playlist_manager()->sequence()) {
281     return "None";
282   }
283 
284   switch (app_->playlist_manager()->sequence()->repeat_mode()) {
285     case PlaylistSequence::Repeat_Album:
286     case PlaylistSequence::Repeat_Playlist:
287       return "Playlist";
288     case PlaylistSequence::Repeat_Track:
289       return "Track";
290     default:
291       return "None";
292   }
293 }
294 
SetLoopStatus(const QString & value)295 void Mpris2::SetLoopStatus(const QString& value) {
296   PlaylistSequence::RepeatMode mode = PlaylistSequence::Repeat_Off;
297 
298   if (value == "None") {
299     mode = PlaylistSequence::Repeat_Off;
300   } else if (value == "Track") {
301     mode = PlaylistSequence::Repeat_Track;
302   } else if (value == "Playlist") {
303     mode = PlaylistSequence::Repeat_Playlist;
304   }
305 
306   app_->playlist_manager()->active()->sequence()->SetRepeatMode(mode);
307 }
308 
Rate() const309 double Mpris2::Rate() const { return 1.0; }
310 
SetRate(double rate)311 void Mpris2::SetRate(double rate) {
312   if (rate == 0) {
313     app_->player()->Pause();
314   }
315 }
316 
Shuffle() const317 bool Mpris2::Shuffle() const {
318   return app_->playlist_manager()->sequence()->shuffle_mode() !=
319          PlaylistSequence::Shuffle_Off;
320 }
321 
SetShuffle(bool enable)322 void Mpris2::SetShuffle(bool enable) {
323   app_->playlist_manager()->active()->sequence()->SetShuffleMode(
324       enable ? PlaylistSequence::Shuffle_All : PlaylistSequence::Shuffle_Off);
325 }
326 
Metadata() const327 QVariantMap Mpris2::Metadata() const { return last_metadata_; }
328 
current_track_id() const329 QString Mpris2::current_track_id() const {
330   return QString("/org/clementineplayer/Clementine/Track/%1")
331       .arg(QString::number(app_->playlist_manager()->active()->current_row()));
332 }
333 
334 // We send Metadata change notification as soon as the process of
335 // changing song starts...
CurrentSongChanged(const Song & song)336 void Mpris2::CurrentSongChanged(const Song& song) {
337   ArtLoaded(song, "");
338   EmitNotification("CanPlay");
339   EmitNotification("CanPause");
340   EmitNotification("CanGoNext", CanGoNext());
341   EmitNotification("CanGoPrevious", CanGoPrevious());
342   EmitNotification("CanSeek", CanSeek());
343 }
344 
345 // ... and we add the cover information later, when it's available.
ArtLoaded(const Song & song,const QString & art_uri)346 void Mpris2::ArtLoaded(const Song& song, const QString& art_uri) {
347   last_metadata_ = QVariantMap();
348   song.ToXesam(&last_metadata_);
349 
350   using mpris::AddMetadata;
351   AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
352 
353   if (song.rating() != -1.0) {
354     AddMetadata("rating", song.rating() * 5, &last_metadata_);
355   }
356   if (!art_uri.isEmpty()) {
357     AddMetadata("mpris:artUrl", art_uri, &last_metadata_);
358   }
359 
360   AddMetadata("year", song.year(), &last_metadata_);
361   AddMetadata("bitrate", song.bitrate(), &last_metadata_);
362 
363   EmitNotification("Metadata", last_metadata_);
364 }
365 
Volume() const366 double Mpris2::Volume() const { return app_->player()->GetVolume() / 100.0; }
367 
SetVolume(double value)368 void Mpris2::SetVolume(double value) { app_->player()->SetVolume(value * 100); }
369 
Position() const370 qlonglong Mpris2::Position() const {
371   return app_->player()->engine()->position_nanosec() / kNsecPerUsec;
372 }
373 
MaximumRate() const374 double Mpris2::MaximumRate() const { return 1.0; }
375 
MinimumRate() const376 double Mpris2::MinimumRate() const { return 1.0; }
377 
CanGoNext() const378 bool Mpris2::CanGoNext() const {
379   return app_->playlist_manager()->active() &&
380          app_->playlist_manager()->active()->next_row() != -1;
381 }
382 
CanGoPrevious() const383 bool Mpris2::CanGoPrevious() const {
384   return app_->playlist_manager()->active() &&
385          (app_->playlist_manager()->active()->previous_row() != -1 ||
386           app_->player()->PreviousWouldRestartTrack());
387 }
388 
CanPlay() const389 bool Mpris2::CanPlay() const {
390   return app_->playlist_manager()->active() &&
391          app_->playlist_manager()->active()->rowCount() != 0 &&
392          !(app_->player()->GetState() == Engine::Playing &&
393            (app_->player()->GetCurrentItem()->options() &
394             PlaylistItem::LastFMControls));
395 }
396 
397 // This one's a bit different than MPRIS 1 - we want this to be true even when
398 // the song is already paused or stopped.
CanPause() const399 bool Mpris2::CanPause() const {
400   return (app_->player()->GetCurrentItem() &&
401           app_->player()->GetState() == Engine::Playing &&
402           !(app_->player()->GetCurrentItem()->options() &
403             PlaylistItem::PauseDisabled)) ||
404          PlaybackStatus() == "Paused" || PlaybackStatus() == "Stopped";
405 }
406 
CanSeek() const407 bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
408 
CanSeek(Engine::State state) const409 bool Mpris2::CanSeek(Engine::State state) const {
410   return app_->player()->GetCurrentItem() && state != Engine::Empty &&
411          !app_->player()->GetCurrentItem()->Metadata().is_stream();
412 }
413 
CanControl() const414 bool Mpris2::CanControl() const { return true; }
415 
Next()416 void Mpris2::Next() {
417   if (CanGoNext()) {
418     app_->player()->Next();
419   }
420 }
421 
Previous()422 void Mpris2::Previous() {
423   if (CanGoPrevious()) {
424     app_->player()->Previous();
425   }
426 }
427 
Pause()428 void Mpris2::Pause() {
429   if (CanPause() && app_->player()->GetState() != Engine::Paused) {
430     app_->player()->Pause();
431   }
432 }
433 
PlayPause()434 void Mpris2::PlayPause() {
435   if (CanPause()) {
436     app_->player()->PlayPause();
437   }
438 }
439 
Stop()440 void Mpris2::Stop() { app_->player()->Stop(); }
441 
Play()442 void Mpris2::Play() {
443   if (CanPlay()) {
444     app_->player()->Play();
445   }
446 }
447 
Seek(qlonglong offset)448 void Mpris2::Seek(qlonglong offset) {
449   if (CanSeek()) {
450     app_->player()->SeekTo(app_->player()->engine()->position_nanosec() /
451                                kNsecPerSec +
452                            offset / kUsecPerSec);
453   }
454 }
455 
SetPosition(const QDBusObjectPath & trackId,qlonglong offset)456 void Mpris2::SetPosition(const QDBusObjectPath& trackId, qlonglong offset) {
457   if (CanSeek() && trackId.path() == current_track_id() && offset >= 0) {
458     offset *= kNsecPerUsec;
459 
460     if (offset <
461         app_->player()->GetCurrentItem()->Metadata().length_nanosec()) {
462       app_->player()->SeekTo(offset / kNsecPerSec);
463     }
464   }
465 }
466 
OpenUri(const QString & uri)467 void Mpris2::OpenUri(const QString& uri) {
468   app_->playlist_manager()->active()->InsertUrls(QList<QUrl>() << QUrl(uri), -1,
469                                                  true);
470 }
471 
Tracks() const472 TrackIds Mpris2::Tracks() const {
473   // TODO(John Maguire): ?
474   return TrackIds();
475 }
476 
CanEditTracks() const477 bool Mpris2::CanEditTracks() const { return false; }
478 
GetTracksMetadata(const TrackIds & tracks) const479 TrackMetadata Mpris2::GetTracksMetadata(const TrackIds& tracks) const {
480   // TODO(John Maguire): ?
481   return TrackMetadata();
482 }
483 
AddTrack(const QString & uri,const QDBusObjectPath & afterTrack,bool setAsCurrent)484 void Mpris2::AddTrack(const QString& uri, const QDBusObjectPath& afterTrack,
485                       bool setAsCurrent) {
486   // TODO(John Maguire): ?
487 }
488 
RemoveTrack(const QDBusObjectPath & trackId)489 void Mpris2::RemoveTrack(const QDBusObjectPath& trackId) {
490   // TODO(John Maguire): ?
491 }
492 
GoTo(const QDBusObjectPath & trackId)493 void Mpris2::GoTo(const QDBusObjectPath& trackId) {
494   // TODO(John Maguire): ?
495 }
496 
PlaylistCount() const497 quint32 Mpris2::PlaylistCount() const {
498   return app_->playlist_manager()->GetAllPlaylists().size();
499 }
500 
Orderings() const501 QStringList Mpris2::Orderings() const { return QStringList() << "User"; }
502 
503 namespace {
504 
MakePlaylistPath(int id)505 QDBusObjectPath MakePlaylistPath(int id) {
506   return QDBusObjectPath(
507       QString("/org/clementineplayer/clementine/PlaylistId/%1").arg(id));
508 }
509 }
510 
ActivePlaylist() const511 MaybePlaylist Mpris2::ActivePlaylist() const {
512   MaybePlaylist maybe_playlist;
513   Playlist* current_playlist = app_->playlist_manager()->current();
514   maybe_playlist.valid = current_playlist;
515   if (!current_playlist) {
516     return maybe_playlist;
517   }
518 
519   maybe_playlist.playlist.id = MakePlaylistPath(current_playlist->id());
520   maybe_playlist.playlist.name =
521       app_->playlist_manager()->GetPlaylistName(current_playlist->id());
522   return maybe_playlist;
523 }
524 
ActivatePlaylist(const QDBusObjectPath & playlist_id)525 void Mpris2::ActivatePlaylist(const QDBusObjectPath& playlist_id) {
526   QStringList split_path = playlist_id.path().split('/');
527   qLog(Debug) << Q_FUNC_INFO << playlist_id.path() << split_path;
528   if (split_path.isEmpty()) {
529     return;
530   }
531   bool ok = false;
532   int p = split_path.last().toInt(&ok);
533   if (!ok) {
534     return;
535   }
536   if (!app_->playlist_manager()->IsPlaylistOpen(p)) {
537     qLog(Error) << "Playlist isn't opened!";
538     return;
539   }
540   app_->playlist_manager()->SetActivePlaylist(p);
541   app_->player()->Next();
542 }
543 
544 // TODO(John Maguire): Support sort orders.
GetPlaylists(quint32 index,quint32 max_count,const QString & order,bool reverse_order)545 MprisPlaylistList Mpris2::GetPlaylists(quint32 index, quint32 max_count,
546                                        const QString& order,
547                                        bool reverse_order) {
548   MprisPlaylistList ret;
549   for (Playlist* p : app_->playlist_manager()->GetAllPlaylists()) {
550     MprisPlaylist mpris_playlist;
551     mpris_playlist.id = MakePlaylistPath(p->id());
552     mpris_playlist.name = app_->playlist_manager()->GetPlaylistName(p->id());
553     ret << mpris_playlist;
554   }
555 
556   if (reverse_order) {
557     std::reverse(ret.begin(), ret.end());
558   }
559 
560   return ret.mid(index, max_count);
561 }
562 
PlaylistChanged(Playlist * playlist)563 void Mpris2::PlaylistChanged(Playlist* playlist) {
564   MprisPlaylist mpris_playlist;
565   mpris_playlist.id = MakePlaylistPath(playlist->id());
566   mpris_playlist.name =
567       app_->playlist_manager()->GetPlaylistName(playlist->id());
568   emit PlaylistChanged(mpris_playlist);
569 }
570 
PlaylistCollectionChanged(Playlist * playlist)571 void Mpris2::PlaylistCollectionChanged(Playlist* playlist) {
572   EmitNotification("PlaylistCount", "", "org.mpris.MediaPlayer2.Playlists");
573 }
574 
575 }  // namespace mpris
576