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