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