![]() |
Mixxx
|
00001 #include <QMessageBox> 00002 #include <QtDebug> 00003 #include <QXmlStreamReader> 00004 #include <QDesktopServices> 00005 #include <QFileDialog> 00006 #include <QMenu> 00007 #include <QAction> 00008 00009 #include "library/itunes/itunesfeature.h" 00010 00011 #include "library/basetrackcache.h" 00012 #include "library/dao/settingsdao.h" 00013 #include "library/itunes/itunesplaylistmodel.h" 00014 #include "library/itunes/itunestrackmodel.h" 00015 00016 const QString ITunesFeature::ITDB_PATH_KEY = "mixxx.itunesfeature.itdbpath"; 00017 00018 00019 ITunesFeature::ITunesFeature(QObject* parent, TrackCollection* pTrackCollection) 00020 : LibraryFeature(parent), 00021 m_pTrackCollection(pTrackCollection), 00022 m_database(pTrackCollection->getDatabase()), 00023 m_cancelImport(false) { 00024 QString tableName = "itunes_library"; 00025 QString idColumn = "id"; 00026 QStringList columns; 00027 columns << "id" 00028 << "artist" 00029 << "title" 00030 << "album" 00031 << "year" 00032 << "genre" 00033 << "tracknumber" 00034 << "location" 00035 << "comment" 00036 << "duration" 00037 << "bitrate" 00038 << "bpm" 00039 << "rating"; 00040 pTrackCollection->addTrackSource( 00041 QString("itunes"), QSharedPointer<BaseTrackCache>( 00042 new BaseTrackCache(m_pTrackCollection, tableName, idColumn, 00043 columns, false))); 00044 m_pITunesTrackModel = new ITunesTrackModel(this, m_pTrackCollection); 00045 m_pITunesPlaylistModel = new ITunesPlaylistModel(this, m_pTrackCollection); 00046 m_isActivated = false; 00047 m_title = tr("iTunes"); 00048 00049 if (!m_database.isOpen()) { 00050 m_database = QSqlDatabase::addDatabase("QSQLITE", "ITUNES_SCANNER"); 00051 m_database.setHostName("localhost"); 00052 m_database.setDatabaseName(MIXXX_DB_PATH); 00053 m_database.setUserName("mixxx"); 00054 m_database.setPassword("mixxx"); 00055 00056 //Open the database connection in this thread. 00057 if (!m_database.open()) { 00058 qDebug() << "Failed to open database for iTunes scanner." << m_database.lastError(); 00059 } 00060 } 00061 00062 connect(&m_future_watcher, SIGNAL(finished()), this, SLOT(onTrackCollectionLoaded())); 00063 } 00064 00065 ITunesFeature::~ITunesFeature() { 00066 m_cancelImport = true; 00067 m_future.waitForFinished(); 00068 delete m_pITunesTrackModel; 00069 delete m_pITunesPlaylistModel; 00070 } 00071 00072 bool ITunesFeature::isSupported() { 00073 // itunes db might just be elsewhere, don't rely on it being in its 00074 // normal place. And since we will load an itdb on any platform... 00075 // update: itunes writes absolute paths which means they generally 00076 // won't translate when you open the itdb from some other os (eg. linux) 00077 // so I'm disabling on non-mac/win platforms -bkgood 00078 #if defined(Q_OS_WIN32) || defined(Q_OS_MAC) 00079 return true; //QFile::exists(getiTunesMusicPath()); 00080 #else 00081 return false; 00082 #endif 00083 } 00084 00085 00086 QVariant ITunesFeature::title() { 00087 return m_title; 00088 } 00089 00090 QIcon ITunesFeature::getIcon() { 00091 return QIcon(":/images/library/ic_library_itunes.png"); 00092 } 00093 00094 void ITunesFeature::activate() { 00095 activate(false); 00096 } 00097 00098 void ITunesFeature::activate(bool forceReload) { 00099 //qDebug("ITunesFeature::activate()"); 00100 00101 if (!m_isActivated || forceReload) { 00102 00103 // first, assume we should use the default 00104 m_dbfile = getiTunesMusicPath(); 00105 00106 SettingsDAO settings(m_pTrackCollection->getDatabase()); 00107 QString dbSetting(settings.getValue(ITDB_PATH_KEY)); 00108 // if a path exists in the database, use it 00109 if (!dbSetting.isEmpty() && QFile::exists(dbSetting)) { 00110 m_dbfile = dbSetting; 00111 } 00112 // if the path we got between the default and the database doesn't 00113 // exist, ask for a new one and use/save it if it exists 00114 if (!QFile::exists(m_dbfile)) { 00115 m_dbfile = QFileDialog::getOpenFileName(NULL, 00116 tr("Select your iTunes library"), 00117 QDir::homePath(), "*.xml"); 00118 if (m_dbfile.isEmpty() || !QFile::exists(m_dbfile)) { 00119 return; 00120 } 00121 settings.setValue(ITDB_PATH_KEY, m_dbfile); 00122 } 00123 m_isActivated = true; 00124 /* Ususally the maximum number of threads 00125 * is > 2 depending on the CPU cores 00126 * Unfortunately, within VirtualBox 00127 * the maximum number of allowed threads 00128 * is 1 at all times We'll need to increase 00129 * the number to > 1, otherwise importing the music collection 00130 * takes place when the GUI threads terminates, i.e., on 00131 * Mixxx shutdown. 00132 */ 00133 QThreadPool::globalInstance()->setMaxThreadCount(4); //Tobias decided to use 4 00134 // Let a worker thread do the XML parsing 00135 m_future = QtConcurrent::run(this, &ITunesFeature::importLibrary); 00136 m_future_watcher.setFuture(m_future); 00137 m_title = tr("(loading) iTunes"); 00138 //calls a slot in the sidebar model such that 'iTunes (isLoading)' is displayed. 00139 emit (featureIsLoading(this)); 00140 } 00141 else{ 00142 emit(showTrackModel(m_pITunesTrackModel)); 00143 } 00144 00145 } 00146 00147 void ITunesFeature::activateChild(const QModelIndex& index) { 00148 //qDebug() << "ITunesFeature::activateChild()" << index; 00149 QString playlist = index.data().toString(); 00150 qDebug() << "Activating " << playlist; 00151 m_pITunesPlaylistModel->setPlaylist(playlist); 00152 emit(showTrackModel(m_pITunesPlaylistModel)); 00153 } 00154 00155 TreeItemModel* ITunesFeature::getChildModel() { 00156 return &m_childModel; 00157 } 00158 00159 void ITunesFeature::onRightClick(const QPoint& globalPos) { 00160 QMenu menu; 00161 QAction useDefault(tr("Use Default Library"), &menu); 00162 QAction chooseNew(tr("Choose Library..."), &menu); 00163 menu.addAction(&useDefault); 00164 menu.addAction(&chooseNew); 00165 QAction *chosen(menu.exec(globalPos)); 00166 if (chosen == &useDefault) { 00167 SettingsDAO settings(m_database); 00168 settings.setValue(ITDB_PATH_KEY, QString()); 00169 activate(true); // clears tables before parsing 00170 } else if (chosen == &chooseNew) { 00171 SettingsDAO settings(m_database); 00172 QString dbfile = QFileDialog::getOpenFileName(NULL, 00173 tr("Select your iTunes library"), 00174 QDir::homePath(), "*.xml"); 00175 if (dbfile.isEmpty() || !QFile::exists(dbfile)) { 00176 return; 00177 } 00178 settings.setValue(ITDB_PATH_KEY, dbfile); 00179 activate(true); // clears tables before parsing 00180 } 00181 } 00182 00183 void ITunesFeature::onRightClickChild(const QPoint& globalPos, QModelIndex index) { 00184 } 00185 00186 bool ITunesFeature::dropAccept(QUrl url) { 00187 return false; 00188 } 00189 00190 bool ITunesFeature::dropAcceptChild(const QModelIndex& index, QUrl url) { 00191 return false; 00192 } 00193 00194 bool ITunesFeature::dragMoveAccept(QUrl url) { 00195 return false; 00196 } 00197 00198 bool ITunesFeature::dragMoveAcceptChild(const QModelIndex& index, QUrl url) { 00199 return false; 00200 } 00201 00202 QString ITunesFeature::getiTunesMusicPath() { 00203 QString musicFolder; 00204 #if defined(__APPLE__) 00205 musicFolder = QDesktopServices::storageLocation(QDesktopServices::MusicLocation) + "/iTunes/iTunes Music Library.xml"; 00206 #elif defined(__WINDOWS__) 00207 musicFolder = QDesktopServices::storageLocation(QDesktopServices::MusicLocation) + "\\iTunes\\iTunes Music Library.xml"; 00208 #elif defined(__LINUX__) 00209 musicFolder = QDir::homePath() + "/iTunes Music Library.xml"; 00210 #else 00211 musicFolder = ""; 00212 #endif 00213 qDebug() << "ITunesLibrary=[" << musicFolder << "]"; 00214 return musicFolder; 00215 } 00216 /* 00217 * This method is executed in a separate thread 00218 * via QtConcurrent::run 00219 */ 00220 TreeItem* ITunesFeature::importLibrary() { 00221 //Give thread a low priority 00222 QThread* thisThread = QThread::currentThread(); 00223 thisThread->setPriority(QThread::LowestPriority); 00224 00225 //Delete all table entries of iTunes feature 00226 m_database.transaction(); 00227 clearTable("itunes_playlist_tracks"); 00228 clearTable("itunes_library"); 00229 clearTable("itunes_playlists"); 00230 m_database.commit(); 00231 00232 qDebug() << "ITunesFeature::importLibrary() "; 00233 00234 00235 m_database.transaction(); 00236 00237 //Parse iTunes XML file using SAX (for performance) 00238 QFile itunes_file(m_dbfile); 00239 if (!itunes_file.open(QIODevice::ReadOnly | QIODevice::Text)) { 00240 qDebug() << "Cannot open iTunes music collection"; 00241 return false; 00242 } 00243 QXmlStreamReader xml(&itunes_file); 00244 TreeItem* playlist_root = NULL; 00245 while (!xml.atEnd() && !m_cancelImport) { 00246 xml.readNext(); 00247 if (xml.isStartElement()) { 00248 if (xml.name() == "key") { 00249 if (xml.readElementText() == "Tracks") { 00250 parseTracks(xml); 00251 playlist_root = parsePlaylists(xml); 00252 } 00253 } 00254 } 00255 } 00256 00257 itunes_file.close(); 00258 00259 // Even if an error occured, commit the transaction. The file may have been 00260 // half-parsed. 00261 m_database.commit(); 00262 00263 if (xml.hasError()) { 00264 // do error handling 00265 qDebug() << "Cannot process iTunes music collection"; 00266 qDebug() << "XML ERROR: " << xml.errorString(); 00267 if(playlist_root) 00268 delete playlist_root; 00269 playlist_root = NULL; 00270 return false; 00271 } 00272 return playlist_root; 00273 } 00274 00275 void ITunesFeature::parseTracks(QXmlStreamReader &xml) { 00276 QSqlQuery query(m_database); 00277 query.prepare("INSERT INTO itunes_library (id, artist, title, album, year, genre, comment, tracknumber," 00278 "bpm, bitrate," 00279 "duration, location," 00280 "rating ) " 00281 "VALUES (:id, :artist, :title, :album, :year, :genre, :comment, :tracknumber," 00282 ":bpm, :bitrate," 00283 ":duration, :location," ":rating )"); 00284 00285 00286 bool in_container_dictionary = false; 00287 bool in_track_dictionary = false; 00288 00289 qDebug() << "Parse iTunes music collection"; 00290 00291 //read all sunsequent <dict> until we reach the closing ENTRY tag 00292 while (!xml.atEnd() && !m_cancelImport) { 00293 xml.readNext(); 00294 00295 if (xml.isStartElement()) { 00296 if (xml.name() == "dict") { 00297 if (!in_track_dictionary && !in_container_dictionary) { 00298 in_container_dictionary = true; 00299 continue; 00300 } else if (in_container_dictionary && !in_track_dictionary) { 00301 //We are in a <dict> tag that holds track information 00302 in_track_dictionary = true; 00303 //Parse track here 00304 parseTrack(xml, query); 00305 } 00306 } 00307 } 00308 00309 if (xml.isEndElement() && xml.name() == "dict") { 00310 if (in_track_dictionary && in_container_dictionary) { 00311 in_track_dictionary = false; 00312 continue; 00313 } else if (in_container_dictionary && !in_track_dictionary) { 00314 // Done parsing tracks. 00315 in_container_dictionary = false; 00316 break; 00317 } 00318 } 00319 } 00320 } 00321 00322 void ITunesFeature::parseTrack(QXmlStreamReader &xml, QSqlQuery &query) { 00323 //qDebug() << "----------------TRACK-----------------"; 00324 int id = -1; 00325 QString title; 00326 QString artist; 00327 QString album; 00328 QString year; 00329 QString genre; 00330 QString location; 00331 00332 int bpm = 0; 00333 int bitrate = 0; 00334 00335 //duration of a track 00336 int playtime = 0; 00337 int rating = 0; 00338 QString comment; 00339 QString tracknumber; 00340 00341 while (!xml.atEnd()) { 00342 xml.readNext(); 00343 00344 if (xml.isStartElement()) { 00345 if (xml.name() == "key") { 00346 QString key = xml.readElementText(); 00347 QString content = ""; 00348 00349 if (readNextStartElement(xml)) { 00350 content = xml.readElementText(); 00351 } 00352 00353 //qDebug() << "Key: " << key << " Content: " << content; 00354 00355 if (key == "Track ID") { 00356 id = content.toInt(); 00357 continue; 00358 } 00359 if (key == "Name") { 00360 title = content; 00361 continue; 00362 } 00363 if (key == "Artist") { 00364 artist = content; 00365 continue; 00366 } 00367 if (key == "Album") { 00368 album = content; 00369 continue; 00370 } 00371 if (key == "Genre") { 00372 genre = content; 00373 continue; 00374 } 00375 if (key == "BPM") { 00376 bpm = content.toInt(); 00377 continue; 00378 } 00379 if (key == "Bit Rate") { 00380 bitrate = content.toInt(); 00381 continue; 00382 } 00383 if (key == "Comments") { 00384 comment = content; 00385 continue; 00386 } 00387 if (key == "Total Time") { 00388 playtime = (content.toInt() / 1000); 00389 continue; 00390 } 00391 if (key == "Year") { 00392 year = content; 00393 continue; 00394 } 00395 if (key == "Location") { 00396 QByteArray strlocbytes = content.toUtf8(); 00397 location = QUrl::fromEncoded(strlocbytes).toLocalFile(); 00398 /* 00399 * Strip the crappy file://localhost/ from the URL and 00400 * format URL as in method ITunesTrackModel::parseTrackNode(QDomNode songNode) 00401 */ 00402 #if defined(__WINDOWS__) 00403 location.remove("//localhost/"); 00404 #else 00405 location.remove("//localhost"); 00406 #endif 00407 continue; 00408 } 00409 if (key == "Track Number") { 00410 tracknumber = content; 00411 continue; 00412 } 00413 if (key == "Rating") { 00414 //value is an integer and ranges from 0 to 100 00415 rating = (content.toInt() / 20); 00416 continue; 00417 } 00418 } 00419 } 00420 //exit loop on closing </dict> 00421 if (xml.isEndElement() && xml.name() == "dict") { 00422 break; 00423 } 00424 } 00425 /* If we reach the end of <dict> 00426 * Save parsed track to database 00427 */ 00428 query.bindValue(":id", id); 00429 query.bindValue(":artist", artist); 00430 query.bindValue(":title", title); 00431 query.bindValue(":album", album); 00432 query.bindValue(":genre", genre); 00433 query.bindValue(":year", year); 00434 query.bindValue(":duration", playtime); 00435 query.bindValue(":location", location); 00436 query.bindValue(":rating", rating); 00437 query.bindValue(":comment", comment); 00438 query.bindValue(":tracknumber", tracknumber); 00439 query.bindValue(":bpm", bpm); 00440 query.bindValue(":bitrate", bitrate); 00441 00442 bool success = query.exec(); 00443 00444 if (!success) { 00445 qDebug() << "SQL Error in itunesfeature.cpp: line" << __LINE__ << " " << query.lastError(); 00446 return; 00447 } 00448 } 00449 00450 TreeItem* ITunesFeature::parsePlaylists(QXmlStreamReader &xml) { 00451 qDebug() << "Parse iTunes playlists"; 00452 TreeItem* rootItem = new TreeItem(); 00453 QSqlQuery query_insert_to_playlists(m_database); 00454 query_insert_to_playlists.prepare("INSERT INTO itunes_playlists (id, name) " 00455 "VALUES (:id, :name)"); 00456 00457 QSqlQuery query_insert_to_playlist_tracks(m_database); 00458 query_insert_to_playlist_tracks.prepare( 00459 "INSERT INTO itunes_playlist_tracks (playlist_id, track_id, position) " 00460 "VALUES (:playlist_id, :track_id, :position)"); 00461 00462 while (!xml.atEnd() && !m_cancelImport) { 00463 xml.readNext(); 00464 //We process and iterate the <dict> tags holding playlist summary information here 00465 if (xml.isStartElement() && xml.name() == "dict") { 00466 parsePlaylist(xml, 00467 query_insert_to_playlists, 00468 query_insert_to_playlist_tracks, 00469 rootItem); 00470 continue; 00471 } 00472 if (xml.isEndElement()) { 00473 if (xml.name() == "array") 00474 break; 00475 } 00476 } 00477 return rootItem; 00478 } 00479 00480 bool ITunesFeature::readNextStartElement(QXmlStreamReader& xml) { 00481 QXmlStreamReader::TokenType token = QXmlStreamReader::NoToken; 00482 while (token != QXmlStreamReader::EndDocument && token != QXmlStreamReader::Invalid) { 00483 token = xml.readNext(); 00484 if (token == QXmlStreamReader::StartElement) { 00485 return true; 00486 } 00487 } 00488 return false; 00489 } 00490 00491 void ITunesFeature::parsePlaylist(QXmlStreamReader &xml, QSqlQuery &query_insert_to_playlists, 00492 QSqlQuery &query_insert_to_playlist_tracks, TreeItem* root) { 00493 //qDebug() << "Parse Playlist"; 00494 00495 QString playlistname; 00496 int playlist_id = -1; 00497 int playlist_position = -1; 00498 int track_reference = -1; 00499 //indicates that we haven't found the < 00500 bool isSystemPlaylist = false; 00501 00502 QString key; 00503 00504 00505 //We process and iterate the <dict> tags holding playlist summary information here 00506 while (!xml.atEnd() && !m_cancelImport) { 00507 xml.readNext(); 00508 00509 if (xml.isStartElement()) { 00510 00511 if (xml.name() == "key") { 00512 QString key = xml.readElementText(); 00513 /* 00514 * The rules are processed in sequence 00515 * That is, XML is ordered. 00516 * For iTunes Playlist names are always followed by the ID. 00517 * Afterwars the playlist entries occur 00518 */ 00519 if (key == "Name") { 00520 readNextStartElement(xml); 00521 playlistname = xml.readElementText(); 00522 continue; 00523 } 00524 //When parsing the ID, the playlistname has already been found 00525 if (key == "Playlist ID") { 00526 readNextStartElement(xml); 00527 playlist_id = xml.readElementText().toInt(); 00528 playlist_position = 1; 00529 continue; 00530 } 00531 //Hide playlists that are system playlists 00532 if (key == "Master" || key == "Movies" || key == "TV Shows" || key == "Music" || 00533 key == "Books" || key == "Purchased") { 00534 isSystemPlaylist = true; 00535 continue; 00536 } 00537 00538 if (key == "Playlist Items") { 00539 //if the playlist is prebuild don't hit the database 00540 if (isSystemPlaylist) continue; 00541 query_insert_to_playlists.bindValue(":id", playlist_id); 00542 query_insert_to_playlists.bindValue(":name", playlistname); 00543 00544 bool success = query_insert_to_playlists.exec(); 00545 if (!success) { 00546 qDebug() << "SQL Error in ITunesTableModel.cpp: line" << __LINE__ 00547 << " " << query_insert_to_playlists.lastError(); 00548 return; 00549 } 00550 //append the playlist to the child model 00551 TreeItem *item = new TreeItem(playlistname, playlistname, this, root); 00552 root->appendChild(item); 00553 00554 } 00555 /* 00556 * When processing playlist entries, playlist name and id have already been processed and persisted 00557 */ 00558 if (key == "Track ID") { 00559 track_reference = -1; 00560 00561 readNextStartElement(xml); 00562 track_reference = xml.readElementText().toInt(); 00563 00564 query_insert_to_playlist_tracks.bindValue(":playlist_id", playlist_id); 00565 query_insert_to_playlist_tracks.bindValue(":track_id", track_reference); 00566 query_insert_to_playlist_tracks.bindValue(":position", playlist_position++); 00567 00568 //Insert tracks if we are not in a pre-build playlist 00569 if (!isSystemPlaylist && !query_insert_to_playlist_tracks.exec()) { 00570 qDebug() << "SQL Error in ITunesFeature.cpp: line" << __LINE__ << " " 00571 << query_insert_to_playlist_tracks.lastError(); 00572 qDebug() << "trackid" << track_reference; 00573 qDebug() << "playlistname; " << playlistname; 00574 qDebug() << "-----------------"; 00575 } 00576 } 00577 } 00578 } 00579 if (xml.isEndElement()) { 00580 if (xml.name() == "array") { 00581 //qDebug() << "exit playlist"; 00582 break; 00583 } 00584 } 00585 } 00586 } 00587 00588 void ITunesFeature::clearTable(QString table_name) { 00589 QSqlQuery query(m_database); 00590 query.prepare("delete from "+table_name); 00591 bool success = query.exec(); 00592 00593 if (!success) { 00594 qDebug() << "Could not delete remove old entries from table " 00595 << table_name << " : " << query.lastError(); 00596 } else { 00597 qDebug() << "iTunes table entries of '" 00598 << table_name <<"' have been cleared."; 00599 } 00600 } 00601 00602 void ITunesFeature::onTrackCollectionLoaded(){ 00603 TreeItem* root = m_future.result(); 00604 if (root) { 00605 m_childModel.setRootItem(root); 00606 00607 // Tell the rhythmbox track source that it should re-build its index. 00608 m_pTrackCollection->getTrackSource("itunes")->buildIndex(); 00609 00610 //m_pITunesTrackModel->select(); 00611 emit(showTrackModel(m_pITunesTrackModel)); 00612 qDebug() << "Itunes library loaded: success"; 00613 } else { 00614 QMessageBox::warning( 00615 NULL, 00616 tr("Error Loading iTunes Library"), 00617 tr("There was an error loading your iTunes library. Some of " 00618 "your iTunes tracks or playlists may not have loaded.")); 00619 } 00620 // calls a slot in the sidebarmodel such that 'isLoading' is removed from the feature title. 00621 m_title = tr("iTunes"); 00622 emit(featureLoadingFinished(this)); 00623 activate(); 00624 } 00625 00626 void ITunesFeature::onLazyChildExpandation(const QModelIndex &index){ 00627 //Nothing to do because the childmodel is not of lazy nature. 00628 }