diff --git a/CMakeLists.txt b/CMakeLists.txt index 79ff65e79c4..c64cbc01985 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -749,6 +749,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/export/trackexportworker.cpp src/library/externaltrackcollection.cpp src/library/hiddentablemodel.cpp + src/library/itunes/itunesdao.cpp src/library/itunes/itunesfeature.cpp src/library/itunes/itunesxmlimporter.cpp src/library/library_prefs.cpp diff --git a/src/library/itunes/itunesdao.cpp b/src/library/itunes/itunesdao.cpp new file mode 100644 index 00000000000..2ca236a4ee0 --- /dev/null +++ b/src/library/itunes/itunesdao.cpp @@ -0,0 +1,155 @@ +#include "library/itunes/itunesdao.h" + +#include +#include +#include + +#include "library/itunes/ituneslocalhosttoken.h" +#include "library/itunes/itunespathmapping.h" +#include "library/queryutil.h" + +void ITunesDAO::initialize(const QSqlDatabase& database) { + m_insertTrackQuery = QSqlQuery(database); + m_insertPlaylistQuery = QSqlQuery(database); + m_insertPlaylistTrackQuery = QSqlQuery(database); + m_applyPathMappingQuery = QSqlQuery(database); + + m_insertTrackQuery.prepare( + "INSERT INTO itunes_library (id, artist, title, album, " + "album_artist, genre, grouping, year, duration, " + "location, rating, comment, tracknumber, bpm, bitrate) " + "VALUES (:id, :artist, :title, :album, :album_artist, " + ":genre, :grouping, :year, :duration, :location, " + ":rating, :comment, :tracknumber, :bpm, :bitrate)"); + + m_insertPlaylistQuery.prepare("INSERT INTO itunes_playlists (id, name) VALUES (:id, :name)"); + + m_insertPlaylistTrackQuery.prepare( + "INSERT INTO itunes_playlist_tracks (playlist_id, track_id, " + "position) VALUES (:playlist_id, :track_id, :position)"); + + m_applyPathMappingQuery.prepare( + "UPDATE itunes_library SET location = replace( location, " + ":itunes_path, :mixxx_path )"); +} + +bool ITunesDAO::importTrack(const ITunesTrack& track) { + QSqlQuery& query = m_insertTrackQuery; + + query.bindValue(":id", track.id); + query.bindValue(":artist", track.artist); + query.bindValue(":title", track.title); + query.bindValue(":album", track.album); + query.bindValue(":album_artist", track.albumArtist); + query.bindValue(":genre", track.genre); + query.bindValue(":grouping", track.grouping); + query.bindValue(":year", track.year > 0 ? QVariant(track.year) : QVariant()); + query.bindValue(":duration", track.duration); + query.bindValue(":location", track.location); + query.bindValue(":rating", track.rating); + query.bindValue(":comment", track.comment); + query.bindValue(":tracknumber", + track.trackNumber > 0 ? QVariant(track.trackNumber) : QVariant()); + query.bindValue(":bpm", track.bpm); + query.bindValue(":bitrate", track.bitrate); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + + return true; +} + +bool ITunesDAO::importPlaylist(const ITunesPlaylist& playlist) { + QString uniqueName = uniquifyPlaylistName(playlist.name); + QSqlQuery& query = m_insertPlaylistQuery; + + query.bindValue(":id", playlist.id); + query.bindValue(":name", uniqueName); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + + m_playlistNameById[playlist.id] = uniqueName; + + return true; +} + +bool ITunesDAO::importPlaylistRelation(int parentId, int childId) { + m_playlistIdsByParentId.insert({parentId, childId}); + return true; +} + +bool ITunesDAO::importPlaylistTrack(int playlistId, int trackId, int position) { + QSqlQuery& query = m_insertPlaylistTrackQuery; + + query.bindValue(":playlist_id", playlistId); + query.bindValue(":track_id", trackId); + query.bindValue(":position", position); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + + return true; +} + +bool ITunesDAO::applyPathMapping(const ITunesPathMapping& pathMapping) { + QSqlQuery& query = m_insertPlaylistTrackQuery; + + query.bindValue(":itunes_path", + QString(pathMapping.dbITunesRoot).replace(kiTunesLocalhostToken, "")); + query.bindValue(":mixxx_path", pathMapping.mixxxITunesRoot); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + + return true; +} + +void ITunesDAO::appendPlaylistTree(gsl::not_null item, int playlistId) { + auto childsRange = m_playlistIdsByParentId.equal_range(playlistId); + std::for_each(childsRange.first, + childsRange.second, + [this, &item](auto childEntry) { + int childId = childEntry.second; + QString childName = m_playlistNameById[childId]; + TreeItem* child = item->appendChild(childName); + appendPlaylistTree(child, childId); + }); +} + +QString ITunesDAO::uniquifyPlaylistName(QString name) { + // iTunes permits users to have multiple playlists with the same name, + // our data model (both the database schema and the tree items) however + // require unique names since they identify the playlist via this name. + // We therefore keep track of duplicates and append a suffix + // accordingly. E.g. if the user has three playlists named 'House' in + // their library, the playlists would get named (in this order): + // + // House + // House #2 + // House #3 + // + + // Avoid empty playlist names + if (name.isEmpty()) { + name = QObject::tr("(empty)"); + } + + auto existing = m_playlistDuplicatesByName.find(name); + if (existing != m_playlistDuplicatesByName.end()) { + m_playlistDuplicatesByName[name] += 1; + return QString("%1 #%2").arg(name).arg( + m_playlistDuplicatesByName[name] + 1); + } else { + m_playlistDuplicatesByName[name] = 0; + return name; + } +} diff --git a/src/library/itunes/itunesdao.h b/src/library/itunes/itunesdao.h new file mode 100644 index 00000000000..4f7f0c49c8a --- /dev/null +++ b/src/library/itunes/itunesdao.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "library/dao/dao.h" +#include "library/itunes/itunespathmapping.h" +#include "library/treeitem.h" + +const int kRootITunesPlaylistId = -1; + +struct ITunesTrack { + int id; + QString artist; + QString title; + QString album; + QString albumArtist; + QString genre; + QString grouping; + int year; + int duration; + QString location; + int rating; + QString comment; + int trackNumber; + int bpm; + int bitrate; +}; + +struct ITunesPlaylist { + int id; + QString name; +}; + +/// A wrapper around the iTunes database tables. Keeps track of the +/// playlist tree, deals with duplicate disambiguation and can export +/// the tree afterwards. +class ITunesDAO : public DAO { + public: + ~ITunesDAO() override = default; + + void initialize(const QSqlDatabase& database) override; + + virtual bool importTrack(const ITunesTrack& track); + virtual bool importPlaylist(const ITunesPlaylist& playlist); + virtual bool importPlaylistRelation(int parentId, int childId); + virtual bool importPlaylistTrack(int playlistId, int trackId, int position); + virtual bool applyPathMapping(const ITunesPathMapping& pathMapping); + + virtual void appendPlaylistTree(gsl::not_null item, + int playlistId = kRootITunesPlaylistId); + + private: + QHash m_playlistDuplicatesByName; + QHash m_playlistNameById; + std::multimap m_playlistIdsByParentId; + + // Note that these queries reference the database, which is expected + // to outlive the DAO. + QSqlQuery m_insertTrackQuery; + QSqlQuery m_insertPlaylistQuery; + QSqlQuery m_insertPlaylistTrackQuery; + QSqlQuery m_applyPathMappingQuery; + + QString uniquifyPlaylistName(QString name); +}; diff --git a/src/library/itunes/itunesfeature.cpp b/src/library/itunes/itunesfeature.cpp index d6cf79e8a4d..9ce7cb73300 100644 --- a/src/library/itunes/itunesfeature.cpp +++ b/src/library/itunes/itunesfeature.cpp @@ -15,6 +15,7 @@ #include "library/baseexternaltrackmodel.h" #include "library/basetrackcache.h" #include "library/dao/settingsdao.h" +#include "library/itunes/itunesdao.h" #include "library/itunes/itunesimporter.h" #include "library/itunes/ituneslocalhosttoken.h" #include "library/itunes/itunesxmlimporter.h" @@ -307,14 +308,16 @@ QString ITunesFeature::getiTunesMusicPath() { } std::unique_ptr ITunesFeature::makeImporter() { + std::unique_ptr dao = std::make_unique(); + dao->initialize(m_database); #ifdef __MACOS_ITUNES_LIBRARY__ if (isMacOSImporterUsed()) { qDebug() << "Using ITunesMacOSImporter to read default iTunes library"; - return std::make_unique(this, m_database, m_cancelImport); + return std::make_unique(this, m_cancelImport, std::move(dao)); } #endif qDebug() << "Using ITunesXMLImporter to read iTunes library from " << m_dbfile; - return std::make_unique(this, m_dbfile, m_database, m_cancelImport); + return std::make_unique(this, m_dbfile, m_cancelImport, std::move(dao)); } // This method is executed in a separate thread diff --git a/src/library/itunes/itunesmacosimporter.h b/src/library/itunes/itunesmacosimporter.h index c106d675ef6..a34da207568 100644 --- a/src/library/itunes/itunesmacosimporter.h +++ b/src/library/itunes/itunesmacosimporter.h @@ -3,7 +3,9 @@ #include #include #include +#include +#include "library/itunes/itunesdao.h" #include "library/itunes/itunesimporter.h" #include "library/libraryfeature.h" @@ -12,8 +14,8 @@ class ITunesMacOSImporter : public ITunesImporter { public: ITunesMacOSImporter(LibraryFeature* parentFeature, - const QSqlDatabase& database, - const std::atomic& cancelImport); + const std::atomic& cancelImport, + std::unique_ptr dao); ITunesImport importLibrary() override; @@ -22,6 +24,6 @@ class ITunesMacOSImporter : public ITunesImporter { // The values behind these references are owned by the parent `ITunesFeature`, // thus there is an implicit contract here that this `ITunesMacOSImporter` cannot // outlive the feature (which should not happen anyway, since importers are short-lived). - const QSqlDatabase& m_database; const std::atomic& m_cancelImport; + std::unique_ptr m_dao; }; diff --git a/src/library/itunes/itunesmacosimporter.mm b/src/library/itunes/itunesmacosimporter.mm index 27a853305fa..80d5e40d589 100644 --- a/src/library/itunes/itunesmacosimporter.mm +++ b/src/library/itunes/itunesmacosimporter.mm @@ -1,6 +1,8 @@ #include "library/itunes/itunesmacosimporter.h" #import +#include +#include "library/itunes/itunesdao.h" #include #include @@ -28,21 +30,11 @@ QString qStringFrom(NSString* nsString) { class ImporterImpl { public: - ImporterImpl( - const QSqlDatabase& database, const std::atomic& cancelImport) - : m_database(database), m_cancelImport(cancelImport) { + ImporterImpl(const std::atomic& cancelImport, ITunesDAO& dao) + : m_cancelImport(cancelImport), m_dao(dao) { } void importPlaylists(NSArray* playlists) { - QSqlQuery queryInsertToPlaylists(m_database); - queryInsertToPlaylists.prepare( - "INSERT INTO itunes_playlists (id, name) VALUES (:id, :name)"); - - QSqlQuery queryInsertToPlaylistTracks(m_database); - queryInsertToPlaylistTracks.prepare( - "INSERT INTO itunes_playlist_tracks (playlist_id, track_id, " - "position) VALUES (:playlist_id, :track_id, :position)"); - qDebug() << "Importing playlists via native iTunesLibrary framework"; // We prefer Objective-C-style for-in loops over C++ loops when dealing @@ -55,21 +47,11 @@ void importPlaylists(NSArray* playlists) { break; } - importPlaylist(playlist, - queryInsertToPlaylists, - queryInsertToPlaylistTracks); + importPlaylist(playlist); } } void importMediaItems(NSArray* items) { - QSqlQuery query(m_database); - query.prepare("INSERT INTO itunes_library (id, artist, title, album, " - "album_artist, genre, grouping, year, duration, " - "location, rating, comment, tracknumber, bpm, bitrate) " - "VALUES (:id, :artist, :title, :album, :album_artist, " - ":genre, :grouping, :year, :duration, :location, " - ":rating, :comment, :tracknumber, :bpm, :bitrate)"); - qDebug() << "Importing media items via native iTunesLibrary framework"; for (ITLibMediaItem* item in items) { @@ -77,30 +59,19 @@ void importMediaItems(NSArray* items) { break; } - importMediaItem(item, query); + importMediaItem(item); } } - void appendPlaylistTree(TreeItem& item, int playlistId = -1) { - auto childsRange = m_playlistDbIdsByParentDbId.equal_range(playlistId); - std::for_each(childsRange.first, - childsRange.second, - [this, &item](auto childEntry) { - int childId = childEntry.second; - QString childName = m_playlistNameByDbId[childId]; - TreeItem* child = item.appendChild(childName); - appendPlaylistTree(*child, childId); - }); + void appendPlaylistTree(gsl::not_null item) { + m_dao.appendPlaylistTree(item); } private: - const QSqlDatabase& m_database; const std::atomic& m_cancelImport; QHash m_dbIdByPersistentId; - QHash m_playlistDuplicatesByName; - QHash m_playlistNameByDbId; - std::multimap m_playlistDbIdsByParentDbId; + ITunesDAO& m_dao; int dbIdFromPersistentId(NSNumber* boxedPersistentId) { // Map a persistent ID as used by iTunes to an (incrementing) database @@ -121,35 +92,6 @@ int dbIdFromPersistentId(NSNumber* boxedPersistentId) { } } - QString uniquifyPlaylistName(QString name) { - // iTunes permits users to have multiple playlists with the same name, - // our data model (both the database schema and the tree items) however - // require unique names since they identify the playlist via this name. - // We therefore keep track of duplicates and append a suffix - // accordingly. E.g. if the user has three playlists named 'House' in - // their library, the playlists would get named (in this order): - // - // House - // House #2 - // House #3 - // - - // Avoid empty playlist names - if (name.isEmpty()) { - name = "(empty)"; - } - - auto existing = m_playlistDuplicatesByName.find(name); - if (existing != m_playlistDuplicatesByName.end()) { - m_playlistDuplicatesByName[name] += 1; - return QString("%1 #%2").arg(name).arg( - m_playlistDuplicatesByName[name] + 1); - } else { - m_playlistDuplicatesByName[name] = 0; - return name; - } - } - bool isPlaylistShown(ITLibPlaylist* playlist) { // Filter out the primary playlist (since we already show the library // under the main iTunes node) @@ -186,44 +128,36 @@ bool isPlaylistShown(ITLibPlaylist* playlist) { return true; } - void importPlaylist(ITLibPlaylist* playlist, - QSqlQuery& queryInsertToPlaylists, - QSqlQuery& queryInsertToPlaylistTracks) { - if (!isPlaylistShown(playlist)) { + void importPlaylist(ITLibPlaylist* itPlaylist) { + if (!isPlaylistShown(itPlaylist)) { return; } - int playlistId = dbIdFromPersistentId(playlist.persistentID); - int parentId = playlist.parentID - ? dbIdFromPersistentId(playlist.parentID) - : -1; - - QString playlistName = uniquifyPlaylistName(qStringFrom(playlist.name)); - - queryInsertToPlaylists.bindValue(":id", playlistId); - queryInsertToPlaylists.bindValue(":name", playlistName); + int playlistId = dbIdFromPersistentId(itPlaylist.persistentID); + int parentId = itPlaylist.parentID + ? dbIdFromPersistentId(itPlaylist.parentID) + : kRootITunesPlaylistId; - if (!queryInsertToPlaylists.exec()) { - LOG_FAILED_QUERY(queryInsertToPlaylists); + ITunesPlaylist playlist = { + .id = playlistId, + .name = qStringFrom(itPlaylist.name), + }; + if (!m_dao.importPlaylist(playlist)) { return; } - m_playlistNameByDbId.insert(playlistId, playlistName); - m_playlistDbIdsByParentDbId.insert({parentId, playlistId}); + if (!m_dao.importPlaylistRelation(parentId, playlistId)) { + return; + } int i = 0; - for (ITLibMediaItem* item in playlist.items) { + for (ITLibMediaItem* item in itPlaylist.items) { if (m_cancelImport.load()) { return; } - queryInsertToPlaylistTracks.bindValue(":playlist_id", playlistId); - queryInsertToPlaylistTracks.bindValue( - ":track_id", dbIdFromPersistentId(item.persistentID)); - queryInsertToPlaylistTracks.bindValue(":position", i); - - if (!queryInsertToPlaylistTracks.exec()) { - LOG_FAILED_QUERY(queryInsertToPlaylistTracks); + int trackId = dbIdFromPersistentId(item.persistentID); + if (!m_dao.importPlaylistTrack(playlistId, trackId, i)) { return; } @@ -231,31 +165,32 @@ void importPlaylist(ITLibPlaylist* playlist, } } - void importMediaItem(ITLibMediaItem* item, QSqlQuery& query) { + void importMediaItem(ITLibMediaItem* item) { // Skip DRM-protected and non-downloaded tracks bool isRemote = item.locationType == ITLibMediaItemLocationTypeRemote; if (item.drmProtected || isRemote) { return; } - query.bindValue(":id", dbIdFromPersistentId(item.persistentID)); - query.bindValue(":artist", qStringFrom(item.artist.name)); - query.bindValue(":title", qStringFrom(item.title)); - query.bindValue(":album", qStringFrom(item.album.title)); - query.bindValue(":album_artist", qStringFrom(item.album.albumArtist)); - query.bindValue(":genre", qStringFrom(item.genre)); - query.bindValue(":grouping", qStringFrom(item.grouping)); - query.bindValue(":year", static_cast(item.year)); - query.bindValue(":duration", static_cast(item.totalTime / 1000)); - query.bindValue(":location", qStringFrom([item.location path])); - query.bindValue(":rating", static_cast(item.rating / 20)); - query.bindValue(":comment", qStringFrom(item.comments)); - query.bindValue(":tracknumber", static_cast(item.trackNumber)); - query.bindValue(":bpm", static_cast(item.beatsPerMinute)); - query.bindValue(":bitrate", static_cast(item.bitrate)); - - if (!query.exec()) { - LOG_FAILED_QUERY(query); + ITunesTrack track = { + .id = dbIdFromPersistentId(item.persistentID), + .artist = qStringFrom(item.artist.name), + .title = qStringFrom(item.title), + .album = qStringFrom(item.album.title), + .albumArtist = qStringFrom(item.album.albumArtist), + .genre = qStringFrom(item.genre), + .grouping = qStringFrom(item.grouping), + .year = static_cast(item.year), + .duration = static_cast(item.totalTime / 1000), + .location = qStringFrom([item.location path]), + .rating = static_cast(item.rating / 20), + .comment = qStringFrom(item.comments), + .trackNumber = static_cast(item.trackNumber), + .bpm = static_cast(item.beatsPerMinute), + .bitrate = static_cast(item.bitrate), + }; + + if (!m_dao.importTrack(track)) { return; } } @@ -264,11 +199,11 @@ void importMediaItem(ITLibMediaItem* item, QSqlQuery& query) { } // anonymous namespace ITunesMacOSImporter::ITunesMacOSImporter(LibraryFeature* parentFeature, - const QSqlDatabase& database, - const std::atomic& cancelImport) + const std::atomic& cancelImport, + std::unique_ptr dao) : m_parentFeature(parentFeature), - m_database(database), - m_cancelImport(cancelImport) { + m_cancelImport(cancelImport), + m_dao(std::move(dao)) { } ITunesImport ITunesMacOSImporter::importLibrary() { @@ -280,11 +215,11 @@ void importMediaItem(ITLibMediaItem* item, QSqlQuery& query) { if (library) { std::unique_ptr rootItem = TreeItem::newRoot(m_parentFeature); - ImporterImpl impl(m_database, m_cancelImport); + ImporterImpl impl(m_cancelImport, *m_dao); impl.importPlaylists(library.allPlaylists); impl.importMediaItems(library.allMediaItems); - impl.appendPlaylistTree(*rootItem); + impl.appendPlaylistTree(rootItem.get()); iTunesImport.playlistRoot = std::move(rootItem); } else if (error) { diff --git a/src/library/itunes/itunesxmlimporter.cpp b/src/library/itunes/itunesxmlimporter.cpp index cd7fd6d3059..d5f8eecbd01 100644 --- a/src/library/itunes/itunesxmlimporter.cpp +++ b/src/library/itunes/itunesxmlimporter.cpp @@ -6,9 +6,9 @@ #include #include +#include "library/itunes/itunesdao.h" #include "library/itunes/itunesimporter.h" #include "library/itunes/ituneslocalhosttoken.h" -#include "library/itunes/itunespathmapping.h" #include "library/queryutil.h" #include "library/treeitemmodel.h" #include "util/lcs.h" @@ -45,14 +45,14 @@ const QString kRemote = "Remote"; ITunesXMLImporter::ITunesXMLImporter(LibraryFeature* parentFeature, const QString& xmlFilePath, - const QSqlDatabase& database, - const std::atomic& cancelImport) + const std::atomic& cancelImport, + std::unique_ptr dao) : m_parentFeature(parentFeature), m_xmlFilePath(xmlFilePath), m_xmlFile(xmlFilePath), m_xml(&m_xmlFile), - m_database(database), - m_cancelImport(cancelImport) { + m_cancelImport(cancelImport), + m_dao(std::move(dao)) { // By default set m_mixxxItunesRoot and m_dbItunesRoot to strip out // file://localhost/ from the URL. When we load the user's iTunes XML // configuration we may replace this with something based on the detected @@ -87,7 +87,12 @@ ITunesImport ITunesXMLImporter::importLibrary() { } } else if (key == "Tracks") { parseTracks(); - iTunesImport.playlistRoot = parsePlaylists(); + parsePlaylists(); + + std::unique_ptr pRootItem = TreeItem::newRoot(m_parentFeature); + m_dao->appendPlaylistTree(pRootItem.get()); + + iTunesImport.playlistRoot = std::move(pRootItem); isTracksParsed = true; } } @@ -108,18 +113,7 @@ ITunesImport ITunesXMLImporter::importLibrary() { << m_pathMapping.mixxxITunesRoot; // In some iTunes files "Music Folder" XML node is located at the end of // file. So, we need to - QSqlQuery query(m_database); - query.prepare( - "UPDATE itunes_library SET location = replace( location, " - ":itunes_path, :mixxx_path )"); - query.bindValue(":itunes_path", - m_pathMapping.dbITunesRoot.replace(kiTunesLocalhostToken, "")); - query.bindValue(":mixxx_path", m_pathMapping.mixxxITunesRoot); - bool success = query.exec(); - - if (!success) { - LOG_FAILED_QUERY(query); - } + m_dao->applyPathMapping(m_pathMapping); } return iTunesImport; @@ -211,18 +205,6 @@ void ITunesXMLImporter::guessMusicLibraryMountpoint() { void ITunesXMLImporter::parseTracks() { bool inContainerDictionary = false; bool inTrackDictionary = false; - QSqlQuery query(m_database); - query.prepare( - "INSERT INTO itunes_library (id, artist, title, album, " - "album_artist, year, genre, grouping, comment, tracknumber," - "bpm, bitrate," - "duration, location," - "rating ) " - "VALUES (:id, :artist, :title, :album, :album_artist, :year, " - ":genre, :grouping, :comment, :tracknumber," - ":bpm, :bitrate," - ":duration, :location," - ":rating )"); qDebug() << "Parse iTunes music collection"; @@ -239,7 +221,7 @@ void ITunesXMLImporter::parseTracks() { // We are in a tag that holds track information inTrackDictionary = true; // Parse track here - parseTrack(query); + parseTrack(); } } } @@ -256,7 +238,7 @@ void ITunesXMLImporter::parseTracks() { } } -void ITunesXMLImporter::parseTrack(QSqlQuery& query) { +void ITunesXMLImporter::parseTrack() { // qDebug() << "----------------TRACK-----------------"; int id = -1; QString title; @@ -379,50 +361,37 @@ void ITunesXMLImporter::parseTrack(QSqlQuery& query) { // If we reach the end of // Save parsed track to database - query.bindValue(":id", id); - query.bindValue(":artist", artist); - query.bindValue(":title", title); - query.bindValue(":album", album); - query.bindValue(":album_artist", albumArtist); - query.bindValue(":genre", genre); - query.bindValue(":grouping", grouping); - query.bindValue(":year", year); - query.bindValue(":duration", playtime); - query.bindValue(":location", location); - query.bindValue(":rating", rating); - query.bindValue(":comment", comment); - query.bindValue(":tracknumber", tracknumber); - query.bindValue(":bpm", bpm); - query.bindValue(":bitrate", bitrate); - - bool success = query.exec(); - - if (!success) { - LOG_FAILED_QUERY(query); + ITunesTrack track = { + .id = id, + .artist = artist, + .title = title, + .album = album, + .albumArtist = albumArtist, + .genre = genre, + .grouping = grouping, + .year = year.toInt(), + .duration = playtime, + .location = location, + .rating = rating, + .comment = comment, + .trackNumber = tracknumber.toInt(), + .bpm = bpm, + .bitrate = bitrate, + }; + + if (!m_dao->importTrack(track)) { return; } } -std::unique_ptr ITunesXMLImporter::parsePlaylists() { +void ITunesXMLImporter::parsePlaylists() { qDebug() << "Parse iTunes playlists"; - std::unique_ptr pRootItem = TreeItem::newRoot(m_parentFeature); - QSqlQuery queryInsertToPlaylists(m_database); - queryInsertToPlaylists.prepare( - "INSERT INTO itunes_playlists (id, name) " - "VALUES (:id, :name)"); - - QSqlQuery queryInsertToPlaylistTracks(m_database); - queryInsertToPlaylistTracks.prepare( - "INSERT INTO itunes_playlist_tracks (playlist_id, track_id, position) " - "VALUES (:playlist_id, :track_id, :position)"); while (!m_xml.atEnd() && !m_cancelImport.load()) { m_xml.readNext(); // We process and iterate the tags holding playlist summary information here if (m_xml.isStartElement() && m_xml.name() == kDict) { - parsePlaylist(queryInsertToPlaylists, - queryInsertToPlaylistTracks, - *pRootItem); + parsePlaylist(); continue; } if (m_xml.isEndElement()) { @@ -431,7 +400,6 @@ std::unique_ptr ITunesXMLImporter::parsePlaylists() { } } } - return pRootItem; } bool ITunesXMLImporter::readNextStartElement() { @@ -445,15 +413,15 @@ bool ITunesXMLImporter::readNextStartElement() { return false; } -void ITunesXMLImporter::parsePlaylist(QSqlQuery& queryInsertToPlaylists, - QSqlQuery& queryInsertToPlaylistTracks, - TreeItem& root) { +void ITunesXMLImporter::parsePlaylist() { // qDebug() << "Parse Playlist"; - QString playlistname; - int playlist_id = -1; - int playlist_position = -1; - int track_reference = -1; + QString name; + int id = -1; + QString persistentId; + QString parentPersistentId; + int trackPosition = -1; + int trackReference = -1; // indicates that we haven't found the < bool isSystemPlaylist = false; bool isPlaylistItemsStarted = false; @@ -471,16 +439,25 @@ void ITunesXMLImporter::parsePlaylist(QSqlQuery& queryInsertToPlaylists, // Afterwars the playlist entries occur if (key == "Name") { readNextStartElement(); - playlistname = m_xml.readElementText(); + name = m_xml.readElementText(); continue; } // When parsing the ID, the playlistname has already been found if (key == "Playlist ID") { readNextStartElement(); - playlist_id = m_xml.readElementText().toInt(); - playlist_position = 1; + id = m_xml.readElementText().toInt(); + trackPosition = 1; continue; } + if (key == "Playlist Persistent ID") { + readNextStartElement(); + persistentId = m_xml.readElementText(); + continue; + } + if (key == "Parent Persistent ID") { + readNextStartElement(); + parentPersistentId = m_xml.readElementText(); + } // Hide playlists that are system playlists if (key == "Master" || key == "Movies" || key == "TV Shows" || key == "Music" || key == "Books" || key == "Purchased") { @@ -491,54 +468,31 @@ void ITunesXMLImporter::parsePlaylist(QSqlQuery& queryInsertToPlaylists, if (key == "Playlist Items") { isPlaylistItemsStarted = true; - // if the playlist is prebuild don't hit the database + // if the playlist is prebuilt don't hit the database if (isSystemPlaylist) { continue; } - queryInsertToPlaylists.bindValue(":id", playlist_id); - queryInsertToPlaylists.bindValue(":name", playlistname); - - bool success = queryInsertToPlaylists.exec(); - if (!success) { - if (queryInsertToPlaylists.lastError() - .nativeErrorCode() == - QString::number(SQLITE_CONSTRAINT)) { - // We assume a duplicate Playlist name - playlistname += QString(" #%1").arg(playlist_id); - queryInsertToPlaylists.bindValue(":name", playlistname); - - bool success = queryInsertToPlaylists.exec(); - if (!success) { - // unexpected error - LOG_FAILED_QUERY(queryInsertToPlaylists); - break; - } - } else { - // unexpected error - LOG_FAILED_QUERY(queryInsertToPlaylists); - return; - } + + ITunesPlaylist playlist = { + .id = id, + .name = name, + }; + if (!m_dao->importPlaylist(playlist)) { + // unexpected error + break; } - // append the playlist to the child model - root.appendChild(playlistname); } // When processing playlist entries, playlist name and id have // already been processed and persisted if (key == kTrackId) { readNextStartElement(); - track_reference = m_xml.readElementText().toInt(); - - queryInsertToPlaylistTracks.bindValue(":playlist_id", playlist_id); - queryInsertToPlaylistTracks.bindValue(":track_id", track_reference); - queryInsertToPlaylistTracks.bindValue(":position", playlist_position++); - - // Insert tracks if we are not in a pre-build playlist - if (!isSystemPlaylist && !queryInsertToPlaylistTracks.exec()) { - qDebug() << "SQL Error in ITunesXMLImporter.cpp: line" << __LINE__ << " " - << queryInsertToPlaylistTracks.lastError(); - qDebug() << "trackid" << track_reference; - qDebug() << "playlistname; " << playlistname; - qDebug() << "-----------------"; + trackReference = m_xml.readElementText().toInt(); + + // Insert tracks if we are not in a pre-built playlist + if (!isSystemPlaylist) { + m_dao->importPlaylistTrack(id, + trackReference, + trackPosition++); } } } @@ -554,4 +508,18 @@ void ITunesXMLImporter::parsePlaylist(QSqlQuery& queryInsertToPlaylists, } } } + + if (!isSystemPlaylist) { + m_playlistIdByPersistentId[persistentId] = id; + + int parentId = kRootITunesPlaylistId; + if (!parentPersistentId.isNull()) { + auto found = m_playlistIdByPersistentId.find(parentPersistentId); + if (found != m_playlistIdByPersistentId.end()) { + parentId = found.value(); + } + } + + m_dao->importPlaylistRelation(parentId, id); + } } diff --git a/src/library/itunes/itunesxmlimporter.h b/src/library/itunes/itunesxmlimporter.h index 1bd2c377c35..beba1e94a96 100644 --- a/src/library/itunes/itunesxmlimporter.h +++ b/src/library/itunes/itunesxmlimporter.h @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include #include +#include "library/itunes/itunesdao.h" #include "library/itunes/itunesimporter.h" #include "library/itunes/itunespathmapping.h" #include "library/libraryfeature.h" @@ -14,8 +16,8 @@ class ITunesXMLImporter : public ITunesImporter { public: ITunesXMLImporter(LibraryFeature* parentFeature, const QString& xmlFilePath, - const QSqlDatabase& database, - const std::atomic& cancelImport); + const std::atomic& cancelImport, + std::unique_ptr dao); ITunesImport importLibrary() override; @@ -24,20 +26,19 @@ class ITunesXMLImporter : public ITunesImporter { const QString m_xmlFilePath; QFile m_xmlFile; QXmlStreamReader m_xml; - // The values behind these references are owned by the parent `ITunesFeature`, + // The values behind the references are owned by the parent `ITunesFeature` // thus there is an implicit contract here that this `ITunesXMLImporter` cannot // outlive the feature (which should not happen anyway, since importers are short-lived). - const QSqlDatabase& m_database; const std::atomic& m_cancelImport; + std::unique_ptr m_dao; ITunesPathMapping m_pathMapping; + QHash m_playlistIdByPersistentId; void parseTracks(); void guessMusicLibraryMountpoint(); - void parseTrack(QSqlQuery& query); - std::unique_ptr parsePlaylists(); + void parseTrack(); + void parsePlaylists(); bool readNextStartElement(); - void parsePlaylist(QSqlQuery& queryInsertToPlaylists, - QSqlQuery& queryInsertToPlaylistTracks, - TreeItem& root); + void parsePlaylist(); };