diff options
author | Dave Blake <oak99sky@yahoo.co.uk> | 2019-04-06 13:42:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-06 13:42:48 +0100 |
commit | 4eb34ebe68452821b97befbcc4a8fcb663924f5b (patch) | |
tree | 70738333ad8476befa5c348d0022109837fb55a4 | |
parent | 392b7ed4be1e5049755028d69b542814a9592d8f (diff) | |
parent | f7caf87377eead02e259e55ef78655b86b744817 (diff) |
Merge pull request #15864 from DaveTBlake/ExportSongs
Music lib export/import song playback history fix import use separate thread from GUI
-rw-r--r-- | addons/resource.language.en_gb/resources/strings.po | 40 | ||||
-rw-r--r-- | xbmc/music/MusicDatabase.cpp | 426 | ||||
-rw-r--r-- | xbmc/music/MusicDatabase.h | 6 | ||||
-rw-r--r-- | xbmc/music/MusicLibraryQueue.cpp | 40 | ||||
-rw-r--r-- | xbmc/music/MusicLibraryQueue.h | 7 | ||||
-rw-r--r-- | xbmc/music/jobs/CMakeLists.txt | 2 | ||||
-rw-r--r-- | xbmc/music/jobs/MusicLibraryImportJob.cpp | 41 | ||||
-rw-r--r-- | xbmc/music/jobs/MusicLibraryImportJob.h | 42 | ||||
-rw-r--r-- | xbmc/settings/LibExportSettings.cpp | 19 | ||||
-rw-r--r-- | xbmc/settings/LibExportSettings.h | 4 | ||||
-rw-r--r-- | xbmc/settings/MediaSettings.cpp | 7 | ||||
-rw-r--r-- | xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp | 44 |
12 files changed, 621 insertions, 57 deletions
diff --git a/addons/resource.language.en_gb/resources/strings.po b/addons/resource.language.en_gb/resources/strings.po index 15c74ef484..4e239efb2e 100644 --- a/addons/resource.language.en_gb/resources/strings.po +++ b/addons/resource.language.en_gb/resources/strings.po @@ -564,6 +564,7 @@ msgstr "" #: xbmc/dialogs/GUIDialogSmartPlaylistRule.cpp #: xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp #: xbmc/media/MediaTypes.cpp +#: xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp msgctxt "#134" msgid "Songs" msgstr "" @@ -2986,12 +2987,12 @@ msgctxt "#648" msgid "Import video library" msgstr "" -#: xbmc/music/MusicDatabase.cpp +#. unused? msgctxt "#649" msgid "Importing" msgstr "" -#: xbmc/music/MusicDatabase.cpp +#. unused? msgctxt "#650" msgid "Exporting" msgstr "" @@ -12393,6 +12394,7 @@ msgctxt "#20196" msgid "Export music library" msgstr "" +#: xbmc/music/MusicDatabase.cpp #: system/settings/settings.xml msgctxt "#20197" msgid "Import music library" @@ -21392,7 +21394,39 @@ msgctxt "#38339" msgid "How to apply information provider settings" msgstr "" -#empty strings from id 38340 to 38999 +#empty strings from id 38340 to 38349 + +#. Progress statement when importing music library is processing song playback history data +#: xbmc/music/MusicDatabase.cpp +msgctxt "#38350" +msgid "Importing song playback history" +msgstr "" + +#. Progress statement when importing music library - data matching phase +#: xbmc/music/MusicDatabase.cpp +msgctxt "#38351" +msgid "Matching data" +msgstr "" + +#. Progress statement when importing music library - song history being updated +#: xbmc/music/MusicDatabase.cpp +msgctxt "#38352" +msgid "Updating songs" +msgstr "" + +#. Notification of import success "Importing song history - <number of songs matched> updated out of <total songs in xml> imported songs" +#: xbmc/music/MusicDatabase.cpp +msgctxt "#38353" +msgid "Importing song history - {0:d} updated out of {0:d} imported songs" +msgstr "" + +#. Message when reading the user specified XML for import to library fails +#: xbmc/music/MusicDatabase.cpp +msgctxt "#38354" +msgid "Unable to read xml file" +msgstr "" + +#empty strings from id 38355 to 38999 #: system/settings/settings.xml msgctxt "#39000" diff --git a/xbmc/music/MusicDatabase.cpp b/xbmc/music/MusicDatabase.cpp index 4367a08f14..1d72b8dd2e 100644 --- a/xbmc/music/MusicDatabase.cpp +++ b/xbmc/music/MusicDatabase.cpp @@ -20,6 +20,8 @@ #include "dialogs/GUIDialogKaiToast.h" #include "dialogs/GUIDialogProgress.h" #include "dialogs/GUIDialogSelect.h" +#include "events/EventLog.h" +#include "events/NotificationEvent.h" #include "FileItem.h" #include "filesystem/Directory.h" #include "filesystem/DirectoryCache.h" @@ -52,6 +54,7 @@ #include "utils/FileUtils.h" #include "utils/LegacyPathTranslation.h" #include "utils/log.h" +#include "utils/MathUtils.h" #include "utils/Random.h" #include "utils/StringUtils.h" #include "utils/URIUtils.h" @@ -9479,12 +9482,13 @@ std::string CMusicDatabase::GetItemById(const std::string &itemType, int id) return ""; } -void CMusicDatabase::ExportToXML(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog /*= NULL*/) +void CMusicDatabase::ExportToXML(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog /*= nullptr*/) { if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) && !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) && !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) && - !settings.IsItemExported(ELIBEXPORT_ALBUMS)) + !settings.IsItemExported(ELIBEXPORT_ALBUMS) && + !settings.IsItemExported(ELIBEXPORT_SONGS)) return; // Exporting albums either art or NFO (or both) selected @@ -9684,6 +9688,13 @@ void CMusicDatabase::ExportToXML(const CLibExportSettings& settings, CGUIDialog } } + // Export song playback history to single file only + if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS)) + { + if (!ExportSongHistory(pMain, progressDialog)) + return; + } + if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty()) { // Find artists to export @@ -9828,45 +9839,106 @@ void CMusicDatabase::ExportToXML(const CLibExportSettings& settings, CGUIDialog if (progressDialog) progressDialog->Close(); - if (iFailCount > 0) + if (iFailCount > 0 && progressDialog) HELPERS::ShowOKDialogLines(CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011).c_str(), iFailCount)}); } -void CMusicDatabase::ImportFromXML(const std::string &xmlFile) +bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog) { - CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + try + { + // Export songs with some playback history + std::string strSQL = "SELECT idSong, song.idAlbum, " + "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, " + "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, " + "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating " + "FROM song JOIN album on album.idAlbum = song.idAlbum " + "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0"; + + CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL.c_str()); + m_pDS->query(strSQL); + + int total = m_pDS->num_rows(); + int current = 0; + while (!m_pDS->eof()) + { + TiXmlElement songElement("song"); + TiXmlNode* song = pNode->InsertEndChild(songElement); + + XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt()); + XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString()); + XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString()); + XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt()); + XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString()); + XMLUtils::SetString(song, "musicbrainztrackid", m_pDS->fv("strMusicBrainzTrackID").get_asString()); + XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt()); + XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString()); + XMLUtils::SetString(song, "musicbrainzalbumid", m_pDS->fv("strMusicBrainzAlbumID").get_asString()); + XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString()); + XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt()); + XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString()); + auto* rating = XMLUtils::SetString(song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat())); + if (rating) + rating->ToElement()->SetAttribute("max", 10); + XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt()); + auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt()); + if (userrating) + userrating->ToElement()->SetAttribute("max", 10); + + if ((current % 100) == 0 && progressDialog) + { + progressDialog->SetLine(1, CVariant{ m_pDS->fv("strAlbum").get_asString() }); + progressDialog->SetPercentage(current * 100 / total); + if (progressDialog->IsCanceled()) + { + m_pDS->close(); + return false; + } + } + current++; + + m_pDS->next(); + } + m_pDS->close(); + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "{0} failed", __FUNCTION__); + } + return false; +} + +void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog) +{ try { if (NULL == m_pDB.get()) return; if (NULL == m_pDS.get()) return; CXBMCTinyXML xmlDoc; - if (!xmlDoc.LoadFile(xmlFile)) + if (!xmlDoc.LoadFile(xmlFile) && progressDialog) + { + HELPERS::ShowOKDialogLines(CVariant{ 20197 }, CVariant{ 38354 }); //"Unable to read xml file" return; + } TiXmlElement *root = xmlDoc.RootElement(); if (!root) return; - if (progress) - { - progress->SetHeading(CVariant{20197}); - progress->SetLine(0, CVariant{649}); - progress->SetLine(1, CVariant{330}); - progress->SetLine(2, CVariant{""}); - progress->SetPercentage(0); - progress->Open(); - progress->ShowProgressBar(true); - } - TiXmlElement *entry = root->FirstChildElement(); int current = 0; int total = 0; - // first count the number of items... + int songtotal = 0; + // Count the number of artists, albums and songs while (entry) { if (strnicmp(entry->Value(), "artist", 6)==0 || strnicmp(entry->Value(), "album", 5)==0) total++; + else if (strnicmp(entry->Value(), "song", 4) == 0) + songtotal++; + entry = entry->NextSiblingElement(); } @@ -9914,14 +9986,13 @@ void CMusicDatabase::ImportFromXML(const std::string &xmlFile) current++; } entry = entry ->NextSiblingElement(); - if (progress && total) + if (progressDialog && total) { - progress->SetPercentage(current * 100 / total); - progress->SetLine(2, CVariant{std::move(strTitle)}); - progress->Progress(); - if (progress->IsCanceled()) + progressDialog->SetPercentage(current * 100 / total); + progressDialog->SetLine(2, CVariant{std::move(strTitle)}); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) { - progress->Close(); RollbackTransaction(); return; } @@ -9929,6 +10000,11 @@ void CMusicDatabase::ImportFromXML(const std::string &xmlFile) } CommitTransaction(); + // Import song playback history <song> entries found + if (songtotal > 0) + if (!ImportSongHistory(xmlFile, songtotal, progressDialog)) + return; + CGUIComponent* gui = CServiceBroker::GetGUI(); if (gui) gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools(); @@ -9938,8 +10014,306 @@ void CMusicDatabase::ImportFromXML(const std::string &xmlFile) CLog::Log(LOGERROR, "%s failed", __FUNCTION__); RollbackTransaction(); } - if (progress) - progress->Close(); + if (progressDialog) + progressDialog->Close(); +} + +bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile, const int total, CGUIDialogProgress* progressDialog) +{ + bool bHistSongExists = false; + try + { + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(xmlFile)) + return false; + + TiXmlElement* root = xmlDoc.RootElement(); + if (!root) + return false; + + TiXmlElement* entry = root->FirstChildElement(); + int current = 0; + + if (progressDialog) + { + progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history" + progressDialog->SetLine(2, CVariant{ "" }); + } + + // As can be many songs do in db, not song at a time which would be slow + // Convert xml entries into a SQL bulk insert statement + std::string strSQL; + entry = root->FirstChildElement(); + while (entry) + { + std::string strArtistDisp; + std::string strTitle; + int iTrack; + std::string strFilename; + std::string strMusicBrainzTrackID; + std::string strAlbum; + std::string strMusicBrainzAlbumID; + std::string strAlbumArtistDisp; + int iTimesplayed; + std::string lastplayed; + int iUserrating = 0; + float fRating = 0.0; + int iVotes; + std::string strSQLSong; + if (strnicmp(entry->Value(), "song", 4) == 0) + { + XMLUtils::GetString(entry, "artistdesc", strArtistDisp); + XMLUtils::GetString(entry, "title", strTitle); + XMLUtils::GetInt(entry, "track", iTrack); + XMLUtils::GetString(entry, "filename", strFilename); + XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID); + XMLUtils::GetString(entry, "albumtitle", strAlbum); + XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID); + XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp); + XMLUtils::GetInt(entry, "timesplayed", iTimesplayed); + XMLUtils::GetString(entry, "lastplayed", lastplayed); + const TiXmlElement* rElement = entry->FirstChildElement("rating"); + if (rElement) + { + float rating = 0; + float max_rating = 10; + XMLUtils::GetFloat(entry, "rating", rating); + if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1) + rating *= (10.f / max_rating); // Normalise the value to between 0 and 10 + if (rating > 10.f) + rating = 10.f; + fRating = rating; + } + XMLUtils::GetInt(entry, "votes", iVotes); + const TiXmlElement* userrating = entry->FirstChildElement("userrating"); + if (userrating) + { + float rating = 0; + float max_rating = 10; + XMLUtils::GetFloat(entry, "userrating", rating); + if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1) + rating *= (10.f / max_rating); // Normalise the value to between 0 and 10 + if (rating > 10.f) + rating = 10.f; + iUserrating = MathUtils::round_int(rating); + } + + strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack); + strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(), strFilename.c_str()); + if (strMusicBrainzTrackID.empty()) + strSQLSong += PrepareSQL("NULL, "); + else + strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str()); + strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str()); + if (strMusicBrainzAlbumID.empty()) + strSQLSong += PrepareSQL("NULL, "); + else + strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str()); + strSQLSong += PrepareSQL("%d, ", iTimesplayed); + if (lastplayed.empty()) + strSQLSong += PrepareSQL("NULL, "); + else + strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str()); + strSQLSong += PrepareSQL("%.1f, %d, %d, -1, -1)", fRating, iVotes, iUserrating); + + if (current > 0) + strSQLSong = ", " + strSQLSong; + strSQL += strSQLSong; + current++; + } + + entry = entry->NextSiblingElement(); + + if ((current % 100) == 0 && progressDialog) + { + progressDialog->SetPercentage(current * 100 / total); + progressDialog->SetLine(3, CVariant{ std::move(strTitle) }); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + return false; + } + } + + CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__, total); + /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of + song table using correlated subqueries to a temp table. An updatable join + to temp table would work in MySQL but SQLite not support updatable joins. + */ + m_pDS->exec("CREATE TABLE HistSong (" + "idSongSrc INTEGER primary key, " + "strAlbum varchar(256), " + "strMusicBrainzAlbumID text, " + "strAlbumArtistDisp text, " + "strArtistDisp text, strTitle varchar(512), " + "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, " + "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, " + "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, " + "userrating INTEGER NOT NULL DEFAULT 0, " + "idAlbum INTEGER, idSong INTEGER)"); + bHistSongExists = true; + + strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, " + "strFileName, strMusicBrainzTrackID, " + "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, " + " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " + strSQL; + m_pDS->exec(strSQL); + + if (progressDialog) + { + progressDialog->SetLine(2, CVariant{38351}); //"Matching data" + progressDialog->SetLine(3, CVariant{ "" }); + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + + BeginTransaction(); + // Match albums first on mbid then artist string and album title, setting idAlbum + strSQL = "UPDATE HistSong " + "SET idAlbum = (SELECT album.idAlbum FROM album " + "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) " + "WHERE EXISTS(SELECT 1 FROM album " + "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0"; + m_pDS->exec(strSQL); + + strSQL = "UPDATE HistSong " + "SET idAlbum = (SELECT album.idAlbum FROM album " + "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp AND HistSong.strAlbum = album.strAlbum) " + "WHERE EXISTS(SELECT 1 FROM album " + "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp AND HistSong.strAlbum = album.strAlbum)" + "AND idAlbum < 0"; + m_pDS->exec(strSQL); + if (progressDialog) + { + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + RollbackTransaction(); + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + + // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong + strSQL = "UPDATE HistSong " + "SET idSong = (SELECT idsong FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND " + "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) " + "WHERE EXISTS(SELECT 1 FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND " + "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0"; + m_pDS->exec(strSQL); + + strSQL = "UPDATE HistSong " + "SET idSong = (SELECT idsong FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) " + "WHERE EXISTS(SELECT 1 FROM song " + "WHERE HistSong.idAlbum = song.idAlbum AND " + "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0"; + m_pDS->exec(strSQL); + CommitTransaction(); + if (progressDialog) + { + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + + // Create an index to speed up the updates + m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)"); + + // Log how many songs matched + int unmatched = static_cast<int>(strtol(GetSingleValue("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS).c_str(), nullptr, 10)); + CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__, total - unmatched, total); + + if (progressDialog) + { + progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history" + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + m_pDS->exec("DROP TABLE HistSong"); // Drops index too + return false; + } + } + + /* Update song table using the song ids we have matched. + Use correlated subqueries as SQLite does not support updatable joins. + MySQL requires HistSong table not to be defined temporary for this. + */ + BeginTransaction(); + // Times played and last played date(when count is greater) + strSQL = "UPDATE song SET iTimesPlayed = " + "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), " + "lastplayed = " + "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) " + "WHERE EXISTS(SELECT 1 FROM HistSong WHERE " + "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)"; + m_pDS->exec(strSQL); + + // User rating + strSQL = "UPDATE song SET userrating = " + "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) " + "WHERE EXISTS(SELECT 1 FROM HistSong WHERE " + "HistSong.idSong = song.idSong AND HistSong.userrating > 0)"; + m_pDS->exec(strSQL); + + // Rating and votes + strSQL = "UPDATE song SET rating = " + "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), " + "votes = " + "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) " + "WHERE EXISTS(SELECT 1 FROM HistSong WHERE " + "HistSong.idSong = song.idSong AND HistSong.rating > 0)"; + m_pDS->exec(strSQL); + + if (progressDialog) + { + progressDialog->Progress(); + if (progressDialog->IsCanceled()) + { + RollbackTransaction(); + m_pDS->exec("DROP TABLE HistSong"); + return false; + } + } + CommitTransaction(); + + // Tidy up temp table (index also removed) + m_pDS->exec("DROP TABLE HistSong"); + // Compact db to recover space as had to add/drop actual table + if (progressDialog) + { + progressDialog->SetLine(2, CVariant{ 331 }); + progressDialog->Progress(); + } + Compress(false); + + // Write event log entry + // "Importing song history {1} of {2} songs matched", total - unmatched, total) + std::string strLine = StringUtils::Format(g_localizeStrings.Get(38353).c_str(), total - unmatched, total); + CServiceBroker::GetEventLog().Add( + EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information))); + + return true; + } + catch (...) + { + CLog::Log(LOGERROR, "%s failed", __FUNCTION__); + RollbackTransaction(); + if (bHistSongExists) + m_pDS->exec("DROP TABLE HistSong"); + } + return false; } void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist) diff --git a/xbmc/music/MusicDatabase.h b/xbmc/music/MusicDatabase.h index 3ef778d6d4..ed65526a94 100644 --- a/xbmc/music/MusicDatabase.h +++ b/xbmc/music/MusicDatabase.h @@ -497,8 +497,10 @@ public: ///////////////////////////////////////////////// // XML ///////////////////////////////////////////////// - void ExportToXML(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog = NULL); - void ImportFromXML(const std::string &xmlFile); + void ExportToXML(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog = nullptr); + bool ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog = nullptr); + void ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog = nullptr); + bool ImportSongHistory(const std::string& xmlFile, const int total, CGUIDialogProgress* progressDialog = nullptr); ///////////////////////////////////////////////// // Properties diff --git a/xbmc/music/MusicLibraryQueue.cpp b/xbmc/music/MusicLibraryQueue.cpp index 740fa3095b..acfaffb114 100644 --- a/xbmc/music/MusicLibraryQueue.cpp +++ b/xbmc/music/MusicLibraryQueue.cpp @@ -17,6 +17,7 @@ #include "GUIUserMessages.h" #include "music/jobs/MusicLibraryCleaningJob.h" #include "music/jobs/MusicLibraryExportJob.h" +#include "music/jobs/MusicLibraryImportJob.h" #include "music/jobs/MusicLibraryScanningJob.h" #include "music/jobs/MusicLibraryJob.h" #include "threads/SingleLock.h" @@ -77,6 +78,45 @@ void CMusicLibraryQueue::ExportLibrary(const CLibExportSettings& settings, bool } } +void CMusicLibraryQueue::ImportLibrary(const std::string& xmlFile, bool showDialog /* = false */) +{ + CGUIDialogProgress* progress = nullptr; + if (showDialog) + { + progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (progress) + { + progress->SetHeading(CVariant{ 20197 }); //"Import music library" + progress->SetText(CVariant{ 649 }); //"Importing" + progress->SetLine(1, CVariant{ 330 }); //"This could take some time" + progress->SetLine(2, CVariant{ "" }); + progress->SetPercentage(0); + progress->Open(); + progress->ShowProgressBar(true); + } + } + + CMusicLibraryImportJob* importJob = new CMusicLibraryImportJob(xmlFile, progress); + if (showDialog) + { + AddJob(importJob); + + // Wait for import to complete or be canceled, but render every 10ms so that the + // pointer movements work on dialog even when import is reporting progress infrequently + if (progress) + progress->Wait(); + } + else + { + m_modal = true; + importJob->DoWork(); + + delete importJob; + m_modal = false; + Refresh(); + } +} + void CMusicLibraryQueue::ScanLibrary(const std::string& strDirectory, int flags /* = 0 */, bool showProgress /* = true */) { AddJob(new CMusicLibraryScanningJob(strDirectory, flags, showProgress)); diff --git a/xbmc/music/MusicLibraryQueue.h b/xbmc/music/MusicLibraryQueue.h index 2ba993e13a..4ccf215524 100644 --- a/xbmc/music/MusicLibraryQueue.h +++ b/xbmc/music/MusicLibraryQueue.h @@ -44,6 +44,13 @@ public: void ExportLibrary(const CLibExportSettings& settings, bool showDialog = false); /*! + \brief Enqueue a music library import job. + \param[in] xmlFile xml file to import + \param[in] showDialog Show a progress dialog while (asynchronously) exporting, otherwise export in synchronous + */ + void ImportLibrary(const std::string& xmlFile, bool showDialog = false); + + /*! \brief Enqueue a music library update job, scanning tags embedded in music files and optionally scraping additional data. \param[in] strDirectory Directory to scan or "" (empty string) for a global scan. \param[in] flags Flags for controlling the scanning process. See xbmc/music/infoscanner/MusicInfoScanner.h for possible values. diff --git a/xbmc/music/jobs/CMakeLists.txt b/xbmc/music/jobs/CMakeLists.txt index 0392781fee..8ee0f5dda6 100644 --- a/xbmc/music/jobs/CMakeLists.txt +++ b/xbmc/music/jobs/CMakeLists.txt @@ -2,12 +2,14 @@ set(SOURCES MusicLibraryJob.cpp MusicLibraryProgressJob.cpp MusicLibraryCleaningJob.cpp MusicLibraryExportJob.cpp + MusicLibraryImportJob.cpp MusicLibraryScanningJob.cpp) set(HEADERS MusicLibraryJob.h MusicLibraryProgressJob.h MusicLibraryCleaningJob.h MusicLibraryExportJob.h + MusicLibraryImportJob.h MusicLibraryScanningJob.h) core_add_library(music_jobs) diff --git a/xbmc/music/jobs/MusicLibraryImportJob.cpp b/xbmc/music/jobs/MusicLibraryImportJob.cpp new file mode 100644 index 0000000000..714e535b6e --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryImportJob.cpp @@ -0,0 +1,41 @@ +/* +* Copyright (C) 2017-2018 Team Kodi +* This file is part of Kodi - https://kodi.tv +* +* SPDX-License-Identifier: GPL-2.0-or-later +* See LICENSES/README.md for more information. +*/ + +#include "MusicLibraryImportJob.h" +#include "dialogs/GUIDialogProgress.h" +#include "music/MusicDatabase.h" + +CMusicLibraryImportJob::CMusicLibraryImportJob(const std::string& xmlFile, CGUIDialogProgress* progressDialog) + : CMusicLibraryProgressJob(nullptr) + , m_xmlFile(xmlFile) +{ + if (progressDialog) + SetProgressIndicators(nullptr, progressDialog); + SetAutoClose(true); +} + +CMusicLibraryImportJob::~CMusicLibraryImportJob() = default; + +bool CMusicLibraryImportJob::operator==(const CJob* job) const +{ + if (strcmp(job->GetType(), GetType()) != 0) + return false; + + const CMusicLibraryImportJob* importJob = dynamic_cast<const CMusicLibraryImportJob*>(job); + if (importJob == nullptr) + return false; + + return !(m_xmlFile != importJob->m_xmlFile); +} + +bool CMusicLibraryImportJob::Work(CMusicDatabase &db) +{ + db.ImportFromXML(m_xmlFile, GetProgressDialog()); + + return true; +} diff --git a/xbmc/music/jobs/MusicLibraryImportJob.h b/xbmc/music/jobs/MusicLibraryImportJob.h new file mode 100644 index 0000000000..bcca44f245 --- /dev/null +++ b/xbmc/music/jobs/MusicLibraryImportJob.h @@ -0,0 +1,42 @@ +/* +* Copyright (C) 2017-2018 Team Kodi +* This file is part of Kodi - https://kodi.tv +* +* SPDX-License-Identifier: GPL-2.0-or-later +* See LICENSES/README.md for more information. +*/ + +#pragma once + +#include "MusicLibraryProgressJob.h" + +class CGUIDialogProgress; + +/*! +\brief Music library job implementation for importing data to the music library. +*/ +class CMusicLibraryImportJob : public CMusicLibraryProgressJob +{ +public: + /*! + \brief Creates a new music library import job for the given xml file. + + \param[in] xmlFile xml file to import + \param[in] progressDialog Progress dialog to be used to display the import progress + */ + CMusicLibraryImportJob(const std::string &xmlFile, CGUIDialogProgress* progressDialog); + + ~CMusicLibraryImportJob() override; + + // specialization of CJob + const char *GetType() const override { return "MusicLibraryImportJob"; } + bool operator==(const CJob* job) const override; + +protected: + // implementation of CMusicLibraryJob + bool Work(CMusicDatabase &db) override; + +private: + std::string m_xmlFile; +}; + diff --git a/xbmc/settings/LibExportSettings.cpp b/xbmc/settings/LibExportSettings.cpp index f58628d75d..15be272ec4 100644 --- a/xbmc/settings/LibExportSettings.cpp +++ b/xbmc/settings/LibExportSettings.cpp @@ -71,7 +71,26 @@ std::vector<int> CLibExportSettings::GetExportItems() const values.emplace_back(ELIBEXPORT_OTHERARTISTS); if (IsItemExported(ELIBEXPORT_ACTORTHUMBS)) values.emplace_back(ELIBEXPORT_ACTORTHUMBS); + if (IsItemExported(ELIBEXPORT_SONGS)) + values.emplace_back(ELIBEXPORT_SONGS); + return values; +} +std::vector<int> CLibExportSettings::GetLimitedItems(int items) const +{ + std::vector<int> values; + if (IsItemExported(ELIBEXPORT_ALBUMS) && (items & ELIBEXPORT_ALBUMS)) + values.emplace_back(ELIBEXPORT_ALBUMS); + if (IsItemExported(ELIBEXPORT_ALBUMARTISTS) && (items & ELIBEXPORT_ALBUMARTISTS)) + values.emplace_back(ELIBEXPORT_ALBUMARTISTS); + if (IsItemExported(ELIBEXPORT_SONGARTISTS) && (items & ELIBEXPORT_SONGARTISTS)) + values.emplace_back(ELIBEXPORT_SONGARTISTS); + if (IsItemExported(ELIBEXPORT_OTHERARTISTS) && (items & ELIBEXPORT_OTHERARTISTS)) + values.emplace_back(ELIBEXPORT_OTHERARTISTS); + if (IsItemExported(ELIBEXPORT_ACTORTHUMBS) && (items & ELIBEXPORT_ACTORTHUMBS)) + values.emplace_back(ELIBEXPORT_ACTORTHUMBS); + if (IsItemExported(ELIBEXPORT_SONGS) && (items & ELIBEXPORT_SONGS)) + values.emplace_back(ELIBEXPORT_SONGS); return values; } diff --git a/xbmc/settings/LibExportSettings.h b/xbmc/settings/LibExportSettings.h index a2e35c01b1..b82f3cc88a 100644 --- a/xbmc/settings/LibExportSettings.h +++ b/xbmc/settings/LibExportSettings.h @@ -30,7 +30,8 @@ enum ELIBEXPORTOPTIONS ELIBEXPORT_ARTWORK = 0x0100, ELIBEXPORT_NFOFILES = 0x0200, ELIBEXPORT_ACTORTHUMBS = 0x0400, - ELIBEXPORT_ARTISTFOLDERS = 0x0800 + ELIBEXPORT_ARTISTFOLDERS = 0x0800, + ELIBEXPORT_SONGS = 0x1000 }; class CLibExportSettings @@ -43,6 +44,7 @@ public: bool IsItemExported(ELIBEXPORTOPTIONS item) const; bool IsArtists() const; std::vector<int> GetExportItems() const; + std::vector<int> GetLimitedItems(int items) const; void ClearItems() { m_itemstoexport = 0; } void AddItem(ELIBEXPORTOPTIONS item) { m_itemstoexport += item; } unsigned int GetItemsToExport() { return m_itemstoexport; } diff --git a/xbmc/settings/MediaSettings.cpp b/xbmc/settings/MediaSettings.cpp index 8cccd71c4d..d88829787c 100644 --- a/xbmc/settings/MediaSettings.cpp +++ b/xbmc/settings/MediaSettings.cpp @@ -19,7 +19,6 @@ #include "guilib/LocalizeStrings.h" #include "interfaces/AnnouncementManager.h" #include "interfaces/builtins/Builtins.h" -#include "music/MusicDatabase.h" #include "music/MusicLibraryQueue.h" #include "messaging/helpers/DialogHelper.h" #include "ServiceBroker.h" @@ -315,10 +314,8 @@ void CMediaSettings::OnSettingAction(std::shared_ptr<const CSetting> setting) if (CGUIDialogFileBrowser::ShowAndGetFile(shares, "musicdb.xml", g_localizeStrings.Get(651) , path)) { - CMusicDatabase musicdatabase; - musicdatabase.Open(); - musicdatabase.ImportFromXML(path); - musicdatabase.Close(); + // Import data to music library showing progress dialog + CMusicLibraryQueue::GetInstance().ImportLibrary(path, true); } } else if (settingId == CSettings::SETTING_VIDEOLIBRARY_CLEANUP) diff --git a/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp b/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp index a7d2334ef9..98dac9acc7 100644 --- a/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp +++ b/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp @@ -258,10 +258,6 @@ void CGUIDialogLibExportSettings::SetupView() SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 38319); SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); - if (m_settings.IsSeparateFiles()) - ToggleState(CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED, !m_settings.m_skipnfo); - else if (m_settings.IsToLibFolders()) - ToggleState(CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED, false); UpdateButtons(); UpdateToggles(); UpdateDescription(); @@ -360,31 +356,39 @@ void CGUIDialogLibExportSettings::InitializeSettings() entries.clear(); if (!m_settings.IsArtistFoldersOnly()) entries.push_back(std::make_pair(132, ELIBEXPORT_ALBUMS)); //ablums + if (m_settings.IsSingleFile()) + entries.push_back(std::make_pair(134, ELIBEXPORT_SONGS)); //songs entries.push_back(std::make_pair(38043, ELIBEXPORT_ALBUMARTISTS)); //album artists entries.push_back(std::make_pair(38312, ELIBEXPORT_SONGARTISTS)); //song artists entries.push_back(std::make_pair(38313, ELIBEXPORT_OTHERARTISTS)); //other artists + + std::vector<int> items; if (m_settings.IsArtistFoldersOnly()) { - std::vector<int> artistitems; // Only artists, not albums - if (m_settings.IsItemExported(ELIBEXPORT_SONGARTISTS)) - artistitems.emplace_back(ELIBEXPORT_SONGARTISTS); - if (m_settings.IsItemExported(ELIBEXPORT_OTHERARTISTS)) - artistitems.emplace_back(ELIBEXPORT_OTHERARTISTS); - if (m_settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) || (artistitems.size() == 0)) - artistitems.emplace_back(ELIBEXPORT_ALBUMARTISTS); - AddList(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS, 38306, SettingLevel::Basic, artistitems, entries, 133, 1); + // Only artists, not albums, at least album artists + items = m_settings.GetLimitedItems(ELIBEXPORT_ALBUMARTISTS + ELIBEXPORT_SONGARTISTS + ELIBEXPORT_OTHERARTISTS); + if (items.size() == 0) + items.emplace_back(ELIBEXPORT_ALBUMARTISTS); } - else + else if (!m_settings.IsSingleFile()) { - AddList(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS, 38306, SettingLevel::Basic, m_settings.GetExportItems(), entries, 133, 1); + // No songs unless single file export, at least album artists + items = m_settings.GetLimitedItems(ELIBEXPORT_ALBUMS + ELIBEXPORT_ALBUMARTISTS + ELIBEXPORT_SONGARTISTS + ELIBEXPORT_OTHERARTISTS); + if (items.size() == 0) + items.emplace_back(ELIBEXPORT_ALBUMARTISTS); + } + else + items = m_settings.GetExportItems(); + + AddList(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS, 38306, SettingLevel::Basic, items, entries, 133, 1); - if (!m_settings.IsSingleFile()) - { - m_settingNFO = AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO, 38309, SettingLevel::Basic, !m_settings.m_skipnfo); + if (m_settings.IsToLibFolders() || m_settings.IsSeparateFiles()) + { + m_settingNFO = AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO, 38309, SettingLevel::Basic, !m_settings.m_skipnfo); + if (m_settings.IsSeparateFiles()) AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED, 38308, SettingLevel::Basic, m_settings.m_unscraped); - m_settingArt = AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK, 38307, SettingLevel::Basic, m_settings.m_artwork); - AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE, 38311, SettingLevel::Basic, m_settings.m_overwrite); - } + m_settingArt = AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK, 38307, SettingLevel::Basic, m_settings.m_artwork); + AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE, 38311, SettingLevel::Basic, m_settings.m_overwrite); } } |