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