1 #include "library/itunes/itunesfeature.h"
2 
3 #include <QAction>
4 #include <QFileDialog>
5 #include <QFileInfo>
6 #include <QMenu>
7 #include <QMessageBox>
8 #include <QStandardPaths>
9 #include <QUrl>
10 #include <QXmlStreamReader>
11 #include <QtDebug>
12 
13 #include "library/baseexternalplaylistmodel.h"
14 #include "library/baseexternaltrackmodel.h"
15 #include "library/basetrackcache.h"
16 #include "library/dao/settingsdao.h"
17 #include "library/library.h"
18 #include "library/queryutil.h"
19 #include "library/trackcollectionmanager.h"
20 #include "moc_itunesfeature.cpp"
21 #include "util/lcs.h"
22 #include "util/sandbox.h"
23 #include "widget/wlibrarysidebar.h"
24 
25 #ifdef __SQLITE3__
26 #include <sqlite3.h>
27 #else // __SQLITE3__
28 #define SQLITE_CONSTRAINT  19 // Abort due to constraint violation
29 #endif // __SQLITE3__
30 
31 namespace {
32 
33 const QString ITDB_PATH_KEY = "mixxx.itunesfeature.itdbpath";
34 
35 const QString kDict = "dict";
36 const QString kKey = "key";
37 const QString kTrackId = "Track ID";
38 const QString kName = "Name";
39 const QString kArtist = "Artist";
40 const QString kAlbum = "Album";
41 const QString kAlbumArtist = "Album Artist";
42 const QString kGenre = "Genre";
43 const QString kGrouping = "Grouping";
44 const QString kBPM = "BPM";
45 const QString kBitRate = "Bit Rate";
46 const QString kComments = "Comments";
47 const QString kTotalTime = "Total Time";
48 const QString kYear = "Year";
49 const QString kLocation = "Location";
50 const QString kTrackNumber = "Track Number";
51 const QString kRating = "Rating";
52 const QString kTrackType = "Track Type";
53 const QString kRemote = "Remote";
54 
localhost_token()55 QString localhost_token() {
56 #if defined(__WINDOWS__)
57     return "//localhost/";
58 #else
59     return "//localhost";
60 #endif
61 }
62 
63 } // anonymous namespace
64 
ITunesFeature(Library * pLibrary,UserSettingsPointer pConfig)65 ITunesFeature::ITunesFeature(Library* pLibrary, UserSettingsPointer pConfig)
66         : BaseExternalLibraryFeature(pLibrary, pConfig),
67           m_cancelImport(false),
68           m_icon(":/images/library/ic_library_itunes.svg") {
69     QString tableName = "itunes_library";
70     QString idColumn = "id";
71     QStringList columns;
72     columns << "id"
73             << "artist"
74             << "title"
75             << "album"
76             << "album_artist"
77             << "year"
78             << "genre"
79             << "grouping"
80             << "tracknumber"
81             << "location"
82             << "comment"
83             << "duration"
84             << "bitrate"
85             << "bpm"
86             << "rating";
87 
88     m_trackSource = QSharedPointer<BaseTrackCache>(
89             new BaseTrackCache(m_pLibrary->trackCollections()->internalCollection(), tableName, idColumn,
90                                columns, false));
91     m_pITunesTrackModel = new BaseExternalTrackModel(
92         this, m_pLibrary->trackCollections(),
93         "mixxx.db.model.itunes",
94         "itunes_library",
95         m_trackSource);
96     m_pITunesPlaylistModel = new BaseExternalPlaylistModel(
97         this, m_pLibrary->trackCollections(),
98         "mixxx.db.model.itunes_playlist",
99         "itunes_playlists",
100         "itunes_playlist_tracks",
101         m_trackSource);
102     m_isActivated = false;
103     m_title = tr("iTunes");
104 
105     m_database = QSqlDatabase::cloneDatabase(m_pLibrary->trackCollections()->internalCollection()->database(), "ITUNES_SCANNER");
106 
107     // Open the database connection in this thread.
108     if (!m_database.open()) {
109         qDebug() << "Failed to open database for iTunes scanner." << m_database.lastError();
110     }
111     connect(&m_future_watcher,
112             &QFutureWatcher<TreeItem*>::finished,
113             this,
114             &ITunesFeature::onTrackCollectionLoaded);
115 }
116 
~ITunesFeature()117 ITunesFeature::~ITunesFeature() {
118     m_database.close();
119     m_cancelImport = true;
120     m_future.waitForFinished();
121     delete m_pITunesTrackModel;
122     delete m_pITunesPlaylistModel;
123 }
124 
getPlaylistModelForPlaylist(const QString & playlist)125 BaseSqlTableModel* ITunesFeature::getPlaylistModelForPlaylist(const QString& playlist) {
126     BaseExternalPlaylistModel* pModel = new BaseExternalPlaylistModel(
127         this, m_pLibrary->trackCollections(),
128         "mixxx.db.model.itunes_playlist",
129         "itunes_playlists",
130         "itunes_playlist_tracks",
131         m_trackSource);
132     pModel->setPlaylist(playlist);
133     return pModel;
134 }
135 
136 // static
isSupported()137 bool ITunesFeature::isSupported() {
138     // itunes db might just be elsewhere, don't rely on it being in its
139     // normal place. And since we will load an itdb on any platform...
140     return true;
141 }
142 
143 
title()144 QVariant ITunesFeature::title() {
145     return m_title;
146 }
147 
getIcon()148 QIcon ITunesFeature::getIcon() {
149     return m_icon;
150 }
151 
bindSidebarWidget(WLibrarySidebar * pSidebarWidget)152 void ITunesFeature::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) {
153     // store the sidebar widget pointer for later use in onRightClick()
154     m_pSidebarWidget = pSidebarWidget;
155     // send it to BaseExternalLibraryFeature for onRightClickChild()
156     BaseExternalLibraryFeature::bindSidebarWidget(pSidebarWidget);
157 }
158 
activate()159 void ITunesFeature::activate() {
160     activate(false);
161     emit enableCoverArtDisplay(false);
162 }
163 
activate(bool forceReload)164 void ITunesFeature::activate(bool forceReload) {
165     //qDebug("ITunesFeature::activate()");
166     if (!m_isActivated || forceReload) {
167 
168         //Delete all table entries of iTunes feature
169         ScopedTransaction transaction(m_database);
170         clearTable("itunes_playlist_tracks");
171         clearTable("itunes_library");
172         clearTable("itunes_playlists");
173         transaction.commit();
174 
175         emit showTrackModel(m_pITunesTrackModel);
176 
177         SettingsDAO settings(m_pTrackCollection->database());
178         QString dbSetting(settings.getValue(ITDB_PATH_KEY));
179         // if a path exists in the database, use it
180         if (!dbSetting.isEmpty() && QFile::exists(dbSetting)) {
181             m_dbfile = dbSetting;
182         } else {
183             // No Path in settings, try the default
184             m_dbfile = getiTunesMusicPath();
185         }
186 
187         QFileInfo dbFile(m_dbfile);
188         if (!m_dbfile.isEmpty() && dbFile.exists()) {
189             // Users of Mixxx <1.12.0 didn't support sandboxing. If we are sandboxed
190             // and using a custom iTunes path then we have to ask for access to this
191             // file.
192             Sandbox::askForAccess(m_dbfile);
193         } else {
194             // if the path we got between the default and the database doesn't
195             // exist, ask for a new one and use/save it if it exists
196             m_dbfile = QFileDialog::getOpenFileName(
197                     nullptr, tr("Select your iTunes library"), QDir::homePath(), "*.xml");
198             QFileInfo dbFile(m_dbfile);
199             if (m_dbfile.isEmpty() || !dbFile.exists()) {
200                 return;
201             }
202 
203             // The user has picked a new directory via a file dialog. This means the
204             // system sandboxer (if we are sandboxed) has granted us permission to
205             // this folder. Create a security bookmark while we have permission so
206             // that we can access the folder on future runs. We need to canonicalize
207             // the path so we first wrap the directory string with a QDir.
208             Sandbox::createSecurityToken(dbFile);
209             settings.setValue(ITDB_PATH_KEY, m_dbfile);
210         }
211         m_isActivated =  true;
212         // Let a worker thread do the XML parsing
213         m_future = QtConcurrent::run(this, &ITunesFeature::importLibrary);
214         m_future_watcher.setFuture(m_future);
215         m_title = tr("(loading) iTunes");
216         // calls a slot in the sidebar model such that 'iTunes (isLoading)' is displayed.
217         emit featureIsLoading(this, true);
218     } else {
219         emit showTrackModel(m_pITunesTrackModel);
220     }
221     emit enableCoverArtDisplay(false);
222 }
223 
activateChild(const QModelIndex & index)224 void ITunesFeature::activateChild(const QModelIndex& index) {
225     //qDebug() << "ITunesFeature::activateChild()" << index;
226     QString playlist = index.data().toString();
227     qDebug() << "Activating " << playlist;
228     m_pITunesPlaylistModel->setPlaylist(playlist);
229     emit showTrackModel(m_pITunesPlaylistModel);
230     emit enableCoverArtDisplay(false);
231 }
232 
getChildModel()233 TreeItemModel* ITunesFeature::getChildModel() {
234     return &m_childModel;
235 }
236 
onRightClick(const QPoint & globalPos)237 void ITunesFeature::onRightClick(const QPoint& globalPos) {
238     BaseExternalLibraryFeature::onRightClick(globalPos);
239     QMenu menu(m_pSidebarWidget);
240     QAction useDefault(tr("Use Default Library"), &menu);
241     QAction chooseNew(tr("Choose Library..."), &menu);
242     menu.addAction(&useDefault);
243     menu.addAction(&chooseNew);
244     QAction *chosen(menu.exec(globalPos));
245     if (chosen == &useDefault) {
246         SettingsDAO settings(m_database);
247         settings.setValue(ITDB_PATH_KEY, QString());
248         activate(true); // clears tables before parsing
249     } else if (chosen == &chooseNew) {
250         SettingsDAO settings(m_database);
251         QString dbfile = QFileDialog::getOpenFileName(
252                 nullptr, tr("Select your iTunes library"), QDir::homePath(), "*.xml");
253 
254         QFileInfo dbFileInfo(dbfile);
255         if (dbfile.isEmpty() || !dbFileInfo.exists()) {
256             return;
257         }
258         // The user has picked a new directory via a file dialog. This means the
259         // system sandboxer (if we are sandboxed) has granted us permission to
260         // this folder. Create a security bookmark while we have permission so
261         // that we can access the folder on future runs. We need to canonicalize
262         // the path so we first wrap the directory string with a QDir.
263         Sandbox::createSecurityToken(dbFileInfo);
264 
265         settings.setValue(ITDB_PATH_KEY, dbfile);
266         activate(true); // clears tables before parsing
267     }
268 }
269 
getiTunesMusicPath()270 QString ITunesFeature::getiTunesMusicPath() {
271     QString musicFolder;
272 #if defined(__APPLE__)
273     musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation)
274                   + "/iTunes/iTunes Music Library.xml";
275 #elif defined(__WINDOWS__)
276     musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation)
277                   + "\\iTunes\\iTunes Music Library.xml";
278 #else
279     musicFolder = "";
280 #endif
281     qDebug() << "ITunesLibrary=[" << musicFolder << "]";
282     return musicFolder;
283 }
284 
guessMusicLibraryMountpoint(QXmlStreamReader & xml)285 void ITunesFeature::guessMusicLibraryMountpoint(QXmlStreamReader& xml) {
286     // Normally the Folder Layout it some thing like that
287     // iTunes/
288     // iTunes/Album Artwork
289     // iTunes/iTunes Media <- this is the "Music Folder"
290     // iTunes/iTunes Music Library.xml <- this location we already knew
291     QString music_folder = QUrl(xml.readElementText()).toLocalFile();
292 
293     QString music_folder_test = music_folder;
294     music_folder_test.replace(localhost_token(), "");
295     QDir music_folder_dir(music_folder_test);
296 
297     // The music folder exists, so a simple transformation
298     // of replacing localhost token with nothing will work.
299     if (music_folder_dir.exists()) {
300         // Leave defaults intact.
301         return;
302     }
303 
304     // The iTunes Music Library doesn't exist! This means we are likely loading
305     // the library from a system that is different from the one that wrote the
306     // iTunes configuration. The configuration file path, m_dbfile is a readable
307     // location that in most situation is "close" to the music library path so
308     // since we can read that file we will try to infer the music library mount
309     // point from it.
310 
311     // Examples:
312 
313     // Windows with non-itunes-managed music:
314     // m_dbfile: c:/Users/LegacyII/Music/iTunes/iTunes Music Library.xml
315     // Music Folder: file://localhost/C:/Users/LegacyII/Music/
316     // Transformation:  "//localhost/" -> ""
317 
318     // Mac OS X with iTunes-managed music:
319     // m_dbfile: /Users/rjryan/Music/iTunes/iTunes Music Library.xml
320     // Music Folder: file://localhost/Users/rjryan/Music/iTunes/iTunes Media/
321     // Transformation: "//localhost" -> ""
322 
323     // Linux reading an OS X partition mounted at /media/foo to an
324     // iTunes-managed music folder:
325     // m_dbfile: /media/foo/Users/rjryan/Music/iTunes/iTunes Music Library.xml
326     // Music Folder: file://localhost/Users/rjryan/Music/iTunes/iTunes Media/
327     // Transformation: "//localhost" -> "/media/foo"
328 
329     // Linux reading a Windows partition mounted at /media/foo to an
330     // non-itunes-managed music folder:
331     // m_dbfile: /media/foo/Users/LegacyII/Music/iTunes/iTunes Music Library.xml
332     // Music Folder: file://localhost/C:/Users/LegacyII/Music/
333     // Transformation:  "//localhost/C:" -> "/media/foo"
334 
335     // Algorithm:
336     // 1. Find the largest common subsequence shared between m_dbfile and "Music
337     //    Folder"
338     // 2. For all tracks, replace the left-side of of the LCS in "Music Folder"
339     //    with the left-side of the LCS in m_dbfile.
340 
341     QString lcs = LCS(m_dbfile, music_folder);
342 
343     if (lcs.size() <= 1) {
344         qDebug() << "ERROR: Couldn't find a suitable transformation to load iTunes data files. Leaving defaults intact.";
345     }
346 
347     int musicFolderLcsIndex = music_folder.indexOf(lcs);
348     if (musicFolderLcsIndex < 0) {
349         qDebug() << "ERROR: Detected LCS" << lcs
350                  << "is not present in music_folder:" << music_folder;
351         return;
352     }
353 
354     int dbfileLcsIndex = m_dbfile.indexOf(lcs);
355     if (dbfileLcsIndex < 0) {
356         qDebug() << "ERROR: Detected LCS" << lcs
357                  << "is not present in m_dbfile" << m_dbfile;
358         return;
359     }
360 
361     m_dbItunesRoot = music_folder.left(musicFolderLcsIndex);
362     m_mixxxItunesRoot = m_dbfile.left(dbfileLcsIndex);
363     qDebug() << "Detected translation rule for iTunes files:"
364              << m_dbItunesRoot << "->" << m_mixxxItunesRoot;
365 }
366 
367 // This method is executed in a separate thread
368 // via QtConcurrent::run
importLibrary()369 TreeItem* ITunesFeature::importLibrary() {
370     bool isTracksParsed=false;
371     bool isMusicFolderLocatedAfterTracks=false;
372 
373     //Give thread a low priority
374     QThread* thisThread = QThread::currentThread();
375     thisThread->setPriority(QThread::LowPriority);
376 
377     qDebug() << "ITunesFeature::importLibrary() ";
378 
379     ScopedTransaction transaction(m_database);
380 
381     // By default set m_mixxxItunesRoot and m_dbItunesRoot to strip out
382     // file://localhost/ from the URL. When we load the user's iTunes XML
383     // configuration we may replace this with something based on the detected
384     // location of the user's iTunes path but the defaults are necessary in case
385     // their iTunes XML does not include the "Music Folder" key.
386     m_mixxxItunesRoot = "";
387     m_dbItunesRoot = localhost_token();
388 
389     //Parse iTunes XML file using SAX (for performance)
390     QFile itunes_file(m_dbfile);
391     if (!itunes_file.open(QIODevice::ReadOnly)) {
392         qDebug() << "Cannot open iTunes music collection";
393         return nullptr;
394     }
395 
396     QXmlStreamReader xml(&itunes_file);
397     TreeItem* playlist_root = nullptr;
398     while (!xml.atEnd() && !m_cancelImport) {
399         xml.readNext();
400         if (xml.isStartElement()) {
401             if (xml.name() == "key") {
402                 QString key = xml.readElementText();
403                 if (key == "Music Folder") {
404                     if (isTracksParsed) {
405                         isMusicFolderLocatedAfterTracks = true;
406                     }
407                     if (readNextStartElement(xml)) {
408                         guessMusicLibraryMountpoint(xml);
409                     }
410                 } else if (key == "Tracks") {
411                     parseTracks(xml);
412                     if (playlist_root != nullptr) {
413                         delete playlist_root;
414                     }
415                     playlist_root = parsePlaylists(xml);
416                     isTracksParsed = true;
417                 }
418             }
419         }
420     }
421 
422     itunes_file.close();
423 
424     if (isMusicFolderLocatedAfterTracks) {
425         qDebug() << "Updating iTunes real path from " << m_dbItunesRoot << " to " << m_mixxxItunesRoot;
426         // In some iTunes files "Music Folder" XML node is located at the end of file. So, we need to
427         QSqlQuery query(m_database);
428         query.prepare("UPDATE itunes_library SET location = replace( location, :itunes_path, :mixxx_path )");
429         query.bindValue(":itunes_path", m_dbItunesRoot.replace(localhost_token(), ""));
430         query.bindValue(":mixxx_path", m_mixxxItunesRoot);
431         bool success = query.exec();
432 
433         if (!success) {
434             LOG_FAILED_QUERY(query);
435         }
436     }
437 
438     // Even if an error occurred, commit the transaction. The file may have been
439     // half-parsed.
440     transaction.commit();
441 
442     if (xml.hasError()) {
443         // do error handling
444         qDebug() << "Abort processing iTunes music collection";
445         qDebug() << "line:" << xml.lineNumber() <<
446                 "column:" << xml.columnNumber() <<
447                 "error:" << xml.errorString();
448         if (playlist_root) {
449             delete playlist_root;
450         }
451         playlist_root = nullptr;
452     }
453     return playlist_root;
454 }
455 
parseTracks(QXmlStreamReader & xml)456 void ITunesFeature::parseTracks(QXmlStreamReader& xml) {
457     bool in_container_dictionary = false;
458     bool in_track_dictionary = false;
459     QSqlQuery query(m_database);
460     query.prepare("INSERT INTO itunes_library (id, artist, title, album, album_artist, year, genre, grouping, comment, tracknumber,"
461                   "bpm, bitrate,"
462                   "duration, location,"
463                   "rating ) "
464                   "VALUES (:id, :artist, :title, :album, :album_artist, :year, :genre, :grouping, :comment, :tracknumber,"
465                   ":bpm, :bitrate,"
466                   ":duration, :location," ":rating )");
467 
468     qDebug() << "Parse iTunes music collection";
469 
470     // read all sunsequent <dict> until we reach the closing ENTRY tag
471     while (!xml.atEnd() && !m_cancelImport) {
472         xml.readNext();
473 
474         if (xml.isStartElement()) {
475             if (xml.name() == kDict) {
476                 if (!in_track_dictionary && !in_container_dictionary) {
477                     in_container_dictionary = true;
478                     continue;
479                 } else if (in_container_dictionary && !in_track_dictionary) {
480                     // We are in a <dict> tag that holds track information
481                     in_track_dictionary = true;
482                     // Parse track here
483                     parseTrack(xml, query);
484                 }
485             }
486         }
487 
488         if (xml.isEndElement() && xml.name() == kDict) {
489             if (in_track_dictionary && in_container_dictionary) {
490                 in_track_dictionary = false;
491                 continue;
492             } else if (in_container_dictionary && !in_track_dictionary) {
493                 // Done parsing tracks.
494                 break;
495             }
496         }
497     }
498 }
499 
parseTrack(QXmlStreamReader & xml,QSqlQuery & query)500 void ITunesFeature::parseTrack(QXmlStreamReader& xml, QSqlQuery& query) {
501     //qDebug() << "----------------TRACK-----------------";
502     int id = -1;
503     QString title;
504     QString artist;
505     QString album;
506     QString album_artist;
507     QString year;
508     QString genre;
509     QString grouping;
510     QString location;
511 
512     int bpm = 0;
513     int bitrate = 0;
514 
515     //duration of a track
516     int playtime = 0;
517     int rating = 0;
518     QString comment;
519     QString tracknumber;
520     QString tracktype;
521 
522     while (!xml.atEnd()) {
523         xml.readNext();
524 
525         if (xml.isStartElement()) {
526             if (xml.name() == kKey) {
527                 QString key = xml.readElementText();
528 
529                 QString content;
530                 if (readNextStartElement(xml)) {
531                     content = xml.readElementText();
532                 }
533 
534                 //qDebug() << "Key: " << key << " Content: " << content;
535 
536                 if (key == kTrackId) {
537                     id = content.toInt();
538                     continue;
539                 }
540                 if (key == kName) {
541                     title = content;
542                     continue;
543                 }
544                 if (key == kArtist) {
545                     artist = content;
546                     continue;
547                 }
548                 if (key == kAlbum) {
549                     album = content;
550                     continue;
551                 }
552                 if (key == kAlbumArtist) {
553                     album_artist = content;
554                     continue;
555                 }
556                 if (key == kGenre) {
557                     genre = content;
558                     continue;
559                 }
560                 if (key == kGrouping) {
561                     grouping = content;
562                     continue;
563                 }
564                 if (key == kBPM) {
565                     bpm = content.toInt();
566                     continue;
567                 }
568                 if (key == kBitRate) {
569                     bitrate =  content.toInt();
570                     continue;
571                 }
572                 if (key == kComments) {
573                     comment = content;
574                     continue;
575                 }
576                 if (key == kTotalTime) {
577                     playtime = (content.toInt() / 1000);
578                     continue;
579                 }
580                 if (key == kYear) {
581                     year = content;
582                     continue;
583                 }
584                 if (key == kLocation) {
585                     location = TrackFile::fromUrl(QUrl(content)).location();
586                     // Replace first part of location with the mixxx iTunes Root
587                     // on systems where iTunes installed it only strips //localhost
588                     // on iTunes from foreign systems the mount point is replaced
589                     if (!m_dbItunesRoot.isEmpty()) {
590                         location.replace(m_dbItunesRoot, m_mixxxItunesRoot);
591                     }
592                     continue;
593                 }
594                 if (key == kTrackNumber) {
595                     tracknumber = content;
596                     continue;
597                 }
598                 if (key == kRating) {
599                     //value is an integer and ranges from 0 to 100
600                     rating = (content.toInt() / 20);
601                     continue;
602                 }
603                 if (key == kTrackType) {
604                     tracktype = content;
605                     continue;
606                 }
607             }
608         }
609         //exit loop on closing </dict>
610         if (xml.isEndElement() && xml.name() == kDict) {
611             break;
612         }
613     }
614 
615     // If file is a remote file from iTunes Match, don't save it to the database.
616     // There's no way that mixxx can access it.
617     if (tracktype == kRemote) {
618         return;
619     }
620 
621     // If we reach the end of <dict>
622     // Save parsed track to database
623     query.bindValue(":id", id);
624     query.bindValue(":artist", artist);
625     query.bindValue(":title", title);
626     query.bindValue(":album", album);
627     query.bindValue(":album_artist", album_artist);
628     query.bindValue(":genre", genre);
629     query.bindValue(":grouping", grouping);
630     query.bindValue(":year", year);
631     query.bindValue(":duration", playtime);
632     query.bindValue(":location", location);
633     query.bindValue(":rating", rating);
634     query.bindValue(":comment", comment);
635     query.bindValue(":tracknumber", tracknumber);
636     query.bindValue(":bpm", bpm);
637     query.bindValue(":bitrate", bitrate);
638 
639     bool success = query.exec();
640 
641     if (!success) {
642         LOG_FAILED_QUERY(query);
643         return;
644     }
645 }
646 
parsePlaylists(QXmlStreamReader & xml)647 TreeItem* ITunesFeature::parsePlaylists(QXmlStreamReader& xml) {
648     qDebug() << "Parse iTunes playlists";
649     std::unique_ptr<TreeItem> pRootItem = TreeItem::newRoot(this);
650     QSqlQuery query_insert_to_playlists(m_database);
651     query_insert_to_playlists.prepare("INSERT INTO itunes_playlists (id, name) "
652                                       "VALUES (:id, :name)");
653 
654     QSqlQuery query_insert_to_playlist_tracks(m_database);
655     query_insert_to_playlist_tracks.prepare(
656         "INSERT INTO itunes_playlist_tracks (playlist_id, track_id, position) "
657         "VALUES (:playlist_id, :track_id, :position)");
658 
659     while (!xml.atEnd() && !m_cancelImport) {
660         xml.readNext();
661         //We process and iterate the <dict> tags holding playlist summary information here
662         if (xml.isStartElement() && xml.name() == kDict) {
663             parsePlaylist(xml,
664                           query_insert_to_playlists,
665                           query_insert_to_playlist_tracks,
666                           pRootItem.get());
667             continue;
668         }
669         if (xml.isEndElement()) {
670             if (xml.name() == "array") {
671                 break;
672             }
673         }
674     }
675     return pRootItem.release();
676 }
677 
readNextStartElement(QXmlStreamReader & xml)678 bool ITunesFeature::readNextStartElement(QXmlStreamReader& xml) {
679     QXmlStreamReader::TokenType token = QXmlStreamReader::NoToken;
680     while (token != QXmlStreamReader::EndDocument && token != QXmlStreamReader::Invalid) {
681         token = xml.readNext();
682         if (token == QXmlStreamReader::StartElement) {
683             return true;
684         }
685     }
686     return false;
687 }
688 
parsePlaylist(QXmlStreamReader & xml,QSqlQuery & query_insert_to_playlists,QSqlQuery & query_insert_to_playlist_tracks,TreeItem * root)689 void ITunesFeature::parsePlaylist(QXmlStreamReader& xml, QSqlQuery& query_insert_to_playlists,
690                                   QSqlQuery& query_insert_to_playlist_tracks, TreeItem* root) {
691     //qDebug() << "Parse Playlist";
692 
693     QString playlistname;
694     int playlist_id = -1;
695     int playlist_position = -1;
696     int track_reference = -1;
697     //indicates that we haven't found the <
698     bool isSystemPlaylist = false;
699     bool isPlaylistItemsStarted = false;
700 
701     //We process and iterate the <dict> tags holding playlist summary information here
702     while (!xml.atEnd() && !m_cancelImport) {
703         xml.readNext();
704 
705         if (xml.isStartElement()) {
706 
707             if (xml.name() == kKey) {
708                 QString key = xml.readElementText();
709                 // The rules are processed in sequence
710                 // That is, XML is ordered.
711                 // For iTunes Playlist names are always followed by the ID.
712                 // Afterwars the playlist entries occur
713                 if (key == "Name") {
714                     readNextStartElement(xml);
715                     playlistname = xml.readElementText();
716                     continue;
717                 }
718                 //When parsing the ID, the playlistname has already been found
719                 if (key == "Playlist ID") {
720                     readNextStartElement(xml);
721                     playlist_id = xml.readElementText().toInt();
722                     playlist_position = 1;
723                     continue;
724                 }
725                 //Hide playlists that are system playlists
726                 if (key == "Master" || key == "Movies" || key == "TV Shows" ||
727                     key == "Music" || key == "Books" || key == "Purchased") {
728                     isSystemPlaylist = true;
729                     continue;
730                 }
731 
732                 if (key == "Playlist Items") {
733                     isPlaylistItemsStarted = true;
734 
735                     //if the playlist is prebuild don't hit the database
736                     if (isSystemPlaylist) {
737                         continue;
738                     }
739                     query_insert_to_playlists.bindValue(":id", playlist_id);
740                     query_insert_to_playlists.bindValue(":name", playlistname);
741 
742                     bool success = query_insert_to_playlists.exec();
743                     if (!success) {
744                         if (query_insert_to_playlists.lastError().nativeErrorCode() == QString::number(SQLITE_CONSTRAINT)) {
745                             // We assume a duplicate Playlist name
746                             playlistname += QString(" #%1").arg(playlist_id);
747                             query_insert_to_playlists.bindValue(":name", playlistname );
748 
749                             bool success = query_insert_to_playlists.exec();
750                             if (!success) {
751                                 // unexpected error
752                                 LOG_FAILED_QUERY(query_insert_to_playlists);
753                                 break;
754                             }
755                         } else {
756                             // unexpected error
757                             LOG_FAILED_QUERY(query_insert_to_playlists);
758                             return;
759                         }
760                     }
761                     //append the playlist to the child model
762                     root->appendChild(playlistname);
763                 }
764                 // When processing playlist entries, playlist name and id have
765                 // already been processed and persisted
766                 if (key == kTrackId) {
767 
768                     readNextStartElement(xml);
769                     track_reference = xml.readElementText().toInt();
770 
771                     query_insert_to_playlist_tracks.bindValue(":playlist_id", playlist_id);
772                     query_insert_to_playlist_tracks.bindValue(":track_id", track_reference);
773                     query_insert_to_playlist_tracks.bindValue(":position", playlist_position++);
774 
775                     //Insert tracks if we are not in a pre-build playlist
776                     if (!isSystemPlaylist && !query_insert_to_playlist_tracks.exec()) {
777                         qDebug() << "SQL Error in ITunesFeature.cpp: line" << __LINE__ << " "
778                                  << query_insert_to_playlist_tracks.lastError();
779                         qDebug() << "trackid" << track_reference;
780                         qDebug() << "playlistname; " << playlistname;
781                         qDebug() << "-----------------";
782                     }
783                 }
784             }
785         }
786         if (xml.isEndElement()) {
787             if (xml.name() == "array") {
788                 //qDebug() << "exit playlist";
789                 break;
790             }
791             if (xml.name() == kDict && !isPlaylistItemsStarted){
792                 // Some playlists can be empty, so we need to exit.
793                 break;
794             }
795         }
796     }
797 }
798 
clearTable(const QString & table_name)799 void ITunesFeature::clearTable(const QString& table_name) {
800     QSqlQuery query(m_database);
801     query.prepare("delete from "+table_name);
802     bool success = query.exec();
803 
804     if (!success) {
805         qDebug() << "Could not delete remove old entries from table "
806                  << table_name << " : " << query.lastError();
807     } else {
808         qDebug() << "iTunes table entries of '"
809                  << table_name <<"' have been cleared.";
810     }
811 }
812 
onTrackCollectionLoaded()813 void ITunesFeature::onTrackCollectionLoaded() {
814     std::unique_ptr<TreeItem> root(m_future.result());
815     if (root) {
816         m_childModel.setRootItem(std::move(root));
817 
818         // Tell the rhythmbox track source that it should re-build its index.
819         m_trackSource->buildIndex();
820 
821         //m_pITunesTrackModel->select();
822         emit showTrackModel(m_pITunesTrackModel);
823         qDebug() << "Itunes library loaded: success";
824     } else {
825         QMessageBox::warning(
826                 nullptr,
827                 tr("Error Loading iTunes Library"),
828                 tr("There was an error loading your iTunes library. Some of "
829                    "your iTunes tracks or playlists may not have loaded."));
830     }
831     // calls a slot in the sidebarmodel such that 'isLoading' is removed from the feature title.
832     m_title = tr("iTunes");
833     emit featureLoadingFinished(this);
834     activate();
835 }
836