1 /*
2  *  Copyright (C) 2005-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "MusicDatabase.h"
10 
11 #include "Album.h"
12 #include "Application.h"
13 #include "Artist.h"
14 #include "FileItem.h"
15 #include "GUIInfoManager.h"
16 #include "LangInfo.h"
17 #include "ServiceBroker.h"
18 #include "Song.h"
19 #include "TextureCache.h"
20 #include "URL.h"
21 #include "Util.h"
22 #include "addons/Addon.h"
23 #include "addons/AddonManager.h"
24 #include "addons/AddonSystemSettings.h"
25 #include "addons/Scraper.h"
26 #include "dbwrappers/dataset.h"
27 #include "dialogs/GUIDialogKaiToast.h"
28 #include "dialogs/GUIDialogProgress.h"
29 #include "dialogs/GUIDialogSelect.h"
30 #include "events/EventLog.h"
31 #include "events/NotificationEvent.h"
32 #include "filesystem/Directory.h"
33 #include "filesystem/DirectoryCache.h"
34 #include "filesystem/File.h"
35 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
36 #include "guilib/GUIComponent.h"
37 #include "guilib/GUIWindowManager.h"
38 #include "guilib/LocalizeStrings.h"
39 #include "guilib/guiinfo/GUIInfoLabels.h"
40 #include "interfaces/AnnouncementManager.h"
41 #include "messaging/helpers/DialogHelper.h"
42 #include "messaging/helpers/DialogOKHelper.h"
43 #include "music/tags/MusicInfoTag.h"
44 #include "network/Network.h"
45 #include "network/cddb.h"
46 #include "playlists/SmartPlayList.h"
47 #include "profiles/ProfileManager.h"
48 #include "settings/AdvancedSettings.h"
49 #include "settings/MediaSourceSettings.h"
50 #include "settings/Settings.h"
51 #include "settings/SettingsComponent.h"
52 #include "storage/MediaManager.h"
53 #include "threads/SystemClock.h"
54 #include "utils/FileUtils.h"
55 #include "utils/LegacyPathTranslation.h"
56 #include "utils/MathUtils.h"
57 #include "utils/Random.h"
58 #include "utils/StringUtils.h"
59 #include "utils/URIUtils.h"
60 #include "utils/XMLUtils.h"
61 #include "utils/log.h"
62 
63 #include <inttypes.h>
64 
65 using namespace XFILE;
66 using namespace MUSICDATABASEDIRECTORY;
67 using namespace KODI::MESSAGING;
68 using namespace MUSIC_INFO;
69 
70 using ADDON::AddonPtr;
71 using KODI::MESSAGING::HELPERS::DialogResponse;
72 
73 #define RECENTLY_PLAYED_LIMIT 25
74 #define MIN_FULL_SEARCH_LENGTH 3
75 
76 #ifdef HAS_DVD_DRIVE
77 using namespace CDDB;
78 using namespace MEDIA_DETECT;
79 #endif
80 
AnnounceRemove(const std::string & content,int id)81 static void AnnounceRemove(const std::string& content, int id)
82 {
83   CVariant data;
84   data["type"] = content;
85   data["id"] = id;
86   if (g_application.IsMusicScanning())
87     data["transaction"] = true;
88   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data);
89 }
90 
AnnounceUpdate(const std::string & content,int id,bool added=false)91 static void AnnounceUpdate(const std::string& content, int id, bool added = false)
92 {
93   CVariant data;
94   data["type"] = content;
95   data["id"] = id;
96   if (g_application.IsMusicScanning())
97     data["transaction"] = true;
98   if (added)
99     data["added"] = true;
100   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data);
101 }
102 
CMusicDatabase(void)103 CMusicDatabase::CMusicDatabase(void)
104 {
105   m_translateBlankArtist = true;
106 }
107 
~CMusicDatabase(void)108 CMusicDatabase::~CMusicDatabase(void)
109 {
110   EmptyCache();
111 }
112 
Open()113 bool CMusicDatabase::Open()
114 {
115   return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic);
116 }
117 
CreateTables()118 void CMusicDatabase::CreateTables()
119 {
120   CLog::Log(LOGINFO, "create artist table");
121   m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, "
122               " strArtist varchar(256), strMusicBrainzArtistID text, "
123               " strSortName text, "
124               " strType text, strGender text, strDisambiguation text, "
125               " strBorn text, strFormed text, strGenres text, strMoods text, "
126               " strStyles text, strInstruments text, strBiography text, "
127               " strDied text, strDisbanded text, strYearsActive text, "
128               " strImage text, "
129               " lastScraped varchar(20) default NULL, "
130               " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
131               " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
132               " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
133   // Create missing artist tag artist [Missing].
134   std::string strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
135      "VALUES( %i, '%s', '%s', '%s' )",
136     BLANKARTIST_ID, BLANKARTIST_NAME.c_str(),
137     BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
138   m_pDS->exec(strSQL);
139 
140   CLog::Log(LOGINFO, "create album table");
141   m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, "
142               " strAlbum varchar(256), strMusicBrainzAlbumID text, "
143               " strReleaseGroupMBID text, "
144               " strArtistDisp text, strArtistSort text, strGenres text, "
145               " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
146               " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
147               " bCompilation integer not null default '0', "
148               " strMoods text, strStyles text, strThemes text, "
149               " strReview text, strImage text, strLabel text, "
150               " strType text, "
151               " strReleaseStatus TEXT, "
152               " fRating FLOAT NOT NULL DEFAULT 0, "
153               " iVotes INTEGER NOT NULL DEFAULT 0, "
154               " iUserrating INTEGER NOT NULL DEFAULT 0, "
155               " lastScraped varchar(20) default NULL, "
156               " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
157               " strReleaseType text, "
158               " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
159               " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
160               " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
161               " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
162 
163   CLog::Log(LOGINFO, "create audiobook table");
164   m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
165               " strBook varchar(256), strAuthor text,"
166               " bookmark integer, file text,"
167               " dateAdded varchar (20) default NULL)");
168 
169   CLog::Log(LOGINFO, "create album_artist table");
170   m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, strArtist text)");
171 
172   CLog::Log(LOGINFO, "create album_source table");
173   m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
174 
175   CLog::Log(LOGINFO, "create genre table");
176   m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
177 
178   CLog::Log(LOGINFO, "create path table");
179   m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
180 
181   CLog::Log(LOGINFO, "create source table");
182   m_pDS->exec("CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
183 
184   CLog::Log(LOGINFO, "create source_path table");
185   m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
186 
187   CLog::Log(LOGINFO, "create song table");
188   m_pDS->exec("CREATE TABLE song (idSong integer primary key, "
189               " idAlbum integer, idPath integer, "
190               " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
191               " iTrack integer, iDuration integer, "
192               " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
193               " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
194               " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
195               " lastplayed varchar(20) default NULL, "
196               " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
197               " userrating INTEGER NOT NULL DEFAULT 0, "
198               " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
199               " iBitRate INTEGER NOT NULL DEFAULT 0, "
200               " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
201               " strReplayGain text, "
202               " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
203   CLog::Log(LOGINFO, "create song_artist table");
204   m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder integer, strArtist text)");
205   CLog::Log(LOGINFO, "create song_genre table");
206   m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
207 
208   CLog::Log(LOGINFO, "create role table");
209   m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
210   m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')");   //Default role
211 
212   CLog::Log(LOGINFO, "create infosetting table");
213   m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, strSettings TEXT)");
214 
215   CLog::Log(LOGINFO, "create discography table");
216   m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
217               "strReleaseGroupMBID TEXT)");
218 
219   CLog::Log(LOGINFO, "create art table");
220   m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
221 
222   CLog::Log(LOGINFO, "create versiontagscan table");
223   m_pDS->exec("CREATE TABLE versiontagscan (idVersion INTEGER, iNeedsScan INTEGER, "
224               "lastscanned VARCHAR(20), "
225               "lastcleaned VARCHAR(20), "
226               "artistlinksupdated VARCHAR(20), "
227               "genresupdated VARCHAR(20))");
228   m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
229                          GetSchemaVersion()));
230 
231   CLog::Log(LOGINFO, "create removed_link table");
232   m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
233 
234 }
235 
CreateAnalytics()236 void CMusicDatabase::CreateAnalytics()
237 {
238   CLog::Log(LOGINFO, "%s - creating indices", __FUNCTION__);
239   m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
240   m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
241   m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
242   m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
243 
244   m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
245   m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
246 
247   m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
248 
249   m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
250   m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
251   m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
252 
253   m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))");
254 
255   m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
256   m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
257 
258   m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
259 
260   m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
261   m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
262 
263   m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))");
264   m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
265   m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
266   m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
267   m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
268   //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
269   m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
270 
271   m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
272   m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
273   m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
274   m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
275 
276   m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
277   m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
278 
279   m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))");
280 
281   m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
282 
283   m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
284 
285   CLog::Log(LOGINFO, "create triggers");
286   m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
287               "  DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
288               "  DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
289               "  DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
290               "  DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
291               " END");
292   m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
293               "  DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
294               "  DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
295               "  DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
296               "  DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
297               " END");
298   m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
299               "  DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
300               "  DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
301               "  DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
302               " END");
303   m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
304               "  DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
305               "  DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
306               " END");
307 
308   /* Maintain date new and last modified for songs, albums and artists using triggers
309      MySQL triggers cannot modify a table that is already being used by the statement that invoked
310      the trigger (to avoid recursion), but can set NEW column values before insert or update.
311      Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
312      Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
313     // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
314   */
315   bool bisMySQL = StringUtils::EqualsNoCase(
316       CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
317 
318   if (!bisMySQL)
319   { // SQLite trigger syntax - AFTER INSERT/UPDATE
320     m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
321                 " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
322                 " AND NEW.dateNew IS NULL;"
323                 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
324                 " END");
325     m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
326                 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
327                 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
328                 " END");
329     m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
330                 " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
331                 " AND NEW.dateNew IS NULL;"
332                 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
333                 " END");
334     m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
335                 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
336                 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
337                 " END");
338     m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
339                 " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
340                 " AND NEW.dateNew IS NULL;"
341                 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
342                 " END");
343     m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
344                 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
345                 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
346                 " END");
347 
348     m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
349                 " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
350                 " END");
351   }
352   else
353   { // MySQL trigger syntax - BEFORE INSERT/UPDATE
354     m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
355                 "  IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now();  END IF;"
356                 "  SET NEW.dateModified = now();"
357                 " END");
358     m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
359                 " SET NEW.dateModified = now()");
360 
361     m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
362                 "  IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now();  END IF;"
363                 "  SET NEW.dateModified = now();"
364                 " END");
365     m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
366                 " SET NEW.dateModified = now()");
367 
368     m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
369                 "  IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now();  END IF;"
370                 "  SET NEW.dateModified = now();"
371                 " END");
372     m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
373                 " SET NEW.dateModified = now()");
374 
375     m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
376                 " UPDATE versiontagscan SET genresupdated = now()");
377   }
378 
379     // Triggers to maintain recent changes to album and song artist links in removed_link table
380   m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
381               "DELETE FROM removed_link "
382               "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
383               "END");
384   m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
385               "DELETE FROM removed_link "
386               "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
387               "END");
388   CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
389 
390   // Create native functions stored in DB (MySQL/MariaDB only)
391   CreateNativeDBFunctions();
392 
393   // we create views last to ensure all indexes are rolled in
394   CreateViews();
395 }
396 
CreateRemovedLinkTriggers()397 void CMusicDatabase::CreateRemovedLinkTriggers()
398 {
399   // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
400   m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
401               " INSERT INTO removed_link (idArtist, idMedia, idRole)"
402               " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
403               " END");
404   m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
405               " INSERT INTO removed_link (idArtist, idMedia, idRole)"
406               " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
407               " END");
408 }
409 
410 
CreateViews()411 void CMusicDatabase::CreateViews()
412 {
413   CLog::Log(LOGINFO, "create song view");
414   m_pDS->exec("CREATE VIEW songview AS SELECT "
415               "        song.idSong AS idSong, "
416               "        song.strArtistDisp AS strArtists,"
417               "        song.strArtistSort AS strArtistSort,"
418               "        song.strGenres AS strGenres,"
419               "        strTitle, "
420               "        iTrack, iDuration, "
421               "        song.strReleaseDate as strReleaseDate, "
422               "        song.strOrigReleaseDate as strOrigReleaseDate, "
423               "        song.strDiscSubtitle as strDiscSubtitle, "
424               "        strFileName, "
425               "        strMusicBrainzTrackID, "
426               "        iTimesPlayed, iStartOffset, iEndOffset, "
427               "        lastplayed, "
428               "        song.rating, "
429               "        song.userrating, "
430               "        song.votes, "
431               "        comment, "
432               "        song.idAlbum AS idAlbum, "
433               "        strAlbum, "
434               "        strPath, "
435               "        album.strReleaseStatus as strReleaseStatus,"
436               "        album.bCompilation AS bCompilation,"
437               "        album.bBoxedSet AS bBoxedSet, "
438               "        album.strArtistDisp AS strAlbumArtists,"
439               "        album.strArtistSort AS strAlbumArtistSort,"
440               "        album.strReleaseType AS strAlbumReleaseType,"
441               "        song.mood as mood,"
442               "        song.strReplayGain, "
443               "        iBPM, "
444               "        iBitRate, "
445               "        iSampleRate, "
446               "        iChannels, "
447               "        album.iAlbumDuration AS iAlbumDuration, "
448               "        album.iDiscTotal as iDiscTotal, "
449               "        song.dateAdded as dateAdded, "
450               "        song.dateNew AS dateNew, "
451               "        song.dateModified AS dateModified "
452               "FROM song"
453               "  JOIN album ON"
454               "    song.idAlbum=album.idAlbum"
455               "  JOIN path ON"
456               "    song.idPath=path.idPath");
457 
458   CLog::Log(LOGINFO, "create album view");
459   m_pDS->exec("CREATE VIEW albumview AS SELECT "
460               "        album.idAlbum AS idAlbum, "
461               "        strAlbum, "
462               "        strMusicBrainzAlbumID, "
463               "        strReleaseGroupMBID, "
464               "        album.strArtistDisp AS strArtists, "
465               "        album.strArtistSort AS strArtistSort, "
466               "        album.strGenres AS strGenres, "
467               "        album.strReleaseDate as strReleaseDate, "
468               "        album.strOrigReleaseDate as strOrigReleaseDate, "
469               "        album.bBoxedSet AS bBoxedSet, "
470               "        album.strMoods AS strMoods, "
471               "        album.strStyles AS strStyles, "
472               "        strThemes, "
473               "        strReview, "
474               "        strLabel, "
475               "        strType, "
476               "        strReleaseStatus, "
477               "        album.strImage as strImage, "
478               "        album.fRating, "
479               "        album.iUserrating, "
480               "        album.iVotes, "
481               "        bCompilation, "
482               "        bScrapedMBID,"
483               "        lastScraped,"
484               "        dateAdded, dateNew, dateModified, "
485               "        (SELECT ROUND(AVG(song.iTimesPlayed)) FROM song WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
486               "        strReleaseType, "
487               "        iDiscTotal, "
488               "        (SELECT MAX(song.lastplayed) FROM song WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
489               "        iAlbumDuration "
490               "FROM album"
491               );
492 
493   CLog::Log(LOGINFO, "create artist view");
494   m_pDS->exec("CREATE VIEW artistview AS SELECT"
495               "  idArtist, strArtist, strSortName, "
496               "  strMusicBrainzArtistID, "
497               "  strType, strGender, strDisambiguation, "
498               "  strBorn, strFormed, strGenres,"
499               "  strMoods, strStyles, strInstruments, "
500               "  strBiography, strDied, strDisbanded, "
501               "  strYearsActive, strImage, "
502               "  bScrapedMBID, lastScraped, "
503               "  dateAdded, dateNew, dateModified "
504               "FROM artist");
505 
506   CLog::Log(LOGINFO, "create albumartist view");
507   m_pDS->exec("CREATE VIEW albumartistview AS SELECT"
508               "  album_artist.idAlbum AS idAlbum, "
509               "  album_artist.idArtist AS idArtist, "
510               "  0 AS idRole, "
511               "  'AlbumArtist' AS strRole, "
512               "  artist.strArtist AS strArtist, "
513               "  artist.strSortName AS strSortName,"
514               "  artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
515               "  album_artist.iOrder AS iOrder "
516               "FROM album_artist "
517               "JOIN artist ON "
518               "     album_artist.idArtist = artist.idArtist");
519 
520   CLog::Log(LOGINFO, "create songartist view");
521   m_pDS->exec("CREATE VIEW songartistview AS SELECT"
522               "  song_artist.idSong AS idSong, "
523               "  song_artist.idArtist AS idArtist, "
524               "  song_artist.idRole AS idRole, "
525               "  role.strRole AS strRole, "
526               "  artist.strArtist AS strArtist, "
527               "  artist.strSortName AS strSortName,"
528               "  artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
529               "  song_artist.iOrder AS iOrder "
530               "FROM song_artist "
531               "JOIN artist ON "
532               "     song_artist.idArtist = artist.idArtist "
533               "JOIN role ON "
534               "     song_artist.idRole = role.idRole");
535 }
536 
CreateNativeDBFunctions()537 void CMusicDatabase::CreateNativeDBFunctions()
538 {
539   // Create native functions in MySQL/MariaDB database only
540   if (!StringUtils::EqualsNoCase(
541           CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
542           "mysql"))
543     return;
544   CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions");
545   /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
546      default utf8_general_ci collation that SQLite does via a collation sequence callback
547      function to StringUtils::AlphaNumericCompare
548      !@todo: the video needs these defined too for sorting in DB, then creation can be made common
549   */
550   // clang-format off
551   // udfFirstNumberPos finds the position of the first digit in a string
552   m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
553   m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
554     "RETURNS int \n"
555     "LANGUAGE SQL \n"
556     "DETERMINISTIC \n"
557     "NO SQL \n"
558     "SQL SECURITY INVOKER \n"
559     "BEGIN \n"
560     "  DECLARE position int; \n"
561     "  DECLARE tmppos int; \n"
562     "  SET position = 5000; \n"
563     "  SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
564     "  SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
565     "  SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
566     "  SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
567     "  SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
568     "  SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
569     "  SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
570     "  SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
571     "  SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
572     "  SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
573     "  IF(position = 5000) THEN RETURN 0; END IF;\n"
574     "  RETURN position; \n"
575     "END\n");
576 
577   // udfSymbolShift adds "/" (the  last symbol before "0"), in front any of the chars input
578   m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
579   m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
580     "RETURNS varchar(1024)\n"
581     "LANGUAGE SQL\n"
582     "DETERMINISTIC\n"
583     "NO SQL\n"
584     "SQL SECURITY INVOKER\n"
585     "BEGIN\n"
586     "  DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
587     "  DECLARE i int;\n"
588     "  DECLARE symbolCharsLen int;\n"
589     "  DECLARE symbol char(1);\n"
590     "  SET sortString = instring;\n"
591     "  SET i = 1;\n"
592     "  SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
593     "  WHILE(i <= symbolCharsLen) DO\n"
594     "    SET symbol = SUBSTRING(symbolChars, i, 1);\n"
595     "    SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
596     "    SET i = i + 1;\n"
597     "  END WHILE;\n"
598     "  RETURN sortString;\n"
599     "END\n");
600 
601   // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
602   m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
603   m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
604     "sameOrderChars char(25))\n"
605     "RETURNS varchar(1024)\n"
606     "LANGUAGE SQL\n"
607     "DETERMINISTIC\n"
608     "NO SQL\n"
609     "SQL SECURITY INVOKER\n"
610     "BEGIN\n"
611     "  DECLARE sortString varchar(1024);\n"
612     "  DECLARE shiftedString varchar(1024);\n"
613     "  DECLARE inLength int;\n"
614     "  DECLARE shiftedLength int;\n"
615     "  DECLARE totalSympadLength int;\n"
616     "  DECLARE symbolshifted512 varchar(1024);\n"
617     "  DECLARE numStartIndex int; \n"
618     "  DECLARE numEndIndex int; \n"
619     "  DECLARE padLength int; \n"
620     "  DECLARE totalPadLength int; \n"
621     "  DECLARE i int; \n"
622     "  DECLARE sameOrderCharsLen int;\n"
623     "  SET totalPadLength = 0; \n"
624     "  SET instring = TRIM(instring);\n"
625     "  SET inLength = CHAR_LENGTH(inString);\n"
626     "  SET sortString = instring; \n"
627     "  SET numStartIndex = udfFirstNumberPos(instring); \n"
628     "  SET numEndIndex = 0; \n"
629     "  SET i = 1; \n"
630     "  SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
631     "  WHILE(i <= sameOrderCharsLen) DO \n"
632     "    SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
633     "    SET i = i + 1; \n"
634     "  END WHILE; \n"
635     "  WHILE(numStartIndex <> 0) DO \n"
636     "    SET numStartIndex = numStartIndex + numEndIndex; \n"
637     "    SET numEndIndex = numStartIndex; \n"
638     "    WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
639     "      SET numEndIndex = numEndIndex + 1; \n"
640     "    END WHILE; \n"
641     "    SET numEndIndex = numEndIndex - 1; \n"
642     "    SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
643     "    IF padLength < 0 THEN \n"
644     "      SET padLength = 0; \n"
645     "    END IF; \n"
646     "    IF inLength + totalPadLength + padlength > 1024 THEN \n"
647     "      -- Padding more digits would be too long, pad this one just enough \n"
648     "      SET padLength = 1024 - inLength - totalPadLength; \n"
649     "      SET numStartIndex = 0; \n"
650     "    END IF; \n"
651     "    SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
652     "    SET totalPadLength = totalPadLength + padLength; \n"
653     "    IF numStartIndex <> 0 THEN \n"
654     "      SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
655     "    END IF; \n"
656     "  END WHILE; \n"
657     "  -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
658     "  -- when there is space as this could double string length.  Note '\\' needs escaping \n"
659     "  SET numStartIndex = 1; \n"
660     "  SET numEndIndex = inLength + totalPadLength; \n"
661     "  IF numEndIndex < 1024 THEN \n"
662     "    SET shiftedLength = 0; \n"
663     "    SET totalSympadLength = 0; \n"
664     "    WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
665     "      SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
666     "      SET numStartIndex = numStartIndex + 512; \n"
667     "      SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
668     "      IF totalSympadLength = 0 THEN \n"
669     "        SET shiftedString = symbolshifted512; \n"
670     "      ELSE \n"
671     "        IF totalSympadLength + shiftedLength > 1024 THEN \n"
672     "          SET shiftedLength = 1024 - totalSympadLength; \n"
673     "          SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
674     "        END IF; \n"
675     "        SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
676     "      END IF; \n"
677     "      SET totalSympadLength = totalSympadLength + shiftedLength; \n"
678     "    END WHILE; \n"
679     "    SET sortString = shiftedString; \n"
680     "  END IF; \n"
681     "  RETURN sortString; \n"
682     "END\n");
683   // clang-format on
684 }
685 
SplitPath(const std::string & strFileNameAndPath,std::string & strPath,std::string & strFileName)686 void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName)
687 {
688   URIUtils::Split(strFileNameAndPath, strPath, strFileName);
689   // Keep protocol options as part of the path
690   if (URIUtils::IsURL(strFileNameAndPath))
691   {
692     CURL url(strFileNameAndPath);
693     if (!url.GetProtocolOptions().empty())
694       strPath += "|" + url.GetProtocolOptions();
695   }
696 }
697 
AddAlbum(CAlbum & album,int idSource)698 bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource)
699 {
700   BeginTransaction();
701   SetLibraryLastUpdated();
702 
703   album.idAlbum = AddAlbum(album.strAlbum,
704                            album.strMusicBrainzAlbumID,
705                            album.strReleaseGroupMBID,
706                            album.GetAlbumArtistString(),
707                            album.GetAlbumArtistSort(),
708                            album.GetGenreString(),
709                            album.strReleaseDate, album.strOrigReleaseDate,
710                            album.bBoxedSet,
711                            album.strLabel, album.strType, album.strReleaseStatus,
712                            album.bCompilation, album.releaseType);
713 
714   // Add the album artists
715   if (album.artistCredits.empty())
716     AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0); // Album must have at least one artist so set artist to [Missing]
717   for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
718   {
719     artistCredit->idArtist = AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(), artistCredit->GetSortName());
720     AddAlbumArtist(artistCredit->idArtist,
721                    album.idAlbum,
722                    artistCredit->GetArtist(),
723                    static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
724   }
725 
726   for (auto song = album.songs.begin(); song != album.songs.end(); ++song)
727   {
728     song->idAlbum = album.idAlbum;
729 
730     song->idSong = AddSong(song->idSong, song->dateNew,
731                            song->idAlbum,
732                            song->strTitle, song->strMusicBrainzTrackID,
733                            song->strFileName, song->strComment,
734                            song->strMood, song->strThumb,
735                            song->GetArtistString(),
736                            song->GetArtistSort(),
737                            song->genre,
738                            song->iTrack, song->iDuration,
739                            song->strReleaseDate, song->strOrigReleaseDate,
740                            song->strDiscSubtitle,
741                            song->iTimesPlayed, song->iStartOffset,
742                            song->iEndOffset,
743                            song->lastPlayed,
744                            song->rating,
745                            song->userrating,
746                            song->votes,
747                            song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels,
748                            song->replayGain);
749 
750     if (song->artistCredits.empty())
751       AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0); // Song must have at least one artist so set artist to [Missing]
752 
753     for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end(); ++artistCredit)
754     {
755       artistCredit->idArtist = AddArtist(artistCredit->GetArtist(),
756                                          artistCredit->GetMusicBrainzArtistID(),
757                                          artistCredit->GetSortName());
758       AddSongArtist(artistCredit->idArtist,
759                     song->idSong,
760                     ROLE_ARTIST,
761                     artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
762                     static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit)));
763     }
764     // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
765     // and use COMPOSERSORT tag data to provide sort names for artists that are composers
766     AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort());
767   }
768 
769   // Set album duration as total of all songs on album.
770   // Folder layout may mean AddAlbum call has added more songs to an existing album
771   std::string strSQL;
772   strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum);
773   int albumDuration = GetSingleValueInt(strSQL);
774   m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration,
775                          album.idAlbum));
776 
777   // Add album sources
778   if (idSource > 0)
779     AddAlbumSource(album.idAlbum, idSource);
780   else
781   {
782     // Use album path, or failing that song paths to determine sources for the album
783     AddAlbumSources(album.idAlbum, album.strPath);
784   }
785 
786   for (const auto &albumArt : album.art)
787     SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second);
788 
789   // Set album disc total
790   m_pDS->exec(
791       PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
792                  "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
793                  album.idAlbum));
794   // Set a non-compilation album as a boxset if it has three or more distinct disc titles
795   if (!album.bBoxedSet && !album.bCompilation)
796   {
797     std::string strSQL;
798     strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
799                         album.idAlbum);
800     int numTitles = GetSingleValueInt(strSQL);
801     if (numTitles >=3)
802     {
803       strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum);
804       m_pDS->exec(strSQL);
805     }
806   }
807   m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
808                          "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
809                          album.idAlbum));
810   m_pDS->exec(PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
811                          "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
812                          album.idAlbum));
813 
814   std::string albumdateadded =
815       GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum));
816   m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
817                          albumdateadded.c_str(), album.idAlbum));
818 
819   /* Update artist dateAdded values for artists involved in album as song or album artists.
820      Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
821      derived from song dateadded values which are usually file dates (or the last scan).
822      It is used to indicate those artists with recent media.
823      For artists that are neither album nor song artists (other roles only) dateadded will be null.
824   */
825   std::vector<std::string> artistIDs;
826   // Get distinct song and album artist IDs for this album
827   GetArtistsByAlbum(album.idAlbum, artistIDs);
828   std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")";
829   strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' "
830                       "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
831                       albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str());
832   m_pDS->exec(strSQL);
833 
834   CommitTransaction();
835   return true;
836 }
837 
UpdateAlbum(CAlbum & album)838 bool CMusicDatabase::UpdateAlbum(CAlbum& album)
839 {
840   BeginTransaction();
841   SetLibraryLastUpdated();
842 
843   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
844   bool isBoxset = false;
845   bool canBeBoxset = false;
846 
847   if (album.bBoxedSet)
848   {
849     isBoxset = IsAlbumBoxset(album.idAlbum);
850     if (!isBoxset)
851     { // not a boxset already so make sure we have enough discs & they all have titles
852       std::string strSQL;
853       strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum);
854       int numDiscs = GetSingleValueInt(strSQL);
855       if (numDiscs >= 2)
856       {
857         canBeBoxset = true;
858         int discValue;
859         for (discValue = 1; discValue <= numDiscs; discValue++)
860         {
861           strSQL =
862               PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
863                          "song.iTrack >> 16 = %i",
864                          album.idAlbum, discValue);
865           std::string currentTitle = GetSingleValue(strSQL);
866           if (currentTitle.empty())
867           {
868             currentTitle = StringUtils::Format("%s %i", g_localizeStrings.Get(427), discValue);
869             strSQL =
870                 PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
871                            "song.iTrack >> 16 = %i",
872                            currentTitle.c_str(), album.idAlbum, discValue);
873             ExecuteQuery(strSQL);
874           }
875         }
876       }
877       if (!canBeBoxset && album.bBoxedSet)
878       {
879         CLog::Log(LOGINFO, "%s : Album with id [%i] does not meet the requirements for a boxset.",
880                   __FUNCTION__, album.idAlbum);
881         album.bBoxedSet = false;
882       }
883     }
884   }
885   UpdateAlbum(album.idAlbum,
886               album.strAlbum, album.strMusicBrainzAlbumID,
887               album.strReleaseGroupMBID,
888               album.GetAlbumArtistString(), album.GetAlbumArtistSort(),
889               album.GetGenreString(),
890               StringUtils::Join(album.moods, itemSeparator),
891               StringUtils::Join(album.styles, itemSeparator),
892               StringUtils::Join(album.themes, itemSeparator),
893               album.strReview,
894               album.thumbURL.GetData(),
895               album.strLabel, album.strType, album.strReleaseStatus,
896               album.fRating, album.iUserrating, album.iVotes,
897               album.strReleaseDate, album.strOrigReleaseDate,
898               album.bBoxedSet,
899               album.bCompilation, album.releaseType,
900               album.bScrapedMBID);
901 
902   if (!album.bArtistSongMerge)
903   {
904     // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
905     for (const auto &artistCredit : album.artistCredits)
906       UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID());
907   }
908   else
909   {
910     // Replace the album artists with those scraped or set by JSON
911     DeleteAlbumArtistsByAlbum(album.idAlbum);
912     if (album.artistCredits.empty())
913       AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0); // Album must have at least one artist so set artist to [Missing]
914     for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
915     {
916       artistCredit->idArtist = AddArtist(artistCredit->GetArtist(),
917         artistCredit->GetMusicBrainzArtistID(), artistCredit->GetSortName(), true);
918       AddAlbumArtist(artistCredit->idArtist,
919         album.idAlbum,
920         artistCredit->GetArtist(),
921         static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
922     }
923     /* Replace the songs with those scraped or imported, but if new songs is empty
924        (such as when called from JSON) do not remove the original ones
925        Also updates nested data e.g. song artists, song genres and contributors
926        Do not check for artist link changes, that is done later for all songs and album
927     */
928     int albumDuration = 0;
929     for (auto &song : album.songs)
930     {
931       UpdateSong(song);
932       albumDuration += song.iDuration;
933     }
934     if (albumDuration > 0)
935       m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
936                              albumDuration, album.idAlbum));
937   }
938 
939   if (!album.art.empty())
940     SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
941 
942   CheckArtistLinksChanged();
943 
944   CommitTransaction();
945   return true;
946 }
947 
NormaliseSongDates(std::string & strRelease,std::string & strOriginal)948 void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal)
949 {
950   // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
951   int iDate;
952    iDate = StringUtils::DateStringToYYYYMMDD(strRelease);
953   if (iDate < 0)
954     strRelease.clear();
955   iDate = StringUtils::DateStringToYYYYMMDD(strOriginal);
956   if (iDate < 0)
957     strOriginal.clear();
958   // Avoid missing release or original values unless both invalid or empty
959   if (!strRelease.empty() && strOriginal.empty())
960     strOriginal = strRelease;
961   else if (strRelease.empty() && !strOriginal.empty())
962     strRelease = strOriginal;
963 }
964 
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)965 int CMusicDatabase::AddSong(const int idSong, const CDateTime& dtDateNew,
966                             const int idAlbum,
967                             const std::string& strTitle, const std::string& strMusicBrainzTrackID,
968                             const std::string& strPathAndFileName, const std::string& strComment,
969                             const std::string& strMood, const std::string& strThumb,
970                             const std::string &artistDisp, const std::string &artistSort,
971                             const std::vector<std::string>& genres,
972                             int iTrack, int iDuration,
973                             const std::string& strReleaseDate,
974                             const std::string& strOrigReleaseDate,
975                             std::string& strDiscSubtitle,
976                             const int iTimesPlayed, int iStartOffset, int iEndOffset,
977                             const CDateTime& dtLastPlayed, float rating, int userrating, int votes,
978                             int iBPM, int iBitRate, int iSampleRate, int iChannels,
979                             const ReplayGain& replayGain)
980 {
981   int idNew = -1;
982   std::string strSQL;
983   try
984   {
985     // We need at least the title
986     if (strTitle.empty())
987       return -1;
988 
989     if (nullptr == m_pDB)
990       return -1;
991     if (nullptr == m_pDS)
992       return -1;
993 
994     std::string strPath, strFileName;
995     SplitPath(strPathAndFileName, strPath, strFileName);
996     int idPath = AddPath(strPath);
997 
998     if (idSong <= 1)
999     {
1000       if (!strMusicBrainzTrackID.empty())
1001         strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1002           "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
1003           idAlbum,
1004           iTrack,
1005           strMusicBrainzTrackID.c_str());
1006       else
1007         strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1008           "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
1009           "AND strMusicBrainzTrackID IS NULL",
1010           idAlbum,
1011           strFileName.c_str(),
1012           strTitle.c_str(),
1013           iTrack);
1014 
1015       if (!m_pDS->query(strSQL))
1016         return -1;
1017     }
1018     if (m_pDS->num_rows() == 0)
1019     {
1020       m_pDS->close();
1021 
1022       // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
1023       bool isBoxset = IsAlbumBoxset(idAlbum);
1024       if (isBoxset && strDiscSubtitle.empty())
1025       {
1026         int discno = iTrack >> 16;
1027         strDiscSubtitle = StringUtils::Format("%s %i", g_localizeStrings.Get(427), discno);
1028       }
1029 
1030       // Validate ISO8601 dates and ensure none missing
1031       std::string strRelease = strReleaseDate;
1032       std::string strOriginal = strOrigReleaseDate;
1033       NormaliseSongDates(strRelease, strOriginal);
1034 
1035       // Get dateAdded from music file timestamp
1036       std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1037 
1038       strSQL = "INSERT INTO song ("
1039         "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
1040         "strTitle, iTrack, iDuration, "
1041         "strReleaseDate, strOrigReleaseDate, iBPM, "
1042         "iBitrate, iSampleRate, iChannels, "
1043         "strDiscSubtitle, strFileName, dateAdded,  "
1044         "strMusicBrainzTrackID, strArtistSort, "
1045         "iTimesPlayed, iStartOffset, iEndOffset, "
1046         "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
1047 
1048       if (idSong <= 0)
1049         // Song ID is autoincremented and dateNew set by trigger
1050         strSQL += PrepareSQL("VALUES (NULL, NULL, ");
1051       else
1052         //Reuse song Id and original date when the Id added
1053         strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str());
1054 
1055       strSQL +=
1056           PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
1057                      idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration,
1058                      strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate,
1059                      iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str());
1060 
1061       if (strMusicBrainzTrackID.empty())
1062         strSQL += PrepareSQL(",NULL");
1063       else
1064         strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str());
1065       if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1066         strSQL += PrepareSQL(",NULL");
1067       else
1068         strSQL += PrepareSQL(",'%s'", artistSort.c_str());
1069 
1070       if (dtLastPlayed.IsValid())
1071         strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", iTimesPlayed,
1072                              iStartOffset, iEndOffset, dtLastPlayed.GetAsDBDateTime().c_str(),
1073                              rating, userrating, votes, strComment.c_str(), strMood.c_str(),
1074                              replayGain.Get().c_str());
1075       else
1076         strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", iTimesPlayed,
1077                              iStartOffset, iEndOffset, rating, userrating, votes,
1078                              strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1079       m_pDS->exec(strSQL);
1080       if (idSong <= 0)
1081         idNew = (int)m_pDS->lastinsertid();
1082       else
1083         idNew = idSong;
1084     }
1085     else
1086     {
1087       idNew = m_pDS->fv("idSong").get_asInt();
1088       m_pDS->close();
1089       UpdateSong(idNew, strTitle, strMusicBrainzTrackID, strPathAndFileName, strComment, strMood,
1090                  strThumb, artistDisp, artistSort, genres, iTrack, iDuration,
1091                  strReleaseDate, strOrigReleaseDate, strDiscSubtitle, iTimesPlayed,
1092                  iStartOffset, iEndOffset, dtLastPlayed, rating, userrating, votes, replayGain,
1093                  iBPM, iBitRate, iSampleRate, iChannels);
1094     }
1095     if (!strThumb.empty())
1096       SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb);
1097 
1098     // Song genres added, and genre string updated to use the standardised genre names
1099     AddSongGenres(idNew, genres);
1100 
1101     AnnounceUpdate(MediaTypeSong, idNew, true);
1102   }
1103   catch (...)
1104   {
1105     CLog::Log(LOGERROR, "musicdatabase:unable to addsong (%s)", strSQL.c_str());
1106   }
1107   return idNew;
1108 }
1109 
GetSong(int idSong,CSong & song)1110 bool CMusicDatabase::GetSong(int idSong, CSong& song)
1111 {
1112   try
1113   {
1114     song.Clear();
1115 
1116     if (nullptr == m_pDB)
1117       return false;
1118     if (nullptr == m_pDS)
1119       return false;
1120 
1121     std::string strSQL=PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
1122                                  " JOIN songartistview ON songview.idSong = songartistview.idSong "
1123                                  " WHERE songview.idSong = %i "
1124                                  " ORDER BY songartistview.idRole, songartistview.iOrder", idSong);
1125 
1126     if (!m_pDS->query(strSQL)) return false;
1127     int iRowsFound = m_pDS->num_rows();
1128     if (iRowsFound == 0)
1129     {
1130       m_pDS->close();
1131       return false;
1132     }
1133 
1134     int songArtistOffset = song_enumCount;
1135 
1136     song = GetSongFromDataset(m_pDS->get_sql_record());
1137     while (!m_pDS->eof())
1138     {
1139       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1140 
1141       int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1142       if (idSongArtistRole == ROLE_ARTIST)
1143         song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
1144       else
1145         song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1146 
1147       m_pDS->next();
1148     }
1149     m_pDS->close(); // cleanup recordset data
1150     return true;
1151   }
1152   catch (...)
1153   {
1154     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
1155   }
1156 
1157   return false;
1158 }
1159 
UpdateSong(CSong & song,bool bArtists,bool bArtistLinks)1160 bool CMusicDatabase::UpdateSong(CSong& song,
1161                                 bool bArtists /*= true*/,
1162                                 bool bArtistLinks /*= true*/)
1163 {
1164   int result = UpdateSong(song.idSong,
1165                     song.strTitle,
1166                     song.strMusicBrainzTrackID,
1167                     song.strFileName,
1168                     song.strComment,
1169                     song.strMood,
1170                     song.strThumb,
1171                     song.GetArtistString(),
1172                     song.GetArtistSort(),
1173                     song.genre,
1174                     song.iTrack,
1175                     song.iDuration,
1176                     song.strReleaseDate,
1177                     song.strOrigReleaseDate,
1178                     song.strDiscSubtitle,
1179                     song.iTimesPlayed,
1180                     song.iStartOffset,
1181                     song.iEndOffset,
1182                     song.lastPlayed,
1183                     song.rating,
1184                     song.userrating,
1185                     song.votes,
1186                     song.replayGain,
1187                     song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels);
1188   if (result < 0)
1189     return false;
1190 
1191   // Replace Song genres and update genre string using the standardised genre names
1192   AddSongGenres(song.idSong, song.genre);
1193   if (bArtists)
1194   {
1195     //Replace song artists and contributors
1196     DeleteSongArtistsBySong(song.idSong);
1197     if (song.artistCredits.empty())
1198       AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0); // Song must have at least one artist so set artist to [Missing]
1199     for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end(); ++artistCredit)
1200     {
1201       artistCredit->idArtist = AddArtist(artistCredit->GetArtist(),
1202         artistCredit->GetMusicBrainzArtistID(), artistCredit->GetSortName());
1203       AddSongArtist(artistCredit->idArtist,
1204         song.idSong,
1205         ROLE_ARTIST,
1206         artistCredit->GetArtist(),
1207         static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit)));
1208     }
1209     // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
1210     // and use COMPOSERSORT tag data to provide sort names for artists that are composers
1211     AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort());
1212 
1213     if (bArtistLinks)
1214       CheckArtistLinksChanged();
1215   }
1216 
1217   return true;
1218 }
1219 
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)1220 int CMusicDatabase::UpdateSong(int idSong,
1221                                const std::string& strTitle, const std::string& strMusicBrainzTrackID,
1222                                const std::string& strPathAndFileName, const std::string& strComment,
1223                                const std::string& strMood, const std::string& strThumb,
1224                                const std::string &artistDisp, const std::string &artistSort,
1225                                const std::vector<std::string>& genres,
1226                                int iTrack, int iDuration,
1227                                const std::string& strReleaseDate,
1228                                const std::string& strOrigReleaseDate,
1229                                const std::string& strDiscSubtitle,
1230                                int iTimesPlayed, int iStartOffset, int iEndOffset,
1231                                const CDateTime& dtLastPlayed, float rating, int userrating, int votes,
1232                                const ReplayGain& replayGain, int iBPM, int iBitRate,
1233                                int iSampleRate,
1234                                int iChannels)
1235 {
1236   if (idSong < 0)
1237     return -1;
1238 
1239   std::string strSQL;
1240   std::string strPath, strFileName;
1241   SplitPath(strPathAndFileName, strPath, strFileName);
1242   int idPath = AddPath(strPath);
1243 
1244   // Validate ISO8601 dates and ensure none missing
1245   std::string strRelease = strReleaseDate;
1246   std::string strOriginal = strOrigReleaseDate;
1247   NormaliseSongDates(strRelease, strOriginal);
1248 
1249   std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1250 
1251   strSQL = PrepareSQL(
1252       "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
1253       " strTitle = '%s', iTrack = %i, iDuration = %i, "
1254       "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
1255       "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
1256       "dateAdded = '%s'",
1257       idPath, artistDisp.c_str(),
1258       StringUtils::Join(
1259           genres,
1260           CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)
1261           .c_str(),
1262       strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(),
1263       strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels,
1264       strDateMedia.c_str());
1265   if (strMusicBrainzTrackID.empty())
1266     strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL");
1267   else
1268     strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str());
1269   if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1270     strSQL += PrepareSQL(", strArtistSort = NULL");
1271   else
1272     strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str());
1273 
1274   strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
1275                        "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
1276                        iStartOffset, iEndOffset, rating, userrating, votes, strComment.c_str(),
1277                        strMood.c_str(), replayGain.Get().c_str());
1278 
1279   if (dtLastPlayed.IsValid())
1280     strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1281                          dtLastPlayed.GetAsDBDateTime().c_str());
1282   else if (iTimesPlayed > 0)
1283     strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1284                          CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
1285   else
1286     strSQL += ", iTimesPlayed = 0, lastplayed = NULL ";
1287   strSQL += PrepareSQL("WHERE idSong = %i", idSong);
1288 
1289   bool status = ExecuteQuery(strSQL);
1290 
1291   if (status)
1292     AnnounceUpdate(MediaTypeSong, idSong);
1293   return idSong;
1294 }
1295 
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)1296 int CMusicDatabase::AddAlbum(const std::string& strAlbum, const std::string& strMusicBrainzAlbumID,
1297                              const std::string& strReleaseGroupMBID,
1298                              const std::string& strArtist, const std::string& strArtistSort,
1299                              const std::string& strGenre,
1300                              const std::string& strReleaseDate, const std::string& strOrigReleaseDate,
1301                              bool bBoxedSet,
1302                              const std::string& strRecordLabel, const std::string& strType,
1303                              const std::string& strReleaseStatus,
1304                              bool bCompilation, CAlbum::ReleaseType releaseType)
1305 {
1306   std::string strSQL;
1307   try
1308   {
1309     if (nullptr == m_pDB)
1310       return -1;
1311     if (nullptr == m_pDS)
1312       return -1;
1313 
1314     if (!strMusicBrainzAlbumID.empty())
1315       strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
1316                         strMusicBrainzAlbumID.c_str());
1317     else
1318       strSQL = PrepareSQL("SELECT * FROM album WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' AND strMusicBrainzAlbumID IS NULL",
1319                           strArtist.c_str(),
1320                           strAlbum.c_str());
1321     m_pDS->query(strSQL);
1322     std::string strCheckFlag = strType;
1323     StringUtils::ToLower(strCheckFlag);
1324     if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type
1325       bBoxedSet = true;
1326     if (m_pDS->num_rows() == 0)
1327     {
1328       m_pDS->close();
1329       // Does not exist, add it
1330       strSQL = PrepareSQL(
1331           "INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
1332           "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
1333           "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType,  "
1334           "strMusicBrainzAlbumID, "
1335           "strReleaseGroupMBID, strArtistSort) "
1336           "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
1337           strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(),
1338           strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet,
1339           strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), bCompilation,
1340           CAlbum::ReleaseTypeToString(releaseType).c_str());
1341 
1342       if (strMusicBrainzAlbumID.empty())
1343         strSQL += PrepareSQL(", NULL");
1344       else
1345         strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str());
1346       if (strReleaseGroupMBID.empty())
1347         strSQL += PrepareSQL(", NULL");
1348       else
1349         strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str());
1350       if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1351         strSQL += PrepareSQL(", NULL");
1352       else
1353         strSQL += PrepareSQL(", '%s'", strArtistSort.c_str());
1354       strSQL += ")";
1355       m_pDS->exec(strSQL);
1356 
1357       return (int)m_pDS->lastinsertid();
1358     }
1359     else
1360     {
1361       /* Exists in our database and being re-scanned from tags, so we should update it as the details
1362          may have changed.
1363 
1364          Note that for multi-folder albums this will mean the last folder scanned will have the information
1365          stored for it.  Most values here should be the same across all songs anyway, but it does mean
1366          that if there's any inconsistencies then only the last folders information will be taken.
1367 
1368          We make sure we clear out the link tables (album artists, album sources) and we reset
1369          the last scraped time to make sure that online metadata is re-fetched. */
1370       int idAlbum = m_pDS->fv("idAlbum").get_asInt();
1371       m_pDS->close();
1372 
1373       strSQL = "UPDATE album SET ";
1374       if (!strMusicBrainzAlbumID.empty())
1375         strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", strAlbum.c_str(), strArtist.c_str());
1376       if (strReleaseGroupMBID.empty())
1377         strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,");
1378       else
1379         strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str());
1380       if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1381         strSQL += PrepareSQL(" strArtistSort = NULL");
1382       else
1383         strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str());
1384 
1385       strSQL +=
1386           PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1387                      "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
1388                      "bCompilation=%i, strReleaseType = '%s', "
1389                      "lastScraped = NULL "
1390                      "WHERE idAlbum=%i",
1391                      strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(),
1392                      bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(),
1393                      bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(),
1394                      idAlbum);
1395       m_pDS->exec(strSQL);
1396       DeleteAlbumArtistsByAlbum(idAlbum);
1397       DeleteAlbumSources(idAlbum);
1398       return idAlbum;
1399     }
1400   }
1401   catch (...)
1402   {
1403     CLog::Log(LOGERROR, "%s failed with query (%s)", __FUNCTION__, strSQL.c_str());
1404   }
1405 
1406   return -1;
1407 }
1408 
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)1409 int CMusicDatabase::UpdateAlbum(int idAlbum,
1410                                 const std::string& strAlbum,
1411                                 const std::string& strMusicBrainzAlbumID,
1412                                 const std::string& strReleaseGroupMBID,
1413                                 const std::string& strArtist,
1414                                 const std::string& strArtistSort,
1415                                 const std::string& strGenre,
1416                                 const std::string& strMoods,
1417                                 const std::string& strStyles,
1418                                 const std::string& strThemes,
1419                                 const std::string& strReview,
1420                                 const std::string& strImage,
1421                                 const std::string& strLabel,
1422                                 const std::string& strType,
1423                                 const std::string& strReleaseStatus,
1424                                 float fRating,
1425                                 int iUserrating,
1426                                 int iVotes,
1427                                 const std::string& strReleaseDate,
1428                                 const std::string& strOrigReleaseDate,
1429                                 bool bBoxedSet,
1430                                 bool bCompilation,
1431                                 CAlbum::ReleaseType releaseType,
1432                                 bool bScrapedMBID)
1433 {
1434   if (idAlbum < 0)
1435     return -1;
1436 
1437   // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1438   // Truncate value cleaning up xml when URLs exceeds this
1439   std::string strImageURLs = strImage;
1440   if (StringUtils::EqualsNoCase(
1441           CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1442           "mysql"))
1443     TrimImageURLs(strImageURLs, 65535);
1444 
1445   std::string strSQL;
1446   strSQL = PrepareSQL("UPDATE album SET "
1447                       " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
1448                       " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
1449                       " strReview = '%s', strImage = '%s', strLabel = '%s', "
1450                       " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
1451                       " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1452                       " bBoxedSet = %i, bCompilation = %i,"
1453                       " strReleaseType = '%s', strReleaseStatus = '%s', "
1454                       " lastScraped = '%s', bScrapedMBID = %i",
1455                       strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(),
1456                       strMoods.c_str(), strStyles.c_str(), strThemes.c_str(),
1457                       strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(),
1458                       strType.c_str(), fRating, iUserrating, iVotes,
1459                       strReleaseDate.c_str(), strOrigReleaseDate.c_str(),
1460                       bBoxedSet, bCompilation,
1461                       CAlbum::ReleaseTypeToString(releaseType).c_str(),
1462                       strReleaseStatus.c_str(),
1463                       CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(),
1464                       bScrapedMBID);
1465   if (strMusicBrainzAlbumID.empty())
1466     strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL");
1467   else
1468     strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str());
1469   if (strReleaseGroupMBID.empty())
1470     strSQL += PrepareSQL(", strReleaseGroupMBID = NULL");
1471   else
1472     strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str());
1473   if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1474     strSQL += PrepareSQL(", strArtistSort = NULL");
1475   else
1476     strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str());
1477 
1478   strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum);
1479 
1480   bool status = ExecuteQuery(strSQL);
1481   if (status)
1482     AnnounceUpdate(MediaTypeAlbum, idAlbum);
1483   return idAlbum;
1484 }
1485 
GetAlbum(int idAlbum,CAlbum & album,bool getSongs)1486 bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */)
1487 {
1488   try
1489   {
1490     if (nullptr == m_pDB)
1491       return false;
1492     if (nullptr == m_pDS)
1493       return false;
1494 
1495     if (idAlbum == -1)
1496       return false; // not in the database
1497 
1498     //Get album, song and album song info data using separate queries/datasets because we can have
1499     //multiple roles per artist for songs and that makes a single combined join impractical
1500     //Get album data
1501     std::string sql;
1502     sql = PrepareSQL("SELECT albumview.*,albumartistview.* "
1503       " FROM albumview "
1504       " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
1505       " WHERE albumview.idAlbum = %ld "
1506       " ORDER BY albumartistview.iOrder", idAlbum);
1507 
1508     CLog::Log(LOGDEBUG, "%s", sql.c_str());
1509     if (!m_pDS->query(sql)) return false;
1510     if (m_pDS->num_rows() == 0)
1511     {
1512       m_pDS->close();
1513       return false;
1514     }
1515 
1516     int albumArtistOffset = album_enumCount;
1517 
1518     album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0, true); // true to grab and parse the imageURL
1519     while (!m_pDS->eof())
1520     {
1521       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1522 
1523       // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
1524       // so there is only one row in the result set for each artist credit.
1525       album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
1526 
1527       m_pDS->next();
1528     }
1529     m_pDS->close(); // cleanup recordset data
1530 
1531     //Get song data
1532     if (getSongs)
1533     {
1534       sql = PrepareSQL("SELECT songview.*, songartistview.*"
1535         " FROM songview "
1536         " JOIN songartistview ON songview.idSong = songartistview.idSong "
1537         " WHERE songview.idAlbum = %ld "
1538         " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder", idAlbum);
1539 
1540       CLog::Log(LOGDEBUG, "%s", sql.c_str());
1541       if (!m_pDS->query(sql)) return false;
1542       if (m_pDS->num_rows() == 0)  //Album with no songs
1543       {
1544         m_pDS->close();
1545         return false;
1546       }
1547 
1548       int songArtistOffset = song_enumCount;
1549       std::set<int> songs;
1550       while (!m_pDS->eof())
1551       {
1552         const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1553 
1554         int idSong = record->at(song_idSong).get_asInt();  //Same as songartist.idSong by join
1555         if (songs.find(idSong) == songs.end())
1556         {
1557           album.songs.emplace_back(GetSongFromDataset(record));
1558           songs.insert(idSong);
1559         }
1560 
1561         int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1562         //By query order song is the last one appended to the album song vector.
1563         if (idSongArtistRole == ROLE_ARTIST)
1564           album.songs.back().artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
1565         else
1566           album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1567 
1568         m_pDS->next();
1569       }
1570       m_pDS->close(); // cleanup recordset data
1571     }
1572 
1573     return true;
1574   }
1575   catch (...)
1576   {
1577     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
1578   }
1579 
1580   return false;
1581 }
1582 
ClearAlbumLastScrapedTime(int idAlbum)1583 bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum)
1584 {
1585   std::string strSQL = PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum);
1586   return ExecuteQuery(strSQL);
1587 }
1588 
HasAlbumBeenScraped(int idAlbum)1589 bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum)
1590 {
1591   std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum);
1592   return GetSingleValue(strSQL).empty();
1593 }
1594 
AddGenre(std::string & strGenre)1595 int CMusicDatabase::AddGenre(std::string& strGenre)
1596 {
1597   std::string strSQL;
1598   try
1599   {
1600     StringUtils::Trim(strGenre);
1601 
1602     if (strGenre.empty())
1603       strGenre=g_localizeStrings.Get(13205); // Unknown
1604 
1605     if (nullptr == m_pDB)
1606       return -1;
1607     if (nullptr == m_pDS)
1608       return -1;
1609 
1610     auto it = m_genreCache.find(strGenre);
1611     if (it != m_genreCache.end())
1612       return it->second;
1613 
1614 
1615     strSQL=PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'", strGenre.c_str());
1616     m_pDS->query(strSQL);
1617     if (m_pDS->num_rows() == 0)
1618     {
1619       m_pDS->close();
1620       // doesn't exists, add it
1621       strSQL=PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )", strGenre.c_str());
1622       m_pDS->exec(strSQL);
1623 
1624       int idGenre = (int)m_pDS->lastinsertid();
1625       m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1626       return idGenre;
1627     }
1628     else
1629     {
1630       int idGenre = m_pDS->fv("idGenre").get_asInt();
1631       strGenre = m_pDS->fv("strGenre").get_asString();
1632       m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1633       m_pDS->close();
1634       return idGenre;
1635     }
1636   }
1637   catch (...)
1638   {
1639     CLog::Log(LOGERROR, "musicdatabase:unable to addgenre (%s)", strSQL.c_str());
1640   }
1641 
1642   return -1;
1643 }
1644 
UpdateArtist(const CArtist & artist)1645 bool CMusicDatabase::UpdateArtist(const CArtist& artist)
1646 {
1647   SetLibraryLastUpdated();
1648 
1649   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
1650 
1651   UpdateArtist(artist.idArtist,
1652                artist.strArtist, artist.strSortName,
1653                artist.strMusicBrainzArtistID, artist.bScrapedMBID,
1654                artist.strType, artist.strGender, artist.strDisambiguation,
1655                artist.strBorn, artist.strFormed,
1656                StringUtils::Join(artist.genre, itemSeparator),
1657                StringUtils::Join(artist.moods, itemSeparator),
1658                StringUtils::Join(artist.styles, itemSeparator),
1659                StringUtils::Join(artist.instruments, itemSeparator),
1660                artist.strBiography, artist.strDied,
1661                artist.strDisbanded,
1662                StringUtils::Join(artist.yearsActive, itemSeparator).c_str(),
1663                artist.thumbURL.GetData());
1664 
1665   DeleteArtistDiscography(artist.idArtist);
1666   for (const auto &disc : artist.discography)
1667   {
1668     AddArtistDiscography(artist.idArtist, disc);
1669   }
1670 
1671   // Set current artwork (held in art table)
1672   if (!artist.art.empty())
1673     SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
1674 
1675   return true;
1676 }
1677 
AddArtist(const std::string & strArtist,const std::string & strMusicBrainzArtistID,const std::string & strSortName,bool bScrapedMBID)1678 int CMusicDatabase::AddArtist(const std::string& strArtist, const std::string& strMusicBrainzArtistID, const std::string& strSortName, bool bScrapedMBID /* = false*/)
1679 {
1680   std::string strSQL;
1681   int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID);
1682   if (idArtist < 0 || strSortName.empty())
1683     return idArtist;
1684 
1685   /* Artist sort name always taken as the first value provided that is different from name, so only
1686      update when current sort name is blank. If a new sortname the same as name is provided then
1687      clear any sortname currently held.
1688   */
1689 
1690   try
1691   {
1692     if (nullptr == m_pDB)
1693       return -1;
1694     if (nullptr == m_pDS)
1695       return -1;
1696 
1697     strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist);
1698     m_pDS->query(strSQL);
1699     if (m_pDS->num_rows() != 1)
1700     {
1701       m_pDS->close();
1702       return -1;
1703     }
1704     std::string strArtistName, strArtistSort;
1705     strArtistName = m_pDS->fv("strArtist").get_asString();
1706     strArtistSort = m_pDS->fv("strSortName").get_asString();
1707     m_pDS->close();
1708 
1709     if (!strArtistSort.empty())
1710     {
1711       if (strSortName.compare(strArtistName) == 0)
1712         m_pDS->exec(PrepareSQL(
1713             "UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist));
1714     }
1715     else if (strSortName.compare(strArtistName) != 0)
1716       m_pDS->exec(PrepareSQL(
1717           "UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
1718           strSortName.c_str(), idArtist));
1719 
1720     return idArtist;
1721   }
1722 
1723   catch (...)
1724   {
1725     CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname (%s)", strSQL.c_str());
1726   }
1727 
1728   return -1;
1729 }
1730 
AddArtist(const std::string & strArtist,const std::string & strMusicBrainzArtistID,bool bScrapedMBID)1731 int CMusicDatabase::AddArtist(const std::string& strArtist, const std::string& strMusicBrainzArtistID, bool bScrapedMBID /* = false*/)
1732 {
1733   std::string strSQL;
1734   try
1735   {
1736     if (nullptr == m_pDB)
1737       return -1;
1738     if (nullptr == m_pDS)
1739       return -1;
1740 
1741     // 1) MusicBrainz
1742     if (!strMusicBrainzArtistID.empty())
1743     {
1744       // 1.a) Match on a MusicBrainz ID
1745       strSQL = PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
1746         strMusicBrainzArtistID.c_str());
1747       m_pDS->query(strSQL);
1748       if (m_pDS->num_rows() > 0)
1749       {
1750         int idArtist = m_pDS->fv("idArtist").get_asInt();
1751         bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0;
1752         m_pDS->close();
1753         if (update)
1754         {
1755           strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' "
1756             "WHERE idArtist = %i", strArtist.c_str(), idArtist);
1757           m_pDS->exec(strSQL);
1758           m_pDS->close();
1759         }
1760         return idArtist;
1761       }
1762       m_pDS->close();
1763 
1764 
1765       // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
1766       //     and update that if it exists.
1767       strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL", strArtist.c_str());
1768       m_pDS->query(strSQL);
1769       if (m_pDS->num_rows() > 0)
1770       {
1771         int idArtist = m_pDS->fv("idArtist").get_asInt();
1772         m_pDS->close();
1773         // 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
1774         strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
1775           "bScrapedMBID = %i WHERE idArtist = %i",
1776           strArtist.c_str(),
1777           strMusicBrainzArtistID.c_str(),
1778           bScrapedMBID,
1779           idArtist);
1780         m_pDS->exec(strSQL);
1781         return idArtist;
1782       }
1783 
1784       // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
1785       //    With MusicBrainz IDs this could return multiple artists and is non-determinstic
1786       //    Always pick the first artist ID returned by the DB to return.
1787     }
1788     else
1789     {
1790       strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'",
1791         strArtist.c_str());
1792 
1793       m_pDS->query(strSQL);
1794       if (m_pDS->num_rows() > 0)
1795       {
1796         int idArtist = m_pDS->fv("idArtist").get_asInt();
1797         m_pDS->close();
1798         return idArtist;
1799       }
1800       m_pDS->close();
1801     }
1802 
1803     // 3) No artist exists at all - add it, flagging when has scraped mbid
1804     if (strMusicBrainzArtistID.empty())
1805       strSQL = PrepareSQL("INSERT INTO artist "
1806         "(idArtist, strArtist, strMusicBrainzArtistID) "
1807         "VALUES( NULL, '%s', NULL)",
1808         strArtist.c_str());
1809     else
1810       strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
1811         "bScrapedMBID) "
1812         "VALUES( NULL, '%s', '%s', %i )",
1813         strArtist.c_str(),
1814         strMusicBrainzArtistID.c_str(), bScrapedMBID);
1815 
1816     m_pDS->exec(strSQL);
1817     int idArtist = (int)m_pDS->lastinsertid();
1818     return idArtist;
1819   }
1820   catch (...)
1821   {
1822     CLog::Log(LOGERROR, "musicdatabase:unable to addartist (%s)", strSQL.c_str());
1823   }
1824 
1825   return -1;
1826 }
1827 
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)1828 int  CMusicDatabase::UpdateArtist(int idArtist,
1829                                   const std::string& strArtist, const std::string& strSortName,
1830                                   const std::string& strMusicBrainzArtistID, const bool bScrapedMBID,
1831                                   const std::string& strType, const std::string& strGender,
1832                                   const std::string& strDisambiguation,
1833                                   const std::string& strBorn, const std::string& strFormed,
1834                                   const std::string& strGenres, const std::string& strMoods,
1835                                   const std::string& strStyles, const std::string& strInstruments,
1836                                   const std::string& strBiography, const std::string& strDied,
1837                                   const std::string& strDisbanded, const std::string& strYearsActive,
1838                                   const std::string& strImage)
1839 {
1840   if (idArtist < 0)
1841     return -1;
1842 
1843   // Check another artist with this mbid not already exist (an alias for example)
1844   bool useMBIDNull = strMusicBrainzArtistID.empty();
1845   bool isScrapedMBID = bScrapedMBID;
1846   std::string artistname;
1847   int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
1848   if (idArtistMbid > 0 && idArtistMbid != idArtist)
1849   {
1850     CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
1851       __FUNCTION__, strMusicBrainzArtistID.c_str(), artistname.c_str(), idArtistMbid,
1852       strArtist.c_str(), idArtist);
1853     useMBIDNull = true;
1854     isScrapedMBID = false;
1855   }
1856 
1857   // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1858   // Truncate value cleaning up xml when URLs exceeds this
1859   std::string strImageURLs = strImage;
1860   if (StringUtils::EqualsNoCase(
1861     CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1862     "mysql"))
1863     TrimImageURLs(strImageURLs, 65535);
1864 
1865   std::string strSQL;
1866   strSQL = PrepareSQL("UPDATE artist SET "
1867                       " strArtist = '%s', "
1868                       " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
1869                       " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
1870                       " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
1871                       " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
1872                       " strYearsActive = '%s', strImage = '%s', "
1873                       " lastScraped = '%s', bScrapedMBID = %i",
1874                       strArtist.c_str(),
1875                       /* strSortName.c_str(),*/
1876                       /* strMusicBrainzArtistID.c_str(), */
1877                       strType.c_str(), strGender.c_str(), strDisambiguation.c_str(),
1878                       strBorn.c_str(), strFormed.c_str(), strGenres.c_str(),
1879                       strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(),
1880                       strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(),
1881                       strYearsActive.c_str(), strImageURLs.c_str(),
1882                       CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID);
1883   if (useMBIDNull)
1884     strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL");
1885   else
1886     strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str());
1887   if (strSortName.empty())
1888     strSQL += PrepareSQL(", strSortName = NULL");
1889   else
1890     strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str());
1891 
1892   strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist);
1893 
1894   bool status = ExecuteQuery(strSQL);
1895   if (status)
1896     AnnounceUpdate(MediaTypeArtist, idArtist);
1897   return idArtist;
1898 }
1899 
UpdateArtistScrapedMBID(int idArtist,const std::string & strMusicBrainzArtistID)1900 bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist, const std::string& strMusicBrainzArtistID)
1901 {
1902   if (strMusicBrainzArtistID.empty() || idArtist < 0)
1903     return false;
1904 
1905   // Check another artist with this mbid not already exist (an alias for example)
1906   std::string artistname;
1907   int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
1908   if (idArtistMbid > 0 && idArtistMbid != idArtist)
1909   {
1910     CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__,
1911               strMusicBrainzArtistID.c_str(), artistname.c_str(), idArtistMbid);
1912     return false;
1913   }
1914 
1915   // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
1916   std::string strSQL;
1917   strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
1918     "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
1919     strMusicBrainzArtistID.c_str(), idArtist);
1920 
1921   bool status = ExecuteQuery(strSQL);
1922   if (status)
1923   {
1924     AnnounceUpdate(MediaTypeArtist, idArtist);
1925     return true;
1926   }
1927   return false;
1928 }
1929 
GetArtist(int idArtist,CArtist & artist,bool fetchAll)1930 bool CMusicDatabase::GetArtist(int idArtist, CArtist &artist, bool fetchAll /* = false */)
1931 {
1932   try
1933   {
1934     unsigned int time = XbmcThreads::SystemClockMillis();
1935     if (nullptr == m_pDB)
1936       return false;
1937     if (nullptr == m_pDS)
1938       return false;
1939 
1940     if (idArtist == -1)
1941       return false; // not in the database
1942 
1943     std::string strSQL;
1944     if (fetchAll)
1945       strSQL = PrepareSQL("SELECT * FROM artistview LEFT JOIN discography ON artistview.idArtist = discography.idArtist WHERE artistview.idArtist = %i", idArtist);
1946     else
1947       strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist);
1948 
1949     if (!m_pDS->query(strSQL)) return false;
1950     if (m_pDS->num_rows() == 0)
1951     {
1952       m_pDS->close();
1953       return false;
1954     }
1955 
1956     int discographyOffset = artist_enumCount;
1957 
1958     artist.discography.clear();
1959     artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs
1960     if (fetchAll)
1961     {
1962       while (!m_pDS->eof())
1963       {
1964         const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1965         CDiscoAlbum discoAlbum;
1966         discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString();
1967         discoAlbum.strYear = record->at(discographyOffset + 2).get_asString();
1968         discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString();
1969         m_pDS->next();
1970       }
1971     }
1972     m_pDS->close(); // cleanup recordset data
1973     CLog::Log(LOGDEBUG, LOGDATABASE, "{0}({1}) - took {2} ms", __FUNCTION__, strSQL, XbmcThreads::SystemClockMillis() - time);
1974     return true;
1975   }
1976   catch (...)
1977   {
1978     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
1979   }
1980 
1981   return false;
1982 }
1983 
GetArtistExists(int idArtist)1984 bool CMusicDatabase::GetArtistExists(int idArtist)
1985 {
1986   try
1987   {
1988     if (nullptr == m_pDB)
1989       return false;
1990     if (nullptr == m_pDS)
1991       return false;
1992 
1993     std::string strSQL = PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist);
1994 
1995     if (!m_pDS->query(strSQL)) return false;
1996     if (m_pDS->num_rows() == 0)
1997     {
1998       m_pDS->close();
1999       return false;
2000     }
2001     m_pDS->close(); // cleanup recordset data
2002     return true;
2003   }
2004   catch (...)
2005   {
2006     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2007   }
2008 
2009   return false;
2010 }
2011 
GetLastArtist()2012 int CMusicDatabase::GetLastArtist()
2013 {
2014   std::string strSQL = "SELECT MAX(idArtist) FROM artist";
2015   std::string lastArtist = GetSingleValue(strSQL);
2016   if (lastArtist.empty())
2017       return -1;
2018 
2019   return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10));
2020 }
2021 
GetArtistFromMBID(const std::string & strMusicBrainzArtistID,std::string & artistname)2022 int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID,
2023                                       std::string& artistname)
2024 {
2025   if (strMusicBrainzArtistID.empty())
2026     return -1;
2027 
2028   std::string strSQL;
2029   try
2030   {
2031     if (nullptr == m_pDB || nullptr == m_pDS2)
2032       return -1;
2033     // Match on MusicBrainz ID, definitively unique
2034     strSQL =
2035         PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
2036                    strMusicBrainzArtistID.c_str());
2037     if (!m_pDS2->query(strSQL))
2038       return -1;
2039     int idArtist = -1;
2040     if (m_pDS2->num_rows() > 0)
2041     {
2042       idArtist = m_pDS2->fv("idArtist").get_asInt();
2043       artistname = m_pDS2->fv("strArtist").get_asString();
2044     }
2045     m_pDS2->close();
2046     return idArtist;
2047   }
2048   catch (...)
2049   {
2050     CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__,
2051               strSQL.c_str());
2052   }
2053   return -1;
2054 }
2055 
HasArtistBeenScraped(int idArtist)2056 bool CMusicDatabase::HasArtistBeenScraped(int idArtist)
2057 {
2058   std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist);
2059   return GetSingleValue(strSQL).empty();
2060 }
2061 
ClearArtistLastScrapedTime(int idArtist)2062 bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist)
2063 {
2064   std::string strSQL = PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist);
2065   return ExecuteQuery(strSQL);
2066 }
2067 
AddArtistDiscography(int idArtist,const CDiscoAlbum & discoAlbum)2068 int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum)
2069 {
2070   std::string strSQL = PrepareSQL("INSERT INTO discography "
2071                                   "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
2072                                   "VALUES(%i, '%s', '%s', '%s')",
2073                                   idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(),
2074                                   discoAlbum.strReleaseGroupMBID.c_str());
2075   return ExecuteQuery(strSQL);
2076 }
2077 
DeleteArtistDiscography(int idArtist)2078 bool CMusicDatabase::DeleteArtistDiscography(int idArtist)
2079 {
2080   std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist);
2081   return ExecuteQuery(strSQL);
2082 }
2083 
GetArtistDiscography(int idArtist,CFileItemList & items)2084 bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items)
2085 {
2086   try
2087   {
2088     if (nullptr == m_pDB)
2089       return false;
2090     if (nullptr == m_pDS)
2091       return false;
2092 
2093     /* Combine entries from discography and album tables
2094        Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using
2095        correlated subqueries to a temp table. An updatable join to temp table would work in MySQL
2096        but SQLite not support updatable joins.
2097     */
2098     m_pDS->exec("CREATE TABLE tempDisco "
2099                 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2100     m_pDS->exec("CREATE TABLE tempAlbum "
2101                 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2102 
2103     std::string strSQL;
2104     strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
2105                         "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
2106                         "strReleaseGroupMBID, NULL "
2107                         "FROM discography WHERE idArtist = %i",
2108                         idArtist);
2109     m_pDS->exec(strSQL);
2110 
2111     strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
2112                         "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
2113                         "strReleaseGroupMBID, album.idAlbum "
2114                         "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
2115                         "WHERE idArtist = %i",
2116                         idArtist);
2117     m_pDS->exec(strSQL);
2118 
2119     // Match albums on release group mbid, if multi-releases then first used
2120     // Only use albums credited to this artist
2121     strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
2122              "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
2123     m_pDS->exec(strSQL);
2124     //Delete matched albums
2125     strSQL = "DELETE FROM tempAlbum "
2126              "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2127     m_pDS->exec(strSQL);
2128 
2129     // Match remaining to albums by artist on title and year
2130     strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2131              "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
2132              "AND tempAlbum.strYear = tempDisco.strYear) "
2133              "WHERE tempDisco.idAlbum is NULL";
2134     m_pDS->exec(strSQL);
2135     //Delete matched albums
2136     strSQL = "DELETE FROM tempAlbum "
2137       "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2138     m_pDS->exec(strSQL);
2139 
2140     // Match remaining to albums by artist on title only
2141     strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2142              "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
2143              "WHERE tempDisco.idAlbum is NULL";
2144     m_pDS->exec(strSQL);
2145     // Use year from album table, when matched by title only as it could be different
2146     strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
2147              "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
2148              "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
2149     m_pDS->exec(strSQL);
2150     //Delete matched albums
2151     strSQL = "DELETE FROM tempAlbum "
2152              "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2153     m_pDS->exec(strSQL);
2154 
2155     // Combine distinctly with any remaining unmatched albums by artist
2156     strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
2157              "UNION "
2158              "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
2159              "ORDER BY strYear, strAlbum, idAlbum";
2160 
2161     if (!m_pDS->query(strSQL))
2162       return false;
2163     int iRowsFound = m_pDS->num_rows();
2164     if (iRowsFound == 0)
2165     {
2166       m_pDS->close();
2167       return true;
2168     }
2169 
2170     while (!m_pDS->eof())
2171     {
2172       int idAlbum = m_pDS->fv("idAlbum").get_asInt();
2173       if (idAlbum == 0)
2174         idAlbum = -1;
2175       std::string strAlbum = m_pDS->fv("strAlbum").get_asString();
2176       if (!strAlbum.empty())
2177       {
2178           CFileItemPtr pItem(new CFileItem(strAlbum));
2179           pItem->SetLabel2(m_pDS->fv("strYear").get_asString());
2180           pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum);
2181           items.Add(pItem);
2182       }
2183       m_pDS->next();
2184     }
2185 
2186     // cleanup
2187     m_pDS->close();
2188     m_pDS->exec("DROP TABLE tempDisco");
2189     m_pDS->exec("DROP TABLE tempAlbum");
2190 
2191     return true;
2192   }
2193   catch (...)
2194   {
2195     m_pDS->exec("DROP TABLE tempDisco");
2196     m_pDS->exec("DROP TABLE tempAlbum");
2197     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
2198   }
2199   return false;
2200 }
2201 
AddRole(const std::string & strRole)2202 int CMusicDatabase::AddRole(const std::string &strRole)
2203 {
2204   int idRole = -1;
2205   std::string strSQL;
2206 
2207   try
2208   {
2209     if (nullptr == m_pDB)
2210       return -1;
2211     if (nullptr == m_pDS)
2212       return -1;
2213     strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str());
2214     m_pDS->query(strSQL);
2215     if (m_pDS->num_rows() > 0)
2216       idRole = m_pDS->fv("idRole").get_asInt();
2217     m_pDS->close();
2218 
2219     if (idRole < 0)
2220     {
2221       strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str());
2222       m_pDS->exec(strSQL);
2223       idRole = static_cast<int>(m_pDS->lastinsertid());
2224       m_pDS->close();
2225     }
2226   }
2227   catch (...)
2228   {
2229     CLog::Log(LOGERROR, "musicdatabase:unable to AddRole (%s)", strSQL.c_str());
2230   }
2231   return idRole;
2232 }
2233 
AddSongArtist(int idArtist,int idSong,const std::string & strRole,const std::string & strArtist,int iOrder)2234 bool CMusicDatabase::AddSongArtist(int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder)
2235 {
2236   int idRole = AddRole(strRole);
2237   return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder);
2238 }
2239 
AddSongArtist(int idArtist,int idSong,int idRole,const std::string & strArtist,int iOrder)2240 bool CMusicDatabase::AddSongArtist(int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder)
2241 {
2242   std::string strSQL;
2243   strSQL = PrepareSQL("replace into song_artist (idArtist, idSong, idRole, strArtist, iOrder) values(%i,%i,%i,'%s',%i)",
2244     idArtist, idSong, idRole, strArtist.c_str(), iOrder);
2245   return ExecuteQuery(strSQL);
2246 }
2247 
AddSongContributor(int idSong,const std::string & strRole,const std::string & strArtist,const std::string & strSort)2248 int CMusicDatabase::AddSongContributor(int idSong, const std::string& strRole, const std::string& strArtist, const std::string &strSort)
2249 {
2250   if (strArtist.empty())
2251     return -1;
2252 
2253   std::string strSQL;
2254   try
2255   {
2256     if (nullptr == m_pDB)
2257       return -1;
2258     if (nullptr == m_pDS)
2259       return -1;
2260 
2261     int idArtist = -1;
2262     // Add artist. As we only have name (no MBID) first try to identify artist from song
2263     // as they may have already been added with a different role (including MBID).
2264     strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ", idSong, strArtist.c_str());
2265     m_pDS->query(strSQL);
2266     if (m_pDS->num_rows() > 0)
2267       idArtist = m_pDS->fv("idArtist").get_asInt();
2268     m_pDS->close();
2269 
2270     if (idArtist < 0)
2271       idArtist = AddArtist(strArtist, "", strSort);
2272 
2273     // Add to song_artist table
2274     AddSongArtist(idArtist, idSong, strRole, strArtist, 0);
2275 
2276     return idArtist;
2277   }
2278   catch (...)
2279   {
2280     CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor (%s)", strSQL.c_str());
2281   }
2282 
2283   return -1;
2284 }
2285 
AddSongContributors(int idSong,const VECMUSICROLES & contributors,const std::string & strSort)2286 void CMusicDatabase::AddSongContributors(int idSong, const VECMUSICROLES& contributors, const std::string &strSort)
2287 {
2288   std::vector<std::string> composerSort;
2289   size_t countComposer = 0;
2290   if (!strSort.empty())
2291   {
2292     composerSort = StringUtils::Split(strSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2293   }
2294 
2295   for (const auto &credit : contributors)
2296   {
2297     std::string strSortName;
2298     //Identify composer sort name if we have it
2299     if (countComposer < composerSort.size())
2300     {
2301       if (credit.GetRoleDesc().compare("Composer") == 0)
2302       {
2303         strSortName = composerSort[countComposer];
2304         countComposer++;
2305       }
2306     }
2307     AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName);
2308   }
2309 }
2310 
GetRoleByName(const std::string & strRole)2311 int CMusicDatabase::GetRoleByName(const std::string& strRole)
2312 {
2313   try
2314   {
2315     if (nullptr == m_pDB)
2316       return false;
2317     if (nullptr == m_pDS)
2318       return false;
2319 
2320     std::string strSQL;
2321     strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str());
2322     // run query
2323     if (!m_pDS->query(strSQL)) return false;
2324     int iRowsFound = m_pDS->num_rows();
2325     if (iRowsFound != 1)
2326     {
2327       m_pDS->close();
2328       return -1;
2329     }
2330     return m_pDS->fv("idRole").get_asInt();
2331   }
2332   catch (...)
2333   {
2334     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
2335   }
2336   return -1;
2337 
2338 }
2339 
GetRolesByArtist(int idArtist,CFileItem * item)2340 bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item)
2341 {
2342   try
2343   {
2344     std::string strSQL = PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole FROM song_artist JOIN role ON "
2345                                     " song_artist.idRole = role.idRole WHERE idArtist = %i ORDER BY song_artist.idRole ASC", idArtist);
2346     if (!m_pDS->query(strSQL))
2347       return false;
2348     if (m_pDS->num_rows() == 0)
2349     {
2350       m_pDS->close();
2351       return true;
2352     }
2353 
2354     CVariant artistRoles(CVariant::VariantTypeArray);
2355 
2356     while (!m_pDS->eof())
2357     {
2358       CVariant roleObj;
2359       roleObj["role"] = m_pDS->fv("strRole").get_asString();
2360       roleObj["roleid"] = m_pDS->fv("idrole").get_asInt();
2361       artistRoles.push_back(roleObj);
2362       m_pDS->next();
2363     }
2364     m_pDS->close();
2365 
2366     item->SetProperty("roles", artistRoles);
2367     return true;
2368   }
2369   catch (...)
2370   {
2371     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2372   }
2373   return false;
2374 }
2375 
DeleteSongArtistsBySong(int idSong)2376 bool CMusicDatabase::DeleteSongArtistsBySong(int idSong)
2377 {
2378   return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong));
2379 }
2380 
AddAlbumArtist(int idArtist,int idAlbum,const std::string & strArtist,int iOrder)2381 bool CMusicDatabase::AddAlbumArtist(int idArtist,
2382                                     int idAlbum,
2383                                     const std::string& strArtist,
2384                                     int iOrder)
2385 {
2386   std::string strSQL;
2387   strSQL = PrepareSQL("replace into album_artist (idArtist, idAlbum, strArtist, iOrder) values(%i,%i,'%s',%i)",
2388     idArtist, idAlbum, strArtist.c_str(), iOrder);
2389   return ExecuteQuery(strSQL);
2390 }
2391 
DeleteAlbumArtistsByAlbum(int idAlbum)2392 bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum)
2393 {
2394   return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum));
2395 }
2396 
AddSongGenres(int idSong,const std::vector<std::string> & genres)2397 bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres)
2398 {
2399   if (idSong == -1)
2400     return true;
2401 
2402   std::string strSQL;
2403   try
2404   {
2405     // Clear current entries for song
2406     strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong);
2407     if (!ExecuteQuery(strSQL))
2408       return false;
2409     unsigned int index = 0;
2410     std::vector<std::string> modgenres = genres;
2411     for (auto &strGenre : modgenres)
2412     {
2413       int idGenre = AddGenre(strGenre); // Genre string trimed and matched case insensitively
2414       strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
2415         idGenre, idSong, index++);
2416       if (!ExecuteQuery(strSQL))
2417         return false;
2418     }
2419     // Update concatenated genre string from the standardised genre values
2420     std::string strGenres = StringUtils::Join(modgenres, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2421     strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", strGenres.c_str(), idSong);
2422     if (!ExecuteQuery(strSQL))
2423       return false;
2424 
2425     return true;
2426   }
2427   catch (...)
2428   {
2429     CLog::Log(LOGERROR, "%s(%i) %s failed", __FUNCTION__, idSong, strSQL.c_str());
2430   }
2431   return false;
2432 }
2433 
GetAlbumsByArtist(int idArtist,std::vector<int> & albums)2434 bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int> &albums)
2435 {
2436   try
2437   {
2438     std::string strSQL;
2439     strSQL = PrepareSQL("SELECT idAlbum  FROM album_artist WHERE idArtist = %i", idArtist);
2440     if (!m_pDS->query(strSQL))
2441       return false;
2442     if (m_pDS->num_rows() == 0)
2443     {
2444       m_pDS->close();
2445       return false;
2446     }
2447 
2448     while (!m_pDS->eof())
2449     {
2450       albums.push_back(m_pDS->fv("idAlbum").get_asInt());
2451       m_pDS->next();
2452     }
2453     m_pDS->close();
2454     return true;
2455   }
2456   catch (...)
2457   {
2458     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2459   }
2460   return false;
2461 }
2462 
GetArtistsByAlbum(int idAlbum,CFileItem * item)2463 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item)
2464 {
2465   try
2466   {
2467     std::string strSQL;
2468 
2469     strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum);
2470 
2471     if (!m_pDS->query(strSQL))
2472       return false;
2473     if (m_pDS->num_rows() == 0)
2474     {
2475       m_pDS->close();
2476       return false;
2477     }
2478 
2479     // Get album artist credits
2480     VECARTISTCREDITS artistCredits;
2481     while (!m_pDS->eof())
2482     {
2483       artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0));
2484       m_pDS->next();
2485     }
2486     m_pDS->close();
2487 
2488     // Populate item with song albumartist credits
2489     std::vector<std::string> musicBrainzID;
2490     std::vector<std::string> albumartists;
2491     CVariant artistidObj(CVariant::VariantTypeArray);
2492     for (const auto &artistCredit : artistCredits)
2493     {
2494       artistidObj.push_back(artistCredit.GetArtistId());
2495       albumartists.emplace_back(artistCredit.GetArtist());
2496       if (!artistCredit.GetMusicBrainzArtistID().empty())
2497         musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID());
2498     }
2499     item->GetMusicInfoTag()->SetAlbumArtist(albumartists);
2500     item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID);
2501     // Add song albumartistIds as separate property as not part of CMusicInfoTag
2502     item->SetProperty("albumartistid", artistidObj);
2503 
2504     return true;
2505   }
2506   catch (...)
2507   {
2508     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
2509   }
2510   return false;
2511 }
2512 
GetArtistsByAlbum(int idAlbum,std::vector<std::string> & artistIDs)2513 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs)
2514 {
2515   try
2516   {
2517     std::string strSQL;
2518     // Get distinct song and album artist IDs for this album, no other roles
2519     // Allow for artists that are only album artists and not song artists
2520     strSQL = PrepareSQL("SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
2521       "UNION \n"
2522       "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
2523       "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
2524       idAlbum, idAlbum);
2525 
2526     if (!m_pDS->query(strSQL))
2527       return false;
2528     if (m_pDS->num_rows() == 0)
2529     {
2530       m_pDS->close();
2531       return false;
2532     }
2533     while (!m_pDS->eof())
2534     {
2535       // Get ID as string so can easily join to make "IN" clause
2536       artistIDs.push_back(m_pDS->fv("idArtist").get_asString());
2537       m_pDS->next();
2538     }
2539     m_pDS->close();
2540     return true;
2541   }
2542   catch (...)
2543   {
2544     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
2545   }
2546   return false;
2547 };
2548 
GetSongsByArtist(int idArtist,std::vector<int> & songs)2549 bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int> &songs)
2550 {
2551   try
2552   {
2553     std::string strSQL;
2554     //Restrict to Artists only, no other roles
2555     strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", idArtist);
2556     if (!m_pDS->query(strSQL))
2557       return false;
2558     if (m_pDS->num_rows() == 0)
2559     {
2560       m_pDS->close();
2561       return false;
2562     }
2563 
2564     while (!m_pDS->eof())
2565     {
2566       songs.push_back(m_pDS->fv("idSong").get_asInt());
2567       m_pDS->next();
2568     }
2569     m_pDS->close();
2570     return true;
2571   }
2572   catch (...)
2573   {
2574     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2575   }
2576   return false;
2577 };
2578 
GetArtistsBySong(int idSong,std::vector<int> & artists)2579 bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int> &artists)
2580 {
2581   try
2582   {
2583     std::string strSQL;
2584     //Restrict to Artists only, no other roles
2585     strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", idSong);
2586     if (!m_pDS->query(strSQL))
2587       return false;
2588     if (m_pDS->num_rows() == 0)
2589     {
2590       m_pDS->close();
2591       return false;
2592     }
2593 
2594     while (!m_pDS->eof())
2595     {
2596       artists.push_back(m_pDS->fv("idArtist").get_asInt());
2597       m_pDS->next();
2598     }
2599     m_pDS->close();
2600     return true;
2601   }
2602   catch (...)
2603   {
2604     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
2605   }
2606   return false;
2607 }
2608 
GetGenresByArtist(int idArtist,CFileItem * item)2609 bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item)
2610 {
2611   try
2612   {
2613     std::string strSQL;
2614     strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2615       "album_artist JOIN song ON album_artist.idAlbum = song.idAlbum "
2616       "JOIN song_genre ON song.idSong = song_genre.idSong "
2617       "JOIN genre ON song_genre.idGenre = genre.idGenre "
2618       "WHERE album_artist.idArtist = %i "
2619       "ORDER BY song_genre.idGenre", idArtist);
2620     if (!m_pDS->query(strSQL))
2621       return false;
2622     if (m_pDS->num_rows() == 0)
2623     {
2624       // Artist does have any song genres via albums may not be an album artist.
2625       // Check via songs artist to fetch song genres from compilations or where they are guest artist
2626       m_pDS->close();
2627       strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2628         "song_artist JOIN song_genre ON song_artist.idSong = song_genre.idSong "
2629         "JOIN genre ON song_genre.idGenre = genre.idGenre "
2630         "WHERE song_artist.idArtist = %i "
2631         "ORDER BY song_genre.idGenre", idArtist);
2632       if (!m_pDS->query(strSQL))
2633         return false;
2634       if (m_pDS->num_rows() == 0)
2635       {
2636         //No song genres, but query sucessfull
2637         m_pDS->close();
2638         return true;
2639       }
2640     }
2641 
2642     CVariant artistSongGenres(CVariant::VariantTypeArray);
2643 
2644     while (!m_pDS->eof())
2645     {
2646       CVariant genreObj;
2647       genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2648       genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2649       artistSongGenres.push_back(genreObj);
2650       m_pDS->next();
2651     }
2652     m_pDS->close();
2653 
2654     item->SetProperty("songgenres", artistSongGenres);
2655     return true;
2656   }
2657   catch (...)
2658   {
2659     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2660   }
2661   return false;
2662 }
2663 
GetGenresByAlbum(int idAlbum,CFileItem * item)2664 bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item)
2665 {
2666   try
2667   {
2668     std::string strSQL;
2669     strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2670       "song JOIN song_genre ON song.idSong = song_genre.idSong "
2671       "JOIN genre ON song_genre.idGenre = genre.idGenre "
2672       "WHERE song.idAlbum = %i "
2673       "ORDER BY song_genre.idSong, song_genre.iOrder", idAlbum);
2674     if (!m_pDS->query(strSQL))
2675       return false;
2676     if (m_pDS->num_rows() == 0)
2677     {
2678       //No song genres, but query sucessfull
2679       m_pDS->close();
2680       return true;
2681     }
2682 
2683     CVariant albumSongGenres(CVariant::VariantTypeArray);
2684 
2685     while (!m_pDS->eof())
2686     {
2687       CVariant genreObj;
2688       genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2689       genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2690       albumSongGenres.push_back(genreObj);
2691       m_pDS->next();
2692     }
2693     m_pDS->close();
2694 
2695     item->SetProperty("songgenres", albumSongGenres);
2696     return true;
2697   }
2698   catch (...)
2699   {
2700     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
2701   }
2702   return false;
2703 }
2704 
GetGenresBySong(int idSong,std::vector<int> & genres)2705 bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres)
2706 {
2707   try
2708   {
2709     std::string strSQL = PrepareSQL("select idGenre from song_genre where idSong = %i ORDER BY iOrder ASC", idSong);
2710     if (!m_pDS->query(strSQL))
2711       return false;
2712     if (m_pDS->num_rows() == 0)
2713     {
2714       m_pDS->close();
2715       return true;
2716     }
2717 
2718     while (!m_pDS->eof())
2719     {
2720       genres.push_back(m_pDS->fv("idGenre").get_asInt());
2721       m_pDS->next();
2722     }
2723     m_pDS->close();
2724 
2725     return true;
2726   }
2727   catch (...)
2728   {
2729     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
2730   }
2731   return false;
2732 }
2733 
GetIsAlbumArtist(int idArtist,CFileItem * item)2734 bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item)
2735 {
2736   try
2737   {
2738     int countalbum = GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist));
2739     CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean);
2740     IsAlbumArtistObj = (countalbum > 0);
2741     item->SetProperty("isalbumartist", IsAlbumArtistObj);
2742     return true;
2743   }
2744   catch (...)
2745   {
2746     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2747   }
2748   return false;
2749 }
2750 
2751 
AddPath(const std::string & strPath1)2752 int CMusicDatabase::AddPath(const std::string& strPath1)
2753 {
2754   std::string strSQL;
2755   try
2756   {
2757     std::string strPath(strPath1);
2758     if (!URIUtils::HasSlashAtEnd(strPath))
2759       URIUtils::AddSlashAtEnd(strPath);
2760 
2761     if (nullptr == m_pDB)
2762       return -1;
2763     if (nullptr == m_pDS)
2764       return -1;
2765 
2766     auto it = m_pathCache.find(strPath);
2767     if (it != m_pathCache.end())
2768       return it->second;
2769 
2770     strSQL=PrepareSQL( "select * from path where strPath='%s'", strPath.c_str());
2771     m_pDS->query(strSQL);
2772     if (m_pDS->num_rows() == 0)
2773     {
2774       m_pDS->close();
2775       // doesn't exists, add it
2776       strSQL=PrepareSQL("insert into path (idPath, strPath) values( NULL, '%s' )", strPath.c_str());
2777       m_pDS->exec(strSQL);
2778 
2779       int idPath = (int)m_pDS->lastinsertid();
2780       m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
2781       return idPath;
2782     }
2783     else
2784     {
2785       int idPath = m_pDS->fv("idPath").get_asInt();
2786       m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
2787       m_pDS->close();
2788       return idPath;
2789     }
2790   }
2791   catch (...)
2792   {
2793     CLog::Log(LOGERROR, "musicdatabase:unable to addpath (%s)", strSQL.c_str());
2794   }
2795 
2796   return -1;
2797 }
2798 
GetSongFromDataset()2799 CSong CMusicDatabase::GetSongFromDataset()
2800 {
2801   return GetSongFromDataset(m_pDS->get_sql_record());
2802 }
2803 
GetSongFromDataset(const dbiplus::sql_record * const record,int offset)2804 CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */)
2805 {
2806   CSong song;
2807   song.idSong = record->at(offset + song_idSong).get_asInt();
2808   // Note this function does not populate artist credits, this must be done separately.
2809   // However artist names are held as a descriptive string
2810   song.strArtistDesc = record->at(offset + song_strArtists).get_asString();
2811   song.strArtistSort = record->at(offset + song_strArtistSort).get_asString();
2812   // Get the full genre string
2813   song.genre = StringUtils::Split(record->at(offset + song_strGenres).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2814   // and the rest...
2815   song.strAlbum = record->at(offset + song_strAlbum).get_asString();
2816   song.idAlbum = record->at(offset + song_idAlbum).get_asInt();
2817   song.iTrack = record->at(offset + song_iTrack).get_asInt() ;
2818   song.iDuration = record->at(offset + song_iDuration).get_asInt() ;
2819   song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString();
2820   song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString();
2821   song.strTitle = record->at(offset + song_strTitle).get_asString();
2822   song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt();
2823   song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString());
2824   song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString());
2825   song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString());
2826   song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString());
2827   song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt();
2828   song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt();
2829   song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString();
2830   song.rating = record->at(offset + song_rating).get_asFloat();
2831   song.userrating = record->at(offset + song_userrating).get_asInt();
2832   song.votes = record->at(offset + song_votes).get_asInt();
2833   song.strComment = record->at(offset + song_comment).get_asString();
2834   song.strMood = record->at(offset + song_mood).get_asString();
2835   song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1;
2836   song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString();
2837   // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
2838   song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString());
2839   // Get filename with full path
2840   song.strFileName = URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(), record->at(offset + song_strFileName).get_asString());
2841   song.iBPM = record->at(offset + song_iBPM).get_asInt();
2842   song.iBitRate = record->at(offset + song_iBitRate).get_asInt();
2843   song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt();
2844   song.iChannels = record->at(offset + song_iChannels).get_asInt();
2845   return song;
2846 }
2847 
GetFileItemFromDataset(CFileItem * item,const CMusicDbUrl & baseUrl)2848 void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl &baseUrl)
2849 {
2850   GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl);
2851 }
2852 
GetFileItemFromDataset(const dbiplus::sql_record * const record,CFileItem * item,const CMusicDbUrl & baseUrl)2853 void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record, CFileItem* item, const CMusicDbUrl &baseUrl)
2854 {
2855   // get the artist string from songview (not the song_artist and artist tables)
2856   item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString());
2857   // get the artist sort name string from songview (not the song_artist and artist tables)
2858   item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString());
2859   // and the full genre string
2860   item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString());
2861   // and the rest...
2862   item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString());
2863   item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt());
2864   item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt());
2865   item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt());
2866   item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong);
2867   item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString());
2868   item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString());
2869   item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString());
2870   item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString());
2871   item->SetLabel(record->at(song_strTitle).get_asString());
2872   item->m_lStartOffset = record->at(song_iStartOffset).get_asInt64();
2873   item->SetProperty("item_start", item->m_lStartOffset);
2874   item->m_lEndOffset = record->at(song_iEndOffset).get_asInt64();
2875   item->GetMusicInfoTag()->SetMusicBrainzTrackID(record->at(song_strMusicBrainzTrackID).get_asString());
2876   item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat());
2877   item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt());
2878   item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt());
2879   item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString());
2880   item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString());
2881   item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt());
2882   item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString());
2883   item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString());
2884   item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString());
2885   item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString());
2886   std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(), record->at(song_strFileName).get_asString());
2887   item->GetMusicInfoTag()->SetURL(strRealPath);
2888   item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1);
2889   item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1);
2890   // get the album artist string from songview (not the album_artist and artist tables)
2891   item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString());
2892   item->GetMusicInfoTag()->SetAlbumReleaseType(CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString()));
2893   item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt());
2894   item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt());
2895   item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt());
2896   item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt());
2897   // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
2898   ReplayGain replaygain;
2899   replaygain.Set(record->at(song_strReplayGain).get_asString());
2900   item->GetMusicInfoTag()->SetReplayGain(replaygain);
2901   item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt());
2902 
2903   item->GetMusicInfoTag()->SetLoaded(true);
2904   // Get filename with full path
2905   if (!baseUrl.IsValid())
2906     item->SetPath(strRealPath);
2907   else
2908   {
2909     CMusicDbUrl itemUrl = baseUrl;
2910     std::string strFileName = record->at(song_strFileName).get_asString();
2911     std::string strExt = URIUtils::GetExtension(strFileName);
2912     std::string path = StringUtils::Format("%i%s", record->at(song_idSong).get_asInt(), strExt.c_str());
2913     itemUrl.AppendPath(path);
2914     item->SetPath(itemUrl.ToString());
2915     item->SetDynPath(strRealPath);
2916   }
2917 }
2918 
GetFileItemFromArtistCredits(VECARTISTCREDITS & artistCredits,CFileItem * item)2919 void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item)
2920 {
2921   // Populate fileitem with artists from vector of artist credits
2922   std::vector<std::string> musicBrainzID;
2923   std::vector<std::string> songartists;
2924   CVariant artistidObj(CVariant::VariantTypeArray);
2925 
2926   // When "missing tag" artist, it is the only artist when present.
2927   if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID)
2928   {
2929     artistidObj.push_back((int)BLANKARTIST_ID);
2930     songartists.push_back(StringUtils::Empty);
2931   }
2932   else
2933   {
2934     for (const auto &artistCredit : artistCredits)
2935     {
2936       artistidObj.push_back(artistCredit.GetArtistId());
2937       songartists.push_back(artistCredit.GetArtist());
2938       if (!artistCredit.GetMusicBrainzArtistID().empty())
2939         musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
2940     }
2941   }
2942   item->GetMusicInfoTag()->SetArtist(songartists); // Also sets ArtistDesc if empty from song.strArtist field
2943   item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID);
2944   // Add album artistIds as separate property as not part of CMusicInfoTag
2945   item->SetProperty("artistid", artistidObj);
2946 }
2947 
GetAlbumFromDataset(dbiplus::Dataset * pDS,int offset,bool imageURL)2948 CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS, int offset /* = 0 */, bool imageURL /* = false*/)
2949 {
2950   return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL);
2951 }
2952 
GetAlbumFromDataset(const dbiplus::sql_record * const record,int offset,bool imageURL)2953 CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */, bool imageURL /* = false*/)
2954 {
2955   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
2956 
2957   CAlbum album;
2958   album.idAlbum = record->at(offset + album_idAlbum).get_asInt();
2959   album.strAlbum = record->at(offset + album_strAlbum).get_asString();
2960   if (album.strAlbum.empty())
2961     album.strAlbum = g_localizeStrings.Get(1050);
2962   album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString();
2963   album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString();
2964   album.strArtistDesc = record->at(offset + album_strArtists).get_asString();
2965   album.strArtistSort = record->at(offset + album_strArtistSort).get_asString();
2966   album.genre = StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator);
2967   album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString();
2968   album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString();
2969   album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1;
2970   if (imageURL)
2971     album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString());
2972   album.fRating = record->at(offset + album_fRating).get_asFloat();
2973   album.iUserrating = record->at(offset + album_iUserrating).get_asInt();
2974   album.iVotes = record->at(offset + album_iVotes).get_asInt();
2975   album.strReview = record->at(offset + album_strReview).get_asString();
2976   album.styles = StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator);
2977   album.moods = StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator);
2978   album.themes = StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator);
2979   album.strLabel = record->at(offset + album_strLabel).get_asString();
2980   album.strType = record->at(offset + album_strType).get_asString();
2981   album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString();
2982   album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1;
2983   album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1;
2984   album.strLastScraped = record->at(offset + album_lastScraped).get_asString();
2985   album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt();
2986   album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString());
2987   album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt();
2988   album.SetDateAdded(record->at(offset + album_dateAdded).get_asString());
2989   album.SetDateNew(record->at(offset + album_dateNew).get_asString());
2990   album.SetDateUpdated(record->at(offset + album_dateModified).get_asString());
2991   album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString());
2992   album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt();
2993   return album;
2994 }
2995 
GetArtistCreditFromDataset(const dbiplus::sql_record * const record,int offset)2996 CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */)
2997 {
2998   CArtistCredit artistCredit;
2999   artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt();
3000   if (artistCredit.idArtist == BLANKARTIST_ID)
3001     artistCredit.m_strArtist = StringUtils::Empty;
3002   else
3003   {
3004     artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString();
3005     artistCredit.m_strMusicBrainzArtistID = record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString();
3006   }
3007   return artistCredit;
3008 }
3009 
GetArtistRoleFromDataset(const dbiplus::sql_record * const record,int offset)3010 CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */)
3011 {
3012   CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(),
3013                         record->at(offset + artistCredit_strRole).get_asString(),
3014                         record->at(offset + artistCredit_strArtist).get_asString(),
3015                         record->at(offset + artistCredit_idArtist).get_asInt());
3016   return ArtistRole;
3017 }
3018 
GetArtistFromDataset(dbiplus::Dataset * pDS,int offset,bool needThumb)3019 CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS, int offset /* = 0 */, bool needThumb /* = true */)
3020 {
3021   return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb);
3022 }
3023 
GetArtistFromDataset(const dbiplus::sql_record * const record,int offset,bool needThumb)3024 CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */, bool needThumb /* = true */)
3025 {
3026   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
3027 
3028   CArtist artist;
3029   artist.idArtist = record->at(offset + artist_idArtist).get_asInt();
3030   if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist)
3031     artist.strArtist = g_localizeStrings.Get(38042);  //Missing artist tag in current language
3032   else
3033     artist.strArtist = record->at(offset + artist_strArtist).get_asString();
3034   artist.strSortName = record->at(offset + artist_strSortName).get_asString();
3035   artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString();
3036   artist.strType = record->at(offset + artist_strType).get_asString();
3037   artist.strGender = record->at(offset + artist_strGender).get_asString();
3038   artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString();
3039   artist.genre = StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator);
3040   artist.strBiography = record->at(offset + artist_strBiography).get_asString();
3041   artist.styles = StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator);
3042   artist.moods = StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator);
3043   artist.strBorn = record->at(offset + artist_strBorn).get_asString();
3044   artist.strFormed = record->at(offset + artist_strFormed).get_asString();
3045   artist.strDied = record->at(offset + artist_strDied).get_asString();
3046   artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString();
3047   artist.yearsActive = StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator);
3048   artist.instruments = StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator);
3049   artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1;
3050   artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString();
3051   artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString());
3052   artist.SetDateNew(record->at(offset + artist_dateNew).get_asString());
3053   artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString());
3054 
3055   if (needThumb)
3056   {
3057     artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString());
3058   }
3059 
3060   return artist;
3061 }
3062 
GetSongByFileName(const std::string & strFileNameAndPath,CSong & song,int64_t startOffset)3063 bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath, CSong& song, int64_t startOffset)
3064 {
3065   song.Clear();
3066   CURL url(strFileNameAndPath);
3067 
3068   if (url.IsProtocol("musicdb"))
3069   {
3070     std::string strFile = URIUtils::GetFileName(strFileNameAndPath);
3071     URIUtils::RemoveExtension(strFile);
3072     return GetSong(atoi(strFile.c_str()), song);
3073   }
3074 
3075   if (nullptr == m_pDB)
3076     return false;
3077   if (nullptr == m_pDS)
3078     return false;
3079 
3080   std::string strPath, strFileName;
3081   SplitPath(strFileNameAndPath, strPath, strFileName);
3082   URIUtils::AddSlashAtEnd(strPath);
3083 
3084   std::string strSQL = PrepareSQL("select idSong from songview "
3085                                  "where strFileName='%s' and strPath='%s'",
3086                                  strFileName.c_str(), strPath.c_str());
3087   if (startOffset)
3088     strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset);
3089 
3090   int idSong = GetSingleValueInt(strSQL);
3091   if (idSong > 0)
3092     return GetSong(idSong, song);
3093 
3094   return false;
3095 }
3096 
GetAlbumIdByPath(const std::string & strPath)3097 int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath)
3098 {
3099   try
3100   {
3101     if (nullptr == m_pDB)
3102       return false;
3103     if (nullptr == m_pDS)
3104       return false;
3105 
3106     std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song JOIN path ON song.idPath = path.idPath WHERE path.strPath='%s'", strPath.c_str());
3107     // run query
3108     if (!m_pDS->query(strSQL)) return false;
3109     int iRowsFound = m_pDS->num_rows();
3110 
3111     int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned
3112     if (iRowsFound == 1)
3113       idAlbum = m_pDS->fv(0).get_asInt();
3114 
3115     m_pDS->close();
3116 
3117     return idAlbum;
3118   }
3119   catch (...)
3120   {
3121     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, strPath.c_str());
3122   }
3123 
3124   return -1;
3125 }
3126 
GetSongByArtistAndAlbumAndTitle(const std::string & strArtist,const std::string & strAlbum,const std::string & strTitle)3127 int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist, const std::string& strAlbum, const std::string& strTitle)
3128 {
3129   try
3130   {
3131     std::string strSQL=PrepareSQL("select idSong from songview "
3132                                 "where strArtists like '%s' and strAlbum like '%s' and "
3133                                 "strTitle like '%s'",strArtist.c_str(),strAlbum.c_str(),strTitle.c_str());
3134 
3135     if (!m_pDS->query(strSQL)) return false;
3136     int iRowsFound = m_pDS->num_rows();
3137     if (iRowsFound == 0)
3138     {
3139       m_pDS->close();
3140       return -1;
3141     }
3142     int lResult = m_pDS->fv(0).get_asInt();
3143     m_pDS->close(); // cleanup recordset data
3144     return lResult;
3145   }
3146   catch (...)
3147   {
3148     CLog::Log(LOGERROR, "%s (%s,%s,%s) failed", __FUNCTION__, strArtist.c_str(),strAlbum.c_str(),strTitle.c_str());
3149   }
3150 
3151   return -1;
3152 }
3153 
SearchArtists(const std::string & search,CFileItemList & artists)3154 bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList &artists)
3155 {
3156   try
3157   {
3158     if (nullptr == m_pDB)
3159       return false;
3160     if (nullptr == m_pDS)
3161       return false;
3162 
3163     std::string strVariousArtists = g_localizeStrings.Get(340).c_str();
3164     std::string strSQL;
3165     if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3166       strSQL=PrepareSQL("select * from artist "
3167                                 "where (strArtist like '%s%%' or strArtist like '%% %s%%') and strArtist <> '%s' "
3168                                 , search.c_str(), search.c_str(), strVariousArtists.c_str() );
3169     else
3170       strSQL=PrepareSQL("select * from artist "
3171                                 "where strArtist like '%s%%' and strArtist <> '%s' "
3172                                 , search.c_str(), strVariousArtists.c_str() );
3173 
3174     if (!m_pDS->query(strSQL)) return false;
3175     if (m_pDS->num_rows() == 0)
3176     {
3177       m_pDS->close();
3178       return false;
3179     }
3180 
3181     const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist
3182     while (!m_pDS->eof())
3183     {
3184       std::string path = StringUtils::Format("musicdb://artists/%i/", m_pDS->fv(0).get_asInt());
3185       CFileItemPtr pItem(new CFileItem(path, true));
3186       std::string label = StringUtils::Format("[%s] %s", artistLabel.c_str(), m_pDS->fv(1).get_asString().c_str());
3187       pItem->SetLabel(label);
3188       label = StringUtils::Format("A %s", m_pDS->fv(1).get_asString().c_str()); // sort label is stored in the title tag
3189       pItem->GetMusicInfoTag()->SetTitle(label);
3190       pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist);
3191       artists.Add(pItem);
3192       m_pDS->next();
3193     }
3194 
3195     m_pDS->close(); // cleanup recordset data
3196     return true;
3197   }
3198   catch (...)
3199   {
3200     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3201   }
3202 
3203   return false;
3204 }
3205 
GetTop100(const std::string & strBaseDir,CFileItemList & items)3206 bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items)
3207 {
3208   try
3209   {
3210     if (nullptr == m_pDB)
3211       return false;
3212     if (nullptr == m_pDS)
3213       return false;
3214 
3215     CMusicDbUrl baseUrl;
3216     if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3217       return false;
3218 
3219     std::string strSQL="select * from songview "
3220                       "where iTimesPlayed>0 "
3221                       "order by iTimesPlayed desc "
3222                       "limit 100";
3223 
3224     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3225     if (!m_pDS->query(strSQL)) return false;
3226     int iRowsFound = m_pDS->num_rows();
3227     if (iRowsFound == 0)
3228     {
3229       m_pDS->close();
3230       return true;
3231     }
3232     items.Reserve(iRowsFound);
3233     while (!m_pDS->eof())
3234     {
3235       CFileItemPtr item(new CFileItem);
3236       GetFileItemFromDataset(item.get(), baseUrl);
3237       items.Add(item);
3238       m_pDS->next();
3239     }
3240 
3241     m_pDS->close(); // cleanup recordset data
3242     return true;
3243   }
3244   catch (...)
3245   {
3246     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3247   }
3248 
3249   return false;
3250 }
3251 
GetTop100Albums(VECALBUMS & albums)3252 bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums)
3253 {
3254   try
3255   {
3256     albums.erase(albums.begin(), albums.end());
3257     if (nullptr == m_pDB)
3258       return false;
3259     if (nullptr == m_pDS)
3260       return false;
3261 
3262     // Get data from album and album_artist tables to fully populate albums
3263     std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview "
3264       "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3265       "WHERE albumartistview.idAlbum in "
3266       "(SELECT albumview.idAlbum FROM albumview "
3267       "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
3268       "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
3269       "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
3270 
3271     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3272     if (!m_pDS->query(strSQL)) return false;
3273     int iRowsFound = m_pDS->num_rows();
3274     if (iRowsFound == 0)
3275     {
3276       m_pDS->close();
3277       return true;
3278     }
3279 
3280     int albumArtistOffset = album_enumCount;
3281     int albumId = -1;
3282     while (!m_pDS->eof())
3283     {
3284       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3285 
3286       if (albumId != record->at(album_idAlbum).get_asInt())
3287       { // New album
3288         albumId = record->at(album_idAlbum).get_asInt();
3289         albums.push_back(GetAlbumFromDataset(record));
3290       }
3291       // Get album artists
3292       albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3293 
3294       m_pDS->next();
3295     }
3296 
3297     m_pDS->close(); // cleanup recordset data
3298     return true;
3299   }
3300   catch (...)
3301   {
3302     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3303   }
3304 
3305   return false;
3306 }
3307 
GetTop100AlbumSongs(const std::string & strBaseDir,CFileItemList & items)3308 bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items)
3309 {
3310   try
3311   {
3312     if (nullptr == m_pDB)
3313       return false;
3314     if (nullptr == m_pDS)
3315       return false;
3316 
3317     CMusicDbUrl baseUrl;
3318     if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir))
3319       return false;
3320 
3321     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");
3322     CLog::Log(LOGDEBUG,"GetTop100AlbumSongs() query: %s", strSQL.c_str());
3323     if (!m_pDS->query(strSQL)) return false;
3324 
3325     int iRowsFound = m_pDS->num_rows();
3326     if (iRowsFound == 0)
3327     {
3328       m_pDS->close();
3329       return true;
3330     }
3331 
3332     // get data from returned rows
3333     items.Reserve(iRowsFound);
3334     while (!m_pDS->eof())
3335     {
3336       CFileItemPtr item(new CFileItem);
3337       GetFileItemFromDataset(item.get(), baseUrl);
3338       items.Add(item);
3339       m_pDS->next();
3340     }
3341 
3342     // cleanup
3343     m_pDS->close();
3344     return true;
3345   }
3346   catch (...)
3347   {
3348     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3349   }
3350   return false;
3351 }
3352 
GetRecentlyPlayedAlbums(VECALBUMS & albums)3353 bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums)
3354 {
3355   try
3356   {
3357     albums.erase(albums.begin(), albums.end());
3358     if (nullptr == m_pDB)
3359       return false;
3360     if (nullptr == m_pDS)
3361       return false;
3362 
3363     unsigned int querytime = 0;
3364     unsigned int time = XbmcThreads::SystemClockMillis();
3365 
3366     // Get data from album and album_artist tables to fully populate albums
3367     std::string strSQL = PrepareSQL("SELECT albumview.*, albumartistview.* FROM "
3368       "(SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3369       "AND albumview.strReleaseType = '%s' "
3370       "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3371       "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
3372       "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3373       "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
3374       CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT);
3375 
3376     querytime = XbmcThreads::SystemClockMillis();
3377     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3378     if (!m_pDS->query(strSQL)) return false;
3379     querytime = XbmcThreads::SystemClockMillis() - querytime;
3380     int iRowsFound = m_pDS->num_rows();
3381     if (iRowsFound == 0)
3382     {
3383       m_pDS->close();
3384       return true;
3385     }
3386 
3387     int albumArtistOffset = album_enumCount;
3388     int albumId = -1;
3389     while (!m_pDS->eof())
3390     {
3391       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3392 
3393       if (albumId != record->at(album_idAlbum).get_asInt())
3394       { // New album
3395         albumId = record->at(album_idAlbum).get_asInt();
3396         albums.push_back(GetAlbumFromDataset(record));
3397       }
3398       // Get album artists
3399       albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3400 
3401       m_pDS->next();
3402     }
3403     m_pDS->close(); // cleanup recordset data
3404 
3405     CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms",
3406       __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
3407     return true;
3408   }
3409   catch (...)
3410   {
3411     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3412   }
3413 
3414   return false;
3415 }
3416 
GetRecentlyPlayedAlbumSongs(const std::string & strBaseDir,CFileItemList & items)3417 bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir, CFileItemList& items)
3418 {
3419   try
3420   {
3421     if (nullptr == m_pDB)
3422       return false;
3423     if (nullptr == m_pDS)
3424       return false;
3425 
3426     CMusicDbUrl baseUrl;
3427     if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3428       return false;
3429 
3430     std::string strSQL = PrepareSQL("SELECT songview.*, songartistview.* FROM "
3431       "(SELECT idAlbum, lastPlayed FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3432       "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3433       "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
3434       "JOIN songartistview ON songview.idSong = songartistview.idSong "
3435       "ORDER BY playedalbums.lastplayed DESC,songartistview.idsong, songartistview.idRole, songartistview.iOrder",
3436       CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryRecentlyAddedItems);
3437     CLog::Log(LOGDEBUG,"GetRecentlyPlayedAlbumSongs() query: %s", strSQL.c_str());
3438     if (!m_pDS->query(strSQL)) return false;
3439 
3440     int iRowsFound = m_pDS->num_rows();
3441     if (iRowsFound == 0)
3442     {
3443       m_pDS->close();
3444       return true;
3445     }
3446 
3447     // Needs a separate query to determine number of songs to set items size.
3448     // Get songs from returned rows. Join means there is a row for every song artist
3449     // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
3450     int songArtistOffset = song_enumCount;
3451     int songId = -1;
3452     VECARTISTCREDITS artistCredits;
3453     while (!m_pDS->eof())
3454     {
3455       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3456 
3457       int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3458       if (songId != record->at(song_idSong).get_asInt())
3459       { //New song
3460         if (songId > 0 && !artistCredits.empty())
3461         {
3462           //Store artist credits for previous song
3463           GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3464           artistCredits.clear();
3465         }
3466         songId = record->at(song_idSong).get_asInt();
3467         CFileItemPtr item(new CFileItem);
3468         GetFileItemFromDataset(record, item.get(), baseUrl);
3469         items.Add(item);
3470       }
3471       // Get song artist credits and contributors
3472       if (idSongArtistRole == ROLE_ARTIST)
3473         artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3474       else
3475         items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
3476 
3477       m_pDS->next();
3478     }
3479     if (!artistCredits.empty())
3480     {
3481       //Store artist credits for final song
3482       GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3483       artistCredits.clear();
3484     }
3485 
3486     // cleanup
3487     m_pDS->close();
3488     return true;
3489   }
3490   catch (...)
3491   {
3492     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3493   }
3494   return false;
3495 }
3496 
GetRecentlyAddedAlbums(VECALBUMS & albums,unsigned int limit)3497 bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit)
3498 {
3499   try
3500   {
3501     albums.erase(albums.begin(), albums.end());
3502     if (nullptr == m_pDB)
3503       return false;
3504     if (nullptr == m_pDS)
3505       return false;
3506 
3507     // Get data from album and album_artist tables to fully populate albums
3508     // Determine the recently added albums from dateAdded (usually derived from music file
3509     // timestamps, nothing to do with when albums added to library)
3510     std::string strSQL = PrepareSQL("SELECT albumview.*, albumartistview.* FROM "
3511       "(SELECT idAlbum FROM album WHERE strAlbum != '' "
3512       "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3513       "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
3514       "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3515       "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
3516        limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryRecentlyAddedItems);
3517 
3518     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3519     if (!m_pDS->query(strSQL)) return false;
3520     int iRowsFound = m_pDS->num_rows();
3521     if (iRowsFound == 0)
3522     {
3523       m_pDS->close();
3524       return true;
3525     }
3526 
3527     int albumArtistOffset = album_enumCount;
3528     int albumId = -1;
3529     while (!m_pDS->eof())
3530     {
3531       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3532 
3533       if (albumId != record->at(album_idAlbum).get_asInt())
3534       { // New album
3535         albumId = record->at(album_idAlbum).get_asInt();
3536         albums.push_back(GetAlbumFromDataset(record));
3537       }
3538       // Get album artists
3539       albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3540 
3541       m_pDS->next();
3542     }
3543     m_pDS->close(); // cleanup recordset data
3544     return true;
3545   }
3546   catch (...)
3547   {
3548     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3549   }
3550 
3551   return false;
3552 }
3553 
GetRecentlyAddedAlbumSongs(const std::string & strBaseDir,CFileItemList & items,unsigned int limit)3554 bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir, CFileItemList& items, unsigned int limit)
3555 {
3556   try
3557   {
3558     if (nullptr == m_pDB)
3559       return false;
3560     if (nullptr == m_pDS)
3561       return false;
3562 
3563     CMusicDbUrl baseUrl;
3564     if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3565       return false;
3566 
3567     // Get data from song and song_artist tables to fully populate songs
3568     // Determine the recently added albums from dateAdded (usually derived from music file
3569     // timestamps, nothing to do with when albums added to library)
3570     std::string strSQL;
3571     strSQL = PrepareSQL("SELECT songview.*, songartistview.* FROM "
3572         "(SELECT idAlbum, dateAdded FROM album ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3573         "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
3574         "JOIN songartistview ON songview.idSong = songartistview.idSong "
3575         "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
3576         "songview.idSong, songartistview.idRole, songartistview.iOrder ",
3577         limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryRecentlyAddedItems);
3578     CLog::Log(LOGDEBUG,"GetRecentlyAddedAlbumSongs() query: %s", strSQL.c_str());
3579     if (!m_pDS->query(strSQL)) return false;
3580 
3581     int iRowsFound = m_pDS->num_rows();
3582     if (iRowsFound == 0)
3583     {
3584       m_pDS->close();
3585       return true;
3586     }
3587 
3588     // Needs a separate query to determine number of songs to set items size.
3589     // Get songs from returned rows. Join means there is a row for every song artist
3590     int songArtistOffset = song_enumCount;
3591     int songId = -1;
3592     VECARTISTCREDITS artistCredits;
3593     while (!m_pDS->eof())
3594     {
3595       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3596 
3597       int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3598       if (songId != record->at(song_idSong).get_asInt())
3599       { //New song
3600         if (songId > 0 && !artistCredits.empty())
3601         {
3602           //Store artist credits for previous song
3603           GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3604           artistCredits.clear();
3605         }
3606         songId = record->at(song_idSong).get_asInt();
3607         CFileItemPtr item(new CFileItem);
3608         GetFileItemFromDataset(record, item.get(), baseUrl);
3609         items.Add(item);
3610       }
3611       // Get song artist credits and contributors
3612       if (idSongArtistRole == ROLE_ARTIST)
3613         artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3614       else
3615         items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
3616 
3617       m_pDS->next();
3618     }
3619     if (!artistCredits.empty())
3620     {
3621       //Store artist credits for final song
3622       GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3623       artistCredits.clear();
3624     }
3625 
3626     // cleanup
3627     m_pDS->close();
3628     return true;
3629   }
3630   catch (...)
3631   {
3632     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3633   }
3634   return false;
3635 }
3636 
IncrementPlayCount(const CFileItem & item)3637 void CMusicDatabase::IncrementPlayCount(const CFileItem& item)
3638 {
3639   try
3640   {
3641     if (nullptr == m_pDB)
3642       return;
3643     if (nullptr == m_pDS)
3644       return;
3645 
3646     int idSong = GetSongIDFromPath(item.GetPath());
3647     std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
3648     std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
3649                                  "WHERE idSong=%i",
3650                                  strDateNow.c_str(), idSong);
3651     m_pDS->exec(sql);
3652   }
3653   catch (...)
3654   {
3655     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, item.GetPath().c_str());
3656   }
3657 }
3658 
GetSongsByPath(const std::string & strPath1,MAPSONGS & songmap,bool bAppendToMap)3659 bool CMusicDatabase::GetSongsByPath(const std::string& strPath1, MAPSONGS& songmap, bool bAppendToMap)
3660 {
3661   std::string strPath(strPath1);
3662   try
3663   {
3664     if (!URIUtils::HasSlashAtEnd(strPath))
3665       URIUtils::AddSlashAtEnd(strPath);
3666 
3667     if (!bAppendToMap)
3668       songmap.clear();
3669 
3670     if (nullptr == m_pDB)
3671       return false;
3672     if (nullptr == m_pDS)
3673       return false;
3674 
3675     // Filename is not unique for a path as songs from a cuesheet have same filename.
3676     // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
3677     // in a folder and some edited and rescanned.
3678     // Hence order by filename so these songs can be gathered together.
3679     std::string strSQL = PrepareSQL(
3680         "SELECT * FROM songview WHERE strPath='%s' ORDER BY strFileName", strPath.c_str());
3681     if (!m_pDS->query(strSQL))
3682       return false;
3683     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3684     int iRowsFound = m_pDS->num_rows();
3685     if (iRowsFound == 0)
3686     {
3687       m_pDS->close();
3688       return false;
3689     }
3690 
3691     // Each file is potentially mapped to a list of songs, gather these and save as list
3692     VECSONGS songs;
3693     std::string filename;
3694     while (!m_pDS->eof())
3695     {
3696       CSong song = GetSongFromDataset();
3697       if (!filename.empty() && filename != song.strFileName)
3698       {
3699         // Save songs for previous filename
3700         songmap.insert(std::make_pair(filename, songs));
3701         songs.clear();
3702       }
3703       filename = song.strFileName;
3704       songs.emplace_back(song);
3705       m_pDS->next();
3706     }
3707     m_pDS->close(); // cleanup recordset data
3708     songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
3709     return true;
3710   }
3711   catch (...)
3712   {
3713     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, strPath.c_str());
3714   }
3715 
3716   return false;
3717 }
3718 
EmptyCache()3719 void CMusicDatabase::EmptyCache()
3720 {
3721   m_genreCache.erase(m_genreCache.begin(), m_genreCache.end());
3722   m_pathCache.erase(m_pathCache.begin(), m_pathCache.end());
3723 }
3724 
Search(const std::string & search,CFileItemList & items)3725 bool CMusicDatabase::Search(const std::string& search, CFileItemList &items)
3726 {
3727   unsigned int time = XbmcThreads::SystemClockMillis();
3728   // first grab all the artists that match
3729   SearchArtists(search, items);
3730   CLog::Log(LOGDEBUG, "%s Artist search in %i ms",
3731             __FUNCTION__, XbmcThreads::SystemClockMillis() - time); time = XbmcThreads::SystemClockMillis();
3732 
3733   // then albums that match
3734   SearchAlbums(search, items);
3735   CLog::Log(LOGDEBUG, "%s Album search in %i ms",
3736             __FUNCTION__, XbmcThreads::SystemClockMillis() - time); time = XbmcThreads::SystemClockMillis();
3737 
3738   // and finally songs
3739   SearchSongs(search, items);
3740   CLog::Log(LOGDEBUG, "%s Songs search in %i ms",
3741             __FUNCTION__, XbmcThreads::SystemClockMillis() - time); time = XbmcThreads::SystemClockMillis();
3742   return true;
3743 }
3744 
SearchSongs(const std::string & search,CFileItemList & items)3745 bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList &items)
3746 {
3747   try
3748   {
3749     if (nullptr == m_pDB)
3750       return false;
3751     if (nullptr == m_pDS)
3752       return false;
3753 
3754     CMusicDbUrl baseUrl;
3755     if (!baseUrl.FromString("musicdb://songs/"))
3756       return false;
3757 
3758     std::string strSQL;
3759     if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3760       strSQL=PrepareSQL("select * from songview where strTitle like '%s%%' or strTitle like '%% %s%%' limit 1000", search.c_str(), search.c_str());
3761     else
3762       strSQL=PrepareSQL("select * from songview where strTitle like '%s%%' limit 1000", search.c_str());
3763 
3764     if (!m_pDS->query(strSQL)) return false;
3765     if (m_pDS->num_rows() == 0) return false;
3766 
3767     while (!m_pDS->eof())
3768     {
3769       CFileItemPtr item(new CFileItem);
3770       GetFileItemFromDataset(item.get(), baseUrl);
3771       items.Add(item);
3772       m_pDS->next();
3773     }
3774 
3775     m_pDS->close();
3776     return true;
3777   }
3778   catch (...)
3779   {
3780     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3781   }
3782 
3783   return false;
3784 }
3785 
SearchAlbums(const std::string & search,CFileItemList & albums)3786 bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList &albums)
3787 {
3788   try
3789   {
3790     if (nullptr == m_pDB)
3791       return false;
3792     if (nullptr == m_pDS)
3793       return false;
3794 
3795     std::string strSQL;
3796     if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3797       strSQL=PrepareSQL("select * from albumview where strAlbum like '%s%%' or strAlbum like '%% %s%%'", search.c_str(), search.c_str());
3798     else
3799       strSQL=PrepareSQL("select * from albumview where strAlbum like '%s%%'", search.c_str());
3800 
3801     if (!m_pDS->query(strSQL)) return false;
3802 
3803     const std::string& albumLabel(g_localizeStrings.Get(558)); // Album
3804     while (!m_pDS->eof())
3805     {
3806       CAlbum album = GetAlbumFromDataset(m_pDS.get());
3807       std::string path = StringUtils::Format("musicdb://albums/%ld/", album.idAlbum);
3808       CFileItemPtr pItem(new CFileItem(path, album));
3809       std::string label = StringUtils::Format("[%s] %s", albumLabel.c_str(), album.strAlbum.c_str());
3810       pItem->SetLabel(label);
3811       label = StringUtils::Format("B %s", album.strAlbum.c_str()); // sort label is stored in the title tag
3812       pItem->GetMusicInfoTag()->SetTitle(label);
3813       albums.Add(pItem);
3814       m_pDS->next();
3815     }
3816     m_pDS->close(); // cleanup recordset data
3817     return true;
3818   }
3819   catch (...)
3820   {
3821     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3822   }
3823   return false;
3824 }
3825 
CleanupSongsByIds(const std::string & strSongIds)3826 bool CMusicDatabase::CleanupSongsByIds(const std::string &strSongIds)
3827 {
3828   try
3829   {
3830     if (nullptr == m_pDB)
3831       return false;
3832     if (nullptr == m_pDS)
3833       return false;
3834     // ok, now find all idSong's
3835     std::string strSQL=PrepareSQL("select * from song join path on song.idPath = path.idPath where song.idSong in %s", strSongIds.c_str());
3836     if (!m_pDS->query(strSQL)) return false;
3837     int iRowsFound = m_pDS->num_rows();
3838     if (iRowsFound == 0)
3839     {
3840       m_pDS->close();
3841       return true;
3842     }
3843     std::vector<std::string> songsToDelete;
3844     while (!m_pDS->eof())
3845     { // get the full song path
3846       std::string strFileName = URIUtils::AddFileToFolder(m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString());
3847 
3848       //  Special case for streams inside an ogg file. (oggstream)
3849       //  The last dir in the path is the ogg file that
3850       //  contains the stream, so test if its there
3851       if (URIUtils::HasExtension(strFileName, ".oggstream|.nsfstream"))
3852       {
3853         strFileName = URIUtils::GetDirectory(strFileName);
3854         // we are dropping back to a file, so remove the slash at end
3855         URIUtils::RemoveSlashAtEnd(strFileName);
3856       }
3857 
3858       if (!CFile::Exists(strFileName, false))
3859       { // file no longer exists, so add to deletion list
3860         songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString());
3861       }
3862       m_pDS->next();
3863     }
3864     m_pDS->close();
3865 
3866     if (!songsToDelete.empty())
3867     {
3868       std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")";
3869       // ok, now delete these songs + all references to them from the linked tables
3870       strSQL = "delete from song where idSong in " + strSongsToDelete;
3871       m_pDS->exec(strSQL);
3872       m_pDS->close();
3873     }
3874     return true;
3875   }
3876   catch (...)
3877   {
3878     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
3879   }
3880   return false;
3881 }
3882 
CleanupSongs(CGUIDialogProgress * progressDialog)3883 bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/)
3884 {
3885   try
3886   {
3887     int total;
3888     // Count total number of songs
3889     total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS);
3890     // No songs to clean
3891     if (total == 0)
3892       return true;
3893 
3894     // run through all songs and get all unique path ids
3895     int iLIMIT = 1000;
3896     for (int i=0;;i+=iLIMIT)
3897     {
3898       std::string strSQL=PrepareSQL("select song.idSong from song order by song.idSong limit %i offset %i",iLIMIT,i);
3899       if (!m_pDS->query(strSQL)) return false;
3900       int iRowsFound = m_pDS->num_rows();
3901       // keep going until no rows are left!
3902       if (iRowsFound == 0)
3903       {
3904         m_pDS->close();
3905         return true;
3906       }
3907 
3908       std::vector<std::string> songIds;
3909       while (!m_pDS->eof())
3910       {
3911         songIds.push_back(m_pDS->fv("song.idSong").get_asString());
3912         m_pDS->next();
3913       }
3914       m_pDS->close();
3915       std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")";
3916       CLog::Log(LOGDEBUG,"Checking songs from song ID list: %s",strSongIds.c_str());
3917       if (progressDialog)
3918       {
3919         int percentage = i * 100 / total;
3920         if (percentage > progressDialog->GetPercentage())
3921         {
3922           progressDialog->SetPercentage(percentage);
3923           progressDialog->Progress();
3924         }
3925         if (progressDialog->IsCanceled())
3926         {
3927           m_pDS->close();
3928           return false;
3929         }
3930       }
3931       if (!CleanupSongsByIds(strSongIds)) return false;
3932     }
3933     return true;
3934   }
3935   catch(...)
3936   {
3937     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()");
3938   }
3939   return false;
3940 }
3941 
CleanupAlbums()3942 bool CMusicDatabase::CleanupAlbums()
3943 {
3944   try
3945   {
3946     // This must be run AFTER songs have been cleaned up
3947     // delete albums with no reference to songs
3948     std::string strSQL = "select * from album where album.idAlbum not in (select idAlbum from song)";
3949     if (!m_pDS->query(strSQL)) return false;
3950     int iRowsFound = m_pDS->num_rows();
3951     if (iRowsFound == 0)
3952     {
3953       m_pDS->close();
3954       return true;
3955     }
3956 
3957     std::vector<std::string> albumIds;
3958     while (!m_pDS->eof())
3959     {
3960       albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString());
3961       m_pDS->next();
3962     }
3963     m_pDS->close();
3964 
3965     std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")";
3966     // ok, now we can delete them and the references in the linked tables
3967     strSQL = "delete from album where idAlbum in " + strAlbumIds;
3968     m_pDS->exec(strSQL);
3969     return true;
3970   }
3971   catch (...)
3972   {
3973     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()");
3974   }
3975   return false;
3976 }
3977 
CleanupPaths()3978 bool CMusicDatabase::CleanupPaths()
3979 {
3980   try
3981   {
3982     // needs to be done AFTER the songs and albums have been cleaned up.
3983     // we can happily delete any path that has no reference to a song
3984     // but we must keep all paths that have been scanned that may contain songs in subpaths
3985 
3986     // first create a temporary table of song paths
3987     m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
3988     m_pDS->exec("INSERT INTO songpaths select idPath,strPath from path where idPath in (select idPath from song)\n");
3989 
3990     // grab all paths that aren't immediately connected with a song
3991     std::string sql = "select * from path where idPath not in (select idPath from song)";
3992     if (!m_pDS->query(sql)) return false;
3993     int iRowsFound = m_pDS->num_rows();
3994     if (iRowsFound == 0)
3995     {
3996       m_pDS->close();
3997       return true;
3998     }
3999     // and construct a list to delete
4000     std::vector<std::string> pathIds;
4001     while (!m_pDS->eof())
4002     {
4003       // anything that isn't a parent path of a song path is to be deleted
4004       std::string path = m_pDS->fv("strPath").get_asString();
4005       std::string sql = PrepareSQL("select count(idPath) from songpaths where SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()), path.c_str());
4006       if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0)
4007         pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete
4008       m_pDS2->close();
4009       m_pDS->next();
4010     }
4011     m_pDS->close();
4012 
4013     if (!pathIds.empty())
4014     {
4015       // do the deletion, and drop our temp table
4016       std::string deleteSQL = "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")";
4017       m_pDS->exec(deleteSQL);
4018     }
4019     m_pDS->exec("drop table songpaths");
4020     return true;
4021   }
4022   catch (...)
4023   {
4024     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
4025   }
4026   return false;
4027 }
4028 
InsideScannedPath(const std::string & path)4029 bool CMusicDatabase::InsideScannedPath(const std::string& path)
4030 {
4031   std::string sql = PrepareSQL("select idPath from path where SUBSTR(strPath,1,%i)='%s' LIMIT 1", path.size(), path.c_str());
4032   return !GetSingleValue(sql).empty();
4033 }
4034 
CleanupArtists()4035 bool CMusicDatabase::CleanupArtists()
4036 {
4037   try
4038   {
4039     // (nested queries by Bobbin007)
4040     // must be executed AFTER the song, album and their artist link tables are cleaned.
4041     // Don't delete [Missing] the missing artist tag artist
4042 
4043     // Create temp table to avoid 1442 trigger hell on mysql
4044     m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
4045     m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
4046     m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
4047     m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID));
4048     // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
4049     // To avoid MySQL hanging or timeout create a table of unique ids with primary key
4050     m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
4051     m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
4052     m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
4053     // Tidy up temp tables
4054     m_pDS->exec("DROP TABLE tmp_delartists");
4055     m_pDS->exec("DROP TABLE tmp_keep");
4056 
4057     return true;
4058   }
4059   catch (...)
4060   {
4061     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
4062   }
4063   return false;
4064 }
4065 
CleanupGenres()4066 bool CMusicDatabase::CleanupGenres()
4067 {
4068   try
4069   {
4070     // Cleanup orphaned song genres (ie those that don't belong to a song entry)
4071     // (nested queries by Bobbin007)
4072     // Must be executed AFTER the song, and song_genre have been cleaned.
4073     std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
4074     m_pDS->exec(strSQL);
4075     return true;
4076   }
4077   catch (...)
4078   {
4079     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
4080   }
4081   return false;
4082 }
4083 
CleanupInfoSettings()4084 bool CMusicDatabase::CleanupInfoSettings()
4085 {
4086   try
4087   {
4088     // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
4089     // Must be executed AFTER the album and artist tables have been cleaned.
4090     std::string strSQL = "DELETE FROM infosetting WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
4091       "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
4092     m_pDS->exec(strSQL);
4093     return true;
4094   }
4095   catch (...)
4096   {
4097     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
4098   }
4099   return false;
4100 }
4101 
CleanupRoles()4102 bool CMusicDatabase::CleanupRoles()
4103 {
4104   try
4105   {
4106     // Cleanup orphaned roles (ie those that don't belong to a song entry)
4107     // Must be executed AFTER the song, and song_artist tables have been cleaned.
4108     // Do not remove default role (ROLE_ARTIST)
4109     std::string strSQL = "DELETE FROM role WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
4110     m_pDS->exec(strSQL);
4111     return true;
4112   }
4113   catch (...)
4114   {
4115     CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
4116   }
4117   return false;
4118 }
4119 
DeleteRemovedLinks()4120 bool CMusicDatabase::DeleteRemovedLinks()
4121 {
4122   try
4123   {
4124     std::string strSQL = "DELETE FROM removed_link";
4125     m_pDS->exec(strSQL);
4126     return true;
4127   }
4128   catch (...)
4129   {
4130     CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks");
4131   }
4132   return false;
4133 }
4134 
CleanupOrphanedItems()4135 bool CMusicDatabase::CleanupOrphanedItems()
4136 {
4137   // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
4138   // remove_links not cleared here - done in CheckArtistLinksChanged()
4139   if (nullptr == m_pDB)
4140     return false;
4141   if (nullptr == m_pDS)
4142     return false;
4143   SetLibraryLastUpdated();
4144   if (!CleanupAlbums()) return false;
4145   if (!CleanupArtists()) return false;
4146   if (!CleanupGenres()) return false;
4147   if (!CleanupRoles()) return false;
4148   if (!CleanupInfoSettings()) return false;
4149   return true;
4150 }
4151 
Cleanup(CGUIDialogProgress * progressDialog)4152 int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/)
4153 {
4154   if (nullptr == m_pDB)
4155     return ERROR_DATABASE;
4156   if (nullptr == m_pDS)
4157     return ERROR_DATABASE;
4158 
4159   int ret = ERROR_OK;
4160   unsigned int time = XbmcThreads::SystemClockMillis();
4161   CLog::Log(LOGINFO, "%s: Starting musicdatabase cleanup ..", __FUNCTION__);
4162   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted");
4163 
4164   SetLibraryLastCleaned();
4165 
4166   // Drop triggers  song_artist and album_artist to avoid creation of entries in removed_link
4167   m_pDS->exec("DROP TRIGGER tgrDeleteSongArtist");
4168   m_pDS->exec("DROP TRIGGER tgrDeleteAlbumArtist");
4169 
4170   // first cleanup any songs with invalid paths
4171   if (progressDialog)
4172   {
4173     progressDialog->SetLine(1, CVariant{318});
4174     progressDialog->SetLine(2, CVariant{330});
4175     progressDialog->SetPercentage(0);
4176     progressDialog->Progress();
4177   }
4178   if (!CleanupSongs(progressDialog))
4179   {
4180     ret = ERROR_REORG_SONGS;
4181     goto error;
4182   }
4183   // then the albums that are not linked to a song or to album, or whose path is removed
4184   if (progressDialog)
4185   {
4186     progressDialog->SetLine(1, CVariant{326});
4187     progressDialog->SetPercentage(20);
4188     progressDialog->Progress();
4189     if (progressDialog->IsCanceled())
4190     {
4191       ret = ERROR_CANCEL;
4192       goto error;
4193     }
4194   }
4195   if (!CleanupAlbums())
4196   {
4197     ret = ERROR_REORG_ALBUM;
4198     goto error;
4199   }
4200   // now the paths
4201   if (progressDialog)
4202   {
4203     progressDialog->SetLine(1, CVariant{324});
4204     progressDialog->SetPercentage(40);
4205     progressDialog->Progress();
4206     if (progressDialog->IsCanceled())
4207     {
4208       ret = ERROR_CANCEL;
4209       goto error;
4210     }
4211   }
4212   if (!CleanupPaths())
4213   {
4214     ret = ERROR_REORG_PATH;
4215     goto error;
4216   }
4217   // and finally artists + genres
4218   if (progressDialog)
4219   {
4220     progressDialog->SetLine(1, CVariant{320});
4221     progressDialog->SetPercentage(60);
4222     progressDialog->Progress();
4223     if (progressDialog->IsCanceled())
4224     {
4225       ret = ERROR_CANCEL;
4226       goto error;
4227     }
4228   }
4229   if (!CleanupArtists())
4230   {
4231     ret = ERROR_REORG_ARTIST;
4232     goto error;
4233   }
4234   //Genres, roles and info settings progess in one step
4235   if (progressDialog)
4236   {
4237     progressDialog->SetLine(1, CVariant{322});
4238     progressDialog->SetPercentage(80);
4239     progressDialog->Progress();
4240     if (progressDialog->IsCanceled())
4241     {
4242       ret = ERROR_CANCEL;
4243       goto error;
4244     }
4245   }
4246   if (!CleanupGenres())
4247   {
4248     ret = ERROR_REORG_OTHER;
4249     goto error;
4250   }
4251   if (!CleanupRoles())
4252   {
4253     ret = ERROR_REORG_OTHER;
4254     goto error;
4255   }
4256   if (!CleanupInfoSettings())
4257   {
4258     ret = ERROR_REORG_OTHER;
4259     goto error;
4260   }
4261   if (!DeleteRemovedLinks())
4262   {
4263     ret = ERROR_REORG_OTHER;
4264     goto error;
4265   }
4266 
4267   // commit transaction
4268   if (progressDialog)
4269   {
4270     progressDialog->SetLine(1, CVariant{328});
4271     progressDialog->SetPercentage(90);
4272     progressDialog->Progress();
4273     if (progressDialog->IsCanceled())
4274     {
4275       ret = ERROR_CANCEL;
4276       goto error;
4277     }
4278   }
4279   if (!CommitTransaction())
4280   {
4281     ret = ERROR_WRITING_CHANGES;
4282     goto error;
4283   }
4284 
4285   // Recreate DELETE triggers on song_artist and album_artist
4286   CreateRemovedLinkTriggers();
4287 
4288   // and compress the database
4289   if (progressDialog)
4290   {
4291     progressDialog->SetLine(1, CVariant{331});
4292     progressDialog->SetPercentage(100);
4293     progressDialog->Close();
4294   }
4295   time = XbmcThreads::SystemClockMillis() - time;
4296   CLog::Log(LOGINFO, "%s: Cleaning musicdatabase done. Operation took %s", __FUNCTION__,
4297             StringUtils::SecondsToTimeString(time / 1000).c_str());
4298   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4299 
4300   if (!Compress(false))
4301   {
4302     return ERROR_COMPRESSING;
4303   }
4304   return ERROR_OK;
4305 
4306 error:
4307   RollbackTransaction();
4308   // Recreate DELETE triggers on song_artist and album_artist
4309   CreateRemovedLinkTriggers();
4310   CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4311   return ret;
4312 }
4313 
TrimImageURLs(std::string & strImage,const size_t space)4314 bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space)
4315 {
4316   if (strImage.length() > space)
4317   {
4318     strImage = strImage.substr(0, space);
4319     // Tidy to last </thumb> tag
4320     size_t iPos = strImage.rfind("</thumb>");
4321     if (iPos == std::string::npos)
4322       return false;
4323     strImage = strImage.substr(0, iPos + 8);
4324   }
4325   return true;
4326 }
4327 
LookupCDDBInfo(bool bRequery)4328 bool CMusicDatabase::LookupCDDBInfo(bool bRequery/*=false*/)
4329 {
4330 #ifdef HAS_DVD_DRIVE
4331   if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOCDS_USECDDB))
4332     return false;
4333 
4334   // check network connectivity
4335   if (!CServiceBroker::GetNetwork().IsAvailable())
4336     return false;
4337 
4338   // Get information for the inserted disc
4339   CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
4340   if (pCdInfo == NULL)
4341     return false;
4342 
4343   // If the disc has no tracks, we are finished here.
4344   int nTracks = pCdInfo->GetTrackCount();
4345   if (nTracks <= 0)
4346     return false;
4347 
4348   //  Delete old info if any
4349   if (bRequery)
4350   {
4351     std::string strFile = StringUtils::Format("%x.cddb", pCdInfo->GetCddbDiscId());
4352     CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4353   }
4354 
4355   // Prepare cddb
4356   Xcddb cddb;
4357   cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4358 
4359   // Do we have to look for cddb information
4360   if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo))
4361   {
4362     CGUIDialogProgress* pDialogProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
4363     CGUIDialogSelect *pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
4364 
4365     if (!pDialogProgress) return false;
4366     if (!pDlgSelect) return false;
4367 
4368     // Show progress dialog if we have to connect to freedb.org
4369     pDialogProgress->SetHeading(CVariant{255}); //CDDB
4370     pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info
4371     pDialogProgress->SetLine(1, CVariant{256});
4372     pDialogProgress->SetLine(2, CVariant{""});
4373     pDialogProgress->ShowProgressBar(false);
4374     pDialogProgress->Open();
4375 
4376     // get cddb information
4377     if (!cddb.queryCDinfo(pCdInfo))
4378     {
4379       pDialogProgress->Close();
4380       int lasterror = cddb.getLastError();
4381 
4382       // Have we found more then on match in cddb for this disc,...
4383       if (lasterror == E_WAIT_FOR_INPUT)
4384       {
4385         // ...yes, show the matches found in a select dialog
4386         // and let the user choose an entry.
4387         pDlgSelect->Reset();
4388         pDlgSelect->SetHeading(CVariant{255});
4389         int i = 1;
4390         while (true)
4391         {
4392           std::string strTitle = cddb.getInexactTitle(i);
4393           if (strTitle == "") break;
4394 
4395           const std::string& strArtist = cddb.getInexactArtist(i);
4396           if (!strArtist.empty())
4397             strTitle += " - " + strArtist;
4398 
4399           pDlgSelect->Add(strTitle);
4400           i++;
4401         }
4402         pDlgSelect->Open();
4403 
4404         // Has the user selected a match...
4405         int iSelectedCD = pDlgSelect->GetSelectedItem();
4406         if (iSelectedCD >= 0)
4407         {
4408           // ...query cddb for the inexact match
4409           if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD))
4410             pCdInfo->SetNoCDDBInfo();
4411         }
4412         else
4413           pCdInfo->SetNoCDDBInfo();
4414       }
4415       else if (lasterror == E_NO_MATCH_FOUND)
4416       {
4417         pCdInfo->SetNoCDDBInfo();
4418       }
4419       else
4420       {
4421         pCdInfo->SetNoCDDBInfo();
4422         // ..no, an error occurred, display it to the user
4423         std::string strErrorText = StringUtils::Format("[%d] %s", cddb.getLastError(), cddb.getLastErrorText());
4424         HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)}, CVariant{0});
4425       }
4426     } // if ( !cddb.queryCDinfo( pCdInfo ) )
4427     else
4428       pDialogProgress->Close();
4429   }
4430 
4431   // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
4432 
4433   return pCdInfo->HasCDDBInfo();
4434 #else
4435   return false;
4436 #endif
4437 }
4438 
DeleteCDDBInfo()4439 void CMusicDatabase::DeleteCDDBInfo()
4440 {
4441 #ifdef HAS_DVD_DRIVE
4442   CFileItemList items;
4443   if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb", DIR_FLAG_NO_FILE_DIRS))
4444   {
4445     HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426});
4446     return ;
4447   }
4448   // Show a selectdialog that the user can select the album to delete
4449   CGUIDialogSelect *pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
4450   if (pDlg)
4451   {
4452     pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
4453     pDlg->Reset();
4454 
4455     std::map<uint32_t, std::string> mapCDDBIds;
4456     for (int i = 0; i < items.Size(); ++i)
4457     {
4458       if (items[i]->m_bIsFolder)
4459         continue;
4460 
4461       std::string strFile = URIUtils::GetFileName(items[i]->GetPath());
4462       strFile.erase(strFile.size() - 5, 5);
4463       uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16);
4464       Xcddb cddb;
4465       cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4466 
4467       if (!cddb.queryCache(lDiscId))
4468         continue;
4469 
4470       std::string strDiskTitle, strDiskArtist;
4471       cddb.getDiskTitle(strDiskTitle);
4472       cddb.getDiskArtist(strDiskArtist);
4473 
4474       std::string str;
4475       if (strDiskArtist.empty())
4476         str = strDiskTitle;
4477       else
4478         str = strDiskTitle + " - " + strDiskArtist;
4479 
4480       pDlg->Add(str);
4481       mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str));
4482     }
4483 
4484     pDlg->Sort();
4485     pDlg->Open();
4486 
4487     // and wait till user selects one
4488     int iSelectedAlbum = pDlg->GetSelectedItem();
4489     if (iSelectedAlbum < 0)
4490     {
4491       mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4492       return ;
4493     }
4494 
4495     std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel();
4496     for (const auto &i : mapCDDBIds)
4497     {
4498       if (i.second == strSelectedAlbum)
4499       {
4500         std::string strFile = StringUtils::Format("%x.cddb", (unsigned int) i.first);
4501         CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4502         break;
4503       }
4504     }
4505     mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4506   }
4507 #endif
4508 }
4509 
Clean()4510 void CMusicDatabase::Clean()
4511 {
4512   // If we are scanning for music info in the background,
4513   // other writing access to the database is prohibited.
4514   if (g_application.IsMusicScanning())
4515   {
4516     HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057});
4517     return;
4518   }
4519 
4520   if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::YES)
4521   {
4522     CMusicDatabase musicdatabase;
4523     if (musicdatabase.Open())
4524     {
4525       int iReturnString = musicdatabase.Cleanup();
4526       musicdatabase.Close();
4527 
4528       if (iReturnString != ERROR_OK)
4529       {
4530         HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString});
4531       }
4532     }
4533   }
4534 }
4535 
GetGenresNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)4536 bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir, CFileItemList& items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
4537 {
4538   try
4539   {
4540     if (nullptr == m_pDB)
4541       return false;
4542     if (nullptr == m_pDS)
4543       return false;
4544 
4545     // get primary genres for songs - could be simplified to just SELECT * FROM genre?
4546     std::string strSQL = "SELECT %s FROM genre ";
4547 
4548     Filter extFilter = filter;
4549     CMusicDbUrl musicUrl;
4550     SortDescription sorting;
4551     if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4552       return false;
4553 
4554     // if there are extra WHERE conditions we might need access
4555     // to songview or albumview for these conditions
4556     if (!extFilter.where.empty())
4557     {
4558       if (extFilter.where.find("artistview") != std::string::npos)
4559       {
4560         extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4561         extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4562         extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
4563         extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
4564       }
4565       else if (extFilter.where.find("songview") != std::string::npos)
4566       {
4567         extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4568         extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4569       }
4570       else if (extFilter.where.find("albumview") != std::string::npos)
4571       {
4572         extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4573         extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
4574         extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
4575       }
4576       extFilter.AppendGroup("genre.idGenre");
4577     }
4578     extFilter.AppendWhere("genre.strGenre != ''");
4579 
4580     if (countOnly)
4581     {
4582       extFilter.fields = "COUNT(DISTINCT genre.idGenre)";
4583       extFilter.group.clear();
4584       extFilter.order.clear();
4585     }
4586 
4587     std::string strSQLExtra;
4588     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
4589       return false;
4590 
4591     strSQL = PrepareSQL(strSQL.c_str(), !extFilter.fields.empty() && extFilter.fields.compare("*") != 0 ? extFilter.fields.c_str() : "genre.*") + strSQLExtra;
4592 
4593     // run query
4594     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4595 
4596     if (!m_pDS->query(strSQL))
4597       return false;
4598     int iRowsFound = m_pDS->num_rows();
4599     if (iRowsFound == 0)
4600     {
4601       m_pDS->close();
4602       return true;
4603     }
4604 
4605     if (countOnly)
4606     {
4607       CFileItemPtr pItem(new CFileItem());
4608       pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4609       items.Add(pItem);
4610 
4611       m_pDS->close();
4612       return true;
4613     }
4614 
4615     // get data from returned rows
4616     while (!m_pDS->eof())
4617     {
4618       CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString()));
4619       pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString());
4620       pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre");
4621 
4622       CMusicDbUrl itemUrl = musicUrl;
4623       std::string strDir = StringUtils::Format("%i/", m_pDS->fv("genre.idGenre").get_asInt());
4624       itemUrl.AppendPath(strDir);
4625       pItem->SetPath(itemUrl.ToString());
4626 
4627       pItem->m_bIsFolder = true;
4628       items.Add(pItem);
4629 
4630       m_pDS->next();
4631     }
4632 
4633     // cleanup
4634     m_pDS->close();
4635 
4636     return true;
4637   }
4638   catch (...)
4639   {
4640     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4641   }
4642   return false;
4643 }
4644 
GetSourcesNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)4645 bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir, CFileItemList& items, const Filter &filter /*= Filter()*/, bool countOnly /*= false*/)
4646 {
4647   try
4648   {
4649     if (nullptr == m_pDB)
4650       return false;
4651     if (nullptr == m_pDS)
4652       return false;
4653 
4654     // Get sources for selection list when add/edit filter or smartplaylist rule
4655     std::string strSQL = "SELECT %s FROM source ";
4656 
4657     Filter extFilter = filter;
4658     CMusicDbUrl musicUrl;
4659     SortDescription sorting;
4660     if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4661       return false;
4662 
4663     // if there are extra WHERE conditions we might need access
4664     // to songview or albumview for these conditions
4665     if (!extFilter.where.empty())
4666     {
4667       if (extFilter.where.find("artistview") != std::string::npos)
4668       {
4669         extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4670         extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
4671         extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
4672       }
4673       else if (extFilter.where.find("songview") != std::string::npos)
4674       {
4675         extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4676         extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
4677       }
4678       else if (extFilter.where.find("albumview") != std::string::npos)
4679       {
4680         extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4681         extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
4682       }
4683       extFilter.AppendGroup("source.idSource");
4684     }
4685     else
4686     { // Get only sources that have been scanned into music library
4687       extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4688       extFilter.AppendGroup("source.idSource");
4689     }
4690 
4691     if (countOnly)
4692     {
4693       extFilter.fields = "COUNT(DISTINCT source.idSource)";
4694       extFilter.group.clear();
4695       extFilter.order.clear();
4696     }
4697 
4698     std::string strSQLExtra;
4699     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
4700       return false;
4701 
4702     strSQL = PrepareSQL(strSQL.c_str(), !extFilter.fields.empty() && extFilter.fields.compare("*") != 0 ? extFilter.fields.c_str() : "source.*") + strSQLExtra;
4703 
4704     // run query
4705     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4706 
4707     if (!m_pDS->query(strSQL))
4708       return false;
4709     int iRowsFound = m_pDS->num_rows();
4710     if (iRowsFound == 0)
4711     {
4712       m_pDS->close();
4713       return true;
4714     }
4715 
4716     if (countOnly)
4717     {
4718       CFileItemPtr pItem(new CFileItem());
4719       pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4720       items.Add(pItem);
4721 
4722       m_pDS->close();
4723       return true;
4724     }
4725 
4726     // get data from returned rows
4727     while (!m_pDS->eof())
4728     {
4729       CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
4730       pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString());
4731       pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source");
4732 
4733       CMusicDbUrl itemUrl = musicUrl;
4734       std::string strDir = StringUtils::Format("%i/", m_pDS->fv("source.idSource").get_asInt());
4735       itemUrl.AppendPath(strDir);
4736       itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt());
4737       pItem->SetPath(itemUrl.ToString());
4738 
4739       pItem->m_bIsFolder = true;
4740       items.Add(pItem);
4741 
4742       m_pDS->next();
4743     }
4744 
4745     // cleanup
4746     m_pDS->close();
4747 
4748     return true;
4749   }
4750   catch (...)
4751   {
4752     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4753   }
4754   return false;
4755 }
4756 
GetYearsNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter)4757 bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir, CFileItemList& items, const Filter& filter /* = Filter() */)
4758 {
4759   try
4760   {
4761     if (nullptr == m_pDB)
4762       return false;
4763     if (nullptr == m_pDS)
4764       return false;
4765 
4766     Filter extFilter = filter;
4767     CMusicDbUrl musicUrl;
4768     SortDescription sorting;
4769     std::string strSQL;
4770     if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4771       return false;
4772 
4773     bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
4774         CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
4775 
4776     useOriginalYears =
4777         useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/");
4778 
4779     if (!useOriginalYears)
4780     { // Get years from year part of release date
4781       strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
4782       extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
4783     }
4784     else
4785     { // Get years from year part of original date
4786       strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
4787       extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
4788     }
4789     if (!BuildSQL(strSQL, extFilter, strSQL))
4790       return false;
4791 
4792     // run query
4793     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4794     if (!m_pDS->query(strSQL))
4795       return false;
4796     int iRowsFound = m_pDS->num_rows();
4797     if (iRowsFound == 0)
4798     {
4799       m_pDS->close();
4800       return true;
4801     }
4802 
4803     // get data from returned rows
4804     while (!m_pDS->eof())
4805     {
4806       CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
4807       pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt());
4808       if (useOriginalYears)
4809         pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
4810       else
4811         pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year");
4812 
4813       CMusicDbUrl itemUrl = musicUrl;
4814       std::string strDir = StringUtils::Format("%i/", m_pDS->fv(0).get_asInt());
4815       itemUrl.AppendPath(strDir);
4816       if (useOriginalYears)
4817         itemUrl.AddOption("useoriginalyear", true);
4818       pItem->SetPath(itemUrl.ToString());
4819 
4820       pItem->m_bIsFolder = true;
4821       items.Add(pItem);
4822 
4823       m_pDS->next();
4824     }
4825 
4826     // cleanup
4827     m_pDS->close();
4828 
4829     return true;
4830   }
4831   catch (...)
4832   {
4833     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4834   }
4835   return false;
4836 }
4837 
GetRolesNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter)4838 bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir, CFileItemList& items, const Filter &filter /* = Filter() */)
4839 {
4840   try
4841   {
4842     if (nullptr == m_pDB)
4843       return false;
4844     if (nullptr == m_pDS)
4845       return false;
4846 
4847     Filter extFilter = filter;
4848     CMusicDbUrl musicUrl;
4849     SortDescription sorting;
4850     if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4851       return false;
4852 
4853     // get roles with artists having that role
4854     std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role "
4855                          "JOIN song_artist ON song_artist.idRole = role.idRole ";
4856 
4857     if (!BuildSQL(strSQL, extFilter, strSQL))
4858       return false;
4859 
4860     // run query
4861     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4862     if (!m_pDS->query(strSQL))
4863       return false;
4864     int iRowsFound = m_pDS->num_rows();
4865     if (iRowsFound == 0)
4866     {
4867       m_pDS->close();
4868       return true;
4869     }
4870 
4871     // get data from returned rows
4872     while (!m_pDS->eof())
4873     {
4874       std::string labelValue = m_pDS->fv("role.strRole").get_asString();
4875       CFileItemPtr pItem(new CFileItem(labelValue));
4876       pItem->GetMusicInfoTag()->SetTitle(labelValue);
4877       pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role");
4878       CMusicDbUrl itemUrl = musicUrl;
4879       std::string strDir = StringUtils::Format("%i/", m_pDS->fv("role.idRole").get_asInt());
4880       itemUrl.AppendPath(strDir);
4881       itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt());
4882       pItem->SetPath(itemUrl.ToString());
4883 
4884       pItem->m_bIsFolder = true;
4885       items.Add(pItem);
4886 
4887       m_pDS->next();
4888     }
4889 
4890     // cleanup
4891     m_pDS->close();
4892 
4893     return true;
4894   }
4895   catch (...)
4896   {
4897     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4898   }
4899   return false;
4900 }
4901 
GetAlbumsByYear(const std::string & strBaseDir,CFileItemList & items,int year)4902 bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year)
4903 {
4904   CMusicDbUrl musicUrl;
4905   if (!musicUrl.FromString(strBaseDir))
4906     return false;
4907 
4908   musicUrl.AddOption("year", year);
4909   musicUrl.AddOption("show_singles", true); // allow singles to be listed
4910 
4911   Filter filter;
4912   return GetAlbumsByWhere(musicUrl.ToString(), filter, items);
4913 }
4914 
GetCommonNav(const std::string & strBaseDir,const std::string & table,const std::string & labelField,CFileItemList & items,const Filter & filter,bool countOnly)4915 bool CMusicDatabase::GetCommonNav(const std::string &strBaseDir, const std::string &table, const std::string &labelField, CFileItemList &items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
4916 {
4917   if (nullptr == m_pDB)
4918     return false;
4919   if (nullptr == m_pDS)
4920     return false;
4921 
4922   if (table.empty() || labelField.empty())
4923     return false;
4924 
4925   try
4926   {
4927     Filter extFilter = filter;
4928     std::string strSQL = "SELECT %s FROM " + table + " ";
4929     extFilter.AppendGroup(labelField);
4930     extFilter.AppendWhere(labelField + " != ''");
4931 
4932     if (countOnly)
4933     {
4934       extFilter.fields = "COUNT(DISTINCT " + labelField + ")";
4935       extFilter.group.clear();
4936       extFilter.order.clear();
4937     }
4938 
4939     // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
4940     // e.g. LIKE '%symphony%' would be taken as a %s format argument
4941     strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str());
4942 
4943     CMusicDbUrl musicUrl;
4944     if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl))
4945       return false;
4946 
4947     // run query
4948     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4949     if (!m_pDS->query(strSQL))
4950       return false;
4951 
4952     int iRowsFound = m_pDS->num_rows();
4953     if (iRowsFound <= 0)
4954     {
4955       m_pDS->close();
4956       return false;
4957     }
4958 
4959     if (countOnly)
4960     {
4961       CFileItemPtr pItem(new CFileItem());
4962       pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4963       items.Add(pItem);
4964 
4965       m_pDS->close();
4966       return true;
4967     }
4968 
4969     // get data from returned rows
4970     while (!m_pDS->eof())
4971     {
4972       std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString();
4973       CFileItemPtr pItem(new CFileItem(labelValue));
4974 
4975       CMusicDbUrl itemUrl = musicUrl;
4976       std::string strDir = StringUtils::Format("%s/", labelValue.c_str());
4977       itemUrl.AppendPath(strDir);
4978       pItem->SetPath(itemUrl.ToString());
4979 
4980       pItem->m_bIsFolder = true;
4981       items.Add(pItem);
4982 
4983       m_pDS->next();
4984     }
4985 
4986     // cleanup
4987     m_pDS->close();
4988 
4989     return true;
4990   }
4991   catch (...)
4992   {
4993     m_pDS->close();
4994     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4995   }
4996 
4997   return false;
4998 }
4999 
GetAlbumTypesNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)5000 bool CMusicDatabase::GetAlbumTypesNav(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
5001 {
5002   return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly);
5003 }
5004 
GetMusicLabelsNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)5005 bool CMusicDatabase::GetMusicLabelsNav(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
5006 {
5007   return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly);
5008 }
5009 
GetArtistsNav(const std::string & strBaseDir,CFileItemList & items,bool albumArtistsOnly,int idGenre,int idAlbum,int idSong,const Filter & filter,const SortDescription & sortDescription,bool countOnly)5010 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 */)
5011 {
5012   if (nullptr == m_pDB)
5013     return false;
5014   if (nullptr == m_pDS)
5015     return false;
5016   try
5017   {
5018     CMusicDbUrl musicUrl;
5019     if (!musicUrl.FromString(strBaseDir))
5020       return false;
5021 
5022     if (idGenre > 0)
5023       musicUrl.AddOption("genreid", idGenre);
5024     else if (idAlbum > 0)
5025       musicUrl.AddOption("albumid", idAlbum);
5026     else if (idSong > 0)
5027       musicUrl.AddOption("songid", idSong);
5028 
5029     // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
5030     // when local option already present in music URL thus allowing it to be an option in custom nodes
5031     if (!musicUrl.HasOption("albumartistsonly"))
5032       musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
5033 
5034     bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5035 
5036     return result;
5037   }
5038   catch (...)
5039   {
5040     m_pDS->close();
5041     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
5042   }
5043   return false;
5044 }
5045 
GetArtistsByWhere(const std::string & strBaseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5046 bool CMusicDatabase::GetArtistsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription /* = SortDescription() */, bool countOnly /* = false */)
5047 {
5048   if (nullptr == m_pDB)
5049     return false;
5050   if (nullptr == m_pDS)
5051     return false;
5052 
5053   try
5054   {
5055     unsigned int querytime = 0;
5056     unsigned int time = XbmcThreads::SystemClockMillis();
5057     int total = -1;
5058 
5059     Filter extFilter = filter;
5060     CMusicDbUrl musicUrl;
5061     SortDescription sorting = sortDescription;
5062     if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5063       return false;
5064 
5065     bool extended = false;
5066     bool limitedInSQL =
5067       extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5068 
5069     // if there are extra WHERE conditions (from media filter dialog) we might
5070     // need access to songview or albumview for these conditions
5071     if (!extFilter.where.empty())
5072     {
5073       if (extFilter.where.find("songview") != std::string::npos)
5074       {
5075         extended = true;
5076         extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist JOIN songview ON songview.idSong = song_artist.idSong");
5077       }
5078       else if (extFilter.where.find("albumview") != std::string::npos)
5079       {
5080         extended = true;
5081         extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
5082       }
5083       if (extended)
5084         extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
5085     }
5086 
5087     std::string strSQLExtra;
5088     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5089       return false;
5090 
5091     // Count number of artsits that satisfy selection criteria (no limit built)
5092     // Count done in full query fetch when unlimited
5093     if (countOnly || limitedInSQL)
5094     {
5095       if (extended)
5096       {
5097         // Count distinct without group by
5098         Filter countFilter = extFilter;
5099         countFilter.group.clear();
5100         std::string strSQLWhere;
5101         if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5102           return false;
5103         total = GetSingleValueInt(
5104             "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS);
5105       }
5106       else
5107         total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS);
5108     }
5109     if (countOnly)
5110     {
5111       CFileItemPtr pItem(new CFileItem());
5112       pItem->SetProperty("total", total);
5113       items.Add(pItem);
5114 
5115       m_pDS->close();
5116       return true;
5117     }
5118 
5119     // Apply any limiting directly in SQL and so sort as well
5120     if (limitedInSQL)
5121     {
5122       extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5123     }
5124 
5125     // Apply sort in SQL
5126     const std::shared_ptr<CSettings> settings =
5127         CServiceBroker::GetSettingsComponent()->GetSettings();
5128     if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5129       sorting.sortAttributes =
5130           static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5131     // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5132     GetOrderFilter(MediaTypeArtist, sorting, extFilter);
5133 
5134     strSQLExtra.clear();
5135     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5136       return false;
5137 
5138     std::string strSQL;
5139     std::string strFields = "artistview.*";
5140     if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5141       strFields = "artistview.*, " + extFilter.fields;
5142     strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra;
5143 
5144     // run query
5145     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
5146     querytime = XbmcThreads::SystemClockMillis();
5147     if (!m_pDS->query(strSQL))
5148       return false;
5149     int iRowsFound = m_pDS->num_rows();
5150     if (iRowsFound == 0)
5151     {
5152       m_pDS->close();
5153       return true;
5154     }
5155     querytime = XbmcThreads::SystemClockMillis() - querytime;
5156 
5157     // Store the total number of artists as a property
5158     if (total < iRowsFound)
5159       total = iRowsFound;
5160     items.SetProperty("total", total);
5161 
5162     DatabaseResults results;
5163     results.reserve(iRowsFound);
5164     // Populate results field vector from dataset
5165     FieldList fields;
5166     if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results))
5167       return false;
5168     // Store item list sort order
5169     items.SetSortMethod(sortDescription.sortBy);
5170     items.SetSortOrder(sortDescription.sortOrder);
5171 
5172     // Get Artists from returned rows
5173     items.Reserve(results.size());
5174     const dbiplus::query_data &data = m_pDS->get_result_set().records;
5175     for (const auto &i : results)
5176     {
5177       unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5178       const dbiplus::sql_record* const record = data.at(targetRow);
5179 
5180       try
5181       {
5182         CArtist artist = GetArtistFromDataset(record, false);
5183         CFileItemPtr pItem(new CFileItem(artist));
5184 
5185         CMusicDbUrl itemUrl = musicUrl;
5186         std::string path = StringUtils::Format("%ld/", artist.idArtist);
5187         itemUrl.AppendPath(path);
5188         pItem->SetPath(itemUrl.ToString());
5189 
5190         pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist);
5191         // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5192         pItem->SetProperty("icon_never_overlay", true);
5193         pItem->SetArt("icon", "DefaultArtist.png");
5194 
5195         SetPropertiesFromArtist(*pItem, artist);
5196         items.Add(pItem);
5197       }
5198       catch (...)
5199       {
5200         m_pDS->close();
5201         CLog::Log(LOGERROR, "%s - out of memory getting listing (got %i)", __FUNCTION__, items.Size());
5202       }
5203     }
5204     // cleanup
5205     m_pDS->close();
5206 
5207     CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1}ms query took {2}ms",
5208       __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
5209     return true;
5210   }
5211   catch (...)
5212   {
5213     m_pDS->close();
5214     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
5215   }
5216   return false;
5217 }
5218 
GetAlbumFromSong(int idSong,CAlbum & album)5219 bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum &album)
5220 {
5221   try
5222   {
5223     if (nullptr == m_pDB)
5224       return false;
5225     if (nullptr == m_pDS)
5226       return false;
5227 
5228     std::string strSQL = PrepareSQL("select albumview.* from song join albumview on song.idAlbum = albumview.idAlbum where song.idSong='%i'", idSong);
5229     if (!m_pDS->query(strSQL)) return false;
5230     int iRowsFound = m_pDS->num_rows();
5231     if (iRowsFound != 1)
5232     {
5233       m_pDS->close();
5234       return false;
5235     }
5236 
5237     album = GetAlbumFromDataset(m_pDS.get());
5238 
5239     m_pDS->close();
5240     return true;
5241 
5242   }
5243   catch (...)
5244   {
5245     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
5246   }
5247   return false;
5248 }
5249 
GetAlbumsNav(const std::string & strBaseDir,CFileItemList & items,int idGenre,int idArtist,const Filter & filter,const SortDescription & sortDescription,bool countOnly)5250 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 */)
5251 {
5252   CMusicDbUrl musicUrl;
5253   if (!musicUrl.FromString(strBaseDir))
5254     return false;
5255 
5256   // where clause
5257   if (idGenre > 0)
5258     musicUrl.AddOption("genreid", idGenre);
5259 
5260   if (idArtist > 0)
5261     musicUrl.AddOption("artistid", idArtist);
5262 
5263   return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5264 }
5265 
GetAlbumsByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5266 bool CMusicDatabase::GetAlbumsByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, const SortDescription &sortDescription /* = SortDescription() */, bool countOnly /* = false */)
5267 {
5268   if (m_pDB == nullptr || m_pDS == nullptr)
5269     return false;
5270 
5271   try
5272   {
5273     unsigned int querytime = 0;
5274     unsigned int time = XbmcThreads::SystemClockMillis();
5275     int total = -1;
5276 
5277     Filter extFilter = filter;
5278     CMusicDbUrl musicUrl;
5279     SortDescription sorting = sortDescription;
5280     if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5281       return false;
5282 
5283     bool extended = false;
5284     bool limitedInSQL =
5285       extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5286 
5287     // If there are extra WHERE conditions (from media filter dialog) we might
5288     // need access to songview for these conditions
5289     if (extFilter.where.find("songview") != std::string::npos)
5290     {
5291       extended = true;
5292       extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
5293       extFilter.AppendGroup("albumview.idAlbum");
5294     }
5295 
5296     std::string strSQLExtra;
5297     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5298       return false;
5299 
5300     // Count number of albums that satisfy selection criteria (no limit built)
5301     // Count done in full query fetch when unlimited
5302     if (countOnly || limitedInSQL)
5303     {
5304       if (extended)
5305       {
5306         // Count distinct without group by
5307         Filter countFilter = extFilter;
5308         countFilter.group.clear();
5309         std::string strSQLWhere;
5310         if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5311           return false;
5312         total = GetSingleValueInt(
5313             "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS);
5314       }
5315       else
5316         total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
5317     }
5318     if (countOnly)
5319     {
5320       CFileItemPtr pItem(new CFileItem());
5321       pItem->SetProperty("total", total);
5322       items.Add(pItem);
5323 
5324       m_pDS->close();
5325       return true;
5326     }
5327 
5328     // Apply any limiting directly in SQL
5329     if (limitedInSQL)
5330     {
5331       extFilter.limit =
5332           DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5333     }
5334 
5335     // Apply sort in SQL
5336     const std::shared_ptr<CSettings> settings =
5337         CServiceBroker::GetSettingsComponent()->GetSettings();
5338     if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5339       sorting.sortAttributes =
5340           static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5341     // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5342     GetOrderFilter(MediaTypeAlbum, sorting, extFilter);
5343     // Modify order to use correct calculated year field
5344     if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5345             CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
5346       StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
5347     else
5348       StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5349 
5350     strSQLExtra.clear();
5351     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5352       return false;
5353 
5354     std::string strSQL;
5355     std::string strFields = "albumview.*";
5356     if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5357       strFields = "albumview.*, " + extFilter.fields;
5358     strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra;
5359 
5360     // run query
5361     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
5362     querytime = XbmcThreads::SystemClockMillis();
5363     if (!m_pDS->query(strSQL))
5364       return false;
5365     int iRowsFound = m_pDS->num_rows();
5366     if (iRowsFound == 0)
5367     {
5368       m_pDS->close();
5369       return true;
5370     }
5371     querytime = XbmcThreads::SystemClockMillis() - querytime;
5372 
5373     // Store the total number of albums as a property
5374     if (total < iRowsFound)
5375       total = iRowsFound;
5376     items.SetProperty("total", total);
5377 
5378     DatabaseResults results;
5379     results.reserve(iRowsFound);
5380     // Populate results field vector from dataset
5381     FieldList fields;
5382     if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results))
5383       return false;
5384     // Store item list sort order
5385     items.SetSortMethod(sorting.sortBy);
5386     items.SetSortOrder(sorting.sortOrder);
5387 
5388     // Get albums from returned rows
5389     items.Reserve(results.size());
5390     const dbiplus::query_data &data = m_pDS->get_result_set().records;
5391     for (const auto &i : results)
5392     {
5393       unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5394       const dbiplus::sql_record* const record = data.at(targetRow);
5395 
5396       try
5397       {
5398         CMusicDbUrl itemUrl = musicUrl;
5399         std::string path = StringUtils::Format("%i/", record->at(album_idAlbum).get_asInt());
5400         itemUrl.AppendPath(path);
5401 
5402         CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record)));
5403         // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5404         pItem->SetProperty("icon_never_overlay", true);
5405         pItem->SetArt("icon", "DefaultAlbumCover.png");
5406         items.Add(pItem);
5407       }
5408       catch (...)
5409       {
5410         m_pDS->close();
5411         CLog::Log(LOGERROR, "%s - out of memory getting listing (got %i)", __FUNCTION__, items.Size());
5412       }
5413     }
5414     // cleanup
5415     m_pDS->close();
5416 
5417     CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms",
5418       __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
5419     return true;
5420   }
5421   catch (...)
5422   {
5423     m_pDS->close();
5424     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, filter.where.c_str());
5425   }
5426   return false;
5427 }
5428 
GetDiscsNav(const std::string & strBaseDir,CFileItemList & items,int idAlbum,const Filter & filter,const SortDescription & sortDescription,bool countOnly)5429 bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir, CFileItemList& items, int idAlbum,
5430   const Filter& filter, const SortDescription& sortDescription, bool countOnly)
5431 {
5432   CMusicDbUrl musicUrl;
5433   if (!musicUrl.FromString(strBaseDir))
5434     return false;
5435 
5436   if (idAlbum > 0)
5437     musicUrl.AddOption("albumid", idAlbum);
5438 
5439   return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5440 }
5441 
GetDiscsByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5442 bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir,
5443                                      const Filter& filter,
5444                                      CFileItemList& items,
5445                                      const SortDescription& sortDescription,
5446                                      bool countOnly)
5447 {
5448   CMusicDbUrl musicUrl;
5449   if (!musicUrl.FromString(baseDir))
5450     return false;
5451   return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5452 }
5453 
GetDiscsByWhere(CMusicDbUrl & musicUrl,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5454 bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl,
5455                                      const Filter& filter,
5456                                      CFileItemList& items,
5457                                      const SortDescription& sortDescription,
5458                                      bool countOnly)
5459 {
5460   if (m_pDB == nullptr || m_pDS == nullptr)
5461     return false;
5462 
5463   try
5464   {
5465     unsigned int querytime = 0;
5466     unsigned int time = XbmcThreads::SystemClockMillis();
5467     int total = -1;
5468     std::string strSQL;
5469 
5470     Filter extFilter = filter;
5471     SortDescription sorting = sortDescription;
5472 
5473     if (!GetFilter(musicUrl, extFilter, sorting))
5474       return false;
5475 
5476     extFilter.AppendGroup("albumview.idAlbum, iDisc");
5477 
5478     // If there are extra songview WHERE conditions adjust to song or albumview
5479     // fields, and join Path table for strPath
5480     // ! @todo: convert songview fields into to song or albumview fields
5481     // But not sure we ever get songview fields in filter - REMOVE??
5482     if (extFilter.where.find("songview.strPath") != std::string::npos)
5483     {
5484       extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
5485     }
5486 
5487     std::string strSQLExtra;
5488     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5489       return false;
5490 
5491     // Apply any limiting directly in SQL if there is either no special sorting or random sort
5492     // When limited, random sort is also applied in SQL
5493     bool limitedInSQL = extFilter.limit.empty() &&
5494       (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) &&
5495       (sorting.limitStart > 0 || sorting.limitEnd > 0);
5496 
5497     if (countOnly || limitedInSQL)
5498     {
5499       // Count number of discs that satisfy selection criteria
5500       // (when fetching all records get total from row count of results dataset)
5501       // Count not allow for same non-null title discs to be grouped together
5502       strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
5503                "albumview.idAlbum " +
5504                strSQLExtra;
5505       strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc ";
5506       total = GetSingleValueInt(strSQL, m_pDS);
5507     }
5508     if (countOnly)
5509     {
5510       items.SetProperty("total", total);
5511       return true;
5512     }
5513     // Apply limits and random sort order directly in SQL
5514     if (limitedInSQL)
5515     {
5516       if (sorting.sortBy == SortByRandom)
5517         strSQLExtra += PrepareSQL(" ORDER BY RANDOM()");
5518       strSQLExtra +=
5519           DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
5520     }
5521     else
5522       strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
5523 
5524     strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
5525       "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " + strSQLExtra;
5526 
5527     // run query
5528     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
5529     querytime = XbmcThreads::SystemClockMillis();
5530     if (!m_pDS->query(strSQL))
5531       return false;
5532     int iRowsFound = m_pDS->num_rows();
5533     if (iRowsFound == 0)
5534     {
5535       m_pDS->close();
5536       return true;
5537     }
5538     querytime = XbmcThreads::SystemClockMillis() - querytime;
5539 
5540     // store the total value of items as a property
5541     if (total < iRowsFound)
5542       total = iRowsFound;
5543     items.SetProperty("total", total);
5544 
5545     DatabaseResults results;
5546     results.reserve(iRowsFound);
5547 
5548     // Avoid sorting with limits, just fetch results from dataset
5549     // Limit when SortByNone already applied in SQL,
5550     // Need guaranteed ordering for dataset processing to group by disc title
5551     // so apply sort later to fileitems list rather than dataset
5552     if (sorting.sortBy != SortByNone)
5553       sorting.sortBy = SortByNone;
5554     if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results))
5555       return false;
5556 
5557     // Get data from returned rows, note possibly multiple albums although usually only one
5558     items.Reserve(total);
5559     int albumOffset = 2;
5560     CAlbum album;
5561     bool useTitle = true; // Assume we want to match by disc title later unless we have no titles
5562     std::string oldDiscTitle;
5563     const dbiplus::query_data& data = m_pDS->get_result_set().records;
5564     for (const auto& i : results)
5565     {
5566       unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger());
5567       const dbiplus::sql_record* const record = data.at(targetRow);
5568       try
5569       {
5570         if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt())
5571         { // New album
5572           useTitle = true;
5573           album = GetAlbumFromDataset(record, albumOffset);
5574         }
5575 
5576         int discnum = record->at(0).get_asInt();
5577         std::string strDiscSubtitle = record->at(1).get_asString();
5578         if (strDiscSubtitle.empty())
5579         { // Make (fake) disc title from disc number, group by disc number as no real title to match
5580           strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum);
5581           useTitle = false;
5582         }
5583         else if (oldDiscTitle == strDiscSubtitle)
5584         { // disc title already added to list, fetch the next disc
5585           continue;
5586         }
5587         oldDiscTitle = strDiscSubtitle;
5588 
5589         CMusicDbUrl itemUrl = musicUrl;
5590         std::string path = StringUtils::Format("%i/", discnum);
5591         itemUrl.AppendPath(path);
5592 
5593         // When disc titles are provided group discs together by title not number.
5594         // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
5595         if (useTitle)
5596           itemUrl.AddOption("disctitle", strDiscSubtitle.c_str());
5597         else
5598           itemUrl.AddOption("discid", discnum);
5599         CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album));
5600         pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order??
5601         pItem->GetMusicInfoTag()->SetDiscNumber(discnum);
5602         pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle);
5603         pItem->SetLabel(strDiscSubtitle);
5604         // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5605         pItem->SetProperty("icon_never_overlay", true);
5606         pItem->SetArt("icon", "DefaultAlbumCover.png");
5607         items.Add(pItem);
5608       }
5609       catch (...)
5610       {
5611         m_pDS->close();
5612         CLog::Log(LOGERROR, "%s - out of memory getting listing (got %i)", __FUNCTION__,
5613                   items.Size());
5614       }
5615     }
5616 
5617     // cleanup
5618     m_pDS->close();
5619 
5620     // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
5621     // that is when have join with songartistview and sorting other than random with limit
5622     if (sorting.sortBy != SortByNone &&
5623         !(limitedInSQL && sorting.sortBy == SortByRandom))
5624       items.Sort(sorting);
5625 
5626     CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms",
5627       __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
5628     return true;
5629   }
5630   catch (...)
5631   {
5632     m_pDS->close();
5633     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, filter.where.c_str());
5634   }
5635 
5636   return false;
5637 }
GetDiscsCount(const std::string & baseDir,const Filter & filter)5638 int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */)
5639 {
5640   int iDiscTotal = -1;
5641   CFileItemList itemscount;
5642   if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true))
5643     iDiscTotal = itemscount.GetProperty("total").asInteger32();
5644   return iDiscTotal;
5645 }
5646 
GetSongsFullByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool artistData)5647 bool CMusicDatabase::GetSongsFullByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, const SortDescription &sortDescription /* = SortDescription() */, bool artistData /* = false*/)
5648 {
5649   if (m_pDB == nullptr || m_pDS == nullptr)
5650     return false;
5651 
5652   try
5653   {
5654     unsigned int querytime = 0;
5655     unsigned int time = XbmcThreads::SystemClockMillis();
5656     int total = -1;
5657 
5658     Filter extFilter = filter;
5659     CMusicDbUrl musicUrl;
5660     SortDescription sorting = sortDescription;
5661     if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5662       return false;
5663 
5664     bool extended = false;
5665     bool limitedInSQL =
5666         extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0);
5667 
5668     // If there are extra WHERE conditions (from media filter dialog) we might
5669     // need access to albumview for these conditions
5670     if (extFilter.where.find("albumview") != std::string::npos)
5671     {
5672       extended = true;
5673       extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
5674     }
5675 
5676     // Build songview <where> for count
5677     std::string strSQLExtra;
5678     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5679       return false;
5680 
5681     // Count (without group by) number of songs that satisfy selection criteria
5682     // Much quicker to use song table, not songview, when filtering only on song fields
5683     if (extended ||
5684         (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos ||
5685                                       extFilter.where.find("strPath") != std::string::npos ||
5686                                       extFilter.where.find("bCompilation") != std::string::npos ||
5687                                       extFilter.where.find("bBoxedset") != std::string::npos)))
5688       total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS);
5689     else
5690     {
5691       std::string strSQLsong = strSQLExtra;
5692       StringUtils::Replace(strSQLsong, "songview", "song");
5693       total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS);
5694     }
5695 
5696     if (extended)
5697       extFilter.AppendGroup("songview.idSong");
5698 
5699     // Apply any limiting directly in SQL
5700     if (limitedInSQL)
5701     {
5702       extFilter.limit =
5703           DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5704     }
5705 
5706     // Apply sort in SQL
5707     const std::shared_ptr<CSettings> settings =
5708         CServiceBroker::GetSettingsComponent()->GetSettings();
5709     if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5710       sorting.sortAttributes =
5711           static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5712     // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5713     GetOrderFilter(MediaTypeSong, sorting, extFilter);
5714     // Modify order to use correct calculated year field
5715     if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5716             CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
5717       StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
5718     else
5719       StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5720 
5721     std::string strFields = "songview.*";
5722     if (!artistData || limitedInSQL)
5723     {
5724       // Build songview <where> + <order by> + <limits>
5725       strSQLExtra.clear();
5726       if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5727         return false;
5728     }
5729     else
5730       strFields = "songview.*, songartistview.*";
5731     if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5732       strFields = strFields + ", " + extFilter.fields;
5733 
5734     std::string strSQL;
5735     if (artistData)
5736     { // Get data from song and song_artist tables to fully populate songs with artists
5737       // All songs now have at least one artist so inner join sufficient
5738       // Build songartistview JOIN part of query
5739       Filter joinFilter;
5740       std::string strSQLJoin;
5741       joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
5742       if (sortDescription.sortBy == SortByRandom)
5743         joinFilter.AppendOrder("songartistview.idSong");
5744       else
5745         joinFilter.order = extFilter.order;
5746       if (limitedInSQL)
5747       {
5748         StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong");
5749         StringUtils::Replace(joinFilter.order, "songview.", "sv.");
5750       }
5751       else
5752         joinFilter.where = extFilter.where;
5753       joinFilter.AppendOrder("songartistview.idRole");
5754       joinFilter.AppendOrder("songartistview.iOrder");
5755       if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
5756         return false;
5757 
5758       if (limitedInSQL)
5759       {
5760         // When have artist data (all roles) and LIMIT on songs use inline view
5761         // SELECT sv.*, songartistview.* FROM
5762         //   (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
5763         //   <order by sv fields>, songartistview.idRole, songartistview.iOrder
5764         // Apply where clause, limits and order to songview, then join to songartistview this gives
5765         // multiple records per song in result set
5766         strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
5767         strSQL = "(" + strSQL + ") AS sv ";
5768         strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin;
5769       }
5770       else
5771         strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin;
5772     }
5773     else
5774       strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
5775 
5776     CLog::Log(LOGDEBUG, "%s query = %s", __FUNCTION__, strSQL.c_str());
5777     querytime = XbmcThreads::SystemClockMillis();
5778     // run query
5779     if (!m_pDS->query(strSQL))
5780       return false;
5781 
5782     int iRowsFound = m_pDS->num_rows();
5783     if (iRowsFound == 0)
5784     {
5785       m_pDS->close();
5786       return true;
5787     }
5788     querytime = XbmcThreads::SystemClockMillis() - querytime;
5789 
5790     // Store the total number of songs as a property
5791     items.SetProperty("total", total);
5792 
5793     DatabaseResults results;
5794     results.reserve(iRowsFound);
5795     // Populate results field vector from dataset
5796     FieldList fields;
5797     if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results))
5798       return false;
5799     // Store item list sort order
5800     items.SetSortMethod(sorting.sortBy);
5801     items.SetSortOrder(sorting.sortOrder);
5802 
5803     // Get songs from returned rows. If join songartistview then there is a row for every artist
5804     items.Reserve(total);
5805     int songArtistOffset = song_enumCount;
5806     int songId = -1;
5807     VECARTISTCREDITS artistCredits;
5808     const dbiplus::query_data &data = m_pDS->get_result_set().records;
5809     int count = 0;
5810     for (const auto &i : results)
5811     {
5812       unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5813       const dbiplus::sql_record* const record = data.at(targetRow);
5814 
5815       try
5816       {
5817         if (songId != record->at(song_idSong).get_asInt())
5818         { //New song
5819           if (songId > 0 && !artistCredits.empty())
5820           {
5821             //Store artist credits for previous song
5822             GetFileItemFromArtistCredits(artistCredits, items[items.Size()-1].get());
5823             artistCredits.clear();
5824           }
5825           songId = record->at(song_idSong).get_asInt();
5826           CFileItemPtr item(new CFileItem);
5827           GetFileItemFromDataset(record, item.get(), musicUrl);
5828           // HACK for sorting by database returned order
5829           item->m_iprogramCount = ++count;
5830           // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5831           item->SetProperty("icon_never_overlay", true);
5832           item->SetArt("icon", "DefaultAudio.png");
5833           items.Add(item);
5834         }
5835         // Get song artist credits and contributors
5836         if (artistData)
5837         {
5838           int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
5839           if (idSongArtistRole == ROLE_ARTIST)
5840             artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
5841           else
5842             items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
5843         }
5844       }
5845       catch (...)
5846       {
5847         m_pDS->close();
5848         CLog::Log(LOGERROR, "%s: out of memory loading query: %s", __FUNCTION__, filter.where.c_str());
5849         return (items.Size() > 0);
5850       }
5851     }
5852     if (!artistCredits.empty())
5853     {
5854       //Store artist credits for final song
5855       GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
5856       artistCredits.clear();
5857     }
5858     // cleanup
5859     m_pDS->close();
5860 
5861     // Ensure random order of item list when results set sorted by idSong for artist processing
5862     // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
5863     // navigation. Order is read later from view state and list sorting is then triggered by
5864     // CGUIMediaWindow::Update in both cases.
5865     // So sorting here is currently redundant, but the consistent place to do it.
5866     // !@ todo: do sorting once, preferably in SQL
5867     if (sorting.sortBy == SortByRandom && artistData)
5868       items.Sort(sorting);
5869 
5870     CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__,
5871               XbmcThreads::SystemClockMillis() - time, querytime);
5872     return true;
5873   }
5874   catch (...)
5875   {
5876     // cleanup
5877     m_pDS->close();
5878     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
5879   }
5880   return false;
5881 }
5882 
GetSongsByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription)5883 bool CMusicDatabase::GetSongsByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, const SortDescription &sortDescription /* = SortDescription() */)
5884 {
5885   if (m_pDB == nullptr || m_pDS == nullptr)
5886     return false;
5887 
5888   try
5889   {
5890     int total = -1;
5891 
5892     std::string strSQL = "SELECT %s FROM songview ";
5893 
5894     Filter extFilter = filter;
5895     CMusicDbUrl musicUrl;
5896     SortDescription sorting = sortDescription;
5897     if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5898       return false;
5899 
5900     // if there are extra WHERE conditions we might need access
5901     // to songview for these conditions
5902     if (extFilter.where.find("albumview") != std::string::npos)
5903     {
5904       extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
5905       extFilter.AppendGroup("songview.idSong");
5906     }
5907 
5908     std::string strSQLExtra;
5909     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5910       return false;
5911 
5912     // Apply the limiting directly here if there's no special sorting but limiting
5913     if (extFilter.limit.empty() &&
5914         sorting.sortBy == SortByNone &&
5915        (sorting.limitStart > 0 || sorting.limitEnd > 0))
5916     {
5917       total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS);
5918       strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
5919     }
5920 
5921     strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0 ? filter.fields.c_str() : "songview.*") + strSQLExtra;
5922 
5923     CLog::Log(LOGDEBUG, "%s query = %s", __FUNCTION__, strSQL.c_str());
5924     // run query
5925     if (!m_pDS->query(strSQL))
5926       return false;
5927 
5928     int iRowsFound = m_pDS->num_rows();
5929     if (iRowsFound == 0)
5930     {
5931       m_pDS->close();
5932       return true;
5933     }
5934 
5935     // store the total value of items as a property
5936     if (total < iRowsFound)
5937       total = iRowsFound;
5938     items.SetProperty("total", total);
5939 
5940     DatabaseResults results;
5941     results.reserve(iRowsFound);
5942     if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results))
5943       return false;
5944 
5945     // get data from returned rows
5946     items.Reserve(results.size());
5947     const dbiplus::query_data &data = m_pDS->get_result_set().records;
5948     int count = 0;
5949     for (const auto &i : results)
5950     {
5951       unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5952       const dbiplus::sql_record* const record = data.at(targetRow);
5953 
5954       try
5955       {
5956         CFileItemPtr item(new CFileItem);
5957         GetFileItemFromDataset(record, item.get(), musicUrl);
5958         // HACK for sorting by database returned order
5959         item->m_iprogramCount = ++count;
5960         items.Add(item);
5961       }
5962       catch (...)
5963       {
5964         m_pDS->close();
5965         CLog::Log(LOGERROR, "%s: out of memory loading query: %s", __FUNCTION__, filter.where.c_str());
5966         return (items.Size() > 0);
5967       }
5968     }
5969 
5970     // cleanup
5971     m_pDS->close();
5972     return true;
5973   }
5974   catch (...)
5975   {
5976     // cleanup
5977     m_pDS->close();
5978     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
5979   }
5980   return false;
5981 }
5982 
GetSongsByYear(const std::string & baseDir,CFileItemList & items,int year)5983 bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year)
5984 {
5985   CMusicDbUrl musicUrl;
5986   if (!musicUrl.FromString(baseDir))
5987     return false;
5988 
5989   musicUrl.AddOption("year", year);
5990 
5991   Filter filter;
5992   return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true);
5993 }
5994 
GetSongsNav(const std::string & strBaseDir,CFileItemList & items,int idGenre,int idArtist,int idAlbum,const SortDescription & sortDescription)5995 bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir, CFileItemList& items, int idGenre, int idArtist, int idAlbum, const SortDescription &sortDescription /* = SortDescription() */)
5996 {
5997   CMusicDbUrl musicUrl;
5998   if (!musicUrl.FromString(strBaseDir))
5999     return false;
6000 
6001   if (idAlbum > 0)
6002     musicUrl.AddOption("albumid", idAlbum);
6003 
6004   if (idGenre > 0)
6005     musicUrl.AddOption("genreid", idGenre);
6006 
6007   if (idArtist > 0)
6008     musicUrl.AddOption("artistid", idArtist);
6009 
6010   Filter filter;
6011   return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true);
6012 }
6013 
6014 // clang-format off
6015 typedef struct
6016 {
6017   std::string fieldJSON;  // Field name in JSON schema
6018   std::string formatJSON; // Format in JSON schema
6019   bool bSimple;           // Fetch field directly to JSON output
6020   std::string fieldDB;    // Name of field in db query
6021   std::string SQL;        // SQL for scalar subqueries or field alias
6022 } translateJSONField;
6023 
6024 static const translateJSONField JSONtoDBArtist[] = {
6025   // Table and single value join fields
6026   { "artist",                    "string", true,  "strArtist",              "" }, // Label field at top
6027   { "sortname",                  "string", true,  "strSortname",            "" },
6028   { "instrument",                 "array", true,  "strInstruments",         "" },
6029   { "description",               "string", true,  "strBiography",           "" },
6030   { "genre",                      "array", true,  "strGenres",              "" },
6031   { "mood",                       "array", true,  "strMoods",               "" },
6032   { "style",                      "array", true,  "strStyles",              "" },
6033   { "yearsactive",                "array", true,  "strYearsActive",         "" },
6034   { "born",                      "string", true,  "strBorn",                "" },
6035   { "formed",                    "string", true,  "strFormed",              "" },
6036   { "died",                      "string", true,  "strDied",                "" },
6037   { "disbanded",                 "string", true,  "strDisbanded",           "" },
6038   { "type",                      "string", true,  "strType",                "" },
6039   { "gender",                    "string", true,  "strGender",              "" },
6040   { "disambiguation",            "string", true,  "strDisambiguation",      "" },
6041   { "musicbrainzartistid",        "array", true,  "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
6042   { "dateadded",                 "string", true,  "dateAdded",              "" },
6043   { "datenew",                   "string", true,  "dateNew",                "" },
6044   { "datemodified",              "string", true,  "dateModified",           "" },
6045 
6046   // JOIN fields (multivalue), same order as _JoinToArtistFields
6047   { "",                                "", false, "isSong",                 "" },
6048   { "sourceid",                  "string", false, "idSourceAlbum",          "album_source.idSource AS idSourceAlbum" },
6049   { "",                          "string", false, "idSourceSong",           "album_source.idSource AS idSourceSong" },
6050   { "songgenres",                 "array", false, "idSongGenreAlbum",       "song_genre.idGenre AS idSongGenreAlbum" },
6051   { "",                           "array", false, "idSongGenreSong",        "song_genre.idGenre AS idSongGenreSong" },
6052   { "",                                "", false, "strSongGenreAlbum",      "genre.strGenre AS strSongGenreAlbum" },
6053   { "",                                "", false, "strSongGenreSong",       "genre.strGenre AS strSongGenreSong" },
6054   { "art",                             "", false, "idArt",                  "art.art_id AS idArt" },
6055   { "",                                "", false, "artType",                "art.type AS artType" },
6056   { "",                                "", false, "artURL",                 "art.url AS artURL" },
6057   { "",                                "", false, "idRole",                 "song_artist.idRole" },
6058   { "roles",                           "", false, "strRole",                "role.strRole" },
6059   { "",                                "", false, "iOrderRole",             "song_artist.iOrder AS iOrderRole" },
6060   // Derived from joined tables
6061   { "isalbumartist",               "bool", false, "",                       "" },
6062   { "thumbnail",                 "string", false, "",                       "" },
6063   { "fanart",                    "string", false, "",                       "" }
6064   // clang-format on
6065   /*
6066   Sources and genre are related via album, and so the dataset only contains source
6067   and genre pairs that exist, rather than all the genres being repeated for every
6068   source. We can not only look at genres for the first source, and genre can be
6069   out of order.
6070   */
6071 
6072 };
6073 
6074 static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField);
6075 
GetArtistsByWhereJSON(const std::set<std::string> & fields,const std::string & baseDir,CVariant & result,int & total,const SortDescription & sortDescription)6076 bool CMusicDatabase::GetArtistsByWhereJSON(const std::set<std::string>& fields, const std::string &baseDir,
6077   CVariant& result, int& total, const SortDescription &sortDescription /* = SortDescription() */)
6078 {
6079   if (nullptr == m_pDB)
6080     return false;
6081   if (nullptr == m_pDS)
6082     return false;
6083 
6084   try
6085   {
6086     total = -1;
6087 
6088     size_t resultcount = 0;
6089     Filter extFilter;
6090     CMusicDbUrl musicUrl;
6091     SortDescription sorting = sortDescription;
6092     //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
6093     if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6094       return false;
6095 
6096     // Replace view names in filter with table names
6097     StringUtils::Replace(extFilter.where, "artistview", "artist");
6098     StringUtils::Replace(extFilter.where, "albumview", "album");
6099 
6100     std::string strSQLExtra;
6101     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6102       return false;
6103 
6104     // Count number of artists that satisfy selection criteria
6105     //(includes xsp limits from filter, but not sort limits)
6106     total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS);
6107     resultcount = static_cast<size_t>(total);
6108 
6109     // Process albumartistsonly option
6110     const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
6111     bool albumArtistsOnly(false);
6112     auto option = options.find("albumartistsonly");
6113     if (option != options.end())
6114       albumArtistsOnly = option->second.asBoolean();
6115     // Process role options
6116     int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles.
6117     option = options.find("roleid");
6118     if (option != options.end())
6119       roleidfilter = static_cast<int>(option->second.asInteger());
6120     else
6121     {
6122       option = options.find("role");
6123       if (option != options.end())
6124       {
6125         if (option->second.asString() == "all" || option->second.asString() == "%")
6126           roleidfilter = -1000; //All roles
6127         else
6128           roleidfilter = GetRoleByName(option->second.asString());
6129       }
6130     }
6131 
6132     // Get order by (and any scalar query artist fields)
6133     int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter);
6134     // Replace artistview field names in order by artist table field names
6135     StringUtils::Replace(extFilter.order, "artistview", "artist");
6136     StringUtils::Replace(extFilter.fields, "artistview", "artist");
6137 
6138     // Grab and adjust artist sort field that may have been added to filter
6139     // These need to be added to the end of the artist table field list
6140     std::string artistsortSQL = extFilter.fields;
6141     extFilter.fields.clear();
6142 
6143     std::string strSQL;
6144 
6145     // Setup fields to query, and album field number mapping
6146     // Find first join field (isSong) in JSONtoDBArtist for offset
6147     int index_firstjoin = -1;
6148     for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++)
6149     {
6150       if (JSONtoDBArtist[i].fieldDB == "isSong")
6151       {
6152         index_firstjoin = i;
6153         break;
6154       }
6155     }
6156     Filter joinFilter;
6157     Filter albumArtistFilter;
6158     Filter songArtistFilter;
6159     DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount));
6160     extFilter.AppendField("artist.idArtist");  // ID "artistid" in JSON
6161     std::vector<int> dbfieldindex;
6162     // JSON "label" field is strArtist which is also output as "artist", query field once output twice
6163     extFilter.AppendField(JSONtoDBArtist[0].fieldDB);
6164     dbfieldindex.emplace_back(0); // Output "artist"
6165 
6166     // Check each otional artist db field that could be retrieved (not "artist")
6167     for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++)
6168     {
6169       bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end();
6170       if (JSONtoDBArtist[i].bSimple)
6171       {
6172         // Check for non-join fields in order too.
6173         // Query these in inline view (but not output) so can ref in outer order
6174         bool foundOrderby(false);
6175         if (!foundJSON)
6176           foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos;
6177         if (foundOrderby || foundJSON)
6178         {
6179           // Store indexes of requested artist table and scalar subquery fields
6180           // to be output, and -1 when not output to JSON
6181           if (!foundJSON)
6182             dbfieldindex.emplace_back(-1);
6183           else
6184             dbfieldindex.emplace_back(i);
6185           // Field from scaler subquery
6186           if (!JSONtoDBArtist[i].SQL.empty())
6187             extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL));
6188           else
6189             // Field from artist table
6190             extFilter.AppendField(JSONtoDBArtist[i].fieldDB);
6191         }
6192       }
6193       else if (foundJSON)
6194         // Field from join or derived from joined fields
6195         joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true);
6196     }
6197 
6198     // Append calculated artistsort field that may have been added to filter
6199     // Field used only for ORDER BY, not output to JSON
6200     extFilter.AppendField(artistsortSQL);
6201     for (int i = 0; i < iAddedFields; i++)
6202       dbfieldindex.emplace_back(-2); // columns in dataset
6203 
6204     // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6205     strSQLExtra = "";
6206     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6207       return false;
6208 
6209     // Add any LIMIT clause to strSQLExtra
6210     if (extFilter.limit.empty() &&
6211       (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
6212     {
6213       strSQLExtra += DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
6214       resultcount = std::min(
6215         DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
6216         resultcount);
6217     }
6218 
6219     // Setup multivalue JOINs, GROUP BY and ORDER BY
6220     bool bJoinAlbumArtist(false);
6221     bool bJoinSongArtist(false);
6222     if (sortDescription.sortBy != SortByRandom)
6223     {
6224       // Repeat inline view order (that always includes idArtist) on join query
6225       std::string order = extFilter.order;
6226       StringUtils::Replace(order, "artist.", "a1.");
6227       joinFilter.AppendOrder(order);
6228     }
6229     else
6230       joinFilter.AppendOrder("a1.idArtist");
6231     joinFilter.AppendGroup("a1.idArtist");
6232     // Album artists and song artists
6233     if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) ||
6234       joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6235       joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) ||
6236       joinLayout.GetFetch(joinToArtist_strRole))
6237     {
6238       bJoinAlbumArtist = true;
6239       albumArtistFilter.AppendField("album_artist.idArtist AS id");
6240       if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole))
6241       {
6242         bJoinSongArtist = true;
6243         songArtistFilter.AppendField("song_artist.idArtist AS id");
6244         songArtistFilter.AppendField("1 AS isSong");
6245         albumArtistFilter.AppendField("0 AS isSong");
6246         joinLayout.SetField(joinToArtist_isSong, JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6247         joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6248         joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6249       }
6250     }
6251     else if (joinLayout.GetFetch(joinToArtist_isalbumartist))
6252     {
6253       // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
6254       // so no need for join to album_artist table. Set fetching fetch false so that
6255       // joinLayout.HasFilterFields() is false
6256       joinLayout.SetFetch(joinToArtist_isalbumartist, false);
6257     }
6258 
6259     // Sources
6260     if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6261     { // Left join as source may have been removed but leaving lib entries
6262       albumArtistFilter.AppendJoin("LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
6263       albumArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL);
6264       joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6265       joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6266       if (bJoinSongArtist)
6267       {
6268         songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
6269         songArtistFilter.AppendJoin("LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
6270         songArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6271         songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL);
6272         albumArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6273         joinLayout.SetField(joinToArtist_idSourceSong, JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6274         joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6275         joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6276       }
6277       else
6278       {
6279         joinLayout.SetField(joinToArtist_idSourceAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true);
6280       }
6281     }
6282 
6283     // Songgenres - id and genres always both
6284     if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
6285     { // All albums have songs, but left join genre as songs may not have genre
6286       albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
6287       albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
6288       albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6289       albumArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL);
6290       albumArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6291       joinLayout.SetField(joinToArtist_strSongGenreAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6292       joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6293       joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6294       if (bJoinSongArtist)
6295       { // Left join genre as songs may not have genre
6296         songArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
6297         songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6298         songArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6299         songArtistFilter.AppendField("'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6300         songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL);
6301         songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL);
6302         albumArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6303         albumArtistFilter.AppendField("'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6304         joinLayout.SetField(joinToArtist_idSongGenreSong, JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6305         joinLayout.SetField(joinToArtist_strSongGenreSong, JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6306         joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6307         joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6308       }
6309       else
6310       {  // Define field alias names in join layout
6311         joinLayout.SetField(joinToArtist_idSongGenreAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL, true);
6312         joinLayout.SetField(joinToArtist_strSongGenreAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6313       }
6314     }
6315 
6316     // Roles
6317     if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole))
6318       // Only looking at album and song artists not other roles (default),
6319       // so filter dataset rows likewise.
6320       songArtistFilter.AppendWhere("song_artist.idRole = 1");
6321     else if (joinLayout.GetFetch(joinToArtist_strRole) ||  // "roles" field
6322              (bJoinSongArtist &&
6323              (joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6324              joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))))
6325     { // Rows from many roles so fetch roleid for "roles", source and genre processing
6326       songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL);
6327       // Add fake column to album_artist query
6328       albumArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6329       joinLayout.SetField(joinToArtist_idRole, JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6330       joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6331       joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6332     }
6333     if (joinLayout.GetFetch(joinToArtist_strRole))
6334     { // Fetch role desc
6335       songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
6336       songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL);
6337       // Add fake column to album_artist query
6338       albumArtistFilter.AppendField("'albumartist' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB);
6339     }
6340 
6341     // Build source, genre and roles part of query
6342     if (bJoinAlbumArtist)
6343     {
6344       if (bJoinSongArtist)
6345       {
6346         // Combine song and album artist filter as UNION and add to join filter as an inline view
6347         std::string strAlbumSQL;
6348         if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL))
6349           return false;
6350         strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL;
6351         std::string strSongSQL;
6352         if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL))
6353           return false;
6354         strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL;
6355 
6356         joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL + ") AS albumSong ON id = a1.idArtist");
6357       }
6358       else
6359       { //Only join album_artist, so move filter elements to join filter
6360         joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
6361         joinFilter.AppendJoin(albumArtistFilter.join);
6362       }
6363     }
6364 
6365     //Art
6366     bool bJoinArt(false);
6367     bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) ||
6368       joinLayout.GetOutput(joinToArtist_thumbnail) ||
6369       joinLayout.GetOutput(joinToArtist_fanart);
6370     if (bJoinArt)
6371     { // Left join as artist may not have any art
6372       joinFilter.AppendJoin("LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
6373       joinLayout.SetField(joinToArtist_idArt, JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL,
6374         joinLayout.GetOutput(joinToArtist_idArt));
6375       joinLayout.SetField(joinToArtist_artType, JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL);
6376       joinLayout.SetField(joinToArtist_artURL, JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL);
6377       joinFilter.AppendGroup("art.art_id");
6378       joinFilter.AppendOrder("arttype");
6379       if (!joinLayout.GetOutput(joinToArtist_idArt))
6380       {
6381         if (!joinLayout.GetOutput(joinToArtist_thumbnail))
6382           // Fanart only
6383           joinFilter.AppendJoin("AND art.type = 'fanart'");
6384         else if (!joinLayout.GetOutput(joinToArtist_fanart))
6385           // Thumb only
6386           joinFilter.AppendJoin("AND art.type = 'thumb'");
6387       }
6388     }
6389     else if (bJoinSongArtist)
6390       joinFilter.group.clear(); // UNION only so no GROUP BY needed
6391 
6392     // Build JOIN part of query (if we have one)
6393     std::string strSQLJoin;
6394     if (joinLayout.HasFilterFields())
6395       if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6396         return false;
6397 
6398     // Adjust where in the results record the join fields are allowing for the
6399     // inline view fields (Quicker than finding field by name every time)
6400     // idArtist + other artist fields
6401     joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
6402 
6403     // Build full query
6404     // When have multiple value joins e.g. song genres, use inline view
6405     // SELECT a1.*, <join fields> FROM
6406     //   (SELECT <artist fields> FROM artist <where> + <order by> +  <limits> ) AS a1
6407     //   <joins> <group by> <order by> + <joins order by>
6408     // Don't use prepareSQL - confuses  arttype = 'thumb' filter
6409 
6410     strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra;
6411     if (joinLayout.HasFilterFields())
6412     {
6413       strSQL = "(" + strSQL + ") AS a1 ";
6414       strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
6415     }
6416 
6417     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
6418     // run query
6419     unsigned int time = XbmcThreads::SystemClockMillis();
6420     if (!m_pDS->query(strSQL))
6421       return false;
6422     CLog::Log(LOGDEBUG, "%s - query took %i ms",
6423       __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
6424 
6425     int iRowsFound = m_pDS->num_rows();
6426     if (iRowsFound <= 0)
6427     {
6428       m_pDS->close();
6429       return true;
6430     }
6431 
6432     // Get artists from returned rows. Joins means there can be many rows per artist
6433     int artistId = -1;
6434     int sourceId = -1;
6435     int genreId = -1;
6436     int roleId = -1;
6437     int artId = -1;
6438     std::vector<int> genreidlist;
6439     std::vector<int> sourceidlist;
6440     std::vector<int> roleidlist;
6441     bool bArtDone(false);
6442     bool bHaveArtist(false);
6443     bool bIsAlbumArtist(true);
6444     bool bGenreFoundViaAlbum(false);
6445     CVariant artistObj;
6446     result["artists"].reserve(resultcount);
6447     while (!m_pDS->eof() || bHaveArtist)
6448     {
6449       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
6450 
6451       if (m_pDS->eof() || artistId != record->at(0).get_asInt())
6452       {
6453         // Store previous or last artist
6454         if (bHaveArtist)
6455         {
6456           // Convert any empty MBid array into an array with one empty element [""]
6457           // to match the number of artist ID (way other mbid arrays handled)
6458           if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty())
6459             artistObj["musicbrainzartistid"].append("");
6460 
6461           result["artists"].append(artistObj);
6462           bHaveArtist = false;
6463           artistObj.clear();
6464         }
6465         if (artistObj.empty())
6466         {
6467           // Initialise fields, ensure those with possible null values are set to correct empty variant type
6468           if (joinLayout.GetOutput(joinToArtist_idSourceAlbum))
6469             artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
6470           if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum))
6471             artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
6472           if (joinLayout.GetOutput(joinToArtist_idArt))
6473             artistObj["art"] = CVariant(CVariant::VariantTypeObject);
6474           if (joinLayout.GetOutput(joinToArtist_thumbnail))
6475             artistObj["thumbnail"] = "";
6476           if (joinLayout.GetOutput(joinToArtist_fanart))
6477             artistObj["fanart"] = "";
6478 
6479           sourceId = -1;
6480           roleId = -1;
6481           genreId = -1;
6482           artId = -1;
6483           genreidlist.clear();
6484           bGenreFoundViaAlbum = false;
6485           sourceidlist.clear();
6486           roleidlist.clear();
6487           bArtDone = false;
6488         }
6489         if (m_pDS->eof())
6490           continue;  // Having saved the last artist stop
6491 
6492         // New artist
6493         artistId = record->at(0).get_asInt();
6494         bHaveArtist = true;
6495         artistObj["artistid"] = artistId;
6496         artistObj["label"] = record->at(1).get_asString();
6497         artistObj["artist"] = record->at(1).get_asString(); // Always have "artist"
6498         bIsAlbumArtist = true;  //Album artist by default
6499         if (joinLayout.GetOutput(joinToArtist_isalbumartist))
6500         {
6501           // Not album artist when fetching song artists too and first row for artist isSong=true
6502           if (bJoinSongArtist)
6503             bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
6504           artistObj["isalbumartist"] = bIsAlbumArtist;
6505         }
6506         for (size_t i = 0; i < dbfieldindex.size(); i++)
6507           if (dbfieldindex[i] > -1)
6508           {
6509             if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer")
6510               artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
6511             else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float")
6512               artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asFloat();
6513             else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array")
6514               artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
6515               StringUtils::Split(record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
6516             else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean")
6517               artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
6518             else
6519               artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
6520           }
6521       }
6522       if (bJoinAlbumArtist)
6523       {
6524         bool bAlbumArtistRow(true);
6525         int idRoleRow = -1;
6526         if (bJoinSongArtist)
6527         {
6528           bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
6529           if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 &&
6530             !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull())
6531           {
6532             idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt();
6533           }
6534         }
6535 
6536         // Sources - gathered via both album_artist and song_artist (with role = 1)
6537         if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6538         {
6539           if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 &&
6540             !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() &&
6541             sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) ||
6542             (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 &&
6543               !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() &&
6544               sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt()))
6545           {
6546             bArtDone = bArtDone || (sourceId > 0);  // Not first source, skip art repeats
6547             bool found(false);
6548             sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt();
6549             if (!bAlbumArtistRow)
6550             {
6551               // Skip other roles (when fetching them)
6552               if (idRoleRow > 1)
6553               {
6554                 found = true;
6555               }
6556               else
6557               {
6558                 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt();
6559                 // Song artist row may repeat sources found via album artist
6560                 // Already have that source?
6561                 for (const auto& i : sourceidlist)
6562                   if (i == sourceId)
6563                   {
6564                     found = true;
6565                     break;
6566                   }
6567               }
6568             }
6569             if (!found)
6570             {
6571               sourceidlist.emplace_back(sourceId);
6572               artistObj["sourceid"].append(sourceId);
6573             }
6574           }
6575         }
6576         // Songgenres - via album artist takes precedence
6577         /*
6578         Sources and genre are related via album, and so the dataset only contains source
6579         and genre pairs that exist, rather than all the genres being repeated for every
6580         source. We can not only look at genres for the first source, and genre can be
6581         found out of order.
6582         Also song artist row may repeat genres found via album artist
6583         */
6584         if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
6585         {
6586           std::string strGenre;
6587           bool newgenre(false);
6588           if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 &&
6589             !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() &&
6590             genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt())
6591           {
6592             bArtDone = bArtDone || (genreId > 0);  // Not first genre, skip art repeats
6593             newgenre = true;
6594             genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt();
6595             strGenre = record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString();
6596           }
6597           else if (!bAlbumArtistRow && !bGenreFoundViaAlbum &&
6598             joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 &&
6599             !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() &&
6600             genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt())
6601           {
6602             bArtDone = bArtDone || (genreId > 0);  // Not first genre, skip art repeats
6603             newgenre = idRoleRow <= 1; // Skip other roles (when fetching them)
6604             genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt();
6605             strGenre = record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString();
6606           }
6607           bool found(false);
6608           if (newgenre)
6609           {
6610             // Already have that genre?
6611             for (const auto& i : genreidlist)
6612               if (i == genreId)
6613               {
6614                 found = true;
6615                 break;
6616               }
6617             if (!found)
6618             {
6619               bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow;
6620               genreidlist.emplace_back(genreId);
6621               CVariant genreObj;
6622               genreObj["genreid"] = genreId;
6623               genreObj["title"] = strGenre;
6624               artistObj["songgenres"].append(genreObj);
6625             }
6626           }
6627         }
6628         // Roles - gathered via song_artist roleid rows
6629         if (joinLayout.GetFetch(joinToArtist_idRole))
6630         {
6631           if (!bAlbumArtistRow && roleId != idRoleRow)
6632           {
6633             bArtDone = bArtDone || (roleId > 0);  // Not first role, skip art repeats
6634             roleId = idRoleRow;
6635             if (joinLayout.GetOutput(joinToArtist_strRole))
6636             {
6637               // Already have that role?
6638               bool found(false);
6639               for (const auto& i : roleidlist)
6640                 if (i == roleId)
6641                 {
6642                   found = true;
6643                   break;
6644                 }
6645               if (!found)
6646               {
6647                 roleidlist.emplace_back(roleId);
6648                 CVariant roleObj;
6649                 roleObj["roleid"] = roleId;
6650                 roleObj["role"] = record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString();
6651                 artistObj["roles"].append(roleObj);
6652               }
6653             }
6654           }
6655         }
6656       }
6657       // Art
6658       if (bJoinArt && !bArtDone &&
6659         !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() &&
6660         record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 &&
6661         artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt())
6662       {
6663         artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt();
6664         if (joinLayout.GetOutput(joinToArtist_idArt))
6665         {
6666           artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] =
6667             CTextureUtils::GetWrappedImageURL(record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
6668         }
6669         if (joinLayout.GetOutput(joinToArtist_thumbnail) &&
6670           record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb")
6671         {
6672           artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL(record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
6673         }
6674         if (joinLayout.GetOutput(joinToArtist_fanart) &&
6675           record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart")
6676         {
6677           artistObj["fanart"] = CTextureUtils::GetWrappedImageURL(record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
6678         }
6679       }
6680 
6681       m_pDS->next();
6682     }
6683     m_pDS->close(); // cleanup recordset data
6684 
6685     // Ensure random order of output when results set is sorted to process multi-value joins
6686     if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
6687       KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array());
6688 
6689     return true;
6690   }
6691   catch (...)
6692   {
6693     m_pDS->close();
6694     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
6695   }
6696   return false;
6697 }
6698 
6699 // clang-format off
6700 static const translateJSONField JSONtoDBAlbum[] = {
6701   // albumview (inc scalar subquery fields use in filter rules)
6702   { "title",                     "string", true,  "strAlbum",               "" },  // Label field at top
6703   { "description",               "string", true,  "strReview",              "" },
6704   { "genre",                      "array", true,  "strGenres",              "" },
6705   { "theme",                      "array", true,  "strThemes",              "" },
6706   { "mood",                       "array", true,  "strMoods",               "" },
6707   { "style",                      "array", true,  "strStyles",              "" },
6708   { "type",                      "string", true,  "strType",                "" },
6709   { "albumlabel",                "string", true,  "strLabel",               "" },
6710   { "rating",                     "float", true,  "fRating",                "" },
6711   { "votes",                    "integer", true,  "iVotes",                 "" },
6712   { "userrating",              "unsigned", true,  "iUserrating",            "" },
6713   { "isboxset",                 "boolean", true,  "bBoxedSet",              "" },
6714   { "musicbrainzalbumid",        "string", true,  "strMusicBrainzAlbumID",  "" },
6715   { "displayartist",             "string", true,  "strArtists",             "" }, //strArtistDisp in album table
6716   { "compilation",              "boolean", true,  "bCompilation",           "" },
6717   { "releasetype",               "string", true,  "strReleaseType",         "" },
6718   { "totaldiscs",               "integer", true,  "iDiscTotal",             "" },
6719   { "sortartist",                "string", true,  "strArtistSort",          "" },
6720   { "musicbrainzreleasegroupid", "string", true,  "strReleaseGroupMBID",    "" },
6721   { "playcount",                "integer", true,  "iTimesPlayed",           "" },  // Scalar subquery in view
6722   { "dateadded",                 "string", true,  "dateAdded",              "" },
6723   { "datenew",                   "string", true,  "dateNew",                "" },
6724   { "datemodified",              "string", true,  "dateModified",           "" },
6725   { "lastplayed",                "string", true,  "lastPlayed",             "" },  // Scalar subquery in view
6726   { "originaldate",              "string", true,  "strOrigReleaseDate",     "" },
6727   { "releasedate",               "string", true,  "strReleaseDate",         "" },
6728   { "albumstatus",               "string", true,  "strReleaseStatus",       "" },
6729   { "albumduration",             "integer", true,  "iAlbumDuration",        "" },
6730   // Scalar subquery fields
6731   { "year",                     "integer", true,  "iYear",                  "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
6732   { "sourceid",                  "string", true,  "sourceid",               "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
6733   { "songgenres",                 "array", true,  "songgenres",             "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
6734       "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
6735   // Single value JOIN fields
6736   { "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
6737   // JOIN fields (multivalue), same order as _JoinToAlbumFields
6738   { "artistid",                   "array", false, "idArtist",               "album_artist.idArtist AS idArtist" },
6739   { "artist",                     "array", false, "strArtist",              "artist.strArtist AS strArtist" },
6740   { "musicbrainzalbumartistid",   "array", false, "strArtistMBID",          "artist.strMusicBrainzArtistID AS strArtistMBID" },
6741   /*
6742    Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
6743    and separate queries to allow for fallback strategy.
6744 
6745    Using albmview, rather than album table, as view has scalar subqueries for
6746    playcount and lastplayed already defined. Needed as MySQL does
6747    not support use of scalar subquery field alias names in where clauses (they
6748    have to be repeated) and these fields can be used by filter rules.
6749    Using this view is no slower than the album table as these scalar fields are
6750    only calculated (slowing query) when field is in field list.
6751   */
6752 };
6753 // clang-format on
6754 
6755 static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField);
6756 
GetAlbumsByWhereJSON(const std::set<std::string> & fields,const std::string & baseDir,CVariant & result,int & total,const SortDescription & sortDescription)6757 bool CMusicDatabase::GetAlbumsByWhereJSON(const std::set<std::string>& fields, const std::string &baseDir,
6758   CVariant& result, int& total, const SortDescription &sortDescription /* = SortDescription() */)
6759 {
6760 
6761   if (nullptr == m_pDB)
6762     return false;
6763   if (nullptr == m_pDS)
6764     return false;
6765 
6766   try
6767   {
6768     total = -1;
6769 
6770     size_t resultcount = 0;
6771     Filter extFilter;
6772     CMusicDbUrl musicUrl;
6773     // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
6774     // passed in at the start of the function
6775     SortDescription sorting = sortDescription;
6776     if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6777       return false;
6778 
6779     // Replace view names in filter with table names
6780     StringUtils::Replace(extFilter.where, "artistview", "artist");
6781 
6782     std::string strSQLExtra;
6783     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6784       return false;
6785 
6786     // Count number of albums that satisfy selection criteria
6787     // (includes xsp limits from filter, but not sort limits)
6788     // Use albumview as filter rules in where clause may use scalar query fields
6789     total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
6790     resultcount = static_cast<size_t>(total);
6791 
6792     // Get order by (and any scalar query artist fields
6793     int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter);
6794 
6795     // Grab calculated artist/title sort fields that may have been added to filter
6796     // These need to be added to the end of the album table field list
6797     std::string calcsortfieldsSQL = extFilter.fields;
6798     extFilter.fields.clear();
6799 
6800     std::string strSQL;
6801 
6802     // Setup fields to query, and album field number mapping
6803     // Find idArtist in JSONtoDBAlbum, offset of first join field
6804     int index_idArtist = -1;
6805     for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++)
6806     {
6807       if (JSONtoDBAlbum[i].fieldDB == "idArtist")
6808       {
6809         index_idArtist = i;
6810         break;
6811       }
6812     }
6813     Filter joinFilter;
6814     DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount));
6815     extFilter.AppendField("albumview.idAlbum");  // ID "albumid" in JSON
6816     std::vector<int> dbfieldindex;
6817     // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
6818     extFilter.AppendField(JSONtoDBAlbum[0].fieldDB);
6819     if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end())
6820       dbfieldindex.emplace_back(0); // Output "title"
6821     else
6822       dbfieldindex.emplace_back(-1); // fetch but not outout
6823 
6824     // Check each optional album db field that could be retrieved (not label)
6825     for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++)
6826     {
6827       bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end();
6828       if (JSONtoDBAlbum[i].bSimple)
6829       {
6830         // Check for non-join fields in order too.
6831         // Query these in inline view (but not output) so can ref in outer order
6832         bool foundOrderby(false);
6833         if (!foundJSON)
6834           foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos;
6835         if (foundOrderby || foundJSON)
6836         {
6837           // Store indexes of requested album table and scalar subquery fields
6838           // to be output, and -1 when not output to JSON
6839           if (!foundJSON)
6840             dbfieldindex.emplace_back(-1);
6841           else
6842             dbfieldindex.emplace_back(i);
6843           if (!JSONtoDBAlbum[i].SQL.empty())
6844             // Field from scaler subquery
6845             extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL));
6846           else
6847             // Field from album table
6848             extFilter.AppendField(JSONtoDBAlbum[i].fieldDB);
6849         }
6850       }
6851       else if (foundJSON)
6852         // Field from join found in JSON request
6853         joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true);
6854     }
6855 
6856     // Append calculated artist/title sort fields that may have been added to filter
6857     // Field used only for ORDER BY, not output to JSON
6858     extFilter.AppendField(calcsortfieldsSQL);
6859     for (int i = 0; i < iAddedFields; i++)
6860       dbfieldindex.emplace_back(-1); // columns in dataset
6861 
6862     // JOIN art tables if needed (fields output and/or in sort)
6863     if (extFilter.fields.find("art.") != std::string::npos)
6864     { // Left join as not all albums have art, but only have one thumb at most
6865       extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
6866         "AND art.media_type = 'album' AND art.type = 'thumb'");
6867     }
6868 
6869     // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6870     strSQLExtra = "";
6871     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6872       return false;
6873 
6874     // Add any LIMIT clause to strSQLExtra
6875     if (extFilter.limit.empty() &&
6876       (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
6877     {
6878       strSQLExtra += DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
6879       resultcount = std::min(
6880           DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
6881           resultcount);
6882     }
6883 
6884     // Setup multivalue JOINs, GROUP BY and ORDER BY
6885     bool bJoinAlbumArtist(false);
6886     if (sortDescription.sortBy != SortByRandom)
6887     {
6888       // Repeat inline view order (that always includes idAlbum) on join query
6889       std::string order = extFilter.order;
6890       StringUtils::Replace(order, "albumview.", "a1.");
6891       joinFilter.AppendOrder(order);
6892     }
6893     else
6894       joinFilter.AppendOrder("a1.idAlbum");
6895     joinFilter.AppendGroup("a1.idAlbum");
6896     // Album artists
6897     if (joinLayout.GetFetch(joinToAlbum_idArtist) ||
6898         joinLayout.GetFetch(joinToAlbum_strArtist) ||
6899         joinLayout.GetFetch(joinToAlbum_strArtistMBID))
6900     { // All albums have at least one artist so inner join sufficient
6901       bJoinAlbumArtist = true;
6902       joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
6903       joinFilter.AppendGroup("album_artist.idArtist");
6904       joinFilter.AppendOrder("album_artist.iOrder");
6905       // Ensure idArtist is queried
6906       if (!joinLayout.GetFetch(joinToAlbum_idArtist))
6907         joinLayout.SetField(joinToAlbum_idArtist, JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL);
6908     }
6909     // artist table needed for strArtist or MBID
6910     // (album_artist.strArtist can be an alias or spelling variation)
6911     if (joinLayout.GetFetch(joinToAlbum_strArtist) || joinLayout.GetFetch(joinToAlbum_strArtistMBID))
6912       joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
6913 
6914     // Build JOIN part of query (if we have one)
6915     std::string strSQLJoin;
6916     if (joinLayout.HasFilterFields())
6917       if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6918         return false;
6919 
6920     // Adjust where in the results record the join fields are allowing for the
6921     // inline view fields (Quicker than finding field by name every time)
6922     // idAlbum + other album fields
6923     joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
6924 
6925     // Build full query
6926     // When have multiple value joins (artists or song genres) use inline view
6927     // SELECT a1.*, <join fields> FROM
6928     //   (SELECT <album fields> FROM albumview <where> + <order by> +  <limits> ) AS a1
6929     //   <joins> <group by> <order by> <joins order by>
6930     // Don't use prepareSQL - confuses  releasetype = 'album' filter and group_concat separator
6931 
6932     strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra;
6933     if (joinLayout.HasFilterFields())
6934     {
6935       strSQL = "(" + strSQL + ") AS a1 ";
6936       strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
6937     }
6938 
6939     // Modify query to use correct year field
6940     if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
6941       CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
6942       StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate");
6943     else
6944       StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate");
6945 
6946     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
6947     // run query
6948     unsigned int time = XbmcThreads::SystemClockMillis();
6949     if (!m_pDS->query(strSQL))
6950       return false;
6951     CLog::Log(LOGDEBUG, "%s - query took %i ms",
6952       __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
6953 
6954     int iRowsFound = m_pDS->num_rows();
6955     if (iRowsFound <= 0)
6956     {
6957       m_pDS->close();
6958       return true;
6959     }
6960 
6961     // Get albums from returned rows. Joins means there can be many rows per album
6962     int albumId = -1;
6963     int artistId = -1;
6964     CVariant albumObj;
6965     result["albums"].reserve(resultcount);
6966     while (!m_pDS->eof() || !albumObj.empty())
6967     {
6968       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
6969 
6970       if (m_pDS->eof() || albumId != record->at(0).get_asInt())
6971       {
6972         // Store previous or last album
6973         if (!albumObj.empty())
6974         {
6975           // Split sources string into int array
6976           if (albumObj.isMember("sourceid"))
6977           {
6978             std::vector<std::string> sources = StringUtils::Split(albumObj["sourceid"].asString(), ";");
6979             albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
6980             for (size_t i = 0; i < sources.size(); i++)
6981               albumObj["sourceid"].append(atoi(sources[i].c_str()));
6982           }
6983           result["albums"].append(albumObj);
6984           albumObj.clear();
6985           artistId = -1;
6986         }
6987         if (m_pDS->eof())
6988           continue; // Having saved last album stop
6989 
6990         // New album
6991         albumId = record->at(0).get_asInt();
6992         albumObj["albumid"] = albumId;
6993         albumObj["label"] = record->at(1).get_asString();
6994         for (size_t i = 0; i < dbfieldindex.size(); i++)
6995           if (dbfieldindex[i] > -1)
6996           {
6997             if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres")
6998             {
6999               // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
7000               std::vector<std::string> values =
7001                   StringUtils::Split(record->at(1 + i).get_asString(), ",");
7002               if (values.size() % 2 == 0) // Must contain an even number of entries
7003               {
7004                 for (size_t i = 0; i + 1 < values.size(); i += 2)
7005                 {
7006                   int idGenre = atoi(values[i].c_str());
7007                   if (idGenre > 0)
7008                   {
7009                     CVariant genreObj;
7010                     genreObj["genreid"] = idGenre;
7011                     genreObj["title"] = values[i + 1];
7012                     albumObj["songgenres"].append(genreObj);
7013                   }
7014                 }
7015               }
7016               // Ensure albums with null songgenres get empty array
7017               if (!albumObj.isMember("songgenres"))
7018                 albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
7019             }
7020             else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer")
7021               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7022             else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned")
7023               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asInt(), 0);
7024             else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float")
7025               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asFloat(), 0.f);
7026             else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array")
7027               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split(record->at(1 + i).get_asString(),
7028                 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
7029             else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean")
7030               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7031             else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image")
7032             {
7033               std::string url = record->at(1 + i).get_asString();
7034               if (!url.empty())
7035                 url = CTextureUtils::GetWrappedImageURL(url);
7036               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url;
7037             }
7038             else
7039               albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
7040           }
7041       }
7042       if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1)
7043       {
7044         if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt())
7045         {
7046           artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt();
7047           if (joinLayout.GetOutput(joinToAlbum_idArtist))
7048             albumObj["artistid"].append(artistId);
7049           if (artistId == BLANKARTIST_ID)
7050           {
7051             if (joinLayout.GetOutput(joinToAlbum_strArtist))
7052               albumObj["artist"].append(StringUtils::Empty);
7053             if (joinLayout.GetOutput(joinToAlbum_strArtistMBID))
7054               albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
7055           }
7056           else
7057           {
7058             if (joinLayout.GetOutput(joinToAlbum_strArtist) && joinLayout.GetRecNo(joinToAlbum_strArtist) > -1)
7059               albumObj["artist"].append(record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString());
7060             if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) && joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1)
7061               albumObj["musicbrainzalbumartistid"].append(record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString());
7062           }
7063         }
7064       }
7065       m_pDS->next();
7066     }
7067     m_pDS->close(); // cleanup recordset data
7068 
7069     // Ensure random order of output when results set is sorted to process multi-value joins
7070     if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7071       KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array());
7072 
7073     return true;
7074   }
7075   catch (...)
7076   {
7077     m_pDS->close();
7078     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
7079   }
7080   return false;
7081 }
7082 
7083 // clang-format off
7084 static const translateJSONField JSONtoDBSong[] = {
7085   // table and single value join fields
7086   { "title",                     "string", true,  "strTitle",               "" }, // Label field at top
7087   { "albumid",                  "integer", true,  "song.idAlbum",           "" },
7088   { "",                                "", true,  "song.iTrack",            "" },
7089   { "displayartist",             "string", true,  "song.strArtistDisp",     "" },
7090   { "sortartist",                "string", true,  "song.strArtistSort",     "" },
7091   { "genre",                      "array", true,  "song.strGenres",         "" },
7092   { "duration",                 "integer", true,  "iDuration",              "" },
7093   { "comment",                   "string", true,  "comment",                "" },
7094   { "",                          "string", true,  "strFileName",            "" },
7095   { "musicbrainztrackid",        "string", true,  "strMusicBrainzTrackID",  "" },
7096   { "playcount",                "integer", true,  "iTimesPlayed",           "" },
7097   { "lastplayed",                "string", true,  "lastPlayed",             "" },
7098   { "rating",                     "float", true,  "rating",                 "" },
7099   { "votes",                    "integer", true,  "votes",                  "" },
7100   { "userrating",              "unsigned", true,  "song.userrating",        "" },
7101   { "mood",                       "array", true,  "mood",                   "" },
7102   { "dateadded",                 "string", true,  "song.dateAdded",         "" },
7103   { "datenew",                   "string", true,  "song.dateNew",           "" },
7104   { "datemodified",              "string", true,  "song.dateModified",      "" },
7105   { "file",                      "string", true,  "strPathFile",            "CONCAT(path.strPath, strFilename) AS strPathFile" },
7106   { "",                          "string", true,  "strPath",                "path.strPath AS strPath" },
7107   { "album",                     "string", true,  "strAlbum",               "album.strAlbum AS strAlbum" },
7108   { "albumreleasetype",          "string", true,  "strAlbumReleaseType",    "album.strReleaseType AS strAlbumReleaseType" },
7109   { "musicbrainzalbumid",        "string", true,  "strMusicBrainzAlbumID",  "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
7110   { "disctitle",                 "string", true,  "song.strDiscSubtitle",   "" },
7111   { "bpm",                      "integer", true,  "iBPM",                   "" },
7112   { "originaldate",             "string" , true,  "song.strOrigReleaseDate","" },
7113   { "releasedate",              "string" , true,  "song.strReleaseDate",    "" },
7114   { "bitrate",                  "integer", true,  "iBitRate",               "" },
7115   { "samplerate",               "integer", true,  "iSampleRate",            "" },
7116   { "channels",                 "integer", true,  "iChannels",              "" },
7117 
7118   // JOIN fields (multivalue), same order as _JoinToSongFields
7119   { "albumartistid",              "array", false, "idAlbumArtist",          "album_artist.idArtist AS idAlbumArtist" },
7120   { "albumartist",                "array", false, "strAlbumArtist",         "albumartist.strArtist AS strAlbumArtist" },
7121   { "musicbrainzalbumartistid",   "array", false, "strAlbumArtistMBID",     "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
7122   { "",                                "", false, "iOrderAlbumArtist",      "album_artist.iOrder AS iOrderAlbumArtist" },
7123   { "artistid",                   "array", false, "idArtist",               "song_artist.idArtist AS idArtist" },
7124   { "artist",                     "array", false, "strArtist",              "songartist.strArtist AS strArtist" },
7125   { "musicbrainzartistid",        "array", false, "strArtistMBID",          "songartist.strMusicBrainzArtistID AS strArtistMBID" },
7126   { "",                                "", false, "iOrderArtist",           "song_artist.iOrder AS iOrderArtist" },
7127   { "",                                "", false, "idRole",                 "song_artist.idRole" },
7128   { "",                                "", false, "strRole",                "role.strRole" },
7129   { "",                                "", false, "iOrderRole",             "song_artist.iOrder AS iOrderRole" },
7130   { "genreid",                    "array", false, "idGenre",                "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
7131   { "",                                "", false, "iOrderGenre",            "song_genre.idOrder AS iOrderGenre" },
7132 
7133   { "contributors",               "array", false, "Role_All",               "song_artist.idRole AS Role_All" },
7134   { "displaycomposer",           "string", false, "Role_Composer",          "song_artist.idRole AS Role_Composer" },
7135   { "displayconductor",          "string", false, "Role_Conductor",         "song_artist.idRole AS Role_Conductor" },
7136   { "displayorchestra",          "string", false, "Role_Orchestra",         "song_artist.idRole AS Role_Orchestra" },
7137   { "displaylyricist",           "string", false, "Role_Lyricist",          "song_artist.idRole AS Role_Lyricist" },
7138 
7139   // Scalar subquery fields
7140   { "year",                     "integer", true,  "iYear",                  "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7141   { "track",                    "integer", true,  "track",                  "(iTrack & 0xffff) AS track" },
7142   { "disc",                     "integer", true,  "disc",                   "(iTrack >> 16) AS disc" },
7143   { "sourceid",                  "string", true,  "sourceid",               "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
7144   /*
7145   Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
7146   thumbloader and separate queries to allow for fallback strategy
7147   "lyrics"?? Can be set for an item (by addons) but not held in db so
7148   AudioLibrary.GetSongs() never fills this field despite being in schema
7149 
7150    FROM ( SELECT * FROM song
7151      JOIN album ON album.idAlbum = song.idAlbum
7152      JOIN path ON path.idPath = song.idPath) AS sv
7153    JOIN album_artist ON album_artist.idAlbum = song.idAlbum
7154    JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
7155    JOIN song_artist ON song_artist.idSong = song.idSong
7156    JOIN artist AS artistsong ON artistsong.idArtist  = song_artist.idArtist
7157    JOIN role ON song_artist.idRole = role.idRole
7158    LEFT JOIN song_genre ON song.idSong = song_genre.idSong
7159 
7160   */
7161 };
7162 // clang-format on
7163 
7164 static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField);
7165 
GetSongsByWhereJSON(const std::set<std::string> & fields,const std::string & baseDir,CVariant & result,int & total,const SortDescription & sortDescription)7166 bool CMusicDatabase::GetSongsByWhereJSON(const std::set<std::string>& fields, const std::string &baseDir,
7167   CVariant& result, int& total, const SortDescription &sortDescription /* = SortDescription() */)
7168 {
7169 
7170   if (nullptr == m_pDB)
7171     return false;
7172   if (nullptr == m_pDS)
7173     return false;
7174 
7175   try
7176   {
7177     total = -1;
7178 
7179     size_t resultcount = 0;
7180     Filter extFilter;
7181     CMusicDbUrl musicUrl;
7182     // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7183     // passed into the function
7184     SortDescription sorting = sortDescription;
7185     if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
7186       return false;
7187 
7188     // Replace view names in filter with table names
7189     StringUtils::Replace(extFilter.where, "artistview", "artist");
7190     StringUtils::Replace(extFilter.where, "albumview", "album");
7191     StringUtils::Replace(extFilter.where, "songview.strPath", "strPath");
7192     StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum");
7193     StringUtils::Replace(extFilter.where, "songview", "song");
7194     StringUtils::Replace(extFilter.where, "songartistview", "song_artist");
7195 
7196     // JOIN album and path tables needed by filter rules in where clause
7197     if (extFilter.where.find("album.") != std::string::npos ||
7198       extFilter.where.find("strAlbum") != std::string::npos)
7199     { // All songs have one album so inner join sufficient
7200       extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7201     }
7202     if (extFilter.where.find("strPath") != std::string::npos)
7203     { // All songs have one path so inner join sufficient
7204       extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7205     }
7206 
7207     // Build JOINs and WHERE needed by filter for counting songs
7208     std::string strSQLExtra;
7209     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7210       return false;
7211 
7212     // Count number of songs that satisfy selection criteria
7213     // (includes xsp limits from filter, but not sort limits)
7214     total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS);
7215     resultcount = static_cast<size_t>(total);
7216 
7217     int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter);
7218     // Replace songview field names in order by with song, album path table field names
7219     // Field names in album same as song:
7220     //   idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
7221     StringUtils::Replace(extFilter.order, "songview.strPath", "strPath");
7222     StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum");
7223     StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation");
7224     StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp");
7225     StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp");
7226     StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort");
7227     StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType");
7228     StringUtils::Replace(extFilter.order, "songview", "song");
7229     StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort");
7230     StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp");
7231     StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum");
7232     StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle");
7233 
7234     // Grab calculated artist/title sort fields that may have been added to filter
7235     // These need to be added to the end of the song table field list
7236     std::string calcsortfieldsSQL = extFilter.fields;
7237     extFilter.fields.clear();
7238 
7239     std::string strSQL;
7240 
7241     // Setup fields to query, and song field number mapping
7242     // Find idAlbumArtist in JSONtoDBSong, offset of first join field
7243     int index_idAlbumArtist = -1;
7244     for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++)
7245     {
7246       if (JSONtoDBSong[i].fieldDB == "idAlbumArtist")
7247       {
7248         index_idAlbumArtist = i;
7249         break;
7250       }
7251     }
7252     Filter joinFilter;
7253     DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount));
7254     extFilter.AppendField("song.idSong");  // ID "songid" in JSON
7255     std::vector<int> dbfieldindex;
7256     // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
7257     extFilter.AppendField(JSONtoDBSong[0].fieldDB);
7258     if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end())
7259       dbfieldindex.emplace_back(0); // Output "title"
7260     else
7261       dbfieldindex.emplace_back(-1); // Fetch but not output
7262     std::vector<std::string> rolefieldlist;
7263     std::vector<int> roleidlist;
7264     // Check each optional db field that could be retrieved (not label)
7265     for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++)
7266     {
7267       bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end();
7268       if (JSONtoDBSong[i].bSimple)
7269       {
7270         // Check for non-join fields in order too.
7271         // Query these in inline view (but not output) so can ref in outer order
7272         bool foundOrderby(false);
7273         if (!foundJSON)
7274           foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos;
7275         if (foundOrderby || foundJSON)
7276         {
7277           // Store indexes of requested album table and scalar subquery fields
7278           // to be output, and -1 when not output to JSON
7279           if (!foundJSON)
7280             dbfieldindex.emplace_back(-1);
7281           else
7282             dbfieldindex.emplace_back(i);
7283           if (!JSONtoDBSong[i].SQL.empty())
7284             // Field from scaler subquery
7285             extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL));
7286           else
7287             // Field from song table
7288             extFilter.AppendField(JSONtoDBSong[i].fieldDB);
7289         }
7290       }
7291       else if (foundJSON)
7292       { // Field from join found in JSON request
7293         if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_"))
7294         {
7295           joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true);
7296         }
7297         else
7298         { // "contributors", "displaycomposer" etc.
7299           rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON);
7300         }
7301       }
7302     }
7303     // Append calculated artist/title sort fields that may have been added to filter
7304     // Field used only for ORDER BY, not output to JSON
7305     extFilter.AppendField(calcsortfieldsSQL);
7306     for (int i = 0; i < iAddedFields; i++)
7307       dbfieldindex.emplace_back(-1); // columns in dataset
7308 
7309     // Build matching list of role id for "displaycomposer", "displayconductor",
7310     // "displayorchestra", "displaylyricist"
7311     if (!rolefieldlist.empty())
7312     {
7313       for (const auto& name : rolefieldlist)
7314       {
7315         int idRole = -1;
7316         if (StringUtils::StartsWith(name, "display"))
7317           idRole = GetRoleByName(name.substr(7));
7318         roleidlist.emplace_back(idRole);
7319       }
7320     }
7321 
7322     // JOIN album and path tables needed for field output and/or in sort
7323     // if not already there for filter
7324     if ((extFilter.fields.find("album.") != std::string::npos ||
7325          extFilter.fields.find("strAlbum") != std::string::npos) &&
7326         extFilter.join.find("JOIN album") == std::string::npos)
7327     { // All songs have one album so inner join sufficient
7328       extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7329     }
7330     if (extFilter.fields.find("path.") != std::string::npos &&
7331         extFilter.join.find("JOIN path") == std::string::npos)
7332     { // All songs have one path so inner join sufficient
7333       extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7334     }
7335 
7336     // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7337     strSQLExtra = "";
7338     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7339       return false;
7340 
7341     // Add any LIMIT clause to strSQLExtra
7342     if (extFilter.limit.empty() &&
7343       (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
7344     {
7345       strSQLExtra += DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
7346       resultcount = std::min(
7347         DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
7348         resultcount);
7349     }
7350 
7351     // Setup multivalue JOINs, GROUP BY and ORDER BY
7352     bool bJoinSongArtist(false);
7353     bool bJoinAlbumArtist(false);
7354     bool bJoinRole(false);
7355     if (sortDescription.sortBy != SortByRandom)
7356     {
7357       // Repeat inline view order (that always includes idSong) on join query
7358       std::string order = extFilter.order;
7359       order = extFilter.order;
7360       StringUtils::Replace(order, "album.", "sv.");
7361       StringUtils::Replace(order, "song.", "sv.");
7362       joinFilter.AppendOrder(order);
7363     }
7364     else
7365       joinFilter.AppendOrder("sv.idSong");
7366     joinFilter.AppendGroup("sv.idSong");
7367 
7368     // Album artists
7369     if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) ||
7370         joinLayout.GetFetch(joinToSongs_strAlbumArtist) ||
7371         joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID))
7372     { // All songs have at least one album artist so inner join sufficient
7373       bJoinAlbumArtist = true;
7374       joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
7375       joinFilter.AppendGroup("album_artist.idArtist");
7376       joinFilter.AppendOrder("album_artist.iOrder");
7377       // Ensure idAlbumArtist is queried for processing repeats
7378       if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist))
7379       {
7380         joinLayout.SetField(joinToSongs_idAlbumArtist,
7381           JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL);
7382       }
7383       // Ensure song.IdAlbum is field of the inline view for join
7384       if (fields.find("albumid") == fields.end())
7385       {
7386         extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
7387         dbfieldindex.emplace_back(-1);
7388       }
7389       // artist table needed for strArtist or MBID
7390       // (album_artist.strArtist can be an alias or spelling variation)
7391       if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) || joinLayout.GetFetch(joinToSongs_strAlbumArtist))
7392         joinFilter.AppendJoin("JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
7393     }
7394 
7395     /*
7396      Song artists
7397      JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
7398      "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
7399     */
7400     if (joinLayout.GetFetch(joinToSongs_idArtist) ||
7401         joinLayout.GetFetch(joinToSongs_strArtist) ||
7402         joinLayout.GetFetch(joinToSongs_strArtistMBID) ||
7403         !rolefieldlist.empty())
7404     { // All songs have at least one artist (idRole = 1) so inner join sufficient
7405       bJoinSongArtist = true;
7406       if (rolefieldlist.empty())
7407       { // song artists only, no other roles needed
7408         joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
7409         joinFilter.AppendGroup("song_artist.idArtist");
7410         joinFilter.AppendOrder("song_artist.iOrder");
7411       }
7412       else
7413       {
7414         // Ensure idRole is queried
7415         if (!joinLayout.GetFetch(joinToSongs_idRole))
7416         {
7417           joinLayout.SetField(joinToSongs_idRole,
7418             JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL);
7419         }
7420         // Ensure strArtist is queried
7421         if (!joinLayout.GetFetch(joinToSongs_strArtist))
7422         {
7423           joinLayout.SetField(joinToSongs_strArtist,
7424             JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL);
7425         }
7426         if (fields.find("contributors") != fields.end())
7427         { // all roles
7428           bJoinRole = true;
7429           // Ensure strRole is queried from role table
7430           joinLayout.SetField(joinToSongs_strRole, "role.strRole");
7431           joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
7432           joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
7433           joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
7434           joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
7435         }
7436         else
7437         { // Get just roles for  "displaycomposer", "displayconductor" etc.
7438           std::string where;
7439           for (size_t i = 0; i < roleidlist.size(); i++)
7440           {
7441             int idRole = roleidlist[i];
7442             if (idRole <= 1)
7443               continue;
7444             if (where.empty())
7445               // Always get song artists too (role = 1) so can do inner join
7446               where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole);
7447             else
7448               where += PrepareSQL(" OR song_artist.idRole = %i", idRole);
7449           }
7450           where = " (" + where + ")";
7451           joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where);
7452           joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
7453           joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
7454         }
7455       }
7456       // Ensure idArtist is queried for processing repeats
7457       if (!joinLayout.GetFetch(joinToSongs_idArtist))
7458       {
7459         joinLayout.SetField(joinToSongs_idArtist,
7460           JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL);
7461       }
7462       // artist table needed for strArtist or MBID
7463       // (song_artist.strArtist can be an alias or spelling variation)
7464       if (joinLayout.GetFetch(joinToSongs_strArtistMBID) || joinLayout.GetFetch(joinToSongs_strArtist))
7465         joinFilter.AppendJoin("JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
7466     }
7467 
7468     // Genre ids
7469     if (joinLayout.GetFetch(joinToSongs_idGenre))
7470     { // song genre ids (strGenre demormalised in song table)
7471       // Left join as songs may not have genre
7472       joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
7473       joinFilter.AppendGroup("song_genre.idGenre");
7474       joinFilter.AppendOrder("song_genre.iOrder");
7475     }
7476 
7477     // Build JOIN part of query (if we have one)
7478     std::string strSQLJoin;
7479     if (joinLayout.HasFilterFields())
7480       if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
7481         return false;
7482 
7483     // Adjust where in the results record the join fields are allowing for the
7484     // inline view fields (Quicker than finding field by name every time)
7485     // idSong + other song fields
7486     joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
7487 
7488     // Build full query
7489     // When have multiple value joins use inline view
7490     // SELECT sv.*, <join fields> FROM
7491     //   (SELECT <song fields> FROM song <JOIN album> <where> + <order by> +  <limits> ) AS sv
7492     //   <joins> <group by>
7493     //   <order by> + <joins order by>
7494     // Don't use prepareSQL - confuses  releasetype = 'album' filter and group_concat separator
7495     strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra;
7496     if (joinLayout.HasFilterFields())
7497     {
7498       strSQL = "("+ strSQL + ") AS sv ";
7499       strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
7500     }
7501 
7502     // Modify query to use correct year field
7503     if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
7504       CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
7505       StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate");
7506     else
7507       StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate");
7508 
7509     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
7510 
7511     // Run query
7512     unsigned int time = XbmcThreads::SystemClockMillis();
7513     if (!m_pDS->query(strSQL))
7514       return false;
7515     CLog::Log(LOGDEBUG, "%s - query took %i ms",
7516       __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
7517 
7518     int iRowsFound = m_pDS->num_rows();
7519     if (iRowsFound <= 0)
7520     {
7521       m_pDS->close();
7522       return true;
7523     }
7524 
7525     // Get song from returned rows. Joins mean there can be many rows per song
7526     int songId = -1;
7527     int albumartistId = -1;
7528     int artistId = -1;
7529     int roleId = -1;
7530     bool bSongGenreDone(false);
7531     bool bSongArtistDone(false);
7532     bool bHaveSong(false);
7533     CVariant songObj;
7534     result["songs"].reserve(resultcount);
7535     while (!m_pDS->eof() || bHaveSong)
7536     {
7537       const dbiplus::sql_record* const record = m_pDS->get_sql_record();
7538 
7539       if (m_pDS->eof() || songId != record->at(0).get_asInt())
7540       {
7541         // Store previous or last song
7542         if (bHaveSong)
7543         {
7544           // Check empty role fields get returned, and format
7545           if (!rolefieldlist.empty())
7546           {
7547             for (const auto& displayXXX : rolefieldlist)
7548             {
7549               if (!StringUtils::StartsWith(displayXXX, "display"))
7550               {
7551                 // "contributors"
7552                 if (!songObj.isMember(displayXXX))
7553                   songObj[displayXXX] = CVariant(CVariant::VariantTypeArray);
7554               }
7555               else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray())
7556               {
7557                 // Convert "displaycomposer", "displayconductor", "displayorchestra",
7558                 // and "displaylyricist" arrays into strings
7559                 std::vector<std::string> names;
7560                 for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array();
7561                      field != songObj[displayXXX].end_array(); field++)
7562                   names.emplace_back(field->asString());
7563 
7564                 std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent()
7565                                                                 ->GetAdvancedSettings()
7566                                                                 ->m_musicItemSeparator);
7567                 songObj[displayXXX] = role;
7568               }
7569               else
7570                 songObj[displayXXX] = "";
7571             }
7572           }
7573           result["songs"].append(songObj);
7574           bHaveSong = false;
7575           songObj.clear();
7576         }
7577         if (songObj.empty())
7578         {
7579           // Initialise fields, ensure those with possible null values are set to correct empty variant type
7580           if (joinLayout.GetOutput(joinToSongs_idGenre))
7581             songObj["genreid"] = CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array
7582 
7583           albumartistId = -1;
7584           artistId = -1;
7585           roleId = -1;
7586           bSongGenreDone = false;
7587           bSongArtistDone = false;
7588         }
7589         if (m_pDS->eof())
7590           continue;  // Having saved the last song stop
7591 
7592         // New song
7593         songId = record->at(0).get_asInt();
7594         bHaveSong = true;
7595         songObj["songid"] = songId;
7596         songObj["label"] = record->at(1).get_asString();
7597         for (size_t i = 0; i < dbfieldindex.size(); i++)
7598           if (dbfieldindex[i] > -1)
7599           {
7600             if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer")
7601               songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7602             else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned")
7603               songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asInt(), 0);
7604             else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float")
7605               songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asFloat(), 0.f);
7606             else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array")
7607               songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split(record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
7608             else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean")
7609               songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7610             else
7611               songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
7612           }
7613 
7614         // Split sources string into int array
7615         if (songObj.isMember("sourceid"))
7616         {
7617           std::vector<std::string> sources = StringUtils::Split(songObj["sourceid"].asString(), ";");
7618           songObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
7619           for (size_t i = 0; i < sources.size(); i++)
7620             songObj["sourceid"].append(atoi(sources[i].c_str()));
7621         }
7622       }
7623 
7624       if (bJoinAlbumArtist)
7625       {
7626         if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt())
7627         {
7628           bSongGenreDone = bSongGenreDone || (albumartistId > 0);  // Not first album artist, skip genre
7629           bSongArtistDone = bSongArtistDone || (albumartistId > 0);  // Not first album artist, skip song artists
7630           albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt();
7631           if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
7632             songObj["albumartistid"].append(albumartistId);
7633           if (albumartistId == BLANKARTIST_ID)
7634           {
7635             if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
7636               songObj["albumartist"].append(StringUtils::Empty);
7637             if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
7638               songObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
7639           }
7640           else
7641           {
7642             if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
7643               songObj["albumartistid"].append(albumartistId);
7644             if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
7645               songObj["albumartist"].append(record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString());
7646             if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
7647               songObj["musicbrainzalbumartistid"].append(record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString());
7648           }
7649         }
7650       }
7651       if (bJoinSongArtist && !bSongArtistDone)
7652       {
7653         if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt())
7654         {
7655           bSongGenreDone = bSongGenreDone || (artistId > 0);  // Not first artist, skip genre
7656           roleId = -1; // Allow for many artists same role
7657           artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
7658           if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 ||
7659               record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1)
7660           {
7661             if (joinLayout.GetOutput(joinToSongs_idArtist))
7662               songObj["artistid"].append(artistId);
7663             if (artistId == BLANKARTIST_ID)
7664             {
7665               if (joinLayout.GetOutput(joinToSongs_strArtist))
7666                 songObj["artist"].append(StringUtils::Empty);
7667               if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
7668                 songObj["musicbrainzartistid"].append(StringUtils::Empty);
7669             }
7670             else
7671             {
7672               if (joinLayout.GetOutput(joinToSongs_strArtist))
7673                 songObj["artist"].append(record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
7674               if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
7675                 songObj["musicbrainzartistid"].append(record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString());
7676             }
7677           }
7678         }
7679         if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 &&
7680             roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt())
7681         {
7682           bSongGenreDone = bSongGenreDone || (roleId > 0);  // Not first role, skip genre
7683           roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt();
7684           if (roleId > 1)
7685           {
7686             if (bJoinRole)
7687             {  //Contributors
7688                CVariant contributor;
7689                contributor["name"] = record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString();
7690                contributor["role"] = record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString();
7691                contributor["roleid"] = roleId;
7692                contributor["artistid"] = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
7693                songObj["contributors"].append(contributor);
7694             }
7695             // "displaycomposer", "displayconductor" etc.
7696             for (size_t i = 0; i < roleidlist.size(); i++)
7697             {
7698               if (roleidlist[i] == roleId)
7699               {
7700                 songObj[rolefieldlist[i]].append(record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
7701                 continue;
7702               }
7703             }
7704           }
7705         }
7706       }
7707       if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 &&
7708           !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull())
7709       {
7710         songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt());
7711       }
7712       m_pDS->next();
7713     }
7714     m_pDS->close(); // cleanup recordset data
7715 
7716     // Ensure random order of output when results set is sorted to process multi-value joins
7717     if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7718       KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array());
7719 
7720     return true;
7721   }
7722   catch (...)
7723   {
7724     m_pDS->close();
7725     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
7726   }
7727   return false;
7728 }
7729 
GetIgnoreArticleSQL(const std::string & strField)7730 std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField)
7731 {
7732   /*
7733   Make SQL clause from ignore article list.
7734   Group tokens the same length together, for example :
7735     WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
7736     THEN SUBSTR(strArtist, 5)
7737     WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
7738     THEN SUBSTR(strArtist, 4)
7739   */
7740   std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
7741   std::string sortclause;
7742   size_t tokenlength = 0;
7743   std::string strWhen;
7744   for (const auto& token : sortTokens)
7745   {
7746     if (token.length() != tokenlength)
7747     {
7748       if (!strWhen.empty())
7749       {
7750         if (!sortclause.empty())
7751            sortclause += " ";
7752         std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
7753         sortclause += "WHEN " + strWhen + strThen;
7754         strWhen.clear();
7755       }
7756       tokenlength = token.length();
7757     }
7758     std::string tokenclause = token;
7759     //Escape any ' or % in the token
7760     StringUtils::Replace(tokenclause, "'", "''");
7761     StringUtils::Replace(tokenclause, "%", "%%");
7762     // Single %, _ and ' so avoid using PrepareSQL
7763     tokenclause = strField + " LIKE '" + tokenclause + "%'";
7764     if (token.find('_') != std::string::npos)
7765       tokenclause += " ESCAPE '_'";
7766     if (!strWhen.empty())
7767        strWhen += " OR ";
7768     strWhen += tokenclause;
7769   }
7770   if (!strWhen.empty())
7771   {
7772     if (!sortclause.empty())
7773        sortclause += " ";
7774     std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
7775     sortclause += "WHEN " + strWhen + strThen;
7776   }
7777   return sortclause;
7778 }
7779 
SortnameBuildSQL(const std::string & strAlias,const SortAttribute & sortAttributes,const std::string & strField,const std::string & strSortField)7780 std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias,
7781                                              const SortAttribute& sortAttributes,
7782                                              const std::string& strField,
7783                                              const std::string& strSortField)
7784 {
7785   /*
7786   Build SQL for sort name scalar subquery from sort attributes and ignore article list.
7787   For example :
7788   CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
7789   WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
7790   WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
7791   ELSE strField
7792   END AS strAlias
7793   */
7794 
7795   std::string sortSQL;
7796   if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName)
7797     sortSQL =
7798         PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str());
7799   if (sortAttributes & SortAttributeIgnoreArticle)
7800   {
7801     if (!sortSQL.empty())
7802       sortSQL += " ";
7803     // Make SQL from ignore article list, grouping tokens the same length together
7804     sortSQL += GetIgnoreArticleSQL(strField);
7805   }
7806   if (!sortSQL.empty())
7807   {
7808     sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc.
7809     sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str());
7810   }
7811 
7812   return sortSQL;
7813 }
7814 
AlphanumericSortSQL(const std::string & strField,const SortOrder & sortOrder)7815 std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField, const SortOrder& sortOrder)
7816 {
7817   /*
7818   Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
7819   and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
7820   Natural number order is not significant for where clause comparison and use of calculated fields
7821   means there is no advantage in defining as column defualt in table create than per query (which
7822   also makes looking at the db with other tools difficult).
7823 
7824   MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
7825   "ascii folding" case insensitive collation. Natural sorting is provided via native functions
7826   stored in the db.
7827   */
7828   std::string DESC;
7829   if (sortOrder == SortOrderDescending)
7830     DESC = " DESC";
7831   std::string strSort;
7832 
7833   if (StringUtils::EqualsNoCase(
7834           CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
7835           "mysql"))
7836     strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str());
7837   else
7838     strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str());
7839   return strSort;
7840 }
7841 
UpdateTables(int version)7842 void CMusicDatabase::UpdateTables(int version)
7843 {
7844   CLog::Log(LOGINFO, "%s - updating tables", __FUNCTION__);
7845   if (version < 34)
7846   {
7847     m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
7848     m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
7849     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");
7850     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");
7851 
7852     m_pDS->exec("DROP TABLE song");
7853     m_pDS->exec("ALTER TABLE song_new RENAME TO song");
7854 
7855     m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
7856   }
7857 
7858   if (version < 36)
7859   {
7860     // translate legacy musicdb:// paths
7861     if (m_pDS->query("SELECT strPath FROM content"))
7862     {
7863       std::vector<std::string> contentPaths;
7864       while (!m_pDS->eof())
7865       {
7866         contentPaths.push_back(m_pDS->fv(0).get_asString());
7867         m_pDS->next();
7868       }
7869       m_pDS->close();
7870 
7871       for (const auto &originalPath : contentPaths)
7872       {
7873         std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath);
7874         m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(), originalPath.c_str()));
7875       }
7876     }
7877   }
7878 
7879   if (version < 39)
7880   {
7881     m_pDS->exec("CREATE TABLE album_new "
7882                 "(idAlbum integer primary key, "
7883                 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
7884                 " strArtists text, strGenres text, "
7885                 " iYear integer, idThumb integer, "
7886                 " bCompilation integer not null default '0', "
7887                 " strMoods text, strStyles text, strThemes text, "
7888                 " strReview text, strImage text, strLabel text, "
7889                 " strType text, "
7890                 " iRating integer, "
7891                 " lastScraped varchar(20) default NULL, "
7892                 " dateAdded varchar (20) default NULL)");
7893     m_pDS->exec("INSERT INTO album_new "
7894                 "(idAlbum, "
7895                 " strAlbum, strMusicBrainzAlbumID, "
7896                 " strArtists, strGenres, "
7897                 " iYear, idThumb, "
7898                 " bCompilation, "
7899                 " strMoods, strStyles, strThemes, "
7900                 " strReview, strImage, strLabel, "
7901                 " strType, "
7902                 " iRating) "
7903                 " SELECT "
7904                 " album.idAlbum, "
7905                 " strAlbum, strMusicBrainzAlbumID, "
7906                 " strArtists, strGenres, "
7907                 " album.iYear, idThumb, "
7908                 " bCompilation, "
7909                 " strMoods, strStyles, strThemes, "
7910                 " strReview, strImage, strLabel, "
7911                 " strType, iRating "
7912                 " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
7913     m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
7914     m_pDS->exec(PrepareSQL("UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)", CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
7915     m_pDS->exec("DROP TABLE album");
7916     m_pDS->exec("DROP TABLE albuminfo");
7917     m_pDS->exec("ALTER TABLE album_new RENAME TO album");
7918   }
7919   if (version < 40)
7920   {
7921     m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
7922                 " strArtist varchar(256), strMusicBrainzArtistID text, "
7923                 " strBorn text, strFormed text, strGenres text, strMoods text, "
7924                 " strStyles text, strInstruments text, strBiography text, "
7925                 " strDied text, strDisbanded text, strYearsActive text, "
7926                 " strImage text, strFanart text, "
7927                 " lastScraped varchar(20) default NULL, "
7928                 " dateAdded varchar (20) default NULL)");
7929     m_pDS->exec("INSERT INTO artist_new "
7930                 "(idArtist, strArtist, strMusicBrainzArtistID, "
7931                 " strBorn, strFormed, strGenres, strMoods, "
7932                 " strStyles , strInstruments , strBiography , "
7933                 " strDied, strDisbanded, strYearsActive, "
7934                 " strImage, strFanart) "
7935                 " SELECT "
7936                 " artist.idArtist, "
7937                 " strArtist, strMusicBrainzArtistID, "
7938                 " strBorn, strFormed, strGenres, strMoods, "
7939                 " strStyles, strInstruments, strBiography, "
7940                 " strDied, strDisbanded, strYearsActive, "
7941                 " strImage, strFanart "
7942                 " FROM artist "
7943                 " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
7944     m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT idArtist FROM artistinfo)", CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
7945     m_pDS->exec("DROP TABLE artist");
7946     m_pDS->exec("DROP TABLE artistinfo");
7947     m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
7948   }
7949   if (version < 42)
7950   {
7951     m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n");
7952     m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n");
7953     // populate these
7954     std::string sql = "select idArtist,strArtist from artist";
7955     m_pDS->query(sql);
7956     while (!m_pDS->eof())
7957     {
7958       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()));
7959       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()));
7960       m_pDS->next();
7961     }
7962   }
7963   if (version < 48)
7964   { // null out columns that are no longer used
7965     m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
7966     m_pDS->exec("UPDATE album SET idThumb=NULL");
7967   }
7968   if (version < 49)
7969   {
7970     m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
7971   }
7972   if (version < 50)
7973   {
7974     // add a new column strReleaseType for albums
7975     m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n");
7976 
7977     // set strReleaseType based on album name
7978     m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''", CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
7979     m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''", CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
7980   }
7981   if (version < 51)
7982   {
7983     m_pDS->exec("ALTER TABLE song ADD mood text\n");
7984   }
7985   if (version < 53)
7986   {
7987     m_pDS->exec("ALTER TABLE song ADD dateAdded text");
7988   }
7989   if (version < 54)
7990   {
7991       //Remove dateAdded from artist table
7992       m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
7993               " strArtist varchar(256), strMusicBrainzArtistID text, "
7994               " strBorn text, strFormed text, strGenres text, strMoods text, "
7995               " strStyles text, strInstruments text, strBiography text, "
7996               " strDied text, strDisbanded text, strYearsActive text, "
7997               " strImage text, strFanart text, "
7998               " lastScraped varchar(20) default NULL)");
7999       m_pDS->exec("INSERT INTO artist_new "
8000           "(idArtist, strArtist, strMusicBrainzArtistID, "
8001           " strBorn, strFormed, strGenres, strMoods, "
8002           " strStyles , strInstruments , strBiography , "
8003           " strDied, strDisbanded, strYearsActive, "
8004           " strImage, strFanart, lastScraped) "
8005           " SELECT "
8006           " idArtist, "
8007           " strArtist, strMusicBrainzArtistID, "
8008           " strBorn, strFormed, strGenres, strMoods, "
8009           " strStyles, strInstruments, strBiography, "
8010           " strDied, strDisbanded, strYearsActive, "
8011           " strImage, strFanart, lastScraped "
8012           " FROM artist");
8013       m_pDS->exec("DROP TABLE artist");
8014       m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8015 
8016       //Remove dateAdded from album table
8017       m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8018               " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8019               " strArtists text, strGenres text, "
8020               " iYear integer, idThumb integer, "
8021               " bCompilation integer not null default '0', "
8022               " strMoods text, strStyles text, strThemes text, "
8023               " strReview text, strImage text, strLabel text, "
8024               " strType text, "
8025               " iRating integer, "
8026               " lastScraped varchar(20) default NULL, "
8027               " strReleaseType text)");
8028       m_pDS->exec("INSERT INTO album_new "
8029           "(idAlbum, "
8030           " strAlbum, strMusicBrainzAlbumID, "
8031           " strArtists, strGenres, "
8032           " iYear, idThumb, "
8033           " bCompilation, "
8034           " strMoods, strStyles, strThemes, "
8035           " strReview, strImage, strLabel, "
8036           " strType, iRating, lastScraped, "
8037           " strReleaseType) "
8038           " SELECT "
8039           " album.idAlbum, "
8040           " strAlbum, strMusicBrainzAlbumID, "
8041           " strArtists, strGenres, "
8042           " iYear, idThumb, "
8043           " bCompilation, "
8044           " strMoods, strStyles, strThemes, "
8045           " strReview, strImage, strLabel, "
8046           " strType, iRating, lastScraped, "
8047           " strReleaseType"
8048           " FROM album");
8049       m_pDS->exec("DROP TABLE album");
8050       m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8051    }
8052    if (version < 55)
8053    {
8054      m_pDS->exec("DROP TABLE karaokedata");
8055    }
8056    if (version < 57)
8057    {
8058      m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
8059      m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
8060      m_pDS->exec("UPDATE song SET userrating = rating * 2");
8061      m_pDS->exec("UPDATE song SET rating = 0");
8062      m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8063        " idAlbum INTEGER, idPath INTEGER, "
8064        " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8065        " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8066        " dwFileNameCRC TEXT, "
8067        " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8068        " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8069        " idThumb INTEGER, "
8070        " lastplayed VARCHAR(20) DEFAULT NULL, "
8071        " rating FLOAT DEFAULT 0, "
8072        " userrating INTEGER DEFAULT 0, "
8073        " comment TEXT, mood TEXT, dateAdded TEXT)");
8074      m_pDS->exec("INSERT INTO song_new "
8075        "(idSong, "
8076        " idAlbum, idPath, "
8077        " strArtists, strGenres, strTitle, "
8078        " iTrack, iDuration, iYear, "
8079        " dwFileNameCRC, "
8080        " strFileName, strMusicBrainzTrackID, "
8081        " iTimesPlayed, iStartOffset, iEndOffset, "
8082        " idThumb, "
8083        " lastplayed,"
8084        " rating, userrating, "
8085        " comment, mood, dateAdded)"
8086        " SELECT "
8087        " idSong, "
8088        " idAlbum, idPath, "
8089        " strArtists, strGenres, strTitle, "
8090        " iTrack, iDuration, iYear, "
8091        " dwFileNameCRC, "
8092        " strFileName, strMusicBrainzTrackID, "
8093        " iTimesPlayed, iStartOffset, iEndOffset, "
8094        " idThumb, "
8095        " lastplayed,"
8096        " rating, "
8097        " userrating, "
8098        " comment, mood, dateAdded"
8099        " FROM song");
8100      m_pDS->exec("DROP TABLE song");
8101      m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8102 
8103      m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
8104      m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
8105      m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8106        " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8107        " strArtists TEXT, strGenres TEXT, "
8108        " iYear INTEGER, idThumb INTEGER, "
8109        " bCompilation INTEGER NOT NULL DEFAULT '0', "
8110        " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8111        " strReview TEXT, strImage TEXT, strLabel TEXT, "
8112        " strType TEXT, "
8113        " fRating FLOAT NOT NULL DEFAULT 0, "
8114        " iUserrating INTEGER NOT NULL DEFAULT 0, "
8115        " lastScraped VARCHAR(20) DEFAULT NULL, "
8116        " strReleaseType TEXT)");
8117      m_pDS->exec("INSERT INTO album_new "
8118        "(idAlbum, "
8119        " strAlbum, strMusicBrainzAlbumID, "
8120        " strArtists, strGenres, "
8121        " iYear, idThumb, "
8122        " bCompilation, "
8123        " strMoods, strStyles, strThemes, "
8124        " strReview, strImage, strLabel, "
8125        " strType, "
8126        " fRating, "
8127        " iUserrating, "
8128        " lastScraped, "
8129        " strReleaseType)"
8130        " SELECT "
8131        " idAlbum, "
8132        " strAlbum, strMusicBrainzAlbumID, "
8133        " strArtists, strGenres, "
8134        " iYear, idThumb, "
8135        " bCompilation, "
8136        " strMoods, strStyles, strThemes, "
8137        " strReview, strImage, strLabel, "
8138        " strType, "
8139        " iRating, "
8140        " iUserrating, "
8141        " lastScraped, "
8142        " strReleaseType"
8143        " FROM album");
8144      m_pDS->exec("DROP TABLE album");
8145      m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8146 
8147      m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
8148      m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
8149    }
8150   if (version < 58)
8151   {
8152      m_pDS->exec("UPDATE album SET fRating = fRating * 2");
8153   }
8154   if (version < 59)
8155   {
8156     m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
8157     m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
8158 
8159     //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
8160     m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, iOrder integer, strArtist text)");
8161     m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
8162       "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
8163     m_pDS->exec("DROP TABLE song_artist");
8164     m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
8165 
8166     //Remove strJoinPhrase, boolFeatured from album_artist table
8167     m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, strArtist text)");
8168     m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
8169       "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
8170     m_pDS->exec("DROP TABLE album_artist");
8171     m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
8172   }
8173   if (version < 60)
8174   {
8175     // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
8176     // do not have an artist tag to ensure all songs in the library have at least one artist.
8177     std::string strSQL;
8178     if (GetArtistExists(BLANKARTIST_ID))
8179     {
8180       // When BLANKARTIST_ID (=1) is already in use, move the record
8181       try
8182       { //No mbid index yet, so can have record for artist twice even with mbid
8183         strSQL = PrepareSQL("INSERT INTO artist SELECT null, "
8184           "strArtist, strMusicBrainzArtistID, "
8185           "strBorn, strFormed, strGenres, strMoods, "
8186           "strStyles, strInstruments, strBiography, "
8187           "strDied, strDisbanded, strYearsActive, "
8188           "strImage, strFanart, lastScraped "
8189           "FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
8190         m_pDS->exec(strSQL);
8191         int idArtist = (int)m_pDS->lastinsertid();
8192         //No triggers, so can delete artist without effecting other tables.
8193         strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
8194         m_pDS->exec(strSQL);
8195 
8196         // Update related tables with the new artist ID
8197         // Indices have been dropped making transactions very slow, so create appropriate temp indices
8198         m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
8199         m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
8200         m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
8201         m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
8202         strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist, BLANKARTIST_ID);
8203         m_pDS->exec(strSQL);
8204         strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist, BLANKARTIST_ID);
8205         m_pDS->exec(strSQL);
8206         strSQL = PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'", idArtist, BLANKARTIST_ID);
8207         m_pDS->exec(strSQL);
8208         strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist, BLANKARTIST_ID);
8209         m_pDS->exec(strSQL);
8210         // Drop temp indices
8211         m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist");
8212         m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
8213         m_pDS->exec("DROP INDEX idxDiscography ON discography");
8214         m_pDS->exec("DROP INDEX ix_art ON art");
8215       }
8216       catch (...)
8217       {
8218         CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed");
8219       }
8220     }
8221 
8222     // Create missing artist tag artist [Missing].
8223     // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
8224     strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
8225       BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
8226     m_pDS->exec(strSQL);
8227 
8228     // Indices have been dropped making transactions very slow, so create temp index
8229     m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
8230     m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
8231 
8232     // Ensure all songs have at least one artist, set those without to [Missing]
8233     strSQL = "SELECT count(idSong) FROM song "
8234              "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8235              "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
8236     int numsongs = GetSingleValueInt(strSQL);
8237     if (numsongs > 0)
8238     {
8239       CLog::Log(LOGDEBUG, "%i songs have no artist, setting artist to [Missing]", numsongs);
8240       // Insert song_artist records for songs that don't have any
8241       try
8242       {
8243         strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
8244           "SELECT %i, idSong, %i, '%s', 0 FROM song "
8245           "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8246           "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
8247           BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST);
8248         ExecuteQuery(strSQL);
8249       }
8250       catch (...)
8251       {
8252         CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed");
8253       }
8254     }
8255 
8256     // Ensure all albums have at least one artist, set those without to [Missing]
8257     strSQL = "SELECT count(idAlbum) FROM album "
8258       "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8259       "WHERE album_artist.idAlbum = album.idAlbum)";
8260     int numalbums = GetSingleValueInt(strSQL);
8261     if (numalbums > 0)
8262     {
8263       CLog::Log(LOGDEBUG, "%i albums have no artist, setting artist to [Missing]", numalbums);
8264       // Insert album_artist records for albums that don't have any
8265       try
8266       {
8267         strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
8268           "SELECT %i, idAlbum, '%s', 0 FROM album "
8269           "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8270           "WHERE album_artist.idAlbum = album.idAlbum)",
8271           BLANKARTIST_ID, BLANKARTIST_NAME.c_str());
8272         ExecuteQuery(strSQL);
8273       }
8274       catch (...)
8275       {
8276         CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed");
8277       }
8278     }
8279     //Remove temp indices, full analytics for database created later
8280     m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist");
8281     m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
8282   }
8283   if (version < 61)
8284   {
8285     // Create versiontagscan table
8286     m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
8287     m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
8288   }
8289   if (version < 62)
8290   {
8291     CLog::Log(LOGINFO, "create audiobook table");
8292     m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
8293         " strBook varchar(256), strAuthor text,"
8294         " bookmark integer, file text,"
8295         " dateAdded varchar (20) default NULL)");
8296   }
8297   if (version < 63)
8298   {
8299     // Add strSortName to Artist table
8300     m_pDS->exec("ALTER TABLE artist ADD strSortName text\n");
8301 
8302     //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
8303     m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8304       " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8305       " strArtistDisp text, strArtistSort text, strGenres text, "
8306       " iYear integer, bCompilation integer not null default '0', "
8307       " strMoods text, strStyles text, strThemes text, "
8308       " strReview text, strImage text, strLabel text, "
8309       " strType text, "
8310       " fRating FLOAT NOT NULL DEFAULT 0, "
8311       " iUserrating INTEGER NOT NULL DEFAULT 0, "
8312       " lastScraped varchar(20) default NULL, "
8313       " strReleaseType text, "
8314       " iVotes INTEGER NOT NULL DEFAULT 0)");
8315     m_pDS->exec("INSERT INTO album_new "
8316       "(idAlbum, "
8317       " strAlbum, strMusicBrainzAlbumID, "
8318       " strArtistDisp, strArtistSort, strGenres, "
8319       " iYear, bCompilation, "
8320       " strMoods, strStyles, strThemes, "
8321       " strReview, strImage, strLabel, "
8322       " strType, "
8323       " fRating, iUserrating, iVotes, "
8324       " lastScraped, "
8325       " strReleaseType)"
8326       " SELECT "
8327       " idAlbum, "
8328       " strAlbum, strMusicBrainzAlbumID, "
8329       " strArtists, NULL, strGenres, "
8330       " iYear, bCompilation, "
8331       " strMoods, strStyles, strThemes, "
8332       " strReview, strImage, strLabel, "
8333       " strType, "
8334       " fRating, iUserrating, iVotes, "
8335       " lastScraped, "
8336       " strReleaseType"
8337       " FROM album");
8338     m_pDS->exec("DROP TABLE album");
8339     m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8340 
8341     //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
8342     m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8343       " idAlbum INTEGER, idPath INTEGER, "
8344       " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8345       " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8346       " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8347       " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8348       " lastplayed VARCHAR(20) DEFAULT NULL, "
8349       " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
8350       " userrating INTEGER NOT NULL DEFAULT 0, "
8351       " comment TEXT, mood TEXT, dateAdded TEXT)");
8352     m_pDS->exec("INSERT INTO song_new "
8353       "(idSong, "
8354       " idAlbum, idPath, "
8355       " strArtistDisp, strArtistSort, strGenres, strTitle, "
8356       " iTrack, iDuration, iYear, "
8357       " strFileName, strMusicBrainzTrackID, "
8358       " iTimesPlayed, iStartOffset, iEndOffset, "
8359       " lastplayed,"
8360       " rating, userrating, votes, "
8361       " comment, mood, dateAdded)"
8362       " SELECT "
8363       " idSong, "
8364       " idAlbum, idPath, "
8365       " strArtists, NULL, strGenres, strTitle, "
8366       " iTrack, iDuration, iYear, "
8367       " strFileName, strMusicBrainzTrackID, "
8368       " iTimesPlayed, iStartOffset, iEndOffset, "
8369       " lastplayed,"
8370       " rating, userrating, votes, "
8371       " comment, mood, dateAdded"
8372       " FROM song");
8373     m_pDS->exec("DROP TABLE song");
8374     m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8375   }
8376   if (version < 65)
8377   {
8378     // Remove cue table
8379     m_pDS->exec("DROP TABLE cue");
8380     // Add strReplayGain to song table
8381     m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
8382   }
8383   if (version < 66)
8384   {
8385     // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
8386     m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
8387     m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
8388     // Add a new column bScrapedMBID for artists
8389     m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
8390   }
8391   if (version < 67)
8392   {
8393     // Add infosetting table
8394     m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, strSettings TEXT)");
8395     // Add a new column for setting to album and artist tables
8396     m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
8397     m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
8398 
8399     // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
8400     m_pDS->exec("CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
8401       "strScraperPath text, strSettings text)");
8402     try
8403     {
8404       m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
8405         "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, strScraperPath, strSettings "
8406         "FROM content WHERE strContent = 'artists' AND strPath LIKE 'musicdb://artists/_%/' ORDER BY idItem"
8407         );
8408     }
8409     catch (...)
8410     {
8411       CLog::Log(LOGERROR, "Migrating specific artist scraper settings has failed, settings not transferred");
8412     }
8413     try
8414     {
8415       m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
8416         "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, strScraperPath, strSettings "
8417         "FROM content WHERE strContent = 'albums' AND strPath LIKE 'musicdb://albums/_%/' ORDER BY idItem"
8418       );
8419     }
8420     catch (...)
8421     {
8422       CLog::Log(LOGERROR, "Migrating specific album scraper settings has failed, settings not transferred");
8423     }
8424     try
8425     {
8426       m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
8427         "SELECT id, strScraperPath, strSettings FROM content_temp");
8428       m_pDS->exec("UPDATE artist SET idInfoSetting = "
8429         "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
8430         "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) ");
8431       m_pDS->exec("UPDATE album SET idInfoSetting = "
8432         "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
8433         "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) ");
8434     }
8435     catch (...)
8436     {
8437       CLog::Log(LOGERROR, "Migrating album and artist scraper settings has failed, settings not transferred");
8438     }
8439     m_pDS->exec("DROP TABLE content_temp");
8440 
8441     // Remove content table
8442     m_pDS->exec("DROP TABLE content");
8443     // Remove albuminfosong table
8444     m_pDS->exec("DROP TABLE albuminfosong");
8445   }
8446   if (version < 68)
8447   {
8448     // Add a new columns strType, strGender, strDisambiguation for artists
8449     m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n");
8450     m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n");
8451     m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
8452   }
8453   if (version < 69)
8454   {
8455     // Remove album_genre table
8456     m_pDS->exec("DROP TABLE album_genre");
8457   }
8458   if (version < 70)
8459   {
8460     // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
8461     m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * 40 / 3 \n");
8462   }
8463   if (version < 71)
8464   {
8465     // Add lastscanned to versiontagscan table
8466     m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
8467     CDateTime dateAdded = CDateTime::GetCurrentDateTime();
8468     m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'", dateAdded.GetAsDBDateTime().c_str()));
8469   }
8470   if (version < 72)
8471   {
8472     // Create source table
8473     m_pDS->exec("CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
8474     // Create source_path table
8475     m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
8476     // Create album_source table
8477     m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
8478     // Populate source and source_path tables from sources.xml
8479     // Filling album_source needs to be done after indexes are created or it is
8480     // very slow. It could be populated during CreateAnalytics but it is checked
8481     // and filled as part of scanning anyway so simply force full rescan.
8482     MigrateSources();
8483   }
8484   if (version < 73)
8485   {
8486     // add bBoxedSet to album table
8487     m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
8488     //  add iDiscTotal to album table
8489     m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
8490     // populate iDiscTotal from the data already in the song table
8491     m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
8492                 "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
8493                 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
8494     // add strDiscSubtitles to song table
8495     m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
8496   }
8497   if (version < 74)
8498   {
8499     //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
8500     m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8501                 "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8502                 "strReleaseGroupMBID TEXT, "
8503                 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
8504                 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
8505                 "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
8506                 "bCompilation INTEGER NOT NULL DEFAULT '0', "
8507                 "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8508                 "strReview TEXT, strImage TEXT, strLabel TEXT, "
8509                 "strType TEXT, "
8510                 "fRating FLOAT NOT NULL DEFAULT 0, "
8511                 "iVotes INTEGER NOT NULL DEFAULT 0, "
8512                 "iUserrating INTEGER NOT NULL DEFAULT 0, "
8513                 "lastScraped VARCHAR(20) DEFAULT NULL, "
8514                 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
8515                 "strReleaseType TEXT, "
8516                 "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
8517                 "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
8518     // Prepare as MySQL has different CAST datatypes
8519     m_pDS->exec(PrepareSQL("INSERT INTO album_new "
8520                 "(idalbum, strAlbum, "
8521                 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
8522                 "strArtistDisp, strArtistSort, strGenres, "
8523                 "strReleaseDate, strOrigReleaseDate, "
8524                 "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
8525                 "strReview, strImage, strLabel, strType, "
8526                 "fRating, iVotes, iUserrating, "
8527                 "lastScraped, bScrapedMBID, strReleaseType, "
8528                 "iDiscTotal, idInfoSetting) "
8529                 "SELECT "
8530                 "idAlbum, strAlbum, "
8531                 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
8532                 "strArtistDisp, strArtistSort, strGenres, "
8533                 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8534                 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8535                 // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
8536                 "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
8537                 "bCompilation, strMoods, strStyles, strThemes, "
8538                 "strReview, strImage, strLabel, strType, "
8539                 "fRating, iVotes, iUserrating, "
8540                 "lastScraped, bScrapedMBID, strReleaseType, "
8541                 "iDiscTotal, idInfoSetting "
8542                 "FROM album"));
8543     m_pDS->exec("DROP TABLE album");
8544     m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8545 
8546     //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
8547     m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8548       "idAlbum INTEGER, idPath INTEGER, "
8549       "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8550       "iTrack INTEGER, iDuration INTEGER, "
8551       "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
8552       "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
8553       "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8554       "lastplayed VARCHAR(20) DEFAULT NULL, "
8555       "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
8556       "userrating INTEGER NOT NULL DEFAULT 0, "
8557       "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
8558       "dateAdded TEXT)");
8559     // Prepare as MySQL has different CAST datatypes
8560     m_pDS->exec(PrepareSQL("INSERT INTO song_new "
8561       "(idSong, "
8562       "idAlbum, idPath, "
8563       "strArtistDisp, strArtistSort, strGenres, strTitle, "
8564       "iTrack, iDuration, "
8565       "strReleaseDate, strOrigReleaseDate, "
8566       "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
8567       "iTimesPlayed, iStartOffset, iEndOffset, "
8568       "lastplayed, "
8569       "rating, userrating, votes, "
8570       "comment, mood, strReplayGain, dateAdded) "
8571       "SELECT "
8572       "idSong, "
8573       "idAlbum, idPath, "
8574       "strArtistDisp, strArtistSort, strGenres, strTitle, "
8575       "iTrack, iDuration, "
8576       "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8577       "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8578       "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
8579       "iTimesPlayed, iStartOffset, iEndOffset, "
8580       "lastplayed, "
8581       "rating, userrating, votes, "
8582       "comment, mood, strReplayGain, dateAdded "
8583       "FROM song"));
8584     m_pDS->exec("DROP TABLE song");
8585     m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8586   }
8587   if (version < 75)
8588   {
8589     m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
8590     m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
8591     m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
8592   }
8593   if (version < 77)
8594   {
8595     m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
8596   }
8597   if (version < 78)
8598   {
8599     std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime();
8600 
8601     // Add removed_link table
8602     m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
8603     // Add lastcleaned and artistlinksupdated to versiontagscan table
8604     m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
8605     m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
8606     m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
8607     // Adjust lastscanned if original local time value is after current UTC
8608     if (GetLibraryLastUpdated() > strUTCNow)
8609       SetLibraryLastUpdated();
8610     m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
8611                 "genresupdated = lastscanned, "
8612                 "artistlinksupdated = lastscanned");
8613 
8614     // Add dateNew, dateModified to song table
8615     m_pDS->exec("ALTER TABLE song ADD dateNew TEXT");
8616     m_pDS->exec("ALTER TABLE song ADD dateModified TEXT");
8617     // Set new to dateAdded and modified to lastplayed as estimates
8618     // Limit those local time values to now UTC, and modified is after new
8619     m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
8620     m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'",
8621                            strUTCNow.c_str(), strUTCNow.c_str()));
8622     m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
8623     m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
8624                            strUTCNow.c_str(), strUTCNow.c_str()));
8625     m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
8626 
8627     // Add dateAdded, dateNew, dateModified to album table
8628     m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT");
8629     m_pDS->exec("ALTER TABLE album ADD dateNew TEXT");
8630     m_pDS->exec("ALTER TABLE album ADD dateModified TEXT");
8631     // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
8632     // Limit modified value to now UTC and after new
8633     // Indices have been dropped making subquery very slow, so create temp index
8634     m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
8635     m_pDS->exec("UPDATE album SET dateAdded = "
8636       "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
8637     m_pDS->exec("UPDATE album SET dateNew = "
8638       "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
8639     m_pDS->exec("UPDATE album SET dateModified = dateNew");
8640     m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
8641     m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
8642                            strUTCNow.c_str(), strUTCNow.c_str()));
8643     //Remove temp index, full analytics for database created later
8644     m_pDS->exec("DROP INDEX idxSong3 ON song");
8645 
8646     // Add dateAdded, dateNew, dateModified to artist table
8647     m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT");
8648     m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT");
8649     m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT");
8650     // dateAdded has NULL values until files rescanned by user
8651     // Set new and modified to now UTC as not worth complexity of estimating from song dates
8652     m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str()));
8653     m_pDS->exec("UPDATE artist SET dateModified = dateNew");
8654   }
8655   if (version < 79)
8656   {
8657     m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
8658   }
8659   if (version < 80)
8660   {
8661     m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
8662     // update duration for all current albums
8663     m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
8664                 "WHERE song.idAlbum = album.idAlbum) "
8665                 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
8666   }
8667   if (version < 82)
8668   {
8669     // Update artist table combining fanart URL data into strImage field
8670     // Clear empty URL data <fanart /> and <thumb />
8671     m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
8672     m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
8673     //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
8674     m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
8675     m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
8676     m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
8677                 "aspect=\"fanart\" preview')");
8678     // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
8679     // Truncate the fanart when total URLs exceeds this
8680     bool bisMySQL = StringUtils::EqualsNoCase(
8681         CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
8682         "mysql");
8683     if (bisMySQL)
8684     {
8685       std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist "
8686                            "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
8687       if (m_pDS->query(strSQL))
8688       {
8689         while (!m_pDS->eof())
8690         {
8691           int idArtist = m_pDS->fv("idArtist").get_asInt();
8692           std::string strFanart = m_pDS->fv("strFanart").get_asString();
8693           std::string strImage = m_pDS->fv("strImage").get_asString();
8694           size_t space = 65535;
8695           // Trim strImage to allow arbitrary half space for fanart
8696           if (!TrimImageURLs(strImage, space / 2))
8697             strImage.clear(); // </thumb> not found, empty field
8698           space = space - strImage.length();
8699           // Trim fanart to fit remaining space
8700           if (!TrimImageURLs(strFanart, space))
8701             strFanart.clear(); // </thumb> not found, empty field
8702 
8703           strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
8704                               "WHERE idArtist = %i",
8705                               strFanart.c_str(), strImage.c_str(), idArtist);
8706           m_pDS2->exec(strSQL); // Use other dataset to update while looping result set
8707 
8708           m_pDS->next();
8709         }
8710         m_pDS->close();
8711       }
8712     }
8713 
8714     // Remove strFanart column from artist table
8715     m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
8716                 "strArtist varchar(256), strMusicBrainzArtistID text, "
8717                 "strSortName text, "
8718                 "strType text, strGender text, strDisambiguation text, "
8719                 "strBorn text, strFormed text, strGenres text, strMoods text, "
8720                 "strStyles text, strInstruments text, strBiography text, "
8721                 "strDied text, strDisbanded text, strYearsActive text, "
8722                 "strImage text, "
8723                 "lastScraped varchar(20) default NULL, "
8724                 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
8725                 "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
8726                 "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
8727     // Concatentate fanart URLs into strImage field
8728     // Prepare SQL to convert CONCAT to || in SQLite
8729     m_pDS->exec(PrepareSQL("INSERT INTO artist_new "
8730                            "(idArtist, strArtist, strMusicBrainzArtistID, "
8731                            "strSortName, strType, strGender, strDisambiguation, "
8732                            "strBorn, strFormed, strGenres, strMoods, "
8733                            "strStyles , strInstruments , strBiography , "
8734                            "strDied, strDisbanded, strYearsActive, "
8735                            "strImage, "
8736                            "lastScraped, bScrapedMBID, idInfoSetting, "
8737                            "dateAdded, dateNew, dateModified) "
8738                            "SELECT "
8739                            "artist.idArtist, "
8740                            "strArtist, strMusicBrainzArtistID, "
8741                            "strSortName, strType, strGender, strDisambiguation, "
8742                            "strBorn, strFormed, strGenres, strMoods, "
8743                            "strStyles, strInstruments, strBiography, "
8744                            "strDied, strDisbanded, strYearsActive, "
8745                            "CONCAT(strImage, strFanart), "
8746                            "lastScraped, bScrapedMBID, idInfoSetting, "
8747                            "dateAdded, dateNew, dateModified "
8748                            "FROM artist"));
8749     m_pDS->exec("DROP TABLE artist");
8750     m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8751   }
8752   // Set the verion of tag scanning required.
8753   // Not every schema change requires the tags to be rescanned, set to the highest schema version
8754   // that needs this. Forced rescanning (of music files that have not changed since they were
8755   // previously scanned) also accommodates any changes to the way tags are processed
8756   // e.g. read tags that were not processed by previous versions.
8757   // The original db version when the tags were scanned, and the minimal db version needed are
8758   // later used to determine if a forced rescan should be prompted
8759 
8760   // The last schema change needing forced rescanning was 73.
8761   // This is because Kodi can now read and process extra tags involved in the creation of box sets
8762 
8763   SetMusicNeedsTagScan(73);
8764 
8765   // After all updates, store the original db version.
8766   // This indicates the version of tag processing that was used to populate db
8767   SetMusicTagScanVersion(version);
8768 }
8769 
GetSchemaVersion() const8770 int CMusicDatabase::GetSchemaVersion() const
8771 {
8772   return 82;
8773 }
8774 
GetMusicNeedsTagScan()8775 int CMusicDatabase::GetMusicNeedsTagScan()
8776 {
8777   try
8778   {
8779     if (nullptr == m_pDB)
8780       return -1;
8781     if (nullptr == m_pDS)
8782       return -1;
8783 
8784     std::string sql = "SELECT * FROM versiontagscan";
8785     if (!m_pDS->query(sql)) return -1;
8786 
8787     if (m_pDS->num_rows() != 1)
8788     {
8789       m_pDS->close();
8790       return -1;
8791     }
8792 
8793     int idVersion = m_pDS->fv("idVersion").get_asInt();
8794     int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt();
8795     m_pDS->close();
8796     if (idVersion < iNeedsScan)
8797       return idVersion;
8798     else
8799       return 0;
8800   }
8801   catch (...)
8802   {
8803     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
8804   }
8805   return -1;
8806 }
8807 
SetMusicNeedsTagScan(int version)8808 void CMusicDatabase::SetMusicNeedsTagScan(int version)
8809 {
8810   m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version));
8811 }
8812 
SetMusicTagScanVersion(int version)8813 void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */)
8814 {
8815   if (version == 0)
8816     m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
8817   else
8818     m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version));
8819 }
8820 
GetLibraryLastUpdated()8821 std::string CMusicDatabase::GetLibraryLastUpdated()
8822 {
8823   return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
8824 }
8825 
SetLibraryLastUpdated()8826 void CMusicDatabase::SetLibraryLastUpdated()
8827 {
8828   CDateTime dateUpdated = CDateTime::GetUTCDateTime();
8829   m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
8830                           dateUpdated.GetAsDBDateTime().c_str()));
8831 }
8832 
GetLibraryLastCleaned()8833 std::string CMusicDatabase::GetLibraryLastCleaned()
8834 {
8835   return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
8836 }
8837 
SetLibraryLastCleaned()8838 void CMusicDatabase::SetLibraryLastCleaned()
8839 {
8840   std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
8841   m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned  = '%s'",
8842                          strUpdated.c_str()));
8843 }
8844 
GetArtistLinksUpdated()8845 std::string CMusicDatabase::GetArtistLinksUpdated()
8846 {
8847   return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
8848 }
8849 
SetArtistLinksUpdated()8850 void CMusicDatabase::SetArtistLinksUpdated()
8851 {
8852   std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
8853   m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'",
8854                          strUpdated.c_str()));
8855 }
8856 
GetGenresLastAdded()8857 std::string CMusicDatabase::GetGenresLastAdded()
8858 {
8859   return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
8860 }
8861 
GetSongsLastAdded()8862 std::string CMusicDatabase::GetSongsLastAdded()
8863 {
8864   return GetSingleValue("SELECT MAX(dateNew) FROM song");
8865 }
8866 
GetAlbumsLastAdded()8867 std::string CMusicDatabase::GetAlbumsLastAdded()
8868 {
8869   return GetSingleValue("SELECT MAX(dateNew) FROM album");
8870 }
8871 
GetArtistsLastAdded()8872 std::string CMusicDatabase::GetArtistsLastAdded()
8873 {
8874   return GetSingleValue("SELECT MAX(dateNew) FROM artist");
8875 }
8876 
GetSongsLastModified()8877 std::string CMusicDatabase::GetSongsLastModified()
8878 {
8879   return GetSingleValue("SELECT MAX(dateModified) FROM song");
8880 }
8881 
GetAlbumsLastModified()8882 std::string CMusicDatabase::GetAlbumsLastModified()
8883 {
8884   return GetSingleValue("SELECT MAX(dateModified) FROM album");
8885 }
8886 
GetArtistsLastModified()8887 std::string CMusicDatabase::GetArtistsLastModified()
8888 {
8889   return GetSingleValue("SELECT MAX(dateModified) FROM artist");
8890 }
8891 
GetRandomSongIDs(const Filter & filter,std::vector<std::pair<int,int>> & songIDs)8892 unsigned int CMusicDatabase::GetRandomSongIDs(const Filter &filter, std::vector<std::pair<int,int> > &songIDs)
8893 {
8894   try
8895   {
8896     if (nullptr == m_pDB)
8897       return 0;
8898     if (nullptr == m_pDS)
8899       return 0;
8900 
8901     std::string strSQL = "SELECT idSong FROM songview ";
8902     if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
8903       return false;
8904     strSQL += PrepareSQL(" ORDER BY RANDOM()");
8905 
8906     if (!m_pDS->query(strSQL)) return 0;
8907     songIDs.clear();
8908     if (m_pDS->num_rows() == 0)
8909     {
8910       m_pDS->close();
8911       return 0;
8912     }
8913     songIDs.reserve(m_pDS->num_rows());
8914     while (!m_pDS->eof())
8915     {
8916       songIDs.push_back(std::make_pair<int,int>(1, m_pDS->fv(song_idSong).get_asInt()));
8917       m_pDS->next();
8918     }    // cleanup
8919     m_pDS->close();
8920     return static_cast<unsigned int>(songIDs.size());
8921   }
8922   catch (...)
8923   {
8924     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
8925   }
8926   return 0;
8927 }
8928 
GetSongsCount(const Filter & filter)8929 int CMusicDatabase::GetSongsCount(const Filter &filter)
8930 {
8931   try
8932   {
8933     if (nullptr == m_pDB)
8934       return 0;
8935     if (nullptr == m_pDS)
8936       return 0;
8937 
8938     std::string strSQL = "select count(idSong) as NumSongs from songview ";
8939     if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
8940       return false;
8941 
8942     if (!m_pDS->query(strSQL)) return false;
8943     if (m_pDS->num_rows() == 0)
8944     {
8945       m_pDS->close();
8946       return 0;
8947     }
8948 
8949     int iNumSongs = m_pDS->fv("NumSongs").get_asInt();
8950     // cleanup
8951     m_pDS->close();
8952     return iNumSongs;
8953   }
8954   catch (...)
8955   {
8956     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
8957   }
8958   return 0;
8959 }
8960 
GetAlbumPath(int idAlbum,std::string & basePath)8961 bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath)
8962 {
8963   basePath.clear();
8964   std::vector<std::pair<std::string, int>> paths;
8965   if (!GetAlbumPaths(idAlbum, paths))
8966     return false;
8967 
8968   for (const auto& pathpair : paths)
8969   {
8970     if (basePath.empty())
8971       basePath = pathpair.first.c_str();
8972     else
8973       URIUtils::GetCommonPath(basePath, pathpair.first.c_str());
8974   }
8975   return true;
8976 }
8977 
GetAlbumPaths(int idAlbum,std::vector<std::pair<std::string,int>> & paths)8978 bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths)
8979 {
8980   paths.clear();
8981   std::string strSQL;
8982   try
8983   {
8984     if (nullptr == m_pDB)
8985       return false;
8986     if (nullptr == m_pDS2)
8987       return false;
8988 
8989     // Get the unique paths of songs on the album, providing there are no songs from
8990     // other albums with the same path. This returns
8991     // a) <album> if is contains all the songs and no others, or
8992     // b) <album>/cd1, <album>/cd2 etc. for disc sets
8993     // but does *not* return any path when albums are mixed together. That could be because of
8994     // deliberate file organisation, or (more likely) because of a tagging error in album name
8995     // or Musicbrainzalbumid. Thus it avoids finding somme generic music path.
8996     strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
8997       "JOIN path ON song.idPath = path.idPath "
8998       "WHERE song.idAlbum = %ld "
8999       "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
9000       "WHERE idPath = song.idPath) = 1", idAlbum);
9001 
9002     if (!m_pDS2->query(strSQL))
9003       return false;
9004     if (m_pDS2->num_rows() == 0)
9005     {
9006       // Album does not have a unique path, files are mixed
9007       m_pDS2->close();
9008       return false;
9009     }
9010 
9011     while (!m_pDS2->eof())
9012     {
9013       paths.emplace_back(m_pDS2->fv("strPath").get_asString(), m_pDS2->fv("song.idPath").get_asInt());
9014       m_pDS2->next();
9015     }
9016     // Cleanup recordset data
9017     m_pDS2->close();
9018     return true;
9019   }
9020   catch (...)
9021   {
9022     CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %s", __FUNCTION__, strSQL.c_str());
9023   }
9024 
9025   return false;
9026 }
9027 
GetDiscnumberForPathID(int idPath)9028 int CMusicDatabase::GetDiscnumberForPathID(int idPath)
9029 {
9030   std::string strSQL;
9031   int result = -1;
9032   try
9033   {
9034     if (nullptr == m_pDB)
9035       return -1;
9036     if (nullptr == m_pDS2)
9037       return -1;
9038 
9039     strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
9040       "WHERE idPath = %i", idPath);
9041 
9042     if (!m_pDS2->query(strSQL))
9043       return -1;
9044     if (m_pDS2->num_rows() == 1)
9045     { // Songs with this path have a unique disc number
9046       result = m_pDS2->fv("discnum").get_asInt();
9047     }
9048     // Cleanup recordset data
9049     m_pDS2->close();
9050   }
9051   catch (...)
9052   {
9053     CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %s", __FUNCTION__, strSQL.c_str());
9054   }
9055   return result;
9056 }
9057 
9058 // Get old "artist path" - where artist.nfo and art was located v17 and below.
9059 // It is the path common to all albums by an (album) artist, but ensure it is unique
9060 // to that artist and not shared with other artists. Previously this caused incorrect nfo
9061 // and art to be applied to multiple artists.
GetOldArtistPath(int idArtist,std::string & basePath)9062 bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string &basePath)
9063 {
9064   basePath.clear();
9065   try
9066   {
9067     if (nullptr == m_pDB)
9068       return false;
9069     if (nullptr == m_pDS2)
9070       return false;
9071 
9072     // find all albums from this artist, and all the paths to the songs from those albums
9073     std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist "
9074       "JOIN song ON album_artist.idAlbum = song.idAlbum "
9075       "JOIN path ON song.idPath = path.idPath "
9076       "WHERE album_artist.idArtist = %ld "
9077       "GROUP BY song.idPath",
9078       idArtist);
9079 
9080     // run query
9081     if (!m_pDS2->query(strSQL)) return false;
9082     int iRowsFound = m_pDS2->num_rows();
9083     if (iRowsFound == 0)
9084     {
9085       // Artist is not an album artist, no path to find
9086       m_pDS2->close();
9087       return false;
9088     }
9089     else if (iRowsFound == 1)
9090     {
9091       // Special case for single path - assume that we're in an artist/album/songs filesystem
9092       URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath);
9093       m_pDS2->close();
9094     }
9095     else
9096     {
9097       // find the common path (if any) to these albums
9098       while (!m_pDS2->eof())
9099       {
9100         std::string path = m_pDS2->fv("strPath").get_asString();
9101         if (basePath.empty())
9102           basePath = path;
9103         else
9104           URIUtils::GetCommonPath(basePath, path);
9105 
9106         m_pDS2->next();
9107       }
9108       m_pDS2->close();
9109     }
9110 
9111     // Check any path found is unique to that album artist, and do *not* return any path
9112     // that is shared with other album artists. That could be because of collaborations
9113     // i.e. albums with more than one album artist, or because there are albums by the
9114     // artist on multiple music sources, or elsewhere in the folder hierarchy.
9115     // Avoid returning some generic music path.
9116     if (!basePath.empty())
9117     {
9118       strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
9119         "JOIN song ON album_artist.idAlbum = song.idAlbum "
9120         "JOIN path ON song.idPath = path.idPath "
9121         "WHERE album_artist.idArtist <> %ld "
9122         "AND strPath LIKE '%s%%'",
9123         idArtist, basePath.c_str());
9124       std::string strValue = GetSingleValue(strSQL, m_pDS2);
9125       if (!strValue.empty())
9126       {
9127         int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9128         if (countartists == 0)
9129           return true;
9130       }
9131     }
9132   }
9133   catch (...)
9134   {
9135     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9136   }
9137   basePath.clear();
9138   return false;
9139 }
9140 
GetArtistPath(const CArtist & artist,std::string & path)9141 bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string &path)
9142 {
9143    // Get path for artist in the artists folder
9144   path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
9145   if (path.empty())
9146     return false; // No Artists folder not set;
9147   // Get unique artist folder name
9148   std::string strFolder;
9149   if (GetArtistFolderName(artist, strFolder))
9150   {
9151       path = URIUtils::AddFileToFolder(path, strFolder);
9152       return true;
9153   }
9154   path.clear();
9155   return false;
9156 }
9157 
GetAlbumFolder(const CAlbum & album,const std::string & strAlbumPath,std::string & strFolder)9158 bool CMusicDatabase::GetAlbumFolder(const CAlbum& album, const std::string &strAlbumPath, std::string &strFolder)
9159 {
9160   strFolder.clear();
9161   // Get a name for the album folder that is unique for the artist to use when
9162   // exporting albums to separate nfo files in a folder under an artist folder
9163 
9164   // When given an album path (common to all the music files containing *only*
9165   // that album) check if that folder name is *unique* looking at folders on
9166   // all levels of the music file paths for the artist
9167   if (!strAlbumPath.empty())
9168   {
9169     // Get last folder from full path
9170     std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath);
9171     if (!folders.empty())
9172     {
9173       strFolder = folders.back();
9174       // The same folder name could be used on different paths for albums by the
9175       // same first artist. The albums could be totally different or also have
9176       // the same name (but different mbid). Be over cautious and look for the
9177       // name any where in the music file paths
9178       std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
9179         "JOIN song ON album_artist.idAlbum = song.idAlbum "
9180         "JOIN path on path.idPath = song.idPath "
9181         "WHERE album_artist.iOrder = 0 "
9182         "AND album_artist.idArtist = %ld "
9183         "AND path.strPath LIKE '%%\\%s\\%%'",
9184         album.artistCredits[0].GetArtistId(), strFolder.c_str());
9185 
9186       if (!m_pDS2->query(strSQL))
9187         return false;
9188       int iRowsFound = m_pDS2->num_rows();
9189       m_pDS2->close();
9190       if (iRowsFound == 1)
9191         return true;
9192     }
9193   }
9194   // Create a valid unique folder name from album title
9195   // @todo: Does UFT8 matter or need normalizing?
9196   // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9197   strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT);
9198   StringUtils::Replace(strFolder, " _ ", "_");
9199 
9200   // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
9201   // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
9202   // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
9203   // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
9204   std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
9205     "JOIN album ON album_artist.idAlbum = album.idAlbum "
9206     "WHERE album_artist.iOrder = 0 "
9207     "AND album_artist.idArtist = %ld "
9208     "AND album.strAlbum LIKE '%s'  ",
9209     album.artistCredits[0].GetArtistId(), album.strAlbum.c_str());
9210   std::string strValue = GetSingleValue(strSQL, m_pDS2);
9211   if (strValue.empty())
9212     return false;
9213   int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9214   if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty())
9215   { // Only one of the duplicate albums can be without mbid
9216     strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4);
9217   }
9218   return !strFolder.empty();
9219 }
9220 
GetArtistFolderName(const CArtist & artist,std::string & strFolder)9221 bool CMusicDatabase::GetArtistFolderName(const CArtist &artist, std::string &strFolder)
9222 {
9223   return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder);
9224 }
9225 
GetArtistFolderName(const std::string & strArtist,const std::string & strMusicBrainzArtistID,std::string & strFolder)9226 bool CMusicDatabase::GetArtistFolderName(const std::string &strArtist, const std::string &strMusicBrainzArtistID,
9227   std::string &strFolder)
9228 {
9229   // Create a valid unique folder name for artist
9230   // @todo: Does UFT8 matter or need normalizing?
9231   // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9232   strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT);
9233   StringUtils::Replace(strFolder, " _ ", "_");
9234 
9235   // Ensure <artist name> is unique e.g. 2 x John Williams.
9236   // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
9237   // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
9238   // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
9239   std::string strSQL = PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
9240   std::string strValue = GetSingleValue(strSQL, m_pDS2);
9241   if (strValue.empty())
9242     return false;
9243   int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9244   if (countartist > 1)
9245     strFolder += "_" + strMusicBrainzArtistID.substr(0, 4);
9246   return !strFolder.empty();
9247 }
9248 
AddSource(const std::string & strName,const std::string & strMultipath,const std::vector<std::string> & vecPaths,int id)9249 int CMusicDatabase::AddSource(const std::string& strName, const std::string& strMultipath, const std::vector<std::string>& vecPaths, int id /*= -1*/)
9250 {
9251   int idSource = -1;
9252   std::string strSQL;
9253   try
9254   {
9255     if (nullptr == m_pDB)
9256       return -1;
9257     if (nullptr == m_pDS)
9258       return -1;
9259 
9260     // Check if source name already exists
9261     idSource = GetSourceByName(strName);
9262     if (idSource < 0)
9263     {
9264       BeginTransaction();
9265       // Add new source and source paths
9266       if (id > 0)
9267         strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) VALUES(%i, '%s', '%s')",
9268           id, strName.c_str(), strMultipath.c_str());
9269       else
9270         strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) VALUES(NULL, '%s', '%s')",
9271           strName.c_str(), strMultipath.c_str());
9272       m_pDS->exec(strSQL);
9273 
9274       idSource = static_cast<int>(m_pDS->lastinsertid());
9275 
9276       int idPath = 1;
9277       for (const auto& path : vecPaths)
9278       {
9279         strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) values(%i,%i,'%s')",
9280           idSource, idPath, path.c_str());
9281         m_pDS->exec(strSQL);
9282         ++idPath;
9283       }
9284 
9285       // Find albums by song path, building WHERE for multiple source paths
9286       // (providing source has a path)
9287       if (vecPaths.size() > 0)
9288       {
9289         std::vector<int> albumIds;
9290         Filter extFilter;
9291         strSQL = "SELECT DISTINCT idAlbum FROM song ";
9292         extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
9293         for (const auto& path : vecPaths)
9294           extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false);
9295         if (!BuildSQL(strSQL, extFilter, strSQL))
9296           return -1;
9297 
9298         if (!m_pDS->query(strSQL))
9299           return -1;
9300 
9301         while (!m_pDS->eof())
9302         {
9303           albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
9304           m_pDS->next();
9305         }
9306         m_pDS->close();
9307 
9308         // Add album_source for related albums
9309         for (auto idAlbum : albumIds)
9310         {
9311           strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) VALUES('%i', '%i')",
9312             idSource, idAlbum);
9313           m_pDS->exec(strSQL);
9314         }
9315       }
9316       CommitTransaction();
9317     }
9318     return idSource;
9319   }
9320   catch (...)
9321   {
9322     CLog::Log(LOGERROR, "%s failed with query (%s)", __FUNCTION__, strSQL.c_str());
9323     RollbackTransaction();
9324   }
9325 
9326   return -1;
9327 }
9328 
UpdateSource(const std::string & strOldName,const std::string & strName,const std::string & strMultipath,const std::vector<std::string> & vecPaths)9329 int CMusicDatabase::UpdateSource(const std::string& strOldName, const std::string& strName, const std::string& strMultipath, const std::vector<std::string>& vecPaths)
9330 {
9331   int idSource = -1;
9332   std::string strSourceMultipath;
9333   std::string strSQL;
9334   try
9335   {
9336     if (nullptr == m_pDB)
9337       return -1;
9338     if (nullptr == m_pDS)
9339       return -1;
9340 
9341     // Get details of named old source
9342     if (!strOldName.empty())
9343     {
9344       strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
9345         strOldName.c_str());
9346       if (!m_pDS->query(strSQL))
9347         return -1;
9348       if (m_pDS->num_rows() > 0)
9349       {
9350         idSource = m_pDS->fv("idSource").get_asInt();
9351         strSourceMultipath = m_pDS->fv("strMultipath").get_asString();
9352       }
9353       m_pDS->close();
9354     }
9355     if (idSource < 0)
9356     {
9357       // Source not found, add new one
9358       return AddSource(strName, strMultipath, vecPaths);
9359     }
9360 
9361     // Nothing changed? (that we hold in db, other source details could be modified)
9362     bool pathschanged = strMultipath.compare(strSourceMultipath) != 0;
9363     if (!pathschanged && strOldName.compare(strName) == 0)
9364       return idSource;
9365 
9366     if (!pathschanged)
9367     {
9368       // Name changed? Could be that none of the values held in db changed
9369       if (strOldName.compare(strName) != 0)
9370       {
9371         strSQL = PrepareSQL("UPDATE source SET strName = '%s' WHERE idSource = %i",
9372           strName.c_str(), idSource);
9373         m_pDS->exec(strSQL);
9374       }
9375       return idSource;
9376     }
9377     else
9378     {
9379       // Change paths (and name) by deleting and re-adding, but keep same ID
9380       strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource);
9381       m_pDS->exec(strSQL);
9382       return AddSource(strName, strMultipath, vecPaths, idSource);
9383     }
9384   }
9385   catch (...)
9386   {
9387     CLog::Log(LOGERROR, "%s failed with query (%s)", __FUNCTION__, strSQL.c_str());
9388     RollbackTransaction();
9389   }
9390 
9391   return -1;
9392 }
9393 
RemoveSource(const std::string & strName)9394 bool CMusicDatabase::RemoveSource(const std::string& strName)
9395 {
9396   // Related album_source and source_path rows removed by trigger
9397   SetLibraryLastCleaned();
9398   return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str()));
9399 }
9400 
GetSourceFromPath(const std::string & strPath1)9401 int CMusicDatabase::GetSourceFromPath(const std::string& strPath1)
9402 {
9403   std::string strSQL;
9404   int idSource = -1;
9405   try
9406   {
9407     std::string strPath(strPath1);
9408     if (!URIUtils::HasSlashAtEnd(strPath))
9409       URIUtils::AddSlashAtEnd(strPath);
9410 
9411     if (nullptr == m_pDB)
9412       return -1;
9413     if (nullptr == m_pDS)
9414       return -1;
9415 
9416     // Check if path is a source matching on multipath
9417     strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str());
9418     if (!m_pDS->query(strSQL))
9419       return -1;
9420     if (m_pDS->num_rows() > 0)
9421       idSource = m_pDS->fv("idSource").get_asInt();
9422     m_pDS->close();
9423     if (idSource > 0)
9424       return idSource;
9425 
9426     // Check if path is a source path (of many) or a subfolder of a single source
9427     strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9428       "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", strPath.c_str());
9429     if (!m_pDS->query(strSQL))
9430       return -1;
9431     if (m_pDS->num_rows() == 1)
9432       idSource = m_pDS->fv("idSource").get_asInt();
9433     m_pDS->close();
9434     return idSource;
9435 
9436   }
9437   catch (...)
9438   {
9439     CLog::Log(LOGERROR, "%s path: %s (%s) failed", __FUNCTION__, strSQL.c_str(), strPath1.c_str());
9440   }
9441 
9442   return -1;
9443 }
9444 
AddAlbumSource(int idAlbum,int idSource)9445 bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource)
9446 {
9447   std::string strSQL;
9448   strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) values(%i, %i)",
9449     idAlbum, idSource);
9450   return ExecuteQuery(strSQL);
9451 }
9452 
AddAlbumSources(int idAlbum,const std::string & strPath)9453 bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath)
9454 {
9455   std::string strSQL;
9456   std::vector<int> sourceIds;
9457   try
9458   {
9459     if (nullptr == m_pDB)
9460       return false;
9461     if (nullptr == m_pDS)
9462       return false;
9463 
9464     if (!strPath.empty())
9465     {
9466       // Find sources related to album using album path
9467       strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9468         "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", strPath.c_str());
9469       if (!m_pDS->query(strSQL))
9470         return false;
9471       while (!m_pDS->eof())
9472       {
9473         sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
9474         m_pDS->next();
9475       }
9476       m_pDS->close();
9477     }
9478     else
9479     {
9480       // Find sources using song paths, check each source path individually
9481       if (nullptr == m_pDS2)
9482         return false;
9483       strSQL = "SELECT idSource, strPath FROM source_path";
9484       if (!m_pDS->query(strSQL))
9485         return false;
9486       while (!m_pDS->eof())
9487       {
9488         std::string sourcepath = m_pDS->fv("strPath").get_asString();
9489         strSQL = PrepareSQL("SELECT 1 FROM song "
9490           "JOIN path ON song.idPath = path.idPath "
9491           "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'", sourcepath.c_str());
9492         if (!m_pDS2->query(strSQL))
9493           return false;
9494         if (m_pDS2->num_rows() > 0)
9495           sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
9496         m_pDS2->close();
9497 
9498         m_pDS->next();
9499       }
9500       m_pDS->close();
9501     }
9502 
9503     //Add album sources
9504     for (auto idSource : sourceIds)
9505     {
9506       AddAlbumSource(idAlbum, idSource);
9507     }
9508 
9509     return true;
9510   }
9511   catch (...)
9512   {
9513     CLog::Log(LOGERROR, "%s path: %s (%s) failed", __FUNCTION__, strSQL.c_str(), strPath.c_str());
9514   }
9515 
9516   return false;
9517 }
9518 
DeleteAlbumSources(int idAlbum)9519 bool CMusicDatabase::DeleteAlbumSources(int idAlbum)
9520 {
9521   return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum));
9522 }
9523 
CheckSources(VECSOURCES & sources)9524 bool CMusicDatabase::CheckSources(VECSOURCES& sources)
9525 {
9526   if (sources.empty())
9527   {
9528     // Source table empty too?
9529     return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
9530   }
9531 
9532   // Check number of entries matches
9533   size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
9534   if (total != sources.size())
9535     return false;
9536 
9537   // Check individual sources match
9538   try
9539   {
9540     if (nullptr == m_pDB)
9541       return false;
9542     if (nullptr == m_pDS)
9543       return false;
9544 
9545     std::string strSQL;
9546     for (const auto& source : sources)
9547     {
9548       // Check each source by name
9549       strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source "
9550         "WHERE strName LIKE '%s'", source.strName.c_str());
9551       m_pDS->query(strSQL);
9552       if (!m_pDS->query(strSQL))
9553         return false;
9554       if (m_pDS->num_rows() != 1)
9555       {
9556         // Missing source, or name duplication
9557         m_pDS->close();
9558         return false;
9559       }
9560       else
9561       {
9562         // Check details. Encoded URLs of source.strPath matched to strMultipath
9563         // field, no need to look at individual paths of source_path table
9564         if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0)
9565         {
9566           // Paths not match
9567           m_pDS->close();
9568           return false;
9569         }
9570         m_pDS->close();
9571       }
9572     }
9573     return true;
9574   }
9575   catch (...)
9576   {
9577     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9578   }
9579   return false;
9580 }
9581 
MigrateSources()9582 bool CMusicDatabase::MigrateSources()
9583 {
9584   //Fetch music sources from xml
9585   VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
9586 
9587   std::string strSQL;
9588   try
9589   {
9590     // Fill source and source paths tables
9591     for (const auto& source : sources)
9592     {
9593       // AddSource(source.strName, source.strPath, source.vecPaths);
9594       // Add new source
9595       strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) VALUES(NULL, '%s', '%s')",
9596         source.strName.c_str(), source.strPath.c_str());
9597       m_pDS->exec(strSQL);
9598       int idSource = static_cast<int>(m_pDS->lastinsertid());
9599 
9600       // Add new source paths
9601       int idPath = 1;
9602       for (const auto& path : source.vecPaths)
9603       {
9604         strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) values(%i,%i,'%s')",
9605           idSource, idPath, path.c_str());
9606         m_pDS->exec(strSQL);
9607         ++idPath;
9608       }
9609     }
9610 
9611     return true;
9612   }
9613   catch (...)
9614   {
9615     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, strSQL.c_str());
9616   }
9617   return false;
9618 }
9619 
UpdateSources()9620  bool CMusicDatabase::UpdateSources()
9621 {
9622   //Check library and xml sources match
9623   VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
9624   if (CheckSources(sources))
9625     return true;
9626 
9627   try
9628   {
9629     // Empty sources table (related link tables removed by trigger);
9630     ExecuteQuery("DELETE FROM source");
9631 
9632     // Fill source table, and album sources
9633     for (const auto& source : sources)
9634       AddSource(source.strName, source.strPath, source.vecPaths);
9635 
9636     return true;
9637   }
9638   catch (...)
9639   {
9640     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9641   }
9642   return false;
9643 }
9644 
GetSources(CFileItemList & items)9645 bool CMusicDatabase::GetSources(CFileItemList& items)
9646 {
9647   try
9648   {
9649     if (nullptr == m_pDB)
9650       return false;
9651     if (nullptr == m_pDS)
9652       return false;
9653 
9654     // Get music sources and individual source paths (may not be scanned or have albums etc.)
9655     std::string strSQL = "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
9656       "FROM source JOIN source_path ON source.idSource = source_path.idSource "
9657       "ORDER BY source.idSource, source_path.idPath";
9658 
9659     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
9660     if (!m_pDS->query(strSQL))
9661       return false;
9662     int iRowsFound = m_pDS->num_rows();
9663     if (iRowsFound == 0)
9664     {
9665       m_pDS->close();
9666       return true;
9667     }
9668 
9669     // Get data from returned rows
9670     // Item has source ID in MusicInfotag, multipath in path, and indiviual paths in property
9671     CVariant sourcePaths(CVariant::VariantTypeArray);
9672     int idSource = -1;
9673     while (!m_pDS->eof())
9674     {
9675       if (idSource != m_pDS->fv("source.idSource").get_asInt())
9676       { // New source
9677         if (idSource > 0 && !sourcePaths.empty())
9678         {
9679           //Store paths for previous source in item list
9680           items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
9681           sourcePaths.clear();
9682         }
9683         idSource = m_pDS->fv("source.idSource").get_asInt();
9684         CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
9685         pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source");
9686         // Set tag URL for "file" property in AudioLibary processing
9687         pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString());
9688         // Set item path as source URL encoded multipath too
9689         pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString());
9690 
9691         pItem->m_bIsFolder = true;
9692         items.Add(pItem);
9693       }
9694       // Get path data
9695       sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString());
9696 
9697       m_pDS->next();
9698     }
9699     if (!sourcePaths.empty())
9700     {
9701       //Store paths for final source
9702       items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
9703       sourcePaths.clear();
9704     }
9705 
9706     // cleanup
9707     m_pDS->close();
9708 
9709     return true;
9710   }
9711   catch (...)
9712   {
9713     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9714   }
9715   return false;
9716 }
9717 
GetSourcesByArtist(int idArtist,CFileItem * item)9718 bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item)
9719 {
9720   if (nullptr == m_pDB)
9721     return false;
9722   if (nullptr == m_pDS)
9723     return false;
9724 
9725   try
9726   {
9727     std::string strSQL;
9728     strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
9729       "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
9730       "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
9731       "WHERE artist.idArtist = %i "
9732       "ORDER BY album_source.idSource", idArtist);
9733     if (!m_pDS->query(strSQL))
9734       return false;
9735     if (m_pDS->num_rows() == 0)
9736     {
9737       // Artist does have any source via albums may not be an album artist.
9738       // Check via songs fetch sources from compilations or where they are guest artist
9739       m_pDS->close();
9740       strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
9741         "JOIN song ON song_artist.idSong = song.idSong "
9742         "JOIN album_source ON album_source.idAlbum = song.idAlbum "
9743         "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
9744         "ORDER BY album_source.idSource", idArtist);
9745       if (!m_pDS->query(strSQL))
9746         return false;
9747       if (m_pDS->num_rows() == 0)
9748       {
9749         //No sources, but query sucessfull
9750         m_pDS->close();
9751         return true;
9752       }
9753     }
9754 
9755     CVariant artistSources(CVariant::VariantTypeArray);
9756     while (!m_pDS->eof())
9757     {
9758       artistSources.push_back(m_pDS->fv("idSource").get_asInt());
9759       m_pDS->next();
9760     }
9761     m_pDS->close();
9762 
9763     item->SetProperty("sourceid", artistSources);
9764     return true;
9765   }
9766   catch (...)
9767   {
9768     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
9769   }
9770   return false;
9771 }
9772 
GetSourcesByAlbum(int idAlbum,CFileItem * item)9773 bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item)
9774 {
9775   if (nullptr == m_pDB)
9776     return false;
9777   if (nullptr == m_pDS)
9778     return false;
9779 
9780   try
9781   {
9782     std::string strSQL;
9783     strSQL = PrepareSQL("SELECT idSource FROM album_source "
9784       "WHERE album_source.idAlbum = %i "
9785       "ORDER BY idSource", idAlbum);
9786     if (!m_pDS->query(strSQL))
9787       return false;
9788     CVariant albumSources(CVariant::VariantTypeArray);
9789     if (m_pDS->num_rows() > 0)
9790     {
9791       while (!m_pDS->eof())
9792       {
9793         albumSources.push_back(m_pDS->fv("idSource").get_asInt());
9794         m_pDS->next();
9795       }
9796       m_pDS->close();
9797     }
9798     else
9799     {
9800       //! @todo: handle singles, or don't waste time checking songs
9801       // Album does have any sources, may be a single??
9802       // Check via song paths, check each source path individually
9803       // usually fewer source paths than songs
9804       m_pDS->close();
9805 
9806       if (nullptr == m_pDS2)
9807         return false;
9808       strSQL = "SELECT idSource, strPath FROM source_path";
9809       if (!m_pDS->query(strSQL))
9810         return false;
9811       while (!m_pDS->eof())
9812       {
9813         std::string sourcepath = m_pDS->fv("strPath").get_asString();
9814         strSQL = PrepareSQL("SELECT 1 FROM song "
9815           "JOIN path ON song.idPath = path.idPath "
9816           "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'", idAlbum, sourcepath.c_str());
9817         if (!m_pDS2->query(strSQL))
9818           return false;
9819         if (m_pDS2->num_rows() > 0)
9820           albumSources.push_back(m_pDS->fv("idSource").get_asInt());
9821         m_pDS2->close();
9822 
9823         m_pDS->next();
9824       }
9825       m_pDS->close();
9826     }
9827 
9828 
9829     item->SetProperty("sourceid", albumSources);
9830     return true;
9831   }
9832   catch (...)
9833   {
9834     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
9835   }
9836   return false;
9837 }
9838 
GetSourcesBySong(int idSong,const std::string & strPath1,CFileItem * item)9839 bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item)
9840 {
9841   if (nullptr == m_pDB)
9842     return false;
9843   if (nullptr == m_pDS)
9844     return false;
9845 
9846   try
9847   {
9848     std::string strSQL;
9849     strSQL = PrepareSQL("SELECT idSource FROM song "
9850       "JOIN album_source ON album_source.idAlbum = song.idAlbum "
9851       "WHERE song.idSong = %i "
9852       "ORDER BY idSource", idSong);
9853     if (!m_pDS->query(strSQL))
9854       return false;
9855     if (m_pDS->num_rows() == 0 && !strPath1.empty())
9856     {
9857       // Check via song path instead
9858       m_pDS->close();
9859       std::string strPath(strPath1);
9860       if (!URIUtils::HasSlashAtEnd(strPath))
9861         URIUtils::AddSlashAtEnd(strPath);
9862 
9863       strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9864         "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", strPath.c_str());
9865       if (!m_pDS->query(strSQL))
9866         return false;
9867     }
9868     CVariant songSources(CVariant::VariantTypeArray);
9869     while (!m_pDS->eof())
9870     {
9871       songSources.push_back(m_pDS->fv("idSource").get_asInt());
9872       m_pDS->next();
9873     }
9874     m_pDS->close();
9875 
9876     item->SetProperty("sourceid", songSources);
9877     return true;
9878   }
9879   catch (...)
9880   {
9881     CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
9882   }
9883   return false;
9884 }
9885 
GetSourceByName(const std::string & strSource)9886 int CMusicDatabase::GetSourceByName(const std::string& strSource)
9887 {
9888   try
9889   {
9890     if (nullptr == m_pDB)
9891       return false;
9892     if (nullptr == m_pDS)
9893       return false;
9894 
9895     std::string strSQL;
9896     strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str());
9897     // run query
9898     if (!m_pDS->query(strSQL)) return false;
9899     int iRowsFound = m_pDS->num_rows();
9900     if (iRowsFound != 1)
9901     {
9902       m_pDS->close();
9903       return -1;
9904     }
9905     return m_pDS->fv("idSource").get_asInt();
9906   }
9907   catch (...)
9908   {
9909     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9910   }
9911   return -1;
9912 }
9913 
GetSourceById(int id)9914 std::string CMusicDatabase::GetSourceById(int id)
9915 {
9916   return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id));
9917 }
9918 
GetArtistByName(const std::string & strArtist)9919 int CMusicDatabase::GetArtistByName(const std::string& strArtist)
9920 {
9921   try
9922   {
9923     if (nullptr == m_pDB)
9924       return false;
9925     if (nullptr == m_pDS)
9926       return false;
9927 
9928     std::string strSQL=PrepareSQL("select idArtist from artist where artist.strArtist like '%s'", strArtist.c_str());
9929 
9930     // run query
9931     if (!m_pDS->query(strSQL)) return false;
9932     int iRowsFound = m_pDS->num_rows();
9933     if (iRowsFound != 1)
9934     {
9935       m_pDS->close();
9936       return -1;
9937     }
9938     int lResult = m_pDS->fv("artist.idArtist").get_asInt();
9939     m_pDS->close();
9940     return lResult;
9941   }
9942   catch (...)
9943   {
9944     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9945   }
9946   return -1;
9947 }
9948 
GetArtistByMatch(const CArtist & artist)9949 int CMusicDatabase::GetArtistByMatch(const CArtist& artist)
9950 {
9951   std::string strSQL;
9952   try
9953   {
9954     if (nullptr == m_pDB || nullptr == m_pDS)
9955       return false;
9956     // Match on MusicBrainz ID, definitively unique
9957     if (!artist.strMusicBrainzArtistID.empty())
9958       strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
9959         artist.strMusicBrainzArtistID.c_str());
9960     else
9961     // No MusicBrainz ID, artist by name with no mbid
9962     strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
9963       artist.strArtist.c_str());
9964     if (!m_pDS->query(strSQL)) return false;
9965     int iRowsFound = m_pDS->num_rows();
9966     if (iRowsFound != 1)
9967     {
9968       m_pDS->close();
9969       // Match on artist name, relax mbid restriction
9970       return GetArtistByName(artist.strArtist);
9971     }
9972     int lResult = m_pDS->fv("idArtist").get_asInt();
9973     m_pDS->close();
9974     return lResult;
9975   }
9976   catch (...)
9977   {
9978     CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %", __FUNCTION__, strSQL.c_str());
9979   }
9980   return -1;
9981 }
9982 
GetArtistFromSong(int idSong,CArtist & artist)9983 bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist &artist)
9984 {
9985   try
9986   {
9987     if (nullptr == m_pDB)
9988       return false;
9989     if (nullptr == m_pDS)
9990       return false;
9991 
9992     std::string strSQL = PrepareSQL(
9993       "SELECT artistview.* FROM song_artist "
9994       "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
9995       "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
9996       idSong);
9997     if (!m_pDS->query(strSQL)) return false;
9998     int iRowsFound = m_pDS->num_rows();
9999     if (iRowsFound != 1)
10000     {
10001       m_pDS->close();
10002       return false;
10003     }
10004 
10005     artist = GetArtistFromDataset(m_pDS.get());
10006 
10007     m_pDS->close();
10008     return true;
10009 
10010   }
10011   catch (...)
10012   {
10013     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10014   }
10015   return false;
10016 }
10017 
IsSongArtist(int idSong,int idArtist)10018 bool CMusicDatabase::IsSongArtist(int idSong, int idArtist)
10019 {
10020   std::string strSQL = PrepareSQL(
10021     "SELECT 1 FROM song_artist "
10022     "WHERE song_artist.idSong= %i AND "
10023     "song_artist.idArtist = %i AND song_artist.idRole = 1",
10024     idSong, idArtist);
10025   return GetSingleValue(strSQL).empty();
10026 }
10027 
IsSongAlbumArtist(int idSong,int idArtist)10028 bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist)
10029 {
10030   std::string strSQL = PrepareSQL(
10031     "SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
10032     "WHERE song.idSong = %i AND album_artist.idArtist = %i",
10033     idSong, idArtist);
10034   return GetSingleValue(strSQL).empty();
10035 }
10036 
IsAlbumBoxset(int idAlbum)10037 bool CMusicDatabase::IsAlbumBoxset(int idAlbum)
10038 {
10039   std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum);
10040   int isBoxSet = GetSingleValueInt(strSQL);
10041   return (isBoxSet == 1 ? true : false);
10042 }
10043 
GetAlbumByName(const std::string & strAlbum,const std::string & strArtist)10044 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist)
10045 {
10046   try
10047   {
10048     if (nullptr == m_pDB)
10049       return false;
10050     if (nullptr == m_pDS)
10051       return false;
10052 
10053     std::string strSQL;
10054     if (strArtist.empty())
10055       strSQL=PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str());
10056     else
10057       strSQL=PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'", strAlbum.c_str(),strArtist.c_str());
10058     // run query
10059     if (!m_pDS->query(strSQL)) return false;
10060     int iRowsFound = m_pDS->num_rows();
10061     if (iRowsFound != 1)
10062     {
10063       m_pDS->close();
10064       return -1;
10065     }
10066     return m_pDS->fv("idAlbum").get_asInt();
10067   }
10068   catch (...)
10069   {
10070     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10071   }
10072   return -1;
10073 }
10074 
GetMatchingMusicVideoAlbum(const std::string & strAlbum,const std::string & strArtist,int & idAlbum,std::string & strReview)10075 bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum,
10076                                                 const std::string& strArtist,
10077                                                 int& idAlbum,
10078                                                 std::string& strReview)
10079 {
10080   /*
10081     Get the first album that matches with the title and artist display name.
10082     Artist(s) and album title may not be sufficient to uniquely identify a match since library can
10083     store multiple releases, and occasionally artists even have different albums with same name.
10084     Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
10085   */
10086   try
10087   {
10088     if (nullptr == m_pDB)
10089       return false;
10090     if (nullptr == m_pDS)
10091       return false;
10092 
10093     std::string strSQL;
10094     if (strArtist.empty())
10095       strSQL =
10096           PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
10097                      strAlbum.c_str());
10098     else
10099       strSQL =
10100           PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s' AND "
10101                      "album.strArtistDisp LIKE '%s'",
10102                      strAlbum.c_str(), strArtist.c_str());
10103     // run query
10104     if (!m_pDS->query(strSQL))
10105       return false;
10106     int iRowsFound = m_pDS->num_rows();
10107     if (iRowsFound > 0)
10108     {
10109       idAlbum = m_pDS->fv("idAlbum").get_asInt();
10110       strReview = m_pDS->fv("strReview").get_asString();
10111       return true;
10112     }
10113   }
10114   catch (...)
10115   {
10116     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10117   }
10118   return false;
10119 }
10120 
SearchAlbumsByArtistName(const std::string & strArtist,CFileItemList & items)10121 bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items)
10122 {
10123   try
10124   {
10125     if (nullptr == m_pDB)
10126       return false;
10127     if (nullptr == m_pDS)
10128       return false;
10129 
10130     std::string strSQL;
10131     strSQL = PrepareSQL("SELECT albumview.* FROM albumview "
10132       "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
10133       "WHERE  album_artist.strArtist LIKE '%s'",
10134       strArtist.c_str());
10135 
10136     if (!m_pDS->query(strSQL))
10137       return false;
10138 
10139     while (!m_pDS->eof())
10140     {
10141       CAlbum album = GetAlbumFromDataset(m_pDS.get());
10142       std::string path = StringUtils::Format("musicdb://albums/%ld/", album.idAlbum);
10143       CFileItemPtr pItem(new CFileItem(path, album));
10144       std::string label =
10145         StringUtils::Format("%s (%i)", album.strAlbum, pItem->GetMusicInfoTag()->GetYear());
10146       pItem->SetLabel(label);
10147       items.Add(pItem);
10148       m_pDS->next();
10149     }
10150     m_pDS->close(); // cleanup recordset data
10151     return true;
10152   }
10153   catch (...)
10154   {
10155     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10156   }
10157   return false;
10158 }
10159 
GetAlbumByName(const std::string & strAlbum,const std::vector<std::string> & artist)10160 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::vector<std::string>& artist)
10161 {
10162   return GetAlbumByName(strAlbum, StringUtils::Join(artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
10163 }
10164 
GetAlbumByMatch(const CAlbum & album)10165 int CMusicDatabase::GetAlbumByMatch(const CAlbum &album)
10166 {
10167   std::string strSQL;
10168   try
10169   {
10170     if (nullptr == m_pDB || nullptr == m_pDS)
10171       return false;
10172     // Match on MusicBrainz ID, definitively unique
10173     if (!album.strMusicBrainzAlbumID.empty())
10174       strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'", album.strMusicBrainzAlbumID.c_str());
10175     else
10176       // No mbid, match on album title and album artist descriptive string, ignore those with mbid
10177       strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' AND strMusicBrainzAlbumID IS NULL",
10178         album.GetAlbumArtistString().c_str(),
10179         album.strAlbum.c_str());
10180     m_pDS->query(strSQL);
10181     if (!m_pDS->query(strSQL)) return false;
10182     int iRowsFound = m_pDS->num_rows();
10183     if (iRowsFound != 1)
10184     {
10185       m_pDS->close();
10186       // Match on album title and album artist descriptive string, relax mbid restriction
10187       return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString());
10188     }
10189     int lResult = m_pDS->fv("idAlbum").get_asInt();
10190     m_pDS->close();
10191     return lResult;
10192   }
10193   catch (...)
10194   {
10195     CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %", __FUNCTION__, strSQL.c_str());
10196   }
10197   return -1;
10198 }
10199 
GetGenreById(int id)10200 std::string CMusicDatabase::GetGenreById(int id)
10201 {
10202   return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id));
10203 }
10204 
GetArtistById(int id)10205 std::string CMusicDatabase::GetArtistById(int id)
10206 {
10207   return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id));
10208 }
10209 
GetRoleById(int id)10210 std::string CMusicDatabase::GetRoleById(int id)
10211 {
10212   return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id));
10213 }
10214 
UpdateArtistSortNames(int idArtist)10215 bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/)
10216 {
10217   // Propagate artist sort names into concatenated artist sort name string for songs and albums
10218   // Avoid updating records where sort same as strArtistDisp
10219   std::string strSQL;
10220 
10221   // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
10222   // (not handled by PrepareSQL)
10223   bool bisMySQL = StringUtils::EqualsNoCase(
10224       CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
10225 
10226   BeginMultipleExecute();
10227   if (bisMySQL)
10228     strSQL = "(SELECT GROUP_CONCAT("
10229              "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10230              "ELSE artist.strSortName END "
10231              "ORDER BY album_artist.idAlbum, album_artist.iOrder "
10232              "SEPARATOR '; ') as val "
10233              "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10234              "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
10235   else
10236     strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10237              "FROM(SELECT album_artist.idAlbum, "
10238              "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10239              "ELSE artist.strSortName END as val "
10240              "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10241              "WHERE album_artist.idAlbum = album.idAlbum "
10242              "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
10243 
10244   strSQL = "UPDATE album SET strArtistSort = " + strSQL +
10245            "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
10246            "AND strArtistDisp <> " + strSQL;
10247   if (idArtist > 0)
10248     strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
10249       "AND album_artist.idAlbum = album.idAlbum)", idArtist);
10250   ExecuteQuery(strSQL);
10251   CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
10252 
10253   if (bisMySQL)
10254     strSQL = "(SELECT GROUP_CONCAT("
10255              "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10256              "ELSE artist.strSortName END "
10257              "ORDER BY song_artist.idSong, song_artist.iOrder "
10258              "SEPARATOR '; ') as val "
10259              "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10260              "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
10261   else
10262     strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10263              "FROM(SELECT song_artist.idSong, "
10264              "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10265              "ELSE artist.strSortName END as val "
10266              "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10267              "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
10268              "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
10269 
10270   strSQL = "UPDATE song SET strArtistSort = " + strSQL +
10271            "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
10272            "AND strArtistDisp <> " + strSQL;
10273   if (idArtist > 0)
10274     strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
10275       "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)", idArtist);
10276   ExecuteQuery(strSQL);
10277   CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
10278 
10279   if (CommitMultipleExecute())
10280     return true;
10281   else
10282     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10283   return false;
10284 }
10285 
GetAlbumById(int id)10286 std::string CMusicDatabase::GetAlbumById(int id)
10287 {
10288   return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id));
10289 }
10290 
GetGenreByName(const std::string & strGenre)10291 int CMusicDatabase::GetGenreByName(const std::string& strGenre)
10292 {
10293   try
10294   {
10295     if (nullptr == m_pDB)
10296       return false;
10297     if (nullptr == m_pDS)
10298       return false;
10299 
10300     std::string strSQL;
10301     strSQL=PrepareSQL("select idGenre from genre where genre.strGenre like '%s'", strGenre.c_str());
10302     // run query
10303     if (!m_pDS->query(strSQL)) return false;
10304     int iRowsFound = m_pDS->num_rows();
10305     if (iRowsFound != 1)
10306     {
10307       m_pDS->close();
10308       return -1;
10309     }
10310     return m_pDS->fv("genre.idGenre").get_asInt();
10311   }
10312   catch (...)
10313   {
10314     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10315   }
10316   return -1;
10317 }
10318 
GetGenresJSON(CFileItemList & items,bool bSources)10319 bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources)
10320 {
10321   std::string strSQL;
10322   try
10323   {
10324     if (nullptr == m_pDB)
10325       return false;
10326     if (nullptr == m_pDS)
10327       return false;
10328 
10329     strSQL = "SELECT %s FROM genre ";
10330     Filter extFilter;
10331     extFilter.AppendField("genre.idGenre");
10332     extFilter.AppendField("genre.strGenre");
10333     if (bSources)
10334     {
10335       strSQL = "SELECT DISTINCT %s FROM genre ";
10336       extFilter.AppendField("album_source.idSource");
10337       extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
10338       extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
10339       extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
10340       extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
10341       extFilter.AppendOrder("genre.strGenre");
10342       extFilter.AppendOrder("album_source.idSource");
10343     }
10344     extFilter.AppendWhere("genre.strGenre != ''");
10345 
10346     std::string strSQLExtra;
10347     if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
10348       return false;
10349 
10350     strSQL = PrepareSQL(strSQL.c_str(), extFilter.fields.c_str()) + strSQLExtra;
10351 
10352     // run query
10353     CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
10354 
10355     if (!m_pDS->query(strSQL))
10356       return false;
10357     int iRowsFound = m_pDS->num_rows();
10358     if (iRowsFound == 0)
10359     {
10360       m_pDS->close();
10361       return true;
10362     }
10363 
10364     if (!bSources)
10365       items.Reserve(iRowsFound);
10366 
10367     // Get data from returned rows
10368     // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
10369     CVariant genreSources(CVariant::VariantTypeArray);
10370     int idGenre = -1;
10371     while (!m_pDS->eof())
10372     {
10373       if (idGenre != m_pDS->fv("genre.idGenre").get_asInt())
10374       { // New genre
10375         if (idGenre > 0 && bSources)
10376         {
10377           //Store sources for previous genre in item list
10378           items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
10379           genreSources.clear();
10380         }
10381         idGenre = m_pDS->fv("genre.idGenre").get_asInt();
10382         std::string strGenre = m_pDS->fv("genre.strGenre").get_asString();
10383         CFileItemPtr pItem(new CFileItem(strGenre));
10384         pItem->GetMusicInfoTag()->SetTitle(strGenre);
10385         pItem->GetMusicInfoTag()->SetGenre(strGenre);
10386         pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre");
10387         pItem->SetPath(StringUtils::Format("musicdb://genres/%i/", idGenre));
10388         pItem->m_bIsFolder = true;
10389         items.Add(pItem);
10390       }
10391       // Get source data
10392       if (bSources)
10393       {
10394         int sourceid = m_pDS->fv("album_source.idSource").get_asInt();
10395         if (sourceid > 0)
10396           genreSources.push_back(sourceid);
10397       }
10398       m_pDS->next();
10399     }
10400     if (bSources)
10401     {
10402       //Store sources for final genre
10403       items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
10404     }
10405 
10406     // cleanup
10407     m_pDS->close();
10408 
10409     return true;
10410   }
10411   catch (...)
10412   {
10413     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, strSQL.c_str());
10414   }
10415   return false;
10416 }
10417 
GetAlbumDiscTitle(int idAlbum,int idDisc)10418 std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc)
10419 {
10420   // Get disc node title from ids allowing for "*all"
10421   std::string disctitle;
10422   std::string albumtitle;
10423   if (idAlbum > 0)
10424     albumtitle = GetAlbumById(idAlbum);
10425   if (idDisc > 0)
10426   {
10427     disctitle = GetSingleValue("song", "strDiscSubtitle",
10428                                PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc));
10429     if (disctitle.empty())
10430       disctitle = StringUtils::Format("%s %i", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc.
10431     if (albumtitle.empty())
10432       albumtitle = disctitle;
10433     else
10434       albumtitle = albumtitle + " - " + disctitle;
10435   }
10436   return albumtitle;
10437 }
10438 
GetBoxsetsCount()10439 int CMusicDatabase::GetBoxsetsCount()
10440 {
10441   return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
10442 }
10443 
GetAlbumDiscsCount(int idAlbum)10444 int CMusicDatabase::GetAlbumDiscsCount(int idAlbum)
10445 {
10446   std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum);
10447   return GetSingleValueInt(strSQL);
10448 }
10449 
GetCompilationAlbumsCount()10450 int CMusicDatabase::GetCompilationAlbumsCount()
10451 {
10452   return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
10453 }
10454 
GetSinglesCount()10455 int CMusicDatabase::GetSinglesCount()
10456 {
10457   CDatabase::Filter filter(PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')", CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
10458   return GetSongsCount(filter);
10459 }
10460 
GetArtistCountForRole(int role)10461 int CMusicDatabase::GetArtistCountForRole(int role)
10462 {
10463   std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role);
10464   return GetSingleValueInt(strSQL);
10465 }
10466 
GetArtistCountForRole(const std::string & strRole)10467 int CMusicDatabase::GetArtistCountForRole(const std::string& strRole)
10468 {
10469   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());
10470   return GetSingleValueInt(strSQL);
10471 }
10472 
SetPathHash(const std::string & path,const std::string & hash)10473 bool CMusicDatabase::SetPathHash(const std::string &path, const std::string &hash)
10474 {
10475   try
10476   {
10477     if (nullptr == m_pDB)
10478       return false;
10479     if (nullptr == m_pDS)
10480       return false;
10481 
10482     if (hash.empty())
10483     { // this is an empty folder - we need only add it to the path table
10484       // if the path actually exists
10485       if (!CDirectory::Exists(path))
10486         return false;
10487     }
10488     int idPath = AddPath(path);
10489     if (idPath < 0) return false;
10490 
10491     std::string strSQL=PrepareSQL("update path set strHash='%s' where idPath=%ld", hash.c_str(), idPath);
10492     m_pDS->exec(strSQL);
10493 
10494     return true;
10495   }
10496   catch (...)
10497   {
10498     CLog::Log(LOGERROR, "%s (%s, %s) failed", __FUNCTION__, path.c_str(), hash.c_str());
10499   }
10500 
10501   return false;
10502 }
10503 
GetPathHash(const std::string & path,std::string & hash)10504 bool CMusicDatabase::GetPathHash(const std::string &path, std::string &hash)
10505 {
10506   try
10507   {
10508     if (nullptr == m_pDB)
10509       return false;
10510     if (nullptr == m_pDS)
10511       return false;
10512 
10513     std::string strSQL=PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
10514     m_pDS->query(strSQL);
10515     if (m_pDS->num_rows() == 0)
10516       return false;
10517     hash = m_pDS->fv("strHash").get_asString();
10518     return true;
10519   }
10520   catch (...)
10521   {
10522     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, path.c_str());
10523   }
10524 
10525   return false;
10526 }
10527 
RemoveSongsFromPath(const std::string & path1,MAPSONGS & songmap,bool exact)10528 bool CMusicDatabase::RemoveSongsFromPath(const std::string &path1, MAPSONGS& songmap, bool exact)
10529 {
10530   // We need to remove all songs from this path, as their tags are going
10531   // to be re-read.  We need to remove all songs from the song table + all links to them
10532   // from the song link tables (as otherwise if a song is added back
10533   // to the table with the same idSong, these tables can't be cleaned up properly later)
10534 
10535   //! @todo SQLite probably doesn't allow this, but can we rely on that??
10536 
10537   // We don't need to remove orphaned albums at this point as in AddAlbum() we check
10538   // first whether the album has already been read during this scan, and if it hasn't
10539   // we check whether it's in the table and update accordingly at that point, removing the entries from
10540   // the album link tables.  The only failure point for this is albums
10541   // that span multiple folders, where just the files in one folder have been changed.  In this case
10542   // any linked fields that are only in the files that haven't changed will be removed.  Clearly
10543   // the primary albumartist still matches (as that's what we looked up based on) so is this really
10544   // an issue?  I don't think it is, as those artists will still have links to the album via the songs
10545   // which is generally what we rely on, so the only failure point is albumartist lookup.  In this
10546   // case, it will return only things in the album_artist table from the newly updated songs (and
10547   // only if they have additional artists).  I think the effect of this is minimal at best, as ALL
10548   // songs in the album should have the same albumartist!
10549 
10550   // we also remove the path at this point as it will be added later on if the
10551   // path still exists.
10552   // After scanning we then remove the orphaned artists, genres and thumbs.
10553 
10554   // Note: when used to remove all songs from a path and its subpath (exact=false), this
10555   // does miss archived songs.
10556   std::string path(path1);
10557   SetLibraryLastUpdated();
10558   try
10559   {
10560     if (!URIUtils::HasSlashAtEnd(path))
10561       URIUtils::AddSlashAtEnd(path);
10562 
10563     if (nullptr == m_pDB)
10564       return false;
10565     if (nullptr == m_pDS)
10566       return false;
10567 
10568     // Filename is not unique for a path as songs from a cuesheet have same filename.
10569     // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
10570     // in a folder and some edited and rescanned.
10571     // Hence order by filename so these songs can be gathered together.
10572     std::string where;
10573     if (exact)
10574       where = PrepareSQL(" where strPath='%s'", path.c_str());
10575     else
10576       where = PrepareSQL(" where SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()), path.c_str());
10577     std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName";
10578     if (!m_pDS->query(sql)) return false;
10579     int iRowsFound = m_pDS->num_rows();
10580     if (iRowsFound > 0)
10581     {
10582       // Each file is potentially mapped to a list of songs, gather these and save as list
10583       VECSONGS songs;
10584       std::string filename;
10585       std::vector<std::string> songIds;
10586       while (!m_pDS->eof())
10587       {
10588         CSong song = GetSongFromDataset();
10589         if (!filename.empty() && filename != song.strFileName)
10590         {
10591           // Save songs for previous filename
10592           songmap.insert(std::make_pair(filename, songs));
10593           songs.clear();
10594         }
10595         song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb");
10596         songs.emplace_back(song);
10597         songIds.push_back(PrepareSQL("%i", song.idSong));
10598         filename = song.strFileName;
10599 
10600         m_pDS->next();
10601       }
10602       m_pDS->close();
10603       songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
10604 
10605       //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
10606       for (const auto& id : songIds)
10607         AnnounceRemove(MediaTypeSong, atoi(id.c_str()));
10608 
10609       // Delete all songs, and anything linked to them via triggers
10610       std::string strIDs = StringUtils::Join(songIds, ",");
10611       sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")";
10612       m_pDS->exec(sql);
10613     }
10614     // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
10615     sql = "delete from path" + where;
10616     m_pDS->exec(sql);
10617     return iRowsFound > 0;
10618   }
10619   catch (...)
10620   {
10621     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, path.c_str());
10622   }
10623   return false;
10624 }
10625 
CheckArtistLinksChanged()10626 void CMusicDatabase::CheckArtistLinksChanged()
10627 {
10628   std::string strSQL = "SELECT COUNT(1) FROM removed_link ";
10629   int iLinks = GetSingleValueInt(strSQL, m_pDS);
10630   if (iLinks > 0)
10631   {
10632     SetArtistLinksUpdated(); // Store datetime artist links last updated
10633     DeleteRemovedLinks(); // Clean-up artist links
10634   }
10635 }
10636 
GetPaths(std::set<std::string> & paths)10637 bool CMusicDatabase::GetPaths(std::set<std::string> &paths)
10638 {
10639   try
10640   {
10641     if (nullptr == m_pDB)
10642       return false;
10643     if (nullptr == m_pDS)
10644       return false;
10645 
10646     paths.clear();
10647 
10648     // find all paths
10649     if (!m_pDS->query("select strPath from path")) return false;
10650     int iRowsFound = m_pDS->num_rows();
10651     if (iRowsFound == 0)
10652     {
10653       m_pDS->close();
10654       return true;
10655     }
10656     while (!m_pDS->eof())
10657     {
10658       paths.insert(m_pDS->fv("strPath").get_asString());
10659       m_pDS->next();
10660     }
10661     m_pDS->close();
10662     return true;
10663   }
10664   catch (...)
10665   {
10666     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10667   }
10668   return false;
10669 }
10670 
SetSongUserrating(const std::string & filePath,int userrating)10671 bool CMusicDatabase::SetSongUserrating(const std::string &filePath, int userrating)
10672 {
10673   try
10674   {
10675     if (filePath.empty()) return false;
10676     if (nullptr == m_pDB)
10677       return false;
10678     if (nullptr == m_pDS)
10679       return false;
10680 
10681     int songID = GetSongIDFromPath(filePath);
10682     if (-1 == songID) return false;
10683 
10684     return SetSongUserrating(songID, userrating);
10685   }
10686   catch (...)
10687   {
10688     CLog::Log(LOGERROR, "%s (%s,%i) failed", __FUNCTION__, filePath.c_str(), userrating);
10689   }
10690   return false;
10691 }
10692 
SetSongUserrating(int idSong,int userrating)10693 bool CMusicDatabase::SetSongUserrating(int idSong, int userrating)
10694 {
10695   try
10696   {
10697     if (nullptr == m_pDB)
10698       return false;
10699     if (nullptr == m_pDS)
10700       return false;
10701 
10702     std::string sql =
10703         PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong);
10704     m_pDS->exec(sql);
10705     return true;
10706   }
10707   catch (...)
10708   {
10709     CLog::Log(LOGERROR, "%s (%i,%i) failed", __FUNCTION__, idSong, userrating);
10710   }
10711   return false;
10712 }
10713 
SetAlbumUserrating(const int idAlbum,int userrating)10714 bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating)
10715 {
10716   try
10717   {
10718     if (nullptr == m_pDB)
10719       return false;
10720     if (nullptr == m_pDS)
10721       return false;
10722 
10723     if (-1 == idAlbum) return false;
10724     std::string sql =
10725         PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum);
10726     m_pDS->exec(sql);
10727     return true;
10728   }
10729   catch (...)
10730   {
10731     CLog::Log(LOGERROR, "%s (%i,%i) failed", __FUNCTION__, idAlbum, userrating);
10732   }
10733   return false;
10734 }
10735 
SetSongVotes(const std::string & filePath,int votes)10736 bool CMusicDatabase::SetSongVotes(const std::string &filePath, int votes)
10737 {
10738   try
10739   {
10740     if (filePath.empty()) return false;
10741     if (nullptr == m_pDB)
10742       return false;
10743     if (nullptr == m_pDS)
10744       return false;
10745 
10746     int songID = GetSongIDFromPath(filePath);
10747     if (-1 == songID) return false;
10748 
10749     std::string sql =
10750       PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID);
10751 
10752     m_pDS->exec(sql);
10753     return true;
10754   }
10755   catch (...)
10756   {
10757     CLog::Log(LOGERROR, "%s (%s,%i) failed", __FUNCTION__, filePath.c_str(), votes);
10758   }
10759   return false;
10760 }
10761 
GetSongIDFromPath(const std::string & filePath)10762 int CMusicDatabase::GetSongIDFromPath(const std::string &filePath)
10763 {
10764   // grab the where string to identify the song id
10765   CURL url(filePath);
10766   if (url.IsProtocol("musicdb"))
10767   {
10768     std::string strFile=URIUtils::GetFileName(filePath);
10769     URIUtils::RemoveExtension(strFile);
10770     return atoi(strFile.c_str());
10771   }
10772   // hit the db
10773   try
10774   {
10775     if (nullptr == m_pDB)
10776       return -1;
10777     if (nullptr == m_pDS)
10778       return -1;
10779 
10780     std::string strPath, strFileName;
10781     SplitPath(filePath, strPath, strFileName);
10782     URIUtils::AddSlashAtEnd(strPath);
10783 
10784     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());
10785     if (!m_pDS->query(sql)) return -1;
10786 
10787     if (m_pDS->num_rows() == 0)
10788     {
10789       m_pDS->close();
10790       return -1;
10791     }
10792 
10793     int songID = m_pDS->fv("idSong").get_asInt();
10794     m_pDS->close();
10795     return songID;
10796   }
10797   catch (...)
10798   {
10799     CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, filePath.c_str());
10800   }
10801   return -1;
10802 }
10803 
CommitTransaction()10804 bool CMusicDatabase::CommitTransaction()
10805 {
10806   if (CDatabase::CommitTransaction())
10807   { // number of items in the db has likely changed, so reset the infomanager cache
10808     CGUIComponent* gui = CServiceBroker::GetGUI();
10809     if (gui)
10810     {
10811       gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(LIBRARY_HAS_MUSIC, GetSongsCount() > 0);
10812       return true;
10813     }
10814   }
10815   return false;
10816 }
10817 
SetScraperAll(const std::string & strBaseDir,const ADDON::ScraperPtr & scraper)10818 bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper)
10819 {
10820   if (nullptr == m_pDB)
10821     return false;
10822   if (nullptr == m_pDS)
10823     return false;
10824   std::string strSQL;
10825   int idSetting = -1;
10826   try
10827   {
10828     CONTENT_TYPE content = CONTENT_NONE;
10829 
10830     // Build where clause from virtual path
10831     Filter extFilter;
10832     CMusicDbUrl musicUrl;
10833     SortDescription sorting;
10834     if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
10835       return false;
10836 
10837     std::string itemType = musicUrl.GetType();
10838     if (StringUtils::EqualsNoCase(itemType, "artists"))
10839     {
10840       content = CONTENT_ARTISTS;
10841     }
10842     else if (StringUtils::EqualsNoCase(itemType, "albums"))
10843     {
10844       content = CONTENT_ALBUMS;
10845     }
10846     else
10847       return false;  //Only artists and albums have info settings
10848 
10849     std::string strSQLWhere;
10850     if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere))
10851       return false;
10852 
10853     // Replace view names with table names
10854     StringUtils::Replace(strSQLWhere, "artistview", "artist");
10855     StringUtils::Replace(strSQLWhere, "albumview", "album");
10856 
10857     BeginTransaction();
10858     // Clear current scraper settings (0 => default scraper used)
10859     if (content == CONTENT_ARTISTS)
10860       strSQL = "UPDATE artist SET idInfoSetting = %i ";
10861     else
10862       strSQL = "UPDATE album SET idInfoSetting = %i ";
10863     strSQL = PrepareSQL(strSQL, 0) + strSQLWhere;
10864     m_pDS->exec(strSQL);
10865 
10866     //Remove orphaned settings
10867     CleanupInfoSettings();
10868 
10869     if (scraper)
10870     {
10871       // Add new info setting
10872       strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
10873       strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
10874       m_pDS->exec(strSQL);
10875       idSetting = static_cast<int>(m_pDS->lastinsertid());
10876 
10877       if (content == CONTENT_ARTISTS)
10878         strSQL = "UPDATE artist SET idInfoSetting = %i ";
10879       else
10880         strSQL = "UPDATE album SET idInfoSetting = %i ";
10881       strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere;
10882       m_pDS->exec(strSQL);
10883     }
10884     CommitTransaction();
10885     return true;
10886   }
10887   catch (...)
10888   {
10889     RollbackTransaction();
10890     CLog::Log(LOGERROR, "%s - (%s, %s) failed", __FUNCTION__, strBaseDir.c_str(), strSQL.c_str());
10891   }
10892   return false;
10893 }
10894 
SetScraper(int id,const CONTENT_TYPE & content,const ADDON::ScraperPtr & scraper)10895 bool CMusicDatabase::SetScraper(int id,
10896                                 const CONTENT_TYPE& content,
10897                                 const ADDON::ScraperPtr& scraper)
10898 {
10899   if (nullptr == m_pDB)
10900     return false;
10901   if (nullptr == m_pDS)
10902     return false;
10903   std::string strSQL;
10904   int idSetting = -1;
10905   try
10906   {
10907     BeginTransaction();
10908     // Fetch current info settings for item, 0 => default is used
10909     if (content == CONTENT_ARTISTS)
10910       strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
10911     else
10912       strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
10913     strSQL = PrepareSQL(strSQL, id);
10914     m_pDS->query(strSQL);
10915     if (m_pDS->num_rows() > 0)
10916       idSetting = m_pDS->fv("idInfoSetting").get_asInt();
10917     m_pDS->close();
10918 
10919     if (idSetting < 1)
10920     { // Add new info setting
10921       strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
10922       strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
10923       m_pDS->exec(strSQL);
10924       idSetting = static_cast<int>(m_pDS->lastinsertid());
10925 
10926       if (content == CONTENT_ARTISTS)
10927         strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
10928       else
10929         strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
10930       strSQL = PrepareSQL(strSQL, idSetting, id);
10931       m_pDS->exec(strSQL);
10932     }
10933     else
10934     {  // Update info setting
10935       strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' WHERE idSetting = %i";
10936       strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting);
10937       m_pDS->exec(strSQL);
10938     }
10939     CommitTransaction();
10940     return true;
10941   }
10942   catch (...)
10943   {
10944     RollbackTransaction();
10945     CLog::Log(LOGERROR, "%s - (%i, %s) failed", __FUNCTION__, id, strSQL.c_str());
10946   }
10947   return false;
10948 }
10949 
GetScraper(int id,const CONTENT_TYPE & content,ADDON::ScraperPtr & scraper)10950 bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE &content, ADDON::ScraperPtr& scraper)
10951 {
10952   std::string scraperUUID;
10953   std::string strSettings;
10954   try
10955   {
10956     if (nullptr == m_pDB)
10957       return false;
10958     if (nullptr == m_pDS)
10959       return false;
10960 
10961     std::string strSQL;
10962     strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
10963     if (content == CONTENT_ARTISTS)
10964       strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting WHERE artist.idArtist = %i";
10965     else
10966       strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting WHERE album.idAlbum = %i";
10967     strSQL = PrepareSQL(strSQL, id);
10968     m_pDS->query(strSQL);
10969     if (!m_pDS->eof())
10970     { // try and ascertain scraper
10971       scraperUUID = m_pDS->fv("strScraperPath").get_asString();
10972       strSettings = m_pDS->fv("strSettings").get_asString();
10973 
10974       // Use pre configured or default scraper
10975       ADDON::AddonPtr addon;
10976       if (!scraperUUID.empty() &&
10977           CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon, ADDON::ADDON_UNKNOWN,
10978                                                  ADDON::OnlyEnabled::YES) &&
10979           addon)
10980       {
10981         scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
10982         if (scraper)
10983           // Set settings
10984           scraper->SetPathSettings(content, strSettings);
10985       }
10986     }
10987     m_pDS->close();
10988 
10989     if (!scraper)
10990     { // use default music scraper instead
10991       ADDON::AddonPtr addon;
10992       if(ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::ScraperTypeFromContent(content), addon))
10993       {
10994         scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
10995         return scraper != NULL;
10996       }
10997       else
10998         return false;
10999     }
11000 
11001     return true;
11002   }
11003   catch (...)
11004   {
11005     CLog::Log(LOGERROR, "%s -(%i, %s %s) failed", __FUNCTION__, id, scraperUUID.c_str(), strSettings.c_str());
11006   }
11007   return false;
11008 }
11009 
ScraperInUse(const std::string & scraperID) const11010 bool CMusicDatabase::ScraperInUse(const std::string &scraperID) const
11011 {
11012   try
11013   {
11014     if (nullptr == m_pDB)
11015       return false;
11016     if (nullptr == m_pDS)
11017       return false;
11018 
11019     std::string sql = PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'",scraperID.c_str());
11020     if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
11021     {
11022       m_pDS->close();
11023       return false;
11024     }
11025     bool found = m_pDS->fv(0).get_asInt() > 0;
11026     m_pDS->close();
11027     return found;
11028   }
11029   catch (...)
11030   {
11031     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, scraperID.c_str());
11032   }
11033   return false;
11034 }
11035 
GetItems(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,const SortDescription & sortDescription)11036 bool CMusicDatabase::GetItems(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */)
11037 {
11038   CMusicDbUrl musicUrl;
11039   if (!musicUrl.FromString(strBaseDir))
11040     return false;
11041 
11042   return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription);
11043 }
11044 
GetItems(const std::string & strBaseDir,const std::string & itemType,CFileItemList & items,const Filter & filter,const SortDescription & sortDescription)11045 bool CMusicDatabase::GetItems(const std::string &strBaseDir, const std::string &itemType, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */)
11046 {
11047   if (StringUtils::EqualsNoCase(itemType, "genres"))
11048     return GetGenresNav(strBaseDir, items, filter);
11049   else if (StringUtils::EqualsNoCase(itemType, "sources"))
11050     return GetSourcesNav(strBaseDir, items, filter);
11051   else if (StringUtils::EqualsNoCase(itemType, "years"))
11052     return GetYearsNav(strBaseDir, items, filter);
11053   else if (StringUtils::EqualsNoCase(itemType, "roles"))
11054     return GetRolesNav(strBaseDir, items, filter);
11055   else if (StringUtils::EqualsNoCase(itemType, "artists"))
11056     return GetArtistsNav(strBaseDir, items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), -1, -1, -1, filter, sortDescription);
11057   else if (StringUtils::EqualsNoCase(itemType, "albums"))
11058     return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription);
11059   else if (StringUtils::EqualsNoCase(itemType, "discs"))
11060     return GetDiscsByWhere(strBaseDir, filter, items, sortDescription);
11061   else if (StringUtils::EqualsNoCase(itemType, "songs"))
11062     return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true);
11063 
11064   return false;
11065 }
11066 
GetItemById(const std::string & itemType,int id)11067 std::string CMusicDatabase::GetItemById(const std::string &itemType, int id)
11068 {
11069   if (StringUtils::EqualsNoCase(itemType, "genres"))
11070     return GetGenreById(id);
11071   else if (StringUtils::EqualsNoCase(itemType, "sources"))
11072     return GetSourceById(id);
11073   else if (StringUtils::EqualsNoCase(itemType, "years"))
11074     return StringUtils::Format("%d", id);
11075   else if (StringUtils::EqualsNoCase(itemType, "artists"))
11076     return GetArtistById(id);
11077   else if (StringUtils::EqualsNoCase(itemType, "albums"))
11078     return GetAlbumById(id);
11079   else if (StringUtils::EqualsNoCase(itemType, "roles"))
11080     return GetRoleById(id);
11081 
11082   return "";
11083 }
11084 
ExportToXML(const CLibExportSettings & settings,CGUIDialogProgress * progressDialog)11085 void CMusicDatabase::ExportToXML(const CLibExportSettings& settings,  CGUIDialogProgress* progressDialog /*= nullptr*/)
11086 {
11087   if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) &&
11088       !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) &&
11089       !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) &&
11090       !settings.IsItemExported(ELIBEXPORT_ALBUMS) &&
11091       !settings.IsItemExported(ELIBEXPORT_SONGS))
11092     return;
11093 
11094   // Exporting albums either art or NFO (or both) selected
11095   if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
11096        settings.m_skipnfo && !settings.m_artwork &&
11097        settings.IsItemExported(ELIBEXPORT_ALBUMS))
11098     return;
11099 
11100   std::string strFolder;
11101   if (settings.IsSingleFile() || settings.IsSeparateFiles())
11102   {
11103     // Exporting to single file or separate files in a specified location
11104     if (settings.m_strPath.empty())
11105       return;
11106 
11107     strFolder = settings.m_strPath;
11108     if (!URIUtils::HasSlashAtEnd(strFolder))
11109       URIUtils::AddSlashAtEnd(strFolder);
11110     strFolder = URIUtils::GetDirectory(strFolder);
11111     if (strFolder.empty())
11112       return;
11113   }
11114   else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists()))
11115   {
11116     // Exporting artist folders only, or artist NFO or art to library folders
11117     // need Artist Information Folder defined.
11118     // (Album NFO and art goes to music folders)
11119     strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
11120     if (strFolder.empty())
11121       return;
11122   }
11123 
11124   //
11125   bool artistfoldersonly;
11126   artistfoldersonly = settings.IsArtistFoldersOnly() ||
11127                       ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
11128                         settings.m_skipnfo && !settings.m_artwork);
11129 
11130   int iFailCount = 0;
11131   try
11132   {
11133     if (nullptr == m_pDB)
11134       return;
11135     if (nullptr == m_pDS)
11136       return;
11137     if (nullptr == m_pDS2)
11138       return;
11139 
11140     // Create our xml document
11141     CXBMCTinyXML xmlDoc;
11142     TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11143     xmlDoc.InsertEndChild(decl);
11144     TiXmlNode *pMain = NULL;
11145     if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly)
11146       pMain = &xmlDoc;
11147     else if (settings.IsSingleFile())
11148     {
11149       TiXmlElement xmlMainElement("musicdb");
11150       pMain = xmlDoc.InsertEndChild(xmlMainElement);
11151     }
11152 
11153     if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly)
11154     {
11155       // Find albums to export
11156       std::vector<int> albumIds;
11157       std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
11158         CAlbum::ReleaseTypeToString(CAlbum::Album).c_str());
11159       if (!settings.m_unscraped)
11160         strSQL += "AND lastScraped IS NOT NULL";
11161       CLog::Log(LOGDEBUG, "CMusicDatabase::%s - %s", __FUNCTION__, strSQL.c_str());
11162       m_pDS->query(strSQL);
11163 
11164       int total = m_pDS->num_rows();
11165       int current = 0;
11166 
11167       albumIds.reserve(total);
11168       while (!m_pDS->eof())
11169       {
11170         albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
11171         m_pDS->next();
11172       }
11173       m_pDS->close();
11174 
11175       for (const auto &albumId : albumIds)
11176       {
11177         CAlbum album;
11178         GetAlbum(albumId, album);
11179         std::string strAlbumPath;
11180         std::string strPath;
11181         // Get album path, empty unless all album songs are under a unique folder, and
11182         // there are no songs from another album in the same folder.
11183         if (!GetAlbumPath(albumId, strAlbumPath))
11184           strAlbumPath.clear();
11185         if (settings.IsSingleFile())
11186         {
11187           // Save album to xml, including album path
11188           album.Save(pMain, "album", strAlbumPath);
11189         }
11190         else
11191         { // Separate files and artwork
11192           bool pathfound = false;
11193           if (settings.IsToLibFolders())
11194           { // Save album.nfo and artwork with music files.
11195             // Most albums are under a unique folder, but if songs from various albums are mixed then
11196             // avoid overwriting by not allow NFO and art to be exported
11197             if (strAlbumPath.empty())
11198               CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as unique path not found",
11199                 __FUNCTION__, album.strAlbum.c_str());
11200             else if (!CDirectory::Exists(strAlbumPath))
11201               CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as found path %s does not exist",
11202                 __FUNCTION__, album.strAlbum.c_str(), strAlbumPath.c_str());
11203             else
11204             {
11205               strPath = strAlbumPath;
11206               pathfound = true;
11207             }
11208           }
11209           else
11210           { // Save album.nfo and artwork to subfolder on export path
11211             // strPath = strFolder/<albumartist name>/<albumname>
11212             // where <albumname> is either the same name as the album folder
11213             // containing the music files (if unique) or is created using the album name
11214             std::string strAlbumArtist;
11215             pathfound = GetArtistFolderName(album.GetAlbumArtist()[0], album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist);
11216             if (pathfound)
11217             {
11218               strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist);
11219               pathfound = CDirectory::Exists(strPath);
11220               if (!pathfound)
11221                 pathfound = CDirectory::Create(strPath);
11222             }
11223             if (!pathfound)
11224               CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as could not create %s",
11225                 __FUNCTION__, album.strAlbum.c_str(), strPath.c_str());
11226             else
11227             {
11228               std::string strAlbumFolder;
11229               pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder);
11230               if (pathfound)
11231               {
11232                 strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder);
11233                 pathfound = CDirectory::Exists(strPath);
11234                 if (!pathfound)
11235                   pathfound = CDirectory::Create(strPath);
11236               }
11237               if (!pathfound)
11238                 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as could not create %s",
11239                   __FUNCTION__, album.strAlbum.c_str(), strPath.c_str());
11240             }
11241           }
11242           if (pathfound)
11243           {
11244             if (!settings.m_skipnfo)
11245             {
11246               // Save album to NFO, including album path
11247               album.Save(pMain, "album", strAlbumPath);
11248               std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo");
11249               if (settings.m_overwrite || !CFile::Exists(nfoFile))
11250               {
11251                 if (!xmlDoc.SaveFile(nfoFile))
11252                 {
11253                   CLog::Log(LOGERROR, "CMusicDatabase::%s: Album nfo export failed! ('%s')", __FUNCTION__, nfoFile.c_str());
11254                   CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(20302), nfoFile);
11255                   iFailCount++;
11256                 }
11257               }
11258             }
11259             if (settings.m_artwork)
11260             {
11261               // Save art in album folder
11262               // Note thumb resoluton may be lower than original when overwriting
11263               std::map<std::string, std::string> artwork;
11264               std::string savedArtfile;
11265               if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork))
11266               {
11267                 for (const auto &art : artwork)
11268                 {
11269                   if (art.first == "thumb")
11270                     savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
11271                   else
11272                     savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
11273                   CTextureCache::GetInstance().Export(art.second, savedArtfile, settings.m_overwrite);
11274                 }
11275               }
11276             }
11277             xmlDoc.Clear();
11278             TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11279             xmlDoc.InsertEndChild(decl);
11280           }
11281         }
11282 
11283         if ((current % 50) == 0 && progressDialog)
11284         {
11285           progressDialog->SetLine(1, CVariant{ album.strAlbum });
11286           progressDialog->SetPercentage(current * 100 / total);
11287           if (progressDialog->IsCanceled())
11288             return;
11289         }
11290         current++;
11291       }
11292     }
11293 
11294     // Export song playback history to single file only
11295     if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS))
11296     {
11297       if (!ExportSongHistory(pMain, progressDialog))
11298         return;
11299     }
11300 
11301     if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty())
11302     {
11303       // Find artists to export
11304       std::vector<int> artistIds;
11305       std::string sql;
11306       Filter filter;
11307 
11308       if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS))
11309         filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist WHERE album_artist.idArtist = artist.idArtist)", false);
11310       if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS))
11311       {
11312         if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
11313           filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = artist.idArtist )", false);
11314         else
11315           filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)", false);
11316       }
11317       else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
11318         filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)", false);
11319 
11320       if (!settings.m_unscraped && !artistfoldersonly)
11321         filter.AppendWhere("lastScraped IS NOT NULL", true);
11322 
11323       std::string strSQL = "SELECT idArtist FROM artist";
11324       BuildSQL(strSQL, filter, strSQL);
11325       CLog::Log(LOGDEBUG, "CMusicDatabase::%s - %s", __FUNCTION__, strSQL.c_str());
11326 
11327       m_pDS->query(strSQL);
11328       int total = m_pDS->num_rows();
11329       int current = 0;
11330       artistIds.reserve(total);
11331       while (!m_pDS->eof())
11332       {
11333         artistIds.push_back(m_pDS->fv("idArtist").get_asInt());
11334         m_pDS->next();
11335       }
11336       m_pDS->close();
11337 
11338       for (const auto &artistId : artistIds)
11339       {
11340         CArtist artist;
11341         GetArtist(artistId, artist, !artistfoldersonly); // include discography when not folders only
11342         std::string strPath;
11343         std::map<std::string, std::string> artwork;
11344         if (settings.IsSingleFile())
11345         {
11346           // Save artist to xml, and old path (common to music files) if it has one
11347           GetOldArtistPath(artist.idArtist, strPath);
11348           artist.Save(pMain, "artist", strPath);
11349 
11350           if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
11351           { // append to the XML
11352             TiXmlElement additionalNode("art");
11353             for (const auto &i : artwork)
11354               XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
11355             pMain->LastChild()->InsertEndChild(additionalNode);
11356           }
11357         }
11358         else
11359         { // Separate files: artist.nfo and artwork in strFolder/<artist name>
11360           // Get unique folder allowing for duplicate names e.g. 2 x John Williams
11361           bool pathfound = GetArtistFolderName(artist, strPath);
11362           if (pathfound)
11363           {
11364             strPath = URIUtils::AddFileToFolder(strFolder, strPath);
11365             pathfound = CDirectory::Exists(strPath);
11366             if (!pathfound)
11367               pathfound = CDirectory::Create(strPath);
11368           }
11369           if (!pathfound)
11370             CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting artist %s as could not create %s",
11371               __FUNCTION__, artist.strArtist.c_str(), strPath.c_str());
11372           else
11373           {
11374             if (!artistfoldersonly)
11375             {
11376               if (!settings.m_skipnfo)
11377               {
11378                 artist.Save(pMain, "artist", strPath);
11379                 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo");
11380                 if (settings.m_overwrite || !CFile::Exists(nfoFile))
11381                 {
11382                   if (!xmlDoc.SaveFile(nfoFile))
11383                   {
11384                     CLog::Log(LOGERROR, "CMusicDatabase::%s: Artist nfo export failed! ('%s')", __FUNCTION__, nfoFile.c_str());
11385                     CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(20302), nfoFile);
11386                     iFailCount++;
11387                   }
11388                 }
11389               }
11390               if (settings.m_artwork)
11391               {
11392                 std::string savedArtfile;
11393                 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
11394                 {
11395                   for (const auto &art : artwork)
11396                   {
11397                     if (art.first == "thumb")
11398                       savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
11399                     else
11400                       savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
11401                     CTextureCache::GetInstance().Export(art.second, savedArtfile, settings.m_overwrite);
11402                   }
11403                 }
11404               }
11405               xmlDoc.Clear();
11406               TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11407               xmlDoc.InsertEndChild(decl);
11408             }
11409           }
11410         }
11411         if ((current % 50) == 0 && progressDialog)
11412         {
11413           progressDialog->SetLine(1, CVariant{ artist.strArtist });
11414           progressDialog->SetPercentage(current * 100 / total);
11415           if (progressDialog->IsCanceled())
11416             return;
11417         }
11418         current++;
11419       }
11420     }
11421 
11422     if (settings.IsSingleFile())
11423     {
11424       std::string xmlFile = URIUtils::AddFileToFolder(strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
11425       if (CFile::Exists(xmlFile))
11426         xmlFile = URIUtils::AddFileToFolder(strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
11427       xmlDoc.SaveFile(xmlFile);
11428 
11429       CVariant data;
11430       data["file"] = xmlFile;
11431       if (iFailCount > 0)
11432         data["failcount"] = iFailCount;
11433       CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport",
11434                                                          data);
11435     }
11436   }
11437   catch (...)
11438   {
11439     CLog::Log(LOGERROR, "CMusicDatabase::%s failed", __FUNCTION__);
11440     iFailCount++;
11441   }
11442 
11443   if (progressDialog)
11444     progressDialog->Close();
11445 
11446   if (iFailCount > 0 && progressDialog)
11447     HELPERS::ShowOKDialogLines(CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011).c_str(), iFailCount)});
11448 }
11449 
ExportSongHistory(TiXmlNode * pNode,CGUIDialogProgress * progressDialog)11450 bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog)
11451 {
11452   try
11453   {
11454     // Export songs with some playback history
11455     std::string strSQL = "SELECT idSong, song.idAlbum, "
11456       "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, "
11457       "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, "
11458       "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating "
11459       "FROM song JOIN album on album.idAlbum = song.idAlbum "
11460       "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0";
11461 
11462     CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL.c_str());
11463     m_pDS->query(strSQL);
11464 
11465     int total = m_pDS->num_rows();
11466     int current = 0;
11467     while (!m_pDS->eof())
11468     {
11469       TiXmlElement songElement("song");
11470       TiXmlNode* song = pNode->InsertEndChild(songElement);
11471 
11472       XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt());
11473       XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString());
11474       XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString());
11475       XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt());
11476       XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString());
11477       XMLUtils::SetString(song, "musicbrainztrackid", m_pDS->fv("strMusicBrainzTrackID").get_asString());
11478       XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt());
11479       XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString());
11480       XMLUtils::SetString(song, "musicbrainzalbumid", m_pDS->fv("strMusicBrainzAlbumID").get_asString());
11481       XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString());
11482       XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt());
11483       XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString());
11484       auto* rating = XMLUtils::SetString(song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat()));
11485       if (rating)
11486         rating->ToElement()->SetAttribute("max", 10);
11487       XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt());
11488       auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt());
11489       if (userrating)
11490         userrating->ToElement()->SetAttribute("max", 10);
11491 
11492       if ((current % 100) == 0 && progressDialog)
11493       {
11494         progressDialog->SetLine(1, CVariant{ m_pDS->fv("strAlbum").get_asString() });
11495         progressDialog->SetPercentage(current * 100 / total);
11496         if (progressDialog->IsCanceled())
11497         {
11498           m_pDS->close();
11499           return false;
11500         }
11501       }
11502       current++;
11503 
11504       m_pDS->next();
11505     }
11506     m_pDS->close();
11507     return true;
11508   }
11509   catch (...)
11510   {
11511     CLog::Log(LOGERROR, "{0} failed", __FUNCTION__);
11512   }
11513   return false;
11514 }
11515 
ImportFromXML(const std::string & xmlFile,CGUIDialogProgress * progressDialog)11516 void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog)
11517 {
11518   try
11519   {
11520     if (nullptr == m_pDB)
11521       return;
11522     if (nullptr == m_pDS)
11523       return;
11524 
11525     CXBMCTinyXML xmlDoc;
11526     if (!xmlDoc.LoadFile(xmlFile) && progressDialog)
11527     {
11528       HELPERS::ShowOKDialogLines(CVariant{ 20197 }, CVariant{ 38354 }); //"Unable to read xml file"
11529       return;
11530     }
11531 
11532     TiXmlElement *root = xmlDoc.RootElement();
11533     if (!root) return;
11534 
11535     TiXmlElement *entry = root->FirstChildElement();
11536     int current = 0;
11537     int total = 0;
11538     int songtotal = 0;
11539     // Count the number of artists, albums and songs
11540     while (entry)
11541     {
11542       if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 ||
11543           StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
11544         total++;
11545       else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
11546         songtotal++;
11547 
11548       entry = entry->NextSiblingElement();
11549     }
11550 
11551     BeginTransaction();
11552     entry = root->FirstChildElement();
11553     while (entry)
11554     {
11555       std::string strTitle;
11556       if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0)
11557       {
11558         CArtist importedArtist;
11559         importedArtist.Load(entry);
11560         strTitle = importedArtist.strArtist;
11561 
11562         // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
11563         int idArtist = GetArtistByMatch(importedArtist);
11564         if (idArtist > -1)
11565         {
11566           CArtist artist;
11567           GetArtist(idArtist, artist, true); // include discography
11568           artist.MergeScrapedArtist(importedArtist, true);
11569           UpdateArtist(artist);
11570         }
11571         else
11572           CLog::Log(LOGDEBUG, "%s - Not import additional artist data as %s not found", __FUNCTION__, importedArtist.strArtist.c_str());
11573         current++;
11574       }
11575       else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
11576       {
11577         CAlbum importedAlbum;
11578         importedAlbum.Load(entry);
11579         strTitle = importedAlbum.strAlbum;
11580         // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
11581         int idAlbum = GetAlbumByMatch(importedAlbum);
11582         if (idAlbum > -1)
11583         {
11584           CAlbum album;
11585           GetAlbum(idAlbum, album, true);
11586           album.MergeScrapedAlbum(importedAlbum, true);
11587           UpdateAlbum(album); //Will replace song artists if present in xml
11588         }
11589         else
11590           CLog::Log(LOGDEBUG, "%s - Not import additional album data as %s not found", __FUNCTION__, importedAlbum.strAlbum.c_str());
11591 
11592         current++;
11593       }
11594       entry = entry ->NextSiblingElement();
11595       if (progressDialog && total)
11596       {
11597         progressDialog->SetPercentage(current * 100 / total);
11598         progressDialog->SetLine(2, CVariant{std::move(strTitle)});
11599         progressDialog->Progress();
11600         if (progressDialog->IsCanceled())
11601         {
11602           RollbackTransaction();
11603           return;
11604         }
11605       }
11606     }
11607     CommitTransaction();
11608 
11609     // Import song playback history <song> entries found
11610     if (songtotal > 0)
11611       if (!ImportSongHistory(xmlFile, songtotal, progressDialog))
11612         return;
11613 
11614     CGUIComponent* gui = CServiceBroker::GetGUI();
11615     if (gui)
11616       gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
11617   }
11618   catch (...)
11619   {
11620     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
11621     RollbackTransaction();
11622   }
11623   if (progressDialog)
11624     progressDialog->Close();
11625 }
11626 
ImportSongHistory(const std::string & xmlFile,const int total,CGUIDialogProgress * progressDialog)11627 bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile, const int total,  CGUIDialogProgress* progressDialog)
11628 {
11629   bool bHistSongExists = false;
11630   try
11631   {
11632     CXBMCTinyXML xmlDoc;
11633     if (!xmlDoc.LoadFile(xmlFile))
11634       return false;
11635 
11636     TiXmlElement* root = xmlDoc.RootElement();
11637     if (!root)
11638       return false;
11639 
11640     TiXmlElement* entry = root->FirstChildElement();
11641     int current = 0;
11642 
11643     if (progressDialog)
11644     {
11645       progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history"
11646       progressDialog->SetLine(2, CVariant{ "" });
11647     }
11648 
11649     // As can be many songs do in db, not song at a time which would be slow
11650     // Convert xml entries into a SQL bulk insert statement
11651     std::string strSQL;
11652     entry = root->FirstChildElement();
11653     while (entry)
11654     {
11655       std::string strArtistDisp;
11656       std::string strTitle;
11657       int iTrack;
11658       std::string strFilename;
11659       std::string strMusicBrainzTrackID;
11660       std::string strAlbum;
11661       std::string strMusicBrainzAlbumID;
11662       std::string strAlbumArtistDisp;
11663       int iTimesplayed;
11664       std::string lastplayed;
11665       int iUserrating = 0;
11666       float fRating = 0.0;
11667       int iVotes;
11668       std::string strSQLSong;
11669       if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
11670       {
11671         XMLUtils::GetString(entry, "artistdesc", strArtistDisp);
11672         XMLUtils::GetString(entry, "title", strTitle);
11673         XMLUtils::GetInt(entry, "track", iTrack);
11674         XMLUtils::GetString(entry, "filename", strFilename);
11675         XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID);
11676         XMLUtils::GetString(entry, "albumtitle", strAlbum);
11677         XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID);
11678         XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp);
11679         XMLUtils::GetInt(entry, "timesplayed", iTimesplayed);
11680         XMLUtils::GetString(entry, "lastplayed", lastplayed);
11681         const TiXmlElement* rElement = entry->FirstChildElement("rating");
11682         if (rElement)
11683         {
11684           float rating = 0;
11685           float max_rating = 10;
11686           XMLUtils::GetFloat(entry, "rating", rating);
11687           if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
11688             rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
11689           if (rating > 10.f)
11690             rating = 10.f;
11691           fRating = rating;
11692         }
11693         XMLUtils::GetInt(entry, "votes", iVotes);
11694         const TiXmlElement* userrating = entry->FirstChildElement("userrating");
11695         if (userrating)
11696         {
11697           float rating = 0;
11698           float max_rating = 10;
11699           XMLUtils::GetFloat(entry, "userrating", rating);
11700           if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
11701             rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
11702           if (rating > 10.f)
11703             rating = 10.f;
11704           iUserrating = MathUtils::round_int(rating);
11705         }
11706 
11707         strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack);
11708         strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(), strFilename.c_str());
11709         if (strMusicBrainzTrackID.empty())
11710           strSQLSong += PrepareSQL("NULL, ");
11711         else
11712           strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str());
11713         strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str());
11714         if (strMusicBrainzAlbumID.empty())
11715           strSQLSong += PrepareSQL("NULL, ");
11716         else
11717           strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str());
11718         strSQLSong += PrepareSQL("%d, ", iTimesplayed);
11719         if (lastplayed.empty())
11720           strSQLSong += PrepareSQL("NULL, ");
11721         else
11722           strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str());
11723         strSQLSong += PrepareSQL("%.1f, %d, %d, -1, -1)", fRating, iVotes, iUserrating);
11724 
11725         if (current > 0)
11726           strSQLSong = ", " + strSQLSong;
11727         strSQL += strSQLSong;
11728         current++;
11729       }
11730 
11731       entry = entry->NextSiblingElement();
11732 
11733       if ((current % 100) == 0 && progressDialog)
11734       {
11735         progressDialog->SetPercentage(current * 100 / total);
11736         progressDialog->SetLine(3, CVariant{ std::move(strTitle) });
11737         progressDialog->Progress();
11738         if (progressDialog->IsCanceled())
11739           return false;
11740       }
11741     }
11742 
11743     CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__, total);
11744     /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of
11745        song table using correlated subqueries to a temp table. An updatable join
11746        to temp table would work in MySQL but SQLite not support updatable joins.
11747     */
11748     m_pDS->exec("CREATE TABLE HistSong ("
11749       "idSongSrc INTEGER primary key, "
11750       "strAlbum varchar(256), "
11751       "strMusicBrainzAlbumID text, "
11752       "strAlbumArtistDisp text, "
11753       "strArtistDisp text, strTitle varchar(512), "
11754       "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, "
11755       "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, "
11756       "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
11757       "userrating INTEGER NOT NULL DEFAULT 0, "
11758       "idAlbum INTEGER, idSong INTEGER)");
11759     bHistSongExists = true;
11760 
11761     strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, "
11762       "strFileName, strMusicBrainzTrackID, "
11763       "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, "
11764       " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " + strSQL;
11765     m_pDS->exec(strSQL);
11766 
11767     if (progressDialog)
11768     {
11769       progressDialog->SetLine(2, CVariant{38351}); //"Matching data"
11770       progressDialog->SetLine(3, CVariant{ "" });
11771       progressDialog->Progress();
11772       if (progressDialog->IsCanceled())
11773       {
11774         m_pDS->exec("DROP TABLE HistSong");
11775         return false;
11776       }
11777     }
11778 
11779     BeginTransaction();
11780     // Match albums first on mbid then artist string and album title, setting idAlbum
11781     // mbid is unique so subquery can only return one result at most
11782     strSQL = "UPDATE HistSong "
11783       "SET idAlbum = (SELECT album.idAlbum FROM album "
11784       "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) "
11785       "WHERE EXISTS(SELECT 1 FROM album "
11786       "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0";
11787     m_pDS->exec(strSQL);
11788 
11789     // Can only be one album with same title and artist(s) and no mbid.
11790     // But could have 2 releases one with and one without mbid, match up those without mbid
11791     strSQL = "UPDATE HistSong "
11792       "SET idAlbum = (SELECT album.idAlbum FROM album "
11793       "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11794       "AND HistSong.strAlbum = album.strAlbum "
11795       "AND album.strMusicBrainzAlbumID IS NULL "
11796       "AND HistSong.strMusicBrainzAlbumID IS NULL) "
11797       "WHERE EXISTS(SELECT 1 FROM album "
11798       "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11799       "AND HistSong.strAlbum = album.strAlbum "
11800       "AND album.strMusicBrainzAlbumID IS NULL "
11801       "AND HistSong.strMusicBrainzAlbumID IS NULL) "
11802       "AND idAlbum < 0";
11803     m_pDS->exec(strSQL);
11804 
11805     // Try match rest by title and artist(s), prioritise one without mbid
11806     // Target could have multiple releases - with mbid (non-matching) or one without mbid
11807     strSQL = "UPDATE HistSong "
11808       "SET idAlbum = (SELECT album.idAlbum FROM album "
11809       "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11810       "AND HistSong.strAlbum = album.strAlbum "
11811       "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
11812       "WHERE EXISTS(SELECT 1 FROM album "
11813       "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11814       "AND HistSong.strAlbum = album.strAlbum) "
11815       "AND idAlbum < 0";
11816     m_pDS->exec(strSQL);
11817     if (progressDialog)
11818     {
11819       progressDialog->Progress();
11820       if (progressDialog->IsCanceled())
11821       {
11822         RollbackTransaction();
11823         m_pDS->exec("DROP TABLE HistSong");
11824         return false;
11825       }
11826     }
11827 
11828     // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong
11829     strSQL = "UPDATE HistSong "
11830       "SET idSong = (SELECT idsong FROM song "
11831       "WHERE HistSong.idAlbum = song.idAlbum AND "
11832       "HistSong.iTrack = song.iTrack AND "
11833       "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) "
11834       "WHERE EXISTS(SELECT 1 FROM song "
11835       "WHERE HistSong.idAlbum = song.idAlbum AND "
11836       "HistSong.iTrack = song.iTrack AND "
11837       "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0";
11838     m_pDS->exec(strSQL);
11839 
11840     // An album can have more than one song with same track and title (although idAlbum, track and
11841     // title is often unique), but not using filename as an identifier to allow for import of song
11842     // history for renamed files. It is about song playback not file playback.
11843     // Pick the first
11844     strSQL = "UPDATE HistSong "
11845       "SET idSong = (SELECT idsong FROM song "
11846       "WHERE HistSong.idAlbum = song.idAlbum AND "
11847       "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
11848       "WHERE EXISTS(SELECT 1 FROM song "
11849       "WHERE HistSong.idAlbum = song.idAlbum AND "
11850       "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0";
11851     m_pDS->exec(strSQL);
11852 
11853     CommitTransaction();
11854     if (progressDialog)
11855     {
11856       progressDialog->Progress();
11857       if (progressDialog->IsCanceled())
11858       {
11859         m_pDS->exec("DROP TABLE HistSong");
11860         return false;
11861       }
11862     }
11863 
11864     // Create an index to speed up the updates
11865     m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)");
11866 
11867     // Log how many songs matched
11868     int unmatched = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS);
11869     CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__, total - unmatched,  total);
11870 
11871     if (progressDialog)
11872     {
11873       progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history"
11874       progressDialog->Progress();
11875       if (progressDialog->IsCanceled())
11876       {
11877         m_pDS->exec("DROP TABLE HistSong"); // Drops index too
11878         return false;
11879       }
11880     }
11881 
11882     /* Update song table using the song ids we have matched.
11883       Use correlated subqueries as SQLite does not support updatable joins.
11884       MySQL requires HistSong table not to be defined temporary for this.
11885     */
11886 
11887     BeginTransaction();
11888     // Times played and last played date(when count is greater)
11889     strSQL = "UPDATE song SET iTimesPlayed = "
11890       "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), "
11891       "lastplayed = "
11892       "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) "
11893       "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
11894       "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)";
11895     m_pDS->exec(strSQL);
11896 
11897     // User rating
11898     strSQL = "UPDATE song SET userrating = "
11899       "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) "
11900       "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
11901       "HistSong.idSong = song.idSong AND HistSong.userrating > 0)";
11902     m_pDS->exec(strSQL);
11903 
11904     // Rating and votes
11905     strSQL = "UPDATE song SET rating = "
11906       "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), "
11907       "votes = "
11908       "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) "
11909       "WHERE  EXISTS(SELECT 1 FROM HistSong WHERE "
11910       "HistSong.idSong = song.idSong AND HistSong.rating > 0)";
11911     m_pDS->exec(strSQL);
11912 
11913     if (progressDialog)
11914     {
11915       progressDialog->Progress();
11916       if (progressDialog->IsCanceled())
11917       {
11918         RollbackTransaction();
11919         m_pDS->exec("DROP TABLE HistSong");
11920         return false;
11921       }
11922     }
11923     CommitTransaction();
11924 
11925     // Tidy up temp table (index also removed)
11926     m_pDS->exec("DROP TABLE HistSong");
11927     // Compact db to recover space as had to add/drop actual table
11928     if (progressDialog)
11929     {
11930       progressDialog->SetLine(2, CVariant{ 331 });
11931       progressDialog->Progress();
11932     }
11933     Compress(false);
11934 
11935     // Write event log entry
11936     // "Importing song history {1} of {2} songs matched", total - unmatched, total)
11937     std::string strLine = StringUtils::Format(g_localizeStrings.Get(38353).c_str(), total - unmatched, total);
11938     CServiceBroker::GetEventLog().Add(
11939       EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information)));
11940 
11941     return true;
11942   }
11943   catch (...)
11944   {
11945     CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
11946     RollbackTransaction();
11947     if (bHistSongExists)
11948       m_pDS->exec("DROP TABLE HistSong");
11949   }
11950   return false;
11951 }
11952 
SetPropertiesFromArtist(CFileItem & item,const CArtist & artist)11953 void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist)
11954 {
11955   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
11956 
11957   item.SetProperty("artist_sortname", artist.strSortName);
11958   item.SetProperty("artist_type", artist.strType);
11959   item.SetProperty("artist_gender", artist.strGender);
11960   item.SetProperty("artist_disambiguation", artist.strDisambiguation);
11961   item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator));
11962   item.SetProperty("artist_instrument_array", artist.instruments);
11963   item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator));
11964   item.SetProperty("artist_style_array", artist.styles);
11965   item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator));
11966   item.SetProperty("artist_mood_array", artist.moods);
11967   item.SetProperty("artist_born", artist.strBorn);
11968   item.SetProperty("artist_formed", artist.strFormed);
11969   item.SetProperty("artist_description", artist.strBiography);
11970   item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator));
11971   item.SetProperty("artist_genre_array", artist.genre);
11972   item.SetProperty("artist_died", artist.strDied);
11973   item.SetProperty("artist_disbanded", artist.strDisbanded);
11974   item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator));
11975   item.SetProperty("artist_yearsactive_array", artist.yearsActive);
11976 }
11977 
SetPropertiesFromAlbum(CFileItem & item,const CAlbum & album)11978 void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album)
11979 {
11980   const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
11981 
11982   item.SetProperty("album_description", album.strReview);
11983   item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator));
11984   item.SetProperty("album_theme_array", album.themes);
11985   item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator));
11986   item.SetProperty("album_mood_array", album.moods);
11987   item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator));
11988   item.SetProperty("album_style_array", album.styles);
11989   item.SetProperty("album_type", album.strType);
11990   item.SetProperty("album_label", album.strLabel);
11991   item.SetProperty("album_artist", album.GetAlbumArtistString());
11992   item.SetProperty("album_artist_array", album.GetAlbumArtist());
11993   item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator));
11994   item.SetProperty("album_genre_array", album.genre);
11995   item.SetProperty("album_title", album.strAlbum);
11996   if (album.fRating > 0)
11997     item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating));
11998   if (album.iUserrating > 0)
11999     item.SetProperty("album_userrating", album.iUserrating);
12000   if (album.iVotes > 0)
12001     item.SetProperty("album_votes", album.iVotes);
12002 
12003   item.SetProperty("album_isboxset", album.bBoxedSet);
12004   item.SetProperty("album_totaldiscs", album.iTotalDiscs);
12005   item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType));
12006   item.SetProperty("album_duration",
12007                    StringUtils::SecondsToTimeString(
12008                        album.iAlbumDuration, static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS)));
12009 }
12010 
SetPropertiesForFileItem(CFileItem & item)12011 void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item)
12012 {
12013   if (!item.HasMusicInfoTag())
12014     return;
12015   // May already have song artist ids as item property set when data read from
12016   // db, but check property is valid array (scripts could set item properties
12017   // incorrectly), otherwise try to fetch artist by name.
12018   int idArtist = -1;
12019   if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray())
12020   {
12021     CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array();
12022     idArtist = static_cast<int>(varid->asInteger());
12023   }
12024   else
12025     idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString());
12026   if (idArtist > -1)
12027   {
12028     CArtist artist;
12029     if (GetArtist(idArtist, artist))
12030       SetPropertiesFromArtist(item,artist);
12031   }
12032   int idAlbum = item.GetMusicInfoTag()->GetAlbumId();
12033   if (idAlbum <= 0)
12034     idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(),
12035                              item.GetMusicInfoTag()->GetArtistString());
12036   if (idAlbum > -1)
12037   {
12038     CAlbum album;
12039     if (GetAlbum(idAlbum, album, false))
12040       SetPropertiesFromAlbum(item,album);
12041   }
12042 }
12043 
SetItemUpdated(int mediaId,const std::string & mediaType)12044 void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType)
12045 {
12046   std::string strSQL;
12047   try
12048   {
12049     if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong)
12050       return;
12051     if (nullptr == m_pDB)
12052       return;
12053     if (nullptr == m_pDS)
12054       return;
12055 
12056     // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
12057     // e.g. when artwork for item is changed from info dialog but not item details.
12058     // Use SQL UPDATE that does not change record data.
12059     if (mediaType == MediaTypeArtist)
12060       strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId);
12061     else if (mediaType == MediaTypeAlbum)
12062       strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId);
12063     else // MediaTypeSong
12064       strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId);
12065     m_pDS->exec(strSQL);
12066   }
12067   catch (...)
12068   {
12069     CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__,
12070               mediaId, mediaType.c_str(), strSQL.c_str());
12071   }
12072 }
12073 
SetArtForItem(int mediaId,const std::string & mediaType,const std::map<std::string,std::string> & art)12074 void CMusicDatabase::SetArtForItem(int mediaId, const std::string &mediaType, const std::map<std::string, std::string> &art)
12075 {
12076   for (const auto &i : art)
12077     SetArtForItem(mediaId, mediaType, i.first, i.second);
12078 }
12079 
SetArtForItem(int mediaId,const std::string & mediaType,const std::string & artType,const std::string & url)12080 void CMusicDatabase::SetArtForItem(int mediaId, const std::string &mediaType, const std::string &artType, const std::string &url)
12081 {
12082   try
12083   {
12084     if (nullptr == m_pDB)
12085       return;
12086     if (nullptr == m_pDS)
12087       return;
12088 
12089     // don't set <foo>.<bar> art types - these are derivative types from parent items
12090     if (artType.find('.') != std::string::npos)
12091       return;
12092 
12093     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());
12094     m_pDS->query(sql);
12095     if (!m_pDS->eof())
12096     { // update
12097       int artId = m_pDS->fv(0).get_asInt();
12098       m_pDS->close();
12099       sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
12100       m_pDS->exec(sql);
12101     }
12102     else
12103     { // insert
12104       m_pDS->close();
12105       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());
12106       m_pDS->exec(sql);
12107     }
12108   }
12109   catch (...)
12110   {
12111     CLog::Log(LOGERROR, "%s(%d, '%s', '%s', '%s') failed", __FUNCTION__, mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
12112   }
12113 }
12114 
GetArtForItem(int songId,int albumId,int artistId,bool bPrimaryArtist,std::vector<ArtForThumbLoader> & art)12115 bool CMusicDatabase::GetArtForItem(int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader> &art)
12116 {
12117   std::string strSQL;
12118   try
12119   {
12120     if (!(songId > 0 || albumId > 0 || artistId > 0)) return false;
12121     if (nullptr == m_pDB)
12122       return false;
12123     if (nullptr == m_pDS2)
12124       return false; // using dataset 2 as we're likely called in loops on dataset 1
12125 
12126     Filter filter;
12127     if (songId > 0)
12128       filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong));
12129     if (albumId > 0)
12130       filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum), false);
12131     if (artistId > 0)
12132       filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false);
12133 
12134     strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder FROM art";
12135     if (!BuildSQL(strSQL, filter, strSQL))
12136       return false;
12137 
12138     if (!(artistId > 0))
12139     {
12140       // Artist ID unknown, so lookup album artist for albums and songs
12141       std::string strSQL2;
12142       if (albumId > 0)
12143       {
12144         //Album ID known, so use it to look up album artist(s)
12145         strSQL2 = PrepareSQL(
12146           "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12147           "url, album_artist.iOrder as iorder FROM art "
12148           "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12149           "WHERE album_artist.idAlbum = %i ",
12150           MediaTypeArtist, albumId);
12151         if (bPrimaryArtist)
12152           strSQL2 += "AND album_artist.iOrder = 0";
12153 
12154         strSQL = strSQL + " UNION " + strSQL2;
12155       }
12156       if (songId > 0)
12157       {
12158         if (albumId < 0)
12159         {
12160           //Album ID unknown, so get from song to look up album artist(s)
12161           strSQL2 = PrepareSQL(
12162             "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12163             "url, album_artist.iOrder as iorder FROM art "
12164             "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12165             "JOIN song ON song.idAlbum = album_artist.idAlbum  "
12166             "WHERE song.idSong = %i ",
12167             MediaTypeArtist, songId);
12168           if (bPrimaryArtist)
12169             strSQL2 += "AND album_artist.iOrder = 0";
12170 
12171           strSQL = strSQL + " UNION " + strSQL2;
12172         }
12173 
12174         // Artist ID unknown, so lookup artist for songs (could be different from album artist)
12175         strSQL2 = PrepareSQL(
12176           "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
12177           "url, song_artist.iOrder as iorder FROM art "
12178           "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
12179           "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
12180           MediaTypeArtist, songId, ROLE_ARTIST);
12181         if (bPrimaryArtist)
12182           strSQL2 += "AND song_artist.iOrder = 0";
12183 
12184         strSQL = strSQL +  " UNION " + strSQL2;
12185       }
12186     }
12187     if (songId > 0 && albumId < 0)
12188     {
12189       //Album ID unknown, so get from song to look up album art
12190       std::string strSQL2;
12191       strSQL2 = PrepareSQL(
12192         "SELECT art_id, media_id, media_type, type, '' as prefix, "
12193         "url, 0 as iorder FROM art "
12194         "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
12195         "WHERE song.idSong = %i ",
12196         MediaTypeAlbum, songId);
12197       strSQL = strSQL + " UNION " + strSQL2;
12198     }
12199 
12200     m_pDS2->query(strSQL);
12201     while (!m_pDS2->eof())
12202     {
12203       ArtForThumbLoader artitem;
12204       artitem.artType = m_pDS2->fv("type").get_asString();
12205       artitem.mediaType = m_pDS2->fv("media_type").get_asString();
12206       artitem.prefix = m_pDS2->fv("prefix").get_asString();
12207       artitem.url = m_pDS2->fv("url").get_asString();
12208       int iOrder = m_pDS2->fv("iorder").get_asInt();
12209       // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
12210       if (iOrder > 0)
12211         artitem.prefix += m_pDS2->fv("iorder").get_asString();
12212 
12213       art.emplace_back(artitem);
12214       m_pDS2->next();
12215     }
12216     m_pDS2->close();
12217     return !art.empty();
12218   }
12219   catch (...)
12220   {
12221     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, strSQL.c_str());
12222   }
12223   return false;
12224 }
12225 
GetArtForItem(int mediaId,const std::string & mediaType,std::map<std::string,std::string> & art)12226 bool CMusicDatabase::GetArtForItem(int mediaId, const std::string &mediaType, std::map<std::string, std::string> &art)
12227 {
12228   try
12229   {
12230     if (nullptr == m_pDB)
12231       return false;
12232     if (nullptr == m_pDS2)
12233       return false; // using dataset 2 as we're likely called in loops on dataset 1
12234 
12235     std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'", mediaId, mediaType.c_str());
12236     m_pDS2->query(sql);
12237     while (!m_pDS2->eof())
12238     {
12239       art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
12240       m_pDS2->next();
12241     }
12242     m_pDS2->close();
12243     return !art.empty();
12244   }
12245   catch (...)
12246   {
12247     CLog::Log(LOGERROR, "%s(%d) failed", __FUNCTION__, mediaId);
12248   }
12249   return false;
12250 }
12251 
GetArtForItem(int mediaId,const std::string & mediaType,const std::string & artType)12252 std::string CMusicDatabase::GetArtForItem(int mediaId, const std::string &mediaType, const std::string &artType)
12253 {
12254   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());
12255   return GetSingleValue(query, m_pDS2);
12256 }
12257 
RemoveArtForItem(int mediaId,const MediaType & mediaType,const std::string & artType)12258 bool CMusicDatabase::RemoveArtForItem(int mediaId, const MediaType & mediaType, const std::string & artType)
12259 {
12260   return ExecuteQuery(PrepareSQL("DELETE FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str()));
12261 }
12262 
RemoveArtForItem(int mediaId,const MediaType & mediaType,const std::set<std::string> & artTypes)12263 bool CMusicDatabase::RemoveArtForItem(int mediaId, const MediaType & mediaType, const std::set<std::string>& artTypes)
12264 {
12265   bool result = true;
12266   for (const auto &i : artTypes)
12267     result &= RemoveArtForItem(mediaId, mediaType, i);
12268 
12269   return result;
12270 }
12271 
GetArtTypes(const MediaType & mediaType,std::vector<std::string> & artTypes)12272 bool CMusicDatabase::GetArtTypes(const MediaType &mediaType, std::vector<std::string> &artTypes)
12273 {
12274   try
12275   {
12276     if (nullptr == m_pDB)
12277       return false;
12278     if (nullptr == m_pDS)
12279       return false;
12280 
12281     std::string strSQL = PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
12282 
12283     if (!m_pDS->query(strSQL)) return false;
12284     int iRowsFound = m_pDS->num_rows();
12285     if (iRowsFound == 0)
12286     {
12287       m_pDS->close();
12288       return false;
12289     }
12290 
12291     while (!m_pDS->eof())
12292     {
12293       artTypes.emplace_back(m_pDS->fv(0).get_asString());
12294       m_pDS->next();
12295     }
12296     m_pDS->close();
12297     return true;
12298   }
12299   catch (...)
12300   {
12301     CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, mediaType.c_str());
12302   }
12303   return false;
12304 }
12305 
GetAvailableArtTypesForItem(int mediaId,const MediaType & mediaType)12306 std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId,
12307   const MediaType& mediaType)
12308 {
12309   CScraperUrl thumbURL;
12310   if (mediaType == MediaTypeArtist)
12311   {
12312     CArtist artist;
12313     if (GetArtist(mediaId, artist))
12314       thumbURL = artist.thumbURL;
12315   }
12316   else if (mediaType == MediaTypeAlbum)
12317   {
12318     CAlbum album;
12319     if (GetAlbum(mediaId, album))
12320       thumbURL = album.thumbURL;
12321   }
12322 
12323   std::vector<std::string> result;
12324   for (const auto& urlEntry : thumbURL.GetUrls())
12325   {
12326     std::string artType = urlEntry.m_aspect;
12327     if (artType.empty())
12328       artType = "thumb";
12329     if (std::find(result.begin(), result.end(), artType) == result.end())
12330       result.push_back(artType);
12331   }
12332   return result;
12333 }
12334 
GetAvailableArtForItem(int mediaId,const MediaType & mediaType,const std::string & artType)12335 std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem(
12336   int mediaId, const MediaType& mediaType, const std::string& artType)
12337 {
12338   CScraperUrl thumbURL;
12339   if (mediaType == MediaTypeArtist)
12340   {
12341     CArtist artist;
12342     if (GetArtist(mediaId, artist))
12343       thumbURL = artist.thumbURL;
12344   }
12345   else if (mediaType == MediaTypeAlbum)
12346   {
12347     CAlbum album;
12348     if (GetAlbum(mediaId, album))
12349       thumbURL = album.thumbURL;
12350   }
12351 
12352   std::vector<CScraperUrl::SUrlEntry> result;
12353   for (auto urlEntry : thumbURL.GetUrls())
12354   {
12355     if (urlEntry.m_aspect.empty())
12356       urlEntry.m_aspect = "thumb";
12357     if (artType.empty() || urlEntry.m_aspect == artType)
12358       result.push_back(urlEntry);
12359   }
12360   return result;
12361 }
12362 
GetOrderFilter(const std::string & type,const SortDescription & sorting,Filter & filter)12363 int CMusicDatabase::GetOrderFilter(const std::string& type,
12364                                    const SortDescription& sorting,
12365                                    Filter& filter)
12366 {
12367   // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
12368   int iFieldsAdded = 0;
12369   filter.fields.clear(); // remove "*"
12370   std::vector<std::string> orderfields;
12371   std::string DESC;
12372 
12373   if (sorting.sortOrder == SortOrderDescending)
12374     DESC = " DESC";
12375 
12376   if (sorting.sortBy == SortByRandom)
12377     orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
12378   else
12379   {
12380     FieldList fields;
12381     SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields);
12382     for (const auto& it : fields)
12383     {
12384       std::string strField;
12385       if (it == FieldYear)
12386         strField = "iYear";
12387       else
12388         strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect);
12389       if (!strField.empty())
12390         orderfields.emplace_back(strField);
12391     }
12392   }
12393 
12394   // Convert field names into order by statement elements
12395   for (auto& name : orderfields)
12396   {
12397     //Add field for adjusted name sorting using sort name and ignoring articles
12398     std::string sortSQL;
12399     if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist"))
12400     {
12401       if (StringUtils::EndsWith(name, "strArtists"))
12402         sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort");
12403       else
12404         sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName");
12405       if (!sortSQL.empty())
12406       {
12407         name = "artistsortname";
12408         filter.AppendField(sortSQL); // Add artistsortname as scalar query field
12409         iFieldsAdded++;
12410       }
12411       // Natural number case insensitve sort
12412       filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
12413     }
12414     else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle"))
12415     {
12416       sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, "");
12417       if (!sortSQL.empty())
12418       {
12419         name = "titlesortname";
12420         filter.AppendField(sortSQL); // Add sortname as scalar query field
12421         iFieldsAdded++;
12422       }
12423       // Natural number case insensitve sort
12424       filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
12425     }
12426     else if (StringUtils::EndsWith(name, "strGenres"))
12427       // Natural number case insensitve sort
12428       filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
12429     else
12430       filter.AppendOrder(name + DESC);
12431   }
12432   return iFieldsAdded;
12433 }
12434 
GetFilter(CDbUrl & musicUrl,Filter & filter,SortDescription & sorting)12435 bool CMusicDatabase::GetFilter(CDbUrl &musicUrl, Filter &filter, SortDescription &sorting)
12436 {
12437   if (!musicUrl.IsValid())
12438     return false;
12439 
12440   std::string type = musicUrl.GetType();
12441   const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
12442 
12443   // Check for playlist rules first, they may contain role criteria
12444   bool hasRoleRules = false;
12445 
12446   auto option = options.find("xsp");
12447   if (option != options.end())
12448   {
12449     CSmartPlaylist xsp;
12450     if (!xsp.LoadFromJson(option->second.asString()))
12451       return false;
12452 
12453     std::set<std::string> playlists;
12454     std::string xspWhere;
12455     xspWhere = xsp.GetWhereClause(*this, playlists);
12456     hasRoleRules = xsp.GetType() == "artists" && xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos;
12457 
12458     // Check if the filter playlist matches the item type
12459     // Allow for grouping name like "originalyears" and type "years"
12460     if (xsp.GetType() == type ||
12461         (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed()))
12462     {
12463       filter.AppendWhere(xspWhere);
12464 
12465       if (xsp.GetLimit() > 0)
12466         sorting.limitEnd = xsp.GetLimit();
12467       if (xsp.GetOrder() != SortByNone)
12468         sorting.sortBy = xsp.GetOrder();
12469       sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
12470       if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
12471         sorting.sortAttributes = SortAttributeIgnoreArticle;
12472     }
12473   }
12474 
12475   //Process role options, common to artist and album type filtering
12476   int idRole = 1; // Default restrict song_artist to "artists" only, no other roles.
12477   option = options.find("roleid");
12478   if (option != options.end())
12479     idRole = static_cast<int>(option->second.asInteger());
12480   else
12481   {
12482     option = options.find("role");
12483     if (option != options.end())
12484     {
12485       if (option->second.asString() == "all" || option->second.asString() == "%")
12486         idRole = -1000; //All roles
12487       else
12488         idRole = GetRoleByName(option->second.asString());
12489     }
12490   }
12491   if (hasRoleRules)
12492   {
12493     // Get Role from role rule(s) here.
12494     // But that requires much change, so for now get all roles as better than none
12495     idRole = -1000; //All roles
12496   }
12497 
12498   std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role
12499   if(idRole > 0) strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole);
12500 
12501   int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1;
12502   int idDisc = -1;
12503   int idSource = -1;
12504   bool albumArtistsOnly = false;
12505   bool useOriginalYear = false;
12506   std::string artistname;
12507 
12508   // Process useoriginalyear option, setting overridden by option
12509   useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
12510       CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
12511   option = options.find("useoriginalyear");
12512   if (option != options.end())
12513     useOriginalYear = option->second.asBoolean();
12514 
12515   // Process albumartistsonly option
12516   option = options.find("albumartistsonly");
12517   if (option != options.end())
12518     albumArtistsOnly = option->second.asBoolean();
12519 
12520   // Process genre option
12521   option = options.find("genreid");
12522   if (option != options.end())
12523     idGenre = static_cast<int>(option->second.asInteger());
12524   else
12525   {
12526     option = options.find("genre");
12527     if (option != options.end())
12528       idGenre = GetGenreByName(option->second.asString());
12529   }
12530 
12531   // Process source option
12532   option = options.find("sourceid");
12533   if (option != options.end())
12534     idSource = static_cast<int>(option->second.asInteger());
12535   else
12536   {
12537     option = options.find("source");
12538     if (option != options.end())
12539       idSource = GetSourceByName(option->second.asString());
12540   }
12541 
12542   // Process album option
12543   option = options.find("albumid");
12544   if (option != options.end())
12545     idAlbum = static_cast<int>(option->second.asInteger());
12546   else
12547   {
12548     option = options.find("album");
12549     if (option != options.end())
12550       idAlbum = GetAlbumByName(option->second.asString());
12551   }
12552 
12553   // Process artist option
12554   option = options.find("artistid");
12555   if (option != options.end())
12556     idArtist = static_cast<int>(option->second.asInteger());
12557   else
12558   {
12559     option = options.find("artist");
12560     if (option != options.end())
12561     {
12562       idArtist = GetArtistByName(option->second.asString());
12563       if (idArtist == -1)
12564       {// not found with that name, or more than one found as artist name is not unique
12565         artistname = option->second.asString();
12566       }
12567     }
12568   }
12569 
12570   //  Process song option
12571   option = options.find("songid");
12572   if (option != options.end())
12573     idSong = static_cast<int>(option->second.asInteger());
12574 
12575   if (type == "artists")
12576   {
12577     if (!hasRoleRules)
12578     { // Not an "artists" smart playlist with roles rules, so get filter from options
12579       if (idArtist > 0)
12580         filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist));
12581       else if (idAlbum > 0)
12582         filter.AppendWhere(PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
12583           "WHERE album_artist.idAlbum = %i)", idAlbum));
12584       else if (idSong > 0)
12585       {
12586         filter.AppendWhere(PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
12587           "WHERE song_artist.idSong = %i %s)", idSong, strRoleSQL.c_str()));
12588       }
12589       else
12590       { /*
12591         Process idRole, idGenre, idSource and albumArtistsOnly options
12592 
12593         For artists these rules are combined because they apply via album and song
12594         and so we need to ensure all criteria are met via the same album or song.
12595         1) Some artists may be only album artists, so for all artists (with linked
12596            albums or songs) we need to check both album_artist and song_artist tables.
12597         2) Role is determined from song_artist table, so even if looking for album artists
12598            only we find those that also have a specific role e.g. which album artist is a
12599            composer of songs in that album, from entries in the song_artist table.
12600         a) Role < -1 is used to indicate that all roles are wanted.
12601         b) When not album artists only and a specific role wanted then only the song_artist
12602            table is checked.
12603         c) When album artists only and role = 1 (an "artist") then only the album_artist
12604            table is checked.
12605         */
12606         std::string albumArtistSQL, songArtistSQL;
12607         ExistsSubQuery albumArtistSub("album_artist", "album_artist.idArtist = artistview.idArtist");
12608         // Prepare album artist subquery SQL
12609         if (idSource > 0)
12610         {
12611           if (idRole == 1 && idGenre < 0)
12612           {
12613             albumArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
12614             albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
12615           }
12616           else
12617           {
12618             albumArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12619               "WHERE album_source.idSource = %i "
12620               "AND album_source.idAlbum = album_artist.idAlbum)", idSource));
12621           }
12622         }
12623         if (idRole <= 1 && idGenre > 0)
12624         { // Check genre of songs of album using nested subquery
12625           std::string strGenre = PrepareSQL("EXISTS(SELECT 1 FROM song "
12626             "JOIN song_genre ON song_genre.idSong = song.idSong "
12627             "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)", idGenre);
12628           albumArtistSub.AppendWhere(strGenre);
12629         }
12630 
12631         // Prepare song artist subquery SQL
12632         ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
12633         if (idRole > 0)
12634           songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
12635         if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1)
12636         {
12637           songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
12638             "JOIN song_genre ON song_genre.idSong = song.idSong "
12639             "WHERE song.idSong = song_artist.idSong "
12640             "AND song_genre.idGenre = %i "
12641             "AND EXISTS(SELECT 1 FROM album_source "
12642             "WHERE album_source.idSource = %i "
12643             "AND album_source.idAlbum = song.idAlbum))", idGenre, idSource));
12644         }
12645         else
12646         {
12647           if (idGenre > 0)
12648           {
12649             songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
12650             songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
12651           }
12652           if (idSource > 0 && !albumArtistsOnly)
12653           {
12654             songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
12655             songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
12656             songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
12657           }
12658           if (idRole > 1 && albumArtistsOnly)
12659           { // Album artists only with role, check AND in album_artist for album of song
12660             // using nested subquery correlated with album_artist
12661             songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
12662             songArtistSub.param = "song_artist.idArtist = album_artist.idArtist";
12663             songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum");
12664           }
12665         }
12666 
12667         // Build filter clause from subqueries
12668         if (idRole > 1 && albumArtistsOnly)
12669         { // Album artists only with role, check AND in album_artist for album of song
12670           // using nested subquery correlated with album_artist
12671           songArtistSub.BuildSQL(songArtistSQL);
12672           albumArtistSub.AppendWhere(songArtistSQL);
12673           albumArtistSub.BuildSQL(albumArtistSQL);
12674           filter.AppendWhere(albumArtistSQL);
12675         }
12676         else
12677         {
12678           songArtistSub.BuildSQL(songArtistSQL);
12679           albumArtistSub.BuildSQL(albumArtistSQL);
12680           if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
12681           { // Artist contributing to songs, any role, check OR album artist too
12682             // as artists can be just album artists but not song artists
12683             filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
12684           }
12685           else if (idRole > 1)
12686           {
12687             // Artist contributes that role (not albmartistsonly as already handled)
12688             filter.AppendWhere(songArtistSQL);
12689           }
12690           else // idRole = 1 and albumArtistsOnly
12691           { // Only look at album artists, not albums where artist features on songs
12692             filter.AppendWhere(albumArtistSQL);
12693           }
12694         }
12695       }
12696     }
12697     // remove the null string
12698     filter.AppendWhere("artistview.strArtist != ''");
12699   }
12700   else if (type == "albums")
12701   {
12702     option = options.find("year");
12703     if (option != options.end())
12704     {
12705       if (!useOriginalYear)
12706         filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
12707           option->second.asString().c_str()));
12708       else
12709         filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
12710           option->second.asString().c_str()));
12711     }
12712     option = options.find("compilation");
12713     if (option != options.end())
12714       filter.AppendWhere(PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
12715 
12716     option = options.find("boxset");
12717     if (option != options.end())
12718       filter.AppendWhere(
12719           PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
12720 
12721     if (idSource > 0)
12722       filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12723         "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)", idSource));
12724 
12725     // Process artist, role and genre options together as song subquery to filter those
12726     // albums that have songs with both that artist and genre
12727     std::string albumArtistSQL, songArtistSQL, genreSQL;
12728     ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum");
12729     genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
12730     genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
12731     ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
12732     ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
12733     songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
12734 
12735     if (idArtist > 0)
12736     {
12737       songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist));
12738       albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist));
12739     }
12740     else if (!artistname.empty())
12741     { // Artist name is not unique, so could get albums or songs from more than one.
12742       songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
12743       songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
12744 
12745       albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
12746       albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
12747     }
12748     if (idRole > 0)
12749       songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
12750     if (idGenre > 0)
12751     {
12752       songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
12753       songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
12754     }
12755 
12756     if (idArtist > 0 || !artistname.empty())
12757     {
12758       if (idRole <= 1 && idGenre > 0)
12759       { // Check genre of songs of album using nested subquery
12760         genreSub.BuildSQL(genreSQL);
12761         albumArtistSub.AppendWhere(genreSQL);
12762       }
12763       if (idRole > 1 && albumArtistsOnly)
12764       {  // Album artists only with role, check AND in album_artist for same song
12765          // using nested subquery correlated with album_artist
12766          songArtistSub.param = "song.idAlbum = album_artist.idAlbum";
12767          songArtistSub.BuildSQL(songArtistSQL);
12768          albumArtistSub.AppendWhere(songArtistSQL);
12769          albumArtistSub.BuildSQL(albumArtistSQL);
12770          filter.AppendWhere(albumArtistSQL);
12771       }
12772       else
12773       {
12774         songArtistSub.BuildSQL(songArtistSQL);
12775         albumArtistSub.BuildSQL(albumArtistSQL);
12776         if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
12777         { // Artist contributing to songs, any role, check OR album artist too
12778           // as artists can be just album artists but not song artists
12779           filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
12780         }
12781         else if (idRole > 1)
12782         { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
12783           filter.AppendWhere(songArtistSQL);
12784         }
12785         else // idRole = 1 and albumArtistsOnly
12786         { // Only look at album artists, not albums where artist features on songs
12787           // This may want to be a separate option so you can choose to see all the albums where that artist
12788           // appears on one or more songs without having to list all song artists in the artists node.
12789           filter.AppendWhere(albumArtistSQL);
12790         }
12791       }
12792     }
12793     else
12794     { // No artist given
12795       if (idGenre > 0)
12796       { // Have genre option but not artist
12797         genreSub.param = "song.idAlbum = albumview.idAlbum";
12798         genreSub.BuildSQL(genreSQL);
12799         filter.AppendWhere(genreSQL);
12800       }
12801       // Exclude any single albums (aka empty tagged albums)
12802       // This causes "albums"  media filter artist selection to only offer album artists
12803       option = options.find("show_singles");
12804       if (option == options.end() || !option->second.asBoolean())
12805         filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'", CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
12806     }
12807   }
12808   else if (type == "discs")
12809   {
12810     if (idAlbum > 0)
12811       filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum));
12812     else
12813     {
12814       option = options.find("year");
12815       if (option != options.end())
12816       {
12817         if (!useOriginalYear)
12818           filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
12819             option->second.asString().c_str()));
12820         else
12821           filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
12822             option->second.asString().c_str()));
12823       }
12824 
12825       option = options.find("compilation");
12826       if (option != options.end())
12827         filter.AppendWhere(
12828             PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
12829 
12830       option = options.find("boxset");
12831       if (option != options.end())
12832         filter.AppendWhere(
12833             PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
12834 
12835       if (idSource > 0)
12836         filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12837           "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)", idSource));
12838     }
12839     option = options.find("discid");
12840     if (option != options.end())
12841       filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger()));
12842 
12843     option = options.find("disctitle");
12844     if (option != options.end())
12845       filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str()));
12846 
12847     if (idGenre > 0)
12848       filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
12849                                     "song.idSong AND song_genre.idGenre = %i)",
12850                                     idGenre));
12851 
12852     std::string songArtistClause, albumArtistClause;
12853     if (idArtist > 0)
12854     {
12855       songArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
12856         "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
12857         idArtist, strRoleSQL.c_str());
12858       albumArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
12859         "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
12860         idArtist);
12861     }
12862     else if (!artistname.empty())
12863     {  // Artist name is not unique, so could get songs from more than one.
12864       songArtistClause = PrepareSQL(
12865           "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
12866           "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
12867           artistname.c_str(), strRoleSQL.c_str());
12868       albumArtistClause =
12869           PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
12870                      "album_artist.idArtist "
12871                      "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
12872                      artistname.c_str());
12873     }
12874 
12875     // Process artist name or id option
12876     if (!songArtistClause.empty())
12877     {
12878       if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
12879         filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12880       else if (idRole > 1)
12881       {
12882         if (albumArtistsOnly)  //Album artists only with role, check AND in album_artist for same song
12883           filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
12884         else // songs where artist contributes that role.
12885           filter.AppendWhere(songArtistClause);
12886       }
12887       else
12888       {
12889         if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
12890           filter.AppendWhere(albumArtistClause);
12891         else // Artist is song artist or album artist
12892           filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12893       }
12894     }
12895   }
12896   else if (type == "songs" || type == "singles")
12897   {
12898     option = options.find("singles");
12899     if (option != options.end())
12900       filter.AppendWhere(PrepareSQL("songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
12901                                     option->second.asBoolean() ? "" : "NOT ",
12902                                     CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
12903 
12904     // When have idAlbum skip year, compilation, boxset criteria as already applied via album
12905     if (idAlbum < 0)
12906     {
12907       option = options.find("year");
12908       if (option != options.end())
12909       {
12910         if (!useOriginalYear)
12911           filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
12912                                         option->second.asString().c_str()));
12913         else
12914           filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
12915                                         option->second.asString().c_str()));
12916       }
12917       option = options.find("compilation");
12918       if (option != options.end())
12919         filter.AppendWhere(
12920             PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
12921 
12922       option = options.find("boxset");
12923       if (option != options.end())
12924         filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
12925                                       "songview.idAlbum AND bBoxedSet = %i)",
12926                                       option->second.asBoolean() ? 1 : 0));
12927     }
12928 
12929     option = options.find("discid");
12930     if (option != options.end())
12931       idDisc = static_cast<int>(option->second.asInteger());
12932 
12933     option = options.find("disctitle");
12934     if (option != options.end())
12935       filter.AppendWhere(
12936           PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str()));
12937 
12938     if (idSong > 0)
12939       filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong));
12940 
12941     if (idAlbum > 0)
12942       filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum));
12943 
12944     if (idDisc > 0)
12945       filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc));
12946 
12947     if (idGenre > 0)
12948       filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre WHERE song_genre.idGenre = %i)", idGenre));
12949 
12950     if (idSource > 0)
12951       filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12952         "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)", idSource));
12953 
12954     std::string songArtistClause, albumArtistClause;
12955     if (idArtist > 0)
12956     {
12957       songArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
12958         "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
12959         idArtist, strRoleSQL.c_str());
12960       albumArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
12961         "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
12962         idArtist);
12963     }
12964     else if (!artistname.empty())
12965     {  // Artist name is not unique, so could get songs from more than one.
12966       songArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
12967         "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
12968         artistname.c_str(), strRoleSQL.c_str());
12969       albumArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = album_artist.idArtist "
12970         "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
12971         artistname.c_str());
12972     }
12973 
12974     // Process artist name or id option
12975     if (!songArtistClause.empty())
12976     {
12977       if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
12978         filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12979       else if (idRole > 1)
12980       {
12981         if (albumArtistsOnly)  //Album artists only with role, check AND in album_artist for same song
12982           filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
12983         else // songs where artist contributes that role.
12984           filter.AppendWhere(songArtistClause);
12985       }
12986       else
12987       {
12988         if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
12989           filter.AppendWhere(albumArtistClause);
12990         else // Artist is song artist or album artist
12991           filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12992       }
12993     }
12994   }
12995 
12996   option = options.find("filter");
12997   if (option != options.end())
12998   {
12999     CSmartPlaylist xspFilter;
13000     if (!xspFilter.LoadFromJson(option->second.asString()))
13001       return false;
13002 
13003     // check if the filter playlist matches the item type
13004     if (xspFilter.GetType() == type)
13005     {
13006       std::set<std::string> playlists;
13007       filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
13008     }
13009     // remove the filter if it doesn't match the item type
13010     else
13011       musicUrl.RemoveOption("filter");
13012   }
13013 
13014   return true;
13015 }
13016 
GetMediaDateFromFile(const std::string & strFileNameAndPath)13017 std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath)
13018 {
13019   if (strFileNameAndPath.empty())
13020     return std::string();
13021 
13022   CDateTime dateMedia;
13023   int code;
13024   code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded;
13025   // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
13026   if (code == 1)
13027     dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath);
13028   //2 using the newer datetime of the file's mtime and ctime
13029   else if (code == 2)
13030     dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath);
13031   //3 using the older datetime of the file's mtime and ctime
13032   else if (code == 3)
13033     dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath);
13034   //0 using the current datetime if none of the above matches or one returns an invalid datetime
13035   if (!dateMedia.IsValid())
13036     dateMedia = CDateTime::GetCurrentDateTime();
13037 
13038   return dateMedia.GetAsDBDateTime();
13039 }
13040 
AddAudioBook(const CFileItem & item)13041 bool CMusicDatabase::AddAudioBook(const CFileItem& item)
13042 {
13043   auto const& artists = item.GetMusicInfoTag()->GetArtist();
13044   std::string strSQL = PrepareSQL(
13045     "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
13046     "VALUES (NULL,'%s','%s',%i,'%s','%s')",
13047     item.GetMusicInfoTag()->GetAlbum().c_str(),
13048     artists.empty() ? "" : artists[0].c_str(),
13049     0,
13050     item.GetDynPath().c_str(),
13051     CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()
13052   );
13053   return ExecuteQuery(strSQL);
13054 }
13055 
SetResumeBookmarkForAudioBook(const CFileItem & item,int bookmark)13056 bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark)
13057 {
13058   std::string strSQL = PrepareSQL("select bookmark from audiobook where file='%s'",
13059                                  item.GetDynPath().c_str());
13060   if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13061   {
13062     if (!AddAudioBook(item))
13063       return false;
13064   }
13065 
13066   strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i WHERE file='%s'",
13067                       bookmark, item.GetDynPath().c_str());
13068 
13069   return ExecuteQuery(strSQL);
13070 }
13071 
GetResumeBookmarkForAudioBook(const CFileItem & item,int & bookmark)13072 bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark)
13073 {
13074   std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'",
13075                                  item.GetDynPath().c_str());
13076   if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13077     return false;
13078 
13079   bookmark = m_pDS->fv(0).get_asInt();
13080   return true;
13081 }
13082