1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 * ----
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "musiclibraryitemalbum.h"
25 #include "musiclibraryitemartist.h"
26 #include "musiclibraryitemsong.h"
27 #include "musiclibraryitemroot.h"
28 #include "devicesmodel.h"
29 #include "playqueuemodel.h"
30 #include "gui/settings.h"
31 #include "roles.h"
32 #include "mpd-interface/mpdparseutils.h"
33 #include "mpd-interface/mpdconnection.h"
34 #include "devices/umsdevice.h"
35 #include "http/httpserver.h"
36 #include "widgets/icons.h"
37 #include "widgets/mirrormenu.h"
38 #include "devices/mountpoints.h"
39 #include "gui/stdactions.h"
40 #include "support/action.h"
41 #include "config.h"
42 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
43 #include "devices/audiocddevice.h"
44 #endif
45 #include "support/globalstatic.h"
46 #include <QStringList>
47 #include <QMimeData>
48 #include <QTimer>
49 #include "solid-lite/device.h"
50 #include "solid-lite/deviceinterface.h"
51 #include "solid-lite/devicenotifier.h"
52 #include "solid-lite/portablemediaplayer.h"
53 #include "solid-lite/storageaccess.h"
54 #include "solid-lite/storagedrive.h"
55 #include "solid-lite/storagevolume.h"
56 #include "solid-lite/opticaldisc.h"
57 #include <algorithm>
58
59 #include <QDebug>
60 static bool debugIsEnabled=false;
61 #define DBUG if (debugIsEnabled) qWarning() << metaObject()->className() << __FUNCTION__
enableDebug()62 void DevicesModel::enableDebug()
63 {
64 debugIsEnabled=true;
65 }
66
debugEnabled()67 bool DevicesModel::debugEnabled()
68 {
69 return debugIsEnabled;
70 }
71
fixDevicePath(const QString & path)72 QString DevicesModel::fixDevicePath(const QString &path)
73 {
74 // Remove MTP IDs, and display storage...
75 if (path.startsWith(QChar('{')) && path.contains(QChar('}'))) {
76 int end=path.indexOf(QChar('}'));
77 QStringList details=path.mid(1, end-1).split(QChar('/'));
78 if (details.length()>3) {
79 return QChar('(')+details.at(3)+QLatin1String(") ")+path.mid(end+1);
80 }
81 return path.mid(end+1);
82 }
83 return path;
84 }
85
GLOBAL_STATIC(DevicesModel,instance)86 GLOBAL_STATIC(DevicesModel, instance)
87
88 DevicesModel::DevicesModel(QObject *parent)
89 : MusicLibraryModel(parent)
90 , itemMenu(nullptr)
91 , enabled(false)
92 , inhibitMenuUpdate(false)
93 {
94 configureAction = new Action(Icons::self()->configureIcon, tr("Configure Device"), this);
95 refreshAction = new Action(Icons::self()->reloadIcon, tr("Refresh Device"), this);
96 connectAction = new Action(Icons::self()->connectIcon, tr("Connect Device"), this);
97 disconnectAction = new Action(Icons::self()->disconnectIcon, tr("Disconnect Device"), this);
98 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
99 editAction = new Action(Icons::self()->editIcon, tr("Edit CD Details"), this);
100 #endif
101 updateItemMenu();
102 connect(this, SIGNAL(add(const QStringList &, int, quint8, bool)), MPDConnection::self(), SLOT(add(const QStringList &, int, quint8, bool)));
103 }
104
~DevicesModel()105 DevicesModel::~DevicesModel()
106 {
107 qDeleteAll(collections);
108 }
109
index(int row,int column,const QModelIndex & parent) const110 QModelIndex DevicesModel::index(int row, int column, const QModelIndex &parent) const
111 {
112 if (!hasIndex(row, column, parent)) {
113 return QModelIndex();
114 }
115
116 if (parent.isValid()) {
117 MusicLibraryItem *p=static_cast<MusicLibraryItem *>(parent.internalPointer());
118
119 if (p) {
120 return row<p->childCount() ? createIndex(row, column, p->childItem(row)) : QModelIndex();
121 }
122 } else {
123 return row<collections.count() ? createIndex(row, column, collections.at(row)) : QModelIndex();
124 }
125
126 return QModelIndex();
127 }
128
parent(const QModelIndex & index) const129 QModelIndex DevicesModel::parent(const QModelIndex &index) const
130 {
131 if (!index.isValid()) {
132 return QModelIndex();
133 }
134
135 MusicLibraryItem *childItem = static_cast<MusicLibraryItem *>(index.internalPointer());
136 MusicLibraryItem *parentItem = childItem->parentItem();
137
138 if (parentItem) {
139 return createIndex(parentItem->parentItem() ? parentItem->row() : row(parentItem), 0, parentItem);
140 } else {
141 return QModelIndex();
142 }
143 }
144
rowCount(const QModelIndex & parent) const145 int DevicesModel::rowCount(const QModelIndex &parent) const
146 {
147 if (parent.column() > 0) {
148 return 0;
149 }
150
151 return parent.isValid() ? static_cast<MusicLibraryItem *>(parent.internalPointer())->childCount() : collections.count();
152 }
153
getDetails(QSet<QString> & artists,QSet<QString> & albumArtists,QSet<QString> & composers,QSet<QString> & albums,QSet<QString> & genres)154 void DevicesModel::getDetails(QSet<QString> &artists, QSet<QString> &albumArtists, QSet<QString> &composers, QSet<QString> &albums, QSet<QString> &genres)
155 {
156 for (MusicLibraryItemRoot *col: collections) {
157 col->getDetails(artists, albumArtists, composers, albums, genres);
158 }
159 }
160
indexOf(const QString & id)161 int DevicesModel::indexOf(const QString &id)
162 {
163 int i=0;
164 for (MusicLibraryItemRoot *col: collections) {
165 if (col->id()==id) {
166 return i;
167 }
168 i++;
169 }
170 return -1;
171 }
172
songs(const QModelIndexList & indexes,bool playableOnly,bool fullPath) const173 QList<Song> DevicesModel::songs(const QModelIndexList &indexes, bool playableOnly, bool fullPath) const
174 {
175 QMap<MusicLibraryItem *, QList<Song> > colSongs;
176 QMap<MusicLibraryItem *, QSet<QString> > colFiles;
177
178 for (QModelIndex index: indexes) {
179 MusicLibraryItem *item = static_cast<MusicLibraryItem *>(index.internalPointer());
180 MusicLibraryItem *p=item;
181
182 while (p->parentItem()) {
183 p=p->parentItem();
184 }
185
186 if (!p) {
187 continue;
188 }
189
190 MusicLibraryItemRoot *parent=static_cast<MusicLibraryItemRoot *>(p);
191
192 if (playableOnly && !parent->canPlaySongs()) {
193 continue;
194 }
195
196 switch (item->itemType()) {
197 case MusicLibraryItem::Type_Root: {
198 if (static_cast<MusicLibraryItemRoot *>(parent)->flat()) {
199 for (const MusicLibraryItem *song: static_cast<const MusicLibraryItemContainer *>(item)->childItems()) {
200 if (MusicLibraryItem::Type_Song==song->itemType() && !colFiles[parent].contains(static_cast<const MusicLibraryItemSong*>(song)->file())) {
201 colSongs[parent] << parent->fixPath(static_cast<const MusicLibraryItemSong*>(song)->song(), fullPath);
202 colFiles[parent] << static_cast<const MusicLibraryItemSong*>(song)->file();
203 }
204 }
205 } else {
206 // First, sort all artists as they would appear in UI...
207 QList<MusicLibraryItem *> artists=static_cast<const MusicLibraryItemContainer *>(item)->childItems();
208 if (artists.isEmpty()) {
209 break;
210 }
211
212 std::sort(artists.begin(), artists.end(), MusicLibraryItemArtist::lessThan);
213
214 for (MusicLibraryItem *a: artists) {
215 const MusicLibraryItemContainer *artist=static_cast<const MusicLibraryItemContainer *>(a);
216 // Now sort all albums as they would appear in UI...
217 QList<MusicLibraryItem *> artistAlbums=artist->childItems();
218 std::sort(artistAlbums.begin(), artistAlbums.end(), MusicLibraryItemAlbum::lessThan);
219 for (MusicLibraryItem *i: artistAlbums) {
220 const MusicLibraryItemContainer *album=static_cast<const MusicLibraryItemContainer *>(i);
221 for (const MusicLibraryItem *song: album->childItems()) {
222 if (MusicLibraryItem::Type_Song==song->itemType() && !colFiles[parent].contains(static_cast<const MusicLibraryItemSong*>(song)->file())) {
223 colSongs[parent] << parent->fixPath(static_cast<const MusicLibraryItemSong*>(song)->song(), fullPath);
224 colFiles[parent] << static_cast<const MusicLibraryItemSong*>(song)->file();
225 }
226 }
227 }
228 }
229 }
230 break;
231 }
232 case MusicLibraryItem::Type_Artist: {
233 // First, sort all albums as they would appear in UI...
234 QList<MusicLibraryItem *> artistAlbums=static_cast<const MusicLibraryItemContainer *>(item)->childItems();
235 std::sort(artistAlbums.begin(), artistAlbums.end(), MusicLibraryItemAlbum::lessThan);
236
237 for (MusicLibraryItem *i: artistAlbums) {
238 const MusicLibraryItemContainer *album=static_cast<const MusicLibraryItemContainer *>(i);
239 for (const MusicLibraryItem *song: album->childItems()) {
240 if (MusicLibraryItem::Type_Song==song->itemType() && !colFiles[parent].contains(static_cast<const MusicLibraryItemSong*>(song)->file())) {
241 colSongs[parent] << parent->fixPath(static_cast<const MusicLibraryItemSong*>(song)->song(), fullPath);
242 colFiles[parent] << static_cast<const MusicLibraryItemSong*>(song)->file();
243 }
244 }
245 }
246 break;
247 }
248 case MusicLibraryItem::Type_Album:
249 for (const MusicLibraryItem *song: static_cast<const MusicLibraryItemContainer *>(item)->childItems()) {
250 if (MusicLibraryItem::Type_Song==song->itemType() && !colFiles[parent].contains(static_cast<const MusicLibraryItemSong*>(song)->file())) {
251 colSongs[parent] << parent->fixPath(static_cast<const MusicLibraryItemSong*>(song)->song(), fullPath);
252 colFiles[parent] << static_cast<const MusicLibraryItemSong*>(song)->file();
253 }
254 }
255 break;
256 case MusicLibraryItem::Type_Song:
257 if (!colFiles[parent].contains(static_cast<const MusicLibraryItemSong*>(item)->file())) {
258 colSongs[parent] << parent->fixPath(static_cast<const MusicLibraryItemSong*>(item)->song(), fullPath);
259 colFiles[parent] << static_cast<const MusicLibraryItemSong*>(item)->file();
260 }
261 break;
262 default:
263 break;
264 }
265 }
266
267 QList<Song> songs;
268 QMap<MusicLibraryItem *, QList<Song> >::Iterator it(colSongs.begin());
269 QMap<MusicLibraryItem *, QList<Song> >::Iterator end(colSongs.end());
270
271 for (; it!=end; ++it) {
272 songs.append(it.value());
273 }
274
275 return songs;
276 }
277
filenames(const QModelIndexList & indexes,bool playableOnly,bool fullPath) const278 QStringList DevicesModel::filenames(const QModelIndexList &indexes, bool playableOnly, bool fullPath) const
279 {
280 QList<Song> songList=songs(indexes, playableOnly, fullPath);
281 QStringList fnames;
282 for (const Song &s: songList) {
283 fnames.append(s.file);
284 }
285 return fnames;
286 }
287
setData(const QModelIndex & index,const QVariant & value,int role)288 bool DevicesModel::setData(const QModelIndex &index, const QVariant &value, int role)
289 {
290 if (!index.isValid()) {
291 return false;
292 }
293
294 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
295 if (Cantata::Role_Image==role) {
296 MusicLibraryItem *item = static_cast<MusicLibraryItem *>(index.internalPointer());
297
298 if (MusicLibraryItem::Type_Root==item->itemType()) {
299 Device *dev=static_cast<Device *>(item);
300 if (Device::AudioCd==dev->devType()) {
301 static_cast<AudioCdDevice *>(dev)->scaleCoverPix(value.toInt());
302 return true;
303 }
304 }
305 }
306 #endif
307 return MusicLibraryModel::setData(index, value, role);
308 }
309
data(const QModelIndex & index,int role) const310 QVariant DevicesModel::data(const QModelIndex &index, int role) const
311 {
312 if (!index.isValid()) {
313 return QVariant();
314 }
315
316 MusicLibraryItem *item = static_cast<MusicLibraryItem *>(index.internalPointer());
317
318 switch (role) {
319 case Qt::DisplayRole:
320 if (MusicLibraryItem::Type_Song==item->itemType()) {
321 MusicLibraryItemSong *song = static_cast<MusicLibraryItemSong *>(item);
322 if (MusicLibraryItem::Type_Root==song->parentItem()->itemType()) {
323 return song->song().trackAndTitleStr();
324 }
325 }
326 break;
327 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
328 case Cantata::Role_Image:
329 if (MusicLibraryItem::Type_Root==item->itemType()) {
330 Device *dev=static_cast<Device *>(item);
331 if (Device::AudioCd==dev->devType()) {
332 return static_cast<AudioCdDevice *>(dev)->coverPix();
333 }
334 }
335 break;
336 #endif
337 case Cantata::Role_SubText:
338 if (MusicLibraryItem::Type_Root==item->itemType()) {
339 Device *dev=static_cast<Device *>(item);
340 if (!dev->statusMessage().isEmpty()) {
341 return dev->statusMessage();
342 }
343 if (!dev->isConnected()) {
344 QString sub=dev->subText();
345 return tr("Not Connected")+(sub.isEmpty() ? QString() : (Song::constSep+sub));
346 }
347 if (Device::AudioCd==dev->devType()) {
348 return dev->subText();
349 }
350 }
351 break;
352 case Cantata::Role_Capacity:
353 if (MusicLibraryItem::Type_Root==item->itemType()) {
354 return static_cast<Device *>(item)->usedCapacity();
355 }
356 return QVariant();
357 case Cantata::Role_CapacityText:
358 if (MusicLibraryItem::Type_Root==item->itemType()) {
359 return static_cast<Device *>(item)->capacityString();
360 }
361 return QVariant();
362 case Cantata::Role_Actions: {
363 QVariant v;
364 if (MusicLibraryItem::Type_Root==item->itemType()) {
365 QList<Action *> actions;
366 if (Device::AudioCd!=static_cast<Device *>(item)->devType()) {
367 actions << configureAction;
368 } else if (HttpServer::self()->isAlive()) {
369 actions << StdActions::self()->replacePlayQueueAction;
370 }
371 actions << refreshAction;
372 if (static_cast<Device *>(item)->supportsDisconnect()) {
373 actions << (static_cast<Device *>(item)->isConnected() ? disconnectAction : connectAction);
374 }
375 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
376 if (Device::AudioCd==static_cast<Device *>(item)->devType()) {
377 actions << editAction;
378 }
379 #endif
380 v.setValue<QList<Action *> >(actions);
381 } else if (root(item)->canPlaySongs() && HttpServer::self()->isAlive()) {
382 v.setValue<QList<Action *> >(QList<Action *>() << StdActions::self()->replacePlayQueueAction << StdActions::self()->appendToPlayQueueAction);
383 }
384 return v;
385 }
386 case Cantata::Role_ListImage:
387 return MusicLibraryItem::Type_Album==item->itemType();
388 default:
389 break;
390 }
391 return MusicLibraryModel::data(index, role);
392 }
393
clear(bool clearConfig)394 void DevicesModel::clear(bool clearConfig)
395 {
396 inhibitMenuUpdate=true;
397 QSet<QString> remoteUdis;
398 QSet<QString> udis;
399 for (MusicLibraryItemRoot *col: collections) {
400 Device *dev=static_cast<Device *>(col);
401 if (Device::RemoteFs==dev->devType()) {
402 remoteUdis.insert(dev->id());
403 } else {
404 udis.insert(dev->id());
405 }
406 }
407
408 for (const QString &u: udis) {
409 deviceRemoved(u);
410 }
411 for (const QString &u: remoteUdis) {
412 removeRemoteDevice(u, clearConfig);
413 }
414
415 collections.clear();
416 volumes.clear();
417 inhibitMenuUpdate=false;
418 updateItemMenu();
419 }
420
setEnabled(bool e)421 void DevicesModel::setEnabled(bool e)
422 {
423 StdActions::self()->copyToDeviceAction->setVisible(e);
424
425 if (e==enabled) {
426 return;
427 }
428
429 enabled=e;
430
431 inhibitMenuUpdate=true;
432 if (enabled) {
433 connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(const QString &)), this, SLOT(deviceAdded(const QString &)));
434 connect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(const QString &)), this, SLOT(deviceRemoved(const QString &)));
435 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
436 connect(Covers::self(), SIGNAL(cover(const Song &, const QImage &, const QString &)),
437 this, SLOT(setCover(const Song &, const QImage &, const QString &)));
438 #endif
439 // Call loadLocal via a timer, so that upon Cantata start-up model is loaded into view before we try and expand items!
440 QTimer::singleShot(0, this, SIGNAL(loadLocal()));
441 connect(MountPoints::self(), SIGNAL(updated()), this, SLOT(mountsChanged()));
442 #ifdef ENABLE_REMOTE_DEVICES
443 loadRemote();
444 #endif
445 } else {
446 stop();
447 clear(false);
448 }
449 inhibitMenuUpdate=false;
450 updateItemMenu();
451 }
452
stop()453 void DevicesModel::stop()
454 {
455 for (MusicLibraryItemRoot *col: collections) {
456 static_cast<Device *>(col)->stop();
457 }
458
459 disconnect(Solid::DeviceNotifier::instance(), SIGNAL(deviceAdded(const QString &)), this, SLOT(deviceAdded(const QString &)));
460 disconnect(Solid::DeviceNotifier::instance(), SIGNAL(deviceRemoved(const QString &)), this, SLOT(deviceRemoved(const QString &)));
461 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
462 disconnect(Covers::self(), SIGNAL(cover(const Song &, const QImage &, const QString &)),
463 this, SLOT(setCover(const Song &, const QImage &, const QString &)));
464 #endif
465 disconnect(MountPoints::self(), SIGNAL(updated()), this, SLOT(mountsChanged()));
466 #if defined ENABLE_REMOTE_DEVICES
467 unmountRemote();
468 #endif
469 }
470
device(const QString & udi)471 Device * DevicesModel::device(const QString &udi)
472 {
473 int idx=indexOf(udi);
474 return idx<0 ? nullptr : static_cast<Device *>(collections.at(idx));
475 }
476
setCover(const Song & song,const QImage & img,const QString & file)477 void DevicesModel::setCover(const Song &song, const QImage &img, const QString &file)
478 {
479 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
480 DBUG << "Set CDDA cover" << song.file << img.isNull() << file << song.isCdda();
481 if (song.isCdda()) {
482 int idx=indexOf(song.title);
483 if (idx>=0) {
484 Device *dev=static_cast<Device *>(collections.at(idx));
485 if (Device::AudioCd==dev->devType()) {
486 DBUG << "Set cover of CD";
487 Covers::self()->updateCover(song, img, file);
488 static_cast<AudioCdDevice *>(dev)->setCover(song, img, file);
489 }
490 }
491 }
492 #else
493 Q_UNUSED(song)
494 Q_UNUSED(img)
495 Q_UNUSED(file)
496 #endif
497 }
498
setCover(const Song & song,const QImage & img)499 void DevicesModel::setCover(const Song &song, const QImage &img)
500 {
501 DBUG << "Set album cover" << song.file << img.isNull();
502 if (img.isNull()) {
503 return;
504 }
505
506 Device *dev=qobject_cast<Device *>(sender());
507 if (!dev) {
508 return;
509 }
510 int i=collections.indexOf(dev);
511 if (i<0) {
512 return;
513 }
514 MusicLibraryItemArtist *artistItem = dev->artist(song, false);
515 if (artistItem) {
516 MusicLibraryItemAlbum *albumItem = artistItem->album(song, false);
517 if (albumItem) {
518 DBUG << "Set cover of album";
519 Covers::self()->updateCover(song, img, QString());
520 QModelIndex idx=index(albumItem->row(), 0, index(artistItem->row(), 0, index(i, 0, QModelIndex())));
521 emit dataChanged(idx, idx);
522 }
523 }
524 }
525
deviceUpdating(const QString & udi,bool state)526 void DevicesModel::deviceUpdating(const QString &udi, bool state)
527 {
528 int idx=indexOf(udi);
529 if (idx>=0) {
530 Device *dev=static_cast<Device *>(collections.at(idx));
531
532 if (state) {
533 QModelIndex modelIndex=createIndex(idx, 0, dev);
534 emit dataChanged(modelIndex, modelIndex);
535 } else {
536 if (dev->haveUpdate()) {
537 dev->applyUpdate();
538 }
539 QModelIndex modelIndex=createIndex(idx, 0, dev);
540 emit dataChanged(modelIndex, modelIndex);
541 emit updated(modelIndex);
542 }
543 }
544 }
545
flags(const QModelIndex & index) const546 Qt::ItemFlags DevicesModel::flags(const QModelIndex &index) const
547 {
548 if (index.isValid()) {
549 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
550 }
551 return Qt::NoItemFlags;
552 }
553
playableUrls(const QModelIndexList & indexes) const554 QStringList DevicesModel::playableUrls(const QModelIndexList &indexes) const
555 {
556 QList<Song> songList=songs(indexes, true, true);
557 QStringList urls;
558 for (const Song &s: songList) {
559 QByteArray encoded=HttpServer::self()->encodeUrl(s);
560 if (!encoded.isEmpty()) {
561 urls.append(encoded);
562 }
563 }
564 return urls;
565 }
566
emitAddToDevice()567 void DevicesModel::emitAddToDevice()
568 {
569 QAction *act=qobject_cast<QAction *>(sender());
570
571 if (act) {
572 emit addToDevice(act->data().toString());
573 }
574 }
575
deviceAdded(const QString & udi)576 void DevicesModel::deviceAdded(const QString &udi)
577 {
578 if (indexOf(udi)>=0) {
579 return;
580 }
581
582 Solid::Device device(udi);
583 DBUG << "Solid device added udi:" << device.udi() << "product:" << device.product() << "vendor:" << device.vendor();
584 Solid::StorageAccess *ssa =nullptr;
585 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
586 Solid::OpticalDisc * opt = device.as<Solid::OpticalDisc>();
587
588 if (opt && (opt->availableContent()&Solid::OpticalDisc::Audio)) {
589 DBUG << "device is audiocd";
590 } else
591 #endif
592 if ((ssa=device.as<Solid::StorageAccess>())) {
593 if ((!device.parent().as<Solid::StorageDrive>() || Solid::StorageDrive::Usb!=device.parent().as<Solid::StorageDrive>()->bus()) &&
594 (!device.as<Solid::StorageDrive>() || Solid::StorageDrive::Usb!=device.as<Solid::StorageDrive>()->bus())) {
595 DBUG << "Found Solid::StorageAccess that is not usb, skipping";
596 return;
597 }
598 DBUG << "volume is generic storage";
599 if (!volumes.contains(device.udi())) {
600 connect(ssa, SIGNAL(accessibilityChanged(bool, const QString&)), this, SLOT(accessibilityChanged(bool, const QString&)));
601 volumes.insert(device.udi());
602 }
603 } else if (device.is<Solid::StorageDrive>()) {
604 DBUG << "device is a Storage drive, still need a volume";
605 } else if (device.is<Solid::PortableMediaPlayer>()) {
606 DBUG << "device is a PMP";
607 } else {
608 DBUG << "device not handled";
609 return;
610 }
611 addLocalDevice(device.udi());
612 }
613
addLocalDevice(const QString & udi)614 void DevicesModel::addLocalDevice(const QString &udi)
615 {
616 if (device(udi)) {
617 return;
618 }
619 Device *dev=Device::create(this, udi);
620 if (dev) {
621 beginInsertRows(QModelIndex(), collections.count(), collections.count());
622 collections.append(dev);
623 endInsertRows();
624 connect(dev, SIGNAL(updating(const QString &, bool)), SLOT(deviceUpdating(const QString &, bool)));
625 connect(dev, SIGNAL(error(const QString &)), SIGNAL(error(const QString &)));
626 connect(dev, SIGNAL(cover(const Song &, const QImage &)), SLOT(setCover(const Song &, const QImage &)));
627 connect(dev, SIGNAL(updatedDetails(QList<Song>)), SIGNAL(updatedDetails(QList<Song>)));
628 connect(dev, SIGNAL(play(QList<Song>)), SLOT(play(QList<Song>)));
629 connect(dev, SIGNAL(renamed()), this, SLOT(updateItemMenu()));
630 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
631 if (Device::AudioCd==dev->devType()) {
632 connect(static_cast<AudioCdDevice *>(dev), SIGNAL(matches(const QString &, const QList<CdAlbum> &)),
633 SIGNAL(matches(const QString &, const QList<CdAlbum> &)));
634 if (!autoplayCd.isEmpty() && static_cast<AudioCdDevice *>(dev)->isAudioDevice(autoplayCd)) {
635 autoplayCd=QString();
636 static_cast<AudioCdDevice *>(dev)->autoplay();
637 }
638 }
639 #endif
640 updateItemMenu();
641 }
642 }
643
deviceRemoved(const QString & udi)644 void DevicesModel::deviceRemoved(const QString &udi)
645 {
646 int idx=indexOf(udi);
647 DBUG << "Solid device removed udi = " << udi << idx;
648 if (idx>=0) {
649 if (volumes.contains(udi)) {
650 Solid::Device device(udi);
651 Solid::StorageAccess *ssa = device.as<Solid::StorageAccess>();
652 if (ssa) {
653 disconnect(ssa, SIGNAL(accessibilityChanged(bool, const QString&)), this, SLOT(accessibilityChanged(bool, const QString&)));
654 }
655 volumes.remove(udi);
656 }
657
658 beginRemoveRows(QModelIndex(), idx, idx);
659 Device *dev=static_cast<Device *>(collections.takeAt(idx));
660 dev->deleteLater();
661 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
662 if (Device::AudioCd==dev->devType()) {
663 static_cast<AudioCdDevice *>(dev)->dequeue();
664 }
665 #endif
666 endRemoveRows();
667 updateItemMenu();
668 }
669 }
670
accessibilityChanged(bool accessible,const QString & udi)671 void DevicesModel::accessibilityChanged(bool accessible, const QString &udi)
672 {
673 Q_UNUSED(accessible)
674 int idx=indexOf(udi);
675 DBUG << "Solid device accesibility changed udi = " << udi << idx << accessible;
676 if (idx>=0) {
677 Device *dev=static_cast<Device *>(collections.at(idx));
678 if (dev) {
679 dev->connectionStateChanged();
680 QModelIndex modelIndex=createIndex(idx, 0, dev);
681 emit dataChanged(modelIndex, modelIndex);
682 }
683 }
684 }
addRemoteDevice(const DeviceOptions & opts,RemoteFsDevice::Details details)685 void DevicesModel::addRemoteDevice(const DeviceOptions &opts, RemoteFsDevice::Details details)
686 {
687 #ifdef ENABLE_REMOTE_DEVICES
688 Device *dev=RemoteFsDevice::create(this, opts, details);
689
690 if (dev) {
691 beginInsertRows(QModelIndex(), collections.count(), collections.count());
692 collections.append(dev);
693 endInsertRows();
694 connect(dev, SIGNAL(updating(const QString &, bool)), SLOT(deviceUpdating(const QString &, bool)));
695 connect(dev, SIGNAL(error(const QString &)), SIGNAL(error(const QString &)));
696 connect(dev, SIGNAL(cover(const Song &, const QImage &)), SLOT(setCover(const Song &, const QImage &)));
697 if (Device::RemoteFs==dev->devType()) {
698 connect(static_cast<RemoteFsDevice *>(dev), SIGNAL(udiChanged()), SLOT(remoteDeviceUdiChanged()));
699 }
700 updateItemMenu();
701 }
702 #else
703 Q_UNUSED(opts)
704 Q_UNUSED(details)
705 #endif
706 }
707
removeRemoteDevice(const QString & udi,bool removeFromConfig)708 void DevicesModel::removeRemoteDevice(const QString &udi, bool removeFromConfig)
709 {
710 #ifdef ENABLE_REMOTE_DEVICES
711 int idx=indexOf(udi);
712 if (idx<0) {
713 return;
714 }
715
716 Device *dev=static_cast<Device *>(collections.at(idx));
717
718 if (dev && Device::RemoteFs==dev->devType()) {
719 beginRemoveRows(QModelIndex(), idx, idx);
720 // Remove device from list, but do NOT delete - it may be scanning!!!!
721 collections.takeAt(idx);
722 endRemoveRows();
723 updateItemMenu();
724 RemoteFsDevice *rfs=qobject_cast<RemoteFsDevice *>(dev);
725 if (rfs) {
726 // Destroy will stop device, and delete it (via deleteLater())
727 rfs->destroy(removeFromConfig);
728 }
729 }
730 #else
731 Q_UNUSED(udi)
732 Q_UNUSED(removeFromConfig)
733 #endif
734 }
735
remoteDeviceUdiChanged()736 void DevicesModel::remoteDeviceUdiChanged()
737 {
738 #ifdef ENABLE_REMOTE_DEVICES
739 updateItemMenu();
740 #endif
741 }
742
mountsChanged()743 void DevicesModel::mountsChanged()
744 {
745 #ifdef ENABLE_REMOTE_DEVICES
746 for (MusicLibraryItemRoot *col: collections) {
747 Device *dev=static_cast<Device *>(col);
748 if (Device::RemoteFs==dev->devType() && ((RemoteFsDevice *)dev)->getDetails().isLocalFile()) {
749 if (0==dev->childCount()) {
750 ((RemoteFsDevice *)dev)->load();
751 } else if (!dev->isConnected()) {
752 ((RemoteFsDevice *)dev)->clear();
753 }
754 }
755 }
756 #endif
757
758 // For some reason if a device without a partition (e.g. /dev/sdc) is mounted whilst cantata is running, then we receive no deviceAdded signal
759 // So, as a work-around, each time a device is mounted - check for all local collections. :-)
760 // BUG:127
761 loadLocal();
762 }
763
loadLocal()764 void DevicesModel::loadLocal()
765 {
766 // Build set of currently known MTP/UMS collections...
767 QSet<QString> existingUdis;
768 for (MusicLibraryItemRoot *col: collections) {
769 Device *dev=static_cast<Device *>(col);
770 if (Device::Mtp==dev->devType() || Device::Ums==dev->devType()) {
771 existingUdis.insert(dev->id());
772 }
773 }
774
775 QList<Solid::Device> deviceList = Solid::Device::listFromType(Solid::DeviceInterface::PortableMediaPlayer);
776 for (const Solid::Device &device: deviceList) {
777 if (existingUdis.contains(device.udi())) {
778 existingUdis.remove(device.udi());
779 continue;
780 }
781 if (device.as<Solid::StorageDrive>()) {
782 DBUG << "Solid PMP that is also a StorageDrive, skipping, udi:" << device.udi() << "product:" << device.product() << "vendor:" << device.vendor();
783 continue;
784 }
785 DBUG << "Solid::PortableMediaPlayer with udi:" << device.udi() << "product:" << device.product() << "vendor:" << device.vendor();
786 addLocalDevice(device.udi());
787 }
788 deviceList = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess);
789 for (const Solid::Device &device: deviceList) {
790 if (existingUdis.contains(device.udi())) {
791 existingUdis.remove(device.udi());
792 continue;
793 }
794 DBUG << "Solid::StorageAccess with udi:" << device.udi() << "product:" << device.product() << "vendor:" << device.vendor();
795 const Solid::StorageAccess *ssa = device.as<Solid::StorageAccess>();
796
797 if (ssa) {
798 if ((!device.parent().as<Solid::StorageDrive>() || Solid::StorageDrive::Usb!=device.parent().as<Solid::StorageDrive>()->bus()) &&
799 (!device.as<Solid::StorageDrive>() || Solid::StorageDrive::Usb!=device.as<Solid::StorageDrive>()->bus())) {
800 DBUG << "Solid::StorageAccess that is not usb, skipping";
801 continue;
802 }
803 if (!volumes.contains(device.udi())) {
804 connect(ssa, SIGNAL(accessibilityChanged(bool, const QString&)), this, SLOT(accessibilityChanged(bool, const QString&)));
805 volumes.insert(device.udi());
806 }
807 addLocalDevice(device.udi());
808 }
809 }
810
811 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
812 deviceList = Solid::Device::listFromType(Solid::DeviceInterface::OpticalDisc);
813 for (const Solid::Device &device: deviceList) {
814 if (existingUdis.contains(device.udi())) {
815 existingUdis.remove(device.udi());
816 continue;
817 }
818 DBUG << "Solid::OpticalDisc with udi:" << device.udi() << "product:" << device.product() << "vendor:" << device.vendor();
819 const Solid::OpticalDisc * opt = device.as<Solid::OpticalDisc>();
820 if (opt && (opt->availableContent()&Solid::OpticalDisc::Audio)) {
821 addLocalDevice(device.udi());
822 } else {
823 DBUG << "Solid::OpticalDisc that is not audio, skipping";
824 }
825 }
826 #endif
827
828 // Remove any previous MTP/UMS devices that were not listed above.
829 // This is to fix BUG:127
830 for (const QString &udi: existingUdis) {
831 deviceRemoved(udi);
832 }
833 }
834
835 #ifdef ENABLE_REMOTE_DEVICES
loadRemote()836 void DevicesModel::loadRemote()
837 {
838 QList<Device *> rem=RemoteFsDevice::loadAll(this);
839 if (rem.count()) {
840 beginInsertRows(QModelIndex(), collections.count(), collections.count()+(rem.count()-1));
841 for (Device *dev: rem) {
842 collections.append(dev);
843 connect(dev, SIGNAL(updating(const QString &, bool)), SLOT(deviceUpdating(const QString &, bool)));
844 connect(dev, SIGNAL(error(const QString &)), SIGNAL(error(const QString &)));
845 connect(dev, SIGNAL(cover(const Song &, const QImage &)), SLOT(setCover(const Song &, const QImage &)));
846 if (Device::RemoteFs==dev->devType()) {
847 connect(static_cast<RemoteFsDevice *>(dev), SIGNAL(udiChanged()), SLOT(remoteDeviceUdiChanged()));
848 }
849 }
850 endInsertRows();
851 updateItemMenu();
852 }
853 }
854
unmountRemote()855 void DevicesModel::unmountRemote()
856 {
857 for (MusicLibraryItemRoot *col: collections) {
858 Device *dev=static_cast<Device *>(col);
859 if (Device::RemoteFs==dev->devType()) {
860 static_cast<RemoteFsDevice *>(dev)->unmount();
861 }
862 }
863 }
864 #endif
865
866 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
playCd(const QString & dev)867 void DevicesModel::playCd(const QString &dev)
868 {
869 for (MusicLibraryItemRoot *col: collections) {
870 Device *d=static_cast<Device *>(col);
871 if (Device::AudioCd==d->devType() && static_cast<AudioCdDevice *>(d)->isAudioDevice(dev)) {
872 static_cast<AudioCdDevice *>(d)->autoplay();
873 return;
874 }
875 }
876 autoplayCd=dev;
877 }
878
879 #endif
880
lessThan(const QString & left,const QString & right)881 static bool lessThan(const QString &left, const QString &right)
882 {
883 return left.localeAwareCompare(right)<0;
884 }
885
updateItemMenu()886 void DevicesModel::updateItemMenu()
887 {
888 if (inhibitMenuUpdate) {
889 return;
890 }
891
892 if (!itemMenu) {
893 itemMenu = new MirrorMenu(nullptr);
894 }
895
896 itemMenu->clear();
897
898 if (!collections.isEmpty()) {
899 QMap<QString, const MusicLibraryItemRoot *> items;
900
901 for (const MusicLibraryItemRoot *d: collections) {
902 if (Device::AudioCd!=static_cast<const Device *>(d)->devType()) {
903 items.insert(d->data(), d);
904 }
905 }
906
907 QStringList keys=items.keys();
908 std::sort(keys.begin(), keys.end(), lessThan);
909
910 for (const QString &k: keys) {
911 const MusicLibraryItemRoot *d=items[k];
912 QAction *act=itemMenu->addAction(d->icon(), k, this, SLOT(emitAddToDevice()));
913 act->setData(d->id());
914 Action::initIcon(act);
915 }
916 }
917
918 if (itemMenu->isEmpty()) {
919 itemMenu->addAction(tr("No Devices Attached"))->setEnabled(false);
920 }
921 }
922
mimeData(const QModelIndexList & indexes) const923 QMimeData * DevicesModel::mimeData(const QModelIndexList &indexes) const
924 {
925 QMimeData *mimeData=nullptr;
926 QStringList paths=playableUrls(indexes);
927
928 if (!paths.isEmpty()) {
929 mimeData=new QMimeData();
930 PlayQueueModel::encode(*mimeData, PlayQueueModel::constUriMimeType, paths);
931 }
932 return mimeData;
933 }
934
play(const QList<Song> & songs)935 void DevicesModel::play(const QList<Song> &songs)
936 {
937 QStringList paths;
938 if (HttpServer::self()->isAlive()) {
939 for (const Song &s: songs) {
940 paths.append(HttpServer::self()->encodeUrl(s));
941 }
942
943 if (!paths.isEmpty()) {
944 emit add(paths, MPDConnection::ReplaceAndplay, 0, false);
945 }
946 }
947 }
948
949 #include "moc_devicesmodel.cpp"
950