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