summaryrefslogtreecommitdiffstats
path: root/xbmc/music
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/music')
-rw-r--r--xbmc/music/Album.cpp671
-rw-r--r--xbmc/music/Album.h183
-rw-r--r--xbmc/music/Artist.cpp239
-rw-r--r--xbmc/music/Artist.h222
-rw-r--r--xbmc/music/CMakeLists.txt25
-rw-r--r--xbmc/music/ContextMenus.cpp129
-rw-r--r--xbmc/music/ContextMenus.h74
-rw-r--r--xbmc/music/GUIViewStateMusic.cpp648
-rw-r--r--xbmc/music/GUIViewStateMusic.h85
-rw-r--r--xbmc/music/MusicDatabase.cpp13822
-rw-r--r--xbmc/music/MusicDatabase.h1157
-rw-r--r--xbmc/music/MusicDbUrl.cpp170
-rw-r--r--xbmc/music/MusicDbUrl.h26
-rw-r--r--xbmc/music/MusicInfoLoader.cpp314
-rw-r--r--xbmc/music/MusicInfoLoader.h46
-rw-r--r--xbmc/music/MusicLibraryQueue.cpp301
-rw-r--r--xbmc/music/MusicLibraryQueue.h135
-rw-r--r--xbmc/music/MusicThumbLoader.cpp385
-rw-r--r--xbmc/music/MusicThumbLoader.h60
-rw-r--r--xbmc/music/MusicUtils.cpp841
-rw-r--r--xbmc/music/MusicUtils.h120
-rw-r--r--xbmc/music/Song.cpp373
-rw-r--r--xbmc/music/Song.h224
-rw-r--r--xbmc/music/dialogs/CMakeLists.txt13
-rw-r--r--xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp479
-rw-r--r--xbmc/music/dialogs/GUIDialogInfoProviderSettings.h92
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicInfo.cpp1043
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicInfo.h80
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicOSD.cpp91
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicOSD.h22
-rw-r--r--xbmc/music/dialogs/GUIDialogSongInfo.cpp523
-rw-r--r--xbmc/music/dialogs/GUIDialogSongInfo.h54
-rw-r--r--xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp98
-rw-r--r--xbmc/music/dialogs/GUIDialogVisualisationPresetList.h32
-rw-r--r--xbmc/music/infoscanner/CMakeLists.txt11
-rw-r--r--xbmc/music/infoscanner/MusicAlbumInfo.cpp52
-rw-r--r--xbmc/music/infoscanner/MusicAlbumInfo.h50
-rw-r--r--xbmc/music/infoscanner/MusicArtistInfo.cpp34
-rw-r--r--xbmc/music/infoscanner/MusicArtistInfo.h39
-rw-r--r--xbmc/music/infoscanner/MusicInfoScanner.cpp2338
-rw-r--r--xbmc/music/infoscanner/MusicInfoScanner.h265
-rw-r--r--xbmc/music/infoscanner/MusicInfoScraper.cpp202
-rw-r--r--xbmc/music/infoscanner/MusicInfoScraper.h82
-rw-r--r--xbmc/music/jobs/CMakeLists.txt15
-rw-r--r--xbmc/music/jobs/MusicLibraryCleaningJob.cpp40
-rw-r--r--xbmc/music/jobs/MusicLibraryCleaningJob.h38
-rw-r--r--xbmc/music/jobs/MusicLibraryExportJob.cpp43
-rw-r--r--xbmc/music/jobs/MusicLibraryExportJob.h42
-rw-r--r--xbmc/music/jobs/MusicLibraryImportJob.cpp42
-rw-r--r--xbmc/music/jobs/MusicLibraryImportJob.h42
-rw-r--r--xbmc/music/jobs/MusicLibraryJob.cpp24
-rw-r--r--xbmc/music/jobs/MusicLibraryJob.h50
-rw-r--r--xbmc/music/jobs/MusicLibraryProgressJob.cpp24
-rw-r--r--xbmc/music/jobs/MusicLibraryProgressJob.h29
-rw-r--r--xbmc/music/jobs/MusicLibraryScanningJob.cpp58
-rw-r--r--xbmc/music/jobs/MusicLibraryScanningJob.h50
-rw-r--r--xbmc/music/tags/CMakeLists.txt22
-rw-r--r--xbmc/music/tags/ImusicInfoTagLoader.h26
-rw-r--r--xbmc/music/tags/MusicInfoTag.cpp1303
-rw-r--r--xbmc/music/tags/MusicInfoTag.h263
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp156
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderCDDA.h23
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp38
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderDatabase.h24
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp168
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h23
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFactory.cpp90
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFactory.h26
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderShn.cpp38
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderShn.h24
-rw-r--r--xbmc/music/tags/ReplayGain.cpp144
-rw-r--r--xbmc/music/tags/ReplayGain.h51
-rw-r--r--xbmc/music/tags/TagLibVFSStream.cpp321
-rw-r--r--xbmc/music/tags/TagLibVFSStream.h122
-rw-r--r--xbmc/music/tags/TagLoaderTagLib.cpp1371
-rw-r--r--xbmc/music/tags/TagLoaderTagLib.h54
-rw-r--r--xbmc/music/tags/test/CMakeLists.txt3
-rw-r--r--xbmc/music/tags/test/TestTagLoaderTagLib.cpp230
-rw-r--r--xbmc/music/windows/CMakeLists.txt15
-rw-r--r--xbmc/music/windows/GUIWindowMusicBase.cpp1097
-rw-r--r--xbmc/music/windows/GUIWindowMusicBase.h106
-rw-r--r--xbmc/music/windows/GUIWindowMusicNav.cpp944
-rw-r--r--xbmc/music/windows/GUIWindowMusicNav.h50
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylist.cpp715
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylist.h44
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp450
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylistEditor.h57
-rw-r--r--xbmc/music/windows/GUIWindowVisualisation.cpp233
-rw-r--r--xbmc/music/windows/GUIWindowVisualisation.h31
-rw-r--r--xbmc/music/windows/MusicFileItemListModifier.cpp114
-rw-r--r--xbmc/music/windows/MusicFileItemListModifier.h24
91 files changed, 34747 insertions, 0 deletions
diff --git a/xbmc/music/Album.cpp b/xbmc/music/Album.cpp
new file mode 100644
index 0000000..efaa7ac
--- /dev/null
+++ b/xbmc/music/Album.cpp
@@ -0,0 +1,671 @@
+/*
+ * Copyright (C) 2005-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 "Album.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace MUSIC_INFO;
+
+typedef struct ReleaseTypeInfo {
+ CAlbum::ReleaseType type;
+ std::string name;
+} ReleaseTypeInfo;
+
+ReleaseTypeInfo releaseTypes[] = {
+ { CAlbum::Album, "album" },
+ { CAlbum::Single, "single" }
+};
+
+CAlbum::CAlbum(const CFileItem& item)
+{
+ Reset();
+ const CMusicInfoTag& tag = *item.GetMusicInfoTag();
+ strAlbum = tag.GetAlbum();
+ strMusicBrainzAlbumID = tag.GetMusicBrainzAlbumID();
+ strReleaseGroupMBID = tag.GetMusicBrainzReleaseGroupID();
+ genre = tag.GetGenre();
+ strArtistDesc = tag.GetAlbumArtistString();
+ //Set sort string before processing artist credits
+ strArtistSort = tag.GetAlbumArtistSort();
+ // Determine artist credits from various tag arrays, inc fallback to song artist names
+ SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(),
+ tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+
+ strOrigReleaseDate = tag.GetOriginalDate();
+ strReleaseDate = tag.GetReleaseDate();
+ strLabel = tag.GetRecordLabel();
+ strType = tag.GetMusicBrainzReleaseType();
+ bCompilation = tag.GetCompilation();
+ iTimesPlayed = 0;
+ bBoxedSet = tag.GetBoxset();
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ lastPlayed.Reset();
+ releaseType = tag.GetAlbumReleaseType();
+ strReleaseStatus = tag.GetAlbumReleaseStatus();
+}
+
+void CAlbum::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids,
+ const std::vector<std::string>& artistnames, const std::vector<std::string>& artisthints,
+ const std::vector<std::string>& artistmbids)
+{
+ std::vector<std::string> albumartistHints = hints;
+ //Split the artist sort string to try and get sort names for individual artists
+ auto artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ artistCredits.clear();
+
+ if (!mbids.empty())
+ { // Have musicbrainz artist info, so use it
+
+ // Vector of possible separators in the order least likely to be part of artist name
+ const std::vector<std::string> separators{ " feat. ", ";", ":", "|", "#", "/", ",", "&" };
+
+ // Establish tag consistency
+ // Do the number of musicbrainz ids and *both* number of names and number of hints mismatch?
+ if (albumartistHints.size() != mbids.size() && names.size() != mbids.size())
+ {
+ // Tags mismatch - report it and then try to fix
+ CLog::Log(LOGDEBUG, "Mismatch in song file albumartist tags: {} mbid {} name album: {} {}",
+ (int)mbids.size(), (int)names.size(), strAlbum, strArtistDesc);
+ /*
+ Most likely we have no hints and a single artist name like "Artist1 feat. Artist2"
+ or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the
+ expected single item separator (default = space-slash-space) as not been used.
+ Comma and slash (no spaces) are poor delimiters as could be in name e.g. AC/DC,
+ but here treat them as such in attempt to find artist names.
+ When there are hints they could be poorly formatted using unexpected separators,
+ so attempt to split them. Or we could have more hints or artist names than
+ musicbrainz so ignore them but raise warning.
+ */
+
+ // Do hints exist yet mismatch
+ if (!albumartistHints.empty() && albumartistHints.size() != mbids.size())
+ {
+ if (names.size() == mbids.size())
+ // Album artist name count matches, use that as hints
+ albumartistHints = names;
+ else if (albumartistHints.size() < mbids.size())
+ { // Try splitting the hints until have matching number
+ albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size());
+ }
+ else
+ // Extra hints, discard them.
+ albumartistHints.resize(mbids.size());
+ }
+ // Do hints not exist or still mismatch, try album artists
+ if (albumartistHints.size() != mbids.size())
+ albumartistHints = names;
+ // Still mismatch, try splitting the hints (now artists) until have matching number
+ if (albumartistHints.size() < mbids.size())
+ albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size());
+ // Try matching on artists or artist hints field, if it is reliable
+ if (albumartistHints.size() != mbids.size())
+ {
+ if (!artistmbids.empty() &&
+ (artistmbids.size() == artistnames.size() ||
+ artistmbids.size() == artisthints.size()))
+ {
+ for (size_t i = 0; i < mbids.size(); i++)
+ {
+ for (size_t j = 0; j < artistmbids.size(); j++)
+ {
+ if (mbids[i] == artistmbids[j])
+ {
+ if (albumartistHints.size() < i + 1)
+ albumartistHints.resize(i + 1);
+ if (artistmbids.size() == artisthints.size())
+ albumartistHints[i] = artisthints[j];
+ else
+ albumartistHints[i] = artistnames[j];
+ }
+ }
+ }
+ }
+ }
+ }
+ else
+ { // Either hints or album artists (or both) name matches number of musicbrainz id
+ // If hints mismatch, use album artists
+ if (albumartistHints.size() != mbids.size())
+ albumartistHints = names;
+ }
+
+ // Try to get number of artist sort names and musicbrainz ids to match. Split sort names
+ // further using multiple possible delimiters, over single separator applied in Tag loader
+ if (artistSort.size() != mbids.size())
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < mbids.size(); i++)
+ {
+ std::string artistId = mbids[i];
+ std::string artistName;
+ /*
+ We try and get the musicbrainz id <-> name matching from the hints and match on the same index.
+ Some album artist hints could be blank (if populated from artist or artist hints).
+ If not found, use the musicbrainz id and hope we later on can update that entry.
+ If we have more names than musicbrainz id they are ignored, but raise a warning.
+ */
+ if (i < albumartistHints.size())
+ artistName = albumartistHints[i];
+ if (artistName.empty())
+ artistName = artistId;
+
+ // Use artist sort name providing we have as many as we have mbid,
+ // otherwise something is wrong with them so ignore and leave blank
+ if (artistSort.size() == mbids.size())
+ artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId);
+ else
+ artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId);
+ }
+ }
+ else
+ {
+ /*
+ No musicbrainz album artist ids so fill artist names directly.
+ This method only called during scanning when there is a musicbrainz album id, so
+ means mbid tags are incomplete. But could also be called by JSON to SetAlbumDetails
+ Try to separate album artist names further, and trim blank space.
+ */
+ std::vector<std::string> albumArtists = names;
+ if (albumartistHints.size() > albumArtists.size())
+ // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names
+ albumArtists = albumartistHints;
+ else
+ // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader
+ albumArtists = StringUtils::SplitMulti(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
+
+ if (artistSort.size() != albumArtists.size())
+ // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < albumArtists.size(); i++)
+ {
+ artistCredits.emplace_back(StringUtils::Trim(albumArtists[i]));
+ // Set artist sort name providing we have as many as we have artists,
+ // otherwise something is wrong with them so ignore rather than guess.
+ if (artistSort.size() == albumArtists.size())
+ artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i]));
+ }
+ }
+}
+
+void CAlbum::MergeScrapedAlbum(const CAlbum& source, bool override /* = true */)
+{
+ /*
+ Initial scraping of album information when there is a Musicbrainz album ID derived from
+ tags is done directly using that ID, otherwise the lookup is based on album and artist names
+ but this can sometimes mis-identify the album (i.e. classical music has many "Symphony No. 5").
+ It is useful to store the scraped mbid, but we need to be able to correct any mistakes. Hence
+ a manual refresh of album information uses either the mbid as derived from tags or the album
+ and artist names, not any previously scraped mbid.
+
+ When overwriting the data derived from tags, AND the original and scraped album have the same
+ Musicbrainz album ID, then merging is used to keep Kodi up to date with changes in the Musicbrainz
+ database including album artist credits, song artist credits and song titles. However it is only
+ appropriate when the music files are tagged with mbids, these are taken as definative, scraped
+ mbids can not be depended on in this way.
+
+ When the album is megerd in this deep way it is flagged so that the database album update is aware
+ artist credits and songs need to be updated too.
+ */
+
+ bArtistSongMerge = override && !bScrapedMBID
+ && !source.strMusicBrainzAlbumID.empty() && !strMusicBrainzAlbumID.empty()
+ && (strMusicBrainzAlbumID.compare(source.strMusicBrainzAlbumID) == 0);
+
+ /*
+ Musicbrainz album (release) ID and release group ID values derived from music file tags are
+ always taken as accurate and so can not be overwritten by a scraped value. When the album does
+ not already have an mbid or has a previously scraped mbid, merge the new scraped value,
+ flagging it as being from the scraper rather than derived from music file tags.
+ */
+ if (!source.strMusicBrainzAlbumID.empty() && (strMusicBrainzAlbumID.empty() || bScrapedMBID))
+ {
+ strMusicBrainzAlbumID = source.strMusicBrainzAlbumID;
+ bScrapedMBID = true;
+ }
+ if (!source.strReleaseGroupMBID.empty() && (strReleaseGroupMBID.empty() || bScrapedMBID))
+ {
+ strReleaseGroupMBID = source.strReleaseGroupMBID;
+ }
+
+ /*
+ Scraping can return different album artists from the originals derived from tags, even when
+ doing a lookup on artist name.
+
+ When overwriting the data derived from tags, AND the original and scraped album have the same
+ Musicbrainz album ID, then merging an album replaces both the album artsts and the song artists
+ with those scraped (providing they are not empty).
+
+ When not doing that kind of merge, for any matching artist names the Musicbrainz artist id
+ returned by the scraper can be used to populate any previously missing Musicbrainz artist id values.
+ */
+ if (bArtistSongMerge && !source.artistCredits.empty())
+ {
+ artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper
+ strArtistDesc.clear(); // @todo: set artist display string e.g. "artist1 & artist2" when scraped
+ }
+ else
+ {
+ // Compare original album artists with those scraped (ignoring order), and set any missing mbid
+ for (auto &artistCredit : artistCredits)
+ {
+ if (artistCredit.GetMusicBrainzArtistID().empty())
+ {
+ for (const auto& sourceartistCredit : source.artistCredits)
+ {
+ if (StringUtils::EqualsNoCase(artistCredit.GetArtist(), sourceartistCredit.GetArtist()))
+ {
+ artistCredit.SetMusicBrainzArtistID(sourceartistCredit.GetMusicBrainzArtistID());
+ artistCredit.SetScrapedMBID(true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ //@todo: scraped album genre needs adding to genre and album_genre tables, this just changes the string
+ if ((override && !source.genre.empty()) || genre.empty())
+ genre = source.genre;
+ if ((override && !source.strAlbum.empty()) || strAlbum.empty())
+ strAlbum = source.strAlbum;
+ //@todo: validate ISO8601 format YYYY, YYYY-MM, or YYYY-MM-DD
+ if ((override && !source.strReleaseDate.empty()) || strReleaseDate.empty())
+ strReleaseDate = source.strReleaseDate;
+ if ((override && !source.strOrigReleaseDate.empty()) || strOrigReleaseDate.empty())
+ strOrigReleaseDate = source.strOrigReleaseDate;
+
+ if (override)
+ bCompilation = source.bCompilation;
+ // iTimesPlayed = source.iTimesPlayed; // times played is derived from songs
+
+ if ((override && !source.strArtistSort.empty()) || strArtistSort.empty())
+ strArtistSort = source.strArtistSort;
+ for (const auto& i : source.art)
+ {
+ if (override || art.find(i.first) == art.end())
+ art[i.first] = i.second;
+ }
+ if((override && !source.strLabel.empty()) || strLabel.empty())
+ strLabel = source.strLabel;
+ thumbURL = source.thumbURL;
+ moods = source.moods;
+ styles = source.styles;
+ themes = source.themes;
+ strReview = source.strReview;
+ if ((override && !source.strType.empty()) || strType.empty())
+ strType = source.strType;
+// strPath = source.strPath; // don't merge the path
+ if ((override && !source.strReleaseStatus.empty()) || strReleaseStatus.empty())
+ strReleaseStatus = source.strReleaseStatus;
+ fRating = source.fRating;
+ iUserrating = source.iUserrating;
+ iVotes = source.iVotes;
+
+ /*
+ When overwriting the data derived from tags, AND the original and scraped album have the same
+ Musicbrainz album ID, update the local songs with scaped Musicbrainz information including the
+ artist credits.
+ */
+ if (bArtistSongMerge)
+ {
+ for (auto &song : songs)
+ {
+ if (!song.strMusicBrainzTrackID.empty())
+ for (const auto& sourceSong : source.songs)
+ if ((sourceSong.strMusicBrainzTrackID == song.strMusicBrainzTrackID) && (sourceSong.iTrack == song.iTrack))
+ song.MergeScrapedSong(sourceSong, override);
+ }
+ }
+}
+
+std::string CAlbum::GetGenreString() const
+{
+ return StringUtils::Join(genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+}
+
+const std::vector<std::string> CAlbum::GetAlbumArtist() const
+{
+ //Get artist names as vector from artist credits
+ std::vector<std::string> albumartists;
+ for (const auto& artistCredit : artistCredits)
+ {
+ albumartists.push_back(artistCredit.GetArtist());
+ }
+ return albumartists;
+}
+
+const std::vector<std::string> CAlbum::GetMusicBrainzAlbumArtistID() const
+{
+ //Get artist MusicBrainz IDs as vector from artist credits
+ std::vector<std::string> musicBrainzID;
+ for (const auto& artistCredit : artistCredits)
+ {
+ musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ return musicBrainzID;
+}
+
+const std::string CAlbum::GetAlbumArtistString() const
+{
+ //Artist description may be different from the artists in artistcredits (see ALBUMARTISTS tag processing)
+ //but is takes precedence as a string because artistcredits is not always filled during processing
+ if (!strArtistDesc.empty())
+ return strArtistDesc;
+ std::vector<std::string> artistvector;
+ for (const auto& i : artistCredits)
+ artistvector.emplace_back(i.GetArtist());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return artistString;
+}
+
+const std::string CAlbum::GetAlbumArtistSort() const
+{
+ //The stored artist sort name string takes precedence but a
+ //value could be created from individual sort names held in artistcredits
+ if (!strArtistSort.empty())
+ return strArtistSort;
+ std::vector<std::string> artistvector;
+ for (const auto& artistcredit : artistCredits)
+ if (!artistcredit.GetSortName().empty())
+ artistvector.emplace_back(artistcredit.GetSortName());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, "; ");
+ return artistString;
+}
+
+const std::vector<int> CAlbum::GetArtistIDArray() const
+{
+ // Get album artist IDs for json rpc
+ std::vector<int> artistids;
+ for (const auto& artistCredit : artistCredits)
+ artistids.push_back(artistCredit.GetArtistId());
+ return artistids;
+}
+
+
+std::string CAlbum::GetReleaseType() const
+{
+ return ReleaseTypeToString(releaseType);
+}
+
+void CAlbum::SetReleaseType(const std::string& strReleaseType)
+{
+ releaseType = ReleaseTypeFromString(strReleaseType);
+}
+
+void CAlbum::SetDateAdded(const std::string& strDateAdded)
+{
+ dateAdded.SetFromDBDateTime(strDateAdded);
+}
+
+void CAlbum::SetDateUpdated(const std::string& strDateUpdated)
+{
+ dateUpdated.SetFromDBDateTime(strDateUpdated);
+}
+
+void CAlbum::SetDateNew(const std::string& strDateNew)
+{
+ dateNew.SetFromDBDateTime(strDateNew);
+}
+
+void CAlbum::SetLastPlayed(const std::string& strLastPlayed)
+{
+ lastPlayed.SetFromDBDateTime(strLastPlayed);
+}
+
+std::string CAlbum::ReleaseTypeToString(CAlbum::ReleaseType releaseType)
+{
+ for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes)
+ {
+ if (releaseTypeInfo.type == releaseType)
+ return releaseTypeInfo.name;
+ }
+
+ return "album";
+}
+
+CAlbum::ReleaseType CAlbum::ReleaseTypeFromString(const std::string& strReleaseType)
+{
+ for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes)
+ {
+ if (releaseTypeInfo.name == strReleaseType)
+ return releaseTypeInfo.type;
+ }
+
+ return Album;
+}
+
+bool CAlbum::operator<(const CAlbum &a) const
+{
+ if (strMusicBrainzAlbumID.empty() && a.strMusicBrainzAlbumID.empty())
+ {
+ if (strAlbum < a.strAlbum) return true;
+ if (strAlbum > a.strAlbum) return false;
+
+ // This will do an std::vector compare (i.e. item by item)
+ if (GetAlbumArtist() < a.GetAlbumArtist()) return true;
+ if (GetAlbumArtist() > a.GetAlbumArtist()) return false;
+ return false;
+ }
+
+ if (strMusicBrainzAlbumID < a.strMusicBrainzAlbumID) return true;
+ if (strMusicBrainzAlbumID > a.strMusicBrainzAlbumID) return false;
+ return false;
+}
+
+bool CAlbum::Load(const TiXmlElement *album, bool append, bool prioritise)
+{
+ if (!album) return false;
+ if (!append)
+ Reset();
+
+ const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ XMLUtils::GetString(album, "title", strAlbum);
+ XMLUtils::GetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID);
+ XMLUtils::GetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID);
+ XMLUtils::GetBoolean(album, "scrapedmbid", bScrapedMBID);
+ XMLUtils::GetString(album, "artistdesc", strArtistDesc);
+ std::vector<std::string> artist; // Support old style <artist></artist> for backwards compatibility
+ XMLUtils::GetStringArray(album, "artist", artist, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "genre", genre, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "style", styles, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "mood", moods, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "theme", themes, prioritise, itemSeparator);
+ XMLUtils::GetBoolean(album, "compilation", bCompilation);
+ XMLUtils::GetBoolean(album, "boxset", bBoxedSet);
+
+ XMLUtils::GetString(album,"review",strReview);
+ XMLUtils::GetString(album,"label",strLabel);
+ XMLUtils::GetInt(album, "duration", iAlbumDuration);
+ XMLUtils::GetString(album,"type",strType);
+ XMLUtils::GetString(album, "releasestatus", strReleaseStatus);
+
+ XMLUtils::GetString(album, "releasedate", strReleaseDate);
+ StringUtils::Trim(strReleaseDate); // @todo: validate ISO8601 format
+ // Support old style <year></year> for backwards compatibility
+ if (strReleaseDate.empty())
+ {
+ int year;
+ XMLUtils::GetInt(album, "year", year);
+ if (year > 0)
+ strReleaseDate = StringUtils::Format("{:04}", year);
+ }
+ XMLUtils::GetString(album, "originalreleasedate", strOrigReleaseDate);
+
+ const TiXmlElement* rElement = album->FirstChildElement("rating");
+ if (rElement)
+ {
+ float rating = 0;
+ float max_rating = 10;
+ XMLUtils::GetFloat(album, "rating", rating);
+ if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating>=1)
+ rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10
+ if (rating > 10.f)
+ rating = 10.f;
+ fRating = rating;
+ }
+ const TiXmlElement* userrating = album->FirstChildElement("userrating");
+ if (userrating)
+ {
+ float rating = 0;
+ float max_rating = 10;
+ XMLUtils::GetFloat(album, "userrating", rating);
+ if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
+ rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10
+ if (rating > 10.f)
+ rating = 10.f;
+ iUserrating = MathUtils::round_int(static_cast<double>(rating));
+ }
+ XMLUtils::GetInt(album, "votes", iVotes);
+
+ size_t iThumbCount = thumbURL.GetUrls().size();
+ std::string xmlAdd = thumbURL.GetData();
+ const TiXmlElement* thumb = album->FirstChildElement("thumb");
+ while (thumb)
+ {
+ thumbURL.ParseAndAppendUrl(thumb);
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *thumb;
+ xmlAdd = temp+xmlAdd;
+ }
+ thumb = thumb->NextSiblingElement("thumb");
+ }
+ // prioritise thumbs from nfos
+ if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size())
+ {
+ auto thumbUrls = thumbURL.GetUrls();
+ rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end());
+ thumbURL.SetUrls(thumbUrls);
+ thumbURL.SetData(xmlAdd);
+ }
+
+ const TiXmlElement* albumArtistCreditsNode = album->FirstChildElement("albumArtistCredits");
+ if (albumArtistCreditsNode)
+ artistCredits.clear();
+
+ while (albumArtistCreditsNode)
+ {
+ if (albumArtistCreditsNode->FirstChild())
+ {
+ CArtistCredit artistCredit;
+ XMLUtils::GetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist);
+ XMLUtils::GetString(albumArtistCreditsNode, "musicBrainzArtistID", artistCredit.m_strMusicBrainzArtistID);
+ artistCredits.push_back(artistCredit);
+ }
+
+ albumArtistCreditsNode = albumArtistCreditsNode->NextSiblingElement("albumArtistCredits");
+ }
+
+ // Support old style <artist></artist> for backwards compatibility
+ // .nfo files should ideally be updated to use the artist credits structure above
+ // or removed entirely in preference for better tags (MusicBrainz?)
+ if (artistCredits.empty() && !artist.empty())
+ {
+ for (const auto& it : artist)
+ {
+ CArtistCredit artistCredit(it);
+ artistCredits.push_back(artistCredit);
+ }
+ }
+
+ std::string strReleaseType;
+ if (XMLUtils::GetString(album, "releasetype", strReleaseType))
+ SetReleaseType(strReleaseType);
+ else
+ releaseType = Album;
+
+ return true;
+}
+
+bool CAlbum::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath)
+{
+ if (!node) return false;
+
+ // we start with a <tag> tag
+ TiXmlElement albumElement(tag.c_str());
+ TiXmlNode *album = node->InsertEndChild(albumElement);
+
+ if (!album) return false;
+
+ XMLUtils::SetString(album, "title", strAlbum);
+ XMLUtils::SetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID);
+ XMLUtils::SetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID);
+ XMLUtils::SetBoolean(album, "scrapedmbid", bScrapedMBID);
+ XMLUtils::SetString(album, "artistdesc", strArtistDesc); //Can be different from artist credits
+ XMLUtils::SetStringArray(album, "genre", genre);
+ XMLUtils::SetStringArray(album, "style", styles);
+ XMLUtils::SetStringArray(album, "mood", moods);
+ XMLUtils::SetStringArray(album, "theme", themes);
+ XMLUtils::SetBoolean(album, "compilation", bCompilation);
+ XMLUtils::SetBoolean(album, "boxset", bBoxedSet);
+
+ XMLUtils::SetString(album, "review", strReview);
+ XMLUtils::SetString(album, "type", strType);
+ XMLUtils::SetString(album, "releasestatus", strReleaseStatus);
+ XMLUtils::SetString(album, "releasedate", strReleaseDate);
+ XMLUtils::SetString(album, "originalreleasedate", strOrigReleaseDate);
+ XMLUtils::SetString(album, "label", strLabel);
+ XMLUtils::SetInt(album, "duration", iAlbumDuration);
+ if (thumbURL.HasData())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(thumbURL.GetData());
+ const TiXmlNode* thumb = doc.FirstChild("thumb");
+ while (thumb)
+ {
+ album->InsertEndChild(*thumb);
+ thumb = thumb->NextSibling("thumb");
+ }
+ }
+ XMLUtils::SetString(album, "path", strPath);
+
+ auto* rating = XMLUtils::SetFloat(album, "rating", fRating);
+ if (rating)
+ rating->ToElement()->SetAttribute("max", 10);
+
+ auto* userrating = XMLUtils::SetInt(album, "userrating", iUserrating);
+ if (userrating)
+ userrating->ToElement()->SetAttribute("max", 10);
+
+ XMLUtils::SetInt(album, "votes", iVotes);
+
+ for (const auto& artistCredit : artistCredits)
+ {
+ // add an <albumArtistCredits> tag
+ TiXmlElement albumArtistCreditsElement("albumArtistCredits");
+ TiXmlNode *albumArtistCreditsNode = album->InsertEndChild(albumArtistCreditsElement);
+ XMLUtils::SetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist);
+ XMLUtils::SetString(albumArtistCreditsNode, "musicBrainzArtistID",
+ artistCredit.m_strMusicBrainzArtistID);
+ }
+
+ XMLUtils::SetString(album, "releasetype", GetReleaseType());
+
+ return true;
+}
+
diff --git a/xbmc/music/Album.h b/xbmc/music/Album.h
new file mode 100644
index 0000000..18a08b7
--- /dev/null
+++ b/xbmc/music/Album.h
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2005-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
+
+/*!
+ \file Album.h
+\brief
+*/
+
+#include "Artist.h"
+#include "Song.h"
+#include "XBDateTime.h"
+#include "utils/ScraperUrl.h"
+
+#include <map>
+#include <vector>
+
+class TiXmlNode;
+class CFileItem;
+class CAlbum
+{
+public:
+ explicit CAlbum(const CFileItem& item);
+ CAlbum() = default;
+ bool operator<(const CAlbum &a) const;
+ void MergeScrapedAlbum(const CAlbum& album, bool override = true);
+
+ void Reset()
+ {
+ idAlbum = -1;
+ strAlbum.clear();
+ strMusicBrainzAlbumID.clear();
+ strReleaseGroupMBID.clear();
+ artistCredits.clear();
+ strArtistDesc.clear();
+ strArtistSort.clear();
+ genre.clear();
+ thumbURL.Clear();
+ moods.clear();
+ styles.clear();
+ themes.clear();
+ art.clear();
+ strReview.clear();
+ strLabel.clear();
+ strType.clear();
+ strReleaseStatus.clear();
+ strPath.clear();
+ fRating = -1;
+ iUserrating = -1;
+ iVotes = -1;
+ strOrigReleaseDate.clear();
+ strReleaseDate.clear();
+ bCompilation = false;
+ bBoxedSet = false;
+ iTimesPlayed = 0;
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ dateNew.Reset();
+ lastPlayed.Reset();
+ iTotalDiscs = -1;
+ songs.clear();
+ releaseType = Album;
+ strLastScraped.clear();
+ bScrapedMBID = false;
+ bArtistSongMerge = false;
+ iAlbumDuration = 0;
+ }
+
+ /*! \brief Get album artist names from the vector of artistcredits objects
+ \return album artist names as a vector of strings
+ */
+ const std::vector<std::string> GetAlbumArtist() const;
+
+ /*! \brief Get album artist MusicBrainz IDs from the vector of artistcredits objects
+ \return album artist MusicBrainz IDs as a vector of strings
+ */
+ const std::vector<std::string> GetMusicBrainzAlbumArtistID() const;
+ std::string GetGenreString() const;
+
+ /*! \brief Get album artist names from the artist description string (if it exists)
+ or concatenated from the vector of artistcredits objects
+ \return album artist names as a single string
+ */
+ const std::string GetAlbumArtistString() const;
+
+ /*! \brief Get album artist sort name from the artist sort string (if it exists)
+ or concatenated from the vector of artistcredits objects
+ \return album artist sort names as a single string
+ */
+ const std::string GetAlbumArtistSort() const;
+
+ /*! \brief Get album artist IDs (for json rpc) from the vector of artistcredits objects
+ \return album artist IDs as a vector of integers
+ */
+ const std::vector<int> GetArtistIDArray() const;
+
+ typedef enum ReleaseType {
+ Album = 0,
+ Single
+ } ReleaseType;
+
+ std::string GetReleaseType() const;
+ void SetReleaseType(const std::string& strReleaseType);
+ void SetDateAdded(const std::string& strDateAdded);
+ void SetDateUpdated(const std::string& strDateUpdated);
+ void SetDateNew(const std::string& strDateNew);
+ void SetLastPlayed(const std::string& strLastPlayed);
+
+ static std::string ReleaseTypeToString(ReleaseType releaseType);
+ static ReleaseType ReleaseTypeFromString(const std::string& strReleaseType);
+
+ /*! \brief Set album artist credits using the arrays of tag values.
+ If strArtistSort (as from ALBUMARTISTSORT tag) is already set then individual
+ artist sort names are also processed.
+ \param names String vector of albumartist names (as from ALBUMARTIST tag)
+ \param hints String vector of albumartist name hints (as from ALBUMARTISTS tag)
+ \param mbids String vector of albumartist Musicbrainz IDs (as from MUSICBRAINZABUMARTISTID tag)
+ \param artistnames String vector of artist names (as from ARTIST tag)
+ \param artisthints String vector of artist name hints (as from ARTISTS tag)
+ \param artistmbids String vector of artist Musicbrainz IDs (as from MUSICBRAINZARTISTID tag)
+ */
+ void SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids,
+ const std::vector<std::string>& artistnames = std::vector<std::string>(),
+ const std::vector<std::string>& artisthints = std::vector<std::string>(),
+ const std::vector<std::string>& artistmbids = std::vector<std::string>());
+
+ /*! \brief Load album information from an XML file.
+ See CVideoInfoTag::Load for a description of the types of elements we load.
+ \param element the root XML element to parse.
+ \param append whether information should be added to the existing tag, or whether it should be reset first.
+ \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false.
+ \sa CVideoInfoTag::Load
+ */
+ bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false);
+ bool Save(TiXmlNode *node, const std::string &tag, const std::string& strPath);
+
+ int idAlbum = -1;
+ std::string strAlbum;
+ std::string strMusicBrainzAlbumID;
+ std::string strReleaseGroupMBID;
+ std::string strArtistDesc;
+ std::string strArtistSort;
+ VECARTISTCREDITS artistCredits;
+ std::vector<std::string> genre;
+ CScraperUrl thumbURL;
+ std::vector<std::string> moods;
+ std::vector<std::string> styles;
+ std::vector<std::string> themes;
+ std::map<std::string, std::string> art;
+ std::string strReview;
+ std::string strLabel;
+ std::string strType;
+ std::string strReleaseStatus;
+ std::string strPath;
+ float fRating = -1;
+ int iUserrating = -1;
+ int iVotes = -1;
+ std::string strReleaseDate;
+ std::string strOrigReleaseDate;
+ bool bBoxedSet = false;
+ bool bCompilation = false;
+ int iTimesPlayed = 0;
+ CDateTime dateAdded; // From related file creation or modification times, or when (re-)scanned
+ CDateTime dateUpdated; // Time db record Last modified
+ CDateTime dateNew; // Time db record created
+ CDateTime lastPlayed;
+ int iTotalDiscs = -1;
+ VECSONGS songs; ///< Local songs
+ ReleaseType releaseType = Album;
+ std::string strLastScraped;
+ bool bScrapedMBID = false;
+ bool bArtistSongMerge = false;
+ int iAlbumDuration = 0;
+};
+
+typedef std::vector<CAlbum> VECALBUMS;
diff --git a/xbmc/music/Artist.cpp b/xbmc/music/Artist.cpp
new file mode 100644
index 0000000..786f7a7
--- /dev/null
+++ b/xbmc/music/Artist.cpp
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2005-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 "Artist.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Fanart.h"
+#include "utils/XMLUtils.h"
+
+#include <algorithm>
+
+void CArtist::MergeScrapedArtist(const CArtist& source, bool override /* = true */)
+{
+ /*
+ Initial scraping of artist information when the mbid is derived from tags is done directly
+ using that ID, otherwise the lookup is based on name and can mis-identify the artist
+ (many have same name). It is useful to store the scraped mbid, but we need to be
+ able to correct any mistakes. Hence a manual refresh of artist information uses either
+ the mbid is derived from tags or the artist name, not any previously scraped mbid.
+
+ A Musicbrainz artist ID derived from music file tags is always taken as accurate and so can
+ not be overwritten by a scraped value. When the artist does not already have an mbid or has
+ a previously scraped mbid, merge the new scraped value, flagging it as being from the
+ scraper rather than derived from music file tags.
+ */
+ if (!source.strMusicBrainzArtistID.empty() && (strMusicBrainzArtistID.empty() || bScrapedMBID))
+ {
+ strMusicBrainzArtistID = source.strMusicBrainzArtistID;
+ bScrapedMBID = true;
+ }
+
+ if ((override && !source.strArtist.empty()) || strArtist.empty())
+ strArtist = source.strArtist;
+
+ if ((override && !source.strSortName.empty()) || strSortName.empty())
+ strSortName = source.strSortName;
+
+ strType = source.strType;
+ strGender = source.strGender;
+ strDisambiguation = source.strDisambiguation;
+ genre = source.genre;
+ strBiography = source.strBiography;
+ styles = source.styles;
+ moods = source.moods;
+ instruments = source.instruments;
+ strBorn = source.strBorn;
+ strFormed = source.strFormed;
+ strDied = source.strDied;
+ strDisbanded = source.strDisbanded;
+ yearsActive = source.yearsActive;
+
+ thumbURL = source.thumbURL; // Available remote art
+ // Current artwork - thumb, fanart etc., to be stored in art table
+ if (!source.art.empty())
+ art = source.art;
+
+ discography = source.discography;
+}
+
+
+bool CArtist::Load(const TiXmlElement *artist, bool append, bool prioritise)
+{
+ if (!artist) return false;
+ if (!append)
+ Reset();
+
+ const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ XMLUtils::GetString(artist, "name", strArtist);
+ XMLUtils::GetString(artist, "musicBrainzArtistID", strMusicBrainzArtistID);
+ XMLUtils::GetString(artist, "sortname", strSortName);
+ XMLUtils::GetString(artist, "type", strType);
+ XMLUtils::GetString(artist, "gender", strGender);
+ XMLUtils::GetString(artist, "disambiguation", strDisambiguation);
+ XMLUtils::GetStringArray(artist, "genre", genre, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "style", styles, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "mood", moods, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "yearsactive", yearsActive, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "instruments", instruments, prioritise, itemSeparator);
+
+ XMLUtils::GetString(artist, "born", strBorn);
+ XMLUtils::GetString(artist, "formed", strFormed);
+ XMLUtils::GetString(artist, "biography", strBiography);
+ XMLUtils::GetString(artist, "died", strDied);
+ XMLUtils::GetString(artist, "disbanded", strDisbanded);
+
+ size_t iThumbCount = thumbURL.GetUrls().size();
+ std::string xmlAdd = thumbURL.GetData();
+
+ // Available artist thumbs
+ const TiXmlElement* thumb = artist->FirstChildElement("thumb");
+ while (thumb)
+ {
+ thumbURL.ParseAndAppendUrl(thumb);
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *thumb;
+ xmlAdd = temp+xmlAdd;
+ }
+ thumb = thumb->NextSiblingElement("thumb");
+ }
+ // prefix thumbs from nfos
+ if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size())
+ {
+ auto thumbUrls = thumbURL.GetUrls();
+ rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end());
+ thumbURL.SetUrls(thumbUrls);
+ thumbURL.SetData(xmlAdd);
+ }
+
+ // Discography
+ const TiXmlElement* node = artist->FirstChildElement("album");
+ if (node)
+ discography.clear();
+ while (node)
+ {
+ if (node->FirstChild())
+ {
+ CDiscoAlbum album;
+ XMLUtils::GetString(node, "title", album.strAlbum);
+ XMLUtils::GetString(node, "year", album.strYear);
+ XMLUtils::GetString(node, "musicbrainzreleasegroupid", album.strReleaseGroupMBID);
+ discography.push_back(album);
+ }
+ node = node->NextSiblingElement("album");
+ }
+
+ // Support old style <fanart></fanart> for backwards compatibility of old nfo files and scrapers
+ const TiXmlElement *fanart2 = artist->FirstChildElement("fanart");
+ if (fanart2)
+ {
+ CFanart fanart;
+ // we prefix to handle mixed-mode nfo's with fanart set
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *fanart2;
+ fanart.m_xml = temp+fanart.m_xml;
+ }
+ else
+ fanart.m_xml << *fanart2;
+ fanart.Unpack();
+ // Append fanart to other image URLs
+ for (unsigned int i = 0; i < fanart.GetNumFanarts(); i++)
+ thumbURL.AddParsedUrl(fanart.GetImageURL(i), "fanart", fanart.GetPreviewURL(i));
+ }
+
+ // Current artwork - thumb, fanart etc. (the chosen art, not the lists of those available)
+ node = artist->FirstChildElement("art");
+ if (node)
+ {
+ const TiXmlNode *artdetailNode = node->FirstChild();
+ while (artdetailNode && artdetailNode->FirstChild())
+ {
+ art.insert(make_pair(artdetailNode->ValueStr(), artdetailNode->FirstChild()->ValueStr()));
+ artdetailNode = artdetailNode->NextSibling();
+ }
+ }
+
+ return true;
+}
+
+bool CArtist::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath)
+{
+ if (!node) return false;
+
+ // we start with a <tag> tag
+ TiXmlElement artistElement(tag.c_str());
+ TiXmlNode *artist = node->InsertEndChild(artistElement);
+
+ if (!artist) return false;
+
+ XMLUtils::SetString(artist, "name", strArtist);
+ XMLUtils::SetString(artist, "musicBrainzArtistID", strMusicBrainzArtistID);
+ XMLUtils::SetString(artist, "sortname", strSortName);
+ XMLUtils::SetString(artist, "type", strType);
+ XMLUtils::SetString(artist, "gender", strGender);
+ XMLUtils::SetString(artist, "disambiguation", strDisambiguation);
+ XMLUtils::SetStringArray(artist, "genre", genre);
+ XMLUtils::SetStringArray(artist, "style", styles);
+ XMLUtils::SetStringArray(artist, "mood", moods);
+ XMLUtils::SetStringArray(artist, "yearsactive", yearsActive);
+ XMLUtils::SetStringArray(artist, "instruments", instruments);
+ XMLUtils::SetString(artist, "born", strBorn);
+ XMLUtils::SetString(artist, "formed", strFormed);
+ XMLUtils::SetString(artist, "biography", strBiography);
+ XMLUtils::SetString(artist, "died", strDied);
+ XMLUtils::SetString(artist, "disbanded", strDisbanded);
+ // Available remote art
+ if (thumbURL.HasData())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(thumbURL.GetData());
+ const TiXmlNode* thumb = doc.FirstChild("thumb");
+ while (thumb)
+ {
+ artist->InsertEndChild(*thumb);
+ thumb = thumb->NextSibling("thumb");
+ }
+ }
+ XMLUtils::SetString(artist, "path", strPath);
+
+ // Discography
+ for (const auto& it : discography)
+ {
+ // add a <album> tag
+ TiXmlElement discoElement("album");
+ TiXmlNode* node = artist->InsertEndChild(discoElement);
+ XMLUtils::SetString(node, "title", it.strAlbum);
+ XMLUtils::SetString(node, "year", it.strYear);
+ XMLUtils::SetString(node, "musicbrainzreleasegroupid", it.strReleaseGroupMBID);
+ }
+
+ return true;
+}
+
+void CArtist::SetDateAdded(const std::string& strDateAdded)
+{
+ dateAdded.SetFromDBDateTime(strDateAdded);
+}
+
+void CArtist::SetDateUpdated(const std::string& strDateUpdated)
+{
+ dateUpdated.SetFromDBDateTime(strDateUpdated);
+}
+
+void CArtist::SetDateNew(const std::string& strDateNew)
+{
+ dateNew.SetFromDBDateTime(strDateNew);
+}
+
diff --git a/xbmc/music/Artist.h b/xbmc/music/Artist.h
new file mode 100644
index 0000000..2ec77a8
--- /dev/null
+++ b/xbmc/music/Artist.h
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2005-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 "XBDateTime.h"
+#include "utils/ScraperUrl.h"
+#include "utils/StringUtils.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class TiXmlNode;
+class CAlbum;
+class CMusicDatabase;
+
+class CDiscoAlbum
+{
+public:
+ std::string strAlbum;
+ std::string strYear;
+ std::string strReleaseGroupMBID;
+};
+
+class CArtist
+{
+public:
+ int idArtist = -1;
+ bool operator<(const CArtist& a) const
+ {
+ if (strMusicBrainzArtistID.empty() && a.strMusicBrainzArtistID.empty())
+ {
+ if (strArtist < a.strArtist) return true;
+ if (strArtist > a.strArtist) return false;
+ return false;
+ }
+
+ if (strMusicBrainzArtistID < a.strMusicBrainzArtistID) return true;
+ if (strMusicBrainzArtistID > a.strMusicBrainzArtistID) return false;
+ return false;
+ }
+
+ void MergeScrapedArtist(const CArtist& source, bool override = true);
+
+ void Reset()
+ {
+ strArtist.clear();
+ strSortName.clear();
+ strType.clear();
+ strGender.clear();
+ strDisambiguation.clear();
+ genre.clear();
+ strBiography.clear();
+ styles.clear();
+ moods.clear();
+ instruments.clear();
+ strBorn.clear();
+ strFormed.clear();
+ strDied.clear();
+ strDisbanded.clear();
+ yearsActive.clear();
+ thumbURL.Clear();
+ art.clear();
+ discography.clear();
+ idArtist = -1;
+ strPath.clear();
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ dateNew.Reset();
+ bScrapedMBID = false;
+ strLastScraped.clear();
+ }
+
+ /*! \brief Load artist information from an XML file.
+ See CVideoInfoTag::Load for a description of the types of elements we load.
+ \param element the root XML element to parse.
+ \param append whether information should be added to the existing tag, or whether it should be reset first.
+ \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false.
+ \sa CVideoInfoTag::Load
+ */
+ bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false);
+ bool Save(TiXmlNode *node, const std::string &tag, const std::string& strPath);
+
+ void SetDateAdded(const std::string& strDateAdded);
+ void SetDateUpdated(const std::string& strDateUpdated);
+ void SetDateNew(const std::string& strDateNew);
+
+ std::string strArtist;
+ std::string strSortName;
+ std::string strMusicBrainzArtistID;
+ std::string strType;
+ std::string strGender;
+ std::string strDisambiguation;
+ std::vector<std::string> genre;
+ std::string strBiography;
+ std::vector<std::string> styles;
+ std::vector<std::string> moods;
+ std::vector<std::string> instruments;
+ std::string strBorn;
+ std::string strFormed;
+ std::string strDied;
+ std::string strDisbanded;
+ std::vector<std::string> yearsActive;
+ std::string strPath;
+ CScraperUrl thumbURL; // Data for available remote art
+ std::map<std::string, std::string> art; // Current artwork - thumb, fanart etc.
+ std::vector<CDiscoAlbum> discography;
+ CDateTime dateAdded; // From related file creation or modification times, or when (re-)scanned
+ CDateTime dateUpdated; // Time db record Last modified
+ CDateTime dateNew; // Time db record created
+ bool bScrapedMBID = false;
+ std::string strLastScraped;
+};
+
+class CArtistCredit
+{
+ friend class CAlbum;
+ friend class CMusicDatabase;
+
+public:
+ CArtistCredit() = default;
+ explicit CArtistCredit(std::string strArtist) : m_strArtist(std::move(strArtist)) {}
+ CArtistCredit(std::string strArtist, std::string strMusicBrainzArtistID)
+ : m_strArtist(std::move(strArtist)), m_strMusicBrainzArtistID(std::move(strMusicBrainzArtistID))
+ {
+ }
+ CArtistCredit(std::string strArtist, std::string strSortName, std::string strMusicBrainzArtistID)
+ : m_strArtist(std::move(strArtist)),
+ m_strSortName(std::move(strSortName)),
+ m_strMusicBrainzArtistID(std::move(strMusicBrainzArtistID))
+ {
+ }
+
+ bool operator<(const CArtistCredit& a) const
+ {
+ if (m_strMusicBrainzArtistID.empty() && a.m_strMusicBrainzArtistID.empty())
+ {
+ if (m_strArtist < a.m_strArtist) return true;
+ if (m_strArtist > a.m_strArtist) return false;
+ return false;
+ }
+
+ if (m_strMusicBrainzArtistID < a.m_strMusicBrainzArtistID) return true;
+ if (m_strMusicBrainzArtistID > a.m_strMusicBrainzArtistID) return false;
+ return false;
+ }
+
+ std::string GetArtist() const { return m_strArtist; }
+ std::string GetSortName() const { return m_strSortName; }
+ std::string GetMusicBrainzArtistID() const { return m_strMusicBrainzArtistID; }
+ int GetArtistId() const { return idArtist; }
+ bool HasScrapedMBID() const { return m_bScrapedMBID; }
+ void SetArtist(const std::string &strArtist) { m_strArtist = strArtist; }
+ void SetSortName(const std::string &strSortName) { m_strSortName = strSortName; }
+ void SetMusicBrainzArtistID(const std::string &strMusicBrainzArtistID) { m_strMusicBrainzArtistID = strMusicBrainzArtistID; }
+ void SetArtistId(int idArtist) { this->idArtist = idArtist; }
+ void SetScrapedMBID(bool scrapedMBID) { this->m_bScrapedMBID = scrapedMBID; }
+
+private:
+ int idArtist = -1;
+ std::string m_strArtist;
+ std::string m_strSortName;
+ std::string m_strMusicBrainzArtistID;
+ bool m_bScrapedMBID = false; // Flag that mbid is from album merge of scarper results not derived from tags
+};
+
+typedef std::vector<CArtist> VECARTISTS;
+typedef std::vector<CArtistCredit> VECARTISTCREDITS;
+
+const std::string BLANKARTIST_FAKEMUSICBRAINZID = "Artist Tag Missing";
+const std::string BLANKARTIST_NAME = "[Missing Tag]";
+const int BLANKARTIST_ID = 1;
+const std::string VARIOUSARTISTS_MBID = "89ad4ac3-39f7-470e-963a-56509c546377";
+
+#define ROLE_ARTIST 1 //Default role
+
+class CMusicRole
+{
+public:
+ CMusicRole() = default;
+ CMusicRole(std::string strRole, std::string strArtist)
+ : idRole(-1), m_strRole(std::move(strRole)), m_strArtist(std::move(strArtist)), idArtist(-1)
+ {
+ }
+ CMusicRole(int role, std::string strRole, std::string strArtist, int ArtistId)
+ : idRole(role),
+ m_strRole(std::move(strRole)),
+ m_strArtist(std::move(strArtist)),
+ idArtist(ArtistId)
+ {
+ }
+ std::string GetArtist() const { return m_strArtist; }
+ std::string GetRoleDesc() const { return m_strRole; }
+ int GetRoleId() const { return idRole; }
+ int GetArtistId() const { return idArtist; }
+ void SetArtistId(int iArtistId) { idArtist = iArtistId; }
+
+ bool operator==(const CMusicRole& a) const
+ {
+ if (StringUtils::EqualsNoCase(m_strRole, a.m_strRole))
+ return StringUtils::EqualsNoCase(m_strArtist, a.m_strArtist);
+ else
+ return false;
+ }
+private:
+ int idRole;
+ std::string m_strRole;
+ std::string m_strArtist;
+ int idArtist;
+};
+
+typedef std::vector<CMusicRole> VECMUSICROLES;
+
+
+
diff --git a/xbmc/music/CMakeLists.txt b/xbmc/music/CMakeLists.txt
new file mode 100644
index 0000000..5e1fd42
--- /dev/null
+++ b/xbmc/music/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(SOURCES Album.cpp
+ Artist.cpp
+ ContextMenus.cpp
+ GUIViewStateMusic.cpp
+ MusicDatabase.cpp
+ MusicDbUrl.cpp
+ MusicInfoLoader.cpp
+ MusicLibraryQueue.cpp
+ MusicThumbLoader.cpp
+ MusicUtils.cpp
+ Song.cpp)
+
+set(HEADERS Album.h
+ Artist.h
+ ContextMenus.h
+ GUIViewStateMusic.h
+ MusicDatabase.h
+ MusicDbUrl.h
+ MusicInfoLoader.h
+ MusicLibraryQueue.h
+ MusicThumbLoader.h
+ MusicUtils.h
+ Song.h)
+
+core_add_library(music)
diff --git a/xbmc/music/ContextMenus.cpp b/xbmc/music/ContextMenus.cpp
new file mode 100644
index 0000000..bf7dd47
--- /dev/null
+++ b/xbmc/music/ContextMenus.cpp
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenus.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "music/MusicUtils.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "tags/MusicInfoTag.h"
+
+#include <utility>
+
+using namespace CONTEXTMENU;
+
+CMusicInfo::CMusicInfo(MediaType mediaType)
+ : CStaticContextMenuAction(19033), m_mediaType(std::move(mediaType))
+{
+}
+
+bool CMusicInfo::IsVisible(const CFileItem& item) const
+{
+ return (item.HasMusicInfoTag() && item.GetMusicInfoTag()->GetType() == m_mediaType) ||
+ (m_mediaType == MediaTypeArtist && item.IsVideoDb() && item.HasProperty("artist_musicid")) ||
+ (m_mediaType == MediaTypeAlbum && item.IsVideoDb() && item.HasProperty("album_musicid"));
+}
+
+bool CMusicInfo::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ CGUIDialogMusicInfo::ShowFor(item.get());
+ return true;
+}
+
+bool CMusicBrowse::IsVisible(const CFileItem& item) const
+{
+ if (item.IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ return false; // handled by CMediaWindow
+
+ return item.m_bIsFolder && MUSIC_UTILS::IsItemPlayable(item);
+}
+
+bool CMusicBrowse::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ if (windowMgr.GetActiveWindow() == WINDOW_MUSIC_NAV)
+ {
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, WINDOW_MUSIC_NAV, 0, GUI_MSG_UPDATE);
+ msg.SetStringParam(item->GetPath());
+ windowMgr.SendMessage(msg);
+ }
+ else
+ {
+ windowMgr.ActivateWindow(WINDOW_MUSIC_NAV, {item->GetPath(), "return"});
+ }
+ return true;
+}
+
+bool CMusicPlay::IsVisible(const CFileItem& item) const
+{
+ return MUSIC_UTILS::IsItemPlayable(item);
+}
+
+bool CMusicPlay::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ MUSIC_UTILS::PlayItem(item);
+ return true;
+}
+
+bool CMusicPlayNext::IsVisible(const CFileItem& item) const
+{
+ if (!item.CanQueue())
+ return false;
+
+ return MUSIC_UTILS::IsItemPlayable(item);
+}
+
+bool CMusicPlayNext::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ MUSIC_UTILS::QueueItem(item, MUSIC_UTILS::QueuePosition::POSITION_BEGIN);
+ return true;
+}
+
+bool CMusicQueue::IsVisible(const CFileItem& item) const
+{
+ if (!item.CanQueue())
+ return false;
+
+ return MUSIC_UTILS::IsItemPlayable(item);
+}
+
+namespace
+{
+void SelectNextItem(int windowID)
+{
+ auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIWindow* window = windowMgr.GetWindow(windowID);
+ if (window)
+ {
+ const int viewContainerID = window->GetViewContainerID();
+ if (viewContainerID > 0)
+ {
+ CGUIMessage msg1(GUI_MSG_ITEM_SELECTED, windowID, viewContainerID);
+ windowMgr.SendMessage(msg1, windowID);
+
+ CGUIMessage msg2(GUI_MSG_ITEM_SELECT, windowID, viewContainerID, msg1.GetParam1() + 1);
+ windowMgr.SendMessage(msg2, windowID);
+ }
+ }
+}
+} // unnamed namespace
+
+bool CMusicQueue::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ MUSIC_UTILS::QueueItem(item, MUSIC_UTILS::QueuePosition::POSITION_END);
+
+ // Set selection to next item in active window's view.
+ const int windowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ SelectNextItem(windowID);
+
+ return true;
+}
diff --git a/xbmc/music/ContextMenus.h b/xbmc/music/ContextMenus.h
new file mode 100644
index 0000000..7327592
--- /dev/null
+++ b/xbmc/music/ContextMenus.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenuItem.h"
+#include "media/MediaType.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace CONTEXTMENU
+{
+
+struct CMusicInfo : CStaticContextMenuAction
+{
+ explicit CMusicInfo(MediaType mediaType);
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+
+private:
+ const MediaType m_mediaType;
+};
+
+struct CAlbumInfo : CMusicInfo
+{
+ CAlbumInfo() : CMusicInfo(MediaTypeAlbum) {}
+};
+
+struct CArtistInfo : CMusicInfo
+{
+ CArtistInfo() : CMusicInfo(MediaTypeArtist) {}
+};
+
+struct CSongInfo : CMusicInfo
+{
+ CSongInfo() : CMusicInfo(MediaTypeSong) {}
+};
+
+struct CMusicBrowse : CStaticContextMenuAction
+{
+ CMusicBrowse() : CStaticContextMenuAction(37015) {} // Browse into
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CMusicPlay : CStaticContextMenuAction
+{
+ CMusicPlay() : CStaticContextMenuAction(208) {} // Play
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CMusicPlayNext : CStaticContextMenuAction
+{
+ CMusicPlayNext() : CStaticContextMenuAction(10008) {} // Play next
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CMusicQueue : CStaticContextMenuAction
+{
+ CMusicQueue() : CStaticContextMenuAction(13347) {} // Queue item
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+} // namespace CONTEXTMENU
diff --git a/xbmc/music/GUIViewStateMusic.cpp b/xbmc/music/GUIViewStateMusic.cpp
new file mode 100644
index 0000000..f14f501
--- /dev/null
+++ b/xbmc/music/GUIViewStateMusic.cpp
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2005-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 "GUIViewStateMusic.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/SortUtils.h"
+#include "utils/log.h"
+#include "view/ViewStateSettings.h"
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+
+PLAYLIST::Id CGUIViewStateWindowMusic::GetPlaylist() const
+{
+ return PLAYLIST::TYPE_MUSIC;
+}
+
+bool CGUIViewStateWindowMusic::AutoPlayNextItem()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ return settings->GetBool(CSettings::SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM) &&
+ !settings->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT);
+}
+
+std::string CGUIViewStateWindowMusic::GetLockType()
+{
+ return "music";
+}
+
+std::string CGUIViewStateWindowMusic::GetExtensions()
+{
+ return CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+}
+
+VECSOURCES& CGUIViewStateWindowMusic::GetSources()
+{
+ return CGUIViewState::GetSources();
+}
+
+CGUIViewStateMusicSearch::CGUIViewStateMusicSearch(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D", "%L", "%A")); // Title - Artist, Duration | Label, Artist
+ SetSortMethod(SortByTitle);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicSearch::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs"));
+}
+
+CGUIViewStateMusicDatabase::CGUIViewStateMusicDatabase(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE NodeType=dir.GetDirectoryChildType(items.GetPath());
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ std::string strAlbum = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_strMusicLibraryAlbumFormat;
+ if (strAlbum.empty())
+ strAlbum = "%B"; // album
+ CLog::Log(LOGDEBUG, "Custom album format = [{}]", strAlbum);
+ SortAttribute sortAttribute = SortAttributeNone;
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_OVERVIEW:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); // Filename, empty | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_TOP100:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); // Filename, empty | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_GENRE:
+ {
+ AddSortMethod(SortByGenre, 515, LABEL_MASKS("%F", "", "%G", "")); // Filename, empty | Genre, empty
+ SetSortMethod(SortByGenre);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ break;
+ case NODE_TYPE_ROLE:
+ {
+ AddSortMethod(SortByNone, 576, LABEL_MASKS("%F", "", "%G", "")); // Filename, empty | Genre, empty
+ SetSortMethod(SortByPlaycount);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_YEAR:
+ {
+ AddSortMethod(SortByLabel, 562, LABEL_MASKS("%F", "", "%Y", "")); // Filename, empty | Year, empty
+ SetSortMethod(SortByLabel);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ break;
+ case NODE_TYPE_ARTIST:
+ {
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", "%A", "")); // Filename, empty | Artist, empty
+ AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", "%A", "%a")); // Filename, empty | Artist, dateAdded
+ SetSortMethod(SortByArtist);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavartists");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_ALBUM:
+ {
+ // album
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined (default=%B), Artist
+ // artist
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined, Artist
+ // artist / year
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%F", "", strAlbum, "%A / %Y")); // Filename, empty | Userdefined, Artist / Year
+ // discs
+ AddSortMethod(
+ SortByTotalDiscs, sortAttribute, 38077,
+ LABEL_MASKS("%F", "", strAlbum, "%b")); // Filename, empty | Userdefined, Total discs
+ // year
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%F", "", strAlbum, "%Y")); // Filename, empty | Userdefined, Year
+ // original release year
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(
+ SortByOrigDate, sortAttribute, 38079,
+ LABEL_MASKS("%F", "", strAlbum, "%e")); // Filename, empty | Userdefined, Original date
+ // album date added
+ AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded
+ // play count
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count
+ // last played
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played
+ // rating
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%F", "", strAlbum, "%R")); // Filename, empty | Userdefined, Rating
+ // userrating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%F", "", strAlbum, "%r")); // Filename, empty | Userdefined, UserRating
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavalbums");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS(strTrack, "%a")); // Userdefined, dateAdded | empty, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ {
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ {
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+ }
+ break;
+ case NODE_TYPE_ALBUM_TOP100:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_SINGLES:
+ {
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+ // original release date (singles can be re-released)
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(SortByOrigDate, 38079,
+ LABEL_MASKS("%T - %A", "%e")); // Title, Artist, Original Date
+ AddSortMethod(SortByDateAdded, 570,
+ LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG:
+ {
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+ // original release date
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(SortByOrigDate, 38079,
+ LABEL_MASKS("%T - %A", "%e")); // Title, Artist, Original Date
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty
+ AddSortMethod(SortByBPM, 38080, LABEL_MASKS(strTrack, "%f")); // Userdefined, bpm, empty,empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ // the "All Albums" entries always default to SortByAlbum as this is most logical - user can always
+ // change it and the change will be saved for this particular path
+ if (dir.IsAllItem(items.GetPath()))
+ SetSortMethod(SortByAlbum);
+ else
+ SetSortMethod(viewState->m_sortDescription);
+
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_SONG_TOP100:
+ {
+ AddSortMethod(SortByNone, 576, LABEL_MASKS("%T - %A", "%V"));
+ SetSortMethod(SortByPlaycount);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_DISC:
+ {
+ AddSortMethod(SortByNone, 427, LABEL_MASKS("%L")); // Use the existing label
+ SetSortMethod(SortByNone);
+ }
+ break;
+ default:
+ break;
+ }
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicDatabase::SaveViewState()
+{
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE NodeType=dir.GetDirectoryChildType(m_items.GetPath());
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_ARTIST:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavartists"));
+ break;
+ case NODE_TYPE_ALBUM:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavalbums"));
+ break;
+ case NODE_TYPE_SINGLES:
+ case NODE_TYPE_SONG:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs"));
+ break;
+ default:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV);
+ break;
+ }
+}
+
+CGUIViewStateMusicSmartPlaylist::CGUIViewStateMusicSmartPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+
+ if (items.GetContent() == "songs" || items.GetContent() == "mixed")
+ {
+ std::string strTrack=settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title, Artist, Rating| empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(SortByOrigDate, 38079,
+ LABEL_MASKS("%T - %A", "%e")); // Title - Artist, original date, empty, empty
+ AddSortMethod(SortByBPM, 38080,
+ LABEL_MASKS("%T - %A", "%f")); // Title - Artist, bpm, empty, empty
+
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS(strTrack, "%D"));
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode);
+ }
+ else if (items.GetContent() == "albums")
+ {
+ std::string strAlbum = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_strMusicLibraryAlbumFormat;
+ if (strAlbum.empty())
+ strAlbum = "%B"; // album
+ // album
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined (default=%B), Artist
+ // artist
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined, Artist
+ // artist / year
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%F", "", strAlbum, "%A / %Y")); // Filename, empty | Userdefined, Artist / Year
+ // discs
+ AddSortMethod(
+ SortByTotalDiscs, sortAttribute, 38077,
+ LABEL_MASKS("%F", "", strAlbum, "%b")); // Filename, empty | Userdefined, Total discs
+ // year
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%F", "", strAlbum, "%Y"));
+ // original release date
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(
+ SortByOrigDate, 38079,
+ LABEL_MASKS("%F", "", strAlbum, "%e")); // Filename, empty | Userdefined, Original date
+ // album date added
+ AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded
+ // play count
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count
+ // last played
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played
+ // rating
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%F", "", strAlbum, "%R")); // Filename, empty | Userdefined, Rating
+ // userrating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%F", "", strAlbum, "%r")); // Filename, empty | Userdefined, UserRating
+
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS("%F", "", strAlbum, "%D"));
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+ }
+ else
+ {
+ CLog::Log(LOGERROR,"Music Smart Playlist must be one of songs, mixed or albums");
+ }
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicSmartPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs"));
+}
+
+CGUIViewStateMusicPlaylist::CGUIViewStateMusicPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title - Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating| empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ SetSortMethod(SortByPlaylistOrder);
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicfiles");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+CGUIViewStateWindowMusicNav::CGUIViewStateWindowMusicNav(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+
+ if (items.IsVirtualDirectoryRoot())
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ else if (items.GetPath() == "special://musicplaylists/")
+ { // playlists list sorts by label only, ignoring folders
+ AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551,
+ LABEL_MASKS("%F", "%D", "%L", "")); // Filename, Duration | Foldername, empty
+ SetSortMethod(SortByLabel);
+ }
+ else
+ {
+ if (items.IsVideoDb() && items.Size() > (settings->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS)?1:0))
+ {
+ XFILE::VIDEODATABASEDIRECTORY::CQueryParams params;
+ XFILE::CVideoDatabaseDirectory::GetQueryParams(items[settings->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS) ? 1 : 0]->GetPath(), params);
+ if (params.GetMVideoId() != -1)
+ {
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS("%T", "%Y")); // Filename, Duration | Foldername, empty
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y"));
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%Y"));
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y"));
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T", "%Y"));
+
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ }
+ else
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%F", "%D", "%L", "")); // Filename, Duration | Foldername, empty
+ SetSortMethod(SortByLabel);
+ }
+ }
+ else
+ {
+ //In navigation of music files tag data is scanned whenever present and can be used as sort criteria
+ //hence sort methods available are similar to song node (not the same as only tag data)
+ //Unfortunately anything here appears at all levels of file navigation even if no song files there.
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS(strTrack, "%D", "%L", ""), // Userdefined, Duration | FolderName, empty
+ settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%F", "%I", "%L", "%I")); // Filename, Size | Foldername, Size
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%F", "%J", "%L", "%J")); // Filename, Date | Foldername, Date
+ AddSortMethod(SortByFile, 561, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist(year), Title, Year| empty, empty
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+
+ SetSortMethod(SortByLabel);
+ }
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateWindowMusicNav::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateWindowMusicNav::AddOnlineShares()
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVirtualShares)
+ return;
+
+ VECSOURCES *musicSources = CMediaSourceSettings::GetInstance().GetSources("music");
+
+ for (int i = 0; i < (int)musicSources->size(); ++i)
+ {
+ CMediaSource share = musicSources->at(i);
+ }
+}
+
+VECSOURCES& CGUIViewStateWindowMusicNav::GetSources()
+{
+ // Setup shares we want to have
+ m_sources.clear();
+ CFileItemList items;
+
+ CDirectory::GetDirectory("library://music/", items, "", DIR_FLAG_DEFAULTS);
+ for (int i=0; i<items.Size(); ++i)
+ {
+ CFileItemPtr item=items[i];
+ CMediaSource share;
+ share.strName = item->GetLabel();
+ share.strPath = item->GetPath();
+ share.m_strThumbnailImage = item->GetArt("icon");
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ m_sources.push_back(share);
+ }
+
+ AddOnlineShares();
+
+ return CGUIViewStateWindowMusic::GetSources();
+}
+
+CGUIViewStateWindowMusicPlaylist::CGUIViewStateWindowMusicPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+
+ AddSortMethod(SortByNone, 551, LABEL_MASKS(strTrack, "%D", "%L", "")); // Userdefined, Duration | FolderName, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_PLAYLIST);
+}
+
+void CGUIViewStateWindowMusicPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_PLAYLIST);
+}
+
+PLAYLIST::Id CGUIViewStateWindowMusicPlaylist::GetPlaylist() const
+{
+ return PLAYLIST::TYPE_MUSIC;
+}
+
+bool CGUIViewStateWindowMusicPlaylist::AutoPlayNextItem()
+{
+ return false;
+}
+
+bool CGUIViewStateWindowMusicPlaylist::HideParentDirItems()
+{
+ return true;
+}
+
+VECSOURCES& CGUIViewStateWindowMusicPlaylist::GetSources()
+{
+ m_sources.clear();
+ // Playlist share
+ CMediaSource share;
+ share.strPath = "playlistmusic://";
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ m_sources.push_back(share);
+
+ // CGUIViewState::GetSources would add music plugins
+ return m_sources;
+}
diff --git a/xbmc/music/GUIViewStateMusic.h b/xbmc/music/GUIViewStateMusic.h
new file mode 100644
index 0000000..7f7311a
--- /dev/null
+++ b/xbmc/music/GUIViewStateMusic.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2005-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 "view/GUIViewState.h"
+
+class CGUIViewStateWindowMusic : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowMusic(const CFileItemList& items) : CGUIViewState(items) {}
+protected:
+ VECSOURCES& GetSources() override;
+ PLAYLIST::Id GetPlaylist() const override;
+ bool AutoPlayNextItem() override;
+ std::string GetLockType() override;
+ std::string GetExtensions() override;
+};
+
+class CGUIViewStateMusicSearch : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicSearch(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateMusicDatabase : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicDatabase(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateMusicSmartPlaylist : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicSmartPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateMusicPlaylist : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowMusicNav : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateWindowMusicNav(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ VECSOURCES& GetSources() override;
+
+private:
+ void AddOnlineShares();
+};
+
+class CGUIViewStateWindowMusicPlaylist : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateWindowMusicPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ PLAYLIST::Id GetPlaylist() const override;
+ bool AutoPlayNextItem() override;
+ bool HideParentDirItems() override;
+ VECSOURCES& GetSources() override;
+};
diff --git a/xbmc/music/MusicDatabase.cpp b/xbmc/music/MusicDatabase.cpp
new file mode 100644
index 0000000..91ff208
--- /dev/null
+++ b/xbmc/music/MusicDatabase.cpp
@@ -0,0 +1,13822 @@
+/*
+ * Copyright (C) 2005-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 "MusicDatabase.h"
+
+#include "Album.h"
+#include "Artist.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "Song.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Scraper.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h"
+#include "dbwrappers/dataset.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "filesystem/File.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/tags/MusicInfoTag.h"
+#include "network/Network.h"
+#include "network/cddb.h"
+#include "playlists/SmartPlayList.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/MathUtils.h"
+#include "utils/Random.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+using namespace MUSIC_INFO;
+
+using ADDON::AddonPtr;
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+#define RECENTLY_PLAYED_LIMIT 25
+#define MIN_FULL_SEARCH_LENGTH 3
+
+#ifdef HAS_DVD_DRIVE
+using namespace CDDB;
+using namespace MEDIA_DETECT;
+#endif
+
+static void AnnounceRemove(const std::string& content, int id)
+{
+ CVariant data;
+ data["type"] = content;
+ data["id"] = id;
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ data["transaction"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data);
+}
+
+static void AnnounceUpdate(const std::string& content, int id, bool added = false)
+{
+ CVariant data;
+ data["type"] = content;
+ data["id"] = id;
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ data["transaction"] = true;
+ if (added)
+ data["added"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data);
+}
+
+CMusicDatabase::CMusicDatabase(void)
+{
+ m_translateBlankArtist = true;
+}
+
+CMusicDatabase::~CMusicDatabase(void)
+{
+ EmptyCache();
+}
+
+bool CMusicDatabase::Open()
+{
+ return CDatabase::Open(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic);
+}
+
+void CMusicDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create artist table");
+ m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, "
+ " strArtist varchar(256), strMusicBrainzArtistID text, "
+ " strSortName text, "
+ " strType text, strGender text, strDisambiguation text, "
+ " strBorn text, strFormed text, strGenres text, strMoods text, "
+ " strStyles text, strInstruments text, strBiography text, "
+ " strDied text, strDisbanded text, strYearsActive text, "
+ " strImage text, "
+ " lastScraped varchar(20) default NULL, "
+ " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
+ " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+ // Create missing artist tag artist [Missing].
+ std::string strSQL =
+ PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
+ "VALUES( %i, '%s', '%s', '%s' )",
+ BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_NAME.c_str(),
+ BLANKARTIST_FAKEMUSICBRAINZID.c_str());
+ m_pDS->exec(strSQL);
+
+ CLog::Log(LOGINFO, "create album table");
+ m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strReleaseGroupMBID text, "
+ " strArtistDisp text, strArtistSort text, strGenres text, "
+ " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
+ " bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " strReleaseStatus TEXT, "
+ " fRating FLOAT NOT NULL DEFAULT 0, "
+ " iVotes INTEGER NOT NULL DEFAULT 0, "
+ " iUserrating INTEGER NOT NULL DEFAULT 0, "
+ " lastScraped varchar(20) default NULL, "
+ " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ " strReleaseType text, "
+ " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
+ " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
+ " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
+ " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+
+ CLog::Log(LOGINFO, "create audiobook table");
+ m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
+ " strBook varchar(256), strAuthor text,"
+ " bookmark integer, file text,"
+ " dateAdded varchar (20) default NULL)");
+
+ CLog::Log(LOGINFO, "create album_artist table");
+ m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, "
+ "strArtist text)");
+
+ CLog::Log(LOGINFO, "create album_source table");
+ m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
+
+ CLog::Log(LOGINFO, "create genre table");
+ m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
+
+ CLog::Log(LOGINFO, "create path table");
+ m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
+
+ CLog::Log(LOGINFO, "create source table");
+ m_pDS->exec(
+ "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
+
+ CLog::Log(LOGINFO, "create source_path table");
+ m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
+
+ CLog::Log(LOGINFO, "create song table");
+ m_pDS->exec("CREATE TABLE song (idSong integer primary key, "
+ " idAlbum integer, idPath integer, "
+ " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
+ " iTrack integer, iDuration integer, "
+ " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
+ " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
+ " lastplayed varchar(20) default NULL, "
+ " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ " userrating INTEGER NOT NULL DEFAULT 0, "
+ " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
+ " iBitRate INTEGER NOT NULL DEFAULT 0, "
+ " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
+ " strReplayGain text, "
+ " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+ CLog::Log(LOGINFO, "create song_artist table");
+ m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder "
+ "integer, strArtist text)");
+ CLog::Log(LOGINFO, "create song_genre table");
+ m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
+
+ CLog::Log(LOGINFO, "create role table");
+ m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
+ m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role
+
+ CLog::Log(LOGINFO, "create infosetting table");
+ m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, "
+ "strScraperPath TEXT, strSettings TEXT)");
+
+ CLog::Log(LOGINFO, "create discography table");
+ m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
+ "strReleaseGroupMBID TEXT)");
+
+ CLog::Log(LOGINFO, "create art table");
+ m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, "
+ "media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
+
+ CLog::Log(LOGINFO, "create versiontagscan table");
+ m_pDS->exec("CREATE TABLE versiontagscan "
+ "(idVersion INTEGER, iNeedsScan INTEGER, "
+ "lastscanned VARCHAR(20), "
+ "lastcleaned VARCHAR(20), "
+ "artistlinksupdated VARCHAR(20), "
+ "genresupdated VARCHAR(20))");
+ m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
+ GetSchemaVersion()));
+
+ CLog::Log(LOGINFO, "create removed_link table");
+ m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
+}
+
+void CMusicDatabase::CreateAnalytics()
+{
+ CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
+ m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
+ m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
+
+ m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
+
+ m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
+ m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
+ m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
+
+ m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))");
+
+ m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
+ m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
+
+ m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))");
+ m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
+ m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
+ m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
+ m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
+ //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
+ m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
+ m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
+ m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
+ m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
+ m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
+
+ m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))");
+
+ m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
+
+ m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
+
+ CLog::Log(LOGINFO, "create triggers");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
+ " DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
+ " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
+ " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
+ " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
+ " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
+ " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
+ " DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
+ " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
+ " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
+ " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
+ " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
+ " DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
+ " DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
+ " END");
+
+ /* Maintain date new and last modified for songs, albums and artists using triggers
+ MySQL triggers cannot modify a table that is already being used by the statement that invoked
+ the trigger (to avoid recursion), but can set NEW column values before insert or update.
+ Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
+ Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
+ // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
+ */
+ bool bisMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
+
+ if (!bisMySQL)
+ { // SQLite trigger syntax - AFTER INSERT/UPDATE
+ m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
+ " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
+ " AND NEW.dateNew IS NULL;"
+ " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
+ " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
+ " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
+ " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
+ " AND NEW.dateNew IS NULL;"
+ " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
+ " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
+ " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
+ " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
+ " AND NEW.dateNew IS NULL;"
+ " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
+ " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
+ " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
+ " END");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
+ " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
+ " END");
+ }
+ else
+ { // MySQL trigger syntax - BEFORE INSERT/UPDATE
+ m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
+ " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
+ " SET NEW.dateModified = now();"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
+ " SET NEW.dateModified = now()");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
+ " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
+ " SET NEW.dateModified = now();"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
+ " SET NEW.dateModified = now()");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
+ " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
+ " SET NEW.dateModified = now();"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
+ " SET NEW.dateModified = now()");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
+ " UPDATE versiontagscan SET genresupdated = now()");
+ }
+
+ // Triggers to maintain recent changes to album and song artist links in removed_link table
+ m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
+ "DELETE FROM removed_link "
+ "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
+ "DELETE FROM removed_link "
+ "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
+ "END");
+ CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
+
+ // Create native functions stored in DB (MySQL/MariaDB only)
+ CreateNativeDBFunctions();
+
+ // we create views last to ensure all indexes are rolled in
+ CreateViews();
+}
+
+void CMusicDatabase::CreateRemovedLinkTriggers()
+{
+ // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
+ m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
+ " INSERT INTO removed_link (idArtist, idMedia, idRole)"
+ " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
+ " INSERT INTO removed_link (idArtist, idMedia, idRole)"
+ " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
+ " END");
+}
+
+
+void CMusicDatabase::CreateViews()
+{
+ CLog::Log(LOGINFO, "create song view");
+ m_pDS->exec("CREATE VIEW songview AS SELECT "
+ " song.idSong AS idSong, "
+ " song.strArtistDisp AS strArtists,"
+ " song.strArtistSort AS strArtistSort,"
+ " song.strGenres AS strGenres,"
+ " strTitle, "
+ " iTrack, iDuration, "
+ " song.strReleaseDate as strReleaseDate, "
+ " song.strOrigReleaseDate as strOrigReleaseDate, "
+ " song.strDiscSubtitle as strDiscSubtitle, "
+ " strFileName, "
+ " strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " lastplayed, "
+ " song.rating, "
+ " song.userrating, "
+ " song.votes, "
+ " comment, "
+ " song.idAlbum AS idAlbum, "
+ " strAlbum, "
+ " strPath, "
+ " album.strReleaseStatus as strReleaseStatus,"
+ " album.bCompilation AS bCompilation,"
+ " album.bBoxedSet AS bBoxedSet, "
+ " album.strArtistDisp AS strAlbumArtists,"
+ " album.strArtistSort AS strAlbumArtistSort,"
+ " album.strReleaseType AS strAlbumReleaseType,"
+ " song.mood as mood,"
+ " song.strReplayGain, "
+ " iBPM, "
+ " iBitRate, "
+ " iSampleRate, "
+ " iChannels, "
+ " album.iAlbumDuration AS iAlbumDuration, "
+ " album.iDiscTotal as iDiscTotal, "
+ " song.dateAdded as dateAdded, "
+ " song.dateNew AS dateNew, "
+ " song.dateModified AS dateModified "
+ "FROM song"
+ " JOIN album ON"
+ " song.idAlbum=album.idAlbum"
+ " JOIN path ON"
+ " song.idPath=path.idPath");
+
+ CLog::Log(LOGINFO, "create album view");
+ m_pDS->exec("CREATE VIEW albumview AS SELECT "
+ "album.idAlbum AS idAlbum, "
+ "strAlbum, "
+ "strMusicBrainzAlbumID, "
+ "strReleaseGroupMBID, "
+ "album.strArtistDisp AS strArtists, "
+ "album.strArtistSort AS strArtistSort, "
+ "album.strGenres AS strGenres, "
+ "album.strReleaseDate as strReleaseDate, "
+ "album.strOrigReleaseDate as strOrigReleaseDate, "
+ "album.bBoxedSet AS bBoxedSet, "
+ "album.strMoods AS strMoods, "
+ "album.strStyles AS strStyles, "
+ "strThemes, "
+ "strReview, "
+ "strLabel, "
+ "strType, "
+ "strReleaseStatus, "
+ "album.strImage as strImage, "
+ "album.fRating, "
+ "album.iUserrating, "
+ "album.iVotes, "
+ "bCompilation, "
+ "bScrapedMBID,"
+ "lastScraped,"
+ "dateAdded, dateNew, dateModified, "
+ "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
+ "strReleaseType, "
+ "iDiscTotal, "
+ "(SELECT MAX(song.lastplayed) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
+ "iAlbumDuration "
+ "FROM album");
+
+ CLog::Log(LOGINFO, "create artist view");
+ m_pDS->exec("CREATE VIEW artistview AS SELECT"
+ " idArtist, strArtist, strSortName, "
+ " strMusicBrainzArtistID, "
+ " strType, strGender, strDisambiguation, "
+ " strBorn, strFormed, strGenres,"
+ " strMoods, strStyles, strInstruments, "
+ " strBiography, strDied, strDisbanded, "
+ " strYearsActive, strImage, "
+ " bScrapedMBID, lastScraped, "
+ " dateAdded, dateNew, dateModified "
+ "FROM artist");
+
+ CLog::Log(LOGINFO, "create albumartist view");
+ m_pDS->exec("CREATE VIEW albumartistview AS SELECT"
+ " album_artist.idAlbum AS idAlbum, "
+ " album_artist.idArtist AS idArtist, "
+ " 0 AS idRole, "
+ " 'AlbumArtist' AS strRole, "
+ " artist.strArtist AS strArtist, "
+ " artist.strSortName AS strSortName,"
+ " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
+ " album_artist.iOrder AS iOrder "
+ "FROM album_artist "
+ "JOIN artist ON "
+ " album_artist.idArtist = artist.idArtist");
+
+ CLog::Log(LOGINFO, "create songartist view");
+ m_pDS->exec("CREATE VIEW songartistview AS SELECT"
+ " song_artist.idSong AS idSong, "
+ " song_artist.idArtist AS idArtist, "
+ " song_artist.idRole AS idRole, "
+ " role.strRole AS strRole, "
+ " artist.strArtist AS strArtist, "
+ " artist.strSortName AS strSortName,"
+ " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
+ " song_artist.iOrder AS iOrder "
+ "FROM song_artist "
+ "JOIN artist ON "
+ " song_artist.idArtist = artist.idArtist "
+ "JOIN role ON "
+ " song_artist.idRole = role.idRole");
+}
+
+void CMusicDatabase::CreateNativeDBFunctions()
+{
+ // Create native functions in MySQL/MariaDB database only
+ if (!StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ return;
+ CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions");
+ /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
+ default utf8_general_ci collation that SQLite does via a collation sequence callback
+ function to StringUtils::AlphaNumericCompare
+ !@todo: the video needs these defined too for sorting in DB, then creation can be made common
+ */
+ // clang-format off
+ // udfFirstNumberPos finds the position of the first digit in a string
+ m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
+ m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
+ "RETURNS int \n"
+ "LANGUAGE SQL \n"
+ "DETERMINISTIC \n"
+ "NO SQL \n"
+ "SQL SECURITY INVOKER \n"
+ "BEGIN \n"
+ " DECLARE position int; \n"
+ " DECLARE tmppos int; \n"
+ " SET position = 5000; \n"
+ " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " IF(position = 5000) THEN RETURN 0; END IF;\n"
+ " RETURN position; \n"
+ "END\n");
+
+ // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input
+ m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
+ m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
+ "RETURNS varchar(1024)\n"
+ "LANGUAGE SQL\n"
+ "DETERMINISTIC\n"
+ "NO SQL\n"
+ "SQL SECURITY INVOKER\n"
+ "BEGIN\n"
+ " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
+ " DECLARE i int;\n"
+ " DECLARE symbolCharsLen int;\n"
+ " DECLARE symbol char(1);\n"
+ " SET sortString = instring;\n"
+ " SET i = 1;\n"
+ " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
+ " WHILE(i <= symbolCharsLen) DO\n"
+ " SET symbol = SUBSTRING(symbolChars, i, 1);\n"
+ " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
+ " SET i = i + 1;\n"
+ " END WHILE;\n"
+ " RETURN sortString;\n"
+ "END\n");
+
+ // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
+ m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
+ m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
+ "sameOrderChars char(25))\n"
+ "RETURNS varchar(1024)\n"
+ "LANGUAGE SQL\n"
+ "DETERMINISTIC\n"
+ "NO SQL\n"
+ "SQL SECURITY INVOKER\n"
+ "BEGIN\n"
+ " DECLARE sortString varchar(1024);\n"
+ " DECLARE shiftedString varchar(1024);\n"
+ " DECLARE inLength int;\n"
+ " DECLARE shiftedLength int;\n"
+ " DECLARE totalSympadLength int;\n"
+ " DECLARE symbolshifted512 varchar(1024);\n"
+ " DECLARE numStartIndex int; \n"
+ " DECLARE numEndIndex int; \n"
+ " DECLARE padLength int; \n"
+ " DECLARE totalPadLength int; \n"
+ " DECLARE i int; \n"
+ " DECLARE sameOrderCharsLen int;\n"
+ " SET totalPadLength = 0; \n"
+ " SET instring = TRIM(instring);\n"
+ " SET inLength = CHAR_LENGTH(inString);\n"
+ " SET sortString = instring; \n"
+ " SET numStartIndex = udfFirstNumberPos(instring); \n"
+ " SET numEndIndex = 0; \n"
+ " SET i = 1; \n"
+ " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
+ " WHILE(i <= sameOrderCharsLen) DO \n"
+ " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
+ " SET i = i + 1; \n"
+ " END WHILE; \n"
+ " WHILE(numStartIndex <> 0) DO \n"
+ " SET numStartIndex = numStartIndex + numEndIndex; \n"
+ " SET numEndIndex = numStartIndex; \n"
+ " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
+ " SET numEndIndex = numEndIndex + 1; \n"
+ " END WHILE; \n"
+ " SET numEndIndex = numEndIndex - 1; \n"
+ " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
+ " IF padLength < 0 THEN \n"
+ " SET padLength = 0; \n"
+ " END IF; \n"
+ " IF inLength + totalPadLength + padlength > 1024 THEN \n"
+ " -- Padding more digits would be too long, pad this one just enough \n"
+ " SET padLength = 1024 - inLength - totalPadLength; \n"
+ " SET numStartIndex = 0; \n"
+ " END IF; \n"
+ " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
+ " SET totalPadLength = totalPadLength + padLength; \n"
+ " IF numStartIndex <> 0 THEN \n"
+ " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
+ " END IF; \n"
+ " END WHILE; \n"
+ " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
+ " -- when there is space as this could double string length. Note '\\' needs escaping \n"
+ " SET numStartIndex = 1; \n"
+ " SET numEndIndex = inLength + totalPadLength; \n"
+ " IF numEndIndex < 1024 THEN \n"
+ " SET shiftedLength = 0; \n"
+ " SET totalSympadLength = 0; \n"
+ " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
+ " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
+ " SET numStartIndex = numStartIndex + 512; \n"
+ " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
+ " IF totalSympadLength = 0 THEN \n"
+ " SET shiftedString = symbolshifted512; \n"
+ " ELSE \n"
+ " IF totalSympadLength + shiftedLength > 1024 THEN \n"
+ " SET shiftedLength = 1024 - totalSympadLength; \n"
+ " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
+ " END IF; \n"
+ " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
+ " END IF; \n"
+ " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
+ " END WHILE; \n"
+ " SET sortString = shiftedString; \n"
+ " END IF; \n"
+ " RETURN sortString; \n"
+ "END\n");
+ // clang-format on
+}
+
+void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath,
+ std::string& strPath,
+ std::string& strFileName)
+{
+ URIUtils::Split(strFileNameAndPath, strPath, strFileName);
+ // Keep protocol options as part of the path
+ if (URIUtils::IsURL(strFileNameAndPath))
+ {
+ CURL url(strFileNameAndPath);
+ if (!url.GetProtocolOptions().empty())
+ strPath += "|" + url.GetProtocolOptions();
+ }
+}
+
+bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource)
+{
+ BeginTransaction();
+ SetLibraryLastUpdated();
+
+ album.idAlbum = AddAlbum(album.strAlbum, //
+ album.strMusicBrainzAlbumID, //
+ album.strReleaseGroupMBID, //
+ album.GetAlbumArtistString(), //
+ album.GetAlbumArtistSort(), //
+ album.GetGenreString(), //
+ album.strReleaseDate, //
+ album.strOrigReleaseDate, //
+ album.bBoxedSet, //
+ album.strLabel, //
+ album.strType, //
+ album.strReleaseStatus, //
+ album.bCompilation, //
+ album.releaseType);
+
+ // Add the album artists
+ // Album must have at least one artist so set artist to [Missing]
+ if (album.artistCredits.empty())
+ AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
+ for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName());
+ AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
+ static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
+ }
+
+ // Add songs
+ for (auto song = album.songs.begin(); song != album.songs.end(); ++song)
+ {
+ song->idAlbum = album.idAlbum;
+
+ song->idSong = AddSong(song->idSong, //
+ song->dateNew, //
+ song->idAlbum, //
+ song->strTitle, //
+ song->strMusicBrainzTrackID, //
+ song->strFileName, //
+ song->strComment, //
+ song->strMood, //
+ song->strThumb, //
+ song->GetArtistString(), //
+ song->GetArtistSort(), //
+ song->genre, //
+ song->iTrack, //
+ song->iDuration, //
+ song->strReleaseDate, //
+ song->strOrigReleaseDate, //
+ song->strDiscSubtitle, //
+ song->iTimesPlayed, //
+ song->iStartOffset, song->iEndOffset, //
+ song->lastPlayed, //
+ song->rating, //
+ song->userrating, //
+ song->votes, //
+ song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels, //
+ song->replayGain);
+
+ // Song must have at least one artist so set artist to [Missing]
+ if (song->artistCredits.empty())
+ AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
+
+ for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName());
+ AddSongArtist(
+ artistCredit->idArtist, song->idSong, ROLE_ARTIST,
+ artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
+ static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit)));
+ }
+ // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
+ // and use COMPOSERSORT tag data to provide sort names for artists that are composers
+ AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort());
+ }
+
+ // Set album duration as total of all songs on album.
+ // Folder layout may mean AddAlbum call has added more songs to an existing album
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum);
+ int albumDuration = GetSingleValueInt(strSQL);
+ m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration,
+ album.idAlbum));
+
+ // Add album sources
+ if (idSource > 0)
+ AddAlbumSource(album.idAlbum, idSource);
+ else
+ {
+ // Use album path, or failing that song paths to determine sources for the album
+ AddAlbumSources(album.idAlbum, album.strPath);
+ }
+
+ for (const auto& albumArt : album.art)
+ SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second);
+
+ // Set album disc total
+ m_pDS->exec(
+ PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
+ album.idAlbum));
+ // Set a non-compilation album as a boxset if it has three or more distinct disc titles
+ if (!album.bBoxedSet && !album.bCompilation)
+ {
+ strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
+ album.idAlbum);
+ int numTitles = GetSingleValueInt(strSQL);
+ if (numTitles >= 3)
+ {
+ strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum);
+ m_pDS->exec(strSQL);
+ }
+ }
+ m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
+ "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
+ album.idAlbum));
+ m_pDS->exec(
+ PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
+ "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
+ album.idAlbum));
+
+ std::string albumdateadded =
+ GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum));
+ m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
+ albumdateadded.c_str(), album.idAlbum));
+
+ /* Update artist dateAdded values for artists involved in album as song or album artists.
+ Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
+ derived from song dateadded values which are usually file dates (or the last scan).
+ It is used to indicate those artists with recent media.
+ For artists that are neither album nor song artists (other roles only) dateadded will be null.
+ */
+ std::vector<std::string> artistIDs;
+ // Get distinct song and album artist IDs for this album
+ GetArtistsByAlbum(album.idAlbum, artistIDs);
+ std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")";
+ strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' "
+ "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
+ albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str());
+ m_pDS->exec(strSQL);
+
+ CommitTransaction();
+ return true;
+}
+
+bool CMusicDatabase::UpdateAlbum(CAlbum& album)
+{
+ BeginTransaction();
+ SetLibraryLastUpdated();
+
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ if (album.bBoxedSet)
+ {
+ bool isBoxset = IsAlbumBoxset(album.idAlbum);
+ if (!isBoxset)
+ { // not a boxset already so make sure we have enough discs & they all have titles
+ bool canBeBoxset = false;
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum);
+ int numDiscs = GetSingleValueInt(strSQL);
+ if (numDiscs >= 2)
+ {
+ canBeBoxset = true;
+ int discValue;
+ for (discValue = 1; discValue <= numDiscs; discValue++)
+ {
+ strSQL =
+ PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
+ "song.iTrack >> 16 = %i",
+ album.idAlbum, discValue);
+ std::string currentTitle = GetSingleValue(strSQL);
+ if (currentTitle.empty())
+ {
+ currentTitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discValue);
+ strSQL =
+ PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
+ "song.iTrack >> 16 = %i",
+ currentTitle.c_str(), album.idAlbum, discValue);
+ ExecuteQuery(strSQL);
+ }
+ }
+ }
+ if (!canBeBoxset && album.bBoxedSet)
+ {
+ CLog::Log(LOGINFO, "{} : Album with id [{}] does not meet the requirements for a boxset.",
+ __FUNCTION__, album.idAlbum);
+ album.bBoxedSet = false;
+ }
+ }
+ }
+ UpdateAlbum(album.idAlbum, album.strAlbum, album.strMusicBrainzAlbumID, //
+ album.strReleaseGroupMBID, //
+ album.GetAlbumArtistString(), album.GetAlbumArtistSort(), //
+ album.GetGenreString(), //
+ StringUtils::Join(album.moods, itemSeparator), //
+ StringUtils::Join(album.styles, itemSeparator), //
+ StringUtils::Join(album.themes, itemSeparator), //
+ album.strReview, //
+ album.thumbURL.GetData(), //
+ album.strLabel, //
+ album.strType, //
+ album.strReleaseStatus, //
+ album.fRating, album.iUserrating, album.iVotes, //
+ album.strReleaseDate, //
+ album.strOrigReleaseDate, //
+ album.bBoxedSet, //
+ album.bCompilation, //
+ album.releaseType, //
+ album.bScrapedMBID);
+
+ if (!album.bArtistSongMerge)
+ {
+ // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
+ for (const auto& artistCredit : album.artistCredits)
+ UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID());
+ }
+ else
+ {
+ // Replace the album artists with those scraped or set by JSON
+ DeleteAlbumArtistsByAlbum(album.idAlbum);
+ // Album must have at least one artist so set artist to [Missing]
+ if (album.artistCredits.empty())
+ AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
+ for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName(), true);
+ AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
+ static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
+ }
+ /* Replace the songs with those scraped or imported, but if new songs is empty
+ (such as when called from JSON) do not remove the original ones
+ Also updates nested data e.g. song artists, song genres and contributors
+ Do not check for artist link changes, that is done later for all songs and album
+ */
+ int albumDuration = 0;
+ for (auto& song : album.songs)
+ {
+ UpdateSong(song);
+ albumDuration += song.iDuration;
+ }
+ if (albumDuration > 0)
+ m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
+ albumDuration, album.idAlbum));
+ }
+
+ if (!album.art.empty())
+ SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
+
+ CheckArtistLinksChanged();
+
+ CommitTransaction();
+ return true;
+}
+
+void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal)
+{
+ // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
+ int iDate;
+ iDate = StringUtils::DateStringToYYYYMMDD(strRelease);
+ if (iDate < 0)
+ strRelease.clear();
+ iDate = StringUtils::DateStringToYYYYMMDD(strOriginal);
+ if (iDate < 0)
+ strOriginal.clear();
+ // Avoid missing release or original values unless both invalid or empty
+ if (!strRelease.empty() && strOriginal.empty())
+ strOriginal = strRelease;
+ else if (strRelease.empty() && !strOriginal.empty())
+ strRelease = strOriginal;
+}
+
+int CMusicDatabase::AddSong(const int idSong,
+ const CDateTime& dtDateNew,
+ const int idAlbum,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ std::string& strDiscSubtitle,
+ const int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels,
+ const ReplayGain& replayGain)
+{
+ int idNew = -1;
+ std::string strSQL;
+ try
+ {
+ // We need at least the title
+ if (strTitle.empty())
+ return -1;
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strPath, strFileName;
+ SplitPath(strPathAndFileName, strPath, strFileName);
+ int idPath = AddPath(strPath);
+
+ if (idSong <= 1)
+ {
+ if (!strMusicBrainzTrackID.empty())
+ strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
+ "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
+ idAlbum, iTrack, strMusicBrainzTrackID.c_str());
+ else
+ strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
+ "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
+ "AND strMusicBrainzTrackID IS NULL",
+ idAlbum, strFileName.c_str(), strTitle.c_str(), iTrack);
+
+ if (!m_pDS->query(strSQL))
+ return -1;
+ }
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+
+ // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
+ bool isBoxset = IsAlbumBoxset(idAlbum);
+ if (isBoxset && strDiscSubtitle.empty())
+ {
+ int discno = iTrack >> 16;
+ strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discno);
+ }
+
+ // Validate ISO8601 dates and ensure none missing
+ std::string strRelease = strReleaseDate;
+ std::string strOriginal = strOrigReleaseDate;
+ NormaliseSongDates(strRelease, strOriginal);
+
+ // Get dateAdded from music file timestamp
+ std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
+
+ strSQL = "INSERT INTO song ("
+ "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
+ "strTitle, iTrack, iDuration, "
+ "strReleaseDate, strOrigReleaseDate, iBPM, "
+ "iBitrate, iSampleRate, iChannels, "
+ "strDiscSubtitle, strFileName, dateAdded, "
+ "strMusicBrainzTrackID, strArtistSort, "
+ "iTimesPlayed, iStartOffset, iEndOffset, "
+ "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
+
+ if (idSong <= 0)
+ // Song ID is autoincremented and dateNew set by trigger
+ strSQL += PrepareSQL("VALUES (NULL, NULL, ");
+ else
+ //Reuse song Id and original date when the Id added
+ strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str());
+
+ strSQL +=
+ PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
+ idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration,
+ strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate,
+ iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str());
+
+ if (strMusicBrainzTrackID.empty())
+ strSQL += PrepareSQL(",NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str());
+ if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
+ strSQL += PrepareSQL(",NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", artistSort.c_str());
+
+ if (dtLastPlayed.IsValid())
+ strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", //
+ iTimesPlayed, iStartOffset, iEndOffset,
+ dtLastPlayed.GetAsDBDateTime().c_str(), //
+ static_cast<double>(rating), userrating, votes, //
+ strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
+ else
+ strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", //
+ iTimesPlayed, iStartOffset, iEndOffset, //
+ static_cast<double>(rating), userrating, votes, //
+ strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
+ m_pDS->exec(strSQL);
+ if (idSong <= 0)
+ idNew = (int)m_pDS->lastinsertid();
+ else
+ idNew = idSong;
+ }
+ else
+ {
+ idNew = m_pDS->fv("idSong").get_asInt();
+ m_pDS->close();
+ UpdateSong(idNew, //
+ strTitle, //
+ strMusicBrainzTrackID, //
+ strPathAndFileName, //
+ strComment, //
+ strMood, //
+ strThumb, //
+ artistDisp, //
+ artistSort, //
+ genres, //
+ iTrack, //
+ iDuration, //
+ strReleaseDate, //
+ strOrigReleaseDate, //
+ strDiscSubtitle, //
+ iTimesPlayed, //
+ iStartOffset, iEndOffset, //
+ dtLastPlayed, //
+ rating, userrating, votes, //
+ replayGain, //
+ iBPM, iBitRate, iSampleRate, iChannels);
+ }
+ if (!strThumb.empty())
+ SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb);
+
+ // Song genres added, and genre string updated to use the standardised genre names
+ AddSongGenres(idNew, genres);
+
+ AnnounceUpdate(MediaTypeSong, idNew, true);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addsong ({})", strSQL);
+ }
+ return idNew;
+}
+
+bool CMusicDatabase::GetSong(int idSong, CSong& song)
+{
+ try
+ {
+ song.Clear();
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
+ " JOIN songartistview ON songview.idSong = songartistview.idSong "
+ " WHERE songview.idSong = %i "
+ " ORDER BY songartistview.idRole, songartistview.iOrder",
+ idSong);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int songArtistOffset = song_enumCount;
+
+ song = GetSongFromDataset(m_pDS->get_sql_record());
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (idSongArtistRole == ROLE_ARTIST)
+ song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::UpdateSong(CSong& song, bool bArtists /*= true*/, bool bArtistLinks /*= true*/)
+{
+ int result = UpdateSong(song.idSong,
+ song.strTitle, //
+ song.strMusicBrainzTrackID, //
+ song.strFileName, //
+ song.strComment, //
+ song.strMood, //
+ song.strThumb, //
+ song.GetArtistString(), //
+ song.GetArtistSort(), //
+ song.genre, //
+ song.iTrack, //
+ song.iDuration, //
+ song.strReleaseDate, //
+ song.strOrigReleaseDate, //
+ song.strDiscSubtitle, //
+ song.iTimesPlayed, //
+ song.iStartOffset, song.iEndOffset, //
+ song.lastPlayed, //
+ song.rating, song.userrating, song.votes, //
+ song.replayGain, //
+ song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels);
+ if (result < 0)
+ return false;
+
+ // Replace Song genres and update genre string using the standardised genre names
+ AddSongGenres(song.idSong, song.genre);
+ if (bArtists)
+ {
+ //Replace song artists and contributors
+ DeleteSongArtistsBySong(song.idSong);
+ // Song must have at least one artist so set artist to [Missing]
+ if (song.artistCredits.empty())
+ AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
+ for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName());
+ AddSongArtist(artistCredit->idArtist, song.idSong, ROLE_ARTIST, artistCredit->GetArtist(),
+ static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit)));
+ }
+ // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
+ // and use COMPOSERSORT tag data to provide sort names for artists that are composers
+ AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort());
+
+ if (bArtistLinks)
+ CheckArtistLinksChanged();
+ }
+
+ return true;
+}
+
+int CMusicDatabase::UpdateSong(int idSong,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ const std::string& strDiscSubtitle,
+ int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ const ReplayGain& replayGain,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels)
+{
+ if (idSong < 0)
+ return -1;
+
+ std::string strSQL;
+ std::string strPath, strFileName;
+ SplitPath(strPathAndFileName, strPath, strFileName);
+ int idPath = AddPath(strPath);
+
+ // Validate ISO8601 dates and ensure none missing
+ std::string strRelease = strReleaseDate;
+ std::string strOriginal = strOrigReleaseDate;
+ NormaliseSongDates(strRelease, strOriginal);
+
+ std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
+
+ strSQL = PrepareSQL(
+ "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
+ " strTitle = '%s', iTrack = %i, iDuration = %i, "
+ "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
+ "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
+ "dateAdded = '%s'",
+ idPath, artistDisp.c_str(),
+ StringUtils::Join(
+ genres,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)
+ .c_str(),
+ strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(),
+ strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels,
+ strDateMedia.c_str());
+ if (strMusicBrainzTrackID.empty())
+ strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL");
+ else
+ strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str());
+ if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
+ strSQL += PrepareSQL(", strArtistSort = NULL");
+ else
+ strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str());
+
+ strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
+ "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
+ iStartOffset, iEndOffset, static_cast<double>(rating), userrating, votes,
+ strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
+
+ if (dtLastPlayed.IsValid())
+ strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
+ dtLastPlayed.GetAsDBDateTime().c_str());
+ else if (iTimesPlayed > 0)
+ strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
+ CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
+ else
+ strSQL += ", iTimesPlayed = 0, lastplayed = NULL ";
+ strSQL += PrepareSQL("WHERE idSong = %i", idSong);
+
+ bool status = ExecuteQuery(strSQL);
+
+ if (status)
+ AnnounceUpdate(MediaTypeSong, idSong);
+ return idSong;
+}
+
+int CMusicDatabase::AddAlbum(const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ const std::string& strRecordLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ if (!strMusicBrainzAlbumID.empty())
+ strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
+ strMusicBrainzAlbumID.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM album "
+ "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
+ "AND strMusicBrainzAlbumID IS NULL",
+ strArtist.c_str(), strAlbum.c_str());
+ m_pDS->query(strSQL);
+ std::string strCheckFlag = strType;
+ StringUtils::ToLower(strCheckFlag);
+ if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type
+ bBoxedSet = true;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // Does not exist, add it
+ strSQL =
+ PrepareSQL("INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
+ "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
+ "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, "
+ "strMusicBrainzAlbumID, "
+ "strReleaseGroupMBID, strArtistSort) "
+ "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
+ strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
+ strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet, //
+ strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), //
+ bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str());
+
+ if (strMusicBrainzAlbumID.empty())
+ strSQL += PrepareSQL(", NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str());
+ if (strReleaseGroupMBID.empty())
+ strSQL += PrepareSQL(", NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str());
+ if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
+ strSQL += PrepareSQL(", NULL");
+ else
+ strSQL += PrepareSQL(", '%s'", strArtistSort.c_str());
+ strSQL += ")";
+ m_pDS->exec(strSQL);
+
+ return (int)m_pDS->lastinsertid();
+ }
+ else
+ {
+ /* Exists in our database and being re-scanned from tags, so we should update it as the details
+ may have changed.
+
+ Note that for multi-folder albums this will mean the last folder scanned will have the information
+ stored for it. Most values here should be the same across all songs anyway, but it does mean
+ that if there's any inconsistencies then only the last folders information will be taken.
+
+ We make sure we clear out the link tables (album artists, album sources) and we reset
+ the last scraped time to make sure that online metadata is re-fetched. */
+ int idAlbum = m_pDS->fv("idAlbum").get_asInt();
+ m_pDS->close();
+
+ strSQL = "UPDATE album SET ";
+ if (!strMusicBrainzAlbumID.empty())
+ strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", //
+ strAlbum.c_str(), strArtist.c_str());
+ if (strReleaseGroupMBID.empty())
+ strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,");
+ else
+ strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str());
+ if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
+ strSQL += PrepareSQL(" strArtistSort = NULL");
+ else
+ strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str());
+
+ strSQL +=
+ PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
+ "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
+ "bCompilation=%i, strReleaseType = '%s', "
+ "lastScraped = NULL "
+ "WHERE idAlbum=%i",
+ strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
+ bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(),
+ bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(), //
+ idAlbum);
+ m_pDS->exec(strSQL);
+ DeleteAlbumArtistsByAlbum(idAlbum);
+ DeleteAlbumSources(idAlbum);
+ return idAlbum;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::UpdateAlbum(int idAlbum,
+ const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strThemes,
+ const std::string& strReview,
+ const std::string& strImage,
+ const std::string& strLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ float fRating,
+ int iUserrating,
+ int iVotes,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType,
+ bool bScrapedMBID)
+{
+ if (idAlbum < 0)
+ return -1;
+
+ // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
+ // Truncate value cleaning up xml when URLs exceeds this
+ std::string strImageURLs = strImage;
+ if (StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ TrimImageURLs(strImageURLs, 65535);
+
+ std::string strSQL;
+ strSQL = PrepareSQL("UPDATE album SET "
+ " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
+ " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
+ " strReview = '%s', strImage = '%s', strLabel = '%s', "
+ " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
+ " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
+ " bBoxedSet = %i, bCompilation = %i,"
+ " strReleaseType = '%s', strReleaseStatus = '%s', "
+ " lastScraped = '%s', bScrapedMBID = %i",
+ strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
+ strMoods.c_str(), strStyles.c_str(), strThemes.c_str(), //
+ strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(), //
+ strType.c_str(), static_cast<double>(fRating), iUserrating, iVotes, //
+ strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
+ bBoxedSet, bCompilation, //
+ CAlbum::ReleaseTypeToString(releaseType).c_str(), strReleaseStatus.c_str(), //
+ CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), bScrapedMBID);
+ if (strMusicBrainzAlbumID.empty())
+ strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL");
+ else
+ strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str());
+ if (strReleaseGroupMBID.empty())
+ strSQL += PrepareSQL(", strReleaseGroupMBID = NULL");
+ else
+ strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str());
+ if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
+ strSQL += PrepareSQL(", strArtistSort = NULL");
+ else
+ strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str());
+
+ strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum);
+
+ bool status = ExecuteQuery(strSQL);
+ if (status)
+ AnnounceUpdate(MediaTypeAlbum, idAlbum);
+ return idAlbum;
+}
+
+bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (idAlbum == -1)
+ return false; // not in the database
+
+ //Get album, song and album song info data using separate queries/datasets because we can have
+ //multiple roles per artist for songs and that makes a single combined join impractical
+ //Get album data
+ std::string sql;
+ sql = PrepareSQL("SELECT albumview.*,albumartistview.* "
+ " FROM albumview "
+ " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ " WHERE albumview.idAlbum = %ld "
+ " ORDER BY albumartistview.iOrder",
+ idAlbum);
+
+ CLog::Log(LOGDEBUG, "{}", sql);
+ if (!m_pDS->query(sql))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int albumArtistOffset = album_enumCount;
+
+ album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0,
+ true); // true to grab and parse the imageURL
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
+ // so there is only one row in the result set for each artist credit.
+ album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ //Get song data
+ if (getSongs)
+ {
+ sql = PrepareSQL("SELECT songview.*, songartistview.*"
+ " FROM songview "
+ " JOIN songartistview ON songview.idSong = songartistview.idSong "
+ " WHERE songview.idAlbum = %ld "
+ " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder",
+ idAlbum);
+
+ CLog::Log(LOGDEBUG, "{}", sql);
+ if (!m_pDS->query(sql))
+ return false;
+ if (m_pDS->num_rows() == 0) //Album with no songs
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int songArtistOffset = song_enumCount;
+ std::set<int> songs;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSong = record->at(song_idSong).get_asInt(); //Same as songartist.idSong by join
+ if (songs.find(idSong) == songs.end())
+ {
+ album.songs.emplace_back(GetSongFromDataset(record));
+ songs.insert(idSong);
+ }
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ //By query order song is the last one appended to the album song vector.
+ if (idSongArtistRole == ROLE_ARTIST)
+ album.songs.back().artistCredits.emplace_back(
+ GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum)
+{
+ std::string strSQL =
+ PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum)
+{
+ std::string strSQL =
+ PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum);
+ return GetSingleValue(strSQL).empty();
+}
+
+int CMusicDatabase::AddGenre(std::string& strGenre)
+{
+ std::string strSQL;
+ try
+ {
+ StringUtils::Trim(strGenre);
+
+ if (strGenre.empty())
+ strGenre = g_localizeStrings.Get(13205); // Unknown
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ auto it = m_genreCache.find(strGenre);
+ if (it != m_genreCache.end())
+ return it->second;
+
+
+ strSQL = PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'",
+ strGenre.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )",
+ strGenre.c_str());
+ m_pDS->exec(strSQL);
+
+ int idGenre = (int)m_pDS->lastinsertid();
+ m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
+ return idGenre;
+ }
+ else
+ {
+ int idGenre = m_pDS->fv("idGenre").get_asInt();
+ strGenre = m_pDS->fv("strGenre").get_asString();
+ m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
+ m_pDS->close();
+ return idGenre;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addgenre ({})", strSQL);
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::UpdateArtist(const CArtist& artist)
+{
+ SetLibraryLastUpdated();
+
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ UpdateArtist(artist.idArtist, //
+ artist.strArtist, //
+ artist.strSortName, //
+ artist.strMusicBrainzArtistID, //
+ artist.bScrapedMBID, //
+ artist.strType, //
+ artist.strGender, //
+ artist.strDisambiguation, //
+ artist.strBorn, //
+ artist.strFormed, //
+ StringUtils::Join(artist.genre, itemSeparator), //
+ StringUtils::Join(artist.moods, itemSeparator), //
+ StringUtils::Join(artist.styles, itemSeparator), //
+ StringUtils::Join(artist.instruments, itemSeparator), //
+ artist.strBiography, //
+ artist.strDied, //
+ artist.strDisbanded, //
+ StringUtils::Join(artist.yearsActive, itemSeparator).c_str(), //
+ artist.thumbURL.GetData());
+
+ DeleteArtistDiscography(artist.idArtist);
+ for (const auto& disc : artist.discography)
+ {
+ AddArtistDiscography(artist.idArtist, disc);
+ }
+
+ // Set current artwork (held in art table)
+ if (!artist.art.empty())
+ SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
+
+ return true;
+}
+
+int CMusicDatabase::AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ const std::string& strSortName,
+ bool bScrapedMBID /* = false*/)
+{
+ std::string strSQL;
+ int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID);
+ if (idArtist < 0 || strSortName.empty())
+ return idArtist;
+
+ /* Artist sort name always taken as the first value provided that is different from name, so only
+ update when current sort name is blank. If a new sortname the same as name is provided then
+ clear any sortname currently held.
+ */
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist);
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ std::string strArtistName, strArtistSort;
+ strArtistName = m_pDS->fv("strArtist").get_asString();
+ strArtistSort = m_pDS->fv("strSortName").get_asString();
+ m_pDS->close();
+
+ if (!strArtistSort.empty())
+ {
+ if (strSortName.compare(strArtistName) == 0)
+ m_pDS->exec(
+ PrepareSQL("UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist));
+ }
+ else if (strSortName.compare(strArtistName) != 0)
+ m_pDS->exec(PrepareSQL("UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
+ strSortName.c_str(), idArtist));
+
+ return idArtist;
+ }
+
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname ({})", strSQL);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ bool bScrapedMBID /* = false*/)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // 1) MusicBrainz
+ if (!strMusicBrainzArtistID.empty())
+ {
+ // 1.a) Match on a MusicBrainz ID
+ strSQL =
+ PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
+ strMusicBrainzArtistID.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0;
+ m_pDS->close();
+ if (update)
+ {
+ strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' "
+ "WHERE idArtist = %i",
+ strArtist.c_str(), idArtist);
+ m_pDS->exec(strSQL);
+ m_pDS->close();
+ }
+ return idArtist;
+ }
+ m_pDS->close();
+
+
+ // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
+ // and update that if it exists.
+ strSQL = PrepareSQL("SELECT idArtist FROM artist "
+ "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
+ strArtist.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+ // 1.b.a) We found an artist by name but with no MusicBrainz ID set, update it and assume it is our artist, flag when mbid scraped
+ strSQL =
+ PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
+ "bScrapedMBID = %i WHERE idArtist = %i",
+ strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID, idArtist);
+ m_pDS->exec(strSQL);
+ return idArtist;
+ }
+
+ // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
+ // With MusicBrainz IDs this could return multiple artists and is non-determinstic
+ // Always pick the first artist ID returned by the DB to return.
+ }
+ else
+ {
+ strSQL =
+ PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
+
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+ return idArtist;
+ }
+ m_pDS->close();
+ }
+
+ // 3) No artist exists at all - add it, flagging when has scraped mbid
+ if (strMusicBrainzArtistID.empty())
+ strSQL = PrepareSQL("INSERT INTO artist "
+ "(idArtist, strArtist, strMusicBrainzArtistID) "
+ "VALUES( NULL, '%s', NULL)",
+ strArtist.c_str());
+ else
+ strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
+ "bScrapedMBID) "
+ "VALUES( NULL, '%s', '%s', %i )",
+ strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID);
+
+ m_pDS->exec(strSQL);
+ int idArtist = (int)m_pDS->lastinsertid();
+ return idArtist;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addartist ({})", strSQL);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::UpdateArtist(int idArtist,
+ const std::string& strArtist,
+ const std::string& strSortName,
+ const std::string& strMusicBrainzArtistID,
+ const bool bScrapedMBID,
+ const std::string& strType,
+ const std::string& strGender,
+ const std::string& strDisambiguation,
+ const std::string& strBorn,
+ const std::string& strFormed,
+ const std::string& strGenres,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strInstruments,
+ const std::string& strBiography,
+ const std::string& strDied,
+ const std::string& strDisbanded,
+ const std::string& strYearsActive,
+ const std::string& strImage)
+{
+ if (idArtist < 0)
+ return -1;
+
+ // Check another artist with this mbid not already exist (an alias for example)
+ bool useMBIDNull = strMusicBrainzArtistID.empty();
+ bool isScrapedMBID = bScrapedMBID;
+ std::string artistname;
+ int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
+ if (idArtistMbid > 0 && idArtistMbid != idArtist)
+ {
+ CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
+ __FUNCTION__, strMusicBrainzArtistID, artistname, idArtistMbid, strArtist, idArtist);
+ useMBIDNull = true;
+ isScrapedMBID = false;
+ }
+
+ // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
+ // Truncate value cleaning up xml when URLs exceeds this
+ std::string strImageURLs = strImage;
+ if (StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ TrimImageURLs(strImageURLs, 65535);
+
+ std::string strSQL;
+ strSQL = PrepareSQL("UPDATE artist SET "
+ " strArtist = '%s', "
+ " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
+ " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
+ " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
+ " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
+ " strYearsActive = '%s', strImage = '%s', "
+ " lastScraped = '%s', bScrapedMBID = %i",
+ strArtist.c_str(),
+ /* strSortName.c_str(),*/
+ /* strMusicBrainzArtistID.c_str(), */
+ strType.c_str(), strGender.c_str(), strDisambiguation.c_str(), //
+ strBorn.c_str(), strFormed.c_str(), strGenres.c_str(), //
+ strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(), //
+ strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(), //
+ strYearsActive.c_str(), strImageURLs.c_str(), //
+ CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID);
+ if (useMBIDNull)
+ strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL");
+ else
+ strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str());
+ if (strSortName.empty())
+ strSQL += PrepareSQL(", strSortName = NULL");
+ else
+ strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str());
+
+ strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist);
+
+ bool status = ExecuteQuery(strSQL);
+ if (status)
+ AnnounceUpdate(MediaTypeArtist, idArtist);
+ return idArtist;
+}
+
+bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist,
+ const std::string& strMusicBrainzArtistID)
+{
+ if (strMusicBrainzArtistID.empty() || idArtist < 0)
+ return false;
+
+ // Check another artist with this mbid not already exist (an alias for example)
+ std::string artistname;
+ int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
+ if (idArtistMbid > 0 && idArtistMbid != idArtist)
+ {
+ CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__,
+ strMusicBrainzArtistID, artistname, idArtistMbid);
+ return false;
+ }
+
+ // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
+ std::string strSQL;
+ strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
+ "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
+ strMusicBrainzArtistID.c_str(), idArtist);
+
+ bool status = ExecuteQuery(strSQL);
+ if (status)
+ {
+ AnnounceUpdate(MediaTypeArtist, idArtist);
+ return true;
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtist(int idArtist, CArtist& artist, bool fetchAll /* = false */)
+{
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (idArtist == -1)
+ return false; // not in the database
+
+ std::string strSQL;
+ if (fetchAll)
+ strSQL = PrepareSQL("SELECT * FROM artistview "
+ "LEFT JOIN discography ON artistview.idArtist = discography.idArtist "
+ "WHERE artistview.idArtist = %i",
+ idArtist);
+ else
+ strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int discographyOffset = artist_enumCount;
+
+ artist.discography.clear();
+ artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs
+ if (fetchAll)
+ {
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+ CDiscoAlbum discoAlbum;
+ discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString();
+ discoAlbum.strYear = record->at(discographyOffset + 2).get_asString();
+ discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString();
+ artist.discography.emplace_back(discoAlbum);
+ m_pDS->next();
+ }
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{0}({1}) - took {2} ms", __FUNCTION__, strSQL,
+ duration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetArtistExists(int idArtist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+
+ return false;
+}
+
+int CMusicDatabase::GetLastArtist()
+{
+ std::string strSQL = "SELECT MAX(idArtist) FROM artist";
+ std::string lastArtist = GetSingleValue(strSQL);
+ if (lastArtist.empty())
+ return -1;
+
+ return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10));
+}
+
+int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID,
+ std::string& artistname)
+{
+ if (strMusicBrainzArtistID.empty())
+ return -1;
+
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS2)
+ return -1;
+ // Match on MusicBrainz ID, definitively unique
+ strSQL =
+ PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
+ strMusicBrainzArtistID.c_str());
+ if (!m_pDS2->query(strSQL))
+ return -1;
+ int idArtist = -1;
+ if (m_pDS2->num_rows() > 0)
+ {
+ idArtist = m_pDS2->fv("idArtist").get_asInt();
+ artistname = m_pDS2->fv("strArtist").get_asString();
+ }
+ m_pDS2->close();
+ return idArtist;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::HasArtistBeenScraped(int idArtist)
+{
+ std::string strSQL = PrepareSQL(
+ "SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist);
+ return GetSingleValue(strSQL).empty();
+}
+
+bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist)
+{
+ std::string strSQL =
+ PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist);
+ return ExecuteQuery(strSQL);
+}
+
+int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum)
+{
+ std::string strSQL = PrepareSQL("INSERT INTO discography "
+ "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
+ "VALUES(%i, '%s', '%s', '%s')",
+ idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(),
+ discoAlbum.strReleaseGroupMBID.c_str());
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::DeleteArtistDiscography(int idArtist)
+{
+ std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ /* Combine entries from discography and album tables
+ Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of 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 tempDisco "
+ "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
+ m_pDS->exec("CREATE TABLE tempAlbum "
+ "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
+
+ std::string strSQL;
+ strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
+ "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
+ "strReleaseGroupMBID, NULL "
+ "FROM discography WHERE idArtist = %i",
+ idArtist);
+ m_pDS->exec(strSQL);
+
+ strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
+ "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
+ "strReleaseGroupMBID, album.idAlbum "
+ "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
+ "WHERE idArtist = %i",
+ idArtist);
+ m_pDS->exec(strSQL);
+
+ // Match albums on release group mbid, if multi-releases then first used
+ // Only use albums credited to this artist
+ strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
+ "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
+ m_pDS->exec(strSQL);
+ //Delete matched albums
+ strSQL = "DELETE FROM tempAlbum "
+ "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
+ m_pDS->exec(strSQL);
+
+ // Match remaining to albums by artist on title and year
+ strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
+ "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
+ "AND tempAlbum.strYear = tempDisco.strYear) "
+ "WHERE tempDisco.idAlbum is NULL";
+ m_pDS->exec(strSQL);
+ //Delete matched albums
+ strSQL = "DELETE FROM tempAlbum "
+ "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
+ m_pDS->exec(strSQL);
+
+ // Match remaining to albums by artist on title only
+ strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
+ "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
+ "WHERE tempDisco.idAlbum is NULL";
+ m_pDS->exec(strSQL);
+ // Use year from album table, when matched by title only as it could be different
+ strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
+ "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
+ "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
+ m_pDS->exec(strSQL);
+ //Delete matched albums
+ strSQL = "DELETE FROM tempAlbum "
+ "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
+ m_pDS->exec(strSQL);
+
+ // Combine distinctly with any remaining unmatched albums by artist
+ strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
+ "UNION "
+ "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
+ "ORDER BY strYear, strAlbum, idAlbum";
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ while (!m_pDS->eof())
+ {
+ int idAlbum = m_pDS->fv("idAlbum").get_asInt();
+ if (idAlbum == 0)
+ idAlbum = -1;
+ std::string strAlbum = m_pDS->fv("strAlbum").get_asString();
+ if (!strAlbum.empty())
+ {
+ CFileItemPtr pItem(new CFileItem(strAlbum));
+ pItem->SetLabel2(m_pDS->fv("strYear").get_asString());
+ pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum);
+ items.Add(pItem);
+ }
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+ m_pDS->exec("DROP TABLE tempDisco");
+ m_pDS->exec("DROP TABLE tempAlbum");
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->exec("DROP TABLE tempDisco");
+ m_pDS->exec("DROP TABLE tempAlbum");
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+int CMusicDatabase::AddRole(const std::string& strRole)
+{
+ int idRole = -1;
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+ strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idRole = m_pDS->fv("idRole").get_asInt();
+ m_pDS->close();
+
+ if (idRole < 0)
+ {
+ strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str());
+ m_pDS->exec(strSQL);
+ idRole = static_cast<int>(m_pDS->lastinsertid());
+ m_pDS->close();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to AddRole ({})", strSQL);
+ }
+ return idRole;
+}
+
+bool CMusicDatabase::AddSongArtist(
+ int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder)
+{
+ int idRole = AddRole(strRole);
+ return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder);
+}
+
+bool CMusicDatabase::AddSongArtist(
+ int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder)
+{
+ std::string strSQL;
+ strSQL = PrepareSQL("REPLACE INTO song_artist (idArtist, idSong, idRole, strArtist, iOrder) "
+ "VALUES(%i, %i, %i,'%s', %i)",
+ idArtist, idSong, idRole, strArtist.c_str(), iOrder);
+ return ExecuteQuery(strSQL);
+}
+
+int CMusicDatabase::AddSongContributor(int idSong,
+ const std::string& strRole,
+ const std::string& strArtist,
+ const std::string& strSort)
+{
+ if (strArtist.empty())
+ return -1;
+
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ int idArtist = -1;
+ // Add artist. As we only have name (no MBID) first try to identify artist from song
+ // as they may have already been added with a different role (including MBID).
+ strSQL =
+ PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ",
+ idSong, strArtist.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idArtist = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+
+ if (idArtist < 0)
+ idArtist = AddArtist(strArtist, "", strSort);
+
+ // Add to song_artist table
+ AddSongArtist(idArtist, idSong, strRole, strArtist, 0);
+
+ return idArtist;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor ({})", strSQL);
+ }
+
+ return -1;
+}
+
+void CMusicDatabase::AddSongContributors(int idSong,
+ const VECMUSICROLES& contributors,
+ const std::string& strSort)
+{
+ std::vector<std::string> composerSort;
+ size_t countComposer = 0;
+ if (!strSort.empty())
+ {
+ composerSort = StringUtils::Split(
+ strSort,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ }
+
+ for (const auto& credit : contributors)
+ {
+ std::string strSortName;
+ //Identify composer sort name if we have it
+ if (countComposer < composerSort.size())
+ {
+ if (credit.GetRoleDesc().compare("Composer") == 0)
+ {
+ strSortName = composerSort[countComposer];
+ countComposer++;
+ }
+ }
+ AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName);
+ }
+}
+
+int CMusicDatabase::GetRoleByName(const std::string& strRole)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("idRole").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL =
+ PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole "
+ "FROM song_artist JOIN role ON song_artist.idRole = role.idRole "
+ "WHERE idArtist = %i ORDER BY song_artist.idRole ASC",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ CVariant artistRoles(CVariant::VariantTypeArray);
+
+ while (!m_pDS->eof())
+ {
+ CVariant roleObj;
+ roleObj["role"] = m_pDS->fv("strRole").get_asString();
+ roleObj["roleid"] = m_pDS->fv("idrole").get_asInt();
+ artistRoles.push_back(roleObj);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("roles", artistRoles);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::DeleteSongArtistsBySong(int idSong)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong));
+}
+
+bool CMusicDatabase::AddAlbumArtist(int idArtist,
+ int idAlbum,
+ const std::string& strArtist,
+ int iOrder)
+{
+ std::string strSQL;
+ strSQL = PrepareSQL("REPLACE INTO album_artist (idArtist, idAlbum, strArtist, iOrder) "
+ "VALUES(%i,%i,'%s',%i)",
+ idArtist, idAlbum, strArtist.c_str(), iOrder);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum));
+}
+
+bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres)
+{
+ if (idSong == -1)
+ return true;
+
+ std::string strSQL;
+ try
+ {
+ // Clear current entries for song
+ strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong);
+ if (!ExecuteQuery(strSQL))
+ return false;
+ unsigned int index = 0;
+ std::vector<std::string> modgenres = genres;
+ for (auto& strGenre : modgenres)
+ {
+ int idGenre = AddGenre(strGenre); // Genre string trimmed and matched case-insensitively
+ strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
+ idGenre, idSong, index++);
+ if (!ExecuteQuery(strSQL))
+ return false;
+ }
+ // Update concatenated genre string from the standardised genre values
+ std::string strGenres = StringUtils::Join(
+ modgenres,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", //
+ strGenres.c_str(), idSong);
+ if (!ExecuteQuery(strSQL))
+ return false;
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) {} failed", __FUNCTION__, idSong, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int>& albums)
+{
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ albums.push_back(m_pDS->fv("idAlbum").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL;
+
+ strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ // Get album artist credits
+ VECARTISTCREDITS artistCredits;
+ while (!m_pDS->eof())
+ {
+ artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0));
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // Populate item with song albumartist credits
+ std::vector<std::string> musicBrainzID;
+ std::vector<std::string> albumartists;
+ CVariant artistidObj(CVariant::VariantTypeArray);
+ for (const auto& artistCredit : artistCredits)
+ {
+ artistidObj.push_back(artistCredit.GetArtistId());
+ albumartists.emplace_back(artistCredit.GetArtist());
+ if (!artistCredit.GetMusicBrainzArtistID().empty())
+ musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ item->GetMusicInfoTag()->SetAlbumArtist(albumartists);
+ item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID);
+ // Add song albumartistIds as separate property as not part of CMusicInfoTag
+ item->SetProperty("albumartistid", artistidObj);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs)
+{
+ try
+ {
+ std::string strSQL;
+ // Get distinct song and album artist IDs for this album, no other roles
+ // Allow for artists that are only album artists and not song artists
+ strSQL = PrepareSQL(
+ "SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
+ "UNION \n"
+ "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
+ "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
+ idAlbum, idAlbum);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+ while (!m_pDS->eof())
+ {
+ // Get ID as string so can easily join to make "IN" clause
+ artistIDs.push_back(m_pDS->fv("idArtist").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+};
+
+bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int>& songs)
+{
+ try
+ {
+ std::string strSQL;
+ //Restrict to Artists only, no other roles
+ strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", //
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ songs.push_back(m_pDS->fv("idSong").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+};
+
+bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int>& artists)
+{
+ try
+ {
+ std::string strSQL;
+ //Restrict to Artists only, no other roles
+ strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", //
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ artists.push_back(m_pDS->fv("idArtist").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
+ "FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN song_genre ON song.idSong = song_genre.idSong "
+ "JOIN genre ON song_genre.idGenre = genre.idGenre "
+ "WHERE album_artist.idArtist = %i "
+ "ORDER BY song_genre.idGenre",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ // Artist does have any song genres via albums may not be an album artist.
+ // Check via songs artist to fetch song genres from compilations or where they are guest artist
+ m_pDS->close();
+ strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
+ "FROM song_artist "
+ "JOIN song_genre ON song_artist.idSong = song_genre.idSong "
+ "JOIN genre ON song_genre.idGenre = genre.idGenre "
+ "WHERE song_artist.idArtist = %i "
+ "ORDER BY song_genre.idGenre",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ //No song genres, but query successful
+ m_pDS->close();
+ return true;
+ }
+ }
+
+ CVariant artistSongGenres(CVariant::VariantTypeArray);
+
+ while (!m_pDS->eof())
+ {
+ CVariant genreObj;
+ genreObj["title"] = m_pDS->fv("strGenre").get_asString();
+ genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
+ artistSongGenres.push_back(genreObj);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("songgenres", artistSongGenres);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
+ "song JOIN song_genre ON song.idSong = song_genre.idSong "
+ "JOIN genre ON song_genre.idGenre = genre.idGenre "
+ "WHERE song.idAlbum = %i "
+ "ORDER BY song_genre.idSong, song_genre.iOrder",
+ idAlbum);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ //No song genres, but query successful
+ m_pDS->close();
+ return true;
+ }
+
+ CVariant albumSongGenres(CVariant::VariantTypeArray);
+
+ while (!m_pDS->eof())
+ {
+ CVariant genreObj;
+ genreObj["title"] = m_pDS->fv("strGenre").get_asString();
+ genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
+ albumSongGenres.push_back(genreObj);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("songgenres", albumSongGenres);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres)
+{
+ try
+ {
+ std::string strSQL = PrepareSQL("SELECT idGenre FROM song_genre "
+ "WHERE idSong = %i ORDER BY iOrder ASC",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ while (!m_pDS->eof())
+ {
+ genres.push_back(m_pDS->fv("idGenre").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item)
+{
+ try
+ {
+ int countalbum =
+ GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist));
+ CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean);
+ IsAlbumArtistObj = (countalbum > 0);
+ item->SetProperty("isalbumartist", IsAlbumArtistObj);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+
+int CMusicDatabase::AddPath(const std::string& strPath1)
+{
+ std::string strSQL;
+ try
+ {
+ std::string strPath(strPath1);
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ auto it = m_pathCache.find(strPath);
+ if (it != m_pathCache.end())
+ return it->second;
+
+ strSQL = PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("INSERT INTO path (idPath, strPath) "
+ "VALUES(NULL, '%s')",
+ strPath.c_str());
+ m_pDS->exec(strSQL);
+
+ int idPath = (int)m_pDS->lastinsertid();
+ m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
+ return idPath;
+ }
+ else
+ {
+ int idPath = m_pDS->fv("idPath").get_asInt();
+ m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
+ m_pDS->close();
+ return idPath;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addpath ({})", strSQL);
+ }
+
+ return -1;
+}
+
+CSong CMusicDatabase::GetSongFromDataset()
+{
+ return GetSongFromDataset(m_pDS->get_sql_record());
+}
+
+CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */)
+{
+ CSong song;
+ song.idSong = record->at(offset + song_idSong).get_asInt();
+ // Note this function does not populate artist credits, this must be done separately.
+ // However artist names are held as a descriptive string
+ song.strArtistDesc = record->at(offset + song_strArtists).get_asString();
+ song.strArtistSort = record->at(offset + song_strArtistSort).get_asString();
+ // Get the full genre string
+ song.genre = StringUtils::Split(
+ record->at(offset + song_strGenres).get_asString(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ // and the rest...
+ song.strAlbum = record->at(offset + song_strAlbum).get_asString();
+ song.idAlbum = record->at(offset + song_idAlbum).get_asInt();
+ song.iTrack = record->at(offset + song_iTrack).get_asInt();
+ song.iDuration = record->at(offset + song_iDuration).get_asInt();
+ song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString();
+ song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString();
+ song.strTitle = record->at(offset + song_strTitle).get_asString();
+ song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt();
+ song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString());
+ song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString());
+ song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString());
+ song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString());
+ song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt();
+ song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt();
+ song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString();
+ song.rating = record->at(offset + song_rating).get_asFloat();
+ song.userrating = record->at(offset + song_userrating).get_asInt();
+ song.votes = record->at(offset + song_votes).get_asInt();
+ song.strComment = record->at(offset + song_comment).get_asString();
+ song.strMood = record->at(offset + song_mood).get_asString();
+ song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1;
+ song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString();
+ // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
+ song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString());
+ // Get filename with full path
+ song.strFileName =
+ URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(),
+ record->at(offset + song_strFileName).get_asString());
+ song.iBPM = record->at(offset + song_iBPM).get_asInt();
+ song.iBitRate = record->at(offset + song_iBitRate).get_asInt();
+ song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt();
+ song.iChannels = record->at(offset + song_iChannels).get_asInt();
+ return song;
+}
+
+void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl)
+{
+ GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl);
+}
+
+void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record,
+ CFileItem* item,
+ const CMusicDbUrl& baseUrl)
+{
+ // get the artist string from songview (not the song_artist and artist tables)
+ item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString());
+ // get the artist sort name string from songview (not the song_artist and artist tables)
+ item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString());
+ // and the full genre string
+ item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString());
+ // and the rest...
+ item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString());
+ item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt());
+ item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt());
+ item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt());
+ item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong);
+ item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString());
+ item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString());
+ item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString());
+ item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString());
+ item->SetLabel(record->at(song_strTitle).get_asString());
+ item->SetStartOffset(record->at(song_iStartOffset).get_asInt64());
+ item->SetProperty("item_start", item->GetStartOffset());
+ item->SetEndOffset(record->at(song_iEndOffset).get_asInt64());
+ item->GetMusicInfoTag()->SetMusicBrainzTrackID(
+ record->at(song_strMusicBrainzTrackID).get_asString());
+ item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat());
+ item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt());
+ item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt());
+ item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString());
+ item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString());
+ item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt());
+ item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString());
+ item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString());
+ item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString());
+ item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString());
+ std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(),
+ record->at(song_strFileName).get_asString());
+ item->GetMusicInfoTag()->SetURL(strRealPath);
+ item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1);
+ item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1);
+ // get the album artist string from songview (not the album_artist and artist tables)
+ item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString());
+ item->GetMusicInfoTag()->SetAlbumReleaseType(
+ CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString()));
+ item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt());
+ item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt());
+ item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt());
+ item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt());
+ // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
+ ReplayGain replaygain;
+ replaygain.Set(record->at(song_strReplayGain).get_asString());
+ item->GetMusicInfoTag()->SetReplayGain(replaygain);
+ item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt());
+
+ item->GetMusicInfoTag()->SetLoaded(true);
+ // Get filename with full path
+ if (!baseUrl.IsValid())
+ item->SetPath(strRealPath);
+ else
+ {
+ CMusicDbUrl itemUrl = baseUrl;
+ std::string strFileName = record->at(song_strFileName).get_asString();
+ std::string strExt = URIUtils::GetExtension(strFileName);
+ std::string path = StringUtils::Format("{}{}", record->at(song_idSong).get_asInt(), strExt);
+ itemUrl.AppendPath(path);
+ item->SetPath(itemUrl.ToString());
+ item->SetDynPath(strRealPath);
+ }
+}
+
+void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item)
+{
+ // Populate fileitem with artists from vector of artist credits
+ std::vector<std::string> musicBrainzID;
+ std::vector<std::string> songartists;
+ CVariant artistidObj(CVariant::VariantTypeArray);
+
+ // When "missing tag" artist, it is the only artist when present.
+ if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID)
+ {
+ artistidObj.push_back((int)BLANKARTIST_ID);
+ songartists.push_back(StringUtils::Empty);
+ }
+ else
+ {
+ for (const auto& artistCredit : artistCredits)
+ {
+ artistidObj.push_back(artistCredit.GetArtistId());
+ songartists.push_back(artistCredit.GetArtist());
+ if (!artistCredit.GetMusicBrainzArtistID().empty())
+ musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ }
+ // Also sets ArtistDesc if empty from song.strArtist field
+ item->GetMusicInfoTag()->SetArtist(songartists);
+ item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID);
+ // Add album artistIds as separate property as not part of CMusicInfoTag
+ item->SetProperty("artistid", artistidObj);
+}
+
+CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS,
+ int offset /* = 0 */,
+ bool imageURL /* = false*/)
+{
+ return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL);
+}
+
+CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */,
+ bool imageURL /* = false*/)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ CAlbum album;
+ album.idAlbum = record->at(offset + album_idAlbum).get_asInt();
+ album.strAlbum = record->at(offset + album_strAlbum).get_asString();
+ if (album.strAlbum.empty())
+ album.strAlbum = g_localizeStrings.Get(1050);
+ album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString();
+ album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString();
+ album.strArtistDesc = record->at(offset + album_strArtists).get_asString();
+ album.strArtistSort = record->at(offset + album_strArtistSort).get_asString();
+ album.genre =
+ StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator);
+ album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString();
+ album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString();
+ album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1;
+ if (imageURL)
+ album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString());
+ album.fRating = record->at(offset + album_fRating).get_asFloat();
+ album.iUserrating = record->at(offset + album_iUserrating).get_asInt();
+ album.iVotes = record->at(offset + album_iVotes).get_asInt();
+ album.strReview = record->at(offset + album_strReview).get_asString();
+ album.styles =
+ StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator);
+ album.moods =
+ StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator);
+ album.themes =
+ StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator);
+ album.strLabel = record->at(offset + album_strLabel).get_asString();
+ album.strType = record->at(offset + album_strType).get_asString();
+ album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString();
+ album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1;
+ album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1;
+ album.strLastScraped = record->at(offset + album_lastScraped).get_asString();
+ album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt();
+ album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString());
+ album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt();
+ album.SetDateAdded(record->at(offset + album_dateAdded).get_asString());
+ album.SetDateNew(record->at(offset + album_dateNew).get_asString());
+ album.SetDateUpdated(record->at(offset + album_dateModified).get_asString());
+ album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString());
+ album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt();
+ return album;
+}
+
+CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */)
+{
+ CArtistCredit artistCredit;
+ artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt();
+ if (artistCredit.idArtist == BLANKARTIST_ID)
+ artistCredit.m_strArtist = StringUtils::Empty;
+ else
+ {
+ artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString();
+ artistCredit.m_strMusicBrainzArtistID =
+ record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString();
+ }
+ return artistCredit;
+}
+
+CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */)
+{
+ CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(),
+ record->at(offset + artistCredit_strRole).get_asString(),
+ record->at(offset + artistCredit_strArtist).get_asString(),
+ record->at(offset + artistCredit_idArtist).get_asInt());
+ return ArtistRole;
+}
+
+CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS,
+ int offset /* = 0 */,
+ bool needThumb /* = true */)
+{
+ return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb);
+}
+
+CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */,
+ bool needThumb /* = true */)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ CArtist artist;
+ artist.idArtist = record->at(offset + artist_idArtist).get_asInt();
+ if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist)
+ artist.strArtist = g_localizeStrings.Get(38042); //Missing artist tag in current language
+ else
+ artist.strArtist = record->at(offset + artist_strArtist).get_asString();
+ artist.strSortName = record->at(offset + artist_strSortName).get_asString();
+ artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString();
+ artist.strType = record->at(offset + artist_strType).get_asString();
+ artist.strGender = record->at(offset + artist_strGender).get_asString();
+ artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString();
+ artist.genre =
+ StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator);
+ artist.strBiography = record->at(offset + artist_strBiography).get_asString();
+ artist.styles =
+ StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator);
+ artist.moods =
+ StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator);
+ artist.strBorn = record->at(offset + artist_strBorn).get_asString();
+ artist.strFormed = record->at(offset + artist_strFormed).get_asString();
+ artist.strDied = record->at(offset + artist_strDied).get_asString();
+ artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString();
+ artist.yearsActive =
+ StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator);
+ artist.instruments =
+ StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator);
+ artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1;
+ artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString();
+ artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString());
+ artist.SetDateNew(record->at(offset + artist_dateNew).get_asString());
+ artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString());
+
+ if (needThumb)
+ {
+ artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString());
+ }
+
+ return artist;
+}
+
+bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath,
+ CSong& song,
+ int64_t startOffset)
+{
+ song.Clear();
+ CURL url(strFileNameAndPath);
+
+ if (url.IsProtocol("musicdb"))
+ {
+ std::string strFile = URIUtils::GetFileName(strFileNameAndPath);
+ URIUtils::RemoveExtension(strFile);
+ return GetSong(atoi(strFile.c_str()), song);
+ }
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strPath, strFileName;
+ SplitPath(strFileNameAndPath, strPath, strFileName);
+ URIUtils::AddSlashAtEnd(strPath);
+
+ std::string strSQL = PrepareSQL("SELECT idSong FROM songview "
+ "WHERE strFileName='%s' AND strPath='%s'",
+ strFileName.c_str(), strPath.c_str());
+ if (startOffset)
+ strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset);
+
+ int idSong = GetSingleValueInt(strSQL);
+ if (idSong > 0)
+ return GetSong(idSong, song);
+
+ return false;
+}
+
+int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE path.strPath='%s'",
+ strPath.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+
+ int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned
+ if (iRowsFound == 1)
+ idAlbum = m_pDS->fv(0).get_asInt();
+
+ m_pDS->close();
+
+ return idAlbum;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist,
+ const std::string& strAlbum,
+ const std::string& strTitle)
+{
+ try
+ {
+ std::string strSQL =
+ PrepareSQL("SELECT idSong FROM songview "
+ "WHERE strArtists LIKE '%s' AND strAlbum LIKE '%s' AND strTitle LIKE '%s'",
+ strArtist.c_str(), strAlbum.c_str(), strTitle.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ int lResult = m_pDS->fv(0).get_asInt();
+ m_pDS->close(); // cleanup recordset data
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{},{}) failed", __FUNCTION__, strArtist, strAlbum, strTitle);
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList& artists)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strVariousArtists = g_localizeStrings.Get(340).c_str();
+ std::string strSQL;
+ if (search.size() >= MIN_FULL_SEARCH_LENGTH)
+ strSQL = PrepareSQL("SELECT * FROM artist "
+ "WHERE (strArtist LIKE '%s%%' OR strArtist LIKE '%% %s%%') "
+ "AND strArtist <> '%s' ",
+ search.c_str(), search.c_str(), strVariousArtists.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM artist "
+ "WHERE strArtist LIKE '%s%%' AND strArtist <> '%s' ",
+ search.c_str(), strVariousArtists.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist
+ while (!m_pDS->eof())
+ {
+ std::string path = StringUtils::Format("musicdb://artists/{}/", m_pDS->fv(0).get_asInt());
+ CFileItemPtr pItem(new CFileItem(path, true));
+ std::string label = StringUtils::Format("[{}] {}", artistLabel, m_pDS->fv(1).get_asString());
+ pItem->SetLabel(label);
+ // sort label is stored in the title tag
+ label = StringUtils::Format("A {}", m_pDS->fv(1).get_asString());
+ pItem->GetMusicInfoTag()->SetTitle(label);
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist);
+ artists.Add(pItem);
+ m_pDS->next();
+ }
+
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL = "SELECT * FROM songview "
+ "WHERE iTimesPlayed>0 "
+ "ORDER BY iTimesPlayed DESC "
+ "LIMIT 100";
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ items.Reserve(iRowsFound);
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(item.get(), baseUrl);
+ items.Add(item);
+ m_pDS->next();
+ }
+
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums)
+{
+ try
+ {
+ albums.erase(albums.begin(), albums.end());
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get data from album and album_artist tables to fully populate albums
+ std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview "
+ "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ "WHERE albumartistview.idAlbum IN "
+ "(SELECT albumview.idAlbum FROM albumview "
+ "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
+ "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
+ "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ int albumArtistOffset = album_enumCount;
+ int albumId = -1;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (albumId != record->at(album_idAlbum).get_asInt())
+ { // New album
+ albumId = record->at(album_idAlbum).get_asInt();
+ albums.push_back(GetAlbumFromDataset(record));
+ }
+ // Get album artists
+ albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL = StringUtils::Format(
+ "SELECT songview.*, albumview.* FROM songview"
+ "JOIN albumview ON (songview.idAlbum = albumview.idAlbum) "
+ "JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song "
+ "WHERE song.iTimesPlayed > 0 "
+ "GROUP BY idAlbum "
+ "ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit "
+ "ON (songview.idAlbum = _albumlimit.idAlbum) "
+ "ORDER BY _albumlimit.iTimesPlayedSum DESC");
+ CLog::Log(LOGDEBUG, "GetTop100AlbumSongs() query: {}", strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ items.Reserve(iRowsFound);
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(item.get(), baseUrl);
+ items.Add(item);
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums)
+{
+ try
+ {
+ albums.erase(albums.begin(), albums.end());
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ // Get data from album and album_artist tables to fully populate albums
+ std::string strSQL =
+ PrepareSQL("SELECT albumview.*, albumartistview.* "
+ "FROM (SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
+ "AND albumview.strReleaseType = '%s' "
+ "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
+ "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
+ "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT);
+
+ auto queryStart = std::chrono::steady_clock::now();
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ int albumArtistOffset = album_enumCount;
+ int albumId = -1;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (albumId != record->at(album_idAlbum).get_asInt())
+ { // New album
+ albumId = record->at(album_idAlbum).get_asInt();
+ albums.push_back(GetAlbumFromDataset(record));
+ }
+ // Get album artists
+ albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir,
+ CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT songview.*, songartistview.* "
+ "FROM (SELECT idAlbum, lastPlayed FROM albumview "
+ "WHERE albumview.lastplayed IS NOT NULL "
+ "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
+ "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
+ "JOIN songartistview ON songview.idSong = songartistview.idSong "
+ "ORDER BY playedalbums.lastplayed DESC, "
+ "songartistview.idsong, songartistview.idRole, songartistview.iOrder",
+ CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iMusicLibraryRecentlyAddedItems);
+ CLog::Log(LOGDEBUG, "GetRecentlyPlayedAlbumSongs() query: {}", strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Needs a separate query to determine number of songs to set items size.
+ // Get songs from returned rows. Join means there is a row for every song artist
+ // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
+ int songArtistOffset = song_enumCount;
+ int songId = -1;
+ VECARTISTCREDITS artistCredits;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (songId != record->at(song_idSong).get_asInt())
+ { //New song
+ if (songId > 0 && !artistCredits.empty())
+ {
+ //Store artist credits for previous song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ songId = record->at(song_idSong).get_asInt();
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), baseUrl);
+ items.Add(item);
+ }
+ // Get song artist credits and contributors
+ if (idSongArtistRole == ROLE_ARTIST)
+ artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
+ GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ if (!artistCredits.empty())
+ {
+ //Store artist credits for final song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit)
+{
+ try
+ {
+ albums.erase(albums.begin(), albums.end());
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get data from album and album_artist tables to fully populate albums
+ // Determine the recently added albums from dateAdded (usually derived from music file
+ // timestamps, nothing to do with when albums added to library)
+ std::string strSQL =
+ PrepareSQL("SELECT albumview.*, albumartistview.* "
+ "FROM (SELECT idAlbum FROM album WHERE strAlbum != '' "
+ "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
+ "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
+ "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
+ limit ? limit
+ : CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iMusicLibraryRecentlyAddedItems);
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ int albumArtistOffset = album_enumCount;
+ int albumId = -1;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (albumId != record->at(album_idAlbum).get_asInt())
+ { // New album
+ albumId = record->at(album_idAlbum).get_asInt();
+ albums.push_back(GetAlbumFromDataset(record));
+ }
+ // Get album artists
+ albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir,
+ CFileItemList& items,
+ unsigned int limit)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
+ return false;
+
+ // Get data from song and song_artist tables to fully populate songs
+ // Determine the recently added albums from dateAdded (usually derived from music file
+ // timestamps, nothing to do with when albums added to library)
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT songview.*, songartistview.* "
+ "FROM (SELECT idAlbum, dateAdded FROM album "
+ "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
+ "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
+ "JOIN songartistview ON songview.idSong = songartistview.idSong "
+ "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
+ "songview.idSong, songartistview.idRole, songartistview.iOrder ",
+ limit ? limit
+ : CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iMusicLibraryRecentlyAddedItems);
+ CLog::Log(LOGDEBUG, "GetRecentlyAddedAlbumSongs() query: {}", strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Needs a separate query to determine number of songs to set items size.
+ // Get songs from returned rows. Join means there is a row for every song artist
+ int songArtistOffset = song_enumCount;
+ int songId = -1;
+ VECARTISTCREDITS artistCredits;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (songId != record->at(song_idSong).get_asInt())
+ { //New song
+ if (songId > 0 && !artistCredits.empty())
+ {
+ //Store artist credits for previous song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ songId = record->at(song_idSong).get_asInt();
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), baseUrl);
+ items.Add(item);
+ }
+ // Get song artist credits and contributors
+ if (idSongArtistRole == ROLE_ARTIST)
+ artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
+ GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ if (!artistCredits.empty())
+ {
+ //Store artist credits for final song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+void CMusicDatabase::IncrementPlayCount(const CFileItem& item)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ int idSong = GetSongIDFromPath(item.GetPath());
+ std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+ std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
+ "WHERE idSong=%i",
+ strDateNow.c_str(), idSong);
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, item.GetPath());
+ }
+}
+
+bool CMusicDatabase::GetSongsByPath(const std::string& strPath1,
+ MAPSONGS& songmap,
+ bool bAppendToMap)
+{
+ std::string strPath(strPath1);
+ try
+ {
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ if (!bAppendToMap)
+ songmap.clear();
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Filename is not unique for a path as songs from a cuesheet have same filename.
+ // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
+ // in a folder and some edited and rescanned.
+ // Hence order by filename so these songs can be gathered together.
+ std::string strSQL = PrepareSQL("SELECT * FROM songview "
+ "WHERE strPath='%s' ORDER BY strFileName",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ // Each file is potentially mapped to a list of songs, gather these and save as list
+ VECSONGS songs;
+ std::string filename;
+ while (!m_pDS->eof())
+ {
+ CSong song = GetSongFromDataset();
+ if (!filename.empty() && filename != song.strFileName)
+ {
+ // Save songs for previous filename
+ songmap.insert(std::make_pair(filename, songs));
+ songs.clear();
+ }
+ filename = song.strFileName;
+ songs.emplace_back(song);
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
+ }
+
+ return false;
+}
+
+void CMusicDatabase::EmptyCache()
+{
+ m_genreCache.erase(m_genreCache.begin(), m_genreCache.end());
+ m_pathCache.erase(m_pathCache.begin(), m_pathCache.end());
+}
+
+bool CMusicDatabase::Search(const std::string& search, CFileItemList& items)
+{
+ auto start = std::chrono::steady_clock::now();
+ // first grab all the artists that match
+ SearchArtists(search, items);
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} Artist search in {} ms", __FUNCTION__, duration.count());
+
+ start = std::chrono::steady_clock::now();
+ // then albums that match
+ SearchAlbums(search, items);
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} Album search in {} ms", __FUNCTION__, duration.count());
+
+ start = std::chrono::steady_clock::now();
+ // and finally songs
+ SearchSongs(search, items);
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} Songs search in {} ms", __FUNCTION__, duration.count());
+
+ return true;
+}
+
+bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!baseUrl.FromString("musicdb://songs/"))
+ return false;
+
+ std::string strSQL;
+ if (search.size() >= MIN_FULL_SEARCH_LENGTH)
+ strSQL = PrepareSQL("SELECT * FROM songview "
+ "WHERE strTitle LIKE '%s%%' or strTitle LIKE '%% %s%%' LIMIT 1000",
+ search.c_str(), search.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM songview "
+ "WHERE strTitle LIKE '%s%%' LIMIT 1000",
+ search.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(item.get(), baseUrl);
+ items.Add(item);
+ m_pDS->next();
+ }
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList& albums)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ if (search.size() >= MIN_FULL_SEARCH_LENGTH)
+ strSQL = PrepareSQL("SELECT * FROM albumview "
+ "WHERE strAlbum LIKE '%s%%' OR strAlbum LIKE '%% %s%%'",
+ search.c_str(), search.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM albumview "
+ "WHERE strAlbum LIKE '%s%%'",
+ search.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ const std::string& albumLabel(g_localizeStrings.Get(558)); // Album
+ while (!m_pDS->eof())
+ {
+ CAlbum album = GetAlbumFromDataset(m_pDS.get());
+ std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(path, album));
+ std::string label = StringUtils::Format("[{}] {}", albumLabel, album.strAlbum);
+ pItem->SetLabel(label);
+ // sort label is stored in the title tag
+ label = StringUtils::Format("B {}", album.strAlbum);
+ pItem->GetMusicInfoTag()->SetTitle(label);
+ albums.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupSongsByIds(const std::string& strSongIds)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ // ok, now find all idSong's
+ std::string strSQL = PrepareSQL("SELECT * FROM song JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idSong IN %s",
+ strSongIds.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ std::vector<std::string> songsToDelete;
+ while (!m_pDS->eof())
+ { // get the full song path
+ std::string strFileName = URIUtils::AddFileToFolder(
+ m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString());
+
+ // Special case for streams inside an audio decoder package file.
+ // The last dir in the path is the audio file that
+ // contains the stream, so test if its there
+ if (StringUtils::EndsWith(URIUtils::GetExtension(strFileName),
+ KODI_ADDON_AUDIODECODER_TRACK_EXT))
+ {
+ strFileName = URIUtils::GetDirectory(strFileName);
+ // we are dropping back to a file, so remove the slash at end
+ URIUtils::RemoveSlashAtEnd(strFileName);
+ }
+
+ if (!CFile::Exists(strFileName, false))
+ { // file no longer exists, so add to deletion list
+ songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString());
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ if (!songsToDelete.empty())
+ {
+ std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")";
+ // ok, now delete these songs + all references to them from the linked tables
+ strSQL = "delete from song where idSong in " + strSongsToDelete;
+ m_pDS->exec(strSQL);
+ m_pDS->close();
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/)
+{
+ try
+ {
+ int total;
+ // Count total number of songs
+ total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS);
+ // No songs to clean
+ if (total == 0)
+ return true;
+
+ // run through all songs and get all unique path ids
+ int iLIMIT = 1000;
+ for (int i = 0;; i += iLIMIT)
+ {
+ std::string strSQL = PrepareSQL("SELECT song.idSong FROM song "
+ "ORDER BY song.idSong LIMIT %i OFFSET %i",
+ iLIMIT, i);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ // keep going until no rows are left!
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ std::vector<std::string> songIds;
+ while (!m_pDS->eof())
+ {
+ songIds.push_back(m_pDS->fv("song.idSong").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")";
+ CLog::Log(LOGDEBUG, "Checking songs from song ID list: {}", strSongIds);
+ if (progressDialog)
+ {
+ int percentage = i * 100 / total;
+ if (percentage > progressDialog->GetPercentage())
+ {
+ progressDialog->SetPercentage(percentage);
+ progressDialog->Progress();
+ }
+ if (progressDialog->IsCanceled())
+ {
+ m_pDS->close();
+ return false;
+ }
+ }
+ if (!CleanupSongsByIds(strSongIds))
+ return false;
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupAlbums()
+{
+ try
+ {
+ // This must be run AFTER songs have been cleaned up
+ // delete albums with no reference to songs
+ std::string strSQL = "SELECT * FROM album "
+ "WHERE album.idAlbum NOT IN (SELECT idAlbum FROM song)";
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ std::vector<std::string> albumIds;
+ while (!m_pDS->eof())
+ {
+ albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")";
+ // ok, now we can delete them and the references in the linked tables
+ strSQL = "delete from album where idAlbum in " + strAlbumIds;
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupPaths()
+{
+ try
+ {
+ // needs to be done AFTER the songs and albums have been cleaned up.
+ // we can happily delete any path that has no reference to a song
+ // but we must keep all paths that have been scanned that may contain songs in subpaths
+
+ // first create a temporary table of song paths
+ m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
+ m_pDS->exec("INSERT INTO songpaths "
+ "SELECT idPath, strPath FROM path "
+ "WHERE idPath IN (SELECT idPath FROM song)\n");
+
+ // grab all paths that aren't immediately connected with a song
+ std::string sql = "SELECT * FROM path WHERE idPath NOT IN (SELECT idPath FROM song)";
+ if (!m_pDS->query(sql))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ // and construct a list to delete
+ std::vector<std::string> pathIds;
+ while (!m_pDS->eof())
+ {
+ // anything that isn't a parent path of a song path is to be deleted
+ std::string path = m_pDS->fv("strPath").get_asString();
+ sql = PrepareSQL("SELECT COUNT(idPath) FROM songpaths WHERE SUBSTR(strPath,1,%i)='%s'",
+ StringUtils::utf8_strlen(path.c_str()), path.c_str());
+ if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0)
+ pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete
+ m_pDS2->close();
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ if (!pathIds.empty())
+ {
+ // do the deletion, and drop our temp table
+ std::string deleteSQL =
+ "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")";
+ m_pDS->exec(deleteSQL);
+ }
+ m_pDS->exec("drop table songpaths");
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::InsideScannedPath(const std::string& path)
+{
+ std::string sql = PrepareSQL("SELECT idPath FROM path WHERE SUBSTR(strPath,1,%i)='%s' LIMIT 1",
+ path.size(), path.c_str());
+ return !GetSingleValue(sql).empty();
+}
+
+bool CMusicDatabase::CleanupArtists()
+{
+ try
+ {
+ // (nested queries by Bobbin007)
+ // must be executed AFTER the song, album and their artist link tables are cleaned.
+ // Don't delete [Missing] the missing artist tag artist
+
+ // Create temp table to avoid 1442 trigger hell on mysql
+ m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
+ m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
+ m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
+ m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID));
+ // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
+ // To avoid MySQL hanging or timeout create a table of unique ids with primary key
+ m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
+ m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
+ m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
+ // Tidy up temp tables
+ m_pDS->exec("DROP TABLE tmp_delartists");
+ m_pDS->exec("DROP TABLE tmp_keep");
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupGenres()
+{
+ try
+ {
+ // Cleanup orphaned song genres (ie those that don't belong to a song entry)
+ // (nested queries by Bobbin007)
+ // Must be executed AFTER the song, and song_genre have been cleaned.
+ std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupInfoSettings()
+{
+ try
+ {
+ // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
+ // Must be executed AFTER the album and artist tables have been cleaned.
+ std::string strSQL = "DELETE FROM infosetting "
+ "WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
+ "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupRoles()
+{
+ try
+ {
+ // Cleanup orphaned roles (ie those that don't belong to a song entry)
+ // Must be executed AFTER the song, and song_artist tables have been cleaned.
+ // Do not remove default role (ROLE_ARTIST)
+ std::string strSQL = "DELETE FROM role "
+ "WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::DeleteRemovedLinks()
+{
+ try
+ {
+ std::string strSQL = "DELETE FROM removed_link";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupOrphanedItems()
+{
+ // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
+ // remove_links not cleared here - done in CheckArtistLinksChanged()
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ SetLibraryLastUpdated();
+ if (!CleanupAlbums())
+ return false;
+ if (!CleanupArtists())
+ return false;
+ if (!CleanupGenres())
+ return false;
+ if (!CleanupRoles())
+ return false;
+ if (!CleanupInfoSettings())
+ return false;
+ return true;
+}
+
+int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/)
+{
+ if (nullptr == m_pDB)
+ return ERROR_DATABASE;
+ if (nullptr == m_pDS)
+ return ERROR_DATABASE;
+
+ int ret;
+ std::chrono::seconds duration;
+ auto time = std::chrono::steady_clock::now();
+ CLog::Log(LOGINFO, "{}: Starting musicdatabase cleanup ..", __FUNCTION__);
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted");
+
+ SetLibraryLastCleaned();
+
+ // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link
+ // Check that triggers actually exist first as interrupting the clean causes them to not be
+ // re-created
+
+ m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteSongArtist");
+ m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteAlbumArtist");
+
+ // first cleanup any songs with invalid paths
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{318});
+ progressDialog->SetLine(2, CVariant{330});
+ progressDialog->SetPercentage(0);
+ progressDialog->Progress();
+ }
+ if (!CleanupSongs(progressDialog))
+ {
+ ret = ERROR_REORG_SONGS;
+ goto error;
+ }
+ // then the albums that are not linked to a song or to album, or whose path is removed
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{326});
+ progressDialog->SetPercentage(20);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupAlbums())
+ {
+ ret = ERROR_REORG_ALBUM;
+ goto error;
+ }
+ // now the paths
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{324});
+ progressDialog->SetPercentage(40);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupPaths())
+ {
+ ret = ERROR_REORG_PATH;
+ goto error;
+ }
+ // and finally artists + genres
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{320});
+ progressDialog->SetPercentage(60);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupArtists())
+ {
+ ret = ERROR_REORG_ARTIST;
+ goto error;
+ }
+ //Genres, roles and info settings progress in one step
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{322});
+ progressDialog->SetPercentage(80);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupGenres())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+ if (!CleanupRoles())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+ if (!CleanupInfoSettings())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+ if (!DeleteRemovedLinks())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+
+ // commit transaction
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{328});
+ progressDialog->SetPercentage(90);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CommitTransaction())
+ {
+ ret = ERROR_WRITING_CHANGES;
+ goto error;
+ }
+
+ // Recreate DELETE triggers on song_artist and album_artist
+ CreateRemovedLinkTriggers();
+
+ // and compress the database
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{331});
+ progressDialog->SetPercentage(100);
+ progressDialog->Close();
+ }
+
+ duration =
+ std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - time);
+ CLog::Log(LOGINFO, "{}: Cleaning musicdatabase done. Operation took {}s", __FUNCTION__,
+ duration.count());
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
+
+ if (!Compress(false))
+ {
+ return ERROR_COMPRESSING;
+ }
+ return ERROR_OK;
+
+error:
+ RollbackTransaction();
+ // Recreate DELETE triggers on song_artist and album_artist
+ CreateRemovedLinkTriggers();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
+ return ret;
+}
+
+bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space)
+{
+ if (strImage.length() > space)
+ {
+ strImage = strImage.substr(0, space);
+ // Tidy to last </thumb> tag
+ size_t iPos = strImage.rfind("</thumb>");
+ if (iPos == std::string::npos)
+ return false;
+ strImage = strImage.substr(0, iPos + 8);
+ }
+ return true;
+}
+
+bool CMusicDatabase::LookupCDDBInfo(bool bRequery /*=false*/)
+{
+#ifdef HAS_DVD_DRIVE
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_AUDIOCDS_USECDDB))
+ return false;
+
+ // check network connectivity
+ if (!CServiceBroker::GetNetwork().IsAvailable())
+ return false;
+
+ // Get information for the inserted disc
+ CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo == NULL)
+ return false;
+
+ // If the disc has no tracks, we are finished here.
+ int nTracks = pCdInfo->GetTrackCount();
+ if (nTracks <= 0)
+ return false;
+
+ // Delete old info if any
+ if (bRequery)
+ {
+ std::string strFile = StringUtils::Format("{:x}.cddb", pCdInfo->GetCddbDiscId());
+ CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
+ }
+
+ // Prepare cddb
+ Xcddb cddb;
+ cddb.setCacheDir(m_profileManager.GetCDDBFolder());
+
+ // Do we have to look for cddb information
+ if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo))
+ {
+ CGUIDialogProgress* pDialogProgress =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ CGUIDialogSelect* pDlgSelect =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+
+ if (!pDialogProgress)
+ return false;
+ if (!pDlgSelect)
+ return false;
+
+ // Show progress dialog if we have to connect to freedb.org
+ pDialogProgress->SetHeading(CVariant{255}); //CDDB
+ pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info
+ pDialogProgress->SetLine(1, CVariant{256});
+ pDialogProgress->SetLine(2, CVariant{""});
+ pDialogProgress->ShowProgressBar(false);
+ pDialogProgress->Open();
+
+ // get cddb information
+ if (!cddb.queryCDinfo(pCdInfo))
+ {
+ pDialogProgress->Close();
+ int lasterror = cddb.getLastError();
+
+ // Have we found more then on match in cddb for this disc,...
+ if (lasterror == E_WAIT_FOR_INPUT)
+ {
+ // ...yes, show the matches found in a select dialog
+ // and let the user choose an entry.
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{255});
+ int i = 1;
+ while (true)
+ {
+ std::string strTitle = cddb.getInexactTitle(i);
+ if (strTitle == "")
+ break;
+
+ const std::string& strArtist = cddb.getInexactArtist(i);
+ if (!strArtist.empty())
+ strTitle += " - " + strArtist;
+
+ pDlgSelect->Add(strTitle);
+ i++;
+ }
+ pDlgSelect->Open();
+
+ // Has the user selected a match...
+ int iSelectedCD = pDlgSelect->GetSelectedItem();
+ if (iSelectedCD >= 0)
+ {
+ // ...query cddb for the inexact match
+ if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD))
+ pCdInfo->SetNoCDDBInfo();
+ }
+ else
+ pCdInfo->SetNoCDDBInfo();
+ }
+ else if (lasterror == E_NO_MATCH_FOUND)
+ {
+ pCdInfo->SetNoCDDBInfo();
+ }
+ else
+ {
+ pCdInfo->SetNoCDDBInfo();
+ // ..no, an error occurred, display it to the user
+ std::string strErrorText =
+ StringUtils::Format("[{}] {}", cddb.getLastError(), cddb.getLastErrorText());
+ HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)},
+ CVariant{0});
+ }
+ } // if ( !cddb.queryCDinfo( pCdInfo ) )
+ else
+ pDialogProgress->Close();
+ }
+
+ // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
+
+ return pCdInfo->HasCDDBInfo();
+#else
+ return false;
+#endif
+}
+
+void CMusicDatabase::DeleteCDDBInfo()
+{
+#ifdef HAS_DVD_DRIVE
+ CFileItemList items;
+ if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb",
+ DIR_FLAG_NO_FILE_DIRS))
+ {
+ HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426});
+ return;
+ }
+ // Show a selectdialog that the user can select the album to delete
+ CGUIDialogSelect* pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (pDlg)
+ {
+ pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
+ pDlg->Reset();
+
+ std::map<uint32_t, std::string> mapCDDBIds;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (items[i]->m_bIsFolder)
+ continue;
+
+ std::string strFile = URIUtils::GetFileName(items[i]->GetPath());
+ strFile.erase(strFile.size() - 5, 5);
+ uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16);
+ Xcddb cddb;
+ cddb.setCacheDir(m_profileManager.GetCDDBFolder());
+
+ if (!cddb.queryCache(lDiscId))
+ continue;
+
+ std::string strDiskTitle, strDiskArtist;
+ cddb.getDiskTitle(strDiskTitle);
+ cddb.getDiskArtist(strDiskArtist);
+
+ std::string str;
+ if (strDiskArtist.empty())
+ str = strDiskTitle;
+ else
+ str = strDiskTitle + " - " + strDiskArtist;
+
+ pDlg->Add(str);
+ mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str));
+ }
+
+ pDlg->Sort();
+ pDlg->Open();
+
+ // and wait till user selects one
+ int iSelectedAlbum = pDlg->GetSelectedItem();
+ if (iSelectedAlbum < 0)
+ {
+ mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
+ return;
+ }
+
+ std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel();
+ for (const auto& i : mapCDDBIds)
+ {
+ if (i.second == strSelectedAlbum)
+ {
+ std::string strFile = StringUtils::Format("{:x}.cddb", (unsigned int)i.first);
+ CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
+ break;
+ }
+ }
+ mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
+ }
+#endif
+}
+
+void CMusicDatabase::Clean()
+{
+ // If we are scanning for music info in the background,
+ // other writing access to the database is prohibited.
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057});
+ return;
+ }
+
+ if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES)
+ {
+ CMusicDatabase musicdatabase;
+ if (musicdatabase.Open())
+ {
+ int iReturnString = musicdatabase.Cleanup();
+ musicdatabase.Close();
+
+ if (iReturnString != ERROR_OK)
+ {
+ HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString});
+ }
+ }
+ }
+}
+
+bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // get primary genres for songs - could be simplified to just SELECT * FROM genre?
+ std::string strSQL = "SELECT %s FROM genre ";
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // if there are extra WHERE conditions we might need access
+ // to songview or albumview for these conditions
+ if (!extFilter.where.empty())
+ {
+ if (extFilter.where.find("artistview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
+ extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
+ extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
+ }
+ else if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
+ }
+ else if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
+ }
+ extFilter.AppendGroup("genre.idGenre");
+ }
+ extFilter.AppendWhere("genre.strGenre != ''");
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(DISTINCT genre.idGenre)";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
+ ? extFilter.fields.c_str()
+ : "genre.*") +
+ strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString()));
+ pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString());
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre");
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv("genre.idGenre").get_asInt());
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /*= Filter()*/,
+ bool countOnly /*= false*/)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get sources for selection list when add/edit filter or smartplaylist rule
+ std::string strSQL = "SELECT %s FROM source ";
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // if there are extra WHERE conditions we might need access
+ // to songview or albumview for these conditions
+ if (!extFilter.where.empty())
+ {
+ if (extFilter.where.find("artistview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
+ extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
+ }
+ else if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
+ }
+ else if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
+ }
+ extFilter.AppendGroup("source.idSource");
+ }
+ else
+ { // Get only sources that have been scanned into music library
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendGroup("source.idSource");
+ }
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(DISTINCT source.idSource)";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
+ ? extFilter.fields.c_str()
+ : "source.*") +
+ strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
+ pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString());
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source");
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv("source.idSource").get_asInt());
+ itemUrl.AppendPath(strDir);
+ itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt());
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ std::string strSQL;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
+
+ useOriginalYears =
+ useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/");
+
+ if (!useOriginalYears)
+ { // Get years from year part of release date
+ strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
+ extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
+ }
+ else
+ { // Get years from year part of original date
+ strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
+ extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
+ }
+ if (!BuildSQL(strSQL, extFilter, strSQL))
+ return false;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
+ pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt());
+ if (useOriginalYears)
+ pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
+ else
+ pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year");
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ itemUrl.AppendPath(strDir);
+ if (useOriginalYears)
+ itemUrl.AddOption("useoriginalyear", true);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // get roles with artists having that role
+ std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role "
+ "JOIN song_artist ON song_artist.idRole = role.idRole ";
+
+ if (!BuildSQL(strSQL, extFilter, strSQL))
+ return false;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ std::string labelValue = m_pDS->fv("role.strRole").get_asString();
+ CFileItemPtr pItem(new CFileItem(labelValue));
+ pItem->GetMusicInfoTag()->SetTitle(labelValue);
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role");
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv("role.idRole").get_asInt());
+ itemUrl.AppendPath(strDir);
+ itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt());
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ musicUrl.AddOption("year", year);
+ musicUrl.AddOption("show_singles", true); // allow singles to be listed
+
+ Filter filter;
+ return GetAlbumsByWhere(musicUrl.ToString(), filter, items);
+}
+
+bool CMusicDatabase::GetCommonNav(const std::string& strBaseDir,
+ const std::string& table,
+ const std::string& labelField,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (table.empty() || labelField.empty())
+ return false;
+
+ try
+ {
+ Filter extFilter = filter;
+ std::string strSQL = "SELECT %s FROM " + table + " ";
+ extFilter.AppendGroup(labelField);
+ extFilter.AppendWhere(labelField + " != ''");
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(DISTINCT " + labelField + ")";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+
+ // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
+ // e.g. LIKE '%symphony%' would be taken as a %s format argument
+ strSQL = PrepareSQL(strSQL,
+ !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str());
+
+ CMusicDbUrl musicUrl;
+ if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl))
+ return false;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString();
+ CFileItemPtr pItem(new CFileItem(labelValue));
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", labelValue);
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumTypesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly);
+}
+
+bool CMusicDatabase::GetMusicLabelsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly);
+}
+
+bool CMusicDatabase::GetArtistsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ bool albumArtistsOnly /* = false */,
+ int idGenre /* = -1 */,
+ int idAlbum /* = -1 */,
+ int idSong /* = -1 */,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ try
+ {
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ if (idGenre > 0)
+ musicUrl.AddOption("genreid", idGenre);
+ else if (idAlbum > 0)
+ musicUrl.AddOption("albumid", idAlbum);
+ else if (idSong > 0)
+ musicUrl.AddOption("songid", idSong);
+
+ // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
+ // when local option already present in music URL thus allowing it to be an option in custom nodes
+ if (!musicUrl.HasOption("albumartistsonly"))
+ musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
+
+ bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
+
+ return result;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtistsByWhere(
+ const std::string& strBaseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool extended = false;
+ bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
+
+ // if there are extra WHERE conditions (from media filter dialog) we might
+ // need access to songview or albumview for these conditions
+ if (!extFilter.where.empty())
+ {
+ if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist "
+ "JOIN songview ON songview.idSong = song_artist.idSong");
+ }
+ else if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist "
+ "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
+ }
+ if (extended)
+ extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of artsits that satisfy selection criteria (no limit built)
+ // Count done in full query fetch when unlimited
+ if (countOnly || limitedInSQL)
+ {
+ if (extended)
+ {
+ // Count distinct without group by
+ Filter countFilter = extFilter;
+ countFilter.group.clear();
+ std::string strSQLWhere;
+ if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
+ return false;
+ total = GetSingleValueInt(
+ "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS);
+ }
+ else
+ total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS);
+ }
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", total);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // Apply any limiting directly in SQL and so sort as well
+ if (limitedInSQL)
+ {
+ extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
+ }
+
+ // Apply sort in SQL
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
+ GetOrderFilter(MediaTypeArtist, sorting, extFilter);
+
+ strSQLExtra.clear();
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ std::string strSQL;
+ std::string strFields = "artistview.*";
+ if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
+ strFields = "artistview.*, " + extFilter.fields;
+ strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ auto queryStart = std::chrono::steady_clock::now();
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ // Store the total number of artists as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ // Populate results field vector from dataset
+ FieldList fields;
+ if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results))
+ return false;
+ // Store item list sort order
+ items.SetSortMethod(sortDescription.sortBy);
+ items.SetSortOrder(sortDescription.sortOrder);
+
+ // Get Artists from returned rows
+ items.Reserve(results.size());
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ CArtist artist = GetArtistFromDataset(record, false);
+ CFileItemPtr pItem(new CFileItem(artist));
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string path = StringUtils::Format("{}/", artist.idArtist);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist);
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ pItem->SetProperty("icon_never_overlay", true);
+ pItem->SetArt("icon", "DefaultArtist.png");
+
+ SetPropertiesFromArtist(*pItem, artist);
+ items.Add(pItem);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
+ items.Size());
+ }
+ }
+ // cleanup
+ m_pDS->close();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1} ms query took {2} ms",
+ __FUNCTION__, duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum& album)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("SELECT albumview.* FROM song "
+ "JOIN albumview on song.idAlbum = albumview.idAlbum "
+ "WHERE song.idSong='%i'",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ album = GetAlbumFromDataset(m_pDS.get());
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre /* = -1 */,
+ int idArtist /* = -1 */,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ // where clause
+ if (idGenre > 0)
+ musicUrl.AddOption("genreid", idGenre);
+
+ if (idArtist > 0)
+ musicUrl.AddOption("artistid", idArtist);
+
+ return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
+}
+
+bool CMusicDatabase::GetAlbumsByWhere(
+ const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool extended = false;
+ bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
+
+ // If there are extra WHERE conditions (from media filter dialog) we might
+ // need access to songview for these conditions
+ if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
+ extFilter.AppendGroup("albumview.idAlbum");
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of albums that satisfy selection criteria (no limit built)
+ // Count done in full query fetch when unlimited
+ if (countOnly || limitedInSQL)
+ {
+ if (extended)
+ {
+ // Count distinct without group by
+ Filter countFilter = extFilter;
+ countFilter.group.clear();
+ std::string strSQLWhere;
+ if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
+ return false;
+ total = GetSingleValueInt(
+ "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS);
+ }
+ else
+ total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
+ }
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", total);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // Apply any limiting directly in SQL
+ if (limitedInSQL)
+ {
+ extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
+ }
+
+ // Apply sort in SQL
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
+ GetOrderFilter(MediaTypeAlbum, sorting, extFilter);
+ // Modify order to use correct calculated year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
+ else
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
+
+ strSQLExtra.clear();
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ std::string strSQL;
+ std::string strFields = "albumview.*";
+ if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
+ strFields = "albumview.*, " + extFilter.fields;
+ strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ auto querytime = std::chrono::steady_clock::now();
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - querytime);
+
+ // Store the total number of albums as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ // Populate results field vector from dataset
+ FieldList fields;
+ if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results))
+ return false;
+ // Store item list sort order
+ items.SetSortMethod(sorting.sortBy);
+ items.SetSortOrder(sorting.sortOrder);
+
+ // Get albums from returned rows
+ items.Reserve(results.size());
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string path = StringUtils::Format("{}/", record->at(album_idAlbum).get_asInt());
+ itemUrl.AppendPath(path);
+
+ CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record)));
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ pItem->SetProperty("icon_never_overlay", true);
+ pItem->SetArt("icon", "DefaultAlbumCover.png");
+ items.Add(pItem);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
+ items.Size());
+ }
+ }
+ // cleanup
+ m_pDS->close();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idAlbum,
+ const Filter& filter,
+ const SortDescription& sortDescription,
+ bool countOnly)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ if (idAlbum > 0)
+ musicUrl.AddOption("albumid", idAlbum);
+
+ return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
+}
+
+bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription,
+ bool countOnly)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(baseDir))
+ return false;
+ return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
+}
+
+bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription,
+ bool countOnly)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+ std::string strSQL;
+
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+
+ if (!GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ extFilter.AppendGroup("albumview.idAlbum, iDisc");
+
+ // If there are extra songview WHERE conditions adjust to song or albumview
+ // fields, and join Path table for strPath
+ // ! @todo: convert songview fields into to song or albumview fields
+ // But not sure we ever get songview fields in filter - REMOVE??
+ if (extFilter.where.find("songview.strPath") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Apply any limiting directly in SQL if there is either no special sorting or random sort
+ // When limited, random sort is also applied in SQL
+ bool limitedInSQL = extFilter.limit.empty() &&
+ (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0);
+
+ if (countOnly || limitedInSQL)
+ {
+ // Count number of discs that satisfy selection criteria
+ // (when fetching all records get total from row count of results dataset)
+ // Count not allow for same non-null title discs to be grouped together
+ strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
+ "albumview.idAlbum " +
+ strSQLExtra;
+ strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc ";
+ total = GetSingleValueInt(strSQL, m_pDS);
+ }
+ if (countOnly)
+ {
+ items.SetProperty("total", total);
+ return true;
+ }
+ // Apply limits and random sort order directly in SQL
+ if (limitedInSQL)
+ {
+ if (sorting.sortBy == SortByRandom)
+ strSQLExtra += PrepareSQL(" ORDER BY RANDOM()");
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+ else
+ strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
+
+ strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
+ "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " +
+ strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ auto queryStart = std::chrono::steady_clock::now();
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+
+ // Avoid sorting with limits, just fetch results from dataset
+ // Limit when SortByNone already applied in SQL,
+ // Need guaranteed ordering for dataset processing to group by disc title
+ // so apply sort later to fileitems list rather than dataset
+ sorting.sortBy = SortByNone;
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results))
+ return false;
+
+ // Get data from returned rows, note possibly multiple albums although usually only one
+ items.Reserve(total);
+ int albumOffset = 2;
+ CAlbum album;
+ bool useTitle = true; // Assume we want to match by disc title later unless we have no titles
+ std::string oldDiscTitle;
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger());
+ const dbiplus::sql_record* const record = data.at(targetRow);
+ try
+ {
+ if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt())
+ { // New album
+ useTitle = true;
+ album = GetAlbumFromDataset(record, albumOffset);
+ }
+
+ int discnum = record->at(0).get_asInt();
+ std::string strDiscSubtitle = record->at(1).get_asString();
+ if (strDiscSubtitle.empty())
+ { // Make (fake) disc title from disc number, group by disc number as no real title to match
+ strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum);
+ useTitle = false;
+ }
+ else if (oldDiscTitle == strDiscSubtitle)
+ { // disc title already added to list, fetch the next disc
+ continue;
+ }
+ oldDiscTitle = strDiscSubtitle;
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string path = StringUtils::Format("{}/", discnum);
+ itemUrl.AppendPath(path);
+
+ // When disc titles are provided group discs together by title not number.
+ // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
+ if (useTitle)
+ itemUrl.AddOption("disctitle", strDiscSubtitle.c_str());
+ else
+ itemUrl.AddOption("discid", discnum);
+ CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album));
+ pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order??
+ pItem->GetMusicInfoTag()->SetDiscNumber(discnum);
+ pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle);
+ pItem->SetLabel(strDiscSubtitle);
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ pItem->SetProperty("icon_never_overlay", true);
+ pItem->SetArt("icon", "DefaultAlbumCover.png");
+ items.Add(pItem);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
+ items.Size());
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
+ // that is when have join with songartistview and sorting other than random with limit
+ if (sorting.sortBy != SortByNone && !(limitedInSQL && sorting.sortBy == SortByRandom))
+ items.Sort(sorting);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
+ }
+
+ return false;
+}
+int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */)
+{
+ int iDiscTotal = -1;
+ CFileItemList itemscount;
+ if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true))
+ iDiscTotal = itemscount.GetProperty("total").asInteger32();
+ return iDiscTotal;
+}
+
+bool CMusicDatabase::GetSongsFullByWhere(
+ const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool artistData /* = false*/)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool extended = false;
+ bool limitedInSQL =
+ extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0);
+
+ // If there are extra WHERE conditions (from media filter dialog) we might
+ // need access to albumview for these conditions
+ if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
+ }
+
+ // Build songview <where> for count
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count (without group by) number of songs that satisfy selection criteria
+ // Much quicker to use song table, not songview, when filtering only on song fields
+ if (extended ||
+ (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos ||
+ extFilter.where.find("strPath") != std::string::npos ||
+ extFilter.where.find("bCompilation") != std::string::npos ||
+ extFilter.where.find("bBoxedset") != std::string::npos)))
+ total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS);
+ else
+ {
+ std::string strSQLsong = strSQLExtra;
+ StringUtils::Replace(strSQLsong, "songview", "song");
+ total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS);
+ }
+
+ if (extended)
+ extFilter.AppendGroup("songview.idSong");
+
+ // Apply any limiting directly in SQL
+ if (limitedInSQL)
+ {
+ extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
+ }
+
+ // Apply sort in SQL
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
+ GetOrderFilter(MediaTypeSong, sorting, extFilter);
+ // Modify order to use correct calculated year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
+ else
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
+
+ std::string strFields = "songview.*";
+ if (!artistData || limitedInSQL)
+ {
+ // Build songview <where> + <order by> + <limits>
+ strSQLExtra.clear();
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+ }
+ else
+ strFields = "songview.*, songartistview.*";
+ if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
+ strFields = strFields + ", " + extFilter.fields;
+
+ std::string strSQL;
+ if (artistData)
+ { // Get data from song and song_artist tables to fully populate songs with artists
+ // All songs now have at least one artist so inner join sufficient
+ // Build songartistview JOIN part of query
+ Filter joinFilter;
+ std::string strSQLJoin;
+ joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
+ if (sortDescription.sortBy == SortByRandom)
+ joinFilter.AppendOrder("songartistview.idSong");
+ else
+ joinFilter.order = extFilter.order;
+ if (limitedInSQL)
+ {
+ StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong");
+ StringUtils::Replace(joinFilter.order, "songview.", "sv.");
+ }
+ else
+ joinFilter.where = extFilter.where;
+ joinFilter.AppendOrder("songartistview.idRole");
+ joinFilter.AppendOrder("songartistview.iOrder");
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ if (limitedInSQL)
+ {
+ // When have artist data (all roles) and LIMIT on songs use inline view
+ // SELECT sv.*, songartistview.* FROM
+ // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
+ // <order by sv fields>, songartistview.idRole, songartistview.iOrder
+ // Apply where clause, limits and order to songview, then join to songartistview this gives
+ // multiple records per song in result set
+ strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
+ strSQL = "(" + strSQL + ") AS sv ";
+ strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin;
+ }
+ else
+ strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin;
+ }
+ else
+ strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
+
+ CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
+ auto queryStart = std::chrono::steady_clock::now();
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ // Store the total number of songs as a property
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ // Populate results field vector from dataset
+ FieldList fields;
+ if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results))
+ return false;
+ // Store item list sort order
+ items.SetSortMethod(sorting.sortBy);
+ items.SetSortOrder(sorting.sortOrder);
+
+ // Get songs from returned rows. If join songartistview then there is a row for every artist
+ items.Reserve(total);
+ int songArtistOffset = song_enumCount;
+ int songId = -1;
+ VECARTISTCREDITS artistCredits;
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ int count = 0;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ if (songId != record->at(song_idSong).get_asInt())
+ { //New song
+ if (songId > 0 && !artistCredits.empty())
+ {
+ //Store artist credits for previous song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ songId = record->at(song_idSong).get_asInt();
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), musicUrl);
+ // HACK for sorting by database returned order
+ item->m_iprogramCount = ++count;
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ item->SetProperty("icon_never_overlay", true);
+ item->SetArt("icon", "DefaultAudio.png");
+ items.Add(item);
+ }
+ // Get song artist credits and contributors
+ if (artistData)
+ {
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (idSongArtistRole == ROLE_ARTIST)
+ artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
+ GetArtistRoleFromDataset(record, songArtistOffset));
+ }
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
+ return (items.Size() > 0);
+ }
+ }
+ if (!artistCredits.empty())
+ {
+ //Store artist credits for final song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ // cleanup
+ m_pDS->close();
+
+ // Ensure random order of item list when results set sorted by idSong for artist processing
+ // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
+ // navigation. Order is read later from view state and list sorting is then triggered by
+ // CGUIMediaWindow::Update in both cases.
+ // So sorting here is currently redundant, but the consistent place to do it.
+ // !@ todo: do sorting once, preferably in SQL
+ if (sorting.sortBy == SortByRandom && artistData)
+ items.Sort(sorting);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ // cleanup
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSongsByWhere(
+ const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ int total = -1;
+
+ std::string strSQL = "SELECT %s FROM songview ";
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // if there are extra WHERE conditions we might need access
+ // to songview for these conditions
+ if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
+ extFilter.AppendGroup("songview.idSong");
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0))
+ {
+ total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0
+ ? filter.fields.c_str()
+ : "songview.*") +
+ strSQLExtra;
+
+ CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results))
+ return false;
+
+ // get data from returned rows
+ items.Reserve(results.size());
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ int count = 0;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), musicUrl);
+ // HACK for sorting by database returned order
+ item->m_iprogramCount = ++count;
+ items.Add(item);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
+ return (items.Size() > 0);
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ // cleanup
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(baseDir))
+ return false;
+
+ musicUrl.AddOption("year", year);
+
+ Filter filter;
+ return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true);
+}
+
+bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre,
+ int idArtist,
+ int idAlbum,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ if (idAlbum > 0)
+ musicUrl.AddOption("albumid", idAlbum);
+
+ if (idGenre > 0)
+ musicUrl.AddOption("genreid", idGenre);
+
+ if (idArtist > 0)
+ musicUrl.AddOption("artistid", idArtist);
+
+ Filter filter;
+ return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true);
+}
+
+// clang-format off
+typedef struct
+{
+ std::string fieldJSON; // Field name in JSON schema
+ std::string formatJSON; // Format in JSON schema
+ bool bSimple; // Fetch field directly to JSON output
+ std::string fieldDB; // Name of field in db query
+ std::string SQL; // SQL for scalar subqueries or field alias
+} translateJSONField;
+
+static const translateJSONField JSONtoDBArtist[] = {
+ // Table and single value join fields
+ { "artist", "string", true, "strArtist", "" }, // Label field at top
+ { "sortname", "string", true, "strSortname", "" },
+ { "instrument", "array", true, "strInstruments", "" },
+ { "description", "string", true, "strBiography", "" },
+ { "genre", "array", true, "strGenres", "" },
+ { "mood", "array", true, "strMoods", "" },
+ { "style", "array", true, "strStyles", "" },
+ { "yearsactive", "array", true, "strYearsActive", "" },
+ { "born", "string", true, "strBorn", "" },
+ { "formed", "string", true, "strFormed", "" },
+ { "died", "string", true, "strDied", "" },
+ { "disbanded", "string", true, "strDisbanded", "" },
+ { "type", "string", true, "strType", "" },
+ { "gender", "string", true, "strGender", "" },
+ { "disambiguation", "string", true, "strDisambiguation", "" },
+ { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
+ { "dateadded", "string", true, "dateAdded", "" },
+ { "datenew", "string", true, "dateNew", "" },
+ { "datemodified", "string", true, "dateModified", "" },
+
+ // JOIN fields (multivalue), same order as _JoinToArtistFields
+ { "", "", false, "isSong", "" },
+ { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" },
+ { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" },
+ { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" },
+ { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" },
+ { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" },
+ { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" },
+ { "art", "", false, "idArt", "art.art_id AS idArt" },
+ { "", "", false, "artType", "art.type AS artType" },
+ { "", "", false, "artURL", "art.url AS artURL" },
+ { "", "", false, "idRole", "song_artist.idRole" },
+ { "roles", "", false, "strRole", "role.strRole" },
+ { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
+ // Derived from joined tables
+ { "isalbumartist", "bool", false, "", "" },
+ { "thumbnail", "string", false, "", "" },
+ { "fanart", "string", false, "", "" }
+ /*
+ Sources and genre are related via album, and so the dataset only contains source and genre
+ pairs that exist, rather than all the genres being repeated for every source. We can not only
+ look at genres for the first source, and genre can be out of order.
+ */
+};
+// clang-format on
+
+static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField);
+
+bool CMusicDatabase::GetArtistsByWhereJSON(
+ const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ total = -1;
+
+ size_t resultcount = 0;
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // Replace view names in filter with table names
+ StringUtils::Replace(extFilter.where, "artistview", "artist");
+ StringUtils::Replace(extFilter.where, "albumview", "album");
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of artists that satisfy selection criteria
+ //(includes xsp limits from filter, but not sort limits)
+ total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS);
+ resultcount = static_cast<size_t>(total);
+
+ // Process albumartistsonly option
+ const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
+ bool albumArtistsOnly(false);
+ auto option = options.find("albumartistsonly");
+ if (option != options.end())
+ albumArtistsOnly = option->second.asBoolean();
+ // Process role options
+ int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles.
+ option = options.find("roleid");
+ if (option != options.end())
+ roleidfilter = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("role");
+ if (option != options.end())
+ {
+ if (option->second.asString() == "all" || option->second.asString() == "%")
+ roleidfilter = -1000; //All roles
+ else
+ roleidfilter = GetRoleByName(option->second.asString());
+ }
+ }
+
+ // Get order by (and any scalar query artist fields)
+ int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter);
+ // Replace artistview field names in order by artist table field names
+ StringUtils::Replace(extFilter.order, "artistview", "artist");
+ StringUtils::Replace(extFilter.fields, "artistview", "artist");
+
+ // Grab and adjust artist sort field that may have been added to filter
+ // These need to be added to the end of the artist table field list
+ std::string artistsortSQL = extFilter.fields;
+ extFilter.fields.clear();
+
+ std::string strSQL;
+
+ // Setup fields to query, and album field number mapping
+ // Find first join field (isSong) in JSONtoDBArtist for offset
+ int index_firstjoin = -1;
+ for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++)
+ {
+ if (JSONtoDBArtist[i].fieldDB == "isSong")
+ {
+ index_firstjoin = i;
+ break;
+ }
+ }
+ Filter joinFilter;
+ Filter albumArtistFilter;
+ Filter songArtistFilter;
+ DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount));
+ extFilter.AppendField("artist.idArtist"); // ID "artistid" in JSON
+ std::vector<int> dbfieldindex;
+ // JSON "label" field is strArtist which is also output as "artist", query field once output twice
+ extFilter.AppendField(JSONtoDBArtist[0].fieldDB);
+ dbfieldindex.emplace_back(0); // Output "artist"
+
+ // Check each optional artist db field that could be retrieved (not "artist")
+ for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++)
+ {
+ bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end();
+ if (JSONtoDBArtist[i].bSimple)
+ {
+ // Check for non-join fields in order too.
+ // Query these in inline view (but not output) so can ref in outer order
+ bool foundOrderby(false);
+ if (!foundJSON)
+ foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos;
+ if (foundOrderby || foundJSON)
+ {
+ // Store indexes of requested artist table and scalar subquery fields
+ // to be output, and -1 when not output to JSON
+ if (!foundJSON)
+ dbfieldindex.emplace_back(-1);
+ else
+ dbfieldindex.emplace_back(i);
+ // Field from scaler subquery
+ if (!JSONtoDBArtist[i].SQL.empty())
+ extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL));
+ else
+ // Field from artist table
+ extFilter.AppendField(JSONtoDBArtist[i].fieldDB);
+ }
+ }
+ else if (foundJSON)
+ // Field from join or derived from joined fields
+ joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true);
+ }
+
+ // Append calculated artistsort field that may have been added to filter
+ // Field used only for ORDER BY, not output to JSON
+ extFilter.AppendField(artistsortSQL);
+ for (int i = 0; i < iAddedFields; i++)
+ dbfieldindex.emplace_back(-2); // columns in dataset
+
+ // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
+ strSQLExtra = "";
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Add any LIMIT clause to strSQLExtra
+ if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
+ {
+ strSQLExtra +=
+ DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
+ resultcount = std::min(
+ DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
+ resultcount);
+ }
+
+ // Setup multivalue JOINs, GROUP BY and ORDER BY
+ bool bJoinAlbumArtist(false);
+ bool bJoinSongArtist(false);
+ if (sortDescription.sortBy != SortByRandom)
+ {
+ // Repeat inline view order (that always includes idArtist) on join query
+ std::string order = extFilter.order;
+ StringUtils::Replace(order, "artist.", "a1.");
+ joinFilter.AppendOrder(order);
+ }
+ else
+ joinFilter.AppendOrder("a1.idArtist");
+ joinFilter.AppendGroup("a1.idArtist");
+ // Album artists and song artists
+ if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) ||
+ joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
+ joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) ||
+ joinLayout.GetFetch(joinToArtist_strRole))
+ {
+ bJoinAlbumArtist = true;
+ albumArtistFilter.AppendField("album_artist.idArtist AS id");
+ if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole))
+ {
+ bJoinSongArtist = true;
+ songArtistFilter.AppendField("song_artist.idArtist AS id");
+ songArtistFilter.AppendField("1 AS isSong");
+ albumArtistFilter.AppendField("0 AS isSong");
+ joinLayout.SetField(joinToArtist_isSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
+ }
+ }
+ else if (joinLayout.GetFetch(joinToArtist_isalbumartist))
+ {
+ // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
+ // so no need for join to album_artist table. Set fetching fetch false so that
+ // joinLayout.HasFilterFields() is false
+ joinLayout.SetFetch(joinToArtist_isalbumartist, false);
+ }
+
+ // Sources
+ if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
+ { // Left join as source may have been removed but leaving lib entries
+ albumArtistFilter.AppendJoin(
+ "LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
+ albumArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
+ if (bJoinSongArtist)
+ {
+ songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+ songArtistFilter.AppendJoin(
+ "LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
+ songArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
+ songArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL);
+ albumArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ joinLayout.SetField(joinToArtist_idSourceSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ }
+ else
+ {
+ joinLayout.SetField(joinToArtist_idSourceAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true);
+ }
+ }
+
+ // Songgenres - id and genres always both
+ if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
+ { // All albums have songs, but left join genre as songs may not have genre
+ albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
+ albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
+ albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
+ albumArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL);
+ albumArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
+ joinLayout.SetField(joinToArtist_strSongGenreAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
+ joinFilter.AppendGroup(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
+ joinFilter.AppendOrder(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
+ if (bJoinSongArtist)
+ { // Left join genre as songs may not have genre
+ songArtistFilter.AppendJoin(
+ "LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
+ songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
+ songArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
+ songArtistFilter.AppendField(
+ "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
+ songArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL);
+ songArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL);
+ albumArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ albumArtistFilter.AppendField(
+ "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
+ joinLayout.SetField(joinToArtist_idSongGenreSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ joinLayout.SetField(
+ joinToArtist_strSongGenreSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
+ joinFilter.AppendGroup(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ joinFilter.AppendOrder(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ }
+ else
+ { // Define field alias names in join layout
+ joinLayout.SetField(joinToArtist_idSongGenreAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL,
+ true);
+ joinLayout.SetField(joinToArtist_strSongGenreAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
+ }
+ }
+
+ // Roles
+ if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole))
+ // Only looking at album and song artists not other roles (default),
+ // so filter dataset rows likewise.
+ songArtistFilter.AppendWhere("song_artist.idRole = 1");
+ else if (joinLayout.GetFetch(joinToArtist_strRole) || // "roles" field
+ (bJoinSongArtist && (joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
+ joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))))
+ { // Rows from many roles so fetch roleid for "roles", source and genre processing
+ songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL);
+ // Add fake column to album_artist query
+ albumArtistFilter.AppendField("-1 AS " +
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ joinLayout.SetField(joinToArtist_idRole,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ }
+ if (joinLayout.GetFetch(joinToArtist_strRole))
+ { // Fetch role desc
+ songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
+ songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL);
+ // Add fake column to album_artist query
+ albumArtistFilter.AppendField("'albumartist' AS " +
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB);
+ }
+
+ // Build source, genre and roles part of query
+ if (bJoinAlbumArtist)
+ {
+ if (bJoinSongArtist)
+ {
+ // Combine song and album artist filter as UNION and add to join filter as an inline view
+ std::string strAlbumSQL;
+ if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL))
+ return false;
+ strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL;
+ std::string strSongSQL;
+ if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL))
+ return false;
+ strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL;
+
+ joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL +
+ ") AS albumSong ON id = a1.idArtist");
+ }
+ else
+ { //Only join album_artist, so move filter elements to join filter
+ joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
+ joinFilter.AppendJoin(albumArtistFilter.join);
+ }
+ }
+
+ //Art
+ bool bJoinArt(false);
+ bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) ||
+ joinLayout.GetOutput(joinToArtist_thumbnail) ||
+ joinLayout.GetOutput(joinToArtist_fanart);
+ if (bJoinArt)
+ { // Left join as artist may not have any art
+ joinFilter.AppendJoin(
+ "LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
+ joinLayout.SetField(joinToArtist_idArt,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL,
+ joinLayout.GetOutput(joinToArtist_idArt));
+ joinLayout.SetField(joinToArtist_artType,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL);
+ joinLayout.SetField(joinToArtist_artURL,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL);
+ joinFilter.AppendGroup("art.art_id");
+ joinFilter.AppendOrder("arttype");
+ if (!joinLayout.GetOutput(joinToArtist_idArt))
+ {
+ if (!joinLayout.GetOutput(joinToArtist_thumbnail))
+ // Fanart only
+ joinFilter.AppendJoin("AND art.type = 'fanart'");
+ else if (!joinLayout.GetOutput(joinToArtist_fanart))
+ // Thumb only
+ joinFilter.AppendJoin("AND art.type = 'thumb'");
+ }
+ }
+ else if (bJoinSongArtist)
+ joinFilter.group.clear(); // UNION only so no GROUP BY needed
+
+ // Build JOIN part of query (if we have one)
+ std::string strSQLJoin;
+ if (joinLayout.HasFilterFields())
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ // Adjust where in the results record the join fields are allowing for the
+ // inline view fields (Quicker than finding field by name every time)
+ // idArtist + other artist fields
+ joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
+
+ // Build full query
+ // When have multiple value joins e.g. song genres, use inline view
+ // SELECT a1.*, <join fields> FROM
+ // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1
+ // <joins> <group by> <order by> + <joins order by>
+ // Don't use prepareSQL - confuses arttype = 'thumb' filter
+
+ strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra;
+ if (joinLayout.HasFilterFields())
+ {
+ strSQL = "(" + strSQL + ") AS a1 ";
+ strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
+ }
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ // run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get artists from returned rows. Joins means there can be many rows per artist
+ int artistId = -1;
+ int sourceId = -1;
+ int genreId = -1;
+ int roleId = -1;
+ int artId = -1;
+ std::vector<int> genreidlist;
+ std::vector<int> sourceidlist;
+ std::vector<int> roleidlist;
+ bool bArtDone(false);
+ bool bHaveArtist(false);
+ bool bIsAlbumArtist(true);
+ bool bGenreFoundViaAlbum(false);
+ CVariant artistObj;
+ result["artists"].reserve(resultcount);
+ while (!m_pDS->eof() || bHaveArtist)
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (m_pDS->eof() || artistId != record->at(0).get_asInt())
+ {
+ // Store previous or last artist
+ if (bHaveArtist)
+ {
+ // Convert any empty MBid array into an array with one empty element [""]
+ // to match the number of artist ID (way other mbid arrays handled)
+ if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty())
+ artistObj["musicbrainzartistid"].append("");
+
+ result["artists"].append(artistObj);
+ bHaveArtist = false;
+ artistObj.clear();
+ }
+ if (artistObj.empty())
+ {
+ // Initialise fields, ensure those with possible null values are set to correct empty variant type
+ if (joinLayout.GetOutput(joinToArtist_idSourceAlbum))
+ artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
+ if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum))
+ artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
+ if (joinLayout.GetOutput(joinToArtist_idArt))
+ artistObj["art"] = CVariant(CVariant::VariantTypeObject);
+ if (joinLayout.GetOutput(joinToArtist_thumbnail))
+ artistObj["thumbnail"] = "";
+ if (joinLayout.GetOutput(joinToArtist_fanart))
+ artistObj["fanart"] = "";
+
+ sourceId = -1;
+ roleId = -1;
+ genreId = -1;
+ artId = -1;
+ genreidlist.clear();
+ bGenreFoundViaAlbum = false;
+ sourceidlist.clear();
+ roleidlist.clear();
+ bArtDone = false;
+ }
+ if (m_pDS->eof())
+ continue; // Having saved the last artist stop
+
+ // New artist
+ artistId = record->at(0).get_asInt();
+ bHaveArtist = true;
+ artistObj["artistid"] = artistId;
+ artistObj["label"] = record->at(1).get_asString();
+ artistObj["artist"] = record->at(1).get_asString(); // Always have "artist"
+ bIsAlbumArtist = true; //Album artist by default
+ if (joinLayout.GetOutput(joinToArtist_isalbumartist))
+ {
+ // Not album artist when fetching song artists too and first row for artist isSong=true
+ if (bJoinSongArtist)
+ bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
+ artistObj["isalbumartist"] = bIsAlbumArtist;
+ }
+ for (size_t i = 0; i < dbfieldindex.size(); i++)
+ if (dbfieldindex[i] > -1)
+ {
+ if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
+ else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
+ record->at(1 + i).get_asFloat();
+ else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
+ record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
+ else
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
+ record->at(1 + i).get_asString();
+ }
+ }
+ if (bJoinAlbumArtist)
+ {
+ bool bAlbumArtistRow(true);
+ int idRoleRow = -1;
+ if (bJoinSongArtist)
+ {
+ bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
+ if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull())
+ {
+ idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt();
+ }
+ }
+
+ // Sources - gathered via both album_artist and song_artist (with role = 1)
+ if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
+ {
+ if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() &&
+ sourceId !=
+ record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) ||
+ (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() &&
+ sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt()))
+ {
+ bArtDone = bArtDone || (sourceId > 0); // Not first source, skip art repeats
+ bool found(false);
+ sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt();
+ if (!bAlbumArtistRow)
+ {
+ // Skip other roles (when fetching them)
+ if (idRoleRow > 1)
+ {
+ found = true;
+ }
+ else
+ {
+ sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt();
+ // Song artist row may repeat sources found via album artist
+ // Already have that source?
+ for (const auto& i : sourceidlist)
+ if (i == sourceId)
+ {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found)
+ {
+ sourceidlist.emplace_back(sourceId);
+ artistObj["sourceid"].append(sourceId);
+ }
+ }
+ }
+ // Songgenres - via album artist takes precedence
+ /*
+ Sources and genre are related via album, and so the dataset only contains source
+ and genre pairs that exist, rather than all the genres being repeated for every
+ source. We can not only look at genres for the first source, and genre can be
+ found out of order.
+ Also song artist row may repeat genres found via album artist
+ */
+ if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
+ {
+ std::string strGenre;
+ bool newgenre(false);
+ if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() &&
+ genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt())
+ {
+ bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
+ newgenre = true;
+ genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt();
+ strGenre =
+ record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString();
+ }
+ else if (!bAlbumArtistRow && !bGenreFoundViaAlbum &&
+ joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() &&
+ genreId !=
+ record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt())
+ {
+ bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
+ newgenre = idRoleRow <= 1; // Skip other roles (when fetching them)
+ genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt();
+ strGenre =
+ record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString();
+ }
+ if (newgenre)
+ {
+ // Already have that genre?
+ bool found(false);
+ for (const auto& i : genreidlist)
+ if (i == genreId)
+ {
+ found = true;
+ break;
+ }
+ if (!found)
+ {
+ bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow;
+ genreidlist.emplace_back(genreId);
+ CVariant genreObj;
+ genreObj["genreid"] = genreId;
+ genreObj["title"] = strGenre;
+ artistObj["songgenres"].append(genreObj);
+ }
+ }
+ }
+ // Roles - gathered via song_artist roleid rows
+ if (joinLayout.GetFetch(joinToArtist_idRole))
+ {
+ if (!bAlbumArtistRow && roleId != idRoleRow)
+ {
+ bArtDone = bArtDone || (roleId > 0); // Not first role, skip art repeats
+ roleId = idRoleRow;
+ if (joinLayout.GetOutput(joinToArtist_strRole))
+ {
+ // Already have that role?
+ bool found(false);
+ for (const auto& i : roleidlist)
+ if (i == roleId)
+ {
+ found = true;
+ break;
+ }
+ if (!found)
+ {
+ roleidlist.emplace_back(roleId);
+ CVariant roleObj;
+ roleObj["roleid"] = roleId;
+ roleObj["role"] =
+ record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString();
+ artistObj["roles"].append(roleObj);
+ }
+ }
+ }
+ }
+ }
+ // Art
+ if (bJoinArt && !bArtDone &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() &&
+ record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 &&
+ artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt())
+ {
+ artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt();
+ if (joinLayout.GetOutput(joinToArtist_idArt))
+ {
+ artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] =
+ CTextureUtils::GetWrappedImageURL(
+ record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
+ }
+ if (joinLayout.GetOutput(joinToArtist_thumbnail) &&
+ record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb")
+ {
+ artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL(
+ record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
+ }
+ if (joinLayout.GetOutput(joinToArtist_fanart) &&
+ record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart")
+ {
+ artistObj["fanart"] = CTextureUtils::GetWrappedImageURL(
+ record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
+ }
+ }
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ // Ensure random order of output when results set is sorted to process multi-value joins
+ if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
+ KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+// clang-format off
+static const translateJSONField JSONtoDBAlbum[] = {
+ // albumview (inc scalar subquery fields use in filter rules)
+ { "title", "string", true, "strAlbum", "" }, // Label field at top
+ { "description", "string", true, "strReview", "" },
+ { "genre", "array", true, "strGenres", "" },
+ { "theme", "array", true, "strThemes", "" },
+ { "mood", "array", true, "strMoods", "" },
+ { "style", "array", true, "strStyles", "" },
+ { "type", "string", true, "strType", "" },
+ { "albumlabel", "string", true, "strLabel", "" },
+ { "rating", "float", true, "fRating", "" },
+ { "votes", "integer", true, "iVotes", "" },
+ { "userrating", "unsigned", true, "iUserrating", "" },
+ { "isboxset", "boolean", true, "bBoxedSet", "" },
+ { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" },
+ { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table
+ { "compilation", "boolean", true, "bCompilation", "" },
+ { "releasetype", "string", true, "strReleaseType", "" },
+ { "totaldiscs", "integer", true, "iDiscTotal", "" },
+ { "sortartist", "string", true, "strArtistSort", "" },
+ { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" },
+ { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view
+ { "dateadded", "string", true, "dateAdded", "" },
+ { "datenew", "string", true, "dateNew", "" },
+ { "datemodified", "string", true, "dateModified", "" },
+ { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view
+ { "originaldate", "string", true, "strOrigReleaseDate", "" },
+ { "releasedate", "string", true, "strReleaseDate", "" },
+ { "albumstatus", "string", true, "strReleaseStatus", "" },
+ { "albumduration", "integer", true, "iAlbumDuration", "" },
+ // Scalar subquery fields
+ { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
+ { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
+ { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
+ "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
+ // Single value JOIN fields
+ { "thumbnail", "image", true, "thumbnail", "art.url AS thumbnail" }, // or (SELECT art.url FROM art WHERE art.media_id = album.idAlbum AND art.media_type = "album" AND art.type = "thumb") as url
+ // JOIN fields (multivalue), same order as _JoinToAlbumFields
+ { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" },
+ { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" },
+ { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" },
+ /*
+ Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
+ and separate queries to allow for fallback strategy.
+
+ Using albmview, rather than album table, as view has scalar subqueries for
+ playcount and lastplayed already defined. Needed as MySQL does
+ not support use of scalar subquery field alias names in where clauses (they
+ have to be repeated) and these fields can be used by filter rules.
+ Using this view is no slower than the album table as these scalar fields are
+ only calculated (slowing query) when field is in field list.
+ */
+};
+// clang-format on
+
+static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField);
+
+bool CMusicDatabase::GetAlbumsByWhereJSON(
+ const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ total = -1;
+
+ size_t resultcount = 0;
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
+ // passed in at the start of the function
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // Replace view names in filter with table names
+ StringUtils::Replace(extFilter.where, "artistview", "artist");
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of albums that satisfy selection criteria
+ // (includes xsp limits from filter, but not sort limits)
+ // Use albumview as filter rules in where clause may use scalar query fields
+ total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
+ resultcount = static_cast<size_t>(total);
+
+ // Get order by (and any scalar query artist fields
+ int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter);
+
+ // Grab calculated artist/title sort fields that may have been added to filter
+ // These need to be added to the end of the album table field list
+ std::string calcsortfieldsSQL = extFilter.fields;
+ extFilter.fields.clear();
+
+ std::string strSQL;
+
+ // Setup fields to query, and album field number mapping
+ // Find idArtist in JSONtoDBAlbum, offset of first join field
+ int index_idArtist = -1;
+ for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++)
+ {
+ if (JSONtoDBAlbum[i].fieldDB == "idArtist")
+ {
+ index_idArtist = i;
+ break;
+ }
+ }
+ Filter joinFilter;
+ DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount));
+ extFilter.AppendField("albumview.idAlbum"); // ID "albumid" in JSON
+ std::vector<int> dbfieldindex;
+ // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
+ extFilter.AppendField(JSONtoDBAlbum[0].fieldDB);
+ if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end())
+ dbfieldindex.emplace_back(0); // Output "title"
+ else
+ dbfieldindex.emplace_back(-1); // fetch but not output
+
+ // Check each optional album db field that could be retrieved (not label)
+ for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++)
+ {
+ bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end();
+ if (JSONtoDBAlbum[i].bSimple)
+ {
+ // Check for non-join fields in order too.
+ // Query these in inline view (but not output) so can ref in outer order
+ bool foundOrderby(false);
+ if (!foundJSON)
+ foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos;
+ if (foundOrderby || foundJSON)
+ {
+ // Store indexes of requested album table and scalar subquery fields
+ // to be output, and -1 when not output to JSON
+ if (!foundJSON)
+ dbfieldindex.emplace_back(-1);
+ else
+ dbfieldindex.emplace_back(i);
+ if (!JSONtoDBAlbum[i].SQL.empty())
+ // Field from scaler subquery
+ extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL));
+ else
+ // Field from album table
+ extFilter.AppendField(JSONtoDBAlbum[i].fieldDB);
+ }
+ }
+ else if (foundJSON)
+ // Field from join found in JSON request
+ joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true);
+ }
+
+ // Append calculated artist/title sort fields that may have been added to filter
+ // Field used only for ORDER BY, not output to JSON
+ extFilter.AppendField(calcsortfieldsSQL);
+ for (int i = 0; i < iAddedFields; i++)
+ dbfieldindex.emplace_back(-1); // columns in dataset
+
+ // JOIN art tables if needed (fields output and/or in sort)
+ if (extFilter.fields.find("art.") != std::string::npos)
+ { // Left join as not all albums have art, but only have one thumb at most
+ extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
+ "AND art.media_type = 'album' AND art.type = 'thumb'");
+ }
+
+ // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
+ strSQLExtra = "";
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Add any LIMIT clause to strSQLExtra
+ if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
+ {
+ strSQLExtra +=
+ DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
+ resultcount = std::min(
+ DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
+ resultcount);
+ }
+
+ // Setup multivalue JOINs, GROUP BY and ORDER BY
+ bool bJoinAlbumArtist(false);
+ if (sortDescription.sortBy != SortByRandom)
+ {
+ // Repeat inline view order (that always includes idAlbum) on join query
+ std::string order = extFilter.order;
+ StringUtils::Replace(order, "albumview.", "a1.");
+ joinFilter.AppendOrder(order);
+ }
+ else
+ joinFilter.AppendOrder("a1.idAlbum");
+ joinFilter.AppendGroup("a1.idAlbum");
+ // Album artists
+ if (joinLayout.GetFetch(joinToAlbum_idArtist) || joinLayout.GetFetch(joinToAlbum_strArtist) ||
+ joinLayout.GetFetch(joinToAlbum_strArtistMBID))
+ { // All albums have at least one artist so inner join sufficient
+ bJoinAlbumArtist = true;
+ joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
+ joinFilter.AppendGroup("album_artist.idArtist");
+ joinFilter.AppendOrder("album_artist.iOrder");
+ // Ensure idArtist is queried
+ if (!joinLayout.GetFetch(joinToAlbum_idArtist))
+ joinLayout.SetField(joinToAlbum_idArtist,
+ JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL);
+ }
+ // artist table needed for strArtist or MBID
+ // (album_artist.strArtist can be an alias or spelling variation)
+ if (joinLayout.GetFetch(joinToAlbum_strArtist) ||
+ joinLayout.GetFetch(joinToAlbum_strArtistMBID))
+ joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
+
+ // Build JOIN part of query (if we have one)
+ std::string strSQLJoin;
+ if (joinLayout.HasFilterFields())
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ // Adjust where in the results record the join fields are allowing for the
+ // inline view fields (Quicker than finding field by name every time)
+ // idAlbum + other album fields
+ joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
+
+ // Build full query
+ // When have multiple value joins (artists or song genres) use inline view
+ // SELECT a1.*, <join fields> FROM
+ // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1
+ // <joins> <group by> <order by> <joins order by>
+ // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
+
+ strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra;
+ if (joinLayout.HasFilterFields())
+ {
+ strSQL = "(" + strSQL + ") AS a1 ";
+ strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
+ }
+
+ // Modify query to use correct year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate");
+ else
+ StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate");
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ // run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get albums from returned rows. Joins means there can be many rows per album
+ int albumId = -1;
+ int artistId = -1;
+ CVariant albumObj;
+ result["albums"].reserve(resultcount);
+ while (!m_pDS->eof() || !albumObj.empty())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (m_pDS->eof() || albumId != record->at(0).get_asInt())
+ {
+ // Store previous or last album
+ if (!albumObj.empty())
+ {
+ // Split sources string into int array
+ if (albumObj.isMember("sourceid"))
+ {
+ std::vector<std::string> sources =
+ StringUtils::Split(albumObj["sourceid"].asString(), ";");
+ albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
+ for (size_t i = 0; i < sources.size(); i++)
+ albumObj["sourceid"].append(atoi(sources[i].c_str()));
+ }
+ result["albums"].append(albumObj);
+ albumObj.clear();
+ artistId = -1;
+ }
+ if (m_pDS->eof())
+ continue; // Having saved last album stop
+
+ // New album
+ albumId = record->at(0).get_asInt();
+ albumObj["albumid"] = albumId;
+ albumObj["label"] = record->at(1).get_asString();
+ for (size_t i = 0; i < dbfieldindex.size(); i++)
+ if (dbfieldindex[i] > -1)
+ {
+ if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres")
+ {
+ // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
+ std::vector<std::string> values =
+ StringUtils::Split(record->at(1 + i).get_asString(), ",");
+ if (values.size() % 2 == 0) // Must contain an even number of entries
+ {
+ for (size_t j = 0; j + 1 < values.size(); j += 2)
+ {
+ int idGenre = atoi(values[j].c_str());
+ if (idGenre > 0)
+ {
+ CVariant genreObj;
+ genreObj["genreid"] = idGenre;
+ genreObj["title"] = values[j + 1];
+ albumObj["songgenres"].append(genreObj);
+ }
+ }
+ }
+ // Ensure albums with null songgenres get empty array
+ if (!albumObj.isMember("songgenres"))
+ albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
+ }
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asInt(), 0);
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asFloat(), 0.f);
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
+ record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image")
+ {
+ std::string url = record->at(1 + i).get_asString();
+ if (!url.empty())
+ url = CTextureUtils::GetWrappedImageURL(url);
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url;
+ }
+ else
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
+ }
+ }
+ if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1)
+ {
+ if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt())
+ {
+ artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt();
+ if (joinLayout.GetOutput(joinToAlbum_idArtist))
+ albumObj["artistid"].append(artistId);
+ if (artistId == BLANKARTIST_ID)
+ {
+ if (joinLayout.GetOutput(joinToAlbum_strArtist))
+ albumObj["artist"].append(StringUtils::Empty);
+ if (joinLayout.GetOutput(joinToAlbum_strArtistMBID))
+ albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
+ }
+ else
+ {
+ if (joinLayout.GetOutput(joinToAlbum_strArtist) &&
+ joinLayout.GetRecNo(joinToAlbum_strArtist) > -1)
+ albumObj["artist"].append(
+ record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString());
+ if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) &&
+ joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1)
+ albumObj["musicbrainzalbumartistid"].append(
+ record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString());
+ }
+ }
+ }
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ // Ensure random order of output when results set is sorted to process multi-value joins
+ if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
+ KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+// clang-format off
+static const translateJSONField JSONtoDBSong[] = {
+ // table and single value join fields
+ { "title", "string", true, "strTitle", "" }, // Label field at top
+ { "albumid", "integer", true, "song.idAlbum", "" },
+ { "", "", true, "song.iTrack", "" },
+ { "displayartist", "string", true, "song.strArtistDisp", "" },
+ { "sortartist", "string", true, "song.strArtistSort", "" },
+ { "genre", "array", true, "song.strGenres", "" },
+ { "duration", "integer", true, "iDuration", "" },
+ { "comment", "string", true, "comment", "" },
+ { "", "string", true, "strFileName", "" },
+ { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" },
+ { "playcount", "integer", true, "iTimesPlayed", "" },
+ { "lastplayed", "string", true, "lastPlayed", "" },
+ { "rating", "float", true, "rating", "" },
+ { "votes", "integer", true, "votes", "" },
+ { "userrating", "unsigned", true, "song.userrating", "" },
+ { "mood", "array", true, "mood", "" },
+ { "dateadded", "string", true, "song.dateAdded", "" },
+ { "datenew", "string", true, "song.dateNew", "" },
+ { "datemodified", "string", true, "song.dateModified", "" },
+ { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" },
+ { "", "string", true, "strPath", "path.strPath AS strPath" },
+ { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" },
+ { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" },
+ { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
+ { "disctitle", "string", true, "song.strDiscSubtitle", "" },
+ { "bpm", "integer", true, "iBPM", "" },
+ { "originaldate", "string" , true, "song.strOrigReleaseDate","" },
+ { "releasedate", "string" , true, "song.strReleaseDate", "" },
+ { "bitrate", "integer", true, "iBitRate", "" },
+ { "samplerate", "integer", true, "iSampleRate", "" },
+ { "channels", "integer", true, "iChannels", "" },
+
+ // JOIN fields (multivalue), same order as _JoinToSongFields
+ { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" },
+ { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" },
+ { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
+ { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" },
+ { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" },
+ { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" },
+ { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" },
+ { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" },
+ { "", "", false, "idRole", "song_artist.idRole" },
+ { "", "", false, "strRole", "role.strRole" },
+ { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
+ { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
+ { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" },
+
+ { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" },
+ { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" },
+ { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" },
+ { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" },
+ { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" },
+
+ // Scalar subquery fields
+ { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
+ { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" },
+ { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" },
+ { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
+ /*
+ Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
+ thumbloader and separate queries to allow for fallback strategy
+ "lyrics"?? Can be set for an item (by addons) but not held in db so
+ AudioLibrary.GetSongs() never fills this field despite being in schema
+
+ FROM ( SELECT * FROM song
+ JOIN album ON album.idAlbum = song.idAlbum
+ JOIN path ON path.idPath = song.idPath) AS sv
+ JOIN album_artist ON album_artist.idAlbum = song.idAlbum
+ JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
+ JOIN song_artist ON song_artist.idSong = song.idSong
+ JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist
+ JOIN role ON song_artist.idRole = role.idRole
+ LEFT JOIN song_genre ON song.idSong = song_genre.idSong
+
+ */
+};
+// clang-format on
+
+static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField);
+
+bool CMusicDatabase::GetSongsByWhereJSON(
+ const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ total = -1;
+
+ size_t resultcount = 0;
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
+ // passed into the function
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // Replace view names in filter with table names
+ StringUtils::Replace(extFilter.where, "artistview", "artist");
+ StringUtils::Replace(extFilter.where, "albumview", "album");
+ StringUtils::Replace(extFilter.where, "songview.strPath", "strPath");
+ StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum");
+ StringUtils::Replace(extFilter.where, "songview", "song");
+ StringUtils::Replace(extFilter.where, "songartistview", "song_artist");
+
+ // JOIN album and path tables needed by filter rules in where clause
+ if (extFilter.where.find("album.") != std::string::npos ||
+ extFilter.where.find("strAlbum") != std::string::npos)
+ { // All songs have one album so inner join sufficient
+ extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
+ }
+ if (extFilter.where.find("strPath") != std::string::npos)
+ { // All songs have one path so inner join sufficient
+ extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
+ }
+
+ // Build JOINs and WHERE needed by filter for counting songs
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of songs that satisfy selection criteria
+ // (includes xsp limits from filter, but not sort limits)
+ total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS);
+ resultcount = static_cast<size_t>(total);
+
+ int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter);
+ // Replace songview field names in order by with song, album path table field names
+ // Field names in album same as song:
+ // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
+ StringUtils::Replace(extFilter.order, "songview.strPath", "strPath");
+ StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum");
+ StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation");
+ StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp");
+ StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp");
+ StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort");
+ StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType");
+ StringUtils::Replace(extFilter.order, "songview", "song");
+ StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort");
+ StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp");
+ StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum");
+ StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle");
+
+ // Grab calculated artist/title sort fields that may have been added to filter
+ // These need to be added to the end of the song table field list
+ std::string calcsortfieldsSQL = extFilter.fields;
+ extFilter.fields.clear();
+
+ std::string strSQL;
+
+ // Setup fields to query, and song field number mapping
+ // Find idAlbumArtist in JSONtoDBSong, offset of first join field
+ int index_idAlbumArtist = -1;
+ for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++)
+ {
+ if (JSONtoDBSong[i].fieldDB == "idAlbumArtist")
+ {
+ index_idAlbumArtist = i;
+ break;
+ }
+ }
+ Filter joinFilter;
+ DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount));
+ extFilter.AppendField("song.idSong"); // ID "songid" in JSON
+ std::vector<int> dbfieldindex;
+ // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
+ extFilter.AppendField(JSONtoDBSong[0].fieldDB);
+ if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end())
+ dbfieldindex.emplace_back(0); // Output "title"
+ else
+ dbfieldindex.emplace_back(-1); // Fetch but not output
+ std::vector<std::string> rolefieldlist;
+ std::vector<int> roleidlist;
+ // Check each optional db field that could be retrieved (not label)
+ for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++)
+ {
+ bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end();
+ if (JSONtoDBSong[i].bSimple)
+ {
+ // Check for non-join fields in order too.
+ // Query these in inline view (but not output) so can ref in outer order
+ bool foundOrderby(false);
+ if (!foundJSON)
+ foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos;
+ if (foundOrderby || foundJSON)
+ {
+ // Store indexes of requested album table and scalar subquery fields
+ // to be output, and -1 when not output to JSON
+ if (!foundJSON)
+ dbfieldindex.emplace_back(-1);
+ else
+ dbfieldindex.emplace_back(i);
+ if (!JSONtoDBSong[i].SQL.empty())
+ // Field from scaler subquery
+ extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL));
+ else
+ // Field from song table
+ extFilter.AppendField(JSONtoDBSong[i].fieldDB);
+ }
+ }
+ else if (foundJSON)
+ { // Field from join found in JSON request
+ if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_"))
+ {
+ joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true);
+ }
+ else
+ { // "contributors", "displaycomposer" etc.
+ rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON);
+ }
+ }
+ }
+ // Append calculated artist/title sort fields that may have been added to filter
+ // Field used only for ORDER BY, not output to JSON
+ extFilter.AppendField(calcsortfieldsSQL);
+ for (int i = 0; i < iAddedFields; i++)
+ dbfieldindex.emplace_back(-1); // columns in dataset
+
+ // Build matching list of role id for "displaycomposer", "displayconductor",
+ // "displayorchestra", "displaylyricist"
+ if (!rolefieldlist.empty())
+ {
+ for (const auto& name : rolefieldlist)
+ {
+ int idRole = -1;
+ if (StringUtils::StartsWith(name, "display"))
+ idRole = GetRoleByName(name.substr(7));
+ roleidlist.emplace_back(idRole);
+ }
+ }
+
+ // JOIN album and path tables needed for field output and/or in sort
+ // if not already there for filter
+ if ((extFilter.fields.find("album.") != std::string::npos ||
+ extFilter.fields.find("strAlbum") != std::string::npos) &&
+ extFilter.join.find("JOIN album") == std::string::npos)
+ { // All songs have one album so inner join sufficient
+ extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
+ }
+ if (extFilter.fields.find("path.") != std::string::npos &&
+ extFilter.join.find("JOIN path") == std::string::npos)
+ { // All songs have one path so inner join sufficient
+ extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
+ }
+
+ // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
+ strSQLExtra = "";
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Add any LIMIT clause to strSQLExtra
+ if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
+ {
+ strSQLExtra +=
+ DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
+ resultcount = std::min(
+ DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
+ resultcount);
+ }
+
+ // Setup multivalue JOINs, GROUP BY and ORDER BY
+ bool bJoinSongArtist(false);
+ bool bJoinAlbumArtist(false);
+ bool bJoinRole(false);
+ if (sortDescription.sortBy != SortByRandom)
+ {
+ // Repeat inline view order (that always includes idSong) on join query
+ std::string order = extFilter.order;
+ order = extFilter.order;
+ StringUtils::Replace(order, "album.", "sv.");
+ StringUtils::Replace(order, "song.", "sv.");
+ joinFilter.AppendOrder(order);
+ }
+ else
+ joinFilter.AppendOrder("sv.idSong");
+ joinFilter.AppendGroup("sv.idSong");
+
+ // Album artists
+ if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) ||
+ joinLayout.GetFetch(joinToSongs_strAlbumArtist) ||
+ joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID))
+ { // All songs have at least one album artist so inner join sufficient
+ bJoinAlbumArtist = true;
+ joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
+ joinFilter.AppendGroup("album_artist.idArtist");
+ joinFilter.AppendOrder("album_artist.iOrder");
+ // Ensure idAlbumArtist is queried for processing repeats
+ if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist))
+ {
+ joinLayout.SetField(joinToSongs_idAlbumArtist,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL);
+ }
+ // Ensure song.IdAlbum is field of the inline view for join
+ if (fields.find("albumid") == fields.end())
+ {
+ extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
+ dbfieldindex.emplace_back(-1);
+ }
+ // artist table needed for strArtist or MBID
+ // (album_artist.strArtist can be an alias or spelling variation)
+ if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) ||
+ joinLayout.GetFetch(joinToSongs_strAlbumArtist))
+ joinFilter.AppendJoin(
+ "JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
+ }
+
+ /*
+ Song artists
+ JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
+ "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
+ */
+ if (joinLayout.GetFetch(joinToSongs_idArtist) || joinLayout.GetFetch(joinToSongs_strArtist) ||
+ joinLayout.GetFetch(joinToSongs_strArtistMBID) || !rolefieldlist.empty())
+ { // All songs have at least one artist (idRole = 1) so inner join sufficient
+ bJoinSongArtist = true;
+ if (rolefieldlist.empty())
+ { // song artists only, no other roles needed
+ joinFilter.AppendJoin(
+ "JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
+ joinFilter.AppendGroup("song_artist.idArtist");
+ joinFilter.AppendOrder("song_artist.iOrder");
+ }
+ else
+ {
+ // Ensure idRole is queried
+ if (!joinLayout.GetFetch(joinToSongs_idRole))
+ {
+ joinLayout.SetField(joinToSongs_idRole,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL);
+ }
+ // Ensure strArtist is queried
+ if (!joinLayout.GetFetch(joinToSongs_strArtist))
+ {
+ joinLayout.SetField(joinToSongs_strArtist,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL);
+ }
+ if (fields.find("contributors") != fields.end())
+ { // all roles
+ bJoinRole = true;
+ // Ensure strRole is queried from role table
+ joinLayout.SetField(joinToSongs_strRole, "role.strRole");
+ joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
+ joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
+ joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
+ joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
+ }
+ else
+ { // Get just roles for "displaycomposer", "displayconductor" etc.
+ std::string where;
+ for (size_t i = 0; i < roleidlist.size(); i++)
+ {
+ int idRole = roleidlist[i];
+ if (idRole <= 1)
+ continue;
+ if (where.empty())
+ // Always get song artists too (role = 1) so can do inner join
+ where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole);
+ else
+ where += PrepareSQL(" OR song_artist.idRole = %i", idRole);
+ }
+ where = " (" + where + ")";
+ joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where);
+ joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
+ joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
+ }
+ }
+ // Ensure idArtist is queried for processing repeats
+ if (!joinLayout.GetFetch(joinToSongs_idArtist))
+ {
+ joinLayout.SetField(joinToSongs_idArtist,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL);
+ }
+ // artist table needed for strArtist or MBID
+ // (song_artist.strArtist can be an alias or spelling variation)
+ if (joinLayout.GetFetch(joinToSongs_strArtistMBID) ||
+ joinLayout.GetFetch(joinToSongs_strArtist))
+ joinFilter.AppendJoin(
+ "JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
+ }
+
+ // Genre ids
+ if (joinLayout.GetFetch(joinToSongs_idGenre))
+ { // song genre ids (strGenre demormalised in song table)
+ // Left join as songs may not have genre
+ joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
+ joinFilter.AppendGroup("song_genre.idGenre");
+ joinFilter.AppendOrder("song_genre.iOrder");
+ }
+
+ // Build JOIN part of query (if we have one)
+ std::string strSQLJoin;
+ if (joinLayout.HasFilterFields())
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ // Adjust where in the results record the join fields are allowing for the
+ // inline view fields (Quicker than finding field by name every time)
+ // idSong + other song fields
+ joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
+
+ // Build full query
+ // When have multiple value joins use inline view
+ // SELECT sv.*, <join fields> FROM
+ // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv
+ // <joins> <group by>
+ // <order by> + <joins order by>
+ // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
+ strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra;
+ if (joinLayout.HasFilterFields())
+ {
+ strSQL = "(" + strSQL + ") AS sv ";
+ strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
+ }
+
+ // Modify query to use correct year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate");
+ else
+ StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate");
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ // Run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get song from returned rows. Joins mean there can be many rows per song
+ int songId = -1;
+ int albumartistId = -1;
+ int artistId = -1;
+ int roleId = -1;
+ bool bSongGenreDone(false);
+ bool bSongArtistDone(false);
+ bool bHaveSong(false);
+ CVariant songObj;
+ result["songs"].reserve(resultcount);
+ while (!m_pDS->eof() || bHaveSong)
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (m_pDS->eof() || songId != record->at(0).get_asInt())
+ {
+ // Store previous or last song
+ if (bHaveSong)
+ {
+ // Check empty role fields get returned, and format
+ if (!rolefieldlist.empty())
+ {
+ for (const auto& displayXXX : rolefieldlist)
+ {
+ if (!StringUtils::StartsWith(displayXXX, "display"))
+ {
+ // "contributors"
+ if (!songObj.isMember(displayXXX))
+ songObj[displayXXX] = CVariant(CVariant::VariantTypeArray);
+ }
+ else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray())
+ {
+ // Convert "displaycomposer", "displayconductor", "displayorchestra",
+ // and "displaylyricist" arrays into strings
+ std::vector<std::string> names;
+ for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array();
+ field != songObj[displayXXX].end_array(); ++field)
+ names.emplace_back(field->asString());
+
+ std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ songObj[displayXXX] = role;
+ }
+ else
+ songObj[displayXXX] = "";
+ }
+ }
+ result["songs"].append(songObj);
+ bHaveSong = false;
+ songObj.clear();
+ }
+ if (songObj.empty())
+ {
+ // Initialise fields, ensure those with possible null values are set to correct empty variant type
+ if (joinLayout.GetOutput(joinToSongs_idGenre))
+ songObj["genreid"] =
+ CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array
+
+ albumartistId = -1;
+ artistId = -1;
+ roleId = -1;
+ bSongGenreDone = false;
+ bSongArtistDone = false;
+ }
+ if (m_pDS->eof())
+ continue; // Having saved the last song stop
+
+ // New song
+ songId = record->at(0).get_asInt();
+ bHaveSong = true;
+ songObj["songid"] = songId;
+ songObj["label"] = record->at(1).get_asString();
+ for (size_t i = 0; i < dbfieldindex.size(); i++)
+ if (dbfieldindex[i] > -1)
+ {
+ if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asInt(), 0);
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asFloat(), 0.f);
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
+ record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
+ else
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
+ }
+
+ // Split sources string into int array
+ if (songObj.isMember("sourceid"))
+ {
+ std::vector<std::string> sources =
+ StringUtils::Split(songObj["sourceid"].asString(), ";");
+ songObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
+ for (size_t i = 0; i < sources.size(); i++)
+ songObj["sourceid"].append(atoi(sources[i].c_str()));
+ }
+ }
+
+ if (bJoinAlbumArtist)
+ {
+ if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt())
+ {
+ bSongGenreDone =
+ bSongGenreDone || (albumartistId > 0); // Not first album artist, skip genre
+ bSongArtistDone =
+ bSongArtistDone || (albumartistId > 0); // Not first album artist, skip song artists
+ albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt();
+ if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
+ songObj["albumartistid"].append(albumartistId);
+ if (albumartistId == BLANKARTIST_ID)
+ {
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
+ songObj["albumartist"].append(StringUtils::Empty);
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
+ songObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
+ }
+ else
+ {
+ if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
+ songObj["albumartistid"].append(albumartistId);
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
+ songObj["albumartist"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString());
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
+ songObj["musicbrainzalbumartistid"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString());
+ }
+ }
+ }
+ if (bJoinSongArtist && !bSongArtistDone)
+ {
+ if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt())
+ {
+ bSongGenreDone = bSongGenreDone || (artistId > 0); // Not first artist, skip genre
+ roleId = -1; // Allow for many artists same role
+ artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
+ if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 ||
+ record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1)
+ {
+ if (joinLayout.GetOutput(joinToSongs_idArtist))
+ songObj["artistid"].append(artistId);
+ if (artistId == BLANKARTIST_ID)
+ {
+ if (joinLayout.GetOutput(joinToSongs_strArtist))
+ songObj["artist"].append(StringUtils::Empty);
+ if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
+ songObj["musicbrainzartistid"].append(StringUtils::Empty);
+ }
+ else
+ {
+ if (joinLayout.GetOutput(joinToSongs_strArtist))
+ songObj["artist"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
+ if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
+ songObj["musicbrainzartistid"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString());
+ }
+ }
+ }
+ if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 &&
+ roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt())
+ {
+ bSongGenreDone = bSongGenreDone || (roleId > 0); // Not first role, skip genre
+ roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt();
+ if (roleId > 1)
+ {
+ if (bJoinRole)
+ { //Contributors
+ CVariant contributor;
+ contributor["name"] =
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString();
+ contributor["role"] =
+ record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString();
+ contributor["roleid"] = roleId;
+ contributor["artistid"] =
+ record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
+ songObj["contributors"].append(contributor);
+ }
+ // "displaycomposer", "displayconductor" etc.
+ for (size_t i = 0; i < roleidlist.size(); i++)
+ {
+ if (roleidlist[i] == roleId)
+ {
+ songObj[rolefieldlist[i]].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
+ continue;
+ }
+ }
+ }
+ }
+ }
+ if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull())
+ {
+ songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt());
+ }
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ // Ensure random order of output when results set is sorted to process multi-value joins
+ if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
+ KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField)
+{
+ /*
+ Make SQL clause from ignore article list.
+ Group tokens the same length together, for example :
+ WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
+ THEN SUBSTR(strArtist, 5)
+ WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
+ THEN SUBSTR(strArtist, 4)
+ */
+ std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
+ std::string sortclause;
+ size_t tokenlength = 0;
+ std::string strWhen;
+ for (const auto& token : sortTokens)
+ {
+ if (token.length() != tokenlength)
+ {
+ if (!strWhen.empty())
+ {
+ if (!sortclause.empty())
+ sortclause += " ";
+ std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
+ sortclause += "WHEN " + strWhen + strThen;
+ strWhen.clear();
+ }
+ tokenlength = token.length();
+ }
+ std::string tokenclause = token;
+ //Escape any ' or % in the token
+ StringUtils::Replace(tokenclause, "'", "''");
+ StringUtils::Replace(tokenclause, "%", "%%");
+ // Single %, _ and ' so avoid using PrepareSQL
+ tokenclause = strField + " LIKE '" + tokenclause + "%'";
+ if (token.find('_') != std::string::npos)
+ tokenclause += " ESCAPE '_'";
+ if (!strWhen.empty())
+ strWhen += " OR ";
+ strWhen += tokenclause;
+ }
+ if (!strWhen.empty())
+ {
+ if (!sortclause.empty())
+ sortclause += " ";
+ std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
+ sortclause += "WHEN " + strWhen + strThen;
+ }
+ return sortclause;
+}
+
+std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias,
+ const SortAttribute& sortAttributes,
+ const std::string& strField,
+ const std::string& strSortField)
+{
+ /*
+ Build SQL for sort name scalar subquery from sort attributes and ignore article list.
+ For example :
+ CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
+ WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
+ WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
+ ELSE strField
+ END AS strAlias
+ */
+
+ std::string sortSQL;
+ if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName)
+ sortSQL =
+ PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str());
+ if (sortAttributes & SortAttributeIgnoreArticle)
+ {
+ if (!sortSQL.empty())
+ sortSQL += " ";
+ // Make SQL from ignore article list, grouping tokens the same length together
+ sortSQL += GetIgnoreArticleSQL(strField);
+ }
+ if (!sortSQL.empty())
+ {
+ sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc.
+ sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str());
+ }
+
+ return sortSQL;
+}
+
+std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField,
+ const SortOrder& sortOrder)
+{
+ /*
+ Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
+ and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
+ Natural number order is not significant for where clause comparison and use of calculated fields
+ means there is no advantage in defining as column default in table create than per query (which
+ also makes looking at the db with other tools difficult).
+
+ MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
+ "ascii folding" case insensitive collation. Natural sorting is provided via native functions
+ stored in the db.
+ */
+ std::string DESC;
+ if (sortOrder == SortOrderDescending)
+ DESC = " DESC";
+ std::string strSort;
+
+ if (StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str());
+ else
+ strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str());
+ return strSort;
+}
+
+void CMusicDatabase::UpdateTables(int version)
+{
+ CLog::Log(LOGINFO, "{} - updating tables", __FUNCTION__);
+ if (version < 34)
+ {
+ m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
+ m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
+ m_pDS->exec(
+ "CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, "
+ "strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration "
+ "integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, "
+ "iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, "
+ "lastplayed varchar(20) default NULL, rating char default '0', comment text)\n");
+ m_pDS->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, "
+ "iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) "
+ "SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, "
+ "dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, "
+ "iEndOffset, idThumb, lastplayed, rating, comment FROM song");
+
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+
+ m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
+ }
+
+ if (version < 36)
+ {
+ // translate legacy musicdb:// paths
+ if (m_pDS->query("SELECT strPath FROM content"))
+ {
+ std::vector<std::string> contentPaths;
+ while (!m_pDS->eof())
+ {
+ contentPaths.push_back(m_pDS->fv(0).get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto& originalPath : contentPaths)
+ {
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath);
+ m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(),
+ originalPath.c_str()));
+ }
+ }
+ }
+
+ if (version < 39)
+ {
+ m_pDS->exec("CREATE TABLE album_new "
+ "(idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strArtists text, strGenres text, "
+ " iYear integer, idThumb integer, "
+ " bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " iRating integer, "
+ " lastScraped varchar(20) default NULL, "
+ " dateAdded varchar (20) default NULL)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " iRating) "
+ " SELECT "
+ " album.idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " album.iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, iRating "
+ " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
+ m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE "
+ "albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
+ m_pDS->exec(PrepareSQL(
+ "UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)",
+ CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("DROP TABLE albuminfo");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+ }
+ if (version < 40)
+ {
+ m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
+ " strArtist varchar(256), strMusicBrainzArtistID text, "
+ " strBorn text, strFormed text, strGenres text, strMoods text, "
+ " strStyles text, strInstruments text, strBiography text, "
+ " strDied text, strDisbanded text, strYearsActive text, "
+ " strImage text, strFanart text, "
+ " lastScraped varchar(20) default NULL, "
+ " dateAdded varchar (20) default NULL)");
+ m_pDS->exec("INSERT INTO artist_new "
+ "(idArtist, strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles , strInstruments , strBiography , "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart) "
+ " SELECT "
+ " artist.idArtist, "
+ " strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles, strInstruments, strBiography, "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart "
+ " FROM artist "
+ " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
+ m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT "
+ "idArtist FROM artistinfo)",
+ CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
+ m_pDS->exec("DROP TABLE artist");
+ m_pDS->exec("DROP TABLE artistinfo");
+ m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
+ }
+ if (version < 42)
+ {
+ m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n");
+ m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n");
+ // populate these
+ std::string sql = "select idArtist,strArtist from artist";
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i",
+ m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
+ m_pDS2->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i",
+ m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ }
+ if (version < 48)
+ { // null out columns that are no longer used
+ m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
+ m_pDS->exec("UPDATE album SET idThumb=NULL");
+ }
+ if (version < 49)
+ {
+ m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
+ }
+ if (version < 50)
+ {
+ // add a new column strReleaseType for albums
+ m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n");
+
+ // set strReleaseType based on album name
+ m_pDS->exec(PrepareSQL(
+ "UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
+ m_pDS->exec(
+ PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''",
+ CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
+ }
+ if (version < 51)
+ {
+ m_pDS->exec("ALTER TABLE song ADD mood text\n");
+ }
+ if (version < 53)
+ {
+ m_pDS->exec("ALTER TABLE song ADD dateAdded text");
+ }
+ if (version < 54)
+ {
+ //Remove dateAdded from artist table
+ m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
+ " strArtist varchar(256), strMusicBrainzArtistID text, "
+ " strBorn text, strFormed text, strGenres text, strMoods text, "
+ " strStyles text, strInstruments text, strBiography text, "
+ " strDied text, strDisbanded text, strYearsActive text, "
+ " strImage text, strFanart text, "
+ " lastScraped varchar(20) default NULL)");
+ m_pDS->exec("INSERT INTO artist_new "
+ "(idArtist, strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles , strInstruments , strBiography , "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart, lastScraped) "
+ " SELECT "
+ " idArtist, "
+ " strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles, strInstruments, strBiography, "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart, lastScraped "
+ " FROM artist");
+ m_pDS->exec("DROP TABLE artist");
+ m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
+
+ //Remove dateAdded from album table
+ m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strArtists text, strGenres text, "
+ " iYear integer, idThumb integer, "
+ " bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " iRating integer, "
+ " lastScraped varchar(20) default NULL, "
+ " strReleaseType text)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, iRating, lastScraped, "
+ " strReleaseType) "
+ " SELECT "
+ " album.idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, iRating, lastScraped, "
+ " strReleaseType"
+ " FROM album");
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+ }
+ if (version < 55)
+ {
+ m_pDS->exec("DROP TABLE karaokedata");
+ }
+ if (version < 57)
+ {
+ m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
+ m_pDS->exec("UPDATE song SET userrating = rating * 2");
+ m_pDS->exec("UPDATE song SET rating = 0");
+ m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
+ " idAlbum INTEGER, idPath INTEGER, "
+ " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
+ " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
+ " dwFileNameCRC TEXT, "
+ " strFileName TEXT, strMusicBrainzTrackID TEXT, "
+ " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
+ " idThumb INTEGER, "
+ " lastplayed VARCHAR(20) DEFAULT NULL, "
+ " rating FLOAT DEFAULT 0, "
+ " userrating INTEGER DEFAULT 0, "
+ " comment TEXT, mood TEXT, dateAdded TEXT)");
+ m_pDS->exec("INSERT INTO song_new "
+ "(idSong, "
+ " idAlbum, idPath, "
+ " strArtists, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " dwFileNameCRC, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " idThumb, "
+ " lastplayed,"
+ " rating, userrating, "
+ " comment, mood, dateAdded)"
+ " SELECT "
+ " idSong, "
+ " idAlbum, idPath, "
+ " strArtists, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " dwFileNameCRC, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " idThumb, "
+ " lastplayed,"
+ " rating, "
+ " userrating, "
+ " comment, mood, dateAdded"
+ " FROM song");
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+
+ m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
+ m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
+ " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
+ " strArtists TEXT, strGenres TEXT, "
+ " iYear INTEGER, idThumb INTEGER, "
+ " bCompilation INTEGER NOT NULL DEFAULT '0', "
+ " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
+ " strReview TEXT, strImage TEXT, strLabel TEXT, "
+ " strType TEXT, "
+ " fRating FLOAT NOT NULL DEFAULT 0, "
+ " iUserrating INTEGER NOT NULL DEFAULT 0, "
+ " lastScraped VARCHAR(20) DEFAULT NULL, "
+ " strReleaseType TEXT)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " fRating, "
+ " iUserrating, "
+ " lastScraped, "
+ " strReleaseType)"
+ " SELECT "
+ " idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " iRating, "
+ " iUserrating, "
+ " lastScraped, "
+ " strReleaseType"
+ " FROM album");
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+
+ m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
+ }
+ if (version < 58)
+ {
+ m_pDS->exec("UPDATE album SET fRating = fRating * 2");
+ }
+ if (version < 59)
+ {
+ m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
+ m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
+
+ //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
+ m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, "
+ "iOrder integer, strArtist text)");
+ m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
+ "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
+ m_pDS->exec("DROP TABLE song_artist");
+ m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
+
+ //Remove strJoinPhrase, boolFeatured from album_artist table
+ m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, "
+ "strArtist text)");
+ m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
+ "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
+ m_pDS->exec("DROP TABLE album_artist");
+ m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
+ }
+ if (version < 60)
+ {
+ // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
+ // do not have an artist tag to ensure all songs in the library have at least one artist.
+ std::string strSQL;
+ if (GetArtistExists(BLANKARTIST_ID))
+ {
+ // When BLANKARTIST_ID (=1) is already in use, move the record
+ try
+ { //No mbid index yet, so can have record for artist twice even with mbid
+ strSQL = PrepareSQL("INSERT INTO artist SELECT null, "
+ "strArtist, strMusicBrainzArtistID, "
+ "strBorn, strFormed, strGenres, strMoods, "
+ "strStyles, strInstruments, strBiography, "
+ "strDied, strDisbanded, strYearsActive, "
+ "strImage, strFanart, lastScraped "
+ "FROM artist WHERE artist.idArtist = %i",
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ int idArtist = (int)m_pDS->lastinsertid();
+ //No triggers, so can delete artist without effecting other tables.
+ strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+
+ // Update related tables with the new artist ID
+ // Indices have been dropped making transactions very slow, so create appropriate temp indices
+ m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
+ m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
+ m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
+ m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
+ strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ strSQL =
+ PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'",
+ idArtist, BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist,
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ // Drop temp indices
+ m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist");
+ m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
+ m_pDS->exec("DROP INDEX idxDiscography ON discography");
+ m_pDS->exec("DROP INDEX ix_art ON art");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed");
+ }
+ }
+
+ // Create missing artist tag artist [Missing].
+ // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
+ strSQL = PrepareSQL(
+ "INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
+ BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
+ m_pDS->exec(strSQL);
+
+ // Indices have been dropped making transactions very slow, so create temp index
+ m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
+ m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
+
+ // Ensure all songs have at least one artist, set those without to [Missing]
+ strSQL = "SELECT count(idSong) FROM song "
+ "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
+ "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
+ int numsongs = GetSingleValueInt(strSQL);
+ if (numsongs > 0)
+ {
+ CLog::Log(LOGDEBUG, "{} songs have no artist, setting artist to [Missing]", numsongs);
+ // Insert song_artist records for songs that don't have any
+ try
+ {
+ strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
+ "SELECT %i, idSong, %i, '%s', 0 FROM song "
+ "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
+ "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
+ BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST);
+ ExecuteQuery(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed");
+ }
+ }
+
+ // Ensure all albums have at least one artist, set those without to [Missing]
+ strSQL = "SELECT count(idAlbum) FROM album "
+ "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
+ "WHERE album_artist.idAlbum = album.idAlbum)";
+ int numalbums = GetSingleValueInt(strSQL);
+ if (numalbums > 0)
+ {
+ CLog::Log(LOGDEBUG, "{} albums have no artist, setting artist to [Missing]", numalbums);
+ // Insert album_artist records for albums that don't have any
+ try
+ {
+ strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
+ "SELECT %i, idAlbum, '%s', 0 FROM album "
+ "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
+ "WHERE album_artist.idAlbum = album.idAlbum)",
+ BLANKARTIST_ID, BLANKARTIST_NAME.c_str());
+ ExecuteQuery(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed");
+ }
+ }
+ //Remove temp indices, full analytics for database created later
+ m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist");
+ m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
+ }
+ if (version < 61)
+ {
+ // Create versiontagscan table
+ m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
+ m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
+ }
+ if (version < 62)
+ {
+ CLog::Log(LOGINFO, "create audiobook table");
+ m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
+ " strBook varchar(256), strAuthor text,"
+ " bookmark integer, file text,"
+ " dateAdded varchar (20) default NULL)");
+ }
+ if (version < 63)
+ {
+ // Add strSortName to Artist table
+ m_pDS->exec("ALTER TABLE artist ADD strSortName text\n");
+
+ //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
+ m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strArtistDisp text, strArtistSort text, strGenres text, "
+ " iYear integer, bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " fRating FLOAT NOT NULL DEFAULT 0, "
+ " iUserrating INTEGER NOT NULL DEFAULT 0, "
+ " lastScraped varchar(20) default NULL, "
+ " strReleaseType text, "
+ " iVotes INTEGER NOT NULL DEFAULT 0)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtistDisp, strArtistSort, strGenres, "
+ " iYear, bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " fRating, iUserrating, iVotes, "
+ " lastScraped, "
+ " strReleaseType)"
+ " SELECT "
+ " idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, NULL, strGenres, "
+ " iYear, bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " fRating, iUserrating, iVotes, "
+ " lastScraped, "
+ " strReleaseType"
+ " FROM album");
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+
+ //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
+ m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
+ " idAlbum INTEGER, idPath INTEGER, "
+ " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
+ " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
+ " strFileName TEXT, strMusicBrainzTrackID TEXT, "
+ " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
+ " lastplayed VARCHAR(20) DEFAULT NULL, "
+ " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ " userrating INTEGER NOT NULL DEFAULT 0, "
+ " comment TEXT, mood TEXT, dateAdded TEXT)");
+ m_pDS->exec("INSERT INTO song_new "
+ "(idSong, "
+ " idAlbum, idPath, "
+ " strArtistDisp, strArtistSort, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " lastplayed,"
+ " rating, userrating, votes, "
+ " comment, mood, dateAdded)"
+ " SELECT "
+ " idSong, "
+ " idAlbum, idPath, "
+ " strArtists, NULL, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " lastplayed,"
+ " rating, userrating, votes, "
+ " comment, mood, dateAdded"
+ " FROM song");
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+ }
+ if (version < 65)
+ {
+ // Remove cue table
+ m_pDS->exec("DROP TABLE cue");
+ // Add strReplayGain to song table
+ m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
+ }
+ if (version < 66)
+ {
+ // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
+ m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
+ m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
+ // Add a new column bScrapedMBID for artists
+ m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
+ }
+ if (version < 67)
+ {
+ // Add infosetting table
+ m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, "
+ "strSettings TEXT)");
+ // Add a new column for setting to album and artist tables
+ m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
+ m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
+
+ // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
+ m_pDS->exec(
+ "CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
+ "strScraperPath text, strSettings text)");
+ try
+ {
+ m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
+ "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, "
+ "strScraperPath, strSettings "
+ "FROM content WHERE strContent = 'artists' AND strPath LIKE "
+ "'musicdb://artists/_%/' ORDER BY idItem");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "Migrating specific artist scraper settings has failed, settings not transferred");
+ }
+ try
+ {
+ m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
+ "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, "
+ "strScraperPath, strSettings "
+ "FROM content WHERE strContent = 'albums' AND strPath LIKE "
+ "'musicdb://albums/_%/' ORDER BY idItem");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "Migrating specific album scraper settings has failed, settings not transferred");
+ }
+ try
+ {
+ m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
+ "SELECT id, strScraperPath, strSettings FROM content_temp");
+ m_pDS->exec(
+ "UPDATE artist SET idInfoSetting = "
+ "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
+ "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = "
+ "idArtist) ");
+ m_pDS->exec("UPDATE album SET idInfoSetting = "
+ "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
+ "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem "
+ "= idAlbum) ");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "Migrating album and artist scraper settings has failed, settings not transferred");
+ }
+ m_pDS->exec("DROP TABLE content_temp");
+
+ // Remove content table
+ m_pDS->exec("DROP TABLE content");
+ // Remove albuminfosong table
+ m_pDS->exec("DROP TABLE albuminfosong");
+ }
+ if (version < 68)
+ {
+ // Add a new columns strType, strGender, strDisambiguation for artists
+ m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n");
+ m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n");
+ m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
+ }
+ if (version < 69)
+ {
+ // Remove album_genre table
+ m_pDS->exec("DROP TABLE album_genre");
+ }
+ if (version < 70)
+ {
+ // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
+ m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * "
+ "40 / 3 \n");
+ }
+ if (version < 71)
+ {
+ // Add lastscanned to versiontagscan table
+ m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
+ CDateTime dateAdded = CDateTime::GetCurrentDateTime();
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
+ dateAdded.GetAsDBDateTime().c_str()));
+ }
+ if (version < 72)
+ {
+ // Create source table
+ m_pDS->exec(
+ "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
+ // Create source_path table
+ m_pDS->exec(
+ "CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
+ // Create album_source table
+ m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
+ // Populate source and source_path tables from sources.xml
+ // Filling album_source needs to be done after indexes are created or it is
+ // very slow. It could be populated during CreateAnalytics but it is checked
+ // and filled as part of scanning anyway so simply force full rescan.
+ MigrateSources();
+ }
+ if (version < 73)
+ {
+ // add bBoxedSet to album table
+ m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
+ // add iDiscTotal to album table
+ m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
+ // populate iDiscTotal from the data already in the song table
+ m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
+ "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
+ "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
+ // add strDiscSubtitles to song table
+ m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
+ }
+ if (version < 74)
+ {
+ //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
+ m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
+ "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
+ "strReleaseGroupMBID TEXT, "
+ "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
+ "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
+ "bCompilation INTEGER NOT NULL DEFAULT '0', "
+ "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
+ "strReview TEXT, strImage TEXT, strLabel TEXT, "
+ "strType TEXT, "
+ "fRating FLOAT NOT NULL DEFAULT 0, "
+ "iVotes INTEGER NOT NULL DEFAULT 0, "
+ "iUserrating INTEGER NOT NULL DEFAULT 0, "
+ "lastScraped VARCHAR(20) DEFAULT NULL, "
+ "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ "strReleaseType TEXT, "
+ "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
+ "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
+ // Prepare as MySQL has different CAST datatypes
+ m_pDS->exec(
+ PrepareSQL("INSERT INTO album_new "
+ "(idalbum, strAlbum, "
+ "strMusicBrainzAlbumID, strReleaseGroupMBID, "
+ "strArtistDisp, strArtistSort, strGenres, "
+ "strReleaseDate, strOrigReleaseDate, "
+ "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
+ "strReview, strImage, strLabel, strType, "
+ "fRating, iVotes, iUserrating, "
+ "lastScraped, bScrapedMBID, strReleaseType, "
+ "iDiscTotal, idInfoSetting) "
+ "SELECT "
+ "idAlbum, strAlbum, "
+ "strMusicBrainzAlbumID, strReleaseGroupMBID, "
+ "strArtistDisp, strArtistSort, strGenres, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
+ "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
+ "bCompilation, strMoods, strStyles, strThemes, "
+ "strReview, strImage, strLabel, strType, "
+ "fRating, iVotes, iUserrating, "
+ "lastScraped, bScrapedMBID, strReleaseType, "
+ "iDiscTotal, idInfoSetting "
+ "FROM album"));
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+
+ //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
+ m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
+ "idAlbum INTEGER, idPath INTEGER, "
+ "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
+ "iTrack INTEGER, iDuration INTEGER, "
+ "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
+ "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
+ "lastplayed VARCHAR(20) DEFAULT NULL, "
+ "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ "userrating INTEGER NOT NULL DEFAULT 0, "
+ "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
+ "dateAdded TEXT)");
+ // Prepare as MySQL has different CAST datatypes
+ m_pDS->exec(PrepareSQL("INSERT INTO song_new "
+ "(idSong, "
+ "idAlbum, idPath, "
+ "strArtistDisp, strArtistSort, strGenres, strTitle, "
+ "iTrack, iDuration, "
+ "strReleaseDate, strOrigReleaseDate, "
+ "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, iStartOffset, iEndOffset, "
+ "lastplayed, "
+ "rating, userrating, votes, "
+ "comment, mood, strReplayGain, dateAdded) "
+ "SELECT "
+ "idSong, "
+ "idAlbum, idPath, "
+ "strArtistDisp, strArtistSort, strGenres, strTitle, "
+ "iTrack, iDuration, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, iStartOffset, iEndOffset, "
+ "lastplayed, "
+ "rating, userrating, votes, "
+ "comment, mood, strReplayGain, dateAdded "
+ "FROM song"));
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+ }
+ if (version < 75)
+ {
+ m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
+ }
+ if (version < 77)
+ {
+ m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
+ }
+ if (version < 78)
+ {
+ std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime();
+
+ // Add removed_link table
+ m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
+ // Add lastcleaned and artistlinksupdated to versiontagscan table
+ m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
+ m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
+ m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
+ // Adjust lastscanned if original local time value is after current UTC
+ if (GetLibraryLastUpdated() > strUTCNow)
+ SetLibraryLastUpdated();
+ m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
+ "genresupdated = lastscanned, "
+ "artistlinksupdated = lastscanned");
+
+ // Add dateNew, dateModified to song table
+ m_pDS->exec("ALTER TABLE song ADD dateNew TEXT");
+ m_pDS->exec("ALTER TABLE song ADD dateModified TEXT");
+ // Set new to dateAdded and modified to lastplayed as estimates
+ // Limit those local time values to now UTC, and modified is after new
+ m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
+ m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'", strUTCNow.c_str(),
+ strUTCNow.c_str()));
+ m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
+ m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
+ strUTCNow.c_str(), strUTCNow.c_str()));
+ m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
+
+ // Add dateAdded, dateNew, dateModified to album table
+ m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT");
+ m_pDS->exec("ALTER TABLE album ADD dateNew TEXT");
+ m_pDS->exec("ALTER TABLE album ADD dateModified TEXT");
+ // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
+ // Limit modified value to now UTC and after new
+ // Indices have been dropped making subquery very slow, so create temp index
+ m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
+ m_pDS->exec("UPDATE album SET dateAdded = "
+ "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
+ m_pDS->exec("UPDATE album SET dateNew = "
+ "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
+ m_pDS->exec("UPDATE album SET dateModified = dateNew");
+ m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
+ m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
+ strUTCNow.c_str(), strUTCNow.c_str()));
+ //Remove temp index, full analytics for database created later
+ m_pDS->exec("DROP INDEX idxSong3 ON song");
+
+ // Add dateAdded, dateNew, dateModified to artist table
+ m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT");
+ m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT");
+ m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT");
+ // dateAdded has NULL values until files rescanned by user
+ // Set new and modified to now UTC as not worth complexity of estimating from song dates
+ m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str()));
+ m_pDS->exec("UPDATE artist SET dateModified = dateNew");
+ }
+ if (version < 79)
+ {
+ m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
+ }
+ if (version < 80)
+ {
+ m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
+ // update duration for all current albums
+ m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) "
+ "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
+ }
+ if (version < 82)
+ {
+ // Update artist table combining fanart URL data into strImage field
+ // Clear empty URL data <fanart /> and <thumb />
+ m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
+ m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
+ //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
+ m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
+ m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
+ m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
+ "aspect=\"fanart\" preview')");
+ // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
+ // Truncate the fanart when total URLs exceeds this
+ bool bisMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql");
+ if (bisMySQL)
+ {
+ std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist "
+ "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
+ if (m_pDS->query(strSQL))
+ {
+ while (!m_pDS->eof())
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ std::string strFanart = m_pDS->fv("strFanart").get_asString();
+ std::string strImage = m_pDS->fv("strImage").get_asString();
+ size_t space = 65535;
+ // Trim strImage to allow arbitrary half space for fanart
+ if (!TrimImageURLs(strImage, space / 2))
+ strImage.clear(); // </thumb> not found, empty field
+ space = space - strImage.length();
+ // Trim fanart to fit remaining space
+ if (!TrimImageURLs(strFanart, space))
+ strFanart.clear(); // </thumb> not found, empty field
+
+ strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
+ "WHERE idArtist = %i",
+ strFanart.c_str(), strImage.c_str(), idArtist);
+ m_pDS2->exec(strSQL); // Use other dataset to update while looping result set
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ }
+
+ // Remove strFanart column from artist table
+ m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
+ "strArtist varchar(256), strMusicBrainzArtistID text, "
+ "strSortName text, "
+ "strType text, strGender text, strDisambiguation text, "
+ "strBorn text, strFormed text, strGenres text, strMoods text, "
+ "strStyles text, strInstruments text, strBiography text, "
+ "strDied text, strDisbanded text, strYearsActive text, "
+ "strImage text, "
+ "lastScraped varchar(20) default NULL, "
+ "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
+ "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+ // Concatenate fanart URLs into strImage field
+ // Prepare SQL to convert CONCAT to || in SQLite
+ m_pDS->exec(PrepareSQL("INSERT INTO artist_new "
+ "(idArtist, strArtist, strMusicBrainzArtistID, "
+ "strSortName, strType, strGender, strDisambiguation, "
+ "strBorn, strFormed, strGenres, strMoods, "
+ "strStyles , strInstruments , strBiography , "
+ "strDied, strDisbanded, strYearsActive, "
+ "strImage, "
+ "lastScraped, bScrapedMBID, idInfoSetting, "
+ "dateAdded, dateNew, dateModified) "
+ "SELECT "
+ "artist.idArtist, "
+ "strArtist, strMusicBrainzArtistID, "
+ "strSortName, strType, strGender, strDisambiguation, "
+ "strBorn, strFormed, strGenres, strMoods, "
+ "strStyles, strInstruments, strBiography, "
+ "strDied, strDisbanded, strYearsActive, "
+ "CONCAT(strImage, strFanart), "
+ "lastScraped, bScrapedMBID, idInfoSetting, "
+ "dateAdded, dateNew, dateModified "
+ "FROM artist"));
+ m_pDS->exec("DROP TABLE artist");
+ m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
+ }
+ // Set the version of tag scanning required.
+ // Not every schema change requires the tags to be rescanned, set to the highest schema version
+ // that needs this. Forced rescanning (of music files that have not changed since they were
+ // previously scanned) also accommodates any changes to the way tags are processed
+ // e.g. read tags that were not processed by previous versions.
+ // The original db version when the tags were scanned, and the minimal db version needed are
+ // later used to determine if a forced rescan should be prompted
+
+ // The last schema change needing forced rescanning was 73.
+ // This is because Kodi can now read and process extra tags involved in the creation of box sets
+
+ SetMusicNeedsTagScan(73);
+
+ // After all updates, store the original db version.
+ // This indicates the version of tag processing that was used to populate db
+ SetMusicTagScanVersion(version);
+}
+
+int CMusicDatabase::GetSchemaVersion() const
+{
+ return 82;
+}
+
+int CMusicDatabase::GetMusicNeedsTagScan()
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string sql = "SELECT * FROM versiontagscan";
+ if (!m_pDS->query(sql))
+ return -1;
+
+ if (m_pDS->num_rows() != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+
+ int idVersion = m_pDS->fv("idVersion").get_asInt();
+ int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt();
+ m_pDS->close();
+ if (idVersion < iNeedsScan)
+ return idVersion;
+ else
+ return 0;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+void CMusicDatabase::SetMusicNeedsTagScan(int version)
+{
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version));
+}
+
+void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */)
+{
+ if (version == 0)
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
+ else
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version));
+}
+
+std::string CMusicDatabase::GetLibraryLastUpdated()
+{
+ return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
+}
+
+void CMusicDatabase::SetLibraryLastUpdated()
+{
+ CDateTime dateUpdated = CDateTime::GetUTCDateTime();
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
+ dateUpdated.GetAsDBDateTime().c_str()));
+}
+
+std::string CMusicDatabase::GetLibraryLastCleaned()
+{
+ return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
+}
+
+void CMusicDatabase::SetLibraryLastCleaned()
+{
+ std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'", strUpdated.c_str()));
+}
+
+std::string CMusicDatabase::GetArtistLinksUpdated()
+{
+ return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
+}
+
+void CMusicDatabase::SetArtistLinksUpdated()
+{
+ std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
+ m_pDS->exec(
+ PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'", strUpdated.c_str()));
+}
+
+std::string CMusicDatabase::GetGenresLastAdded()
+{
+ return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
+}
+
+std::string CMusicDatabase::GetSongsLastAdded()
+{
+ return GetSingleValue("SELECT MAX(dateNew) FROM song");
+}
+
+std::string CMusicDatabase::GetAlbumsLastAdded()
+{
+ return GetSingleValue("SELECT MAX(dateNew) FROM album");
+}
+
+std::string CMusicDatabase::GetArtistsLastAdded()
+{
+ return GetSingleValue("SELECT MAX(dateNew) FROM artist");
+}
+
+std::string CMusicDatabase::GetSongsLastModified()
+{
+ return GetSingleValue("SELECT MAX(dateModified) FROM song");
+}
+
+std::string CMusicDatabase::GetAlbumsLastModified()
+{
+ return GetSingleValue("SELECT MAX(dateModified) FROM album");
+}
+
+std::string CMusicDatabase::GetArtistsLastModified()
+{
+ return GetSingleValue("SELECT MAX(dateModified) FROM artist");
+}
+
+unsigned int CMusicDatabase::GetRandomSongIDs(const Filter& filter,
+ std::vector<std::pair<int, int>>& songIDs)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return 0;
+ if (nullptr == m_pDS)
+ return 0;
+
+ std::string strSQL = "SELECT idSong FROM songview ";
+ if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
+ return false;
+ strSQL += PrepareSQL(" ORDER BY RANDOM()");
+
+ if (!m_pDS->query(strSQL))
+ return 0;
+ songIDs.clear();
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return 0;
+ }
+ songIDs.reserve(m_pDS->num_rows());
+ while (!m_pDS->eof())
+ {
+ songIDs.push_back(std::make_pair<int, int>(1, m_pDS->fv(song_idSong).get_asInt()));
+ m_pDS->next();
+ } // cleanup
+ m_pDS->close();
+ return static_cast<unsigned int>(songIDs.size());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return 0;
+}
+
+int CMusicDatabase::GetSongsCount(const Filter& filter)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return 0;
+ if (nullptr == m_pDS)
+ return 0;
+
+ std::string strSQL = "select count(idSong) as NumSongs from songview ";
+ if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
+ return false;
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return 0;
+ }
+
+ int iNumSongs = m_pDS->fv("NumSongs").get_asInt();
+ // cleanup
+ m_pDS->close();
+ return iNumSongs;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return 0;
+}
+
+bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath)
+{
+ basePath.clear();
+ std::vector<std::pair<std::string, int>> paths;
+ if (!GetAlbumPaths(idAlbum, paths))
+ return false;
+
+ for (const auto& pathpair : paths)
+ {
+ if (basePath.empty())
+ basePath = pathpair.first.c_str();
+ else
+ URIUtils::GetCommonPath(basePath, pathpair.first.c_str());
+ }
+ return true;
+}
+
+bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths)
+{
+ paths.clear();
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false;
+
+ // Get the unique paths of songs on the album, providing there are no songs from
+ // other albums with the same path. This returns
+ // a) <album> if is contains all the songs and no others, or
+ // b) <album>/cd1, <album>/cd2 etc. for disc sets
+ // but does *not* return any path when albums are mixed together. That could be because of
+ // deliberate file organisation, or (more likely) because of a tagging error in album name
+ // or Musicbrainzalbumid. Thus it avoids finding some generic music path.
+ strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idAlbum = %ld "
+ "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
+ "WHERE idPath = song.idPath) = 1",
+ idAlbum);
+
+ if (!m_pDS2->query(strSQL))
+ return false;
+ if (m_pDS2->num_rows() == 0)
+ {
+ // Album does not have a unique path, files are mixed
+ m_pDS2->close();
+ return false;
+ }
+
+ while (!m_pDS2->eof())
+ {
+ paths.emplace_back(m_pDS2->fv("strPath").get_asString(),
+ m_pDS2->fv("song.idPath").get_asInt());
+ m_pDS2->next();
+ }
+ // Cleanup recordset data
+ m_pDS2->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+
+ return false;
+}
+
+int CMusicDatabase::GetDiscnumberForPathID(int idPath)
+{
+ std::string strSQL;
+ int result = -1;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS2)
+ return -1;
+
+ strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
+ "WHERE idPath = %i",
+ idPath);
+
+ if (!m_pDS2->query(strSQL))
+ return -1;
+ if (m_pDS2->num_rows() == 1)
+ { // Songs with this path have a unique disc number
+ result = m_pDS2->fv("discnum").get_asInt();
+ }
+ // Cleanup recordset data
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+ return result;
+}
+
+// Get old "artist path" - where artist.nfo and art was located v17 and below.
+// It is the path common to all albums by an (album) artist, but ensure it is unique
+// to that artist and not shared with other artists. Previously this caused incorrect nfo
+// and art to be applied to multiple artists.
+bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string& basePath)
+{
+ basePath.clear();
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false;
+
+ // find all albums from this artist, and all the paths to the songs from those albums
+ std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE album_artist.idArtist = %ld "
+ "GROUP BY song.idPath",
+ idArtist);
+
+ // run query
+ if (!m_pDS2->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS2->num_rows();
+ if (iRowsFound == 0)
+ {
+ // Artist is not an album artist, no path to find
+ m_pDS2->close();
+ return false;
+ }
+ else if (iRowsFound == 1)
+ {
+ // Special case for single path - assume that we're in an artist/album/songs filesystem
+ URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath);
+ m_pDS2->close();
+ }
+ else
+ {
+ // find the common path (if any) to these albums
+ while (!m_pDS2->eof())
+ {
+ std::string path = m_pDS2->fv("strPath").get_asString();
+ if (basePath.empty())
+ basePath = path;
+ else
+ URIUtils::GetCommonPath(basePath, path);
+
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+
+ // Check any path found is unique to that album artist, and do *not* return any path
+ // that is shared with other album artists. That could be because of collaborations
+ // i.e. albums with more than one album artist, or because there are albums by the
+ // artist on multiple music sources, or elsewhere in the folder hierarchy.
+ // Avoid returning some generic music path.
+ if (!basePath.empty())
+ {
+ strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE album_artist.idArtist <> %ld "
+ "AND strPath LIKE '%s%%'",
+ idArtist, basePath.c_str());
+ std::string strValue = GetSingleValue(strSQL, m_pDS2);
+ if (!strValue.empty())
+ {
+ int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
+ if (countartists == 0)
+ return true;
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ basePath.clear();
+ return false;
+}
+
+bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string& path)
+{
+ // Get path for artist in the artists folder
+ path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (path.empty())
+ return false; // No Artists folder not set;
+ // Get unique artist folder name
+ std::string strFolder;
+ if (GetArtistFolderName(artist, strFolder))
+ {
+ path = URIUtils::AddFileToFolder(path, strFolder);
+ return true;
+ }
+ path.clear();
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumFolder(const CAlbum& album,
+ const std::string& strAlbumPath,
+ std::string& strFolder)
+{
+ strFolder.clear();
+ // Get a name for the album folder that is unique for the artist to use when
+ // exporting albums to separate nfo files in a folder under an artist folder
+
+ // When given an album path (common to all the music files containing *only*
+ // that album) check if that folder name is *unique* looking at folders on
+ // all levels of the music file paths for the artist
+ if (!strAlbumPath.empty())
+ {
+ // Get last folder from full path
+ std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath);
+ if (!folders.empty())
+ {
+ strFolder = folders.back();
+ // The same folder name could be used on different paths for albums by the
+ // same first artist. The albums could be totally different or also have
+ // the same name (but different mbid). Be over cautious and look for the
+ // name any where in the music file paths
+ std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN path on path.idPath = song.idPath "
+ "WHERE album_artist.iOrder = 0 "
+ "AND album_artist.idArtist = %ld "
+ "AND path.strPath LIKE '%%\\%s\\%%'",
+ album.artistCredits[0].GetArtistId(), strFolder.c_str());
+
+ if (!m_pDS2->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS2->num_rows();
+ m_pDS2->close();
+ if (iRowsFound == 1)
+ return true;
+ }
+ }
+ // Create a valid unique folder name from album title
+ // @todo: Does UFT8 matter or need normalizing?
+ // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
+ strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT);
+ StringUtils::Replace(strFolder, " _ ", "_");
+
+ // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
+ // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
+ // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
+ // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
+ std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
+ "JOIN album ON album_artist.idAlbum = album.idAlbum "
+ "WHERE album_artist.iOrder = 0 "
+ "AND album_artist.idArtist = %ld "
+ "AND album.strAlbum LIKE '%s' ",
+ album.artistCredits[0].GetArtistId(), album.strAlbum.c_str());
+ std::string strValue = GetSingleValue(strSQL, m_pDS2);
+ if (strValue.empty())
+ return false;
+ int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
+ if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty())
+ { // Only one of the duplicate albums can be without mbid
+ strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4);
+ }
+ return !strFolder.empty();
+}
+
+bool CMusicDatabase::GetArtistFolderName(const CArtist& artist, std::string& strFolder)
+{
+ return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder);
+}
+
+bool CMusicDatabase::GetArtistFolderName(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ std::string& strFolder)
+{
+ // Create a valid unique folder name for artist
+ // @todo: Does UFT8 matter or need normalizing?
+ // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
+ strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT);
+ StringUtils::Replace(strFolder, " _ ", "_");
+
+ // Ensure <artist name> is unique e.g. 2 x John Williams.
+ // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
+ // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
+ // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
+ std::string strSQL =
+ PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
+ std::string strValue = GetSingleValue(strSQL, m_pDS2);
+ if (strValue.empty())
+ return false;
+ int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
+ if (countartist > 1)
+ strFolder += "_" + strMusicBrainzArtistID.substr(0, 4);
+ return !strFolder.empty();
+}
+
+int CMusicDatabase::AddSource(const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths,
+ int id /*= -1*/)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // Check if source name already exists
+ int idSource = GetSourceByName(strName);
+ if (idSource < 0)
+ {
+ BeginTransaction();
+ // Add new source and source paths
+ if (id > 0)
+ strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
+ "VALUES(%i, '%s', '%s')",
+ id, strName.c_str(), strMultipath.c_str());
+ else
+ strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
+ "VALUES(NULL, '%s', '%s')",
+ strName.c_str(), strMultipath.c_str());
+ m_pDS->exec(strSQL);
+
+ idSource = static_cast<int>(m_pDS->lastinsertid());
+
+ int idPath = 1;
+ for (const auto& path : vecPaths)
+ {
+ strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
+ "VALUES(%i,%i,'%s')",
+ idSource, idPath, path.c_str());
+ m_pDS->exec(strSQL);
+ ++idPath;
+ }
+
+ // Find albums by song path, building WHERE for multiple source paths
+ // (providing source has a path)
+ if (vecPaths.size() > 0)
+ {
+ std::vector<int> albumIds;
+ Filter extFilter;
+ strSQL = "SELECT DISTINCT idAlbum FROM song ";
+ extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
+ for (const auto& path : vecPaths)
+ extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false);
+ if (!BuildSQL(strSQL, extFilter, strSQL))
+ return -1;
+
+ if (!m_pDS->query(strSQL))
+ return -1;
+
+ while (!m_pDS->eof())
+ {
+ albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // Add album_source for related albums
+ for (auto idAlbum : albumIds)
+ {
+ strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) "
+ "VALUES('%i', '%i')",
+ idSource, idAlbum);
+ m_pDS->exec(strSQL);
+ }
+ }
+ CommitTransaction();
+ }
+ return idSource;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
+ RollbackTransaction();
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::UpdateSource(const std::string& strOldName,
+ const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths)
+{
+ int idSource = -1;
+ std::string strSourceMultipath;
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // Get details of named old source
+ if (!strOldName.empty())
+ {
+ strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
+ strOldName.c_str());
+ if (!m_pDS->query(strSQL))
+ return -1;
+ if (m_pDS->num_rows() > 0)
+ {
+ idSource = m_pDS->fv("idSource").get_asInt();
+ strSourceMultipath = m_pDS->fv("strMultipath").get_asString();
+ }
+ m_pDS->close();
+ }
+ if (idSource < 0)
+ {
+ // Source not found, add new one
+ return AddSource(strName, strMultipath, vecPaths);
+ }
+
+ // Nothing changed? (that we hold in db, other source details could be modified)
+ bool pathschanged = strMultipath.compare(strSourceMultipath) != 0;
+ if (!pathschanged && strOldName.compare(strName) == 0)
+ return idSource;
+
+ if (!pathschanged)
+ {
+ // Name changed? Could be that none of the values held in db changed
+ if (strOldName.compare(strName) != 0)
+ {
+ strSQL = PrepareSQL("UPDATE source SET strName = '%s' "
+ "WHERE idSource = %i",
+ strName.c_str(), idSource);
+ m_pDS->exec(strSQL);
+ }
+ return idSource;
+ }
+ else
+ {
+ // Change paths (and name) by deleting and re-adding, but keep same ID
+ strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource);
+ m_pDS->exec(strSQL);
+ return AddSource(strName, strMultipath, vecPaths, idSource);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
+ RollbackTransaction();
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::RemoveSource(const std::string& strName)
+{
+ // Related album_source and source_path rows removed by trigger
+ SetLibraryLastCleaned();
+ return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str()));
+}
+
+int CMusicDatabase::GetSourceFromPath(const std::string& strPath1)
+{
+ std::string strSQL;
+ int idSource = -1;
+ try
+ {
+ std::string strPath(strPath1);
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // Check if path is a source matching on multipath
+ strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return -1;
+ if (m_pDS->num_rows() > 0)
+ idSource = m_pDS->fv("idSource").get_asInt();
+ m_pDS->close();
+ if (idSource > 0)
+ return idSource;
+
+ // Check if path is a source path (of many) or a subfolder of a single source
+ strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
+ "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return -1;
+ if (m_pDS->num_rows() == 1)
+ idSource = m_pDS->fv("idSource").get_asInt();
+ m_pDS->close();
+ return idSource;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath1);
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource)
+{
+ std::string strSQL;
+ strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) "
+ "VALUES(%i, %i)",
+ idAlbum, idSource);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath)
+{
+ std::string strSQL;
+ std::vector<int> sourceIds;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (!strPath.empty())
+ {
+ // Find sources related to album using album path
+ strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
+ "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ while (!m_pDS->eof())
+ {
+ sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ else
+ {
+ // Find sources using song paths, check each source path individually
+ if (nullptr == m_pDS2)
+ return false;
+ strSQL = "SELECT idSource, strPath FROM source_path";
+ if (!m_pDS->query(strSQL))
+ return false;
+ while (!m_pDS->eof())
+ {
+ std::string sourcepath = m_pDS->fv("strPath").get_asString();
+ strSQL = PrepareSQL("SELECT 1 FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
+ sourcepath.c_str());
+ if (!m_pDS2->query(strSQL))
+ return false;
+ if (m_pDS2->num_rows() > 0)
+ sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS2->close();
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+ //Add album sources
+ for (auto idSource : sourceIds)
+ {
+ AddAlbumSource(idAlbum, idSource);
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::DeleteAlbumSources(int idAlbum)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum));
+}
+
+bool CMusicDatabase::CheckSources(VECSOURCES& sources)
+{
+ if (sources.empty())
+ {
+ // Source table empty too?
+ return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
+ }
+
+ // Check number of entries matches
+ size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
+ if (total != sources.size())
+ return false;
+
+ // Check individual sources match
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ for (const auto& source : sources)
+ {
+ // Check each source by name
+ strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source "
+ "WHERE strName LIKE '%s'",
+ source.strName.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() != 1)
+ {
+ // Missing source, or name duplication
+ m_pDS->close();
+ return false;
+ }
+ else
+ {
+ // Check details. Encoded URLs of source.strPath matched to strMultipath
+ // field, no need to look at individual paths of source_path table
+ if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0)
+ {
+ // Paths not match
+ m_pDS->close();
+ return false;
+ }
+ m_pDS->close();
+ }
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::MigrateSources()
+{
+ //Fetch music sources from xml
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+
+ std::string strSQL;
+ try
+ {
+ // Fill source and source paths tables
+ for (const auto& source : sources)
+ {
+ // AddSource(source.strName, source.strPath, source.vecPaths);
+ // Add new source
+ strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
+ "VALUES(NULL, '%s', '%s')",
+ source.strName.c_str(), source.strPath.c_str());
+ m_pDS->exec(strSQL);
+ int idSource = static_cast<int>(m_pDS->lastinsertid());
+
+ // Add new source paths
+ int idPath = 1;
+ for (const auto& path : source.vecPaths)
+ {
+ strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
+ "VALUES(%i,%i,'%s')",
+ idSource, idPath, path.c_str());
+ m_pDS->exec(strSQL);
+ ++idPath;
+ }
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::UpdateSources()
+{
+ //Check library and xml sources match
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+ if (CheckSources(sources))
+ return true;
+
+ try
+ {
+ // Empty sources table (related link tables removed by trigger);
+ ExecuteQuery("DELETE FROM source");
+
+ // Fill source table, and album sources
+ for (const auto& source : sources)
+ AddSource(source.strName, source.strPath, source.vecPaths);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSources(CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get music sources and individual source paths (may not be scanned or have albums etc.)
+ std::string strSQL =
+ "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
+ "FROM source JOIN source_path ON source.idSource = source_path.idSource "
+ "ORDER BY source.idSource, source_path.idPath";
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get data from returned rows
+ // Item has source ID in MusicInfotag, multipath in path, and individual paths in property
+ CVariant sourcePaths(CVariant::VariantTypeArray);
+ int idSource = -1;
+ while (!m_pDS->eof())
+ {
+ if (idSource != m_pDS->fv("source.idSource").get_asInt())
+ { // New source
+ if (idSource > 0 && !sourcePaths.empty())
+ {
+ //Store paths for previous source in item list
+ items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
+ sourcePaths.clear();
+ }
+ idSource = m_pDS->fv("source.idSource").get_asInt();
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
+ pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source");
+ // Set tag URL for "file" property in AudioLibary processing
+ pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString());
+ // Set item path as source URL encoded multipath too
+ pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+ // Get path data
+ sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString());
+
+ m_pDS->next();
+ }
+ if (!sourcePaths.empty())
+ {
+ //Store paths for final source
+ items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
+ sourcePaths.clear();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
+ "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
+ "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
+ "WHERE artist.idArtist = %i "
+ "ORDER BY album_source.idSource",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ // Artist does have any source via albums may not be an album artist.
+ // Check via songs fetch sources from compilations or where they are guest artist
+ m_pDS->close();
+ strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
+ "JOIN song ON song_artist.idSong = song.idSong "
+ "JOIN album_source ON album_source.idAlbum = song.idAlbum "
+ "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
+ "ORDER BY album_source.idSource",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ //No sources, but query successful
+ m_pDS->close();
+ return true;
+ }
+ }
+
+ CVariant artistSources(CVariant::VariantTypeArray);
+ while (!m_pDS->eof())
+ {
+ artistSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("sourceid", artistSources);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idSource FROM album_source "
+ "WHERE album_source.idAlbum = %i "
+ "ORDER BY idSource",
+ idAlbum);
+ if (!m_pDS->query(strSQL))
+ return false;
+ CVariant albumSources(CVariant::VariantTypeArray);
+ if (m_pDS->num_rows() > 0)
+ {
+ while (!m_pDS->eof())
+ {
+ albumSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ else
+ {
+ //! @todo: handle singles, or don't waste time checking songs
+ // Album does have any sources, may be a single??
+ // Check via song paths, check each source path individually
+ // usually fewer source paths than songs
+ m_pDS->close();
+
+ if (nullptr == m_pDS2)
+ return false;
+ strSQL = "SELECT idSource, strPath FROM source_path";
+ if (!m_pDS->query(strSQL))
+ return false;
+ while (!m_pDS->eof())
+ {
+ std::string sourcepath = m_pDS->fv("strPath").get_asString();
+ strSQL = PrepareSQL("SELECT 1 FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
+ idAlbum, sourcepath.c_str());
+ if (!m_pDS2->query(strSQL))
+ return false;
+ if (m_pDS2->num_rows() > 0)
+ albumSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS2->close();
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+
+ item->SetProperty("sourceid", albumSources);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idSource FROM song "
+ "JOIN album_source ON album_source.idAlbum = song.idAlbum "
+ "WHERE song.idSong = %i "
+ "ORDER BY idSource",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0 && !strPath1.empty())
+ {
+ // Check via song path instead
+ m_pDS->close();
+ std::string strPath(strPath1);
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
+ "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ }
+ CVariant songSources(CVariant::VariantTypeArray);
+ while (!m_pDS->eof())
+ {
+ songSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("sourceid", songSources);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+ return false;
+}
+
+int CMusicDatabase::GetSourceByName(const std::string& strSource)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("idSource").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+std::string CMusicDatabase::GetSourceById(int id)
+{
+ return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id));
+}
+
+int CMusicDatabase::GetArtistByName(const std::string& strArtist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'",
+ strArtist.c_str());
+
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ int lResult = m_pDS->fv("artist.idArtist").get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+int CMusicDatabase::GetArtistByMatch(const CArtist& artist)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+ // Match on MusicBrainz ID, definitively unique
+ if (!artist.strMusicBrainzArtistID.empty())
+ strSQL = PrepareSQL("SELECT idArtist FROM artist "
+ "WHERE strMusicBrainzArtistID = '%s'",
+ artist.strMusicBrainzArtistID.c_str());
+ else
+ // No MusicBrainz ID, artist by name with no mbid
+ strSQL = PrepareSQL("SELECT idArtist FROM artist "
+ "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
+ artist.strArtist.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ // Match on artist name, relax mbid restriction
+ return GetArtistByName(artist.strArtist);
+ }
+ int lResult = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist& artist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL(
+ "SELECT artistview.* FROM song_artist "
+ "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
+ "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ artist = GetArtistFromDataset(m_pDS.get());
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::IsSongArtist(int idSong, int idArtist)
+{
+ std::string strSQL = PrepareSQL("SELECT 1 FROM song_artist "
+ "WHERE song_artist.idSong= %i AND "
+ "song_artist.idArtist = %i AND song_artist.idRole = 1",
+ idSong, idArtist);
+ return GetSingleValue(strSQL).empty();
+}
+
+bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist)
+{
+ std::string strSQL =
+ PrepareSQL("SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
+ "WHERE song.idSong = %i AND album_artist.idArtist = %i",
+ idSong, idArtist);
+ return GetSingleValue(strSQL).empty();
+}
+
+bool CMusicDatabase::IsAlbumBoxset(int idAlbum)
+{
+ std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum);
+ int isBoxSet = GetSingleValueInt(strSQL);
+ return (isBoxSet == 1 ? true : false);
+}
+
+int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ if (strArtist.empty())
+ strSQL =
+ PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str());
+ else
+ strSQL = PrepareSQL("SELECT idAlbum FROM album "
+ "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
+ strAlbum.c_str(), strArtist.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("idAlbum").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum,
+ const std::string& strArtist,
+ int& idAlbum,
+ std::string& strReview)
+{
+ /*
+ Get the first album that matches with the title and artist display name.
+ Artist(s) and album title may not be sufficient to uniquely identify a match since library can
+ store multiple releases, and occasionally artists even have different albums with same name.
+ Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
+ */
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ if (strArtist.empty())
+ strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
+ strAlbum.c_str());
+ else
+ strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album "
+ "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
+ strAlbum.c_str(), strArtist.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound > 0)
+ {
+ idAlbum = m_pDS->fv("idAlbum").get_asInt();
+ strReview = m_pDS->fv("strReview").get_asString();
+ return true;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT albumview.* FROM albumview "
+ "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
+ "WHERE album_artist.strArtist LIKE '%s'",
+ strArtist.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ CAlbum album = GetAlbumFromDataset(m_pDS.get());
+ std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(path, album));
+ std::string label =
+ StringUtils::Format("{} ({})", album.strAlbum, pItem->GetMusicInfoTag()->GetYear());
+ pItem->SetLabel(label);
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+int CMusicDatabase::GetAlbumByName(const std::string& strAlbum,
+ const std::vector<std::string>& artist)
+{
+ return GetAlbumByName(
+ strAlbum,
+ StringUtils::Join(
+ artist,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+int CMusicDatabase::GetAlbumByMatch(const CAlbum& album)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+ // Match on MusicBrainz ID, definitively unique
+ if (!album.strMusicBrainzAlbumID.empty())
+ strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'",
+ album.strMusicBrainzAlbumID.c_str());
+ else
+ // No mbid, match on album title and album artist descriptive string, ignore those with mbid
+ strSQL = PrepareSQL("SELECT idAlbum FROM album "
+ "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
+ "AND strMusicBrainzAlbumID IS NULL",
+ album.GetAlbumArtistString().c_str(), album.strAlbum.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ // Match on album title and album artist descriptive string, relax mbid restriction
+ return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString());
+ }
+ int lResult = m_pDS->fv("idAlbum").get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+std::string CMusicDatabase::GetGenreById(int id)
+{
+ return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id));
+}
+
+std::string CMusicDatabase::GetArtistById(int id)
+{
+ return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id));
+}
+
+std::string CMusicDatabase::GetRoleById(int id)
+{
+ return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id));
+}
+
+bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/)
+{
+ // Propagate artist sort names into concatenated artist sort name string for songs and albums
+ // Avoid updating records where sort same as strArtistDisp
+ std::string strSQL;
+
+ // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
+ // (not handled by PrepareSQL)
+ bool bisMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
+
+ BeginMultipleExecute();
+ if (bisMySQL)
+ strSQL = "(SELECT GROUP_CONCAT("
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END "
+ "ORDER BY album_artist.idAlbum, album_artist.iOrder "
+ "SEPARATOR '; ') as val "
+ "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
+ "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
+ else
+ strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
+ "FROM(SELECT album_artist.idAlbum, "
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END as val "
+ "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
+ "WHERE album_artist.idAlbum = album.idAlbum "
+ "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
+
+ strSQL = "UPDATE album SET strArtistSort = " + strSQL +
+ "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
+ "AND strArtistDisp <> " +
+ strSQL;
+ if (idArtist > 0)
+ strSQL +=
+ PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
+ "AND album_artist.idAlbum = album.idAlbum)",
+ idArtist);
+ ExecuteQuery(strSQL);
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (bisMySQL)
+ strSQL = "(SELECT GROUP_CONCAT("
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END "
+ "ORDER BY song_artist.idSong, song_artist.iOrder "
+ "SEPARATOR '; ') as val "
+ "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
+ else
+ strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
+ "FROM(SELECT song_artist.idSong, "
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END as val "
+ "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
+ "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
+
+ strSQL = "UPDATE song SET strArtistSort = " + strSQL +
+ "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
+ "AND strArtistDisp <> " +
+ strSQL;
+ if (idArtist > 0)
+ strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
+ "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)",
+ idArtist);
+ ExecuteQuery(strSQL);
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (CommitMultipleExecute())
+ return true;
+ else
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ return false;
+}
+
+std::string CMusicDatabase::GetAlbumById(int id)
+{
+ return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id));
+}
+
+int CMusicDatabase::GetGenreByName(const std::string& strGenre)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idGenre FROM genre "
+ "WHERE genre.strGenre LIKE '%s'",
+ strGenre.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("genre.idGenre").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ strSQL = "SELECT %s FROM genre ";
+ Filter extFilter;
+ extFilter.AppendField("genre.idGenre");
+ extFilter.AppendField("genre.strGenre");
+ if (bSources)
+ {
+ strSQL = "SELECT DISTINCT %s FROM genre ";
+ extFilter.AppendField("album_source.idSource");
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
+ extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
+ extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
+ extFilter.AppendOrder("genre.strGenre");
+ extFilter.AppendOrder("album_source.idSource");
+ }
+ extFilter.AppendWhere("genre.strGenre != ''");
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ strSQL = PrepareSQL(strSQL, extFilter.fields.c_str()) + strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (!bSources)
+ items.Reserve(iRowsFound);
+
+ // Get data from returned rows
+ // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
+ CVariant genreSources(CVariant::VariantTypeArray);
+ int idGenre = -1;
+ while (!m_pDS->eof())
+ {
+ if (idGenre != m_pDS->fv("genre.idGenre").get_asInt())
+ { // New genre
+ if (idGenre > 0 && bSources)
+ {
+ //Store sources for previous genre in item list
+ items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
+ genreSources.clear();
+ }
+ idGenre = m_pDS->fv("genre.idGenre").get_asInt();
+ std::string strGenre = m_pDS->fv("genre.strGenre").get_asString();
+ CFileItemPtr pItem(new CFileItem(strGenre));
+ pItem->GetMusicInfoTag()->SetTitle(strGenre);
+ pItem->GetMusicInfoTag()->SetGenre(strGenre);
+ pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre");
+ pItem->SetPath(StringUtils::Format("musicdb://genres/{}/", idGenre));
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+ // Get source data
+ if (bSources)
+ {
+ int sourceid = m_pDS->fv("album_source.idSource").get_asInt();
+ if (sourceid > 0)
+ genreSources.push_back(sourceid);
+ }
+ m_pDS->next();
+ }
+ if (bSources)
+ {
+ //Store sources for final genre
+ items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc)
+{
+ // Get disc node title from ids allowing for "*all"
+ std::string disctitle;
+ std::string albumtitle;
+ if (idAlbum > 0)
+ albumtitle = GetAlbumById(idAlbum);
+ if (idDisc > 0)
+ {
+ disctitle = GetSingleValue("song", "strDiscSubtitle",
+ PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc));
+ if (disctitle.empty())
+ disctitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc.
+ if (albumtitle.empty())
+ albumtitle = disctitle;
+ else
+ albumtitle = albumtitle + " - " + disctitle;
+ }
+ return albumtitle;
+}
+
+int CMusicDatabase::GetBoxsetsCount()
+{
+ return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
+}
+
+int CMusicDatabase::GetAlbumDiscsCount(int idAlbum)
+{
+ std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum);
+ return GetSingleValueInt(strSQL);
+}
+
+int CMusicDatabase::GetCompilationAlbumsCount()
+{
+ return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
+}
+
+int CMusicDatabase::GetSinglesCount()
+{
+ CDatabase::Filter filter(
+ PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
+ CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
+ return GetSongsCount(filter);
+}
+
+int CMusicDatabase::GetArtistCountForRole(int role)
+{
+ std::string strSQL = PrepareSQL(
+ "SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role);
+ return GetSingleValueInt(strSQL);
+}
+
+int CMusicDatabase::GetArtistCountForRole(const std::string& strRole)
+{
+ std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist "
+ "JOIN role ON song_artist.idRole = role.idRole "
+ "WHERE role.strRole LIKE '%s'",
+ strRole.c_str());
+ return GetSingleValueInt(strSQL);
+}
+
+bool CMusicDatabase::SetPathHash(const std::string& path, const std::string& hash)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (hash.empty())
+ { // this is an empty folder - we need only add it to the path table
+ // if the path actually exists
+ if (!CDirectory::Exists(path))
+ return false;
+ }
+ int idPath = AddPath(path);
+ if (idPath < 0)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash.c_str(), idPath);
+ m_pDS->exec(strSQL);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetPathHash(const std::string& path, std::string& hash)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ return false;
+ hash = m_pDS->fv("strHash").get_asString();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::RemoveSongsFromPath(const std::string& path1, MAPSONGS& songmap, bool exact)
+{
+ // We need to remove all songs from this path, as their tags are going
+ // to be re-read. We need to remove all songs from the song table + all links to them
+ // from the song link tables (as otherwise if a song is added back
+ // to the table with the same idSong, these tables can't be cleaned up properly later)
+
+ //! @todo SQLite probably doesn't allow this, but can we rely on that??
+
+ // We don't need to remove orphaned albums at this point as in AddAlbum() we check
+ // first whether the album has already been read during this scan, and if it hasn't
+ // we check whether it's in the table and update accordingly at that point, removing the entries from
+ // the album link tables. The only failure point for this is albums
+ // that span multiple folders, where just the files in one folder have been changed. In this case
+ // any linked fields that are only in the files that haven't changed will be removed. Clearly
+ // the primary albumartist still matches (as that's what we looked up based on) so is this really
+ // an issue? I don't think it is, as those artists will still have links to the album via the songs
+ // which is generally what we rely on, so the only failure point is albumartist lookup. In this
+ // case, it will return only things in the album_artist table from the newly updated songs (and
+ // only if they have additional artists). I think the effect of this is minimal at best, as ALL
+ // songs in the album should have the same albumartist!
+
+ // we also remove the path at this point as it will be added later on if the
+ // path still exists.
+ // After scanning we then remove the orphaned artists, genres and thumbs.
+
+ // Note: when used to remove all songs from a path and its subpath (exact=false), this
+ // does miss archived songs.
+ std::string path(path1);
+ SetLibraryLastUpdated();
+ try
+ {
+ if (!URIUtils::HasSlashAtEnd(path))
+ URIUtils::AddSlashAtEnd(path);
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Filename is not unique for a path as songs from a cuesheet have same filename.
+ // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
+ // in a folder and some edited and rescanned.
+ // Hence order by filename so these songs can be gathered together.
+ std::string where;
+ if (exact)
+ where = PrepareSQL(" WHERE strPath='%s'", path.c_str());
+ else
+ where = PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()),
+ path.c_str());
+ std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName";
+ if (!m_pDS->query(sql))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound > 0)
+ {
+ // Each file is potentially mapped to a list of songs, gather these and save as list
+ VECSONGS songs;
+ std::string filename;
+ std::vector<std::string> songIds;
+ while (!m_pDS->eof())
+ {
+ CSong song = GetSongFromDataset();
+ if (!filename.empty() && filename != song.strFileName)
+ {
+ // Save songs for previous filename
+ songmap.insert(std::make_pair(filename, songs));
+ songs.clear();
+ }
+ song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb");
+ songs.emplace_back(song);
+ songIds.push_back(PrepareSQL("%i", song.idSong));
+ filename = song.strFileName;
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
+
+ //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
+ for (const auto& id : songIds)
+ AnnounceRemove(MediaTypeSong, atoi(id.c_str()));
+
+ // Delete all songs, and anything linked to them via triggers
+ std::string strIDs = StringUtils::Join(songIds, ",");
+ sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")";
+ m_pDS->exec(sql);
+ }
+ // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
+ sql = "delete from path" + where;
+ m_pDS->exec(sql);
+ return iRowsFound > 0;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
+ }
+ return false;
+}
+
+void CMusicDatabase::CheckArtistLinksChanged()
+{
+ std::string strSQL = "SELECT COUNT(1) FROM removed_link ";
+ int iLinks = GetSingleValueInt(strSQL, m_pDS);
+ if (iLinks > 0)
+ {
+ SetArtistLinksUpdated(); // Store datetime artist links last updated
+ DeleteRemovedLinks(); // Clean-up artist links
+ }
+}
+
+bool CMusicDatabase::GetPaths(std::set<std::string>& paths)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ paths.clear();
+
+ // find all paths
+ if (!m_pDS->query("SELECT strPath FROM path"))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ while (!m_pDS->eof())
+ {
+ paths.insert(m_pDS->fv("strPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetSongUserrating(const std::string& filePath, int userrating)
+{
+ try
+ {
+ if (filePath.empty())
+ return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int songID = GetSongIDFromPath(filePath);
+ if (-1 == songID)
+ return false;
+
+ return SetSongUserrating(songID, userrating);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, userrating);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetSongUserrating(int idSong, int userrating)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql =
+ PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong);
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idSong, userrating);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (-1 == idAlbum)
+ return false;
+ std::string sql =
+ PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum);
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idAlbum, userrating);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetSongVotes(const std::string& filePath, int votes)
+{
+ try
+ {
+ if (filePath.empty())
+ return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int songID = GetSongIDFromPath(filePath);
+ if (-1 == songID)
+ return false;
+
+ std::string sql = PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID);
+
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, votes);
+ }
+ return false;
+}
+
+int CMusicDatabase::GetSongIDFromPath(const std::string& filePath)
+{
+ // grab the where string to identify the song id
+ CURL url(filePath);
+ if (url.IsProtocol("musicdb"))
+ {
+ std::string strFile = URIUtils::GetFileName(filePath);
+ URIUtils::RemoveExtension(strFile);
+ return atoi(strFile.c_str());
+ }
+ // hit the db
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strPath, strFileName;
+ SplitPath(filePath, strPath, strFileName);
+ URIUtils::AddSlashAtEnd(strPath);
+
+ std::string sql = PrepareSQL("SELECT idSong FROM song JOIN path ON song.idPath = path.idPath "
+ "WHERE song.strFileName='%s' AND path.strPath='%s'",
+ strFileName.c_str(), strPath.c_str());
+ if (!m_pDS->query(sql))
+ return -1;
+
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return -1;
+ }
+
+ int songID = m_pDS->fv("idSong").get_asInt();
+ m_pDS->close();
+ return songID;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::CommitTransaction()
+{
+ if (CDatabase::CommitTransaction())
+ { // number of items in the db has likely changed, so reset the infomanager cache
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(
+ LIBRARY_HAS_MUSIC, GetSongsCount() > 0);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ std::string strSQL;
+ int idSetting = -1;
+ try
+ {
+ CONTENT_TYPE content = CONTENT_NONE;
+
+ // Build where clause from virtual path
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ std::string itemType = musicUrl.GetType();
+ if (StringUtils::EqualsNoCase(itemType, "artists"))
+ {
+ content = CONTENT_ARTISTS;
+ }
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ {
+ content = CONTENT_ALBUMS;
+ }
+ else
+ return false; //Only artists and albums have info settings
+
+ std::string strSQLWhere;
+ if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere))
+ return false;
+
+ // Replace view names with table names
+ StringUtils::Replace(strSQLWhere, "artistview", "artist");
+ StringUtils::Replace(strSQLWhere, "albumview", "album");
+
+ BeginTransaction();
+ // Clear current scraper settings (0 => default scraper used)
+ if (content == CONTENT_ARTISTS)
+ strSQL = "UPDATE artist SET idInfoSetting = %i ";
+ else
+ strSQL = "UPDATE album SET idInfoSetting = %i ";
+ strSQL = PrepareSQL(strSQL, 0) + strSQLWhere;
+ m_pDS->exec(strSQL);
+
+ //Remove orphaned settings
+ CleanupInfoSettings();
+
+ if (scraper)
+ {
+ // Add new info setting
+ strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
+ strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
+ m_pDS->exec(strSQL);
+ idSetting = static_cast<int>(m_pDS->lastinsertid());
+
+ if (content == CONTENT_ARTISTS)
+ strSQL = "UPDATE artist SET idInfoSetting = %i ";
+ else
+ strSQL = "UPDATE album SET idInfoSetting = %i ";
+ strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere;
+ m_pDS->exec(strSQL);
+ }
+ CommitTransaction();
+ return true;
+ }
+ catch (...)
+ {
+ RollbackTransaction();
+ CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, strBaseDir, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetScraper(int id,
+ const CONTENT_TYPE& content,
+ const ADDON::ScraperPtr& scraper)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ std::string strSQL;
+ int idSetting = -1;
+ try
+ {
+ BeginTransaction();
+ // Fetch current info settings for item, 0 => default is used
+ if (content == CONTENT_ARTISTS)
+ strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
+ else
+ strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
+ strSQL = PrepareSQL(strSQL, id);
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idSetting = m_pDS->fv("idInfoSetting").get_asInt();
+ m_pDS->close();
+
+ if (idSetting < 1)
+ { // Add new info setting
+ strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
+ strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
+ m_pDS->exec(strSQL);
+ idSetting = static_cast<int>(m_pDS->lastinsertid());
+
+ if (content == CONTENT_ARTISTS)
+ strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
+ else
+ strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
+ strSQL = PrepareSQL(strSQL, idSetting, id);
+ m_pDS->exec(strSQL);
+ }
+ else
+ { // Update info setting
+ strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' "
+ "WHERE idSetting = %i";
+ strSQL =
+ PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting);
+ m_pDS->exec(strSQL);
+ }
+ CommitTransaction();
+ return true;
+ }
+ catch (...)
+ {
+ RollbackTransaction();
+ CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, id, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper)
+{
+ std::string scraperUUID;
+ std::string strSettings;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
+ if (content == CONTENT_ARTISTS)
+ strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting "
+ "WHERE artist.idArtist = %i";
+ else
+ strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting "
+ "WHERE album.idAlbum = %i";
+ strSQL = PrepareSQL(strSQL, id);
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ { // try and ascertain scraper
+ scraperUUID = m_pDS->fv("strScraperPath").get_asString();
+ strSettings = m_pDS->fv("strSettings").get_asString();
+
+ // Use pre configured or default scraper
+ ADDON::AddonPtr addon;
+ if (!scraperUUID.empty() &&
+ CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon,
+ ADDON::OnlyEnabled::CHOICE_YES) &&
+ addon)
+ {
+ scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+ if (scraper)
+ // Set settings
+ scraper->SetPathSettings(content, strSettings);
+ }
+ }
+ m_pDS->close();
+
+ if (!scraper)
+ { // use default music scraper instead
+ ADDON::AddonPtr addon;
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
+ ADDON::ScraperTypeFromContent(content), addon))
+ {
+ scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+ return scraper != NULL;
+ }
+ else
+ return false;
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} -({}, {} {}) failed", __FUNCTION__, id, scraperUUID, strSettings);
+ }
+ return false;
+}
+
+bool CMusicDatabase::ScraperInUse(const std::string& scraperID) const
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql =
+ PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID.c_str());
+ if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+ bool found = m_pDS->fv(0).get_asInt() > 0;
+ m_pDS->close();
+ return found;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetItems(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription);
+}
+
+bool CMusicDatabase::GetItems(const std::string& strBaseDir,
+ const std::string& itemType,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (StringUtils::EqualsNoCase(itemType, "genres"))
+ return GetGenresNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "sources"))
+ return GetSourcesNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "years"))
+ return GetYearsNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "roles"))
+ return GetRolesNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "artists"))
+ return GetArtistsNav(strBaseDir, items,
+ !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS),
+ -1, -1, -1, filter, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "discs"))
+ return GetDiscsByWhere(strBaseDir, filter, items, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "songs"))
+ return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true);
+
+ return false;
+}
+
+std::string CMusicDatabase::GetItemById(const std::string& itemType, int id)
+{
+ if (StringUtils::EqualsNoCase(itemType, "genres"))
+ return GetGenreById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "sources"))
+ return GetSourceById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "years"))
+ return std::to_string(id);
+ else if (StringUtils::EqualsNoCase(itemType, "artists"))
+ return GetArtistById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ return GetAlbumById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "roles"))
+ return GetRoleById(id);
+
+ return "";
+}
+
+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_SONGS))
+ return;
+
+ // Exporting albums either art or NFO (or both) selected
+ if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && settings.m_skipnfo &&
+ !settings.m_artwork && settings.IsItemExported(ELIBEXPORT_ALBUMS))
+ return;
+
+ std::string strFolder;
+ if (settings.IsSingleFile() || settings.IsSeparateFiles())
+ {
+ // Exporting to single file or separate files in a specified location
+ if (settings.m_strPath.empty())
+ return;
+
+ strFolder = settings.m_strPath;
+ if (!URIUtils::HasSlashAtEnd(strFolder))
+ URIUtils::AddSlashAtEnd(strFolder);
+ strFolder = URIUtils::GetDirectory(strFolder);
+ if (strFolder.empty())
+ return;
+ }
+ else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists()))
+ {
+ // Exporting artist folders only, or artist NFO or art to library folders
+ // need Artist Information Folder defined.
+ // (Album NFO and art goes to music folders)
+ strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (strFolder.empty())
+ return;
+ }
+
+ //
+ bool artistfoldersonly;
+ artistfoldersonly = settings.IsArtistFoldersOnly() ||
+ ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
+ settings.m_skipnfo && !settings.m_artwork);
+
+ int iFailCount = 0;
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ if (nullptr == m_pDS2)
+ return;
+
+ // Create our xml document
+ CXBMCTinyXML xmlDoc;
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ TiXmlNode* pMain = NULL;
+ if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly)
+ pMain = &xmlDoc;
+ else if (settings.IsSingleFile())
+ {
+ TiXmlElement xmlMainElement("musicdb");
+ pMain = xmlDoc.InsertEndChild(xmlMainElement);
+ }
+
+ if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly)
+ {
+ // Find albums to export
+ std::vector<int> albumIds;
+ std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str());
+ if (!settings.m_unscraped)
+ strSQL += "AND lastScraped IS NOT NULL";
+ CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
+ m_pDS->query(strSQL);
+
+ int total = m_pDS->num_rows();
+ int current = 0;
+
+ albumIds.reserve(total);
+ while (!m_pDS->eof())
+ {
+ albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto& albumId : albumIds)
+ {
+ CAlbum album;
+ GetAlbum(albumId, album);
+ std::string strAlbumPath;
+ std::string strPath;
+ // Get album path, empty unless all album songs are under a unique folder, and
+ // there are no songs from another album in the same folder.
+ if (!GetAlbumPath(albumId, strAlbumPath))
+ strAlbumPath.clear();
+ if (settings.IsSingleFile())
+ {
+ // Save album to xml, including album path
+ album.Save(pMain, "album", strAlbumPath);
+ }
+ else
+ { // Separate files and artwork
+ bool pathfound = false;
+ if (settings.IsToLibFolders())
+ { // Save album.nfo and artwork with music files.
+ // Most albums are under a unique folder, but if songs from various albums are mixed then
+ // avoid overwriting by not allow NFO and art to be exported
+ if (strAlbumPath.empty())
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as unique path not found",
+ __FUNCTION__, album.strAlbum);
+ else if (!CDirectory::Exists(strAlbumPath))
+ CLog::Log(
+ LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist",
+ __FUNCTION__, album.strAlbum, strAlbumPath);
+ else
+ {
+ strPath = strAlbumPath;
+ pathfound = true;
+ }
+ }
+ else
+ { // Save album.nfo and artwork to subfolder on export path
+ // strPath = strFolder/<albumartist name>/<albumname>
+ // where <albumname> is either the same name as the album folder
+ // containing the music files (if unique) or is created using the album name
+ std::string strAlbumArtist;
+ pathfound = GetArtistFolderName(album.GetAlbumArtist()[0],
+ album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist);
+ if (pathfound)
+ {
+ strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist);
+ pathfound = CDirectory::Exists(strPath);
+ if (!pathfound)
+ pathfound = CDirectory::Create(strPath);
+ }
+ if (!pathfound)
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as could not create {}",
+ __FUNCTION__, album.strAlbum, strPath);
+ else
+ {
+ std::string strAlbumFolder;
+ pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder);
+ if (pathfound)
+ {
+ strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder);
+ pathfound = CDirectory::Exists(strPath);
+ if (!pathfound)
+ pathfound = CDirectory::Create(strPath);
+ }
+ if (!pathfound)
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as could not create {}",
+ __FUNCTION__, album.strAlbum, strPath);
+ }
+ }
+ if (pathfound)
+ {
+ if (!settings.m_skipnfo)
+ {
+ // Save album to NFO, including album path
+ album.Save(pMain, "album", strAlbumPath);
+ std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo");
+ if (settings.m_overwrite || !CFile::Exists(nfoFile))
+ {
+ if (!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{}: Album nfo export failed! ('{}')",
+ __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ if (settings.m_artwork)
+ {
+ // Save art in album folder
+ // Note thumb resolution may be lower than original when overwriting
+ std::map<std::string, std::string> artwork;
+ std::string savedArtfile;
+ if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork))
+ {
+ for (const auto& art : artwork)
+ {
+ if (art.first == "thumb")
+ savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
+ else
+ savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
+ CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
+ settings.m_overwrite);
+ }
+ }
+ }
+ xmlDoc.Clear();
+ xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
+ }
+ }
+
+ if ((current % 50) == 0 && progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{album.strAlbum});
+ progressDialog->SetPercentage(current * 100 / total);
+ if (progressDialog->IsCanceled())
+ return;
+ }
+ current++;
+ }
+ }
+
+ // 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
+ std::vector<int> artistIds;
+ Filter filter;
+
+ if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS))
+ filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist "
+ "WHERE album_artist.idArtist = artist.idArtist)",
+ false);
+ if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS))
+ {
+ if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
+ filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idArtist = artist.idArtist )",
+ false);
+ else
+ filter.AppendWhere(
+ "EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)",
+ false);
+ }
+ else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
+ filter.AppendWhere(
+ "EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)",
+ false);
+
+ if (!settings.m_unscraped && !artistfoldersonly)
+ filter.AppendWhere("lastScraped IS NOT NULL", true);
+
+ std::string strSQL = "SELECT idArtist FROM artist";
+ BuildSQL(strSQL, filter, strSQL);
+ CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
+
+ m_pDS->query(strSQL);
+ int total = m_pDS->num_rows();
+ int current = 0;
+ artistIds.reserve(total);
+ while (!m_pDS->eof())
+ {
+ artistIds.push_back(m_pDS->fv("idArtist").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto& artistId : artistIds)
+ {
+ CArtist artist;
+ // Include discography when not folders only
+ GetArtist(artistId, artist, !artistfoldersonly);
+ std::string strPath;
+ std::map<std::string, std::string> artwork;
+ if (settings.IsSingleFile())
+ {
+ // Save artist to xml, and old path (common to music files) if it has one
+ GetOldArtistPath(artist.idArtist, strPath);
+ artist.Save(pMain, "artist", strPath);
+
+ if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
+ { // append to the XML
+ TiXmlElement additionalNode("art");
+ for (const auto& i : artwork)
+ XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
+ pMain->LastChild()->InsertEndChild(additionalNode);
+ }
+ }
+ else
+ { // Separate files: artist.nfo and artwork in strFolder/<artist name>
+ // Get unique folder allowing for duplicate names e.g. 2 x John Williams
+ bool pathfound = GetArtistFolderName(artist, strPath);
+ if (pathfound)
+ {
+ strPath = URIUtils::AddFileToFolder(strFolder, strPath);
+ pathfound = CDirectory::Exists(strPath);
+ if (!pathfound)
+ pathfound = CDirectory::Create(strPath);
+ }
+ if (!pathfound)
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting artist {} as could not create {}",
+ __FUNCTION__, artist.strArtist, strPath);
+ else
+ {
+ if (!artistfoldersonly)
+ {
+ if (!settings.m_skipnfo)
+ {
+ artist.Save(pMain, "artist", strPath);
+ std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo");
+ if (settings.m_overwrite || !CFile::Exists(nfoFile))
+ {
+ if (!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{}: Artist nfo export failed! ('{}')",
+ __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ if (settings.m_artwork)
+ {
+ std::string savedArtfile;
+ if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
+ {
+ for (const auto& art : artwork)
+ {
+ if (art.first == "thumb")
+ savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
+ else
+ savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
+ CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
+ settings.m_overwrite);
+ }
+ }
+ }
+ xmlDoc.Clear();
+ xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
+ }
+ }
+ }
+ if ((current % 50) == 0 && progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{artist.strArtist});
+ progressDialog->SetPercentage(current * 100 / total);
+ if (progressDialog->IsCanceled())
+ return;
+ }
+ current++;
+ }
+ }
+
+ if (settings.IsSingleFile())
+ {
+ std::string xmlFile = URIUtils::AddFileToFolder(
+ strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
+ if (CFile::Exists(xmlFile))
+ xmlFile = URIUtils::AddFileToFolder(
+ strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
+ xmlDoc.SaveFile(xmlFile);
+
+ CVariant data;
+ data["file"] = xmlFile;
+ if (iFailCount > 0)
+ data["failcount"] = iFailCount;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport",
+ data);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} failed", __FUNCTION__);
+ iFailCount++;
+ }
+
+ if (progressDialog)
+ progressDialog->Close();
+
+ if (iFailCount > 0 && progressDialog)
+ HELPERS::ShowOKDialogLines(
+ CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)});
+}
+
+bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog)
+{
+ 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);
+ 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 (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(xmlFile) && progressDialog)
+ {
+ HELPERS::ShowOKDialogLines(CVariant{20197}, CVariant{38354}); //"Unable to read xml file"
+ return;
+ }
+
+ TiXmlElement* root = xmlDoc.RootElement();
+ if (!root)
+ return;
+
+ TiXmlElement* entry = root->FirstChildElement();
+ int current = 0;
+ int total = 0;
+ int songtotal = 0;
+ // Count the number of artists, albums and songs
+ while (entry)
+ {
+ if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 ||
+ StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
+ total++;
+ else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
+ songtotal++;
+
+ entry = entry->NextSiblingElement();
+ }
+
+ BeginTransaction();
+ entry = root->FirstChildElement();
+ while (entry)
+ {
+ std::string strTitle;
+ if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0)
+ {
+ CArtist importedArtist;
+ importedArtist.Load(entry);
+ strTitle = importedArtist.strArtist;
+
+ // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
+ int idArtist = GetArtistByMatch(importedArtist);
+ if (idArtist > -1)
+ {
+ CArtist artist;
+ GetArtist(idArtist, artist, true); // include discography
+ artist.MergeScrapedArtist(importedArtist, true);
+ UpdateArtist(artist);
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} - Not import additional artist data as {} not found",
+ __FUNCTION__, importedArtist.strArtist);
+ current++;
+ }
+ else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
+ {
+ CAlbum importedAlbum;
+ importedAlbum.Load(entry);
+ strTitle = importedAlbum.strAlbum;
+ // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
+ int idAlbum = GetAlbumByMatch(importedAlbum);
+ if (idAlbum > -1)
+ {
+ CAlbum album;
+ GetAlbum(idAlbum, album, true);
+ album.MergeScrapedAlbum(importedAlbum, true);
+ UpdateAlbum(album); //Will replace song artists if present in xml
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} - Not import additional album data as {} not found", __FUNCTION__,
+ importedAlbum.strAlbum);
+
+ current++;
+ }
+ entry = entry->NextSiblingElement();
+ if (progressDialog && total)
+ {
+ progressDialog->SetPercentage(current * 100 / total);
+ progressDialog->SetLine(2, CVariant{std::move(strTitle)});
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ RollbackTransaction();
+ return;
+ }
+ }
+ }
+ 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();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+ 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 (StringUtils::CompareNoCase(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(static_cast<double>(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)", static_cast<double>(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
+ // mbid is unique so subquery can only return one result at most
+ 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);
+
+ // Can only be one album with same title and artist(s) and no mbid.
+ // But could have 2 releases one with and one without mbid, match up those without mbid
+ strSQL = "UPDATE HistSong "
+ "SET idAlbum = (SELECT album.idAlbum FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum "
+ "AND album.strMusicBrainzAlbumID IS NULL "
+ "AND HistSong.strMusicBrainzAlbumID IS NULL) "
+ "WHERE EXISTS(SELECT 1 FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum "
+ "AND album.strMusicBrainzAlbumID IS NULL "
+ "AND HistSong.strMusicBrainzAlbumID IS NULL) "
+ "AND idAlbum < 0";
+ m_pDS->exec(strSQL);
+
+ // Try match rest by title and artist(s), prioritise one without mbid
+ // Target could have multiple releases - with mbid (non-matching) or one without mbid
+ strSQL = "UPDATE HistSong "
+ "SET idAlbum = (SELECT album.idAlbum FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum "
+ "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
+ "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);
+
+ // An album can have more than one song with same track and title (although idAlbum, track and
+ // title is often unique), but not using filename as an identifier to allow for import of song
+ // history for renamed files. It is about song playback not file playback.
+ // Pick the first
+ strSQL = "UPDATE HistSong "
+ "SET idSong = (SELECT idsong FROM song "
+ "WHERE HistSong.idAlbum = song.idAlbum AND "
+ "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
+ "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 = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS);
+ 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), total - unmatched, total);
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information)));
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ if (bHistSongExists)
+ m_pDS->exec("DROP TABLE HistSong");
+ }
+ return false;
+}
+
+void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ item.SetProperty("artist_sortname", artist.strSortName);
+ item.SetProperty("artist_type", artist.strType);
+ item.SetProperty("artist_gender", artist.strGender);
+ item.SetProperty("artist_disambiguation", artist.strDisambiguation);
+ item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator));
+ item.SetProperty("artist_instrument_array", artist.instruments);
+ item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator));
+ item.SetProperty("artist_style_array", artist.styles);
+ item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator));
+ item.SetProperty("artist_mood_array", artist.moods);
+ item.SetProperty("artist_born", artist.strBorn);
+ item.SetProperty("artist_formed", artist.strFormed);
+ item.SetProperty("artist_description", artist.strBiography);
+ item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator));
+ item.SetProperty("artist_genre_array", artist.genre);
+ item.SetProperty("artist_died", artist.strDied);
+ item.SetProperty("artist_disbanded", artist.strDisbanded);
+ item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator));
+ item.SetProperty("artist_yearsactive_array", artist.yearsActive);
+}
+
+void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ item.SetProperty("album_description", album.strReview);
+ item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator));
+ item.SetProperty("album_theme_array", album.themes);
+ item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator));
+ item.SetProperty("album_mood_array", album.moods);
+ item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator));
+ item.SetProperty("album_style_array", album.styles);
+ item.SetProperty("album_type", album.strType);
+ item.SetProperty("album_label", album.strLabel);
+ item.SetProperty("album_artist", album.GetAlbumArtistString());
+ item.SetProperty("album_artist_array", album.GetAlbumArtist());
+ item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator));
+ item.SetProperty("album_genre_array", album.genre);
+ item.SetProperty("album_title", album.strAlbum);
+ if (album.fRating > 0)
+ item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating));
+ if (album.iUserrating > 0)
+ item.SetProperty("album_userrating", album.iUserrating);
+ if (album.iVotes > 0)
+ item.SetProperty("album_votes", album.iVotes);
+
+ item.SetProperty("album_isboxset", album.bBoxedSet);
+ item.SetProperty("album_totaldiscs", album.iTotalDiscs);
+ item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType));
+ item.SetProperty("album_duration",
+ StringUtils::SecondsToTimeString(album.iAlbumDuration,
+ static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS)));
+}
+
+void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item)
+{
+ if (!item.HasMusicInfoTag())
+ return;
+ // May already have song artist ids as item property set when data read from
+ // db, but check property is valid array (scripts could set item properties
+ // incorrectly), otherwise try to fetch artist by name.
+ int idArtist = -1;
+ if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray())
+ {
+ CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array();
+ idArtist = static_cast<int>(varid->asInteger());
+ }
+ else
+ idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString());
+ if (idArtist > -1)
+ {
+ CArtist artist;
+ if (GetArtist(idArtist, artist))
+ SetPropertiesFromArtist(item, artist);
+ }
+ int idAlbum = item.GetMusicInfoTag()->GetAlbumId();
+ if (idAlbum <= 0)
+ idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(),
+ item.GetMusicInfoTag()->GetArtistString());
+ if (idAlbum > -1)
+ {
+ CAlbum album;
+ if (GetAlbum(idAlbum, album, false))
+ SetPropertiesFromAlbum(item, album);
+ }
+}
+
+void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType)
+{
+ std::string strSQL;
+ try
+ {
+ if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong)
+ return;
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
+ // e.g. when artwork for item is changed from info dialog but not item details.
+ // Use SQL UPDATE that does not change record data.
+ if (mediaType == MediaTypeArtist)
+ strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId);
+ else if (mediaType == MediaTypeAlbum)
+ strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId);
+ else // MediaTypeSong
+ strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId);
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__,
+ mediaId, mediaType, strSQL);
+ }
+}
+
+void CMusicDatabase::SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::map<std::string, std::string>& art)
+{
+ for (const auto& i : art)
+ SetArtForItem(mediaId, mediaType, i.first, i.second);
+}
+
+void CMusicDatabase::SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::string& artType,
+ const std::string& url)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ // don't set <foo>.<bar> art types - these are derivative types from parent items
+ if (artType.find('.') != std::string::npos)
+ return;
+
+ std::string sql = PrepareSQL("SELECT art_id FROM art "
+ "WHERE media_id=%i AND media_type='%s' AND type='%s'",
+ mediaId, mediaType.c_str(), artType.c_str());
+ m_pDS->query(sql);
+ if (!m_pDS->eof())
+ { // update
+ int artId = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
+ m_pDS->exec(sql);
+ }
+ else
+ { // insert
+ m_pDS->close();
+ sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
+ "VALUES (%d, '%s', '%s', '%s')",
+ mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType,
+ artType, url);
+ }
+}
+
+bool CMusicDatabase::GetArtForItem(
+ int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader>& art)
+{
+ std::string strSQL;
+ try
+ {
+ if (!(songId > 0 || albumId > 0 || artistId > 0))
+ return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ Filter filter;
+ if (songId > 0)
+ filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong));
+ if (albumId > 0)
+ filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum),
+ false);
+ if (artistId > 0)
+ filter.AppendWhere(
+ PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false);
+
+ strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder "
+ "FROM art";
+ if (!BuildSQL(strSQL, filter, strSQL))
+ return false;
+
+ if (!(artistId > 0))
+ {
+ // Artist ID unknown, so lookup album artist for albums and songs
+ std::string strSQL2;
+ if (albumId > 0)
+ {
+ //Album ID known, so use it to look up album artist(s)
+ strSQL2 = PrepareSQL(
+ "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
+ "url, album_artist.iOrder as iorder FROM art "
+ "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
+ "WHERE album_artist.idAlbum = %i ",
+ MediaTypeArtist, albumId);
+ if (bPrimaryArtist)
+ strSQL2 += "AND album_artist.iOrder = 0";
+
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+ if (songId > 0)
+ {
+ if (albumId < 0)
+ {
+ //Album ID unknown, so get from song to look up album artist(s)
+ strSQL2 = PrepareSQL(
+ "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
+ "url, album_artist.iOrder as iorder FROM art "
+ "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
+ "JOIN song ON song.idAlbum = album_artist.idAlbum "
+ "WHERE song.idSong = %i ",
+ MediaTypeArtist, songId);
+ if (bPrimaryArtist)
+ strSQL2 += "AND album_artist.iOrder = 0";
+
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+
+ // Artist ID unknown, so lookup artist for songs (could be different from album artist)
+ strSQL2 = PrepareSQL(
+ "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
+ "url, song_artist.iOrder as iorder FROM art "
+ "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
+ "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
+ MediaTypeArtist, songId, ROLE_ARTIST);
+ if (bPrimaryArtist)
+ strSQL2 += "AND song_artist.iOrder = 0";
+
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+ }
+ if (songId > 0 && albumId < 0)
+ {
+ //Album ID unknown, so get from song to look up album art
+ std::string strSQL2;
+ strSQL2 = PrepareSQL("SELECT art_id, media_id, media_type, type, '' as prefix, "
+ "url, 0 as iorder FROM art "
+ "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
+ "WHERE song.idSong = %i ",
+ MediaTypeAlbum, songId);
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+
+ m_pDS2->query(strSQL);
+ while (!m_pDS2->eof())
+ {
+ ArtForThumbLoader artitem;
+ artitem.artType = m_pDS2->fv("type").get_asString();
+ artitem.mediaType = m_pDS2->fv("media_type").get_asString();
+ artitem.prefix = m_pDS2->fv("prefix").get_asString();
+ artitem.url = m_pDS2->fv("url").get_asString();
+ int iOrder = m_pDS2->fv("iorder").get_asInt();
+ // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
+ if (iOrder > 0)
+ artitem.prefix += m_pDS2->fv("iorder").get_asString();
+
+ art.emplace_back(artitem);
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return !art.empty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtForItem(int mediaId,
+ const std::string& mediaType,
+ std::map<std::string, std::string>& art)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'",
+ mediaId, mediaType.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return !art.empty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId);
+ }
+ return false;
+}
+
+std::string CMusicDatabase::GetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::string& artType)
+{
+ std::string query = PrepareSQL("SELECT url FROM art "
+ "WHERE media_id=%i AND media_type='%s' AND type='%s'",
+ mediaId, mediaType.c_str(), artType.c_str());
+ return GetSingleValue(query, m_pDS2);
+}
+
+bool CMusicDatabase::RemoveArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::string& artType)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM art "
+ "WHERE media_id=%i AND media_type='%s' AND type='%s'",
+ mediaId, mediaType.c_str(), artType.c_str()));
+}
+
+bool CMusicDatabase::RemoveArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::set<std::string>& artTypes)
+{
+ bool result = true;
+ for (const auto& i : artTypes)
+ result &= RemoveArtForItem(mediaId, mediaType, i);
+
+ return result;
+}
+
+bool CMusicDatabase::GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ artTypes.emplace_back(m_pDS->fv(0).get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType);
+ }
+ return false;
+}
+
+std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId,
+ const MediaType& mediaType)
+{
+ CScraperUrl thumbURL;
+ if (mediaType == MediaTypeArtist)
+ {
+ CArtist artist;
+ if (GetArtist(mediaId, artist))
+ thumbURL = artist.thumbURL;
+ }
+ else if (mediaType == MediaTypeAlbum)
+ {
+ CAlbum album;
+ if (GetAlbum(mediaId, album))
+ thumbURL = album.thumbURL;
+ }
+
+ std::vector<std::string> result;
+ for (const auto& urlEntry : thumbURL.GetUrls())
+ {
+ std::string artType = urlEntry.m_aspect;
+ if (artType.empty())
+ artType = "thumb";
+ if (std::find(result.begin(), result.end(), artType) == result.end())
+ result.push_back(artType);
+ }
+ return result;
+}
+
+std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem(
+ int mediaId, const MediaType& mediaType, const std::string& artType)
+{
+ CScraperUrl thumbURL;
+ if (mediaType == MediaTypeArtist)
+ {
+ CArtist artist;
+ if (GetArtist(mediaId, artist))
+ thumbURL = artist.thumbURL;
+ }
+ else if (mediaType == MediaTypeAlbum)
+ {
+ CAlbum album;
+ if (GetAlbum(mediaId, album))
+ thumbURL = album.thumbURL;
+ }
+
+ std::vector<CScraperUrl::SUrlEntry> result;
+ for (auto urlEntry : thumbURL.GetUrls())
+ {
+ if (urlEntry.m_aspect.empty())
+ urlEntry.m_aspect = "thumb";
+ if (artType.empty() || urlEntry.m_aspect == artType)
+ result.push_back(urlEntry);
+ }
+ return result;
+}
+
+int CMusicDatabase::GetOrderFilter(const std::string& type,
+ const SortDescription& sorting,
+ Filter& filter)
+{
+ // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
+ int iFieldsAdded = 0;
+ filter.fields.clear(); // remove "*"
+ std::vector<std::string> orderfields;
+ std::string DESC;
+
+ if (sorting.sortOrder == SortOrderDescending)
+ DESC = " DESC";
+
+ if (sorting.sortBy == SortByRandom)
+ orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
+ else
+ {
+ FieldList fields;
+ SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields);
+ for (const auto& it : fields)
+ {
+ std::string strField;
+ if (it == FieldYear)
+ strField = "iYear";
+ else
+ strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect);
+ if (!strField.empty())
+ orderfields.emplace_back(strField);
+ }
+ }
+
+ // Convert field names into order by statement elements
+ for (auto& name : orderfields)
+ {
+ //Add field for adjusted name sorting using sort name and ignoring articles
+ std::string sortSQL;
+ if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist"))
+ {
+ if (StringUtils::EndsWith(name, "strArtists"))
+ sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort");
+ else
+ sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName");
+ if (!sortSQL.empty())
+ {
+ name = "artistsortname";
+ filter.AppendField(sortSQL); // Add artistsortname as scalar query field
+ iFieldsAdded++;
+ }
+ // Natural number case-insensitive sort
+ filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
+ }
+ else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle"))
+ {
+ sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, "");
+ if (!sortSQL.empty())
+ {
+ name = "titlesortname";
+ filter.AppendField(sortSQL); // Add sortname as scalar query field
+ iFieldsAdded++;
+ }
+ // Natural number case-insensitive sort
+ filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
+ }
+ else if (StringUtils::EndsWith(name, "strGenres"))
+ // Natural number case-insensitive sort
+ filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
+ else
+ filter.AppendOrder(name + DESC);
+ }
+ return iFieldsAdded;
+}
+
+bool CMusicDatabase::GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting)
+{
+ if (!musicUrl.IsValid())
+ return false;
+
+ std::string type = musicUrl.GetType();
+ const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
+
+ // Check for playlist rules first, they may contain role criteria
+ bool hasRoleRules = false;
+
+ auto option = options.find("xsp");
+ if (option != options.end())
+ {
+ CSmartPlaylist xsp;
+ if (!xsp.LoadFromJson(option->second.asString()))
+ return false;
+
+ std::set<std::string> playlists;
+ std::string xspWhere;
+ xspWhere = xsp.GetWhereClause(*this, playlists);
+ hasRoleRules = xsp.GetType() == "artists" &&
+ xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos;
+
+ // Check if the filter playlist matches the item type
+ // Allow for grouping name like "originalyears" and type "years"
+ if (xsp.GetType() == type ||
+ (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed()))
+ {
+ filter.AppendWhere(xspWhere);
+
+ if (xsp.GetLimit() > 0)
+ sorting.limitEnd = xsp.GetLimit();
+ if (xsp.GetOrder() != SortByNone)
+ sorting.sortBy = xsp.GetOrder();
+ sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sorting.sortAttributes = SortAttributeIgnoreArticle;
+ }
+ }
+
+ //Process role options, common to artist and album type filtering
+ int idRole = 1; // Default restrict song_artist to "artists" only, no other roles.
+ option = options.find("roleid");
+ if (option != options.end())
+ idRole = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("role");
+ if (option != options.end())
+ {
+ if (option->second.asString() == "all" || option->second.asString() == "%")
+ idRole = -1000; //All roles
+ else
+ idRole = GetRoleByName(option->second.asString());
+ }
+ }
+ if (hasRoleRules)
+ {
+ // Get Role from role rule(s) here.
+ // But that requires much change, so for now get all roles as better than none
+ idRole = -1000; //All roles
+ }
+
+ std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role
+ if (idRole > 0)
+ strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole);
+
+ int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1;
+ int idDisc = -1;
+ int idSource = -1;
+ bool albumArtistsOnly = false;
+ bool useOriginalYear = false;
+ std::string artistname;
+
+ // Process useoriginalyear option, setting overridden by option
+ useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
+ option = options.find("useoriginalyear");
+ if (option != options.end())
+ useOriginalYear = option->second.asBoolean();
+
+ // Process albumartistsonly option
+ option = options.find("albumartistsonly");
+ if (option != options.end())
+ albumArtistsOnly = option->second.asBoolean();
+
+ // Process genre option
+ option = options.find("genreid");
+ if (option != options.end())
+ idGenre = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("genre");
+ if (option != options.end())
+ idGenre = GetGenreByName(option->second.asString());
+ }
+
+ // Process source option
+ option = options.find("sourceid");
+ if (option != options.end())
+ idSource = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("source");
+ if (option != options.end())
+ idSource = GetSourceByName(option->second.asString());
+ }
+
+ // Process album option
+ option = options.find("albumid");
+ if (option != options.end())
+ idAlbum = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("album");
+ if (option != options.end())
+ idAlbum = GetAlbumByName(option->second.asString());
+ }
+
+ // Process artist option
+ option = options.find("artistid");
+ if (option != options.end())
+ idArtist = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("artist");
+ if (option != options.end())
+ {
+ idArtist = GetArtistByName(option->second.asString());
+ if (idArtist == -1)
+ { // not found with that name, or more than one found as artist name is not unique
+ artistname = option->second.asString();
+ }
+ }
+ }
+
+ // Process song option
+ option = options.find("songid");
+ if (option != options.end())
+ idSong = static_cast<int>(option->second.asInteger());
+
+ if (type == "artists")
+ {
+ if (!hasRoleRules)
+ { // Not an "artists" smart playlist with roles rules, so get filter from options
+ if (idArtist > 0)
+ filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist));
+ else if (idAlbum > 0)
+ filter.AppendWhere(
+ PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
+ "WHERE album_artist.idAlbum = %i)",
+ idAlbum));
+ else if (idSong > 0)
+ {
+ filter.AppendWhere(
+ PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
+ "WHERE song_artist.idSong = %i %s)",
+ idSong, strRoleSQL.c_str()));
+ }
+ else
+ { /*
+ Process idRole, idGenre, idSource and albumArtistsOnly options
+
+ For artists these rules are combined because they apply via album and song
+ and so we need to ensure all criteria are met via the same album or song.
+ 1) Some artists may be only album artists, so for all artists (with linked
+ albums or songs) we need to check both album_artist and song_artist tables.
+ 2) Role is determined from song_artist table, so even if looking for album artists
+ only we find those that also have a specific role e.g. which album artist is a
+ composer of songs in that album, from entries in the song_artist table.
+ a) Role < -1 is used to indicate that all roles are wanted.
+ b) When not album artists only and a specific role wanted then only the song_artist
+ table is checked.
+ c) When album artists only and role = 1 (an "artist") then only the album_artist
+ table is checked.
+ */
+ std::string albumArtistSQL, songArtistSQL;
+ ExistsSubQuery albumArtistSub("album_artist",
+ "album_artist.idArtist = artistview.idArtist");
+ // Prepare album artist subquery SQL
+ if (idSource > 0)
+ {
+ if (idRole == 1 && idGenre < 0)
+ {
+ albumArtistSub.AppendJoin(
+ "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
+ albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
+ }
+ else
+ {
+ albumArtistSub.AppendWhere(
+ PrepareSQL("EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idSource = %i "
+ "AND album_source.idAlbum = album_artist.idAlbum)",
+ idSource));
+ }
+ }
+ if (idRole <= 1 && idGenre > 0)
+ { // Check genre of songs of album using nested subquery
+ std::string strGenre =
+ PrepareSQL("EXISTS(SELECT 1 FROM song "
+ "JOIN song_genre ON song_genre.idSong = song.idSong "
+ "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)",
+ idGenre);
+ albumArtistSub.AppendWhere(strGenre);
+ }
+
+ // Prepare song artist subquery SQL
+ ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
+ if (idRole > 0)
+ songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
+ if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1)
+ {
+ songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
+ "JOIN song_genre ON song_genre.idSong = song.idSong "
+ "WHERE song.idSong = song_artist.idSong "
+ "AND song_genre.idGenre = %i "
+ "AND EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idSource = %i "
+ "AND album_source.idAlbum = song.idAlbum))",
+ idGenre, idSource));
+ }
+ else
+ {
+ if (idGenre > 0)
+ {
+ songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
+ songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
+ }
+ if (idSource > 0 && !albumArtistsOnly)
+ {
+ songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+ songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
+ songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
+ }
+ if (idRole > 1 && albumArtistsOnly)
+ { // Album artists only with role, check AND in album_artist for album of song
+ // using nested subquery correlated with album_artist
+ songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+ songArtistSub.param = "song_artist.idArtist = album_artist.idArtist";
+ songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum");
+ }
+ }
+
+ // Build filter clause from subqueries
+ if (idRole > 1 && albumArtistsOnly)
+ { // Album artists only with role, check AND in album_artist for album of song
+ // using nested subquery correlated with album_artist
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.AppendWhere(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ filter.AppendWhere(albumArtistSQL);
+ }
+ else
+ {
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
+ { // Artist contributing to songs, any role, check OR album artist too
+ // as artists can be just album artists but not song artists
+ filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
+ }
+ else if (idRole > 1)
+ {
+ // Artist contributes that role (not albmartistsonly as already handled)
+ filter.AppendWhere(songArtistSQL);
+ }
+ else // idRole = 1 and albumArtistsOnly
+ { // Only look at album artists, not albums where artist features on songs
+ filter.AppendWhere(albumArtistSQL);
+ }
+ }
+ }
+ }
+ // remove the null string
+ filter.AppendWhere("artistview.strArtist != ''");
+ }
+ else if (type == "albums")
+ {
+ option = options.find("year");
+ if (option != options.end())
+ {
+ if (!useOriginalYear)
+ filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ else
+ filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ }
+ option = options.find("compilation");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
+
+ option = options.find("boxset");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
+
+ if (idSource > 0)
+ filter.AppendWhere(PrepareSQL(
+ "EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
+ idSource));
+
+ // Process artist, role and genre options together as song subquery to filter those
+ // albums that have songs with both that artist and genre
+ std::string albumArtistSQL, songArtistSQL, genreSQL;
+ ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum");
+ genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
+ genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
+ ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
+ ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
+ songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+
+ if (idArtist > 0)
+ {
+ songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist));
+ albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist));
+ }
+ else if (!artistname.empty())
+ { // Artist name is not unique, so could get albums or songs from more than one.
+ songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
+ songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
+
+ albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
+ albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
+ }
+ if (idRole > 0)
+ songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
+ if (idGenre > 0)
+ {
+ songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
+ songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
+ }
+
+ if (idArtist > 0 || !artistname.empty())
+ {
+ if (idRole <= 1 && idGenre > 0)
+ { // Check genre of songs of album using nested subquery
+ genreSub.BuildSQL(genreSQL);
+ albumArtistSub.AppendWhere(genreSQL);
+ }
+ if (idRole > 1 && albumArtistsOnly)
+ { // Album artists only with role, check AND in album_artist for same song
+ // using nested subquery correlated with album_artist
+ songArtistSub.param = "song.idAlbum = album_artist.idAlbum";
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.AppendWhere(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ filter.AppendWhere(albumArtistSQL);
+ }
+ else
+ {
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
+ { // Artist contributing to songs, any role, check OR album artist too
+ // as artists can be just album artists but not song artists
+ filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
+ }
+ else if (idRole > 1)
+ { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
+ filter.AppendWhere(songArtistSQL);
+ }
+ else // idRole = 1 and albumArtistsOnly
+ { // Only look at album artists, not albums where artist features on songs
+ // This may want to be a separate option so you can choose to see all the albums where that artist
+ // appears on one or more songs without having to list all song artists in the artists node.
+ filter.AppendWhere(albumArtistSQL);
+ }
+ }
+ }
+ else
+ { // No artist given
+ if (idGenre > 0)
+ { // Have genre option but not artist
+ genreSub.param = "song.idAlbum = albumview.idAlbum";
+ genreSub.BuildSQL(genreSQL);
+ filter.AppendWhere(genreSQL);
+ }
+ // Exclude any single albums (aka empty tagged albums)
+ // This causes "albums" media filter artist selection to only offer album artists
+ option = options.find("show_singles");
+ if (option == options.end() || !option->second.asBoolean())
+ filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
+ }
+ }
+ else if (type == "discs")
+ {
+ if (idAlbum > 0)
+ filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum));
+ else
+ {
+ option = options.find("year");
+ if (option != options.end())
+ {
+ if (!useOriginalYear)
+ filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ else
+ filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ }
+
+ option = options.find("compilation");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
+
+ option = options.find("boxset");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
+
+ if (idSource > 0)
+ filter.AppendWhere(PrepareSQL(
+ "EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
+ idSource));
+ }
+ option = options.find("discid");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger()));
+
+ option = options.find("disctitle");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str()));
+
+ if (idGenre > 0)
+ filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
+ "song.idSong AND song_genre.idGenre = %i)",
+ idGenre));
+
+ std::string songArtistClause, albumArtistClause;
+ if (idArtist > 0)
+ {
+ songArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
+ idArtist, strRoleSQL.c_str());
+ albumArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
+ "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
+ idArtist);
+ }
+ else if (!artistname.empty())
+ { // Artist name is not unique, so could get songs from more than one.
+ songArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
+ artistname.c_str(), strRoleSQL.c_str());
+ albumArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
+ "album_artist.idArtist "
+ "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
+ artistname.c_str());
+ }
+
+ // Process artist name or id option
+ if (!songArtistClause.empty())
+ {
+ if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ else if (idRole > 1)
+ {
+ if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
+ filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
+ else // songs where artist contributes that role.
+ filter.AppendWhere(songArtistClause);
+ }
+ else
+ {
+ if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
+ filter.AppendWhere(albumArtistClause);
+ else // Artist is song artist or album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ }
+ }
+ }
+ else if (type == "songs" || type == "singles")
+ {
+ option = options.find("singles");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL(
+ "songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
+ option->second.asBoolean() ? "" : "NOT ",
+ CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
+
+ // When have idAlbum skip year, compilation, boxset criteria as already applied via album
+ if (idAlbum < 0)
+ {
+ option = options.find("year");
+ if (option != options.end())
+ {
+ if (!useOriginalYear)
+ filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ else
+ filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ }
+ option = options.find("compilation");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
+
+ option = options.find("boxset");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
+ "songview.idAlbum AND bBoxedSet = %i)",
+ option->second.asBoolean() ? 1 : 0));
+ }
+
+ option = options.find("discid");
+ if (option != options.end())
+ idDisc = static_cast<int>(option->second.asInteger());
+
+ option = options.find("disctitle");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str()));
+
+ if (idSong > 0)
+ filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong));
+
+ if (idAlbum > 0)
+ filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum));
+
+ if (idDisc > 0)
+ filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc));
+
+ if (idGenre > 0)
+ filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre "
+ "WHERE song_genre.idGenre = %i)",
+ idGenre));
+
+ if (idSource > 0)
+ filter.AppendWhere(PrepareSQL(
+ "EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)",
+ idSource));
+
+ std::string songArtistClause, albumArtistClause;
+ if (idArtist > 0)
+ {
+ songArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
+ idArtist, strRoleSQL.c_str());
+ albumArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM album_artist "
+ "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
+ idArtist);
+ }
+ else if (!artistname.empty())
+ { // Artist name is not unique, so could get songs from more than one.
+ songArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM song_artist "
+ "JOIN artist ON artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
+ artistname.c_str(), strRoleSQL.c_str());
+ albumArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM album_artist "
+ "JOIN artist ON artist.idArtist = album_artist.idArtist "
+ "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
+ artistname.c_str());
+ }
+
+ // Process artist name or id option
+ if (!songArtistClause.empty())
+ {
+ if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ else if (idRole > 1)
+ {
+ if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
+ filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
+ else // songs where artist contributes that role.
+ filter.AppendWhere(songArtistClause);
+ }
+ else
+ {
+ if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
+ filter.AppendWhere(albumArtistClause);
+ else // Artist is song artist or album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ }
+ }
+ }
+
+ option = options.find("filter");
+ if (option != options.end())
+ {
+ CSmartPlaylist xspFilter;
+ if (!xspFilter.LoadFromJson(option->second.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ if (xspFilter.GetType() == type)
+ {
+ std::set<std::string> playlists;
+ filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
+ }
+ // remove the filter if it doesn't match the item type
+ else
+ musicUrl.RemoveOption("filter");
+ }
+
+ return true;
+}
+
+std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath)
+{
+ if (strFileNameAndPath.empty())
+ return std::string();
+
+ CDateTime dateMedia;
+ int code;
+ code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded;
+ // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
+ if (code == 1)
+ dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath);
+ //2 using the newer datetime of the file's mtime and ctime
+ else if (code == 2)
+ dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath);
+ //3 using the older datetime of the file's mtime and ctime
+ else if (code == 3)
+ dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath);
+ //0 using the current datetime if none of the above matches or one returns an invalid datetime
+ if (!dateMedia.IsValid())
+ dateMedia = CDateTime::GetCurrentDateTime();
+
+ return dateMedia.GetAsDBDateTime();
+}
+
+bool CMusicDatabase::AddAudioBook(const CFileItem& item)
+{
+ auto const& artists = item.GetMusicInfoTag()->GetArtist();
+ std::string strSQL = PrepareSQL(
+ "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
+ "VALUES (NULL,'%s','%s',%i,'%s','%s')",
+ item.GetMusicInfoTag()->GetAlbum().c_str(), artists.empty() ? "" : artists[0].c_str(), 0,
+ item.GetDynPath().c_str(), CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark)
+{
+ std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook "
+ "WHERE file='%s'",
+ item.GetDynPath().c_str());
+ if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
+ {
+ if (!AddAudioBook(item))
+ return false;
+ }
+
+ strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i "
+ "WHERE file='%s'",
+ bookmark, item.GetDynPath().c_str());
+
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark)
+{
+ std::string strSQL =
+ PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'", item.GetDynPath().c_str());
+ if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
+ return false;
+
+ bookmark = m_pDS->fv(0).get_asInt();
+ return true;
+}
diff --git a/xbmc/music/MusicDatabase.h b/xbmc/music/MusicDatabase.h
new file mode 100644
index 0000000..8544eb2
--- /dev/null
+++ b/xbmc/music/MusicDatabase.h
@@ -0,0 +1,1157 @@
+/*
+ * Copyright (C) 2005-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
+
+/*!
+ \file MusicDatabase.h
+\brief
+*/
+
+#include "Album.h"
+#include "MediaSource.h"
+#include "addons/Scraper.h"
+#include "dbwrappers/Database.h"
+#include "settings/LibExportSettings.h"
+#include "utils/SortUtils.h"
+
+#include <utility>
+#include <vector>
+
+class CArtist;
+class CFileItem;
+class CMusicDbUrl;
+
+namespace dbiplus
+{
+class field_value;
+typedef std::vector<field_value> sql_record;
+} // namespace dbiplus
+
+#include <set>
+#include <string>
+
+// return codes of Cleaning up the Database
+// numbers are strings from strings.po
+#define ERROR_OK 317
+#define ERROR_CANCEL 0
+#define ERROR_DATABASE 315
+#define ERROR_REORG_SONGS 319
+#define ERROR_REORG_ARTIST 321
+#define ERROR_REORG_OTHER 323
+#define ERROR_REORG_PATH 325
+#define ERROR_REORG_ALBUM 327
+#define ERROR_WRITING_CHANGES 329
+#define ERROR_COMPRESSING 332
+
+#define NUM_SONGS_BEFORE_COMMIT 500
+
+/*!
+ \ingroup music
+ \brief A set of std::string objects, used for CMusicDatabase
+ \sa ISETPATHS, CMusicDatabase
+ */
+typedef std::set<std::string> SETPATHS;
+
+/*!
+ \ingroup music
+ \brief The SETPATHS iterator
+ \sa SETPATHS, CMusicDatabase
+ */
+typedef std::set<std::string>::iterator ISETPATHS;
+
+/*!
+\ingroup music
+\brief A structure used for fetching music art data
+\sa CMusicDatabase::GetArtForItem()
+*/
+
+typedef struct
+{
+ std::string mediaType;
+ std::string artType;
+ std::string prefix;
+ std::string url;
+} ArtForThumbLoader;
+
+class CGUIDialogProgress;
+class CFileItemList;
+
+/*!
+ \ingroup music
+ \brief Class to store and read tag information
+
+ CMusicDatabase can be used to read and store
+ tag information for faster access. It is based on
+ sqlite (http://www.sqlite.org).
+
+ Here is the database layout:
+ \image html musicdatabase.png
+
+ \sa CAlbum, CSong, VECSONGS, CMapSong, VECARTISTS, VECALBUMS, VECGENRES
+ */
+class CMusicDatabase : public CDatabase
+{
+ friend class DatabaseUtils;
+ friend class TestDatabaseUtilsHelper;
+
+public:
+ CMusicDatabase(void);
+ ~CMusicDatabase(void) override;
+
+ bool Open() override;
+ bool CommitTransaction() override;
+ void EmptyCache();
+ void Clean();
+ int Cleanup(CGUIDialogProgress* progressDialog = nullptr);
+ bool LookupCDDBInfo(bool bRequery = false);
+ void DeleteCDDBInfo();
+
+ /////////////////////////////////////////////////
+ // Song CRUD
+ /////////////////////////////////////////////////
+ /*! \brief Add a song to the database
+ \param idSong [in] the original database ID of the song to reuse (-1 when new)
+ \param dtDateNew [in] the datetime the original ID was new
+ \param idAlbum [in] the database ID of the album for the song
+ \param strTitle [in] the title of the song (required to be non-empty)
+ \param strMusicBrainzTrackID [in] the MusicBrainz track ID of the song
+ \param strPathAndFileName [in] the path and filename to the song
+ \param strComment [in] the ids of the added songs
+ \param strMood [in] the mood of the added song
+ \param strThumb [in] the ids of the added songs
+ \param artistDisp [in] the assembled artist name(s) display string
+ \param artistSort [in] the artist name(s) sort string
+ \param genres [in] a vector of genres to which this song belongs
+ \param iTrack [in] the track number and disc number of the song
+ \param iDuration [in] the duration of the song
+ \param strReleaseDate [in] the release date of the song ISO8601 format
+ \param strOrigReleaseDate [in] the original release date of the song ISO8601 format
+ \param strDiscSubtitle [in] subtitle of a disc
+ \param iTimesPlayed [in] the number of times the song has been played
+ \param iStartOffset [in] the start offset of the song (when using a single audio file with a .cue)
+ \param iEndOffset [in] the end offset of the song (when using a single audio file with .cue)
+ \param dtLastPlayed [in] the time the song was last played
+ \param rating [in] a rating for the song
+ \param userrating [in] a userrating (my rating) for the song
+ \param votes [in] a vote counter for the song rating
+ \param replayGain [in] album and track replaygain and peak values
+ \return the id of the song
+ */
+ int AddSong(const int idSong,
+ const CDateTime& dtDateNew,
+ const int idAlbum,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ std::string& strDiscSubtitle,
+ const int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels,
+ const ReplayGain& replayGain);
+ bool GetSong(int idSong, CSong& song);
+
+ /*! \brief Update a song and all its nested entities (genres, artists, contributors)
+ \param song [in/out] the song to update, artist ids are returned in artist credits
+ \param bArtists to update artist credits and contributors, default is true
+ \param bArtists to check and log if artist links have changed, default is true
+ \return true if successful
+ */
+ bool UpdateSong(CSong& song, bool bArtists = true, bool bArtistLinks = true);
+
+ /*! \brief Update a song in the database
+ \param idSong [in] the database ID of the song to update
+ \param strTitle [in] the title of the song (required to be non-empty)
+ \param strMusicBrainzTrackID [in] the MusicBrainz track ID of the song
+ \param strPathAndFileName [in] the path and filename to the song
+ \param strComment [in] the ids of the added songs
+ \param strMood [in] the mood of the added song
+ \param strThumb [in] the ids of the added songs
+ \param artistDisp [in] the artist name(s) display string
+ \param artistSort [in] the artist name(s) sort string
+ \param genres [in] a vector of genres to which this song belongs
+ \param iTrack [in] the track number and disc number of the song
+ \param iDuration [in] the duration of the song
+ \param strReleaseDate [in] the release date of the song ISO8601 format
+ \param strOrigReleaseDate [in] the original release date of the song ISO8601 format
+ \param strDiscSubtitle [in] subtitle of a disc
+ \param iTimesPlayed [in] the number of times the song has been played
+ \param iStartOffset [in] the start offset of the song (when using a single audio file with a .cue)
+ \param iEndOffset [in] the end offset of the song (when using a single audio file with .cue)
+ \param dtLastPlayed [in] the time the song was last played
+ \param rating [in] a rating for the song
+ \param userrating [in] a userrating (my rating) for the song
+ \param votes [in] a vote counter for the song rating
+ \param replayGain [in] album and track replaygain and peak values
+ \param iBPM [in] the beats per minute of a song
+ \param iBitRate [in] the bitrate of the song file
+ \param iSampleRate [in] the sample rate of the song file
+ \param iChannels [in] the number of audio channels in the song file
+ \return the id of the song
+ */
+ int UpdateSong(int idSong,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ const std::string& strDiscSubtitle,
+ int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ const ReplayGain& replayGain,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels);
+
+ //// Misc Song
+ bool GetSongByFileName(const std::string& strFileName, CSong& song, int64_t startOffset = 0);
+ bool GetSongsByPath(const std::string& strPath, MAPSONGS& songmap, bool bAppendToMap = false);
+ bool Search(const std::string& search, CFileItemList& items);
+ bool RemoveSongsFromPath(const std::string& path, MAPSONGS& songmap, bool exact = true);
+ void CheckArtistLinksChanged();
+ bool SetSongUserrating(const std::string& filePath, int userrating);
+ bool SetSongUserrating(int idSong, int userrating);
+ bool SetSongVotes(const std::string& filePath, int votes);
+ int GetSongByArtistAndAlbumAndTitle(const std::string& strArtist,
+ const std::string& strAlbum,
+ const std::string& strTitle);
+
+ /////////////////////////////////////////////////
+ // Album
+ /////////////////////////////////////////////////
+ /*! \brief Add an album and all its songs to the database
+ \param album the album to add
+ \param idSource the music source id
+ \return the id of the album
+ */
+ bool AddAlbum(CAlbum& album, int idSource);
+
+ /*! \brief Update an album and all its nested entities (artists, songs etc)
+ \param album the album to update
+ \return true or false
+ */
+ bool UpdateAlbum(CAlbum& album);
+
+ /*! \brief Add an album to the database
+ \param strAlbum the album title
+ \param strMusicBrainzAlbumID the Musicbrainz Id
+ \param strArtist the album artist name(s) display string
+ \param strArtistSort the album artist name(s) sort string
+ \param strGenre the album genre(s)
+ \param strReleaseDate [in] the release date of the album ISO8601 format
+ \param strOrigReleaseDate [in] the original release date of the album ISO8601 format
+ \param bBoxedSet if the album is a boxset
+ \param strRecordLabel the recording label
+ \param strType album type (Musicbrainz release type e.g. "Broadcast, Soundtrack, live"),
+ \param strReleaseStatus (see https://musicbrainz.org/doc/Release#Status)
+ \param bCompilation if the album is a compilation
+ \param releaseType "album" or "single"
+ \return the id of the album
+ */
+ int AddAlbum(const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ const std::string& strRecordLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType);
+
+ /*! \brief retrieve an album, optionally with all songs.
+ \param idAlbum the database id of the album.
+ \param album [out] the album to fill.
+ \param getSongs whether or not to retrieve songs, defaults to true.
+ \return true if the album is retrieved, false otherwise.
+ */
+ bool GetAlbum(int idAlbum, CAlbum& album, bool getSongs = true);
+ int UpdateAlbum(int idAlbum,
+ const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strThemes,
+ const std::string& strReview,
+ const std::string& strImage,
+ const std::string& strLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ float fRating,
+ int iUserrating,
+ int iVotes,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType,
+ bool bScrapedMBID);
+ bool ClearAlbumLastScrapedTime(int idAlbum);
+ bool HasAlbumBeenScraped(int idAlbum);
+
+ /////////////////////////////////////////////////
+ // Audiobook
+ /////////////////////////////////////////////////
+ bool AddAudioBook(const CFileItem& item);
+ bool SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark);
+ bool GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark);
+
+ /*! \brief Checks if the given path is inside a folder that has already been scanned into the library
+ \param path the path we want to check
+ */
+ bool InsideScannedPath(const std::string& path);
+
+ //// Misc Album
+ int GetAlbumIdByPath(const std::string& path);
+ bool GetAlbumFromSong(int idSong, CAlbum& album);
+ int GetAlbumByName(const std::string& strAlbum, const std::string& strArtist = "");
+ int GetAlbumByName(const std::string& strAlbum, const std::vector<std::string>& artist);
+ bool GetMatchingMusicVideoAlbum(const std::string& strAlbum,
+ const std::string& strArtist,
+ int& idAlbum,
+ std::string& strReview);
+ bool SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items);
+ int GetAlbumByMatch(const CAlbum& album);
+ std::string GetAlbumById(int id);
+ std::string GetAlbumDiscTitle(int idAlbum, int idDisc);
+ bool SetAlbumUserrating(const int idAlbum, int userrating);
+ int GetAlbumDiscsCount(int idAlbum);
+
+ /////////////////////////////////////////////////
+ // Artist CRUD
+ /////////////////////////////////////////////////
+ bool UpdateArtist(const CArtist& artist);
+
+ int AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ const std::string& strSortName,
+ bool bScrapedMBID = false);
+ int AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ bool bScrapedMBID = false);
+ bool GetArtist(int idArtist, CArtist& artist, bool fetchAll = false);
+ bool GetArtistExists(int idArtist);
+ int GetLastArtist();
+ int GetArtistFromMBID(const std::string& strMusicBrainzArtistID, std::string& artistname);
+ int UpdateArtist(int idArtist,
+ const std::string& strArtist,
+ const std::string& strSortName,
+ const std::string& strMusicBrainzArtistID,
+ bool bScrapedMBID,
+ const std::string& strType,
+ const std::string& strGender,
+ const std::string& strDisambiguation,
+ const std::string& strBorn,
+ const std::string& strFormed,
+ const std::string& strGenres,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strInstruments,
+ const std::string& strBiography,
+ const std::string& strDied,
+ const std::string& strDisbanded,
+ const std::string& strYearsActive,
+ const std::string& strImage);
+ bool UpdateArtistScrapedMBID(int idArtist, const std::string& strMusicBrainzArtistID);
+ bool GetTranslateBlankArtist() { return m_translateBlankArtist; }
+ void SetTranslateBlankArtist(bool translate) { m_translateBlankArtist = translate; }
+ bool HasArtistBeenScraped(int idArtist);
+ bool ClearArtistLastScrapedTime(int idArtist);
+ int AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum);
+ bool DeleteArtistDiscography(int idArtist);
+ bool GetArtistDiscography(int idArtist, CFileItemList& items);
+
+ std::string GetArtistById(int id);
+ int GetArtistByName(const std::string& strArtist);
+ int GetArtistByMatch(const CArtist& artist);
+ bool GetArtistFromSong(int idSong, CArtist& artist);
+ bool IsSongArtist(int idSong, int idArtist);
+ bool IsSongAlbumArtist(int idSong, int idArtist);
+ std::string GetRoleById(int id);
+
+ /*! \brief Propagate artist sort name into the concatenated artist sort name strings
+ held for songs and albums
+ \param int idArtist to propagate sort name for, -1 means all artists
+ */
+ bool UpdateArtistSortNames(int idArtist = -1);
+
+ /////////////////////////////////////////////////
+ // Paths
+ /////////////////////////////////////////////////
+ int AddPath(const std::string& strPath);
+
+ bool GetPaths(std::set<std::string>& paths);
+ bool SetPathHash(const std::string& path, const std::string& hash);
+ bool GetPathHash(const std::string& path, std::string& hash);
+ bool GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths);
+ bool GetAlbumPath(int idAlbum, std::string& basePath);
+ int GetDiscnumberForPathID(int idPath);
+ bool GetOldArtistPath(int idArtist, std::string& path);
+ bool GetArtistPath(const CArtist& artist, std::string& path);
+ bool GetAlbumFolder(const CAlbum& album, const std::string& strAlbumPath, std::string& strFolder);
+ bool GetArtistFolderName(const CArtist& artist, std::string& strFolder);
+ bool GetArtistFolderName(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ std::string& strFolder);
+
+ /////////////////////////////////////////////////
+ // Sources
+ /////////////////////////////////////////////////
+ bool UpdateSources();
+ int AddSource(const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths,
+ int id = -1);
+ int UpdateSource(const std::string& strOldName,
+ const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths);
+ bool RemoveSource(const std::string& strName);
+ int GetSourceFromPath(const std::string& strPath);
+ bool AddAlbumSource(int idAlbum, int idSource);
+ bool AddAlbumSources(int idAlbum, const std::string& strPath);
+ bool DeleteAlbumSources(int idAlbum);
+ bool GetSources(CFileItemList& items);
+
+ bool GetSourcesByArtist(int idArtist, CFileItem* item);
+ bool GetSourcesByAlbum(int idAlbum, CFileItem* item);
+ bool GetSourcesBySong(int idSong, const std::string& strPath, CFileItem* item);
+ int GetSourceByName(const std::string& strSource);
+ std::string GetSourceById(int id);
+
+ /////////////////////////////////////////////////
+ // Genres
+ /////////////////////////////////////////////////
+ int AddGenre(std::string& strGenre);
+ std::string GetGenreById(int id);
+ int GetGenreByName(const std::string& strGenre);
+
+ /////////////////////////////////////////////////
+ // Link tables
+ /////////////////////////////////////////////////
+ bool AddAlbumArtist(int idArtist, int idAlbum, const std::string& strArtist, int iOrder);
+ bool GetAlbumsByArtist(int idArtist, std::vector<int>& albums);
+ bool GetArtistsByAlbum(int idAlbum, CFileItem* item);
+ bool GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs);
+ bool DeleteAlbumArtistsByAlbum(int idAlbum);
+
+ int AddRole(const std::string& strRole);
+ bool AddSongArtist(int idArtist,
+ int idSong,
+ const std::string& strRole,
+ const std::string& strArtist,
+ int iOrder);
+ bool AddSongArtist(
+ int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder);
+ int AddSongContributor(int idSong,
+ const std::string& strRole,
+ const std::string& strArtist,
+ const std::string& strSort);
+ void AddSongContributors(int idSong,
+ const VECMUSICROLES& contributors,
+ const std::string& strSort);
+ int GetRoleByName(const std::string& strRole);
+ bool GetRolesByArtist(int idArtist, CFileItem* item);
+ bool GetSongsByArtist(int idArtist, std::vector<int>& songs);
+ bool GetArtistsBySong(int idSong, std::vector<int>& artists);
+ bool DeleteSongArtistsBySong(int idSong);
+
+ bool AddSongGenres(int idSong, const std::vector<std::string>& genres);
+ bool GetGenresBySong(int idSong, std::vector<int>& genres);
+
+ bool GetGenresByAlbum(int idAlbum, CFileItem* item);
+
+ bool GetGenresByArtist(int idArtist, CFileItem* item);
+ bool GetIsAlbumArtist(int idArtist, CFileItem* item);
+
+ /////////////////////////////////////////////////
+ // Top 100
+ /////////////////////////////////////////////////
+ bool GetTop100(const std::string& strBaseDir, CFileItemList& items);
+ bool GetTop100Albums(VECALBUMS& albums);
+ bool GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& item);
+
+ /////////////////////////////////////////////////
+ // Recently added
+ /////////////////////////////////////////////////
+ bool GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit = 0);
+ bool GetRecentlyAddedAlbumSongs(const std::string& strBaseDir,
+ CFileItemList& item,
+ unsigned int limit = 0);
+ bool GetRecentlyPlayedAlbums(VECALBUMS& albums);
+ bool GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir, CFileItemList& item);
+
+ /////////////////////////////////////////////////
+ // Compilations
+ /////////////////////////////////////////////////
+ int GetCompilationAlbumsCount();
+
+ ////////////////////////////////////////////////
+ // Boxsets
+ ////////////////////////////////////////////////
+ bool IsAlbumBoxset(int idAlbum);
+ int GetBoxsetsCount();
+
+ int GetSinglesCount();
+
+ int GetArtistCountForRole(int role);
+ int GetArtistCountForRole(const std::string& strRole);
+
+ /*! \brief Increment the playcount of an item
+ Increments the playcount and updates the last played date
+ \param item CFileItem to increment the playcount for
+ */
+ void IncrementPlayCount(const CFileItem& item);
+ bool CleanupOrphanedItems();
+
+ /////////////////////////////////////////////////
+ // VIEWS
+ /////////////////////////////////////////////////
+ bool GetGenresNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetSourcesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetYearsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter());
+ bool GetRolesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter());
+ bool GetArtistsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ bool albumArtistsOnly = false,
+ int idGenre = -1,
+ int idAlbum = -1,
+ int idSong = -1,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetCommonNav(const std::string& strBaseDir,
+ const std::string& table,
+ const std::string& labelField,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */);
+ bool GetAlbumTypesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetMusicLabelsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetAlbumsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre = -1,
+ int idArtist = -1,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetDiscsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idAlbum,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year);
+ bool GetSongsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre,
+ int idArtist,
+ int idAlbum,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year);
+ bool GetSongsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetSongsFullByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool artistData = false);
+ bool GetAlbumsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetDiscsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetDiscsByWhere(CMusicDbUrl& musicUrl,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetArtistsByWhere(const std::string& strBaseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ int GetDiscsCount(const std::string& baseDir, const Filter& filter = Filter());
+ int GetSongsCount(const Filter& filter = Filter());
+ bool GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting) override;
+ int GetOrderFilter(const std::string& type, const SortDescription& sorting, Filter& filter);
+
+ /////////////////////////////////////////////////
+ // Party Mode
+ /////////////////////////////////////////////////
+ /*! \brief Gets song IDs in random order that match the filter criteria
+ \param filter the criteria to apply in the query
+ \param songIDs a vector of <1, id> pairs suited to party mode use
+ \return count of song ids found.
+ */
+ unsigned int GetRandomSongIDs(const Filter& filter, std::vector<std::pair<int, int>>& songIDs);
+
+ /////////////////////////////////////////////////
+ // JSON-RPC
+ /////////////////////////////////////////////////
+ bool GetGenresJSON(CFileItemList& items, bool bSources = false);
+ bool GetArtistsByWhereJSON(const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetAlbumsByWhereJSON(const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetSongsByWhereJSON(const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription = SortDescription());
+
+ /////////////////////////////////////////////////
+ // Scraper
+ /////////////////////////////////////////////////
+ bool SetScraper(int id, const CONTENT_TYPE& content, const ADDON::ScraperPtr& scraper);
+ bool SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper);
+ bool GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper);
+
+ /*! \brief Check whether a given scraper is in use.
+ \param scraperID the scraper to check for.
+ \return true if the scraper is in use, false otherwise.
+ */
+ bool ScraperInUse(const std::string& scraperID) const;
+
+ /////////////////////////////////////////////////
+ // Filters
+ /////////////////////////////////////////////////
+ bool GetItems(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription());
+ bool GetItems(const std::string& strBaseDir,
+ const std::string& itemType,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription());
+ std::string GetItemById(const std::string& itemType, int id);
+
+ /////////////////////////////////////////////////
+ // XML
+ /////////////////////////////////////////////////
+ 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
+ /////////////////////////////////////////////////
+ void SetPropertiesForFileItem(CFileItem& item);
+ static void SetPropertiesFromArtist(CFileItem& item, const CArtist& artist);
+ static void SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album);
+ void SetItemUpdated(int mediaId, const std::string& mediaType);
+
+ /////////////////////////////////////////////////
+ // Art
+ /////////////////////////////////////////////////
+ /*! \brief Sets art for a database item.
+ Sets a single piece of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artType the type of art to set, e.g. "thumb", "fanart"
+ \param url the url to the art (this is the original url, not a cached url).
+ \sa GetArtForItem
+ */
+ void SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::string& artType,
+ const std::string& url);
+
+ /*! \brief Sets art for a database item.
+ Sets multiple pieces of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param art a map of <type, url> where type is "thumb", "fanart", etc. and url is the original url of the art.
+ \sa GetArtForItem
+ */
+ void SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::map<std::string, std::string>& art);
+
+
+ /*! \brief Fetch all related art for a database item.
+ Fetches multiple pieces of art for a database item including that for related media types
+ Given song id art for the related album, artist(s) and albumartist(s) will also be fetched, looking up the
+ album and artist when ids are not provided.
+ Given album id (and not song id) art for the related artist(s) will also be fetched, looking up the
+ artist(s) when id are not provided.
+ \param songId the id in the song table, -1 when song art not being fetched
+ \param albumId the id in the album table, -1 when album art not being fetched
+ \param artistId the id in the artist table, -1 when artist not known
+ \param bPrimaryArtist true if art from only the first song artist or album artist is to be fetched
+ \param art [out] a vector, each element having media type e.g. "artist", "album" or "song",
+ artType e.g. "thumb", "fanart", etc., prefix of "", "artist" or "albumartist" etc. giving the kind of artist
+ relationship, and the original url of the art.
+
+ \return true if art is retrieved, false if no art is found.
+ \sa SetArtForItem
+ */
+ bool GetArtForItem(int songId,
+ int albumId,
+ int artistId,
+ bool bPrimaryArtist,
+ std::vector<ArtForThumbLoader>& art);
+
+ /*! \brief Fetch art for a database item.
+ Fetches multiple pieces of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param art [out] a map of <type, url> where type is "thumb", "fanart", etc. and url is the original url of the art.
+ \return true if art is retrieved, false if no art is found.
+ \sa SetArtForItem
+ */
+ bool GetArtForItem(int mediaId,
+ const std::string& mediaType,
+ std::map<std::string, std::string>& art);
+
+ /*! \brief Fetch art for a database item.
+ Fetches a single piece of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artType the type of art to retrieve, eg "thumb", "fanart".
+ \return the original URL to the piece of art, if available.
+ \sa SetArtForItem
+ */
+ std::string GetArtForItem(int mediaId, const std::string& mediaType, const std::string& artType);
+
+ /*! \brief Remove art for a database item.
+ Removes a single piece of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artType the type of art to remove, eg "thumb", "fanart".
+ \return true if art is removed, false if no art is found.
+ \sa RemoveArtForItem
+ */
+ bool RemoveArtForItem(int mediaId, const MediaType& mediaType, const std::string& artType);
+
+ /*! \brief Remove art for a database item.
+ Removes multiple pieces of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param arttypes a set of types, e.g. "thumb", "fanart", etc. to be removed.
+ \return true if art is removed, false if no art is found.
+ \sa RemoveArtForItem
+ */
+ bool RemoveArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::set<std::string>& artTypes);
+
+ /*! \brief Fetch the distinct types of art held in the database for a type of media.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artTypes [out] the types of art e.g. "thumb", "fanart", etc.
+ \return true if art is found, false if no art is found.
+ */
+ bool GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes);
+
+ /*! \brief Fetch the distinct types of available-but-unassigned art held in the
+ database for a specific media item.
+ \param mediaId the id in the media (artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (artist/album).
+ \return the types of art e.g. "thumb", "fanart", etc.
+ */
+ std::vector<std::string> GetAvailableArtTypesForItem(int mediaId, const MediaType& mediaType);
+
+ /*! \brief Fetch the list of available-but-unassigned art URLs held in the
+ database for a specific media item and art type.
+ \param mediaId the id in the media (artist/album) table.
+ \param mediaType corresponds to the table the item resides in (artist/album).
+ \param artType e.g. "thumb", "fanart", etc.
+ \return list of URLs
+ */
+ std::vector<CScraperUrl::SUrlEntry> GetAvailableArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::string& artType);
+
+ /////////////////////////////////////////////////
+ // Tag Scan Version
+ /////////////////////////////////////////////////
+ /*! \brief Check if music files need all tags rescanning regardless of file being unchanged
+ because the tag processing has changed (which may happen without db version changes) since they
+ where last scanned.
+ \return -1 if an error occurred, 0 if no scan is needed, or the version number of tags if not the same as current.
+ */
+ virtual int GetMusicNeedsTagScan();
+
+ /*! \brief Set minimum version number of db needed when tag data scanned from music files
+ \param version the version number of db
+ */
+ void SetMusicNeedsTagScan(int version);
+
+ /*! \brief Set the version number of tag data
+ \param version the version number of db when tags last scanned, 0 (default) means current db version
+ */
+ void SetMusicTagScanVersion(int version = 0);
+
+ std::string GetLibraryLastUpdated();
+ void SetLibraryLastUpdated();
+ std::string GetLibraryLastCleaned();
+ void SetLibraryLastCleaned();
+ std::string GetArtistLinksUpdated();
+ void SetArtistLinksUpdated();
+ std::string GetGenresLastAdded();
+ std::string GetSongsLastAdded();
+ std::string GetAlbumsLastAdded();
+ std::string GetArtistsLastAdded();
+ std::string GetSongsLastModified();
+ std::string GetAlbumsLastModified();
+ std::string GetArtistsLastModified();
+
+
+protected:
+ std::map<std::string, int> m_genreCache;
+ std::map<std::string, int> m_pathCache;
+
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ int GetMinSchemaVersion() const override { return 32; }
+ int GetSchemaVersion() const override;
+
+ const char* GetBaseDBName() const override { return "MyMusic"; }
+
+private:
+ /*! \brief (Re)Create the generic database views for songs and albums
+ */
+ virtual void CreateViews();
+ void CreateNativeDBFunctions();
+ void CreateRemovedLinkTriggers();
+
+ void SplitPath(const std::string& strFileNameAndPath,
+ std::string& strPath,
+ std::string& strFileName);
+
+ CSong GetSongFromDataset();
+ CSong GetSongFromDataset(const dbiplus::sql_record* const record, int offset = 0);
+ CArtist GetArtistFromDataset(dbiplus::Dataset* pDS, int offset = 0, bool needThumb = true);
+ CArtist GetArtistFromDataset(const dbiplus::sql_record* const record,
+ int offset = 0,
+ bool needThumb = true);
+ CAlbum GetAlbumFromDataset(dbiplus::Dataset* pDS, int offset = 0, bool imageURL = false);
+ CAlbum GetAlbumFromDataset(const dbiplus::sql_record* const record,
+ int offset = 0,
+ bool imageURL = false);
+ CArtistCredit GetArtistCreditFromDataset(const dbiplus::sql_record* const record, int offset = 0);
+ CMusicRole GetArtistRoleFromDataset(const dbiplus::sql_record* const record, int offset = 0);
+ std::string GetMediaDateFromFile(const std::string& strFileNameAndPath);
+ void GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl);
+ void GetFileItemFromDataset(const dbiplus::sql_record* const record,
+ CFileItem* item,
+ const CMusicDbUrl& baseUrl);
+ void GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item);
+
+ bool DeleteRemovedLinks();
+
+ bool CleanupSongs(CGUIDialogProgress* progressDialog = nullptr);
+ bool CleanupSongsByIds(const std::string& strSongIds);
+ bool CleanupPaths();
+ bool CleanupAlbums();
+ bool CleanupArtists();
+ bool CleanupGenres();
+ bool CleanupInfoSettings();
+ bool CleanupRoles();
+ void UpdateTables(int version) override;
+ bool SearchArtists(const std::string& search, CFileItemList& artists);
+ bool SearchAlbums(const std::string& search, CFileItemList& albums);
+ bool SearchSongs(const std::string& strSearch, CFileItemList& songs);
+ int GetSongIDFromPath(const std::string& filePath);
+ void NormaliseSongDates(std::string& strRelease, std::string& strOriginal);
+ bool TrimImageURLs(std::string& strImage, const size_t space);
+
+ /*! \brief Build SQL for sort subquery from ignore article token list
+ \param strField original name or title field that articles could be removed from
+ \return SQL string e.g. WHEN strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
+ */
+ std::string GetIgnoreArticleSQL(const std::string& strField);
+
+ /*! \brief Build SQL for sort name scalar subquery from sort attributes and ignore article list.
+ \param strAlias alias name of scalar subquery field
+ \param sortAttributes the sort attributes e.g. SortAttributeIgnoreArticle
+ \param strField original name or title field that articles could be removed from
+ \param strSortField sort name or title field to be used instead of original (when data not null)
+ \return SQL string e.g.
+ CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
+ WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
+ ELSE strField
+ END AS strAlias
+ */
+ std::string SortnameBuildSQL(const std::string& strAlias,
+ const SortAttribute& sortAttributes,
+ const std::string& strField,
+ const std::string& strSortField);
+
+ /*! \brief Build SQL for sorting field naturally and case-insensitively (in SQLite).
+ \param strField field name
+ \param sortOrder the sort order
+ \return SQL string e.g.
+ CASE WHEN CAST(strTitle AS INTEGER) = 0 THEN 100000000
+ ELSE CAST(strTitle AS INTEGER) END DESC, strTitle COLLATE NOCASE DESC
+ */
+ std::string AlphanumericSortSQL(const std::string& strField, const SortOrder& sortOrder);
+
+ /*! \brief Checks that source table matches sources.xml
+ returns true when they do
+ */
+ bool CheckSources(VECSOURCES& sources);
+
+ /*! \brief Initially fills source table from sources.xml for use only at
+ migration of db from an earlier version than 72
+ returns true when successfully done
+ */
+ bool MigrateSources();
+
+ bool m_translateBlankArtist;
+
+ // Fields should be ordered as they
+ // appear in the songview
+ static enum _SongFields {
+ song_idSong = 0,
+ song_strArtists,
+ song_strArtistSort,
+ song_strGenres,
+ song_strTitle,
+ song_iTrack,
+ song_iDuration,
+ song_strReleaseDate,
+ song_strOrigReleaseDate,
+ song_strDiscSubtitle,
+ song_strFileName,
+ song_strMusicBrainzTrackID,
+ song_iTimesPlayed,
+ song_iStartOffset,
+ song_iEndOffset,
+ song_lastplayed,
+ song_rating,
+ song_userrating,
+ song_votes,
+ song_comment,
+ song_idAlbum,
+ song_strAlbum,
+ song_strPath,
+ song_strReleaseStatus,
+ song_bCompilation,
+ song_bBoxedSet,
+ song_strAlbumArtists,
+ song_strAlbumArtistSort,
+ song_strAlbumReleaseType,
+ song_mood,
+ song_strReplayGain,
+ song_iBPM,
+ song_iBitRate,
+ song_iSampleRate,
+ song_iChannels,
+ song_iAlbumDuration,
+ song_iDiscTotal,
+ song_dateAdded,
+ song_dateNew,
+ song_dateModified,
+ song_enumCount // end of the enum, do not add past here
+ } SongFields;
+
+ // Fields should be ordered as they
+ // appear in the albumview
+ static enum _AlbumFields {
+ album_idAlbum = 0,
+ album_strAlbum,
+ album_strMusicBrainzAlbumID,
+ album_strReleaseGroupMBID,
+ album_strArtists,
+ album_strArtistSort,
+ album_strGenres,
+ album_strReleaseDate,
+ album_strOrigReleaseDate,
+ album_bBoxedSet,
+ album_strMoods,
+ album_strStyles,
+ album_strThemes,
+ album_strReview,
+ album_strLabel,
+ album_strType,
+ album_strReleaseStatus,
+ album_strThumbURL,
+ album_fRating,
+ album_iUserrating,
+ album_iVotes,
+ album_bCompilation,
+ album_bScrapedMBID,
+ album_lastScraped,
+ album_dateAdded,
+ album_dateNew,
+ album_dateModified,
+ album_iTimesPlayed,
+ album_strReleaseType,
+ album_iTotalDiscs,
+ album_dtLastPlayed,
+ album_iAlbumDuration,
+ album_enumCount // end of the enum, do not add past here
+ } AlbumFields;
+
+ // Fields should be ordered as they
+ // appear in the songartistview/albumartistview
+ static enum _ArtistCreditFields {
+ // used for GetAlbum to get the cascaded album/song artist credits
+ artistCredit_idEntity = 0, // can be idSong or idAlbum depending on context
+ artistCredit_idArtist,
+ artistCredit_idRole,
+ artistCredit_strRole,
+ artistCredit_strArtist,
+ artistCredit_strSortName,
+ artistCredit_strMusicBrainzArtistID,
+ artistCredit_iOrder,
+ artistCredit_enumCount
+ } ArtistCreditFields;
+
+ // Fields should be ordered as they
+ // appear in the artistview
+ static enum _ArtistFields {
+ artist_idArtist = 0,
+ artist_strArtist,
+ artist_strSortName,
+ artist_strMusicBrainzArtistID,
+ artist_strType,
+ artist_strGender,
+ artist_strDisambiguation,
+ artist_strBorn,
+ artist_strFormed,
+ artist_strGenres,
+ artist_strMoods,
+ artist_strStyles,
+ artist_strInstruments,
+ artist_strBiography,
+ artist_strDied,
+ artist_strDisbanded,
+ artist_strYearsActive,
+ artist_strImage,
+ artist_bScrapedMBID,
+ artist_lastScraped,
+ artist_dateAdded,
+ artist_dateNew,
+ artist_dateModified,
+ artist_enumCount // end of the enum, do not add past here
+ } ArtistFields;
+
+ // Fields fetched by GetArtistsByWhereJSON, order same as in JSONtoDBArtist
+ static enum _JoinToArtistFields {
+ joinToArtist_isSong = 0,
+ joinToArtist_idSourceAlbum,
+ joinToArtist_idSourceSong,
+ joinToArtist_idSongGenreAlbum,
+ joinToArtist_idSongGenreSong,
+ joinToArtist_strSongGenreAlbum,
+ joinToArtist_strSongGenreSong,
+ joinToArtist_idArt,
+ joinToArtist_artType,
+ joinToArtist_artURL,
+ joinToArtist_idRole,
+ joinToArtist_strRole,
+ joinToArtist_iOrderRole,
+ joinToArtist_isalbumartist,
+ joinToArtist_thumbnail,
+ joinToArtist_fanart,
+ joinToArtist_enumCount // end of the enum, do not add past here
+ } JoinToArtistFields;
+
+ // Fields fetched by GetAlbumsByWhereJSON, order same as in JSONtoDBAlbum
+ static enum _JoinToAlbumFields {
+ joinToAlbum_idArtist = 0,
+ joinToAlbum_strArtist,
+ joinToAlbum_strArtistMBID,
+ joinToAlbum_enumCount // end of the enum, do not add past here
+ } JoinToAlbumFields;
+
+ // Fields fetched by GetSongsByWhereJSON, order same as in JSONtoDBSong
+ static enum _JoinToSongFields {
+ // Used by GetSongsByWhereJSON
+ joinToSongs_idAlbumArtist = 0,
+ joinToSongs_strAlbumArtist,
+ joinToSongs_strAlbumArtistMBID,
+ joinToSongs_iOrderAlbumArtist,
+ joinToSongs_idArtist,
+ joinToSongs_strArtist,
+ joinToSongs_strArtistMBID,
+ joinToSongs_iOrderArtist,
+ joinToSongs_idRole,
+ joinToSongs_strRole,
+ joinToSongs_iOrderRole,
+ joinToSongs_idGenre,
+ joinToSongs_iOrderGenre,
+ joinToSongs_enumCount // end of the enum, do not add past here
+ } JoinToSongFields;
+};
diff --git a/xbmc/music/MusicDbUrl.cpp b/xbmc/music/MusicDbUrl.cpp
new file mode 100644
index 0000000..3b4893b
--- /dev/null
+++ b/xbmc/music/MusicDbUrl.cpp
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012-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 "MusicDbUrl.h"
+
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "playlists/SmartPlayList.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace XFILE;
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CMusicDbUrl::CMusicDbUrl()
+ : CDbUrl()
+{ }
+
+CMusicDbUrl::~CMusicDbUrl() = default;
+
+bool CMusicDbUrl::parse()
+{
+ // the URL must start with musicdb://
+ if (!m_url.IsProtocol("musicdb") || m_url.GetFileName().empty())
+ return false;
+
+ std::string path = m_url.Get();
+
+ // Parse path for directory node types and query params
+ NODE_TYPE dirType;
+ NODE_TYPE childType;
+ CQueryParams queryParams;
+ if (!CMusicDatabaseDirectory::GetDirectoryNodeInfo(path, dirType, childType, queryParams))
+ return false;
+
+ switch (dirType)
+ {
+ case NODE_TYPE_ARTIST:
+ m_type = "artists";
+ break;
+
+ case NODE_TYPE_ALBUM:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_TOP100:
+ m_type = "albums";
+ break;
+
+ case NODE_TYPE_DISC:
+ m_type = "discs";
+ break;
+
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG:
+ case NODE_TYPE_SONG_TOP100:
+ case NODE_TYPE_SINGLES:
+ m_type = "songs";
+ break;
+
+ default:
+ break;
+ }
+
+ switch (childType)
+ {
+ case NODE_TYPE_ARTIST:
+ m_type = "artists";
+ break;
+
+ case NODE_TYPE_ALBUM:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_TOP100:
+ m_type = "albums";
+ break;
+
+ case NODE_TYPE_DISC:
+ m_type = "discs";
+ break;
+
+ case NODE_TYPE_SONG:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG_TOP100:
+ case NODE_TYPE_SINGLES:
+ m_type = "songs";
+ break;
+
+ case NODE_TYPE_GENRE:
+ m_type = "genres";
+ break;
+
+ case NODE_TYPE_SOURCE:
+ m_type = "sources";
+ break;
+
+ case NODE_TYPE_ROLE:
+ m_type = "roles";
+ break;
+
+ case NODE_TYPE_YEAR:
+ m_type = "years";
+ break;
+
+ case NODE_TYPE_TOP100:
+ m_type = "top100";
+ break;
+
+ case NODE_TYPE_ROOT:
+ case NODE_TYPE_OVERVIEW:
+ default:
+ return false;
+ }
+
+ if (m_type.empty())
+ return false;
+
+ // retrieve and parse all options
+ AddOptions(m_url.GetOptions());
+
+ // add options based on the node type
+ if (dirType == NODE_TYPE_SINGLES || childType == NODE_TYPE_SINGLES)
+ AddOption("singles", true);
+
+ // add options based on the QueryParams
+ if (queryParams.GetArtistId() != -1)
+ AddOption("artistid", (int)queryParams.GetArtistId());
+ if (queryParams.GetAlbumId() != -1)
+ AddOption("albumid", (int)queryParams.GetAlbumId());
+ if (queryParams.GetGenreId() != -1)
+ AddOption("genreid", (int)queryParams.GetGenreId());
+ if (queryParams.GetSongId() != -1)
+ AddOption("songid", (int)queryParams.GetSongId());
+ if (queryParams.GetYear() != -1)
+ AddOption("year", (int)queryParams.GetYear());
+
+ // Decode legacy use of "musicdb://compilations/" path for filtered albums
+ if (m_url.GetFileName() == "compilations/")
+ AddOption("compilation", true);
+
+ return true;
+}
+
+bool CMusicDbUrl::validateOption(const std::string &key, const CVariant &value)
+{
+ if (!CDbUrl::validateOption(key, value))
+ return false;
+
+ // if the value is empty it will remove the option which is ok
+ // otherwise we only care about the "filter" option here
+ if (value.empty() || !StringUtils::EqualsNoCase(key, "filter"))
+ return true;
+
+ if (!value.isString())
+ return false;
+
+ CSmartPlaylist xspFilter;
+ if (!xspFilter.LoadFromJson(value.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ return xspFilter.GetType() == m_type;
+}
diff --git a/xbmc/music/MusicDbUrl.h b/xbmc/music/MusicDbUrl.h
new file mode 100644
index 0000000..06f3efe
--- /dev/null
+++ b/xbmc/music/MusicDbUrl.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012-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 "DbUrl.h"
+
+#include <string>
+
+class CVariant;
+
+class CMusicDbUrl : public CDbUrl
+{
+public:
+ CMusicDbUrl();
+ ~CMusicDbUrl() override;
+
+protected:
+ bool parse() override;
+ bool validateOption(const std::string &key, const CVariant &value) override;
+};
diff --git a/xbmc/music/MusicInfoLoader.cpp b/xbmc/music/MusicInfoLoader.cpp
new file mode 100644
index 0000000..2732bd4
--- /dev/null
+++ b/xbmc/music/MusicInfoLoader.cpp
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoLoader.h"
+
+#include "Album.h"
+#include "Artist.h"
+#include "FileItem.h"
+#include "MusicDatabase.h"
+#include "MusicThumbLoader.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "filesystem/MusicDatabaseDirectory/QueryParams.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+using namespace MUSIC_INFO;
+
+// HACK until we make this threadable - specify 1 thread only for now
+CMusicInfoLoader::CMusicInfoLoader()
+ : CBackgroundInfoLoader()
+ , m_databaseHits{0}
+ , m_tagReads{0}
+{
+ m_mapFileItems = new CFileItemList;
+
+ m_thumbLoader = new CMusicThumbLoader();
+}
+
+CMusicInfoLoader::~CMusicInfoLoader()
+{
+ StopThread();
+ delete m_mapFileItems;
+ delete m_thumbLoader;
+}
+
+void CMusicInfoLoader::OnLoaderStart()
+{
+ // Load previously cached items from HD
+ if (!m_strCacheFileName.empty())
+ LoadCache(m_strCacheFileName, *m_mapFileItems);
+ else
+ {
+ m_mapFileItems->SetPath(m_pVecItems->GetPath());
+ m_mapFileItems->Load();
+ m_mapFileItems->SetFastLookup(true);
+ }
+
+ m_strPrevPath.clear();
+
+ m_databaseHits = m_tagReads = 0;
+
+ if (m_pProgressCallback)
+ m_pProgressCallback->SetProgressMax(m_pVecItems->GetFileCount());
+
+ m_musicDatabase.Open();
+
+ if (m_thumbLoader)
+ m_thumbLoader->OnLoaderStart();
+}
+
+bool CMusicInfoLoader::LoadAdditionalTagInfo(CFileItem* pItem)
+{
+ if (!pItem || (pItem->m_bIsFolder && !pItem->IsAudio()) ||
+ pItem->IsPlayList() || pItem->IsNFO() || pItem->IsInternetStream())
+ return false;
+
+ if (pItem->GetProperty("hasfullmusictag") == "true")
+ return false; // already have the information
+
+ std::string path(pItem->GetPath());
+ // For songs in library set the (primary) song artist and album properties
+ // Use song Id (not path) as called for items from either library or file view,
+ // but could also be listitem with tag loaded by a script
+ if (pItem->HasMusicInfoTag() &&
+ pItem->GetMusicInfoTag()->GetType() == MediaTypeSong &&
+ pItem->GetMusicInfoTag()->GetDatabaseId() > 0)
+ {
+ CMusicDatabase database;
+ database.Open();
+ // May already have song artist ids as item property set when data read from
+ // db, but check property is valid array (scripts could set item properties
+ // incorrectly), otherwise fetch artist using song id.
+ CArtist artist;
+ bool artistfound = false;
+ if (pItem->HasProperty("artistid") && pItem->GetProperty("artistid").isArray())
+ {
+ CVariant::const_iterator_array varid = pItem->GetProperty("artistid").begin_array();
+ int idArtist = static_cast<int>(varid->asInteger());
+ artistfound = database.GetArtist(idArtist, artist, false);
+ }
+ else
+ artistfound = database.GetArtistFromSong(pItem->GetMusicInfoTag()->GetDatabaseId(), artist);
+ if (artistfound)
+ CMusicDatabase::SetPropertiesFromArtist(*pItem, artist);
+
+ // May already have album id, otherwise fetch album from song id
+ CAlbum album;
+ bool albumfound = false;
+ int idAlbum = pItem->GetMusicInfoTag()->GetAlbumId();
+ if (idAlbum > 0)
+ albumfound = database.GetAlbum(idAlbum, album, false);
+ else
+ albumfound = database.GetAlbumFromSong(pItem->GetMusicInfoTag()->GetDatabaseId(), album);
+ if (albumfound)
+ CMusicDatabase::SetPropertiesFromAlbum(*pItem, album);
+
+ path = pItem->GetMusicInfoTag()->GetURL();
+ }
+
+ CLog::Log(LOGDEBUG, "Loading additional tag info for file {}", path);
+
+ // we load up the actual tag for this file in order to
+ // fetch the lyrics and add it to the current music info tag
+ CFileItem tempItem(path, false);
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(tempItem));
+ if (nullptr != pLoader)
+ {
+ CMusicInfoTag tag;
+ pLoader->Load(path, tag);
+ pItem->GetMusicInfoTag()->SetLyrics(tag.GetLyrics());
+ pItem->SetProperty("hasfullmusictag", "true");
+ return true;
+ }
+ return false;
+}
+
+bool CMusicInfoLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CMusicInfoLoader::LoadItemCached(CFileItem* pItem)
+{
+ if ((pItem->m_bIsFolder && !pItem->IsAudio()) ||
+ pItem->IsPlayList() || pItem->IsSmartPlayList() ||
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newplaylist://") ||
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://") ||
+ pItem->IsNFO() || (pItem->IsInternetStream() && !pItem->IsMusicDb()))
+ return false;
+
+ // Get thumb for item
+ m_thumbLoader->LoadItem(pItem);
+
+ return true;
+}
+
+bool CMusicInfoLoader::LoadItemLookup(CFileItem* pItem)
+{
+ if (m_pProgressCallback && !pItem->m_bIsFolder)
+ m_pProgressCallback->SetProgressAdvance();
+
+ if ((pItem->m_bIsFolder && !pItem->IsAudio()) || //
+ pItem->IsPlayList() || pItem->IsSmartPlayList() || //
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newplaylist://") || //
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://") || //
+ pItem->IsNFO() || (pItem->IsInternetStream() && !pItem->IsMusicDb()))
+ return false;
+
+ if ((!pItem->HasMusicInfoTag() || !pItem->GetMusicInfoTag()->Loaded()) && pItem->IsAudio())
+ {
+ // first check the cached item
+ CFileItemPtr mapItem = (*m_mapFileItems)[pItem->GetPath()];
+ if (mapItem && mapItem->m_dateTime==pItem->m_dateTime && mapItem->HasMusicInfoTag() && mapItem->GetMusicInfoTag()->Loaded())
+ { // Query map if we previously cached the file on HD
+ *pItem->GetMusicInfoTag() = *mapItem->GetMusicInfoTag();
+ if (mapItem->HasArt("thumb"))
+ pItem->SetArt("thumb", mapItem->GetArt("thumb"));
+ }
+ else
+ {
+ std::string strPath = URIUtils::GetDirectory(pItem->GetPath());
+ URIUtils::AddSlashAtEnd(strPath);
+ if (strPath!=m_strPrevPath)
+ {
+ // The item is from another directory as the last one,
+ // query the database for the new directory...
+ m_musicDatabase.GetSongsByPath(strPath, m_songsMap);
+ m_databaseHits++;
+ }
+
+ /*
+ This only loads the item with the song from the database when it maps to a single song,
+ it can not load song data for items with cuesheets that expand to multiple songs.
+ For songs from embedded or separate cuesheets strFileName is not unique, so the song map for
+ the path will have the list of songs from that file. But items with cuesheets are expanded
+ (replacing each item with items for every track) elsewhere. When the item we are looking up
+ has a cuesheet document or is a music file with a cuesheet embedded in the tags, and it maps
+ to more than one song then we can not fill the tag data and thumb from the database.
+ */
+ MAPSONGS::iterator it = m_songsMap.find(pItem->GetPath()); // Find file in song map
+ if (it != m_songsMap.end() && it->second.size() == 1)
+ {
+ // Have we loaded this item from database before,
+ // and even if it has a cuesheet it has only one song
+ pItem->GetMusicInfoTag()->SetSong(it->second[0]);
+ if (!it->second[0].strThumb.empty())
+ pItem->SetArt("thumb", it->second[0].strThumb);
+ }
+ else if (pItem->IsMusicDb())
+ { // a music db item that doesn't have tag loaded - grab details from the database
+ XFILE::MUSICDATABASEDIRECTORY::CQueryParams param;
+ XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(pItem->GetPath(),param);
+ CSong song;
+ if (m_musicDatabase.GetSong(param.GetSongId(), song))
+ {
+ pItem->GetMusicInfoTag()->SetSong(song);
+ if (!song.strThumb.empty())
+ pItem->SetArt("thumb", song.strThumb);
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_USETAGS) || pItem->IsCDDA())
+ { // Nothing found, load tag from file,
+ // always try to load cddb info
+ // get correct tag parser
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem));
+ if (nullptr != pLoader)
+ // get tag
+ pLoader->Load(pItem->GetPath(), *pItem->GetMusicInfoTag());
+ m_tagReads++;
+ }
+
+ m_strPrevPath = strPath;
+ }
+ }
+
+ return true;
+}
+
+void CMusicInfoLoader::OnLoaderFinish()
+{
+ // cleanup last loaded songs from database
+ m_songsMap.clear();
+
+ // cleanup cache loaded from HD
+ m_mapFileItems->Clear();
+
+ // Save loaded items to HD
+ if (!m_strCacheFileName.empty())
+ SaveCache(m_strCacheFileName, *m_pVecItems);
+ else if (!m_bStop && (m_databaseHits > 1 || m_tagReads > 0))
+ m_pVecItems->Save();
+
+ m_musicDatabase.Close();
+
+ if (m_thumbLoader)
+ m_thumbLoader->OnLoaderFinish();
+}
+
+void CMusicInfoLoader::UseCacheOnHD(const std::string& strFileName)
+{
+ m_strCacheFileName = strFileName;
+}
+
+void CMusicInfoLoader::LoadCache(const std::string& strFileName, CFileItemList& items)
+{
+ CFile file;
+
+ if (file.Open(strFileName))
+ {
+ CArchive ar(&file, CArchive::load);
+ int iSize = 0;
+ ar >> iSize;
+ for (int i = 0; i < iSize; i++)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ ar >> *pItem;
+ items.Add(pItem);
+ }
+ ar.Close();
+ file.Close();
+ items.SetFastLookup(true);
+ }
+}
+
+void CMusicInfoLoader::SaveCache(const std::string& strFileName, CFileItemList& items)
+{
+ int iSize = items.Size();
+
+ if (iSize <= 0)
+ return ;
+
+ CFile file;
+
+ if (file.OpenForWrite(strFileName))
+ {
+ CArchive ar(&file, CArchive::store);
+ ar << items.Size();
+ for (int i = 0; i < iSize; i++)
+ {
+ CFileItemPtr pItem = items[i];
+ ar << *pItem;
+ }
+ ar.Close();
+ file.Close();
+ }
+
+}
diff --git a/xbmc/music/MusicInfoLoader.h b/xbmc/music/MusicInfoLoader.h
new file mode 100644
index 0000000..d6cab41
--- /dev/null
+++ b/xbmc/music/MusicInfoLoader.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005-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 "BackgroundInfoLoader.h"
+#include "MusicDatabase.h"
+
+class CFileItemList;
+class CMusicThumbLoader;
+
+namespace MUSIC_INFO
+{
+class CMusicInfoLoader : public CBackgroundInfoLoader
+{
+public:
+ CMusicInfoLoader();
+ ~CMusicInfoLoader() override;
+
+ void UseCacheOnHD(const std::string& strFileName);
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+ static bool LoadAdditionalTagInfo(CFileItem* pItem);
+
+protected:
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+ void LoadCache(const std::string& strFileName, CFileItemList& items);
+ void SaveCache(const std::string& strFileName, CFileItemList& items);
+protected:
+ std::string m_strCacheFileName;
+ CFileItemList* m_mapFileItems;
+ MAPSONGS m_songsMap;
+ std::string m_strPrevPath;
+ CMusicDatabase m_musicDatabase;
+ unsigned int m_databaseHits;
+ unsigned int m_tagReads;
+ CMusicThumbLoader *m_thumbLoader;
+};
+}
diff --git a/xbmc/music/MusicLibraryQueue.cpp b/xbmc/music/MusicLibraryQueue.cpp
new file mode 100644
index 0000000..f16272f
--- /dev/null
+++ b/xbmc/music/MusicLibraryQueue.cpp
@@ -0,0 +1,301 @@
+/*
+ * 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 "MusicLibraryQueue.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/jobs/MusicLibraryCleaningJob.h"
+#include "music/jobs/MusicLibraryExportJob.h"
+#include "music/jobs/MusicLibraryImportJob.h"
+#include "music/jobs/MusicLibraryJob.h"
+#include "music/jobs/MusicLibraryScanningJob.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Variant.h"
+
+#include <mutex>
+#include <utility>
+
+CMusicLibraryQueue::CMusicLibraryQueue()
+ : CJobQueue(false, 1, CJob::PRIORITY_LOW),
+ m_jobs()
+{ }
+
+CMusicLibraryQueue::~CMusicLibraryQueue()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_jobs.clear();
+}
+
+CMusicLibraryQueue& CMusicLibraryQueue::GetInstance()
+{
+ static CMusicLibraryQueue s_instance;
+ return s_instance;
+}
+
+void CMusicLibraryQueue::ExportLibrary(const CLibExportSettings& settings, bool showDialog /* = false */)
+{
+ CGUIDialogProgress* progress = NULL;
+ if (showDialog)
+ {
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{ 20196 }); //"Export music library"
+ progress->SetText(CVariant{ 650 }); //"Exporting"
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ }
+
+ CMusicLibraryExportJob* exportJob = new CMusicLibraryExportJob(settings, progress);
+ if (showDialog)
+ {
+ AddJob(exportJob);
+
+ // Wait for export to complete or be canceled, but render every 10ms so that the
+ // pointer movements work on dialog even when export is reporting progress infrequently
+ if (progress)
+ progress->Wait();
+ }
+ else
+ {
+ m_modal = true;
+ exportJob->DoWork();
+
+ delete exportJob;
+ m_modal = false;
+ Refresh();
+ }
+}
+
+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 */)
+{
+ if (flags == MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO))
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_ONLINE;
+ }
+
+ if (!showProgress || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE))
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_BACKGROUND;
+
+ AddJob(new CMusicLibraryScanningJob(strDirectory, flags, showProgress));
+}
+
+void CMusicLibraryQueue::StartAlbumScan(const std::string & strDirectory, bool refresh)
+{
+ int flags = MUSIC_INFO::CMusicInfoScanner::SCAN_ALBUMS;
+ if (refresh)
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN;
+ AddJob(new CMusicLibraryScanningJob(strDirectory, flags, true));
+}
+
+void CMusicLibraryQueue::StartArtistScan(const std::string& strDirectory, bool refresh)
+{
+ int flags = MUSIC_INFO::CMusicInfoScanner::SCAN_ARTISTS;
+ if (refresh)
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN;
+ AddJob(new CMusicLibraryScanningJob(strDirectory, flags, true));
+}
+
+bool CMusicLibraryQueue::IsScanningLibrary() const
+{
+ // check if the library is being cleaned synchronously
+ if (m_cleaning)
+ return true;
+
+ // check if the library is being scanned asynchronously
+ MusicLibraryJobMap::const_iterator scanningJobs = m_jobs.find("MusicLibraryScanningJob");
+ if (scanningJobs != m_jobs.end() && !scanningJobs->second.empty())
+ return true;
+
+ // check if the library is being cleaned asynchronously
+ MusicLibraryJobMap::const_iterator cleaningJobs = m_jobs.find("MusicLibraryCleaningJob");
+ if (cleaningJobs != m_jobs.end() && !cleaningJobs->second.empty())
+ return true;
+
+ return false;
+}
+
+void CMusicLibraryQueue::StopLibraryScanning()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ MusicLibraryJobMap::const_iterator scanningJobs = m_jobs.find("MusicLibraryScanningJob");
+ if (scanningJobs == m_jobs.end())
+ return;
+
+ // get a copy of the scanning jobs because CancelJob() will modify m_scanningJobs
+ MusicLibraryJobs tmpScanningJobs(scanningJobs->second.begin(), scanningJobs->second.end());
+
+ // cancel all scanning jobs
+ for (const auto& job : tmpScanningJobs)
+ CancelJob(job);
+ Refresh();
+}
+
+void CMusicLibraryQueue::CleanLibrary(bool showDialog /* = false */)
+{
+ CGUIDialogProgress* progress = NULL;
+ if (showDialog)
+ {
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{ 700 });
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ }
+
+ CMusicLibraryCleaningJob* cleaningJob = new CMusicLibraryCleaningJob(progress);
+ AddJob(cleaningJob);
+
+ // Wait for cleaning to complete or be canceled, but render every 20ms so that the
+ // pointer movements work on dialog even when cleaning is reporting progress infrequently
+ if (progress)
+ progress->Wait(20);
+}
+
+void CMusicLibraryQueue::AddJob(CMusicLibraryJob *job)
+{
+ if (job == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!CJobQueue::AddJob(job))
+ return;
+
+ // add the job to our list of queued/running jobs
+ std::string jobType = job->GetType();
+ MusicLibraryJobMap::iterator jobsIt = m_jobs.find(jobType);
+ if (jobsIt == m_jobs.end())
+ {
+ MusicLibraryJobs jobs;
+ jobs.insert(job);
+ m_jobs.insert(std::make_pair(jobType, jobs));
+ }
+ else
+ jobsIt->second.insert(job);
+}
+
+void CMusicLibraryQueue::CancelJob(CMusicLibraryJob *job)
+{
+ if (job == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // remember the job type needed later because the job might be deleted
+ // in the call to CJobQueue::CancelJob()
+ std::string jobType;
+ if (job->GetType() != NULL)
+ jobType = job->GetType();
+
+ // check if the job supports cancellation and cancel it
+ if (job->CanBeCancelled())
+ job->Cancel();
+
+ // remove the job from the job queue
+ CJobQueue::CancelJob(job);
+
+ // remove the job from our list of queued/running jobs
+ MusicLibraryJobMap::iterator jobsIt = m_jobs.find(jobType);
+ if (jobsIt != m_jobs.end())
+ jobsIt->second.erase(job);
+}
+
+void CMusicLibraryQueue::CancelAllJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ CJobQueue::CancelJobs();
+
+ // remove all scanning jobs
+ m_jobs.clear();
+}
+
+bool CMusicLibraryQueue::IsRunning() const
+{
+ return CJobQueue::IsProcessing() || m_modal;
+}
+
+void CMusicLibraryQueue::Refresh()
+{
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CMusicLibraryQueue::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ if (success)
+ {
+ if (QueueEmpty())
+ Refresh();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // remove the job from our list of queued/running jobs
+ MusicLibraryJobMap::iterator jobsIt = m_jobs.find(job->GetType());
+ if (jobsIt != m_jobs.end())
+ jobsIt->second.erase(static_cast<CMusicLibraryJob*>(job));
+ }
+
+ return CJobQueue::OnJobComplete(jobID, success, job);
+}
diff --git a/xbmc/music/MusicLibraryQueue.h b/xbmc/music/MusicLibraryQueue.h
new file mode 100644
index 0000000..f7948f7
--- /dev/null
+++ b/xbmc/music/MusicLibraryQueue.h
@@ -0,0 +1,135 @@
+/*
+ * 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 "settings/LibExportSettings.h"
+#include "threads/CriticalSection.h"
+#include "utils/JobManager.h"
+
+#include <map>
+#include <set>
+
+class CGUIDialogProgressBarHandle;
+class CMusicLibraryJob;
+class CGUIDialogProgress;
+
+/*!
+ \brief Queue for music library jobs.
+
+ The queue can only process a single job at any time and every job will be
+ executed at the lowest priority.
+ */
+class CMusicLibraryQueue : protected CJobQueue
+{
+public:
+ ~CMusicLibraryQueue() override;
+
+ /*!
+ \brief Gets the singleton instance of the music library queue.
+ */
+ static CMusicLibraryQueue& GetInstance();
+
+ /*!
+ \brief Enqueue a music library export job.
+ \param[in] settings Library export settings
+ \param[in] showDialog Show a progress dialog while (asynchronously) exporting, otherwise export in synchronous
+ */
+ 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.
+ \param[in] showProgress Whether or not to show a progress dialog. Defaults to true
+ */
+ void ScanLibrary(const std::string& strDirectory, int flags = 0, bool showProgress = true);
+
+ /*!
+ \brief Enqueue an album scraping job fetching additional album data.
+ \param[in] strDirectory Virtual path that identifies which albums to process or "" (empty string) for all albums
+ \param[in] refresh Whether or not to refresh data for albums that have previously been scraped
+ */
+ void StartAlbumScan(const std::string& strDirectory, bool refresh = false);
+
+ /*!
+ \brief Enqueue an artist scraping job fetching additional artist data.
+ \param[in] strDirectory Virtual path that identifies which artists to process or "" (empty string) for all artists
+ \param[in] refresh Whether or not to refresh data for artists that have previously been scraped
+ */
+ void StartArtistScan(const std::string& strDirectory, bool refresh = false);
+
+ /*!
+ \brief Check if a library scan or cleaning is in progress.
+ \return True if a scan or clean is in progress, false otherwise
+ */
+ bool IsScanningLibrary() const;
+
+ /*!
+ \brief Stop and dequeue all scanning jobs.
+ */
+ void StopLibraryScanning();
+
+ /*!
+ \brief Enqueue an asynchronous library cleaning job.
+ \param[in] showDialog Show a model progress dialog while cleaning. Default is false.
+ */
+ void CleanLibrary(bool showDialog = false);
+
+ /*!
+ \brief Adds the given job to the queue.
+ \param[in] job Music library job to be queued.
+ */
+ void AddJob(CMusicLibraryJob *job);
+
+ /*!
+ \brief Cancels the given job and removes it from the queue.
+ \param[in] job Music library job to be canceled and removed from the queue.
+ */
+ void CancelJob(CMusicLibraryJob *job);
+
+ /*!
+ \brief Cancels all running and queued jobs.
+ */
+ void CancelAllJobs();
+
+ /*!
+ \brief Whether any jobs are running or not.
+ */
+ bool IsRunning() const;
+
+protected:
+ // implementation of IJobCallback
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ /*!
+ \brief Notifies all to refresh the current listings.
+ */
+ void Refresh();
+
+private:
+ CMusicLibraryQueue();
+ CMusicLibraryQueue(const CMusicLibraryQueue&);
+ CMusicLibraryQueue const& operator=(CMusicLibraryQueue const&);
+
+ typedef std::set<CMusicLibraryJob*> MusicLibraryJobs;
+ typedef std::map<std::string, MusicLibraryJobs> MusicLibraryJobMap;
+ MusicLibraryJobMap m_jobs;
+ CCriticalSection m_critical;
+
+ bool m_modal = false;
+ bool m_exporting = false;
+ bool m_cleaning = false;
+};
diff --git a/xbmc/music/MusicThumbLoader.cpp b/xbmc/music/MusicThumbLoader.cpp
new file mode 100644
index 0000000..98d369a
--- /dev/null
+++ b/xbmc/music/MusicThumbLoader.cpp
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2012-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 "MusicThumbLoader.h"
+
+#include "FileItem.h"
+#include "TextureDatabase.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "utils/StringUtils.h"
+#include "video/VideoThumbLoader.h"
+
+#include <utility>
+
+using namespace MUSIC_INFO;
+
+CMusicThumbLoader::CMusicThumbLoader() : CThumbLoader()
+{
+ m_musicDatabase = new CMusicDatabase;
+}
+
+CMusicThumbLoader::~CMusicThumbLoader()
+{
+ delete m_musicDatabase;
+}
+
+void CMusicThumbLoader::OnLoaderStart()
+{
+ m_musicDatabase->Open();
+ m_albumArt.clear();
+ CThumbLoader::OnLoaderStart();
+}
+
+void CMusicThumbLoader::OnLoaderFinish()
+{
+ m_musicDatabase->Close();
+ m_albumArt.clear();
+ CThumbLoader::OnLoaderFinish();
+}
+
+bool CMusicThumbLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CMusicThumbLoader::LoadItemCached(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive)
+ return false;
+
+ if (pItem->HasMusicInfoTag() && !pItem->GetProperty("libraryartfilled").asBoolean())
+ {
+ if (FillLibraryArt(*pItem))
+ return true;
+
+ if (pItem->GetMusicInfoTag()->GetType() == MediaTypeArtist)
+ return false; // No fallback
+ }
+
+ if (pItem->HasVideoInfoTag() && !pItem->HasArt("thumb"))
+ { // music video
+ CVideoThumbLoader loader;
+ if (loader.LoadItemCached(pItem))
+ return true;
+ }
+
+ // Fallback to folder thumb when path has one cached
+ if (!pItem->HasArt("thumb"))
+ {
+ std::string art = GetCachedImage(*pItem, "thumb");
+ if (!art.empty())
+ pItem->SetArt("thumb", art);
+ }
+
+ // Fallback to folder fanart when path has one cached
+ //! @todo Remove as "fanart" is never been cached for music folders (only for
+ // artists) or start caching fanart for folders?
+ if (!pItem->HasArt("fanart"))
+ {
+ std::string art = GetCachedImage(*pItem, "fanart");
+ if (!art.empty())
+ {
+ pItem->SetArt("fanart", art);
+ }
+ }
+
+ return false;
+}
+
+bool CMusicThumbLoader::LoadItemLookup(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive)
+ return false;
+
+ if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->GetType() == MediaTypeArtist) // No fallback for artist
+ return false;
+
+ if (pItem->HasVideoInfoTag())
+ { // music video
+ CVideoThumbLoader loader;
+ if (loader.LoadItemLookup(pItem))
+ return true;
+ }
+
+ if (!pItem->HasArt("thumb"))
+ {
+ // Look for embedded art
+ if (pItem->HasMusicInfoTag() && !pItem->GetMusicInfoTag()->GetCoverArtInfo().Empty())
+ {
+ // The item has got embedded art but user thumbs overrule, so check for those first
+ if (!FillThumb(*pItem, false)) // Check for user thumbs but ignore folder thumbs
+ {
+ // No user thumb, use embedded art
+ std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "music");
+ pItem->SetArt("thumb", thumb);
+ }
+ }
+ else
+ {
+ // Check for user thumbs
+ FillThumb(*pItem, true);
+ }
+ }
+
+ return true;
+}
+
+bool CMusicThumbLoader::FillThumb(CFileItem &item, bool folderThumbs /* = true */)
+{
+ if (item.HasArt("thumb"))
+ return true;
+ std::string thumb = GetCachedImage(item, "thumb");
+ if (thumb.empty())
+ {
+ thumb = item.GetUserMusicThumb(false, folderThumbs);
+ if (!thumb.empty())
+ SetCachedImage(item, "thumb", thumb);
+ }
+ if (!thumb.empty())
+ item.SetArt("thumb", thumb);
+ return !thumb.empty();
+}
+
+bool CMusicThumbLoader::FillLibraryArt(CFileItem &item)
+{
+ /* Called for any item with MusicInfoTag and no art.
+ Items on Genres, Sources and Roles nodes have ID (although items on Years
+ node do not) so check for song/album/artist specifically.
+ Non-library songs (file view) can also have MusicInfoTag but no ID or type
+ */
+ bool artfound(false);
+ std::vector<ArtForThumbLoader> art;
+ CMusicInfoTag &tag = *item.GetMusicInfoTag();
+ if (tag.GetDatabaseId() > -1 &&
+ (tag.GetType() == MediaTypeSong || tag.GetType() == MediaTypeAlbum ||
+ tag.GetType() == MediaTypeArtist))
+ {
+ // Item in music library, fetch the art
+ m_musicDatabase->Open();
+ if (tag.GetType() == MediaTypeSong)
+ artfound = m_musicDatabase->GetArtForItem(tag.GetDatabaseId(), tag.GetAlbumId(), -1, false, art);
+ else if (tag.GetType() == MediaTypeAlbum)
+ artfound = m_musicDatabase->GetArtForItem(-1, tag.GetDatabaseId(), -1, false, art);
+ else //Artist
+ artfound = m_musicDatabase->GetArtForItem(-1, -1, tag.GetDatabaseId(), true, art);
+
+ m_musicDatabase->Close();
+ }
+ else if (!tag.GetArtist().empty() &&
+ (tag.GetType() == MediaTypeNone || tag.GetType() == MediaTypeSong))
+ {
+ /*
+ Could be non-library song - has musictag but no ID or type (may have
+ thumb already). Try to fetch both song artist(s) and album artist(s) art by
+ artist name, e.g. "artist.thumb", "artist.fanart", "artist.clearlogo",
+ "artist.banner", "artist1.thumb", "artist1.fanart", "artist1.clearlogo",
+ "artist1.banner", "albumartist.thumb", "albumartist.fanart" etc.
+ Set fanart as fallback.
+ */
+ CSong song;
+ // Try to split song artist names (various tags) into artist credits
+ song.SetArtistCredits(tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+ if (!song.artistCredits.empty())
+ {
+ tag.SetType(MediaTypeSong); // Makes "Information" context menu visible
+ m_musicDatabase->Open();
+ int iOrder = 0;
+ // Song artist art
+ for (const auto& artistCredit : song.artistCredits)
+ {
+ int idArtist = m_musicDatabase->GetArtistByName(artistCredit.GetArtist());
+ if (idArtist > 0)
+ {
+ std::vector<ArtForThumbLoader> artistart;
+ if (m_musicDatabase->GetArtForItem(-1, -1, idArtist, true, artistart))
+ {
+ for (auto& artitem : artistart)
+ {
+ if (iOrder > 0)
+ artitem.prefix = StringUtils::Format("artist{}", iOrder);
+ else
+ artitem.prefix = "artist";
+ }
+ art.insert(art.end(), artistart.begin(), artistart.end());
+ }
+ }
+ ++iOrder;
+ }
+ // Album artist art
+ if (!tag.GetAlbumArtist().empty() && tag.GetArtistString().compare(tag.GetAlbumArtistString()) != 0)
+ {
+ // Split song artist names correctly into artist credits from various tag
+ // arrays, inc. fallback to song artist names
+ CAlbum album;
+ album.SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(),
+ tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+
+ iOrder = 0;
+ for (const auto& artistCredit : album.artistCredits)
+ {
+ int idArtist = m_musicDatabase->GetArtistByName(artistCredit.GetArtist());
+ if (idArtist > 0)
+ {
+ std::vector<ArtForThumbLoader> artistart;
+ if (m_musicDatabase->GetArtForItem(-1, -1, idArtist, true, artistart))
+ {
+ for (auto& artitem : artistart)
+ {
+ if (iOrder > 0)
+ artitem.prefix = StringUtils::Format("albumartist{}", iOrder);
+ else
+ artitem.prefix = "albumartist";
+ }
+ art.insert(art.end(), artistart.begin(), artistart.end());
+ }
+ }
+ ++iOrder;
+ }
+ }
+ else
+ {
+ // Replicate the artist art as album artist art
+ std::vector<ArtForThumbLoader> artistart;
+ for (const auto& artitem : art)
+ {
+ ArtForThumbLoader newart;
+ newart.artType = artitem.artType;
+ newart.mediaType = artitem.mediaType;
+ newart.prefix = "album" + artitem.prefix;
+ newart.url = artitem.url;
+ artistart.emplace_back(newart);
+ }
+ art.insert(art.end(), artistart.begin(), artistart.end());
+ }
+ artfound = !art.empty();
+ m_musicDatabase->Close();
+ }
+ }
+
+ if (artfound)
+ {
+ std::string fanartfallback;
+ std::string artname;
+ std::map<std::string, std::string> artmap;
+ std::map<std::string, std::string> discartmap;
+ for (auto artitem : art)
+ {
+ /* Add art to artmap, naming according to media type.
+ For example: artists have "thumb", "fanart", "poster" etc.,
+ albums have "thumb", "artist.thumb", "artist.fanart",... "artist1.thumb", "artist1.fanart" etc.,
+ songs have "thumb", "album.thumb", "artist.thumb", "albumartist.thumb", "albumartist1.thumb" etc.
+ */
+ if (tag.GetType() == artitem.mediaType)
+ artname = artitem.artType;
+ else if (artitem.prefix.empty())
+ artname = artitem.mediaType + "." + artitem.artType;
+ else
+ {
+ if (tag.GetType() == MediaTypeAlbum)
+ StringUtils::Replace(artitem.prefix, "albumartist", "artist");
+ artname = artitem.prefix + "." + artitem.artType;
+ }
+
+ // Pull out album art for this specific disc e.g. "thumb2", skip art for other discs
+ if (artitem.mediaType == MediaTypeAlbum && tag.GetDiscNumber() > 0)
+ {
+ // Find any trailing digits
+ size_t startnum = artitem.artType.find_last_not_of("0123456789");
+ std::string digits = artitem.artType.substr(startnum + 1);
+ int num = atoi(digits.c_str());
+ if (num > 0 && startnum < artitem.artType.size())
+ {
+ if (num == tag.GetDiscNumber())
+ discartmap.insert(std::make_pair(artitem.artType.substr(0, startnum + 1), artitem.url));
+ continue;
+ }
+ }
+
+ artmap.insert(std::make_pair(artname, artitem.url));
+
+ // Add fallback art for "thumb" and "fanart" art types only
+ // Set album thumb as the fallback used when song thumb is missing
+ if (tag.GetType() == MediaTypeSong && artitem.mediaType == MediaTypeAlbum &&
+ artitem.artType == "thumb")
+ {
+ item.SetArtFallback(artitem.artType, artname);
+ }
+
+ // For albums and songs set fallback fanart from the artist.
+ // For songs prefer primary song artist over primary albumartist fanart as fallback fanart
+ if (artitem.prefix == "artist" && artitem.artType == "fanart")
+ fanartfallback = artname;
+ if (artitem.prefix == "albumartist" && artitem.artType == "fanart" && fanartfallback.empty())
+ fanartfallback = artname;
+ }
+ if (!fanartfallback.empty())
+ item.SetArtFallback("fanart", fanartfallback);
+
+ // Process specific disc art when we have some
+ for (const auto& discart : discartmap)
+ {
+ std::map<std::string, std::string>::iterator it;
+ if (tag.GetType() == MediaTypeAlbum)
+ {
+ // Insert or replace album art with specific disc art
+ it = artmap.find(discart.first);
+ if (it != artmap.end())
+ it->second = discart.second;
+ else
+ artmap.insert(discart);
+ }
+ else if (tag.GetType() == MediaTypeSong)
+ {
+ // Use disc thumb rather than album as fallback for song thumb
+ // (Fallback approach is used to fill missing thumbs).
+ if (discart.first == "thumb")
+ {
+ it = artmap.find("album.thumb");
+ if (it != artmap.end())
+ // Replace "album.thumb" already set as fallback
+ it->second = discart.second;
+ else
+ {
+ // Insert thumb for album and set as fallback
+ artmap.insert(std::make_pair("album.thumb", discart.second));
+ item.SetArtFallback("thumb", "album.thumb");
+ }
+ }
+ else
+ {
+ // Apply disc art as song art when not have that type (fallback does not apply).
+ // Art of other types could been set via JSON, or in future read from metadata
+ it = artmap.find(discart.first);
+ if (it == artmap.end())
+ artmap.insert(discart);
+ }
+ }
+ }
+
+ item.AppendArt(artmap);
+ }
+
+ item.SetProperty("libraryartfilled", true);
+ return artfound;
+}
+
+bool CMusicThumbLoader::GetEmbeddedThumb(const std::string &path, EmbeddedArt &art)
+{
+ CFileItem item(path, false);
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(item));
+ CMusicInfoTag tag;
+ if (nullptr != pLoader)
+ pLoader->Load(path, tag, &art);
+
+ return !art.Empty();
+}
diff --git a/xbmc/music/MusicThumbLoader.h b/xbmc/music/MusicThumbLoader.h
new file mode 100644
index 0000000..fa02ad5
--- /dev/null
+++ b/xbmc/music/MusicThumbLoader.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012-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 "ThumbLoader.h"
+
+#include <map>
+
+class CFileItem;
+class CMusicDatabase;
+class EmbeddedArt;
+
+class CMusicThumbLoader : public CThumbLoader
+{
+public:
+ CMusicThumbLoader();
+ ~CMusicThumbLoader() override;
+
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+
+ /*! \brief Helper function to fill all the art for a music library item
+ This fetches the original url for each type of art, and sets fallback thumb and fanart.
+ For songs the art for the related album and artist(s) is also set, and for albums that
+ of the related artist(s). Art type is named according to media type of the item,
+ for example:
+ artists may have "thumb", "fanart", "logo", "poster" etc.,
+ albums may have "thumb", "spine" etc. and "artist.thumb", "artist.fanart" etc.,
+ songs may have "thumb", "album.thumb", "artist.thumb", "artist.fanart", "artist.logo",...
+ "artist1.thumb", "artist1.fanart",... "albumartist.thumb", "albumartist1.thumb" etc.
+ \param item a music CFileItem
+ \return true if we fill art, false if there is no art found
+ */
+ bool FillLibraryArt(CFileItem &item) override;
+
+ /*! \brief Fill the thumb of a music file/folder item
+ First uses a cached thumb from a previous run, then checks for a local thumb
+ and caches it for the next run
+ \param item the CFileItem object to fill
+ \return true if we fill the thumb, false otherwise
+ */
+ virtual bool FillThumb(CFileItem &item, bool folderThumbs = true);
+
+ static bool GetEmbeddedThumb(const std::string &path, EmbeddedArt &art);
+
+protected:
+ CMusicDatabase *m_musicDatabase;
+ typedef std::map<int, std::map<std::string, std::string> > ArtCache;
+ ArtCache m_albumArt;
+};
diff --git a/xbmc/music/MusicUtils.cpp b/xbmc/music/MusicUtils.cpp
new file mode 100644
index 0000000..ac66aa3
--- /dev/null
+++ b/xbmc/music/MusicUtils.cpp
@@ -0,0 +1,841 @@
+/*
+ * Copyright (C) 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 "MusicUtils.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "media/MediaType.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/IRunnable.h"
+#include "utils/FileUtils.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "view/GUIViewState.h"
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+namespace MUSIC_UTILS
+{
+class CSetArtJob : public CJob
+{
+ CFileItemPtr pItem;
+ std::string m_artType;
+ std::string m_newArt;
+
+public:
+ CSetArtJob(const CFileItemPtr& item, const std::string& type, const std::string& newArt)
+ : pItem(item), m_artType(type), m_newArt(newArt)
+ {
+ }
+
+ ~CSetArtJob(void) override = default;
+
+ bool HasSongExtraArtChanged(const CFileItemPtr& pSongItem,
+ const std::string& type,
+ const int itemID,
+ CMusicDatabase& db)
+ {
+ if (!pSongItem->HasMusicInfoTag())
+ return false;
+ int idSong = pSongItem->GetMusicInfoTag()->GetDatabaseId();
+ if (idSong <= 0)
+ return false;
+ bool result = false;
+ if (type == MediaTypeAlbum)
+ // Update art when song is from album
+ result = (itemID == pSongItem->GetMusicInfoTag()->GetAlbumId());
+ else if (type == MediaTypeArtist)
+ {
+ // Update art when artist is song or album artist of the song
+ if (pSongItem->HasProperty("artistid"))
+ {
+ // Check artistid property when we have it
+ for (CVariant::const_iterator_array varid =
+ pSongItem->GetProperty("artistid").begin_array();
+ varid != pSongItem->GetProperty("artistid").end_array(); ++varid)
+ {
+ int idArtist = static_cast<int>(varid->asInteger());
+ result = (itemID == idArtist);
+ if (result)
+ break;
+ }
+ }
+ else
+ { // Check song artists in database
+ result = db.IsSongArtist(idSong, itemID);
+ }
+ if (!result)
+ {
+ // Check song album artists
+ result = db.IsSongAlbumArtist(idSong, itemID);
+ }
+ }
+ return result;
+ }
+
+ // Asynchronously update song, album or artist art in library
+ // and trigger update to album & artist art of the currently playing song
+ // and songs queued in the current playlist
+ bool DoWork(void) override
+ {
+ int itemID = pItem->GetMusicInfoTag()->GetDatabaseId();
+ if (itemID <= 0)
+ return false;
+ std::string type = pItem->GetMusicInfoTag()->GetType();
+ CMusicDatabase db;
+ if (!db.Open())
+ return false;
+ if (!m_newArt.empty())
+ db.SetArtForItem(itemID, type, m_artType, m_newArt);
+ else
+ db.RemoveArtForItem(itemID, type, m_artType);
+ // Artwork changed so set datemodified field for artist, album or song
+ db.SetItemUpdated(itemID, type);
+
+ /* Update the art of the songs of the current music playlist.
+ Song thumb is often a fallback from the album and fanart is from the artist(s).
+ Clear the art if it is a song from the album or by the artist
+ (as song or album artist) that has modified artwork. The new artwork gets
+ loaded when the playlist is shown.
+ */
+ bool clearcache(false);
+ const PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC);
+
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ CFileItemPtr songitem = playlist[i];
+ if (HasSongExtraArtChanged(songitem, type, itemID, db))
+ {
+ songitem->ClearArt(); // Art gets reloaded when the current playist is shown
+ clearcache = true;
+ }
+ }
+ if (clearcache)
+ {
+ // Clear the music playlist from cache
+ CFileItemList items("playlistmusic://");
+ items.RemoveDiscCache(WINDOW_MUSIC_PLAYLIST);
+ }
+
+ // Similarly update the art of the currently playing song so it shows on OSD
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() && g_application.CurrentFileItem().HasMusicInfoTag())
+ {
+ CFileItemPtr songitem = CFileItemPtr(new CFileItem(g_application.CurrentFileItem()));
+ if (HasSongExtraArtChanged(songitem, type, itemID, db))
+ g_application.UpdateCurrentPlayArt();
+ }
+
+ db.Close();
+ return true;
+ }
+};
+
+class CSetSongRatingJob : public CJob
+{
+ std::string strPath;
+ int idSong;
+ int iUserrating;
+
+public:
+ CSetSongRatingJob(const std::string& filePath, int userrating)
+ : strPath(filePath), idSong(-1), iUserrating(userrating)
+ {
+ }
+
+ CSetSongRatingJob(int songId, int userrating) : strPath(), idSong(songId), iUserrating(userrating)
+ {
+ }
+
+ ~CSetSongRatingJob(void) override = default;
+
+ bool DoWork(void) override
+ {
+ // Asynchronously update song userrating in library
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ if (idSong > 0)
+ db.SetSongUserrating(idSong, iUserrating);
+ else
+ db.SetSongUserrating(strPath, iUserrating);
+ db.Close();
+ }
+
+ return true;
+ }
+};
+
+void UpdateArtJob(const std::shared_ptr<CFileItem>& pItem,
+ const std::string& strType,
+ const std::string& strArt)
+{
+ // Asynchronously update that type of art in the database
+ CSetArtJob* job = new CSetArtJob(pItem, strType, strArt);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+}
+
+// Add art types required in Kodi and configured by the user
+void AddHardCodedAndExtendedArtTypes(std::vector<std::string>& artTypes, const CMusicInfoTag& tag)
+{
+ for (const auto& artType : GetArtTypesToScan(tag.GetType()))
+ {
+ if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end())
+ artTypes.push_back(artType);
+ }
+}
+
+// Add art types currently assigned to the media item
+void AddCurrentArtTypes(std::vector<std::string>& artTypes,
+ const CMusicInfoTag& tag,
+ CMusicDatabase& db)
+{
+ std::map<std::string, std::string> currentArt;
+ db.GetArtForItem(tag.GetDatabaseId(), tag.GetType(), currentArt);
+ for (const auto& art : currentArt)
+ {
+ if (!art.second.empty() && find(artTypes.begin(), artTypes.end(), art.first) == artTypes.end())
+ artTypes.push_back(art.first);
+ }
+}
+
+// Add art types that exist for other media items of the same type
+void AddMediaTypeArtTypes(std::vector<std::string>& artTypes,
+ const CMusicInfoTag& tag,
+ CMusicDatabase& db)
+{
+ std::vector<std::string> dbArtTypes;
+ db.GetArtTypes(tag.GetType(), dbArtTypes);
+ for (const auto& artType : dbArtTypes)
+ {
+ if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end())
+ artTypes.push_back(artType);
+ }
+}
+
+// Add art types from available but unassigned artwork for this media item
+void AddAvailableArtTypes(std::vector<std::string>& artTypes,
+ const CMusicInfoTag& tag,
+ CMusicDatabase& db)
+{
+ for (const auto& artType : db.GetAvailableArtTypesForItem(tag.GetDatabaseId(), tag.GetType()))
+ {
+ if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end())
+ artTypes.push_back(artType);
+ }
+}
+
+bool FillArtTypesList(CFileItem& musicitem, CFileItemList& artlist)
+{
+ const CMusicInfoTag& tag = *musicitem.GetMusicInfoTag();
+ if (tag.GetDatabaseId() < 1 || tag.GetType().empty())
+ return false;
+ if (tag.GetType() != MediaTypeArtist && tag.GetType() != MediaTypeAlbum &&
+ tag.GetType() != MediaTypeSong)
+ return false;
+
+ artlist.Clear();
+
+ CMusicDatabase db;
+ db.Open();
+
+ std::vector<std::string> artTypes;
+
+ AddHardCodedAndExtendedArtTypes(artTypes, tag);
+ AddCurrentArtTypes(artTypes, tag, db);
+ AddMediaTypeArtTypes(artTypes, tag, db);
+ AddAvailableArtTypes(artTypes, tag, db);
+
+ db.Close();
+
+ for (const auto& type : artTypes)
+ {
+ CFileItemPtr artitem(new CFileItem(type, false));
+ // Localise the names of common types of art
+ if (type == "banner")
+ artitem->SetLabel(g_localizeStrings.Get(20020));
+ else if (type == "fanart")
+ artitem->SetLabel(g_localizeStrings.Get(20445));
+ else if (type == "poster")
+ artitem->SetLabel(g_localizeStrings.Get(20021));
+ else if (type == "thumb")
+ artitem->SetLabel(g_localizeStrings.Get(21371));
+ else
+ artitem->SetLabel(type);
+ // Set art type as art item property
+ artitem->SetProperty("arttype", type);
+ // Set current art as art item thumb
+ if (musicitem.HasArt(type))
+ artitem->SetArt("thumb", musicitem.GetArt(type));
+ artlist.Add(artitem);
+ }
+
+ return !artlist.IsEmpty();
+}
+
+std::string ShowSelectArtTypeDialog(CFileItemList& artitems)
+{
+ // Prompt for choice
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return "";
+
+ dialog->SetHeading(CVariant{13521});
+ dialog->Reset();
+ dialog->SetUseDetails(true);
+ dialog->EnableButton(true, 13516);
+
+ dialog->SetItems(artitems);
+ dialog->Open();
+
+ if (dialog->IsButtonPressed())
+ {
+ // Get the new art type name
+ std::string strArtTypeName;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strArtTypeName,
+ CVariant{g_localizeStrings.Get(13516)}, false))
+ return "";
+ // Add new type to the list of art types
+ CFileItemPtr artitem(new CFileItem(strArtTypeName, false));
+ artitem->SetLabel(strArtTypeName);
+ artitem->SetProperty("arttype", strArtTypeName);
+ artitems.Add(artitem);
+
+ return strArtTypeName;
+ }
+
+ return dialog->GetSelectedFileItem()->GetProperty("arttype").asString();
+}
+
+int ShowSelectRatingDialog(int iSelected)
+{
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (dialog)
+ {
+ dialog->SetHeading(CVariant{38023});
+ dialog->Add(g_localizeStrings.Get(38022));
+ for (int i = 1; i <= 10; i++)
+ dialog->Add(StringUtils::Format("{}: {}", g_localizeStrings.Get(563), i));
+ dialog->SetSelected(iSelected);
+ dialog->Open();
+
+ int userrating = dialog->GetSelectedItem();
+ userrating = std::max(userrating, -1);
+ userrating = std::min(userrating, 10);
+ return userrating;
+ }
+ return -1;
+}
+
+void UpdateSongRatingJob(const std::shared_ptr<CFileItem>& pItem, int userrating)
+{
+ // Asynchronously update the song user rating in music library
+ const CMusicInfoTag* tag = pItem->GetMusicInfoTag();
+ CSetSongRatingJob* job;
+ if (tag && tag->GetType() == MediaTypeSong && tag->GetDatabaseId() > 0)
+ // Use song ID when known
+ job = new CSetSongRatingJob(tag->GetDatabaseId(), userrating);
+ else
+ job = new CSetSongRatingJob(pItem->GetPath(), userrating);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+}
+
+std::vector<std::string> GetArtTypesToScan(const MediaType& mediaType)
+{
+ std::vector<std::string> arttypes;
+ // Get default types of art that are to be automatically fetched during scanning
+ if (mediaType == MediaTypeArtist)
+ {
+ arttypes = {"thumb", "fanart"};
+ for (auto& artType : CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST))
+ {
+ if (find(arttypes.begin(), arttypes.end(), artType.asString()) == arttypes.end())
+ arttypes.emplace_back(artType.asString());
+ }
+ }
+ else if (mediaType == MediaTypeAlbum)
+ {
+ arttypes = {"thumb"};
+ for (auto& artType : CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST))
+ {
+ if (find(arttypes.begin(), arttypes.end(), artType.asString()) == arttypes.end())
+ arttypes.emplace_back(artType.asString());
+ }
+ }
+ return arttypes;
+}
+
+bool IsValidArtType(const std::string& potentialArtType)
+{
+ // Check length and is ascii
+ return potentialArtType.length() <= 25 &&
+ std::find_if_not(potentialArtType.begin(), potentialArtType.end(),
+ StringUtils::isasciialphanum) == potentialArtType.end();
+}
+
+} // namespace MUSIC_UTILS
+
+namespace
+{
+class CAsyncGetItemsForPlaylist : public IRunnable
+{
+public:
+ CAsyncGetItemsForPlaylist(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+ : m_item(item), m_queuedItems(queuedItems)
+ {
+ }
+
+ ~CAsyncGetItemsForPlaylist() override = default;
+
+ void Run() override
+ {
+ // fast lookup is needed here
+ m_queuedItems.SetFastLookup(true);
+
+ m_musicDatabase.Open();
+ GetItemsForPlaylist(m_item);
+ m_musicDatabase.Close();
+ }
+
+private:
+ void GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item);
+
+ const std::shared_ptr<CFileItem> m_item;
+ CFileItemList& m_queuedItems;
+ CMusicDatabase m_musicDatabase;
+};
+
+SortDescription GetSortDescription(const CGUIViewState& state, const CFileItemList& items)
+{
+ SortDescription sortDescTrackNumber;
+
+ auto sortDescriptions = state.GetSortDescriptions();
+ for (auto& sortDescription : sortDescriptions)
+ {
+ if (sortDescription.sortBy == SortByTrackNumber)
+ {
+ // check whether at least one item has actually a track number set
+ for (const auto& item : items)
+ {
+ if (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetTrackNumber() > 0)
+ {
+ // First choice for folders containing a single album
+ sortDescTrackNumber = sortDescription;
+ sortDescTrackNumber.sortOrder = SortOrderAscending;
+ break; // leave items loop. we can still find ByArtistThenYear. so, no return here.
+ }
+ }
+ }
+ else if (sortDescription.sortBy == SortByArtistThenYear)
+ {
+ // check whether songs from at least two different albums are in the list
+ int lastAlbumId = -1;
+ for (const auto& item : items)
+ {
+ if (item->HasMusicInfoTag())
+ {
+ const auto tag = item->GetMusicInfoTag();
+ if (lastAlbumId != -1 && tag->GetAlbumId() != lastAlbumId)
+ {
+ // First choice for folders containing multiple albums
+ sortDescription.sortOrder = SortOrderAscending;
+ return sortDescription;
+ }
+ lastAlbumId = tag->GetAlbumId();
+ }
+ }
+ }
+ }
+
+ if (sortDescTrackNumber.sortBy != SortByNone)
+ return sortDescTrackNumber;
+ else
+ return state.GetSortMethod(); // last resort
+}
+
+void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item)
+{
+ if (item->IsParentFolder() || !item->CanQueue() || item->IsRAR() || item->IsZIP())
+ return;
+
+ if (item->IsMusicDb() && item->m_bIsFolder && !item->IsParentFolder())
+ {
+ // we have a music database folder, just grab the "all" item underneath it
+ XFILE::CMusicDatabaseDirectory dir;
+
+ if (!dir.ContainsSongs(item->GetPath()))
+ {
+ // grab the ALL item in this category
+ // Genres will still require 2 lookups, and queuing the entire Genre folder
+ // will require 3 lookups (genre, artist, album)
+ CMusicDbUrl musicUrl;
+ if (musicUrl.FromString(item->GetPath()))
+ {
+ musicUrl.AppendPath("-1/");
+
+ const auto allItem = std::make_shared<CFileItem>(musicUrl.ToString(), true);
+ allItem->SetCanQueue(true); // workaround for CanQueue() check above
+ GetItemsForPlaylist(allItem);
+ }
+ return;
+ }
+ }
+
+ if (item->m_bIsFolder)
+ {
+ // Check if we add a locked share
+ if (item->m_bIsShareOrDrive)
+ {
+ if (!g_passwordManager.IsItemUnlocked(item.get(), "music"))
+ return;
+ }
+
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ const std::unique_ptr<CGUIViewState> state(
+ CGUIViewState::GetViewState(WINDOW_MUSIC_NAV, items));
+ if (state)
+ {
+ LABEL_MASKS labelMasks;
+ state->GetSortMethodLabelMasks(labelMasks);
+
+ const CLabelFormatter fileFormatter(labelMasks.m_strLabelFile, labelMasks.m_strLabel2File);
+ const CLabelFormatter folderFormatter(labelMasks.m_strLabelFolder,
+ labelMasks.m_strLabel2Folder);
+ for (const auto& i : items)
+ {
+ if (i->IsLabelPreformatted())
+ continue;
+
+ if (i->m_bIsFolder)
+ folderFormatter.FormatLabels(i.get());
+ else
+ fileFormatter.FormatLabels(i.get());
+ }
+
+ SortDescription sortDesc;
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV)
+ sortDesc = state->GetSortMethod();
+ else
+ sortDesc = GetSortDescription(*state, items);
+
+ if (sortDesc.sortBy == SortByLabel)
+ items.ClearSortState();
+
+ items.Sort(sortDesc);
+ }
+
+ for (const auto& i : items)
+ {
+ GetItemsForPlaylist(i);
+ }
+ }
+ else
+ {
+ if (item->IsPlayList())
+ {
+ const std::unique_ptr<PLAYLIST::CPlayList> playList(
+ PLAYLIST::CPlayListFactory::Create(*item));
+ if (!playList)
+ {
+ CLog::Log(LOGERROR, "{} failed to create playlist {}", __FUNCTION__, item->GetPath());
+ return;
+ }
+
+ if (!playList->Load(item->GetPath()))
+ {
+ CLog::Log(LOGERROR, "{} failed to load playlist {}", __FUNCTION__, item->GetPath());
+ return;
+ }
+
+ for (int i = 0; i < playList->size(); ++i)
+ {
+ GetItemsForPlaylist((*playList)[i]);
+ }
+ }
+ else if (item->IsInternetStream() && !item->IsMusicDb())
+ {
+ // just queue the internet stream, it will be expanded on play
+ m_queuedItems.Add(item);
+ }
+ else if (item->IsPlugin() && item->GetProperty("isplayable").asBoolean())
+ {
+ // python files can be played
+ m_queuedItems.Add(item);
+ }
+ else if (!item->IsNFO() && (item->IsAudio() || item->IsVideo()))
+ {
+ const auto itemCheck = m_queuedItems.Get(item->GetPath());
+ if (!itemCheck || itemCheck->GetStartOffset() != item->GetStartOffset())
+ {
+ // add item
+ m_musicDatabase.SetPropertiesForFileItem(*item);
+ m_queuedItems.Add(item);
+ }
+ }
+ }
+}
+
+void ShowToastNotification(const CFileItem& item, int titleId)
+{
+ std::string localizedMediaType;
+ std::string title;
+
+ if (item.HasMusicInfoTag())
+ {
+ localizedMediaType = CMediaTypes::GetCapitalLocalization(item.GetMusicInfoTag()->GetType());
+ title = item.GetMusicInfoTag()->GetTitle();
+ }
+
+ if (title.empty())
+ title = item.GetLabel();
+ if (title.empty())
+ return; // no meaningful toast possible.
+
+ const std::string message =
+ localizedMediaType.empty() ? title : localizedMediaType + ": " + title;
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(titleId),
+ message);
+}
+} // unnamed namespace
+
+namespace MUSIC_UTILS
+{
+void PlayItem(const std::shared_ptr<CFileItem>& itemIn)
+{
+ auto item = itemIn;
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!itemIn->CanQueue())
+ {
+ // make a copy to not alter the original item
+ item = std::make_shared<CFileItem>(*itemIn);
+ item->SetCanQueue(true);
+ }
+
+ if (item->m_bIsFolder)
+ {
+ // build a playlist and play it
+ CFileItemList queuedItems;
+ GetItemsForPlayList(item, queuedItems);
+
+ auto& player = CServiceBroker::GetPlaylistPlayer();
+ player.ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ player.Reset();
+ player.Add(PLAYLIST::TYPE_MUSIC, queuedItems);
+ player.SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ player.Play();
+ }
+ else if (item->HasMusicInfoTag())
+ {
+ // song, so just play it
+ CServiceBroker::GetPlaylistPlayer().Play(item, "");
+ }
+}
+
+void QueueItem(const std::shared_ptr<CFileItem>& itemIn, QueuePosition pos)
+{
+ auto item = itemIn;
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!itemIn->CanQueue())
+ {
+ // make a copy to not alter the original item
+ item = std::make_shared<CFileItem>(*itemIn);
+ item->SetCanQueue(true);
+ }
+
+ auto& player = CServiceBroker::GetPlaylistPlayer();
+
+ PLAYLIST::Id playlistId = player.GetCurrentPlaylist();
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ playlistId = components.GetComponent<CApplicationPlayer>()->GetPreferredPlaylist();
+ }
+
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ playlistId = PLAYLIST::TYPE_MUSIC;
+
+ // Check for the partymode playlist item, do nothing when "PartyMode.xsp" not exists
+ if (item->IsSmartPlayList() && !CFileUtils::Exists(item->GetPath()))
+ {
+ const auto profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if (item->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp"))
+ return;
+ }
+
+ const int oldSize = player.GetPlaylist(playlistId).size();
+
+ CFileItemList queuedItems;
+ GetItemsForPlayList(item, queuedItems);
+
+ // if party mode, add items but DONT start playing
+ if (g_partyModeManager.IsEnabled())
+ {
+ g_partyModeManager.AddUserSongs(queuedItems, false);
+ return;
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (pos == QueuePosition::POSITION_BEGIN && appPlayer->IsPlaying())
+ player.Insert(playlistId, queuedItems,
+ CServiceBroker::GetPlaylistPlayer().GetCurrentSong() + 1);
+ else
+ player.Add(playlistId, queuedItems);
+
+ bool playbackStarted = false;
+
+ if (!appPlayer->IsPlaying() && player.GetPlaylist(playlistId).size())
+ {
+ const int winID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (winID == WINDOW_MUSIC_NAV)
+ {
+ CGUIViewState* viewState = CGUIViewState::GetViewState(winID, queuedItems);
+ if (viewState)
+ viewState->SetPlaylistDirectory("playlistmusic://");
+ }
+
+ player.Reset();
+ player.SetCurrentPlaylist(playlistId);
+ player.Play(oldSize, ""); // start playing at the first new item
+
+ playbackStarted = true;
+ }
+
+ if (!playbackStarted)
+ {
+ if (pos == QueuePosition::POSITION_END)
+ ShowToastNotification(*item, 38082); // Added to end of playlist
+ else
+ ShowToastNotification(*item, 38083); // Added to playlist to play next
+ }
+}
+
+bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+{
+ CAsyncGetItemsForPlaylist getItems(item, queuedItems);
+ return CGUIDialogBusy::Wait(&getItems,
+ 500, // 500ms before busy dialog appears
+ true); // can be cancelled
+}
+
+bool IsItemPlayable(const CFileItem& item)
+{
+ // Exclude all parent folders
+ if (item.IsParentFolder())
+ return false;
+
+ // Exclude all video library items
+ if (item.IsVideoDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://video/"))
+ return false;
+
+ // Exclude other components
+ if (item.IsPVR() || item.IsPlugin() || item.IsScript() || item.IsAddonsPath())
+ return false;
+
+ // Exclude special items
+ if (StringUtils::StartsWithNoCase(item.GetPath(), "newsmartplaylist://") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "newplaylist://"))
+ return false;
+
+ // Exclude unwanted windows
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_PLAYLIST)
+ return false;
+
+ // Include playlists located at one of the possible music playlist locations
+ if (item.IsPlayList())
+ {
+ if (StringUtils::StartsWithNoCase(item.GetMimeType(), "audio/"))
+ return true;
+
+ if (StringUtils::StartsWithNoCase(item.GetPath(), "special://musicplaylists/") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/music/"))
+ return true;
+
+ // Has user changed default playlists location and the list is located there?
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string path = settings->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ StringUtils::TrimRight(path, "/");
+ if (StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/music/", path)))
+ return true;
+
+ if (!item.m_bIsFolder)
+ {
+ // Unknown location. Type cannot be determined for non-folder items.
+ return false;
+ }
+ }
+
+ if (item.m_bIsFolder &&
+ (item.IsMusicDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://music/")))
+ {
+ // Exclude top level nodes - eg can't play 'genres' just a specific genre etc
+ const XFILE::MUSICDATABASEDIRECTORY::NODE_TYPE node =
+ XFILE::CMusicDatabaseDirectory::GetDirectoryParentType(item.GetPath());
+ if (node == XFILE::MUSICDATABASEDIRECTORY::NODE_TYPE_OVERVIEW)
+ return false;
+
+ return true;
+ }
+
+ if (item.HasMusicInfoTag() && item.CanQueue())
+ return true;
+ else if (!item.m_bIsFolder && item.IsAudio())
+ return true;
+ else if (item.m_bIsFolder)
+ {
+ // Not a music-specific folder (just file:// or nfs://). Allow play if context is Music window.
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV &&
+ item.GetPath() != "add") // Exclude "Add music source" item
+ return true;
+ }
+ return false;
+}
+
+} // namespace MUSIC_UTILS
diff --git a/xbmc/music/MusicUtils.h b/xbmc/music/MusicUtils.h
new file mode 100644
index 0000000..c9b6f94
--- /dev/null
+++ b/xbmc/music/MusicUtils.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 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 "media/MediaType.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+namespace MUSIC_UTILS
+{
+/*! \brief Show a dialog to allow the selection of type of art from a list.
+ Input is a fileitem list, with each item having an "arttype" property
+ e.g. "thumb", current art URL (if art exists), and label. One of these art types
+ can be selected, or a new art type added. The new art type is added as a new item
+ in the list, as well as returned as the selected art type.
+ \param artitems [in/out] a fileitem list to display
+ \return the selected art type e.g. "fanart" or empty string when cancelled.
+ \sa FillArtTypesList
+ */
+std::string ShowSelectArtTypeDialog(CFileItemList& artitems);
+
+/*! \brief Helper function to build a list of art types for a music library item.
+ This fetches the possible types of art for a song, album or artist, and the
+ current art URL (if the item has art of that type), for display on a dialog.
+ \param musicitem a music CFileItem (song, album or artist)
+ \param artitems [out] a fileitem list, each item having "arttype" property
+ e.g. "thumb", current art URL (if art exists), and localized label (for common arttypes)
+ \return true if art types are retrieved, false if none is found.
+ \sa ShowSelectArtTypeDialog
+ */
+bool FillArtTypesList(CFileItem& musicitem, CFileItemList& artlist);
+
+/*! \brief Helper function to asynchronously update art in the music database
+ and then refresh the album & artist art of the currently playing song.
+ For the song, album or artist this adds a job to the queue to update the art table
+ modifying, adding or deleting that type of art. Changes to album or artist art are
+ then passed to the currently playing song (if there is one).
+ \param item a shared pointer to a music CFileItem (song, album or artist)
+ \param strType the type of art e.g. "fanart" or "thumb" etc.
+ \param strArt art URL, when empty the entry for that type of art is deleted.
+ */
+void UpdateArtJob(const std::shared_ptr<CFileItem>& pItem,
+ const std::string& strType,
+ const std::string& strArt);
+
+/*! \brief Show a dialog to allow the selection of user rating.
+ \param iSelected the rating to show initially
+ \return the selected rating, 0 (no rating), 1 to 10 or -1 no rating selected
+ */
+int ShowSelectRatingDialog(int iSelected);
+
+/*! \brief Helper function to asynchronously update the user rating of a song
+ \param pItem pointer to song item being rated
+ \param userrating the userrating 0 = no rating, 1 to 10
+ */
+void UpdateSongRatingJob(const std::shared_ptr<CFileItem>& pItem, int userrating);
+
+/*! \brief Get the types of art for an artist or album that are to be
+ automatically fetched from local files during scanning
+ \param mediaType [in] artist or album
+ \return vector of art types that are to be fetched during scanning
+ */
+std::vector<std::string> GetArtTypesToScan(const MediaType& mediaType);
+
+/*! \brief Validate string is acceptable as the name of an additional art type
+ - limited length, and ascii alphanumberic characters only
+ \param potentialArtType [in] potential art type name
+ \return true if the art type is valid
+ */
+bool IsValidArtType(const std::string& potentialArtType);
+
+/*! \brief Start playback of the given item. If the item is a folder, build a playlist with
+ all items contained in the folder and start playback of the playlist. If item is a single music
+ item, start playback directly, without adding it to the music playlist first.
+ \param item [in] the item to play
+ */
+void PlayItem(const std::shared_ptr<CFileItem>& item);
+
+enum class QueuePosition
+{
+ POSITION_BEGIN, // place at begin of queue, before other items
+ POSITION_END, // place at end of queue, after other items
+};
+
+/*! \brief Queue the given item in the currently active playlist. If none is active, put the
+ item into the music playlist. Start playback of the playlist, if player is not already playing.
+ \param item [in] the item to queue
+ \param pos [in] whether to place the item and the begin or the end of the queue
+ */
+void QueueItem(const std::shared_ptr<CFileItem>& item, QueuePosition pos);
+
+/*! \brief For a given item, get the items to put in a playlist. If the item is a folder, all
+ subitems will be added recursively to the returned item list. If the item is a playlist, the
+ playlist will be loaded and contained items will be added to the returned item list. Shows a
+ busy dialog if action takes certain amount of time to give the user visual feedback.
+ \param item [in] the item to add to the playlist
+ \param queuedItems [out] the items that can be put in a play list
+ \return true on success, false otherwise
+ */
+bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems);
+
+/*!
+ \brief Check whether the given item can be played by the app playlist player as one or more songs.
+ \param item The item to check
+ \return True if playable, false otherwise.
+ */
+bool IsItemPlayable(const CFileItem& item);
+
+} // namespace MUSIC_UTILS
diff --git a/xbmc/music/Song.cpp b/xbmc/music/Song.cpp
new file mode 100644
index 0000000..165889c
--- /dev/null
+++ b/xbmc/music/Song.cpp
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2005-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 "Song.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace MUSIC_INFO;
+
+CSong::CSong(CFileItem& item)
+{
+ CMusicInfoTag& tag = *item.GetMusicInfoTag();
+ strTitle = tag.GetTitle();
+ genre = tag.GetGenre();
+ strArtistDesc = tag.GetArtistString();
+ //Set sort string before processing artist credits
+ strArtistSort = tag.GetArtistSort();
+ m_strComposerSort = tag.GetComposerSort();
+
+ // Determine artist credits from various tag arrays
+ SetArtistCredits(tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+
+ strAlbum = tag.GetAlbum();
+ m_albumArtist = tag.GetAlbumArtist();
+ // Separate album artist names further, if possible, and trim blank space.
+ if (tag.GetMusicBrainzAlbumArtistHints().size() > m_albumArtist.size())
+ // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names
+ m_albumArtist = tag.GetMusicBrainzAlbumArtistHints();
+ else
+ // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader
+ m_albumArtist = StringUtils::SplitMulti(m_albumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
+ for (auto artistname : m_albumArtist)
+ StringUtils::Trim(artistname);
+ m_strAlbumArtistSort = tag.GetAlbumArtistSort();
+
+ strMusicBrainzTrackID = tag.GetMusicBrainzTrackID();
+ m_musicRoles = tag.GetContributors();
+ strComment = tag.GetComment();
+ strCueSheet = tag.GetCueSheet();
+ strMood = tag.GetMood();
+ rating = tag.GetRating();
+ userrating = tag.GetUserrating();
+ votes = tag.GetVotes();
+ strOrigReleaseDate = tag.GetOriginalDate();
+ strReleaseDate = tag.GetReleaseDate();
+ strDiscSubtitle = tag.GetDiscSubtitle();
+ iTrack = tag.GetTrackAndDiscNumber();
+ iDuration = tag.GetDuration();
+ strRecordLabel = tag.GetRecordLabel();
+ strAlbumType = tag.GetMusicBrainzReleaseType();
+ bCompilation = tag.GetCompilation();
+ embeddedArt = tag.GetCoverArtInfo();
+ strFileName = tag.GetURL().empty() ? item.GetPath() : tag.GetURL();
+ dateAdded = tag.GetDateAdded();
+ replayGain = tag.GetReplayGain();
+ strThumb = item.GetUserMusicThumb(true);
+ iStartOffset = static_cast<int>(item.GetStartOffset());
+ iEndOffset = static_cast<int>(item.GetEndOffset());
+ idSong = -1;
+ iTimesPlayed = 0;
+ idAlbum = -1;
+ iBPM = tag.GetBPM();
+ iSampleRate = tag.GetSampleRate();
+ iBitRate = tag.GetBitRate();
+ iChannels = tag.GetNoOfChannels();
+}
+
+CSong::CSong()
+{
+ Clear();
+}
+
+void CSong::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids)
+{
+ artistCredits.clear();
+ std::vector<std::string> artistHints = hints;
+ //Split the artist sort string to try and get sort names for individual artists
+ std::vector<std::string> artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+
+ if (!mbids.empty())
+ { // Have musicbrainz artist info, so use it
+
+ // Vector of possible separators in the order least likely to be part of artist name
+ const std::vector<std::string> separators{ " feat. ", " ft. ", " Feat. "," Ft. ", ";", ":", "|", "#", "/", " with ", ",", "&" };
+
+ // Establish tag consistency - do the number of musicbrainz ids and number of names in hints or artist match
+ if (mbids.size() != artistHints.size() && mbids.size() != names.size())
+ {
+ // Tags mismatch - report it and then try to fix
+ CLog::Log(LOGDEBUG, "Mismatch in song file tags: {} mbid {} names {} {}", (int)mbids.size(),
+ (int)names.size(), strTitle, strArtistDesc);
+ /*
+ Most likely we have no hints and a single artist name like "Artist1 feat. Artist2"
+ or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the
+ expected single item separator (default = space-slash-space) as not been used.
+ Ampersand (&), comma and slash (no spaces) are poor delimiters as could be in name
+ e.g. "AC/DC", "Earth, Wind & Fire", but here treat them as such in attempt to find artist names.
+ When there are hints but count not match mbid they could be poorly formatted using unexpected
+ separators so attempt to split them. Or we could have more hints or artist names than
+ musicbrainz id so ignore them but raise warning.
+ */
+ // Do hints exist yet mismatch
+ if (artistHints.size() > 0 &&
+ artistHints.size() != mbids.size())
+ {
+ if (names.size() == mbids.size())
+ // Artist name count matches, use that as hints
+ artistHints = names;
+ else if (artistHints.size() < mbids.size())
+ { // Try splitting the hints until have matching number
+ artistHints = StringUtils::SplitMulti(artistHints, separators, mbids.size());
+ }
+ else
+ // Extra hints, discard them.
+ artistHints.resize(mbids.size());
+ }
+ // Do hints not exist or still mismatch, try artists
+ if (artistHints.size() != mbids.size())
+ artistHints = names;
+ // Still mismatch, try splitting the hints (now artists) until have matching number
+ if (artistHints.size() < mbids.size())
+ {
+ artistHints = StringUtils::SplitMulti(artistHints, separators, mbids.size());
+ }
+ }
+ else
+ { // Either hints or artist names (or both) matches number of musicbrainz id
+ // If hints mismatch, use artists
+ if (artistHints.size() != mbids.size())
+ artistHints = names;
+ }
+
+ // Try to get number of artist sort names and musicbrainz ids to match. Split sort names
+ // further using multiple possible delimiters, over single separator applied in Tag loader
+ if (artistSort.size() != mbids.size())
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < mbids.size(); i++)
+ {
+ std::string artistId = mbids[i];
+ std::string artistName;
+ /*
+ We try and get the corresponding artist name from the hints list.
+ Having already attempted to make the number of hints match, if they
+ still don't then use musicbrainz id as the name and hope later on we
+ can update that entry.
+ */
+ if (i < artistHints.size())
+ artistName = artistHints[i];
+ else
+ artistName = artistId;
+
+ // Use artist sort name providing we have as many as we have mbid,
+ // otherwise something is wrong with them so ignore and leave blank
+ if (artistSort.size() == mbids.size())
+ artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId);
+ else
+ artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId);
+ }
+ }
+ else
+ { // No musicbrainz artist ids, so fill in directly
+ // Separate artist names further, if possible, and trim blank space.
+ std::vector<std::string> artists = names;
+ if (artistHints.size() > names.size())
+ // Make use of hints (ARTISTS tag), when present, to separate artist names
+ artists = artistHints;
+ else
+ // Split artist names further using multiple possible delimiters, over single separator applied in Tag loader
+ artists = StringUtils::SplitMulti(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
+
+ if (artistSort.size() != artists.size())
+ // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < artists.size(); i++)
+ {
+ artistCredits.emplace_back(StringUtils::Trim(artists[i]));
+ // Set artist sort name providing we have as many as we have artists,
+ // otherwise something is wrong with them so ignore rather than guess.
+ if (artistSort.size() == artists.size())
+ artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i]));
+ }
+ }
+
+}
+
+void CSong::MergeScrapedSong(const CSong& source, bool override)
+{
+ // Merge when MusicBrainz Track ID match (checked in CAlbum::MergeScrapedAlbum)
+ if ((override && !source.strTitle.empty()) || strTitle.empty())
+ strTitle = source.strTitle;
+ if ((override && source.iTrack != 0) || iTrack == 0)
+ iTrack = source.iTrack;
+ if (override)
+ {
+ artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper
+ strArtistDesc.clear(); // @todo: set artist display string e.g. "artist1 feat. artist2" when scraped
+ }
+}
+
+void CSong::Serialize(CVariant& value) const
+{
+ value["filename"] = strFileName;
+ value["title"] = strTitle;
+ value["artist"] = GetArtist();
+ value["artistsort"] = GetArtistSort(); // a string for the song not vector of values for each artist
+ value["album"] = strAlbum;
+ value["albumartist"] = GetAlbumArtist();
+ value["genre"] = genre;
+ value["duration"] = iDuration;
+ value["track"] = iTrack;
+ value["year"] = atoi(strReleaseDate.c_str());;
+ value["musicbrainztrackid"] = strMusicBrainzTrackID;
+ value["comment"] = strComment;
+ value["mood"] = strMood;
+ value["rating"] = rating;
+ value["userrating"] = userrating;
+ value["votes"] = votes;
+ value["timesplayed"] = iTimesPlayed;
+ value["lastplayed"] = lastPlayed.IsValid() ? lastPlayed.GetAsDBDateTime() : "";
+ value["dateadded"] = dateAdded.IsValid() ? dateAdded.GetAsDBDateTime() : "";
+ value["albumid"] = idAlbum;
+ value["albumreleasedate"] = strReleaseDate;
+ value["bpm"] = iBPM;
+ value["bitrate"] = iBitRate;
+ value["samplerate"] = iSampleRate;
+ value["channels"] = iChannels;
+}
+
+void CSong::Clear()
+{
+ strFileName.clear();
+ strTitle.clear();
+ strAlbum.clear();
+ strArtistSort.clear();
+ strArtistDesc.clear();
+ m_albumArtist.clear();
+ m_strAlbumArtistSort.clear();
+ genre.clear();
+ strThumb.clear();
+ strMusicBrainzTrackID.clear();
+ m_musicRoles.clear();
+ strComment.clear();
+ strMood.clear();
+ rating = 0;
+ userrating = 0;
+ votes = 0;
+ iTrack = 0;
+ iDuration = 0;
+ strOrigReleaseDate.clear();
+ strReleaseDate.clear();
+ strDiscSubtitle.clear();
+ iStartOffset = 0;
+ iEndOffset = 0;
+ idSong = -1;
+ iTimesPlayed = 0;
+ lastPlayed.Reset();
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ dateNew.Reset();
+ idAlbum = -1;
+ bCompilation = false;
+ embeddedArt.Clear();
+ iBPM = 0;
+ iBitRate = 0;
+ iSampleRate = 0;
+ iChannels = 0;
+
+ replayGain = ReplayGain();
+}
+const std::vector<std::string> CSong::GetArtist() const
+{
+ //Get artist names as vector from artist credits
+ std::vector<std::string> songartists;
+ for (const auto& artistCredit : artistCredits)
+ {
+ songartists.push_back(artistCredit.GetArtist());
+ }
+ //When artist credits have not been populated attempt to build an artist vector from the description string
+ //This is a temporary fix, in the longer term other areas should query the song_artist table and populate
+ //artist credits. Note that splitting the string may not give the same artists as held in the song_artist table
+ if (songartists.empty() && !strArtistDesc.empty())
+ songartists = StringUtils::Split(strArtistDesc, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return songartists;
+}
+
+const std::string CSong::GetArtistSort() const
+{
+ //The stored artist sort name string takes precedence but a
+ //value could be created from individual sort names held in artistcredits
+ if (!strArtistSort.empty())
+ return strArtistSort;
+ std::vector<std::string> artistvector;
+ for (const auto& artistcredit : artistCredits)
+ if (!artistcredit.GetSortName().empty())
+ artistvector.emplace_back(artistcredit.GetSortName());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, "; ");
+ return artistString;
+}
+
+const std::vector<std::string> CSong::GetMusicBrainzArtistID() const
+{
+ //Get artist MusicBrainz IDs as vector from artist credits
+ std::vector<std::string> musicBrainzID;
+ for (const auto& artistCredit : artistCredits)
+ {
+ musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ return musicBrainzID;
+}
+
+const std::string CSong::GetArtistString() const
+{
+ //Artist description may be different from the artists in artistcredits (see ARTISTS tag processing)
+ //but is takes precedence as a string because artistcredits is not always filled during processing
+ if (!strArtistDesc.empty())
+ return strArtistDesc;
+ std::vector<std::string> artistvector;
+ for (const auto& i : artistCredits)
+ artistvector.push_back(i.GetArtist());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return artistString;
+}
+
+const std::vector<int> CSong::GetArtistIDArray() const
+{
+ // Get song artist IDs for json rpc
+ std::vector<int> artistids;
+ for (const auto& artistCredit : artistCredits)
+ artistids.push_back(artistCredit.GetArtistId());
+ return artistids;
+}
+
+void CSong::AppendArtistRole(const CMusicRole& musicRole)
+{
+ m_musicRoles.push_back(musicRole);
+}
+
+bool CSong::HasArt() const
+{
+ if (!strThumb.empty()) return true;
+ if (!embeddedArt.Empty()) return true;
+ return false;
+}
+
+bool CSong::ArtMatches(const CSong &right) const
+{
+ return (right.strThumb == strThumb &&
+ embeddedArt.Matches(right.embeddedArt));
+}
+
+const std::string CSong::GetDiscSubtitle() const
+{
+ return strDiscSubtitle;
+}
diff --git a/xbmc/music/Song.h b/xbmc/music/Song.h
new file mode 100644
index 0000000..3fc127a
--- /dev/null
+++ b/xbmc/music/Song.h
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2005-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
+
+/*!
+ \file Song.h
+\brief
+*/
+
+#include "Artist.h"
+#include "XBDateTime.h"
+#include "music/tags/ReplayGain.h"
+#include "utils/EmbeddedArt.h"
+#include "utils/ISerializable.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CVariant;
+
+/*!
+ \ingroup music
+ \brief Class to store and read album information from CMusicDatabase
+ \sa CSong, CMusicDatabase
+ */
+
+class CGenre
+{
+public:
+ int idGenre;
+ std::string strGenre;
+};
+
+class CFileItem;
+
+/*!
+ \ingroup music
+ \brief Class to store and read song information from CMusicDatabase
+ \sa CAlbum, CMusicDatabase
+ */
+class CSong final : public ISerializable
+{
+public:
+ CSong() ;
+ explicit CSong(CFileItem& item);
+ void Clear() ;
+ void MergeScrapedSong(const CSong& source, bool override);
+ void Serialize(CVariant& value) const override;
+
+ bool operator<(const CSong &song) const
+ {
+ if (strFileName < song.strFileName) return true;
+ if (strFileName > song.strFileName) return false;
+ if (iTrack < song.iTrack) return true;
+ return false;
+ }
+
+ /*! \brief Get artist names from the vector of artistcredits objects
+ \return artist names as a vector of strings
+ */
+ const std::vector<std::string> GetArtist() const;
+
+ /*! \brief Get artist sort name string
+ \return artist sort name as a single string
+ */
+ const std::string GetArtistSort() const;
+
+ /*! \brief Get artist MusicBrainz IDs from the vector of artistcredits objects
+ \return artist MusicBrainz IDs as a vector of strings
+ */
+ const std::vector<std::string> GetMusicBrainzArtistID() const;
+
+ /*! \brief Get artist names from the artist description string (if it exists)
+ or concatenated from the vector of artistcredits objects
+ \return artist names as a single string
+ */
+ const std::string GetArtistString() const;
+
+ /*! \brief Get song artist IDs (for json rpc) from the vector of artistcredits objects
+ \return album artist IDs as a vector of integers
+ */
+ const std::vector<int> GetArtistIDArray() const;
+
+ /*! \brief Get album artist names associated with song from tag data
+ Note for initial album processing only, normalised album artist data belongs to album
+ and is stored in album artist credits
+ \return album artist names as a vector of strings
+ */
+ const std::vector<std::string> GetAlbumArtist() const { return m_albumArtist; }
+
+ /*! \brief Get album artist sort name string
+ \return album artist sort name as a single string
+ */
+ const std::string GetAlbumArtistSort() const { return m_strAlbumArtistSort; }
+
+ /*! \brief Get disc subtitle string where one exists
+ \return disc subtitle as a single string
+ */
+ const std::string GetDiscSubtitle() const;
+
+ /*! \brief Get composer sort name string
+ \return composer sort name as a single string
+ */
+ const std::string GetComposerSort() const { return m_strComposerSort; }
+
+ /*! \brief Get the full list of artist names and the role each played for those
+ that contributed to the recording. Given in music file tags other than ARTIST
+ or ALBUMARTIST, e.g. COMPOSER or CONDUCTOR etc.
+ \return a vector of all contributing artist names and their roles
+ */
+ const VECMUSICROLES& GetContributors() const { return m_musicRoles; }
+ //void AddArtistRole(const int &role, const std::string &artist);
+ void AppendArtistRole(const CMusicRole& musicRole);
+
+ /*! \brief Set album artist vector.
+ Album artist is held local to song until album created for initial processing only.
+ Normalised album artist data belongs to album and is stored in album artist credits
+ \param album artist names as a vector of strings
+ */
+ void SetAlbumArtist(const std::vector<std::string>& albumartists) { m_albumArtist = albumartists; }
+
+ /*! \brief Whether this song has any artists in artist credits vector
+ Tests if artist credits has been populated yet, during processing there can be
+ artists in the artist description but not yet in the credits
+ */
+ bool HasArtistCredits() const { return !artistCredits.empty(); }
+
+ /*! \brief Whether this song has any artists in music roles (contributors) vector
+ Tests if contributors has been populated yet, there may be none.
+ */
+ bool HasContributors() const { return !m_musicRoles.empty(); }
+
+ /*! \brief whether this song has art associated with it
+ Tests both the strThumb and embeddedArt members.
+ */
+ bool HasArt() const;
+
+ /*! \brief whether the art from this song matches the art from another
+ Tests both the strThumb and embeddedArt members.
+ */
+ bool ArtMatches(const CSong &right) const;
+
+ /*! \brief Set artist credits using the arrays of tag values.
+ If strArtistSort (as from ARTISTSORT tag) is already set then individual
+ artist sort names are also processed.
+ \param names String vector of artist names (as from ARTIST tag)
+ \param hints String vector of artist name hints (as from ARTISTS tag)
+ \param mbids String vector of artist Musicbrainz IDs (as from MUSICBRAINZARTISTID tag)
+ */
+ void SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids);
+
+ int idSong;
+ int idAlbum;
+ std::string strFileName;
+ std::string strTitle;
+ std::string strArtistSort;
+ std::string strArtistDesc;
+ VECARTISTCREDITS artistCredits;
+ std::string strAlbum;
+ std::vector<std::string> genre;
+ std::string strThumb;
+ EmbeddedArtInfo embeddedArt;
+ std::string strMusicBrainzTrackID;
+ std::string strComment;
+ std::string strMood;
+ std::string strCueSheet;
+ float rating;
+ int userrating;
+ int votes;
+ int iTrack;
+ int iDuration;
+ std::string strOrigReleaseDate;
+ std::string strReleaseDate;
+ std::string strDiscSubtitle;
+ int iTimesPlayed;
+ CDateTime lastPlayed;
+ CDateTime dateAdded; // File creation or modification time, or when tags (re-)scanned
+ CDateTime dateUpdated; // Time db record Last modified
+ CDateTime dateNew; // Time db record created
+ int iStartOffset;
+ int iEndOffset;
+ bool bCompilation;
+ int iBPM;
+ int iSampleRate;
+ int iBitRate;
+ int iChannels;
+ std::string strRecordLabel; // Record label from tag for album processing by CMusicInfoScanner::FileItemsToAlbums
+ std::string strAlbumType; // (Musicbrainz release type) album type from tag for album processing by CMusicInfoScanner::FileItemsToAlbums
+
+ ReplayGain replayGain;
+private:
+ std::vector<std::string> m_albumArtist; // Album artist from tag for album processing, no desc or MBID
+ std::string m_strAlbumArtistSort; // Albumartist sort string from tag for album processing by CMusicInfoScanner::FileItemsToAlbums
+ std::string m_strComposerSort;
+ VECMUSICROLES m_musicRoles;
+};
+
+/*!
+ \ingroup music
+ \brief A vector of CSong objects, used for CMusicDatabase
+ \sa CMusicDatabase
+ */
+typedef std::vector<CSong> VECSONGS;
+
+/*!
+ \ingroup music
+ \brief A map of a vector of CSong objects key by filename, used for CMusicDatabase
+ */
+typedef std::map<std::string, VECSONGS> MAPSONGS;
+
+/*!
+ \ingroup music
+ \brief A vector of std::string objects, used for CMusicDatabase
+ \sa CMusicDatabase
+ */
+typedef std::vector<CGenre> VECGENRES;
diff --git a/xbmc/music/dialogs/CMakeLists.txt b/xbmc/music/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..40b6e90
--- /dev/null
+++ b/xbmc/music/dialogs/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES GUIDialogInfoProviderSettings.cpp
+ GUIDialogMusicInfo.cpp
+ GUIDialogMusicOSD.cpp
+ GUIDialogSongInfo.cpp
+ GUIDialogVisualisationPresetList.cpp)
+
+set(HEADERS GUIDialogInfoProviderSettings.h
+ GUIDialogMusicInfo.h
+ GUIDialogMusicOSD.h
+ GUIDialogSongInfo.h
+ GUIDialogVisualisationPresetList.h)
+
+core_add_library(music_dialogs)
diff --git a/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp
new file mode 100644
index 0000000..a9e2232
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp
@@ -0,0 +1,479 @@
+/*
+ * 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 "GUIDialogInfoProviderSettings.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/AddonsDirectory.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/builtins/Builtins.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <limits.h>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace ADDON;
+
+const std::string SETTING_ALBUMSCRAPER_SETTINGS = "albumscrapersettings";
+const std::string SETTING_ARTISTSCRAPER_SETTINGS = "artistscrapersettings";
+const std::string SETTING_APPLYTOITEMS = "applysettingstoitems";
+
+CGUIDialogInfoProviderSettings::CGUIDialogInfoProviderSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_INFOPROVIDER_SETTINGS, "DialogSettings.xml")
+{ }
+
+bool CGUIDialogInfoProviderSettings::Show()
+{
+ CGUIDialogInfoProviderSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogInfoProviderSettings>(WINDOW_DIALOG_INFOPROVIDER_SETTINGS);
+ if (!dialog)
+ return false;
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ dialog->m_showSingleScraper = false;
+
+ // Get current default info provider settings from service broker
+ dialog->m_fetchInfo = settings->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO);
+
+ ADDON::AddonPtr defaultScraper;
+ // Get default album scraper (when enabled - can default scraper be disabled??)
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS,
+ defaultScraper))
+ {
+ ADDON::ScraperPtr scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper);
+ dialog->SetAlbumScraper(scraper);
+ }
+
+ // Get default artist scraper
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS,
+ defaultScraper))
+ {
+ ADDON::ScraperPtr scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper);
+ dialog->SetArtistScraper(scraper);
+ }
+
+ dialog->m_strArtistInfoPath = settings->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+
+ dialog->Open();
+
+ dialog->ResetDefaults();
+ return dialog->IsConfirmed();
+}
+
+int CGUIDialogInfoProviderSettings::Show(ADDON::ScraperPtr& scraper)
+{
+ CGUIDialogInfoProviderSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogInfoProviderSettings>(WINDOW_DIALOG_INFOPROVIDER_SETTINGS);
+ if (!dialog || !scraper)
+ return -1;
+ if (scraper->Content() != CONTENT_ARTISTS && scraper->Content() != CONTENT_ALBUMS)
+ return -1;
+
+ dialog->m_showSingleScraper = true;
+ dialog->m_singleScraperType = scraper->Content();
+
+ if (dialog->m_singleScraperType == CONTENT_ALBUMS)
+ dialog->SetAlbumScraper(scraper);
+ else
+ dialog->SetArtistScraper(scraper);
+ // toast selected but disabled scrapers
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(scraper->ID()))
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(24024), scraper->Name(), 2000, true);
+
+ dialog->Open();
+
+ bool confirmed = dialog->IsConfirmed();
+ unsigned int applyToItems = dialog->m_applyToItems;
+ if (confirmed)
+ {
+ if (dialog->m_singleScraperType == CONTENT_ALBUMS)
+ scraper = dialog->GetAlbumScraper();
+ else
+ {
+ scraper = dialog->GetArtistScraper();
+ // Save artist information folder (here not in the caller) when applying setting as default for all artists
+ if (applyToItems == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, dialog->m_strArtistInfoPath);
+ }
+ if (scraper)
+ scraper->SetPathSettings(dialog->m_singleScraperType, "");
+ }
+
+ dialog->ResetDefaults();
+
+ if (confirmed)
+ return applyToItems;
+ else
+ return -1;
+}
+
+void CGUIDialogInfoProviderSettings::OnInitWindow()
+{
+ CGUIDialogSettingsManualBase::OnInitWindow();
+}
+
+void CGUIDialogInfoProviderSettings::OnSettingChanged(
+ const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO)
+ {
+ m_fetchInfo = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ SetupView();
+ SetFocus(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO);
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER)
+ m_strArtistInfoPath = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_APPLYTOITEMS)
+ {
+ m_applyToItems = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ SetupView();
+ SetFocus(SETTING_APPLYTOITEMS);
+ }
+}
+
+void CGUIDialogInfoProviderSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER)
+ {
+ std::string currentScraperId;
+ if (m_albumscraper)
+ currentScraperId = m_albumscraper->ID();
+ std::string selectedAddonId = currentScraperId;
+
+ if (CGUIWindowAddonBrowser::SelectAddonID(AddonType::SCRAPER_ALBUMS, selectedAddonId, false) ==
+ 1 &&
+ selectedAddonId != currentScraperId)
+ {
+ AddonPtr scraperAddon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon,
+ OnlyEnabled::CHOICE_YES))
+ {
+ m_albumscraper = std::dynamic_pointer_cast<CScraper>(scraperAddon);
+ SetupView();
+ SetFocus(settingId);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not get settings for addon: {}", __FUNCTION__,
+ selectedAddonId);
+ }
+ }
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER)
+ {
+ std::string currentScraperId;
+ if (m_artistscraper)
+ currentScraperId = m_artistscraper->ID();
+ std::string selectedAddonId = currentScraperId;
+
+ if (CGUIWindowAddonBrowser::SelectAddonID(AddonType::SCRAPER_ARTISTS, selectedAddonId, false) ==
+ 1 &&
+ selectedAddonId != currentScraperId)
+ {
+ AddonPtr scraperAddon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon,
+ OnlyEnabled::CHOICE_YES))
+ {
+ m_artistscraper = std::dynamic_pointer_cast<CScraper>(scraperAddon);
+ SetupView();
+ SetFocus(settingId);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not get addon: {}", __FUNCTION__, selectedAddonId);
+ }
+ }
+ }
+ else if (settingId == SETTING_ALBUMSCRAPER_SETTINGS)
+ CGUIDialogAddonSettings::ShowForAddon(m_albumscraper, false);
+ else if (settingId == SETTING_ARTISTSCRAPER_SETTINGS)
+ CGUIDialogAddonSettings::ShowForAddon(m_artistscraper, false);
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER)
+ {
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+ std::string strDirectory = m_strArtistInfoPath;
+ if (!strDirectory.empty())
+ {
+ URIUtils::AddSlashAtEnd(strDirectory);
+ bool bIsSource;
+ if (CUtil::GetMatchingSource(strDirectory, shares, bIsSource) < 0) // path is outside shares - add it as a separate one
+ {
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13278);
+ share.strPath = strDirectory;
+ shares.push_back(share);
+ }
+ }
+ else
+ strDirectory = "default location";
+
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(20223), strDirectory, true))
+ {
+ if (!strDirectory.empty())
+ {
+ m_strArtistInfoPath = strDirectory;
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, strDirectory);
+ SetFocus(CSettings::CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ }
+ }
+ }
+}
+
+bool CGUIDialogInfoProviderSettings::Save()
+{
+ if (m_showSingleScraper)
+ return true; //Save done by caller of ::Show
+
+ // Save default settings for fetching additional information and art
+ CLog::Log(LOGINFO, "{} called", __FUNCTION__);
+ // Save Fetch addiitional info during update
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO, m_fetchInfo);
+ // Save default scrapers and addon setting values
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->ID());
+ m_albumscraper->SaveSettings();
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->ID());
+ m_artistscraper->SaveSettings();
+ // Save artist information folder
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_strArtistInfoPath);
+ settings->Save();
+
+ return true;
+}
+
+void CGUIDialogInfoProviderSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_strArtistInfoPath);
+
+ if (!m_showSingleScraper)
+ {
+ SetHeading(38330);
+ if (!m_fetchInfo)
+ {
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, false);
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, false);
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, false);
+ }
+ else
+ { // Album scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, true);
+ if (m_albumscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_albumscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->Name());
+ if (m_albumscraper && m_albumscraper->HasSettings())
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ // Artist scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, true);
+ if (m_artistscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_artistscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->Name());
+ if (m_artistscraper && m_artistscraper->HasSettings())
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ // Artist Information Folder
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, true);
+ }
+ }
+ else if (m_singleScraperType == CONTENT_ALBUMS)
+ {
+ SetHeading(38331);
+ // Album scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, true);
+ if (m_albumscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_albumscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->Name());
+ if (m_albumscraper && m_albumscraper->HasSettings())
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ }
+ else
+ {
+ SetHeading(38332);
+ // Artist scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, true);
+ if (m_artistscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_artistscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->Name());
+ if (m_artistscraper && m_artistscraper->HasSettings())
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ // Artist Information Folder when default settings
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_applyToItems == INFOPROVIDER_DEFAULT);
+ }
+}
+
+void CGUIDialogInfoProviderSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ std::shared_ptr<CSettingCategory> category = AddCategory("infoprovidersettings", -1);
+ if (category == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__);
+ return;
+ }
+ std::shared_ptr<CSettingGroup> group1 = AddGroup(category);
+ if (group1 == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__);
+ return;
+ }
+
+ if (!m_showSingleScraper)
+ {
+ AddToggle(group1, CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO, 38333, SettingLevel::Basic, m_fetchInfo); // "Fetch additional information during scan"
+ }
+ else
+ {
+ TranslatableIntegerSettingOptions entries;
+ entries.clear();
+ if (m_singleScraperType == CONTENT_ALBUMS)
+ {
+ entries.push_back(TranslatableIntegerSettingOption(38066, INFOPROVIDER_THISITEM));
+ entries.push_back(TranslatableIntegerSettingOption(38067, INFOPROVIDER_ALLVIEW));
+ }
+ else
+ {
+ entries.push_back(TranslatableIntegerSettingOption(38064, INFOPROVIDER_THISITEM));
+ entries.push_back(TranslatableIntegerSettingOption(38065, INFOPROVIDER_ALLVIEW));
+ }
+ entries.push_back(TranslatableIntegerSettingOption(38063, INFOPROVIDER_DEFAULT));
+ AddList(group1, SETTING_APPLYTOITEMS, 38338, SettingLevel::Basic, m_applyToItems, entries, 38339); // "Apply settings to"
+ }
+
+ std::shared_ptr<CSettingGroup> group = AddGroup(category, 38337);
+ if (group == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__);
+ return;
+ }
+ std::shared_ptr<CSettingAction> subsetting;
+ if (!m_showSingleScraper || m_singleScraperType == CONTENT_ALBUMS)
+ {
+ AddButton(group, CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, 38334, SettingLevel::Basic); //Provider for album information
+ subsetting = AddButton(group, SETTING_ALBUMSCRAPER_SETTINGS, 10004, SettingLevel::Basic); //"settings"
+ if (subsetting)
+ subsetting->SetParent(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER);
+ }
+ if (!m_showSingleScraper || m_singleScraperType == CONTENT_ARTISTS)
+ {
+ AddButton(group, CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, 38335, SettingLevel::Basic); //Provider for artist information
+ subsetting = AddButton(group, SETTING_ARTISTSCRAPER_SETTINGS, 10004, SettingLevel::Basic); //"settings"
+ if (subsetting)
+ subsetting->SetParent(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER);
+
+ AddButton(group, CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, 38336, SettingLevel::Basic);
+ }
+}
+
+void CGUIDialogInfoProviderSettings::SetLabel2(const std::string &settingid, const std::string &label)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_LABEL2(settingControl->GetID(), label);
+}
+
+void CGUIDialogInfoProviderSettings::ToggleState(const std::string &settingid, bool enabled)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ if (enabled)
+ CONTROL_ENABLE(settingControl->GetID());
+ else
+ CONTROL_DISABLE(settingControl->GetID());
+ }
+}
+
+void CGUIDialogInfoProviderSettings::SetFocus(const std::string &settingid)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_FOCUS(settingControl->GetID(), 0);
+}
+
+void CGUIDialogInfoProviderSettings::ResetDefaults()
+{
+ m_showSingleScraper = false;
+ m_singleScraperType = CONTENT_NONE;
+ m_applyToItems = INFOPROVIDER_THISITEM;
+}
diff --git a/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h
new file mode 100644
index 0000000..56a33b5
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "addons/Addon.h"
+#include "addons/Scraper.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <map>
+#include <utility>
+
+class CFileItemList;
+
+// Enumeration of what combination of items to apply the scraper settings
+enum INFOPROVIDERAPPLYOPTIONS
+{
+ INFOPROVIDER_DEFAULT = 0x0000,
+ INFOPROVIDER_ALLVIEW = 0x0001,
+ INFOPROVIDER_THISITEM = 0x0002
+};
+
+class CGUIDialogInfoProviderSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogInfoProviderSettings();
+
+ // specialization of CGUIWindow
+ bool HasListItems() const override { return true; }
+
+ const ADDON::ScraperPtr& GetAlbumScraper() const { return m_albumscraper; }
+ void SetAlbumScraper(ADDON::ScraperPtr scraper) { m_albumscraper = std::move(scraper); }
+ const ADDON::ScraperPtr& GetArtistScraper() const { return m_artistscraper; }
+ void SetArtistScraper(ADDON::ScraperPtr scraper) { m_artistscraper = std::move(scraper); }
+
+ /*! \brief Show dialog to change information provider for either artists or albums (not both).
+ Has a list to select how settings are to be applied - as system default, to just current item or to all the filtered items on the node.
+ This does not save the settings itself, that is left to the caller
+ \param scraper [in/out] the selected scraper addon and settings. Scraper content must be artists or albums.
+ \return 0 settings apply as system default, 1 to all items on node, 2 to just the selected item or -1 if dialog cancelled or error occurs
+ */
+ static int Show(ADDON::ScraperPtr& scraper);
+
+ /*! \brief Show dialog to change the music scraping settings including default information providers for both artists or albums.
+ This saves the settings when the dialog is confirmed.
+ \return true if the dialog is confirmed, false otherwise
+ */
+ static bool Show();
+
+protected:
+ // specializations of CGUIWindow
+ void OnInitWindow() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ void SetLabel2(const std::string &settingid, const std::string &label);
+ void ToggleState(const std::string &settingid, bool enabled);
+ using CGUIDialogSettingsManualBase::SetFocus;
+ void SetFocus(const std::string &settingid);
+ void ResetDefaults();
+
+ /*!
+ * @brief The currently selected album scraper
+ */
+ ADDON::ScraperPtr m_albumscraper;
+ /*!
+ * @brief The currently selected artist scraper
+ */
+ ADDON::ScraperPtr m_artistscraper;
+
+ std::string m_strArtistInfoPath;
+ bool m_showSingleScraper = false;
+ CONTENT_TYPE m_singleScraperType = CONTENT_NONE;
+ bool m_fetchInfo;
+ unsigned int m_applyToItems = INFOPROVIDER_THISITEM;
+};
diff --git a/xbmc/music/dialogs/GUIDialogMusicInfo.cpp b/xbmc/music/dialogs/GUIDialogMusicInfo.cpp
new file mode 100644
index 0000000..a4b282a
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicInfo.cpp
@@ -0,0 +1,1043 @@
+/*
+ * Copyright (C) 2005-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 "GUIDialogMusicInfo.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory/QueryParams.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/MusicUtils.h"
+#include "music/dialogs/GUIDialogSongInfo.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/windows/GUIWindowMusicBase.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/ProgressJob.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+using namespace MUSIC_INFO;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTN_REFRESH 6
+#define CONTROL_USERRATING 7
+#define CONTROL_BTN_PLAY 8
+#define CONTROL_BTN_GET_THUMB 10
+#define CONTROL_ARTISTINFO 12
+
+#define CONTROL_LIST 50
+
+#define TIME_TO_BUSY_DIALOG 500
+
+class CGetInfoJob : public CJob
+{
+public:
+ ~CGetInfoJob(void) override = default;
+
+ // Fetch full album/artist information including art types list
+ bool DoWork() override
+ {
+ CGUIDialogMusicInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (!dialog)
+ return false;
+ if (dialog->IsCancelled())
+ return false;
+ CFileItemPtr m_item = dialog->GetCurrentListItem();
+ CMusicInfoTag& tag = *m_item->GetMusicInfoTag();
+
+ CMusicDatabase database;
+ database.Open();
+ // May only have partially populated music item, so fetch all artist or album data from db
+ if (tag.GetType() == MediaTypeArtist)
+ {
+ int artistId = tag.GetDatabaseId();
+ CArtist artist;
+ if (!database.GetArtist(artistId, artist))
+ return false;
+ tag.SetArtist(artist);
+ CMusicDatabase::SetPropertiesFromArtist(*m_item, artist);
+ m_item->SetLabel(artist.strArtist);
+
+ // Get artist folder where local art could be found
+ // Get the *name* of the folder for this artist within the Artist Info folder (may not exist).
+ // If there is no Artist Info folder specified in settings this will be blank
+ database.GetArtistPath(artist, artist.strPath);
+ // Get the old location for those album artists with a unique folder (local to music files)
+ // If there is no folder for the artist and *only* the artist this will be blank
+ std::string oldartistpath;
+ bool oldpathfound = database.GetOldArtistPath(artist.idArtist, oldartistpath);
+
+ // Set up path for *item folder when browsing for art, by default this is
+ // in the Artist Info Folder (when it exists), but could end up blank
+ std::string artistItemPath = artist.strPath;
+ if (!CDirectory::Exists(artistItemPath))
+ {
+ // Fall back local to music files (historic location for those album artists with a unique folder)
+ // although there may not be such a unique folder for the arist
+ if (oldpathfound)
+ artistItemPath = oldartistpath;
+ else
+ // Fall back further to browse the Artist Info Folder itself
+ artistItemPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ }
+ m_item->SetPath(artistItemPath);
+
+ // Store info as CArtist as well as item properties
+ dialog->SetArtist(artist, oldartistpath);
+
+ // Fetch artist discography as scraped from online sources, but always
+ // include all the albums in the music library
+ dialog->SetDiscography(database);
+ }
+ else
+ {
+ // tag.GetType == MediaTypeAlbum
+ int albumId = tag.GetDatabaseId();
+ CAlbum album;
+ if (!database.GetAlbum(albumId, album))
+ return false;
+ tag.SetAlbum(album);
+ CMusicDatabase::SetPropertiesFromAlbum(*m_item, album);
+
+ // Get album folder where local art could be found
+ database.GetAlbumPath(albumId, album.strPath);
+ // Set up path for *item folder when browsing for art
+ m_item->SetPath(album.strPath);
+ // Store info as CAlbum as well as item properties
+ dialog->SetAlbum(album, album.strPath);
+
+ // Set the list of songs and related art
+ dialog->SetSongs(album.songs);
+ }
+ database.Close();
+
+ /*
+ Load current art (to CGUIListItem.m_art)
+ For albums this includes related artist(s) art and artist fanart set as
+ fallback album fanart.
+ Clear item art first to ensure fresh not cached/partial art
+ */
+ m_item->ClearArt();
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_item.get());
+
+ // Fill vector of possible art types with current art, when it exists,
+ // for display on the art type selection dialog
+ CFileItemList artlist;
+ MUSIC_UTILS::FillArtTypesList(*m_item, artlist);
+ dialog->SetArtTypeList(artlist);
+ if (dialog->IsCancelled())
+ return false;
+
+ // Tell waiting MusicDialog that job is complete
+ dialog->FetchComplete();
+
+ return true;
+ }
+};
+
+class CSetUserratingJob : public CJob
+{
+ int idAlbum;
+ int iUserrating;
+public:
+ CSetUserratingJob(int albumId, int userrating) :
+ idAlbum(albumId),
+ iUserrating(userrating)
+ { }
+
+ ~CSetUserratingJob(void) override = default;
+
+ bool DoWork(void) override
+ {
+ // Asynchronously update userrating in library
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ db.SetAlbumUserrating(idAlbum, iUserrating);
+ db.Close();
+ }
+
+ return true;
+ }
+};
+
+class CRefreshInfoJob : public CProgressJob
+{
+public:
+ CRefreshInfoJob(CGUIDialogProgress* progressDialog)
+ : CProgressJob(nullptr)
+ {
+ if (progressDialog)
+ SetProgressIndicators(nullptr, progressDialog);
+ SetAutoClose(true);
+ }
+
+ ~CRefreshInfoJob(void) override = default;
+
+ // Refresh album/artist information including art types list
+ bool DoWork() override
+ {
+ CGUIDialogMusicInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (!dialog)
+ return false;
+ if (dialog->IsCancelled())
+ return false;
+ CFileItemPtr m_item = dialog->GetCurrentListItem();
+ CMusicInfoTag& tag = *m_item->GetMusicInfoTag();
+ CArtist& m_artist = dialog->GetArtist();
+ CAlbum& m_album = dialog->GetAlbum();
+
+ CGUIDialogProgress* dlgProgress = GetProgressDialog();
+ CMusicDatabase database;
+ database.Open();
+ if (tag.GetType() == MediaTypeArtist)
+ {
+ ADDON::ScraperPtr scraper;
+ if (!database.GetScraper(m_artist.idArtist, CONTENT_ARTISTS, scraper))
+ return false;
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ database.ClearArtistLastScrapedTime(m_artist.idArtist);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ CMusicInfoScanner scanner;
+ if (scanner.UpdateArtistInfo(m_artist, scraper, true, dlgProgress) != CInfoScanner::INFO_ADDED)
+ return false;
+ else
+ // Tell info dialog, so can show message
+ dialog->SetScrapedInfo(true);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ //That changed DB and m_artist, now update dialog item with new info and art
+ tag.SetArtist(m_artist);
+ CMusicDatabase::SetPropertiesFromArtist(*m_item, m_artist);
+
+ // Fetch artist discography as scraped from online sources, but always
+ // include all the albums in the music library
+ dialog->SetDiscography(database);
+ }
+ else
+ {
+ // tag.GetType == MediaTypeAlbum
+ ADDON::ScraperPtr scraper;
+ if (!database.GetScraper(m_album.idAlbum, CONTENT_ALBUMS, scraper))
+ return false;
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ database.ClearAlbumLastScrapedTime(m_album.idAlbum);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ CMusicInfoScanner scanner;
+ if (scanner.UpdateAlbumInfo(m_album, scraper, true, GetProgressDialog()) != CInfoScanner::INFO_ADDED)
+ return false;
+ else
+ // Tell info dialog, so can show message
+ dialog->SetScrapedInfo(true);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ //That changed DB and m_album, now update dialog item with new info and art
+ // Album songs are unchanged by refresh (even with Musicbrainz sync?)
+ tag.SetAlbum(m_album);
+ CMusicDatabase::SetPropertiesFromAlbum(*m_item, m_album);
+
+ // Set the list of songs and related art
+ dialog->SetSongs(m_album.songs);
+ }
+ database.Close();
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ /*
+ Load current art (to CGUIListItem.m_art)
+ For albums this includes related artist(s) art and artist fanart set as
+ fallback album fanart.
+ Clear item art first to ensure fresh not cached/partial art
+ */
+ m_item->ClearArt();
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_item.get());
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ // Fill vector of possible art types with current art, when it exists,
+ // for display on the art type selection dialog
+ CFileItemList artlist;
+ MUSIC_UTILS::FillArtTypesList(*m_item, artlist);
+ dialog->SetArtTypeList(artlist);
+ if (dialog->IsCancelled())
+ return false;
+
+ // Tell waiting MusicDialog that job is complete
+ MarkFinished();
+ return true;
+ }
+};
+
+CGUIDialogMusicInfo::CGUIDialogMusicInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_MUSIC_INFO, "DialogMusicInfo.xml"),
+ m_albumSongs(new CFileItemList),
+ m_item(new CFileItem),
+ m_artTypeList(new CFileItemList)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogMusicInfo::~CGUIDialogMusicInfo(void)
+{
+}
+
+bool CGUIDialogMusicInfo::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_artTypeList->Clear();
+ // For albums update user rating if it has changed
+ if (!m_bArtistInfo && m_startUserrating != m_item->GetMusicInfoTag()->GetUserrating())
+ {
+ m_hasUpdatedUserrating = true;
+
+ // Asynchronously update song userrating in library
+ CSetUserratingJob *job = new CSetUserratingJob(m_item->GetMusicInfoTag()->GetAlbumId(),
+ m_item->GetMusicInfoTag()->GetUserrating());
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+ }
+ if (m_hasRefreshed || m_hasUpdatedUserrating)
+ {
+ // Send a message to all windows to tell them to update the item.
+ // This communicates changes to the music lib window.
+ // The music lib window item is updated to but changes to the rating when it is the sort
+ // do not show on screen until refresh() that fetches the list from scratch, sorts etc.
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST);
+ OnMessage(msg);
+ m_albumSongs->Clear();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIDialog::OnMessage(message);
+ Update();
+ m_cancelled = false;
+ return true;
+ }
+ break;
+
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_USERRATING)
+ {
+ OnSetUserrating();
+ }
+ else if (iControl == CONTROL_BTN_REFRESH)
+ {
+ RefreshInfo();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_GET_THUMB)
+ {
+ OnGetArt();
+ return true;
+ }
+ else if (iControl == CONTROL_ARTISTINFO)
+ {
+ if (!m_bArtistInfo)
+ OnArtistInfo(m_album.artistCredits[0].GetArtistId());
+ return true;
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ int iAction = message.GetParam1();
+ if (m_bArtistInfo && (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction))
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ int iItem = msg.GetParam1();
+ int id = -1;
+ if (iItem >= 0 && iItem < m_albumSongs->Size())
+ id = m_albumSongs->Get(iItem)->GetMusicInfoTag()->GetDatabaseId();
+ if (id > 0)
+ {
+ OnAlbumInfo(id);
+ return true;
+ }
+ }
+ }
+ else if (iControl == CONTROL_BTN_PLAY)
+ {
+ if (m_album.idAlbum >= 0)
+ {
+ // Play album
+ const std::string path = StringUtils::Format("musicdb://albums/{}", m_album.idAlbum);
+ OnPlayItem(std::make_shared<CFileItem>(path, m_album));
+ return true;
+ }
+ else
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ const int iItem = msg.GetParam1();
+ if (iItem >= 0 && iItem < m_albumSongs->Size())
+ {
+ // Play selected song
+ OnPlayItem(m_albumSongs->Get(iItem));
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogMusicInfo::OnAction(const CAction &action)
+{
+ int userrating = m_item->GetMusicInfoTag()->GetUserrating();
+ if (action.GetID() == ACTION_INCREASE_RATING)
+ {
+ SetUserrating(userrating + 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_DECREASE_RATING)
+ {
+ SetUserrating(userrating - 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogMusicInfo::SetItem(CFileItem* item)
+{
+ *m_item = *item;
+ m_event.Reset();
+ m_cancelled = false; // Happens before win_init
+
+ // In a separate job fetch info and fill list of art types.
+ int jobid =
+ CServiceBroker::GetJobManager()->AddJob(new CGetInfoJob(), nullptr, CJob::PRIORITY_LOW);
+
+ // Wait to get all data before show, allowing user to cancel if fetch is slow
+ if (!CGUIDialogBusy::WaitOnEvent(m_event, TIME_TO_BUSY_DIALOG))
+ {
+ // Cancel job still waiting in queue (unlikely)
+ CServiceBroker::GetJobManager()->CancelJob(jobid);
+ // Flag to stop job already in progress
+ m_cancelled = true;
+ return false;
+ }
+
+ return true;
+}
+
+void CGUIDialogMusicInfo::SetAlbum(const CAlbum& album, const std::string &path)
+{
+ m_album = album;
+ m_item->SetPath(album.strPath);
+
+ m_startUserrating = m_album.iUserrating;
+ m_fallbackartpath.clear();
+ m_bArtistInfo = false;
+ m_hasUpdatedUserrating = false;
+ m_hasRefreshed = false;
+}
+
+void CGUIDialogMusicInfo::SetArtist(const CArtist& artist, const std::string &path)
+{
+ m_artist = artist;
+ m_fallbackartpath = path;
+ m_bArtistInfo = true;
+ m_hasRefreshed = false;
+}
+
+void CGUIDialogMusicInfo::SetSongs(const VECSONGS &songs) const
+{
+ m_albumSongs->Clear();
+ CMusicThumbLoader loader;
+ for (unsigned int i = 0; i < songs.size(); i++)
+ {
+ const CSong& song = songs[i];
+ CFileItemPtr item(new CFileItem(song));
+ // Load the song art and related artist(s) (that may be different from album artist) art
+ loader.LoadItem(item.get());
+ m_albumSongs->Add(item);
+ }
+}
+
+void CGUIDialogMusicInfo::SetDiscography(CMusicDatabase& database) const
+{
+ m_albumSongs->Clear();
+ database.GetArtistDiscography(m_artist.idArtist, *m_albumSongs);
+ CMusicThumbLoader loader;
+ for (const auto& item : *m_albumSongs)
+ {
+ // Load all the album art and related artist(s) art (could be other collaborating artists)
+ loader.LoadItem(item.get());
+ if (item->GetMusicInfoTag()->GetDatabaseId() == -1)
+ item->SetArt("thumb", "DefaultAlbumCover.png");
+ }
+}
+
+void CGUIDialogMusicInfo::Update()
+{
+ if (m_bArtistInfo)
+ {
+ SET_CONTROL_HIDDEN(CONTROL_ARTISTINFO);
+ SET_CONTROL_HIDDEN(CONTROL_USERRATING);
+
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_albumSongs.get());
+ OnMessage(message);
+
+ }
+ else
+ {
+ SET_CONTROL_VISIBLE(CONTROL_ARTISTINFO);
+ SET_CONTROL_VISIBLE(CONTROL_USERRATING);
+
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_albumSongs.get());
+ OnMessage(message);
+
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // Disable the Choose Art button if the user isn't allowed it
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB,
+ profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser);
+}
+
+void CGUIDialogMusicInfo::SetLabel(int iControl, const std::string& strLabel)
+{
+ if (strLabel.empty())
+ {
+ SET_CONTROL_LABEL(iControl, 416);
+ }
+ else
+ {
+ SET_CONTROL_LABEL(iControl, strLabel);
+ }
+}
+
+void CGUIDialogMusicInfo::OnInitWindow()
+{
+ SET_CONTROL_LABEL(CONTROL_BTN_REFRESH, 184);
+ SET_CONTROL_LABEL(CONTROL_USERRATING, 38023);
+ SET_CONTROL_LABEL(CONTROL_BTN_GET_THUMB, 13511);
+ SET_CONTROL_LABEL(CONTROL_ARTISTINFO, 21891);
+ SET_CONTROL_LABEL(CONTROL_BTN_PLAY, 208);
+
+ if (m_bArtistInfo)
+ {
+ SET_CONTROL_HIDDEN(CONTROL_ARTISTINFO);
+ SET_CONTROL_HIDDEN(CONTROL_USERRATING);
+ SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY);
+ }
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogMusicInfo::FetchComplete()
+{
+ //Trigger the event to indicate data has been fetched
+ m_event.Set();
+}
+
+
+void CGUIDialogMusicInfo::RefreshInfo()
+{
+ // Double check we have permission (button should be hidden when not)
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if (!profileManager->GetCurrentProfile().canWriteDatabases() && !g_passwordManager.bMasterUser)
+ return;
+
+ // Check if scanning
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{ 189 }, CVariant{ 14057 });
+ return;
+ }
+
+ CGUIDialogProgress* dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (!dlgProgress)
+ return;
+
+ if (m_bArtistInfo)
+ { // Show dialog box indicating we're searching for the artist
+ dlgProgress->SetHeading(CVariant{ 21889 });
+ dlgProgress->SetLine(0, CVariant{ m_artist.strArtist });
+ dlgProgress->SetLine(1, CVariant{ "" });
+ dlgProgress->SetLine(2, CVariant{ "" });
+ }
+ else
+ { // Show dialog box indicating we're searching for the album
+ dlgProgress->SetHeading(CVariant{ 185 });
+ dlgProgress->SetLine(0, CVariant{ m_album.strAlbum });
+ dlgProgress->SetLine(1, CVariant{ m_album.strArtistDesc });
+ dlgProgress->SetLine(2, CVariant{ "" });
+ }
+ dlgProgress->Open();
+
+ SetScrapedInfo(false);
+ // Start separate job to scrape info and fill list of art types.
+ CServiceBroker::GetJobManager()->AddJob(new CRefreshInfoJob(dlgProgress), nullptr,
+ CJob::PRIORITY_HIGH);
+
+ // Wait for refresh to complete or be canceled, but render every 10ms so that the
+ // pointer movements works on dialog even when job is reporting progress infrequently
+ if (dlgProgress)
+ dlgProgress->Wait(10);
+
+ if (dlgProgress->IsCanceled())
+ {
+ return;
+ }
+
+ // Show message when scraper was unsuccessful
+ if (!HasScrapedInfo())
+ {
+ if (m_bArtistInfo)
+ HELPERS::ShowOKDialogText(CVariant{ 21889 }, CVariant{ 20199 });
+ else
+ HELPERS::ShowOKDialogText(CVariant{ 185 }, CVariant{ 500 });
+ return;
+ }
+
+ // Show new values on screen
+ Update();
+ m_hasRefreshed = true;
+
+ if (dlgProgress)
+ dlgProgress->Close();
+}
+
+void CGUIDialogMusicInfo::SetUserrating(int userrating) const
+{
+ userrating = std::max(userrating, 0);
+ userrating = std::min(userrating, 10);
+ if (userrating != m_item->GetMusicInfoTag()->GetUserrating())
+ {
+ m_item->GetMusicInfoTag()->SetUserrating(userrating);
+ }
+}
+
+void CGUIDialogMusicInfo::OnAlbumInfo(int id)
+{
+ // Switch to show album info for given album ID
+ // Close current (artist) dialog to save art changes
+ Close(true);
+ ShowForAlbum(id);
+}
+
+void CGUIDialogMusicInfo::OnArtistInfo(int id)
+{
+ // Switch to show artist info for given artist ID
+ // Close current (album) dialog to save art and rating changes
+ Close(true);
+ ShowForArtist(id);
+}
+
+CFileItemPtr CGUIDialogMusicInfo::GetCurrentListItem(int offset)
+{
+ return m_item;
+}
+
+std::string CGUIDialogMusicInfo::GetContent()
+{
+ if (m_item->GetMusicInfoTag()->GetType() == MediaTypeArtist)
+ return "artists";
+ else
+ return "albums";
+}
+
+void CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item)
+{
+ std::string itemDir;
+ std::string artistFolder;
+
+ itemDir = item.GetPath();
+ if (item.HasMusicInfoTag())
+ {
+ if (item.GetMusicInfoTag()->GetType() == MediaTypeSong)
+ itemDir = URIUtils::GetParentPath(item.GetMusicInfoTag()->GetURL());
+
+ // For artist add Artist Info Folder path to browser sources
+ if (item.GetMusicInfoTag()->GetType() == MediaTypeArtist)
+ {
+ artistFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (!artistFolder.empty() && artistFolder.compare(itemDir) == 0)
+ itemDir.clear(); // skip *item when artist not have a unique path
+ }
+ }
+ // Add "*Item folder" path to file browser sources
+ if (!itemDir.empty() && CDirectory::Exists(itemDir))
+ {
+ CMediaSource itemSource;
+ itemSource.strName = g_localizeStrings.Get(36041);
+ itemSource.strPath = itemDir;
+ sources.push_back(itemSource);
+ }
+
+ // For artist add Artist Info Folder path to browser sources
+ if (!artistFolder.empty() && CDirectory::Exists(artistFolder))
+ {
+ CMediaSource itemSource;
+ itemSource.strName = "* " + g_localizeStrings.Get(20223);
+ itemSource.strPath = artistFolder;
+ sources.push_back(itemSource);
+ }
+}
+
+void CGUIDialogMusicInfo::SetArtTypeList(CFileItemList& artlist)
+{
+ m_artTypeList->Clear();
+ m_artTypeList->Copy(artlist);
+}
+
+/*
+Allow user to choose artwork for the artist or album
+For each type of art the options are:
+1. Current art
+2. Local art (thumb found by filename)
+3. Remote art (scraped list of urls from online sources e.g. fanart.tv)
+5. Embedded art (@todo)
+6. None
+*/
+void CGUIDialogMusicInfo::OnGetArt()
+{
+ std::string type = MUSIC_UTILS::ShowSelectArtTypeDialog(*m_artTypeList);
+ if (type.empty())
+ return; // Cancelled
+
+ CFileItemList items;
+ CGUIListItem::ArtMap primeArt = m_item->GetArt(); // art without fallbacks
+ bool bHasArt = m_item->HasArt(type);
+ bool bFallback(false);
+ if (bHasArt)
+ {
+ // Check if that type of art is actually a fallback, e.g. artist fanart
+ CGUIListItem::ArtMap::const_iterator i = primeArt.find(type);
+ bFallback = (i == primeArt.end());
+ }
+
+ // Build list of possible images of that art type
+ if (bHasArt)
+ {
+ // Add item for current artwork
+ // For album it could be a fallback from artist
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", m_item->GetArt(type));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13512));
+ items.Add(item);
+ }
+
+ // Grab the thumbnails of this art type scraped from the web
+ std::vector<std::string> remotethumbs;
+ // Art type is encoded into the scraper XML as optional "aspect=" field
+ // Type "thumb" returns URLs for all types of art including those without aspect.
+ // Those URL without aspect are also returned for all other type values.
+ if (m_bArtistInfo)
+ m_artist.thumbURL.GetThumbUrls(remotethumbs, type);
+ else
+ m_album.thumbURL.GetThumbUrls(remotethumbs, type);
+
+ for (unsigned int i = 0; i < remotethumbs.size(); ++i)
+ {
+ std::string strItemPath;
+ strItemPath = StringUtils::Format("thumb://Remote{}", i);
+ CFileItemPtr item(new CFileItem(strItemPath, false));
+ item->SetArt("thumb", remotethumbs[i]);
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13513));
+
+ items.Add(item);
+ }
+
+ // Local art
+ std::string localArt;
+ std::vector<std::string> paths;
+ if (m_bArtistInfo)
+ {
+ // Individual artist subfolder within the Artist Information Folder
+ paths.emplace_back(m_artist.strPath);
+ // Fallback local to music files (when there is a unique folder)
+ paths.emplace_back(m_fallbackartpath);
+ }
+ else
+ // Album folder, when a unique one exists, no fallback
+ paths.emplace_back(m_album.strPath);
+ for (const auto& path : paths)
+ {
+ if (!localArt.empty() && CFileUtils::Exists(localArt))
+ break;
+ if (!path.empty())
+ {
+ CFileItem item(path, true);
+ if (type == "thumb")
+ // Local music thumbnail images named by <musicthumbs>
+ localArt = item.GetUserMusicThumb(true);
+ else
+ { // Check case and ext insenitively for local images with type as name
+ // e.g. <arttype>.jpg
+ CFileItemList items;
+ CDirectory::GetDirectory(path, items,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+
+ for (int j = 0; j < items.Size(); j++)
+ {
+ std::string strCandidate = URIUtils::GetFileName(items[j]->GetPath());
+ URIUtils::RemoveExtension(strCandidate);
+ if (StringUtils::EqualsNoCase(strCandidate, type))
+ {
+ localArt = items[j]->GetPath();
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!localArt.empty() && CFileUtils::Exists(localArt))
+ {
+ CFileItemPtr item(new CFileItem("Local Art: " + localArt, false));
+ item->SetArt("thumb", localArt);
+ item->SetLabel(g_localizeStrings.Get(13514)); // "Local art"
+ items.Add(item);
+ }
+
+ // No art
+ if (bHasArt && !bFallback)
+ { // Actually has this type of art (not a fallback) so
+ // allow the user to delete it by selecting "no art".
+ CFileItemPtr item(new CFileItem("thumb://None", false));
+ if (m_bArtistInfo)
+ item->SetArt("icon", "DefaultArtist.png");
+ else
+ item->SetArt("icon", "DefaultAlbumCover.png");
+ item->SetLabel(g_localizeStrings.Get(13515));
+ items.Add(item);
+ }
+
+ //! @todo: Add support for extracting embedded art from song files to use for album
+
+ // Clear local images of this type from cache so user will see any recent
+ // local file changes immediately
+ for (auto& item : items)
+ {
+ // Skip images from remote sources, recache done by refresh (could be slow)
+ if (StringUtils::StartsWith(item->GetPath(), "thumb://Remote"))
+ continue;
+ std::string thumb(item->GetArt("thumb"));
+ if (thumb.empty())
+ continue;
+ CURL url(CTextureUtils::UnwrapImageURL(thumb));
+ // Skip images from remote sources (current thumb could be remote)
+ if (url.IsProtocol("http") || url.IsProtocol("https"))
+ continue;
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ // Remove any thumbnail of local image too (created when browsing files)
+ std::string thumbthumb(CTextureUtils::GetWrappedThumbURL(thumb));
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumbthumb);
+ }
+
+ // Show list of possible art for user selection
+ // Note that during selection thumbs of *all* images shown are cached. When
+ // browsing folders there could be many irrelevant thumbs cached that are
+ // never used by Kodi again, but there is no obvious way to clear these
+ // thumbs from the cache automatically.
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+ CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, *m_item);
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) &&
+ result != "thumb://Current")
+ {
+ // User didn't choose the one they have.
+ // Overwrite with the new art or clear it
+ std::string newArt;
+ if (StringUtils::StartsWith(result, "thumb://Remote"))
+ {
+ int number = atoi(result.substr(14).c_str());
+ newArt = remotethumbs[number];
+ }
+ else if (result == "thumb://Thumb")
+ newArt = m_item->GetArt("thumb");
+ else if (StringUtils::StartsWith(result, "Local Art: "))
+ newArt = localArt;
+ else if (CFileUtils::Exists(result))
+ newArt = result;
+ else // none
+ newArt.clear();
+
+ // Asynchronously update that type of art in the database and then
+ // refresh artist, album and fallback art of currently playing song
+ MUSIC_UTILS::UpdateArtJob(m_item, type, newArt);
+
+ // Update local item and art list with current art
+ m_item->SetArt(type, newArt);
+ for (const auto& artitem : *m_artTypeList)
+ {
+ if (artitem->GetProperty("artType") == type)
+ {
+ artitem->SetArt("thumb", newArt);
+ break;
+ }
+ }
+
+ // Get new artwork to show in other places e.g. on music lib window, but this does
+ // not update artist, album or fallback art for the currently playing song as it
+ // is a different item with different ID and media type
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ // Re-open the art type selection dialog as we come back from
+ // the image selection dialog
+ OnGetArt();
+}
+
+void CGUIDialogMusicInfo::OnSetUserrating() const
+{
+ int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_item->GetMusicInfoTag()->GetUserrating());
+ if (userrating < 0) // Nothing selected, so rating unchanged
+ return;
+
+ SetUserrating(userrating);
+}
+
+
+void CGUIDialogMusicInfo::ShowForAlbum(int idAlbum)
+{
+ std::string path = StringUtils::Format("musicdb://albums/{}", idAlbum);
+ CFileItem item(path, true); // An album, but IsAlbum() not set as didn't use SetAlbum()
+ ShowFor(&item);
+}
+
+void CGUIDialogMusicInfo::ShowForArtist(int idArtist)
+{
+ std::string path = StringUtils::Format("musicdb://artists/{}", idArtist);
+ CFileItem item(path, true);
+ ShowFor(&item);
+}
+
+void CGUIDialogMusicInfo::ShowFor(CFileItem* pItem)
+{
+ if (pItem->IsParentFolder() || URIUtils::IsSpecial(pItem->GetPath()) ||
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "musicsearch://"))
+ return; // nothing to do
+
+ if (!pItem->m_bIsFolder)
+ { // Show Song information dialog
+ CGUIDialogSongInfo::ShowFor(pItem);
+ return;
+ }
+
+ CFileItem musicitem("musicdb://", true);
+
+ // We have a folder album/artist info dialog only shown for db items
+ // or for music video with artist/album in music library
+ if (pItem->IsMusicDb())
+ {
+ if (!pItem->HasMusicInfoTag() || pItem->GetMusicInfoTag()->GetDatabaseId() < 1)
+ {
+ // Maybe only path is set, then set MusicInfoTag
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(pItem->GetPath(), params);
+ if (params.GetArtistId() > 0)
+ pItem->GetMusicInfoTag()->SetDatabaseId(params.GetArtistId(), MediaTypeArtist);
+ else if (params.GetAlbumId() > 0)
+ pItem->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum);
+ else
+ return; // nothing to do
+ }
+ musicitem.SetFromMusicInfoTag(*pItem->GetMusicInfoTag());
+ }
+ else if (pItem->HasProperty("artist_musicid"))
+ {
+ musicitem.GetMusicInfoTag()->SetDatabaseId(pItem->GetProperty("artist_musicid").asInteger32(),
+ MediaTypeArtist);
+ }
+ else if (pItem->HasProperty("album_musicid"))
+ {
+ musicitem.GetMusicInfoTag()->SetDatabaseId(pItem->GetProperty("album_musicid").asInteger32(),
+ MediaTypeAlbum);
+ }
+ else
+ return; // nothing to do
+
+
+ CGUIDialogMusicInfo *pDlgMusicInfo = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (pDlgMusicInfo)
+ {
+ if (pDlgMusicInfo->SetItem(&musicitem))
+ {
+ pDlgMusicInfo->Open();
+ if (pItem->GetMusicInfoTag()->GetType() == MediaTypeAlbum &&
+ pDlgMusicInfo->HasUpdatedUserrating())
+ {
+ auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowMusicBase>(WINDOW_MUSIC_NAV);
+ if (window)
+ window->RefreshContent("albums");
+ }
+ }
+ }
+}
+
+void CGUIDialogMusicInfo::OnPlayItem(const std::shared_ptr<CFileItem>& item)
+{
+ Close(true);
+ MUSIC_UTILS::PlayItem(item);
+}
diff --git a/xbmc/music/dialogs/GUIDialogMusicInfo.h b/xbmc/music/dialogs/GUIDialogMusicInfo.h
new file mode 100644
index 0000000..781a7d1
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicInfo.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2005-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 "MediaSource.h"
+#include "guilib/GUIDialog.h"
+#include "music/Album.h"
+#include "music/Artist.h"
+#include "music/Song.h"
+#include "threads/Event.h"
+
+#include <memory>
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogMusicInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogMusicInfo(void);
+ ~CGUIDialogMusicInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool SetItem(CFileItem* item);
+ void SetAlbum(const CAlbum& album, const std::string &path);
+ void SetArtist(const CArtist& artist, const std::string &path);
+ bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; }
+ bool HasRefreshed() const { return m_hasRefreshed; }
+
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+ std::string GetContent();
+ static void AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item);
+ void SetDiscography(CMusicDatabase& database) const;
+ void SetSongs(const VECSONGS &songs) const;
+ void SetArtTypeList(CFileItemList& artlist);
+ void SetScrapedInfo(bool bScraped) { m_scraperAddInfo = bScraped; }
+ CArtist& GetArtist() { return m_artist; }
+ CAlbum& GetAlbum() { return m_album; }
+ bool IsArtistInfo() const { return m_bArtistInfo; }
+ bool IsCancelled() const { return m_cancelled; }
+ bool HasScrapedInfo() const { return m_scraperAddInfo; }
+ void FetchComplete();
+ void RefreshInfo();
+
+ static void ShowForAlbum(int idAlbum);
+ static void ShowForArtist(int idArtist);
+ static void ShowFor(CFileItem* pItem);
+protected:
+ void OnInitWindow() override;
+ void Update();
+ void SetLabel(int iControl, const std::string& strLabel);
+ void OnGetArt();
+ void OnAlbumInfo(int id);
+ void OnArtistInfo(int id);
+ void OnSetUserrating() const;
+ void SetUserrating(int userrating) const;
+ void OnPlayItem(const std::shared_ptr<CFileItem>& item);
+
+ CAlbum m_album;
+ CArtist m_artist;
+ int m_startUserrating = -1;
+ bool m_hasUpdatedUserrating = false;
+ bool m_hasRefreshed = false;
+ bool m_bArtistInfo = false;
+ bool m_cancelled = false;
+ bool m_scraperAddInfo = false;
+ std::unique_ptr<CFileItemList> m_albumSongs;
+ std::shared_ptr<CFileItem> m_item;
+ std::unique_ptr<CFileItemList> m_artTypeList;
+ CEvent m_event;
+ std::string m_fallbackartpath;
+};
diff --git a/xbmc/music/dialogs/GUIDialogMusicOSD.cpp b/xbmc/music/dialogs/GUIDialogMusicOSD.cpp
new file mode 100644
index 0000000..6cd110b
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicOSD.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2005-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 "GUIDialogMusicOSD.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/Key.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+#define CONTROL_VIS_BUTTON 500
+#define CONTROL_LOCK_BUTTON 501
+
+CGUIDialogMusicOSD::CGUIDialogMusicOSD(void)
+ : CGUIDialog(WINDOW_DIALOG_MUSIC_OSD, "MusicOSD.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogMusicOSD::~CGUIDialogMusicOSD(void) = default;
+
+bool CGUIDialogMusicOSD::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ unsigned int iControl = message.GetSenderId();
+ if (iControl == CONTROL_VIS_BUTTON)
+ {
+ std::string addonID;
+ if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::VISUALIZATION, addonID, true) ==
+ 1)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION, addonID);
+ settings->Save();
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_VISUALISATION_RELOAD, 0, 0);
+ }
+ }
+ else if (iControl == CONTROL_LOCK_BUTTON)
+ {
+ CGUIMessage msg(GUI_MSG_VISUALISATION_ACTION, 0, 0, ACTION_VIS_PRESET_LOCK);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ return true;
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogMusicOSD::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_OSD:
+ Close();
+ return true;
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogMusicOSD::FrameMove()
+{
+ if (m_autoClosing)
+ {
+ // check for movement of mouse or a submenu open
+ if (CServiceBroker::GetInputManager().IsMouseActive() ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIS_SETTINGS) ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIS_PRESET_LIST) ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_RADIO_RDS_INFO))
+ // extend show time by original value
+ SetAutoClose(m_showDuration);
+ }
+ CGUIDialog::FrameMove();
+}
diff --git a/xbmc/music/dialogs/GUIDialogMusicOSD.h b/xbmc/music/dialogs/GUIDialogMusicOSD.h
new file mode 100644
index 0000000..99a208c
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicOSD.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2005-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 "guilib/GUIDialog.h"
+
+class CGUIDialogMusicOSD :
+ public CGUIDialog
+{
+public:
+ CGUIDialogMusicOSD(void);
+ ~CGUIDialogMusicOSD(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+ bool OnAction(const CAction &action) override;
+ void FrameMove() override;
+};
diff --git a/xbmc/music/dialogs/GUIDialogSongInfo.cpp b/xbmc/music/dialogs/GUIDialogSongInfo.cpp
new file mode 100644
index 0000000..8efca94
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogSongInfo.cpp
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2005-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 "GUIDialogSongInfo.h"
+
+#include "GUIDialogMusicInfo.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "Util.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicUtils.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/windows/GUIWindowMusicBase.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+
+#define CONTROL_BTN_REFRESH 6
+#define CONTROL_USERRATING 7
+#define CONTROL_BTN_PLAY 8
+#define CONTROL_BTN_GET_THUMB 10
+#define CONTROL_ALBUMINFO 12
+
+#define CONTROL_LIST 50
+
+#define TIME_TO_BUSY_DIALOG 500
+
+
+
+class CGetSongInfoJob : public CJob
+{
+public:
+ ~CGetSongInfoJob(void) override = default;
+
+ // Fetch full song information including art types list
+ bool DoWork() override
+ {
+ CGUIDialogSongInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSongInfo>(WINDOW_DIALOG_SONG_INFO);
+ if (!dialog)
+ return false;
+ if (dialog->IsCancelled())
+ return false;
+ CFileItemPtr m_song = dialog->GetCurrentListItem();
+
+ // Fetch tag data from library using filename of item path, or scanning file
+ // (if item does not already have this loaded)
+ if (!m_song->LoadMusicTag())
+ {
+ // Stop SongInfoDialog waiting
+ dialog->FetchComplete();
+ return false;
+ }
+ if (dialog->IsCancelled())
+ return false;
+ // Fetch album and primary song artist data from library as properties
+ // and lyrics by scanning tags from file
+ MUSIC_INFO::CMusicInfoLoader::LoadAdditionalTagInfo(m_song.get());
+ if (dialog->IsCancelled())
+ return false;
+
+ // Get album path (for use in browsing art selection)
+ std::string albumpath;
+ CMusicDatabase db;
+ db.Open();
+ db.GetAlbumPath(m_song->GetMusicInfoTag()->GetAlbumId(), albumpath);
+ m_song->SetProperty("album_path", albumpath);
+ db.Close();
+ if (dialog->IsCancelled())
+ return false;
+
+ // Load song art.
+ // For songs in library this includes related album and artist(s) art.
+ // Also fetches artist art for non library songs when artist can be found
+ // uniquely by name, otherwise just embedded or cached thumb is fetched.
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_song.get());
+ if (dialog->IsCancelled())
+ return false;
+
+ // For songs in library fill vector of possible art types, with current art when it exists
+ // for display on the art type selection dialog
+ CFileItemList artlist;
+ MUSIC_UTILS::FillArtTypesList(*m_song, artlist);
+ dialog->SetArtTypeList(artlist);
+ if (dialog->IsCancelled())
+ return false;
+
+ // Tell waiting SongInfoDialog that job is complete
+ dialog->FetchComplete();
+
+ return true;
+ }
+};
+
+CGUIDialogSongInfo::CGUIDialogSongInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_SONG_INFO, "DialogMusicInfo.xml")
+ , m_song(new CFileItem)
+{
+ m_cancelled = false;
+ m_hasUpdatedUserrating = false;
+ m_startUserrating = -1;
+ m_artTypeList.Clear();
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSongInfo::~CGUIDialogSongInfo(void) = default;
+
+bool CGUIDialogSongInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_artTypeList.Clear();
+ if (m_startUserrating != m_song->GetMusicInfoTag()->GetUserrating())
+ {
+ m_hasUpdatedUserrating = true;
+
+ // Asynchronously update song userrating in library
+ MUSIC_UTILS::UpdateSongRatingJob(m_song, m_song->GetMusicInfoTag()->GetUserrating());
+
+ // Send a message to all windows to tell them to update the fileitem
+ // This communicates the rating change to the music lib window, current playlist and OSD.
+ // The music lib window item is updated to but changes to the rating when it is the sort
+ // do not show on screen until refresh() that fetches the list from scratch, sorts etc.
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_song);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST);
+ OnMessage(msg);
+ break;
+ }
+ case GUI_MSG_WINDOW_INIT:
+ CGUIDialog::OnMessage(message);
+ Update();
+ m_cancelled = false;
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_USERRATING)
+ {
+ OnSetUserrating();
+ }
+ else if (iControl == CONTROL_ALBUMINFO)
+ {
+ CGUIDialogMusicInfo::ShowForAlbum(m_albumId);
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_GET_THUMB)
+ {
+ OnGetArt();
+ return true;
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ int iAction = message.GetParam1();
+ if ((ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction))
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ int iItem = msg.GetParam1();
+ if (iItem < 0 || iItem >= static_cast<int>(m_song->GetMusicInfoTag()->GetContributors().size()))
+ break;
+ int idArtist = m_song->GetMusicInfoTag()->GetContributors()[iItem].GetArtistId();
+ if (idArtist > 0)
+ CGUIDialogMusicInfo::ShowForArtist(idArtist);
+ return true;
+ }
+ }
+ else if (iControl == CONTROL_BTN_PLAY)
+ {
+ OnPlaySong(m_song);
+ return true;
+ }
+ return false;
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogSongInfo::OnAction(const CAction& action)
+{
+ int userrating = m_song->GetMusicInfoTag()->GetUserrating();
+ if (action.GetID() == ACTION_INCREASE_RATING)
+ {
+ SetUserrating(userrating + 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_DECREASE_RATING)
+ {
+ SetUserrating(userrating - 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogSongInfo::OnBack(int actionID)
+{
+ m_cancelled = true;
+ return CGUIDialog::OnBack(actionID);
+}
+
+void CGUIDialogSongInfo::FetchComplete()
+{
+ //Trigger the event to indicate data has been fetched
+ m_event.Set();
+}
+
+void CGUIDialogSongInfo::OnInitWindow()
+{
+ // Enable album info button when we know album
+ m_albumId = m_song->GetMusicInfoTag()->GetAlbumId();
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_ALBUMINFO, m_albumId > 0);
+
+ // Disable music user rating button for plugins as they don't have tables to save this
+ if (m_song->IsPlugin())
+ CONTROL_DISABLE(CONTROL_USERRATING);
+ else
+ CONTROL_ENABLE(CONTROL_USERRATING);
+
+ // Disable the Choose Art button if the user isn't allowed it
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB,
+ profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser);
+
+ SET_CONTROL_HIDDEN(CONTROL_BTN_REFRESH);
+ SET_CONTROL_LABEL(CONTROL_USERRATING, 38023);
+ SET_CONTROL_LABEL(CONTROL_BTN_GET_THUMB, 13511);
+ SET_CONTROL_LABEL(CONTROL_ALBUMINFO, 10523);
+ SET_CONTROL_LABEL(CONTROL_BTN_PLAY, 208);
+
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogSongInfo::Update()
+{
+ CFileItemList items;
+ for (const auto& contributor : m_song->GetMusicInfoTag()->GetContributors())
+ {
+ auto item = std::make_shared<CFileItem>(contributor.GetRoleDesc());
+ item->SetLabel2(contributor.GetArtist());
+ item->GetMusicInfoTag()->SetDatabaseId(contributor.GetArtistId(), MediaTypeArtist);
+ items.Add(std::move(item));
+ }
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, &items);
+ OnMessage(message);
+}
+
+void CGUIDialogSongInfo::SetUserrating(int userrating)
+{
+ userrating = std::max(userrating, 0);
+ userrating = std::min(userrating, 10);
+ if (userrating != m_song->GetMusicInfoTag()->GetUserrating())
+ {
+ m_song->GetMusicInfoTag()->SetUserrating(userrating);
+ }
+}
+
+bool CGUIDialogSongInfo::SetSong(CFileItem* item)
+{
+ *m_song = *item;
+ m_event.Reset();
+ m_cancelled = false; // SetSong happens before win_init
+ // In a separate job fetch song info and fill list of art types.
+ int jobid =
+ CServiceBroker::GetJobManager()->AddJob(new CGetSongInfoJob(), nullptr, CJob::PRIORITY_LOW);
+
+ // Wait to get all data before show, allowing user to cancel if fetch is slow
+ if (!CGUIDialogBusy::WaitOnEvent(m_event, TIME_TO_BUSY_DIALOG))
+ {
+ // Cancel job still waiting in queue (unlikely)
+ CServiceBroker::GetJobManager()->CancelJob(jobid);
+ // Flag to stop job already in progress
+ m_cancelled = true;
+ return false;
+ }
+
+ // Store initial userrating
+ m_startUserrating = m_song->GetMusicInfoTag()->GetUserrating();
+ m_hasUpdatedUserrating = false;
+ return true;
+}
+
+void CGUIDialogSongInfo::SetArtTypeList(CFileItemList& artlist)
+{
+ m_artTypeList.Copy(artlist);
+}
+
+CFileItemPtr CGUIDialogSongInfo::GetCurrentListItem(int offset)
+{
+ return m_song;
+}
+
+std::string CGUIDialogSongInfo::GetContent()
+{
+ return "songs";
+}
+
+/*
+ Allow user to choose artwork for the song
+ For each type of art the options are:
+ 1. Current art
+ 2. Local art (thumb found by filename)
+ 3. Embedded art (@todo)
+ 4. None
+ Note that songs are not scraped, hence there is no list of urls for possible remote art
+*/
+void CGUIDialogSongInfo::OnGetArt()
+{
+ std::string type = MUSIC_UTILS::ShowSelectArtTypeDialog(m_artTypeList);
+ if (type.empty())
+ return; // Cancelled
+
+ CFileItemList items;
+ CGUIListItem::ArtMap primeArt = m_song->GetArt(); // Song art without fallbacks
+ bool bHasArt = m_song->HasArt(type);
+ bool bFallback(false);
+ if (bHasArt)
+ {
+ // Check if that type of art is actually a fallback, e.g. album thumb or artist fanart
+ CGUIListItem::ArtMap::const_iterator i = primeArt.find(type);
+ bFallback = (i == primeArt.end());
+ }
+
+ // Build list of possible images of that art type
+ if (bHasArt)
+ {
+ // Add item for current artwork, could a fallback from album/artist
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", m_song->GetArt(type));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13512)); //! @todo: label fallback art so user knows?
+ items.Add(item);
+ }
+ else if (m_song->HasArt("thumb"))
+ { // For missing art of that type add the thumb (when it exists and not a fallback)
+ CGUIListItem::ArtMap::const_iterator i = primeArt.find("thumb");
+ if (i != primeArt.end())
+ {
+ CFileItemPtr item(new CFileItem("thumb://Thumb", false));
+ item->SetArt("thumb", m_song->GetArt("thumb"));
+ item->SetArt("icon", "DefaultAlbumCover.png");
+ item->SetLabel(g_localizeStrings.Get(21371));
+ items.Add(item);
+ }
+ }
+
+ std::string localThumb;
+ if (type == "thumb")
+ { // Local thumb type art held in <filename>.tbn (for non-library items)
+ localThumb = m_song->GetUserMusicThumb(true);
+ if (m_song->IsMusicDb())
+ {
+ CFileItem item(m_song->GetMusicInfoTag()->GetURL(), false);
+ localThumb = item.GetUserMusicThumb(true);
+ }
+ if (CFileUtils::Exists(localThumb))
+ {
+ CFileItemPtr item(new CFileItem("thumb://Local", false));
+ item->SetArt("thumb", localThumb);
+ item->SetLabel(g_localizeStrings.Get(20017));
+ items.Add(item);
+ }
+ }
+
+ // Clear these local images from cache so user will see any recent
+ // local file changes immediately
+ for (auto& item : items)
+ {
+ std::string thumb(item->GetArt("thumb"));
+ if (thumb.empty())
+ continue;
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ // Remove any thumbnail of local image too (created when browsing files)
+ std::string thumbthumb(CTextureUtils::GetWrappedThumbURL(thumb));
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumbthumb);
+ }
+
+ if (bHasArt && !bFallback)
+ { // Actually has this type of art (not a fallback) so
+ // allow the user to delete it by selecting "no art".
+ CFileItemPtr item(new CFileItem("thumb://None", false));
+ item->SetArt("thumb", "DefaultAlbumCover.png");
+ item->SetLabel(g_localizeStrings.Get(13515));
+ items.Add(item);
+ }
+
+ //! @todo: Add support for extracting embedded art
+
+ // Show list of possible art for user selection
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+ // Add album folder as source (could be disc set)
+ std::string albumpath = m_song->GetProperty("album_path").asString();
+ if (!albumpath.empty())
+ {
+ CFileItem pathItem(albumpath, true);
+ CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, pathItem);
+ }
+ else // Add parent folder of song
+ CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, *m_song);
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) &&
+ result != "thumb://Current")
+ {
+ // User didn't choose the one they have, or the fallback image.
+ // Overwrite with the new art or clear it
+ std::string newArt;
+ if (result == "thumb://Thumb")
+ newArt = m_song->GetArt("thumb");
+ else if (result == "thumb://Local")
+ newArt = localThumb;
+// else if (result == "thumb://Embedded")
+// newArt = embeddedArt;
+ else if (CFileUtils::Exists(result))
+ newArt = result;
+ else // none
+ newArt.clear();
+
+ // Asynchronously update that type of art in the database
+ MUSIC_UTILS::UpdateArtJob(m_song, type, newArt);
+
+ // Update local song with current art
+ if (newArt.empty())
+ {
+ // Remove that type of art from the song
+ primeArt.erase(type);
+ m_song->SetArt(primeArt);
+ }
+ else
+ // Add or modify the type of art
+ m_song->SetArt(type, newArt);
+
+ // Update local art list with current art
+ // Show any fallback art when song art removed
+ if (newArt.empty() && m_song->HasArt(type))
+ newArt = m_song->GetArt(type);
+ for (const auto& artitem : m_artTypeList)
+ {
+ if (artitem->GetProperty("artType") == type)
+ {
+ artitem->SetArt("thumb", newArt);
+ break;
+ }
+ }
+
+ // Get new artwork to show in other places e.g. on music lib window,
+ // current playlist and player OSD.
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_song);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ }
+
+ // Re-open the art type selection dialog as we come back from
+ // the image selection dialog
+ OnGetArt();
+}
+
+void CGUIDialogSongInfo::OnSetUserrating()
+{
+ int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_song->GetMusicInfoTag()->GetUserrating());
+ if (userrating < 0) // Nothing selected, so rating unchanged
+ return;
+
+ SetUserrating(userrating);
+}
+
+void CGUIDialogSongInfo::ShowFor(CFileItem* pItem)
+{
+ if (pItem->m_bIsFolder)
+ return;
+ if (!pItem->IsMusicDb())
+ pItem->LoadMusicTag();
+ if (!pItem->HasMusicInfoTag())
+ return;
+
+ CGUIDialogSongInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogSongInfo>(WINDOW_DIALOG_SONG_INFO);
+ if (dialog)
+ {
+ if (dialog->SetSong(pItem)) // Fetch full song info asynchronously
+ {
+ dialog->Open();
+ if (dialog->HasUpdatedUserrating())
+ {
+ auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowMusicBase>(WINDOW_MUSIC_NAV);
+ if (window)
+ window->RefreshContent("songs");
+ }
+ }
+ }
+}
+
+void CGUIDialogSongInfo::OnPlaySong(const std::shared_ptr<CFileItem>& item)
+{
+ Close(true);
+ MUSIC_UTILS::PlayItem(item);
+}
diff --git a/xbmc/music/dialogs/GUIDialogSongInfo.h b/xbmc/music/dialogs/GUIDialogSongInfo.h
new file mode 100644
index 0000000..274f1ac
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogSongInfo.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2005-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 "FileItem.h"
+#include "guilib/GUIDialog.h"
+#include "threads/Event.h"
+
+#include <memory>
+
+class CGUIDialogSongInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogSongInfo(void);
+ ~CGUIDialogSongInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool SetSong(CFileItem* item);
+ void SetArtTypeList(CFileItemList& artlist);
+ bool OnAction(const CAction& action) override;
+ bool OnBack(int actionID) override;
+ bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; }
+
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+ std::string GetContent();
+ //const CFileItemList& CurrentDirectory() const { return m_artTypeList; }
+ bool IsCancelled() const { return m_cancelled; }
+ void FetchComplete();
+
+ static void ShowFor(CFileItem* pItem);
+protected:
+ void OnInitWindow() override;
+ void Update();
+ void OnGetArt();
+ void SetUserrating(int userrating);
+ void OnSetUserrating();
+ void OnPlaySong(const std::shared_ptr<CFileItem>& item);
+
+ CFileItemPtr m_song;
+ CFileItemList m_artTypeList;
+ CEvent m_event;
+ int m_startUserrating;
+ bool m_cancelled;
+ bool m_hasUpdatedUserrating;
+ long m_albumId = -1;
+
+};
diff --git a/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp
new file mode 100644
index 0000000..b5f2607
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2005-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 "GUIDialogVisualisationPresetList.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIVisualisationControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+CGUIDialogVisualisationPresetList::CGUIDialogVisualisationPresetList()
+ : CGUIDialogSelect(WINDOW_DIALOG_VIS_PRESET_LIST)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CGUIDialogVisualisationPresetList::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_VISUALISATION_UNLOADING:
+ ClearVisualisation();
+ break;
+ }
+ return CGUIDialogSelect::OnMessage(message);
+}
+
+void CGUIDialogVisualisationPresetList::OnSelect(int idx)
+{
+ if (m_viz)
+ m_viz->SetPreset(idx);
+}
+
+void CGUIDialogVisualisationPresetList::ClearVisualisation()
+{
+ m_viz = nullptr;
+ Reset();
+}
+
+void CGUIDialogVisualisationPresetList::SetVisualisation(CGUIVisualisationControl* vis)
+{
+ m_viz = vis;
+ Reset();
+ if (!m_viz)
+ { // No viz, but show something if this dialog activated
+ SetHeading(CVariant{ 10122 });
+ CFileItem item(g_localizeStrings.Get(13389));
+ Add(item);
+ }
+ else
+ {
+ SetUseDetails(false);
+ SetMultiSelection(false);
+ SetHeading(CVariant{StringUtils::Format(g_localizeStrings.Get(13407), m_viz->Name())});
+ std::vector<std::string> presets;
+ if (m_viz->GetPresetList(presets))
+ {
+ for (const auto& preset : presets)
+ {
+ CFileItem item(preset);
+ item.RemoveExtension();
+ Add(item);
+ }
+ SetSelected(m_viz->GetActivePreset());
+ }
+ else
+ { // Viz does not have any presets
+ // "There are no presets available for this visualisation"
+ CFileItem item(g_localizeStrings.Get(13389));
+ Add(item);
+ }
+ }
+}
+
+void CGUIDialogVisualisationPresetList::OnInitWindow()
+{
+ CGUIMessage msg(GUI_MSG_GET_VISUALISATION, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ SetVisualisation(static_cast<CGUIVisualisationControl*>(msg.GetPointer()));
+ CGUIDialogSelect::OnInitWindow();
+}
+
+void CGUIDialogVisualisationPresetList::OnDeinitWindow(int nextWindowID)
+{
+ ClearVisualisation();
+ CGUIDialogSelect::OnDeinitWindow(nextWindowID);
+}
diff --git a/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h
new file mode 100644
index 0000000..c832163
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-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 "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIDialog.h"
+
+class CGUIVisualisationControl;
+class CFileItemList;
+
+class CGUIDialogVisualisationPresetList : public CGUIDialogSelect
+{
+public:
+ CGUIDialogVisualisationPresetList();
+ bool OnMessage(CGUIMessage &message) override;
+
+protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void OnSelect(int idx) override;
+
+private:
+ void ClearVisualisation();
+ void SetVisualisation(CGUIVisualisationControl *addon);
+ CGUIVisualisationControl* m_viz = nullptr;
+};
diff --git a/xbmc/music/infoscanner/CMakeLists.txt b/xbmc/music/infoscanner/CMakeLists.txt
new file mode 100644
index 0000000..b8c24a0
--- /dev/null
+++ b/xbmc/music/infoscanner/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES MusicAlbumInfo.cpp
+ MusicArtistInfo.cpp
+ MusicInfoScanner.cpp
+ MusicInfoScraper.cpp)
+
+set(HEADERS MusicAlbumInfo.h
+ MusicArtistInfo.h
+ MusicInfoScanner.h
+ MusicInfoScraper.h)
+
+core_add_library(music_infoscanner)
diff --git a/xbmc/music/infoscanner/MusicAlbumInfo.cpp b/xbmc/music/infoscanner/MusicAlbumInfo.cpp
new file mode 100644
index 0000000..90aedd3
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicAlbumInfo.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2005-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 "MusicAlbumInfo.h"
+
+#include "addons/Scraper.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/StringUtils.h"
+
+using namespace MUSIC_GRABBER;
+
+CMusicAlbumInfo::CMusicAlbumInfo(const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL)
+ : m_strTitle2(strAlbumInfo), m_albumURL(strAlbumURL)
+{
+ m_relevance = -1;
+ m_bLoaded = false;
+}
+
+CMusicAlbumInfo::CMusicAlbumInfo(const std::string& strAlbum,
+ const std::string& strArtist,
+ const std::string& strAlbumInfo,
+ const CScraperUrl& strAlbumURL)
+ : m_strTitle2(strAlbumInfo), m_albumURL(strAlbumURL)
+{
+ m_album.strAlbum = strAlbum;
+ //Just setting artist desc, not populating album artist credits.
+ m_album.strArtistDesc = strArtist;
+ m_relevance = -1;
+ m_bLoaded = false;
+}
+
+void CMusicAlbumInfo::SetAlbum(CAlbum& album)
+{
+ m_album = album;
+ m_strTitle2 = "";
+ m_bLoaded = true;
+}
+
+bool CMusicAlbumInfo::Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper)
+{
+ bool fSuccess = scraper->GetAlbumDetails(http, m_albumURL, m_album);
+ if (fSuccess && m_strTitle2.empty())
+ m_strTitle2 = m_album.strAlbum;
+ SetLoaded(fSuccess);
+ return fSuccess;
+}
+
diff --git a/xbmc/music/infoscanner/MusicAlbumInfo.h b/xbmc/music/infoscanner/MusicAlbumInfo.h
new file mode 100644
index 0000000..91b8d5d
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicAlbumInfo.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2005-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 "addons/Scraper.h"
+#include "music/Album.h"
+#include "utils/ScraperUrl.h"
+
+class CXBMCTinyXML;
+
+namespace XFILE { class CCurlFile; }
+
+namespace MUSIC_GRABBER
+{
+class CMusicAlbumInfo
+{
+public:
+ CMusicAlbumInfo() = default;
+ CMusicAlbumInfo(const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL);
+ CMusicAlbumInfo(const std::string& strAlbum, const std::string& strArtist, const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL);
+ virtual ~CMusicAlbumInfo() = default;
+
+ bool Loaded() const { return m_bLoaded; }
+ void SetLoaded(bool bLoaded) { m_bLoaded = bLoaded; }
+ const CAlbum &GetAlbum() const { return m_album; }
+ CAlbum& GetAlbum() { return m_album; }
+ void SetAlbum(CAlbum& album);
+ const std::string& GetTitle2() const { return m_strTitle2; }
+ void SetTitle(const std::string& strTitle) { m_album.strAlbum = strTitle; }
+ const CScraperUrl& GetAlbumURL() const { return m_albumURL; }
+ float GetRelevance() const { return m_relevance; }
+ void SetRelevance(float relevance) { m_relevance = relevance; }
+
+ bool Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper);
+
+protected:
+ bool m_bLoaded = false;
+ CAlbum m_album;
+ float m_relevance = -1;
+ std::string m_strTitle2;
+ CScraperUrl m_albumURL;
+};
+
+}
diff --git a/xbmc/music/infoscanner/MusicArtistInfo.cpp b/xbmc/music/infoscanner/MusicArtistInfo.cpp
new file mode 100644
index 0000000..3a3f64a
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicArtistInfo.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2005-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 "MusicArtistInfo.h"
+
+#include "addons/Scraper.h"
+
+using namespace XFILE;
+using namespace MUSIC_GRABBER;
+
+CMusicArtistInfo::CMusicArtistInfo(const std::string& strArtist, const CScraperUrl& strArtistURL):
+ m_artistURL(strArtistURL)
+{
+ m_artist.strArtist = strArtist;
+ m_bLoaded = false;
+}
+
+void CMusicArtistInfo::SetArtist(const CArtist& artist)
+{
+ m_artist = artist;
+ m_bLoaded = true;
+}
+
+bool CMusicArtistInfo::Load(CCurlFile& http, const ADDON::ScraperPtr& scraper,
+ const std::string &strSearch)
+{
+ return m_bLoaded = scraper->GetArtistDetails(http, m_artistURL, strSearch, m_artist);
+}
+
diff --git a/xbmc/music/infoscanner/MusicArtistInfo.h b/xbmc/music/infoscanner/MusicArtistInfo.h
new file mode 100644
index 0000000..1817589
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicArtistInfo.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2005-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 "addons/Scraper.h"
+#include "music/Artist.h"
+
+class CXBMCTinyXML;
+class CScraperUrl;
+
+namespace MUSIC_GRABBER
+{
+class CMusicArtistInfo
+{
+public:
+ CMusicArtistInfo() = default;
+ CMusicArtistInfo(const std::string& strArtist, const CScraperUrl& strArtistURL);
+ virtual ~CMusicArtistInfo() = default;
+ bool Loaded() const { return m_bLoaded; }
+ void SetLoaded() { m_bLoaded = true; }
+ void SetArtist(const CArtist& artist);
+ const CArtist& GetArtist() const { return m_artist; }
+ CArtist& GetArtist() { return m_artist; }
+ const CScraperUrl& GetArtistURL() const { return m_artistURL; }
+ bool Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper,
+ const std::string &strSearch);
+
+protected:
+ CArtist m_artist;
+ CScraperUrl m_artistURL;
+ bool m_bLoaded = false;
+};
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScanner.cpp b/xbmc/music/infoscanner/MusicInfoScanner.cpp
new file mode 100644
index 0000000..f828b44
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScanner.cpp
@@ -0,0 +1,2338 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoScanner.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "MusicAlbumInfo.h"
+#include "MusicInfoScraper.h"
+#include "NfoFile.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Scraper.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "events/EventLog.h"
+#include "events/MediaLibraryEvent.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "filesystem/SmartPlaylistDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/MusicUtils.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace MUSIC_GRABBER;
+using namespace ADDON;
+using KODI::UTILITY::CDigest;
+
+CMusicInfoScanner::CMusicInfoScanner()
+: m_fileCountReader(this, "MusicFileCounter")
+{
+ m_bStop = false;
+ m_currentItem=0;
+ m_itemCount=0;
+ m_flags = 0;
+}
+
+CMusicInfoScanner::~CMusicInfoScanner() = default;
+
+void CMusicInfoScanner::Process()
+{
+ m_bStop = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanStarted");
+ try
+ {
+ if (m_showDialog && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE))
+ {
+ CGUIDialogExtendedProgressBar* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ if (dialog)
+ m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
+ }
+
+ // check if we only need to perform a cleaning
+ if (m_bClean && m_pathsToScan.empty())
+ {
+ CMusicLibraryQueue::GetInstance().CleanLibrary(false);
+ m_handle = NULL;
+ m_bRunning = false;
+
+ return;
+ }
+
+ auto tick = std::chrono::steady_clock::now();
+ m_musicDatabase.Open();
+ m_bCanInterrupt = true;
+
+ if (m_scanType == 0) // load info from files
+ {
+ CLog::Log(LOGDEBUG, "{} - Starting scan", __FUNCTION__);
+
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(505));
+
+ // Reset progress vars
+ m_currentItem=0;
+ m_itemCount=-1;
+
+ // Create the thread to count all files to be scanned
+ if (m_handle)
+ m_fileCountReader.Create();
+
+ // Database operations should not be canceled
+ // using Interrupt() while scanning as it could
+ // result in unexpected behaviour.
+ m_bCanInterrupt = false;
+ m_needsCleanup = false;
+
+ bool commit = true;
+ for (const auto& it : m_pathsToScan)
+ {
+ if (!CDirectory::Exists(it) && !m_bClean)
+ {
+ /*
+ * Note that this will skip scanning (if m_bClean is disabled) if the directory really
+ * doesn't exist. Since the music scanner is fed with a list of existing paths from the DB
+ * and cleans out all songs under that path as its first step before re-adding files, if
+ * the entire source is offline we totally empty the music database in one go.
+ */
+ CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan.", __FUNCTION__,
+ it);
+ m_seenPaths.insert(it);
+ continue;
+ }
+
+ // Clear list of albums added by this scan
+ m_albumsAdded.clear();
+ bool scancomplete = DoScan(it);
+ if (scancomplete)
+ {
+ if (m_albumsAdded.size() > 0)
+ {
+ // Set local art for added album disc sets and primary album artists
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) !=
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE)
+ RetrieveLocalArt();
+
+ if (m_flags & SCAN_ONLINE)
+ // Download additional album and artist information for the recently added albums.
+ // This also identifies any local artist art if it exists, and gives it priority,
+ // otherwise it is set to the first available from the remote art that was scraped.
+ ScrapeInfoAddedAlbums();
+ }
+ }
+ else
+ {
+ commit = false;
+ break;
+ }
+ }
+
+ if (commit)
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+
+ if (m_needsCleanup)
+ {
+ if (m_handle)
+ {
+ m_handle->SetTitle(g_localizeStrings.Get(700));
+ m_handle->SetText("");
+ }
+
+ m_musicDatabase.CleanupOrphanedItems();
+ m_musicDatabase.CheckArtistLinksChanged();
+
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(331));
+
+ m_musicDatabase.Compress(false);
+ }
+ }
+
+ m_fileCountReader.StopThread();
+
+ m_musicDatabase.EmptyCache();
+
+ auto elapsed =
+ std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - tick);
+ CLog::Log(LOGINFO,
+ "My Music: Scanning for music info using worker thread, operation took {}s",
+ elapsed.count());
+ }
+ if (m_scanType == 1) // load album info
+ {
+ for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it)
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(*it, params);
+ // Only scrape information for albums that have not been scraped before
+ // For refresh of information the lastscraped date is optionally clearered elsewhere
+ if (m_musicDatabase.HasAlbumBeenScraped(params.GetAlbumId()))
+ continue;
+
+ CAlbum album;
+ m_musicDatabase.GetAlbum(params.GetAlbumId(), album);
+ if (m_handle)
+ {
+ float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size());
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ m_handle->SetPercentage(percentage);
+ }
+
+ // find album info
+ ADDON::ScraperPtr scraper;
+ if (!m_musicDatabase.GetScraper(album.idAlbum, CONTENT_ALBUMS, scraper))
+ continue;
+
+ UpdateDatabaseAlbumInfo(album, scraper, false);
+
+ if (m_bStop)
+ break;
+ }
+ }
+ if (m_scanType == 2) // load artist info
+ {
+ for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it)
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(*it, params);
+ // Only scrape information for artists that have not been scraped before
+ // For refresh of information the lastscraped date is optionally clearered elsewhere
+ if (m_musicDatabase.HasArtistBeenScraped(params.GetArtistId()))
+ continue;
+
+ CArtist artist;
+ m_musicDatabase.GetArtist(params.GetArtistId(), artist);
+ m_musicDatabase.GetArtistPath(artist, artist.strPath);
+
+ if (m_handle)
+ {
+ float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size());
+ m_handle->SetText(artist.strArtist);
+ m_handle->SetPercentage(percentage);
+ }
+
+ // find album info
+ ADDON::ScraperPtr scraper;
+ if (!m_musicDatabase.GetScraper(artist.idArtist, CONTENT_ARTISTS, scraper) || !scraper)
+ continue;
+
+ UpdateDatabaseArtistInfo(artist, scraper, false);
+
+ if (m_bStop)
+ break;
+ }
+ }
+ //propagate artist sort names to albums and songs
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryArtistSortOnUpdate)
+ m_musicDatabase.UpdateArtistSortNames();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "MusicInfoScanner: Exception while scanning.");
+ }
+ m_musicDatabase.Close();
+ CLog::Log(LOGDEBUG, "{} - Finished scan", __FUNCTION__);
+
+ m_bRunning = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanFinished");
+
+ // we need to clear the musicdb cache and update any active lists
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+ if (m_handle)
+ m_handle->MarkFinished();
+ m_handle = NULL;
+}
+
+void CMusicInfoScanner::Start(const std::string& strDirectory, int flags)
+{
+ m_fileCountReader.StopThread();
+
+ m_pathsToScan.clear();
+ m_seenPaths.clear();
+ m_albumsAdded.clear();
+ m_flags = flags;
+
+ m_musicDatabase.Open();
+ // Check db sources match xml file and update if they don't
+ m_musicDatabase.UpdateSources();
+
+ if (strDirectory.empty())
+ { // Scan all paths in the database. We do this by scanning all paths in the
+ // db, and crossing them off the list as we go.
+ m_musicDatabase.GetPaths(m_pathsToScan);
+ m_idSourcePath = -1;
+ }
+ else
+ {
+ m_pathsToScan.insert(strDirectory);
+ m_idSourcePath = m_musicDatabase.GetSourceFromPath(strDirectory);
+ }
+ m_musicDatabase.Close();
+
+ m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryCleanOnUpdate;
+
+ m_scanType = 0;
+ m_bRunning = true;
+ Process();
+}
+
+void CMusicInfoScanner::FetchAlbumInfo(const std::string& strDirectory,
+ bool refresh)
+{
+ m_fileCountReader.StopThread();
+ m_pathsToScan.clear();
+
+ CFileItemList items;
+ if (strDirectory.empty())
+ {
+ m_musicDatabase.Open();
+ m_musicDatabase.GetAlbumsNav("musicdb://albums/", items);
+ m_musicDatabase.Close();
+ }
+ else
+ {
+ CURL pathToUrl(strDirectory);
+
+ if (pathToUrl.IsProtocol("musicdb"))
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(strDirectory, params);
+ if (params.GetAlbumId() != -1)
+ {
+ //Add single album (id and path) as item to scan
+ CFileItemPtr item(new CFileItem(strDirectory, false));
+ item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum);
+ items.Add(item);
+ }
+ else
+ {
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory);
+ if (childtype == NODE_TYPE_ALBUM)
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+ else if (StringUtils::EndsWith(strDirectory, ".xsp"))
+ {
+ CSmartPlaylistDirectory dir;
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+
+ m_musicDatabase.Open();
+ for (int i=0;i<items.Size();++i)
+ {
+ if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
+ continue;
+
+ m_pathsToScan.insert(items[i]->GetPath());
+ if (refresh)
+ {
+ m_musicDatabase.ClearAlbumLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId());
+ }
+ }
+ m_musicDatabase.Close();
+
+ m_scanType = 1;
+ m_bRunning = true;
+ Process();
+}
+
+void CMusicInfoScanner::FetchArtistInfo(const std::string& strDirectory,
+ bool refresh)
+{
+ m_fileCountReader.StopThread();
+ m_pathsToScan.clear();
+ CFileItemList items;
+
+ if (strDirectory.empty())
+ {
+ m_musicDatabase.Open();
+ m_musicDatabase.GetArtistsNav("musicdb://artists/", items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), -1);
+ m_musicDatabase.Close();
+ }
+ else
+ {
+ CURL pathToUrl(strDirectory);
+
+ if (pathToUrl.IsProtocol("musicdb"))
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(strDirectory, params);
+ if (params.GetArtistId() != -1)
+ {
+ //Add single artist (id and path) as item to scan
+ CFileItemPtr item(new CFileItem(strDirectory, false));
+ item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeArtist);
+ items.Add(item);
+ }
+ else
+ {
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory);
+ if (childtype == NODE_TYPE_ARTIST)
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+ else if (StringUtils::EndsWith(strDirectory, ".xsp"))
+ {
+ CSmartPlaylistDirectory dir;
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+
+ m_musicDatabase.Open();
+ for (int i=0;i<items.Size();++i)
+ {
+ if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
+ continue;
+
+ m_pathsToScan.insert(items[i]->GetPath());
+ if (refresh)
+ {
+ m_musicDatabase.ClearArtistLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId());
+ }
+ }
+ m_musicDatabase.Close();
+
+ m_scanType = 2;
+ m_bRunning = true;
+ Process();
+}
+
+void CMusicInfoScanner::Stop()
+{
+ if (m_bCanInterrupt)
+ m_musicDatabase.Interrupt();
+
+ m_bStop = true;
+}
+
+static void OnDirectoryScanned(const std::string& strDirectory)
+{
+ CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
+ msg.SetStringParam(strDirectory);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+static std::string Prettify(const std::string& strDirectory)
+{
+ CURL url(strDirectory);
+
+ return CURL::Decode(url.GetWithoutUserDetails());
+}
+
+bool CMusicInfoScanner::DoScan(const std::string& strDirectory)
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..."
+ m_handle->SetText(Prettify(strDirectory));
+ }
+
+ std::set<std::string>::const_iterator it = m_seenPaths.find(strDirectory);
+ if (it != m_seenPaths.end())
+ return true;
+
+ m_seenPaths.insert(strDirectory);
+
+ // Discard all excluded files defined by m_musicExcludeRegExps
+ const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps;
+
+ if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
+ return true;
+
+ if (HasNoMedia(strDirectory))
+ return true;
+
+ // load subfolder
+ CFileItemList items;
+ CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|.jpg|.tbn|.lrc|.cdg", DIR_FLAG_DEFAULTS);
+
+ // sort and get the path hash. Note that we don't filter .cue sheet items here as we want
+ // to detect changes in the .cue sheet as well. The .cue sheet items only need filtering
+ // if we have a changed hash.
+ items.Sort(SortByLabel, SortOrderAscending);
+ std::string hash;
+ GetPathHash(items, hash);
+
+ // check whether we need to rescan or not
+ std::string dbHash;
+ if ((m_flags & SCAN_RESCAN) || !m_musicDatabase.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
+ { // path has changed - rescan
+ if (dbHash.empty())
+ CLog::Log(LOGDEBUG, "{} Scanning dir '{}' as not in the database", __FUNCTION__,
+ CURL::GetRedacted(strDirectory));
+ else
+ CLog::Log(LOGDEBUG, "{} Rescanning dir '{}' due to change", __FUNCTION__,
+ CURL::GetRedacted(strDirectory));
+
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(505)); //"Loading media information from files..."
+
+ // filter items in the sub dir (for .cue sheet support)
+ items.FilterCueItems();
+ items.Sort(SortByLabel, SortOrderAscending);
+
+ // and then scan in the new information from tags
+ if (RetrieveMusicInfo(strDirectory, items) > 0)
+ {
+ if (m_handle)
+ OnDirectoryScanned(strDirectory);
+ }
+
+ // save information about this folder
+ m_musicDatabase.SetPathHash(strDirectory, hash);
+ }
+ else
+ { // path is the same - no need to rescan
+ CLog::Log(LOGDEBUG, "{} Skipping dir '{}' due to no change", __FUNCTION__,
+ CURL::GetRedacted(strDirectory));
+ m_currentItem += CountFiles(items, false); // false for non-recursive
+
+ // updated the dialog with our progress
+ if (m_handle)
+ {
+ if (m_itemCount>0)
+ m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount));
+ OnDirectoryScanned(strDirectory);
+ }
+ }
+
+ // now scan the subfolders
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+
+ if (m_bStop)
+ break;
+ // if we have a directory item (non-playlist) we then recurse into that folder
+ if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList())
+ {
+ std::string strPath=pItem->GetPath();
+ if (!DoScan(strPath))
+ {
+ m_bStop = true;
+ }
+ }
+ }
+ return !m_bStop;
+}
+
+CInfoScanner::INFO_RET CMusicInfoScanner::ScanTags(const CFileItemList& items,
+ CFileItemList& scannedItems)
+{
+ std::vector<std::string> regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps;
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (m_bStop)
+ return INFO_CANCELLED;
+
+ CFileItemPtr pItem = items[i];
+
+ if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), regexps))
+ continue;
+
+ if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics())
+ continue;
+
+ m_currentItem++;
+
+ CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
+ if (!tag.Loaded())
+ {
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem));
+ if (nullptr != pLoader)
+ pLoader->Load(pItem->GetPath(), tag);
+ }
+
+ if (m_handle && m_itemCount>0)
+ m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount));
+
+ if (!tag.Loaded() && !pItem->HasCueDocument())
+ {
+ CLog::Log(LOGDEBUG, "{} - No tag found for: {}", __FUNCTION__, pItem->GetPath());
+ continue;
+ }
+ else
+ {
+ if (!tag.GetCueSheet().empty())
+ pItem->LoadEmbeddedCue();
+ }
+
+ if (pItem->HasCueDocument())
+ pItem->LoadTracksFromCueDocument(scannedItems);
+ else
+ scannedItems.Add(pItem);
+ }
+ return INFO_ADDED;
+}
+
+static bool SortSongsByTrack(const CSong& song, const CSong& song2)
+{
+ return song.iTrack < song2.iTrack;
+}
+
+void CMusicInfoScanner::FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap /* = NULL */)
+{
+ /*
+ * Step 1: Convert the FileItems into Songs.
+ * If they're MB tagged, create albums directly from the FileItems.
+ * If they're non-MB tagged, index them by album name ready for step 2.
+ */
+ std::map<std::string, VECSONGS> songsByAlbumNames;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CMusicInfoTag& tag = *items[i]->GetMusicInfoTag();
+ CSong song(*items[i]);
+
+ // keep the db-only fields intact on rescan...
+ if (songsMap != NULL)
+ {
+ // Match up item to songs in library previously scanned with this path
+ MAPSONGS::iterator songlist = songsMap->find(items[i]->GetPath());
+ if (songlist != songsMap->end())
+ {
+ VECSONGS::iterator foundsong;
+ if (songlist->second.size() == 1)
+ foundsong = songlist->second.begin();
+ else
+ {
+ // When filename mapped to multiple songs it is from cuesheet, match on disc/track number
+ int disctrack = tag.GetTrackAndDiscNumber();
+ foundsong = std::find_if(songlist->second.begin(), songlist->second.end(),
+ [&](const CSong& song) { return disctrack == song.iTrack; });
+ }
+ if (foundsong != songlist->second.end())
+ {
+ song.idSong = foundsong->idSong; // Reuse ID
+ song.dateNew = foundsong->dateNew; // Keep date originally created
+ song.iTimesPlayed = foundsong->iTimesPlayed;
+ song.lastPlayed = foundsong->lastPlayed;
+ if (song.rating == 0)
+ song.rating = foundsong->rating;
+ if (song.userrating == 0)
+ song.userrating = foundsong->userrating;
+ if (song.strThumb.empty())
+ song.strThumb = foundsong->strThumb;
+ }
+ }
+ }
+
+ if (!tag.GetMusicBrainzAlbumID().empty())
+ {
+ VECALBUMS::iterator it;
+ for (it = albums.begin(); it != albums.end(); ++it)
+ if (it->strMusicBrainzAlbumID == tag.GetMusicBrainzAlbumID())
+ break;
+
+ if (it == albums.end())
+ {
+ CAlbum album(*items[i]);
+ album.songs.push_back(song);
+ albums.push_back(album);
+ }
+ else
+ it->songs.push_back(song);
+ }
+ else
+ songsByAlbumNames[tag.GetAlbum()].push_back(song);
+ }
+
+ /*
+ Step 2: Split into unique albums based on album name and album artist
+ In the case where the album artist is unknown, we use the primary artist
+ (i.e. first artist from each song).
+ */
+ for (auto& songsByAlbumName : songsByAlbumNames)
+ {
+ VECSONGS& songs = songsByAlbumName.second;
+ // sort the songs by tracknumber to identify duplicate track numbers
+ sort(songs.begin(), songs.end(), SortSongsByTrack);
+
+ // map the songs to their primary artists
+ bool tracksOverlap = false;
+ bool hasAlbumArtist = false;
+ bool isCompilation = true;
+ std::string old_DiscSubtitle;
+
+ std::map<std::string, std::vector<CSong *> > artists;
+ for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song)
+ {
+ // test for song overlap
+ if (song != songs.begin() && song->iTrack == (song - 1)->iTrack)
+ tracksOverlap = true;
+
+ if (!song->bCompilation)
+ isCompilation = false;
+
+ if (song->strDiscSubtitle != old_DiscSubtitle)
+ old_DiscSubtitle = song->strDiscSubtitle;
+
+ // get primary artist
+ std::string primary;
+ if (!song->GetAlbumArtist().empty())
+ {
+ primary = song->GetAlbumArtist()[0];
+ hasAlbumArtist = true;
+ }
+ else if (!song->artistCredits.empty())
+ primary = song->artistCredits.begin()->GetArtist();
+
+ // add to the artist map
+ artists[primary].push_back(&(*song));
+ }
+
+ /*
+ We have a Various Artists compilation if
+ 1. album name is non-empty AND
+ 2a. no tracks overlap OR
+ 2b. all tracks are marked as part of compilation AND
+ 3a. a unique primary artist is specified as "various", "various artists" or the localized value
+ OR
+ 3b. we have at least two primary artists and no album artist specified.
+ */
+ std::string various = g_localizeStrings.Get(340); // Various Artists
+ bool compilation =
+ !songsByAlbumName.first.empty() && (isCompilation || !tracksOverlap); // 1+2b+2a
+ if (artists.size() == 1)
+ {
+ std::string artist = artists.begin()->first; StringUtils::ToLower(artist);
+ if (!StringUtils::EqualsNoCase(artist, "various") &&
+ !StringUtils::EqualsNoCase(artist, "various artists") &&
+ !StringUtils::EqualsNoCase(artist, various)) // 3a
+ compilation = false;
+ else
+ // Grab name for use in "various artist" artist
+ various = artists.begin()->first;
+ }
+ else if (hasAlbumArtist) // 3b
+ compilation = false;
+
+ // Such a compilation album is stored under a unique artist that matches on Musicbrainz ID
+ // the "various artists" artist for music tagged with mbids.
+ if (compilation)
+ {
+ CLog::Log(LOGDEBUG, "Album '{}' is a compilation as there's no overlapping tracks and {}",
+ songsByAlbumName.first,
+ hasAlbumArtist ? "the album artist is 'Various'"
+ : "there is more than one unique artist");
+ // Clear song artists from artists map, put songs under "various artists" mbid entry
+ artists.clear();
+ for (auto& song : songs)
+ artists[VARIOUSARTISTS_MBID].push_back(&song);
+ }
+
+ /*
+ We also have a compilation album when album name is non-empty and ALL tracks are marked as part of
+ a compilation even if an album artist is given, or all songs have the same primary artist. For
+ example an anthology - a collection of recordings from various old sources
+ combined together such as a "best of", retrospective or rarities type release.
+
+ Such an anthology compilation will not have been caught by the previous tests as it fails 3a and 3b.
+ The album artist can be determined just like any normal album.
+ */
+ if (!compilation && !songsByAlbumName.first.empty() && isCompilation)
+ {
+ compilation = true;
+ CLog::Log(LOGDEBUG,
+ "Album '{}' is a compilation as all songs are marked as part of a compilation",
+ songsByAlbumName.first);
+ }
+
+ /*
+ Step 3: Find the common albumartist for each song and assign
+ albumartist to those tracks that don't have it set.
+ */
+ for (auto& j : artists)
+ {
+ /* Find the common artist(s) for these songs (grouped under primary artist).
+ Various artist compilations already under the unique "various artists" mbid.
+ Take from albumartist tag when present, or use artist tag.
+ When from albumartist tag also check albumartistsort tag and take first non-empty value
+ */
+ std::vector<CSong*>& artistSongs = j.second;
+ std::vector<std::string> common;
+ std::string albumartistsort;
+ if (artistSongs.front()->GetAlbumArtist().empty())
+ common = artistSongs.front()->GetArtist();
+ else
+ {
+ common = artistSongs.front()->GetAlbumArtist();
+ albumartistsort = artistSongs.front()->GetAlbumArtistSort();
+ }
+ for (std::vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k)
+ {
+ unsigned int match = 0;
+ std::vector<std::string> compare;
+ if ((*k)->GetAlbumArtist().empty())
+ compare = (*k)->GetArtist();
+ else
+ {
+ compare = (*k)->GetAlbumArtist();
+ if (albumartistsort.empty())
+ albumartistsort = (*k)->GetAlbumArtistSort();
+ }
+ for (; match < common.size() && match < compare.size(); match++)
+ {
+ if (compare[match] != common[match])
+ break;
+ }
+ common.erase(common.begin() + match, common.end());
+ }
+ if (j.first == VARIOUSARTISTS_MBID)
+ {
+ common.clear();
+ common.emplace_back(VARIOUSARTISTS_MBID);
+ }
+
+ /*
+ Step 4: Assign the album artist for each song that doesn't have it set
+ and add to the album vector
+ */
+ CAlbum album;
+ album.strAlbum = songsByAlbumName.first;
+
+ //Split the albumartist sort string to try and get sort names for individual artists
+ std::vector<std::string> sortnames = StringUtils::Split(albumartistsort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (sortnames.size() != common.size())
+ // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
+ sortnames = StringUtils::SplitMulti(sortnames, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < common.size(); i++)
+ {
+ if (common[i] == VARIOUSARTISTS_MBID)
+ /* Treat "various", "various artists" and the localized equivalent name as the same
+ album artist as the artist with Musicbrainz ID 89ad4ac3-39f7-470e-963a-56509c546377.
+ If adding this artist for the first time then the name will be set to either the primary
+ artist read from tags when 3a, or the localized value for "various artists" when not 3a.
+ This means that tag values are no longer translated into the current language.
+ */
+ album.artistCredits.emplace_back(various, VARIOUSARTISTS_MBID);
+ else
+ {
+ album.artistCredits.emplace_back(StringUtils::Trim(common[i]));
+ // Set artist sort name providing we have as many as we have artists,
+ // otherwise something is wrong with them so ignore rather than guess.
+ if (sortnames.size() == common.size())
+ album.artistCredits.back().SetSortName(StringUtils::Trim(sortnames[i]));
+ }
+ }
+ album.bCompilation = compilation;
+ for (auto& k : artistSongs)
+ {
+ if (k->GetAlbumArtist().empty())
+ k->SetAlbumArtist(common);
+ //! @todo in future we may wish to union up the genres, for now we assume they're the same
+ album.genre = k->genre;
+ album.strArtistSort = k->GetAlbumArtistSort();
+ // in addition, we may want to use release date as discriminating between albums
+ album.strReleaseDate = k->strReleaseDate,
+ album.strLabel = k->strRecordLabel;
+ album.strType = k->strAlbumType;
+ album.songs.push_back(*k);
+ }
+ albums.push_back(album);
+ }
+ }
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateAlbumInfo(CAlbum& album,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog)
+{
+ m_musicDatabase.Open();
+ INFO_RET result = UpdateDatabaseAlbumInfo(album, scraper, bAllowSelection, pDialog);
+ m_musicDatabase.Close();
+ return result;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateArtistInfo(CArtist& artist,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog)
+{
+ m_musicDatabase.Open();
+ INFO_RET result = UpdateDatabaseArtistInfo(artist, scraper, bAllowSelection, pDialog);
+ m_musicDatabase.Close();
+ return result;
+}
+
+int CMusicInfoScanner::RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items)
+{
+ MAPSONGS songsMap;
+
+ // get all information for all files in current directory from database, and remove them
+ if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap))
+ m_needsCleanup = true;
+
+ CFileItemList scannedItems;
+ if (ScanTags(items, scannedItems) == INFO_CANCELLED || scannedItems.Size() == 0)
+ return 0;
+
+ VECALBUMS albums;
+ FileItemsToAlbums(scannedItems, albums, &songsMap);
+
+ /*
+ Set thumb for songs and, if only one album in folder, store the thumb for
+ the album (music db) and the folder path (in Textures db) too.
+ The album and path thumb is either set to the folder art, or failing that to
+ the art embedded in the first music file.
+ Song thumb is only set when it varies, otherwise it is cleared so that it will
+ fallback to the album art (that may be from the first file, or that of the
+ folder or set later by scraping from NFO files or remote sources). Clearing
+ saves caching repeats of the same image.
+
+ However even if all songs are from one album this may not be the album
+ folder. It could be just a subfolder containing some of the songs from a disc
+ set e.g. CD1, CD2 etc., or the album could spread across many folders. In
+ this case the album art gets reset every time a folder with songs from just
+ that album is processed, and needs to be corrected later once all the parts
+ of the album have been scanned.
+ */
+ FindArtForAlbums(albums, items.GetPath());
+
+ /* Strategy: Having scanned tags and made a list of albums, add them to the library. Only then try
+ to scrape additional album and artist information. Music is often tagged to a mixed standard
+ - some albums have mbid tags, some don't. Once all the music files have been added to the library,
+ the mbid for an artist will be known even if it was only tagged on one song. The artist is best
+ scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
+ That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
+ even when it was not in the tagging for that album.
+
+ Doing scraping, generally the slower activity, in the background after scanning has fully populated
+ the library also means that the user can use their library to select music to play sooner.
+ */
+
+ int numAdded = 0;
+
+ // Add all albums to the library, and hence any new song or album artists or other contributors
+ for (auto& album : albums)
+ {
+ if (m_bStop)
+ break;
+
+ // mark albums without a title as singles
+ if (album.strAlbum.empty())
+ album.releaseType = CAlbum::Single;
+
+ album.strPath = strDirectory;
+ m_musicDatabase.AddAlbum(album, m_idSourcePath);
+ m_albumsAdded.insert(album.idAlbum);
+
+ numAdded += static_cast<int>(album.songs.size());
+ }
+ return numAdded;
+}
+
+void MUSIC_INFO::CMusicInfoScanner::ScrapeInfoAddedAlbums()
+{
+ /* Strategy: Having scanned tags, make a list of albums and add them to the library, only then try
+ to scrape additional album and artist information. Music is often tagged to a mixed standard
+ - some albums have mbid tags, some don't. Once all the music files have been added to the library,
+ the mbid for an artist will be known even if it was only tagged on one song. The artist is best
+ scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
+ That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
+ even when it was not in the tagging for that album.
+
+ Doing scraping, generally the slower activity, in the background after scanning has fully populated
+ the library also means that the user can use their library to select music to play sooner.
+ */
+
+ /* Scrape additional album and artist data.
+ For albums and artists without mbids, matching on album-artist pair can
+ be used to identify artist with greater accuracy than artist name alone.
+ Artist mbid returned by album scraper is used if we do not already have it.
+ Hence scrape album then related artists.
+ */
+ ADDON::AddonPtr addon;
+
+ ADDON::ScraperPtr albumScraper;
+ ADDON::ScraperPtr artistScraper;
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS, addon))
+ albumScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS,
+ addon))
+ artistScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+
+ bool albumartistsonly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS);
+
+ if (!albumScraper || !artistScraper)
+ return;
+
+ int i = 0;
+ std::set<int> artists;
+ for (auto albumId : m_albumsAdded)
+ {
+ i++;
+ if (m_bStop)
+ break;
+ // Scrape album data
+ CAlbum album;
+ if (!m_musicDatabase.HasAlbumBeenScraped(albumId))
+ {
+ if (m_handle)
+ {
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ m_handle->SetProgress(i, static_cast<int>(m_albumsAdded.size()));
+ }
+
+ // Fetch any artist mbids for album artist(s) and song artists when scraping those too.
+ m_musicDatabase.GetAlbum(albumId, album, !albumartistsonly);
+ UpdateDatabaseAlbumInfo(album, albumScraper, false);
+
+ // Scrape information for artists that have not been scraped before, avoiding repeating
+ // unsuccessful attempts for every album and song.
+ for (const auto &artistCredit : album.artistCredits)
+ {
+ if (m_bStop)
+ break;
+
+ if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) &&
+ artists.find(artistCredit.GetArtistId()) == artists.end())
+ {
+ artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted
+ CArtist artist;
+ m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist);
+ UpdateDatabaseArtistInfo(artist, artistScraper, false);
+ }
+ }
+ // Only scrape song artists if they are being displayed in artists node by default
+ if (!albumartistsonly)
+ {
+ for (auto &song : album.songs)
+ {
+ if (m_bStop)
+ break;
+ for (const auto &artistCredit : song.artistCredits)
+ {
+ if (m_bStop)
+ break;
+
+ CMusicArtistInfo musicArtistInfo;
+ if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) &&
+ artists.find(artistCredit.GetArtistId()) == artists.end())
+ {
+ artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted
+ CArtist artist;
+ m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist);
+ UpdateDatabaseArtistInfo(artist, artistScraper, false);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/*
+ Set thumb for songs and the album(if only one album in folder).
+ The album thumb is either set to the folder art, or failing that to the art
+ embedded in the first music file. However this does not allow for there being
+ other folders with more songs from the album e.g. this was a subfolder CD1
+ and there is CD2 etc. yet to be processed
+ Song thumb is only set when it varies, otherwise it is cleared so that it will
+ fallback to the album art(that may be from the first file, or that of the
+ folder or set later by scraping from NFO files or remote sources).Clearing
+ saves caching repeats of the same image.
+*/
+void CMusicInfoScanner::FindArtForAlbums(VECALBUMS &albums, const std::string &path)
+{
+ /*
+ If there's a single album in the folder, then art can be taken from
+ the folder art.
+ */
+ std::string albumArt;
+ if (albums.size() == 1)
+ {
+ CFileItem album(path, true);
+ /*
+ If we are scanning a directory served over http(s) the root directory for an album will set
+ IsInternetStream to true which prevents scanning it for art. As we can't reach this point
+ without having read some tags (and tags are not read from streams) we can safely check for
+ that case and set the IsHTTPDirectory property to enable scanning for art.
+ */
+ if (StringUtils::StartsWithNoCase(path, "http") && StringUtils::EndsWith(path, "/"))
+ album.SetProperty("IsHTTPDirectory", true);
+ albumArt = album.GetUserMusicThumb(true);
+ if (!albumArt.empty())
+ albums[0].art["thumb"] = albumArt;
+ }
+ for (auto& album : albums)
+ {
+ if (albums.size() != 1)
+ albumArt = "";
+
+ /*
+ Find art that is common across these items
+ If we find a single art image we treat it as the album art
+ and discard song art else we use first as album art and
+ keep everything as song art.
+ */
+ bool singleArt = true;
+ CSong *art = NULL;
+ for (auto& song : album.songs)
+ {
+ if (song.HasArt())
+ {
+ if (art && !art->ArtMatches(song))
+ {
+ singleArt = false;
+ break;
+ }
+ if (!art)
+ art = &song;
+ }
+ }
+
+ /*
+ assign the first art found to the album - better than no art at all
+ */
+
+ if (art && albumArt.empty())
+ {
+ if (!art->strThumb.empty())
+ albumArt = art->strThumb;
+ else
+ albumArt = CTextureUtils::GetWrappedImageURL(art->strFileName, "music");
+ }
+
+ if (!albumArt.empty())
+ album.art["thumb"] = albumArt;
+
+ if (singleArt)
+ { //if singleArt then we can clear the artwork for all songs
+ for (auto& k : album.songs)
+ k.strThumb.clear();
+ }
+ else
+ { // more than one piece of art was found for these songs, so cache per song
+ for (auto& k : album.songs)
+ {
+ if (k.strThumb.empty() && !k.embeddedArt.Empty())
+ k.strThumb = CTextureUtils::GetWrappedImageURL(k.strFileName, "music");
+ }
+ }
+ }
+ if (albums.size() == 1 && !albumArt.empty())
+ {
+ // assign to folder thumb as well
+ CFileItem albumItem(path, true);
+ CMusicThumbLoader loader;
+ loader.SetCachedImage(albumItem, "thumb", albumArt);
+ }
+}
+
+void MUSIC_INFO::CMusicInfoScanner::RetrieveLocalArt()
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..."
+ //!@todo: title = Checking for local art
+ }
+
+ std::set<int> artistsArtDone; // artists processed to avoid unsuccessful repeats
+ int count = 0;
+ for (auto albumId : m_albumsAdded)
+ {
+ count++;
+ if (m_bStop)
+ break;
+ CAlbum album;
+ m_musicDatabase.GetAlbum(albumId, album, false);
+ if (m_handle)
+ {
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ m_handle->SetProgress(count, static_cast<int>(m_albumsAdded.size()));
+ }
+
+ /*
+ Automatically fetch local art from album folder and any disc sets subfolders
+
+ Providing all songs from an album are are under a unique common album
+ folder (no songs from other albums) then thumb has been set to local art,
+ or failing that to embedded art, during scanning by FindArtForAlbums().
+ But when songs are also spread over multiple subfolders within it e.g. disc
+ sets, it will have been set to either the art of the last subfolder that was
+ processed (if there is any), or from the first song in that subfolder with
+ embedded art (if there is any). To correct this and find any thumb in the
+ (common) album folder add "thumb" to those missing.
+ */
+ AddAlbumArtwork(album);
+
+ /*
+ Local album artist art
+
+ Look in the nominated "Artist Information Folder" for thumbs and fanart.
+ Failing that, for backward compatibility, fallback to the folder immediately
+ above the album folder.
+ It can only fallback if the album has a unique folder, and can only do so
+ for the first album artist if the album is a collaboration e.g. composer,
+ conductor, orchestra, or by several pop artists in their own right.
+ Avoids repeatedly processing the same artist by maintaining a set.
+
+ Adding the album may have added new artists, or provide art for an existing
+ (song) artist, but does not replace any artwork already set. Hence once art
+ has been found for an album artist, art is not searched for in other folders.
+
+ It will find art for "various artists", if artwork is located above the
+ folder containing compilatons.
+ */
+ for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
+ {
+ if (m_bStop)
+ break;
+ int idArtist = artistCredit->GetArtistId();
+ if (artistsArtDone.find(idArtist) == artistsArtDone.end())
+ {
+ artistsArtDone.insert(idArtist); // Artist processed
+
+ // Get artist and subfolder within the Artist Information Folder
+ CArtist artist;
+ m_musicDatabase.GetArtist(idArtist, artist);
+ m_musicDatabase.GetArtistPath(artist, artist.strPath);
+ // Location of local art
+ std::string artfolder;
+ if (CDirectory::Exists(artist.strPath))
+ // When subfolder exists that is only place we look for local art
+ artfolder = artist.strPath;
+ else if (!album.strPath.empty() && artistCredit == album.artistCredits.begin())
+ {
+ // If no individual artist subfolder has been found, for primary
+ // album artist only look in the folder immediately above the album
+ // folder. Not using GetOldArtistPath here because may not have not
+ // have scanned all the albums yet.
+ artfolder = URIUtils::GetParentPath(album.strPath);
+ }
+ AddArtistArtwork(artist, artfolder);
+ }
+ }
+ }
+}
+
+int CMusicInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
+{
+ // Create a hash based on the filenames, filesize and filedate. Also count the number of files
+ if (0 == items.Size()) return 0;
+ CDigest digest{CDigest::Type::MD5};
+ int count = 0;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ const CFileItemPtr pItem = items[i];
+ digest.Update(pItem->GetPath());
+ digest.Update((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
+ KODI::TIME::FileTime time = pItem->m_dateTime;
+ digest.Update((unsigned char*)&time, sizeof(KODI::TIME::FileTime));
+ if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
+ count++;
+ }
+ hash = digest.Finalize();
+ return count;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateDatabaseAlbumInfo(CAlbum& album,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog /* = NULL */)
+{
+ if (!scraper)
+ return INFO_ERROR;
+
+ CMusicAlbumInfo albumInfo;
+ INFO_RET albumDownloadStatus(INFO_CANCELLED);
+ std::string origArtist(album.GetAlbumArtistString());
+ std::string origAlbum(album.strAlbum);
+
+ bool stop(false);
+ while (!stop)
+ {
+ stop = true;
+ CLog::Log(LOGDEBUG, "{} downloading info for: {}", __FUNCTION__, album.strAlbum);
+ albumDownloadStatus = DownloadAlbumInfo(album, scraper, albumInfo, !bAllowSelection, pDialog);
+ if (albumDownloadStatus == INFO_NOT_FOUND)
+ {
+ if (pDialog && bAllowSelection)
+ {
+ std::string strTempAlbum(album.strAlbum);
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strTempAlbum, CVariant{ g_localizeStrings.Get(16011) }, false))
+ albumDownloadStatus = INFO_CANCELLED;
+ else
+ {
+ std::string strTempArtist(album.GetAlbumArtistString());
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strTempArtist, CVariant{ g_localizeStrings.Get(16025) }, false))
+ albumDownloadStatus = INFO_CANCELLED;
+ else
+ {
+ album.strAlbum = strTempAlbum;
+ album.strArtistDesc = strTempArtist;
+ stop = false;
+ }
+ }
+ }
+ else
+ {
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CMediaLibraryEvent(
+ MediaTypeAlbum, album.strPath, 24146,
+ StringUtils::Format(g_localizeStrings.Get(24147), MediaTypeAlbum, album.strAlbum),
+ CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType()),
+ CURL::GetRedacted(album.strPath), EventLevel::Warning)));
+ }
+ }
+ }
+
+ // Restore original album and artist name, possibly changed by manual entry
+ // to get info but should not change outside merge
+ album.strAlbum = origAlbum;
+ album.strArtistDesc = origArtist;
+
+ if (albumDownloadStatus == INFO_ADDED)
+ {
+ bool overridetags = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS);
+ // Remove art accidentally set by the Python scraper, it only provides URLs of possible artwork
+ // Art is selected later applying whitelist and other art preferences
+ albumInfo.GetAlbum().art.clear();
+ album.MergeScrapedAlbum(albumInfo.GetAlbum(), overridetags);
+ m_musicDatabase.UpdateAlbum(album);
+ albumInfo.SetLoaded(true);
+ }
+
+ // Check album art.
+ // Fill any gaps with local art files or use first available from scraped URL list (when it has
+ // been successfully scraped) as controlled by whitelist. Do this even when no info added
+ // (cancelled, not found or error), there may be new local art files.
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) !=
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE &&
+ AddAlbumArtwork(album))
+ albumDownloadStatus = INFO_ADDED; // Local art added
+
+ return albumDownloadStatus;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateDatabaseArtistInfo(CArtist& artist,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog /* = NULL */)
+{
+ if (!scraper)
+ return INFO_ERROR;
+
+ CMusicArtistInfo artistInfo;
+ INFO_RET artistDownloadStatus(INFO_CANCELLED);
+ std::string origArtist(artist.strArtist);
+
+ bool stop(false);
+ while (!stop)
+ {
+ stop = true;
+ CLog::Log(LOGDEBUG, "{} downloading info for: {}", __FUNCTION__, artist.strArtist);
+ artistDownloadStatus = DownloadArtistInfo(artist, scraper, artistInfo, !bAllowSelection, pDialog);
+ if (artistDownloadStatus == INFO_NOT_FOUND)
+ {
+ if (pDialog && bAllowSelection)
+ {
+ if (!CGUIKeyboardFactory::ShowAndGetInput(artist.strArtist, CVariant{ g_localizeStrings.Get(16025) }, false))
+ artistDownloadStatus = INFO_CANCELLED;
+ else
+ stop = false;
+ }
+ else
+ {
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CMediaLibraryEvent(
+ MediaTypeArtist, artist.strPath, 24146,
+ StringUtils::Format(g_localizeStrings.Get(24147), MediaTypeArtist, artist.strArtist),
+ CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType()),
+ CURL::GetRedacted(artist.strPath), EventLevel::Warning)));
+ }
+ }
+ }
+
+ // Restore original artist name, possibly changed by manual entry to get info
+ // but should not change outside merge
+ artist.strArtist = origArtist;
+
+ if (artistDownloadStatus == INFO_ADDED)
+ {
+ artist.MergeScrapedArtist(artistInfo.GetArtist(), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS));
+ m_musicDatabase.UpdateArtist(artist);
+ artistInfo.SetLoaded();
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) ==
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE)
+ return artistDownloadStatus;
+
+ // Check artist art.
+ // Fill any gaps with local art files, or use first available from scraped
+ // list (when it has been successfully scraped). Do this even when no info
+ // added (cancelled, not found or error), there may be new local art files.
+ // Get individual artist subfolder within the Artist Information Folder
+ m_musicDatabase.GetArtistPath(artist, artist.strPath);
+ // Location of local art
+ std::string artfolder;
+ if (CDirectory::Exists(artist.strPath))
+ // When subfolder exists that is only place we look for art
+ artfolder = artist.strPath;
+ else
+ {
+ // Fallback to the old location local to music files (when there is a
+ // unique folder). If there is no folder for the artist, and *only* the
+ // artist, this will be blank
+ m_musicDatabase.GetOldArtistPath(artist.idArtist, artfolder);
+ }
+ if (AddArtistArtwork(artist, artfolder))
+ artistDownloadStatus = INFO_ADDED; // Local art added
+
+ return artistDownloadStatus; // Added, cancelled or not found
+}
+
+#define THRESHOLD .95f
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::DownloadAlbumInfo(const CAlbum& album,
+ const ADDON::ScraperPtr& info,
+ CMusicAlbumInfo& albumInfo,
+ bool bUseScrapedMBID,
+ CGUIDialogProgress* pDialog)
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20321), info->Name()));
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ }
+
+ // clear our scraper cache
+ info->ClearCache();
+
+ CMusicInfoScraper scraper(info);
+ bool bMusicBrainz = false;
+ /*
+ When the mbid is derived from tags scraping of album information is done directly
+ using that ID, otherwise the lookup is based on album and artist names and can mis-identify the
+ album (i.e. classical music has many "Symphony No. 5"). To be able to correct any mistakes a
+ manual refresh of artist information uses either the mbid if derived from tags or the album
+ and artist names, not any previously scraped mbid.
+ */
+ if (!album.strMusicBrainzAlbumID.empty() && (!album.bScrapedMBID || bUseScrapedMBID))
+ {
+ CScraperUrl musicBrainzURL;
+ if (ResolveMusicBrainz(album.strMusicBrainzAlbumID, info, musicBrainzURL))
+ {
+ CMusicAlbumInfo albumNfo("nfo", musicBrainzURL);
+ scraper.GetAlbums().clear();
+ scraper.GetAlbums().push_back(albumNfo);
+ bMusicBrainz = true;
+ }
+ }
+
+ // handle nfo files
+ bool existsNFO = false;
+ std::string path = album.strPath;
+ if (path.empty())
+ m_musicDatabase.GetAlbumPath(album.idAlbum, path);
+
+ std::string strNfo = URIUtils::AddFileToFolder(path, "album.nfo");
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ CNfoFile nfoReader;
+ existsNFO = CFileUtils::Exists(strNfo);
+ // When on GUI ask user if they want to ignore nfo and refresh from Internet
+ if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(10523, 20446))
+ {
+ existsNFO = false;
+ CLog::Log(LOGDEBUG, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo));
+ }
+ if (existsNFO)
+ {
+ CLog::Log(LOGDEBUG, "Found matching nfo file: {}", CURL::GetRedacted(strNfo));
+ result = nfoReader.Create(strNfo, info);
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ CLog::Log(LOGDEBUG, "{} Got details from nfo", __FUNCTION__);
+ nfoReader.GetDetails(albumInfo.GetAlbum());
+ return INFO_ADDED;
+ }
+ else if (result == CInfoScanner::URL_NFO ||
+ result == CInfoScanner::COMBINED_NFO)
+ {
+ CScraperUrl scrUrl(nfoReader.ScraperUrl());
+ CMusicAlbumInfo albumNfo("nfo",scrUrl);
+ ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo();
+ CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", nfoReaderScraper->Name());
+ CLog::Log(LOGDEBUG, "-- nfo url: {}", scrUrl.GetFirstThumbUrl());
+ scraper.SetScraperInfo(nfoReaderScraper);
+ scraper.GetAlbums().clear();
+ scraper.GetAlbums().push_back(albumNfo);
+ }
+ else if (result != CInfoScanner::OVERRIDE_NFO)
+ CLog::Log(LOGERROR, "Unable to find an url in nfo file: {}", strNfo);
+ }
+
+ if (!scraper.CheckValidOrFallback(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER)))
+ { // the current scraper is invalid, as is the default - bail
+ CLog::Log(LOGERROR, "{} - current and default scrapers are invalid. Pick another one",
+ __FUNCTION__);
+ return INFO_ERROR;
+ }
+
+ if (!scraper.GetAlbumCount())
+ {
+ scraper.FindAlbumInfo(album.strAlbum, album.GetAlbumArtistString());
+
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+ /*
+ Finding album using xml scraper may request data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
+ returns 503 errors for all calls from that IP address.
+ To stay below the rate-limit threshold wait 1s before proceeding
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+ }
+
+ CGUIDialogSelect *pDlg = NULL;
+ int iSelectedAlbum=0;
+ if ((result == CInfoScanner::NO_NFO || result == CInfoScanner::OVERRIDE_NFO)
+ && !bMusicBrainz)
+ {
+ iSelectedAlbum = -1; // set negative so that we can detect a failure
+ if (scraper.Succeeded() && scraper.GetAlbumCount() >= 1)
+ {
+ double bestRelevance = 0;
+ double minRelevance = static_cast<double>(THRESHOLD);
+ if (pDialog || scraper.GetAlbumCount() > 1) // score the matches
+ {
+ //show dialog with all albums found
+ if (pDialog)
+ {
+ pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
+ pDlg->Reset();
+ pDlg->EnableButton(true, 413); // manual
+ pDlg->SetUseDetails(true);
+ }
+
+ CFileItemList items;
+ for (int i = 0; i < scraper.GetAlbumCount(); ++i)
+ {
+ CMusicAlbumInfo& info = scraper.GetAlbum(i);
+ double relevance = static_cast<double>(info.GetRelevance());
+ if (relevance < 0)
+ relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, album.strAlbum,
+ info.GetAlbum().GetAlbumArtistString(),
+ album.GetAlbumArtistString());
+
+ // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches)
+ // otherwise, perfect matches only
+ if (relevance >= std::max(minRelevance, bestRelevance))
+ { // we auto-select the best of these
+ bestRelevance = relevance;
+ iSelectedAlbum = i;
+ }
+ if (pDialog)
+ {
+ // set the label to [relevance] album - artist
+ std::string strTemp = StringUtils::Format("[{:0.2f}] {}", relevance, info.GetTitle2());
+ CFileItemPtr item(new CFileItem("", false));
+ item->SetLabel(strTemp);
+
+ std::string strTemp2;
+ if (!scraper.GetAlbum(i).GetAlbum().strType.empty())
+ strTemp2 += scraper.GetAlbum(i).GetAlbum().strType;
+ if (!scraper.GetAlbum(i).GetAlbum().strReleaseDate.empty())
+ strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseDate;
+ if (!scraper.GetAlbum(i).GetAlbum().strReleaseStatus.empty())
+ strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseStatus;
+ if (!scraper.GetAlbum(i).GetAlbum().strLabel.empty())
+ strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strLabel;
+ item->SetLabel2(strTemp2);
+
+ item->SetArt(scraper.GetAlbum(i).GetAlbum().art);
+
+ items.Add(item);
+ }
+ if (!pDialog && relevance > 0.999) // we're so close, no reason to search further
+ break;
+ }
+
+ if (pDialog)
+ {
+ pDlg->Sort(false);
+ pDlg->SetItems(items);
+ pDlg->Open();
+
+ // and wait till user selects one
+ if (pDlg->GetSelectedItem() < 0)
+ { // none chosen
+ if (!pDlg->IsButtonPressed())
+ return INFO_CANCELLED;
+
+ // manual button pressed
+ std::string strNewAlbum = album.strAlbum;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strNewAlbum, CVariant{g_localizeStrings.Get(16011)}, false))
+ return INFO_CANCELLED;
+ if (strNewAlbum == "")
+ return INFO_CANCELLED;
+
+ std::string strNewArtist = album.GetAlbumArtistString();
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false))
+ return INFO_CANCELLED;
+
+ pDialog->SetLine(0, CVariant{strNewAlbum});
+ pDialog->SetLine(1, CVariant{strNewArtist});
+ pDialog->Progress();
+
+ CAlbum newAlbum = album;
+ newAlbum.strAlbum = strNewAlbum;
+ newAlbum.strArtistDesc = strNewArtist;
+
+ return DownloadAlbumInfo(newAlbum, info, albumInfo, bUseScrapedMBID, pDialog);
+ }
+ iSelectedAlbum = pDlg->GetSelectedItem();
+ }
+ }
+ else
+ {
+ CMusicAlbumInfo& info = scraper.GetAlbum(0);
+ double relevance = static_cast<double>(info.GetRelevance());
+ if (relevance < 0)
+ relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum,
+ album.strAlbum,
+ info.GetAlbum().GetAlbumArtistString(),
+ album.GetAlbumArtistString());
+ if (relevance < static_cast<double>(THRESHOLD))
+ return INFO_NOT_FOUND;
+
+ iSelectedAlbum = 0;
+ }
+ }
+
+ if (iSelectedAlbum < 0)
+ return INFO_NOT_FOUND;
+
+ }
+
+ scraper.LoadAlbumInfo(iSelectedAlbum);
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+ if (!scraper.Succeeded())
+ return INFO_ERROR;
+ /*
+ Fetching album details using xml scraper may makes requests for data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
+ returns 503 errors for all calls from that IP address.
+ To stay below the rate-limit threshold wait 1s before proceeding incase next action is
+ to scrape another album or artist
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+
+ albumInfo = scraper.GetAlbum(iSelectedAlbum);
+
+ if (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
+ nfoReader.GetDetails(albumInfo.GetAlbum(), NULL, true);
+
+ return INFO_ADDED;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::DownloadArtistInfo(const CArtist& artist,
+ const ADDON::ScraperPtr& info,
+ MUSIC_GRABBER::CMusicArtistInfo& artistInfo,
+ bool bUseScrapedMBID,
+ CGUIDialogProgress* pDialog)
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20320), info->Name()));
+ m_handle->SetText(artist.strArtist);
+ }
+
+ // clear our scraper cache
+ info->ClearCache();
+
+ CMusicInfoScraper scraper(info);
+ bool bMusicBrainz = false;
+ /*
+ When the mbid is derived from tags scraping of artist information is done directly
+ using that ID, otherwise the lookup is based on name and can mis-identify the artist
+ (many have same name). To be able to correct any mistakes a manual refresh of artist
+ information uses either the mbid if derived from tags or the artist name, not any previously
+ scraped mbid.
+ */
+ if (!artist.strMusicBrainzArtistID.empty() && (!artist.bScrapedMBID || bUseScrapedMBID))
+ {
+ CScraperUrl musicBrainzURL;
+ if (ResolveMusicBrainz(artist.strMusicBrainzArtistID, info, musicBrainzURL))
+ {
+ CMusicArtistInfo artistNfo("nfo", musicBrainzURL);
+ scraper.GetArtists().clear();
+ scraper.GetArtists().push_back(artistNfo);
+ bMusicBrainz = true;
+ }
+ }
+
+ // Handle nfo files
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ CNfoFile nfoReader;
+ std::string strNfo;
+ std::string path;
+ bool existsNFO = false;
+ // First look for nfo in the artists folder, the primary location
+ path = artist.strPath;
+ // Get path when don't already have it.
+ bool artistpathfound = !path.empty();
+ if (!artistpathfound)
+ artistpathfound = m_musicDatabase.GetArtistPath(artist, path);
+ if (artistpathfound)
+ {
+ strNfo = URIUtils::AddFileToFolder(path, "artist.nfo");
+ existsNFO = CFileUtils::Exists(strNfo);
+ }
+
+ // If not there fall back local to music files (historic location for those album artists with a unique folder)
+ if (!existsNFO)
+ {
+ artistpathfound = m_musicDatabase.GetOldArtistPath(artist.idArtist, path);
+ if (artistpathfound)
+ {
+ strNfo = URIUtils::AddFileToFolder(path, "artist.nfo");
+ existsNFO = CFileUtils::Exists(strNfo);
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} not have path, nfo file not possible", artist.strArtist);
+ }
+
+ // When on GUI ask user if they want to ignore nfo and refresh from Internet
+ if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(21891, 20446))
+ {
+ existsNFO = false;
+ CLog::Log(LOGDEBUG, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo));
+ }
+
+ if (existsNFO)
+ {
+ CLog::Log(LOGDEBUG, "Found matching nfo file: {}", CURL::GetRedacted(strNfo));
+ result = nfoReader.Create(strNfo, info);
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ CLog::Log(LOGDEBUG, "{} Got details from nfo", __FUNCTION__);
+ nfoReader.GetDetails(artistInfo.GetArtist());
+ return INFO_ADDED;
+ }
+ else if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
+ {
+ CScraperUrl scrUrl(nfoReader.ScraperUrl());
+ CMusicArtistInfo artistNfo("nfo", scrUrl);
+ ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo();
+ CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", nfoReaderScraper->Name());
+ CLog::Log(LOGDEBUG, "-- nfo url: {}", scrUrl.GetFirstThumbUrl());
+ scraper.SetScraperInfo(nfoReaderScraper);
+ scraper.GetArtists().push_back(artistNfo);
+ }
+ else
+ CLog::Log(LOGERROR, "Unable to find an url in nfo file: {}", strNfo);
+ }
+
+ if (!scraper.GetArtistCount())
+ {
+ scraper.FindArtistInfo(artist.strArtist);
+
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+ /*
+ Finding artist using xml scraper makes a request for data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter
+ the server returns 503 errors for all calls from that IP address. To stay
+ below the rate-limit threshold wait 1s before proceeding
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+ }
+
+ int iSelectedArtist = 0;
+ if (result == CInfoScanner::NO_NFO && !bMusicBrainz)
+ {
+ if (scraper.GetArtistCount() >= 1)
+ {
+ // now load the first match
+ if (pDialog && scraper.GetArtistCount() > 1)
+ {
+ // if we found more then 1 album, let user choose one
+ CGUIDialogSelect *pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDlg)
+ {
+ pDlg->SetHeading(CVariant{g_localizeStrings.Get(21890)});
+ pDlg->Reset();
+ pDlg->EnableButton(true, 413); // manual
+
+ for (int i = 0; i < scraper.GetArtistCount(); ++i)
+ {
+ // set the label to artist
+ CFileItem item(scraper.GetArtist(i).GetArtist());
+ std::string strTemp = scraper.GetArtist(i).GetArtist().strArtist;
+ if (!scraper.GetArtist(i).GetArtist().strBorn.empty())
+ strTemp += " ("+scraper.GetArtist(i).GetArtist().strBorn+")";
+ if (!scraper.GetArtist(i).GetArtist().strDisambiguation.empty())
+ strTemp += " - " + scraper.GetArtist(i).GetArtist().strDisambiguation;
+ if (!scraper.GetArtist(i).GetArtist().genre.empty())
+ {
+ std::string genres = StringUtils::Join(scraper.GetArtist(i).GetArtist().genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (!genres.empty())
+ strTemp = StringUtils::Format("[{}] {}", genres, strTemp);
+ }
+ item.SetLabel(strTemp);
+ item.m_idepth = i; // use this to hold the index of the album in the scraper
+ pDlg->Add(item);
+ }
+ pDlg->Open();
+
+ // and wait till user selects one
+ if (pDlg->GetSelectedItem() < 0)
+ { // none chosen
+ if (!pDlg->IsButtonPressed())
+ return INFO_CANCELLED;
+
+ // manual button pressed
+ std::string strNewArtist = artist.strArtist;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false))
+ return INFO_CANCELLED;
+
+ if (pDialog)
+ {
+ pDialog->SetLine(0, CVariant{strNewArtist});
+ pDialog->Progress();
+ }
+
+ CArtist newArtist;
+ newArtist.strArtist = strNewArtist;
+ return DownloadArtistInfo(newArtist, info, artistInfo, bUseScrapedMBID, pDialog);
+ }
+ iSelectedArtist = pDlg->GetSelectedFileItem()->m_idepth;
+ }
+ }
+ }
+ else
+ return INFO_NOT_FOUND;
+ }
+ /*
+ Fetching artist details using xml scraper makes requests for data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
+ returns 503 errors for all calls from that IP address.
+ To stay below the rate-limit threshold wait 1s before proceeding incase next action is
+ to scrape another album or artist
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+
+ scraper.LoadArtistInfo(iSelectedArtist, artist.strArtist);
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+
+ if (!scraper.Succeeded())
+ return INFO_ERROR;
+
+ artistInfo = scraper.GetArtist(iSelectedArtist);
+
+ if (result == CInfoScanner::COMBINED_NFO)
+ nfoReader.GetDetails(artistInfo.GetArtist(), NULL, true);
+
+ return INFO_ADDED;
+}
+
+bool CMusicInfoScanner::ResolveMusicBrainz(const std::string &strMusicBrainzID, const ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL)
+{
+ // We have a MusicBrainz ID
+ // Get a scraper that can resolve it to a MusicBrainz URL & force our
+ // search directly to the specific album.
+ bool bMusicBrainz = false;
+ try
+ {
+ musicBrainzURL = preferredScraper->ResolveIDToUrl(strMusicBrainzID);
+ }
+ catch (const ADDON::CScraperError &sce)
+ {
+ if (sce.FAborted())
+ return false;
+ }
+
+ if (musicBrainzURL.HasUrls())
+ {
+ CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", preferredScraper->Name());
+ CLog::Log(LOGDEBUG, "-- nfo url: {}", musicBrainzURL.GetFirstThumbUrl());
+ bMusicBrainz = true;
+ }
+
+ return bMusicBrainz;
+}
+
+void CMusicInfoScanner::ScannerWait(unsigned int milliseconds)
+{
+ if (milliseconds > 10)
+ {
+ CEvent m_StopEvent;
+ m_StopEvent.Wait(std::chrono::milliseconds(milliseconds));
+ }
+ else
+ std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
+}
+
+bool CMusicInfoScanner::AddArtistArtwork(CArtist& artist, const std::string& artfolder)
+{
+ if (!artist.thumbURL.HasUrls() && artfolder.empty())
+ return false; // No local or scraped possible art to process
+
+ if (artist.art.empty())
+ m_musicDatabase.GetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
+
+ std::map<std::string, std::string> addedart;
+ std::string strArt;
+
+ // Handle thumb separately, can be from multiple confgurable file names
+ if (artist.art.find("thumb") == artist.art.end())
+ {
+ if (!artfolder.empty())
+ { // Local music thumbnail images named by "musiclibrary.musicthumbs"
+ CFileItem item(artfolder, true);
+ strArt = item.GetUserMusicThumb(true);
+ }
+ if (strArt.empty())
+ strArt = CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType("thumb"));
+ if (!strArt.empty())
+ addedart.insert(std::make_pair("thumb", strArt));
+ }
+
+ // Process additional art types in artist folder
+ AddLocalArtwork(addedart, MediaTypeArtist, artist.strArtist, artfolder);
+
+ // Process remote artist art filling gaps with first of scraped art URLs
+ AddRemoteArtwork(addedart, MediaTypeArtist, artist.thumbURL);
+
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+
+ for (const auto& it : addedart)
+ {
+ // Cache thumb, fanart and other whitelisted artwork immediately
+ // (other art types will be cached when first displayed)
+ if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" ||
+ it.first == "fanart")
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(it.second);
+ auto ret = artist.art.insert(it);
+ if (ret.second)
+ m_musicDatabase.SetArtForItem(artist.idArtist, MediaTypeArtist, it.first, it.second);
+ }
+ return addedart.size() > 0;
+}
+
+bool CMusicInfoScanner::AddAlbumArtwork(CAlbum& album)
+{
+ // Fetch album path and any subfolders (disc sets).
+ // No paths found when songs from different albums are in one folder
+ std::vector<std::pair<std::string, int>> paths;
+ m_musicDatabase.GetAlbumPaths(album.idAlbum, paths);
+ for (const auto& pathpair : paths)
+ {
+ if (album.strPath.empty())
+ album.strPath = pathpair.first.c_str();
+ else
+ // When more than one album path is the common path
+ URIUtils::GetCommonPath(album.strPath, pathpair.first.c_str());
+ }
+
+ if (!album.thumbURL.HasUrls() && album.strPath.empty())
+ return false; // No local or scraped possible art to process
+
+ if (album.art.empty())
+ m_musicDatabase.GetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
+ auto thumb = album.art.find("thumb"); // Find "thumb", may want to replace it
+
+ bool replaceThumb = paths.size() > 1;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_PREFERONLINEALBUMART))
+ {
+ // When "prefer online album art" enabled and we have a thumb as embedded art
+ // then replace it if we find a scraped cover
+ if (thumb != album.art.end() && StringUtils::StartsWith(thumb->second, "image://"))
+ replaceThumb = true;
+ }
+
+ std::map<std::string, std::string> addedart;
+ std::string strArt;
+
+ // Fetch local art from album folder
+ // Handle thumbs separately, can be from multiple confgurable file names
+ if (replaceThumb || thumb == album.art.end())
+ {
+ if (!album.strPath.empty())
+ { // Local music thumbnail images named by "musiclibrary.musicthumbs"
+ CFileItem item(album.strPath, true);
+ strArt = item.GetUserMusicThumb(true);
+ }
+ if (strArt.empty())
+ strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType("thumb"));
+ if (!strArt.empty())
+ {
+ if (thumb != album.art.end())
+ album.art.erase(thumb);
+ addedart.insert(std::make_pair("thumb", strArt));
+ }
+ }
+ // Process additional art types in album folder
+ AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, album.strPath);
+
+ // Fetch local art from disc subfolders
+ if (paths.size() > 1)
+ {
+ CMusicThumbLoader loader;
+ std::string firstDiscThumb;
+ int iDiscThumb = 10000;
+ for (const auto& pathpair : paths)
+ {
+ strArt.clear();
+
+ int discnum = m_musicDatabase.GetDiscnumberForPathID(pathpair.second);
+ if (discnum > 0)
+ {
+ // Handle thumbs separately. Get thumb for path from textures db cached during scan
+ // (could be embedded or local file from multiple confgurable file names)
+ CFileItem item(pathpair.first.c_str(), true);
+ std::string strArtType = StringUtils::Format("{}{}", "thumb", discnum);
+ strArt = loader.GetCachedImage(item, "thumb");
+ if (strArt.empty())
+ strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType(strArtType));
+ if (!strArt.empty())
+ {
+ addedart.insert(std::make_pair(strArtType, strArt));
+ // Store thumb of first disc with a thumb
+ if (discnum < iDiscThumb)
+ {
+ iDiscThumb = discnum;
+ firstDiscThumb = strArt;
+ }
+ }
+ }
+ // Process additional art types in disc subfolder
+ AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, pathpair.first, discnum);
+ }
+ // Finally if we still don't have album thumb then use the art from the
+ // first disc in the set with a thumb
+ if (!firstDiscThumb.empty() && album.art.find("thumb") == album.art.end())
+ {
+ m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, "thumb", firstDiscThumb);
+ // Assign art as folder thumb (in textures db) as well
+
+ CFileItem albumItem(album.strPath, true);
+ loader.SetCachedImage(albumItem, "thumb", firstDiscThumb);
+ }
+ }
+
+ // Process remote album art filling gaps with first of scraped art URLs
+ AddRemoteArtwork(addedart, MediaTypeAlbum, album.thumbURL);
+
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+ for (const auto& it : addedart)
+ {
+ // Cache thumb, fanart and whitelisted artwork immediately
+ // (other art types will be cached when first displayed)
+ if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" ||
+ it.first == "fanart")
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(it.second);
+
+ auto ret = album.art.insert(it);
+ if (ret.second)
+ m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, it.first, it.second);
+ }
+ return addedart.size() > 0;
+}
+
+std::vector<CVariant> CMusicInfoScanner::GetArtWhitelist(const MediaType& mediaType, int iArtLevel)
+{
+ std::vector<CVariant> whitelistarttypes;
+ if (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC)
+ {
+ // Basic artist artwork = thumb + fanart (but not "family" fanart1, fanart2 etc.)
+ // Basic album artwork = thumb only, thumb handled separately not in whitelist
+ if (mediaType == MediaTypeArtist)
+ whitelistarttypes.emplace_back("fanart");
+ }
+ else
+ {
+ if (mediaType == MediaTypeArtist)
+ whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST);
+ else
+ whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST);
+ }
+
+ return whitelistarttypes;
+}
+
+bool CMusicInfoScanner::AddLocalArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const std::string& mediaName,
+ const std::string& artfolder,
+ int discnum)
+{
+ if (artfolder.empty())
+ return false;
+
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+
+ std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel);
+ bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) ||
+ ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEALLLOCALART));
+
+ // Not useall and empty whitelist means no extra art is picked up from either place
+ if (!bUseAll && whitelistarttypes.empty())
+ return false;
+
+ // Image files used as thumbs
+ std::vector<CVariant> thumbs = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS);
+
+ // Find local art
+ CFileItemList availableArtFiles;
+ CDirectory::GetDirectory(artfolder, availableArtFiles,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+
+ for (const auto& artFile : availableArtFiles)
+ {
+ if (artFile->m_bIsFolder)
+ continue;
+ std::string strCandidate = URIUtils::GetFileName(artFile->GetPath());
+ // Strip media name
+ if (!mediaName.empty() && StringUtils::StartsWith(strCandidate, mediaName))
+ strCandidate.erase(0, mediaName.length());
+ StringUtils::ToLower(strCandidate);
+ // Skip files already used as "thumb"
+ // Typically folder.jpg but can be from multiple confgurable file names
+ if (std::find(thumbs.begin(), thumbs.end(), strCandidate) != thumbs.end())
+ continue;
+ // Grab and strip file extension
+ std::string strExt;
+ size_t period = strCandidate.find_last_of("./\\");
+ if (period != std::string::npos && strCandidate[period] == '.')
+ {
+ strExt = strCandidate.substr(period); // ".jpg", ".png" etc.
+ strCandidate.erase(period); // "abc16" for file Abc16.jpg
+ }
+ if (strCandidate.empty())
+ continue;
+ // Validate art type name
+ size_t last_index = strCandidate.find_last_not_of("0123456789");
+ std::string strDigits = strCandidate.substr(last_index + 1);
+ std::string strFamily = strCandidate.substr(0, last_index + 1); // "abc" of "abc16"
+ if (strFamily.empty())
+ continue;
+ if (!MUSIC_UTILS::IsValidArtType(strCandidate))
+ continue;
+ // Disc specific art from disc subfolder
+ // Skip art where digits of filename do not match disc number
+ if (discnum > 0 && !strDigits.empty() && (atoi(strDigits.c_str()) != discnum))
+ continue;
+
+ // Use all art, or check for basic level art in whitelist exactly allowing for disc number,
+ // or for custom art check whitelist contains art type family (strip trailing digits)
+ // e.g. 'fanart', 'fanart1', 'fanart2' etc. all match whitelist entry 'fanart'
+ std::string strCheck = strCandidate;
+ if (discnum > 0 || iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM)
+ strCheck = strFamily;
+ if (bUseAll || std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strCheck) !=
+ whitelistarttypes.end())
+ {
+ if (!strDigits.empty())
+ {
+ // Catch any variants of music thumbs e.g. folder2.jpg as "thumb2"
+ // Used for disc sets when files all in one album folder
+ if (std::find(thumbs.begin(), thumbs.end(), strFamily + strExt) != thumbs.end())
+ strCandidate = "thumb" + strDigits;
+ }
+ else if (discnum > 0)
+ // Append disc number when candidate art type (and file) not have it
+ strCandidate += std::to_string(discnum);
+
+ if (art.find(strCandidate) == art.end())
+ art.insert(std::make_pair(strCandidate, artFile->GetPath()));
+ }
+ }
+
+ return art.size() > 0;
+}
+
+bool CMusicInfoScanner::AddRemoteArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const CScraperUrl& thumbURL)
+{
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+
+ std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel);
+ bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) ||
+ ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEALLREMOTEART));
+
+ // not useall and empty whitelist means no extra art is picked up from either place
+ if (!bUseAll && whitelistarttypes.empty())
+ return false;
+
+ // Add online art
+ // Done for artists and albums, so not need repeating at disc level
+ for (const auto& url : thumbURL.GetUrls())
+ {
+ // Art type is encoded into the scraper XML held in thumbURL as optional "aspect=" field.
+ // Those URL without aspect are also returned for all other type values.
+ // Loop through all the first URLS of each type except "thumb" and add if art missing
+ if (url.m_aspect.empty() || url.m_aspect == "thumb")
+ continue;
+ if (!bUseAll)
+ { // Check whitelist for art type family e.g. "discart" for aspect="discart2"
+ std::string strName = url.m_aspect;
+ if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC)
+ StringUtils::TrimRight(strName, "0123456789");
+ if (std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strName) ==
+ whitelistarttypes.end())
+ continue;
+ }
+ if (art.find(url.m_aspect) == art.end())
+ {
+ std::string strArt = CScraperUrl::GetThumbUrl(url);
+ if (!strArt.empty())
+ art.insert(std::make_pair(url.m_aspect, strArt));
+ }
+ }
+
+ return art.size() > 0;
+}
+
+// This function is the Run() function of the IRunnable
+// CFileCountReader and runs in a separate thread.
+void CMusicInfoScanner::Run()
+{
+ int count = 0;
+ for (auto& it : m_pathsToScan)
+ {
+ count += CountFilesRecursively(it);
+ }
+ m_itemCount = count;
+}
+
+// Recurse through all folders we scan and count files
+int CMusicInfoScanner::CountFilesRecursively(const std::string& strPath)
+{
+ // load subfolder
+ CFileItemList items;
+ CDirectory::GetDirectory(strPath, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), DIR_FLAG_NO_FILE_DIRS);
+
+ if (m_bStop)
+ return 0;
+
+ // true for recursive counting
+ int count = CountFiles(items, true);
+ return count;
+}
+
+int CMusicInfoScanner::CountFiles(const CFileItemList &items, bool recursive)
+{
+ int count = 0;
+ for (int i=0; i<items.Size(); ++i)
+ {
+ const CFileItemPtr pItem=items[i];
+
+ if (recursive && pItem->m_bIsFolder)
+ count+=CountFilesRecursively(pItem->GetPath());
+ else if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
+ count++;
+ }
+ return count;
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScanner.h b/xbmc/music/infoscanner/MusicInfoScanner.h
new file mode 100644
index 0000000..d5d5470
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScanner.h
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2005-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 "InfoScanner.h"
+#include "MusicAlbumInfo.h"
+#include "MusicInfoScraper.h"
+#include "music/MusicDatabase.h"
+#include "threads/IRunnable.h"
+#include "threads/Thread.h"
+#include "utils/ScraperUrl.h"
+
+class CAlbum;
+class CArtist;
+class CGUIDialogProgressBarHandle;
+
+namespace MUSIC_INFO
+{
+
+class CMusicInfoScanner : public IRunnable, public CInfoScanner
+{
+public:
+ /*! \brief Flags for controlling the scanning process
+ */
+ enum SCAN_FLAGS { SCAN_NORMAL = 0,
+ SCAN_ONLINE = 1 << 0,
+ SCAN_BACKGROUND = 1 << 1,
+ SCAN_RESCAN = 1 << 2,
+ SCAN_ARTISTS = 1 << 3,
+ SCAN_ALBUMS = 1 << 4 };
+
+ CMusicInfoScanner();
+ ~CMusicInfoScanner() override;
+
+ void Start(const std::string& strDirectory, int flags);
+ void FetchAlbumInfo(const std::string& strDirectory, bool refresh = false);
+ void FetchArtistInfo(const std::string& strDirectory, bool refresh = false);
+ void Stop();
+
+ /*! \brief Categorize FileItems into Albums, Songs, and Artists
+ This takes a list of FileItems and turns it into a tree of Albums,
+ Artists, and Songs.
+ Albums are defined uniquely by the album name and album artist.
+
+ \param songs [in/out] list of songs to categorise - albumartist field may be altered.
+ \param albums [out] albums found within these songs.
+ */
+ static void FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap = NULL);
+
+ /*! \brief Scrape additional album information and update the music database with it.
+ Given an album, search for it using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param album [in/out] the album to update
+ \param scraper [in] the album scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateAlbumInfo(CAlbum& album, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Scrape additional artist information and update the music database with it.
+ Given an artist, search for it using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param artist [in/out] the artist to update
+ \param scraper [in] the artist scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateArtistInfo(CArtist& artist, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+protected:
+ virtual void Process();
+ bool DoScan(const std::string& strDirectory) override;
+
+
+ /*! \brief Find art for albums
+ Based on the albums in the folder, finds whether we have unique album art
+ and assigns to the album if we do.
+
+ In order of priority:
+ 1. If there is a single album in the folder, then the folder art is assigned to the album.
+ 2. We find the art for each song. A .tbn file takes priority over embedded art.
+ 3. If we have a unique piece of art for all songs in the album, we assign that to the album
+ and remove that art from each song so that they inherit from the album.
+ 4. If there is not a unique piece of art for each song, then no art is assigned
+ to the album.
+
+ \param albums [in/out] list of albums to categorise - art field may be altered.
+ \param path [in] path containing albums.
+ */
+ static void FindArtForAlbums(VECALBUMS &albums, const std::string &path);
+
+ /*! \brief Scrape additional album information and update the database.
+ Search for the given album using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param album [in/out] the album to update
+ \param scraper [in] the album scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateDatabaseAlbumInfo(CAlbum& album, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Scrape additional artist information and update the database.
+ Search for the given artist using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param artist [in/out] the artist to update
+ \param scraper [in] the artist scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateDatabaseArtistInfo(CArtist& artist, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Using the scrapers download metadata for an album
+ Given a CAlbum style struct containing some data about an album, query
+ the scrapers to try and get more information about the album. The responsibility
+ is with the caller to do something with that information. It will be passed back
+ in a MusicInfo struct, which you can save, display to the user or throw away.
+ \param album [in] a partially or fully filled out album structure containing the search query
+ \param scraper [in] the scraper to query, usually the default or the relevant scraper for the musicdb path
+ \param albumInfo [in/out] a CMusicAlbumInfo struct which will be populated with the output of the scraper
+ \param bUseScrapedMBID [in] should scraper use any previously scraped mbid to identify the artist, or use artist name?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET DownloadAlbumInfo(const CAlbum& album, const ADDON::ScraperPtr& scraper, MUSIC_GRABBER::CMusicAlbumInfo& albumInfo, bool bUseScrapedMBID, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Using the scrapers download metadata for an artist
+ Given a CAlbum style struct containing some data about an artist, query
+ the scrapers to try and get more information about the artist. The responsibility
+ is with the caller to do something with that information. It will be passed back
+ in a MusicInfo struct, which you can save, display to the user or throw away.
+ \param artist [in] a partially or fully filled out artist structure containing the search query
+ \param scraper [in] the scraper to query, usually the default or the relevant scraper for the musicdb path
+ \param artistInfo [in/out] a CMusicAlbumInfo struct which will be populated with the output of the scraper
+ \param bUseScrapedMBID [in] should scraper use any previously scraped mbid to identify the album, or use album and artist name?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET DownloadArtistInfo(const CArtist& artist, const ADDON::ScraperPtr& scraper, MUSIC_GRABBER::CMusicArtistInfo& artistInfo, bool bUseScrapedMBID, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Get the types of art for an artist or album that are to be
+ automatically fetched from local files during scanning
+ \param mediaType [in] artist or album
+ \param iArtLevel [in] art level
+ \return vector of art types that are to be fetched during scanning
+ */
+ std::vector<CVariant> GetArtWhitelist(const MediaType& mediaType, int iArtLevel);
+
+ /*! \brief Add extra local artwork for albums and artists
+ This common utility scans the given folder for local (non-thumb) art.
+ The art types checked are determined by whitelist and usealllocalart settings.
+ \param art [in/out] map of art type and file location (URL or path) pairs
+ \param mediaType [in] artist or album
+ \param mediaName [in] artist or album name that may be stripped from image file names
+ \param artfolder [in] path of folder containing local image files
+ \return true when art is added
+ */
+ bool AddLocalArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const std::string& mediaName,
+ const std::string& artfolder,
+ int discnum = 0);
+
+ /*! \brief Add extra remote artwork for albums and artists
+ This common utility fills the gaps in artwork using the first available art of each type from the
+ possible art URL results of previous scraping.
+ The art types applied are determined by whitelist and usealllocalart settings.
+ \param art [in/out] map of art type and file location (URL or path) pairs
+ \param mediaType [in] artist or album
+ \param thumbURL [in] URLs for potential remote artwork (provided by scrapers)
+ \return true when art is added
+ */
+ bool AddRemoteArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const CScraperUrl& thumbURL);
+
+ /*! \brief Add art for an artist
+ This scans the given folder for local art and/or applies the first available art of each type
+ from the possible art URLs previously scraped. Art is added to any already stored by previous
+ scanning etc.The art types processed are determined by whitelist and other art settings.
+ When usealllocalart is enabled then all local image files are applied as art (providing name is
+ valid for an art type), and then the URL list of remote art is checked adding the first available
+ image of each art type not yet in the art map.
+ Art found is saved in the album structure and the music database. The images found are cached.
+ \param artist [in/out] an artist, the art is set
+ \param artfolder [in] path of the location to search for local art files
+ \return true when art is added
+ */
+ bool AddArtistArtwork(CArtist& artist, const std::string& artfolder);
+
+ /*! \brief Add art for an album
+ This scans the album folder, and any disc set subfolders, for local art and/or applies the first
+ available art of each type from the possible art URLs previously scraped. Only those subfolders
+ beneath the album folder containing music files tagged with same unique disc number are scanned.
+ Art is added to any already stored by previous scanning, only "thumb" is optionally replaced.
+ The art types processed are determined by whitelist and other art settings. When usealllocalart is
+ enabled then all local image files are applied as art (providing name is valid for an art type),
+ and then the URL list of remote art is checked adding the first available image of each art type
+ not yet in the art map.
+ Art found is saved in the album structure and the music database. The images found are cached.
+ \param artist [in/out] an album, the art is set
+ \return true when art is added
+ */
+ bool AddAlbumArtwork(CAlbum& album);
+
+ /*! \brief Scan in the ID3/Ogg/FLAC tags for a bunch of FileItems
+ Given a list of FileItems, scan in the tags for those FileItems
+ and populate a new FileItemList with the files that were successfully scanned.
+ Add album to library, populate a list of album ids added for possible scraping later.
+ Any files which couldn't be scanned (no/bad tags) are discarded in the process.
+ \param items [in] list of FileItems to scan
+ \param scannedItems [in] list to populate with the scannedItems
+ */
+ int RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items);
+
+ void RetrieveLocalArt();
+ void ScrapeInfoAddedAlbums();
+
+ /*! \brief Scan in the ID3/Ogg/FLAC tags for a bunch of FileItems
+ Given a list of FileItems, scan in the tags for those FileItems
+ and populate a new FileItemList with the files that were successfully scanned.
+ Any files which couldn't be scanned (no/bad tags) are discarded in the process.
+ \param items [in] list of FileItems to scan
+ \param scannedItems [in] list to populate with the scannedItems
+ */
+ INFO_RET ScanTags(const CFileItemList& items, CFileItemList& scannedItems);
+ int GetPathHash(const CFileItemList &items, std::string &hash);
+
+ void Run() override;
+ int CountFiles(const CFileItemList& items, bool recursive);
+ int CountFilesRecursively(const std::string& strPath);
+
+ /*! \brief Resolve a MusicBrainzID to a URL
+ If we have a MusicBrainz ID for an artist or album,
+ resolve it to an MB URL and set up the scrapers accordingly.
+
+ \param preferredScraper [in] A ScraperPtr to the preferred album/artist scraper.
+ \param musicBrainzURL [out] will be populated with the MB URL for the artist/album.
+ */
+ bool ResolveMusicBrainz(const std::string &strMusicBrainzID, const ADDON::ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL);
+
+ void ScannerWait(unsigned int milliseconds);
+
+ int m_currentItem;
+ int m_itemCount;
+ bool m_bStop;
+ bool m_needsCleanup = false;
+ int m_scanType = 0; // 0 - load from files, 1 - albums, 2 - artists
+ int m_idSourcePath;
+ CMusicDatabase m_musicDatabase;
+
+ std::set<int> m_albumsAdded;
+
+ std::set<std::string> m_seenPaths;
+ int m_flags;
+ CThread m_fileCountReader;
+};
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScraper.cpp b/xbmc/music/infoscanner/MusicInfoScraper.cpp
new file mode 100644
index 0000000..630c2a7
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScraper.cpp
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoScraper.h"
+
+#include "filesystem/CurlFile.h"
+#include "utils/log.h"
+
+using namespace MUSIC_GRABBER;
+using namespace ADDON;
+using namespace std::chrono_literals;
+
+CMusicInfoScraper::CMusicInfoScraper(const ADDON::ScraperPtr& scraper)
+ : CThread("MusicInfoScraper"), m_scraper(scraper)
+{
+ m_bSucceeded=false;
+ m_bCanceled=false;
+ m_iAlbum=-1;
+ m_iArtist=-1;
+ m_http = new XFILE::CCurlFile;
+}
+
+CMusicInfoScraper::~CMusicInfoScraper(void)
+{
+ StopThread();
+ delete m_http;
+}
+
+int CMusicInfoScraper::GetAlbumCount() const
+{
+ return (int)m_vecAlbums.size();
+}
+
+int CMusicInfoScraper::GetArtistCount() const
+{
+ return (int)m_vecArtists.size();
+}
+
+CMusicAlbumInfo& CMusicInfoScraper::GetAlbum(int iAlbum)
+{
+ return m_vecAlbums[iAlbum];
+}
+
+CMusicArtistInfo& CMusicInfoScraper::GetArtist(int iArtist)
+{
+ return m_vecArtists[iArtist];
+}
+
+void CMusicInfoScraper::FindAlbumInfo(const std::string& strAlbum, const std::string& strArtist /* = "" */)
+{
+ m_strAlbum=strAlbum;
+ m_strArtist=strArtist;
+ m_bSucceeded=false;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::FindArtistInfo(const std::string& strArtist)
+{
+ m_strArtist=strArtist;
+ m_bSucceeded=false;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::FindAlbumInfo()
+{
+ m_vecAlbums = m_scraper->FindAlbum(*m_http, m_strAlbum, m_strArtist);
+ m_bSucceeded = !m_vecAlbums.empty();
+}
+
+void CMusicInfoScraper::FindArtistInfo()
+{
+ m_vecArtists = m_scraper->FindArtist(*m_http, m_strArtist);
+ m_bSucceeded = !m_vecArtists.empty();
+}
+
+void CMusicInfoScraper::LoadAlbumInfo(int iAlbum)
+{
+ m_iAlbum=iAlbum;
+ m_iArtist=-1;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::LoadArtistInfo(int iArtist, const std::string &strSearch)
+{
+ m_iAlbum=-1;
+ m_iArtist=iArtist;
+ m_strSearch=strSearch;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::LoadAlbumInfo()
+{
+ if (m_iAlbum<0 || m_iAlbum>=(int)m_vecAlbums.size())
+ return;
+
+ CMusicAlbumInfo& album=m_vecAlbums[m_iAlbum];
+ // Clear album artist credits
+ album.GetAlbum().artistCredits.clear();
+ if (album.Load(*m_http,m_scraper))
+ m_bSucceeded=true;
+}
+
+void CMusicInfoScraper::LoadArtistInfo()
+{
+ if (m_iArtist<0 || m_iArtist>=(int)m_vecArtists.size())
+ return;
+
+ CMusicArtistInfo& artist=m_vecArtists[m_iArtist];
+ artist.GetArtist().strArtist.clear();
+ if (artist.Load(*m_http,m_scraper,m_strSearch))
+ m_bSucceeded=true;
+}
+
+bool CMusicInfoScraper::Completed()
+{
+ return Join(10ms);
+}
+
+bool CMusicInfoScraper::Succeeded()
+{
+ return !m_bCanceled && m_bSucceeded;
+}
+
+void CMusicInfoScraper::Cancel()
+{
+ m_http->Cancel();
+ m_bCanceled=true;
+ m_http->Reset();
+}
+
+bool CMusicInfoScraper::IsCanceled()
+{
+ return m_bCanceled;
+}
+
+void CMusicInfoScraper::OnStartup()
+{
+ m_bSucceeded=false;
+ m_bCanceled=false;
+}
+
+void CMusicInfoScraper::Process()
+{
+ try
+ {
+ if (m_strAlbum.size())
+ {
+ FindAlbumInfo();
+ m_strAlbum.clear();
+ m_strArtist.clear();
+ }
+ else if (m_strArtist.size())
+ {
+ FindArtistInfo();
+ m_strArtist.clear();
+ }
+ if (m_iAlbum>-1)
+ {
+ LoadAlbumInfo();
+ m_iAlbum=-1;
+ }
+ if (m_iArtist>-1)
+ {
+ LoadArtistInfo();
+ m_iArtist=-1;
+ }
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicInfoScraper::Process()");
+ }
+}
+
+bool CMusicInfoScraper::CheckValidOrFallback(const std::string &fallbackScraper)
+{
+ return true;
+//! @todo Handle fallback mechanism
+ /*
+ if (m_scraper->Path() != fallbackScraper &&
+ parser.Load("special://xbmc/system/scrapers/music/" + fallbackScraper))
+ {
+ CLog::Log(LOGWARNING, "{} - scraper {} fails to load, falling back to {}", __FUNCTION__, m_info.strPath, fallbackScraper);
+ m_info.strPath = fallbackScraper;
+ m_info.strContent = "albums";
+ m_info.strTitle = parser.GetName();
+ m_info.strDate = parser.GetDate();
+ m_info.strFramework = parser.GetFramework();
+ m_info.strLanguage = parser.GetLanguage();
+ m_info.settings.LoadSettingsXML("special://xbmc/system/scrapers/music/" + m_info.strPath);
+ return true;
+ }
+ return false; */
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScraper.h b/xbmc/music/infoscanner/MusicInfoScraper.h
new file mode 100644
index 0000000..fcab576
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScraper.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2005-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 "MusicAlbumInfo.h"
+#include "MusicArtistInfo.h"
+#include "addons/Scraper.h"
+#include "threads/Thread.h"
+
+#include <vector>
+
+namespace XFILE
+{
+class CurlFile;
+}
+
+namespace MUSIC_GRABBER
+{
+class CMusicInfoScraper : public CThread
+{
+public:
+ explicit CMusicInfoScraper(const ADDON::ScraperPtr &scraper);
+ ~CMusicInfoScraper(void) override;
+ void FindAlbumInfo(const std::string& strAlbum, const std::string& strArtist = "");
+ void LoadAlbumInfo(int iAlbum);
+ void FindArtistInfo(const std::string& strArtist);
+ void LoadArtistInfo(int iArtist, const std::string &strSearch);
+ bool Completed();
+ bool Succeeded();
+ void Cancel();
+ bool IsCanceled();
+ int GetAlbumCount() const;
+ int GetArtistCount() const;
+ CMusicAlbumInfo& GetAlbum(int iAlbum);
+ CMusicArtistInfo& GetArtist(int iArtist);
+ std::vector<CMusicArtistInfo>& GetArtists()
+ {
+ return m_vecArtists;
+ }
+ std::vector<CMusicAlbumInfo>& GetAlbums()
+ {
+ return m_vecAlbums;
+ }
+ void SetScraperInfo(const ADDON::ScraperPtr& scraper)
+ {
+ m_scraper = scraper;
+ }
+ /*!
+ \brief Checks whether we have a valid scraper. If not, we try the fallbackScraper
+ First tests the current scraper for validity by loading it. If it is not valid we
+ attempt to load the fallback scraper. If this is also invalid we return false.
+ \param fallbackScraper name of scraper to use as a fallback
+ \return true if we have a valid scraper (or the default is valid).
+ */
+ bool CheckValidOrFallback(const std::string &fallbackScraper);
+protected:
+ void FindAlbumInfo();
+ void LoadAlbumInfo();
+ void FindArtistInfo();
+ void LoadArtistInfo();
+ void OnStartup() override;
+ void Process() override;
+ std::vector<CMusicAlbumInfo> m_vecAlbums;
+ std::vector<CMusicArtistInfo> m_vecArtists;
+ std::string m_strAlbum;
+ std::string m_strArtist;
+ std::string m_strSearch;
+ int m_iAlbum;
+ int m_iArtist;
+ bool m_bSucceeded;
+ bool m_bCanceled;
+ XFILE::CCurlFile* m_http;
+ ADDON::ScraperPtr m_scraper;
+};
+
+}
diff --git a/xbmc/music/jobs/CMakeLists.txt b/xbmc/music/jobs/CMakeLists.txt
new file mode 100644
index 0000000..8ee0f5d
--- /dev/null
+++ b/xbmc/music/jobs/CMakeLists.txt
@@ -0,0 +1,15 @@
+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/MusicLibraryCleaningJob.cpp b/xbmc/music/jobs/MusicLibraryCleaningJob.cpp
new file mode 100644
index 0000000..4361566
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryCleaningJob.cpp
@@ -0,0 +1,40 @@
+/*
+ * 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 "MusicLibraryCleaningJob.h"
+
+#include "dialogs/GUIDialogProgress.h"
+#include "music/MusicDatabase.h"
+
+CMusicLibraryCleaningJob::CMusicLibraryCleaningJob(CGUIDialogProgress* progressDialog)
+ : CMusicLibraryProgressJob(nullptr)
+{
+ if (progressDialog)
+ SetProgressIndicators(nullptr, progressDialog);
+ SetAutoClose(true);
+}
+
+CMusicLibraryCleaningJob::~CMusicLibraryCleaningJob() = default;
+
+bool CMusicLibraryCleaningJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryCleaningJob* cleaningJob = dynamic_cast<const CMusicLibraryCleaningJob*>(job);
+ if (cleaningJob == nullptr)
+ return false;
+
+ return true;
+}
+
+bool CMusicLibraryCleaningJob::Work(CMusicDatabase &db)
+{
+ db.Cleanup(GetProgressDialog());
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryCleaningJob.h b/xbmc/music/jobs/MusicLibraryCleaningJob.h
new file mode 100644
index 0000000..6e893c6
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryCleaningJob.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "music/jobs/MusicLibraryProgressJob.h"
+
+#include <set>
+
+/*!
+ \brief Music library job implementation for cleaning the video library.
+*/
+class CMusicLibraryCleaningJob : public CMusicLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new music library cleaning job.
+ \param[in] progressDialog Progress dialog to be used to display the cleaning progress
+ */
+ CMusicLibraryCleaningJob(CGUIDialogProgress* progressDialog);
+ ~CMusicLibraryCleaningJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryCleaningJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+
+};
diff --git a/xbmc/music/jobs/MusicLibraryExportJob.cpp b/xbmc/music/jobs/MusicLibraryExportJob.cpp
new file mode 100644
index 0000000..ca63d13
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryExportJob.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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 "MusicLibraryExportJob.h"
+
+#include "dialogs/GUIDialogProgress.h"
+#include "music/MusicDatabase.h"
+#include "settings/LibExportSettings.h"
+
+CMusicLibraryExportJob::CMusicLibraryExportJob(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog)
+ : CMusicLibraryProgressJob(NULL),
+ m_settings(settings)
+{
+ if (progressDialog)
+ SetProgressIndicators(NULL, progressDialog);
+ SetAutoClose(true);
+}
+
+CMusicLibraryExportJob::~CMusicLibraryExportJob() = default;
+
+bool CMusicLibraryExportJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryExportJob* exportJob = dynamic_cast<const CMusicLibraryExportJob*>(job);
+ if (exportJob == NULL)
+ return false;
+
+ return !(m_settings != exportJob->m_settings);
+}
+
+bool CMusicLibraryExportJob::Work(CMusicDatabase &db)
+{
+ db.ExportToXML(m_settings, GetProgressDialog());
+
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryExportJob.h b/xbmc/music/jobs/MusicLibraryExportJob.h
new file mode 100644
index 0000000..4dc759e
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryExportJob.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"
+#include "settings/LibExportSettings.h"
+
+class CGUIDialogProgress;
+
+/*!
+ \brief Music library job implementation for exporting the music library.
+*/
+class CMusicLibraryExportJob : public CMusicLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new music library export job for the given paths.
+
+ \param[in] settings Library export settings
+ \param[in] progressDialog Progress dialog to be used to display the export progress
+ */
+ CMusicLibraryExportJob(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog);
+
+ ~CMusicLibraryExportJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryExportJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+ CLibExportSettings m_settings;
+};
diff --git a/xbmc/music/jobs/MusicLibraryImportJob.cpp b/xbmc/music/jobs/MusicLibraryImportJob.cpp
new file mode 100644
index 0000000..6fcae05
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryImportJob.cpp
@@ -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.
+*/
+
+#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 0000000..bcca44f
--- /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/music/jobs/MusicLibraryJob.cpp b/xbmc/music/jobs/MusicLibraryJob.cpp
new file mode 100644
index 0000000..281d7f9
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryJob.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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 "MusicLibraryJob.h"
+
+#include "music/MusicDatabase.h"
+
+CMusicLibraryJob::CMusicLibraryJob() = default;
+
+CMusicLibraryJob::~CMusicLibraryJob() = default;
+
+bool CMusicLibraryJob::DoWork()
+{
+ CMusicDatabase db;
+ if (!db.Open())
+ return false;
+
+ return Work(db);
+}
diff --git a/xbmc/music/jobs/MusicLibraryJob.h b/xbmc/music/jobs/MusicLibraryJob.h
new file mode 100644
index 0000000..81f68b7
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryJob.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "utils/Job.h"
+
+class CMusicDatabase;
+
+/*!
+ \brief Basic implementation/interface of a CJob which interacts with the
+ music database.
+ */
+class CMusicLibraryJob : public CJob
+{
+public:
+ ~CMusicLibraryJob() override;
+
+ /*!
+ \brief Whether the job can be cancelled or not.
+ */
+ virtual bool CanBeCancelled() const { return false; }
+
+ /*!
+ \brief Tries to cancel the running job.
+ \return True if the job was cancelled, false otherwise
+ */
+ virtual bool Cancel() { return false; }
+
+ // implementation of CJob
+ bool DoWork() override;
+ const char *GetType() const override { return "MusicLibraryJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+
+protected:
+ CMusicLibraryJob();
+
+ /*!
+ \brief Worker method to be implemented by an actual implementation.
+
+ \param[in] db Already open music database to be used for interaction
+ \return True if the process succeeded, false otherwise
+ */
+ virtual bool Work(CMusicDatabase &db) = 0;
+};
diff --git a/xbmc/music/jobs/MusicLibraryProgressJob.cpp b/xbmc/music/jobs/MusicLibraryProgressJob.cpp
new file mode 100644
index 0000000..d07e85b
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryProgressJob.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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 "MusicLibraryProgressJob.h"
+
+CMusicLibraryProgressJob::CMusicLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar)
+ : CProgressJob(progressBar)
+{ }
+
+CMusicLibraryProgressJob::~CMusicLibraryProgressJob() = default;
+
+bool CMusicLibraryProgressJob::DoWork()
+{
+ bool result = CMusicLibraryJob::DoWork();
+
+ MarkFinished();
+
+ return result;
+}
diff --git a/xbmc/music/jobs/MusicLibraryProgressJob.h b/xbmc/music/jobs/MusicLibraryProgressJob.h
new file mode 100644
index 0000000..ae843da
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryProgressJob.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "music/jobs/MusicLibraryJob.h"
+#include "utils/ProgressJob.h"
+
+/*!
+ \brief Combined base implementation of a music library job with a progress bar.
+ */
+class CMusicLibraryProgressJob : public CProgressJob, public CMusicLibraryJob
+{
+public:
+ ~CMusicLibraryProgressJob() override;
+
+ // implementation of CJob
+ bool DoWork() override;
+ const char *GetType() const override { return "CMusicLibraryProgressJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+
+protected:
+ explicit CMusicLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar);
+};
diff --git a/xbmc/music/jobs/MusicLibraryScanningJob.cpp b/xbmc/music/jobs/MusicLibraryScanningJob.cpp
new file mode 100644
index 0000000..d2626a1
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryScanningJob.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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 "MusicLibraryScanningJob.h"
+
+#include "music/MusicDatabase.h"
+
+CMusicLibraryScanningJob::CMusicLibraryScanningJob(const std::string& directory, int flags, bool showProgress /* = true */)
+ : m_scanner(),
+ m_directory(directory),
+ m_showProgress(showProgress),
+ m_flags(flags)
+{ }
+
+CMusicLibraryScanningJob::~CMusicLibraryScanningJob() = default;
+
+bool CMusicLibraryScanningJob::Cancel()
+{
+ if (!m_scanner.IsScanning())
+ return true;
+
+ m_scanner.Stop();
+ return true;
+}
+
+bool CMusicLibraryScanningJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryScanningJob* scanningJob = dynamic_cast<const CMusicLibraryScanningJob*>(job);
+ if (scanningJob == nullptr)
+ return false;
+
+ return m_directory == scanningJob->m_directory &&
+ m_flags == scanningJob->m_flags;
+}
+
+bool CMusicLibraryScanningJob::Work(CMusicDatabase &db)
+{
+ m_scanner.ShowDialog(m_showProgress);
+ if (m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_ALBUMS)
+ // Scrape additional album information
+ m_scanner.FetchAlbumInfo(m_directory, m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN);
+ else if (m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_ARTISTS)
+ // Scrape additional artist information
+ m_scanner.FetchArtistInfo(m_directory, m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN);
+ else
+ // Scan tags from music files, and optionally scrape artist and album info
+ m_scanner.Start(m_directory, m_flags);
+
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryScanningJob.h b/xbmc/music/jobs/MusicLibraryScanningJob.h
new file mode 100644
index 0000000..6c5ea13
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryScanningJob.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "music/infoscanner/MusicInfoScanner.h"
+#include "music/jobs/MusicLibraryJob.h"
+
+#include <string>
+
+/*!
+ \brief Music library job implementation for scanning items.
+ Uses CMusicInfoScanner for scanning and can be run with or
+ without a visible progress bar.
+ */
+class CMusicLibraryScanningJob : public CMusicLibraryJob
+{
+public:
+ /*!
+ \brief Creates a new music library tag scanning and data scraping job.
+ \param[in] directory Directory to be scanned for new items
+ \param[in] flags What kind of scan to do
+ \param[in] showProgress Whether to show a progress bar or not
+ */
+ CMusicLibraryScanningJob(const std::string& directory, int flags, bool showProgress = true);
+ ~CMusicLibraryScanningJob() override;
+
+ // specialization of CMusicLibraryJob
+ bool CanBeCancelled() const override { return true; }
+ bool Cancel() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryScanningJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+ MUSIC_INFO::CMusicInfoScanner m_scanner;
+ std::string m_directory;
+ bool m_showProgress;
+ int m_flags;
+};
diff --git a/xbmc/music/tags/CMakeLists.txt b/xbmc/music/tags/CMakeLists.txt
new file mode 100644
index 0000000..bbd3462
--- /dev/null
+++ b/xbmc/music/tags/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SOURCES MusicInfoTag.cpp
+ MusicInfoTagLoaderCDDA.cpp
+ MusicInfoTagLoaderDatabase.cpp
+ MusicInfoTagLoaderFactory.cpp
+ MusicInfoTagLoaderFFmpeg.cpp
+ MusicInfoTagLoaderShn.cpp
+ ReplayGain.cpp
+ TagLibVFSStream.cpp
+ TagLoaderTagLib.cpp)
+
+set(HEADERS ImusicInfoTagLoader.h
+ MusicInfoTag.h
+ MusicInfoTagLoaderCDDA.h
+ MusicInfoTagLoaderDatabase.h
+ MusicInfoTagLoaderFactory.h
+ MusicInfoTagLoaderFFmpeg.h
+ MusicInfoTagLoaderShn.h
+ ReplayGain.h
+ TagLibVFSStream.h
+ TagLoaderTagLib.h)
+
+core_add_library(music_tags)
diff --git a/xbmc/music/tags/ImusicInfoTagLoader.h b/xbmc/music/tags/ImusicInfoTagLoader.h
new file mode 100644
index 0000000..c3e07fe
--- /dev/null
+++ b/xbmc/music/tags/ImusicInfoTagLoader.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2005-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 <string>
+
+class EmbeddedArt;
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+ class IMusicInfoTagLoader
+ {
+ public:
+ IMusicInfoTagLoader(void) = default;
+ virtual ~IMusicInfoTagLoader() = default;
+
+ virtual bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) = 0;
+ };
+}
diff --git a/xbmc/music/tags/MusicInfoTag.cpp b/xbmc/music/tags/MusicInfoTag.cpp
new file mode 100644
index 0000000..62548bf
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTag.cpp
@@ -0,0 +1,1303 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/Artist.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+
+using namespace MUSIC_INFO;
+
+CMusicInfoTag::CMusicInfoTag(void)
+{
+ Clear();
+}
+
+bool CMusicInfoTag::operator !=(const CMusicInfoTag& tag) const
+{
+ if (this == &tag) return false;
+ if (m_strURL != tag.m_strURL) return true;
+ if (m_strTitle != tag.m_strTitle) return true;
+ if (m_bCompilation != tag.m_bCompilation) return true;
+ if (m_artist != tag.m_artist) return true;
+ if (m_albumArtist != tag.m_albumArtist) return true;
+ if (m_strAlbum != tag.m_strAlbum) return true;
+ if (m_iDuration != tag.m_iDuration) return true;
+ if (m_strDiscSubtitle != tag.m_strDiscSubtitle)
+ return true;
+ if (m_iTrack != tag.m_iTrack)
+ return true;
+ if (m_albumReleaseType != tag.m_albumReleaseType) return true;
+ return false;
+}
+
+int CMusicInfoTag::GetTrackNumber() const
+{
+ return (m_iTrack & 0xffff);
+}
+
+int CMusicInfoTag::GetDiscNumber() const
+{
+ return (m_iTrack >> 16);
+}
+
+int CMusicInfoTag::GetTrackAndDiscNumber() const
+{
+ return m_iTrack;
+}
+
+int CMusicInfoTag::GetDuration() const
+{
+ return m_iDuration;
+}
+
+const std::string& CMusicInfoTag::GetTitle() const
+{
+ return m_strTitle;
+}
+
+const std::string& CMusicInfoTag::GetURL() const
+{
+ return m_strURL;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetArtist() const
+{
+ return m_artist;
+}
+
+const std::string CMusicInfoTag::GetArtistString() const
+{
+ if (!m_strArtistDesc.empty())
+ return m_strArtistDesc;
+ else if (!m_artist.empty())
+ return StringUtils::Join(m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ else
+ return StringUtils::Empty;
+}
+
+const std::string& CMusicInfoTag::GetArtistSort() const
+{
+ return m_strArtistSort;
+}
+
+const std::string& CMusicInfoTag::GetComposerSort() const
+{
+ return m_strComposerSort;
+}
+
+const std::string& CMusicInfoTag::GetAlbum() const
+{
+ return m_strAlbum;
+}
+
+const std::string& CMusicInfoTag::GetDiscSubtitle() const
+{
+ return m_strDiscSubtitle;
+}
+
+const std::string& CMusicInfoTag::GetOriginalDate() const
+{
+ return m_strOriginalDate;
+}
+
+const std::string MUSIC_INFO::CMusicInfoTag::GetOriginalYear() const
+{
+ return StringUtils::Left(m_strOriginalDate, 4);
+}
+
+int CMusicInfoTag::GetAlbumId() const
+{
+ return m_iAlbumId;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetAlbumArtist() const
+{
+ return m_albumArtist;
+}
+
+const std::string CMusicInfoTag::GetAlbumArtistString() const
+{
+ if (!m_strAlbumArtistDesc.empty())
+ return m_strAlbumArtistDesc;
+ if (!m_albumArtist.empty())
+ return StringUtils::Join(m_albumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ else
+ return StringUtils::Empty;
+}
+
+const std::string& CMusicInfoTag::GetAlbumArtistSort() const
+{
+ return m_strAlbumArtistSort;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetGenre() const
+{
+ return m_genre;
+}
+
+int CMusicInfoTag::GetDatabaseId() const
+{
+ return m_iDbId;
+}
+
+const std::string &CMusicInfoTag::GetType() const
+{
+ return m_type;
+}
+
+int CMusicInfoTag::GetYear() const
+{
+ return atoi(GetYearString().c_str());
+}
+
+std::string CMusicInfoTag::GetYearString() const
+{
+ /* Get year as YYYY from release or original dates depending on setting
+ This is how GUI and by year sorting swiches to using original year.
+ For ripper and non-library items (library entries have both values):
+ when release date missing try to fallback to original date
+ when original date missing use release date
+ */
+ std::string value;
+ value = GetReleaseYear();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE) ||
+ value.empty())
+ {
+ std::string origvalue = GetOriginalYear();
+ if (!origvalue.empty())
+ return origvalue;
+ }
+ return value;
+}
+
+const std::string &CMusicInfoTag::GetComment() const
+{
+ return m_strComment;
+}
+
+const std::string &CMusicInfoTag::GetMood() const
+{
+ return m_strMood;
+}
+
+const std::string &CMusicInfoTag::GetRecordLabel() const
+{
+ return m_strRecordLabel;
+}
+
+const std::string &CMusicInfoTag::GetLyrics() const
+{
+ return m_strLyrics;
+}
+
+const std::string &CMusicInfoTag::GetCueSheet() const
+{
+ return m_cuesheet;
+}
+
+float CMusicInfoTag::GetRating() const
+{
+ return m_Rating;
+}
+
+int CMusicInfoTag::GetUserrating() const
+{
+ return m_Userrating;
+}
+
+int CMusicInfoTag::GetVotes() const
+{
+ return m_Votes;
+}
+
+int CMusicInfoTag::GetListeners() const
+{
+ return m_listeners;
+}
+
+int CMusicInfoTag::GetPlayCount() const
+{
+ return m_iTimesPlayed;
+}
+
+const CDateTime &CMusicInfoTag::GetLastPlayed() const
+{
+ return m_lastPlayed;
+}
+
+const CDateTime &CMusicInfoTag::GetDateAdded() const
+{
+ return m_dateAdded;
+}
+
+bool CMusicInfoTag::GetCompilation() const
+{
+ return m_bCompilation;
+}
+
+bool CMusicInfoTag::GetBoxset() const
+{
+ return m_bBoxset;
+}
+
+int CMusicInfoTag::GetTotalDiscs() const
+{
+ return m_iDiscTotal;
+}
+
+const EmbeddedArtInfo &CMusicInfoTag::GetCoverArtInfo() const
+{
+ return m_coverArt;
+}
+
+const ReplayGain& CMusicInfoTag::GetReplayGain() const
+{
+ return m_replayGain;
+}
+
+CAlbum::ReleaseType CMusicInfoTag::GetAlbumReleaseType() const
+{
+ return m_albumReleaseType;
+}
+
+int CMusicInfoTag::GetBPM() const
+{
+ return m_iBPM;
+}
+
+int CMusicInfoTag::GetBitRate() const
+{
+ return m_bitrate;
+}
+
+int CMusicInfoTag::GetSampleRate() const
+{
+ return m_samplerate;
+}
+
+int CMusicInfoTag::GetNoOfChannels() const
+{
+ return m_channels;
+}
+
+const std::string& CMusicInfoTag::GetReleaseDate() const
+{
+ return m_strReleaseDate;
+}
+
+const std::string MUSIC_INFO::CMusicInfoTag::GetReleaseYear() const
+{
+ return StringUtils::Left(m_strReleaseDate, 4);
+}
+
+// This is the Musicbrainz release status tag. See https://musicbrainz.org/doc/Release#Status
+
+const std::string& CMusicInfoTag::GetAlbumReleaseStatus() const
+{
+ return m_strReleaseStatus;
+}
+
+const std::string& CMusicInfoTag::GetStationName() const
+{
+ return m_stationName;
+}
+
+const std::string& CMusicInfoTag::GetStationArt() const
+{
+ return m_stationArt;
+}
+
+void CMusicInfoTag::SetURL(const std::string& strURL)
+{
+ m_strURL = strURL;
+}
+
+void CMusicInfoTag::SetTitle(const std::string& strTitle)
+{
+ m_strTitle = Trim(strTitle);
+}
+
+void CMusicInfoTag::SetArtist(const std::string& strArtist)
+{
+ if (!strArtist.empty())
+ {
+ SetArtistDesc(strArtist);
+ SetArtist(StringUtils::Split(strArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ }
+ else
+ {
+ m_strArtistDesc.clear();
+ m_artist.clear();
+ }
+}
+
+void CMusicInfoTag::SetArtist(const std::vector<std::string>& artists, bool FillDesc /* = false*/)
+{
+ m_artist = artists;
+ if (m_strArtistDesc.empty() || FillDesc)
+ {
+ SetArtistDesc(StringUtils::Join(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ }
+}
+
+void CMusicInfoTag::SetArtistDesc(const std::string& strArtistDesc)
+{
+ m_strArtistDesc = strArtistDesc;
+}
+
+void CMusicInfoTag::SetArtistSort(const std::string& strArtistsort)
+{
+ m_strArtistSort = strArtistsort;
+}
+
+void CMusicInfoTag::SetComposerSort(const std::string& strComposerSort)
+{
+ m_strComposerSort = strComposerSort;
+}
+
+void CMusicInfoTag::SetAlbum(const std::string& strAlbum)
+{
+ m_strAlbum = Trim(strAlbum);
+}
+
+void CMusicInfoTag::SetAlbumId(const int iAlbumId)
+{
+ m_iAlbumId = iAlbumId;
+}
+
+void CMusicInfoTag::SetAlbumArtist(const std::string& strAlbumArtist)
+{
+ if (!strAlbumArtist.empty())
+ {
+ SetAlbumArtistDesc(strAlbumArtist);
+ SetAlbumArtist(StringUtils::Split(strAlbumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ }
+ else
+ {
+ m_strAlbumArtistDesc.clear();
+ m_albumArtist.clear();
+ }
+}
+
+void CMusicInfoTag::SetAlbumArtist(const std::vector<std::string>& albumArtists, bool FillDesc /* = false*/)
+{
+ m_albumArtist = albumArtists;
+ if (m_strAlbumArtistDesc.empty() || FillDesc)
+ SetAlbumArtistDesc(StringUtils::Join(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CMusicInfoTag::SetAlbumArtistDesc(const std::string& strAlbumArtistDesc)
+{
+ m_strAlbumArtistDesc = strAlbumArtistDesc;
+}
+
+void CMusicInfoTag::SetAlbumArtistSort(const std::string& strAlbumArtistSort)
+{
+ m_strAlbumArtistSort = strAlbumArtistSort;
+}
+
+void CMusicInfoTag::SetGenre(const std::string& strGenre, bool bTrim /* = false*/)
+{
+ if (!strGenre.empty())
+ SetGenre(StringUtils::Split(strGenre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator), bTrim);
+ else
+ m_genre.clear();
+}
+
+void CMusicInfoTag::SetGenre(const std::vector<std::string>& genres, bool bTrim /* = false*/)
+{
+ m_genre = genres;
+ if (bTrim)
+ for (auto genre : m_genre)
+ StringUtils::Trim(genre);
+}
+
+void CMusicInfoTag::SetYear(int year)
+{
+ // Parse integer year value into YYYY ISO8601 format (partial) date string
+ // Add century for to 2 digit numbers, 41 -> 1941, 40 -> 2040
+ if (year > 99)
+ SetReleaseDate(StringUtils::Format("{:04}", year));
+ else if (year > 40)
+ SetReleaseDate(StringUtils::Format("{:04}", 19 + year));
+ else if (year > 0)
+ SetReleaseDate(StringUtils::Format("{:04}", 20 + year));
+ else
+ m_strReleaseDate.clear();
+}
+
+void CMusicInfoTag::SetDatabaseId(int id, const std::string &type)
+{
+ m_iDbId = id;
+ m_type = type;
+}
+
+void CMusicInfoTag::SetTrackNumber(int iTrack)
+{
+ m_iTrack = (m_iTrack & 0xffff0000) | (iTrack & 0xffff);
+}
+
+void CMusicInfoTag::SetDiscNumber(int iDiscNumber)
+{
+ m_iTrack = (m_iTrack & 0xffff) | (iDiscNumber << 16);
+}
+
+void CMusicInfoTag::SetDiscSubtitle(const std::string& strDiscSubtitle)
+{
+ m_strDiscSubtitle = strDiscSubtitle;
+}
+
+void CMusicInfoTag::SetTotalDiscs(int iDiscTotal)
+{
+ m_iDiscTotal = iDiscTotal;
+}
+
+void CMusicInfoTag::SetReleaseDate(const std::string& strReleaseDate)
+{
+ // Date in ISO8601 YYYY, YYYY-MM or YYYY-MM-DD
+ m_strReleaseDate = strReleaseDate;
+}
+
+void CMusicInfoTag::SetOriginalDate(const std::string& strOriginalDate)
+{
+ // Date in ISO8601 YYYY, YYYY-MM or YYYY-MM-DD
+ m_strOriginalDate = strOriginalDate;
+}
+
+void CMusicInfoTag::AddOriginalDate(const std::string& strDateYear)
+{
+ // Avoid overwriting YYYY-MM or YYYY-MM-DD (from DATE tag) with just YYYY (from YEAR tag)
+ if (strDateYear.size() > m_strOriginalDate.size())
+ m_strOriginalDate = strDateYear;
+}
+
+void CMusicInfoTag::AddReleaseDate(const std::string& strDateYear, bool isMonth /*= false*/)
+{
+ // Given MMDD (from ID3 v2.3 TDAT tag) set MM-DD part of ISO8601 string
+ if (isMonth && !strDateYear.empty())
+ {
+ std::string strYYYY = GetReleaseYear();
+ if (strYYYY.empty())
+ strYYYY = "0000"; // Fake year when TYER not read yet
+ m_strReleaseDate = StringUtils::Format("{}-{}-{}", strYYYY, StringUtils::Left(strDateYear, 2),
+ StringUtils::Right(strDateYear, 2));
+ }
+ // Given YYYY only (from YEAR tag) and already have YYYY-MM or YYYY-MM-DD (from DATE tag)
+ else if (strDateYear.size() == 4 && (m_strReleaseDate.size() > 4))
+ {
+ // Have 0000-MM-DD where ID3 v2.3 TDAT tag read first, fill YYYY part from TYER
+ if (GetReleaseYear() == "0000")
+ StringUtils::Replace(m_strReleaseDate, "0000", strDateYear);
+ }
+ else
+ m_strReleaseDate = strDateYear; // Could be YYYY, YYYY-MM or YYYY-MM-DD
+}
+
+void CMusicInfoTag::SetTrackAndDiscNumber(int iTrackAndDisc)
+{
+ m_iTrack = iTrackAndDisc;
+}
+
+void CMusicInfoTag::SetDuration(int iSec)
+{
+ m_iDuration = iSec;
+}
+
+void CMusicInfoTag::SetBitRate(int bitrate)
+{
+ m_bitrate = bitrate;
+}
+
+void CMusicInfoTag::SetNoOfChannels(int channels)
+{
+ m_channels = channels;
+}
+
+void CMusicInfoTag::SetSampleRate(int samplerate)
+{
+ m_samplerate = samplerate;
+}
+
+void CMusicInfoTag::SetComment(const std::string& comment)
+{
+ m_strComment = comment;
+}
+
+void CMusicInfoTag::SetMood(const std::string& mood)
+{
+ m_strMood = mood;
+}
+
+void CMusicInfoTag::SetRecordLabel(const std::string& publisher)
+{
+ m_strRecordLabel = publisher;
+}
+
+void CMusicInfoTag::SetCueSheet(const std::string& cueSheet)
+{
+ m_cuesheet = cueSheet;
+}
+
+void CMusicInfoTag::SetLyrics(const std::string& lyrics)
+{
+ m_strLyrics = lyrics;
+}
+
+void CMusicInfoTag::SetRating(float rating)
+{
+ //This value needs to be between 0-10 - 0 will unset the rating
+ rating = std::max(rating, 0.f);
+ rating = std::min(rating, 10.f);
+
+ m_Rating = rating;
+}
+
+void CMusicInfoTag::SetVotes(int votes)
+{
+ m_Votes = votes;
+}
+
+void CMusicInfoTag::SetUserrating(int rating)
+{
+ //This value needs to be between 0-10 - 0 will unset the userrating
+ rating = std::max(rating, 0);
+ rating = std::min(rating, 10);
+
+ m_Userrating = rating;
+}
+
+void CMusicInfoTag::SetListeners(int listeners)
+{
+ m_listeners = std::max(listeners, 0);
+}
+
+void CMusicInfoTag::SetPlayCount(int playcount)
+{
+ m_iTimesPlayed = playcount;
+}
+
+void CMusicInfoTag::SetLastPlayed(const std::string& lastplayed)
+{
+ m_lastPlayed.SetFromDBDateTime(lastplayed);
+}
+
+void CMusicInfoTag::SetLastPlayed(const CDateTime& lastplayed)
+{
+ m_lastPlayed = lastplayed;
+}
+
+void CMusicInfoTag::SetDateAdded(const std::string& strDateAdded)
+{
+ m_dateAdded.SetFromDBDateTime(strDateAdded);
+}
+
+void CMusicInfoTag::SetDateAdded(const CDateTime& dateAdded)
+{
+ m_dateAdded = dateAdded;
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateUpdated(const std::string& strDateUpdated)
+{
+ m_dateUpdated.SetFromDBDateTime(strDateUpdated);
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateUpdated(const CDateTime& dateUpdated)
+{
+ m_dateUpdated = dateUpdated;
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateNew(const std::string& strDateNew)
+{
+ m_dateNew.SetFromDBDateTime(strDateNew);
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateNew(const CDateTime& dateNew)
+{
+ m_dateNew = dateNew;
+}
+
+void CMusicInfoTag::SetCompilation(bool compilation)
+{
+ m_bCompilation = compilation;
+}
+
+void CMusicInfoTag::SetBoxset(bool boxset)
+{
+ m_bBoxset = boxset;
+}
+
+void CMusicInfoTag::SetLoaded(bool bOnOff)
+{
+ m_bLoaded = bOnOff;
+}
+
+bool CMusicInfoTag::Loaded() const
+{
+ return m_bLoaded;
+}
+
+void CMusicInfoTag::SetBPM(int bpm)
+{
+ m_iBPM = bpm;
+}
+
+void CMusicInfoTag::SetStationName(const std::string& strStationName)
+{
+ m_stationName = strStationName;
+}
+
+const std::string& CMusicInfoTag::GetMusicBrainzTrackID() const
+{
+ return m_strMusicBrainzTrackID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzArtistID() const
+{
+ return m_musicBrainzArtistID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzArtistHints() const
+{
+ return m_musicBrainzArtistHints;
+}
+
+const std::string& CMusicInfoTag::GetMusicBrainzAlbumID() const
+{
+ return m_strMusicBrainzAlbumID;
+}
+
+const std::string & MUSIC_INFO::CMusicInfoTag::GetMusicBrainzReleaseGroupID() const
+{
+ return m_strMusicBrainzReleaseGroupID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzAlbumArtistID() const
+{
+ return m_musicBrainzAlbumArtistID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzAlbumArtistHints() const
+{
+ return m_musicBrainzAlbumArtistHints;
+}
+
+const std::string &CMusicInfoTag::GetMusicBrainzReleaseType() const
+{
+ return m_strMusicBrainzReleaseType;
+}
+
+void CMusicInfoTag::SetMusicBrainzTrackID(const std::string& strTrackID)
+{
+ m_strMusicBrainzTrackID = strTrackID;
+}
+
+void CMusicInfoTag::SetMusicBrainzArtistID(const std::vector<std::string>& musicBrainzArtistId)
+{
+ m_musicBrainzArtistID = musicBrainzArtistId;
+}
+
+void CMusicInfoTag::SetMusicBrainzArtistHints(const std::vector<std::string>& musicBrainzArtistHints)
+{
+ m_musicBrainzArtistHints = musicBrainzArtistHints;
+}
+
+void CMusicInfoTag::SetMusicBrainzAlbumID(const std::string& strAlbumID)
+{
+ m_strMusicBrainzAlbumID = strAlbumID;
+}
+
+void CMusicInfoTag::SetMusicBrainzAlbumArtistID(const std::vector<std::string>& musicBrainzAlbumArtistId)
+{
+ m_musicBrainzAlbumArtistID = musicBrainzAlbumArtistId;
+}
+
+void CMusicInfoTag::SetMusicBrainzAlbumArtistHints(const std::vector<std::string>& musicBrainzAlbumArtistHints)
+{
+ m_musicBrainzAlbumArtistHints = musicBrainzAlbumArtistHints;
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetMusicBrainzReleaseGroupID(const std::string & strReleaseGroupID)
+{
+ m_strMusicBrainzReleaseGroupID = strReleaseGroupID;
+}
+
+void CMusicInfoTag::SetMusicBrainzReleaseType(const std::string& ReleaseType)
+{
+ m_strMusicBrainzReleaseType = ReleaseType;
+}
+
+void CMusicInfoTag::SetCoverArtInfo(size_t size, const std::string &mimeType)
+{
+ m_coverArt.Set(size, mimeType);
+}
+
+void CMusicInfoTag::SetReplayGain(const ReplayGain& aGain)
+{
+ m_replayGain = aGain;
+}
+
+void CMusicInfoTag::SetAlbumReleaseType(CAlbum::ReleaseType releaseType)
+{
+ m_albumReleaseType = releaseType;
+}
+
+void CMusicInfoTag::SetType(const MediaType& mediaType)
+{
+ m_type = mediaType;
+}
+
+// This is the Musicbrainz release status tag. See https://musicbrainz.org/doc/Release#Status
+
+void CMusicInfoTag::SetAlbumReleaseStatus(const std::string& ReleaseStatus)
+{
+ m_strReleaseStatus = ReleaseStatus;
+}
+
+void CMusicInfoTag::SetStationArt(const std::string& strStationArt)
+{
+ m_stationArt = strStationArt;
+}
+
+void CMusicInfoTag::SetArtist(const CArtist& artist)
+{
+ SetArtist(artist.strArtist);
+ SetArtistSort(artist.strSortName);
+ SetAlbumArtist(artist.strArtist);
+ SetAlbumArtistSort(artist.strSortName);
+ SetMusicBrainzArtistID({ artist.strMusicBrainzArtistID });
+ SetMusicBrainzAlbumArtistID({ artist.strMusicBrainzArtistID });
+ SetGenre(artist.genre);
+ SetMood(StringUtils::Join(artist.moods, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ SetDateAdded(artist.dateAdded);
+ SetDateUpdated(artist.dateUpdated);
+ SetDateNew(artist.dateNew);
+ SetDatabaseId(artist.idArtist, MediaTypeArtist);
+
+ SetLoaded();
+}
+
+void CMusicInfoTag::SetAlbum(const CAlbum& album)
+{
+ Clear();
+ //Set all artist information from album artist credits and artist description
+ SetArtistDesc(album.GetAlbumArtistString());
+ SetArtist(album.GetAlbumArtist());
+ SetArtistSort(album.GetAlbumArtistSort());
+ SetMusicBrainzArtistID(album.GetMusicBrainzAlbumArtistID());
+ SetAlbumArtistDesc(album.GetAlbumArtistString());
+ SetAlbumArtist(album.GetAlbumArtist());
+ SetAlbumArtistSort(album.GetAlbumArtistSort());
+ SetMusicBrainzAlbumArtistID(album.GetMusicBrainzAlbumArtistID());
+ SetAlbumId(album.idAlbum);
+ SetAlbum(album.strAlbum);
+ SetTitle(album.strAlbum);
+ SetMusicBrainzAlbumID(album.strMusicBrainzAlbumID);
+ SetMusicBrainzReleaseGroupID(album.strReleaseGroupMBID);
+ SetMusicBrainzReleaseType(album.strType);
+ SetAlbumReleaseStatus(album.strReleaseStatus);
+ SetGenre(album.genre);
+ SetMood(StringUtils::Join(album.moods, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ SetRecordLabel(album.strLabel);
+ SetRating(album.fRating);
+ SetUserrating(album.iUserrating);
+ SetVotes(album.iVotes);
+ SetCompilation(album.bCompilation);
+ SetOriginalDate(album.strOrigReleaseDate);
+ SetReleaseDate(album.strReleaseDate);
+ SetBoxset(album.bBoxedSet);
+ SetAlbumReleaseType(album.releaseType);
+ SetDateAdded(album.dateAdded);
+ SetDateUpdated(album.dateUpdated);
+ SetDateNew(album.dateNew);
+ SetPlayCount(album.iTimesPlayed);
+ SetDatabaseId(album.idAlbum, MediaTypeAlbum);
+ SetLastPlayed(album.lastPlayed);
+ SetTotalDiscs(album.iTotalDiscs);
+ SetDuration(album.iAlbumDuration);
+
+ SetLoaded();
+}
+
+void CMusicInfoTag::SetSong(const CSong& song)
+{
+ Clear();
+ SetTitle(song.strTitle);
+ SetGenre(song.genre);
+ /* Set all artist information from song artist credits and artist description.
+ During processing e.g. Cue Sheets, song may only have artist description string
+ rather than a fully populated artist credits vector.
+ */
+ if (!song.HasArtistCredits())
+ SetArtist(song.GetArtistString()); //Sets both artist description string and artist vector from string
+ else
+ {
+ SetArtistDesc(song.GetArtistString());
+ SetArtist(song.GetArtist());
+ SetMusicBrainzArtistID(song.GetMusicBrainzArtistID());
+ }
+ SetArtistSort(song.GetArtistSort());
+ SetAlbum(song.strAlbum);
+ SetAlbumArtist(song.GetAlbumArtist()); //Only have album artist in song as vector, no desc or MBID
+ SetAlbumArtistSort(song.GetAlbumArtistSort());
+ SetMusicBrainzTrackID(song.strMusicBrainzTrackID);
+ SetContributors(song.GetContributors());
+ SetComment(song.strComment);
+ SetCueSheet(song.strCueSheet);
+ SetPlayCount(song.iTimesPlayed);
+ SetLastPlayed(song.lastPlayed);
+ SetDateAdded(song.dateAdded);
+ SetDateUpdated(song.dateUpdated);
+ SetDateNew(song.dateNew);
+ SetCoverArtInfo(song.embeddedArt.m_size, song.embeddedArt.m_mime);
+ SetRating(song.rating);
+ SetUserrating(song.userrating);
+ SetVotes(song.votes);
+ SetURL(song.strFileName);
+ SetReleaseDate(song.strReleaseDate);
+ SetOriginalDate(song.strOrigReleaseDate);
+ SetTrackAndDiscNumber(song.iTrack);
+ SetDiscSubtitle(song.strDiscSubtitle);
+ SetDuration(song.iDuration);
+ SetMood(song.strMood);
+ SetCompilation(song.bCompilation);
+ SetAlbumId(song.idAlbum);
+ SetDatabaseId(song.idSong, MediaTypeSong);
+ SetBPM(song.iBPM);
+ SetBitRate(song.iBitRate);
+ SetSampleRate(song.iSampleRate);
+ SetNoOfChannels(song.iChannels);
+
+ if (song.replayGain.Get(ReplayGain::TRACK).Valid())
+ m_replayGain.Set(ReplayGain::TRACK, song.replayGain.Get(ReplayGain::TRACK));
+ if (song.replayGain.Get(ReplayGain::ALBUM).Valid())
+ m_replayGain.Set(ReplayGain::ALBUM, song.replayGain.Get(ReplayGain::ALBUM));
+
+ SetLoaded();
+}
+
+void CMusicInfoTag::Serialize(CVariant& value) const
+{
+ value["url"] = m_strURL;
+ value["title"] = m_strTitle;
+ if (m_type.compare(MediaTypeArtist) == 0 && m_artist.size() == 1)
+ value["artist"] = m_artist[0];
+ else
+ value["artist"] = m_artist;
+ // There are situations where the individual artist(s) are not queried from the song_artist and artist tables e.g. playlist,
+ // only artist description from song table. Since processing of the ARTISTS tag was added the individual artists may not always
+ // be accurately derived by simply splitting the artist desc. Hence m_artist is only populated when the individual artists are
+ // queried, whereas GetArtistString() will always return the artist description.
+ // To avoid empty artist array in JSON, when m_artist is empty then an attempt is made to split the artist desc into artists.
+ // A longer term solution would be to ensure that when individual artists are to be returned then the song_artist and artist tables
+ // are queried.
+ if (m_artist.empty())
+ value["artist"] = StringUtils::Split(GetArtistString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+
+ value["displayartist"] = GetArtistString();
+ value["displayalbumartist"] = GetAlbumArtistString();
+ value["sortartist"] = GetArtistSort();
+ value["album"] = m_strAlbum;
+ value["albumartist"] = m_albumArtist;
+ value["sortalbumartist"] = m_strAlbumArtistSort;
+ value["genre"] = m_genre;
+ value["duration"] = m_iDuration;
+ value["track"] = GetTrackNumber();
+ value["disc"] = GetDiscNumber();
+ value["loaded"] = m_bLoaded;
+ value["year"] = GetYear(); // Optionally from m_strOriginalDate
+ value["musicbrainztrackid"] = m_strMusicBrainzTrackID;
+ value["musicbrainzartistid"] = m_musicBrainzArtistID;
+ value["musicbrainzalbumid"] = m_strMusicBrainzAlbumID;
+ value["musicbrainzreleasegroupid"] = m_strMusicBrainzReleaseGroupID;
+ value["musicbrainzalbumartistid"] = m_musicBrainzAlbumArtistID;
+ value["comment"] = m_strComment;
+ value["contributors"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& role : m_musicRoles)
+ {
+ CVariant contributor;
+ contributor["name"] = role.GetArtist();
+ contributor["role"] = role.GetRoleDesc();
+ contributor["roleid"] = role.GetRoleId();
+ contributor["artistid"] = (int)(role.GetArtistId());
+ value["contributors"].push_back(contributor);
+ }
+ value["displaycomposer"] = GetArtistStringForRole("composer"); //TCOM
+ value["displayconductor"] = GetArtistStringForRole("conductor"); //TPE3
+ value["displayorchestra"] = GetArtistStringForRole("orchestra");
+ value["displaylyricist"] = GetArtistStringForRole("lyricist"); //TEXT
+ value["mood"] = StringUtils::Split(m_strMood, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ value["recordlabel"] = m_strRecordLabel;
+ value["rating"] = m_Rating;
+ value["userrating"] = m_Userrating;
+ value["votes"] = m_Votes;
+ value["playcount"] = m_iTimesPlayed;
+ value["lastplayed"] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty;
+ value["dateadded"] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty;
+ value["datenew"] = m_dateNew.IsValid() ? m_dateNew.GetAsDBDateTime() : StringUtils::Empty;
+ value["datemodified"] =
+ m_dateUpdated.IsValid() ? m_dateUpdated.GetAsDBDateTime() : StringUtils::Empty;
+ value["lyrics"] = m_strLyrics;
+ value["albumid"] = m_iAlbumId;
+ value["compilationartist"] = m_bCompilation;
+ value["compilation"] = m_bCompilation;
+ if (m_type.compare(MediaTypeAlbum) == 0)
+ value["releasetype"] = CAlbum::ReleaseTypeToString(m_albumReleaseType);
+ else if (m_type.compare(MediaTypeSong) == 0)
+ value["albumreleasetype"] = CAlbum::ReleaseTypeToString(m_albumReleaseType);
+ value["isboxset"] = m_bBoxset;
+ value["totaldiscs"] = m_iDiscTotal;
+ value["disctitle"] = m_strDiscSubtitle;
+ value["releasedate"] = m_strReleaseDate;
+ value["originaldate"] = m_strOriginalDate;
+ value["albumstatus"] = m_strReleaseStatus;
+ value["bpm"] = m_iBPM;
+ value["bitrate"] = m_bitrate;
+ value["samplerate"] = m_samplerate;
+ value["channels"] = m_channels;
+}
+
+void CMusicInfoTag::ToSortable(SortItem& sortable, Field field) const
+{
+ switch (field)
+ {
+ case FieldTitle:
+ {
+ // make sure not to overwrite an existing path with an empty one
+ std::string title = m_strTitle;
+ if (!title.empty() || sortable.find(FieldTitle) == sortable.end())
+ sortable[FieldTitle] = title;
+ break;
+ }
+ case FieldArtist: sortable[FieldArtist] = m_strArtistDesc; break;
+ case FieldArtistSort: sortable[FieldArtistSort] = m_strArtistSort; break;
+ case FieldAlbum: sortable[FieldAlbum] = m_strAlbum; break;
+ case FieldAlbumArtist: sortable[FieldAlbumArtist] = m_strAlbumArtistDesc; break;
+ case FieldGenre: sortable[FieldGenre] = m_genre; break;
+ case FieldTime: sortable[FieldTime] = m_iDuration; break;
+ case FieldTrackNumber: sortable[FieldTrackNumber] = m_iTrack; break;
+ case FieldTotalDiscs:
+ sortable[FieldTotalDiscs] = m_iDiscTotal;
+ break;
+ case FieldYear:
+ sortable[FieldYear] = GetYear(); // Optionally from m_strOriginalDate
+ break;
+ case FieldComment: sortable[FieldComment] = m_strComment; break;
+ case FieldMoods: sortable[FieldMoods] = m_strMood; break;
+ case FieldRating: sortable[FieldRating] = m_Rating; break;
+ case FieldUserRating: sortable[FieldUserRating] = m_Userrating; break;
+ case FieldVotes: sortable[FieldVotes] = m_Votes; break;
+ case FieldPlaycount: sortable[FieldPlaycount] = m_iTimesPlayed; break;
+ case FieldLastPlayed: sortable[FieldLastPlayed] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; break;
+ case FieldDateAdded: sortable[FieldDateAdded] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; break;
+ case FieldListeners: sortable[FieldListeners] = m_listeners; break;
+ case FieldId: sortable[FieldId] = (int64_t)m_iDbId; break;
+ case FieldOrigDate: sortable[FieldOrigDate] = m_strOriginalDate; break;
+ case FieldBPM: sortable[FieldBPM] = m_iBPM; break;
+ default: break;
+ }
+}
+
+void CMusicInfoTag::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_strURL;
+ ar << m_strTitle;
+ ar << m_artist;
+ ar << m_strArtistSort;
+ ar << m_strArtistDesc;
+ ar << m_strAlbum;
+ ar << m_albumArtist;
+ ar << m_strAlbumArtistDesc;
+ ar << m_genre;
+ ar << m_iDuration;
+ ar << m_iTrack;
+ ar << m_bLoaded;
+ ar << m_strReleaseDate;
+ ar << m_strOriginalDate;
+ ar << m_strMusicBrainzTrackID;
+ ar << m_musicBrainzArtistID;
+ ar << m_strMusicBrainzAlbumID;
+ ar << m_strMusicBrainzReleaseGroupID;
+ ar << m_musicBrainzAlbumArtistID;
+ ar << m_strDiscSubtitle;
+ ar << m_bBoxset;
+ ar << m_iDiscTotal;
+ ar << m_strMusicBrainzReleaseType;
+ ar << m_lastPlayed;
+ ar << m_dateAdded;
+ ar << m_strComment;
+ ar << (int)m_musicRoles.size();
+ for (const auto& credit : m_musicRoles)
+ {
+ ar << credit.GetRoleId();
+ ar << credit.GetRoleDesc();
+ ar << credit.GetArtist();
+ ar << credit.GetArtistId();
+ }
+ ar << m_strMood;
+ ar << m_strRecordLabel;
+ ar << m_Rating;
+ ar << m_Userrating;
+ ar << m_Votes;
+ ar << m_iTimesPlayed;
+ ar << m_iAlbumId;
+ ar << m_iDbId;
+ ar << m_type;
+ ar << m_strReleaseStatus;
+ ar << m_strLyrics;
+ ar << m_bCompilation;
+ ar << m_listeners;
+ ar << m_coverArt;
+ ar << m_cuesheet;
+ ar << static_cast<int>(m_albumReleaseType);
+ ar << m_iBPM;
+ ar << m_samplerate;
+ ar << m_bitrate;
+ ar << m_channels;
+ }
+ else
+ {
+ ar >> m_strURL;
+ ar >> m_strTitle;
+ ar >> m_artist;
+ ar >> m_strArtistSort;
+ ar >> m_strArtistDesc;
+ ar >> m_strAlbum;
+ ar >> m_albumArtist;
+ ar >> m_strAlbumArtistDesc;
+ ar >> m_genre;
+ ar >> m_iDuration;
+ ar >> m_iTrack;
+ ar >> m_bLoaded;
+ ar >> m_strReleaseDate;
+ ar >> m_strOriginalDate;
+ ar >> m_strMusicBrainzTrackID;
+ ar >> m_musicBrainzArtistID;
+ ar >> m_strMusicBrainzAlbumID;
+ ar >> m_strMusicBrainzReleaseGroupID;
+ ar >> m_musicBrainzAlbumArtistID;
+ ar >> m_strDiscSubtitle;
+ ar >> m_bBoxset;
+ ar >> m_iDiscTotal;
+ ar >> m_strMusicBrainzReleaseType;
+ ar >> m_lastPlayed;
+ ar >> m_dateAdded;
+ ar >> m_strComment;
+ int iMusicRolesSize;
+ ar >> iMusicRolesSize;
+ m_musicRoles.reserve(iMusicRolesSize);
+ for (int i = 0; i < iMusicRolesSize; ++i)
+ {
+ int idRole;
+ int idArtist;
+ std::string strArtist;
+ std::string strRole;
+ ar >> idRole;
+ ar >> strRole;
+ ar >> strArtist;
+ ar >> idArtist;
+ m_musicRoles.emplace_back(idRole, strRole, strArtist, idArtist);
+ }
+ ar >> m_strMood;
+ ar >> m_strRecordLabel;
+ ar >> m_Rating;
+ ar >> m_Userrating;
+ ar >> m_Votes;
+ ar >> m_iTimesPlayed;
+ ar >> m_iAlbumId;
+ ar >> m_iDbId;
+ ar >> m_type;
+ ar >> m_strReleaseStatus;
+ ar >> m_strLyrics;
+ ar >> m_bCompilation;
+ ar >> m_listeners;
+ ar >> m_coverArt;
+ ar >> m_cuesheet;
+
+ int albumReleaseType;
+ ar >> albumReleaseType;
+ m_albumReleaseType = static_cast<CAlbum::ReleaseType>(albumReleaseType);
+ ar >> m_iBPM;
+ ar >> m_samplerate;
+ ar >> m_bitrate;
+ ar >> m_channels;
+ }
+}
+
+void CMusicInfoTag::Clear()
+{
+ m_strURL.clear();
+ m_artist.clear();
+ m_strArtistSort.clear();
+ m_strComposerSort.clear();
+ m_strAlbum.clear();
+ m_albumArtist.clear();
+ m_genre.clear();
+ m_strTitle.clear();
+ m_strMusicBrainzTrackID.clear();
+ m_musicBrainzArtistID.clear();
+ m_strMusicBrainzAlbumID.clear();
+ m_strMusicBrainzReleaseGroupID.clear();
+ m_musicBrainzAlbumArtistID.clear();
+ m_strMusicBrainzReleaseType.clear();
+ m_musicRoles.clear();
+ m_iDuration = 0;
+ m_iTrack = 0;
+ m_bLoaded = false;
+ m_lastPlayed.Reset();
+ m_dateAdded.Reset();
+ m_dateNew.Reset();
+ m_dateUpdated.Reset();
+ m_bCompilation = false;
+ m_bBoxset = false;
+ m_strDiscSubtitle.clear();
+ m_strComment.clear();
+ m_strMood.clear();
+ m_strRecordLabel.clear();
+ m_cuesheet.clear();
+ m_iDbId = -1;
+ m_type.clear();
+ m_strReleaseStatus.clear();
+ m_iTimesPlayed = 0;
+ m_strReleaseDate.clear();
+ m_strOriginalDate.clear();
+ m_iAlbumId = -1;
+ m_coverArt.Clear();
+ m_replayGain = ReplayGain();
+ m_albumReleaseType = CAlbum::Album;
+ m_listeners = 0;
+ m_Rating = 0;
+ m_Userrating = 0;
+ m_Votes = 0;
+ m_iDiscTotal = 0;
+ m_iBPM = 0;
+ m_samplerate = 0;
+ m_bitrate = 0;
+ m_channels = 0;
+ m_stationName.clear();
+ m_stationArt.clear();
+}
+
+void CMusicInfoTag::AppendArtist(const std::string &artist)
+{
+ for (unsigned int index = 0; index < m_artist.size(); index++)
+ {
+ if (StringUtils::EqualsNoCase(artist, m_artist.at(index)))
+ return;
+ }
+
+ m_artist.push_back(artist);
+}
+
+void CMusicInfoTag::AppendAlbumArtist(const std::string &albumArtist)
+{
+ for (unsigned int index = 0; index < m_albumArtist.size(); index++)
+ {
+ if (StringUtils::EqualsNoCase(albumArtist, m_albumArtist.at(index)))
+ return;
+ }
+
+ m_albumArtist.push_back(albumArtist);
+}
+
+void CMusicInfoTag::AppendGenre(const std::string &genre)
+{
+ for (unsigned int index = 0; index < m_genre.size(); index++)
+ {
+ if (StringUtils::EqualsNoCase(genre, m_genre.at(index)))
+ return;
+ }
+
+ m_genre.push_back(genre);
+}
+
+void CMusicInfoTag::AddArtistRole(const std::string& Role, const std::string& strArtist)
+{
+ if (!strArtist.empty() && !Role.empty())
+ AddArtistRole(Role, StringUtils::Split(strArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CMusicInfoTag::AddArtistRole(const std::string& Role, const std::vector<std::string>& artists)
+{
+ for (unsigned int index = 0; index < artists.size(); index++)
+ {
+ CMusicRole ArtistCredit(Role, Trim(artists.at(index)));
+ //Prevent duplicate entries
+ auto credit = find(m_musicRoles.begin(), m_musicRoles.end(), ArtistCredit);
+ if (credit == m_musicRoles.end())
+ m_musicRoles.push_back(ArtistCredit);
+ }
+}
+
+void CMusicInfoTag::AppendArtistRole(const CMusicRole& ArtistRole)
+{
+ //Append contributor, no check for duplicates as from database
+ m_musicRoles.push_back(ArtistRole);
+}
+
+const std::string CMusicInfoTag::GetArtistStringForRole(const std::string& strRole) const
+{
+ std::vector<std::string> artistvector;
+ for (const auto& credit : m_musicRoles)
+ {
+ if (StringUtils::EqualsNoCase(credit.GetRoleDesc(), strRole))
+ artistvector.push_back(credit.GetArtist());
+ }
+ return StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+}
+
+const std::string CMusicInfoTag::GetContributorsText() const
+{
+ std::string strLabel;
+ for (const auto& credit : m_musicRoles)
+ {
+ strLabel += StringUtils::Format("{}\n", credit.GetArtist());
+ }
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+const std::string CMusicInfoTag::GetContributorsAndRolesText() const
+{
+ std::string strLabel;
+ for (const auto& credit : m_musicRoles)
+ {
+ strLabel += StringUtils::Format("{} - {}\n", credit.GetRoleDesc(), credit.GetArtist());
+ }
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+
+const VECMUSICROLES &CMusicInfoTag::GetContributors() const
+{
+ return m_musicRoles;
+}
+
+void CMusicInfoTag::SetContributors(const VECMUSICROLES& contributors)
+{
+ m_musicRoles = contributors;
+}
+
+std::string CMusicInfoTag::Trim(const std::string &value) const
+{
+ std::string trimmedValue(value);
+ StringUtils::TrimLeft(trimmedValue, " ");
+ StringUtils::TrimRight(trimmedValue, " \n\r");
+ return trimmedValue;
+}
diff --git a/xbmc/music/tags/MusicInfoTag.h b/xbmc/music/tags/MusicInfoTag.h
new file mode 100644
index 0000000..3c1b994
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTag.h
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2005-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
+
+class CSong;
+class CArtist;
+class CVariant;
+
+#include "ReplayGain.h"
+#include "XBDateTime.h"
+#include "music/Album.h"
+#include "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <string>
+#include <vector>
+
+namespace MUSIC_INFO
+{
+class CMusicInfoTag final : public IArchivable, public ISerializable, public ISortable
+{
+public:
+ CMusicInfoTag(void);
+ bool operator !=(const CMusicInfoTag& tag) const;
+ bool Loaded() const;
+ const std::string& GetTitle() const;
+ const std::string& GetURL() const;
+ const std::vector<std::string>& GetArtist() const;
+ const std::string& GetArtistSort() const;
+ const std::string GetArtistString() const;
+ const std::string& GetComposerSort() const;
+ const std::string& GetAlbum() const;
+ int GetAlbumId() const;
+ const std::vector<std::string>& GetAlbumArtist() const;
+ const std::string GetAlbumArtistString() const;
+ const std::string& GetAlbumArtistSort() const;
+ const std::vector<std::string>& GetGenre() const;
+ int GetTrackNumber() const;
+ int GetDiscNumber() const;
+ int GetTrackAndDiscNumber() const;
+ int GetTotalDiscs() const;
+ int GetDuration() const; // may be set even if Loaded() returns false
+ int GetYear() const;
+ const std::string& GetReleaseDate() const;
+ const std::string GetReleaseYear() const;
+ const std::string& GetOriginalDate() const;
+ const std::string GetOriginalYear() const;
+ int GetDatabaseId() const;
+ const std::string &GetType() const;
+ const std::string& GetDiscSubtitle() const;
+ int GetBPM() const;
+ std::string GetYearString() const;
+ const std::string& GetMusicBrainzTrackID() const;
+ const std::vector<std::string>& GetMusicBrainzArtistID() const;
+ const std::vector<std::string>& GetMusicBrainzArtistHints() const;
+ const std::string& GetMusicBrainzAlbumID() const;
+ const std::string& GetMusicBrainzReleaseGroupID() const;
+ const std::vector<std::string>& GetMusicBrainzAlbumArtistID() const;
+ const std::vector<std::string>& GetMusicBrainzAlbumArtistHints() const;
+ const std::string& GetMusicBrainzReleaseType() const;
+ const std::string& GetComment() const;
+ const std::string& GetMood() const;
+ const std::string& GetRecordLabel() const;
+ const std::string& GetLyrics() const;
+ const std::string& GetCueSheet() const;
+ const CDateTime& GetLastPlayed() const;
+ const CDateTime& GetDateAdded() const;
+ bool GetCompilation() const;
+ bool GetBoxset() const;
+ float GetRating() const;
+ int GetUserrating() const;
+ int GetVotes() const;
+ int GetListeners() const;
+ int GetPlayCount() const;
+ int GetBitRate() const;
+ int GetNoOfChannels() const;
+ int GetSampleRate() const;
+ const std::string& GetAlbumReleaseStatus() const;
+ const std::string& GetStationName() const;
+ const std::string& GetStationArt() const;
+ const EmbeddedArtInfo &GetCoverArtInfo() const;
+ const ReplayGain& GetReplayGain() const;
+ CAlbum::ReleaseType GetAlbumReleaseType() const;
+
+ void SetURL(const std::string& strURL);
+ void SetTitle(const std::string& strTitle);
+ void SetArtist(const std::string& strArtist);
+ void SetArtist(const std::vector<std::string>& artists, bool FillDesc = false);
+ void SetArtistDesc(const std::string& strArtistDesc);
+ void SetArtistSort(const std::string& strArtistsort);
+ void SetComposerSort(const std::string& strComposerSort);
+ void SetAlbum(const std::string& strAlbum);
+ void SetAlbumId(const int iAlbumId);
+ void SetAlbumArtist(const std::string& strAlbumArtist);
+ void SetAlbumArtist(const std::vector<std::string>& albumArtists, bool FillDesc = false);
+ void SetAlbumArtistDesc(const std::string& strAlbumArtistDesc);
+ void SetAlbumArtistSort(const std::string& strAlbumArtistSort);
+ void SetGenre(const std::string& strGenre, bool bTrim = false);
+ void SetGenre(const std::vector<std::string>& genres, bool bTrim = false);
+ void SetYear(int year);
+ void SetOriginalDate(const std::string& strOriginalDate);
+ void SetReleaseDate(const std::string& strReleaseDate);
+ void SetDatabaseId(int id, const std::string &type);
+ void SetTrackNumber(int iTrack);
+ void SetDiscNumber(int iDiscNumber);
+ void SetTrackAndDiscNumber(int iTrackAndDisc);
+ void SetDuration(int iSec);
+ void SetLoaded(bool bOnOff = true);
+ void SetArtist(const CArtist& artist);
+ void SetAlbum(const CAlbum& album);
+ void SetSong(const CSong& song);
+ void SetMusicBrainzTrackID(const std::string& strTrackID);
+ void SetMusicBrainzArtistID(const std::vector<std::string>& musicBrainzArtistId);
+ void SetMusicBrainzArtistHints(const std::vector<std::string>& musicBrainzArtistHints);
+ void SetMusicBrainzAlbumID(const std::string& strAlbumID);
+ void SetMusicBrainzAlbumArtistID(const std::vector<std::string>& musicBrainzAlbumArtistId);
+ void SetMusicBrainzAlbumArtistHints(const std::vector<std::string>& musicBrainzAlbumArtistHints);
+ void SetMusicBrainzReleaseGroupID(const std::string& strReleaseGroupID);
+ void SetMusicBrainzReleaseType(const std::string& ReleaseType);
+ void SetComment(const std::string& comment);
+ void SetMood(const std::string& mood);
+ void SetRecordLabel(const std::string& publisher);
+ void SetLyrics(const std::string& lyrics);
+ void SetCueSheet(const std::string& cueSheet);
+ void SetRating(float rating);
+ void SetUserrating(int rating);
+ void SetVotes(int votes);
+ void SetListeners(int listeners);
+ void SetPlayCount(int playcount);
+ void SetLastPlayed(const std::string& strLastPlayed);
+ void SetLastPlayed(const CDateTime& strLastPlayed);
+ void SetDateAdded(const std::string& strDateAdded);
+ void SetDateAdded(const CDateTime& dateAdded);
+ void SetDateUpdated(const std::string& strDateUpdated);
+ void SetDateUpdated(const CDateTime& dateUpdated);
+ void SetDateNew(const std::string& strDateNew);
+ void SetDateNew(const CDateTime& dateNew);
+ void SetCompilation(bool compilation);
+ void SetBoxset(bool boxset);
+ void SetCoverArtInfo(size_t size, const std::string &mimeType);
+ void SetReplayGain(const ReplayGain& aGain);
+ void SetAlbumReleaseType(CAlbum::ReleaseType releaseType);
+ void SetType(const MediaType& mediaType);
+ void SetDiscSubtitle(const std::string& strDiscSubtitle);
+ void SetTotalDiscs(int iDiscTotal);
+ void SetBPM(int iBPM);
+ void SetBitRate(int bitrate);
+ void SetNoOfChannels(int channels);
+ void SetSampleRate(int samplerate);
+ void SetAlbumReleaseStatus(const std::string& strReleaseStatus);
+ void SetStationName(const std::string& strStationName); // name of online radio station
+ void SetStationArt(const std::string& strStationArt);
+
+ /*! \brief Append a unique artist to the artist list
+ Checks if we have this artist already added, and if not adds it to the songs artist list.
+ \param value artist to add.
+ */
+ void AppendArtist(const std::string &artist);
+
+ /*! \brief Append a unique album artist to the artist list
+ Checks if we have this album artist already added, and if not adds it to the songs album artist list.
+ \param albumArtist album artist to add.
+ */
+ void AppendAlbumArtist(const std::string &albumArtist);
+
+ /*! \brief Append a unique genre to the genre list
+ Checks if we have this genre already added, and if not adds it to the songs genre list.
+ \param genre genre to add.
+ */
+ void AppendGenre(const std::string &genre);
+ void AddOriginalDate(const std::string& strDateYear);
+ void AddReleaseDate(const std::string& strDateYear, bool isMonth = false);
+
+ void AddArtistRole(const std::string& Role, const std::string& strArtist);
+ void AddArtistRole(const std::string& Role, const std::vector<std::string>& artists);
+ void AppendArtistRole(const CMusicRole& ArtistRole);
+ const std::string GetArtistStringForRole(const std::string& strRole) const;
+ const std::string GetContributorsText() const;
+ const std::string GetContributorsAndRolesText() const;
+ const VECMUSICROLES &GetContributors() const;
+ void SetContributors(const VECMUSICROLES& contributors);
+ bool HasContributors() const { return !m_musicRoles.empty(); }
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& ar) const override;
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ void Clear();
+
+protected:
+ /*! \brief Trim whitespace off the given string
+ \param value string to trim
+ \return trimmed value, with spaces removed from left and right, as well as carriage returns from the right.
+ */
+ std::string Trim(const std::string &value) const;
+
+ std::string m_strURL;
+ std::string m_strTitle;
+ std::vector<std::string> m_artist;
+ std::string m_strArtistSort;
+ std::string m_strArtistDesc;
+ std::string m_strComposerSort;
+ std::string m_strAlbum;
+ std::vector<std::string> m_albumArtist;
+ std::string m_strAlbumArtistDesc;
+ std::string m_strAlbumArtistSort;
+ std::vector<std::string> m_genre;
+ std::string m_strMusicBrainzTrackID;
+ std::vector<std::string> m_musicBrainzArtistID;
+ std::vector<std::string> m_musicBrainzArtistHints;
+ std::string m_strMusicBrainzAlbumID;
+ std::vector<std::string> m_musicBrainzAlbumArtistID;
+ std::vector<std::string> m_musicBrainzAlbumArtistHints;
+ std::string m_strMusicBrainzReleaseGroupID;
+ std::string m_strMusicBrainzReleaseType;
+ VECMUSICROLES m_musicRoles; //Artists contributing to the recording and role (from tags other than ARTIST or ALBUMARTIST)
+ std::string m_strComment;
+ std::string m_strMood;
+ std::string m_strRecordLabel;
+ std::string m_strLyrics;
+ std::string m_cuesheet;
+ std::string m_strDiscSubtitle;
+ std::string m_strReleaseDate; //ISO8601 date YYYY, YYYY-MM or YYYY-MM-DD
+ std::string m_strOriginalDate; //ISO8601 date YYYY, YYYY-MM or YYYY-MM-DD
+ CDateTime m_lastPlayed;
+ CDateTime m_dateNew;
+ CDateTime m_dateAdded;
+ CDateTime m_dateUpdated;
+ bool m_bCompilation;
+ int m_iDuration;
+ int m_iTrack; // consists of the disk number in the high 16 bits, the track number in the low 16bits
+ int m_iDbId;
+ MediaType m_type; ///< item type "music", "song", "album", "artist"
+ bool m_bLoaded;
+ float m_Rating;
+ int m_Userrating;
+ int m_Votes;
+ int m_listeners;
+ int m_iTimesPlayed;
+ int m_iAlbumId;
+ int m_iDiscTotal;
+ bool m_bBoxset;
+ int m_iBPM;
+ CAlbum::ReleaseType m_albumReleaseType;
+ std::string m_strReleaseStatus;
+ int m_samplerate;
+ int m_channels;
+ int m_bitrate;
+ std::string m_stationName;
+ std::string m_stationArt; // Used to fetch thumb URL for Shoutcasts
+
+ EmbeddedArtInfo m_coverArt; ///< art information
+
+ ReplayGain m_replayGain; ///< ReplayGain information
+};
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp b/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp
new file mode 100644
index 0000000..66938d3
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoTagLoaderCDDA.h"
+
+#include "MusicInfoTag.h"
+#include "ServiceBroker.h"
+#include "network/cddb.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/log.h"
+
+using namespace MUSIC_INFO;
+
+#ifdef HAS_DVD_DRIVE
+using namespace MEDIA_DETECT;
+using namespace CDDB;
+#endif
+
+//! @todo - remove after Ubuntu 16.04 (Xenial) is EOL
+#if !defined(LIBCDIO_VERSION_NUM) || (LIBCDIO_VERSION_NUM <= 83)
+#define CDTEXT_FIELD_TITLE CDTEXT_TITLE
+#define CDTEXT_FIELD_PERFORMER CDTEXT_PERFORMER
+#define CDTEXT_FIELD_GENRE CDTEXT_GENRE
+#endif
+
+CMusicInfoTagLoaderCDDA::CMusicInfoTagLoaderCDDA(void) = default;
+
+CMusicInfoTagLoaderCDDA::~CMusicInfoTagLoaderCDDA() = default;
+
+bool CMusicInfoTagLoaderCDDA::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+#ifdef HAS_DVD_DRIVE
+ try
+ {
+ tag.SetURL(strFileName);
+ bool bResult = false;
+
+ // Get information for the inserted disc
+ CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo == NULL)
+ return bResult;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // Prepare cddb
+ Xcddb cddb;
+ cddb.setCacheDir(profileManager->GetCDDBFolder());
+
+ int iTrack = atoi(strFileName.substr(13, strFileName.size() - 13 - 5).c_str());
+
+ // duration is always available
+ tag.SetDuration( ( pCdInfo->GetTrackInformation(iTrack).nMins * 60 )
+ + pCdInfo->GetTrackInformation(iTrack).nSecs );
+
+ // Only load cached cddb info in this tag loader, the internet database query is made in CCDDADirectory
+ if (pCdInfo->HasCDDBInfo() && cddb.isCDCached(pCdInfo))
+ {
+ // get cddb information
+ if (cddb.queryCDinfo(pCdInfo))
+ {
+ // Fill the fileitems music tag with cddb information, if available
+ const std::string& strTitle = cddb.getTrackTitle(iTrack);
+ if (!strTitle.empty())
+ {
+ // Tracknumber
+ tag.SetTrackNumber(iTrack);
+
+ // Title
+ tag.SetTitle(strTitle);
+
+ // Artist: Use track artist or disc artist
+ std::string strArtist = cddb.getTrackArtist(iTrack);
+ if (strArtist.empty())
+ cddb.getDiskArtist(strArtist);
+ tag.SetArtist(strArtist);
+
+ // Album
+ std::string strAlbum;
+ cddb.getDiskTitle( strAlbum );
+ tag.SetAlbum(strAlbum);
+
+ // Album Artist
+ std::string strAlbumArtist;
+ cddb.getDiskArtist(strAlbumArtist);
+ tag.SetAlbumArtist(strAlbumArtist);
+
+ // Year
+ tag.SetReleaseDate(cddb.getYear());
+
+ // Genre
+ tag.SetGenre( cddb.getGenre() );
+
+ tag.SetLoaded(true);
+ bResult = true;
+ }
+ }
+ }
+ else
+ {
+ // No cddb info, maybe we have CD-Text
+ trackinfo ti = pCdInfo->GetTrackInformation(iTrack);
+
+ // Fill the fileitems music tag with CD-Text information, if available
+ std::string strTitle = ti.cdtext[CDTEXT_FIELD_TITLE];
+ if (!strTitle.empty())
+ {
+ // Tracknumber
+ tag.SetTrackNumber(iTrack);
+
+ // Title
+ tag.SetTitle(strTitle);
+
+ // Get info for track zero, as we may have and need CD-Text Album info
+ xbmc_cdtext_t discCDText = pCdInfo->GetDiscCDTextInformation();
+
+ // Artist: Use track artist or disc artist
+ std::string strArtist = ti.cdtext[CDTEXT_FIELD_PERFORMER];
+ if (strArtist.empty())
+ strArtist = discCDText[CDTEXT_FIELD_PERFORMER];
+ tag.SetArtist(strArtist);
+
+ // Album
+ std::string strAlbum;
+ strAlbum = discCDText[CDTEXT_FIELD_TITLE];
+ tag.SetAlbum(strAlbum);
+
+ // Genre: use track or disc genre
+ std::string strGenre = ti.cdtext[CDTEXT_FIELD_GENRE];
+ if (strGenre.empty())
+ strGenre = discCDText[CDTEXT_FIELD_GENRE];
+ tag.SetGenre( strGenre );
+
+ tag.SetLoaded(true);
+ bResult = true;
+ }
+ }
+ return bResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Tag loader CDDB: exception in file {}", strFileName);
+ }
+
+#endif
+
+ tag.SetLoaded(false);
+
+ return false;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderCDDA.h b/xbmc/music/tags/MusicInfoTagLoaderCDDA.h
new file mode 100644
index 0000000..95fc35f
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderCDDA.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2005-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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderCDDA: public IMusicInfoTagLoader
+ {
+ public:
+ CMusicInfoTagLoaderCDDA(void);
+ ~CMusicInfoTagLoaderCDDA() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+ };
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp b/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp
new file mode 100644
index 0000000..1409809
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoTagLoaderDatabase.h"
+
+#include "MusicInfoTag.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "music/MusicDatabase.h"
+
+using namespace MUSIC_INFO;
+
+CMusicInfoTagLoaderDatabase::CMusicInfoTagLoaderDatabase(void) = default;
+
+CMusicInfoTagLoaderDatabase::~CMusicInfoTagLoaderDatabase() = default;
+
+bool CMusicInfoTagLoaderDatabase::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+ tag.SetLoaded(false);
+ CMusicDatabase database;
+ database.Open();
+ XFILE::MUSICDATABASEDIRECTORY::CQueryParams param;
+ XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strFileName,param);
+
+ CSong song;
+ if (database.GetSong(param.GetSongId(),song))
+ tag.SetSong(song);
+
+ database.Close();
+
+ return tag.Loaded();
+}
+
diff --git a/xbmc/music/tags/MusicInfoTagLoaderDatabase.h b/xbmc/music/tags/MusicInfoTagLoaderDatabase.h
new file mode 100644
index 0000000..85272e5
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderDatabase.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2005-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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderDatabase: public IMusicInfoTagLoader
+ {
+ public:
+ CMusicInfoTagLoaderDatabase(void);
+ ~CMusicInfoTagLoaderDatabase() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+ };
+}
+
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp
new file mode 100644
index 0000000..9bb2cdc
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoTagLoaderFFmpeg.h"
+
+#include "MusicInfoTag.h"
+#include "cores/FFmpeg.h"
+#include "filesystem/File.h"
+#include "utils/StringUtils.h"
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+static int vfs_file_read(void *h, uint8_t* buf, int size)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ return pFile->Read(buf, size);
+}
+
+static int64_t vfs_file_seek(void *h, int64_t pos, int whence)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ if (whence == AVSEEK_SIZE)
+ return pFile->GetLength();
+ else
+ return pFile->Seek(pos, whence & ~AVSEEK_FORCE);
+}
+
+CMusicInfoTagLoaderFFmpeg::CMusicInfoTagLoaderFFmpeg(void) = default;
+
+CMusicInfoTagLoaderFFmpeg::~CMusicInfoTagLoaderFFmpeg() = default;
+
+bool CMusicInfoTagLoaderFFmpeg::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+ tag.SetLoaded(false);
+
+ CFile file;
+ if (!file.Open(strFileName))
+ return false;
+
+ int bufferSize = 4096;
+ int blockSize = file.GetChunkSize();
+ if (blockSize > 1)
+ bufferSize = blockSize;
+ uint8_t* buffer = (uint8_t*)av_malloc(bufferSize);
+ AVIOContext* ioctx = avio_alloc_context(buffer, bufferSize, 0,
+ &file, vfs_file_read, NULL,
+ vfs_file_seek);
+
+ AVFormatContext* fctx = avformat_alloc_context();
+ fctx->pb = ioctx;
+
+ if (file.IoControl(IOCTRL_SEEK_POSSIBLE, NULL) != 1)
+ ioctx->seekable = 0;
+
+ AVInputFormat* iformat=NULL;
+ av_probe_input_buffer(ioctx, &iformat, strFileName.c_str(), NULL, 0, 0);
+
+ if (avformat_open_input(&fctx, strFileName.c_str(), iformat, NULL) < 0)
+ {
+ if (fctx)
+ avformat_close_input(&fctx);
+ av_free(ioctx->buffer);
+ av_free(ioctx);
+ return false;
+ }
+
+ /* ffmpeg supports the return of ID3v2 metadata but has its own naming system
+ for some, but not all, of the keys. In particular the key for the conductor
+ tag TPE3 is called "performer".
+ See https://github.com/xbmc/FFmpeg/blob/master/libavformat/id3v2.c#L43
+ Other keys are retuened using their 4 char name.
+ Only single frame values are returned even for v2.4 fomart tags e.g. while
+ tagged with multiple TPE1 frames "artist1", "artist2", "artist3" only
+ "artist1" is returned by ffmpeg.
+ Hence, like with v2.3 format tags, multiple values for artist, genre etc.
+ need to be combined when tagging into a single value using a known item
+ separator e.g. "artist1 / artist2 / artist3"
+
+ Any changes to ID3v2 tag processing in CTagLoaderTagLib need to be
+ repeated here
+ */
+ auto&& ParseTag = [&tag](AVDictionaryEntry* avtag)
+ {
+ if (StringUtils::CompareNoCase(avtag->key, "album") == 0)
+ tag.SetAlbum(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "artist") == 0)
+ tag.SetArtist(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "album_artist") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "album artist") == 0)
+ tag.SetAlbumArtist(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "title") == 0)
+ tag.SetTitle(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "genre") == 0)
+ tag.SetGenre(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "part_number") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "track") == 0)
+ tag.SetTrackNumber(
+ static_cast<int>(strtol(avtag->value, nullptr, 10)));
+ else if (StringUtils::CompareNoCase(avtag->key, "disc") == 0)
+ tag.SetDiscNumber(
+ static_cast<int>(strtol(avtag->value, nullptr, 10)));
+ else if (StringUtils::CompareNoCase(avtag->key, "date") == 0)
+ tag.SetReleaseDate(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "compilation") == 0)
+ tag.SetCompilation((strtol(avtag->value, nullptr, 10) == 0) ? false : true);
+ else if (StringUtils::CompareNoCase(avtag->key, "encoded_by") == 0) {}
+ else if (StringUtils::CompareNoCase(avtag->key, "composer") == 0)
+ tag.AddArtistRole("Composer", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "performer") == 0) // Conductor or TPE3 tag
+ tag.AddArtistRole("Conductor", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "TEXT") == 0)
+ tag.AddArtistRole("Lyricist", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "TPE4") == 0)
+ tag.AddArtistRole("Remixer", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "LABEL") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TPUB") == 0)
+ tag.SetRecordLabel(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "copyright") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TCOP") == 0) {} // Copyright message
+ else if (StringUtils::CompareNoCase(avtag->key, "TDRC") == 0)
+ tag.SetReleaseDate(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "TDOR") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TORY") == 0)
+ tag.SetOriginalDate(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key , "TDAT") == 0)
+ tag.AddReleaseDate(avtag->value, true); // MMDD part
+ else if (StringUtils::CompareNoCase(avtag->key, "TYER") == 0)
+ tag.AddReleaseDate(avtag->value); // YYYY part
+ else if (StringUtils::CompareNoCase(avtag->key, "TBPM") == 0)
+ tag.SetBPM(static_cast<int>(strtol(avtag->value, nullptr, 10)));
+ else if (StringUtils::CompareNoCase(avtag->key, "TDTG") == 0) {} // Tagging time
+ else if (StringUtils::CompareNoCase(avtag->key, "language") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TLAN") == 0) {} // Languages
+ else if (StringUtils::CompareNoCase(avtag->key, "mood") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TMOO") == 0)
+ tag.SetMood(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "artist-sort") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TSOP") == 0) {}
+ else if (StringUtils::CompareNoCase(avtag->key, "TSO2") == 0) {} // Album artist sort
+ else if (StringUtils::CompareNoCase(avtag->key, "TSOC") == 0) {} // composer sort
+ else if (StringUtils::CompareNoCase(avtag->key, "TSST") == 0)
+ tag.SetDiscSubtitle(avtag->value);
+ };
+
+ AVDictionaryEntry* avtag=nullptr;
+ while ((avtag = av_dict_get(fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ ParseTag(avtag);
+
+ const AVStream* st = fctx->streams[0];
+ if (st)
+ while ((avtag = av_dict_get(st->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ ParseTag(avtag);
+
+ if (!tag.GetTitle().empty())
+ tag.SetLoaded(true);
+
+ avformat_close_input(&fctx);
+ av_free(ioctx->buffer);
+ av_free(ioctx);
+
+ return true;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h
new file mode 100644
index 0000000..272c065
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2005-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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderFFmpeg: public IMusicInfoTagLoader
+ {
+ public:
+ CMusicInfoTagLoaderFFmpeg(void);
+ ~CMusicInfoTagLoaderFFmpeg() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+ };
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp b/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp
new file mode 100644
index 0000000..56b2c17
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoTagLoaderFactory.h"
+
+#include "FileItem.h"
+#include "MusicInfoTagLoaderCDDA.h"
+#include "MusicInfoTagLoaderDatabase.h"
+#include "MusicInfoTagLoaderFFmpeg.h"
+#include "MusicInfoTagLoaderShn.h"
+#include "ServiceBroker.h"
+#include "TagLoaderTagLib.h"
+#include "addons/AudioDecoder.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/addoninfo/AddonType.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace KODI::ADDONS;
+using namespace MUSIC_INFO;
+
+CMusicInfoTagLoaderFactory::CMusicInfoTagLoaderFactory() = default;
+
+CMusicInfoTagLoaderFactory::~CMusicInfoTagLoaderFactory() = default;
+
+IMusicInfoTagLoader* CMusicInfoTagLoaderFactory::CreateLoader(const CFileItem& item)
+{
+ // dont try to read the tags for streams & shoutcast
+ if (item.IsInternetStream())
+ return NULL;
+
+ if (item.IsMusicDb())
+ return new CMusicInfoTagLoaderDatabase();
+
+ std::string strExtension = URIUtils::GetExtension(item.GetPath());
+ StringUtils::ToLower(strExtension);
+ StringUtils::TrimLeft(strExtension, ".");
+
+ if (strExtension.empty())
+ return NULL;
+
+ const auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
+ "." + strExtension, CExtsMimeSupportList::FilterSelect::hasTags);
+ for (const auto& addonInfo : addonInfos)
+ {
+ if (addonInfo.first == ADDON::AddonType::AUDIODECODER)
+ {
+ std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second);
+ if (!result->CreateDecoder() && result->SupportsFile(item.GetPath()))
+ continue;
+
+ return result.release();
+ }
+ }
+
+ if (strExtension == "aac" || strExtension == "ape" || strExtension == "mac" ||
+ strExtension == "mp3" || strExtension == "wma" || strExtension == "flac" ||
+ strExtension == "m4a" || strExtension == "mp4" || strExtension == "m4b" ||
+ strExtension == "m4v" || strExtension == "mpc" || strExtension == "mpp" ||
+ strExtension == "mp+" || strExtension == "ogg" || strExtension == "oga" ||
+ strExtension == "opus" || strExtension == "aif" || strExtension == "aiff" ||
+ strExtension == "wav" || strExtension == "mod" || strExtension == "s3m" ||
+ strExtension == "it" || strExtension == "xm" || strExtension == "wv")
+ {
+ CTagLoaderTagLib *pTagLoader = new CTagLoaderTagLib();
+ return pTagLoader;
+ }
+#ifdef HAS_DVD_DRIVE
+ else if (strExtension == "cdda")
+ {
+ CMusicInfoTagLoaderCDDA *pTagLoader = new CMusicInfoTagLoaderCDDA();
+ return pTagLoader;
+ }
+#endif
+ else if (strExtension == "shn")
+ {
+ CMusicInfoTagLoaderSHN *pTagLoader = new CMusicInfoTagLoaderSHN();
+ return pTagLoader;
+ }
+ else if (strExtension == "mka" || strExtension == "dsf" ||
+ strExtension == "dff")
+ return new CMusicInfoTagLoaderFFmpeg();
+
+ return NULL;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFactory.h b/xbmc/music/tags/MusicInfoTagLoaderFactory.h
new file mode 100644
index 0000000..ba2aba2
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFactory.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2005-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 "ImusicInfoTagLoader.h"
+
+class CFileItem; // forward
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderFactory
+ {
+ public:
+ CMusicInfoTagLoaderFactory(void);
+ virtual ~CMusicInfoTagLoaderFactory();
+
+ static IMusicInfoTagLoader* CreateLoader(const CFileItem& item);
+ };
+}
+
diff --git a/xbmc/music/tags/MusicInfoTagLoaderShn.cpp b/xbmc/music/tags/MusicInfoTagLoaderShn.cpp
new file mode 100644
index 0000000..2b9bd18
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderShn.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-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 "MusicInfoTagLoaderShn.h"
+
+#include "MusicInfoTag.h"
+#include "utils/log.h"
+
+using namespace MUSIC_INFO;
+
+CMusicInfoTagLoaderSHN::CMusicInfoTagLoaderSHN(void) = default;
+
+CMusicInfoTagLoaderSHN::~CMusicInfoTagLoaderSHN() = default;
+
+bool CMusicInfoTagLoaderSHN::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+ try
+ {
+
+ tag.SetURL(strFileName);
+ tag.SetDuration((long)0); //! @todo Use libavformat to calculate duration.
+ tag.SetLoaded(false);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Tag loader shn: exception in file {}", strFileName);
+ }
+
+ tag.SetLoaded(false);
+ return false;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderShn.h b/xbmc/music/tags/MusicInfoTagLoaderShn.h
new file mode 100644
index 0000000..d932320
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderShn.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2005-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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+
+class CMusicInfoTagLoaderSHN: public IMusicInfoTagLoader
+{
+public:
+ CMusicInfoTagLoaderSHN(void);
+ ~CMusicInfoTagLoaderSHN() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+};
+}
diff --git a/xbmc/music/tags/ReplayGain.cpp b/xbmc/music/tags/ReplayGain.cpp
new file mode 100644
index 0000000..c9947f4
--- /dev/null
+++ b/xbmc/music/tags/ReplayGain.cpp
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2005-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 "ReplayGain.h"
+
+#include "utils/StringUtils.h"
+
+#include <stdlib.h>
+
+static bool TypeIsValid(ReplayGain::Type aType)
+{
+ return (aType > ReplayGain::NONE && aType <= ReplayGain::TRACK);
+}
+
+static int index(ReplayGain::Type aType)
+{
+ return static_cast<int>(aType) - 1;
+}
+
+///////////////////////////////////////////////////////////////
+// class ReplayGain
+///////////////////////////////////////////////////////////////
+
+const ReplayGain::Info& ReplayGain::Get(Type aType) const
+{
+ if (TypeIsValid(aType))
+ return m_data[index(aType)];
+
+ static Info invalid;
+ return invalid;
+}
+
+void ReplayGain::Set(Type aType, const Info& aInfo)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)] = aInfo;
+}
+
+void ReplayGain::ParseGain(Type aType, const std::string& aStrGain)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetGain(aStrGain);
+}
+
+void ReplayGain::SetGain(Type aType, float aGain)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetGain(aGain);
+}
+
+void ReplayGain::ParsePeak(Type aType, const std::string& aStrPeak)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetPeak(aStrPeak);
+}
+
+void ReplayGain::SetPeak(Type aType, float aPeak)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetPeak(aPeak);
+}
+
+std::string ReplayGain::Get() const
+{
+ if (!Get(ALBUM).Valid() && !Get(TRACK).Valid())
+ return std::string();
+
+ std::string rg;
+ if (Get(ALBUM).Valid())
+ rg = StringUtils::Format("{:.3f},{:.3f},", Get(ALBUM).Gain(), Get(ALBUM).Peak());
+ else
+ rg = "-1000, -1,";
+ if (Get(TRACK).Valid())
+ rg += StringUtils::Format("{:.3f},{:.3f}", Get(TRACK).Gain(), Get(TRACK).Peak());
+ else
+ rg += "-1000, -1";
+ return rg;
+}
+
+void ReplayGain::Set(const std::string& strReplayGain)
+{
+ std::vector<std::string> values = StringUtils::Split(strReplayGain, ",");
+ if (values.size() == 4)
+ {
+ ParseGain(ALBUM, values[0]);
+ ParsePeak(ALBUM, values[1]);
+ ParseGain(TRACK, values[2]);
+ ParsePeak(TRACK, values[3]);
+ }
+}
+
+///////////////////////////////////////////////////////////////
+// class ReplayGain::Info
+///////////////////////////////////////////////////////////////
+
+void ReplayGain::Info::SetGain(float aGain)
+{
+ m_gain = aGain;
+}
+
+void ReplayGain::Info::SetGain(const std::string& aStrGain)
+{
+ SetGain(static_cast<float>(atof(aStrGain.c_str())));
+}
+
+float ReplayGain::Info::Gain() const
+{
+ return m_gain;
+}
+
+void ReplayGain::Info::SetPeak(const std::string& aStrPeak)
+{
+ SetPeak(static_cast<float>(atof(aStrPeak.c_str())));
+}
+
+void ReplayGain::Info::SetPeak(float aPeak)
+{
+ m_peak = aPeak;
+}
+
+float ReplayGain::Info::Peak() const
+{
+ return m_peak;
+}
+
+bool ReplayGain::Info::HasGain() const
+{
+ return m_gain != REPLAY_GAIN_NO_GAIN;
+}
+
+bool ReplayGain::Info::HasPeak() const
+{
+ return m_peak != REPLAY_GAIN_NO_PEAK;
+}
+
+bool ReplayGain::Info::Valid() const
+{
+ return HasPeak() && HasGain();
+}
diff --git a/xbmc/music/tags/ReplayGain.h b/xbmc/music/tags/ReplayGain.h
new file mode 100644
index 0000000..efcd784
--- /dev/null
+++ b/xbmc/music/tags/ReplayGain.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2005-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 <string>
+
+#define REPLAY_GAIN_NO_PEAK -1.0f
+#define REPLAY_GAIN_NO_GAIN -1000.0f
+
+class ReplayGain
+{
+public:
+ enum Type {
+ NONE = 0,
+ ALBUM,
+ TRACK
+ };
+public:
+ class Info
+ {
+ public:
+ void SetGain(float aGain);
+ void SetGain(const std::string& aStrGain);
+ float Gain() const;
+ void SetPeak(const std::string& aStrPeak);
+ void SetPeak(float aPeak);
+ float Peak() const;
+ bool HasGain() const;
+ bool HasPeak() const;
+ bool Valid() const;
+ private:
+ float m_gain = REPLAY_GAIN_NO_GAIN; // measured in milliBels
+ float m_peak = REPLAY_GAIN_NO_PEAK; // 1.0 == full digital scale
+ };
+ const Info& Get(Type aType) const;
+ void Set(Type aType, const Info& aInfo);
+ void ParseGain(Type aType, const std::string& aStrGain);
+ void SetGain(Type aType, float aGain);
+ void ParsePeak(Type aType, const std::string& aStrPeak);
+ void SetPeak(Type aType, float aPeak);
+ std::string Get() const;
+ void Set(const std::string& strReplayGain);
+private:
+ Info m_data[TRACK];
+};
diff --git a/xbmc/music/tags/TagLibVFSStream.cpp b/xbmc/music/tags/TagLibVFSStream.cpp
new file mode 100644
index 0000000..1d2b454
--- /dev/null
+++ b/xbmc/music/tags/TagLibVFSStream.cpp
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2005-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 "TagLibVFSStream.h"
+
+#include "filesystem/File.h"
+
+#include <limits.h>
+
+#include <taglib/tiostream.h>
+
+using namespace XFILE;
+using namespace TagLib;
+using namespace MUSIC_INFO;
+
+/*!
+ * Construct a File object and opens the \a file. \a file should be a
+ * be an XBMC Vfile.
+ */
+TagLibVFSStream::TagLibVFSStream(const std::string& strFileName, bool readOnly)
+{
+ m_bIsOpen = true;
+ if (readOnly)
+ {
+ if (!m_file.Open(strFileName))
+ m_bIsOpen = false;
+ }
+ else
+ {
+ if (!m_file.OpenForWrite(strFileName))
+ m_bIsOpen = false;
+ }
+ m_strFileName = strFileName;
+ m_bIsReadOnly = readOnly || !m_bIsOpen;
+}
+
+/*!
+ * Destroys this ByteVectorStream instance.
+ */
+TagLibVFSStream::~TagLibVFSStream()
+{
+ m_file.Close();
+}
+
+/*!
+ * Returns the file name in the local file system encoding.
+ */
+FileName TagLibVFSStream::name() const
+{
+ return m_strFileName.c_str();
+}
+
+/*!
+ * Reads a block of size \a length at the current get pointer.
+ */
+ByteVector TagLibVFSStream::readBlock(TagLib::ulong length)
+{
+ ByteVector byteVector(static_cast<TagLib::uint>(length));
+ ssize_t read = m_file.Read(byteVector.data(), length);
+ if (read > 0)
+ byteVector.resize(read);
+ else
+ byteVector.clear();
+
+ return byteVector;
+}
+
+/*!
+ * Attempts to write the block \a data at the current get pointer. If the
+ * file is currently only opened read only -- i.e. readOnly() returns true --
+ * this attempts to reopen the file in read/write mode.
+ *
+ * \note This should be used instead of using the streaming output operator
+ * for a ByteVector. And even this function is significantly slower than
+ * doing output with a char[].
+ */
+void TagLibVFSStream::writeBlock(const ByteVector &data)
+{
+ m_file.Write(data.data(), data.size());
+}
+
+/*!
+ * Insert \a data at position \a start in the file overwriting \a replace
+ * bytes of the original content.
+ *
+ * \note This method is slow since it requires rewriting all of the file
+ * after the insertion point.
+ */
+void TagLibVFSStream::insert(const ByteVector &data, TagLib::ulong start, TagLib::ulong replace)
+{
+ if (data.size() == replace)
+ {
+ seek(start);
+ writeBlock(data);
+ return;
+ }
+ else if (data.size() < replace)
+ {
+ seek(start);
+ writeBlock(data);
+ removeBlock(start + data.size(), replace - data.size());
+ }
+
+ // Woohoo! Faster (about 20%) than id3lib at last. I had to get hardcore
+ // and avoid TagLib's high level API for rendering just copying parts of
+ // the file that don't contain tag data.
+ //
+ // Now I'll explain the steps in this ugliness:
+
+ // First, make sure that we're working with a buffer that is longer than
+ // the *difference* in the tag sizes. We want to avoid overwriting parts
+ // that aren't yet in memory, so this is necessary.
+ TagLib::ulong bufferLength = bufferSize();
+
+ while (data.size() - replace > bufferLength)
+ bufferLength += bufferSize();
+
+ // Set where to start the reading and writing.
+ long readPosition = start + replace;
+ long writePosition = start;
+ ByteVector buffer;
+ ByteVector aboutToOverwrite(static_cast<TagLib::uint>(bufferLength));
+
+ // This is basically a special case of the loop below. Here we're just
+ // doing the same steps as below, but since we aren't using the same buffer
+ // size -- instead we're using the tag size -- this has to be handled as a
+ // special case. We're also using File::writeBlock() just for the tag.
+ // That's a bit slower than using char *'s so, we're only doing it here.
+ seek(readPosition);
+ ssize_t bytesRead = m_file.Read(aboutToOverwrite.data(), bufferLength);
+ if (bytesRead <= 0)
+ return; // error
+ readPosition += bufferLength;
+
+ seek(writePosition);
+ writeBlock(data);
+ writePosition += data.size();
+
+ buffer = aboutToOverwrite;
+ buffer.resize(bytesRead);
+
+ // Ok, here's the main loop. We want to loop until the read fails, which
+ // means that we hit the end of the file.
+ while (!buffer.isEmpty())
+ {
+ // Seek to the current read position and read the data that we're about
+ // to overwrite. Appropriately increment the readPosition.
+ seek(readPosition);
+ bytesRead = m_file.Read(aboutToOverwrite.data(), bufferLength);
+ if (bytesRead <= 0)
+ return; // error
+ aboutToOverwrite.resize(bytesRead);
+ readPosition += bufferLength;
+
+ // Check to see if we just read the last block. We need to call clear()
+ // if we did so that the last write succeeds.
+ if (TagLib::ulong(bytesRead) < bufferLength)
+ clear();
+
+ // Seek to the write position and write our buffer. Increment the
+ // writePosition.
+ seek(writePosition);
+ if (m_file.Write(buffer.data(), buffer.size()) < static_cast<ssize_t>(buffer.size()))
+ return; // error
+ writePosition += buffer.size();
+
+ buffer = aboutToOverwrite;
+ bufferLength = bytesRead;
+ }
+}
+
+/*!
+ * Removes a block of the file starting a \a start and continuing for
+ * \a length bytes.
+ *
+ * \note This method is slow since it involves rewriting all of the file
+ * after the removed portion.
+ */
+void TagLibVFSStream::removeBlock(TagLib::ulong start, TagLib::ulong length)
+{
+ TagLib::ulong bufferLength = bufferSize();
+
+ long readPosition = start + length;
+ long writePosition = start;
+
+ ByteVector buffer(static_cast<TagLib::uint>(bufferLength));
+
+ TagLib::ulong bytesRead = 1;
+
+ while(bytesRead != 0)
+ {
+ seek(readPosition);
+ ssize_t read = m_file.Read(buffer.data(), bufferLength);
+ if (read < 0)
+ return;// explicit error
+
+ bytesRead = static_cast<TagLib::ulong>(read);
+ readPosition += bytesRead;
+
+ // Check to see if we just read the last block. We need to call clear()
+ // if we did so that the last write succeeds.
+ if(bytesRead < bufferLength)
+ clear();
+
+ seek(writePosition);
+ if (m_file.Write(buffer.data(), bytesRead) != static_cast<ssize_t>(bytesRead))
+ return; // error
+ writePosition += bytesRead;
+ }
+ truncate(writePosition);
+}
+
+/*!
+ * Returns true if the file is read only (or if the file can not be opened).
+ */
+bool TagLibVFSStream::readOnly() const
+{
+ return m_bIsReadOnly;
+}
+
+/*!
+ * Since the file can currently only be opened as an argument to the
+ * constructor (sort-of by design), this returns if that open succeeded.
+ */
+bool TagLibVFSStream::isOpen() const
+{
+ return m_bIsOpen;
+}
+
+/*!
+ * Move the I/O pointer to \a offset in the file from position \a p. This
+ * defaults to seeking from the beginning of the file.
+ *
+ * \see Position
+ */
+void TagLibVFSStream::seek(long offset, Position p)
+{
+ const long fileLen = length();
+ if (m_bIsReadOnly && fileLen > 0)
+ {
+ long startPos;
+ if (p == Beginning)
+ startPos = 0;
+ else if (p == Current)
+ startPos = tell();
+ else if (p == End)
+ startPos = fileLen;
+ else
+ return; // wrong Position value
+
+ // When parsing some broken files, taglib may try to seek above end of file.
+ // If underlying VFS does not move I/O pointer in this case, taglib will parse
+ // same part of file several times and ends with error. To prevent this
+ // situation, force seek to last valid position so VFS move I/O pointer.
+ if (startPos >= 0)
+ {
+ if (offset < 0 && startPos + offset < 0)
+ {
+ m_file.Seek(0, SEEK_SET);
+ return;
+ }
+ if (offset > 0 && startPos + offset > fileLen)
+ {
+ m_file.Seek(fileLen, SEEK_SET);
+ return;
+ }
+ }
+ }
+
+ switch(p)
+ {
+ case Beginning:
+ m_file.Seek(offset, SEEK_SET);
+ break;
+ case Current:
+ m_file.Seek(offset, SEEK_CUR);
+ break;
+ case End:
+ m_file.Seek(offset, SEEK_END);
+ break;
+ }
+}
+
+/*!
+ * Reset the end-of-file and error flags on the file.
+ */
+void TagLibVFSStream::clear()
+{
+}
+
+/*!
+ * Returns the current offset within the file.
+ */
+long TagLibVFSStream::tell() const
+{
+ int64_t pos = m_file.GetPosition();
+ if(pos > LONG_MAX)
+ return -1;
+ else
+ return (long)pos;
+}
+
+/*!
+ * Returns the length of the file.
+ */
+long TagLibVFSStream::length()
+{
+ return (long)m_file.GetLength();
+}
+
+/*!
+ * Truncates the file to a \a length.
+ */
+void TagLibVFSStream::truncate(long length)
+{
+ m_file.Truncate(length);
+}
diff --git a/xbmc/music/tags/TagLibVFSStream.h b/xbmc/music/tags/TagLibVFSStream.h
new file mode 100644
index 0000000..d567055
--- /dev/null
+++ b/xbmc/music/tags/TagLibVFSStream.h
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2005-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 "filesystem/File.h"
+
+#include <taglib/tiostream.h>
+
+namespace MUSIC_INFO
+{
+ class TagLibVFSStream : public TagLib::IOStream
+ {
+ public:
+ /*!
+ * Construct a File object and opens the \a file. \a file should be a
+ * be an XBMC Vfile.
+ */
+ TagLibVFSStream(const std::string& strFileName, bool readOnly);
+
+ /*!
+ * Destroys this ByteVectorStream instance.
+ */
+ ~TagLibVFSStream() override;
+
+ /*!
+ * Returns the file name in the local file system encoding.
+ */
+ TagLib::FileName name() const override;
+
+ /*!
+ * Reads a block of size \a length at the current get pointer.
+ */
+ TagLib::ByteVector readBlock(TagLib::ulong length) override;
+
+ /*!
+ * Attempts to write the block \a data at the current get pointer. If the
+ * file is currently only opened read only -- i.e. readOnly() returns true --
+ * this attempts to reopen the file in read/write mode.
+ *
+ * \note This should be used instead of using the streaming output operator
+ * for a ByteVector. And even this function is significantly slower than
+ * doing output with a char[].
+ */
+ void writeBlock(const TagLib::ByteVector &data) override;
+
+ /*!
+ * Insert \a data at position \a start in the file overwriting \a replace
+ * bytes of the original content.
+ *
+ * \note This method is slow since it requires rewriting all of the file
+ * after the insertion point.
+ */
+ void insert(const TagLib::ByteVector &data, TagLib::ulong start = 0, TagLib::ulong replace = 0) override;
+
+ /*!
+ * Removes a block of the file starting a \a start and continuing for
+ * \a length bytes.
+ *
+ * \note This method is slow since it involves rewriting all of the file
+ * after the removed portion.
+ */
+ void removeBlock(TagLib::ulong start = 0, TagLib::ulong length = 0) override;
+
+ /*!
+ * Returns true if the file is read only (or if the file can not be opened).
+ */
+ bool readOnly() const override;
+
+ /*!
+ * Since the file can currently only be opened as an argument to the
+ * constructor (sort-of by design), this returns if that open succeeded.
+ */
+ bool isOpen() const override;
+
+ /*!
+ * Move the I/O pointer to \a offset in the file from position \a p. This
+ * defaults to seeking from the beginning of the file.
+ *
+ * \see Position
+ */
+ void seek(long offset, TagLib::IOStream::Position p = Beginning) override;
+
+ /*!
+ * Reset the end-of-file and error flags on the file.
+ */
+ void clear() override;
+
+ /*!
+ * Returns the current offset within the file.
+ */
+ long tell() const override;
+
+ /*!
+ * Returns the length of the file.
+ */
+ long length() override;
+
+ /*!
+ * Truncates the file to a \a length.
+ */
+ void truncate(long length) override;
+
+ protected:
+ /*!
+ * Returns the buffer size that is used for internal buffering.
+ */
+ static TagLib::uint bufferSize() { return 1024; }
+
+ private:
+ std::string m_strFileName;
+ XFILE::CFile m_file;
+ bool m_bIsReadOnly;
+ bool m_bIsOpen;
+ };
+}
+
diff --git a/xbmc/music/tags/TagLoaderTagLib.cpp b/xbmc/music/tags/TagLoaderTagLib.cpp
new file mode 100644
index 0000000..6eb591a
--- /dev/null
+++ b/xbmc/music/tags/TagLoaderTagLib.cpp
@@ -0,0 +1,1371 @@
+/*
+ * Copyright (C) 2005-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 "TagLoaderTagLib.h"
+
+#include <vector>
+
+#include <taglib/id3v1tag.h>
+#include <taglib/apetag.h>
+#include <taglib/asftag.h>
+#include <taglib/id3v1genres.h>
+#include <taglib/aifffile.h>
+#include <taglib/apefile.h>
+#include <taglib/asffile.h>
+#include <taglib/modfile.h>
+#include <taglib/mp4file.h>
+#include <taglib/mpegfile.h>
+#include <taglib/oggfile.h>
+#include <taglib/oggflacfile.h>
+#include <taglib/opusfile.h>
+#include <taglib/rifffile.h>
+#include <taglib/speexfile.h>
+#include <taglib/s3mfile.h>
+#include <taglib/trueaudiofile.h>
+#include <taglib/vorbisfile.h>
+#include <taglib/wavfile.h>
+#include <taglib/wavpackfile.h>
+#include <taglib/xmfile.h>
+#include <taglib/flacfile.h>
+#include <taglib/itfile.h>
+#include <taglib/mpcfile.h>
+#include <taglib/id3v2tag.h>
+#include <taglib/xiphcomment.h>
+#include <taglib/mp4tag.h>
+
+#include <taglib/textidentificationframe.h>
+#include <taglib/uniquefileidentifierframe.h>
+#include <taglib/popularimeterframe.h>
+#include <taglib/commentsframe.h>
+#include <taglib/unsynchronizedlyricsframe.h>
+#include <taglib/attachedpictureframe.h>
+
+#include <taglib/tstring.h>
+#include <taglib/tpropertymap.h>
+
+#include "TagLibVFSStream.h"
+#include "MusicInfoTag.h"
+#include "ReplayGain.h"
+#include "utils/RegExp.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "utils/StringUtils.h"
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+#include "utils/Base64.h"
+#endif
+
+using namespace TagLib;
+using namespace MUSIC_INFO;
+
+namespace
+{
+std::vector<std::string> StringListToVectorString(const StringList& stringList)
+{
+ std::vector<std::string> values;
+ for (const auto& it: stringList)
+ values.push_back(it.to8Bit(true));
+ return values;
+}
+
+std::vector<std::string> GetASFStringList(const List<ASF::Attribute>& list)
+{
+ std::vector<std::string> values;
+ for (const auto& at: list)
+ values.push_back(at.toString().to8Bit(true));
+ return values;
+}
+
+std::vector<std::string> GetID3v2StringList(const ID3v2::FrameList& frameList)
+{
+ auto frame = dynamic_cast<const ID3v2::TextIdentificationFrame *>(frameList.front());
+ if (frame)
+ return StringListToVectorString(frame->fieldList());
+ return std::vector<std::string>();
+}
+
+void SetFlacArt(FLAC::File *flacFile, EmbeddedArt *art, CMusicInfoTag &tag)
+{
+ FLAC::Picture *cover[2] = {};
+ auto pictures = flacFile->pictureList();
+ for (List<FLAC::Picture *>::ConstIterator i = pictures.begin(); i != pictures.end(); ++i)
+ {
+ FLAC::Picture *picture = *i;
+ if (picture->type() == FLAC::Picture::FrontCover)
+ cover[0] = picture;
+ else // anything else is taken as second priority
+ cover[1] = picture;
+ }
+ for (const FLAC::Picture* const c : cover)
+ {
+ if (c)
+ {
+ tag.SetCoverArtInfo(c->data().size(), c->mimeType().to8Bit(true));
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(c->data().data()), c->data().size(), c->mimeType().to8Bit(true));
+ return; // one is enough
+ }
+ }
+}
+}
+
+bool CTagLoaderTagLib::Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag, EmbeddedArt *art /* = NULL */)
+{
+ return Load(strFileName, tag, "", art);
+}
+
+
+template<>
+bool CTagLoaderTagLib::ParseTag(ASF::Tag *asf, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!asf)
+ return false;
+
+ ReplayGain replayGainInfo;
+ tag.SetTitle(asf->title().to8Bit(true));
+ const ASF::AttributeListMap& attributeListMap = asf->attributeListMap();
+ for (ASF::AttributeListMap::ConstIterator it = attributeListMap.begin(); it != attributeListMap.end(); ++it)
+ {
+ if (it->first == "Author")
+ SetArtist(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/ArtistSortOrder")
+ SetArtistSort(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/AlbumArtist")
+ SetAlbumArtist(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/AlbumArtistSortOrder")
+ SetAlbumArtistSort(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/ComposerSortOrder")
+ SetComposerSort(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/AlbumTitle")
+ tag.SetAlbum(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/TrackNumber" ||
+ it->first == "WM/Track")
+ {
+ if (it->second.front().type() == ASF::Attribute::DWordType)
+ tag.SetTrackNumber(it->second.front().toUInt());
+ else
+ tag.SetTrackNumber(atoi(it->second.front().toString().toCString(true)));
+ }
+ else if (it->first == "WM/PartOfSet")
+ tag.SetDiscNumber(atoi(it->second.front().toString().toCString(true)));
+ else if (it->first == "WM/Genre")
+ SetGenre(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/Mood")
+ tag.SetMood(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/Composer")
+ AddArtistRole(tag, "Composer", GetASFStringList(it->second));
+ else if (it->first == "WM/Conductor")
+ AddArtistRole(tag, "Conductor", GetASFStringList(it->second));
+ //No ASF/WMA tag from Taglib for "ensemble"
+ else if (it->first == "WM/Writer")
+ AddArtistRole(tag, "Lyricist", GetASFStringList(it->second));
+ else if (it->first == "WM/ModifiedBy")
+ AddArtistRole(tag, "Remixer", GetASFStringList(it->second));
+ else if (it->first == "WM/Engineer")
+ AddArtistRole(tag, "Engineer", GetASFStringList(it->second));
+ else if (it->first == "WM/Producer")
+ AddArtistRole(tag, "Producer", GetASFStringList(it->second));
+ else if (it->first == "WM/DJMixer")
+ AddArtistRole(tag, "DJMixer", GetASFStringList(it->second));
+ else if (it->first == "WM/Mixer")
+ AddArtistRole(tag, "mixer", GetASFStringList(it->second));
+ else if (it->first == "WM/Publisher")
+ tag.SetRecordLabel(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/Script")
+ {} // Known unsupported, suppress warnings
+ else if (it->first == "WM/Year")
+ tag.SetReleaseDate(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/OriginalReleaseYear")
+ tag.SetOriginalDate(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/SetSubTitle")
+ tag.SetDiscSubtitle(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Artist Id")
+ tag.SetMusicBrainzArtistID(SplitMBID(GetASFStringList(it->second)));
+ else if (it->first == "MusicBrainz/Album Id")
+ tag.SetMusicBrainzAlbumID(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Release Group Id")
+ tag.SetMusicBrainzReleaseGroupID(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Album Artist")
+ SetAlbumArtist(tag, GetASFStringList(it->second));
+ else if (it->first == "MusicBrainz/Album Artist Id")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(GetASFStringList(it->second)));
+ else if (it->first == "MusicBrainz/Track Id")
+ tag.SetMusicBrainzTrackID(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Album Status")
+ tag.SetAlbumReleaseStatus(it->second.front().toString().toCString(true));
+ else if (it->first == "MusicBrainz/Album Type")
+ SetReleaseType(tag, GetASFStringList(it->second));
+ else if (it->first == "MusicIP/PUID")
+ {}
+ else if (it->first == "WM/BeatsPerMinute")
+ tag.SetBPM(atoi(it->second.front().toString().toCString(true)));
+ else if (it->first == "replaygain_track_gain" || it->first == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toString().toCString(true));
+ else if (it->first == "replaygain_album_gain" || it->first == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toString().toCString(true));
+ else if (it->first == "replaygain_track_peak" || it->first == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toString().toCString(true));
+ else if (it->first == "replaygain_album_peak" || it->first == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toString().toCString(true));
+ else if (it->first == "WM/Picture")
+ { // picture
+ ASF::Picture pic = it->second.front().toPicture();
+ tag.SetCoverArtInfo(pic.picture().size(), pic.mimeType().toCString());
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t *>(pic.picture().data()), pic.picture().size(), pic.mimeType().toCString());
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized ASF tag name: {}", it->first.toCString(true));
+ }
+ // artist may be specified in the ContentDescription block rather than using the 'Author' attribute.
+ if (tag.GetArtist().empty())
+ tag.SetArtist(asf->artist().toCString(true));
+
+ if (!asf->comment().isEmpty())
+ tag.SetComment(asf->comment().toCString(true));
+ tag.SetReplayGain(replayGainInfo);
+ tag.SetLoaded(true);
+ return true;
+}
+
+int CTagLoaderTagLib::POPMtoXBMC(int popm)
+{
+ // Ratings:
+ // FROM: http://www.mediamonkey.com/forum/viewtopic.php?f=7&t=40532&start=30#p391067
+ // The following schemes are used by the other POPM-compatible players:
+ // WMP/Vista: "Windows Media Player 9 Series" ratings:
+ // 1 = 1, 2 = 64, 3=128, 4=196 (not 192), 5=255
+ // MediaMonkey (v4.2.1): "no@email" ratings:
+ // 0.5=13, 1=1, 1.5=54, 2=64, 2.5=118,
+ // 3=128, 3.5=186, 4=196, 4.5=242, 5=255
+ // Note 1 star written as 1 while half a star is 13, a higher value
+ // Accommodate these mapped values in a scale from 0-255
+ if (popm == 0) return 0;
+ if (popm == 1) return 2;
+ if (popm < 23) return 1;
+ if (popm < 32) return 2;
+ if (popm < 64) return 3;
+ if (popm < 96) return 4;
+ if (popm < 128) return 5;
+ if (popm < 160) return 6;
+ if (popm < 196) return 7;
+ if (popm < 224) return 8;
+ if (popm < 255) return 9;
+ else return 10;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(ID3v1::Tag *id3v1, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!id3v1) return false;
+ tag.SetTitle(id3v1->title().to8Bit(true));
+ tag.SetArtist(id3v1->artist().to8Bit(true));
+ tag.SetAlbum(id3v1->album().to8Bit(true));
+ tag.SetComment(id3v1->comment().to8Bit(true));
+ tag.SetGenre(id3v1->genre().to8Bit(true), true);
+ tag.SetYear(id3v1->year());
+ tag.SetTrackNumber(id3v1->track());
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(ID3v2::Tag *id3v2, EmbeddedArt *art, MUSIC_INFO::CMusicInfoTag& tag)
+{
+ if (!id3v2) return false;
+ ReplayGain replayGainInfo;
+
+ ID3v2::AttachedPictureFrame *pictures[3] = {};
+ const ID3v2::FrameListMap& frameListMap = id3v2->frameListMap();
+ for (ID3v2::FrameListMap::ConstIterator it = frameListMap.begin(); it != frameListMap.end(); ++it)
+ {
+ // It is possible that the taglist is empty. In that case no useable values can be extracted.
+ // and we should skip the tag.
+ if (it->second.isEmpty()) continue;
+
+ if (it->first == "TPE1") SetArtist(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TSOP") SetArtistSort(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TALB") tag.SetAlbum(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TPE2") SetAlbumArtist(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TSO2") SetAlbumArtistSort(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TSOC") SetComposerSort(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TIT2") tag.SetTitle(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TCON") SetGenre(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TRCK")
+ tag.SetTrackNumber(
+ static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10)));
+ else if (it->first == "TPOS")
+ tag.SetDiscNumber(
+ static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10)));
+ else if (it->first == "TDOR" || it->first == "TORY") // TDOR - ID3v2.4, TORY - ID3v2.3
+ tag.SetOriginalDate(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TDAT") {} // empty as taglib has moved the value to TDRC
+ else if (it->first == "TCMP") tag.SetCompilation((strtol(it->second.front()->toString().toCString(true), nullptr, 10) == 0) ? false : true);
+ else if (it->first == "TENC") {} // EncodedBy
+ else if (it->first == "TCOM") AddArtistRole(tag, "Composer", GetID3v2StringList(it->second));
+ else if (it->first == "TPE3") AddArtistRole(tag, "Conductor", GetID3v2StringList(it->second));
+ else if (it->first == "TEXT") AddArtistRole(tag, "Lyricist", GetID3v2StringList(it->second));
+ else if (it->first == "TPE4") AddArtistRole(tag, "Remixer", GetID3v2StringList(it->second));
+ else if (it->first == "TPUB") tag.SetRecordLabel(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TCOP") {} // Copyright message
+ else if (it->first == "TDRC") // taglib concatenates TYER & TDAT into this field if v2.3
+ tag.SetReleaseDate(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TDRL") {} // Not set by Picard or used in community generally
+ else if (it->first == "TDTG") {} // Tagging time
+ else if (it->first == "TLAN") {} // Languages
+ else if (it->first == "TMOO") tag.SetMood(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TSST")
+ tag.SetDiscSubtitle(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TBPM")
+ tag.SetBPM(
+ static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10)));
+ else if (it->first == "USLT")
+ // Loop through any lyrics frames. Could there be multiple frames, how to choose?
+ for (ID3v2::FrameList::ConstIterator lt = it->second.begin(); lt != it->second.end(); ++lt)
+ {
+ auto lyricsFrame = dynamic_cast<ID3v2::UnsynchronizedLyricsFrame *> (*lt);
+ if (lyricsFrame)
+ tag.SetLyrics(lyricsFrame->text().to8Bit(true));
+ }
+ else if (it->first == "COMM")
+ // Loop through and look for the main (no description) comment
+ for (ID3v2::FrameList::ConstIterator ct = it->second.begin(); ct != it->second.end(); ++ct)
+ {
+ ID3v2::CommentsFrame *commentsFrame = dynamic_cast<ID3v2::CommentsFrame *> (*ct);
+ if (commentsFrame && commentsFrame->description().isEmpty())
+ tag.SetComment(commentsFrame->text().to8Bit(true));
+ }
+ else if (it->first == "TXXX")
+ // Loop through and process the UserTextIdentificationFrames
+ for (ID3v2::FrameList::ConstIterator ut = it->second.begin(); ut != it->second.end(); ++ut)
+ {
+ ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<ID3v2::UserTextIdentificationFrame *> (*ut);
+ if (!frame) continue;
+
+ // First field is the same as the description
+ StringList stringList = frame->fieldList();
+ if (stringList.size() == 1) continue;
+ stringList.erase(stringList.begin());
+ String desc = frame->description().upper();
+ if (desc == "MUSICBRAINZ ARTIST ID")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(stringList)));
+ else if (desc == "MUSICBRAINZ ALBUM ID")
+ tag.SetMusicBrainzAlbumID(stringList.front().to8Bit(true));
+ else if (desc == "MUSICBRAINZ RELEASE GROUP ID")
+ tag.SetMusicBrainzReleaseGroupID(stringList.front().to8Bit(true));
+ else if (desc == "MUSICBRAINZ ALBUM ARTIST ID")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(stringList)));
+ else if (desc == "MUSICBRAINZ ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(stringList));
+ else if (desc == "MUSICBRAINZ ALBUM TYPE")
+ SetReleaseType(tag, StringListToVectorString(stringList));
+ else if (desc == "MUSICBRAINZ ALBUM STATUS")
+ tag.SetAlbumReleaseStatus(stringList.front().to8Bit(true));
+ else if (desc == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, stringList.front().toCString(true));
+ else if (desc == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, stringList.front().toCString(true));
+ else if (desc == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, stringList.front().toCString(true));
+ else if (desc == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, stringList.front().toCString(true));
+ else if (desc == "ALBUMARTIST" || desc == "ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(stringList));
+ else if (desc == "ALBUMARTISTSORT" || desc == "ALBUM ARTIST SORT")
+ SetAlbumArtistSort(tag, StringListToVectorString(stringList));
+ else if (desc == "ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(stringList));
+ else if (desc == "ALBUMARTISTS" || desc == "ALBUM ARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(stringList));
+ else if (desc == "WRITER") // How Picard >1.3 tags writer in ID3
+ AddArtistRole(tag, "Writer", StringListToVectorString(stringList));
+ else if (desc == "COMPOSERSORT" || desc == "COMPOSER SORT")
+ SetComposerSort(tag, StringListToVectorString(stringList));
+ else if (desc == "MOOD")
+ tag.SetMood(stringList.front().to8Bit(true));
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized user text tag detected: TXXX:{}",
+ frame->description().toCString(true));
+ }
+ else if (it->first == "TIPL")
+ // Loop through and process the involved people list
+ // For example Arranger, Engineer, Producer, DJMixer or Mixer
+ // In fieldlist every odd field is a function, and every even is an artist or a comma delimited list of artists.
+ for (ID3v2::FrameList::ConstIterator ip = it->second.begin(); ip != it->second.end(); ++ip)
+ {
+ auto tiplframe = dynamic_cast<ID3v2::TextIdentificationFrame*> (*ip);
+ if (tiplframe)
+ AddArtistRole(tag, StringListToVectorString(tiplframe->fieldList()));
+ }
+ else if (it->first == "TMCL")
+ // Loop through and process the musician credits list
+ // It is a mapping between the instrument and the person that played it, but also includes "orchestra" or "soloist".
+ // In fieldlist every odd field is an instrument, and every even is an artist or a comma delimited list of artists.
+ for (ID3v2::FrameList::ConstIterator ip = it->second.begin(); ip != it->second.end(); ++ip)
+ {
+ auto tiplframe = dynamic_cast<ID3v2::TextIdentificationFrame*> (*ip);
+ if (tiplframe)
+ AddArtistRole(tag, StringListToVectorString(tiplframe->fieldList()));
+ }
+ else if (it->first == "UFID")
+ // Loop through any UFID frames and set them
+ for (ID3v2::FrameList::ConstIterator ut = it->second.begin(); ut != it->second.end(); ++ut)
+ {
+ auto ufid = dynamic_cast<ID3v2::UniqueFileIdentifierFrame*> (*ut);
+ if (ufid && ufid->owner() == "http://musicbrainz.org")
+ {
+ // MusicBrainz pads with a \0, but the spec requires binary, be cautious
+ char cUfid[64];
+ int max_size = std::min(static_cast<int>(ufid->identifier().size()), 63);
+ strncpy(cUfid, ufid->identifier().data(), max_size);
+ cUfid[max_size] = '\0';
+ tag.SetMusicBrainzTrackID(cUfid);
+ }
+ }
+ else if (it->first == "APIC")
+ // Loop through all pictures and store the frame pointers for the picture types we want
+ for (ID3v2::FrameList::ConstIterator pi = it->second.begin(); pi != it->second.end(); ++pi)
+ {
+ auto pictureFrame = dynamic_cast<ID3v2::AttachedPictureFrame *> (*pi);
+ if (!pictureFrame) continue;
+
+ if (pictureFrame->type() == ID3v2::AttachedPictureFrame::FrontCover) pictures[0] = pictureFrame;
+ else if (pictureFrame->type() == ID3v2::AttachedPictureFrame::Other) pictures[1] = pictureFrame;
+ else if (pi == it->second.begin()) pictures[2] = pictureFrame;
+ }
+ else if (it->first == "POPM")
+ // Loop through and process ratings
+ for (ID3v2::FrameList::ConstIterator ct = it->second.begin(); ct != it->second.end(); ++ct)
+ {
+ auto popFrame = dynamic_cast<ID3v2::PopularimeterFrame *> (*ct);
+ if (!popFrame) continue;
+
+ // @xbmc.org ratings trump others (of course)
+ if (popFrame->email() == "ratings@xbmc.org")
+ tag.SetUserrating(popFrame->rating() / 51); //! @todo wtf? Why 51 find some explanation, somewhere...
+ else if (tag.GetUserrating() == 0)
+ {
+ if (popFrame->email() != "Windows Media Player 9 Series" &&
+ popFrame->email() != "Banshee" &&
+ popFrame->email() != "no@email" &&
+ popFrame->email() != "quodlibet@lists.sacredchao.net" &&
+ popFrame->email() != "rating@winamp.com")
+ CLog::Log(LOGDEBUG, "unrecognized ratings schema detected: {}",
+ popFrame->email().toCString(true));
+ tag.SetUserrating(POPMtoXBMC(popFrame->rating()));
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized ID3 frame detected: {}{}{}{}", it->first[0], it->first[1],
+ it->first[2], it->first[3]);
+ } // for
+
+ // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = First Found picture
+ for (const ID3v2::AttachedPictureFrame* const picture : pictures)
+ if (picture)
+ {
+ std::string mime = picture->mimeType().to8Bit(true);
+ TagLib::uint size = picture->picture().size();
+ tag.SetCoverArtInfo(size, mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(picture->picture().data()), size, mime);
+
+ // Stop after we find the first picture for now.
+ break;
+ }
+
+
+ if (!id3v2->comment().isEmpty())
+ tag.SetComment(id3v2->comment().toCString(true));
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(APE::Tag *ape, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!ape)
+ return false;
+
+ ReplayGain replayGainInfo;
+ const APE::ItemListMap itemListMap = ape->itemListMap();
+ for (APE::ItemListMap::ConstIterator it = itemListMap.begin(); it != itemListMap.end(); ++it)
+ {
+ if (it->first == "ARTIST")
+ SetArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ARTISTSORT")
+ SetArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUMARTISTSORT")
+ SetAlbumArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "COMPOSERSORT")
+ SetComposerSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUM")
+ tag.SetAlbum(it->second.toString().to8Bit(true));
+ else if (it->first == "TITLE")
+ tag.SetTitle(it->second.toString().to8Bit(true));
+ else if (it->first == "TRACKNUMBER" || it->first == "TRACK")
+ tag.SetTrackNumber(it->second.toString().toInt());
+ else if (it->first == "DISCNUMBER" || it->first == "DISC")
+ tag.SetDiscNumber(it->second.toString().toInt());
+ else if (it->first == "YEAR")
+ tag.SetReleaseDate(it->second.toString().to8Bit(true));
+ else if (it->first == "DISCSUBTITLE")
+ tag.SetDiscSubtitle(it->second.toString().to8Bit(true));
+ else if (it->first == "ORIGINALYEAR")
+ tag.SetOriginalDate(it->second.toString().to8Bit(true));
+ else if (it->first == "GENRE")
+ SetGenre(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "MOOD")
+ tag.SetMood(it->second.toString().to8Bit(true));
+ else if (it->first == "COMMENT")
+ tag.SetComment(it->second.toString().to8Bit(true));
+ else if (it->first == "CUESHEET")
+ tag.SetCueSheet(it->second.toString().to8Bit(true));
+ else if (it->first == "ENCODEDBY")
+ {}
+ else if (it->first == "COMPOSER")
+ AddArtistRole(tag, "Composer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "CONDUCTOR")
+ AddArtistRole(tag, "Conductor", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "BAND")
+ AddArtistRole(tag, "Band", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ENSEMBLE")
+ AddArtistRole(tag, "Ensemble", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "LYRICIST")
+ AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "WRITER")
+ AddArtistRole(tag, "Writer", StringListToVectorString(it->second.toStringList()));
+ else if ((it->first == "MIXARTIST") || (it->first == "REMIXER"))
+ AddArtistRole(tag, "Remixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ARRANGER")
+ AddArtistRole(tag, "Arranger", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ENGINEER")
+ AddArtistRole(tag, "Engineer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "PRODUCER")
+ AddArtistRole(tag, "Producer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "DJMIXER")
+ AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "MIXER")
+ AddArtistRole(tag, "Mixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "PERFORMER")
+ // Picard uses PERFORMER tag as musician credits list formatted "name (instrument)"
+ AddArtistInstrument(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "LABEL")
+ tag.SetRecordLabel(it->second.toString().to8Bit(true));
+ else if (it->first == "COMPILATION")
+ tag.SetCompilation(it->second.toString().toInt() == 1);
+ else if (it->first == "LYRICS")
+ tag.SetLyrics(it->second.toString().to8Bit(true));
+ else if (it->first == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.toString().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.toString().toCString(true));
+ else if (it->first == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.toString().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.toString().toCString(true));
+ else if (it->first == "MUSICBRAINZ_ARTISTID")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTISTID")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "MUSICBRAINZ_ALBUMID")
+ tag.SetMusicBrainzAlbumID(it->second.toString().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_RELEASEGROUPID")
+ tag.SetMusicBrainzReleaseGroupID(it->second.toString().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_TRACKID")
+ tag.SetMusicBrainzTrackID(it->second.toString().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_ALBUMTYPE")
+ SetReleaseType(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "BPM")
+ tag.SetBPM(it->second.toString().toInt());
+ else if (it->first == "MUSICBRAINZ_ALBUMSTATUS")
+ tag.SetAlbumReleaseStatus(it->second.toString().to8Bit(true));
+ else if (it->first == "COVER ART (FRONT)")
+ {
+ TagLib::ByteVector tdata = it->second.binaryData();
+ // The image data follows a null byte, which can optionally be preceded by a filename
+ const uint offset = tdata.find('\0') + 1;
+ ByteVector bv(tdata.data() + offset, tdata.size() - offset);
+ // Infer the mimetype
+ std::string mime{};
+ if (bv.startsWith("\xFF\xD8\xFF"))
+ mime = "image/jpeg";
+ else if (bv.startsWith("\x89\x50\x4E\x47"))
+ mime = "image/png";
+ else if (bv.startsWith("\x47\x49\x46\x38"))
+ mime = "image/gif";
+ else if (bv.startsWith("\x42\x4D"))
+ mime = "image/bmp";
+ if ((offset > 0) && (offset <= tdata.size()) && (mime.size() > 0))
+ {
+ tag.SetCoverArtInfo(bv.size(), mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(bv.data()), bv.size(), mime);
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized APE tag: {}", it->first.toCString(true));
+ }
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(Ogg::XiphComment *xiph, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!xiph)
+ return false;
+
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+ FLAC::Picture pictures[3];
+#endif
+ ReplayGain replayGainInfo;
+
+ const Ogg::FieldListMap& fieldListMap = xiph->fieldListMap();
+ for (Ogg::FieldListMap::ConstIterator it = fieldListMap.begin(); it != fieldListMap.end(); ++it)
+ {
+ if (it->first == "ARTIST")
+ SetArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "ARTISTSORT")
+ SetArtistSort(tag, StringListToVectorString(it->second));
+ else if (it->first == "ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUMARTISTSORT" || it->first == "ALBUM ARTIST SORT")
+ SetAlbumArtistSort(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(it->second));
+ else if (it->first == "COMPOSERSORT")
+ SetComposerSort(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUM")
+ tag.SetAlbum(it->second.front().to8Bit(true));
+ else if (it->first == "TITLE")
+ tag.SetTitle(it->second.front().to8Bit(true));
+ else if (it->first == "TRACKNUMBER")
+ tag.SetTrackNumber(it->second.front().toInt());
+ else if (it->first == "DISCNUMBER")
+ tag.SetDiscNumber(it->second.front().toInt());
+ else if (it->first == "YEAR" || it->first == "DATE")
+ tag.AddReleaseDate(it->second.front().to8Bit(true));
+ else if (it->first == "GENRE")
+ SetGenre(tag, StringListToVectorString(it->second));
+ else if (it->first == "MOOD")
+ tag.SetMood(it->second.front().to8Bit(true));
+ else if (it->first == "COMMENT")
+ tag.SetComment(it->second.front().to8Bit(true));
+ else if (it->first == "ORIGINALYEAR" || it->first == "ORIGINALDATE")
+ tag.AddOriginalDate(it->second.front().to8Bit(true));
+ else if (it->first == "CUESHEET")
+ tag.SetCueSheet(it->second.front().to8Bit(true));
+ else if (it->first == "DISCSUBTITLE")
+ tag.SetDiscSubtitle(it->second.front().to8Bit(true));
+ else if (it->first == "ENCODEDBY")
+ {} // Known but unsupported, suppress warnings
+ else if (it->first == "COMPOSER")
+ AddArtistRole(tag, "Composer", StringListToVectorString(it->second));
+ else if (it->first == "CONDUCTOR")
+ AddArtistRole(tag, "Conductor", StringListToVectorString(it->second));
+ else if (it->first == "BAND")
+ AddArtistRole(tag, "Band", StringListToVectorString(it->second));
+ else if (it->first == "ENSEMBLE")
+ AddArtistRole(tag, "Ensemble", StringListToVectorString(it->second));
+ else if (it->first == "LYRICIST")
+ AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second));
+ else if (it->first == "WRITER")
+ AddArtistRole(tag, "Writer", StringListToVectorString(it->second));
+ else if ((it->first == "MIXARTIST") || (it->first == "REMIXER"))
+ AddArtistRole(tag, "Remixer", StringListToVectorString(it->second));
+ else if (it->first == "ARRANGER")
+ AddArtistRole(tag, "Arranger", StringListToVectorString(it->second));
+ else if (it->first == "ENGINEER")
+ AddArtistRole(tag, "Engineer", StringListToVectorString(it->second));
+ else if (it->first == "PRODUCER")
+ AddArtistRole(tag, "Producer", StringListToVectorString(it->second));
+ else if (it->first == "DJMIXER")
+ AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second));
+ else if (it->first == "MIXER")
+ AddArtistRole(tag, "Mixer", StringListToVectorString(it->second));
+ else if (it->first == "PERFORMER")
+ // Picard uses PERFORMER tag as musician credits list formatted "name (instrument)"
+ AddArtistInstrument(tag, StringListToVectorString(it->second));
+ else if (it->first == "LABEL")
+ tag.SetRecordLabel(it->second.front().to8Bit(true));
+ else if (it->first == "COMPILATION")
+ tag.SetCompilation(it->second.front().toInt() == 1);
+ else if (it->first == "LYRICS")
+ tag.SetLyrics(it->second.front().to8Bit(true));
+ else if (it->first == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toCString(true));
+ else if (it->first == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toCString(true));
+ else if (it->first == "MUSICBRAINZ_ARTISTID")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second)));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTISTID")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second)));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "MUSICBRAINZ_ALBUMID")
+ tag.SetMusicBrainzAlbumID(it->second.front().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_RELEASEGROUPID")
+ tag.SetMusicBrainzReleaseGroupID(it->second.front().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_TRACKID")
+ tag.SetMusicBrainzTrackID(it->second.front().to8Bit(true));
+ else if (it->first == "RELEASETYPE")
+ SetReleaseType(tag, StringListToVectorString(it->second));
+ else if (it->first == "BPM")
+ tag.SetBPM(strtol(it->second.front().toCString(true), nullptr, 10));
+ else if (it->first == "RELEASESTATUS")
+ tag.SetAlbumReleaseStatus(it->second.front().toCString(true));
+ else if (it->first == "RATING")
+ {
+ // Vorbis ratings are a mess because the standard forgot to mention anything about them.
+ // If you want to see how emotive the issue is and the varying standards, check here:
+ // http://forums.winamp.com/showthread.php?t=324512
+ // The most common standard in that thread seems to be a 0-100 scale for 1-5 stars.
+ // So, that's what we'll support for now.
+ int iUserrating = it->second.front().toInt();
+ if (iUserrating > 0 && iUserrating <= 100)
+ tag.SetUserrating((iUserrating / 10));
+ }
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+ else if (it->first == "METADATA_BLOCK_PICTURE")
+ {
+ const char* b64 = it->second.front().toCString();
+ std::string decoded_block = Base64::Decode(b64, it->second.front().size());
+ ByteVector bv(decoded_block.data(), decoded_block.size());
+ TagLib::FLAC::Picture* pictureFrame = new TagLib::FLAC::Picture(bv);
+
+ if (pictureFrame->type() == FLAC::Picture::FrontCover) pictures[0].parse(bv);
+ else if (pictureFrame->type() == FLAC::Picture::Other) pictures[1].parse(bv);
+
+ delete pictureFrame;
+ }
+ else if (it->first == "COVERART")
+ {
+ const char* b64 = it->second.front().toCString();
+ std::string decoded_block = Base64::Decode(b64, it->second.front().size());
+ ByteVector bv(decoded_block.data(), decoded_block.size());
+ pictures[2].setData(bv);
+ // Assume jpeg
+ if (pictures[2].mimeType().isEmpty())
+ pictures[2].setMimeType("image/jpeg");
+ }
+ else if (it->first == "COVERARTMIME")
+ {
+ pictures[2].setMimeType(it->second.front());
+ }
+#endif
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized XipComment name: {}", it->first.toCString(true));
+ }
+
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+ // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = COVERART/COVERARTMIME
+ for (int i = 0; i < 3; ++i)
+ if (pictures[i].data().size())
+ {
+ std::string mime = pictures[i].mimeType().toCString();
+ if (mime.compare(0, 6, "image/") != 0)
+ continue;
+ TagLib::uint size = pictures[i].data().size();
+ tag.SetCoverArtInfo(size, mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(pictures[i].data().data()), size, mime);
+
+ break;
+ }
+#else
+ auto pictureList = xiph->pictureList();
+ FLAC::Picture *cover[2] = {};
+
+ for (auto i: pictureList)
+ {
+ FLAC::Picture *picture = i;
+ if (picture->type() == FLAC::Picture::FrontCover)
+ cover[0] = picture;
+ else // anything else is taken as second priority
+ cover[1] = picture;
+ }
+ for (const FLAC::Picture* const c : cover)
+ {
+ if (c)
+ {
+ tag.SetCoverArtInfo(c->data().size(), c->mimeType().to8Bit(true));
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(c->data().data()), c->data().size(), c->mimeType().to8Bit(true));
+ break; // one is enough
+ }
+ }
+#endif
+
+ if (!xiph->comment().isEmpty())
+ tag.SetComment(xiph->comment().toCString(true));
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(MP4::Tag *mp4, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!mp4)
+ return false;
+
+ ReplayGain replayGainInfo;
+ const MP4::ItemMap itemMap = mp4->itemMap();
+ for (auto it = itemMap.begin(); it != itemMap.end(); ++it)
+ {
+ if (it->first == "\251nam")
+ tag.SetTitle(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "\251ART")
+ SetArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "soar")
+ SetArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "\251alb")
+ tag.SetAlbum(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "aART")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "soaa")
+ SetAlbumArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:albumartists" ||
+ it->first == "----:com.apple.iTunes:ALBUMARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "soco")
+ SetComposerSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "\251gen")
+ SetGenre(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MOOD")
+ tag.SetMood(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "\251cmt")
+ tag.SetComment(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "\251wrt")
+ AddArtistRole(tag, "Composer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:CONDUCTOR")
+ AddArtistRole(tag, "Conductor", StringListToVectorString(it->second.toStringList()));
+ //No MP4 standard tag for "ensemble"
+ else if (it->first == "----:com.apple.iTunes:LYRICIST")
+ AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:REMIXER")
+ AddArtistRole(tag, "Remixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:ENGINEER")
+ AddArtistRole(tag, "Engineer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:PRODUCER")
+ AddArtistRole(tag, "Producer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:DJMIXER")
+ AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MIXER")
+ AddArtistRole(tag, "Mixer", StringListToVectorString(it->second.toStringList()));
+ //No MP4 standard tag for musician credits
+ else if (it->first == "----:com.apple.iTunes:LABEL")
+ tag.SetRecordLabel(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:DISCSUBTITLE")
+ tag.SetDiscSubtitle(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "cpil")
+ tag.SetCompilation(it->second.toBool());
+ else if (it->first == "trkn")
+ tag.SetTrackNumber(it->second.toIntPair().first);
+ else if (it->first == "disk")
+ tag.SetDiscNumber(it->second.toIntPair().first);
+ else if (it->first == "\251day")
+ tag.SetReleaseDate(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:originaldate")
+ tag.SetOriginalDate(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:replaygain_track_gain" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:replaygain_album_gain" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:replaygain_track_peak" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:replaygain_album_peak" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Artist Id")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Artist Id")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Artist")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Id")
+ tag.SetMusicBrainzAlbumID(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Release Group Id")
+ tag.SetMusicBrainzReleaseGroupID(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Track Id")
+ tag.SetMusicBrainzTrackID(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Type")
+ SetReleaseType(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Status")
+ tag.SetAlbumReleaseStatus(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "tmpo")
+ tag.SetBPM(it->second.toIntPair().first);
+ else if (it->first == "covr")
+ {
+ MP4::CoverArtList coverArtList = it->second.toCoverArtList();
+ for (MP4::CoverArtList::ConstIterator pt = coverArtList.begin(); pt != coverArtList.end(); ++pt)
+ {
+ std::string mime;
+ switch (pt->format())
+ {
+ case MP4::CoverArt::PNG:
+ mime = "image/png";
+ break;
+ case MP4::CoverArt::JPEG:
+ mime = "image/jpeg";
+ break;
+ default:
+ break;
+ }
+ if (mime.empty())
+ continue;
+ tag.SetCoverArtInfo(pt->data().size(), mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t *>(pt->data().data()), pt->data().size(), mime);
+ break; // one is enough
+ }
+ }
+ }
+
+ if (!mp4->comment().isEmpty())
+ tag.SetComment(mp4->comment().toCString(true));
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(Tag *genericTag, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!genericTag)
+ return false;
+
+ PropertyMap properties = genericTag->properties();
+ for (PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it)
+ {
+ if (it->first == "ARTIST")
+ SetArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUM")
+ tag.SetAlbum(it->second.front().to8Bit(true));
+ else if (it->first == "TITLE")
+ tag.SetTitle(it->second.front().to8Bit(true));
+ else if (it->first == "TRACKNUMBER")
+ tag.SetTrackNumber(it->second.front().toInt());
+ else if (it->first == "YEAR")
+ tag.SetYear(it->second.front().toInt());
+ else if (it->first == "GENRE")
+ SetGenre(tag, StringListToVectorString(it->second));
+ else if (it->first == "COMMENT")
+ tag.SetComment(it->second.front().to8Bit(true));
+ }
+
+ return true;
+}
+
+
+
+
+void CTagLoaderTagLib::SetArtist(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetArtist(values[0]);
+ else
+ // Fill both artist vector and artist desc from tag value.
+ // Note desc may not be empty as it could have been set by previous parsing of ID3v2 before APE
+ tag.SetArtist(values, true);
+}
+
+void CTagLoaderTagLib::SetArtistSort(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // ARTISTSORT/TSOP tag is often a single string, when not take union of values
+ if (values.size() == 1)
+ tag.SetArtistSort(values[0]);
+ else
+ tag.SetArtistSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::SetArtistHints(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetMusicBrainzArtistHints(StringUtils::Split(values[0], CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ else
+ tag.SetMusicBrainzArtistHints(values);
+}
+
+std::vector<std::string> CTagLoaderTagLib::SplitMBID(const std::vector<std::string> &values)
+{
+ if (values.empty() || values.size() > 1)
+ return values;
+
+ // Picard, and other taggers use a heap of different separators. We use a regexp to detect
+ // MBIDs to make sure we hit them all...
+ std::vector<std::string> ret;
+ std::string value = values[0];
+ StringUtils::ToLower(value);
+ CRegExp reg;
+ if (reg.RegComp("([[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12})"))
+ {
+ int pos = -1;
+ while ((pos = reg.RegFind(value, pos+1)) > -1)
+ ret.push_back(reg.GetMatch(1));
+ }
+ return ret;
+}
+
+void CTagLoaderTagLib::SetAlbumArtist(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetAlbumArtist(values[0]);
+ else
+ // Fill both artist vector and artist desc from tag value.
+ // Note desc may not be empty as it could have been set by previous parsing of ID3v2 before APE
+ tag.SetAlbumArtist(values, true);
+}
+
+void CTagLoaderTagLib::SetAlbumArtistSort(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // ALBUMARTISTSORT/TSOP tag is often a single string, when not take union of values
+ if (values.size() == 1)
+ tag.SetAlbumArtistSort(values[0]);
+ else
+ tag.SetAlbumArtistSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::SetAlbumArtistHints(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetMusicBrainzAlbumArtistHints(StringUtils::Split(values[0], CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ else
+ tag.SetMusicBrainzAlbumArtistHints(values);
+}
+
+void CTagLoaderTagLib::SetComposerSort(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // COMPOSRSORT/TSOC tag is often a single string, when not take union of values
+ if (values.size() == 1)
+ tag.SetComposerSort(values[0]);
+ else
+ tag.SetComposerSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::SetGenre(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ /*
+ TagLib doesn't resolve ID3v1 genre numbers in the case were only
+ a number is specified, thus this workaround.
+ */
+ std::vector<std::string> genres;
+ for (const std::string& i : values)
+ {
+ std::string genre = i;
+ if (StringUtils::IsNaturalNumber(genre))
+ {
+ int number = strtol(i.c_str(), nullptr, 10);
+ if (number >= 0 && number < 256)
+ genre = ID3v1::genre(number).to8Bit(true);
+ }
+ genres.push_back(genre);
+ }
+ if (genres.size() == 1)
+ tag.SetGenre(genres[0], true);
+ else
+ tag.SetGenre(genres, true);
+}
+
+void CTagLoaderTagLib::SetReleaseType(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetMusicBrainzReleaseType(values[0]);
+ else
+ tag.SetMusicBrainzReleaseType(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::AddArtistRole(CMusicInfoTag &tag, const std::string& strRole, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.AddArtistRole(strRole, values[0]);
+ else
+ tag.AddArtistRole(strRole, values);
+}
+
+void CTagLoaderTagLib::SetDiscSubtitle(CMusicInfoTag& tag, const std::vector<std::string>& values)
+{
+ if (values.size() == 1)
+ tag.SetDiscSubtitle(values[0]);
+ else
+ tag.SetDiscSubtitle(std::string());
+}
+
+void CTagLoaderTagLib::AddArtistRole(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // Values contains role, name pairs (as in ID3 standard for TIPL or TMCL tags)
+ // Every odd entry is a function (e.g. Producer, Arranger etc.) or instrument (e.g. Orchestra, Vocal, Piano)
+ // and every even is an artist or a comma delimited list of artists.
+
+ if (values.size() % 2 != 0) // Must contain an even number of entries
+ return;
+
+ // Vector of possible separators
+ const std::vector<std::string> separators{ ";", "/", ",", "&", " and " };
+
+ for (size_t i = 0; i + 1 < values.size(); i += 2)
+ {
+ std::vector<std::string> roles;
+ //Split into individual roles
+ roles = StringUtils::Split(values[i], separators);
+ for (auto role : roles)
+ {
+ StringUtils::Trim(role);
+ StringUtils::ToCapitalize(role);
+ tag.AddArtistRole(role, StringUtils::Split(values[i + 1], ","));
+ }
+ }
+}
+
+void CTagLoaderTagLib::AddArtistInstrument(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ /* Values is a musician credits list, each entry is artist name followed by instrument (or function)
+ e.g. violin, drums, background vocals, solo, orchestra etc. in brackets. This is how Picard uses
+ the PERFORMER tag. Multiple instruments may be in one tag
+ e.g "Pierre Marchand (bass, drum machine and hammond organ)",
+ these will be separated into individual roles.
+ If there is not a pair of brackets then role is "performer" by default, and the whole entry is
+ taken as artist name.
+ */
+ // Vector of possible separators
+ const std::vector<std::string> separators{";", "/", ",", "&", " and "};
+
+ for (size_t i = 0; i < values.size(); ++i)
+ {
+ std::vector<std::string> roles;
+ std::string strArtist = values[i];
+ size_t firstLim = values[i].find_first_of('(');
+ size_t lastLim = values[i].find_last_of(')');
+ if (lastLim != std::string::npos && firstLim != std::string::npos && firstLim < lastLim - 1)
+ {
+ //Pair of brackets with something between them
+ strArtist.erase(firstLim, lastLim - firstLim + 1);
+ std::string strRole = values[i].substr(firstLim + 1, lastLim - firstLim - 1);
+ //Split into individual roles
+ roles = StringUtils::Split(strRole, separators);
+ }
+ StringUtils::Trim(strArtist);
+ if (roles.empty())
+ tag.AddArtistRole("Performer", strArtist);
+ else
+ for (auto role : roles)
+ {
+ StringUtils::Trim(role);
+ StringUtils::ToCapitalize(role);
+ tag.AddArtistRole(role, strArtist);
+ }
+ }
+}
+
+bool CTagLoaderTagLib::Load(const std::string& strFileName, CMusicInfoTag& tag, const std::string& fallbackFileExtension, EmbeddedArt *art /* = NULL */)
+{
+ std::string strExtension = URIUtils::GetExtension(strFileName);
+ StringUtils::TrimLeft(strExtension, ".");
+
+ if (strExtension.empty())
+ {
+ strExtension = fallbackFileExtension;
+ if (strExtension.empty())
+ return false;
+ }
+
+ StringUtils::ToLower(strExtension);
+ TagLibVFSStream* stream = new TagLibVFSStream(strFileName, true);
+ if (!stream)
+ {
+ CLog::Log(LOGERROR, "could not create TagLib VFS stream for: {}", strFileName);
+ return false;
+ }
+
+ long file_length = stream->length();
+
+ if (file_length == 0) // a stream returns zero as the length
+ {
+ delete stream; // scrap this instance
+ return false; // and quit without attempting to read non-existent tags
+ }
+
+ TagLib::File* file = nullptr;
+ TagLib::APE::File* apeFile = nullptr;
+ TagLib::ASF::File* asfFile = nullptr;
+ TagLib::FLAC::File* flacFile = nullptr;
+ TagLib::MP4::File* mp4File = nullptr;
+ TagLib::MPC::File* mpcFile = nullptr;
+ TagLib::MPEG::File* mpegFile = nullptr;
+ TagLib::Ogg::Vorbis::File* oggVorbisFile = nullptr;
+ TagLib::Ogg::FLAC::File* oggFlacFile = nullptr;
+ TagLib::Ogg::Opus::File* oggOpusFile = nullptr;
+ TagLib::TrueAudio::File* ttaFile = nullptr;
+ TagLib::WavPack::File* wvFile = nullptr;
+ TagLib::RIFF::WAV::File * wavFile = nullptr;
+ TagLib::RIFF::AIFF::File * aiffFile = nullptr;
+
+ try
+ {
+ if (strExtension == "ape")
+ file = apeFile = new APE::File(stream);
+ else if (strExtension == "asf" || strExtension == "wmv" || strExtension == "wma")
+ file = asfFile = new ASF::File(stream);
+ else if (strExtension == "flac")
+ file = flacFile = new FLAC::File(stream, ID3v2::FrameFactory::instance());
+ else if (strExtension == "it")
+ file = new IT::File(stream);
+ else if (strExtension == "mod" || strExtension == "module" || strExtension == "nst" || strExtension == "wow")
+ file = new Mod::File(stream);
+ else if (strExtension == "mp4" || strExtension == "m4a" || strExtension == "m4v" ||
+ strExtension == "m4r" || strExtension == "m4b" ||
+ strExtension == "m4p" || strExtension == "3g2")
+ file = mp4File = new MP4::File(stream);
+ else if (strExtension == "mpc")
+ file = mpcFile = new MPC::File(stream);
+ else if (strExtension == "mp3" || strExtension == "aac")
+ file = mpegFile = new MPEG::File(stream, ID3v2::FrameFactory::instance());
+ else if (strExtension == "s3m")
+ file = new S3M::File(stream);
+ else if (strExtension == "tta")
+ file = ttaFile = new TrueAudio::File(stream, ID3v2::FrameFactory::instance());
+ else if (strExtension == "wv")
+ file = wvFile = new WavPack::File(stream);
+ else if (strExtension == "aif" || strExtension == "aiff")
+ file = aiffFile = new RIFF::AIFF::File(stream);
+ else if (strExtension == "wav")
+ file = wavFile = new RIFF::WAV::File(stream);
+ else if (strExtension == "xm")
+ file = new XM::File(stream);
+ else if (strExtension == "ogg")
+ file = oggVorbisFile = new Ogg::Vorbis::File(stream);
+ else if (strExtension == "opus")
+ file = oggOpusFile = new Ogg::Opus::File(stream);
+ else if (strExtension == "oga") // Leave this madness until last - oga container can have Vorbis or FLAC
+ {
+ file = oggFlacFile = new Ogg::FLAC::File(stream);
+ if (!file || !file->isValid())
+ {
+ delete file;
+ oggFlacFile = nullptr;
+ file = oggVorbisFile = new Ogg::Vorbis::File(stream);
+ }
+ }
+ }
+ catch (const std::exception& ex)
+ {
+ CLog::Log(LOGERROR, "Taglib exception: {}", ex.what());
+ }
+
+ if (!file || !file->isOpen())
+ {
+ delete file;
+ delete stream;
+ CLog::Log(LOGDEBUG, "file {} could not be opened for tag reading", strFileName);
+ return false;
+ }
+
+ APE::Tag *ape = nullptr;
+ ASF::Tag *asf = nullptr;
+ MP4::Tag *mp4 = nullptr;
+ ID3v1::Tag *id3v1 = nullptr;
+ ID3v2::Tag *id3v2 = nullptr;
+ Ogg::XiphComment *xiph = nullptr;
+ Tag *genericTag = nullptr;
+
+ if (apeFile)
+ ape = apeFile->APETag(false);
+ else if (asfFile)
+ asf = asfFile->tag();
+ else if (flacFile)
+ {
+ xiph = flacFile->xiphComment(false);
+ id3v2 = flacFile->ID3v2Tag(false);
+ }
+ else if (mp4File)
+ mp4 = mp4File->tag();
+ else if (mpegFile)
+ {
+ id3v1 = mpegFile->ID3v1Tag(false);
+ id3v2 = mpegFile->ID3v2Tag(false);
+ ape = mpegFile->APETag(false);
+ }
+ else if (oggFlacFile)
+ xiph = oggFlacFile->tag();
+ else if (oggVorbisFile)
+ xiph = oggVorbisFile->tag();
+ else if (oggOpusFile)
+ xiph = oggOpusFile->tag();
+ else if (ttaFile)
+ id3v2 = ttaFile->ID3v2Tag(false);
+ else if (aiffFile)
+ id3v2 = aiffFile->tag();
+ else if (wavFile)
+ id3v2 = wavFile->ID3v2Tag();
+ else if (wvFile)
+ ape = wvFile->APETag(false);
+ else if (mpcFile)
+ ape = mpcFile->APETag(false);
+ else // This is a catch all to get generic information for other files types (s3m, xm, it, mod, etc)
+ genericTag = file->tag();
+
+ if (file->audioProperties())
+ {
+ tag.SetDuration(file->audioProperties()->length());
+ tag.SetBitRate(file->audioProperties()->bitrate());
+ tag.SetNoOfChannels(file->audioProperties()->channels());
+ tag.SetSampleRate(file->audioProperties()->sampleRate());
+ }
+
+ if (asf)
+ ParseTag(asf, art, tag);
+ if (id3v1)
+ ParseTag(id3v1, art, tag);
+ if (id3v2)
+ ParseTag(id3v2, art, tag);
+ if (genericTag)
+ ParseTag(genericTag, art, tag);
+ if (mp4)
+ ParseTag(mp4, art, tag);
+ if (xiph) // xiph tags override id3v2 tags in badly tagged FLACs
+ ParseTag(xiph, art, tag);
+ if (ape && (!id3v2 || CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_prioritiseAPEv2tags)) // ape tags override id3v2 if we're prioritising them
+ ParseTag(ape, art, tag);
+
+ // art for flac files is outside the tag
+ if (flacFile)
+ SetFlacArt(flacFile, art, tag);
+
+ if (!tag.GetTitle().empty() || !tag.GetArtist().empty() || !tag.GetAlbum().empty())
+ tag.SetLoaded();
+ tag.SetURL(strFileName);
+
+ delete file;
+ delete stream;
+
+ return true;
+}
diff --git a/xbmc/music/tags/TagLoaderTagLib.h b/xbmc/music/tags/TagLoaderTagLib.h
new file mode 100644
index 0000000..a4b82ad
--- /dev/null
+++ b/xbmc/music/tags/TagLoaderTagLib.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2005-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 "ImusicInfoTagLoader.h"
+
+#include <string>
+#include <vector>
+
+class EmbeddedArt;
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+};
+
+class CTagLoaderTagLib : public MUSIC_INFO::IMusicInfoTagLoader
+{
+public:
+ CTagLoaderTagLib() = default;
+ ~CTagLoaderTagLib() override = default;
+ bool Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag,
+ EmbeddedArt *art = nullptr) override;
+ bool Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag,
+ const std::string& fallbackFileExtension, EmbeddedArt *art = nullptr);
+
+ static std::vector<std::string> SplitMBID(const std::vector<std::string> &values);
+protected:
+ static void SetArtist(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetArtistSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetArtistHints(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetAlbumArtist(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetAlbumArtistSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetAlbumArtistHints(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetComposerSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetDiscSubtitle(MUSIC_INFO::CMusicInfoTag& tag,
+ const std::vector<std::string>& values);
+ static void SetGenre(MUSIC_INFO::CMusicInfoTag& tag, const std::vector<std::string>& values);
+ static void SetReleaseType(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void AddArtistRole(MUSIC_INFO::CMusicInfoTag &tag, const std::string& strRole, const std::vector<std::string> &values);
+ static void AddArtistRole(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void AddArtistInstrument(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static int POPMtoXBMC(int popm);
+
+template<typename T>
+ static bool ParseTag(T *tag, EmbeddedArt *art, MUSIC_INFO::CMusicInfoTag& infoTag);
+};
+
diff --git a/xbmc/music/tags/test/CMakeLists.txt b/xbmc/music/tags/test/CMakeLists.txt
new file mode 100644
index 0000000..e228a9c
--- /dev/null
+++ b/xbmc/music/tags/test/CMakeLists.txt
@@ -0,0 +1,3 @@
+set(SOURCES TestTagLoaderTagLib.cpp)
+
+core_add_test_library(musictags_test)
diff --git a/xbmc/music/tags/test/TestTagLoaderTagLib.cpp b/xbmc/music/tags/test/TestTagLoaderTagLib.cpp
new file mode 100644
index 0000000..0818f04
--- /dev/null
+++ b/xbmc/music/tags/test/TestTagLoaderTagLib.cpp
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2012-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 "music/tags/MusicInfoTag.h"
+#include "music/tags/TagLoaderTagLib.h"
+
+#include <gtest/gtest.h>
+#include <taglib/apetag.h>
+#include <taglib/asftag.h>
+#include <taglib/id3v1genres.h>
+#include <taglib/id3v1tag.h>
+#include <taglib/id3v2tag.h>
+#include <taglib/mp4tag.h>
+#include <taglib/tpropertymap.h>
+#include <taglib/xiphcomment.h>
+
+using namespace TagLib;
+using namespace MUSIC_INFO;
+
+template <typename T>
+class TestTagParser : public ::testing::Test, public CTagLoaderTagLib {
+ public:
+ T value_;
+};
+
+
+typedef ::testing::Types<ID3v2::Tag, ID3v1::Tag, ASF::Tag, APE::Tag, Ogg::XiphComment, MP4::Tag> TagTypes;
+TYPED_TEST_SUITE(TestTagParser, TagTypes);
+
+TYPED_TEST(TestTagParser, ParsesBasicTag) {
+ // Create a basic tag
+ TypeParam *tg = &this->value_;
+ // Configure a basic tag..
+ tg->setTitle ("title");
+ tg->setArtist ("artist");
+ tg->setAlbum ("album");
+ tg->setComment("comment");
+ tg->setGenre("Jazz");
+ tg->setYear (1985);
+ tg->setTrack (2);
+
+ CMusicInfoTag tag;
+ EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag));
+
+ EXPECT_EQ(1985, tag.GetYear());
+ EXPECT_EQ(2, tag.GetTrackNumber());
+ EXPECT_EQ(1u, tag.GetArtist().size());
+ if (!tag.GetArtist().empty())
+ {
+ EXPECT_EQ("artist", tag.GetArtist().front());
+ }
+ EXPECT_EQ("album", tag.GetAlbum());
+ EXPECT_EQ("comment", tag.GetComment());
+ EXPECT_EQ(1u, tag.GetGenre().size());
+ if (!tag.GetGenre().empty())
+ {
+ EXPECT_EQ("Jazz", tag.GetGenre().front());
+ }
+ EXPECT_EQ("title", tag.GetTitle());
+}
+
+
+TYPED_TEST(TestTagParser, HandleNullTag) {
+ // A Null tag should not parse, and not break us either
+ CMusicInfoTag tag;
+ EXPECT_FALSE(CTagLoaderTagLib::ParseTag<TypeParam>(NULL, NULL, tag));
+}
+
+template<typename T, size_t N>
+T * end(T (&ra)[N]) {
+ return ra + N;
+}
+
+const char *tags[] = { "APIC", "ASPI", "COMM", "COMR", "ENCR", "EQU2",
+ "ETCO", "GEOB", "GRID", "LINK", "MCDI", "MLLT", "OWNE", "PRIV", "PCNT",
+ "POPM", "POSS", "RBUF", "RVA2", "RVRB", "SEEK", "SIGN", "SYLT",
+ "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP", "TDEN", "TDLY", "TDOR",
+ "TDRC", "TDRL", "TDTG", "TENC", "TEXT", "TFLT", "TIPL", "TIT1", "TIT2",
+ "TIT3", "TKEY", "TLAN", "TLEN", "TMCL", "TMED", "TMOO", "TOAL", "TOFN",
+ "TOLY", "TOPE", "TOWN", "TPE1", "TPE2", "TPE3", "TPE4", "TPOS", "TPRO",
+ "TPUB", "TRCK", "TRSN", "TRSO", "TSOA", "TSOP", "TSOT", "TSRC", "TSSE",
+ "TSST", "TXXX", "UFID", "USER", "USLT", "WCOM", "WCOP", "WOAF", "WOAR",
+ "WOAS", "WORS", "WPAY", "WPUB", "WXXX", "ARTIST", "ARTISTS",
+ "ALBUMARTIST" , "ALBUM ARTIST", "ALBUMARTISTS" , "ALBUM ARTISTS", "ALBUM",
+ "TITLE", "TRACKNUMBER" "TRACK", "DISCNUMBER" "DISC", "YEAR", "GENRE",
+ "COMMENT", "CUESHEET", "ENCODEDBY", "COMPILATION", "LYRICS",
+ "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_TRACK_PEAK",
+ "REPLAYGAIN_ALBUM_PEAK", "MUSICBRAINZ_ARTISTID",
+ "MUSICBRAINZ_ALBUMARTISTID", "RATING", "MUSICBRAINZ_ALBUMARTIST",
+ "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ_TRACKID", "METADATA_BLOCK_PICTURE",
+ "COVERART"
+};
+
+
+// This test exposes a bug in taglib library (#670) so for now we will not run it for all tag types
+// See https://github.com/taglib/taglib/issues/670 for details.
+typedef ::testing::Types<ID3v2::Tag, ID3v1::Tag, ASF::Tag, APE::Tag, Ogg::XiphComment> EmptyPropertiesTagTypes;
+template <typename T>
+class EmptyTagParser : public ::testing::Test, public CTagLoaderTagLib {
+ public:
+ T value_;
+};
+TYPED_TEST_SUITE(EmptyTagParser, EmptyPropertiesTagTypes);
+
+TYPED_TEST(EmptyTagParser, EmptyProperties) {
+ TypeParam *tg = &this->value_;
+ CMusicInfoTag tag;
+ PropertyMap props;
+ int tagcount = end(tags) - tags;
+ for(int i = 0; i < tagcount; i++) {
+ props.insert(tags[i], StringList());
+ }
+
+ // Even though all the properties are empty, we shouldn't
+ // crash
+ EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag));
+}
+
+
+
+TYPED_TEST(TestTagParser, FooProperties) {
+ TypeParam *tg = &this->value_;
+ CMusicInfoTag tag;
+ PropertyMap props;
+ int tagcount = end(tags) - tags;
+ for(int i = 0; i < tagcount; i++) {
+ props.insert(tags[i], String("foo"));
+ }
+ tg->setProperties(props);
+
+ EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag));
+ EXPECT_EQ(0, tag.GetYear());
+ EXPECT_EQ(0, tag.GetTrackNumber());
+ EXPECT_EQ(1u, tag.GetArtist().size());
+ if (!tag.GetArtist().empty())
+ {
+ EXPECT_EQ("foo", tag.GetArtist().front());
+ }
+ EXPECT_EQ("foo", tag.GetAlbum());
+ EXPECT_EQ("foo", tag.GetComment());
+ if (!tag.GetGenre().empty())
+ {
+ EXPECT_EQ("foo", tag.GetGenre().front());
+ }
+ EXPECT_EQ("foo", tag.GetTitle());
+}
+
+class TestTagLoaderTagLib : public ::testing::Test, public CTagLoaderTagLib {};
+TEST_F(TestTagLoaderTagLib, SetGenre)
+{
+ CMusicInfoTag tag, tag2;
+ const char *genre_nr[] = {"0", "2", "4"};
+ const char *names[] = { "Jazz", "Funk", "Ska" };
+ std::vector<std::string> genres(genre_nr, end(genre_nr));
+ std::vector<std::string> named_genre(names, end(names));
+
+ CTagLoaderTagLib::SetGenre(tag, genres);
+ EXPECT_EQ(3u, tag.GetGenre().size());
+ EXPECT_EQ("Blues", tag.GetGenre()[0]);
+ EXPECT_EQ("Country", tag.GetGenre()[1]);
+ EXPECT_EQ("Disco", tag.GetGenre()[2]);
+
+ CTagLoaderTagLib::SetGenre(tag2, named_genre);
+ EXPECT_EQ(3u, tag2.GetGenre().size());
+ for(int i = 0; i < 3; i++)
+ EXPECT_EQ(names[i], tag2.GetGenre()[i]);
+
+}
+
+TEST_F(TestTagLoaderTagLib, SplitMBID)
+{
+ CTagLoaderTagLib lib;
+
+ // SplitMBID should return the vector if it's empty or longer than 1
+ std::vector<std::string> values;
+ EXPECT_TRUE(lib.SplitMBID(values).empty());
+
+ values.emplace_back("1");
+ values.emplace_back("2");
+ EXPECT_EQ(values, lib.SplitMBID(values));
+
+ // length 1 and invalid should return empty
+ values.clear();
+ values.emplace_back("invalid");
+ EXPECT_TRUE(lib.SplitMBID(values).empty());
+
+ // length 1 and valid should return the valid id
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_EQ(lib.SplitMBID(values), values);
+
+ // case shouldn't matter
+ values.clear();
+ values.emplace_back("0383DaDf-2A4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_EQ(lib.SplitMBID(values).size(), 1u);
+ EXPECT_STREQ(lib.SplitMBID(values)[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+
+ // valid with some stuff off the end or start should return valid
+ values.clear();
+ values.emplace_back("foo0383dadf-2a4e-4d10-a46a-e9e041da8eb3 blah");
+ EXPECT_EQ(lib.SplitMBID(values).size(), 1u);
+ EXPECT_STREQ(lib.SplitMBID(values)[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+
+ // two valid with various separators
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3;53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+ std::vector<std::string> result = lib.SplitMBID(values);
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3/53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+ result = lib.SplitMBID(values);
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3 / 53b106e7-0cc6-42cc-ac95-ed8d30a3a98e; ");
+ result = lib.SplitMBID(values);
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+}
diff --git a/xbmc/music/windows/CMakeLists.txt b/xbmc/music/windows/CMakeLists.txt
new file mode 100644
index 0000000..031d735
--- /dev/null
+++ b/xbmc/music/windows/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES GUIWindowMusicBase.cpp
+ GUIWindowMusicNav.cpp
+ GUIWindowMusicPlaylist.cpp
+ GUIWindowMusicPlaylistEditor.cpp
+ GUIWindowVisualisation.cpp
+ MusicFileItemListModifier.cpp)
+
+set(HEADERS GUIWindowMusicBase.h
+ GUIWindowMusicNav.h
+ GUIWindowMusicPlaylist.h
+ GUIWindowMusicPlaylistEditor.h
+ GUIWindowVisualisation.h
+ MusicFileItemListModifier.h)
+
+core_add_library(music_windows)
diff --git a/xbmc/music/windows/GUIWindowMusicBase.cpp b/xbmc/music/windows/GUIWindowMusicBase.cpp
new file mode 100644
index 0000000..df45141
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicBase.cpp
@@ -0,0 +1,1097 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicBase.h"
+
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicUtils.h"
+#include "music/dialogs/GUIDialogInfoProviderSettings.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#ifdef HAS_CDDA_RIPPER
+#include "cdrip/CDDARipper.h"
+#endif
+#include "Autorun.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIPassword.h"
+#include "PartyModeManager.h"
+#include "URL.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "view/GUIViewState.h"
+
+#include <algorithm>
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace MUSIC_GRABBER;
+using namespace MUSIC_INFO;
+using namespace KODI::MESSAGING;
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+using namespace std::chrono_literals;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNPLAYLISTS 7
+#define CONTROL_BTNSCAN 9
+#define CONTROL_BTNRIP 11
+
+CGUIWindowMusicBase::CGUIWindowMusicBase(int id, const std::string &xmlFile)
+ : CGUIMediaWindow(id, xmlFile.c_str())
+{
+ m_dlgProgress = NULL;
+ m_thumbLoader.SetObserver(this);
+}
+
+CGUIWindowMusicBase::~CGUIWindowMusicBase () = default;
+
+bool CGUIWindowMusicBase::OnBack(int actionID)
+{
+ if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ CUtil::RemoveTempFiles();
+ }
+ return CGUIMediaWindow::OnBack(actionID);
+}
+
+/*!
+ \brief Handle messages on window.
+ \param message GUI Message that can be reacted on.
+ \return if a message can't be processed, return \e false
+
+ On these messages this class reacts.\n
+ When retrieving...
+ - #GUI_MSG_WINDOW_DEINIT\n
+ ...the last focused control is saved to m_iLastControl.
+ - #GUI_MSG_WINDOW_INIT\n
+ ...the musicdatabase is opend and the music extensions and shares are set.
+ The last focused control is set.
+ - #GUI_MSG_CLICKED\n
+ ... the base class reacts on the following controls:\n
+ Buttons:\n
+ - #CONTROL_BTNVIEWASICONS - switch between list, thumb and with large items
+ - #CONTROL_BTNSEARCH - Search for items\n
+ Other Controls:
+ - The container controls\n
+ Have the following actions in message them clicking on them.
+ - #ACTION_QUEUE_ITEM - add selected item to end of playlist
+ - #ACTION_QUEUE_ITEM_NEXT - add selected item to next pos in playlist
+ - #ACTION_SHOW_INFO - retrieve album info from the internet
+ - #ACTION_SELECT_ITEM - Item has been selected. Overwrite OnClick() to react on it
+ */
+bool CGUIWindowMusicBase::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ m_musicdatabase.Close();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+
+ m_musicdatabase.Open();
+
+ if (!CGUIMediaWindow::OnMessage(message))
+ return false;
+
+ return true;
+ }
+ break;
+ case GUI_MSG_DIRECTORY_SCANNED:
+ {
+ CFileItem directory(message.GetStringParam(), true);
+
+ // Only update thumb on a local drive
+ if (directory.IsHD())
+ {
+ std::string strParent;
+ URIUtils::GetParentPath(directory.GetPath(), strParent);
+ if (directory.GetPath() == m_vecItems->GetPath() || strParent == m_vecItems->GetPath())
+ Refresh();
+ }
+ }
+ break;
+
+ // update the display
+ case GUI_MSG_SCAN_FINISHED:
+ case GUI_MSG_REFRESH_THUMBS: // Never called as is secondary msg sent as GUI_MSG_NOTIFY_ALL
+ Refresh();
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNRIP)
+ {
+ OnRipCD();
+ }
+ else if (iControl == CONTROL_BTNPLAYLISTS)
+ {
+ if (!m_vecItems->IsPath("special://musicplaylists/"))
+ Update("special://musicplaylists/");
+ }
+ else if (iControl == CONTROL_BTNSCAN)
+ {
+ OnScan(-1);
+ }
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_QUEUE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK)
+ {
+ OnQueueItem(iItem);
+ }
+ else if (iAction == ACTION_QUEUE_ITEM_NEXT)
+ {
+ OnQueueItem(iItem, true);
+ }
+ else if (iAction == ACTION_SHOW_INFO)
+ {
+ OnItemInfo(iItem);
+ }
+ else if (iAction == ACTION_DELETE_ITEM)
+ {
+ // is delete allowed?
+ // must be at the playlists directory
+ if (m_vecItems->IsPath("special://musicplaylists/"))
+ OnDeleteItem(iItem);
+
+ else
+ return false;
+ }
+ // use play button to add folders of items to temp playlist
+ else if (iAction == ACTION_PLAYER_PLAY)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // if playback is paused or playback speed != 1, return
+ if (appPlayer->IsPlayingAudio())
+ {
+ if (appPlayer->IsPausedPlayback())
+ return false;
+ if (appPlayer->GetPlaySpeed() != 1)
+ return false;
+ }
+
+ // not playing audio, or playback speed == 1
+ PlayItem(iItem);
+
+ return true;
+ }
+ }
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA)
+ CUtil::DeleteDirectoryCache("r-");
+ }
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowMusicBase::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC ||
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC).size() > 0)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ return true;
+ }
+ }
+
+ if (action.GetID() == ACTION_SCAN_ITEM)
+ {
+ int item = m_viewControl.GetSelectedItem();
+ if (item > -1 && m_vecItems->Get(item)->m_bIsFolder)
+ OnScan(item);
+
+ return true;
+ }
+
+ return CGUIMediaWindow::OnAction(action);
+}
+
+void CGUIWindowMusicBase::OnItemInfoAll(const std::string& strPath, bool refresh)
+{
+ if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "albums"))
+ {
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ return;
+
+ CMusicLibraryQueue::GetInstance().StartAlbumScan(strPath, refresh);
+ }
+ else if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "artists"))
+ {
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ return;
+
+ CMusicLibraryQueue::GetInstance().StartArtistScan(strPath, refresh);
+ }
+}
+
+void CGUIWindowMusicBase::OnItemInfo(int iItem)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() )
+ return;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ // Match visibility test of CMusicInfo::IsVisible
+ if (item->IsVideoDb() && item->HasVideoInfoTag() &&
+ (item->HasProperty("artist_musicid") || item->HasProperty("album_musicid")))
+ {
+ // Music video artist or album (navigation by music > music video > artist))
+ CGUIDialogMusicInfo::ShowFor(item.get());
+ return;
+ }
+
+ if (item->IsVideo() && item->HasVideoInfoTag() &&
+ item->GetVideoInfoTag()->m_type == MediaTypeMusicVideo)
+ { // Music video on a mixed current playlist or navigation by music > music video > artist > video
+ CGUIDialogVideoInfo::ShowFor(*item);
+ return;
+ }
+
+ if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript()))
+ {
+ CGUIDialogAddonInfo::ShowForItem(item);
+ return;
+ }
+
+ // Match visibility test of CMusicInfo::IsVisible
+ if (item->HasMusicInfoTag() && (item->GetMusicInfoTag()->GetType() == MediaTypeSong ||
+ item->GetMusicInfoTag()->GetType() == MediaTypeAlbum ||
+ item->GetMusicInfoTag()->GetType() == MediaTypeArtist))
+ CGUIDialogMusicInfo::ShowFor(item.get());
+}
+
+void CGUIWindowMusicBase::RefreshContent(const std::string& strContent)
+{
+ if ( CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV &&
+ m_vecItems->GetContent() == strContent &&
+ m_vecItems->GetSortMethod() == SortByUserRating)
+ // When music library window is active and showing songs or albums sorted
+ // by userrating refresh the list to resort items and show new userrating
+ Refresh(true);
+}
+
+/// \brief Retrieve tag information for \e m_vecItems
+void CGUIWindowMusicBase::RetrieveMusicInfo()
+{
+ auto start = std::chrono::steady_clock::now();
+
+ OnRetrieveMusicInfo(*m_vecItems);
+
+ //! @todo Scan for multitrack items here...
+ std::vector<std::string> itemsForRemove;
+ CFileItemList itemsForAdd;
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = (*m_vecItems)[i];
+ if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics() || pItem->IsVideo())
+ continue;
+
+ CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
+ if (tag.Loaded() && !tag.GetCueSheet().empty())
+ pItem->LoadEmbeddedCue();
+
+ if (pItem->HasCueDocument()
+ && pItem->LoadTracksFromCueDocument(itemsForAdd))
+ {
+ itemsForRemove.push_back(pItem->GetPath());
+ }
+ }
+ for (size_t i = 0; i < itemsForRemove.size(); ++i)
+ {
+ for (int j = 0; j < m_vecItems->Size(); ++j)
+ {
+ if ((*m_vecItems)[j]->GetPath() == itemsForRemove[i])
+ {
+ m_vecItems->Remove(j);
+ break;
+ }
+ }
+ }
+ m_vecItems->Append(itemsForAdd);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "RetrieveMusicInfo() took {} ms", duration.count());
+}
+
+/// \brief Add selected list/thumb control item to playlist and start playing
+/// \param iItem Selected Item in list/thumb control
+void CGUIWindowMusicBase::OnQueueItem(int iItem, bool first)
+{
+ // don't re-queue items from playlist window
+ if (iItem < 0 || iItem >= m_vecItems->Size() || GetID() == WINDOW_MUSIC_PLAYLIST)
+ return;
+
+ // add item 2 playlist
+ const auto item = m_vecItems->Get(iItem);
+
+ if (item->IsRAR() || item->IsZIP())
+ return;
+
+ MUSIC_UTILS::QueueItem(item, first ? MUSIC_UTILS::QueuePosition::POSITION_BEGIN
+ : MUSIC_UTILS::QueuePosition::POSITION_END);
+
+ // select next item
+ m_viewControl.SetSelectedItem(iItem + 1);
+}
+
+void CGUIWindowMusicBase::UpdateButtons()
+{
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTNRIP, CServiceBroker::GetMediaManager().IsAudio());
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTNSCAN,
+ !(m_vecItems->IsVirtualDirectoryRoot() ||
+ m_vecItems->IsMusicDb()));
+
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ SET_CONTROL_LABEL(CONTROL_BTNSCAN, 14056); // Stop Scan
+ else
+ SET_CONTROL_LABEL(CONTROL_BTNSCAN, 102); // Scan
+
+ CGUIMediaWindow::UpdateButtons();
+}
+
+void CGUIWindowMusicBase::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ if (item)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // Check for the partymode playlist item.
+ // When "PartyMode.xsp" not exist, only context menu button is edit
+ if (item->IsSmartPlayList() &&
+ (item->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) &&
+ !CFileUtils::Exists(item->GetPath()))
+ {
+ buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586);
+ return;
+ }
+
+ if (!item->IsParentFolder())
+ {
+ //! @todo get rid of IsAddonsPath and IsScript check. CanQueue should be enough!
+ if (item->CanQueue() && !item->IsAddonsPath() && !item->IsScript())
+ {
+ if (!item->m_bIsFolder &&
+ (!item->IsPlayList() ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders))
+ {
+ const CPlayerCoreFactory& playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // check what players we have, if we have multiple display play with option
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ if (players.size() >= 1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With...
+ }
+
+ if (item->IsSmartPlayList())
+ buttons.Add(CONTEXT_BUTTON_PLAY_PARTYMODE, 15216); // Play in Partymode
+
+ if (item->IsSmartPlayList() || m_vecItems->IsSmartPlayList())
+ buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586);
+ else if (item->IsPlayList() || m_vecItems->IsPlayList())
+ buttons.Add(CONTEXT_BUTTON_EDIT, 586);
+ }
+#ifdef HAS_DVD_DRIVE
+ // enable Rip CD Audio or Track button if we have an audio disc
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive() && m_vecItems->IsCDDA())
+ {
+ // those cds can also include Audio Tracks: CDExtra and MixedMode!
+ MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1))
+ buttons.Add(CONTEXT_BUTTON_RIP_TRACK, 610);
+ }
+#endif
+ }
+
+ // enable CDDB lookup if the current dir is CDDA
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive() && m_vecItems->IsCDDA() &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser))
+ {
+ buttons.Add(CONTEXT_BUTTON_CDDB, 16002);
+ }
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+void CGUIWindowMusicBase::GetNonContextButtons(CContextButtons &buttons)
+{
+}
+
+bool CGUIWindowMusicBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ if (CGUIDialogContextMenu::OnContextButton("music", item, button))
+ {
+ if (button == CONTEXT_BUTTON_REMOVE_SOURCE)
+ OnRemoveSource(itemNumber);
+
+ Update(m_vecItems->GetPath());
+ return true;
+ }
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_INFO:
+ OnItemInfo(itemNumber);
+ return true;
+
+ case CONTEXT_BUTTON_EDIT:
+ {
+ std::string playlist = item->IsPlayList() ? item->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST_EDITOR, playlist);
+ // need to update
+ m_vecItems->RemoveDiscCache(GetID());
+ return true;
+ }
+
+ case CONTEXT_BUTTON_EDIT_SMART_PLAYLIST:
+ {
+ std::string playlist = item->IsSmartPlayList() ? item->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist, "music"))
+ Refresh(true); // need to update
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_WITH:
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ OnClick(itemNumber, player);
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_PARTYMODE:
+ g_partyModeManager.Enable(PARTYMODECONTEXT_MUSIC, item->GetPath());
+ return true;
+
+ case CONTEXT_BUTTON_RIP_CD:
+ OnRipCD();
+ return true;
+
+#ifdef HAS_CDDA_RIPPER
+ case CONTEXT_BUTTON_CANCEL_RIP_CD:
+ KODI::CDRIP::CCDDARipper::GetInstance().CancelJobs();
+ return true;
+#endif
+
+ case CONTEXT_BUTTON_RIP_TRACK:
+ OnRipTrack(itemNumber);
+ return true;
+
+ case CONTEXT_BUTTON_SCAN:
+ // Check if scanning already and inform user
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ HELPERS::ShowOKDialogText(CVariant{ 189 }, CVariant{ 14057 });
+ else
+ OnScan(itemNumber, true);
+ return true;
+
+ case CONTEXT_BUTTON_CDDB:
+ if (m_musicdatabase.LookupCDDBInfo(true))
+ Refresh();
+ return true;
+
+ default:
+ break;
+ }
+
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowMusicBase::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("music");
+}
+
+void CGUIWindowMusicBase::OnRipCD()
+{
+ if (CServiceBroker::GetMediaManager().IsAudio())
+ {
+ if (!g_application.CurrentFileItem().IsCDDA())
+ {
+#ifdef HAS_CDDA_RIPPER
+ KODI::CDRIP::CCDDARipper::GetInstance().RipCD();
+#endif
+ }
+ else
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20099});
+ }
+}
+
+void CGUIWindowMusicBase::OnRipTrack(int iItem)
+{
+ if (CServiceBroker::GetMediaManager().IsAudio())
+ {
+ if (!g_application.CurrentFileItem().IsCDDA())
+ {
+#ifdef HAS_CDDA_RIPPER
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ KODI::CDRIP::CCDDARipper::GetInstance().RipTrack(item.get());
+#endif
+ }
+ else
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20099});
+ }
+}
+
+void CGUIWindowMusicBase::PlayItem(int iItem)
+{
+ // restrictions should be placed in the appropriate window code
+ // only call the base code if the item passes since this clears
+ // the current playlist
+
+ const CFileItemPtr pItem = m_vecItems->Get(iItem);
+#ifdef HAS_DVD_DRIVE
+ if (pItem->IsDVD())
+ {
+ MEDIA_DETECT::CAutorun::PlayDiscAskResume(pItem->GetPath());
+ return;
+ }
+#endif
+
+ // Check for the partymode playlist item, do nothing when "PartyMode.xsp" not exist
+ if (pItem->IsSmartPlayList())
+ {
+ const std::shared_ptr<CProfileManager> profileManager =
+ CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if ((pItem->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) &&
+ !CFileUtils::Exists(pItem->GetPath()))
+ return;
+ }
+
+ // if its a folder, build a playlist
+ if (pItem->m_bIsFolder && !pItem->IsPlugin())
+ {
+ // make a copy so that we can alter the queue state
+ CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem)));
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!item->CanQueue())
+ item->SetCanQueue(true);
+
+ // skip ".."
+ if (item->IsParentFolder())
+ return;
+
+ CFileItemList queuedItems;
+ MUSIC_UTILS::GetItemsForPlayList(item, queuedItems);
+ if (g_partyModeManager.IsEnabled())
+ {
+ g_partyModeManager.AddUserSongs(queuedItems, true);
+ return;
+ }
+
+ /*
+ std::string strPlayListDirectory = m_vecItems->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPlayListDirectory);
+ */
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_MUSIC, queuedItems);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+
+ // play!
+ CServiceBroker::GetPlaylistPlayer().Play();
+ }
+ else if (pItem->IsPlayList())
+ {
+ // load the playlist the old way
+ LoadPlayList(pItem->GetPath());
+ }
+ else
+ {
+ // just a single item, play it
+ //! @todo Add music-specific code for single playback of an item here (See OnClick in MediaWindow, and OnPlayMedia below)
+ OnClick(iItem);
+ }
+}
+
+void CGUIWindowMusicBase::LoadPlayList(const std::string& strPlayList)
+{
+ // if partymode is active, we disable it
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+
+ // load a playlist like .m3u, .pls
+ // first get correct factory to load playlist
+ std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList));
+ if (pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return; //hmmm unable to load playlist?
+ }
+ }
+
+ int iSize = pPlayList->size();
+ if (g_application.ProcessAndStartPlaylist(strPlayList, *pPlayList, PLAYLIST::TYPE_MUSIC))
+ {
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory("playlistmusic://");
+ // activate the playlist window if its not activated yet
+ if (GetID() == CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() && iSize > 1)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ }
+ }
+}
+
+bool CGUIWindowMusicBase::OnPlayMedia(int iItem, const std::string &player)
+{
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ // party mode
+ if (g_partyModeManager.IsEnabled())
+ {
+ PLAYLIST::CPlayList playlistTemp;
+ playlistTemp.Add(pItem);
+ g_partyModeManager.AddUserSongs(playlistTemp, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT));
+ return true;
+ }
+ else if (!pItem->IsPlayList() && !pItem->IsInternetStream())
+ { // single music file - if we get here then we have autoplaynextitem turned off or queuebydefault
+ // turned on, but we still want to use the playlist player in order to handle more queued items
+ // following etc.
+ if ( (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT) && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_MUSIC_PLAYLIST_EDITOR) )
+ {
+ //! @todo Should the playlist be cleared if nothing is already playing?
+ OnQueueItem(iItem);
+ return true;
+ }
+ pItem->SetProperty("playlist_type_hint", m_guiState->GetPlaylist());
+ CServiceBroker::GetPlaylistPlayer().Play(pItem, player);
+ return true;
+ }
+ return CGUIMediaWindow::OnPlayMedia(iItem, player);
+}
+
+/// \brief Can be overwritten to implement an own tag filling function.
+/// \param items File items to fill
+void CGUIWindowMusicBase::OnRetrieveMusicInfo(CFileItemList& items)
+{
+ // No need to attempt to read music file tags for music videos
+ if (items.IsVideoDb())
+ return;
+ if (items.GetFolderCount()==items.Size() || items.IsMusicDb() ||
+ (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_USETAGS) && !items.IsCDDA()))
+ {
+ return;
+ }
+ // Start the music info loader thread
+ m_musicInfoLoader.SetProgressCallback(m_dlgProgress);
+ m_musicInfoLoader.Load(items);
+
+ bool bShowProgress = !CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true);
+ bool bProgressVisible = false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ while (m_musicInfoLoader.IsLoading())
+ {
+ if (bShowProgress)
+ { // Do we have to init a progress dialog?
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (!bProgressVisible && duration.count() > 1500 && m_dlgProgress)
+ { // tag loading takes more then 1.5 secs, show a progress dialog
+ CURL url(items.GetPath());
+ m_dlgProgress->SetHeading(CVariant{189});
+ m_dlgProgress->SetLine(0, CVariant{505});
+ m_dlgProgress->SetLine(1, CVariant{""});
+ m_dlgProgress->SetLine(2, CVariant{url.GetWithoutUserDetails()});
+ m_dlgProgress->Open();
+ m_dlgProgress->ShowProgressBar(true);
+ bProgressVisible = true;
+ }
+
+ if (bProgressVisible && m_dlgProgress && !m_dlgProgress->IsCanceled())
+ { // keep GUI alive
+ m_dlgProgress->Progress();
+ }
+ } // if (bShowProgress)
+ KODI::TIME::Sleep(1ms);
+ } // while (m_musicInfoLoader.IsLoading())
+
+ if (bProgressVisible && m_dlgProgress)
+ m_dlgProgress->Close();
+}
+
+bool CGUIWindowMusicBase::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ items.ClearArt();
+ bool bResult = CGUIMediaWindow::GetDirectory(strDirectory, items);
+ if (bResult)
+ {
+ // We want to expand disc images when browsing in file view but not on library, smartplaylist
+ // or node menu music windows
+ if (!items.GetPath().empty() && !StringUtils::StartsWithNoCase(items.GetPath(), "musicdb://") &&
+ !StringUtils::StartsWithNoCase(items.GetPath(), "special://") &&
+ !StringUtils::StartsWithNoCase(items.GetPath(), "library://"))
+ CDirectory::FilterFileDirectories(items, ".iso", true);
+
+ CMusicThumbLoader loader;
+ loader.FillThumb(items);
+
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(items.GetPath(), params);
+
+ // Get art for directory when album or artist
+ bool artfound = false;
+ std::vector<ArtForThumbLoader> art;
+ if (params.GetAlbumId() > 0)
+ { // Get album and related artist(s) art
+ artfound = m_musicdatabase.GetArtForItem(-1, params.GetAlbumId(), -1, false, art);
+ }
+ else if (params.GetArtistId() > 0)
+ { // get artist art
+ artfound = m_musicdatabase.GetArtForItem(-1, -1, params.GetArtistId(), true, art);
+ }
+ if (artfound)
+ {
+ std::string dirType = MediaTypeArtist;
+ if (params.GetAlbumId() > 0)
+ dirType = MediaTypeAlbum;
+ std::map<std::string, std::string> artmap;
+ for (auto artitem : art)
+ {
+ std::string artname;
+ if (dirType == artitem.mediaType)
+ artname = artitem.artType;
+ else if (artitem.prefix.empty())
+ artname = artitem.mediaType + "." + artitem.artType;
+ else
+ {
+ if (dirType == MediaTypeAlbum)
+ StringUtils::Replace(artitem.prefix, "albumartist", "artist");
+ artname = artitem.prefix + "." + artitem.artType;
+ }
+ artmap.insert(std::make_pair(artname, artitem.url));
+ }
+ items.SetArt(artmap);
+ }
+
+ int iWindow = GetID();
+ // Add "New Playlist" items when in the playlists folder, except on playlist editor screen
+ if ((iWindow != WINDOW_MUSIC_PLAYLIST_EDITOR) &&
+ (items.GetPath() == "special://musicplaylists/") && !items.Contains("newplaylist://"))
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CFileItemPtr newPlaylist(new CFileItem(profileManager->GetUserDataItem("PartyMode.xsp"),false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(16035));
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetArt("icon", "DefaultPartyMode.png");
+ newPlaylist->m_bIsFolder = true;
+ items.Add(newPlaylist);
+
+ newPlaylist.reset(new CFileItem("newplaylist://", false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(525));
+ newPlaylist->SetArt("icon", "DefaultAddSource.png");
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetSpecialSort(SortSpecialOnBottom);
+ newPlaylist->SetCanQueue(false);
+ items.Add(newPlaylist);
+
+ newPlaylist.reset(new CFileItem("newsmartplaylist://music", false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(21437));
+ newPlaylist->SetArt("icon", "DefaultAddSource.png");
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetSpecialSort(SortSpecialOnBottom);
+ newPlaylist->SetCanQueue(false);
+ items.Add(newPlaylist);
+ }
+
+ // check for .CUE files here.
+ items.FilterCueItems();
+
+ std::string label;
+ if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("music"), &label))
+ items.SetLabel(label);
+ }
+
+ return bResult;
+}
+
+bool CGUIWindowMusicBase::CheckFilterAdvanced(CFileItemList &items) const
+{
+ const std::string& content = items.GetContent();
+ if ((items.IsMusicDb() || CanContainFilter(m_strFilterPath)) &&
+ (StringUtils::EqualsNoCase(content, "artists") ||
+ StringUtils::EqualsNoCase(content, "albums") ||
+ StringUtils::EqualsNoCase(content, "songs")))
+ return true;
+
+ return false;
+}
+
+bool CGUIWindowMusicBase::CanContainFilter(const std::string &strDirectory) const
+{
+ return URIUtils::IsProtocol(strDirectory, "musicdb");
+}
+
+bool CGUIWindowMusicBase::OnSelect(int iItem)
+{
+ auto item = m_vecItems->Get(iItem);
+ if (item->IsAudioBook())
+ {
+ int bookmark;
+ if (m_musicdatabase.GetResumeBookmarkForAudioBook(*item, bookmark) && bookmark > 0)
+ {
+ // find which chapter the bookmark belongs to
+ auto itemIt =
+ std::find_if(m_vecItems->cbegin(), m_vecItems->cend(),
+ [&](const CFileItemPtr& item) { return bookmark < item->GetEndOffset(); });
+
+ if (itemIt != m_vecItems->cend())
+ {
+ // ask the user if they want to play or resume
+ CContextButtons choices;
+ choices.Add(MUSIC_SELECT_ACTION_PLAY, 208); // 208 = Play
+ choices.Add(MUSIC_SELECT_ACTION_RESUME,
+ StringUtils::Format(g_localizeStrings.Get(12022), // 12022 = Resume from ...
+ (*itemIt)->GetMusicInfoTag()->GetTitle()));
+
+ auto choice = CGUIDialogContextMenu::Show(choices);
+ if (choice == MUSIC_SELECT_ACTION_RESUME)
+ {
+ (*itemIt)->SetProperty("audiobook_bookmark", bookmark);
+ return CGUIMediaWindow::OnSelect(static_cast<int>(itemIt - m_vecItems->cbegin()));
+ }
+ else if (choice < 0)
+ return true;
+ }
+ }
+ }
+
+ return CGUIMediaWindow::OnSelect(iItem);
+}
+
+void CGUIWindowMusicBase::OnInitWindow()
+{
+ CGUIMediaWindow::OnInitWindow();
+ // Prompt for rescan of library to read music file tags that were not processed by previous versions
+ // and accommodate any changes to the way some tags are processed
+ if (m_musicdatabase.GetMusicNeedsTagScan() != 0)
+ {
+ if (CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetLibraryInfoProvider()
+ .GetLibraryBool(LIBRARY_HAS_MUSIC) &&
+ !CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ // rescan of music library required
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{799}, CVariant{38060}))
+ {
+ int flags = CMusicInfoScanner::SCAN_RESCAN;
+ // When set to fetch information on update enquire about scraping that as well
+ // It may take some time, so the user may want to do it later by "Query Info For All"
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO))
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{799}, CVariant{38061}))
+ flags |= CMusicInfoScanner::SCAN_ONLINE;
+
+ CMusicLibraryQueue::GetInstance().ScanLibrary("", flags, true);
+
+ m_musicdatabase.SetMusicTagScanVersion(); // once is enough (user may interrupt, but that's up to them)
+ }
+ }
+ else
+ {
+ // no need to force a rescan if there's no music in the library or if a library scan is already active
+ m_musicdatabase.SetMusicTagScanVersion();
+ }
+ }
+}
+
+std::string CGUIWindowMusicBase::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "plugins" || lower == "addons")
+ return "addons://sources/audio/";
+ else if (lower == "$playlists" || lower == "playlists")
+ return "special://musicplaylists/";
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
+
+void CGUIWindowMusicBase::OnScan(int iItem, bool bPromptRescan /*= false*/)
+{
+ std::string strPath;
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ strPath = m_vecItems->GetPath();
+ else if (m_vecItems->Get(iItem)->m_bIsFolder)
+ strPath = m_vecItems->Get(iItem)->GetPath();
+ else
+ { //! @todo MUSICDB - should we allow scanning a single item into the database?
+ //! This will require changes to the info scanner, which assumes we're running on a folder
+ strPath = m_vecItems->GetPath();
+ }
+ // Ask for full rescan of music files when scan item from file view context menu
+ bool doRescan = false;
+ if (bPromptRescan)
+ doRescan = CGUIDialogYesNo::ShowAndGetInput(CVariant{ 799 }, CVariant{ 38062 });
+
+ DoScan(strPath, doRescan);
+}
+
+void CGUIWindowMusicBase::DoScan(const std::string &strPath, bool bRescan /*= false*/)
+{
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ CMusicLibraryQueue::GetInstance().StopLibraryScanning();
+ return;
+ }
+
+ // Start background loader
+ int iControl=GetFocusedControlID();
+ int flags = 0;
+ if (bRescan)
+ flags = CMusicInfoScanner::SCAN_RESCAN;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO))
+ flags |= CMusicInfoScanner::SCAN_ONLINE;
+
+ CMusicLibraryQueue::GetInstance().ScanLibrary(strPath, flags, true);
+
+ SET_CONTROL_FOCUS(iControl, 0);
+ UpdateButtons();
+}
+
+void CGUIWindowMusicBase::OnRemoveSource(int iItem)
+{
+
+ //Remove music source from library, even when leaving songs
+ CMusicDatabase database;
+ database.Open();
+ database.RemoveSource(m_vecItems->Get(iItem)->GetLabel());
+
+ bool bCanceled;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{522}, CVariant{20340}, bCanceled, CVariant{""}, CVariant{""}, CGUIDialogYesNo::NO_TIMEOUT))
+ {
+ MAPSONGS songs;
+ database.RemoveSongsFromPath(m_vecItems->Get(iItem)->GetPath(), songs, false);
+ database.CleanupOrphanedItems();
+ database.CheckArtistLinksChanged();
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+ m_vecItems->RemoveDiscCache(GetID());
+ }
+ database.Close();
+}
+
+void CGUIWindowMusicBase::OnPrepareFileItems(CFileItemList &items)
+{
+ CGUIMediaWindow::OnPrepareFileItems(items);
+
+ if (!items.IsMusicDb() && !items.IsSmartPlayList())
+ RetrieveMusicInfo();
+}
+
+void CGUIWindowMusicBase::OnAssignContent(const std::string& oldName, const CMediaSource& source)
+{
+ // Music scrapers are not source specific, so unlike video there is no content selection logic here.
+ // Called on having added or edited a music source, this starts scanning items into library when required
+
+ //! @todo: do async as updating sources for all albums could be slow??
+ //Store music source in the music library, even those not scanned
+ CMusicDatabase database;
+ database.Open();
+ database.UpdateSource(oldName, source.strName, source.strPath, source.vecPaths);
+ database.Close();
+
+ // "Add to library" yes/no dialog with additional "settings" custom button
+ // "Do you want to add the media from this source to your library?"
+ DialogResponse rep = DialogResponse::CHOICE_CUSTOM;
+ while (rep == DialogResponse::CHOICE_CUSTOM)
+ {
+ rep = HELPERS::ShowYesNoCustomDialog(CVariant{20444}, CVariant{20447}, CVariant{106}, CVariant{107}, CVariant{10004});
+ if (rep == DialogResponse::CHOICE_CUSTOM)
+ // Edit default info provider settings so can be applied during scan
+ CGUIDialogInfoProviderSettings::Show();
+ }
+ if (rep == DialogResponse::CHOICE_YES)
+ CMusicLibraryQueue::GetInstance().ScanLibrary(source.strPath,
+ MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL, true);
+}
+
diff --git a/xbmc/music/windows/GUIWindowMusicBase.h b/xbmc/music/windows/GUIWindowMusicBase.h
new file mode 100644
index 0000000..85f4931
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicBase.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2005-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
+
+/*!
+\file GUIWindowMusicBase.h
+\brief
+*/
+
+#include "music/MusicDatabase.h"
+#include "music/MusicInfoLoader.h"
+#include "music/MusicThumbLoader.h"
+#include "music/infoscanner/MusicInfoScraper.h"
+#include "windows/GUIMediaWindow.h"
+
+#include <vector>
+
+enum MusicSelectAction
+{
+ MUSIC_SELECT_ACTION_PLAY,
+ MUSIC_SELECT_ACTION_RESUME,
+};
+
+/*!
+ \ingroup windows
+ \brief The base class for music windows
+
+ CGUIWindowMusicBase is the base class for
+ all music windows.
+ */
+class CGUIWindowMusicBase : public CGUIMediaWindow, public IBackgroundLoaderObserver
+{
+public:
+ CGUIWindowMusicBase(int id, const std::string &xmlFile);
+ ~CGUIWindowMusicBase(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+
+ void DoScan(const std::string &strPath, bool bRescan = false);
+ void RefreshContent(const std::string& strContent);
+
+ /*! \brief Once a music source is added, store source in library, and prompt
+ the user to scan this folder into the library
+ \param oldName the original music source name
+ \param source details of the music source (just added or edited)
+ */
+ static void OnAssignContent(const std::string& oldName, const CMediaSource& source);
+
+protected:
+ void OnInitWindow() override;
+ /*!
+ \brief Will be called when an popup context menu has been asked for
+ \param itemNumber List/thumb control item that has been clicked on
+ */
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ void GetNonContextButtons(CContextButtons &buttons);
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ /*!
+ \brief Overwrite to update your gui buttons (visible, enable,...)
+ */
+ void UpdateButtons() override;
+
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ virtual void OnRetrieveMusicInfo(CFileItemList& items);
+ void OnPrepareFileItems(CFileItemList& items) override;
+ void OnRipCD();
+ std::string GetStartFolder(const std::string &dir) override;
+ void OnItemLoaded(CFileItem* pItem) override {}
+
+ virtual void OnScan(int iItem, bool bPromptRescan = false);
+
+ bool CheckFilterAdvanced(CFileItemList &items) const override;
+ bool CanContainFilter(const std::string &strDirectory) const override;
+
+ bool OnSelect(int iItem) override;
+
+ // new methods
+ virtual void PlayItem(int iItem);
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+
+ void RetrieveMusicInfo();
+ void OnItemInfo(int iItem);
+ void OnItemInfoAll(const std::string& strPath, bool refresh = false);
+ virtual void OnQueueItem(int iItem, bool first = false);
+ enum ALLOW_SELECTION { SELECTION_ALLOWED = 0, SELECTION_AUTO, SELECTION_FORCED };
+
+ void OnRipTrack(int iItem);
+ void LoadPlayList(const std::string& strPlayList) override;
+ virtual void OnRemoveSource(int iItem);
+
+ typedef std::vector <CFileItem*>::iterator ivecItems; ///< CFileItem* vector Iterator
+ CGUIDialogProgress* m_dlgProgress; ///< Progress dialog
+
+ CMusicDatabase m_musicdatabase;
+ MUSIC_INFO::CMusicInfoLoader m_musicInfoLoader;
+
+ CMusicThumbLoader m_thumbLoader;
+};
diff --git a/xbmc/music/windows/GUIWindowMusicNav.cpp b/xbmc/music/windows/GUIWindowMusicNav.cpp
new file mode 100644
index 0000000..f8ba5a4
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicNav.cpp
@@ -0,0 +1,944 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicNav.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonSystemSettings.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/dialogs/GUIDialogInfoProviderSettings.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "video/windows/GUIWindowVideoNav.h"
+#include "view/GUIViewState.h"
+
+using namespace XFILE;
+using namespace PLAYLIST;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNTYPE 5
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_SEARCH 8
+#define CONTROL_FILTER 15
+#define CONTROL_BTNPARTYMODE 16
+#define CONTROL_BTNMANUALINFO 17
+#define CONTROL_BTN_FILTER 19
+
+#define CONTROL_UPDATE_LIBRARY 20
+
+CGUIWindowMusicNav::CGUIWindowMusicNav(void)
+ : CGUIWindowMusicBase(WINDOW_MUSIC_NAV, "MyMusicNav.xml")
+{
+ m_vecItems->SetPath("?");
+ m_searchWithEdit = false;
+}
+
+CGUIWindowMusicNav::~CGUIWindowMusicNav(void) = default;
+
+bool CGUIWindowMusicNav::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_RESET:
+ m_vecItems->SetPath("?");
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ /* We don't want to show Autosourced items (ie removable pendrives, memorycards) in Library mode */
+ m_rootDir.AllowNonLocalSources(false);
+
+ // is this the first time the window is opened?
+ if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty())
+ message.SetStringParam(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW));
+
+ if (!CGUIWindowMusicBase::OnMessage(message))
+ return false;
+
+ if (message.GetStringParam(0) != "")
+ {
+ CURL url(message.GetStringParam(0));
+
+ int i = 0;
+ for (; i < m_vecItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+
+ // skip ".."
+ if (pItem->IsParentFolder())
+ continue;
+
+ if (URIUtils::PathEquals(pItem->GetPath(), message.GetStringParam(0), true, true))
+ {
+ m_viewControl.SetSelectedItem(i);
+ i = -1;
+ if (url.GetOption("showinfo") == "true")
+ OnItemInfo(i);
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNPARTYMODE)
+ {
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+ else
+ {
+ if (!g_partyModeManager.Enable())
+ {
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE,false);
+ return false;
+ }
+
+ // Playlist directory is the root of the playlist window
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory("playlistmusic://");
+
+ return true;
+ }
+ UpdateButtons();
+ }
+ else if (iControl == CONTROL_SEARCH)
+ {
+ if (m_searchWithEdit)
+ {
+ // search updated - reset timer
+ m_searchTimer.StartZero();
+ // grab our search string
+ CGUIMessage selected(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SEARCH);
+ OnMessage(selected);
+ SetProperty("search", selected.GetLabel());
+ return true;
+ }
+ std::string search(GetProperty("search").asString());
+ CGUIKeyboardFactory::ShowAndGetFilter(search, true);
+ SetProperty("search", search);
+ return true;
+ }
+ else if (iControl == CONTROL_UPDATE_LIBRARY)
+ {
+ if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ CMusicLibraryQueue::GetInstance().ScanLibrary("");
+ else
+ CMusicLibraryQueue::GetInstance().StopLibraryScanning();
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_PLAYBACK_STOPPED:
+ case GUI_MSG_PLAYBACK_ENDED:
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ case GUI_MSG_PLAYBACK_STARTED:
+ {
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled());
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1() == GUI_MSG_SEARCH_UPDATE && IsActive())
+ {
+ // search updated - reset timer
+ m_searchTimer.StartZero();
+ SetProperty("search", message.GetStringParam());
+ }
+ }
+ }
+ return CGUIWindowMusicBase::OnMessage(message);
+}
+
+bool CGUIWindowMusicNav::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_SCAN_ITEM)
+ {
+ int item = m_viewControl.GetSelectedItem();
+ CMusicDatabaseDirectory dir;
+ if (item > -1 && m_vecItems->Get(item)->m_bIsFolder
+ && (m_vecItems->Get(item)->IsAlbum()||
+ dir.IsArtistDir(m_vecItems->Get(item)->GetPath())))
+ {
+ OnContextButton(item,CONTEXT_BUTTON_INFO);
+ return true;
+ }
+ }
+
+ return CGUIWindowMusicBase::OnAction(action);
+}
+
+bool CGUIWindowMusicNav::ManageInfoProvider(const CFileItemPtr& item)
+{
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(item->GetPath(), params);
+ // Management of Info provider only valid for specific artist or album items
+ if (params.GetAlbumId() == -1 && params.GetArtistId() == -1)
+ return false;
+
+ // Set things up for processing artist or albums
+ CONTENT_TYPE content = CONTENT_ALBUMS;
+ int id = params.GetAlbumId();
+ if (id == -1)
+ {
+ content = CONTENT_ARTISTS;
+ id = params.GetArtistId();
+ }
+
+ ADDON::ScraperPtr scraper;
+ // Get specific scraper and settings for current item or use default
+ if (!m_musicdatabase.GetScraper(id, content, scraper))
+ {
+ ADDON::AddonPtr defaultScraper;
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
+ ADDON::ScraperTypeFromContent(content), defaultScraper))
+ {
+ scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper);
+ }
+ }
+
+ // Set Information provider and settings
+ int applyto = CGUIDialogInfoProviderSettings::Show(scraper);
+ if (applyto >= 0)
+ {
+ bool result = false;
+ CVariant msgctxt;
+ switch (applyto)
+ {
+ case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_THISITEM: // Change information provider for specific item
+ result = m_musicdatabase.SetScraper(id, content, scraper);
+ break;
+ case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_ALLVIEW: // Change information provider for the filtered items shown on this node
+ {
+ msgctxt = 38069;
+ if (content == CONTENT_ARTISTS)
+ msgctxt = 38068;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{ 20195 }, msgctxt)) // Change information provider, confirm for all shown
+ {
+ // Set scraper for all items on current view.
+ std::string strPath = "musicdb://";
+ if (content == CONTENT_ARTISTS)
+ strPath += "artists";
+ else
+ strPath += "albums";
+ URIUtils::AddSlashAtEnd(strPath);
+ // Items on view could be limited by navigation criteria, smart playlist rules or a filter.
+ // Get these options, except ID, from item path
+ CURL musicUrl(item->GetPath()); //Use CURL, as CMusicDbUrl removes "filter" option
+ if (content == CONTENT_ARTISTS)
+ musicUrl.RemoveOption("artistid");
+ else
+ musicUrl.RemoveOption("albumid");
+ strPath += musicUrl.GetOptions();
+ result = m_musicdatabase.SetScraperAll(strPath, scraper);
+ }
+ }
+ break;
+ case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT: // Change information provider for all items
+ {
+ msgctxt = 38071;
+ if (content == CONTENT_ARTISTS)
+ msgctxt = 38070;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, msgctxt)) // Change information provider, confirm default and clear
+ {
+ // Save scraper addon default setting values
+ scraper->SaveSettings();
+ // Set default scraper
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (content == CONTENT_ARTISTS)
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, scraper->ID());
+ else
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, scraper->ID());
+ settings->Save();
+ // Clear all item specific settings
+ if (content == CONTENT_ARTISTS)
+ result = m_musicdatabase.SetScraperAll("musicdb://artists/", nullptr);
+ else
+ result = m_musicdatabase.SetScraperAll("musicdb://albums/", nullptr);
+ }
+ }
+ default:
+ break;
+ }
+ if (!result)
+ return false;
+
+ // Refresh additional information using the new settings
+ if (applyto == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_ALLVIEW || applyto == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT)
+ {
+ // Change information provider, all artists or albums
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, CVariant{38072}))
+ OnItemInfoAll(m_vecItems->GetPath(), true);
+ }
+ else
+ {
+ // Change information provider, selected artist or album
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, CVariant{38073}))
+ {
+ std::string itempath = StringUtils::Format("musicdb://albums/{}/", id);
+ if (content == CONTENT_ARTISTS)
+ itempath = StringUtils::Format("musicdb://artists/{}/", id);
+ OnItemInfoAll(itempath, true);
+ }
+ }
+ }
+ return true;
+}
+
+bool CGUIWindowMusicNav::OnClick(int iItem, const std::string &player /* = "" */)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return false;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (StringUtils::StartsWith(item->GetPath(), "musicsearch://"))
+ {
+ if (m_searchWithEdit)
+ OnSearchUpdate();
+ else
+ {
+ std::string search(GetProperty("search").asString());
+ CGUIKeyboardFactory::ShowAndGetFilter(search, true);
+ SetProperty("search", search);
+ }
+ return true;
+ }
+ if (item->IsMusicDb() && !item->m_bIsFolder)
+ m_musicdatabase.SetPropertiesForFileItem(*item);
+
+ if (item->IsPlayList() &&
+ !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders)
+ {
+ PlayItem(iItem);
+ return true;
+ }
+ return CGUIWindowMusicBase::OnClick(iItem, player);
+}
+
+bool CGUIWindowMusicNav::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (CGUIWindowMusicBase::Update(strDirectory, updateFilterPath))
+ {
+ m_thumbLoader.Load(*m_unfilteredItems);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIWindowMusicNav::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ if (strDirectory.empty())
+ AddSearchFolder();
+
+ bool bResult = CGUIWindowMusicBase::GetDirectory(strDirectory, items);
+ if (bResult)
+ {
+ if (items.IsPlayList())
+ OnRetrieveMusicInfo(items);
+ }
+
+ // update our content in the info manager
+ if (StringUtils::StartsWithNoCase(strDirectory, "videodb://") || items.IsVideoDb())
+ {
+ CVideoDatabaseDirectory dir;
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath());
+ if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS ||
+ node == VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS)
+ items.SetContent("musicvideos");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE)
+ items.SetContent("genres");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_COUNTRY)
+ items.SetContent("countries");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR)
+ items.SetContent("artists");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_DIRECTOR)
+ items.SetContent("directors");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_STUDIO)
+ items.SetContent("studios");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_YEAR)
+ items.SetContent("years");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM)
+ items.SetContent("albums");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_TAGS)
+ items.SetContent("tags");
+ else
+ items.SetContent("");
+ }
+ else if (StringUtils::StartsWithNoCase(strDirectory, "musicdb://") || items.IsMusicDb())
+ {
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath());
+ if (node == NODE_TYPE_ALBUM ||
+ node == NODE_TYPE_ALBUM_RECENTLY_ADDED ||
+ node == NODE_TYPE_ALBUM_RECENTLY_PLAYED ||
+ node == NODE_TYPE_ALBUM_TOP100 ||
+ node == NODE_TYPE_DISC) // ! @todo: own content type "discs"??
+ items.SetContent("albums");
+ else if (node == NODE_TYPE_ARTIST)
+ items.SetContent("artists");
+ else if (node == NODE_TYPE_SONG ||
+ node == NODE_TYPE_SONG_TOP100 ||
+ node == NODE_TYPE_SINGLES ||
+ node == NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS ||
+ node == NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS ||
+ node == NODE_TYPE_ALBUM_TOP100_SONGS)
+ items.SetContent("songs");
+ else if (node == NODE_TYPE_GENRE)
+ items.SetContent("genres");
+ else if (node == NODE_TYPE_SOURCE)
+ items.SetContent("sources");
+ else if (node == NODE_TYPE_ROLE)
+ items.SetContent("roles");
+ else if (node == NODE_TYPE_YEAR)
+ items.SetContent("years");
+ else
+ items.SetContent("");
+ }
+ else if (items.IsPlayList())
+ items.SetContent("songs");
+ else if (URIUtils::PathEquals(strDirectory, "special://musicplaylists/") ||
+ URIUtils::PathEquals(strDirectory, "library://music/playlists.xml/"))
+ items.SetContent("playlists");
+ else if (URIUtils::PathEquals(strDirectory, "plugin://music/"))
+ items.SetContent("plugins");
+ else if (items.IsAddonsPath())
+ items.SetContent("addons");
+ else if (!items.IsSourcesPath() && !items.IsVirtualDirectoryRoot() &&
+ !items.IsLibraryFolder() && !items.IsPlugin() && !items.IsSmartPlayList())
+ items.SetContent("files");
+
+ return bResult;
+}
+
+void CGUIWindowMusicNav::UpdateButtons()
+{
+ CGUIWindowMusicBase::UpdateButtons();
+
+ // Update object count
+ int iItems = m_vecItems->Size();
+ if (iItems)
+ {
+ // check for parent dir and "all" items
+ // should always be the first two items
+ for (int i = 0; i <= (iItems>=2 ? 1 : 0); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ if (pItem->IsParentFolder()) iItems--;
+ if (StringUtils::StartsWith(pItem->GetPath(), "/-1/")) iItems--;
+ }
+ // or the last item
+ if (m_vecItems->Size() > 2 &&
+ StringUtils::StartsWith(m_vecItems->Get(m_vecItems->Size()-1)->GetPath(), "/-1/"))
+ iItems--;
+ }
+ std::string items = StringUtils::Format("{} {}", iItems, g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+
+ // set the filter label
+ std::string strLabel;
+
+ // "Playlists"
+ if (m_vecItems->IsPath("special://musicplaylists/"))
+ strLabel = g_localizeStrings.Get(136);
+ // "{Playlist Name}"
+ else if (m_vecItems->IsPlayList())
+ {
+ // get playlist name from path
+ std::string strDummy;
+ URIUtils::Split(m_vecItems->GetPath(), strDummy, strLabel);
+ }
+ // everything else is from a musicdb:// path
+ else
+ {
+ CMusicDatabaseDirectory dir;
+ dir.GetLabel(m_vecItems->GetPath(), strLabel);
+ }
+
+ SET_CONTROL_LABEL(CONTROL_FILTER, strLabel);
+
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled());
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_UPDATE_LIBRARY, !m_vecItems->IsAddonsPath() && !m_vecItems->IsPlugin() && !m_vecItems->IsScript());
+}
+
+void CGUIWindowMusicNav::PlayItem(int iItem)
+{
+ // unlike additemtoplaylist, we need to check the items here
+ // before calling it since the current playlist will be stopped
+ // and cleared!
+
+ // root is not allowed
+ if (m_vecItems->IsVirtualDirectoryRoot() && !m_vecItems->Get(iItem)->IsDVD())
+ return;
+
+ CGUIWindowMusicBase::PlayItem(iItem);
+}
+
+void CGUIWindowMusicNav::OnWindowLoaded()
+{
+ const CGUIControl *control = GetControl(CONTROL_SEARCH);
+ m_searchWithEdit = (control && control->GetControlType() == CGUIControl::GUICONTROL_EDIT);
+
+ CGUIWindowMusicBase::OnWindowLoaded();
+
+ if (m_searchWithEdit)
+ {
+ SendMessage(GUI_MSG_SET_TYPE, CONTROL_SEARCH, CGUIEditControl::INPUT_TYPE_SEARCH);
+ SET_CONTROL_LABEL2(CONTROL_SEARCH, GetProperty("search").asString());
+ }
+}
+
+void CGUIWindowMusicNav::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ if (item)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // are we in the playlists location?
+ bool inPlaylists = m_vecItems->IsPath(CUtil::MusicPlaylistsLocation()) ||
+ m_vecItems->IsPath("special://musicplaylists/");
+
+ if (m_vecItems->IsPath("sources://music/"))
+ {
+ // get the usual music shares, and anything for all media windows
+ CGUIDialogContextMenu::GetContextButtons("music", item, buttons);
+#ifdef HAS_DVD_DRIVE
+ // enable Rip CD an audio disc
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive() && item->IsCDDA())
+ {
+ // those cds can also include Audio Tracks: CDExtra and MixedMode!
+ MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1))
+ {
+ if (CServiceBroker::GetJobManager()->IsProcessing("cdrip"))
+ buttons.Add(CONTEXT_BUTTON_CANCEL_RIP_CD, 14100);
+ else
+ buttons.Add(CONTEXT_BUTTON_RIP_CD, 600);
+ }
+ }
+#endif
+ // Scan button for music sources except ".." and "Add music source" items
+ if (!item->IsPath("add") && !item->IsParentFolder() &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser))
+ {
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13352);
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+ }
+ else
+ {
+ CGUIWindowMusicBase::GetContextButtons(itemNumber, buttons);
+
+ // Scan button for real folders containing files when navigating within music sources.
+ // Blacklist the bespoke Kodi protocols as to many valid external protocols to whitelist
+ if (m_vecItems->GetContent() == "files" && // Other content not scanned to library
+ !inPlaylists && !m_vecItems->IsInternetStream() && // Not playlists locations or streams
+ !item->IsPath("add") && !item->IsParentFolder() && // Not ".." and "Add items
+ item->m_bIsFolder && // Folders only, but playlists can be folders too
+ !URIUtils::IsLibraryContent(item->GetPath()) && // database folder or .xsp files
+ !URIUtils::IsSpecial(item->GetPath()) && !item->IsPlugin() && !item->IsScript() &&
+ !item->IsPlayList() && // .m3u etc. that as flagged as folders when playlistasfolders
+ !StringUtils::StartsWithNoCase(item->GetPath(), "addons://") &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() ||
+ g_passwordManager.bMasterUser))
+ {
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13352);
+ }
+
+ CMusicDatabaseDirectory dir;
+
+ if (!item->IsParentFolder() && !dir.IsAllItem(item->GetPath()))
+ {
+ if (item->m_bIsFolder && !item->IsVideoDb() &&
+ !item->IsPlugin() && !StringUtils::StartsWithNoCase(item->GetPath(), "musicsearch://"))
+ {
+ if (item->IsAlbum())
+ // enable query all albums button only in album view
+ buttons.Add(CONTEXT_BUTTON_INFO_ALL, 20059);
+ else if (dir.IsArtistDir(item->GetPath()))
+ // enable query all artist button only in artist view
+ buttons.Add(CONTEXT_BUTTON_INFO_ALL, 21884);
+
+ //Set default or clear default
+ NODE_TYPE nodetype = dir.GetDirectoryType(item->GetPath());
+ if (!inPlaylists &&
+ (nodetype == NODE_TYPE_ROOT ||
+ nodetype == NODE_TYPE_OVERVIEW ||
+ nodetype == NODE_TYPE_TOP100))
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!item->IsPath(settings->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW)))
+ buttons.Add(CONTEXT_BUTTON_SET_DEFAULT, 13335); // set default
+ if (!settings->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW).empty())
+ buttons.Add(CONTEXT_BUTTON_CLEAR_DEFAULT, 13403); // clear default
+ }
+
+ //Change information provider
+ if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "albums") ||
+ StringUtils::EqualsNoCase(m_vecItems->GetContent(), "artists"))
+ {
+ // we allow the user to set information provider for albums and artists
+ buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20195);
+ }
+ }
+ if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetArtistString().empty())
+ {
+ CVideoDatabase database;
+ database.Open();
+ if (database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString()) > -1)
+ buttons.Add(CONTEXT_BUTTON_GO_TO_ARTIST, 20400);
+ }
+ if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetArtistString().empty() &&
+ !item->GetMusicInfoTag()->GetAlbum().empty() &&
+ !item->GetMusicInfoTag()->GetTitle().empty())
+ {
+ CVideoDatabase database;
+ database.Open();
+ if (database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString(), item->GetMusicInfoTag()->GetAlbum(), item->GetMusicInfoTag()->GetTitle()) > -1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_OTHER, 20401);
+ }
+ if (item->HasVideoInfoTag() && !item->m_bIsFolder)
+ {
+ if ((profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) && !item->IsPlugin())
+ {
+ buttons.Add(CONTEXT_BUTTON_RENAME, 16105);
+ buttons.Add(CONTEXT_BUTTON_DELETE, 646);
+ }
+ }
+ if (inPlaylists && URIUtils::GetFileName(item->GetPath()) != "PartyMode.xsp"
+ && (item->IsPlayList() || item->IsSmartPlayList()))
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+
+ if (!item->IsReadOnly() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("filelists.allowfiledeletion"))
+ {
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+ buttons.Add(CONTEXT_BUTTON_RENAME, 118);
+ }
+ }
+ }
+ }
+ // noncontextual buttons
+
+ CGUIWindowMusicBase::GetNonContextButtons(buttons);
+}
+
+bool CGUIWindowMusicNav::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_INFO:
+ {
+ if (!item->IsVideoDb())
+ return CGUIWindowMusicBase::OnContextButton(itemNumber,button);
+
+ // music videos - artists
+ if (StringUtils::StartsWithNoCase(item->GetPath(), "videodb://musicvideos/artists/"))
+ {
+ int idArtist = m_musicdatabase.GetArtistByName(item->GetLabel());
+ if (idArtist == -1)
+ return false;
+ std::string path = StringUtils::Format("musicdb://artists/{}/", idArtist);
+ CArtist artist;
+ m_musicdatabase.GetArtist(idArtist, artist, false);
+ *item = CFileItem(artist);
+ item->SetPath(path);
+ CGUIWindowMusicBase::OnContextButton(itemNumber,button);
+ Refresh();
+ m_viewControl.SetSelectedItem(itemNumber);
+ return true;
+ }
+
+ // music videos - albums
+ if (StringUtils::StartsWithNoCase(item->GetPath(), "videodb://musicvideos/albums/"))
+ {
+ int idAlbum = m_musicdatabase.GetAlbumByName(item->GetLabel());
+ if (idAlbum == -1)
+ return false;
+ std::string path = StringUtils::Format("musicdb://albums/{}/", idAlbum);
+ CAlbum album;
+ m_musicdatabase.GetAlbum(idAlbum, album, false);
+ *item = CFileItem(path,album);
+ item->SetPath(path);
+ CGUIWindowMusicBase::OnContextButton(itemNumber,button);
+ Refresh();
+ m_viewControl.SetSelectedItem(itemNumber);
+ return true;
+ }
+
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty())
+ {
+ CGUIDialogVideoInfo::ShowFor(*item);
+ Refresh();
+ }
+ return true;
+ }
+
+ case CONTEXT_BUTTON_INFO_ALL:
+ OnItemInfoAll(m_vecItems->GetPath());
+ return true;
+
+ case CONTEXT_BUTTON_SET_DEFAULT:
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW, item->GetPath());
+ settings->Save();
+ return true;
+ }
+
+ case CONTEXT_BUTTON_CLEAR_DEFAULT:
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW, "");
+ settings->Save();
+ return true;
+ }
+
+ case CONTEXT_BUTTON_GO_TO_ARTIST:
+ {
+ std::string strPath;
+ CVideoDatabase database;
+ database.Open();
+ strPath = StringUtils::Format(
+ "videodb://musicvideos/artists/{}/",
+ database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString()));
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath);
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_OTHER:
+ {
+ CVideoDatabase database;
+ database.Open();
+ CVideoInfoTag details;
+ database.GetMusicVideoInfo("", details, database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString(), item->GetMusicInfoTag()->GetAlbum(), item->GetMusicInfoTag()->GetTitle()));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0,
+ static_cast<void*>(new CFileItem(details)));
+ return true;
+ }
+
+ case CONTEXT_BUTTON_RENAME:
+ if (!item->IsVideoDb() && !item->IsReadOnly())
+ OnRenameItem(itemNumber);
+
+ CGUIDialogVideoInfo::UpdateVideoItemTitle(item);
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ Refresh();
+ return true;
+
+ case CONTEXT_BUTTON_DELETE:
+ if (item->IsPlayList() || item->IsSmartPlayList())
+ {
+ item->m_bIsFolder = false;
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(item->GetPath()))
+ CFileUtils::DeleteItem(item);
+ }
+ else if (!item->IsVideoDb())
+ OnDeleteItem(itemNumber);
+ else
+ {
+ CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(item);
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ }
+ Refresh();
+ return true;
+
+ case CONTEXT_BUTTON_SET_CONTENT:
+ return ManageInfoProvider(item);
+
+ default:
+ break;
+ }
+
+ return CGUIWindowMusicBase::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowMusicNav::GetSongsFromPlayList(const std::string& strPlayList, CFileItemList &items)
+{
+ std::string strParentPath=m_history.GetParentPath();
+
+ if (m_guiState.get() && !m_guiState->HideParentDirItems())
+ {
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath(strParentPath);
+ items.Add(pItem);
+ }
+
+ items.SetPath(strPlayList);
+ CLog::Log(LOGDEBUG, "CGUIWindowMusicNav, opening playlist [{}]", strPlayList);
+
+ std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(strPlayList));
+ if (nullptr != pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return false; //hmmm unable to load playlist?
+ }
+ CPlayList playlist = *pPlayList;
+ // convert playlist items to songs
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ items.Add(playlist[i]);
+ }
+ }
+
+ return true;
+}
+
+void CGUIWindowMusicNav::OnSearchUpdate()
+{
+ std::string search(CURL::Encode(GetProperty("search").asString()));
+ if (!search.empty())
+ {
+ std::string path = "musicsearch://" + search + "/";
+ m_history.ClearSearchHistory();
+ Update(path);
+ }
+ else if (m_vecItems->IsVirtualDirectoryRoot())
+ {
+ Update("");
+ }
+}
+
+void CGUIWindowMusicNav::FrameMove()
+{
+ static const int search_timeout = 2000;
+ // update our searching
+ if (m_searchTimer.IsRunning() && m_searchTimer.GetElapsedMilliseconds() > search_timeout)
+ {
+ m_searchTimer.Stop();
+ OnSearchUpdate();
+ }
+ CGUIWindowMusicBase::FrameMove();
+}
+
+void CGUIWindowMusicNav::AddSearchFolder()
+{
+ // we use a general viewstate (and not our member) here as our
+ // current viewstate may be specific to some other folder, and
+ // we know we're in the root here
+ CFileItemList items;
+ CGUIViewState* viewState = CGUIViewState::GetViewState(GetID(), items);
+ if (viewState)
+ {
+ // add our remove the musicsearch source
+ VECSOURCES &sources = viewState->GetSources();
+ bool haveSearchSource = false;
+ bool needSearchSource = !GetProperty("search").empty() || !m_searchWithEdit; // we always need it if we don't have the edit control
+ for (IVECSOURCES it = sources.begin(); it != sources.end(); ++it)
+ {
+ CMediaSource& share = *it;
+ if (share.strPath == "musicsearch://")
+ {
+ haveSearchSource = true;
+ if (!needSearchSource)
+ { // remove it
+ sources.erase(it);
+ break;
+ }
+ }
+ }
+ if (!haveSearchSource && needSearchSource)
+ {
+ // add search share
+ CMediaSource share;
+ share.strName=g_localizeStrings.Get(137); // Search
+ share.strPath = "musicsearch://";
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ sources.push_back(share);
+ }
+ m_rootDir.SetSources(sources);
+ delete viewState;
+ }
+}
+
+std::string CGUIWindowMusicNav::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "genres")
+ return "musicdb://genres/";
+ else if (lower == "artists")
+ return "musicdb://artists/";
+ else if (lower == "albums")
+ return "musicdb://albums/";
+ else if (lower == "singles")
+ return "musicdb://singles/";
+ else if (lower == "songs")
+ return "musicdb://songs/";
+ else if (lower == "top100")
+ return "musicdb://top100/";
+ else if (lower == "top100songs")
+ return "musicdb://top100/songs/";
+ else if (lower == "top100albums")
+ return "musicdb://top100/albums/";
+ else if (lower == "recentlyaddedalbums")
+ return "musicdb://recentlyaddedalbums/";
+ else if (lower == "recentlyplayedalbums")
+ return "musicdb://recentlyplayedalbums/";
+ else if (lower == "compilations")
+ return "musicdb://compilations/";
+ else if (lower == "years")
+ return "musicdb://years/";
+ else if (lower == "files")
+ return "sources://music/";
+ else if (lower == "boxsets")
+ return "musicdb://boxsets/";
+
+ return CGUIWindowMusicBase::GetStartFolder(dir);
+}
diff --git a/xbmc/music/windows/GUIWindowMusicNav.h b/xbmc/music/windows/GUIWindowMusicNav.h
new file mode 100644
index 0000000..021eaad
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicNav.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicBase.h"
+#include "utils/Stopwatch.h"
+
+class CFileItemList;
+
+class CGUIWindowMusicNav : public CGUIWindowMusicBase
+{
+public:
+
+ CGUIWindowMusicNav(void);
+ ~CGUIWindowMusicNav(void) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void FrameMove() override;
+
+protected:
+ void OnItemLoaded(CFileItem* pItem) override {};
+ // override base class methods
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ void UpdateButtons() override;
+ void PlayItem(int iItem) override;
+ void OnWindowLoaded() override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnClick(int iItem, const std::string &player = "") override;
+ std::string GetStartFolder(const std::string &url) override;
+
+ bool GetSongsFromPlayList(const std::string& strPlayList, CFileItemList &items);
+ bool ManageInfoProvider(const CFileItemPtr& item);
+
+ VECSOURCES m_shares;
+
+ // searching
+ void OnSearchUpdate();
+ void AddSearchFolder();
+ CStopWatch m_searchTimer; ///< Timer to delay a search while more characters are entered
+ bool m_searchWithEdit; ///< Whether the skin supports the new edit control searching
+};
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylist.cpp b/xbmc/music/windows/GUIWindowMusicPlaylist.cpp
new file mode 100644
index 0000000..acc46f6
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylist.cpp
@@ -0,0 +1,715 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicPlaylist.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayListM3U.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/LabelFormatter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "view/GUIViewState.h"
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_BTNSHUFFLE 20
+#define CONTROL_BTNSAVE 21
+#define CONTROL_BTNCLEAR 22
+
+#define CONTROL_BTNPLAY 23
+#define CONTROL_BTNNEXT 24
+#define CONTROL_BTNPREVIOUS 25
+#define CONTROL_BTNREPEAT 26
+
+CGUIWindowMusicPlayList::CGUIWindowMusicPlayList(void)
+ : CGUIWindowMusicBase(WINDOW_MUSIC_PLAYLIST, "MyPlaylist.xml")
+{
+ m_musicInfoLoader.SetObserver(this);
+ m_movingFrom = -1;
+}
+
+CGUIWindowMusicPlayList::~CGUIWindowMusicPlayList(void) = default;
+
+bool CGUIWindowMusicPlayList::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_PLAYLISTPLAYER_REPEAT:
+ {
+ UpdateButtons();
+ }
+ break;
+
+ case GUI_MSG_PLAYLISTPLAYER_RANDOM:
+ case GUI_MSG_PLAYLIST_CHANGED:
+ {
+ // global playlist changed outside playlist window
+ UpdateButtons();
+
+ if (m_vecItemsUpdating)
+ {
+ CLog::Log(LOGWARNING, "CGUIWindowMusicPlayList::OnMessage - updating in progress");
+ return true;
+ }
+ CUpdateGuard ug(m_vecItemsUpdating);
+
+ Refresh(true);
+
+ if (m_viewControl.HasControl(m_iLastControl) && m_vecItems->Size() <= 0)
+ {
+ m_iLastControl = CONTROL_BTNVIEWASICONS;
+ SET_CONTROL_FOCUS(m_iLastControl, 0);
+ }
+
+ }
+ break;
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_musicInfoLoader.IsLoading())
+ m_musicInfoLoader.StopThread();
+
+ m_movingFrom = -1;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // Setup item cache for tagloader
+ m_musicInfoLoader.UseCacheOnHD("special://temp/archive_cache/MusicPlaylist.fi");
+
+ m_vecItems->SetPath("playlistmusic://");
+
+ // updatebuttons is called in here
+ if (!CGUIWindowMusicBase::OnMessage(message))
+ return false;
+
+ if (m_vecItems->Size() <= 0)
+ {
+ m_iLastControl = CONTROL_BTNVIEWASICONS;
+ SET_CONTROL_FOCUS(m_iLastControl, 0);
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSong >= 0 && iSong <= m_vecItems->Size())
+ m_viewControl.SetSelectedItem(iSong);
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNSHUFFLE)
+ {
+ if (!g_partyModeManager.IsEnabled())
+ {
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(
+ PLAYLIST::TYPE_MUSIC,
+ !(CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC)));
+ CMediaSettings::GetInstance().SetMusicPlaylistShuffled(
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ UpdateButtons();
+ Refresh();
+ }
+ }
+ else if (iControl == CONTROL_BTNSAVE)
+ {
+ if (m_musicInfoLoader.IsLoading()) // needed since we destroy m_vecitems to save memory
+ m_musicInfoLoader.StopThread();
+
+ SavePlayList();
+ }
+ else if (iControl == CONTROL_BTNCLEAR)
+ {
+ if (m_musicInfoLoader.IsLoading())
+ m_musicInfoLoader.StopThread();
+
+ ClearPlayList();
+ }
+ else if (iControl == CONTROL_BTNPLAY)
+ {
+ m_guiState->SetPlaylistDirectory("playlistmusic://");
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Play(m_viewControl.GetSelectedItem(), "");
+ UpdateButtons();
+ }
+ else if (iControl == CONTROL_BTNNEXT)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().PlayNext();
+ }
+ else if (iControl == CONTROL_BTNPREVIOUS)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().PlayPrevious();
+ }
+ else if (iControl == CONTROL_BTNREPEAT)
+ {
+ // increment repeat state
+ PLAYLIST::RepeatState state =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC);
+ if (state == PLAYLIST::RepeatState::NONE)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::ALL);
+ else if (state == PLAYLIST::RepeatState::ALL)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::ONE);
+ else
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::NONE);
+
+ // save settings
+ CMediaSettings::GetInstance().SetMusicPlaylistRepeat(
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC) ==
+ PLAYLIST::RepeatState::ALL);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ UpdateButtons();
+ }
+ else if (m_viewControl.HasControl(iControl))
+ {
+ int iAction = message.GetParam1();
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iAction == ACTION_DELETE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK)
+ {
+ RemovePlayListItem(iItem);
+ MarkPlaying();
+ }
+ }
+ }
+ break;
+
+ }
+ return CGUIWindowMusicBase::OnMessage(message);
+}
+
+bool CGUIWindowMusicPlayList::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR)
+ {
+ // Playlist has no parent dirs
+ return true;
+ }
+ if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+ if ((action.GetID() == ACTION_MOVE_ITEM_UP) || (action.GetID() == ACTION_MOVE_ITEM_DOWN))
+ {
+ int iItem = -1;
+ int iFocusedControl = GetFocusedControlID();
+ if (m_viewControl.HasControl(iFocusedControl))
+ iItem = m_viewControl.GetSelectedItem();
+ OnMove(iItem, action.GetID());
+ return true;
+ }
+ return CGUIWindowMusicBase::OnAction(action);
+}
+
+bool CGUIWindowMusicPlayList::OnBack(int actionID)
+{
+ CancelUpdateItems();
+
+ if (actionID == ACTION_NAV_BACK)
+ return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up
+ return CGUIWindowMusicBase::OnBack(actionID);
+}
+
+bool CGUIWindowMusicPlayList::MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate /* = true */)
+{
+ int iSelected = iItem;
+ int iNew = iSelected;
+ if (iAction == ACTION_MOVE_ITEM_UP)
+ iNew--;
+ else
+ iNew++;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // is the currently playing item affected?
+ bool bFixCurrentSong = false;
+ if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC) &&
+ appPlayer->IsPlayingAudio() &&
+ ((CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iSelected) ||
+ (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iNew)))
+ bFixCurrentSong = true;
+
+ PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC);
+ if (playlist.Swap(iSelected, iNew))
+ {
+ // Correct the current playing song in playlistplayer
+ if (bFixCurrentSong)
+ {
+ int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSelected == iCurrentSong)
+ iCurrentSong = iNew;
+ else if (iNew == iCurrentSong)
+ iCurrentSong = iSelected;
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong);
+ }
+
+ if (bUpdate)
+ Refresh();
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIWindowMusicPlayList::SavePlayList()
+{
+ std::string strNewFileName;
+ if (CGUIKeyboardFactory::ShowAndGetInput(strNewFileName, CVariant{g_localizeStrings.Get(16012)}, false))
+ {
+ // need 2 rename it
+ strNewFileName = CUtil::MakeLegalFileName(strNewFileName);
+ strNewFileName += ".m3u";
+ std::string strPath = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH),
+ "music",
+ strNewFileName);
+
+ // get selected item
+ int iItem = m_viewControl.GetSelectedItem();
+ std::string strSelectedItem = "";
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ if (!pItem->IsParentFolder())
+ {
+ GetDirectoryHistoryString(pItem.get(), strSelectedItem);
+ }
+ }
+
+ std::string strOldDirectory = m_vecItems->GetPath();
+ m_history.SetSelectedItem(strSelectedItem, strOldDirectory);
+
+ PLAYLIST::CPlayListM3U playlist;
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+
+ // Musicdatabase items should contain the real path instead of a musicdb url
+ // otherwise the user can't save and reuse the playlist when the musicdb gets deleted
+ if (pItem->IsMusicDb())
+ pItem->SetPath(pItem->GetMusicInfoTag()->GetURL());
+
+ playlist.Add(pItem);
+ }
+ CLog::Log(LOGDEBUG, "Saving music playlist: [{}]", strPath);
+ playlist.Save(strPath);
+ Refresh(); // need to update
+ }
+}
+
+void CGUIWindowMusicPlayList::ClearPlayList()
+{
+ ClearFileItems();
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ }
+ Refresh();
+ SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0);
+}
+
+void CGUIWindowMusicPlayList::RemovePlayListItem(int iItem)
+{
+ if (iItem < 0 || iItem > m_vecItems->Size()) return;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // The current playing song can't be removed
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC &&
+ appPlayer->IsPlayingAudio() && CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iItem)
+ return ;
+
+ CServiceBroker::GetPlaylistPlayer().Remove(PLAYLIST::TYPE_MUSIC, iItem);
+
+ Refresh();
+
+ if (m_vecItems->Size() <= 0)
+ {
+ SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0);
+ }
+ else
+ {
+ m_viewControl.SetSelectedItem(iItem);
+ }
+
+ g_partyModeManager.OnSongChange();
+}
+
+void CGUIWindowMusicPlayList::UpdateButtons()
+{
+ CGUIWindowMusicBase::UpdateButtons();
+
+ // Update playlist buttons
+ if (m_vecItems->Size() && !g_partyModeManager.IsEnabled())
+ {
+ CONTROL_ENABLE(CONTROL_BTNSHUFFLE);
+ CONTROL_ENABLE(CONTROL_BTNSAVE);
+ CONTROL_ENABLE(CONTROL_BTNCLEAR);
+ CONTROL_ENABLE(CONTROL_BTNREPEAT);
+ CONTROL_ENABLE(CONTROL_BTNPLAY);
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ CONTROL_ENABLE(CONTROL_BTNNEXT);
+ CONTROL_ENABLE(CONTROL_BTNPREVIOUS);
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_BTNNEXT);
+ CONTROL_DISABLE(CONTROL_BTNPREVIOUS);
+ }
+ }
+ else
+ {
+ // disable buttons if party mode is enabled too
+ CONTROL_DISABLE(CONTROL_BTNSHUFFLE);
+ CONTROL_DISABLE(CONTROL_BTNSAVE);
+ CONTROL_DISABLE(CONTROL_BTNCLEAR);
+ CONTROL_DISABLE(CONTROL_BTNREPEAT);
+ CONTROL_DISABLE(CONTROL_BTNPLAY);
+ CONTROL_DISABLE(CONTROL_BTNNEXT);
+ CONTROL_DISABLE(CONTROL_BTNPREVIOUS);
+ }
+
+ // update buttons
+ CONTROL_DESELECT(CONTROL_BTNSHUFFLE);
+ if (CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC))
+ CONTROL_SELECT(CONTROL_BTNSHUFFLE);
+
+ // update repeat button
+ int iLocalizedString;
+ PLAYLIST::RepeatState repState =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC);
+ if (repState == PLAYLIST::RepeatState::NONE)
+ iLocalizedString = 595; // Repeat: Off
+ else if (repState == PLAYLIST::RepeatState::ONE)
+ iLocalizedString = 596; // Repeat: One
+ else
+ iLocalizedString = 597; // Repeat: All
+
+ SET_CONTROL_LABEL(CONTROL_BTNREPEAT, g_localizeStrings.Get(iLocalizedString));
+
+ // Update object count label
+ std::string items =
+ StringUtils::Format("{} {}", m_vecItems->GetObjectCount(), g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+
+ MarkPlaying();
+}
+
+bool CGUIWindowMusicPlayList::OnPlayMedia(int iItem, const std::string &player)
+{
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Play(iItem);
+ else
+ {
+ PLAYLIST::Id playlistId = m_guiState->GetPlaylist();
+ if (playlistId != PLAYLIST::TYPE_NONE)
+ {
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory(m_vecItems->GetPath());
+
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Play(iItem, player);
+ }
+ else
+ {
+ // Reset Playlistplayer, playback started now does
+ // not use the playlistplayer.
+ CFileItemPtr pItem=m_vecItems->Get(iItem);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ g_application.PlayFile(*pItem, player);
+ }
+ }
+
+ return true;
+}
+
+void CGUIWindowMusicPlayList::OnItemLoaded(CFileItem* pItem)
+{
+ if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->Loaded())
+ { // set label 1+2 from tags
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ CLabelFormatter formatter(strTrack, "%D");
+ formatter.FormatLabels(pItem);
+ } // if (pItem->m_musicInfoTag.Loaded())
+ else
+ {
+ // Our tag may have a duration even if its not loaded
+ if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->GetDuration())
+ {
+ int nDuration = pItem->GetMusicInfoTag()->GetDuration();
+ if (nDuration > 0)
+ pItem->SetLabel2(StringUtils::SecondsToTimeString(nDuration));
+ }
+ else if (pItem->GetLabel() == "") // pls labels come in preformatted
+ {
+ // FIXME: get the position of the item in the playlist
+ // currently it is hacked into m_iprogramCount
+
+ // No music info and it's not CDDA so we'll just show the filename
+ std::string str;
+ str = CUtil::GetTitleFromPath(pItem->GetPath());
+ str = StringUtils::Format("{:02}. {} ", pItem->m_iprogramCount, str);
+ pItem->SetLabel(str);
+ }
+ }
+}
+
+bool CGUIWindowMusicPlayList::Update(const std::string& strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_musicInfoLoader.IsLoading())
+ m_musicInfoLoader.StopThread();
+
+ if (!CGUIWindowMusicBase::Update(strDirectory, updateFilterPath))
+ return false;
+
+ if (m_vecItems->GetContent().empty())
+ m_vecItems->SetContent("songs");
+
+ m_musicInfoLoader.Load(*m_vecItems);
+ return true;
+}
+
+void CGUIWindowMusicPlayList::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ // is this playlist playing?
+ int itemPlaying = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ {
+ CFileItemPtr item;
+ item = m_vecItems->Get(itemNumber);
+
+ if (m_movingFrom >= 0)
+ {
+ // we can move the item to any position not where we are, and any position not above currently
+ // playing item in party mode
+ if (itemNumber != m_movingFrom && (!g_partyModeManager.IsEnabled() || itemNumber > itemPlaying))
+ buttons.Add(CONTEXT_BUTTON_MOVE_HERE, 13252); // move item here
+ buttons.Add(CONTEXT_BUTTON_CANCEL_MOVE, 13253);
+ }
+ else
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // aren't in a move
+ // check what players we have, if we have multiple display play with option
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ if (players.size() > 1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With...
+
+ if (itemNumber > (g_partyModeManager.IsEnabled() ? 1 : 0))
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332);
+ if (itemNumber + 1 < m_vecItems->Size())
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333);
+ if (!g_partyModeManager.IsEnabled() || itemNumber != itemPlaying)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM, 13251);
+ if (itemNumber != itemPlaying)
+ buttons.Add(CONTEXT_BUTTON_DELETE, 1210); // Remove
+ }
+ }
+
+ if (g_partyModeManager.IsEnabled())
+ {
+ buttons.Add(CONTEXT_BUTTON_EDIT_PARTYMODE, 21439);
+ buttons.Add(CONTEXT_BUTTON_CANCEL_PARTYMODE, 588); // cancel party mode
+ }
+}
+
+bool CGUIWindowMusicPlayList::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_PLAY_WITH:
+ {
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ if (!item)
+ break;
+
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ OnClick(itemNumber, player);
+ return true;
+ }
+ case CONTEXT_BUTTON_MOVE_ITEM:
+ m_movingFrom = itemNumber;
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_HERE:
+ MoveItem(m_movingFrom, itemNumber);
+ m_movingFrom = -1;
+ return true;
+
+ case CONTEXT_BUTTON_CANCEL_MOVE:
+ m_movingFrom = -1;
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_ITEM_UP:
+ OnMove(itemNumber, ACTION_MOVE_ITEM_UP);
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_ITEM_DOWN:
+ OnMove(itemNumber, ACTION_MOVE_ITEM_DOWN);
+ return true;
+
+ case CONTEXT_BUTTON_DELETE:
+ RemovePlayListItem(itemNumber);
+ return true;
+
+ case CONTEXT_BUTTON_CANCEL_PARTYMODE:
+ g_partyModeManager.Disable();
+ return true;
+
+ case CONTEXT_BUTTON_EDIT_PARTYMODE:
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string playlist = profileManager->GetUserDataItem("PartyMode.xsp");
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist))
+ {
+ // apply new rules
+ g_partyModeManager.Disable();
+ g_partyModeManager.Enable();
+ }
+ return true;
+ }
+
+ default:
+ break;
+ }
+ return CGUIWindowMusicBase::OnContextButton(itemNumber, button);
+}
+
+
+void CGUIWindowMusicPlayList::OnMove(int iItem, int iAction)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return;
+
+ bool bRestart = m_musicInfoLoader.IsLoading();
+ if (bRestart)
+ m_musicInfoLoader.StopThread();
+
+ MoveCurrentPlayListItem(iItem, iAction);
+
+ if (bRestart)
+ m_musicInfoLoader.Load(*m_vecItems);
+}
+
+void CGUIWindowMusicPlayList::MoveItem(int iStart, int iDest)
+{
+ if (iStart < 0 || iStart >= m_vecItems->Size()) return;
+ if (iDest < 0 || iDest >= m_vecItems->Size()) return;
+
+ // default to move up
+ int iAction = ACTION_MOVE_ITEM_UP;
+ int iDirection = -1;
+ // are we moving down?
+ if (iStart < iDest)
+ {
+ iAction = ACTION_MOVE_ITEM_DOWN;
+ iDirection = 1;
+ }
+
+ bool bRestart = m_musicInfoLoader.IsLoading();
+ if (bRestart)
+ m_musicInfoLoader.StopThread();
+
+ // keep swapping until you get to the destination or you
+ // hit the currently playing song
+ int i = iStart;
+ while (i != iDest)
+ {
+ // try to swap adjacent items
+ if (MoveCurrentPlayListItem(i, iAction, false))
+ i = i + (1 * iDirection);
+ // we hit currently playing song, so abort
+ else
+ break;
+ }
+ Refresh();
+
+ if (bRestart)
+ m_musicInfoLoader.Load(*m_vecItems);
+}
+
+void CGUIWindowMusicPlayList::MarkPlaying()
+{
+ /* // clear markings
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ m_vecItems->Get(i)->Select(false);
+
+ // mark the currently playing item
+ if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == TYPE_MUSIC) && (g_application.GetAppPlayer().IsPlayingAudio()))
+ {
+ int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSong >= 0 && iSong <= m_vecItems->Size())
+ m_vecItems->Get(iSong)->Select(true);
+ }*/
+}
+
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylist.h b/xbmc/music/windows/GUIWindowMusicPlaylist.h
new file mode 100644
index 0000000..019a1c7
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylist.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicBase.h"
+
+class CGUIWindowMusicPlayList : public CGUIWindowMusicBase
+{
+public:
+ CGUIWindowMusicPlayList(void);
+ ~CGUIWindowMusicPlayList(void) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+
+ void RemovePlayListItem(int iItem);
+ void MoveItem(int iStart, int iDest);
+
+protected:
+ bool GoParentFolder() override { return false; }
+ void UpdateButtons() override;
+ void OnItemLoaded(CFileItem* pItem) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void OnMove(int iItem, int iAction);
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+
+ void SavePlayList();
+ void ClearPlayList();
+ void MarkPlaying();
+
+ bool MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate = true);
+
+ int m_movingFrom;
+ VECSOURCES m_shares;
+};
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp
new file mode 100644
index 0000000..393923d
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicPlaylistEditor.h"
+
+#include "Autorun.h"
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/PlaylistFileDirectory.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "music/MusicUtils.h"
+#include "playlists/PlayListM3U.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_LOAD_PLAYLIST 6
+#define CONTROL_SAVE_PLAYLIST 7
+#define CONTROL_CLEAR_PLAYLIST 8
+
+#define CONTROL_LIST 50
+#define CONTROL_PLAYLIST 100
+#define CONTROL_LABEL_PLAYLIST 101
+
+CGUIWindowMusicPlaylistEditor::CGUIWindowMusicPlaylistEditor(void)
+ : CGUIWindowMusicBase(WINDOW_MUSIC_PLAYLIST_EDITOR, "MyMusicPlaylistEditor.xml")
+{
+ m_playlistThumbLoader.SetObserver(this);
+ m_playlist = new CFileItemList;
+}
+
+CGUIWindowMusicPlaylistEditor::~CGUIWindowMusicPlaylistEditor(void)
+{
+ delete m_playlist;
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnBack(int actionID)
+{
+ if (actionID == ACTION_NAV_BACK && !m_viewControl.HasControl(GetFocusedControlID()))
+ return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up
+ return CGUIWindowMusicBase::OnBack(actionID);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_CONTEXT_MENU)
+ {
+ int iControl = GetFocusedControlID();
+ if (iControl == CONTROL_PLAYLIST)
+ {
+ OnPlaylistContext();
+ return true;
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ OnSourcesContext();
+ return true;
+ }
+ }
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnClick(int iItem, const std::string& player /* = "" */)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return false;
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ // Expand .m3u files in sources list when clicked on regardless of <playlistasfolders>
+ if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ return Update(item->GetPath());
+ // Avoid playback (default click behaviour) of media files
+ if (!item->m_bIsFolder)
+ return false;
+
+ return CGUIWindowMusicBase::OnClick(iItem, player);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ if (m_playlistThumbLoader.IsLoading())
+ m_playlistThumbLoader.StopThread();
+ CGUIWindowMusicBase::OnMessage(message);
+ return true;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ if (m_vecItems->GetPath() == "?")
+ m_vecItems->SetPath("");
+ CGUIWindowMusicBase::OnMessage(message);
+
+ if (message.GetNumStringParams())
+ LoadPlaylist(message.GetStringParam());
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA)
+ DeleteRemoveableMediaDirectoryCache();
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int control = message.GetSenderId();
+ if (control == CONTROL_PLAYLIST)
+ {
+ int item = GetCurrentPlaylistItem();
+ int action = message.GetParam1();
+ if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK)
+ OnPlaylistContext();
+ else if (action == ACTION_QUEUE_ITEM || action == ACTION_DELETE_ITEM ||
+ action == ACTION_MOUSE_MIDDLE_CLICK)
+ OnDeletePlaylistItem(item);
+ else if (action == ACTION_MOVE_ITEM_UP)
+ OnMovePlaylistItem(item, -1);
+ else if (action == ACTION_MOVE_ITEM_DOWN)
+ OnMovePlaylistItem(item, 1);
+ return true;
+ }
+ else if (control == CONTROL_LIST)
+ {
+ int action = message.GetParam1();
+ if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ OnSourcesContext();
+ return true;
+ }
+ }
+ else if (control == CONTROL_LOAD_PLAYLIST)
+ { // load a playlist
+ OnLoadPlaylist();
+ return true;
+ }
+ else if (control == CONTROL_SAVE_PLAYLIST)
+ { // save the playlist
+ OnSavePlaylist();
+ return true;
+ }
+ else if (control == CONTROL_CLEAR_PLAYLIST)
+ { // clear the playlist
+ ClearPlaylist();
+ return true;
+ }
+ }
+ break;
+ }
+
+ return CGUIWindowMusicBase::OnMessage(message);
+}
+
+bool CGUIWindowMusicPlaylistEditor::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ items.Clear();
+ if (strDirectory.empty())
+ { // root listing - list files:// and musicdb://
+ CFileItemPtr files(new CFileItem("sources://music/", true));
+ files->SetLabel(g_localizeStrings.Get(744));
+ files->SetLabelPreformatted(true);
+ files->m_bIsShareOrDrive = true;
+ items.Add(files);
+
+ CFileItemPtr mdb(new CFileItem("library://music/", true));
+ mdb->SetLabel(g_localizeStrings.Get(14022));
+ mdb->SetLabelPreformatted(true);
+ mdb->m_bIsShareOrDrive = true;
+ items.SetPath("");
+ items.Add(mdb);
+
+ CFileItemPtr vdb(new CFileItem("videodb://musicvideos/", true));
+ vdb->SetLabel(g_localizeStrings.Get(20389));
+ vdb->SetLabelPreformatted(true);
+ vdb->m_bIsShareOrDrive = true;
+ items.SetPath("");
+ items.Add(vdb);
+
+ return true;
+ }
+
+ if (!CGUIWindowMusicBase::GetDirectory(strDirectory, items))
+ return false;
+
+ // check for .CUE files here.
+ items.FilterCueItems();
+
+ return true;
+}
+
+void CGUIWindowMusicPlaylistEditor::OnPrepareFileItems(CFileItemList &items)
+{
+ CGUIWindowMusicBase::OnPrepareFileItems(items);
+
+ RetrieveMusicInfo();
+}
+
+void CGUIWindowMusicPlaylistEditor::UpdateButtons()
+{
+ CGUIWindowMusicBase::UpdateButtons();
+
+ // Update object count label
+ std::string items = StringUtils::Format("{} {}", m_vecItems->GetObjectCount(),
+ g_localizeStrings.Get(127)); // " 14 Objects"
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+}
+
+void CGUIWindowMusicPlaylistEditor::DeleteRemoveableMediaDirectoryCache()
+{
+ CUtil::DeleteDirectoryCache("r-");
+}
+
+void CGUIWindowMusicPlaylistEditor::PlayItem(int iItem)
+{
+ // unlike additemtoplaylist, we need to check the items here
+ // before calling it since the current playlist will be stopped
+ // and cleared!
+
+ // we're at the root source listing
+ if (m_vecItems->IsVirtualDirectoryRoot() && !m_vecItems->Get(iItem)->IsDVD())
+ return;
+
+#ifdef HAS_DVD_DRIVE
+ if (m_vecItems->Get(iItem)->IsDVD())
+ MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems->Get(iItem)->GetPath());
+ else
+#endif
+ CGUIWindowMusicBase::PlayItem(iItem);
+}
+
+void CGUIWindowMusicPlaylistEditor::OnQueueItem(int iItem, bool)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ // add this item to our playlist. We make a new copy here as we may be rendering them side by side,
+ // and thus want a different layout for each item
+ CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem)));
+ CFileItemList newItems;
+ MUSIC_UTILS::GetItemsForPlayList(item, newItems);
+ AppendToPlaylist(newItems);
+}
+
+bool CGUIWindowMusicPlaylistEditor::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath))
+ return false;
+
+ m_vecItems->SetContent("files");
+ m_thumbLoader.Load(*m_vecItems);
+
+ // update our playlist control
+ UpdatePlaylist();
+ return true;
+}
+
+void CGUIWindowMusicPlaylistEditor::ClearPlaylist()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PLAYLIST);
+ OnMessage(msg);
+
+ m_playlist->Clear();
+}
+
+void CGUIWindowMusicPlaylistEditor::UpdatePlaylist()
+{
+ if (m_playlistThumbLoader.IsLoading())
+ m_playlistThumbLoader.StopThread();
+
+ // deselect all items
+ for (int i = 0; i < m_playlist->Size(); i++)
+ m_playlist->Get(i)->Select(false);
+
+ // bind them to the list
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PLAYLIST, 0, 0, m_playlist);
+ OnMessage(msg);
+
+ // indicate how many songs we have
+ std::string items = StringUtils::Format("{} {}", m_playlist->Size(),
+ g_localizeStrings.Get(134)); // "123 Songs"
+ SET_CONTROL_LABEL(CONTROL_LABEL_PLAYLIST, items);
+
+ m_playlistThumbLoader.Load(*m_playlist);
+}
+
+int CGUIWindowMusicPlaylistEditor::GetCurrentPlaylistItem()
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PLAYLIST);
+ OnMessage(msg);
+ int item = msg.GetParam1();
+ if (item > m_playlist->Size())
+ return -1;
+ return item;
+}
+
+void CGUIWindowMusicPlaylistEditor::OnDeletePlaylistItem(int item)
+{
+ if (item < 0) return;
+ m_playlist->Remove(item);
+ UpdatePlaylist();
+ // select the next item
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PLAYLIST, item);
+ OnMessage(msg);
+}
+
+void CGUIWindowMusicPlaylistEditor::OnMovePlaylistItem(int item, int direction)
+{
+ if (item < 0) return;
+ if (item + direction >= m_playlist->Size() || item + direction < 0)
+ return;
+ m_playlist->Swap(item, item + direction);
+ UpdatePlaylist();
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PLAYLIST, item + direction);
+ OnMessage(msg);
+}
+
+void CGUIWindowMusicPlaylistEditor::OnLoadPlaylist()
+{
+ // Prompt user for file to load from music playlists folder
+ std::string playlist;
+ if (CGUIDialogFileBrowser::ShowAndGetFile("special://musicplaylists/",
+ ".m3u|.pls|.b4s|.wpl|.xspf", g_localizeStrings.Get(656),
+ playlist))
+ LoadPlaylist(playlist);
+}
+
+void CGUIWindowMusicPlaylistEditor::LoadPlaylist(const std::string &playlist)
+{
+ const CURL pathToUrl(playlist);
+ if (pathToUrl.IsProtocol("newplaylist"))
+ {
+ ClearPlaylist();
+ m_strLoadedPlaylist.clear();
+ return;
+ }
+
+ XFILE::CPlaylistFileDirectory dir;
+ CFileItemList items;
+ if (dir.GetDirectory(pathToUrl, items))
+ {
+ ClearPlaylist();
+ AppendToPlaylist(items);
+ m_strLoadedPlaylist = playlist;
+ }
+}
+
+void CGUIWindowMusicPlaylistEditor::OnSavePlaylist()
+{
+ // saves playlist to the playlist folder
+ std::string name = URIUtils::GetFileName(m_strLoadedPlaylist);
+ URIUtils::RemoveExtension(name);
+
+ if (CGUIKeyboardFactory::ShowAndGetInput(name, CVariant{g_localizeStrings.Get(16012)}, false))
+ { // save playlist as an .m3u
+ PLAYLIST::CPlayListM3U playlist;
+ playlist.Add(*m_playlist);
+ std::string path = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH),
+ "music",
+ name + ".m3u");
+
+ playlist.Save(path);
+ m_strLoadedPlaylist = name;
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(559), // "Playlist"
+ g_localizeStrings.Get(35259)); // "Saved"
+ }
+}
+
+void CGUIWindowMusicPlaylistEditor::AppendToPlaylist(CFileItemList &newItems)
+{
+ OnRetrieveMusicInfo(newItems);
+ FormatItemLabels(newItems, LABEL_MASKS(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT), "%D", "%L", ""));
+ m_playlist->Append(newItems);
+ UpdatePlaylist();
+}
+
+void CGUIWindowMusicPlaylistEditor::OnSourcesContext()
+{
+ CFileItemPtr item = GetCurrentListItem();
+ CContextButtons buttons;
+ if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ buttons.Add(CONTEXT_BUTTON_BROWSE_INTO, 37015); //Browse into
+ if (item && !item->IsParentFolder() && !m_vecItems->IsVirtualDirectoryRoot())
+ buttons.Add(CONTEXT_BUTTON_QUEUE_ITEM, 15019); // Add (to playlist)
+
+ int btnid = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (btnid == CONTEXT_BUTTON_QUEUE_ITEM)
+ OnQueueItem(m_viewControl.GetSelectedItem(), false);
+ else if (btnid == CONTEXT_BUTTON_BROWSE_INTO)
+ Update(item->GetPath());
+}
+
+void CGUIWindowMusicPlaylistEditor::OnPlaylistContext()
+{
+ int item = GetCurrentPlaylistItem();
+ CContextButtons buttons;
+ if (item > 0)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332);
+ if (item >= 0 && item < m_playlist->Size() - 1)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333);
+ if (item >= 0)
+ buttons.Add(CONTEXT_BUTTON_DELETE, 1210);
+
+ int btnid = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (btnid == CONTEXT_BUTTON_MOVE_ITEM_UP)
+ OnMovePlaylistItem(item, -1);
+ else if (btnid == CONTEXT_BUTTON_MOVE_ITEM_DOWN)
+ OnMovePlaylistItem(item, 1);
+ else if (btnid == CONTEXT_BUTTON_DELETE)
+ OnDeletePlaylistItem(item);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_QUEUE_ITEM:
+ OnQueueItem(itemNumber);
+ return true;
+
+ default:
+ break;
+ }
+
+ return CGUIWindowMusicBase::OnContextButton(itemNumber, button);
+}
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h
new file mode 100644
index 0000000..3859b55
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowMusicBase.h"
+
+class CFileItemList;
+
+class CGUIWindowMusicPlaylistEditor : public CGUIWindowMusicBase
+{
+public:
+ CGUIWindowMusicPlaylistEditor(void);
+ ~CGUIWindowMusicPlaylistEditor(void) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnClick(int iItem, const std::string &player = "") override;
+ bool OnBack(int actionID) override;
+
+protected:
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ void UpdateButtons() override;
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ void OnPrepareFileItems(CFileItemList &items) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void OnQueueItem(int iItem, bool first = false) override;
+ std::string GetStartFolder(const std::string& dir) override { return ""; }
+
+ void OnSourcesContext();
+ void OnPlaylistContext();
+ int GetCurrentPlaylistItem();
+ void OnDeletePlaylistItem(int item);
+ void UpdatePlaylist();
+ void ClearPlaylist();
+ void OnSavePlaylist();
+ void OnLoadPlaylist();
+ void AppendToPlaylist(CFileItemList &newItems);
+ void OnMovePlaylistItem(int item, int direction);
+
+ void LoadPlaylist(const std::string &playlist);
+
+ // new method
+ void PlayItem(int iItem) override;
+
+ void DeleteRemoveableMediaDirectoryCache();
+
+ CMusicThumbLoader m_playlistThumbLoader;
+
+ CFileItemList* m_playlist;
+ std::string m_strLoadedPlaylist;
+};
diff --git a/xbmc/music/windows/GUIWindowVisualisation.cpp b/xbmc/music/windows/GUIWindowVisualisation.cpp
new file mode 100644
index 0000000..805c2df
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowVisualisation.cpp
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2005-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 "GUIWindowVisualisation.h"
+
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIDialog.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace MUSIC_INFO;
+
+#define START_FADE_LENGTH 2.0f // 2 seconds on startup
+
+#define CONTROL_VIS 2
+
+CGUIWindowVisualisation::CGUIWindowVisualisation(void)
+ : CGUIWindow(WINDOW_VISUALISATION, "MusicVisualisation.xml")
+{
+ m_bShowPreset = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CGUIWindowVisualisation::OnAction(const CAction &action)
+{
+ bool passToVis = false;
+ switch (action.GetID())
+ {
+ case ACTION_VIS_PRESET_NEXT:
+ case ACTION_VIS_PRESET_PREV:
+ case ACTION_VIS_PRESET_RANDOM:
+ case ACTION_VIS_RATE_PRESET_PLUS:
+ case ACTION_VIS_RATE_PRESET_MINUS:
+ passToVis = true;
+ break;
+
+ case ACTION_SHOW_INFO:
+ {
+ m_initTimer.Stop();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS,
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().ToggleShowInfo());
+ return true;
+ }
+ break;
+
+ case ACTION_SHOW_OSD:
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_MUSIC_OSD);
+ return true;
+
+ case ACTION_SHOW_GUI:
+ // save the settings
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ break;
+
+ case ACTION_VIS_PRESET_LOCK:
+ { // show the locked icon + fall through so that the vis handles the locking
+ if (!m_bShowPreset)
+ {
+ m_lockedTimer.StartZero();
+ }
+ passToVis = true;
+ }
+ break;
+ case ACTION_VIS_PRESET_SHOW:
+ {
+ if (!m_lockedTimer.IsRunning() || m_bShowPreset)
+ m_bShowPreset = !m_bShowPreset;
+ return true;
+ }
+ break;
+
+ case ACTION_DECREASE_RATING:
+ case ACTION_INCREASE_RATING:
+ {
+ // actual action is taken care of in CApplication::OnAction()
+ m_initTimer.StartZero();
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true);
+ }
+ break;
+ //! @todo These should be mapped to its own function - at the moment it's overriding
+ //! the global action of fastforward/rewind and OSD.
+/* case KEY_BUTTON_Y:
+ g_application.m_CdgParser.Pause();
+ return true;
+ break;
+
+ case ACTION_ANALOG_FORWARD:
+ // calculate the speed based on the amount the button is held down
+ if (action.GetAmount())
+ {
+ float AVDelay = g_application.m_CdgParser.GetAVDelay();
+ g_application.m_CdgParser.SetAVDelay(AVDelay - action.GetAmount() / 4.0f);
+ return true;
+ }
+ break;*/
+ }
+
+ if (passToVis)
+ {
+ CGUIControl *control = GetControl(CONTROL_VIS);
+ if (control)
+ return control->OnAction(action);
+ }
+
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIWindowVisualisation::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_GET_VISUALISATION:
+ case GUI_MSG_VISUALISATION_RELOAD:
+ case GUI_MSG_PLAYBACK_STARTED:
+ {
+ CGUIControl *control = GetControl(CONTROL_VIS);
+ if (control)
+ return control->OnMessage(message);
+ }
+ break;
+ case GUI_MSG_VISUALISATION_ACTION:
+ {
+ CAction action(message.GetParam1());
+ return OnAction(action);
+ }
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (IsActive()) // save any changed settings from the OSD
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ // close all active modal dialogs
+ CServiceBroker::GetGUI()->GetWindowManager().CloseInternalModalDialogs(true);
+ }
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // check whether we've come back here from a window during which time we've actually
+ // stopped playing music
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (message.GetParam1() == WINDOW_INVALID && !appPlayer->IsPlayingAudio())
+ { // why are we here if nothing is playing???
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+
+ // hide or show the preset button(s)
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true); // always show the info initially.
+ CGUIWindow::OnMessage(message);
+ if (infoMgr.GetCurrentSongTag())
+ m_tag = *infoMgr.GetCurrentSongTag();
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS))
+ { // always on
+ m_initTimer.Stop();
+ }
+ else
+ {
+ // start display init timer (fade out after 3 secs...)
+ m_initTimer.StartZero();
+ }
+ return true;
+ }
+ }
+ return CGUIWindow::OnMessage(message);
+}
+
+EVENT_RESULT CGUIWindowVisualisation::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_RIGHT_CLICK)
+ { // no control found to absorb this click - go back to GUI
+ OnAction(CAction(ACTION_SHOW_GUI));
+ return EVENT_RESULT_HANDLED;
+ }
+ if (event.m_id == ACTION_GESTURE_NOTIFY)
+ return EVENT_RESULT_UNHANDLED;
+ if (event.m_id != ACTION_MOUSE_MOVE || event.m_offsetX || event.m_offsetY)
+ { // some other mouse action has occurred - bring up the OSD
+ CGUIDialog *pOSD = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_MUSIC_OSD);
+ if (pOSD)
+ {
+ pOSD->SetAutoClose(3000);
+ pOSD->Open();
+ }
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIWindowVisualisation::FrameMove()
+{
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ // check for a tag change
+ const CMusicInfoTag* tag = infoMgr.GetCurrentSongTag();
+ if (tag && *tag != m_tag)
+ { // need to fade in then out again
+ m_tag = *tag;
+ // fade in
+ m_initTimer.StartZero();
+ infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true);
+ }
+ if (m_initTimer.IsRunning() && m_initTimer.GetElapsedSeconds() > (float)CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_songInfoDuration)
+ {
+ m_initTimer.Stop();
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS))
+ { // reached end of fade in, fade out again
+ infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(false);
+ }
+ }
+ // show or hide the locked texture
+ if (m_lockedTimer.IsRunning() && m_lockedTimer.GetElapsedSeconds() > START_FADE_LENGTH)
+ {
+ m_lockedTimer.Stop();
+ }
+ CGUIWindow::FrameMove();
+}
diff --git a/xbmc/music/windows/GUIWindowVisualisation.h b/xbmc/music/windows/GUIWindowVisualisation.h
new file mode 100644
index 0000000..ce9a198
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowVisualisation.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2005-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 "guilib/GUIWindow.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/Stopwatch.h"
+
+class CGUIWindowVisualisation :
+ public CGUIWindow
+{
+public:
+ CGUIWindowVisualisation(void);
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ void FrameMove() override;
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+
+ CStopWatch m_initTimer;
+ CStopWatch m_lockedTimer;
+ bool m_bShowPreset;
+ MUSIC_INFO::CMusicInfoTag m_tag; // current tag info, for finding when the info manager updates
+};
+
diff --git a/xbmc/music/windows/MusicFileItemListModifier.cpp b/xbmc/music/windows/MusicFileItemListModifier.cpp
new file mode 100644
index 0000000..c78de79
--- /dev/null
+++ b/xbmc/music/windows/MusicFileItemListModifier.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016-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 "MusicFileItemListModifier.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDbUrl.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+bool CMusicFileItemListModifier::CanModify(const CFileItemList &items) const
+{
+ if (items.IsMusicDb())
+ return true;
+
+ return false;
+}
+
+bool CMusicFileItemListModifier::Modify(CFileItemList &items) const
+{
+ AddQueuingFolder(items);
+ return true;
+}
+
+// Add an "* All ..." folder to the CFileItemList
+// depending on the child node
+void CMusicFileItemListModifier::AddQueuingFolder(CFileItemList& items)
+{
+ if (!items.IsMusicDb())
+ return;
+
+ auto directoryNode = CDirectoryNode::ParseURL(items.GetPath());
+
+ CFileItemPtr pItem;
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(directoryNode->BuildPath()))
+ return;
+
+ // always show "all" items by default
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWALLITEMS))
+ return;
+
+ // no need for "all" item when only one item
+ if (items.GetObjectCount() <= 1)
+ return;
+
+ auto nodeChildType = directoryNode->GetChildType();
+
+ // No need for "*all" when overview node and child node is "albums" or "artists"
+ // without options (hence all albums or artists unfiltered).
+ if (directoryNode->GetType() == NODE_TYPE_OVERVIEW &&
+ (nodeChildType == NODE_TYPE_ARTIST || nodeChildType == NODE_TYPE_ALBUM) &&
+ musicUrl.GetOptions().empty())
+ return;
+ // Smart playlist rules on parent node do not get applied to child nodes so no "*all"
+ // ! @Todo: Remove this allowing "*all" once rules do get applied to child nodes.
+ if (directoryNode->GetType() == NODE_TYPE_OVERVIEW &&
+ (nodeChildType == NODE_TYPE_ARTIST || nodeChildType == NODE_TYPE_ALBUM) &&
+ musicUrl.HasOption("xsp"))
+ return;
+
+ switch (nodeChildType)
+ {
+ case NODE_TYPE_ARTIST:
+ pItem.reset(new CFileItem(g_localizeStrings.Get(15103))); // "All Artists"
+ musicUrl.AppendPath("-1/");
+ pItem->SetPath(musicUrl.ToString());
+ break;
+
+ // All album related nodes
+ case NODE_TYPE_ALBUM:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_TOP100:
+ pItem.reset(new CFileItem(g_localizeStrings.Get(15102))); // "All Albums"
+ musicUrl.AppendPath("-1/");
+ pItem->SetPath(musicUrl.ToString());
+ break;
+
+ // Disc node
+ case NODE_TYPE_DISC:
+ pItem.reset(new CFileItem(g_localizeStrings.Get(38075))); // "All Discs"
+ musicUrl.AppendPath("-1/");
+ pItem->SetPath(musicUrl.ToString());
+ break;
+
+ default:
+ break;
+ }
+
+ if (pItem)
+ {
+ pItem->m_bIsFolder = true;
+ pItem->SetSpecialSort(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryAllItemsOnBottom ? SortSpecialOnBottom : SortSpecialOnTop);
+ pItem->SetCanQueue(false);
+ pItem->SetLabelPreformatted(true);
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryAllItemsOnBottom)
+ items.Add(pItem);
+ else
+ items.AddFront(pItem, (items.Size() > 0 && items[0]->IsParentFolder()) ? 1 : 0);
+ }
+}
diff --git a/xbmc/music/windows/MusicFileItemListModifier.h b/xbmc/music/windows/MusicFileItemListModifier.h
new file mode 100644
index 0000000..9a2190b
--- /dev/null
+++ b/xbmc/music/windows/MusicFileItemListModifier.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016-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 "IFileItemListModifier.h"
+
+class CMusicFileItemListModifier : public IFileItemListModifier
+{
+public:
+ CMusicFileItemListModifier() = default;
+ ~CMusicFileItemListModifier() override = default;
+
+ bool CanModify(const CFileItemList &items) const override;
+ bool Modify(CFileItemList &items) const override;
+
+private:
+ static void AddQueuingFolder(CFileItemList & items);
+};