Mixxx

/home/maxime/Projets/Mixxx/1.10/mixxx/src/library/itunes/itunesfeature.cpp

Go to the documentation of this file.
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 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines