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 "mtpdevice.h"
25 #include "models/musiclibraryitemsong.h"
26 #include "models/musiclibraryitemalbum.h"
27 #include "models/musiclibraryitemartist.h"
28 #include "models/musiclibraryitemroot.h"
29 #include "models/mpdlibrarymodel.h"
30 #include "models/devicesmodel.h"
31 #include "devicepropertiesdialog.h"
32 #include "devicepropertieswidget.h"
33 #include "gui/covers.h"
34 #include "mpd-interface/song.h"
35 #include "encoders.h"
36 #include "transcodingjob.h"
37 #include "support/utils.h"
38 #include "mpd-interface/mpdparseutils.h"
39 #include "mpd-interface/mpdconnection.h"
40 #include "filejob.h"
41 #include "support/configuration.h"
42 #include "support/thread.h"
43 #include "support/monoicon.h"
44 #include <QTimer>
45 #include <QDir>
46 #include <QTemporaryFile>
47 #include <sys/types.h>
48 #include <sys/stat.h>
49 #include <unistd.h>
50 //#define TIME_MTP_OPERATIONS
51 #ifdef TIME_MTP_OPERATIONS
52 #include <QElapsedTimer>
53 #endif
54 #include <QDebug>
55 
56 #define COVER_DBUG if (Covers::debugEnabled()) qWarning() << metaObject()->className() << __FUNCTION__
57 #define DBUG_CLASS(CLASS) if (DevicesModel::debugEnabled()) qWarning() << CLASS << QThread::currentThread()->objectName() << __FUNCTION__
58 #define DBUG DBUG_CLASS(metaObject()->className())
59 
60 // Enable the following #define to have Cantata attempt to ascertain the AlbumArtist tag by
61 // looking at the file path
62 #define MTP_FAKE_ALBUMARTIST_SUPPORT
63 
64 // Enable the following #define to have Cantata attempt to ascertain the tracks's track number
65 // from its filename. This will only be done if the device returns '0' as the track number.
66 #define MTP_TRACKNUMBER_FROM_FILENAME
67 
68 static const QLatin1String constMtpDefaultCover("AlbumArt.jpg");
69 static const uint32_t constRootFolder=0xffffffffU;
70 static const QString constMusicFolder=QLatin1String("Music");
71 
72 static const quint16 constOrigFileName = Song::Performer;
73 
progressMonitor(uint64_t const processed,uint64_t const total,void const * const data)74 static int progressMonitor(uint64_t const processed, uint64_t const total, void const * const data)
75 {
76     const MtpConnection *con=static_cast<const MtpConnection *>(data);
77     const_cast<MtpConnection *>(con)->emitProgress((int)(((processed*1.0)/(total*1.0)*100.0)+0.5));
78     return con->abortWasRequested() ? -1 : 0;
79 }
80 
trackListMonitor(uint64_t const processed,uint64_t const total,void const * const data)81 static int trackListMonitor(uint64_t const processed, uint64_t const total, void const * const data)
82 {
83     const MtpConnection *con=static_cast<const MtpConnection *>(data);
84     const_cast<MtpConnection *>(con)->trackListProgress(((processed*100.0)/(total*1.0))+0.5);
85     return con->abortWasRequested() ? -1 : 0;
86 }
87 
fileReceiver(void * params,void * priv,uint32_t sendlen,unsigned char * data,uint32_t * putlen)88 static uint16_t fileReceiver(void *params, void *priv, uint32_t sendlen, unsigned char *data, uint32_t *putlen)
89 {
90     Q_UNUSED(params)
91     QByteArray *byteData=static_cast<QByteArray *>(priv);
92     (*byteData)+=QByteArray((char *)data, (int)sendlen);
93     *putlen = sendlen;
94     return LIBMTP_HANDLER_RETURN_OK;
95 }
96 
MtpConnection(const QString & id,unsigned int bus,unsigned int dev,bool aaSupport)97 MtpConnection::MtpConnection(const QString &id, unsigned int bus, unsigned int dev, bool aaSupport)
98     : device(0)
99     #ifdef MTP_CLEAN_ALBUMS
100     , albums(0)
101     #endif
102     , library(0)
103     , lastListPercent(-1)
104     , abortRequested(false)
105     , busNum(bus)
106     , devNum(dev)
107     , supprtAlbumArtistTag(aaSupport)
108 {
109     size=0;
110     used=0;
111     LIBMTP_Init();
112     thread=new Thread(metaObject()->className()+QLatin1String("::")+id);
113     moveToThread(thread);
114     thread->start();
115 }
116 
~MtpConnection()117 MtpConnection::~MtpConnection()
118 {
119     stop();
120 }
121 
takeLibrary()122 MusicLibraryItemRoot * MtpConnection::takeLibrary()
123 {
124     MusicLibraryItemRoot *lib=library;
125     library=0;
126     return lib;
127 }
128 
emitProgress(int percent)129 void MtpConnection::emitProgress(int percent)
130 {
131     emit progress(percent);
132 }
133 
trackListProgress(int percent)134 void MtpConnection::trackListProgress(int percent)
135 {
136     if (percent!=lastListPercent) {
137         lastListPercent=percent;
138         emit updatePercentage(percent);
139     }
140 }
141 
connectToDevice()142 void MtpConnection::connectToDevice()
143 {
144     #ifdef TIME_MTP_OPERATIONS
145     QElapsedTimer timer;
146     QElapsedTimer totalTimer;
147     timer.start();
148     totalTimer.start();
149     #endif
150 
151     device=0;
152     storage.clear();
153     defaultMusicFolder=0;
154     LIBMTP_raw_device_t *rawDevices=0;
155     int numDev=-1;
156     emit statusMessage(tr("Connecting to device..."));
157     if (LIBMTP_ERROR_NONE!=LIBMTP_Detect_Raw_Devices(&rawDevices, &numDev) || numDev<=0) {
158         emit statusMessage(tr("No devices found"));
159         return;
160     }
161     #ifdef TIME_MTP_OPERATIONS
162     qWarning() << "Connect to device:" << timer.elapsed();
163     timer.restart();
164     #endif
165 
166     LIBMTP_mtpdevice_t *mptDev=0;
167     for (int i = 0; i < numDev; i++) {
168         if (0!=busNum && 0!=devNum) {
169             if (rawDevices[i].bus_location==busNum && rawDevices[i].devnum==devNum) {
170                 mptDev = LIBMTP_Open_Raw_Device_Uncached(&rawDevices[i]);
171                 break;
172             }
173         } else {
174             if ((mptDev = LIBMTP_Open_Raw_Device_Uncached(&rawDevices[i]))) {
175                 break;
176             }
177         }
178     }
179 
180     #ifdef TIME_MTP_OPERATIONS
181     qWarning() << "Open raw device:" << timer.elapsed();
182     timer.restart();
183     #endif
184 
185     size=0;
186     used=0;
187     device=mptDev;
188     updateStorage();
189     #ifdef TIME_MTP_OPERATIONS
190     qWarning() << "Update storage:" << timer.elapsed();
191     #endif
192 
193     free(rawDevices);
194     if (!device) {
195         emit statusMessage(tr("No devices found"));
196         #ifdef TIME_MTP_OPERATIONS
197         qWarning() << "TOTAL connect:" <<totalTimer.elapsed();
198         #endif
199         return;
200     }
201     char *ser=LIBMTP_Get_Serialnumber(device);
202     if (ser) {
203         emit deviceDetails(QString::fromUtf8(ser));
204         delete ser;
205     } else {
206         emit deviceDetails(QString());
207     }
208 
209     defaultMusicFolder=device->default_music_folder;
210     emit statusMessage(tr("Connected to device"));
211     #ifdef TIME_MTP_OPERATIONS
212     qWarning() << "TOTAL connect:" <<totalTimer.elapsed();
213     #endif
214 }
215 
disconnectFromDevice(bool showStatus)216 void MtpConnection::disconnectFromDevice(bool showStatus)
217 {
218     if (device) {
219         destroyData();
220         LIBMTP_Release_Device(device);
221         device=0;
222         if (showStatus) {
223             emit statusMessage(tr("Disconnected from device"));
224         }
225     }
226 }
227 
228 #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
229 struct MtpAlbum {
230     uint32_t folder;
231     QSet<QString> artists;
232     QList<MusicLibraryItemSong *> songs;
233 };
234 struct MtpFolder {
MtpFolderMtpFolder235     MtpFolder(const QString &ar=QString(), const QString &al=QString()) : artist(ar), album(al) { }
236     QString artist;
237     QString album;
238 };
239 #endif
240 
241 struct Path {
PathPath242     Path() : storage(0), parent(0), id(0) { }
okPath243     bool ok() const { return 0!=storage && 0!=parent && 0!=id; }
244     uint32_t storage;
245     uint32_t parent;
246     uint32_t id;
247     QString path;
248 };
249 
encodePath(LIBMTP_track_t * track,const QString & path,const QString & store)250 static QString encodePath(LIBMTP_track_t *track, const QString &path, const QString &store)
251 {
252     return QChar('{')+QString::number(track->storage_id)+QChar('/')+QString::number(track->parent_id)+QChar('/')+QString::number(track->item_id)+
253            (store.isEmpty() ? QString() : (QChar('/')+store))+QChar('}')+path;
254 }
255 
decodePath(const QString & path)256 static Path decodePath(const QString &path)
257 {
258     Path p;
259     if (path.startsWith(QChar('{')) && path.contains(QChar('}'))) {
260         int end=path.indexOf(QChar('}'));
261         QStringList details=path.mid(1, end-1).split(QChar('/'));
262         if (details.count()>=3) {
263             p.storage=details.at(0).toUInt();
264             p.parent=details.at(1).toUInt();
265             p.id=details.at(2).toUInt();
266         }
267         p.path=path.mid(end+1);
268     } else {
269         p.path=path;
270     }
271 
272     return p;
273 }
274 
updateLibrary(const DeviceOptions & opts)275 void MtpConnection::updateLibrary(const DeviceOptions &opts)
276 {
277     if (!isConnected()) {
278         connectToDevice();
279     }
280 
281     destroyData();
282 
283     if (!isConnected()) {
284         emit libraryUpdated();
285         return;
286     }
287 
288     #ifdef TIME_MTP_OPERATIONS
289     QElapsedTimer timer;
290     QElapsedTimer totalTimer;
291     timer.start();
292     totalTimer.start();
293     #endif
294 
295     library = new MusicLibraryItemRoot;
296     emit statusMessage(tr("Updating folders..."));
297     updateFilesAndFolders();
298     if (abortRequested) {
299         return;
300     }
301     #ifdef TIME_MTP_OPERATIONS
302     qWarning() << "Folder update:" << timer.elapsed();
303     timer.restart();
304     #endif
305     if (folderMap.isEmpty()) {
306         destroyData();
307         emit libraryUpdated();
308         return;
309     }
310     #ifdef MTP_CLEAN_ALBUMS
311     updateAlbums();
312     #ifdef TIME_MTP_OPERATIONS
313     qWarning() << "Clean albums:" << timer.elapsed();
314     timer.restart();
315     #endif
316     #endif
317     emit statusMessage(tr("Updating tracks..."));
318     lastListPercent=-1;
319     LIBMTP_track_t *tracks=LIBMTP_Get_Tracklisting_With_Callback(device, &trackListMonitor, this);
320     QMap<uint32_t, Folder>::ConstIterator folderEnd=folderMap.constEnd();
321     QList<Storage>::Iterator store=storage.begin();
322     QList<Storage>::Iterator storeEnd=storage.end();
323     QMap<int, QString> storageNames;
324     for (; store!=storeEnd; ++store) {
325         setMusicFolder(*store);
326         storageNames[(*store).id]=(*store).description;
327     }
328     if (abortRequested) {
329         return;
330     }
331 
332     MusicLibraryItemArtist *artistItem = 0;
333     MusicLibraryItemAlbum *albumItem = 0;
334     #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
335     QMap<QString, MtpAlbum> albumMap;
336     QMap<uint32_t, MtpFolder> folders;
337     bool getAlbumArtistFromPath=opts.scheme.startsWith(DeviceOptions::constAlbumArtist+QChar('/')+DeviceOptions::constAlbumTitle+QChar('/'));
338     #endif
339     #ifdef TIME_MTP_OPERATIONS
340     qWarning() << "Tracks update:" << timer.elapsed();
341     timer.restart();
342     #endif
343     while (tracks) {
344         if (abortRequested) {
345             return;
346         }
347         LIBMTP_track_t *track=tracks;
348         tracks=tracks->next;
349 
350         QMap<uint32_t, Folder>::ConstIterator folder=folderMap.find(track->parent_id);
351         if (folder==folderEnd) {
352             // We only care about tracks in the music folder...
353             LIBMTP_destroy_track_t(track);
354             continue;
355         }
356         Song s;
357         QString trackFilename=QString::fromUtf8(track->filename);
358         s.id=track->item_id;
359         s.file=encodePath(track, folder.value().path+trackFilename, storageNames.count()>1 ? storageNames[track->storage_id] : QString());
360         s.album=QString::fromUtf8(track->album);
361         s.artist=QString::fromUtf8(track->artist);
362         s.albumartist=s.artist; // TODO: ALBUMARTIST: Read from 'track' when libMTP supports album artist!
363         QString composer=QString::fromUtf8(track->composer);
364         if (!composer.isEmpty()) {
365             s.setComposer(composer);
366         }
367         s.year=QString::fromUtf8(track->date).mid(0, 4).toUInt();
368         s.title=QString::fromUtf8(track->title);
369         s.genres[0]=QString::fromUtf8(track->genre);
370         s.track=track->tracknumber;
371         s.time=(track->duration/1000.0)+0.5;
372         s.size=track->filesize;
373         s.fillEmptyFields();
374         s.populateSorts();
375         #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
376         if (getAlbumArtistFromPath) {
377             QStringList folderParts=(*folder).path.split('/', QString::SkipEmptyParts);
378             if (folderParts.length()>=3) {
379                 // Path should be "Music/${AlbumArtist}/${Album}"
380                 int artistPath=1;
381                 int albumPath=2;
382                 // BubbleUPNP will download to "Music/Artist/${AlbumArtist}/${Album}" if it is configured to
383                 // download to "Music" and "Preserve folder structure" (At least this is the folder structure
384                 // from MiniDLNA...)
385                 if (folderParts.length()>=4 && QLatin1String("Artist")==folderParts.at(1)) {
386                     artistPath++;
387                     albumPath++;
388                 }
389                 MtpFolder folder(folderParts.at(artistPath), folderParts.at(albumPath));
390                 folders.insert(track->parent_id, folder);
391                 if (folder.album==s.album && Song::isVariousArtists(folder.artist)) {
392                     s.albumartist=folder.artist;
393                 }
394             }
395         }
396         #endif
397         #ifdef MTP_TRACKNUMBER_FROM_FILENAME
398         if (0==s.track) {
399             int space=trackFilename.indexOf(' ');
400             if (space>0 && space<=3) {
401                 s.track=trackFilename.mid(0, space).toInt();
402             }
403         }
404         #endif
405         if (!artistItem || (supprtAlbumArtistTag ? s.albumArtistOrComposer()!=artistItem->data() : s.artist!=artistItem->data())) {
406             artistItem = library->artist(s);
407         }
408         if (!albumItem || albumItem->parentItem()!=artistItem || s.albumName()!=albumItem->data()) {
409             albumItem = artistItem->album(s);
410         }
411         MusicLibraryItemSong *songItem = new MusicLibraryItemSong(s, albumItem);
412         albumItem->append(songItem);
413 
414         #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
415         // Store AlbumName->Artists/Songs mapping
416         MtpAlbum &al=albumMap[s.album];
417         al.artists.insert(s.artist);
418         al.songs.append(songItem);
419         al.folder=track->parent_id;
420         #endif
421         LIBMTP_destroy_track_t(track);
422     }
423 
424     while (tracks) {
425         LIBMTP_track_t *track=tracks;
426         tracks=tracks->next;
427         LIBMTP_destroy_track_t(track);
428     }
429 
430     #ifdef TIME_MTP_OPERATIONS
431     qWarning() << "Tracks parse:" << timer.elapsed();
432     timer.restart();
433     #endif
434     #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
435     // Use Album map to determine 'AlbumArtist' tag for various artist albums, and
436     // albums that have tracks where artist is set to '${artist} and somebodyelse'
437     QMap<QString, MtpAlbum>::ConstIterator it=albumMap.constBegin();
438     QMap<QString, MtpAlbum>::ConstIterator end=albumMap.constEnd();
439     for (; it!=end; ++it) {
440         if (abortRequested) {
441             return;
442         }
443         if ((*it).artists.count()>1) {
444             QSet<quint16> tracks;
445             QString shortestArtist;
446             bool duplicateTrackNumbers=false;
447             for (const MusicLibraryItemSong *s: (*it).songs) {
448                 if (tracks.contains(s->track())) {
449                     duplicateTrackNumbers=true;
450                     break;
451                 } else {
452                     if (shortestArtist.isEmpty() || s->song().artist.length()<shortestArtist.length()) {
453                         shortestArtist=s->song().artist;
454                     }
455                     tracks.insert(s->track());
456                 }
457             }
458             // If an album has mutiple tracks with the same track number, then we probably have X albums
459             // by X artists - in which case we proceeed no further.
460             if (!duplicateTrackNumbers) {
461                 MtpFolder &f=folders[(*it).folder];
462                 // Now, check to see if all artists contain 'shortestArtist'. If so then use 'shortestArtist' as the album
463                 // artist. This is probably due to songs which have artist set to '${artist} and somebodyelse'
464                 QString albumArtist=shortestArtist;
465                 for (const QString &artist: (*it).artists) {
466                     if (!artist.contains(shortestArtist)) {
467                         // Found an artist that did not contain 'shortestArtist', so use 'Various Artists' for album artist
468                         albumArtist=!f.artist.isEmpty() && f.album==it.key() ? f.artist : Song::variousArtists();
469                         break;
470                     }
471                 }
472 
473                 // Now move songs to correct artist/album...
474                 for (MusicLibraryItemSong *s: (*it).songs) {
475                     if (s->song().albumartist==albumArtist) {
476                         continue;
477                     }
478                     Song song=s->song();
479                     song.albumartist=albumArtist;
480                     artistItem=library->artist(song);
481                     albumItem=artistItem->album(song);
482                     MusicLibraryItemSong *songItem = new MusicLibraryItemSong(song, albumItem);
483                     albumItem->append(songItem);
484                     MusicLibraryItemAlbum *prevAlbum=(MusicLibraryItemAlbum *)s->parentItem();
485                     prevAlbum->remove(s);
486                     if (0==prevAlbum->childCount()) {
487                         // Album no longer has any songs, so remove!
488                         MusicLibraryItemArtist *prevArtist=(MusicLibraryItemArtist *)prevAlbum->parentItem();
489                         prevArtist->remove(prevAlbum);
490                         if (0==prevArtist->childCount()) {
491                             // Artist no longer has any albums, so remove!
492                             library->remove(prevArtist);
493                         }
494                     }
495                 }
496             }
497         }
498     }
499     #ifdef TIME_MTP_OPERATIONS
500     qWarning() << "AlbumArtist detection:" << timer.elapsed();
501     timer.restart();
502     #endif
503     #endif
504     if (abortRequested) {
505         return;
506     }
507     #ifdef TIME_MTP_OPERATIONS
508     qWarning() << "Grouping:" << timer.elapsed();
509     qWarning() << "TOTAL update:" <<totalTimer.elapsed();
510     #endif
511     emit libraryUpdated();
512 }
513 
setMusicFolder(Storage & store)514 void MtpConnection::setMusicFolder(Storage &store)
515 {
516     if (0==store.musicFolderId) {
517         store.musicFolderId=getFolder(constMusicFolder+QLatin1Char('/'), store.id);
518         if (0==store.musicFolderId) {
519             store.musicFolderId=createFolder(constMusicFolder, constMusicFolder+QLatin1Char('/'), 0, store.id);
520         }
521         if (0!=store.musicFolderId) {
522             store.musicPath=getPath(store.musicFolderId);
523         }
524     }
525 }
526 
updateFilesAndFolders()527 void MtpConnection::updateFilesAndFolders()
528 {
529     folderMap.clear();
530     for (const Storage &st: storage) {
531         if (abortRequested) {
532             return;
533         }
534         listFolder(st.id, constRootFolder, 0);
535     }
536 }
537 
listFolder(uint32_t storage,uint32_t parentDir,Folder * f)538 void MtpConnection::listFolder(uint32_t storage, uint32_t parentDir, Folder *f)
539 {
540     LIBMTP_file_t *files=LIBMTP_Get_Files_And_Folders(device, storage, parentDir);
541 
542     while (files && !abortRequested) {
543         LIBMTP_file_t *file=files;
544         files = files->next;
545 
546         QString name=QString::fromUtf8(file->filename);
547         if (LIBMTP_FILETYPE_FOLDER==file->filetype) {
548             bool isMusic=constRootFolder!=parentDir || 0==name.compare(constMusicFolder, Qt::CaseInsensitive);
549             if (isMusic) {
550                 QMap<uint32_t, Folder>::ConstIterator it=folderMap.find(file->parent_id);
551                 QString path;
552                 if (it!=folderMap.constEnd()) {
553                     path=it.value().path+name+QLatin1Char('/');
554                 } else {
555                     path=name+QLatin1Char('/');
556                 }
557 
558                 QMap<uint32_t, Folder>::iterator entry=folderMap.insert(file->item_id, Folder(path, file->item_id, file->parent_id, file->storage_id));
559                 if (folderMap.contains(file->parent_id)) {
560                     folderMap[file->parent_id].folders.insert(file->item_id);
561                 }
562                 listFolder(storage, file->item_id, &(entry.value()));
563             }
564         } else if (f && constRootFolder!=parentDir) {
565             if (name.endsWith(".jpg", Qt::CaseInsensitive) || name.endsWith(".png", Qt::CaseInsensitive) || QLatin1String("albumart.pamp")==name) {
566                 f->covers.insert(file->item_id, File(name, file->filesize, file->item_id));
567             } else {
568                 f->files.insert(file->item_id, File(name, file->filesize, file->item_id));
569             }
570         }
571         LIBMTP_destroy_file_t(file);
572     }
573 }
574 
updateStorage()575 void MtpConnection::updateStorage()
576 {
577     uint64_t sizeCalc=0;
578     uint64_t usedCalc=0;
579     if (device && LIBMTP_ERROR_NONE==LIBMTP_Get_Storage(device, LIBMTP_STORAGE_SORTBY_MAXSPACE)) {
580         LIBMTP_devicestorage_struct *s=device->storage;
581         while (s) {
582             QString volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier);
583             if (volumeIdentifier.isEmpty()) {
584                 volumeIdentifier=QString::fromUtf8(s->StorageDescription);
585             }
586             if (volumeIdentifier.isEmpty()) {
587                 volumeIdentifier="ID:"+QString::number(s->id);
588             }
589             QList<Storage>::Iterator it=storage.begin();
590             QList<Storage>::Iterator end=storage.end();
591             for ( ;it!=end; ++it) {
592                 if ((*it).volumeIdentifier==volumeIdentifier) {
593                     // We know about this storage ID, so update its size...
594                     (*it).size=s->MaxCapacity;
595                     (*it).used=s->MaxCapacity-s->FreeSpaceInBytes;
596                     break;
597                 }
598             }
599 
600             if (it==end) {
601                 // Unknown storage ID, so add to list!
602                 Storage store;
603                 store.id=s->id;
604                 store.description=QString::fromUtf8(s->StorageDescription);
605                 store.volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier);
606                 store.size=s->MaxCapacity;
607                 store.used=s->MaxCapacity-s->FreeSpaceInBytes;
608                 storage.append(store);
609             }
610             sizeCalc+=s->MaxCapacity;
611             usedCalc+=s->MaxCapacity-s->FreeSpaceInBytes;
612             s=s->next;
613         }
614     }
615     size=sizeCalc;
616     used=usedCalc;
617 }
618 
getStorageList() const619 QList<DeviceStorage> MtpConnection::getStorageList() const
620 {
621     QList<DeviceStorage> s;
622     QList<Storage>::ConstIterator it=storage.constBegin();
623     QList<Storage>::ConstIterator end=storage.constEnd();
624     for ( ;it!=end; ++it) {
625         DeviceStorage store;
626         store.size=(*it).size;
627         store.used=(*it).used;
628         store.description=(*it).description;
629         store.volumeIdentifier=(*it).volumeIdentifier;
630         s.append(store);
631     }
632 
633     return s;
634 }
635 
getStorage(const QString & volumeIdentifier)636 MtpConnection::Storage & MtpConnection::getStorage(const QString &volumeIdentifier)
637 {
638     QList<Storage>::Iterator first=storage.begin();
639     if (!volumeIdentifier.isEmpty()) {
640         QList<Storage>::Iterator it=first;
641         QList<Storage>::Iterator end=storage.end();
642         for ( ;it!=end; ++it) {
643             if ((*it).volumeIdentifier==volumeIdentifier) {
644                 return *it;
645             }
646         }
647     }
648 
649     return *first;
650 }
651 
getStorage(uint32_t id)652 MtpConnection::Storage & MtpConnection::getStorage(uint32_t id)
653 {
654     QList<Storage>::Iterator first=storage.begin();
655     QList<Storage>::Iterator it=first;
656     QList<Storage>::Iterator end=storage.end();
657     for ( ;it!=end; ++it) {
658         if ((*it).id==id) {
659             return *it;
660         }
661     }
662 
663     return *first;
664 }
665 
createFolder(const QString & name,const QString & fullPath,uint32_t parentId,uint32_t storageId)666 uint32_t MtpConnection::createFolder(const QString &name, const QString &fullPath, uint32_t parentId, uint32_t storageId)
667 {
668     char *nameCopy = qstrdup(name.toUtf8().constData());
669     uint32_t newFolderId=LIBMTP_Create_Folder(device, nameCopy, parentId, storageId);
670     delete nameCopy;
671     if (0==newFolderId)  {
672         return 0;
673     }
674 
675     folderMap.insert(newFolderId, Folder(fullPath, newFolderId, parentId, storageId));
676     return newFolderId;
677 }
678 
getFolder(const QString & path,uint32_t storageId)679 uint32_t MtpConnection::getFolder(const QString &path, uint32_t storageId)
680 {
681     QMap<uint32_t, Folder>::ConstIterator it=folderMap.constBegin();
682     QMap<uint32_t, Folder>::ConstIterator end=folderMap.constEnd();
683 
684     for (; it!=end; ++it) {
685         if (storageId==(*it).storageId && 0==(*it).path.compare(path, Qt::CaseInsensitive)) {
686             return (*it).id;
687         }
688     }
689     return 0;
690 }
691 
getPath(uint32_t folderId)692 QString MtpConnection::getPath(uint32_t folderId)
693 {
694     return folderMap.contains(folderId) ? folderMap[folderId].path : QString();
695 }
696 
checkFolderStructure(const QStringList & dirs,Storage & store)697 uint32_t MtpConnection::checkFolderStructure(const QStringList &dirs, Storage &store)
698 {
699     setMusicFolder(store);
700     QString path;
701     uint32_t parentId=store.musicFolderId;
702 
703     for (const QString &d: dirs) {
704         path+=d+QChar('/');
705         uint32_t folderId=getFolder(path, store.id);
706         if (0==folderId) {
707             folderId=createFolder(d, path, parentId, store.id);
708             if (0==folderId) {
709                 return parentId;
710             } else {
711                 parentId=folderId;
712             }
713         } else {
714             parentId=folderId;
715         }
716     }
717     return parentId;
718 }
719 
createString(const QString & str)720 static char * createString(const QString &str)
721 {
722     return str.isEmpty() ? qstrdup("") : qstrdup(str.toUtf8());
723 }
724 
mtpFileType(const QString & f)725 static LIBMTP_filetype_t mtpFileType(const QString &f)
726 {
727     if (f.endsWith(".mp3", Qt::CaseInsensitive)) {
728         return LIBMTP_FILETYPE_MP3;
729     }
730     if (f.endsWith(".ogg", Qt::CaseInsensitive)) {
731         return LIBMTP_FILETYPE_OGG;
732     }
733     if (f.endsWith(".wma", Qt::CaseInsensitive)) {
734         return LIBMTP_FILETYPE_WMA;
735     }
736     if (f.endsWith(".m4a", Qt::CaseInsensitive)) {
737         return LIBMTP_FILETYPE_M4A; // LIBMTP_FILETYPE_MP4
738     }
739     if (f.endsWith(".aac", Qt::CaseInsensitive)) {
740         return LIBMTP_FILETYPE_AAC;
741     }
742     if (f.endsWith(".flac", Qt::CaseInsensitive)) {
743         return LIBMTP_FILETYPE_FLAC;
744     }
745     if (f.endsWith(".wav", Qt::CaseInsensitive)) {
746         return LIBMTP_FILETYPE_WAV;
747     }
748     if (f.endsWith(".jpg", Qt::CaseInsensitive)) {
749         return LIBMTP_FILETYPE_JPEG;
750     }
751     if (f.endsWith(".png", Qt::CaseInsensitive)) {
752         return LIBMTP_FILETYPE_PNG;
753     }
754     return LIBMTP_FILETYPE_UNDEF_AUDIO;
755 }
756 
saveImageToTemp(const QImage & img,const QString & name)757 static QTemporaryFile * saveImageToTemp(const QImage &img, const QString &name)
758 {
759     QTemporaryFile *temp=new QTemporaryFile();
760 
761     int index=name.lastIndexOf('.');
762     if (index>0) {
763         temp=new QTemporaryFile("cantata_XXXXXX"+name.mid(index));
764     } else {
765         temp=new QTemporaryFile("cantata_XXXXXX");
766     }
767     img.save(temp);
768     temp->close();
769     return temp;
770 }
771 
putSong(const Song & s,bool fixVa,const DeviceOptions & opts,bool overwrite,bool copyCover)772 void MtpConnection::putSong(const Song &s, bool fixVa, const DeviceOptions &opts, bool overwrite, bool copyCover)
773 {
774     int status=Device::Failed;
775     bool fixedVa=false;
776     bool embedCoverImage=Device::constEmbedCover==opts.coverName;
777     bool copiedCover=false;
778     LIBMTP_track_t *meta=0;
779     QString destName;
780     Storage store=getStorage(opts.volumeId);
781     uint32_t folderId=0;
782 
783     copyCover=copyCover && !embedCoverImage && Device::constNoCover!=opts.coverName;
784     if (device) {
785         meta=LIBMTP_new_track_t();
786         meta->item_id=0;
787         Song song=s;
788         QString fileName=song.file;
789         QTemporaryFile *temp=0;
790 
791         if (fixVa || embedCoverImage) {
792             // Need to 'workaround' broken various artists handling, and/or embedding cover, so write to a temporary file first...
793             temp=Device::copySongToTemp(song);
794             if (temp) {
795                 if (embedCoverImage) {
796                     Device::embedCover(song.file, song, opts.coverMaxSize);
797                 }
798                 if (fixVa && Device::fixVariousArtists(temp->fileName(), song, true)) {
799                     fixedVa=true;
800                 }
801                 song.file=temp->fileName();
802             }
803         }
804         struct stat statBuf;
805         if (0==stat(QFile::encodeName(song.file).constData(), &statBuf)) {
806             meta->filesize=statBuf.st_size;
807             meta->modificationdate=statBuf.st_mtime;
808         }
809 
810         // Check if storage has enough space, if not try to find one that does!
811         if (store.freeSpace()<meta->filesize) {
812             QList<Storage>::Iterator it=storage.begin();
813             QList<Storage>::Iterator end=storage.end();
814             for ( ;it!=end; ++it) {
815                 if ((*it).freeSpace()>=meta->filesize) {
816                     store=*it;
817                     break;
818                 }
819             }
820         }
821 
822         meta->parent_id=folderId=store.musicFolderId;
823         meta->storage_id=store.id;
824         destName=store.musicPath+opts.createFilename(song);
825         QStringList dirs=destName.split('/', QString::SkipEmptyParts);
826         if (dirs.count()>1) {
827             destName=dirs.takeLast();
828             meta->parent_id=folderId=checkFolderStructure(dirs, store);
829         }
830         Folder &folder=folderMap[folderId];
831         QMap<uint32_t, File>::ConstIterator it=folder.files.constBegin();
832         QMap<uint32_t, File>::ConstIterator end=folder.files.constEnd();
833         for (; it!=end; ++it) {
834             if ((*it).name==destName) {
835                 if (!overwrite || 0!=LIBMTP_Delete_Object(device, (*it).id)) {
836                     status=Device::SongExists;
837                 }
838                 break;
839             }
840         }
841 
842         if (status!=Device::SongExists) {
843             meta->title=createString(song.title);
844             meta->artist=createString(song.artist);
845             meta->composer=createString(song.composer());
846             meta->genre=createString(song.genres[0]);
847             meta->album=createString(song.album);
848             meta->date=createString(QStringLiteral("%1").arg(song.year, 4)+"0101T0000.0");
849             meta->filename=createString(destName);
850             meta->tracknumber=song.track;
851             meta->duration=song.time*1000;
852             meta->rating=0;
853             meta->usecount=0;
854             meta->filetype=mtpFileType(song.file);
855             meta->next=0;
856             switch (LIBMTP_Send_Track_From_File(device, fileName.toUtf8(), meta, &progressMonitor, this)) {
857             case LIBMTP_ERROR_NONE:         status=Device::Ok;      break;
858             case LIBMTP_ERROR_STORAGE_FULL: status=Device::NoSpace; break;
859             default:                        status=Device::Failed;  break;
860             }
861         }
862         if (temp) {
863             // Delete the temp file...
864             temp->remove();
865             delete temp;
866         }
867     }
868 
869     if (Device::Ok==status) {
870         Folder &folder=folderMap[folderId];
871         // LibMTP seems to reset parent_id to 0 - but we NEED the correct value for encodePath
872         meta->parent_id=folderId;
873         if (copyCover) {
874             QMap<uint32_t, File>::ConstIterator it=folder.covers.constBegin();
875             QMap<uint32_t, File>::ConstIterator end=folder.covers.constEnd();
876 
877             for (; it!=end; ++it) {
878                 if (it.value().name==opts.coverName) {
879                     copiedCover=true;
880                     copyCover=false;
881                     break;
882                 }
883             }
884         }
885         // Send cover, as a plain file...
886         if (copyCover) {
887             QString srcFile;
888 
889             // If we have transcoded a song, the song.file will point to /tmp!!!
890             Song orig = s;
891             if (orig.hasExtraField(constOrigFileName)) {
892                 orig.file=orig.extraField(constOrigFileName);
893             }
894             Covers::Image coverImage=Covers::self()->getImage(orig);
895             QTemporaryFile *temp=0;
896             if (!coverImage.img.isNull() && !coverImage.fileName.isEmpty()) {
897                 if (opts.coverMaxSize && (coverImage.img.width()>(int)opts.coverMaxSize || coverImage.img.height()>(int)opts.coverMaxSize)) {
898                     temp=saveImageToTemp(coverImage.img.scaled(QSize(opts.coverMaxSize, opts.coverMaxSize), Qt::KeepAspectRatio, Qt::SmoothTransformation), opts.coverName);
899                 } else if (!coverImage.fileName.endsWith(".jpg", Qt::CaseInsensitive) || !QFile::exists(coverImage.fileName)) {
900                     temp=saveImageToTemp(coverImage.img, opts.coverName);
901                 } else {
902                     srcFile=coverImage.fileName;
903                 }
904                 if (temp) {
905                     srcFile=temp->fileName();
906                 }
907             }
908 
909             if (!srcFile.isEmpty()) {
910                 LIBMTP_file_t *fileMeta=LIBMTP_new_file_t();
911                 fileMeta->item_id=0;
912                 fileMeta->parent_id=folderId;
913                 fileMeta->storage_id=store.id;
914                 fileMeta->filename=createString(opts.coverName);
915                 struct stat statBuf;
916                 if (0==stat(QFile::encodeName(srcFile).constData(), &statBuf)) {
917                     fileMeta->filesize=statBuf.st_size;
918                     fileMeta->modificationdate=statBuf.st_mtime;
919                 }
920                 meta->filetype=mtpFileType(opts.coverName);
921 
922                 if (0==LIBMTP_Send_File_From_File(device, srcFile.toUtf8(), fileMeta, 0, 0)) {
923                     folder.covers.insert(fileMeta->item_id, File(opts.coverName, statBuf.st_size, fileMeta->item_id));
924                     copiedCover=true;
925                 }
926                 LIBMTP_destroy_file_t(fileMeta);
927             }
928             if (temp) {
929                 temp->remove();
930                 delete temp;
931             }
932         }
933         folder.files.insert(meta->item_id, File(destName, meta->filesize, meta->item_id));
934         updateStorage();
935     }
936     emit putSongStatus(status,
937                        meta ? encodePath(meta, destName, storage.count()>1 ? store.description : QString()) : QString(),
938                        fixedVa, copiedCover);
939     if (meta) {
940         LIBMTP_destroy_track_t(meta);
941     }
942 }
943 
getCoverDetils(const Song & s)944 MtpConnection::File MtpConnection::getCoverDetils(const Song &s)
945 {
946     File cover;
947     Path path=decodePath(s.file);
948     if (path.ok() && folderMap.contains(path.parent) && !folderMap[path.parent].covers.isEmpty()) {
949         QMap<uint32_t, File> &covers=folderMap[path.parent].covers;
950         QMap<uint32_t, File>::ConstIterator it=covers.constBegin();
951         QMap<uint32_t, File>::ConstIterator end=covers.constEnd();
952 
953         for (; it!=end; ++it) {
954             if (it.value().size>cover.size) {
955                 cover=it.value();
956             }
957         }
958     }
959 
960     return cover;
961 }
962 
getSong(const Song & song,const QString & dest,bool fixVa,bool copyCover)963 void MtpConnection::getSong(const Song &song, const QString &dest, bool fixVa, bool copyCover)
964 {
965     bool copiedSong=device && 0==LIBMTP_Get_File_To_File(device, song.id, dest.toUtf8(), &progressMonitor, this);
966     bool copiedCover=false;
967 
968     if (copiedSong && !abortRequested && copyCover) {
969         QString destDir=Utils::getDir(dest);
970 
971         if (QDir(destDir).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable).isEmpty()) {
972             File cover=getCoverDetils(song);
973 
974             if (0!=cover.id) {
975                 QString fileName=QString(destDir+Covers::albumFileName(song)+(cover.name.endsWith(".jpg", Qt::CaseInsensitive) ? ".jpg" : ".png"));
976                 QByteArray fileNameUtf8=fileName.toUtf8();
977                 copiedCover=0==LIBMTP_Get_File_To_File(device, cover.id, fileNameUtf8.constData(), 0, 0);
978                 if (copiedCover) {
979                     Utils::setFilePerms(fileName);
980                 }
981             }
982         }
983     }
984 
985     if (copiedSong && fixVa && !abortRequested) {
986         Song s(song);
987         Device::fixVariousArtists(dest, s, false);
988     }
989     emit getSongStatus(copiedSong, copiedCover);
990 }
991 
delSong(const Song & song)992 void MtpConnection::delSong(const Song &song)
993 {
994     Path path=decodePath(song.file);
995     bool deletedSong=device && path.ok() && 0==LIBMTP_Delete_Object(device, path.id);
996     if (deletedSong) {
997         folderMap[path.parent].files.remove(path.id);
998         #ifdef MTP_CLEAN_ALBUMS
999         // Remove track from album. Remove album (and cover) if no tracks.
1000         LIBMTP_album_t *album=getAlbum(song);
1001         if (album) {
1002             if (0==album->no_tracks || (1==album->no_tracks && album->tracks[0]==(uint32_t)song.id)) {
1003                 LIBMTP_Delete_Object(device, album->album_id);
1004                 updateAlbums();
1005             } else if (album->no_tracks>1) {
1006                 // Remove track from album...
1007                 uint32_t *tracks = (uint32_t *)malloc((album->no_tracks-1)*sizeof(uint32_t));
1008                 if (tracks) {
1009                     bool found=false;
1010                     for (uint32_t i=0, j=0; i<album->no_tracks && j<(album->no_tracks-1); ++i) {
1011                         if (album->tracks[i]!=(uint32_t)song.id) {
1012                             tracks[j++]=album->tracks[i];
1013                         } else {
1014                             found=true;
1015                         }
1016                     }
1017                     if (found) {
1018                         album->no_tracks--;
1019                         free(album->tracks);
1020                         album->tracks = tracks;
1021                         LIBMTP_Update_Album(device, album);
1022                     } else {
1023                         free(tracks);
1024                     }
1025                 }
1026             }
1027         }
1028         #endif
1029         updateStorage();
1030     }
1031     emit delSongStatus(deletedSong);
1032 }
1033 
removeFolder(uint32_t folderId)1034 bool MtpConnection::removeFolder(uint32_t folderId)
1035 {
1036     QMap<uint32_t, Folder>::iterator folder=folderMap.find(folderId);
1037     if (folderMap.end()!=folder && (*folder).folders.isEmpty() && (*folder).files.isEmpty()) {
1038         // Delete any cover files...
1039         QList<uint32_t> toRemove=(*folder).covers.keys();
1040         for (uint32_t cover: toRemove) {
1041             if (0==LIBMTP_Delete_Object(device, cover)) {
1042                 (*folder).covers.remove(cover);
1043             }
1044         }
1045 
1046         // Delete folder, if it is now empty...
1047         if ((*folder).covers.isEmpty() && 0==LIBMTP_Delete_Object(device, folderId)) {
1048             if (folderMap.contains((*folder).parentId)) {
1049                 folderMap[(*folder).parentId].folders.remove(folderId);
1050             }
1051             folderMap.remove(folderId);
1052             return true;
1053         }
1054     }
1055     return false;
1056 }
1057 
cleanDirs(const QSet<QString> & dirs)1058 void MtpConnection::cleanDirs(const QSet<QString> &dirs)
1059 {
1060     for (const QString &d: dirs) {
1061         Path path=decodePath(d);
1062         Storage &store=getStorage(path.storage);
1063         if (0==store.musicFolderId) {
1064             continue;
1065         }
1066         uint32_t folderId=path.parent;
1067         while (0!=folderId && folderId!=store.musicFolderId) {
1068             QMap<uint32_t, Folder>::iterator it=folderMap.find(folderId);
1069             if (it!=folderMap.end()) {
1070                 if (removeFolder(folderId)) {
1071                     folderId=(*it).parentId;
1072                     folderMap.erase(it);
1073                 } else {
1074                     break;
1075                 }
1076             } else {
1077                 break;
1078             }
1079         }
1080     }
1081 
1082     updateStorage();
1083     emit cleanDirsStatus(true);
1084 }
1085 
getCover(const Song & song)1086 void MtpConnection::getCover(const Song &song)
1087 {
1088     File c=getCoverDetils(song);
1089 
1090     COVER_DBUG << c.name << c.id;
1091 
1092     if (0!=c.id) {
1093         QByteArray data;
1094         if (0==LIBMTP_Get_File_To_Handler(device, c.id, fileReceiver, &data, 0, 0)) {
1095             QImage img;
1096             if (img.loadFromData(data)) {
1097                 COVER_DBUG << "loaded cover";
1098                 emit cover(song, img);
1099             }
1100         }
1101     }
1102 }
1103 
stop()1104 void MtpConnection::stop()
1105 {
1106     if (thread) {
1107         disconnectFromDevice(false);
1108         thread->stop();
1109         thread=0;
1110     }
1111 }
1112 
1113 #ifdef MTP_CLEAN_ALBUMS
updateAlbums()1114 void MtpConnection::updateAlbums()
1115 {
1116     while (albums) {
1117         LIBMTP_album_t *album=albums;
1118         albums=albums->next;
1119         LIBMTP_destroy_album_t(album);
1120     }
1121     albums=LIBMTP_Get_Album_List(device);
1122 }
1123 
getAlbum(const Song & song)1124 LIBMTP_album_t * MtpConnection::getAlbum(const Song &song)
1125 {
1126     LIBMTP_album_t *al=albums;
1127     LIBMTP_album_t *possible=0;
1128 
1129     while (al) {
1130         if (QString::fromUtf8(al->name)==song.album) {
1131             // For some reason, MTP sometimes leaves blank albums behind.
1132             // So, when looking for an album if we find one with the same name, but no tracks - then save this as a possibility...
1133             if (0==al->no_tracks) {
1134                 QString aa(QString::fromUtf8(al->artist));
1135                 if (aa.isEmpty() || aa==song.albumArtist()) {
1136                     possible=al;
1137                 }
1138             } else {
1139                 for (uint32_t i=0; i<al->no_tracks; ++i) {
1140                     if (al->tracks[i]==(uint32_t)song.id) {
1141                         return al;
1142                     }
1143                 }
1144             }
1145         }
1146         al=al->next;
1147     }
1148 
1149     return possible;
1150 }
1151 #endif
1152 
destroyData()1153 void MtpConnection::destroyData()
1154 {
1155     folderMap.clear();
1156     if (library) {
1157         delete library;
1158         library=0;
1159     }
1160 
1161     #ifdef MTP_CLEAN_ALBUMS
1162     while (albums) {
1163         LIBMTP_album_t *album=albums;
1164         albums=albums->next;
1165         LIBMTP_destroy_album_t(album);
1166     }
1167     #endif
1168 }
1169 
cfgKey(Solid::Device & dev,const QString & serial)1170 QString cfgKey(Solid::Device &dev, const QString &serial)
1171 {
1172     QString key=QLatin1String("MTP-")+dev.vendor()+QChar('-')+dev.product()+QChar('-')+serial;
1173     key.replace('/', '_');
1174     return key;
1175 }
1176 
MtpDevice(MusicLibraryModel * m,Solid::Device & dev,unsigned int busNum,unsigned int devNum)1177 MtpDevice::MtpDevice(MusicLibraryModel *m, Solid::Device &dev, unsigned int busNum, unsigned int devNum)
1178     : Device(m, dev,
1179              #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
1180              true
1181              #else
1182              false
1183              #endif
1184              )
1185     , pmp(dev.as<Solid::PortableMediaPlayer>())
1186     , tempFile(0)
1187     , mtpUpdating(false)
1188 {
1189     static bool registeredTypes=false;
1190     if (!registeredTypes) {
1191         qRegisterMetaType<QSet<QString> >("QSet<QString>");
1192         qRegisterMetaType<DeviceOptions >("DeviceOptions");
1193         registeredTypes=true;
1194     }
1195 
1196     connection=new MtpConnection(data(), busNum, devNum, supportsAlbumArtistTag());
1197     connect(this, SIGNAL(updateLibrary(const DeviceOptions &)), connection, SLOT(updateLibrary(const DeviceOptions &)));
1198     connect(connection, SIGNAL(libraryUpdated()), this, SLOT(libraryUpdated()));
1199     connect(connection, SIGNAL(progress(int)), this, SLOT(emitProgress(int)));
1200     connect(this, SIGNAL(putSong(const Song &, bool, const DeviceOptions &, bool, bool)), connection, SLOT(putSong(const Song &, bool, const DeviceOptions &, bool, bool)));
1201     connect(connection, SIGNAL(putSongStatus(int, const QString &, bool, bool)), this, SLOT(putSongStatus(int, const QString &, bool, bool)));
1202     connect(this, SIGNAL(getSong(const Song &, const QString &, bool, bool)), connection, SLOT(getSong(const Song &, const QString &, bool, bool)));
1203     connect(connection, SIGNAL(getSongStatus(bool, bool)), this, SLOT(getSongStatus(bool, bool)));
1204     connect(this, SIGNAL(delSong(const Song &)), connection, SLOT(delSong(const Song &)));
1205     connect(connection, SIGNAL(delSongStatus(bool)), this, SLOT(delSongStatus(bool)));
1206     connect(this, SIGNAL(cleanMusicDirs(const QSet<QString> &)), connection, SLOT(cleanDirs(const QSet<QString> &)));
1207     connect(this, SIGNAL(getCover(const Song &)), connection, SLOT(getCover(const Song &)));
1208     connect(connection, SIGNAL(cleanDirsStatus(bool)), this, SLOT(cleanDirsStatus(bool)));
1209     connect(connection, SIGNAL(statusMessage(const QString &)), this, SLOT(setStatusMessage(const QString &)));
1210     connect(connection, SIGNAL(deviceDetails(const QString &)), this, SLOT(deviceDetails(const QString &)));
1211     connect(connection, SIGNAL(updatePercentage(int)), this, SLOT(updatePercentage(int)));
1212     connect(connection, SIGNAL(cover(const Song &, const QImage &)), this, SIGNAL(cover(const Song &, const QImage &)));
1213     opts.fixVariousArtists=false;
1214     opts.coverName=constMtpDefaultCover;
1215     QTimer::singleShot(0, this, SLOT(rescan(bool)));
1216     defaultName=data();
1217     if (!opts.name.isEmpty()) {
1218         DBUG << "setName" << opts.name;
1219         setData(opts.name);
1220     }
1221     icn=MonoIcon::icon(FontAwesome::mobilephone, Utils::monoIconColor());
1222 }
1223 
~MtpDevice()1224 MtpDevice::~MtpDevice()
1225 {
1226     stop();
1227 }
1228 
deviceDetails(const QString & s)1229 void MtpDevice::deviceDetails(const QString &s)
1230 {
1231     DBUG << s;
1232     if ((s!=serial || serial.isEmpty()) && solidDev.isValid()) {
1233         serial=s;
1234         QString configKey=cfgKey(solidDev, serial);
1235         opts.load(configKey);
1236         configured=Configuration().hasGroup(configKey);
1237         if (!opts.name.isEmpty() && opts.name!=defaultName) {
1238             DBUG << "setName" << opts.name;
1239             setData(opts.name);
1240             emit renamed();
1241         }
1242     }
1243 }
1244 
isConnected() const1245 bool MtpDevice::isConnected() const
1246 {
1247     return solidDev.isValid() && pmp && pmp->isValid() && connection->isConnected();
1248 }
1249 
stop()1250 void MtpDevice::stop()
1251 {
1252     abortJob();
1253     deleteTemp();
1254     if (connection) {
1255         disconnect(connection, SIGNAL(libraryUpdated()), this, SLOT(libraryUpdated()));
1256         disconnect(connection, SIGNAL(progress(int)), this, SLOT(emitProgress(int)));
1257         disconnect(connection, SIGNAL(putSongStatus(int, const QString &, bool, bool)), this, SLOT(putSongStatus(int, const QString &, bool, bool)));
1258         disconnect(connection, SIGNAL(getSongStatus(bool, bool)), this, SLOT(getSongStatus(bool, bool)));
1259         disconnect(connection, SIGNAL(delSongStatus(bool)), this, SLOT(delSongStatus(bool)));
1260         disconnect(connection, SIGNAL(cleanDirsStatus(bool)), this, SLOT(cleanDirsStatus(bool)));
1261         disconnect(connection, SIGNAL(statusMessage(const QString &)), this, SLOT(setStatusMessage(const QString &)));
1262         disconnect(connection, SIGNAL(deviceDetails(const QString &)), this, SLOT(deviceDetails(const QString &)));
1263         disconnect(connection, SIGNAL(updatePercentage(int)), this, SLOT(updatePercentage(int)));
1264         disconnect(connection, SIGNAL(cover(const Song &, const QImage &)), this, SIGNAL(cover(const Song &, const QImage &)));
1265         metaObject()->invokeMethod(connection, "stop", Qt::QueuedConnection);
1266         connection->deleteLater();
1267         connection=0;
1268     }
1269 }
1270 
configure(QWidget * parent)1271 void MtpDevice::configure(QWidget *parent)
1272 {
1273     if (!isIdle()) {
1274         return;
1275     }
1276 
1277     DevicePropertiesDialog *dlg=new DevicePropertiesDialog(parent);
1278     connect(dlg, SIGNAL(updatedSettings(const QString &, const DeviceOptions &)), SLOT(saveProperties(const QString &, const DeviceOptions &)));
1279     if (!configured) {
1280         connect(dlg, SIGNAL(cancelled()), SLOT(saveProperties()));
1281     }
1282     DeviceOptions o=opts;
1283     if (o.name.isEmpty()) {
1284         o.name=data();
1285     }
1286     dlg->show(QString(), o, connection->getStorageList(), DevicePropertiesWidget::Prop_Name|DevicePropertiesWidget::Prop_CoversAll|DevicePropertiesWidget::Prop_Va|DevicePropertiesWidget::Prop_Transcoder);
1287 }
1288 
rescan(bool full)1289 void MtpDevice::rescan(bool full)
1290 {
1291     Q_UNUSED(full)
1292     if (mtpUpdating || !solidDev.isValid()) {
1293         return;
1294     }
1295     if (childCount()) {
1296         update=new MusicLibraryItemRoot();
1297         applyUpdate();
1298     }
1299     mtpUpdating=true;
1300     emit updating(solidDev.udi(), true);
1301     emit updateLibrary(opts);
1302 }
1303 
addSong(const Song & s,bool overwrite,bool copyCover)1304 void MtpDevice::addSong(const Song &s, bool overwrite, bool copyCover)
1305 {
1306     requestAbort(false);
1307     if (!isConnected()) {
1308         emit actionStatus(NotConnected);
1309         return;
1310     }
1311 
1312     needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
1313 
1314     if (!overwrite) {
1315         Song check=s;
1316 
1317         if (needToFixVa) {
1318             Device::fixVariousArtists(QString(), check, true);
1319         }
1320         if (songExists(check)) {
1321             emit actionStatus(SongExists);
1322             return;
1323         }
1324     }
1325 
1326     if (!QFile::exists(s.file)) {
1327         emit actionStatus(SourceFileDoesNotExist);
1328         return;
1329     }
1330     currentSong=s;
1331 
1332     Encoders::Encoder encoder;
1333 
1334     if (!opts.transcoderCodec.isEmpty()) {
1335         encoder=Encoders::getEncoder(opts.transcoderCodec);
1336         if (encoder.codec.isEmpty()) {
1337             emit actionStatus(CodecNotAvailable);
1338             return;
1339         }
1340     }
1341 
1342     transcoding = !opts.transcoderCodec.isEmpty() &&
1343             (DeviceOptions::TW_IfDifferent!=opts.transcoderWhen || encoder.isDifferent(s.file)) &&
1344             (DeviceOptions::TW_IfLossess!=opts.transcoderWhen || Device::isLossless(s.file));
1345 
1346     if (transcoding) {
1347         deleteTemp();
1348         tempFile=new QTemporaryFile("cantata_XXXXXX."+encoder.extension);
1349         tempFile->setAutoRemove(false);
1350 
1351         if (!tempFile->open()) {
1352             deleteTemp();
1353             emit actionStatus(FailedToCreateTempFile);
1354             return;
1355         }
1356         QString destFile=tempFile->fileName();
1357         tempFile->close();
1358         if (QFile::exists(destFile)) {
1359             QFile::remove(destFile);
1360         }
1361         transcoding=true;
1362         TranscodingJob *job=new TranscodingJob(encoder, opts.transcoderValue, s.file, destFile);
1363         job->setProperty("overwrite", overwrite);
1364         job->setProperty("copyCover", copyCover);
1365         connect(job, SIGNAL(result(int)), SLOT(transcodeSongResult(int)));
1366         connect(job, SIGNAL(percent(int)), SLOT(transcodePercent(int)));
1367         job->start();
1368         currentSong.setExtraField(constOrigFileName, currentSong.file);
1369         currentSong.file=destFile;
1370     } else {
1371         emit putSong(currentSong, needToFixVa, opts, overwrite, copyCover);
1372     }
1373 }
1374 
copySongTo(const Song & s,const QString & musicPath,bool overwrite,bool copyCover)1375 void MtpDevice::copySongTo(const Song &s, const QString &musicPath, bool overwrite, bool copyCover)
1376 {
1377     requestAbort(false);
1378     transcoding=false;
1379     if (!isConnected()) {
1380         emit actionStatus(NotConnected);
1381         return;
1382     }
1383 
1384     needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
1385 
1386     if (!overwrite) {
1387         Song check=s;
1388 
1389         if (needToFixVa) {
1390             Device::fixVariousArtists(QString(), check, false);
1391         }
1392         if (MpdLibraryModel::self()->songExists(check)) {
1393             emit actionStatus(SongExists);
1394             return;
1395         }
1396     }
1397 
1398     if (!songExists(s)) {
1399         emit actionStatus(SongDoesNotExist);
1400         return;
1401     }
1402 
1403     QString baseDir=MPDConnection::self()->getDetails().dir;
1404     currentDestFile=baseDir+musicPath;
1405     QDir dir(Utils::getDir(currentDestFile));
1406     if (!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), baseDir)) {
1407         emit actionStatus(DirCreationFaild);
1408         return;
1409     }
1410 
1411     currentSong=s;
1412     emit getSong(s, currentDestFile, needToFixVa, copyCover);
1413 }
1414 
removeSong(const Song & s)1415 void MtpDevice::removeSong(const Song &s)
1416 {
1417     requestAbort(false);
1418     if (!isConnected()) {
1419         emit actionStatus(NotConnected);
1420         return;
1421     }
1422 
1423     if (!songExists(s)) {
1424         emit actionStatus(SongDoesNotExist);
1425         return;
1426     }
1427 
1428     currentSong=s;
1429     emit delSong(s);
1430 }
1431 
cleanDirs(const QSet<QString> & dirs)1432 void MtpDevice::cleanDirs(const QSet<QString> &dirs)
1433 {
1434     requestAbort(false);
1435     if (!isConnected()) {
1436         emit actionStatus(NotConnected);
1437         return;
1438     }
1439     emit cleanMusicDirs(dirs);
1440 }
1441 
requestCover(const Song & song)1442 Covers::Image MtpDevice::requestCover(const Song &song)
1443 {
1444     COVER_DBUG << song.file;
1445     requestAbort(false);
1446     if (isConnected()) {
1447         COVER_DBUG << "Get cover from connection";
1448         emit getCover(song);
1449     }
1450     return Covers::Image();
1451 }
1452 
putSongStatus(int status,const QString & file,bool fixedVa,bool copiedCover)1453 void MtpDevice::putSongStatus(int status, const QString &file, bool fixedVa, bool copiedCover)
1454 {
1455     deleteTemp();
1456     if (jobAbortRequested) {
1457         return;
1458     }
1459     if (Ok!=status) {
1460         emit actionStatus(status);
1461     } else {
1462         currentSong.file=file;
1463         if (needToFixVa && fixedVa) {
1464             currentSong.fixVariousArtists();
1465         }
1466         #ifndef MTP_FAKE_ALBUMARTIST_SUPPORT
1467         else if (!opts.fixVariousArtists) { // TODO: ALBUMARTIST: Remove when libMTP supports album artist!
1468             currentSong.albumartist=currentSong.artist;
1469         }
1470         #endif
1471         addSongToList(currentSong);
1472         emit actionStatus(Ok, copiedCover);
1473     }
1474 }
1475 
transcodeSongResult(int status)1476 void MtpDevice::transcodeSongResult(int status)
1477 {
1478     TranscodingJob *job=qobject_cast<TranscodingJob *>(sender());
1479     if (!job) {
1480         return;
1481     }
1482     FileJob::finished(job);
1483     if (jobAbortRequested) {
1484         deleteTemp();
1485         return;
1486     }
1487     if (Ok!=status) {
1488         emit actionStatus(status);
1489     } else {
1490         emit putSong(currentSong, needToFixVa, opts, job->property("overwrite").toBool(), job->property("copyCover").toBool());
1491     }
1492 }
1493 
transcodePercent(int percent)1494 void MtpDevice::transcodePercent(int percent)
1495 {
1496     if (jobAbortRequested) {
1497         FileJob *job=qobject_cast<FileJob *>(sender());
1498         if (job) {
1499             job->stop();
1500         }
1501         return;
1502     }
1503     emit progress(percent/2);
1504 }
1505 
emitProgress(int percent)1506 void MtpDevice::emitProgress(int percent)
1507 {
1508     if (jobAbortRequested) {
1509         return;
1510     }
1511     emit progress(transcoding ? (50+(percent/2)) : percent);
1512 }
1513 
getSongStatus(bool ok,bool copiedCover)1514 void MtpDevice::getSongStatus(bool ok, bool copiedCover)
1515 {
1516     if (jobAbortRequested) {
1517         return;
1518     }
1519     if (!ok) {
1520         emit actionStatus(Failed);
1521     } else {
1522         currentSong.file=currentDestFile.mid(MPDConnection::self()->getDetails().dir.length());
1523         QString origPath;
1524         if (MPDConnection::self()->isMopidy()) {
1525             origPath=currentSong.file;
1526             currentSong.file=Song::encodePath(currentSong.file);
1527         }
1528         if (needToFixVa) {
1529             currentSong.revertVariousArtists();
1530         }
1531         Utils::setFilePerms(currentDestFile);
1532 //        MusicLibraryModel::self()->addSongToList(currentSong);
1533 //        DirViewModel::self()->addFileToList(origPath.isEmpty() ? currentSong.file : origPath,
1534 //                                            origPath.isEmpty() ? QString() : currentSong.file);
1535         emit actionStatus(Ok, copiedCover);
1536     }
1537 }
1538 
delSongStatus(bool ok)1539 void MtpDevice::delSongStatus(bool ok)
1540 {
1541     if (jobAbortRequested) {
1542         return;
1543     }
1544     if (!ok) {
1545         emit actionStatus(Failed);
1546     } else {
1547         removeSongFromList(currentSong);
1548         emit actionStatus(Ok);
1549     }
1550 }
1551 
cleanDirsStatus(bool ok)1552 void MtpDevice::cleanDirsStatus(bool ok)
1553 {
1554     emit actionStatus(ok ? Ok : Failed);
1555 }
1556 
usedCapacity()1557 double MtpDevice::usedCapacity()
1558 {
1559     if (!isConnected()) {
1560         return -1.0;
1561     }
1562 
1563     return connection->capacity()>0 ? (connection->usedSpace()*1.0)/(connection->capacity()*1.0) : -1.0;
1564 }
1565 
capacityString()1566 QString MtpDevice::capacityString()
1567 {
1568     if (!isConnected()) {
1569         return tr("Not Connected");
1570     }
1571 
1572     return tr("%1 free").arg(Utils::formatByteSize(connection->capacity()-connection->usedSpace()));
1573 }
1574 
freeSpace()1575 qint64 MtpDevice::freeSpace()
1576 {
1577     if (!isConnected()) {
1578         return 0;
1579     }
1580 
1581     return connection->capacity()-connection->usedSpace();
1582 }
1583 
libraryUpdated()1584 void MtpDevice::libraryUpdated()
1585 {
1586     if (update) {
1587         delete update;
1588     }
1589     update=connection->takeLibrary();
1590     setStatusMessage(QString());
1591     emit updating(id(), false);
1592     mtpUpdating=false;
1593 }
1594 
saveProperties(const QString &,const DeviceOptions & newOpts)1595 void MtpDevice::saveProperties(const QString &, const DeviceOptions &newOpts)
1596 {
1597     if (configured && opts==newOpts) {
1598         return;
1599     }
1600 
1601     QString newName=newOpts.name.isEmpty() ? defaultName : newOpts.name;
1602     bool diffName=opts.name!=newName;
1603     opts=newOpts;
1604     if (diffName) {
1605         DBUG << "setName" << newName;
1606         setData(newName);
1607     }
1608     saveProperties();
1609     if (diffName) {
1610         emit renamed();
1611     }
1612 }
1613 
saveProperties()1614 void MtpDevice::saveProperties()
1615 {
1616     if (solidDev.isValid()) {
1617         configured=true;
1618         opts.save(cfgKey(solidDev, serial), false, true, false); // Dont save fileame scheme - cant be changed!
1619         emit configurationChanged();
1620     }
1621 }
1622 
saveOptions()1623 void MtpDevice::saveOptions()
1624 {
1625     if (solidDev.isValid()) {
1626         opts.save(cfgKey(solidDev, serial), false, true, false); // Dont save fileame scheme - cant be changed!
1627     }
1628 }
1629 
deleteTemp()1630 void MtpDevice::deleteTemp()
1631 {
1632     if (tempFile) {
1633         tempFile->remove();
1634         delete tempFile;
1635         tempFile=0;
1636     }
1637 }
1638 
1639 #include "moc_mtpdevice.cpp"
1640