[Windows] Fix driver version detection of AMD RDNA+ GPU on Windows 10
[xbmc.git] / xbmc / music / MusicDatabase.cpp
blobf09351b5fbb4adef05188af23ec295bbb5b27a1c
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "MusicDatabase.h"
11 #include "Album.h"
12 #include "Artist.h"
13 #include "FileItem.h"
14 #include "FileItemList.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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h"
27 #include "dbwrappers/dataset.h"
28 #include "dialogs/GUIDialogKaiToast.h"
29 #include "dialogs/GUIDialogProgress.h"
30 #include "dialogs/GUIDialogSelect.h"
31 #include "events/EventLog.h"
32 #include "events/NotificationEvent.h"
33 #include "filesystem/Directory.h"
34 #include "filesystem/DirectoryCache.h"
35 #include "filesystem/File.h"
36 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
37 #include "guilib/GUIComponent.h"
38 #include "guilib/GUIWindowManager.h"
39 #include "guilib/LocalizeStrings.h"
40 #include "guilib/guiinfo/GUIInfoLabels.h"
41 #include "interfaces/AnnouncementManager.h"
42 #include "messaging/helpers/DialogHelper.h"
43 #include "messaging/helpers/DialogOKHelper.h"
44 #include "music/MusicDbUrl.h"
45 #include "music/MusicLibraryQueue.h"
46 #include "music/tags/MusicInfoTag.h"
47 #include "network/Network.h"
48 #include "network/cddb.h"
49 #include "playlists/SmartPlayList.h"
50 #include "profiles/ProfileManager.h"
51 #include "settings/AdvancedSettings.h"
52 #include "settings/MediaSourceSettings.h"
53 #include "settings/Settings.h"
54 #include "settings/SettingsComponent.h"
55 #include "storage/MediaManager.h"
56 #include "utils/FileUtils.h"
57 #include "utils/LegacyPathTranslation.h"
58 #include "utils/MathUtils.h"
59 #include "utils/Random.h"
60 #include "utils/StringUtils.h"
61 #include "utils/URIUtils.h"
62 #include "utils/XMLUtils.h"
63 #include "utils/log.h"
65 #include <inttypes.h>
67 using namespace KODI;
68 using namespace XFILE;
69 using namespace MUSICDATABASEDIRECTORY;
70 using namespace KODI::MESSAGING;
71 using namespace MUSIC_INFO;
73 using ADDON::AddonPtr;
74 using KODI::MESSAGING::HELPERS::DialogResponse;
76 #define RECENTLY_PLAYED_LIMIT 25
77 #define MIN_FULL_SEARCH_LENGTH 3
79 #ifdef HAS_OPTICAL_DRIVE
80 using namespace CDDB;
81 using namespace MEDIA_DETECT;
82 #endif
84 static void AnnounceRemove(const std::string& content, int id)
86 CVariant data;
87 data["type"] = content;
88 data["id"] = id;
89 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
90 data["transaction"] = true;
91 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data);
94 static void AnnounceUpdate(const std::string& content, int id, bool added = false)
96 CVariant data;
97 data["type"] = content;
98 data["id"] = id;
99 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
100 data["transaction"] = true;
101 if (added)
102 data["added"] = true;
103 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data);
106 CMusicDatabase::CMusicDatabase(void)
108 m_translateBlankArtist = true;
111 CMusicDatabase::~CMusicDatabase(void)
113 EmptyCache();
116 bool CMusicDatabase::Open()
118 return CDatabase::Open(
119 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic);
122 void CMusicDatabase::CreateTables()
124 CLog::Log(LOGINFO, "create artist table");
125 m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, "
126 " strArtist varchar(256), strMusicBrainzArtistID text, "
127 " strSortName text, "
128 " strType text, strGender text, strDisambiguation text, "
129 " strBorn text, strFormed text, strGenres text, strMoods text, "
130 " strStyles text, strInstruments text, strBiography text, "
131 " strDied text, strDisbanded text, strYearsActive text, "
132 " strImage text, "
133 " lastScraped varchar(20) default NULL, "
134 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
135 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
136 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
137 // Create missing artist tag artist [Missing].
138 std::string strSQL =
139 PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
140 "VALUES( %i, '%s', '%s', '%s' )",
141 BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_NAME.c_str(),
142 BLANKARTIST_FAKEMUSICBRAINZID.c_str());
143 m_pDS->exec(strSQL);
145 CLog::Log(LOGINFO, "create album table");
146 m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, "
147 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
148 " strReleaseGroupMBID text, "
149 " strArtistDisp text, strArtistSort text, strGenres text, "
150 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
151 " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
152 " bCompilation integer not null default '0', "
153 " strMoods text, strStyles text, strThemes text, "
154 " strReview text, strImage text, strLabel text, "
155 " strType text, "
156 " strReleaseStatus TEXT, "
157 " fRating FLOAT NOT NULL DEFAULT 0, "
158 " iVotes INTEGER NOT NULL DEFAULT 0, "
159 " iUserrating INTEGER NOT NULL DEFAULT 0, "
160 " lastScraped varchar(20) default NULL, "
161 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
162 " strReleaseType text, "
163 " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
164 " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
165 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
166 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
168 CLog::Log(LOGINFO, "create audiobook table");
169 m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
170 " strBook varchar(256), strAuthor text,"
171 " bookmark integer, file text,"
172 " dateAdded varchar (20) default NULL)");
174 CLog::Log(LOGINFO, "create album_artist table");
175 m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, "
176 "strArtist text)");
178 CLog::Log(LOGINFO, "create album_source table");
179 m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
181 CLog::Log(LOGINFO, "create genre table");
182 m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
184 CLog::Log(LOGINFO, "create path table");
185 m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
187 CLog::Log(LOGINFO, "create source table");
188 m_pDS->exec(
189 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
191 CLog::Log(LOGINFO, "create source_path table");
192 m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
194 CLog::Log(LOGINFO, "create song table");
195 m_pDS->exec("CREATE TABLE song (idSong integer primary key, "
196 " idAlbum integer, idPath integer, "
197 " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
198 " iTrack integer, iDuration integer, "
199 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
200 " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
201 " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
202 " lastplayed varchar(20) default NULL, "
203 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
204 " userrating INTEGER NOT NULL DEFAULT 0, "
205 " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
206 " iBitRate INTEGER NOT NULL DEFAULT 0, "
207 " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
208 " strVideoURL TEXT, "
209 " strReplayGain text, "
210 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
211 CLog::Log(LOGINFO, "create song_artist table");
212 m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder "
213 "integer, strArtist text)");
214 CLog::Log(LOGINFO, "create song_genre table");
215 m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
217 CLog::Log(LOGINFO, "create role table");
218 m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
219 m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role
221 CLog::Log(LOGINFO, "create infosetting table");
222 m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, "
223 "strScraperPath TEXT, strSettings TEXT)");
225 CLog::Log(LOGINFO, "create discography table");
226 m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
227 "strReleaseGroupMBID TEXT)");
229 CLog::Log(LOGINFO, "create art table");
230 m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, "
231 "media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
233 CLog::Log(LOGINFO, "create versiontagscan table");
234 m_pDS->exec("CREATE TABLE versiontagscan "
235 "(idVersion INTEGER, iNeedsScan INTEGER, "
236 "lastscanned VARCHAR(20), "
237 "lastcleaned VARCHAR(20), "
238 "artistlinksupdated VARCHAR(20), "
239 "genresupdated VARCHAR(20))");
240 m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
241 GetSchemaVersion()));
243 CLog::Log(LOGINFO, "create removed_link table");
244 m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
247 void CMusicDatabase::CreateAnalytics()
249 CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__);
250 m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
251 m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
252 m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
253 m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
255 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
256 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
258 m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
260 m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
261 m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
262 m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
264 m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))");
266 m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
267 m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
269 m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
271 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
272 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
274 m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))");
275 m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
276 m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
277 m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
278 m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
279 //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
280 m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
282 m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
283 m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
284 m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
285 m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
287 m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
288 m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
290 m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))");
292 m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
294 m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
296 CLog::Log(LOGINFO, "create triggers");
297 m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
298 " DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
299 " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
300 " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
301 " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
302 " END");
303 m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
304 " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
305 " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
306 " DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
307 " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
308 " END");
309 m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
310 " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
311 " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
312 " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
313 " END");
314 m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
315 " DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
316 " DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
317 " END");
319 /* Maintain date new and last modified for songs, albums and artists using triggers
320 MySQL triggers cannot modify a table that is already being used by the statement that invoked
321 the trigger (to avoid recursion), but can set NEW column values before insert or update.
322 Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
323 Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
324 // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
326 bool bisMySQL = StringUtils::EqualsNoCase(
327 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
329 if (!bisMySQL)
330 { // SQLite trigger syntax - AFTER INSERT/UPDATE
331 m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
332 " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
333 " AND NEW.dateNew IS NULL;"
334 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
335 " END");
336 m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
337 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
338 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
339 " END");
340 m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
341 " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
342 " AND NEW.dateNew IS NULL;"
343 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
344 " END");
345 m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
346 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
347 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
348 " END");
349 m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
350 " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
351 " AND NEW.dateNew IS NULL;"
352 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
353 " END");
354 m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
355 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
356 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
357 " END");
359 m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
360 " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
361 " END");
363 else
364 { // MySQL trigger syntax - BEFORE INSERT/UPDATE
365 m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
366 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
367 " SET NEW.dateModified = now();"
368 " END");
369 m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
370 " SET NEW.dateModified = now()");
372 m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
373 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
374 " SET NEW.dateModified = now();"
375 " END");
376 m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
377 " SET NEW.dateModified = now()");
379 m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
380 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
381 " SET NEW.dateModified = now();"
382 " END");
383 m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
384 " SET NEW.dateModified = now()");
386 m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
387 " UPDATE versiontagscan SET genresupdated = now()");
390 // Triggers to maintain recent changes to album and song artist links in removed_link table
391 m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
392 "DELETE FROM removed_link "
393 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
394 "END");
395 m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
396 "DELETE FROM removed_link "
397 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
398 "END");
399 CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
401 // Create native functions stored in DB (MySQL/MariaDB only)
402 CreateNativeDBFunctions();
404 // we create views last to ensure all indexes are rolled in
405 CreateViews();
408 void CMusicDatabase::CreateRemovedLinkTriggers()
410 // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
411 m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
412 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
413 " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
414 " END");
415 m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
416 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
417 " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
418 " END");
422 void CMusicDatabase::CreateViews()
424 CLog::Log(LOGINFO, "create song view");
425 m_pDS->exec("CREATE VIEW songview AS SELECT "
426 " song.idSong AS idSong, "
427 " song.strArtistDisp AS strArtists,"
428 " song.strArtistSort AS strArtistSort,"
429 " song.strGenres AS strGenres,"
430 " strTitle, "
431 " iTrack, iDuration, "
432 " song.strReleaseDate as strReleaseDate, "
433 " song.strOrigReleaseDate as strOrigReleaseDate, "
434 " song.strDiscSubtitle as strDiscSubtitle, "
435 " strFileName, "
436 " strMusicBrainzTrackID, "
437 " iTimesPlayed, iStartOffset, iEndOffset, "
438 " lastplayed, "
439 " song.rating, "
440 " song.userrating, "
441 " song.votes, "
442 " comment, "
443 " song.idAlbum AS idAlbum, "
444 " strAlbum, "
445 " strPath, "
446 " album.strReleaseStatus as strReleaseStatus,"
447 " album.bCompilation AS bCompilation,"
448 " album.bBoxedSet AS bBoxedSet, "
449 " album.strArtistDisp AS strAlbumArtists,"
450 " album.strArtistSort AS strAlbumArtistSort,"
451 " album.strReleaseType AS strAlbumReleaseType,"
452 " song.mood as mood,"
453 " song.strReplayGain, "
454 " iBPM, "
455 " iBitRate, "
456 " iSampleRate, "
457 " iChannels, "
458 " song.strVideoURL as strVideoURL, "
459 " album.iAlbumDuration AS iAlbumDuration, "
460 " album.iDiscTotal as iDiscTotal, "
461 " song.dateAdded as dateAdded, "
462 " song.dateNew AS dateNew, "
463 " song.dateModified AS dateModified "
464 "FROM song"
465 " JOIN album ON"
466 " song.idAlbum=album.idAlbum"
467 " JOIN path ON"
468 " song.idPath=path.idPath");
470 CLog::Log(LOGINFO, "create album view");
471 m_pDS->exec("CREATE VIEW albumview AS SELECT "
472 "album.idAlbum AS idAlbum, "
473 "strAlbum, "
474 "strMusicBrainzAlbumID, "
475 "strReleaseGroupMBID, "
476 "album.strArtistDisp AS strArtists, "
477 "album.strArtistSort AS strArtistSort, "
478 "album.strGenres AS strGenres, "
479 "album.strReleaseDate as strReleaseDate, "
480 "album.strOrigReleaseDate as strOrigReleaseDate, "
481 "album.bBoxedSet AS bBoxedSet, "
482 "album.strMoods AS strMoods, "
483 "album.strStyles AS strStyles, "
484 "strThemes, "
485 "strReview, "
486 "strLabel, "
487 "strType, "
488 "strReleaseStatus, "
489 "album.strImage as strImage, "
490 "album.fRating, "
491 "album.iUserrating, "
492 "album.iVotes, "
493 "bCompilation, "
494 "bScrapedMBID,"
495 "lastScraped,"
496 "dateAdded, dateNew, dateModified, "
497 "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song "
498 "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
499 "strReleaseType, "
500 "iDiscTotal, "
501 "(SELECT MAX(song.lastplayed) FROM song "
502 "WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
503 "iAlbumDuration "
504 "FROM album");
506 CLog::Log(LOGINFO, "create artist view");
507 m_pDS->exec("CREATE VIEW artistview AS SELECT"
508 " idArtist, strArtist, strSortName, "
509 " strMusicBrainzArtistID, "
510 " strType, strGender, strDisambiguation, "
511 " strBorn, strFormed, strGenres,"
512 " strMoods, strStyles, strInstruments, "
513 " strBiography, strDied, strDisbanded, "
514 " strYearsActive, strImage, "
515 " bScrapedMBID, lastScraped, "
516 " dateAdded, dateNew, dateModified "
517 "FROM artist");
519 CLog::Log(LOGINFO, "create albumartist view");
520 m_pDS->exec("CREATE VIEW albumartistview AS SELECT"
521 " album_artist.idAlbum AS idAlbum, "
522 " album_artist.idArtist AS idArtist, "
523 " 0 AS idRole, "
524 " 'AlbumArtist' AS strRole, "
525 " artist.strArtist AS strArtist, "
526 " artist.strSortName AS strSortName,"
527 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
528 " album_artist.iOrder AS iOrder "
529 "FROM album_artist "
530 "JOIN artist ON "
531 " album_artist.idArtist = artist.idArtist");
533 CLog::Log(LOGINFO, "create songartist view");
534 m_pDS->exec("CREATE VIEW songartistview AS SELECT"
535 " song_artist.idSong AS idSong, "
536 " song_artist.idArtist AS idArtist, "
537 " song_artist.idRole AS idRole, "
538 " role.strRole AS strRole, "
539 " artist.strArtist AS strArtist, "
540 " artist.strSortName AS strSortName,"
541 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
542 " song_artist.iOrder AS iOrder "
543 "FROM song_artist "
544 "JOIN artist ON "
545 " song_artist.idArtist = artist.idArtist "
546 "JOIN role ON "
547 " song_artist.idRole = role.idRole");
550 void CMusicDatabase::CreateNativeDBFunctions()
552 // Create native functions in MySQL/MariaDB database only
553 if (!StringUtils::EqualsNoCase(
554 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
555 "mysql"))
556 return;
557 CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions");
558 /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
559 default utf8_general_ci collation that SQLite does via a collation sequence callback
560 function to StringUtils::AlphaNumericCompare
561 !@todo: the video needs these defined too for sorting in DB, then creation can be made common
563 // clang-format off
564 // udfFirstNumberPos finds the position of the first digit in a string
565 m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
566 m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
567 "RETURNS int \n"
568 "LANGUAGE SQL \n"
569 "DETERMINISTIC \n"
570 "NO SQL \n"
571 "SQL SECURITY INVOKER \n"
572 "BEGIN \n"
573 " DECLARE position int; \n"
574 " DECLARE tmppos int; \n"
575 " SET position = 5000; \n"
576 " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
577 " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
578 " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
579 " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
580 " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
581 " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
582 " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
583 " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
584 " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
585 " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
586 " IF(position = 5000) THEN RETURN 0; END IF;\n"
587 " RETURN position; \n"
588 "END\n");
590 // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input
591 m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
592 m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
593 "RETURNS varchar(1024)\n"
594 "LANGUAGE SQL\n"
595 "DETERMINISTIC\n"
596 "NO SQL\n"
597 "SQL SECURITY INVOKER\n"
598 "BEGIN\n"
599 " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
600 " DECLARE i int;\n"
601 " DECLARE symbolCharsLen int;\n"
602 " DECLARE symbol char(1);\n"
603 " SET sortString = instring;\n"
604 " SET i = 1;\n"
605 " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
606 " WHILE(i <= symbolCharsLen) DO\n"
607 " SET symbol = SUBSTRING(symbolChars, i, 1);\n"
608 " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
609 " SET i = i + 1;\n"
610 " END WHILE;\n"
611 " RETURN sortString;\n"
612 "END\n");
614 // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
615 m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
616 m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
617 "sameOrderChars char(25))\n"
618 "RETURNS varchar(1024)\n"
619 "LANGUAGE SQL\n"
620 "DETERMINISTIC\n"
621 "NO SQL\n"
622 "SQL SECURITY INVOKER\n"
623 "BEGIN\n"
624 " DECLARE sortString varchar(1024);\n"
625 " DECLARE shiftedString varchar(1024);\n"
626 " DECLARE inLength int;\n"
627 " DECLARE shiftedLength int;\n"
628 " DECLARE totalSympadLength int;\n"
629 " DECLARE symbolshifted512 varchar(1024);\n"
630 " DECLARE numStartIndex int; \n"
631 " DECLARE numEndIndex int; \n"
632 " DECLARE padLength int; \n"
633 " DECLARE totalPadLength int; \n"
634 " DECLARE i int; \n"
635 " DECLARE sameOrderCharsLen int;\n"
636 " SET totalPadLength = 0; \n"
637 " SET instring = TRIM(instring);\n"
638 " SET inLength = CHAR_LENGTH(inString);\n"
639 " SET sortString = instring; \n"
640 " SET numStartIndex = udfFirstNumberPos(instring); \n"
641 " SET numEndIndex = 0; \n"
642 " SET i = 1; \n"
643 " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
644 " WHILE(i <= sameOrderCharsLen) DO \n"
645 " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
646 " SET i = i + 1; \n"
647 " END WHILE; \n"
648 " WHILE(numStartIndex <> 0) DO \n"
649 " SET numStartIndex = numStartIndex + numEndIndex; \n"
650 " SET numEndIndex = numStartIndex; \n"
651 " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
652 " SET numEndIndex = numEndIndex + 1; \n"
653 " END WHILE; \n"
654 " SET numEndIndex = numEndIndex - 1; \n"
655 " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
656 " IF padLength < 0 THEN \n"
657 " SET padLength = 0; \n"
658 " END IF; \n"
659 " IF inLength + totalPadLength + padlength > 1024 THEN \n"
660 " -- Padding more digits would be too long, pad this one just enough \n"
661 " SET padLength = 1024 - inLength - totalPadLength; \n"
662 " SET numStartIndex = 0; \n"
663 " END IF; \n"
664 " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
665 " SET totalPadLength = totalPadLength + padLength; \n"
666 " IF numStartIndex <> 0 THEN \n"
667 " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
668 " END IF; \n"
669 " END WHILE; \n"
670 " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
671 " -- when there is space as this could double string length. Note '\\' needs escaping \n"
672 " SET numStartIndex = 1; \n"
673 " SET numEndIndex = inLength + totalPadLength; \n"
674 " IF numEndIndex < 1024 THEN \n"
675 " SET shiftedLength = 0; \n"
676 " SET totalSympadLength = 0; \n"
677 " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
678 " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
679 " SET numStartIndex = numStartIndex + 512; \n"
680 " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
681 " IF totalSympadLength = 0 THEN \n"
682 " SET shiftedString = symbolshifted512; \n"
683 " ELSE \n"
684 " IF totalSympadLength + shiftedLength > 1024 THEN \n"
685 " SET shiftedLength = 1024 - totalSympadLength; \n"
686 " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
687 " END IF; \n"
688 " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
689 " END IF; \n"
690 " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
691 " END WHILE; \n"
692 " SET sortString = shiftedString; \n"
693 " END IF; \n"
694 " RETURN sortString; \n"
695 "END\n");
696 // clang-format on
699 void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath,
700 std::string& strPath,
701 std::string& strFileName)
703 URIUtils::Split(strFileNameAndPath, strPath, strFileName);
704 // Keep protocol options as part of the path
705 if (URIUtils::IsURL(strFileNameAndPath))
707 CURL url(strFileNameAndPath);
708 if (!url.GetProtocolOptions().empty())
709 strPath += "|" + url.GetProtocolOptions();
713 bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource)
715 BeginTransaction();
716 SetLibraryLastUpdated();
718 album.idAlbum = AddAlbum(album.strAlbum, //
719 album.strMusicBrainzAlbumID, //
720 album.strReleaseGroupMBID, //
721 album.GetAlbumArtistString(), //
722 album.GetAlbumArtistSort(), //
723 album.GetGenreString(), //
724 album.strReleaseDate, //
725 album.strOrigReleaseDate, //
726 album.bBoxedSet, //
727 album.strLabel, //
728 album.strType, //
729 album.strReleaseStatus, //
730 album.bCompilation, //
731 album.releaseType);
733 // Add the album artists
734 // Album must have at least one artist so set artist to [Missing]
735 if (album.artistCredits.empty())
736 AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
737 for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
738 ++artistCredit)
740 artistCredit->idArtist =
741 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
742 artistCredit->GetSortName());
743 AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
744 static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
747 // Add songs
748 for (auto song = album.songs.begin(); song != album.songs.end(); ++song)
750 song->idAlbum = album.idAlbum;
752 song->idSong = AddSong(song->idSong, //
753 song->dateNew, //
754 song->idAlbum, //
755 song->strTitle, //
756 song->strMusicBrainzTrackID, //
757 song->strFileName, //
758 song->strComment, //
759 song->strMood, //
760 song->strThumb, //
761 song->GetArtistString(), //
762 song->GetArtistSort(), //
763 song->genre, //
764 song->iTrack, //
765 song->iDuration, //
766 song->strReleaseDate, //
767 song->strOrigReleaseDate, //
768 song->strDiscSubtitle, //
769 song->iTimesPlayed, //
770 song->iStartOffset, song->iEndOffset, //
771 song->lastPlayed, //
772 song->rating, //
773 song->userrating, //
774 song->votes, //
775 song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels, //
776 song->songVideoURL, //
777 song->replayGain);
779 // Song must have at least one artist so set artist to [Missing]
780 if (song->artistCredits.empty())
781 AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
783 for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end();
784 ++artistCredit)
786 artistCredit->idArtist =
787 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
788 artistCredit->GetSortName());
789 AddSongArtist(
790 artistCredit->idArtist, song->idSong, ROLE_ARTIST,
791 artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
792 static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit)));
794 // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
795 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
796 AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort());
799 // Set album duration as total of all songs on album.
800 // Folder layout may mean AddAlbum call has added more songs to an existing album
801 std::string strSQL;
802 strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum);
803 int albumDuration = GetSingleValueInt(strSQL);
804 m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration,
805 album.idAlbum));
807 // Add album sources
808 if (idSource > 0)
809 AddAlbumSource(album.idAlbum, idSource);
810 else
812 // Use album path, or failing that song paths to determine sources for the album
813 AddAlbumSources(album.idAlbum, album.strPath);
816 for (const auto& albumArt : album.art)
817 SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second);
819 // Set album disc total
820 m_pDS->exec(
821 PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
822 "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
823 album.idAlbum));
824 // Set a non-compilation album as a boxset if it has three or more distinct disc titles
825 if (!album.bBoxedSet && !album.bCompilation)
827 strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
828 album.idAlbum);
829 int numTitles = GetSingleValueInt(strSQL);
830 if (numTitles >= 3)
832 strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum);
833 m_pDS->exec(strSQL);
836 m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
837 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
838 album.idAlbum));
839 m_pDS->exec(
840 PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
841 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
842 album.idAlbum));
844 std::string albumdateadded =
845 GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum));
846 m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
847 albumdateadded.c_str(), album.idAlbum));
849 /* Update artist dateAdded values for artists involved in album as song or album artists.
850 Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
851 derived from song dateadded values which are usually file dates (or the last scan).
852 It is used to indicate those artists with recent media.
853 For artists that are neither album nor song artists (other roles only) dateadded will be null.
855 std::vector<std::string> artistIDs;
856 // Get distinct song and album artist IDs for this album
857 GetArtistsByAlbum(album.idAlbum, artistIDs);
858 std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")";
859 strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' "
860 "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
861 albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str());
862 m_pDS->exec(strSQL);
864 CommitTransaction();
865 return true;
868 bool CMusicDatabase::UpdateAlbum(CAlbum& album)
870 BeginTransaction();
871 SetLibraryLastUpdated();
873 const std::string itemSeparator =
874 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
876 if (album.bBoxedSet)
878 bool isBoxset = IsAlbumBoxset(album.idAlbum);
879 if (!isBoxset)
880 { // not a boxset already so make sure we have enough discs & they all have titles
881 bool canBeBoxset = false;
882 std::string strSQL;
883 strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum);
884 int numDiscs = GetSingleValueInt(strSQL);
885 if (numDiscs >= 2)
887 canBeBoxset = true;
888 int discValue;
889 for (discValue = 1; discValue <= numDiscs; discValue++)
891 strSQL =
892 PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
893 "song.iTrack >> 16 = %i",
894 album.idAlbum, discValue);
895 std::string currentTitle = GetSingleValue(strSQL);
896 if (currentTitle.empty())
898 currentTitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discValue);
899 strSQL =
900 PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
901 "song.iTrack >> 16 = %i",
902 currentTitle.c_str(), album.idAlbum, discValue);
903 ExecuteQuery(strSQL);
907 if (!canBeBoxset && album.bBoxedSet)
909 CLog::Log(LOGINFO, "{} : Album with id [{}] does not meet the requirements for a boxset.",
910 __FUNCTION__, album.idAlbum);
911 album.bBoxedSet = false;
915 UpdateAlbum(album.idAlbum, album.strAlbum, album.strMusicBrainzAlbumID, //
916 album.strReleaseGroupMBID, //
917 album.GetAlbumArtistString(), album.GetAlbumArtistSort(), //
918 album.GetGenreString(), //
919 StringUtils::Join(album.moods, itemSeparator), //
920 StringUtils::Join(album.styles, itemSeparator), //
921 StringUtils::Join(album.themes, itemSeparator), //
922 album.strReview, //
923 album.thumbURL.GetData(), //
924 album.strLabel, //
925 album.strType, //
926 album.strReleaseStatus, //
927 album.fRating, album.iUserrating, album.iVotes, //
928 album.strReleaseDate, //
929 album.strOrigReleaseDate, //
930 album.bBoxedSet, //
931 album.bCompilation, //
932 album.releaseType, //
933 album.bScrapedMBID);
935 if (!album.bArtistSongMerge)
937 // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
938 for (const auto& artistCredit : album.artistCredits)
939 UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID());
941 else
943 // Replace the album artists with those scraped or set by JSON
944 DeleteAlbumArtistsByAlbum(album.idAlbum);
945 // Album must have at least one artist so set artist to [Missing]
946 if (album.artistCredits.empty())
947 AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
948 for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
949 ++artistCredit)
951 artistCredit->idArtist =
952 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
953 artistCredit->GetSortName(), true);
954 AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
955 static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
957 /* Replace the songs with those scraped or imported, but if new songs is empty
958 (such as when called from JSON) do not remove the original ones
959 Also updates nested data e.g. song artists, song genres and contributors
960 Do not check for artist link changes, that is done later for all songs and album
962 int albumDuration = 0;
963 for (auto& song : album.songs)
965 UpdateSong(song);
966 albumDuration += song.iDuration;
968 if (albumDuration > 0)
969 m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
970 albumDuration, album.idAlbum));
973 if (!album.art.empty())
974 SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
976 CheckArtistLinksChanged();
978 CommitTransaction();
979 return true;
982 void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal)
984 // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
985 int iDate;
986 iDate = StringUtils::DateStringToYYYYMMDD(strRelease);
987 if (iDate < 0)
988 strRelease.clear();
989 iDate = StringUtils::DateStringToYYYYMMDD(strOriginal);
990 if (iDate < 0)
991 strOriginal.clear();
992 // Avoid missing release or original values unless both invalid or empty
993 if (!strRelease.empty() && strOriginal.empty())
994 strOriginal = strRelease;
995 else if (strRelease.empty() && !strOriginal.empty())
996 strRelease = strOriginal;
999 int CMusicDatabase::AddSong(const int idSong,
1000 const CDateTime& dtDateNew,
1001 const int idAlbum,
1002 const std::string& strTitle,
1003 const std::string& strMusicBrainzTrackID,
1004 const std::string& strPathAndFileName,
1005 const std::string& strComment,
1006 const std::string& strMood,
1007 const std::string& strThumb,
1008 const std::string& artistDisp,
1009 const std::string& artistSort,
1010 const std::vector<std::string>& genres,
1011 int iTrack,
1012 int iDuration,
1013 const std::string& strReleaseDate,
1014 const std::string& strOrigReleaseDate,
1015 std::string& strDiscSubtitle,
1016 const int iTimesPlayed,
1017 int iStartOffset,
1018 int iEndOffset,
1019 const CDateTime& dtLastPlayed,
1020 float rating,
1021 int userrating,
1022 int votes,
1023 int iBPM,
1024 int iBitRate,
1025 int iSampleRate,
1026 int iChannels,
1027 const std::string& songVideoURL,
1028 const ReplayGain& replayGain)
1030 int idNew = -1;
1031 std::string strSQL;
1034 // We need at least the title
1035 if (strTitle.empty())
1036 return -1;
1038 if (nullptr == m_pDB)
1039 return -1;
1040 if (nullptr == m_pDS)
1041 return -1;
1043 std::string strPath, strFileName;
1044 SplitPath(strPathAndFileName, strPath, strFileName);
1045 int idPath = AddPath(strPath);
1047 if (idSong <= 1)
1049 if (!strMusicBrainzTrackID.empty())
1050 strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1051 "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
1052 idAlbum, iTrack, strMusicBrainzTrackID.c_str());
1053 else
1054 strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1055 "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
1056 "AND strMusicBrainzTrackID IS NULL",
1057 idAlbum, strFileName.c_str(), strTitle.c_str(), iTrack);
1059 if (!m_pDS->query(strSQL))
1060 return -1;
1062 if (m_pDS->num_rows() == 0)
1064 m_pDS->close();
1066 // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
1067 bool isBoxset = IsAlbumBoxset(idAlbum);
1068 if (isBoxset && strDiscSubtitle.empty())
1070 int discno = iTrack >> 16;
1071 strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discno);
1074 // Validate ISO8601 dates and ensure none missing
1075 std::string strRelease = strReleaseDate;
1076 std::string strOriginal = strOrigReleaseDate;
1077 NormaliseSongDates(strRelease, strOriginal);
1079 // Get dateAdded from music file timestamp
1080 std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1082 strSQL = "INSERT INTO song ("
1083 "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
1084 "strTitle, iTrack, iDuration, "
1085 "strReleaseDate, strOrigReleaseDate, iBPM, "
1086 "iBitrate, iSampleRate, iChannels, "
1087 "strDiscSubtitle, strFileName, dateAdded, "
1088 "strMusicBrainzTrackID, strArtistSort, "
1089 "iTimesPlayed, iStartOffset, iEndOffset, "
1090 "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
1092 if (idSong <= 0)
1093 // Song ID is autoincremented and dateNew set by trigger
1094 strSQL += PrepareSQL("VALUES (NULL, NULL, ");
1095 else
1096 //Reuse song Id and original date when the Id added
1097 strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str());
1099 strSQL +=
1100 PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
1101 idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration,
1102 strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate,
1103 iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str());
1105 if (strMusicBrainzTrackID.empty())
1106 strSQL += PrepareSQL(",NULL");
1107 else
1108 strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str());
1109 if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1110 strSQL += PrepareSQL(",NULL");
1111 else
1112 strSQL += PrepareSQL(",'%s'", artistSort.c_str());
1114 if (dtLastPlayed.IsValid())
1115 strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", //
1116 iTimesPlayed, iStartOffset, iEndOffset,
1117 dtLastPlayed.GetAsDBDateTime().c_str(), //
1118 static_cast<double>(rating), userrating, votes, //
1119 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1120 else
1121 strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", //
1122 iTimesPlayed, iStartOffset, iEndOffset, //
1123 static_cast<double>(rating), userrating, votes, //
1124 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1125 m_pDS->exec(strSQL);
1126 if (idSong <= 0)
1127 idNew = (int)m_pDS->lastinsertid();
1128 else
1129 idNew = idSong;
1131 else
1133 idNew = m_pDS->fv("idSong").get_asInt();
1134 m_pDS->close();
1135 UpdateSong(idNew, //
1136 strTitle, //
1137 strMusicBrainzTrackID, //
1138 strPathAndFileName, //
1139 strComment, //
1140 strMood, //
1141 strThumb, //
1142 artistDisp, //
1143 artistSort, //
1144 genres, //
1145 iTrack, //
1146 iDuration, //
1147 strReleaseDate, //
1148 strOrigReleaseDate, //
1149 strDiscSubtitle, //
1150 iTimesPlayed, //
1151 iStartOffset, iEndOffset, //
1152 dtLastPlayed, //
1153 rating, userrating, votes, //
1154 replayGain, //
1155 iBPM, iBitRate, iSampleRate, iChannels, songVideoURL);
1157 if (!strThumb.empty())
1158 SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb);
1160 // Song genres added, and genre string updated to use the standardised genre names
1161 AddSongGenres(idNew, genres);
1163 AnnounceUpdate(MediaTypeSong, idNew, true);
1165 catch (...)
1167 CLog::Log(LOGERROR, "musicdatabase:unable to addsong ({})", strSQL);
1169 return idNew;
1172 bool CMusicDatabase::GetSong(int idSong, CSong& song)
1176 song.Clear();
1178 if (nullptr == m_pDB)
1179 return false;
1180 if (nullptr == m_pDS)
1181 return false;
1183 std::string strSQL =
1184 PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
1185 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1186 " WHERE songview.idSong = %i "
1187 " ORDER BY songartistview.idRole, songartistview.iOrder",
1188 idSong);
1190 if (!m_pDS->query(strSQL))
1191 return false;
1192 int iRowsFound = m_pDS->num_rows();
1193 if (iRowsFound == 0)
1195 m_pDS->close();
1196 return false;
1199 int songArtistOffset = song_enumCount;
1201 song = GetSongFromDataset(m_pDS->get_sql_record());
1202 while (!m_pDS->eof())
1204 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1206 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1207 if (idSongArtistRole == ROLE_ARTIST)
1208 song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
1209 else
1210 song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1212 m_pDS->next();
1214 m_pDS->close(); // cleanup recordset data
1215 return true;
1217 catch (...)
1219 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
1222 return false;
1225 bool CMusicDatabase::UpdateSong(CSong& song, bool bArtists /*= true*/, bool bArtistLinks /*= true*/)
1227 int result = UpdateSong(song.idSong,
1228 song.strTitle, //
1229 song.strMusicBrainzTrackID, //
1230 song.strFileName, //
1231 song.strComment, //
1232 song.strMood, //
1233 song.strThumb, //
1234 song.GetArtistString(), //
1235 song.GetArtistSort(), //
1236 song.genre, //
1237 song.iTrack, //
1238 song.iDuration, //
1239 song.strReleaseDate, //
1240 song.strOrigReleaseDate, //
1241 song.strDiscSubtitle, //
1242 song.iTimesPlayed, //
1243 song.iStartOffset, song.iEndOffset, //
1244 song.lastPlayed, //
1245 song.rating, song.userrating, song.votes, //
1246 song.replayGain, //
1247 song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels, //
1248 song.songVideoURL);
1249 if (result < 0)
1250 return false;
1252 // Replace Song genres and update genre string using the standardised genre names
1253 AddSongGenres(song.idSong, song.genre);
1254 if (bArtists)
1256 //Replace song artists and contributors
1257 DeleteSongArtistsBySong(song.idSong);
1258 // Song must have at least one artist so set artist to [Missing]
1259 if (song.artistCredits.empty())
1260 AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
1261 for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end();
1262 ++artistCredit)
1264 artistCredit->idArtist =
1265 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
1266 artistCredit->GetSortName());
1267 AddSongArtist(artistCredit->idArtist, song.idSong, ROLE_ARTIST, artistCredit->GetArtist(),
1268 static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit)));
1270 // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
1271 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
1272 AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort());
1274 if (bArtistLinks)
1275 CheckArtistLinksChanged();
1278 return true;
1281 int CMusicDatabase::UpdateSong(int idSong,
1282 const std::string& strTitle,
1283 const std::string& strMusicBrainzTrackID,
1284 const std::string& strPathAndFileName,
1285 const std::string& strComment,
1286 const std::string& strMood,
1287 const std::string& strThumb,
1288 const std::string& artistDisp,
1289 const std::string& artistSort,
1290 const std::vector<std::string>& genres,
1291 int iTrack,
1292 int iDuration,
1293 const std::string& strReleaseDate,
1294 const std::string& strOrigReleaseDate,
1295 const std::string& strDiscSubtitle,
1296 int iTimesPlayed,
1297 int iStartOffset,
1298 int iEndOffset,
1299 const CDateTime& dtLastPlayed,
1300 float rating,
1301 int userrating,
1302 int votes,
1303 const ReplayGain& replayGain,
1304 int iBPM,
1305 int iBitRate,
1306 int iSampleRate,
1307 int iChannels,
1308 const std::string& songVideoURL)
1310 if (idSong < 0)
1311 return -1;
1313 std::string strSQL;
1314 std::string strPath, strFileName;
1315 SplitPath(strPathAndFileName, strPath, strFileName);
1316 int idPath = AddPath(strPath);
1318 // Validate ISO8601 dates and ensure none missing
1319 std::string strRelease = strReleaseDate;
1320 std::string strOriginal = strOrigReleaseDate;
1321 NormaliseSongDates(strRelease, strOriginal);
1323 std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1325 strSQL = PrepareSQL(
1326 "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
1327 " strTitle = '%s', iTrack = %i, iDuration = %i, "
1328 "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
1329 "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
1330 "dateAdded = '%s', strVideoURL = '%s'",
1331 idPath, artistDisp.c_str(),
1332 StringUtils::Join(
1333 genres,
1334 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)
1335 .c_str(),
1336 strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(),
1337 strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels,
1338 strDateMedia.c_str(), songVideoURL.c_str());
1339 if (strMusicBrainzTrackID.empty())
1340 strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL");
1341 else
1342 strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str());
1343 if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1344 strSQL += PrepareSQL(", strArtistSort = NULL");
1345 else
1346 strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str());
1348 strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
1349 "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
1350 iStartOffset, iEndOffset, static_cast<double>(rating), userrating, votes,
1351 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1353 if (dtLastPlayed.IsValid())
1354 strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1355 dtLastPlayed.GetAsDBDateTime().c_str());
1356 else if (iTimesPlayed > 0)
1357 strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1358 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
1359 else
1360 strSQL += ", iTimesPlayed = 0, lastplayed = NULL ";
1361 strSQL += PrepareSQL("WHERE idSong = %i", idSong);
1363 bool status = ExecuteQuery(strSQL);
1365 if (status)
1366 AnnounceUpdate(MediaTypeSong, idSong);
1367 return idSong;
1370 int CMusicDatabase::AddAlbum(const std::string& strAlbum,
1371 const std::string& strMusicBrainzAlbumID,
1372 const std::string& strReleaseGroupMBID,
1373 const std::string& strArtist,
1374 const std::string& strArtistSort,
1375 const std::string& strGenre,
1376 const std::string& strReleaseDate,
1377 const std::string& strOrigReleaseDate,
1378 bool bBoxedSet,
1379 const std::string& strRecordLabel,
1380 const std::string& strType,
1381 const std::string& strReleaseStatus,
1382 bool bCompilation,
1383 CAlbum::ReleaseType releaseType)
1385 std::string strSQL;
1388 if (nullptr == m_pDB)
1389 return -1;
1390 if (nullptr == m_pDS)
1391 return -1;
1393 if (!strMusicBrainzAlbumID.empty())
1394 strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
1395 strMusicBrainzAlbumID.c_str());
1396 else
1397 strSQL = PrepareSQL("SELECT * FROM album "
1398 "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
1399 "AND strMusicBrainzAlbumID IS NULL",
1400 strArtist.c_str(), strAlbum.c_str());
1401 m_pDS->query(strSQL);
1402 std::string strCheckFlag = strType;
1403 StringUtils::ToLower(strCheckFlag);
1404 if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type
1405 bBoxedSet = true;
1406 if (m_pDS->num_rows() == 0)
1408 m_pDS->close();
1409 // Does not exist, add it
1410 strSQL =
1411 PrepareSQL("INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
1412 "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
1413 "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, "
1414 "strMusicBrainzAlbumID, "
1415 "strReleaseGroupMBID, strArtistSort) "
1416 "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
1417 strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
1418 strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet, //
1419 strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), //
1420 bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str());
1422 if (strMusicBrainzAlbumID.empty())
1423 strSQL += PrepareSQL(", NULL");
1424 else
1425 strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str());
1426 if (strReleaseGroupMBID.empty())
1427 strSQL += PrepareSQL(", NULL");
1428 else
1429 strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str());
1430 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1431 strSQL += PrepareSQL(", NULL");
1432 else
1433 strSQL += PrepareSQL(", '%s'", strArtistSort.c_str());
1434 strSQL += ")";
1435 m_pDS->exec(strSQL);
1437 return (int)m_pDS->lastinsertid();
1439 else
1441 /* Exists in our database and being re-scanned from tags, so we should update it as the details
1442 may have changed.
1444 Note that for multi-folder albums this will mean the last folder scanned will have the information
1445 stored for it. Most values here should be the same across all songs anyway, but it does mean
1446 that if there's any inconsistencies then only the last folders information will be taken.
1448 We make sure we clear out the link tables (album artists, album sources) and we reset
1449 the last scraped time to make sure that online metadata is re-fetched. */
1450 int idAlbum = m_pDS->fv("idAlbum").get_asInt();
1451 m_pDS->close();
1453 strSQL = "UPDATE album SET ";
1454 if (!strMusicBrainzAlbumID.empty())
1455 strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", //
1456 strAlbum.c_str(), strArtist.c_str());
1457 if (strReleaseGroupMBID.empty())
1458 strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,");
1459 else
1460 strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str());
1461 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1462 strSQL += PrepareSQL(" strArtistSort = NULL");
1463 else
1464 strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str());
1466 strSQL +=
1467 PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1468 "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
1469 "bCompilation=%i, strReleaseType = '%s', "
1470 "lastScraped = NULL "
1471 "WHERE idAlbum=%i",
1472 strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
1473 bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(),
1474 bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(), //
1475 idAlbum);
1476 m_pDS->exec(strSQL);
1477 DeleteAlbumArtistsByAlbum(idAlbum);
1478 DeleteAlbumSources(idAlbum);
1479 return idAlbum;
1482 catch (...)
1484 CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
1487 return -1;
1490 int CMusicDatabase::UpdateAlbum(int idAlbum,
1491 const std::string& strAlbum,
1492 const std::string& strMusicBrainzAlbumID,
1493 const std::string& strReleaseGroupMBID,
1494 const std::string& strArtist,
1495 const std::string& strArtistSort,
1496 const std::string& strGenre,
1497 const std::string& strMoods,
1498 const std::string& strStyles,
1499 const std::string& strThemes,
1500 const std::string& strReview,
1501 const std::string& strImage,
1502 const std::string& strLabel,
1503 const std::string& strType,
1504 const std::string& strReleaseStatus,
1505 float fRating,
1506 int iUserrating,
1507 int iVotes,
1508 const std::string& strReleaseDate,
1509 const std::string& strOrigReleaseDate,
1510 bool bBoxedSet,
1511 bool bCompilation,
1512 CAlbum::ReleaseType releaseType,
1513 bool bScrapedMBID)
1515 if (idAlbum < 0)
1516 return -1;
1518 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1519 // Truncate value cleaning up xml when URLs exceeds this
1520 std::string strImageURLs = strImage;
1521 if (StringUtils::EqualsNoCase(
1522 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1523 "mysql"))
1524 TrimImageURLs(strImageURLs, 65535);
1526 std::string strSQL;
1527 strSQL = PrepareSQL("UPDATE album SET "
1528 " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
1529 " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
1530 " strReview = '%s', strImage = '%s', strLabel = '%s', "
1531 " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
1532 " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1533 " bBoxedSet = %i, bCompilation = %i,"
1534 " strReleaseType = '%s', strReleaseStatus = '%s', "
1535 " lastScraped = '%s', bScrapedMBID = %i",
1536 strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
1537 strMoods.c_str(), strStyles.c_str(), strThemes.c_str(), //
1538 strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(), //
1539 strType.c_str(), static_cast<double>(fRating), iUserrating, iVotes, //
1540 strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
1541 bBoxedSet, bCompilation, //
1542 CAlbum::ReleaseTypeToString(releaseType).c_str(), strReleaseStatus.c_str(), //
1543 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), bScrapedMBID);
1544 if (strMusicBrainzAlbumID.empty())
1545 strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL");
1546 else
1547 strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str());
1548 if (strReleaseGroupMBID.empty())
1549 strSQL += PrepareSQL(", strReleaseGroupMBID = NULL");
1550 else
1551 strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str());
1552 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1553 strSQL += PrepareSQL(", strArtistSort = NULL");
1554 else
1555 strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str());
1557 strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum);
1559 bool status = ExecuteQuery(strSQL);
1560 if (status)
1561 AnnounceUpdate(MediaTypeAlbum, idAlbum);
1562 return idAlbum;
1565 bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */)
1569 if (nullptr == m_pDB)
1570 return false;
1571 if (nullptr == m_pDS)
1572 return false;
1574 if (idAlbum == -1)
1575 return false; // not in the database
1577 //Get album, song and album song info data using separate queries/datasets because we can have
1578 //multiple roles per artist for songs and that makes a single combined join impractical
1579 //Get album data
1580 std::string sql;
1581 sql = PrepareSQL("SELECT albumview.*,albumartistview.* "
1582 " FROM albumview "
1583 " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
1584 " WHERE albumview.idAlbum = %ld "
1585 " ORDER BY albumartistview.iOrder",
1586 idAlbum);
1588 CLog::Log(LOGDEBUG, "{}", sql);
1589 if (!m_pDS->query(sql))
1590 return false;
1591 if (m_pDS->num_rows() == 0)
1593 m_pDS->close();
1594 return false;
1597 int albumArtistOffset = album_enumCount;
1599 album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0,
1600 true); // true to grab and parse the imageURL
1601 while (!m_pDS->eof())
1603 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1605 // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
1606 // so there is only one row in the result set for each artist credit.
1607 album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
1609 m_pDS->next();
1611 m_pDS->close(); // cleanup recordset data
1613 //Get song data
1614 if (getSongs)
1616 sql = PrepareSQL("SELECT songview.*, songartistview.*"
1617 " FROM songview "
1618 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1619 " WHERE songview.idAlbum = %ld "
1620 " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder",
1621 idAlbum);
1623 CLog::Log(LOGDEBUG, "{}", sql);
1624 if (!m_pDS->query(sql))
1625 return false;
1626 if (m_pDS->num_rows() == 0) //Album with no songs
1628 m_pDS->close();
1629 return false;
1632 int songArtistOffset = song_enumCount;
1633 std::set<int> songs;
1634 while (!m_pDS->eof())
1636 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1638 int idSong = record->at(song_idSong).get_asInt(); //Same as songartist.idSong by join
1639 if (songs.find(idSong) == songs.end())
1641 album.songs.emplace_back(GetSongFromDataset(record));
1642 songs.insert(idSong);
1645 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1646 //By query order song is the last one appended to the album song vector.
1647 if (idSongArtistRole == ROLE_ARTIST)
1648 album.songs.back().artistCredits.emplace_back(
1649 GetArtistCreditFromDataset(record, songArtistOffset));
1650 else
1651 album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1653 m_pDS->next();
1655 m_pDS->close(); // cleanup recordset data
1658 return true;
1660 catch (...)
1662 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
1665 return false;
1668 bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum)
1670 std::string strSQL =
1671 PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum);
1672 return ExecuteQuery(strSQL);
1675 bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum)
1677 std::string strSQL =
1678 PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum);
1679 return GetSingleValue(strSQL).empty();
1682 int CMusicDatabase::AddGenre(std::string& strGenre)
1684 std::string strSQL;
1687 StringUtils::Trim(strGenre);
1689 if (strGenre.empty())
1690 strGenre = g_localizeStrings.Get(13205); // Unknown
1692 if (nullptr == m_pDB)
1693 return -1;
1694 if (nullptr == m_pDS)
1695 return -1;
1697 auto it = m_genreCache.find(strGenre);
1698 if (it != m_genreCache.end())
1699 return it->second;
1702 strSQL = PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'",
1703 strGenre.c_str());
1704 m_pDS->query(strSQL);
1705 if (m_pDS->num_rows() == 0)
1707 m_pDS->close();
1708 // doesn't exists, add it
1709 strSQL = PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )",
1710 strGenre.c_str());
1711 m_pDS->exec(strSQL);
1713 int idGenre = (int)m_pDS->lastinsertid();
1714 m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1715 return idGenre;
1717 else
1719 int idGenre = m_pDS->fv("idGenre").get_asInt();
1720 strGenre = m_pDS->fv("strGenre").get_asString();
1721 m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1722 m_pDS->close();
1723 return idGenre;
1726 catch (...)
1728 CLog::Log(LOGERROR, "musicdatabase:unable to addgenre ({})", strSQL);
1731 return -1;
1734 bool CMusicDatabase::UpdateArtist(const CArtist& artist)
1736 SetLibraryLastUpdated();
1738 const std::string itemSeparator =
1739 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
1741 UpdateArtist(artist.idArtist, //
1742 artist.strArtist, //
1743 artist.strSortName, //
1744 artist.strMusicBrainzArtistID, //
1745 artist.bScrapedMBID, //
1746 artist.strType, //
1747 artist.strGender, //
1748 artist.strDisambiguation, //
1749 artist.strBorn, //
1750 artist.strFormed, //
1751 StringUtils::Join(artist.genre, itemSeparator), //
1752 StringUtils::Join(artist.moods, itemSeparator), //
1753 StringUtils::Join(artist.styles, itemSeparator), //
1754 StringUtils::Join(artist.instruments, itemSeparator), //
1755 artist.strBiography, //
1756 artist.strDied, //
1757 artist.strDisbanded, //
1758 StringUtils::Join(artist.yearsActive, itemSeparator).c_str(), //
1759 artist.thumbURL.GetData());
1761 DeleteArtistDiscography(artist.idArtist);
1762 for (const auto& disc : artist.discography)
1764 AddArtistDiscography(artist.idArtist, disc);
1767 if (!DeleteArtistVideoLinks(artist.idArtist))
1768 CLog::Log(LOGERROR, "MusicDatabase: Error deleting ArtistVideoLinks");
1770 AddArtistVideoLinks(artist);
1772 // Set current artwork (held in art table)
1773 if (!artist.art.empty())
1774 SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
1776 return true;
1779 int CMusicDatabase::AddArtist(const std::string& strArtist,
1780 const std::string& strMusicBrainzArtistID,
1781 const std::string& strSortName,
1782 bool bScrapedMBID /* = false*/)
1784 std::string strSQL;
1785 int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID);
1786 if (idArtist < 0 || strSortName.empty())
1787 return idArtist;
1789 /* Artist sort name always taken as the first value provided that is different from name, so only
1790 update when current sort name is blank. If a new sortname the same as name is provided then
1791 clear any sortname currently held.
1796 if (nullptr == m_pDB)
1797 return -1;
1798 if (nullptr == m_pDS)
1799 return -1;
1801 strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist);
1802 m_pDS->query(strSQL);
1803 if (m_pDS->num_rows() != 1)
1805 m_pDS->close();
1806 return -1;
1808 std::string strArtistName, strArtistSort;
1809 strArtistName = m_pDS->fv("strArtist").get_asString();
1810 strArtistSort = m_pDS->fv("strSortName").get_asString();
1811 m_pDS->close();
1813 if (!strArtistSort.empty())
1815 if (strSortName.compare(strArtistName) == 0)
1816 m_pDS->exec(
1817 PrepareSQL("UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist));
1819 else if (strSortName.compare(strArtistName) != 0)
1820 m_pDS->exec(PrepareSQL("UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
1821 strSortName.c_str(), idArtist));
1823 return idArtist;
1826 catch (...)
1828 CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname ({})", strSQL);
1831 return -1;
1834 int CMusicDatabase::AddArtist(const std::string& strArtist,
1835 const std::string& strMusicBrainzArtistID,
1836 bool bScrapedMBID /* = false*/)
1838 std::string strSQL;
1841 if (nullptr == m_pDB)
1842 return -1;
1843 if (nullptr == m_pDS)
1844 return -1;
1846 // 1) MusicBrainz
1847 if (!strMusicBrainzArtistID.empty())
1849 // 1.a) Match on a MusicBrainz ID
1850 strSQL =
1851 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
1852 strMusicBrainzArtistID.c_str());
1853 m_pDS->query(strSQL);
1854 if (m_pDS->num_rows() > 0)
1856 int idArtist = m_pDS->fv("idArtist").get_asInt();
1857 bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0;
1858 m_pDS->close();
1859 if (update)
1861 strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' "
1862 "WHERE idArtist = %i",
1863 strArtist.c_str(), idArtist);
1864 m_pDS->exec(strSQL);
1865 m_pDS->close();
1867 return idArtist;
1869 m_pDS->close();
1872 // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
1873 // and update that if it exists.
1874 strSQL = PrepareSQL("SELECT idArtist FROM artist "
1875 "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
1876 strArtist.c_str());
1877 m_pDS->query(strSQL);
1878 if (m_pDS->num_rows() > 0)
1880 int idArtist = m_pDS->fv("idArtist").get_asInt();
1881 m_pDS->close();
1882 // 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
1883 strSQL =
1884 PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
1885 "bScrapedMBID = %i WHERE idArtist = %i",
1886 strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID, idArtist);
1887 m_pDS->exec(strSQL);
1888 return idArtist;
1891 // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
1892 // With MusicBrainz IDs this could return multiple artists and is non-determinstic
1893 // Always pick the first artist ID returned by the DB to return.
1895 else
1897 strSQL =
1898 PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
1900 m_pDS->query(strSQL);
1901 if (m_pDS->num_rows() > 0)
1903 int idArtist = m_pDS->fv("idArtist").get_asInt();
1904 m_pDS->close();
1905 return idArtist;
1907 m_pDS->close();
1910 // 3) No artist exists at all - add it, flagging when has scraped mbid
1911 if (strMusicBrainzArtistID.empty())
1912 strSQL = PrepareSQL("INSERT INTO artist "
1913 "(idArtist, strArtist, strMusicBrainzArtistID) "
1914 "VALUES( NULL, '%s', NULL)",
1915 strArtist.c_str());
1916 else
1917 strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
1918 "bScrapedMBID) "
1919 "VALUES( NULL, '%s', '%s', %i )",
1920 strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID);
1922 m_pDS->exec(strSQL);
1923 int idArtist = (int)m_pDS->lastinsertid();
1924 return idArtist;
1926 catch (...)
1928 CLog::Log(LOGERROR, "musicdatabase:unable to addartist ({})", strSQL);
1931 return -1;
1934 int CMusicDatabase::UpdateArtist(int idArtist,
1935 const std::string& strArtist,
1936 const std::string& strSortName,
1937 const std::string& strMusicBrainzArtistID,
1938 const bool bScrapedMBID,
1939 const std::string& strType,
1940 const std::string& strGender,
1941 const std::string& strDisambiguation,
1942 const std::string& strBorn,
1943 const std::string& strFormed,
1944 const std::string& strGenres,
1945 const std::string& strMoods,
1946 const std::string& strStyles,
1947 const std::string& strInstruments,
1948 const std::string& strBiography,
1949 const std::string& strDied,
1950 const std::string& strDisbanded,
1951 const std::string& strYearsActive,
1952 const std::string& strImage)
1954 if (idArtist < 0)
1955 return -1;
1957 // Check another artist with this mbid not already exist (an alias for example)
1958 bool useMBIDNull = strMusicBrainzArtistID.empty();
1959 bool isScrapedMBID = bScrapedMBID;
1960 std::string artistname;
1961 int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
1962 if (idArtistMbid > 0 && idArtistMbid != idArtist)
1964 CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
1965 __FUNCTION__, strMusicBrainzArtistID, artistname, idArtistMbid, strArtist, idArtist);
1966 useMBIDNull = true;
1967 isScrapedMBID = false;
1970 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1971 // Truncate value cleaning up xml when URLs exceeds this
1972 std::string strImageURLs = strImage;
1973 if (StringUtils::EqualsNoCase(
1974 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1975 "mysql"))
1976 TrimImageURLs(strImageURLs, 65535);
1978 std::string strSQL;
1979 strSQL = PrepareSQL("UPDATE artist SET "
1980 " strArtist = '%s', "
1981 " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
1982 " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
1983 " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
1984 " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
1985 " strYearsActive = '%s', strImage = '%s', "
1986 " lastScraped = '%s', bScrapedMBID = %i",
1987 strArtist.c_str(),
1988 /* strSortName.c_str(),*/
1989 /* strMusicBrainzArtistID.c_str(), */
1990 strType.c_str(), strGender.c_str(), strDisambiguation.c_str(), //
1991 strBorn.c_str(), strFormed.c_str(), strGenres.c_str(), //
1992 strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(), //
1993 strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(), //
1994 strYearsActive.c_str(), strImageURLs.c_str(), //
1995 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID);
1996 if (useMBIDNull)
1997 strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL");
1998 else
1999 strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str());
2000 if (strSortName.empty())
2001 strSQL += PrepareSQL(", strSortName = NULL");
2002 else
2003 strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str());
2005 strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist);
2007 bool status = ExecuteQuery(strSQL);
2008 if (status)
2009 AnnounceUpdate(MediaTypeArtist, idArtist);
2010 return idArtist;
2013 bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist,
2014 const std::string& strMusicBrainzArtistID)
2016 if (strMusicBrainzArtistID.empty() || idArtist < 0)
2017 return false;
2019 // Check another artist with this mbid not already exist (an alias for example)
2020 std::string artistname;
2021 int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
2022 if (idArtistMbid > 0 && idArtistMbid != idArtist)
2024 CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__,
2025 strMusicBrainzArtistID, artistname, idArtistMbid);
2026 return false;
2029 // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
2030 std::string strSQL;
2031 strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
2032 "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
2033 strMusicBrainzArtistID.c_str(), idArtist);
2035 bool status = ExecuteQuery(strSQL);
2036 if (status)
2038 AnnounceUpdate(MediaTypeArtist, idArtist);
2039 return true;
2041 return false;
2044 bool CMusicDatabase::GetArtist(int idArtist, CArtist& artist, bool fetchAll /* = false */)
2048 auto start = std::chrono::steady_clock::now();
2049 if (nullptr == m_pDB)
2050 return false;
2051 if (nullptr == m_pDS)
2052 return false;
2053 if (nullptr == m_pDS2)
2054 return false;
2056 if (idArtist == -1)
2057 return false; // not in the database
2059 std::string strSQL;
2060 if (fetchAll)
2061 strSQL = PrepareSQL("SELECT * FROM artistview "
2062 "LEFT JOIN discography ON artistview.idArtist = discography.idArtist "
2063 "WHERE artistview.idArtist = %i",
2064 idArtist);
2065 else
2066 strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist);
2068 if (!m_pDS->query(strSQL))
2069 return false;
2070 if (m_pDS->num_rows() == 0)
2072 m_pDS->close();
2073 return false;
2075 std::string debugSQL = strSQL + " - ";
2076 int discographyOffset = artist_enumCount;
2078 artist.discography.clear();
2079 artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs
2080 if (fetchAll)
2082 while (!m_pDS->eof())
2084 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
2085 CDiscoAlbum discoAlbum;
2086 discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString();
2087 discoAlbum.strYear = record->at(discographyOffset + 2).get_asString();
2088 discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString();
2089 artist.discography.emplace_back(discoAlbum);
2090 m_pDS->next();
2093 m_pDS->close(); // cleanup recordset data
2095 artist.videolinks.clear();
2096 if (fetchAll)
2098 strSQL = PrepareSQL("SELECT idSong, strTitle, strMusicBrainzTrackID, strVideoURL, url "
2099 "FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
2100 "LEFT JOIN art ON art.media_id = song.idSong AND art.type = 'videothumb' "
2101 "WHERE album_artist.idArtist = %i AND "
2102 "song.strVideoURL is not NULL GROUP by song.strVideoURL ORDER BY idSong",
2103 idArtist);
2104 debugSQL += strSQL;
2105 m_pDS->query(strSQL);
2106 while (!m_pDS->eof())
2108 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
2109 ArtistVideoLinks videoLink;
2110 videoLink.title = record->at(1).get_asString();
2111 videoLink.mbTrackID = record->at(2).get_asString();
2112 videoLink.videoURL = record->at(3).get_asString();
2113 videoLink.thumbURL = record->at(4).get_asString();
2115 artist.videolinks.emplace_back(std::move(videoLink));
2116 m_pDS->next();
2118 m_pDS->close();
2121 auto end = std::chrono::steady_clock::now();
2122 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
2124 CLog::LogF(LOGDEBUG, "{} - took {} ms", debugSQL, duration.count());
2125 return true;
2127 catch (...)
2129 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2132 return false;
2135 bool CMusicDatabase::GetArtistExists(int idArtist)
2139 if (nullptr == m_pDB)
2140 return false;
2141 if (nullptr == m_pDS)
2142 return false;
2144 std::string strSQL =
2145 PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist);
2147 if (!m_pDS->query(strSQL))
2148 return false;
2149 if (m_pDS->num_rows() == 0)
2151 m_pDS->close();
2152 return false;
2154 m_pDS->close(); // cleanup recordset data
2155 return true;
2157 catch (...)
2159 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2162 return false;
2165 int CMusicDatabase::GetLastArtist()
2167 std::string strSQL = "SELECT MAX(idArtist) FROM artist";
2168 std::string lastArtist = GetSingleValue(strSQL);
2169 if (lastArtist.empty())
2170 return -1;
2172 return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10));
2175 int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID,
2176 std::string& artistname)
2178 if (strMusicBrainzArtistID.empty())
2179 return -1;
2181 std::string strSQL;
2184 if (nullptr == m_pDB || nullptr == m_pDS2)
2185 return -1;
2186 // Match on MusicBrainz ID, definitively unique
2187 strSQL =
2188 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
2189 strMusicBrainzArtistID.c_str());
2190 if (!m_pDS2->query(strSQL))
2191 return -1;
2192 int idArtist = -1;
2193 if (m_pDS2->num_rows() > 0)
2195 idArtist = m_pDS2->fv("idArtist").get_asInt();
2196 artistname = m_pDS2->fv("strArtist").get_asString();
2198 m_pDS2->close();
2199 return idArtist;
2201 catch (...)
2203 CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__, strSQL);
2205 return -1;
2208 bool CMusicDatabase::HasArtistBeenScraped(int idArtist)
2210 std::string strSQL = PrepareSQL(
2211 "SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist);
2212 return GetSingleValue(strSQL).empty();
2215 bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist)
2217 std::string strSQL =
2218 PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist);
2219 return ExecuteQuery(strSQL);
2222 bool CMusicDatabase::AddArtistVideoLinks(const CArtist& artist)
2224 auto start = std::chrono::steady_clock::now();
2225 std::string dbSong;
2229 if (nullptr == m_pDB || nullptr == m_pDS || nullptr == m_pDS2)
2230 return false;
2232 for (const auto& videoURL : artist.videolinks)
2234 dbSong = videoURL.title;
2235 std::string strSQL = PrepareSQL(
2236 "SELECT idSong, strTitle FROM song WHERE strMusicBrainzTrackID = '%s' OR (EXISTS "
2237 "(SELECT 1 FROM album_artist WHERE album_artist.idAlbum = song.idAlbum AND "
2238 "album_artist.idArtist = '%i' AND song.strTitle LIKE '%%%s%%'))",
2239 videoURL.mbTrackID.c_str(), artist.idArtist, videoURL.title.c_str());
2241 if (!m_pDS->query(strSQL))
2242 return false;
2243 if (m_pDS->num_rows() == 0)
2244 continue;
2246 while (!m_pDS->eof())
2248 const int songId = m_pDS->fv(0).get_asInt();
2249 std::string strSQL2 = PrepareSQL("UPDATE song SET strVideoURL='%s' WHERE idSong = %i",
2250 videoURL.videoURL.c_str(), songId);
2251 CLog::Log(LOGDEBUG, "Adding videolink for song {} with id {}", dbSong, songId);
2252 m_pDS2->exec(strSQL2);
2254 if (!videoURL.thumbURL.empty())
2255 { // already have a videothumb for this song ?
2256 strSQL2 = PrepareSQL("SELECT art_id FROM art "
2257 "WHERE media_id=%i AND media_type='%s' AND type='videothumb'",
2258 songId, MediaTypeSong);
2259 m_pDS2->query(strSQL2);
2260 if (!m_pDS2->eof())
2261 { // update existing thumb
2262 const int artId = m_pDS2->fv(0).get_asInt();
2263 m_pDS2->close();
2264 strSQL2 = PrepareSQL("UPDATE art SET url='%s' where art_id=%d",
2265 videoURL.thumbURL.c_str(), artId);
2266 m_pDS2->exec(strSQL2);
2268 else
2269 { // insert new thumb
2270 m_pDS2->close();
2271 strSQL2 = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
2272 "VALUES (%d, '%s', '%s', '%s')",
2273 songId, MediaTypeSong, "videothumb", videoURL.thumbURL.c_str());
2274 m_pDS2->exec(strSQL2);
2276 m_pDS2->close();
2278 m_pDS->next();
2280 m_pDS->close();
2282 auto end = std::chrono::steady_clock::now();
2283 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
2284 CLog::LogF(LOGDEBUG, "Time to store videolinks {}ms ", duration.count());
2285 return true;
2287 catch (...)
2289 CLog::Log(LOGERROR, "MusicDatabase: Unable to add videolink for song ({})", dbSong);
2290 return false;
2294 bool CMusicDatabase::DeleteArtistVideoLinks(const int idArtist)
2296 std::string strSQL = PrepareSQL("UPDATE song SET strVideoURL = NULL WHERE idAlbum IN "
2297 "(SELECT idAlbum FROM album_artist WHERE idArtist = %i)",
2298 idArtist);
2299 if (!ExecuteQuery(strSQL))
2300 return false;
2301 strSQL = PrepareSQL(
2302 "DELETE FROM art WHERE art.type = 'videothumb' AND art.media_id IN (SELECT idSong FROM song "
2303 "JOIN album_artist ON song.idAlbum = album_artist.idAlbum WHERE album_artist.idArtist = %i)",
2304 idArtist);
2305 return ExecuteQuery(strSQL);
2308 int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum)
2310 std::string strSQL = PrepareSQL("INSERT INTO discography "
2311 "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
2312 "VALUES(%i, '%s', '%s', '%s')",
2313 idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(),
2314 discoAlbum.strReleaseGroupMBID.c_str());
2315 return ExecuteQuery(strSQL);
2318 bool CMusicDatabase::DeleteArtistDiscography(int idArtist)
2320 std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist);
2321 return ExecuteQuery(strSQL);
2324 bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items)
2328 if (nullptr == m_pDB)
2329 return false;
2330 if (nullptr == m_pDS)
2331 return false;
2333 /* Combine entries from discography and album tables
2334 Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using
2335 correlated subqueries to a temp table. An updatable join to temp table would work in MySQL
2336 but SQLite not support updatable joins.
2338 m_pDS->exec("CREATE TABLE tempDisco "
2339 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2340 m_pDS->exec("CREATE TABLE tempAlbum "
2341 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2343 std::string strSQL;
2344 strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
2345 "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
2346 "strReleaseGroupMBID, NULL "
2347 "FROM discography WHERE idArtist = %i",
2348 idArtist);
2349 m_pDS->exec(strSQL);
2351 strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
2352 "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
2353 "strReleaseGroupMBID, album.idAlbum "
2354 "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
2355 "WHERE idArtist = %i",
2356 idArtist);
2357 m_pDS->exec(strSQL);
2359 // Match albums on release group mbid, if multi-releases then first used
2360 // Only use albums credited to this artist
2361 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
2362 "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
2363 m_pDS->exec(strSQL);
2364 //Delete matched albums
2365 strSQL = "DELETE FROM tempAlbum "
2366 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2367 m_pDS->exec(strSQL);
2369 // Match remaining to albums by artist on title and year
2370 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2371 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
2372 "AND tempAlbum.strYear = tempDisco.strYear) "
2373 "WHERE tempDisco.idAlbum is NULL";
2374 m_pDS->exec(strSQL);
2375 //Delete matched albums
2376 strSQL = "DELETE FROM tempAlbum "
2377 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2378 m_pDS->exec(strSQL);
2380 // Match remaining to albums by artist on title only
2381 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2382 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
2383 "WHERE tempDisco.idAlbum is NULL";
2384 m_pDS->exec(strSQL);
2385 // Use year from album table, when matched by title only as it could be different
2386 strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
2387 "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
2388 "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
2389 m_pDS->exec(strSQL);
2390 //Delete matched albums
2391 strSQL = "DELETE FROM tempAlbum "
2392 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2393 m_pDS->exec(strSQL);
2395 // Combine distinctly with any remaining unmatched albums by artist
2396 strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
2397 "UNION "
2398 "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
2399 "ORDER BY strYear, strAlbum, idAlbum";
2401 if (!m_pDS->query(strSQL))
2402 return false;
2403 int iRowsFound = m_pDS->num_rows();
2404 if (iRowsFound == 0)
2406 m_pDS->close();
2407 return true;
2410 while (!m_pDS->eof())
2412 int idAlbum = m_pDS->fv("idAlbum").get_asInt();
2413 if (idAlbum == 0)
2414 idAlbum = -1;
2415 std::string strAlbum = m_pDS->fv("strAlbum").get_asString();
2416 if (!strAlbum.empty())
2418 CFileItemPtr pItem(new CFileItem(strAlbum));
2419 pItem->SetLabel2(m_pDS->fv("strYear").get_asString());
2420 pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum);
2421 items.Add(pItem);
2423 m_pDS->next();
2426 // cleanup
2427 m_pDS->close();
2428 m_pDS->exec("DROP TABLE tempDisco");
2429 m_pDS->exec("DROP TABLE tempAlbum");
2431 return true;
2433 catch (...)
2435 m_pDS->exec("DROP TABLE tempDisco");
2436 m_pDS->exec("DROP TABLE tempAlbum");
2437 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
2439 return false;
2442 int CMusicDatabase::AddRole(const std::string& strRole)
2444 int idRole = -1;
2445 std::string strSQL;
2449 if (nullptr == m_pDB)
2450 return -1;
2451 if (nullptr == m_pDS)
2452 return -1;
2453 strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str());
2454 m_pDS->query(strSQL);
2455 if (m_pDS->num_rows() > 0)
2456 idRole = m_pDS->fv("idRole").get_asInt();
2457 m_pDS->close();
2459 if (idRole < 0)
2461 strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str());
2462 m_pDS->exec(strSQL);
2463 idRole = static_cast<int>(m_pDS->lastinsertid());
2464 m_pDS->close();
2467 catch (...)
2469 CLog::Log(LOGERROR, "musicdatabase:unable to AddRole ({})", strSQL);
2471 return idRole;
2474 bool CMusicDatabase::AddSongArtist(
2475 int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder)
2477 int idRole = AddRole(strRole);
2478 return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder);
2481 bool CMusicDatabase::AddSongArtist(
2482 int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder)
2484 std::string strSQL;
2485 strSQL = PrepareSQL("REPLACE INTO song_artist (idArtist, idSong, idRole, strArtist, iOrder) "
2486 "VALUES(%i, %i, %i,'%s', %i)",
2487 idArtist, idSong, idRole, strArtist.c_str(), iOrder);
2488 return ExecuteQuery(strSQL);
2491 int CMusicDatabase::AddSongContributor(int idSong,
2492 const std::string& strRole,
2493 const std::string& strArtist,
2494 const std::string& strSort)
2496 if (strArtist.empty())
2497 return -1;
2499 std::string strSQL;
2502 if (nullptr == m_pDB)
2503 return -1;
2504 if (nullptr == m_pDS)
2505 return -1;
2507 int idArtist = -1;
2508 // Add artist. As we only have name (no MBID) first try to identify artist from song
2509 // as they may have already been added with a different role (including MBID).
2510 strSQL =
2511 PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ",
2512 idSong, strArtist.c_str());
2513 m_pDS->query(strSQL);
2514 if (m_pDS->num_rows() > 0)
2515 idArtist = m_pDS->fv("idArtist").get_asInt();
2516 m_pDS->close();
2518 if (idArtist < 0)
2519 idArtist = AddArtist(strArtist, "", strSort);
2521 // Add to song_artist table
2522 AddSongArtist(idArtist, idSong, strRole, strArtist, 0);
2524 return idArtist;
2526 catch (...)
2528 CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor ({})", strSQL);
2531 return -1;
2534 void CMusicDatabase::AddSongContributors(int idSong,
2535 const VECMUSICROLES& contributors,
2536 const std::string& strSort)
2538 std::vector<std::string> composerSort;
2539 size_t countComposer = 0;
2540 if (!strSort.empty())
2542 composerSort = StringUtils::Split(
2543 strSort,
2544 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2547 for (const auto& credit : contributors)
2549 std::string strSortName;
2550 //Identify composer sort name if we have it
2551 if (countComposer < composerSort.size())
2553 if (credit.GetRoleDesc().compare("Composer") == 0)
2555 strSortName = composerSort[countComposer];
2556 countComposer++;
2559 AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName);
2563 int CMusicDatabase::GetRoleByName(const std::string& strRole)
2567 if (nullptr == m_pDB)
2568 return false;
2569 if (nullptr == m_pDS)
2570 return false;
2572 std::string strSQL;
2573 strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str());
2574 // run query
2575 if (!m_pDS->query(strSQL))
2576 return false;
2577 int iRowsFound = m_pDS->num_rows();
2578 if (iRowsFound != 1)
2580 m_pDS->close();
2581 return -1;
2583 return m_pDS->fv("idRole").get_asInt();
2585 catch (...)
2587 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
2589 return -1;
2592 bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item)
2596 std::string strSQL =
2597 PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole "
2598 "FROM song_artist JOIN role ON song_artist.idRole = role.idRole "
2599 "WHERE idArtist = %i ORDER BY song_artist.idRole ASC",
2600 idArtist);
2601 if (!m_pDS->query(strSQL))
2602 return false;
2603 if (m_pDS->num_rows() == 0)
2605 m_pDS->close();
2606 return true;
2609 CVariant artistRoles(CVariant::VariantTypeArray);
2611 while (!m_pDS->eof())
2613 CVariant roleObj;
2614 roleObj["role"] = m_pDS->fv("strRole").get_asString();
2615 roleObj["roleid"] = m_pDS->fv("idrole").get_asInt();
2616 artistRoles.push_back(roleObj);
2617 m_pDS->next();
2619 m_pDS->close();
2621 item->SetProperty("roles", artistRoles);
2622 return true;
2624 catch (...)
2626 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2628 return false;
2631 bool CMusicDatabase::DeleteSongArtistsBySong(int idSong)
2633 return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong));
2636 bool CMusicDatabase::AddAlbumArtist(int idArtist,
2637 int idAlbum,
2638 const std::string& strArtist,
2639 int iOrder)
2641 std::string strSQL;
2642 strSQL = PrepareSQL("REPLACE INTO album_artist (idArtist, idAlbum, strArtist, iOrder) "
2643 "VALUES(%i,%i,'%s',%i)",
2644 idArtist, idAlbum, strArtist.c_str(), iOrder);
2645 return ExecuteQuery(strSQL);
2648 bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum)
2650 return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum));
2653 bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres)
2655 if (idSong == -1)
2656 return true;
2658 std::string strSQL;
2661 // Clear current entries for song
2662 strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong);
2663 if (!ExecuteQuery(strSQL))
2664 return false;
2665 unsigned int index = 0;
2666 std::vector<std::string> modgenres = genres;
2667 for (auto& strGenre : modgenres)
2669 int idGenre = AddGenre(strGenre); // Genre string trimmed and matched case-insensitively
2670 strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
2671 idGenre, idSong, index++);
2672 if (!ExecuteQuery(strSQL))
2673 return false;
2675 // Update concatenated genre string from the standardised genre values
2676 std::string strGenres = StringUtils::Join(
2677 modgenres,
2678 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2679 strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", //
2680 strGenres.c_str(), idSong);
2681 if (!ExecuteQuery(strSQL))
2682 return false;
2684 return true;
2686 catch (...)
2688 CLog::Log(LOGERROR, "{}({}) {} failed", __FUNCTION__, idSong, strSQL);
2690 return false;
2693 bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int>& albums)
2697 std::string strSQL;
2698 strSQL = PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist);
2699 if (!m_pDS->query(strSQL))
2700 return false;
2701 if (m_pDS->num_rows() == 0)
2703 m_pDS->close();
2704 return false;
2707 while (!m_pDS->eof())
2709 albums.push_back(m_pDS->fv("idAlbum").get_asInt());
2710 m_pDS->next();
2712 m_pDS->close();
2713 return true;
2715 catch (...)
2717 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2719 return false;
2722 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item)
2726 std::string strSQL;
2728 strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum);
2730 if (!m_pDS->query(strSQL))
2731 return false;
2732 if (m_pDS->num_rows() == 0)
2734 m_pDS->close();
2735 return false;
2738 // Get album artist credits
2739 VECARTISTCREDITS artistCredits;
2740 while (!m_pDS->eof())
2742 artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0));
2743 m_pDS->next();
2745 m_pDS->close();
2747 // Populate item with song albumartist credits
2748 std::vector<std::string> musicBrainzID;
2749 std::vector<std::string> albumartists;
2750 CVariant artistidObj(CVariant::VariantTypeArray);
2751 for (const auto& artistCredit : artistCredits)
2753 artistidObj.push_back(artistCredit.GetArtistId());
2754 albumartists.emplace_back(artistCredit.GetArtist());
2755 if (!artistCredit.GetMusicBrainzArtistID().empty())
2756 musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID());
2758 item->GetMusicInfoTag()->SetAlbumArtist(albumartists);
2759 item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID);
2760 // Add song albumartistIds as separate property as not part of CMusicInfoTag
2761 item->SetProperty("albumartistid", artistidObj);
2763 return true;
2765 catch (...)
2767 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
2769 return false;
2772 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs)
2776 std::string strSQL;
2777 // Get distinct song and album artist IDs for this album, no other roles
2778 // Allow for artists that are only album artists and not song artists
2779 strSQL = PrepareSQL(
2780 "SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
2781 "UNION \n"
2782 "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
2783 "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
2784 idAlbum, idAlbum);
2786 if (!m_pDS->query(strSQL))
2787 return false;
2788 if (m_pDS->num_rows() == 0)
2790 m_pDS->close();
2791 return false;
2793 while (!m_pDS->eof())
2795 // Get ID as string so can easily join to make "IN" clause
2796 artistIDs.push_back(m_pDS->fv("idArtist").get_asString());
2797 m_pDS->next();
2799 m_pDS->close();
2800 return true;
2802 catch (...)
2804 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
2806 return false;
2809 bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int>& songs)
2813 std::string strSQL;
2814 //Restrict to Artists only, no other roles
2815 strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", //
2816 idArtist);
2817 if (!m_pDS->query(strSQL))
2818 return false;
2819 if (m_pDS->num_rows() == 0)
2821 m_pDS->close();
2822 return false;
2825 while (!m_pDS->eof())
2827 songs.push_back(m_pDS->fv("idSong").get_asInt());
2828 m_pDS->next();
2830 m_pDS->close();
2831 return true;
2833 catch (...)
2835 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2837 return false;
2840 bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int>& artists)
2844 std::string strSQL;
2845 //Restrict to Artists only, no other roles
2846 strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", //
2847 idSong);
2848 if (!m_pDS->query(strSQL))
2849 return false;
2850 if (m_pDS->num_rows() == 0)
2852 m_pDS->close();
2853 return false;
2856 while (!m_pDS->eof())
2858 artists.push_back(m_pDS->fv("idArtist").get_asInt());
2859 m_pDS->next();
2861 m_pDS->close();
2862 return true;
2864 catch (...)
2866 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
2868 return false;
2871 bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item)
2875 std::string strSQL;
2876 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
2877 "FROM album_artist "
2878 "JOIN song ON album_artist.idAlbum = song.idAlbum "
2879 "JOIN song_genre ON song.idSong = song_genre.idSong "
2880 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2881 "WHERE album_artist.idArtist = %i "
2882 "ORDER BY song_genre.idGenre",
2883 idArtist);
2884 if (!m_pDS->query(strSQL))
2885 return false;
2886 if (m_pDS->num_rows() == 0)
2888 // Artist does have any song genres via albums may not be an album artist.
2889 // Check via songs artist to fetch song genres from compilations or where they are guest artist
2890 m_pDS->close();
2891 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
2892 "FROM song_artist "
2893 "JOIN song_genre ON song_artist.idSong = song_genre.idSong "
2894 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2895 "WHERE song_artist.idArtist = %i "
2896 "ORDER BY song_genre.idGenre",
2897 idArtist);
2898 if (!m_pDS->query(strSQL))
2899 return false;
2900 if (m_pDS->num_rows() == 0)
2902 //No song genres, but query successful
2903 m_pDS->close();
2904 return true;
2908 CVariant artistSongGenres(CVariant::VariantTypeArray);
2910 while (!m_pDS->eof())
2912 CVariant genreObj;
2913 genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2914 genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2915 artistSongGenres.push_back(genreObj);
2916 m_pDS->next();
2918 m_pDS->close();
2920 item->SetProperty("songgenres", artistSongGenres);
2921 return true;
2923 catch (...)
2925 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2927 return false;
2930 bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item)
2934 std::string strSQL;
2935 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2936 "song JOIN song_genre ON song.idSong = song_genre.idSong "
2937 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2938 "WHERE song.idAlbum = %i "
2939 "ORDER BY song_genre.idSong, song_genre.iOrder",
2940 idAlbum);
2941 if (!m_pDS->query(strSQL))
2942 return false;
2943 if (m_pDS->num_rows() == 0)
2945 //No song genres, but query successful
2946 m_pDS->close();
2947 return true;
2950 CVariant albumSongGenres(CVariant::VariantTypeArray);
2952 while (!m_pDS->eof())
2954 CVariant genreObj;
2955 genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2956 genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2957 albumSongGenres.push_back(genreObj);
2958 m_pDS->next();
2960 m_pDS->close();
2962 item->SetProperty("songgenres", albumSongGenres);
2963 return true;
2965 catch (...)
2967 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
2969 return false;
2972 bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres)
2976 std::string strSQL = PrepareSQL("SELECT idGenre FROM song_genre "
2977 "WHERE idSong = %i ORDER BY iOrder ASC",
2978 idSong);
2979 if (!m_pDS->query(strSQL))
2980 return false;
2981 if (m_pDS->num_rows() == 0)
2983 m_pDS->close();
2984 return true;
2987 while (!m_pDS->eof())
2989 genres.push_back(m_pDS->fv("idGenre").get_asInt());
2990 m_pDS->next();
2992 m_pDS->close();
2994 return true;
2996 catch (...)
2998 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
3000 return false;
3003 bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item)
3007 int countalbum =
3008 GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist));
3009 CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean);
3010 IsAlbumArtistObj = (countalbum > 0);
3011 item->SetProperty("isalbumartist", IsAlbumArtistObj);
3012 return true;
3014 catch (...)
3016 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
3018 return false;
3022 int CMusicDatabase::AddPath(const std::string& strPath1)
3024 std::string strSQL;
3027 std::string strPath(strPath1);
3028 if (!URIUtils::HasSlashAtEnd(strPath))
3029 URIUtils::AddSlashAtEnd(strPath);
3031 if (nullptr == m_pDB)
3032 return -1;
3033 if (nullptr == m_pDS)
3034 return -1;
3036 auto it = m_pathCache.find(strPath);
3037 if (it != m_pathCache.end())
3038 return it->second;
3040 strSQL = PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath.c_str());
3041 m_pDS->query(strSQL);
3042 if (m_pDS->num_rows() == 0)
3044 m_pDS->close();
3045 // doesn't exists, add it
3046 strSQL = PrepareSQL("INSERT INTO path (idPath, strPath) "
3047 "VALUES(NULL, '%s')",
3048 strPath.c_str());
3049 m_pDS->exec(strSQL);
3051 int idPath = (int)m_pDS->lastinsertid();
3052 m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
3053 return idPath;
3055 else
3057 int idPath = m_pDS->fv("idPath").get_asInt();
3058 m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
3059 m_pDS->close();
3060 return idPath;
3063 catch (...)
3065 CLog::Log(LOGERROR, "musicdatabase:unable to addpath ({})", strSQL);
3068 return -1;
3071 CSong CMusicDatabase::GetSongFromDataset()
3073 return GetSongFromDataset(m_pDS->get_sql_record());
3076 CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record,
3077 int offset /* = 0 */)
3079 CSong song;
3080 song.idSong = record->at(offset + song_idSong).get_asInt();
3081 // Note this function does not populate artist credits, this must be done separately.
3082 // However artist names are held as a descriptive string
3083 song.strArtistDesc = record->at(offset + song_strArtists).get_asString();
3084 song.strArtistSort = record->at(offset + song_strArtistSort).get_asString();
3085 // Get the full genre string
3086 song.genre = StringUtils::Split(
3087 record->at(offset + song_strGenres).get_asString(),
3088 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
3089 // and the rest...
3090 song.strAlbum = record->at(offset + song_strAlbum).get_asString();
3091 song.idAlbum = record->at(offset + song_idAlbum).get_asInt();
3092 song.iTrack = record->at(offset + song_iTrack).get_asInt();
3093 song.iDuration = record->at(offset + song_iDuration).get_asInt();
3094 song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString();
3095 song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString();
3096 song.strTitle = record->at(offset + song_strTitle).get_asString();
3097 song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt();
3098 song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString());
3099 song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString());
3100 song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString());
3101 song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString());
3102 song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt();
3103 song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt();
3104 song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString();
3105 song.rating = record->at(offset + song_rating).get_asFloat();
3106 song.userrating = record->at(offset + song_userrating).get_asInt();
3107 song.votes = record->at(offset + song_votes).get_asInt();
3108 song.strComment = record->at(offset + song_comment).get_asString();
3109 song.strMood = record->at(offset + song_mood).get_asString();
3110 song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1;
3111 song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString();
3112 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
3113 song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString());
3114 // Get filename with full path
3115 song.strFileName =
3116 URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(),
3117 record->at(offset + song_strFileName).get_asString());
3118 song.iBPM = record->at(offset + song_iBPM).get_asInt();
3119 song.iBitRate = record->at(offset + song_iBitRate).get_asInt();
3120 song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt();
3121 song.iChannels = record->at(offset + song_iChannels).get_asInt();
3122 song.songVideoURL = record->at(offset + song_songVideoURL).get_asString();
3123 return song;
3126 void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl)
3128 GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl);
3131 void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record,
3132 CFileItem* item,
3133 const CMusicDbUrl& baseUrl)
3135 // get the artist string from songview (not the song_artist and artist tables)
3136 item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString());
3137 // get the artist sort name string from songview (not the song_artist and artist tables)
3138 item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString());
3139 // and the full genre string
3140 item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString());
3141 // and the rest...
3142 item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString());
3143 item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt());
3144 item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt());
3145 item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt());
3146 item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong);
3147 item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString());
3148 item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString());
3149 item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString());
3150 item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString());
3151 item->SetLabel(record->at(song_strTitle).get_asString());
3152 item->SetStartOffset(record->at(song_iStartOffset).get_asInt64());
3153 item->SetProperty("item_start", item->GetStartOffset());
3154 item->SetEndOffset(record->at(song_iEndOffset).get_asInt64());
3155 item->GetMusicInfoTag()->SetMusicBrainzTrackID(
3156 record->at(song_strMusicBrainzTrackID).get_asString());
3157 item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat());
3158 item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt());
3159 item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt());
3160 item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString());
3161 item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString());
3162 item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt());
3163 item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString());
3164 item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString());
3165 item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString());
3166 item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString());
3167 std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(),
3168 record->at(song_strFileName).get_asString());
3169 item->GetMusicInfoTag()->SetURL(strRealPath);
3170 item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1);
3171 item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1);
3172 // get the album artist string from songview (not the album_artist and artist tables)
3173 item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString());
3174 item->GetMusicInfoTag()->SetAlbumReleaseType(
3175 CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString()));
3176 item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt());
3177 item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt());
3178 item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt());
3179 item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt());
3180 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
3181 ReplayGain replaygain;
3182 replaygain.Set(record->at(song_strReplayGain).get_asString());
3183 item->GetMusicInfoTag()->SetReplayGain(replaygain);
3184 item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt());
3185 item->GetMusicInfoTag()->SetSongVideoURL(record->at(song_songVideoURL).get_asString());
3187 item->GetMusicInfoTag()->SetLoaded(true);
3188 // Get filename with full path
3189 if (!baseUrl.IsValid())
3190 item->SetPath(strRealPath);
3191 else
3193 CMusicDbUrl itemUrl = baseUrl;
3194 std::string strFileName = record->at(song_strFileName).get_asString();
3195 std::string strExt = URIUtils::GetExtension(strFileName);
3196 std::string path = StringUtils::Format("{}{}", record->at(song_idSong).get_asInt(), strExt);
3197 itemUrl.AppendPath(path);
3198 item->SetPath(itemUrl.ToString());
3199 item->SetDynPath(strRealPath);
3203 void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item)
3205 // Populate fileitem with artists from vector of artist credits
3206 std::vector<std::string> musicBrainzID;
3207 std::vector<std::string> songartists;
3208 CVariant artistidObj(CVariant::VariantTypeArray);
3210 // When "missing tag" artist, it is the only artist when present.
3211 if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID)
3213 artistidObj.push_back((int)BLANKARTIST_ID);
3214 songartists.push_back(StringUtils::Empty);
3216 else
3218 for (const auto& artistCredit : artistCredits)
3220 artistidObj.push_back(artistCredit.GetArtistId());
3221 songartists.push_back(artistCredit.GetArtist());
3222 if (!artistCredit.GetMusicBrainzArtistID().empty())
3223 musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
3226 // Also sets ArtistDesc if empty from song.strArtist field
3227 item->GetMusicInfoTag()->SetArtist(songartists);
3228 item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID);
3229 // Add album artistIds as separate property as not part of CMusicInfoTag
3230 item->SetProperty("artistid", artistidObj);
3233 CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS,
3234 int offset /* = 0 */,
3235 bool imageURL /* = false*/)
3237 return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL);
3240 CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record,
3241 int offset /* = 0 */,
3242 bool imageURL /* = false*/)
3244 const std::string itemSeparator =
3245 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
3247 CAlbum album;
3248 album.idAlbum = record->at(offset + album_idAlbum).get_asInt();
3249 album.strAlbum = record->at(offset + album_strAlbum).get_asString();
3250 if (album.strAlbum.empty())
3251 album.strAlbum = g_localizeStrings.Get(1050);
3252 album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString();
3253 album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString();
3254 album.strArtistDesc = record->at(offset + album_strArtists).get_asString();
3255 album.strArtistSort = record->at(offset + album_strArtistSort).get_asString();
3256 album.genre =
3257 StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator);
3258 album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString();
3259 album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString();
3260 album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1;
3261 if (imageURL)
3262 album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString());
3263 album.fRating = record->at(offset + album_fRating).get_asFloat();
3264 album.iUserrating = record->at(offset + album_iUserrating).get_asInt();
3265 album.iVotes = record->at(offset + album_iVotes).get_asInt();
3266 album.strReview = record->at(offset + album_strReview).get_asString();
3267 album.styles =
3268 StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator);
3269 album.moods =
3270 StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator);
3271 album.themes =
3272 StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator);
3273 album.strLabel = record->at(offset + album_strLabel).get_asString();
3274 album.strType = record->at(offset + album_strType).get_asString();
3275 album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString();
3276 album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1;
3277 album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1;
3278 album.strLastScraped = record->at(offset + album_lastScraped).get_asString();
3279 album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt();
3280 album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString());
3281 album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt();
3282 album.SetDateAdded(record->at(offset + album_dateAdded).get_asString());
3283 album.SetDateNew(record->at(offset + album_dateNew).get_asString());
3284 album.SetDateUpdated(record->at(offset + album_dateModified).get_asString());
3285 album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString());
3286 album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt();
3287 return album;
3290 CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record,
3291 int offset /* = 0 */)
3293 CArtistCredit artistCredit;
3294 artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt();
3295 if (artistCredit.idArtist == BLANKARTIST_ID)
3296 artistCredit.m_strArtist = StringUtils::Empty;
3297 else
3299 artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString();
3300 artistCredit.m_strMusicBrainzArtistID =
3301 record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString();
3303 return artistCredit;
3306 CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record,
3307 int offset /* = 0 */)
3309 CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(),
3310 record->at(offset + artistCredit_strRole).get_asString(),
3311 record->at(offset + artistCredit_strArtist).get_asString(),
3312 record->at(offset + artistCredit_idArtist).get_asInt());
3313 return ArtistRole;
3316 CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS,
3317 int offset /* = 0 */,
3318 bool needThumb /* = true */)
3320 return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb);
3323 CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record,
3324 int offset /* = 0 */,
3325 bool needThumb /* = true */)
3327 const std::string itemSeparator =
3328 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
3330 CArtist artist;
3331 artist.idArtist = record->at(offset + artist_idArtist).get_asInt();
3332 if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist)
3333 artist.strArtist = g_localizeStrings.Get(38042); //Missing artist tag in current language
3334 else
3335 artist.strArtist = record->at(offset + artist_strArtist).get_asString();
3336 artist.strSortName = record->at(offset + artist_strSortName).get_asString();
3337 artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString();
3338 artist.strType = record->at(offset + artist_strType).get_asString();
3339 artist.strGender = record->at(offset + artist_strGender).get_asString();
3340 artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString();
3341 artist.genre =
3342 StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator);
3343 artist.strBiography = record->at(offset + artist_strBiography).get_asString();
3344 artist.styles =
3345 StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator);
3346 artist.moods =
3347 StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator);
3348 artist.strBorn = record->at(offset + artist_strBorn).get_asString();
3349 artist.strFormed = record->at(offset + artist_strFormed).get_asString();
3350 artist.strDied = record->at(offset + artist_strDied).get_asString();
3351 artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString();
3352 artist.yearsActive =
3353 StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator);
3354 artist.instruments =
3355 StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator);
3356 artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1;
3357 artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString();
3358 artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString());
3359 artist.SetDateNew(record->at(offset + artist_dateNew).get_asString());
3360 artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString());
3362 if (needThumb)
3364 artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString());
3367 return artist;
3370 bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath,
3371 CSong& song,
3372 int64_t startOffset)
3374 song.Clear();
3375 CURL url(strFileNameAndPath);
3377 if (url.IsProtocol("musicdb"))
3379 std::string strFile = URIUtils::GetFileName(strFileNameAndPath);
3380 URIUtils::RemoveExtension(strFile);
3381 return GetSong(atoi(strFile.c_str()), song);
3384 if (nullptr == m_pDB)
3385 return false;
3386 if (nullptr == m_pDS)
3387 return false;
3389 std::string strPath, strFileName;
3390 SplitPath(strFileNameAndPath, strPath, strFileName);
3391 URIUtils::AddSlashAtEnd(strPath);
3393 std::string strSQL = PrepareSQL("SELECT idSong FROM songview "
3394 "WHERE strFileName='%s' AND strPath='%s'",
3395 strFileName.c_str(), strPath.c_str());
3396 if (startOffset)
3397 strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset);
3399 int idSong = GetSingleValueInt(strSQL);
3400 if (idSong > 0)
3401 return GetSong(idSong, song);
3403 return false;
3406 int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath)
3410 if (nullptr == m_pDB)
3411 return false;
3412 if (nullptr == m_pDS)
3413 return false;
3415 std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song "
3416 "JOIN path ON song.idPath = path.idPath "
3417 "WHERE path.strPath='%s'",
3418 strPath.c_str());
3419 // run query
3420 if (!m_pDS->query(strSQL))
3421 return false;
3422 int iRowsFound = m_pDS->num_rows();
3424 int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned
3425 if (iRowsFound == 1)
3426 idAlbum = m_pDS->fv(0).get_asInt();
3428 m_pDS->close();
3430 return idAlbum;
3432 catch (...)
3434 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
3437 return -1;
3440 int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist,
3441 const std::string& strAlbum,
3442 const std::string& strTitle)
3446 std::string strSQL =
3447 PrepareSQL("SELECT idSong FROM songview "
3448 "WHERE strArtists LIKE '%s' AND strAlbum LIKE '%s' AND strTitle LIKE '%s'",
3449 strArtist.c_str(), strAlbum.c_str(), strTitle.c_str());
3451 if (!m_pDS->query(strSQL))
3452 return false;
3453 int iRowsFound = m_pDS->num_rows();
3454 if (iRowsFound == 0)
3456 m_pDS->close();
3457 return -1;
3459 int lResult = m_pDS->fv(0).get_asInt();
3460 m_pDS->close(); // cleanup recordset data
3461 return lResult;
3463 catch (...)
3465 CLog::Log(LOGERROR, "{} ({},{},{}) failed", __FUNCTION__, strArtist, strAlbum, strTitle);
3468 return -1;
3471 bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList& artists)
3475 if (nullptr == m_pDB)
3476 return false;
3477 if (nullptr == m_pDS)
3478 return false;
3480 std::string strVariousArtists = g_localizeStrings.Get(340).c_str();
3481 std::string strSQL;
3482 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3483 strSQL = PrepareSQL("SELECT * FROM artist "
3484 "WHERE (strArtist LIKE '%s%%' OR strArtist LIKE '%% %s%%') "
3485 "AND strArtist <> '%s' ",
3486 search.c_str(), search.c_str(), strVariousArtists.c_str());
3487 else
3488 strSQL = PrepareSQL("SELECT * FROM artist "
3489 "WHERE strArtist LIKE '%s%%' AND strArtist <> '%s' ",
3490 search.c_str(), strVariousArtists.c_str());
3492 if (!m_pDS->query(strSQL))
3493 return false;
3494 if (m_pDS->num_rows() == 0)
3496 m_pDS->close();
3497 return false;
3500 const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist
3501 while (!m_pDS->eof())
3503 std::string path = StringUtils::Format("musicdb://artists/{}/", m_pDS->fv(0).get_asInt());
3504 CFileItemPtr pItem(new CFileItem(path, true));
3505 std::string label = StringUtils::Format("[{}] {}", artistLabel, m_pDS->fv(1).get_asString());
3506 pItem->SetLabel(label);
3507 // sort label is stored in the title tag
3508 label = StringUtils::Format("A {}", m_pDS->fv(1).get_asString());
3509 pItem->GetMusicInfoTag()->SetTitle(label);
3510 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist);
3511 artists.Add(pItem);
3512 m_pDS->next();
3515 m_pDS->close(); // cleanup recordset data
3516 return true;
3518 catch (...)
3520 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3523 return false;
3526 bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items)
3530 if (nullptr == m_pDB)
3531 return false;
3532 if (nullptr == m_pDS)
3533 return false;
3535 CMusicDbUrl baseUrl;
3536 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3537 return false;
3539 std::string strSQL = "SELECT * FROM songview "
3540 "WHERE iTimesPlayed>0 "
3541 "ORDER BY iTimesPlayed DESC "
3542 "LIMIT 100";
3544 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3545 if (!m_pDS->query(strSQL))
3546 return false;
3547 int iRowsFound = m_pDS->num_rows();
3548 if (iRowsFound == 0)
3550 m_pDS->close();
3551 return true;
3553 items.Reserve(iRowsFound);
3554 while (!m_pDS->eof())
3556 CFileItemPtr item(new CFileItem);
3557 GetFileItemFromDataset(item.get(), baseUrl);
3558 items.Add(item);
3559 m_pDS->next();
3562 m_pDS->close(); // cleanup recordset data
3563 return true;
3565 catch (...)
3567 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3570 return false;
3573 bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums)
3577 albums.erase(albums.begin(), albums.end());
3578 if (nullptr == m_pDB)
3579 return false;
3580 if (nullptr == m_pDS)
3581 return false;
3583 // Get data from album and album_artist tables to fully populate albums
3584 std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview "
3585 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3586 "WHERE albumartistview.idAlbum IN "
3587 "(SELECT albumview.idAlbum FROM albumview "
3588 "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
3589 "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
3590 "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
3592 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3593 if (!m_pDS->query(strSQL))
3594 return false;
3595 int iRowsFound = m_pDS->num_rows();
3596 if (iRowsFound == 0)
3598 m_pDS->close();
3599 return true;
3602 int albumArtistOffset = album_enumCount;
3603 int albumId = -1;
3604 while (!m_pDS->eof())
3606 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3608 if (albumId != record->at(album_idAlbum).get_asInt())
3609 { // New album
3610 albumId = record->at(album_idAlbum).get_asInt();
3611 albums.push_back(GetAlbumFromDataset(record));
3613 // Get album artists
3614 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3616 m_pDS->next();
3619 m_pDS->close(); // cleanup recordset data
3620 return true;
3622 catch (...)
3624 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3627 return false;
3630 bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items)
3634 if (nullptr == m_pDB)
3635 return false;
3636 if (nullptr == m_pDS)
3637 return false;
3639 CMusicDbUrl baseUrl;
3640 if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir))
3641 return false;
3643 std::string strSQL = StringUtils::Format(
3644 "SELECT songview.*, albumview.* FROM songview"
3645 "JOIN albumview ON (songview.idAlbum = albumview.idAlbum) "
3646 "JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song "
3647 "WHERE song.iTimesPlayed > 0 "
3648 "GROUP BY idAlbum "
3649 "ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit "
3650 "ON (songview.idAlbum = _albumlimit.idAlbum) "
3651 "ORDER BY _albumlimit.iTimesPlayedSum DESC");
3652 CLog::Log(LOGDEBUG, "GetTop100AlbumSongs() query: {}", strSQL);
3653 if (!m_pDS->query(strSQL))
3654 return false;
3656 int iRowsFound = m_pDS->num_rows();
3657 if (iRowsFound == 0)
3659 m_pDS->close();
3660 return true;
3663 // get data from returned rows
3664 items.Reserve(iRowsFound);
3665 while (!m_pDS->eof())
3667 CFileItemPtr item(new CFileItem);
3668 GetFileItemFromDataset(item.get(), baseUrl);
3669 items.Add(item);
3670 m_pDS->next();
3673 // cleanup
3674 m_pDS->close();
3675 return true;
3677 catch (...)
3679 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3681 return false;
3684 bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums)
3688 albums.erase(albums.begin(), albums.end());
3689 if (nullptr == m_pDB)
3690 return false;
3691 if (nullptr == m_pDS)
3692 return false;
3694 auto start = std::chrono::steady_clock::now();
3696 // Get data from album and album_artist tables to fully populate albums
3697 std::string strSQL =
3698 PrepareSQL("SELECT albumview.*, albumartistview.* "
3699 "FROM (SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3700 "AND albumview.strReleaseType = '%s' "
3701 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3702 "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
3703 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3704 "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
3705 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT);
3707 auto queryStart = std::chrono::steady_clock::now();
3708 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3709 if (!m_pDS->query(strSQL))
3710 return false;
3712 auto queryEnd = std::chrono::steady_clock::now();
3713 auto queryDuration =
3714 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
3716 int iRowsFound = m_pDS->num_rows();
3717 if (iRowsFound == 0)
3719 m_pDS->close();
3720 return true;
3723 int albumArtistOffset = album_enumCount;
3724 int albumId = -1;
3725 while (!m_pDS->eof())
3727 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3729 if (albumId != record->at(album_idAlbum).get_asInt())
3730 { // New album
3731 albumId = record->at(album_idAlbum).get_asInt();
3732 albums.push_back(GetAlbumFromDataset(record));
3734 // Get album artists
3735 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3737 m_pDS->next();
3739 m_pDS->close(); // cleanup recordset data
3741 auto end = std::chrono::steady_clock::now();
3742 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
3744 CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
3745 duration.count(), queryDuration.count());
3747 return true;
3749 catch (...)
3751 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3754 return false;
3757 bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir,
3758 CFileItemList& items)
3762 if (nullptr == m_pDB)
3763 return false;
3764 if (nullptr == m_pDS)
3765 return false;
3767 CMusicDbUrl baseUrl;
3768 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3769 return false;
3771 std::string strSQL =
3772 PrepareSQL("SELECT songview.*, songartistview.* "
3773 "FROM (SELECT idAlbum, lastPlayed FROM albumview "
3774 "WHERE albumview.lastplayed IS NOT NULL "
3775 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3776 "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
3777 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3778 "ORDER BY playedalbums.lastplayed DESC, "
3779 "songartistview.idsong, songartistview.idRole, songartistview.iOrder",
3780 CServiceBroker::GetSettingsComponent()
3781 ->GetAdvancedSettings()
3782 ->m_iMusicLibraryRecentlyAddedItems);
3783 CLog::Log(LOGDEBUG, "GetRecentlyPlayedAlbumSongs() query: {}", strSQL);
3784 if (!m_pDS->query(strSQL))
3785 return false;
3787 int iRowsFound = m_pDS->num_rows();
3788 if (iRowsFound == 0)
3790 m_pDS->close();
3791 return true;
3794 // Needs a separate query to determine number of songs to set items size.
3795 // Get songs from returned rows. Join means there is a row for every song artist
3796 // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
3797 int songArtistOffset = song_enumCount;
3798 int songId = -1;
3799 VECARTISTCREDITS artistCredits;
3800 while (!m_pDS->eof())
3802 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3804 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3805 if (songId != record->at(song_idSong).get_asInt())
3806 { //New song
3807 if (songId > 0 && !artistCredits.empty())
3809 //Store artist credits for previous song
3810 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3811 artistCredits.clear();
3813 songId = record->at(song_idSong).get_asInt();
3814 CFileItemPtr item(new CFileItem);
3815 GetFileItemFromDataset(record, item.get(), baseUrl);
3816 items.Add(item);
3818 // Get song artist credits and contributors
3819 if (idSongArtistRole == ROLE_ARTIST)
3820 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3821 else
3822 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3823 GetArtistRoleFromDataset(record, songArtistOffset));
3825 m_pDS->next();
3827 if (!artistCredits.empty())
3829 //Store artist credits for final song
3830 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3831 artistCredits.clear();
3834 // cleanup
3835 m_pDS->close();
3836 return true;
3838 catch (...)
3840 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3842 return false;
3845 bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit)
3849 albums.erase(albums.begin(), albums.end());
3850 if (nullptr == m_pDB)
3851 return false;
3852 if (nullptr == m_pDS)
3853 return false;
3855 // Get data from album and album_artist tables to fully populate albums
3856 // Determine the recently added albums from dateAdded (usually derived from music file
3857 // timestamps, nothing to do with when albums added to library)
3858 std::string strSQL =
3859 PrepareSQL("SELECT albumview.*, albumartistview.* "
3860 "FROM (SELECT idAlbum FROM album WHERE strAlbum != '' "
3861 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3862 "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
3863 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3864 "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
3865 limit ? limit
3866 : CServiceBroker::GetSettingsComponent()
3867 ->GetAdvancedSettings()
3868 ->m_iMusicLibraryRecentlyAddedItems);
3870 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3871 if (!m_pDS->query(strSQL))
3872 return false;
3873 int iRowsFound = m_pDS->num_rows();
3874 if (iRowsFound == 0)
3876 m_pDS->close();
3877 return true;
3880 int albumArtistOffset = album_enumCount;
3881 int albumId = -1;
3882 while (!m_pDS->eof())
3884 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3886 if (albumId != record->at(album_idAlbum).get_asInt())
3887 { // New album
3888 albumId = record->at(album_idAlbum).get_asInt();
3889 albums.push_back(GetAlbumFromDataset(record));
3891 // Get album artists
3892 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3894 m_pDS->next();
3896 m_pDS->close(); // cleanup recordset data
3897 return true;
3899 catch (...)
3901 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3904 return false;
3907 bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir,
3908 CFileItemList& items,
3909 unsigned int limit)
3913 if (nullptr == m_pDB)
3914 return false;
3915 if (nullptr == m_pDS)
3916 return false;
3918 CMusicDbUrl baseUrl;
3919 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3920 return false;
3922 // Get data from song and song_artist tables to fully populate songs
3923 // Determine the recently added albums from dateAdded (usually derived from music file
3924 // timestamps, nothing to do with when albums added to library)
3925 std::string strSQL;
3926 strSQL = PrepareSQL("SELECT songview.*, songartistview.* "
3927 "FROM (SELECT idAlbum, dateAdded FROM album "
3928 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3929 "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
3930 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3931 "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
3932 "songview.idSong, songartistview.idRole, songartistview.iOrder ",
3933 limit ? limit
3934 : CServiceBroker::GetSettingsComponent()
3935 ->GetAdvancedSettings()
3936 ->m_iMusicLibraryRecentlyAddedItems);
3937 CLog::Log(LOGDEBUG, "GetRecentlyAddedAlbumSongs() query: {}", strSQL);
3938 if (!m_pDS->query(strSQL))
3939 return false;
3941 int iRowsFound = m_pDS->num_rows();
3942 if (iRowsFound == 0)
3944 m_pDS->close();
3945 return true;
3948 // Needs a separate query to determine number of songs to set items size.
3949 // Get songs from returned rows. Join means there is a row for every song artist
3950 int songArtistOffset = song_enumCount;
3951 int songId = -1;
3952 VECARTISTCREDITS artistCredits;
3953 while (!m_pDS->eof())
3955 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3957 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3958 if (songId != record->at(song_idSong).get_asInt())
3959 { //New song
3960 if (songId > 0 && !artistCredits.empty())
3962 //Store artist credits for previous song
3963 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3964 artistCredits.clear();
3966 songId = record->at(song_idSong).get_asInt();
3967 CFileItemPtr item(new CFileItem);
3968 GetFileItemFromDataset(record, item.get(), baseUrl);
3969 items.Add(item);
3971 // Get song artist credits and contributors
3972 if (idSongArtistRole == ROLE_ARTIST)
3973 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3974 else
3975 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3976 GetArtistRoleFromDataset(record, songArtistOffset));
3978 m_pDS->next();
3980 if (!artistCredits.empty())
3982 //Store artist credits for final song
3983 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3984 artistCredits.clear();
3987 // cleanup
3988 m_pDS->close();
3989 return true;
3991 catch (...)
3993 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3995 return false;
3998 void CMusicDatabase::IncrementPlayCount(const CFileItem& item)
4002 if (nullptr == m_pDB)
4003 return;
4004 if (nullptr == m_pDS)
4005 return;
4007 int idSong = GetSongIDFromPath(item.GetPath());
4008 std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
4009 std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
4010 "WHERE idSong=%i",
4011 strDateNow.c_str(), idSong);
4012 m_pDS->exec(sql);
4014 catch (...)
4016 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, item.GetPath());
4020 bool CMusicDatabase::GetSongsByPath(const std::string& strPath1,
4021 MAPSONGS& songmap,
4022 bool bAppendToMap)
4024 std::string strPath(strPath1);
4027 if (!URIUtils::HasSlashAtEnd(strPath))
4028 URIUtils::AddSlashAtEnd(strPath);
4030 if (!bAppendToMap)
4031 songmap.clear();
4033 if (nullptr == m_pDB)
4034 return false;
4035 if (nullptr == m_pDS)
4036 return false;
4038 // Filename is not unique for a path as songs from a cuesheet have same filename.
4039 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
4040 // in a folder and some edited and rescanned.
4041 // Hence order by filename so these songs can be gathered together.
4042 std::string strSQL = PrepareSQL("SELECT * FROM songview "
4043 "WHERE strPath='%s' ORDER BY strFileName",
4044 strPath.c_str());
4045 if (!m_pDS->query(strSQL))
4046 return false;
4047 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
4048 int iRowsFound = m_pDS->num_rows();
4049 if (iRowsFound == 0)
4051 m_pDS->close();
4052 return false;
4055 // Each file is potentially mapped to a list of songs, gather these and save as list
4056 VECSONGS songs;
4057 std::string filename;
4058 while (!m_pDS->eof())
4060 CSong song = GetSongFromDataset();
4061 if (!filename.empty() && filename != song.strFileName)
4063 // Save songs for previous filename
4064 songmap.insert(std::make_pair(filename, songs));
4065 songs.clear();
4067 filename = song.strFileName;
4068 songs.emplace_back(song);
4069 m_pDS->next();
4071 m_pDS->close(); // cleanup recordset data
4072 songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
4073 return true;
4075 catch (...)
4077 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
4080 return false;
4083 void CMusicDatabase::EmptyCache()
4085 m_genreCache.erase(m_genreCache.begin(), m_genreCache.end());
4086 m_pathCache.erase(m_pathCache.begin(), m_pathCache.end());
4089 bool CMusicDatabase::Search(const std::string& search, CFileItemList& items)
4091 auto start = std::chrono::steady_clock::now();
4092 // first grab all the artists that match
4093 SearchArtists(search, items);
4094 auto end = std::chrono::steady_clock::now();
4095 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
4096 CLog::Log(LOGDEBUG, "{} Artist search in {} ms", __FUNCTION__, duration.count());
4098 start = std::chrono::steady_clock::now();
4099 // then albums that match
4100 SearchAlbums(search, items);
4101 end = std::chrono::steady_clock::now();
4102 duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
4103 CLog::Log(LOGDEBUG, "{} Album search in {} ms", __FUNCTION__, duration.count());
4105 start = std::chrono::steady_clock::now();
4106 // and finally songs
4107 SearchSongs(search, items);
4108 end = std::chrono::steady_clock::now();
4109 duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
4110 CLog::Log(LOGDEBUG, "{} Songs search in {} ms", __FUNCTION__, duration.count());
4112 return true;
4115 bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList& items)
4119 if (nullptr == m_pDB)
4120 return false;
4121 if (nullptr == m_pDS)
4122 return false;
4124 CMusicDbUrl baseUrl;
4125 if (!baseUrl.FromString("musicdb://songs/"))
4126 return false;
4128 std::string strSQL;
4129 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
4130 strSQL = PrepareSQL("SELECT * FROM songview "
4131 "WHERE strTitle LIKE '%s%%' or strTitle LIKE '%% %s%%' LIMIT 1000",
4132 search.c_str(), search.c_str());
4133 else
4134 strSQL = PrepareSQL("SELECT * FROM songview "
4135 "WHERE strTitle LIKE '%s%%' LIMIT 1000",
4136 search.c_str());
4138 if (!m_pDS->query(strSQL))
4139 return false;
4140 if (m_pDS->num_rows() == 0)
4141 return false;
4143 while (!m_pDS->eof())
4145 CFileItemPtr item(new CFileItem);
4146 GetFileItemFromDataset(item.get(), baseUrl);
4147 items.Add(item);
4148 m_pDS->next();
4151 m_pDS->close();
4152 return true;
4154 catch (...)
4156 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
4159 return false;
4162 bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList& albums)
4166 if (nullptr == m_pDB)
4167 return false;
4168 if (nullptr == m_pDS)
4169 return false;
4171 std::string strSQL;
4172 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
4173 strSQL = PrepareSQL("SELECT * FROM albumview "
4174 "WHERE strAlbum LIKE '%s%%' OR strAlbum LIKE '%% %s%%'",
4175 search.c_str(), search.c_str());
4176 else
4177 strSQL = PrepareSQL("SELECT * FROM albumview "
4178 "WHERE strAlbum LIKE '%s%%'",
4179 search.c_str());
4181 if (!m_pDS->query(strSQL))
4182 return false;
4184 const std::string& albumLabel(g_localizeStrings.Get(558)); // Album
4185 while (!m_pDS->eof())
4187 CAlbum album = GetAlbumFromDataset(m_pDS.get());
4188 std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
4189 CFileItemPtr pItem(new CFileItem(path, album));
4190 std::string label = StringUtils::Format("[{}] {}", albumLabel, album.strAlbum);
4191 pItem->SetLabel(label);
4192 // sort label is stored in the title tag
4193 label = StringUtils::Format("B {}", album.strAlbum);
4194 pItem->GetMusicInfoTag()->SetTitle(label);
4195 albums.Add(pItem);
4196 m_pDS->next();
4198 m_pDS->close(); // cleanup recordset data
4199 return true;
4201 catch (...)
4203 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
4205 return false;
4208 bool CMusicDatabase::CleanupSongsByIds(const std::string& strSongIds)
4212 if (nullptr == m_pDB)
4213 return false;
4214 if (nullptr == m_pDS)
4215 return false;
4216 // ok, now find all idSong's
4217 std::string strSQL = PrepareSQL("SELECT * FROM song JOIN path ON song.idPath = path.idPath "
4218 "WHERE song.idSong IN %s",
4219 strSongIds.c_str());
4220 if (!m_pDS->query(strSQL))
4221 return false;
4222 int iRowsFound = m_pDS->num_rows();
4223 if (iRowsFound == 0)
4225 m_pDS->close();
4226 return true;
4228 std::vector<std::string> songsToDelete;
4229 while (!m_pDS->eof())
4230 { // get the full song path
4231 std::string strFileName = URIUtils::AddFileToFolder(
4232 m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString());
4234 // Special case for streams inside an audio decoder package file.
4235 // The last dir in the path is the audio file that
4236 // contains the stream, so test if its there
4237 if (StringUtils::EndsWith(URIUtils::GetExtension(strFileName),
4238 KODI_ADDON_AUDIODECODER_TRACK_EXT))
4240 strFileName = URIUtils::GetDirectory(strFileName);
4241 // we are dropping back to a file, so remove the slash at end
4242 URIUtils::RemoveSlashAtEnd(strFileName);
4245 if (!CFile::Exists(strFileName, false))
4246 { // file no longer exists, so add to deletion list
4247 songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString());
4249 m_pDS->next();
4251 m_pDS->close();
4253 if (!songsToDelete.empty())
4255 std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")";
4256 // ok, now delete these songs + all references to them from the linked tables
4257 strSQL = "delete from song where idSong in " + strSongsToDelete;
4258 m_pDS->exec(strSQL);
4259 m_pDS->close();
4261 return true;
4263 catch (...)
4265 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
4267 return false;
4270 bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/)
4274 int total;
4275 // Count total number of songs
4276 total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS);
4277 // No songs to clean
4278 if (total == 0)
4279 return true;
4281 // run through all songs and get all unique path ids
4282 int iLIMIT = 1000;
4283 for (int i = 0;; i += iLIMIT)
4285 std::string strSQL = PrepareSQL("SELECT song.idSong FROM song "
4286 "ORDER BY song.idSong LIMIT %i OFFSET %i",
4287 iLIMIT, i);
4288 if (!m_pDS->query(strSQL))
4289 return false;
4290 int iRowsFound = m_pDS->num_rows();
4291 // keep going until no rows are left!
4292 if (iRowsFound == 0)
4294 m_pDS->close();
4295 return true;
4298 std::vector<std::string> songIds;
4299 while (!m_pDS->eof())
4301 songIds.push_back(m_pDS->fv("song.idSong").get_asString());
4302 m_pDS->next();
4304 m_pDS->close();
4305 std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")";
4306 CLog::Log(LOGDEBUG, "Checking songs from song ID list: {}", strSongIds);
4307 if (progressDialog)
4309 int percentage = i * 100 / total;
4310 if (percentage > progressDialog->GetPercentage())
4312 progressDialog->SetPercentage(percentage);
4313 progressDialog->Progress();
4315 if (progressDialog->IsCanceled())
4317 m_pDS->close();
4318 return false;
4321 if (!CleanupSongsByIds(strSongIds))
4322 return false;
4324 return true;
4326 catch (...)
4328 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()");
4330 return false;
4333 bool CMusicDatabase::CleanupAlbums()
4337 // This must be run AFTER songs have been cleaned up
4338 // delete albums with no reference to songs
4339 std::string strSQL = "SELECT * FROM album "
4340 "WHERE album.idAlbum NOT IN (SELECT idAlbum FROM song)";
4341 if (!m_pDS->query(strSQL))
4342 return false;
4343 int iRowsFound = m_pDS->num_rows();
4344 if (iRowsFound == 0)
4346 m_pDS->close();
4347 return true;
4350 std::vector<std::string> albumIds;
4351 while (!m_pDS->eof())
4353 albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString());
4354 m_pDS->next();
4356 m_pDS->close();
4358 std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")";
4359 // ok, now we can delete them and the references in the linked tables
4360 strSQL = "delete from album where idAlbum in " + strAlbumIds;
4361 m_pDS->exec(strSQL);
4362 return true;
4364 catch (...)
4366 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()");
4368 return false;
4371 bool CMusicDatabase::CleanupPaths()
4375 // needs to be done AFTER the songs and albums have been cleaned up.
4376 // we can happily delete any path that has no reference to a song
4377 // but we must keep all paths that have been scanned that may contain songs in subpaths
4379 // first create a temporary table of song paths
4380 m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
4381 m_pDS->exec("INSERT INTO songpaths "
4382 "SELECT idPath, strPath FROM path "
4383 "WHERE idPath IN (SELECT idPath FROM song)\n");
4385 // grab all paths that aren't immediately connected with a song
4386 std::string sql = "SELECT * FROM path WHERE idPath NOT IN (SELECT idPath FROM song)";
4387 if (!m_pDS->query(sql))
4388 return false;
4389 int iRowsFound = m_pDS->num_rows();
4390 if (iRowsFound == 0)
4392 m_pDS->close();
4393 return true;
4395 // and construct a list to delete
4396 std::vector<std::string> pathIds;
4397 while (!m_pDS->eof())
4399 // anything that isn't a parent path of a song path is to be deleted
4400 std::string path = m_pDS->fv("strPath").get_asString();
4401 sql = PrepareSQL("SELECT COUNT(idPath) FROM songpaths WHERE SUBSTR(strPath,1,%i)='%s'",
4402 StringUtils::utf8_strlen(path.c_str()), path.c_str());
4403 if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0)
4404 pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete
4405 m_pDS2->close();
4406 m_pDS->next();
4408 m_pDS->close();
4410 if (!pathIds.empty())
4412 // do the deletion, and drop our temp table
4413 std::string deleteSQL =
4414 "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")";
4415 m_pDS->exec(deleteSQL);
4417 m_pDS->exec("drop table songpaths");
4418 return true;
4420 catch (...)
4422 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
4424 return false;
4427 bool CMusicDatabase::InsideScannedPath(const std::string& path)
4429 std::string sql = PrepareSQL("SELECT idPath FROM path WHERE SUBSTR(strPath,1,%i)='%s' LIMIT 1",
4430 path.size(), path.c_str());
4431 return !GetSingleValue(sql).empty();
4434 bool CMusicDatabase::CleanupArtists()
4438 // (nested queries by Bobbin007)
4439 // must be executed AFTER the song, album and their artist link tables are cleaned.
4440 // Don't delete [Missing] the missing artist tag artist
4442 // Create temp table to avoid 1442 trigger hell on mysql
4443 m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
4444 m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
4445 m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
4446 m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID));
4447 // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
4448 // To avoid MySQL hanging or timeout create a table of unique ids with primary key
4449 m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
4450 m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
4451 m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
4452 // Tidy up temp tables
4453 m_pDS->exec("DROP TABLE tmp_delartists");
4454 m_pDS->exec("DROP TABLE tmp_keep");
4456 return true;
4458 catch (...)
4460 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
4462 return false;
4465 bool CMusicDatabase::CleanupGenres()
4469 // Cleanup orphaned song genres (ie those that don't belong to a song entry)
4470 // (nested queries by Bobbin007)
4471 // Must be executed AFTER the song, and song_genre have been cleaned.
4472 std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
4473 m_pDS->exec(strSQL);
4474 return true;
4476 catch (...)
4478 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
4480 return false;
4483 bool CMusicDatabase::CleanupInfoSettings()
4487 // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
4488 // Must be executed AFTER the album and artist tables have been cleaned.
4489 std::string strSQL = "DELETE FROM infosetting "
4490 "WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
4491 "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
4492 m_pDS->exec(strSQL);
4493 return true;
4495 catch (...)
4497 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
4499 return false;
4502 bool CMusicDatabase::CleanupRoles()
4506 // Cleanup orphaned roles (ie those that don't belong to a song entry)
4507 // Must be executed AFTER the song, and song_artist tables have been cleaned.
4508 // Do not remove default role (ROLE_ARTIST)
4509 std::string strSQL = "DELETE FROM role "
4510 "WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
4511 m_pDS->exec(strSQL);
4512 return true;
4514 catch (...)
4516 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
4518 return false;
4521 bool CMusicDatabase::DeleteRemovedLinks()
4525 std::string strSQL = "DELETE FROM removed_link";
4526 m_pDS->exec(strSQL);
4527 return true;
4529 catch (...)
4531 CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks");
4533 return false;
4536 bool CMusicDatabase::CleanupOrphanedItems()
4538 // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
4539 // remove_links not cleared here - done in CheckArtistLinksChanged()
4540 if (nullptr == m_pDB)
4541 return false;
4542 if (nullptr == m_pDS)
4543 return false;
4544 SetLibraryLastUpdated();
4545 if (!CleanupAlbums())
4546 return false;
4547 if (!CleanupArtists())
4548 return false;
4549 if (!CleanupGenres())
4550 return false;
4551 if (!CleanupRoles())
4552 return false;
4553 if (!CleanupInfoSettings())
4554 return false;
4555 return true;
4558 int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/)
4560 if (nullptr == m_pDB)
4561 return ERROR_DATABASE;
4562 if (nullptr == m_pDS)
4563 return ERROR_DATABASE;
4565 int ret;
4566 std::chrono::seconds duration;
4567 auto time = std::chrono::steady_clock::now();
4568 CLog::Log(LOGINFO, "{}: Starting musicdatabase cleanup ..", __FUNCTION__);
4569 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted");
4571 SetLibraryLastCleaned();
4573 // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link
4574 // Check that triggers actually exist first as interrupting the clean causes them to not be
4575 // re-created
4577 m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteSongArtist");
4578 m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteAlbumArtist");
4580 // first cleanup any songs with invalid paths
4581 if (progressDialog)
4583 progressDialog->SetLine(1, CVariant{318});
4584 progressDialog->SetLine(2, CVariant{330});
4585 progressDialog->SetPercentage(0);
4586 progressDialog->Progress();
4588 if (!CleanupSongs(progressDialog))
4590 ret = ERROR_REORG_SONGS;
4591 goto error;
4593 // then the albums that are not linked to a song or to album, or whose path is removed
4594 if (progressDialog)
4596 progressDialog->SetLine(1, CVariant{326});
4597 progressDialog->SetPercentage(20);
4598 progressDialog->Progress();
4599 if (progressDialog->IsCanceled())
4601 ret = ERROR_CANCEL;
4602 goto error;
4605 if (!CleanupAlbums())
4607 ret = ERROR_REORG_ALBUM;
4608 goto error;
4610 // now the paths
4611 if (progressDialog)
4613 progressDialog->SetLine(1, CVariant{324});
4614 progressDialog->SetPercentage(40);
4615 progressDialog->Progress();
4616 if (progressDialog->IsCanceled())
4618 ret = ERROR_CANCEL;
4619 goto error;
4622 if (!CleanupPaths())
4624 ret = ERROR_REORG_PATH;
4625 goto error;
4627 // and finally artists + genres
4628 if (progressDialog)
4630 progressDialog->SetLine(1, CVariant{320});
4631 progressDialog->SetPercentage(60);
4632 progressDialog->Progress();
4633 if (progressDialog->IsCanceled())
4635 ret = ERROR_CANCEL;
4636 goto error;
4639 if (!CleanupArtists())
4641 ret = ERROR_REORG_ARTIST;
4642 goto error;
4644 //Genres, roles and info settings progress in one step
4645 if (progressDialog)
4647 progressDialog->SetLine(1, CVariant{322});
4648 progressDialog->SetPercentage(80);
4649 progressDialog->Progress();
4650 if (progressDialog->IsCanceled())
4652 ret = ERROR_CANCEL;
4653 goto error;
4656 if (!CleanupGenres())
4658 ret = ERROR_REORG_OTHER;
4659 goto error;
4661 if (!CleanupRoles())
4663 ret = ERROR_REORG_OTHER;
4664 goto error;
4666 if (!CleanupInfoSettings())
4668 ret = ERROR_REORG_OTHER;
4669 goto error;
4671 if (!DeleteRemovedLinks())
4673 ret = ERROR_REORG_OTHER;
4674 goto error;
4677 // commit transaction
4678 if (progressDialog)
4680 progressDialog->SetLine(1, CVariant{328});
4681 progressDialog->SetPercentage(90);
4682 progressDialog->Progress();
4683 if (progressDialog->IsCanceled())
4685 ret = ERROR_CANCEL;
4686 goto error;
4689 if (!CommitTransaction())
4691 ret = ERROR_WRITING_CHANGES;
4692 goto error;
4695 // Recreate DELETE triggers on song_artist and album_artist
4696 CreateRemovedLinkTriggers();
4698 // and compress the database
4699 if (progressDialog)
4701 progressDialog->SetLine(1, CVariant{331});
4702 progressDialog->SetPercentage(100);
4703 progressDialog->Close();
4706 duration =
4707 std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - time);
4708 CLog::Log(LOGINFO, "{}: Cleaning musicdatabase done. Operation took {}s", __FUNCTION__,
4709 duration.count());
4710 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4712 if (!Compress(false))
4714 return ERROR_COMPRESSING;
4716 return ERROR_OK;
4718 error:
4719 RollbackTransaction();
4720 // Recreate DELETE triggers on song_artist and album_artist
4721 CreateRemovedLinkTriggers();
4722 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4723 return ret;
4726 bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space)
4728 if (strImage.length() > space)
4730 strImage.resize(space);
4731 // Tidy to last </thumb> tag
4732 size_t iPos = strImage.rfind("</thumb>");
4733 if (iPos == std::string::npos)
4734 return false;
4735 strImage.resize(iPos + 8);
4737 return true;
4740 bool CMusicDatabase::LookupCDDBInfo(bool bRequery /*=false*/)
4742 #ifdef HAS_OPTICAL_DRIVE
4743 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
4744 CSettings::SETTING_AUDIOCDS_USECDDB))
4745 return false;
4747 // check network connectivity
4748 if (!CServiceBroker::GetNetwork().IsAvailable())
4749 return false;
4751 // Get information for the inserted disc
4752 CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
4753 if (pCdInfo == NULL)
4754 return false;
4756 // If the disc has no tracks, we are finished here.
4757 int nTracks = pCdInfo->GetTrackCount();
4758 if (nTracks <= 0)
4759 return false;
4761 // Delete old info if any
4762 if (bRequery)
4764 std::string strFile = StringUtils::Format("{:x}.cddb", pCdInfo->GetCddbDiscId());
4765 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4768 // Prepare cddb
4769 Xcddb cddb;
4770 cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4772 // Do we have to look for cddb information
4773 if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo))
4775 CGUIDialogProgress* pDialogProgress =
4776 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
4777 WINDOW_DIALOG_PROGRESS);
4778 CGUIDialogSelect* pDlgSelect =
4779 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
4780 WINDOW_DIALOG_SELECT);
4782 if (!pDialogProgress)
4783 return false;
4784 if (!pDlgSelect)
4785 return false;
4787 // Show progress dialog if we have to connect to freedb.org
4788 pDialogProgress->SetHeading(CVariant{255}); //CDDB
4789 pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info
4790 pDialogProgress->SetLine(1, CVariant{256});
4791 pDialogProgress->SetLine(2, CVariant{""});
4792 pDialogProgress->ShowProgressBar(false);
4793 pDialogProgress->Open();
4795 // get cddb information
4796 if (!cddb.queryCDinfo(pCdInfo))
4798 pDialogProgress->Close();
4799 int lasterror = cddb.getLastError();
4801 // Have we found more then on match in cddb for this disc,...
4802 if (lasterror == E_WAIT_FOR_INPUT)
4804 // ...yes, show the matches found in a select dialog
4805 // and let the user choose an entry.
4806 pDlgSelect->Reset();
4807 pDlgSelect->SetHeading(CVariant{255});
4808 int i = 1;
4809 while (true)
4811 std::string strTitle = cddb.getInexactTitle(i);
4812 if (strTitle == "")
4813 break;
4815 const std::string& strArtist = cddb.getInexactArtist(i);
4816 if (!strArtist.empty())
4817 strTitle += " - " + strArtist;
4819 pDlgSelect->Add(strTitle);
4820 i++;
4822 pDlgSelect->Open();
4824 // Has the user selected a match...
4825 int iSelectedCD = pDlgSelect->GetSelectedItem();
4826 if (iSelectedCD >= 0)
4828 // ...query cddb for the inexact match
4829 if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD))
4830 pCdInfo->SetNoCDDBInfo();
4832 else
4833 pCdInfo->SetNoCDDBInfo();
4835 else if (lasterror == E_NO_MATCH_FOUND)
4837 pCdInfo->SetNoCDDBInfo();
4839 else
4841 pCdInfo->SetNoCDDBInfo();
4842 // ..no, an error occurred, display it to the user
4843 std::string strErrorText =
4844 StringUtils::Format("[{}] {}", cddb.getLastError(), cddb.getLastErrorText());
4845 HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)},
4846 CVariant{0});
4848 } // if ( !cddb.queryCDinfo( pCdInfo ) )
4849 else
4850 pDialogProgress->Close();
4853 // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
4855 return pCdInfo->HasCDDBInfo();
4856 #else
4857 return false;
4858 #endif
4861 void CMusicDatabase::DeleteCDDBInfo()
4863 #ifdef HAS_OPTICAL_DRIVE
4864 CFileItemList items;
4865 if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb",
4866 DIR_FLAG_NO_FILE_DIRS))
4868 HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426});
4869 return;
4871 // Show a selectdialog that the user can select the album to delete
4872 CGUIDialogSelect* pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
4873 WINDOW_DIALOG_SELECT);
4874 if (pDlg)
4876 pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
4877 pDlg->Reset();
4879 std::map<uint32_t, std::string> mapCDDBIds;
4880 for (int i = 0; i < items.Size(); ++i)
4882 if (items[i]->m_bIsFolder)
4883 continue;
4885 std::string strFile = URIUtils::GetFileName(items[i]->GetPath());
4886 strFile.erase(strFile.size() - 5, 5);
4887 uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16);
4888 Xcddb cddb;
4889 cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4891 if (!cddb.queryCache(lDiscId))
4892 continue;
4894 std::string strDiskTitle, strDiskArtist;
4895 cddb.getDiskTitle(strDiskTitle);
4896 cddb.getDiskArtist(strDiskArtist);
4898 std::string str;
4899 if (strDiskArtist.empty())
4900 str = strDiskTitle;
4901 else
4902 str = strDiskTitle + " - " + strDiskArtist;
4904 pDlg->Add(str);
4905 mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str));
4908 pDlg->Sort();
4909 pDlg->Open();
4911 // and wait till user selects one
4912 int iSelectedAlbum = pDlg->GetSelectedItem();
4913 if (iSelectedAlbum < 0)
4915 mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4916 return;
4919 std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel();
4920 for (const auto& i : mapCDDBIds)
4922 if (i.second == strSelectedAlbum)
4924 std::string strFile = StringUtils::Format("{:x}.cddb", (unsigned int)i.first);
4925 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4926 break;
4929 mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4931 #endif
4934 void CMusicDatabase::Clean()
4936 // If we are scanning for music info in the background,
4937 // other writing access to the database is prohibited.
4938 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
4940 HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057});
4941 return;
4944 if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES)
4946 CMusicDatabase musicdatabase;
4947 if (musicdatabase.Open())
4949 int iReturnString = musicdatabase.Cleanup();
4950 musicdatabase.Close();
4952 if (iReturnString != ERROR_OK)
4954 HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString});
4960 bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir,
4961 CFileItemList& items,
4962 const Filter& filter /* = Filter() */,
4963 bool countOnly /* = false */)
4967 if (nullptr == m_pDB)
4968 return false;
4969 if (nullptr == m_pDS)
4970 return false;
4972 // get primary genres for songs - could be simplified to just SELECT * FROM genre?
4973 std::string strSQL = "SELECT %s FROM genre ";
4975 Filter extFilter = filter;
4976 CMusicDbUrl musicUrl;
4977 SortDescription sorting;
4978 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4979 return false;
4981 // if there are extra WHERE conditions we might need access
4982 // to songview or albumview for these conditions
4983 if (!extFilter.where.empty())
4985 if (extFilter.where.find("artistview") != std::string::npos)
4987 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4988 extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4989 extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
4990 extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
4992 else if (extFilter.where.find("songview") != std::string::npos)
4994 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4995 extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4997 else if (extFilter.where.find("albumview") != std::string::npos)
4999 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
5000 extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
5001 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
5003 extFilter.AppendGroup("genre.idGenre");
5005 extFilter.AppendWhere("genre.strGenre != ''");
5007 if (countOnly)
5009 extFilter.fields = "COUNT(DISTINCT genre.idGenre)";
5010 extFilter.group.clear();
5011 extFilter.order.clear();
5014 std::string strSQLExtra;
5015 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5016 return false;
5018 strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
5019 ? extFilter.fields.c_str()
5020 : "genre.*") +
5021 strSQLExtra;
5023 // run query
5024 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5026 if (!m_pDS->query(strSQL))
5027 return false;
5028 int iRowsFound = m_pDS->num_rows();
5029 if (iRowsFound == 0)
5031 m_pDS->close();
5032 return true;
5035 if (countOnly)
5037 CFileItemPtr pItem(new CFileItem());
5038 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
5039 items.Add(pItem);
5041 m_pDS->close();
5042 return true;
5045 // get data from returned rows
5046 while (!m_pDS->eof())
5048 CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString()));
5049 pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString());
5050 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre");
5052 CMusicDbUrl itemUrl = musicUrl;
5053 std::string strDir = StringUtils::Format("{}/", m_pDS->fv("genre.idGenre").get_asInt());
5054 itemUrl.AppendPath(strDir);
5055 pItem->SetPath(itemUrl.ToString());
5057 pItem->m_bIsFolder = true;
5058 items.Add(pItem);
5060 m_pDS->next();
5063 // cleanup
5064 m_pDS->close();
5066 return true;
5068 catch (...)
5070 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5072 return false;
5075 bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir,
5076 CFileItemList& items,
5077 const Filter& filter /*= Filter()*/,
5078 bool countOnly /*= false*/)
5082 if (nullptr == m_pDB)
5083 return false;
5084 if (nullptr == m_pDS)
5085 return false;
5087 // Get sources for selection list when add/edit filter or smartplaylist rule
5088 std::string strSQL = "SELECT %s FROM source ";
5090 Filter extFilter = filter;
5091 CMusicDbUrl musicUrl;
5092 SortDescription sorting;
5093 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5094 return false;
5096 // if there are extra WHERE conditions we might need access
5097 // to songview or albumview for these conditions
5098 if (!extFilter.where.empty())
5100 if (extFilter.where.find("artistview") != std::string::npos)
5102 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5103 extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
5104 extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
5106 else if (extFilter.where.find("songview") != std::string::npos)
5108 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5109 extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
5111 else if (extFilter.where.find("albumview") != std::string::npos)
5113 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5114 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
5116 extFilter.AppendGroup("source.idSource");
5118 else
5119 { // Get only sources that have been scanned into music library
5120 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5121 extFilter.AppendGroup("source.idSource");
5124 if (countOnly)
5126 extFilter.fields = "COUNT(DISTINCT source.idSource)";
5127 extFilter.group.clear();
5128 extFilter.order.clear();
5131 std::string strSQLExtra;
5132 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5133 return false;
5135 strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
5136 ? extFilter.fields.c_str()
5137 : "source.*") +
5138 strSQLExtra;
5140 // run query
5141 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5143 if (!m_pDS->query(strSQL))
5144 return false;
5145 int iRowsFound = m_pDS->num_rows();
5146 if (iRowsFound == 0)
5148 m_pDS->close();
5149 return true;
5152 if (countOnly)
5154 CFileItemPtr pItem(new CFileItem());
5155 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
5156 items.Add(pItem);
5158 m_pDS->close();
5159 return true;
5162 // get data from returned rows
5163 while (!m_pDS->eof())
5165 CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
5166 pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString());
5167 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source");
5169 CMusicDbUrl itemUrl = musicUrl;
5170 std::string strDir = StringUtils::Format("{}/", m_pDS->fv("source.idSource").get_asInt());
5171 itemUrl.AppendPath(strDir);
5172 itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt());
5173 pItem->SetPath(itemUrl.ToString());
5175 pItem->m_bIsFolder = true;
5176 items.Add(pItem);
5178 m_pDS->next();
5181 // cleanup
5182 m_pDS->close();
5184 return true;
5186 catch (...)
5188 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5190 return false;
5193 bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir,
5194 CFileItemList& items,
5195 const Filter& filter /* = Filter() */)
5199 if (nullptr == m_pDB)
5200 return false;
5201 if (nullptr == m_pDS)
5202 return false;
5204 Filter extFilter = filter;
5205 CMusicDbUrl musicUrl;
5206 SortDescription sorting;
5207 std::string strSQL;
5208 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5209 return false;
5211 bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5212 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
5214 useOriginalYears =
5215 useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/");
5217 if (!useOriginalYears)
5218 { // Get years from year part of release date
5219 strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
5220 extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
5222 else
5223 { // Get years from year part of original date
5224 strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
5225 extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
5227 if (!BuildSQL(strSQL, extFilter, strSQL))
5228 return false;
5230 // run query
5231 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5232 if (!m_pDS->query(strSQL))
5233 return false;
5234 int iRowsFound = m_pDS->num_rows();
5235 if (iRowsFound == 0)
5237 m_pDS->close();
5238 return true;
5241 // get data from returned rows
5242 while (!m_pDS->eof())
5244 CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
5245 pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt());
5246 if (useOriginalYears)
5247 pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
5248 else
5249 pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year");
5251 CMusicDbUrl itemUrl = musicUrl;
5252 std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
5253 itemUrl.AppendPath(strDir);
5254 if (useOriginalYears)
5255 itemUrl.AddOption("useoriginalyear", true);
5256 pItem->SetPath(itemUrl.ToString());
5258 pItem->m_bIsFolder = true;
5259 items.Add(pItem);
5261 m_pDS->next();
5264 // cleanup
5265 m_pDS->close();
5267 return true;
5269 catch (...)
5271 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5273 return false;
5276 bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir,
5277 CFileItemList& items,
5278 const Filter& filter /* = Filter() */)
5282 if (nullptr == m_pDB)
5283 return false;
5284 if (nullptr == m_pDS)
5285 return false;
5287 Filter extFilter = filter;
5288 CMusicDbUrl musicUrl;
5289 SortDescription sorting;
5290 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5291 return false;
5293 // get roles with artists having that role
5294 std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role "
5295 "JOIN song_artist ON song_artist.idRole = role.idRole ";
5297 if (!BuildSQL(strSQL, extFilter, strSQL))
5298 return false;
5300 // run query
5301 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5302 if (!m_pDS->query(strSQL))
5303 return false;
5304 int iRowsFound = m_pDS->num_rows();
5305 if (iRowsFound == 0)
5307 m_pDS->close();
5308 return true;
5311 // get data from returned rows
5312 while (!m_pDS->eof())
5314 std::string labelValue = m_pDS->fv("role.strRole").get_asString();
5315 CFileItemPtr pItem(new CFileItem(labelValue));
5316 pItem->GetMusicInfoTag()->SetTitle(labelValue);
5317 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role");
5318 CMusicDbUrl itemUrl = musicUrl;
5319 std::string strDir = StringUtils::Format("{}/", m_pDS->fv("role.idRole").get_asInt());
5320 itemUrl.AppendPath(strDir);
5321 itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt());
5322 pItem->SetPath(itemUrl.ToString());
5324 pItem->m_bIsFolder = true;
5325 items.Add(pItem);
5327 m_pDS->next();
5330 // cleanup
5331 m_pDS->close();
5333 return true;
5335 catch (...)
5337 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5339 return false;
5342 bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year)
5344 CMusicDbUrl musicUrl;
5345 if (!musicUrl.FromString(strBaseDir))
5346 return false;
5348 musicUrl.AddOption("year", year);
5349 musicUrl.AddOption("show_singles", true); // allow singles to be listed
5351 Filter filter;
5352 return GetAlbumsByWhere(musicUrl.ToString(), filter, items);
5355 bool CMusicDatabase::GetCommonNav(const std::string& strBaseDir,
5356 const std::string& table,
5357 const std::string& labelField,
5358 CFileItemList& items,
5359 const Filter& filter /* = Filter() */,
5360 bool countOnly /* = false */)
5362 if (nullptr == m_pDB)
5363 return false;
5364 if (nullptr == m_pDS)
5365 return false;
5367 if (table.empty() || labelField.empty())
5368 return false;
5372 Filter extFilter = filter;
5373 std::string strSQL = "SELECT %s FROM " + table + " ";
5374 extFilter.AppendGroup(labelField);
5375 extFilter.AppendWhere(labelField + " != ''");
5377 if (countOnly)
5379 extFilter.fields = "COUNT(DISTINCT " + labelField + ")";
5380 extFilter.group.clear();
5381 extFilter.order.clear();
5384 // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
5385 // e.g. LIKE '%symphony%' would be taken as a %s format argument
5386 strSQL = PrepareSQL(strSQL,
5387 !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str());
5389 CMusicDbUrl musicUrl;
5390 if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl))
5391 return false;
5393 // run query
5394 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5395 if (!m_pDS->query(strSQL))
5396 return false;
5398 int iRowsFound = m_pDS->num_rows();
5399 if (iRowsFound <= 0)
5401 m_pDS->close();
5402 return false;
5405 if (countOnly)
5407 CFileItemPtr pItem(new CFileItem());
5408 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
5409 items.Add(pItem);
5411 m_pDS->close();
5412 return true;
5415 // get data from returned rows
5416 while (!m_pDS->eof())
5418 std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString();
5419 CFileItemPtr pItem(new CFileItem(labelValue));
5421 CMusicDbUrl itemUrl = musicUrl;
5422 std::string strDir = StringUtils::Format("{}/", labelValue);
5423 itemUrl.AppendPath(strDir);
5424 pItem->SetPath(itemUrl.ToString());
5426 pItem->m_bIsFolder = true;
5427 items.Add(pItem);
5429 m_pDS->next();
5432 // cleanup
5433 m_pDS->close();
5435 return true;
5437 catch (...)
5439 m_pDS->close();
5440 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5443 return false;
5446 bool CMusicDatabase::GetAlbumTypesNav(const std::string& strBaseDir,
5447 CFileItemList& items,
5448 const Filter& filter /* = Filter() */,
5449 bool countOnly /* = false */)
5451 return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly);
5454 bool CMusicDatabase::GetMusicLabelsNav(const std::string& strBaseDir,
5455 CFileItemList& items,
5456 const Filter& filter /* = Filter() */,
5457 bool countOnly /* = false */)
5459 return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly);
5462 bool CMusicDatabase::GetArtistsNav(const std::string& strBaseDir,
5463 CFileItemList& items,
5464 bool albumArtistsOnly /* = false */,
5465 int idGenre /* = -1 */,
5466 int idAlbum /* = -1 */,
5467 int idSong /* = -1 */,
5468 const Filter& filter /* = Filter() */,
5469 const SortDescription& sortDescription /* = SortDescription() */,
5470 bool countOnly /* = false */)
5472 if (nullptr == m_pDB)
5473 return false;
5474 if (nullptr == m_pDS)
5475 return false;
5478 CMusicDbUrl musicUrl;
5479 if (!musicUrl.FromString(strBaseDir))
5480 return false;
5482 if (idGenre > 0)
5483 musicUrl.AddOption("genreid", idGenre);
5484 else if (idAlbum > 0)
5485 musicUrl.AddOption("albumid", idAlbum);
5486 else if (idSong > 0)
5487 musicUrl.AddOption("songid", idSong);
5489 // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
5490 // when local option already present in music URL thus allowing it to be an option in custom nodes
5491 if (!musicUrl.HasOption("albumartistsonly"))
5492 musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
5494 bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5496 return result;
5498 catch (...)
5500 m_pDS->close();
5501 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5503 return false;
5506 bool CMusicDatabase::GetArtistsByWhere(
5507 const std::string& strBaseDir,
5508 const Filter& filter,
5509 CFileItemList& items,
5510 const SortDescription& sortDescription /* = SortDescription() */,
5511 bool countOnly /* = false */)
5513 if (nullptr == m_pDB)
5514 return false;
5515 if (nullptr == m_pDS)
5516 return false;
5520 auto start = std::chrono::steady_clock::now();
5521 int total = -1;
5523 Filter extFilter = filter;
5524 CMusicDbUrl musicUrl;
5525 SortDescription sorting = sortDescription;
5526 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5527 return false;
5529 bool extended = false;
5530 bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5532 // if there are extra WHERE conditions (from media filter dialog) we might
5533 // need access to songview or albumview for these conditions
5534 if (!extFilter.where.empty())
5536 if (extFilter.where.find("songview") != std::string::npos)
5538 extended = true;
5539 extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist "
5540 "JOIN songview ON songview.idSong = song_artist.idSong");
5542 else if (extFilter.where.find("albumview") != std::string::npos)
5544 extended = true;
5545 extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist "
5546 "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
5548 if (extended)
5549 extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
5552 std::string strSQLExtra;
5553 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5554 return false;
5556 // Count number of artsits that satisfy selection criteria (no limit built)
5557 // Count done in full query fetch when unlimited
5558 if (countOnly || limitedInSQL)
5560 if (extended)
5562 // Count distinct without group by
5563 Filter countFilter = extFilter;
5564 countFilter.group.clear();
5565 std::string strSQLWhere;
5566 if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5567 return false;
5568 total = GetSingleValueInt(
5569 "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS);
5571 else
5572 total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS);
5574 if (countOnly)
5576 CFileItemPtr pItem(new CFileItem());
5577 pItem->SetProperty("total", total);
5578 items.Add(pItem);
5580 m_pDS->close();
5581 return true;
5584 // Apply any limiting directly in SQL and so sort as well
5585 if (limitedInSQL)
5587 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5590 // Apply sort in SQL
5591 const std::shared_ptr<CSettings> settings =
5592 CServiceBroker::GetSettingsComponent()->GetSettings();
5593 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5594 sorting.sortAttributes =
5595 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5596 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5597 GetOrderFilter(MediaTypeArtist, sorting, extFilter);
5599 strSQLExtra.clear();
5600 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5601 return false;
5603 std::string strSQL;
5604 std::string strFields = "artistview.*";
5605 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5606 strFields = "artistview.*, " + extFilter.fields;
5607 strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra;
5609 // run query
5610 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5611 auto queryStart = std::chrono::steady_clock::now();
5612 if (!m_pDS->query(strSQL))
5613 return false;
5614 int iRowsFound = m_pDS->num_rows();
5615 if (iRowsFound == 0)
5617 m_pDS->close();
5618 return true;
5621 auto queryEnd = std::chrono::steady_clock::now();
5622 auto queryDuration =
5623 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
5625 // Store the total number of artists as a property
5626 if (total < iRowsFound)
5627 total = iRowsFound;
5628 items.SetProperty("total", total);
5630 DatabaseResults results;
5631 results.reserve(iRowsFound);
5632 // Populate results field vector from dataset
5633 FieldList fields;
5634 if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results))
5635 return false;
5636 // Store item list sort order
5637 items.SetSortMethod(sortDescription.sortBy);
5638 items.SetSortOrder(sortDescription.sortOrder);
5640 // Get Artists from returned rows
5641 items.Reserve(results.size());
5642 const dbiplus::query_data& data = m_pDS->get_result_set().records;
5643 for (const auto& i : results)
5645 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5646 const dbiplus::sql_record* const record = data.at(targetRow);
5650 CArtist artist = GetArtistFromDataset(record, false);
5651 CFileItemPtr pItem(new CFileItem(artist));
5653 CMusicDbUrl itemUrl = musicUrl;
5654 std::string path = StringUtils::Format("{}/", artist.idArtist);
5655 itemUrl.AppendPath(path);
5656 pItem->SetPath(itemUrl.ToString());
5658 pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist);
5659 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5660 pItem->SetProperty("icon_never_overlay", true);
5661 pItem->SetArt("icon", "DefaultArtist.png");
5663 SetPropertiesFromArtist(*pItem, artist);
5664 items.Add(pItem);
5666 catch (...)
5668 m_pDS->close();
5669 CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
5670 items.Size());
5673 // cleanup
5674 m_pDS->close();
5676 auto end = std::chrono::steady_clock::now();
5677 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
5679 CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1} ms query took {2} ms",
5680 __FUNCTION__, duration.count(), queryDuration.count());
5682 return true;
5684 catch (...)
5686 m_pDS->close();
5687 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5689 return false;
5692 bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum& album)
5696 if (nullptr == m_pDB)
5697 return false;
5698 if (nullptr == m_pDS)
5699 return false;
5701 std::string strSQL = PrepareSQL("SELECT albumview.* FROM song "
5702 "JOIN albumview on song.idAlbum = albumview.idAlbum "
5703 "WHERE song.idSong='%i'",
5704 idSong);
5705 if (!m_pDS->query(strSQL))
5706 return false;
5707 int iRowsFound = m_pDS->num_rows();
5708 if (iRowsFound != 1)
5710 m_pDS->close();
5711 return false;
5714 album = GetAlbumFromDataset(m_pDS.get());
5716 m_pDS->close();
5717 return true;
5719 catch (...)
5721 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5723 return false;
5726 bool CMusicDatabase::GetAlbumsNav(const std::string& strBaseDir,
5727 CFileItemList& items,
5728 int idGenre /* = -1 */,
5729 int idArtist /* = -1 */,
5730 const Filter& filter /* = Filter() */,
5731 const SortDescription& sortDescription /* = SortDescription() */,
5732 bool countOnly /* = false */)
5734 CMusicDbUrl musicUrl;
5735 if (!musicUrl.FromString(strBaseDir))
5736 return false;
5738 // where clause
5739 if (idGenre > 0)
5740 musicUrl.AddOption("genreid", idGenre);
5742 if (idArtist > 0)
5743 musicUrl.AddOption("artistid", idArtist);
5745 return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5748 bool CMusicDatabase::GetAlbumsByWhere(
5749 const std::string& baseDir,
5750 const Filter& filter,
5751 CFileItemList& items,
5752 const SortDescription& sortDescription /* = SortDescription() */,
5753 bool countOnly /* = false */)
5755 if (m_pDB == nullptr || m_pDS == nullptr)
5756 return false;
5760 auto start = std::chrono::steady_clock::now();
5761 int total = -1;
5763 Filter extFilter = filter;
5764 CMusicDbUrl musicUrl;
5765 SortDescription sorting = sortDescription;
5766 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5767 return false;
5769 bool extended = false;
5770 bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5772 // If there are extra WHERE conditions (from media filter dialog) we might
5773 // need access to songview for these conditions
5774 if (extFilter.where.find("songview") != std::string::npos)
5776 extended = true;
5777 extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
5778 extFilter.AppendGroup("albumview.idAlbum");
5781 std::string strSQLExtra;
5782 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5783 return false;
5785 // Count number of albums that satisfy selection criteria (no limit built)
5786 // Count done in full query fetch when unlimited
5787 if (countOnly || limitedInSQL)
5789 if (extended)
5791 // Count distinct without group by
5792 Filter countFilter = extFilter;
5793 countFilter.group.clear();
5794 std::string strSQLWhere;
5795 if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5796 return false;
5797 total = GetSingleValueInt(
5798 "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS);
5800 else
5801 total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
5803 if (countOnly)
5805 CFileItemPtr pItem(new CFileItem());
5806 pItem->SetProperty("total", total);
5807 items.Add(pItem);
5809 m_pDS->close();
5810 return true;
5813 // Apply any limiting directly in SQL
5814 if (limitedInSQL)
5816 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5819 // Apply sort in SQL
5820 const std::shared_ptr<CSettings> settings =
5821 CServiceBroker::GetSettingsComponent()->GetSettings();
5822 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5823 sorting.sortAttributes =
5824 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5825 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5826 GetOrderFilter(MediaTypeAlbum, sorting, extFilter);
5827 // Modify order to use correct calculated year field
5828 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5829 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
5830 StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
5831 else
5832 StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5834 strSQLExtra.clear();
5835 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5836 return false;
5838 std::string strSQL;
5839 std::string strFields = "albumview.*";
5840 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5841 strFields = "albumview.*, " + extFilter.fields;
5842 strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra;
5844 // run query
5845 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5846 auto querytime = std::chrono::steady_clock::now();
5847 if (!m_pDS->query(strSQL))
5848 return false;
5849 int iRowsFound = m_pDS->num_rows();
5850 if (iRowsFound == 0)
5852 m_pDS->close();
5853 return true;
5856 auto queryEnd = std::chrono::steady_clock::now();
5857 auto queryDuration =
5858 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - querytime);
5860 // Store the total number of albums as a property
5861 if (total < iRowsFound)
5862 total = iRowsFound;
5863 items.SetProperty("total", total);
5865 DatabaseResults results;
5866 results.reserve(iRowsFound);
5867 // Populate results field vector from dataset
5868 FieldList fields;
5869 if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results))
5870 return false;
5871 // Store item list sort order
5872 items.SetSortMethod(sorting.sortBy);
5873 items.SetSortOrder(sorting.sortOrder);
5875 // Get albums from returned rows
5876 items.Reserve(results.size());
5877 const dbiplus::query_data& data = m_pDS->get_result_set().records;
5878 for (const auto& i : results)
5880 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5881 const dbiplus::sql_record* const record = data.at(targetRow);
5885 CMusicDbUrl itemUrl = musicUrl;
5886 std::string path = StringUtils::Format("{}/", record->at(album_idAlbum).get_asInt());
5887 itemUrl.AppendPath(path);
5889 CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record)));
5890 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5891 pItem->SetProperty("icon_never_overlay", true);
5892 pItem->SetArt("icon", "DefaultAlbumCover.png");
5893 items.Add(pItem);
5895 catch (...)
5897 m_pDS->close();
5898 CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
5899 items.Size());
5902 // cleanup
5903 m_pDS->close();
5905 auto end = std::chrono::steady_clock::now();
5906 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
5908 CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
5909 duration.count(), queryDuration.count());
5911 return true;
5913 catch (...)
5915 m_pDS->close();
5916 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
5918 return false;
5921 bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir,
5922 CFileItemList& items,
5923 int idAlbum,
5924 const Filter& filter,
5925 const SortDescription& sortDescription,
5926 bool countOnly)
5928 CMusicDbUrl musicUrl;
5929 if (!musicUrl.FromString(strBaseDir))
5930 return false;
5932 if (idAlbum > 0)
5933 musicUrl.AddOption("albumid", idAlbum);
5935 return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5938 bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir,
5939 const Filter& filter,
5940 CFileItemList& items,
5941 const SortDescription& sortDescription,
5942 bool countOnly)
5944 CMusicDbUrl musicUrl;
5945 if (!musicUrl.FromString(baseDir))
5946 return false;
5947 return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5950 bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl,
5951 const Filter& filter,
5952 CFileItemList& items,
5953 const SortDescription& sortDescription,
5954 bool countOnly)
5956 if (m_pDB == nullptr || m_pDS == nullptr)
5957 return false;
5961 auto start = std::chrono::steady_clock::now();
5962 int total = -1;
5963 std::string strSQL;
5965 Filter extFilter = filter;
5966 SortDescription sorting = sortDescription;
5968 if (!GetFilter(musicUrl, extFilter, sorting))
5969 return false;
5971 extFilter.AppendGroup("albumview.idAlbum, iDisc");
5973 // If there are extra songview WHERE conditions adjust to song or albumview
5974 // fields, and join Path table for strPath
5975 // ! @todo: convert songview fields into to song or albumview fields
5976 // But not sure we ever get songview fields in filter - REMOVE??
5977 if (extFilter.where.find("songview.strPath") != std::string::npos)
5979 extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
5982 std::string strSQLExtra;
5983 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5984 return false;
5986 // Apply any limiting directly in SQL if there is either no special sorting or random sort
5987 // When limited, random sort is also applied in SQL
5988 bool limitedInSQL = extFilter.limit.empty() &&
5989 (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) &&
5990 (sorting.limitStart > 0 || sorting.limitEnd > 0);
5992 if (countOnly || limitedInSQL)
5994 // Count number of discs that satisfy selection criteria
5995 // (when fetching all records get total from row count of results dataset)
5996 // Count not allow for same non-null title discs to be grouped together
5997 strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
5998 "albumview.idAlbum " +
5999 strSQLExtra;
6000 strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc ";
6001 total = GetSingleValueInt(strSQL, m_pDS);
6003 if (countOnly)
6005 items.SetProperty("total", total);
6006 return true;
6008 // Apply limits and random sort order directly in SQL
6009 if (limitedInSQL)
6011 if (sorting.sortBy == SortByRandom)
6012 strSQLExtra += PrepareSQL(" ORDER BY RANDOM()");
6013 strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
6015 else
6016 strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
6018 strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
6019 "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " +
6020 strSQLExtra;
6022 // run query
6023 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
6024 auto queryStart = std::chrono::steady_clock::now();
6025 if (!m_pDS->query(strSQL))
6026 return false;
6027 int iRowsFound = m_pDS->num_rows();
6028 if (iRowsFound == 0)
6030 m_pDS->close();
6031 return true;
6034 auto queryEnd = std::chrono::steady_clock::now();
6035 auto queryDuration =
6036 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
6038 // store the total value of items as a property
6039 if (total < iRowsFound)
6040 total = iRowsFound;
6041 items.SetProperty("total", total);
6043 DatabaseResults results;
6044 results.reserve(iRowsFound);
6046 // Avoid sorting with limits, just fetch results from dataset
6047 // Limit when SortByNone already applied in SQL,
6048 // Need guaranteed ordering for dataset processing to group by disc title
6049 // so apply sort later to fileitems list rather than dataset
6050 sorting.sortBy = SortByNone;
6051 if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results))
6052 return false;
6054 // Get data from returned rows, note possibly multiple albums although usually only one
6055 items.Reserve(total);
6056 int albumOffset = 2;
6057 CAlbum album;
6058 bool useTitle = true; // Assume we want to match by disc title later unless we have no titles
6059 std::string oldDiscTitle;
6060 const dbiplus::query_data& data = m_pDS->get_result_set().records;
6061 for (const auto& i : results)
6063 unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger());
6064 const dbiplus::sql_record* const record = data.at(targetRow);
6067 if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt())
6068 { // New album
6069 useTitle = true;
6070 album = GetAlbumFromDataset(record, albumOffset);
6073 int discnum = record->at(0).get_asInt();
6074 std::string strDiscSubtitle = record->at(1).get_asString();
6075 if (strDiscSubtitle.empty())
6076 { // Make (fake) disc title from disc number, group by disc number as no real title to match
6077 strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum);
6078 useTitle = false;
6080 else if (oldDiscTitle == strDiscSubtitle)
6081 { // disc title already added to list, fetch the next disc
6082 continue;
6084 oldDiscTitle = strDiscSubtitle;
6086 CMusicDbUrl itemUrl = musicUrl;
6087 std::string path = StringUtils::Format("{}/", discnum);
6088 itemUrl.AppendPath(path);
6090 // When disc titles are provided group discs together by title not number.
6091 // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
6092 if (useTitle)
6093 itemUrl.AddOption("disctitle", strDiscSubtitle.c_str());
6094 else
6095 itemUrl.AddOption("discid", discnum);
6096 CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album));
6097 pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order??
6098 pItem->GetMusicInfoTag()->SetDiscNumber(discnum);
6099 pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle);
6100 pItem->SetLabel(strDiscSubtitle);
6101 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
6102 pItem->SetProperty("icon_never_overlay", true);
6103 pItem->SetArt("icon", "DefaultAlbumCover.png");
6104 items.Add(pItem);
6106 catch (...)
6108 m_pDS->close();
6109 CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
6110 items.Size());
6114 // cleanup
6115 m_pDS->close();
6117 // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
6118 // that is when have join with songartistview and sorting other than random with limit
6119 if (sorting.sortBy != SortByNone && !(limitedInSQL && sorting.sortBy == SortByRandom))
6120 items.Sort(sorting);
6122 auto end = std::chrono::steady_clock::now();
6123 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
6125 CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms", __FUNCTION__,
6126 duration.count(), queryDuration.count());
6128 return true;
6130 catch (...)
6132 m_pDS->close();
6133 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
6136 return false;
6138 int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */)
6140 int iDiscTotal = -1;
6141 CFileItemList itemscount;
6142 if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true))
6143 iDiscTotal = itemscount.GetProperty("total").asInteger32();
6144 return iDiscTotal;
6147 bool CMusicDatabase::GetSongsFullByWhere(
6148 const std::string& baseDir,
6149 const Filter& filter,
6150 CFileItemList& items,
6151 const SortDescription& sortDescription /* = SortDescription() */,
6152 bool artistData /* = false*/)
6154 if (m_pDB == nullptr || m_pDS == nullptr)
6155 return false;
6159 auto start = std::chrono::steady_clock::now();
6160 int total = -1;
6162 Filter extFilter = filter;
6163 CMusicDbUrl musicUrl;
6164 SortDescription sorting = sortDescription;
6165 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6166 return false;
6168 bool extended = false;
6169 bool limitedInSQL =
6170 extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0);
6172 // If there are extra WHERE conditions (from media filter dialog) we might
6173 // need access to albumview for these conditions
6174 if (extFilter.where.find("albumview") != std::string::npos)
6176 extended = true;
6177 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
6180 // Build songview <where> for count
6181 std::string strSQLExtra;
6182 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6183 return false;
6185 // Count (without group by) number of songs that satisfy selection criteria
6186 // Much quicker to use song table, not songview, when filtering only on song fields
6187 if (extended ||
6188 (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos ||
6189 extFilter.where.find("strPath") != std::string::npos ||
6190 extFilter.where.find("bCompilation") != std::string::npos ||
6191 extFilter.where.find("bBoxedset") != std::string::npos)))
6192 total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS);
6193 else
6195 std::string strSQLsong = strSQLExtra;
6196 StringUtils::Replace(strSQLsong, "songview", "song");
6197 total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS);
6200 if (extended)
6201 extFilter.AppendGroup("songview.idSong");
6203 // Apply any limiting directly in SQL
6204 if (limitedInSQL)
6206 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
6209 // Apply sort in SQL
6210 const std::shared_ptr<CSettings> settings =
6211 CServiceBroker::GetSettingsComponent()->GetSettings();
6212 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
6213 sorting.sortAttributes =
6214 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
6215 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
6216 GetOrderFilter(MediaTypeSong, sorting, extFilter);
6217 // Modify order to use correct calculated year field
6218 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
6219 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
6220 StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
6221 else
6222 StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
6224 std::string strFields = "songview.*";
6225 if (!artistData || limitedInSQL)
6227 // Build songview <where> + <order by> + <limits>
6228 strSQLExtra.clear();
6229 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6230 return false;
6232 else
6233 strFields = "songview.*, songartistview.*";
6234 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
6235 strFields = strFields + ", " + extFilter.fields;
6237 std::string strSQL;
6238 if (artistData)
6239 { // Get data from song and song_artist tables to fully populate songs with artists
6240 // All songs now have at least one artist so inner join sufficient
6241 // Build songartistview JOIN part of query
6242 Filter joinFilter;
6243 std::string strSQLJoin;
6244 joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
6245 if (sortDescription.sortBy == SortByRandom)
6246 joinFilter.AppendOrder("songartistview.idSong");
6247 else
6248 joinFilter.order = extFilter.order;
6249 if (limitedInSQL)
6251 StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong");
6252 StringUtils::Replace(joinFilter.order, "songview.", "sv.");
6254 else
6255 joinFilter.where = extFilter.where;
6256 joinFilter.AppendOrder("songartistview.idRole");
6257 joinFilter.AppendOrder("songartistview.iOrder");
6258 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6259 return false;
6261 if (limitedInSQL)
6263 // When have artist data (all roles) and LIMIT on songs use inline view
6264 // SELECT sv.*, songartistview.* FROM
6265 // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
6266 // <order by sv fields>, songartistview.idRole, songartistview.iOrder
6267 // Apply where clause, limits and order to songview, then join to songartistview this gives
6268 // multiple records per song in result set
6269 strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
6270 strSQL = "(" + strSQL + ") AS sv ";
6271 strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin;
6273 else
6274 strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin;
6276 else
6277 strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
6279 CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
6280 auto queryStart = std::chrono::steady_clock::now();
6281 // run query
6282 if (!m_pDS->query(strSQL))
6283 return false;
6285 int iRowsFound = m_pDS->num_rows();
6286 if (iRowsFound == 0)
6288 m_pDS->close();
6289 return true;
6292 auto queryEnd = std::chrono::steady_clock::now();
6293 auto queryDuration =
6294 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
6296 // Store the total number of songs as a property
6297 items.SetProperty("total", total);
6299 DatabaseResults results;
6300 results.reserve(iRowsFound);
6301 // Populate results field vector from dataset
6302 FieldList fields;
6303 if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results))
6304 return false;
6305 // Store item list sort order
6306 items.SetSortMethod(sorting.sortBy);
6307 items.SetSortOrder(sorting.sortOrder);
6309 // Get songs from returned rows. If join songartistview then there is a row for every artist
6310 items.Reserve(total);
6311 int songArtistOffset = song_enumCount;
6312 int songId = -1;
6313 VECARTISTCREDITS artistCredits;
6314 const dbiplus::query_data& data = m_pDS->get_result_set().records;
6315 int count = 0;
6316 for (const auto& i : results)
6318 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
6319 const dbiplus::sql_record* const record = data.at(targetRow);
6323 if (songId != record->at(song_idSong).get_asInt())
6324 { //New song
6325 if (songId > 0 && !artistCredits.empty())
6327 //Store artist credits for previous song
6328 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
6329 artistCredits.clear();
6331 songId = record->at(song_idSong).get_asInt();
6332 CFileItemPtr item(new CFileItem);
6333 GetFileItemFromDataset(record, item.get(), musicUrl);
6334 // HACK for sorting by database returned order
6335 item->m_iprogramCount = ++count;
6336 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
6337 item->SetProperty("icon_never_overlay", true);
6338 item->SetArt("icon", "DefaultAudio.png");
6339 items.Add(item);
6341 // Get song artist credits and contributors
6342 if (artistData)
6344 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
6345 if (idSongArtistRole == ROLE_ARTIST)
6346 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
6347 else
6348 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
6349 GetArtistRoleFromDataset(record, songArtistOffset));
6352 catch (...)
6354 m_pDS->close();
6355 CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
6356 return (items.Size() > 0);
6359 if (!artistCredits.empty())
6361 //Store artist credits for final song
6362 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
6363 artistCredits.clear();
6365 // cleanup
6366 m_pDS->close();
6368 // Ensure random order of item list when results set sorted by idSong for artist processing
6369 // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
6370 // navigation. Order is read later from view state and list sorting is then triggered by
6371 // CGUIMediaWindow::Update in both cases.
6372 // So sorting here is currently redundant, but the consistent place to do it.
6373 // !@ todo: do sorting once, preferably in SQL
6374 if (sorting.sortBy == SortByRandom && artistData)
6375 items.Sort(sorting);
6377 auto end = std::chrono::steady_clock::now();
6378 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
6380 CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__,
6381 duration.count(), queryDuration.count());
6383 return true;
6385 catch (...)
6387 // cleanup
6388 m_pDS->close();
6389 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
6391 return false;
6394 bool CMusicDatabase::GetSongsByWhere(
6395 const std::string& baseDir,
6396 const Filter& filter,
6397 CFileItemList& items,
6398 const SortDescription& sortDescription /* = SortDescription() */)
6400 if (m_pDB == nullptr || m_pDS == nullptr)
6401 return false;
6405 int total = -1;
6407 std::string strSQL = "SELECT %s FROM songview ";
6409 Filter extFilter = filter;
6410 CMusicDbUrl musicUrl;
6411 SortDescription sorting = sortDescription;
6412 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6413 return false;
6415 // if there are extra WHERE conditions we might need access
6416 // to songview for these conditions
6417 if (extFilter.where.find("albumview") != std::string::npos)
6419 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
6420 extFilter.AppendGroup("songview.idSong");
6423 std::string strSQLExtra;
6424 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6425 return false;
6427 // Apply the limiting directly here if there's no special sorting but limiting
6428 if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
6429 (sorting.limitStart > 0 || sorting.limitEnd > 0))
6431 total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS);
6432 strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
6435 strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0
6436 ? filter.fields.c_str()
6437 : "songview.*") +
6438 strSQLExtra;
6440 CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
6441 // run query
6442 if (!m_pDS->query(strSQL))
6443 return false;
6445 int iRowsFound = m_pDS->num_rows();
6446 if (iRowsFound == 0)
6448 m_pDS->close();
6449 return true;
6452 // store the total value of items as a property
6453 if (total < iRowsFound)
6454 total = iRowsFound;
6455 items.SetProperty("total", total);
6457 DatabaseResults results;
6458 results.reserve(iRowsFound);
6459 if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results))
6460 return false;
6462 // get data from returned rows
6463 items.Reserve(results.size());
6464 const dbiplus::query_data& data = m_pDS->get_result_set().records;
6465 int count = 0;
6466 for (const auto& i : results)
6468 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
6469 const dbiplus::sql_record* const record = data.at(targetRow);
6473 CFileItemPtr item(new CFileItem);
6474 GetFileItemFromDataset(record, item.get(), musicUrl);
6475 // HACK for sorting by database returned order
6476 item->m_iprogramCount = ++count;
6477 items.Add(item);
6479 catch (...)
6481 m_pDS->close();
6482 CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
6483 return (items.Size() > 0);
6487 // cleanup
6488 m_pDS->close();
6489 return true;
6491 catch (...)
6493 // cleanup
6494 m_pDS->close();
6495 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
6497 return false;
6500 bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year)
6502 CMusicDbUrl musicUrl;
6503 if (!musicUrl.FromString(baseDir))
6504 return false;
6506 musicUrl.AddOption("year", year);
6508 Filter filter;
6509 return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true);
6512 bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir,
6513 CFileItemList& items,
6514 int idGenre,
6515 int idArtist,
6516 int idAlbum,
6517 const SortDescription& sortDescription /* = SortDescription() */)
6519 CMusicDbUrl musicUrl;
6520 if (!musicUrl.FromString(strBaseDir))
6521 return false;
6523 if (idAlbum > 0)
6524 musicUrl.AddOption("albumid", idAlbum);
6526 if (idGenre > 0)
6527 musicUrl.AddOption("genreid", idGenre);
6529 if (idArtist > 0)
6530 musicUrl.AddOption("artistid", idArtist);
6532 Filter filter;
6533 return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true);
6536 // clang-format off
6537 typedef struct
6539 std::string fieldJSON; // Field name in JSON schema
6540 std::string formatJSON; // Format in JSON schema
6541 bool bSimple; // Fetch field directly to JSON output
6542 std::string fieldDB; // Name of field in db query
6543 std::string SQL; // SQL for scalar subqueries or field alias
6544 } translateJSONField;
6546 static const translateJSONField JSONtoDBArtist[] = {
6547 // Table and single value join fields
6548 { "artist", "string", true, "strArtist", "" }, // Label field at top
6549 { "sortname", "string", true, "strSortname", "" },
6550 { "instrument", "array", true, "strInstruments", "" },
6551 { "description", "string", true, "strBiography", "" },
6552 { "genre", "array", true, "strGenres", "" },
6553 { "mood", "array", true, "strMoods", "" },
6554 { "style", "array", true, "strStyles", "" },
6555 { "yearsactive", "array", true, "strYearsActive", "" },
6556 { "born", "string", true, "strBorn", "" },
6557 { "formed", "string", true, "strFormed", "" },
6558 { "died", "string", true, "strDied", "" },
6559 { "disbanded", "string", true, "strDisbanded", "" },
6560 { "type", "string", true, "strType", "" },
6561 { "gender", "string", true, "strGender", "" },
6562 { "disambiguation", "string", true, "strDisambiguation", "" },
6563 { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
6564 { "dateadded", "string", true, "dateAdded", "" },
6565 { "datenew", "string", true, "dateNew", "" },
6566 { "datemodified", "string", true, "dateModified", "" },
6568 // JOIN fields (multivalue), same order as _JoinToArtistFields
6569 { "", "", false, "isSong", "" },
6570 { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" },
6571 { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" },
6572 { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" },
6573 { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" },
6574 { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" },
6575 { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" },
6576 { "art", "", false, "idArt", "art.art_id AS idArt" },
6577 { "", "", false, "artType", "art.type AS artType" },
6578 { "", "", false, "artURL", "art.url AS artURL" },
6579 { "", "", false, "idRole", "song_artist.idRole" },
6580 { "roles", "", false, "strRole", "role.strRole" },
6581 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
6582 // Derived from joined tables
6583 { "isalbumartist", "bool", false, "", "" },
6584 { "thumbnail", "string", false, "", "" },
6585 { "fanart", "string", false, "", "" }
6587 Sources and genre are related via album, and so the dataset only contains source and genre
6588 pairs that exist, rather than all the genres being repeated for every source. We can not only
6589 look at genres for the first source, and genre can be out of order.
6592 // clang-format on
6594 static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField);
6596 bool CMusicDatabase::GetArtistsByWhereJSON(
6597 const std::set<std::string>& fields,
6598 const std::string& baseDir,
6599 CVariant& result,
6600 int& total,
6601 const SortDescription& sortDescription /* = SortDescription() */)
6603 if (nullptr == m_pDB)
6604 return false;
6605 if (nullptr == m_pDS)
6606 return false;
6610 total = -1;
6612 size_t resultcount = 0;
6613 Filter extFilter;
6614 CMusicDbUrl musicUrl;
6615 SortDescription sorting = sortDescription;
6616 //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
6617 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6618 return false;
6620 // Replace view names in filter with table names
6621 StringUtils::Replace(extFilter.where, "artistview", "artist");
6622 StringUtils::Replace(extFilter.where, "albumview", "album");
6624 std::string strSQLExtra;
6625 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6626 return false;
6628 // Count number of artists that satisfy selection criteria
6629 //(includes xsp limits from filter, but not sort limits)
6630 total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS);
6631 resultcount = static_cast<size_t>(total);
6633 // Process albumartistsonly option
6634 const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
6635 bool albumArtistsOnly(false);
6636 auto option = options.find("albumartistsonly");
6637 if (option != options.end())
6638 albumArtistsOnly = option->second.asBoolean();
6639 // Process role options
6640 int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles.
6641 option = options.find("roleid");
6642 if (option != options.end())
6643 roleidfilter = static_cast<int>(option->second.asInteger());
6644 else
6646 option = options.find("role");
6647 if (option != options.end())
6649 if (option->second.asString() == "all" || option->second.asString() == "%")
6650 roleidfilter = -1000; //All roles
6651 else
6652 roleidfilter = GetRoleByName(option->second.asString());
6656 // Get order by (and any scalar query artist fields)
6657 int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter);
6658 // Replace artistview field names in order by artist table field names
6659 StringUtils::Replace(extFilter.order, "artistview", "artist");
6660 StringUtils::Replace(extFilter.fields, "artistview", "artist");
6662 // Grab and adjust artist sort field that may have been added to filter
6663 // These need to be added to the end of the artist table field list
6664 std::string artistsortSQL = extFilter.fields;
6665 extFilter.fields.clear();
6667 std::string strSQL;
6669 // Setup fields to query, and album field number mapping
6670 // Find first join field (isSong) in JSONtoDBArtist for offset
6671 int index_firstjoin = -1;
6672 for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++)
6674 if (JSONtoDBArtist[i].fieldDB == "isSong")
6676 index_firstjoin = i;
6677 break;
6680 Filter joinFilter;
6681 Filter albumArtistFilter;
6682 Filter songArtistFilter;
6683 DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount));
6684 extFilter.AppendField("artist.idArtist"); // ID "artistid" in JSON
6685 std::vector<int> dbfieldindex;
6686 // JSON "label" field is strArtist which is also output as "artist", query field once output twice
6687 extFilter.AppendField(JSONtoDBArtist[0].fieldDB);
6688 dbfieldindex.emplace_back(0); // Output "artist"
6690 // Check each optional artist db field that could be retrieved (not "artist")
6691 for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++)
6693 bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end();
6694 if (JSONtoDBArtist[i].bSimple)
6696 // Check for non-join fields in order too.
6697 // Query these in inline view (but not output) so can ref in outer order
6698 bool foundOrderby(false);
6699 if (!foundJSON)
6700 foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos;
6701 if (foundOrderby || foundJSON)
6703 // Store indexes of requested artist table and scalar subquery fields
6704 // to be output, and -1 when not output to JSON
6705 if (!foundJSON)
6706 dbfieldindex.emplace_back(-1);
6707 else
6708 dbfieldindex.emplace_back(i);
6709 // Field from scaler subquery
6710 if (!JSONtoDBArtist[i].SQL.empty())
6711 extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL));
6712 else
6713 // Field from artist table
6714 extFilter.AppendField(JSONtoDBArtist[i].fieldDB);
6717 else if (foundJSON)
6718 // Field from join or derived from joined fields
6719 joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true);
6722 // Append calculated artistsort field that may have been added to filter
6723 // Field used only for ORDER BY, not output to JSON
6724 extFilter.AppendField(artistsortSQL);
6725 for (int i = 0; i < iAddedFields; i++)
6726 dbfieldindex.emplace_back(-2); // columns in dataset
6728 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6729 strSQLExtra = "";
6730 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6731 return false;
6733 // Add any LIMIT clause to strSQLExtra
6734 if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
6736 strSQLExtra +=
6737 DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
6738 resultcount = std::min(
6739 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
6740 resultcount);
6743 // Setup multivalue JOINs, GROUP BY and ORDER BY
6744 bool bJoinAlbumArtist(false);
6745 bool bJoinSongArtist(false);
6746 if (sortDescription.sortBy != SortByRandom)
6748 // Repeat inline view order (that always includes idArtist) on join query
6749 std::string order = extFilter.order;
6750 StringUtils::Replace(order, "artist.", "a1.");
6751 joinFilter.AppendOrder(order);
6753 else
6754 joinFilter.AppendOrder("a1.idArtist");
6755 joinFilter.AppendGroup("a1.idArtist");
6756 // Album artists and song artists
6757 if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) ||
6758 joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6759 joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) ||
6760 joinLayout.GetFetch(joinToArtist_strRole))
6762 bJoinAlbumArtist = true;
6763 albumArtistFilter.AppendField("album_artist.idArtist AS id");
6764 if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole))
6766 bJoinSongArtist = true;
6767 songArtistFilter.AppendField("song_artist.idArtist AS id");
6768 songArtistFilter.AppendField("1 AS isSong");
6769 albumArtistFilter.AppendField("0 AS isSong");
6770 joinLayout.SetField(joinToArtist_isSong,
6771 JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6772 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6773 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6776 else if (joinLayout.GetFetch(joinToArtist_isalbumartist))
6778 // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
6779 // so no need for join to album_artist table. Set fetching fetch false so that
6780 // joinLayout.HasFilterFields() is false
6781 joinLayout.SetFetch(joinToArtist_isalbumartist, false);
6784 // Sources
6785 if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6786 { // Left join as source may have been removed but leaving lib entries
6787 albumArtistFilter.AppendJoin(
6788 "LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
6789 albumArtistFilter.AppendField(
6790 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL);
6791 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6792 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6793 if (bJoinSongArtist)
6795 songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
6796 songArtistFilter.AppendJoin(
6797 "LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
6798 songArtistFilter.AppendField(
6799 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6800 songArtistFilter.AppendField(
6801 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL);
6802 albumArtistFilter.AppendField(
6803 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6804 joinLayout.SetField(joinToArtist_idSourceSong,
6805 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6806 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6807 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6809 else
6811 joinLayout.SetField(joinToArtist_idSourceAlbum,
6812 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true);
6816 // Songgenres - id and genres always both
6817 if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
6818 { // All albums have songs, but left join genre as songs may not have genre
6819 albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
6820 albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
6821 albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6822 albumArtistFilter.AppendField(
6823 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL);
6824 albumArtistFilter.AppendField(
6825 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6826 joinLayout.SetField(joinToArtist_strSongGenreAlbum,
6827 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6828 joinFilter.AppendGroup(
6829 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6830 joinFilter.AppendOrder(
6831 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6832 if (bJoinSongArtist)
6833 { // Left join genre as songs may not have genre
6834 songArtistFilter.AppendJoin(
6835 "LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
6836 songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6837 songArtistFilter.AppendField(
6838 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6839 songArtistFilter.AppendField(
6840 "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6841 songArtistFilter.AppendField(
6842 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL);
6843 songArtistFilter.AppendField(
6844 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL);
6845 albumArtistFilter.AppendField(
6846 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6847 albumArtistFilter.AppendField(
6848 "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6849 joinLayout.SetField(joinToArtist_idSongGenreSong,
6850 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6851 joinLayout.SetField(
6852 joinToArtist_strSongGenreSong,
6853 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6854 joinFilter.AppendGroup(
6855 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6856 joinFilter.AppendOrder(
6857 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6859 else
6860 { // Define field alias names in join layout
6861 joinLayout.SetField(joinToArtist_idSongGenreAlbum,
6862 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL,
6863 true);
6864 joinLayout.SetField(joinToArtist_strSongGenreAlbum,
6865 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6869 // Roles
6870 if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole))
6871 // Only looking at album and song artists not other roles (default),
6872 // so filter dataset rows likewise.
6873 songArtistFilter.AppendWhere("song_artist.idRole = 1");
6874 else if (joinLayout.GetFetch(joinToArtist_strRole) || // "roles" field
6875 (bJoinSongArtist && (joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6876 joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))))
6877 { // Rows from many roles so fetch roleid for "roles", source and genre processing
6878 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL);
6879 // Add fake column to album_artist query
6880 albumArtistFilter.AppendField("-1 AS " +
6881 JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6882 joinLayout.SetField(joinToArtist_idRole,
6883 JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6884 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6885 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6887 if (joinLayout.GetFetch(joinToArtist_strRole))
6888 { // Fetch role desc
6889 songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
6890 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL);
6891 // Add fake column to album_artist query
6892 albumArtistFilter.AppendField("'albumartist' AS " +
6893 JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB);
6896 // Build source, genre and roles part of query
6897 if (bJoinAlbumArtist)
6899 if (bJoinSongArtist)
6901 // Combine song and album artist filter as UNION and add to join filter as an inline view
6902 std::string strAlbumSQL;
6903 if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL))
6904 return false;
6905 strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL;
6906 std::string strSongSQL;
6907 if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL))
6908 return false;
6909 strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL;
6911 joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL +
6912 ") AS albumSong ON id = a1.idArtist");
6914 else
6915 { //Only join album_artist, so move filter elements to join filter
6916 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
6917 joinFilter.AppendJoin(albumArtistFilter.join);
6921 //Art
6922 bool bJoinArt(false);
6923 bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) ||
6924 joinLayout.GetOutput(joinToArtist_thumbnail) ||
6925 joinLayout.GetOutput(joinToArtist_fanart);
6926 if (bJoinArt)
6927 { // Left join as artist may not have any art
6928 joinFilter.AppendJoin(
6929 "LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
6930 joinLayout.SetField(joinToArtist_idArt,
6931 JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL,
6932 joinLayout.GetOutput(joinToArtist_idArt));
6933 joinLayout.SetField(joinToArtist_artType,
6934 JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL);
6935 joinLayout.SetField(joinToArtist_artURL,
6936 JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL);
6937 joinFilter.AppendGroup("art.art_id");
6938 joinFilter.AppendOrder("arttype");
6939 if (!joinLayout.GetOutput(joinToArtist_idArt))
6941 if (!joinLayout.GetOutput(joinToArtist_thumbnail))
6942 // Fanart only
6943 joinFilter.AppendJoin("AND art.type = 'fanart'");
6944 else if (!joinLayout.GetOutput(joinToArtist_fanart))
6945 // Thumb only
6946 joinFilter.AppendJoin("AND art.type = 'thumb'");
6949 else if (bJoinSongArtist)
6950 joinFilter.group.clear(); // UNION only so no GROUP BY needed
6952 // Build JOIN part of query (if we have one)
6953 std::string strSQLJoin;
6954 if (joinLayout.HasFilterFields())
6955 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6956 return false;
6958 // Adjust where in the results record the join fields are allowing for the
6959 // inline view fields (Quicker than finding field by name every time)
6960 // idArtist + other artist fields
6961 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
6963 // Build full query
6964 // When have multiple value joins e.g. song genres, use inline view
6965 // SELECT a1.*, <join fields> FROM
6966 // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1
6967 // <joins> <group by> <order by> + <joins order by>
6968 // Don't use prepareSQL - confuses arttype = 'thumb' filter
6970 strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra;
6971 if (joinLayout.HasFilterFields())
6973 strSQL = "(" + strSQL + ") AS a1 ";
6974 strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
6977 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
6978 // run query
6979 auto start = std::chrono::steady_clock::now();
6981 if (!m_pDS->query(strSQL))
6982 return false;
6984 auto end = std::chrono::steady_clock::now();
6985 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
6987 CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
6989 int iRowsFound = m_pDS->num_rows();
6990 if (iRowsFound <= 0)
6992 m_pDS->close();
6993 return true;
6996 // Get artists from returned rows. Joins means there can be many rows per artist
6997 int artistId = -1;
6998 int sourceId = -1;
6999 int genreId = -1;
7000 int roleId = -1;
7001 int artId = -1;
7002 std::vector<int> genreidlist;
7003 std::vector<int> sourceidlist;
7004 std::vector<int> roleidlist;
7005 bool bArtDone(false);
7006 bool bHaveArtist(false);
7007 bool bIsAlbumArtist(true);
7008 bool bGenreFoundViaAlbum(false);
7009 CVariant artistObj;
7010 result["artists"].reserve(resultcount);
7011 while (!m_pDS->eof() || bHaveArtist)
7013 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
7015 if (m_pDS->eof() || artistId != record->at(0).get_asInt())
7017 // Store previous or last artist
7018 if (bHaveArtist)
7020 // Convert any empty MBid array into an array with one empty element [""]
7021 // to match the number of artist ID (way other mbid arrays handled)
7022 if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty())
7023 artistObj["musicbrainzartistid"].append("");
7025 result["artists"].append(artistObj);
7026 bHaveArtist = false;
7027 artistObj.clear();
7029 if (artistObj.empty())
7031 // Initialise fields, ensure those with possible null values are set to correct empty variant type
7032 if (joinLayout.GetOutput(joinToArtist_idSourceAlbum))
7033 artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
7034 if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum))
7035 artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
7036 if (joinLayout.GetOutput(joinToArtist_idArt))
7037 artistObj["art"] = CVariant(CVariant::VariantTypeObject);
7038 if (joinLayout.GetOutput(joinToArtist_thumbnail))
7039 artistObj["thumbnail"] = "";
7040 if (joinLayout.GetOutput(joinToArtist_fanart))
7041 artistObj["fanart"] = "";
7043 sourceId = -1;
7044 roleId = -1;
7045 genreId = -1;
7046 artId = -1;
7047 genreidlist.clear();
7048 bGenreFoundViaAlbum = false;
7049 sourceidlist.clear();
7050 roleidlist.clear();
7051 bArtDone = false;
7053 if (m_pDS->eof())
7054 continue; // Having saved the last artist stop
7056 // New artist
7057 artistId = record->at(0).get_asInt();
7058 bHaveArtist = true;
7059 artistObj["artistid"] = artistId;
7060 artistObj["label"] = record->at(1).get_asString();
7061 artistObj["artist"] = record->at(1).get_asString(); // Always have "artist"
7062 bIsAlbumArtist = true; //Album artist by default
7063 if (joinLayout.GetOutput(joinToArtist_isalbumartist))
7065 // Not album artist when fetching song artists too and first row for artist isSong=true
7066 if (bJoinSongArtist)
7067 bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
7068 artistObj["isalbumartist"] = bIsAlbumArtist;
7070 for (size_t i = 0; i < dbfieldindex.size(); i++)
7071 if (dbfieldindex[i] > -1)
7073 if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer")
7074 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7075 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float")
7076 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
7077 record->at(1 + i).get_asFloat();
7078 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array")
7079 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
7080 record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
7081 ->GetAdvancedSettings()
7082 ->m_musicItemSeparator);
7083 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean")
7084 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7085 else
7086 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
7087 record->at(1 + i).get_asString();
7090 if (bJoinAlbumArtist)
7092 bool bAlbumArtistRow(true);
7093 int idRoleRow = -1;
7094 if (bJoinSongArtist)
7096 bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
7097 if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 &&
7098 !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull())
7100 idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt();
7104 // Sources - gathered via both album_artist and song_artist (with role = 1)
7105 if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
7107 if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 &&
7108 !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() &&
7109 sourceId !=
7110 record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) ||
7111 (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 &&
7112 !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() &&
7113 sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt()))
7115 bArtDone = bArtDone || (sourceId > 0); // Not first source, skip art repeats
7116 bool found(false);
7117 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt();
7118 if (!bAlbumArtistRow)
7120 // Skip other roles (when fetching them)
7121 if (idRoleRow > 1)
7123 found = true;
7125 else
7127 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt();
7128 // Song artist row may repeat sources found via album artist
7129 // Already have that source?
7130 for (const auto& i : sourceidlist)
7131 if (i == sourceId)
7133 found = true;
7134 break;
7138 if (!found)
7140 sourceidlist.emplace_back(sourceId);
7141 artistObj["sourceid"].append(sourceId);
7145 // Songgenres - via album artist takes precedence
7147 Sources and genre are related via album, and so the dataset only contains source
7148 and genre pairs that exist, rather than all the genres being repeated for every
7149 source. We can not only look at genres for the first source, and genre can be
7150 found out of order.
7151 Also song artist row may repeat genres found via album artist
7153 if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
7155 std::string strGenre;
7156 bool newgenre(false);
7157 if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 &&
7158 !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() &&
7159 genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt())
7161 bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
7162 newgenre = true;
7163 genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt();
7164 strGenre =
7165 record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString();
7167 else if (!bAlbumArtistRow && !bGenreFoundViaAlbum &&
7168 joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 &&
7169 !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() &&
7170 genreId !=
7171 record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt())
7173 bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
7174 newgenre = idRoleRow <= 1; // Skip other roles (when fetching them)
7175 genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt();
7176 strGenre =
7177 record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString();
7179 if (newgenre)
7181 // Already have that genre?
7182 bool found(false);
7183 for (const auto& i : genreidlist)
7184 if (i == genreId)
7186 found = true;
7187 break;
7189 if (!found)
7191 bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow;
7192 genreidlist.emplace_back(genreId);
7193 CVariant genreObj;
7194 genreObj["genreid"] = genreId;
7195 genreObj["title"] = strGenre;
7196 artistObj["songgenres"].append(genreObj);
7200 // Roles - gathered via song_artist roleid rows
7201 if (joinLayout.GetFetch(joinToArtist_idRole))
7203 if (!bAlbumArtistRow && roleId != idRoleRow)
7205 bArtDone = bArtDone || (roleId > 0); // Not first role, skip art repeats
7206 roleId = idRoleRow;
7207 if (joinLayout.GetOutput(joinToArtist_strRole))
7209 // Already have that role?
7210 bool found(false);
7211 for (const auto& i : roleidlist)
7212 if (i == roleId)
7214 found = true;
7215 break;
7217 if (!found)
7219 roleidlist.emplace_back(roleId);
7220 CVariant roleObj;
7221 roleObj["roleid"] = roleId;
7222 roleObj["role"] =
7223 record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString();
7224 artistObj["roles"].append(roleObj);
7230 // Art
7231 if (bJoinArt && !bArtDone &&
7232 !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() &&
7233 record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 &&
7234 artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt())
7236 artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt();
7237 if (joinLayout.GetOutput(joinToArtist_idArt))
7239 artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] =
7240 CTextureUtils::GetWrappedImageURL(
7241 record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
7243 if (joinLayout.GetOutput(joinToArtist_thumbnail) &&
7244 record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb")
7246 artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL(
7247 record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
7249 if (joinLayout.GetOutput(joinToArtist_fanart) &&
7250 record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart")
7252 artistObj["fanart"] = CTextureUtils::GetWrappedImageURL(
7253 record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
7257 m_pDS->next();
7259 m_pDS->close(); // cleanup recordset data
7261 // Ensure random order of output when results set is sorted to process multi-value joins
7262 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7263 KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array());
7265 return true;
7267 catch (...)
7269 m_pDS->close();
7270 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
7272 return false;
7275 // clang-format off
7276 static const translateJSONField JSONtoDBAlbum[] = {
7277 // albumview (inc scalar subquery fields use in filter rules)
7278 { "title", "string", true, "strAlbum", "" }, // Label field at top
7279 { "description", "string", true, "strReview", "" },
7280 { "genre", "array", true, "strGenres", "" },
7281 { "theme", "array", true, "strThemes", "" },
7282 { "mood", "array", true, "strMoods", "" },
7283 { "style", "array", true, "strStyles", "" },
7284 { "type", "string", true, "strType", "" },
7285 { "albumlabel", "string", true, "strLabel", "" },
7286 { "rating", "float", true, "fRating", "" },
7287 { "votes", "integer", true, "iVotes", "" },
7288 { "userrating", "unsigned", true, "iUserrating", "" },
7289 { "isboxset", "boolean", true, "bBoxedSet", "" },
7290 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" },
7291 { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table
7292 { "compilation", "boolean", true, "bCompilation", "" },
7293 { "releasetype", "string", true, "strReleaseType", "" },
7294 { "totaldiscs", "integer", true, "iDiscTotal", "" },
7295 { "sortartist", "string", true, "strArtistSort", "" },
7296 { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" },
7297 { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view
7298 { "dateadded", "string", true, "dateAdded", "" },
7299 { "datenew", "string", true, "dateNew", "" },
7300 { "datemodified", "string", true, "dateModified", "" },
7301 { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view
7302 { "originaldate", "string", true, "strOrigReleaseDate", "" },
7303 { "releasedate", "string", true, "strReleaseDate", "" },
7304 { "albumstatus", "string", true, "strReleaseStatus", "" },
7305 { "albumduration", "integer", true, "iAlbumDuration", "" },
7306 // Scalar subquery fields
7307 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7308 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
7309 { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
7310 "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
7311 // Single value JOIN fields
7312 { "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
7313 // JOIN fields (multivalue), same order as _JoinToAlbumFields
7314 { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" },
7315 { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" },
7316 { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" },
7318 Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
7319 and separate queries to allow for fallback strategy.
7321 Using albmview, rather than album table, as view has scalar subqueries for
7322 playcount and lastplayed already defined. Needed as MySQL does
7323 not support use of scalar subquery field alias names in where clauses (they
7324 have to be repeated) and these fields can be used by filter rules.
7325 Using this view is no slower than the album table as these scalar fields are
7326 only calculated (slowing query) when field is in field list.
7329 // clang-format on
7331 static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField);
7333 bool CMusicDatabase::GetAlbumsByWhereJSON(
7334 const std::set<std::string>& fields,
7335 const std::string& baseDir,
7336 CVariant& result,
7337 int& total,
7338 const SortDescription& sortDescription /* = SortDescription() */)
7341 if (nullptr == m_pDB)
7342 return false;
7343 if (nullptr == m_pDS)
7344 return false;
7348 total = -1;
7350 size_t resultcount = 0;
7351 Filter extFilter;
7352 CMusicDbUrl musicUrl;
7353 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7354 // passed in at the start of the function
7355 SortDescription sorting = sortDescription;
7356 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
7357 return false;
7359 // Replace view names in filter with table names
7360 StringUtils::Replace(extFilter.where, "artistview", "artist");
7362 std::string strSQLExtra;
7363 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7364 return false;
7366 // Count number of albums that satisfy selection criteria
7367 // (includes xsp limits from filter, but not sort limits)
7368 // Use albumview as filter rules in where clause may use scalar query fields
7369 total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
7370 resultcount = static_cast<size_t>(total);
7372 // Get order by (and any scalar query artist fields
7373 int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter);
7375 // Grab calculated artist/title sort fields that may have been added to filter
7376 // These need to be added to the end of the album table field list
7377 std::string calcsortfieldsSQL = extFilter.fields;
7378 extFilter.fields.clear();
7380 std::string strSQL;
7382 // Setup fields to query, and album field number mapping
7383 // Find idArtist in JSONtoDBAlbum, offset of first join field
7384 int index_idArtist = -1;
7385 for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++)
7387 if (JSONtoDBAlbum[i].fieldDB == "idArtist")
7389 index_idArtist = i;
7390 break;
7393 Filter joinFilter;
7394 DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount));
7395 extFilter.AppendField("albumview.idAlbum"); // ID "albumid" in JSON
7396 std::vector<int> dbfieldindex;
7397 // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
7398 extFilter.AppendField(JSONtoDBAlbum[0].fieldDB);
7399 if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end())
7400 dbfieldindex.emplace_back(0); // Output "title"
7401 else
7402 dbfieldindex.emplace_back(-1); // fetch but not output
7404 // Check each optional album db field that could be retrieved (not label)
7405 for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++)
7407 bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end();
7408 if (JSONtoDBAlbum[i].bSimple)
7410 // Check for non-join fields in order too.
7411 // Query these in inline view (but not output) so can ref in outer order
7412 bool foundOrderby(false);
7413 if (!foundJSON)
7414 foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos;
7415 if (foundOrderby || foundJSON)
7417 // Store indexes of requested album table and scalar subquery fields
7418 // to be output, and -1 when not output to JSON
7419 if (!foundJSON)
7420 dbfieldindex.emplace_back(-1);
7421 else
7422 dbfieldindex.emplace_back(i);
7423 if (!JSONtoDBAlbum[i].SQL.empty())
7424 // Field from scaler subquery
7425 extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL));
7426 else
7427 // Field from album table
7428 extFilter.AppendField(JSONtoDBAlbum[i].fieldDB);
7431 else if (foundJSON)
7432 // Field from join found in JSON request
7433 joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true);
7436 // Append calculated artist/title sort fields that may have been added to filter
7437 // Field used only for ORDER BY, not output to JSON
7438 extFilter.AppendField(calcsortfieldsSQL);
7439 for (int i = 0; i < iAddedFields; i++)
7440 dbfieldindex.emplace_back(-1); // columns in dataset
7442 // JOIN art tables if needed (fields output and/or in sort)
7443 if (extFilter.fields.find("art.") != std::string::npos)
7444 { // Left join as not all albums have art, but only have one thumb at most
7445 extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
7446 "AND art.media_type = 'album' AND art.type = 'thumb'");
7449 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7450 strSQLExtra = "";
7451 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7452 return false;
7454 // Add any LIMIT clause to strSQLExtra
7455 if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
7457 strSQLExtra +=
7458 DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
7459 resultcount = std::min(
7460 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
7461 resultcount);
7464 // Setup multivalue JOINs, GROUP BY and ORDER BY
7465 bool bJoinAlbumArtist(false);
7466 if (sortDescription.sortBy != SortByRandom)
7468 // Repeat inline view order (that always includes idAlbum) on join query
7469 std::string order = extFilter.order;
7470 StringUtils::Replace(order, "albumview.", "a1.");
7471 joinFilter.AppendOrder(order);
7473 else
7474 joinFilter.AppendOrder("a1.idAlbum");
7475 joinFilter.AppendGroup("a1.idAlbum");
7476 // Album artists
7477 if (joinLayout.GetFetch(joinToAlbum_idArtist) || joinLayout.GetFetch(joinToAlbum_strArtist) ||
7478 joinLayout.GetFetch(joinToAlbum_strArtistMBID))
7479 { // All albums have at least one artist so inner join sufficient
7480 bJoinAlbumArtist = true;
7481 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
7482 joinFilter.AppendGroup("album_artist.idArtist");
7483 joinFilter.AppendOrder("album_artist.iOrder");
7484 // Ensure idArtist is queried
7485 if (!joinLayout.GetFetch(joinToAlbum_idArtist))
7486 joinLayout.SetField(joinToAlbum_idArtist,
7487 JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL);
7489 // artist table needed for strArtist or MBID
7490 // (album_artist.strArtist can be an alias or spelling variation)
7491 if (joinLayout.GetFetch(joinToAlbum_strArtist) ||
7492 joinLayout.GetFetch(joinToAlbum_strArtistMBID))
7493 joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
7495 // Build JOIN part of query (if we have one)
7496 std::string strSQLJoin;
7497 if (joinLayout.HasFilterFields())
7498 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
7499 return false;
7501 // Adjust where in the results record the join fields are allowing for the
7502 // inline view fields (Quicker than finding field by name every time)
7503 // idAlbum + other album fields
7504 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
7506 // Build full query
7507 // When have multiple value joins (artists or song genres) use inline view
7508 // SELECT a1.*, <join fields> FROM
7509 // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1
7510 // <joins> <group by> <order by> <joins order by>
7511 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
7513 strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra;
7514 if (joinLayout.HasFilterFields())
7516 strSQL = "(" + strSQL + ") AS a1 ";
7517 strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
7520 // Modify query to use correct year field
7521 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
7522 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
7523 StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate");
7524 else
7525 StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate");
7527 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
7528 // run query
7529 auto start = std::chrono::steady_clock::now();
7531 if (!m_pDS->query(strSQL))
7532 return false;
7534 auto end = std::chrono::steady_clock::now();
7535 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
7537 CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
7539 int iRowsFound = m_pDS->num_rows();
7540 if (iRowsFound <= 0)
7542 m_pDS->close();
7543 return true;
7546 // Get albums from returned rows. Joins means there can be many rows per album
7547 int albumId = -1;
7548 int artistId = -1;
7549 CVariant albumObj;
7550 result["albums"].reserve(resultcount);
7551 while (!m_pDS->eof() || !albumObj.empty())
7553 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
7555 if (m_pDS->eof() || albumId != record->at(0).get_asInt())
7557 // Store previous or last album
7558 if (!albumObj.empty())
7560 // Split sources string into int array
7561 if (albumObj.isMember("sourceid"))
7563 std::vector<std::string> sources =
7564 StringUtils::Split(albumObj["sourceid"].asString(), ";");
7565 albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
7566 for (size_t i = 0; i < sources.size(); i++)
7567 albumObj["sourceid"].append(atoi(sources[i].c_str()));
7569 result["albums"].append(albumObj);
7570 albumObj.clear();
7571 artistId = -1;
7573 if (m_pDS->eof())
7574 continue; // Having saved last album stop
7576 // New album
7577 albumId = record->at(0).get_asInt();
7578 albumObj["albumid"] = albumId;
7579 albumObj["label"] = record->at(1).get_asString();
7580 for (size_t i = 0; i < dbfieldindex.size(); i++)
7581 if (dbfieldindex[i] > -1)
7583 if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres")
7585 // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
7586 std::vector<std::string> values =
7587 StringUtils::Split(record->at(1 + i).get_asString(), ",");
7588 if (values.size() % 2 == 0) // Must contain an even number of entries
7590 for (size_t j = 0; j + 1 < values.size(); j += 2)
7592 int idGenre = atoi(values[j].c_str());
7593 if (idGenre > 0)
7595 CVariant genreObj;
7596 genreObj["genreid"] = idGenre;
7597 genreObj["title"] = values[j + 1];
7598 albumObj["songgenres"].append(genreObj);
7602 // Ensure albums with null songgenres get empty array
7603 if (!albumObj.isMember("songgenres"))
7604 albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
7606 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer")
7607 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7608 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned")
7609 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
7610 std::max(record->at(1 + i).get_asInt(), 0);
7611 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float")
7612 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
7613 std::max(record->at(1 + i).get_asFloat(), 0.f);
7614 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array")
7615 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
7616 record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
7617 ->GetAdvancedSettings()
7618 ->m_musicItemSeparator);
7619 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean")
7620 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7621 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image")
7623 std::string url = record->at(1 + i).get_asString();
7624 if (!url.empty())
7625 url = CTextureUtils::GetWrappedImageURL(url);
7626 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url;
7628 else
7629 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
7632 if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1)
7634 if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt())
7636 artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt();
7637 if (joinLayout.GetOutput(joinToAlbum_idArtist))
7638 albumObj["artistid"].append(artistId);
7639 if (artistId == BLANKARTIST_ID)
7641 if (joinLayout.GetOutput(joinToAlbum_strArtist))
7642 albumObj["artist"].append(StringUtils::Empty);
7643 if (joinLayout.GetOutput(joinToAlbum_strArtistMBID))
7644 albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
7646 else
7648 if (joinLayout.GetOutput(joinToAlbum_strArtist) &&
7649 joinLayout.GetRecNo(joinToAlbum_strArtist) > -1)
7650 albumObj["artist"].append(
7651 record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString());
7652 if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) &&
7653 joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1)
7654 albumObj["musicbrainzalbumartistid"].append(
7655 record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString());
7659 m_pDS->next();
7661 m_pDS->close(); // cleanup recordset data
7663 // Ensure random order of output when results set is sorted to process multi-value joins
7664 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7665 KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array());
7667 return true;
7669 catch (...)
7671 m_pDS->close();
7672 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
7674 return false;
7677 // clang-format off
7678 static const translateJSONField JSONtoDBSong[] = {
7679 // table and single value join fields
7680 { "title", "string", true, "strTitle", "" }, // Label field at top
7681 { "albumid", "integer", true, "song.idAlbum", "" },
7682 { "", "", true, "song.iTrack", "" },
7683 { "displayartist", "string", true, "song.strArtistDisp", "" },
7684 { "sortartist", "string", true, "song.strArtistSort", "" },
7685 { "genre", "array", true, "song.strGenres", "" },
7686 { "duration", "integer", true, "iDuration", "" },
7687 { "comment", "string", true, "comment", "" },
7688 { "", "string", true, "strFileName", "" },
7689 { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" },
7690 { "playcount", "integer", true, "iTimesPlayed", "" },
7691 { "lastplayed", "string", true, "lastPlayed", "" },
7692 { "rating", "float", true, "rating", "" },
7693 { "votes", "integer", true, "votes", "" },
7694 { "userrating", "unsigned", true, "song.userrating", "" },
7695 { "mood", "array", true, "mood", "" },
7696 { "dateadded", "string", true, "song.dateAdded", "" },
7697 { "datenew", "string", true, "song.dateNew", "" },
7698 { "datemodified", "string", true, "song.dateModified", "" },
7699 { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" },
7700 { "", "string", true, "strPath", "path.strPath AS strPath" },
7701 { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" },
7702 { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" },
7703 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
7704 { "disctitle", "string", true, "song.strDiscSubtitle", "" },
7705 { "bpm", "integer", true, "iBPM", "" },
7706 { "originaldate", "string" , true, "song.strOrigReleaseDate","" },
7707 { "releasedate", "string" , true, "song.strReleaseDate", "" },
7708 { "bitrate", "integer", true, "iBitRate", "" },
7709 { "samplerate", "integer", true, "iSampleRate", "" },
7710 { "channels", "integer", true, "iChannels", "" },
7711 { "songvideourl", "string", true, "strVideoURL", "" },
7713 // JOIN fields (multivalue), same order as _JoinToSongFields
7714 { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" },
7715 { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" },
7716 { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
7717 { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" },
7718 { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" },
7719 { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" },
7720 { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" },
7721 { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" },
7722 { "", "", false, "idRole", "song_artist.idRole" },
7723 { "", "", false, "strRole", "role.strRole" },
7724 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
7725 { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
7726 { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" },
7728 { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" },
7729 { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" },
7730 { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" },
7731 { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" },
7732 { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" },
7734 // Scalar subquery fields
7735 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7736 { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" },
7737 { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" },
7738 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
7740 Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
7741 thumbloader and separate queries to allow for fallback strategy
7742 "lyrics"?? Can be set for an item (by addons) but not held in db so
7743 AudioLibrary.GetSongs() never fills this field despite being in schema
7745 FROM ( SELECT * FROM song
7746 JOIN album ON album.idAlbum = song.idAlbum
7747 JOIN path ON path.idPath = song.idPath) AS sv
7748 JOIN album_artist ON album_artist.idAlbum = song.idAlbum
7749 JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
7750 JOIN song_artist ON song_artist.idSong = song.idSong
7751 JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist
7752 JOIN role ON song_artist.idRole = role.idRole
7753 LEFT JOIN song_genre ON song.idSong = song_genre.idSong
7757 // clang-format on
7759 static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField);
7761 bool CMusicDatabase::GetSongsByWhereJSON(
7762 const std::set<std::string>& fields,
7763 const std::string& baseDir,
7764 CVariant& result,
7765 int& total,
7766 const SortDescription& sortDescription /* = SortDescription() */)
7769 if (nullptr == m_pDB)
7770 return false;
7771 if (nullptr == m_pDS)
7772 return false;
7776 total = -1;
7778 size_t resultcount = 0;
7779 Filter extFilter;
7780 CMusicDbUrl musicUrl;
7781 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7782 // passed into the function
7783 SortDescription sorting = sortDescription;
7784 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
7785 return false;
7787 // Replace view names in filter with table names
7788 StringUtils::Replace(extFilter.where, "artistview", "artist");
7789 StringUtils::Replace(extFilter.where, "albumview", "album");
7790 StringUtils::Replace(extFilter.where, "songview.strPath", "strPath");
7791 StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum");
7792 StringUtils::Replace(extFilter.where, "songview", "song");
7793 StringUtils::Replace(extFilter.where, "songartistview", "song_artist");
7795 // JOIN album and path tables needed by filter rules in where clause
7796 if (extFilter.where.find("album.") != std::string::npos ||
7797 extFilter.where.find("strAlbum") != std::string::npos)
7798 { // All songs have one album so inner join sufficient
7799 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7801 if (extFilter.where.find("strPath") != std::string::npos)
7802 { // All songs have one path so inner join sufficient
7803 extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7806 // Build JOINs and WHERE needed by filter for counting songs
7807 std::string strSQLExtra;
7808 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7809 return false;
7811 // Count number of songs that satisfy selection criteria
7812 // (includes xsp limits from filter, but not sort limits)
7813 total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS);
7814 resultcount = static_cast<size_t>(total);
7816 int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter);
7817 // Replace songview field names in order by with song, album path table field names
7818 // Field names in album same as song:
7819 // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
7820 StringUtils::Replace(extFilter.order, "songview.strPath", "strPath");
7821 StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum");
7822 StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation");
7823 StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp");
7824 StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp");
7825 StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort");
7826 StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType");
7827 StringUtils::Replace(extFilter.order, "songview", "song");
7828 StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort");
7829 StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp");
7830 StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum");
7831 StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle");
7833 // Grab calculated artist/title sort fields that may have been added to filter
7834 // These need to be added to the end of the song table field list
7835 std::string calcsortfieldsSQL = extFilter.fields;
7836 extFilter.fields.clear();
7838 std::string strSQL;
7840 // Setup fields to query, and song field number mapping
7841 // Find idAlbumArtist in JSONtoDBSong, offset of first join field
7842 int index_idAlbumArtist = -1;
7843 for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++)
7845 if (JSONtoDBSong[i].fieldDB == "idAlbumArtist")
7847 index_idAlbumArtist = i;
7848 break;
7851 Filter joinFilter;
7852 DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount));
7853 extFilter.AppendField("song.idSong"); // ID "songid" in JSON
7854 std::vector<int> dbfieldindex;
7855 // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
7856 extFilter.AppendField(JSONtoDBSong[0].fieldDB);
7857 if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end())
7858 dbfieldindex.emplace_back(0); // Output "title"
7859 else
7860 dbfieldindex.emplace_back(-1); // Fetch but not output
7861 std::vector<std::string> rolefieldlist;
7862 std::vector<int> roleidlist;
7863 // Check each optional db field that could be retrieved (not label)
7864 for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++)
7866 bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end();
7867 if (JSONtoDBSong[i].bSimple)
7869 // Check for non-join fields in order too.
7870 // Query these in inline view (but not output) so can ref in outer order
7871 bool foundOrderby(false);
7872 if (!foundJSON)
7873 foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos;
7874 if (foundOrderby || foundJSON)
7876 // Store indexes of requested album table and scalar subquery fields
7877 // to be output, and -1 when not output to JSON
7878 if (!foundJSON)
7879 dbfieldindex.emplace_back(-1);
7880 else
7881 dbfieldindex.emplace_back(i);
7882 if (!JSONtoDBSong[i].SQL.empty())
7883 // Field from scaler subquery
7884 extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL));
7885 else
7886 // Field from song table
7887 extFilter.AppendField(JSONtoDBSong[i].fieldDB);
7890 else if (foundJSON)
7891 { // Field from join found in JSON request
7892 if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_"))
7894 joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true);
7896 else
7897 { // "contributors", "displaycomposer" etc.
7898 rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON);
7902 // Append calculated artist/title sort fields that may have been added to filter
7903 // Field used only for ORDER BY, not output to JSON
7904 extFilter.AppendField(calcsortfieldsSQL);
7905 for (int i = 0; i < iAddedFields; i++)
7906 dbfieldindex.emplace_back(-1); // columns in dataset
7908 // Build matching list of role id for "displaycomposer", "displayconductor",
7909 // "displayorchestra", "displaylyricist"
7910 if (!rolefieldlist.empty())
7912 for (const auto& name : rolefieldlist)
7914 int idRole = -1;
7915 if (StringUtils::StartsWith(name, "display"))
7916 idRole = GetRoleByName(name.substr(7));
7917 roleidlist.emplace_back(idRole);
7921 // JOIN album and path tables needed for field output and/or in sort
7922 // if not already there for filter
7923 if ((extFilter.fields.find("album.") != std::string::npos ||
7924 extFilter.fields.find("strAlbum") != std::string::npos) &&
7925 extFilter.join.find("JOIN album") == std::string::npos)
7926 { // All songs have one album so inner join sufficient
7927 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7929 if (extFilter.fields.find("path.") != std::string::npos &&
7930 extFilter.join.find("JOIN path") == std::string::npos)
7931 { // All songs have one path so inner join sufficient
7932 extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7935 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7936 strSQLExtra = "";
7937 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7938 return false;
7940 // Add any LIMIT clause to strSQLExtra
7941 if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
7943 strSQLExtra +=
7944 DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
7945 resultcount = std::min(
7946 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
7947 resultcount);
7950 // Setup multivalue JOINs, GROUP BY and ORDER BY
7951 bool bJoinSongArtist(false);
7952 bool bJoinAlbumArtist(false);
7953 bool bJoinRole(false);
7954 if (sortDescription.sortBy != SortByRandom)
7956 // Repeat inline view order (that always includes idSong) on join query
7957 std::string order = extFilter.order;
7958 order = extFilter.order;
7959 StringUtils::Replace(order, "album.", "sv.");
7960 StringUtils::Replace(order, "song.", "sv.");
7961 joinFilter.AppendOrder(order);
7963 else
7964 joinFilter.AppendOrder("sv.idSong");
7965 joinFilter.AppendGroup("sv.idSong");
7967 // Album artists
7968 if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) ||
7969 joinLayout.GetFetch(joinToSongs_strAlbumArtist) ||
7970 joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID))
7971 { // All songs have at least one album artist so inner join sufficient
7972 bJoinAlbumArtist = true;
7973 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
7974 joinFilter.AppendGroup("album_artist.idArtist");
7975 joinFilter.AppendOrder("album_artist.iOrder");
7976 // Ensure idAlbumArtist is queried for processing repeats
7977 if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist))
7979 joinLayout.SetField(joinToSongs_idAlbumArtist,
7980 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL);
7982 // Ensure song.IdAlbum is field of the inline view for join
7983 if (fields.find("albumid") == fields.end())
7985 extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
7986 dbfieldindex.emplace_back(-1);
7988 // artist table needed for strArtist or MBID
7989 // (album_artist.strArtist can be an alias or spelling variation)
7990 if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) ||
7991 joinLayout.GetFetch(joinToSongs_strAlbumArtist))
7992 joinFilter.AppendJoin(
7993 "JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
7997 Song artists
7998 JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
7999 "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
8001 if (joinLayout.GetFetch(joinToSongs_idArtist) || joinLayout.GetFetch(joinToSongs_strArtist) ||
8002 joinLayout.GetFetch(joinToSongs_strArtistMBID) || !rolefieldlist.empty())
8003 { // All songs have at least one artist (idRole = 1) so inner join sufficient
8004 bJoinSongArtist = true;
8005 if (rolefieldlist.empty())
8006 { // song artists only, no other roles needed
8007 joinFilter.AppendJoin(
8008 "JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
8009 joinFilter.AppendGroup("song_artist.idArtist");
8010 joinFilter.AppendOrder("song_artist.iOrder");
8012 else
8014 // Ensure idRole is queried
8015 if (!joinLayout.GetFetch(joinToSongs_idRole))
8017 joinLayout.SetField(joinToSongs_idRole,
8018 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL);
8020 // Ensure strArtist is queried
8021 if (!joinLayout.GetFetch(joinToSongs_strArtist))
8023 joinLayout.SetField(joinToSongs_strArtist,
8024 JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL);
8026 if (fields.find("contributors") != fields.end())
8027 { // all roles
8028 bJoinRole = true;
8029 // Ensure strRole is queried from role table
8030 joinLayout.SetField(joinToSongs_strRole, "role.strRole");
8031 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
8032 joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
8033 joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
8034 joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
8036 else
8037 { // Get just roles for "displaycomposer", "displayconductor" etc.
8038 std::string where;
8039 for (size_t i = 0; i < roleidlist.size(); i++)
8041 int idRole = roleidlist[i];
8042 if (idRole <= 1)
8043 continue;
8044 if (where.empty())
8045 // Always get song artists too (role = 1) so can do inner join
8046 where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole);
8047 else
8048 where += PrepareSQL(" OR song_artist.idRole = %i", idRole);
8050 where = " (" + where + ")";
8051 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where);
8052 joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
8053 joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
8056 // Ensure idArtist is queried for processing repeats
8057 if (!joinLayout.GetFetch(joinToSongs_idArtist))
8059 joinLayout.SetField(joinToSongs_idArtist,
8060 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL);
8062 // artist table needed for strArtist or MBID
8063 // (song_artist.strArtist can be an alias or spelling variation)
8064 if (joinLayout.GetFetch(joinToSongs_strArtistMBID) ||
8065 joinLayout.GetFetch(joinToSongs_strArtist))
8066 joinFilter.AppendJoin(
8067 "JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
8070 // Genre ids
8071 if (joinLayout.GetFetch(joinToSongs_idGenre))
8072 { // song genre ids (strGenre demormalised in song table)
8073 // Left join as songs may not have genre
8074 joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
8075 joinFilter.AppendGroup("song_genre.idGenre");
8076 joinFilter.AppendOrder("song_genre.iOrder");
8079 // Build JOIN part of query (if we have one)
8080 std::string strSQLJoin;
8081 if (joinLayout.HasFilterFields())
8082 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
8083 return false;
8085 // Adjust where in the results record the join fields are allowing for the
8086 // inline view fields (Quicker than finding field by name every time)
8087 // idSong + other song fields
8088 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
8090 // Build full query
8091 // When have multiple value joins use inline view
8092 // SELECT sv.*, <join fields> FROM
8093 // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv
8094 // <joins> <group by>
8095 // <order by> + <joins order by>
8096 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
8097 strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra;
8098 if (joinLayout.HasFilterFields())
8100 strSQL = "(" + strSQL + ") AS sv ";
8101 strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
8104 // Modify query to use correct year field
8105 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
8106 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
8107 StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate");
8108 else
8109 StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate");
8111 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
8113 // Run query
8114 auto start = std::chrono::steady_clock::now();
8116 if (!m_pDS->query(strSQL))
8117 return false;
8119 auto end = std::chrono::steady_clock::now();
8120 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
8122 CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
8124 int iRowsFound = m_pDS->num_rows();
8125 if (iRowsFound <= 0)
8127 m_pDS->close();
8128 return true;
8131 // Get song from returned rows. Joins mean there can be many rows per song
8132 int songId = -1;
8133 int albumartistId = -1;
8134 int artistId = -1;
8135 int roleId = -1;
8136 bool bSongGenreDone(false);
8137 bool bSongArtistDone(false);
8138 bool bHaveSong(false);
8139 CVariant songObj;
8140 result["songs"].reserve(resultcount);
8141 while (!m_pDS->eof() || bHaveSong)
8143 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
8145 if (m_pDS->eof() || songId != record->at(0).get_asInt())
8147 // Store previous or last song
8148 if (bHaveSong)
8150 // Check empty role fields get returned, and format
8151 if (!rolefieldlist.empty())
8153 for (const auto& displayXXX : rolefieldlist)
8155 if (!StringUtils::StartsWith(displayXXX, "display"))
8157 // "contributors"
8158 if (!songObj.isMember(displayXXX))
8159 songObj[displayXXX] = CVariant(CVariant::VariantTypeArray);
8161 else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray())
8163 // Convert "displaycomposer", "displayconductor", "displayorchestra",
8164 // and "displaylyricist" arrays into strings
8165 std::vector<std::string> names;
8166 for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array();
8167 field != songObj[displayXXX].end_array(); ++field)
8168 names.emplace_back(field->asString());
8170 std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent()
8171 ->GetAdvancedSettings()
8172 ->m_musicItemSeparator);
8173 songObj[displayXXX] = role;
8175 else
8176 songObj[displayXXX] = "";
8179 result["songs"].append(songObj);
8180 bHaveSong = false;
8181 songObj.clear();
8183 if (songObj.empty())
8185 // Initialise fields, ensure those with possible null values are set to correct empty variant type
8186 if (joinLayout.GetOutput(joinToSongs_idGenre))
8187 songObj["genreid"] =
8188 CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array
8190 albumartistId = -1;
8191 artistId = -1;
8192 roleId = -1;
8193 bSongGenreDone = false;
8194 bSongArtistDone = false;
8196 if (m_pDS->eof())
8197 continue; // Having saved the last song stop
8199 // New song
8200 songId = record->at(0).get_asInt();
8201 bHaveSong = true;
8202 songObj["songid"] = songId;
8203 songObj["label"] = record->at(1).get_asString();
8204 for (size_t i = 0; i < dbfieldindex.size(); i++)
8205 if (dbfieldindex[i] > -1)
8207 if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer")
8208 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
8209 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned")
8210 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
8211 std::max(record->at(1 + i).get_asInt(), 0);
8212 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float")
8213 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
8214 std::max(record->at(1 + i).get_asFloat(), 0.f);
8215 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array")
8216 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
8217 record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
8218 ->GetAdvancedSettings()
8219 ->m_musicItemSeparator);
8220 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean")
8221 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
8222 else
8223 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
8226 // Split sources string into int array
8227 if (songObj.isMember("sourceid"))
8229 std::vector<std::string> sources =
8230 StringUtils::Split(songObj["sourceid"].asString(), ";");
8231 songObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
8232 for (size_t i = 0; i < sources.size(); i++)
8233 songObj["sourceid"].append(atoi(sources[i].c_str()));
8237 if (bJoinAlbumArtist)
8239 if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt())
8241 bSongGenreDone =
8242 bSongGenreDone || (albumartistId > 0); // Not first album artist, skip genre
8243 bSongArtistDone =
8244 bSongArtistDone || (albumartistId > 0); // Not first album artist, skip song artists
8245 albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt();
8246 if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
8247 songObj["albumartistid"].append(albumartistId);
8248 if (albumartistId == BLANKARTIST_ID)
8250 if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
8251 songObj["albumartist"].append(StringUtils::Empty);
8252 if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
8253 songObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
8255 else
8257 if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
8258 songObj["albumartistid"].append(albumartistId);
8259 if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
8260 songObj["albumartist"].append(
8261 record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString());
8262 if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
8263 songObj["musicbrainzalbumartistid"].append(
8264 record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString());
8268 if (bJoinSongArtist && !bSongArtistDone)
8270 if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt())
8272 bSongGenreDone = bSongGenreDone || (artistId > 0); // Not first artist, skip genre
8273 roleId = -1; // Allow for many artists same role
8274 artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
8275 if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 ||
8276 record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1)
8278 if (joinLayout.GetOutput(joinToSongs_idArtist))
8279 songObj["artistid"].append(artistId);
8280 if (artistId == BLANKARTIST_ID)
8282 if (joinLayout.GetOutput(joinToSongs_strArtist))
8283 songObj["artist"].append(StringUtils::Empty);
8284 if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
8285 songObj["musicbrainzartistid"].append(StringUtils::Empty);
8287 else
8289 if (joinLayout.GetOutput(joinToSongs_strArtist))
8290 songObj["artist"].append(
8291 record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
8292 if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
8293 songObj["musicbrainzartistid"].append(
8294 record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString());
8298 if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 &&
8299 roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt())
8301 bSongGenreDone = bSongGenreDone || (roleId > 0); // Not first role, skip genre
8302 roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt();
8303 if (roleId > 1)
8305 if (bJoinRole)
8306 { //Contributors
8307 CVariant contributor;
8308 contributor["name"] =
8309 record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString();
8310 contributor["role"] =
8311 record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString();
8312 contributor["roleid"] = roleId;
8313 contributor["artistid"] =
8314 record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
8315 songObj["contributors"].append(contributor);
8317 // "displaycomposer", "displayconductor" etc.
8318 for (size_t i = 0; i < roleidlist.size(); i++)
8320 if (roleidlist[i] == roleId)
8322 songObj[rolefieldlist[i]].append(
8323 record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
8324 continue;
8330 if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 &&
8331 !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull())
8333 songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt());
8335 m_pDS->next();
8337 m_pDS->close(); // cleanup recordset data
8339 // Ensure random order of output when results set is sorted to process multi-value joins
8340 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
8341 KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array());
8343 return true;
8345 catch (...)
8347 m_pDS->close();
8348 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
8350 return false;
8353 std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField)
8356 Make SQL clause from ignore article list.
8357 Group tokens the same length together, for example :
8358 WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
8359 THEN SUBSTR(strArtist, 5)
8360 WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
8361 THEN SUBSTR(strArtist, 4)
8363 std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
8364 std::string sortclause;
8365 size_t tokenlength = 0;
8366 std::string strWhen;
8367 for (const auto& token : sortTokens)
8369 if (token.length() != tokenlength)
8371 if (!strWhen.empty())
8373 if (!sortclause.empty())
8374 sortclause += " ";
8375 std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
8376 sortclause += "WHEN " + strWhen + strThen;
8377 strWhen.clear();
8379 tokenlength = token.length();
8381 std::string tokenclause = token;
8382 //Escape any ' or % in the token
8383 StringUtils::Replace(tokenclause, "'", "''");
8384 StringUtils::Replace(tokenclause, "%", "%%");
8385 // Single %, _ and ' so avoid using PrepareSQL
8386 tokenclause = strField + " LIKE '" + tokenclause + "%'";
8387 if (token.find('_') != std::string::npos)
8388 tokenclause += " ESCAPE '_'";
8389 if (!strWhen.empty())
8390 strWhen += " OR ";
8391 strWhen += tokenclause;
8393 if (!strWhen.empty())
8395 if (!sortclause.empty())
8396 sortclause += " ";
8397 std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
8398 sortclause += "WHEN " + strWhen + strThen;
8400 return sortclause;
8403 std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias,
8404 const SortAttribute& sortAttributes,
8405 const std::string& strField,
8406 const std::string& strSortField)
8409 Build SQL for sort name scalar subquery from sort attributes and ignore article list.
8410 For example :
8411 CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
8412 WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
8413 WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
8414 ELSE strField
8415 END AS strAlias
8418 std::string sortSQL;
8419 if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName)
8420 sortSQL =
8421 PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str());
8422 if (sortAttributes & SortAttributeIgnoreArticle)
8424 if (!sortSQL.empty())
8425 sortSQL += " ";
8426 // Make SQL from ignore article list, grouping tokens the same length together
8427 sortSQL += GetIgnoreArticleSQL(strField);
8429 if (!sortSQL.empty())
8431 sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc.
8432 sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str());
8435 return sortSQL;
8438 std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField,
8439 const SortOrder& sortOrder)
8442 Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
8443 and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
8444 Natural number order is not significant for where clause comparison and use of calculated fields
8445 means there is no advantage in defining as column default in table create than per query (which
8446 also makes looking at the db with other tools difficult).
8448 MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
8449 "ascii folding" case insensitive collation. Natural sorting is provided via native functions
8450 stored in the db.
8452 std::string DESC;
8453 if (sortOrder == SortOrderDescending)
8454 DESC = " DESC";
8455 std::string strSort;
8457 if (StringUtils::EqualsNoCase(
8458 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
8459 "mysql"))
8460 strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str());
8461 else
8462 strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str());
8463 return strSort;
8466 void CMusicDatabase::UpdateTables(int version)
8468 CLog::Log(LOGINFO, "{} - updating tables", __FUNCTION__);
8469 if (version < 34)
8471 m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
8472 m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
8473 m_pDS->exec(
8474 "CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, "
8475 "strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration "
8476 "integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, "
8477 "iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, "
8478 "lastplayed varchar(20) default NULL, rating char default '0', comment text)\n");
8479 m_pDS->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, "
8480 "iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, "
8481 "iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) "
8482 "SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, "
8483 "dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, "
8484 "iEndOffset, idThumb, lastplayed, rating, comment FROM song");
8486 m_pDS->exec("DROP TABLE song");
8487 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8489 m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
8492 if (version < 36)
8494 // translate legacy musicdb:// paths
8495 if (m_pDS->query("SELECT strPath FROM content"))
8497 std::vector<std::string> contentPaths;
8498 while (!m_pDS->eof())
8500 contentPaths.push_back(m_pDS->fv(0).get_asString());
8501 m_pDS->next();
8503 m_pDS->close();
8505 for (const auto& originalPath : contentPaths)
8507 std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath);
8508 m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(),
8509 originalPath.c_str()));
8514 if (version < 39)
8516 m_pDS->exec("CREATE TABLE album_new "
8517 "(idAlbum integer primary key, "
8518 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8519 " strArtists text, strGenres text, "
8520 " iYear integer, idThumb integer, "
8521 " bCompilation integer not null default '0', "
8522 " strMoods text, strStyles text, strThemes text, "
8523 " strReview text, strImage text, strLabel text, "
8524 " strType text, "
8525 " iRating integer, "
8526 " lastScraped varchar(20) default NULL, "
8527 " dateAdded varchar (20) default NULL)");
8528 m_pDS->exec("INSERT INTO album_new "
8529 "(idAlbum, "
8530 " strAlbum, strMusicBrainzAlbumID, "
8531 " strArtists, strGenres, "
8532 " iYear, idThumb, "
8533 " bCompilation, "
8534 " strMoods, strStyles, strThemes, "
8535 " strReview, strImage, strLabel, "
8536 " strType, "
8537 " iRating) "
8538 " SELECT "
8539 " album.idAlbum, "
8540 " strAlbum, strMusicBrainzAlbumID, "
8541 " strArtists, strGenres, "
8542 " album.iYear, idThumb, "
8543 " bCompilation, "
8544 " strMoods, strStyles, strThemes, "
8545 " strReview, strImage, strLabel, "
8546 " strType, iRating "
8547 " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
8548 m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE "
8549 "albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
8550 m_pDS->exec(PrepareSQL(
8551 "UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)",
8552 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
8553 m_pDS->exec("DROP TABLE album");
8554 m_pDS->exec("DROP TABLE albuminfo");
8555 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8557 if (version < 40)
8559 m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
8560 " strArtist varchar(256), strMusicBrainzArtistID text, "
8561 " strBorn text, strFormed text, strGenres text, strMoods text, "
8562 " strStyles text, strInstruments text, strBiography text, "
8563 " strDied text, strDisbanded text, strYearsActive text, "
8564 " strImage text, strFanart text, "
8565 " lastScraped varchar(20) default NULL, "
8566 " dateAdded varchar (20) default NULL)");
8567 m_pDS->exec("INSERT INTO artist_new "
8568 "(idArtist, strArtist, strMusicBrainzArtistID, "
8569 " strBorn, strFormed, strGenres, strMoods, "
8570 " strStyles , strInstruments , strBiography , "
8571 " strDied, strDisbanded, strYearsActive, "
8572 " strImage, strFanart) "
8573 " SELECT "
8574 " artist.idArtist, "
8575 " strArtist, strMusicBrainzArtistID, "
8576 " strBorn, strFormed, strGenres, strMoods, "
8577 " strStyles, strInstruments, strBiography, "
8578 " strDied, strDisbanded, strYearsActive, "
8579 " strImage, strFanart "
8580 " FROM artist "
8581 " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
8582 m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT "
8583 "idArtist FROM artistinfo)",
8584 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
8585 m_pDS->exec("DROP TABLE artist");
8586 m_pDS->exec("DROP TABLE artistinfo");
8587 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8589 if (version < 42)
8591 m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n");
8592 m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n");
8593 // populate these
8594 std::string sql = "select idArtist,strArtist from artist";
8595 m_pDS->query(sql);
8596 while (!m_pDS->eof())
8598 m_pDS2->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i",
8599 m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
8600 m_pDS2->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i",
8601 m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
8602 m_pDS->next();
8605 if (version < 48)
8606 { // null out columns that are no longer used
8607 m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
8608 m_pDS->exec("UPDATE album SET idThumb=NULL");
8610 if (version < 49)
8612 m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
8614 if (version < 50)
8616 // add a new column strReleaseType for albums
8617 m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n");
8619 // set strReleaseType based on album name
8620 m_pDS->exec(PrepareSQL(
8621 "UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''",
8622 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
8623 m_pDS->exec(
8624 PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''",
8625 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
8627 if (version < 51)
8629 m_pDS->exec("ALTER TABLE song ADD mood text\n");
8631 if (version < 53)
8633 m_pDS->exec("ALTER TABLE song ADD dateAdded text");
8635 if (version < 54)
8637 //Remove dateAdded from artist table
8638 m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
8639 " strArtist varchar(256), strMusicBrainzArtistID text, "
8640 " strBorn text, strFormed text, strGenres text, strMoods text, "
8641 " strStyles text, strInstruments text, strBiography text, "
8642 " strDied text, strDisbanded text, strYearsActive text, "
8643 " strImage text, strFanart text, "
8644 " lastScraped varchar(20) default NULL)");
8645 m_pDS->exec("INSERT INTO artist_new "
8646 "(idArtist, strArtist, strMusicBrainzArtistID, "
8647 " strBorn, strFormed, strGenres, strMoods, "
8648 " strStyles , strInstruments , strBiography , "
8649 " strDied, strDisbanded, strYearsActive, "
8650 " strImage, strFanart, lastScraped) "
8651 " SELECT "
8652 " idArtist, "
8653 " strArtist, strMusicBrainzArtistID, "
8654 " strBorn, strFormed, strGenres, strMoods, "
8655 " strStyles, strInstruments, strBiography, "
8656 " strDied, strDisbanded, strYearsActive, "
8657 " strImage, strFanart, lastScraped "
8658 " FROM artist");
8659 m_pDS->exec("DROP TABLE artist");
8660 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8662 //Remove dateAdded from album table
8663 m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8664 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8665 " strArtists text, strGenres text, "
8666 " iYear integer, idThumb integer, "
8667 " bCompilation integer not null default '0', "
8668 " strMoods text, strStyles text, strThemes text, "
8669 " strReview text, strImage text, strLabel text, "
8670 " strType text, "
8671 " iRating integer, "
8672 " lastScraped varchar(20) default NULL, "
8673 " strReleaseType text)");
8674 m_pDS->exec("INSERT INTO album_new "
8675 "(idAlbum, "
8676 " strAlbum, strMusicBrainzAlbumID, "
8677 " strArtists, strGenres, "
8678 " iYear, idThumb, "
8679 " bCompilation, "
8680 " strMoods, strStyles, strThemes, "
8681 " strReview, strImage, strLabel, "
8682 " strType, iRating, lastScraped, "
8683 " strReleaseType) "
8684 " SELECT "
8685 " album.idAlbum, "
8686 " strAlbum, strMusicBrainzAlbumID, "
8687 " strArtists, strGenres, "
8688 " iYear, idThumb, "
8689 " bCompilation, "
8690 " strMoods, strStyles, strThemes, "
8691 " strReview, strImage, strLabel, "
8692 " strType, iRating, lastScraped, "
8693 " strReleaseType"
8694 " FROM album");
8695 m_pDS->exec("DROP TABLE album");
8696 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8698 if (version < 55)
8700 m_pDS->exec("DROP TABLE karaokedata");
8702 if (version < 57)
8704 m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
8705 m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
8706 m_pDS->exec("UPDATE song SET userrating = rating * 2");
8707 m_pDS->exec("UPDATE song SET rating = 0");
8708 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8709 " idAlbum INTEGER, idPath INTEGER, "
8710 " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8711 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8712 " dwFileNameCRC TEXT, "
8713 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8714 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8715 " idThumb INTEGER, "
8716 " lastplayed VARCHAR(20) DEFAULT NULL, "
8717 " rating FLOAT DEFAULT 0, "
8718 " userrating INTEGER DEFAULT 0, "
8719 " comment TEXT, mood TEXT, dateAdded TEXT)");
8720 m_pDS->exec("INSERT INTO song_new "
8721 "(idSong, "
8722 " idAlbum, idPath, "
8723 " strArtists, strGenres, strTitle, "
8724 " iTrack, iDuration, iYear, "
8725 " dwFileNameCRC, "
8726 " strFileName, strMusicBrainzTrackID, "
8727 " iTimesPlayed, iStartOffset, iEndOffset, "
8728 " idThumb, "
8729 " lastplayed,"
8730 " rating, userrating, "
8731 " comment, mood, dateAdded)"
8732 " SELECT "
8733 " idSong, "
8734 " idAlbum, idPath, "
8735 " strArtists, strGenres, strTitle, "
8736 " iTrack, iDuration, iYear, "
8737 " dwFileNameCRC, "
8738 " strFileName, strMusicBrainzTrackID, "
8739 " iTimesPlayed, iStartOffset, iEndOffset, "
8740 " idThumb, "
8741 " lastplayed,"
8742 " rating, "
8743 " userrating, "
8744 " comment, mood, dateAdded"
8745 " FROM song");
8746 m_pDS->exec("DROP TABLE song");
8747 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8749 m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
8750 m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
8751 m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8752 " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8753 " strArtists TEXT, strGenres TEXT, "
8754 " iYear INTEGER, idThumb INTEGER, "
8755 " bCompilation INTEGER NOT NULL DEFAULT '0', "
8756 " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8757 " strReview TEXT, strImage TEXT, strLabel TEXT, "
8758 " strType TEXT, "
8759 " fRating FLOAT NOT NULL DEFAULT 0, "
8760 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8761 " lastScraped VARCHAR(20) DEFAULT NULL, "
8762 " strReleaseType TEXT)");
8763 m_pDS->exec("INSERT INTO album_new "
8764 "(idAlbum, "
8765 " strAlbum, strMusicBrainzAlbumID, "
8766 " strArtists, strGenres, "
8767 " iYear, idThumb, "
8768 " bCompilation, "
8769 " strMoods, strStyles, strThemes, "
8770 " strReview, strImage, strLabel, "
8771 " strType, "
8772 " fRating, "
8773 " iUserrating, "
8774 " lastScraped, "
8775 " strReleaseType)"
8776 " SELECT "
8777 " idAlbum, "
8778 " strAlbum, strMusicBrainzAlbumID, "
8779 " strArtists, strGenres, "
8780 " iYear, idThumb, "
8781 " bCompilation, "
8782 " strMoods, strStyles, strThemes, "
8783 " strReview, strImage, strLabel, "
8784 " strType, "
8785 " iRating, "
8786 " iUserrating, "
8787 " lastScraped, "
8788 " strReleaseType"
8789 " FROM album");
8790 m_pDS->exec("DROP TABLE album");
8791 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8793 m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
8794 m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
8796 if (version < 58)
8798 m_pDS->exec("UPDATE album SET fRating = fRating * 2");
8800 if (version < 59)
8802 m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
8803 m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
8805 //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
8806 m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, "
8807 "iOrder integer, strArtist text)");
8808 m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
8809 "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
8810 m_pDS->exec("DROP TABLE song_artist");
8811 m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
8813 //Remove strJoinPhrase, boolFeatured from album_artist table
8814 m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, "
8815 "strArtist text)");
8816 m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
8817 "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
8818 m_pDS->exec("DROP TABLE album_artist");
8819 m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
8821 if (version < 60)
8823 // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
8824 // do not have an artist tag to ensure all songs in the library have at least one artist.
8825 std::string strSQL;
8826 if (GetArtistExists(BLANKARTIST_ID))
8828 // When BLANKARTIST_ID (=1) is already in use, move the record
8830 { //No mbid index yet, so can have record for artist twice even with mbid
8831 strSQL = PrepareSQL("INSERT INTO artist SELECT null, "
8832 "strArtist, strMusicBrainzArtistID, "
8833 "strBorn, strFormed, strGenres, strMoods, "
8834 "strStyles, strInstruments, strBiography, "
8835 "strDied, strDisbanded, strYearsActive, "
8836 "strImage, strFanart, lastScraped "
8837 "FROM artist WHERE artist.idArtist = %i",
8838 BLANKARTIST_ID);
8839 m_pDS->exec(strSQL);
8840 int idArtist = (int)m_pDS->lastinsertid();
8841 //No triggers, so can delete artist without effecting other tables.
8842 strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
8843 m_pDS->exec(strSQL);
8845 // Update related tables with the new artist ID
8846 // Indices have been dropped making transactions very slow, so create appropriate temp indices
8847 m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
8848 m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
8849 m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
8850 m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
8851 strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
8852 BLANKARTIST_ID);
8853 m_pDS->exec(strSQL);
8854 strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
8855 BLANKARTIST_ID);
8856 m_pDS->exec(strSQL);
8857 strSQL =
8858 PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'",
8859 idArtist, BLANKARTIST_ID);
8860 m_pDS->exec(strSQL);
8861 strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist,
8862 BLANKARTIST_ID);
8863 m_pDS->exec(strSQL);
8864 // Drop temp indices
8865 m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist");
8866 m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
8867 m_pDS->exec("DROP INDEX idxDiscography ON discography");
8868 m_pDS->exec("DROP INDEX ix_art ON art");
8870 catch (...)
8872 CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed");
8876 // Create missing artist tag artist [Missing].
8877 // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
8878 strSQL = PrepareSQL(
8879 "INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
8880 BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
8881 m_pDS->exec(strSQL);
8883 // Indices have been dropped making transactions very slow, so create temp index
8884 m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
8885 m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
8887 // Ensure all songs have at least one artist, set those without to [Missing]
8888 strSQL = "SELECT count(idSong) FROM song "
8889 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8890 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
8891 int numsongs = GetSingleValueInt(strSQL);
8892 if (numsongs > 0)
8894 CLog::Log(LOGDEBUG, "{} songs have no artist, setting artist to [Missing]", numsongs);
8895 // Insert song_artist records for songs that don't have any
8898 strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
8899 "SELECT %i, idSong, %i, '%s', 0 FROM song "
8900 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8901 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
8902 BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST);
8903 ExecuteQuery(strSQL);
8905 catch (...)
8907 CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed");
8911 // Ensure all albums have at least one artist, set those without to [Missing]
8912 strSQL = "SELECT count(idAlbum) FROM album "
8913 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8914 "WHERE album_artist.idAlbum = album.idAlbum)";
8915 int numalbums = GetSingleValueInt(strSQL);
8916 if (numalbums > 0)
8918 CLog::Log(LOGDEBUG, "{} albums have no artist, setting artist to [Missing]", numalbums);
8919 // Insert album_artist records for albums that don't have any
8922 strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
8923 "SELECT %i, idAlbum, '%s', 0 FROM album "
8924 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8925 "WHERE album_artist.idAlbum = album.idAlbum)",
8926 BLANKARTIST_ID, BLANKARTIST_NAME.c_str());
8927 ExecuteQuery(strSQL);
8929 catch (...)
8931 CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed");
8934 //Remove temp indices, full analytics for database created later
8935 m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist");
8936 m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
8938 if (version < 61)
8940 // Create versiontagscan table
8941 m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
8942 m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
8944 if (version < 62)
8946 CLog::Log(LOGINFO, "create audiobook table");
8947 m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
8948 " strBook varchar(256), strAuthor text,"
8949 " bookmark integer, file text,"
8950 " dateAdded varchar (20) default NULL)");
8952 if (version < 63)
8954 // Add strSortName to Artist table
8955 m_pDS->exec("ALTER TABLE artist ADD strSortName text\n");
8957 //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
8958 m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8959 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8960 " strArtistDisp text, strArtistSort text, strGenres text, "
8961 " iYear integer, bCompilation integer not null default '0', "
8962 " strMoods text, strStyles text, strThemes text, "
8963 " strReview text, strImage text, strLabel text, "
8964 " strType text, "
8965 " fRating FLOAT NOT NULL DEFAULT 0, "
8966 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8967 " lastScraped varchar(20) default NULL, "
8968 " strReleaseType text, "
8969 " iVotes INTEGER NOT NULL DEFAULT 0)");
8970 m_pDS->exec("INSERT INTO album_new "
8971 "(idAlbum, "
8972 " strAlbum, strMusicBrainzAlbumID, "
8973 " strArtistDisp, strArtistSort, strGenres, "
8974 " iYear, bCompilation, "
8975 " strMoods, strStyles, strThemes, "
8976 " strReview, strImage, strLabel, "
8977 " strType, "
8978 " fRating, iUserrating, iVotes, "
8979 " lastScraped, "
8980 " strReleaseType)"
8981 " SELECT "
8982 " idAlbum, "
8983 " strAlbum, strMusicBrainzAlbumID, "
8984 " strArtists, NULL, strGenres, "
8985 " iYear, bCompilation, "
8986 " strMoods, strStyles, strThemes, "
8987 " strReview, strImage, strLabel, "
8988 " strType, "
8989 " fRating, iUserrating, iVotes, "
8990 " lastScraped, "
8991 " strReleaseType"
8992 " FROM album");
8993 m_pDS->exec("DROP TABLE album");
8994 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8996 //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
8997 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8998 " idAlbum INTEGER, idPath INTEGER, "
8999 " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
9000 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
9001 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
9002 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
9003 " lastplayed VARCHAR(20) DEFAULT NULL, "
9004 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
9005 " userrating INTEGER NOT NULL DEFAULT 0, "
9006 " comment TEXT, mood TEXT, dateAdded TEXT)");
9007 m_pDS->exec("INSERT INTO song_new "
9008 "(idSong, "
9009 " idAlbum, idPath, "
9010 " strArtistDisp, strArtistSort, strGenres, strTitle, "
9011 " iTrack, iDuration, iYear, "
9012 " strFileName, strMusicBrainzTrackID, "
9013 " iTimesPlayed, iStartOffset, iEndOffset, "
9014 " lastplayed,"
9015 " rating, userrating, votes, "
9016 " comment, mood, dateAdded)"
9017 " SELECT "
9018 " idSong, "
9019 " idAlbum, idPath, "
9020 " strArtists, NULL, strGenres, strTitle, "
9021 " iTrack, iDuration, iYear, "
9022 " strFileName, strMusicBrainzTrackID, "
9023 " iTimesPlayed, iStartOffset, iEndOffset, "
9024 " lastplayed,"
9025 " rating, userrating, votes, "
9026 " comment, mood, dateAdded"
9027 " FROM song");
9028 m_pDS->exec("DROP TABLE song");
9029 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
9031 if (version < 65)
9033 // Remove cue table
9034 m_pDS->exec("DROP TABLE cue");
9035 // Add strReplayGain to song table
9036 m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
9038 if (version < 66)
9040 // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
9041 m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
9042 m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
9043 // Add a new column bScrapedMBID for artists
9044 m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
9046 if (version < 67)
9048 // Add infosetting table
9049 m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, "
9050 "strSettings TEXT)");
9051 // Add a new column for setting to album and artist tables
9052 m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
9053 m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
9055 // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
9056 m_pDS->exec(
9057 "CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
9058 "strScraperPath text, strSettings text)");
9061 m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
9062 "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, "
9063 "strScraperPath, strSettings "
9064 "FROM content WHERE strContent = 'artists' AND strPath LIKE "
9065 "'musicdb://artists/_%/' ORDER BY idItem");
9067 catch (...)
9069 CLog::Log(LOGERROR,
9070 "Migrating specific artist scraper settings has failed, settings not transferred");
9074 m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
9075 "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, "
9076 "strScraperPath, strSettings "
9077 "FROM content WHERE strContent = 'albums' AND strPath LIKE "
9078 "'musicdb://albums/_%/' ORDER BY idItem");
9080 catch (...)
9082 CLog::Log(LOGERROR,
9083 "Migrating specific album scraper settings has failed, settings not transferred");
9087 m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
9088 "SELECT id, strScraperPath, strSettings FROM content_temp");
9089 m_pDS->exec(
9090 "UPDATE artist SET idInfoSetting = "
9091 "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
9092 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = "
9093 "idArtist) ");
9094 m_pDS->exec("UPDATE album SET idInfoSetting = "
9095 "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
9096 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem "
9097 "= idAlbum) ");
9099 catch (...)
9101 CLog::Log(LOGERROR,
9102 "Migrating album and artist scraper settings has failed, settings not transferred");
9104 m_pDS->exec("DROP TABLE content_temp");
9106 // Remove content table
9107 m_pDS->exec("DROP TABLE content");
9108 // Remove albuminfosong table
9109 m_pDS->exec("DROP TABLE albuminfosong");
9111 if (version < 68)
9113 // Add a new columns strType, strGender, strDisambiguation for artists
9114 m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n");
9115 m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n");
9116 m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
9118 if (version < 69)
9120 // Remove album_genre table
9121 m_pDS->exec("DROP TABLE album_genre");
9123 if (version < 70)
9125 // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
9126 m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * "
9127 "40 / 3 \n");
9129 if (version < 71)
9131 // Add lastscanned to versiontagscan table
9132 m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
9133 CDateTime dateAdded = CDateTime::GetCurrentDateTime();
9134 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
9135 dateAdded.GetAsDBDateTime().c_str()));
9137 if (version < 72)
9139 // Create source table
9140 m_pDS->exec(
9141 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
9142 // Create source_path table
9143 m_pDS->exec(
9144 "CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
9145 // Create album_source table
9146 m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
9147 // Populate source and source_path tables from sources.xml
9148 // Filling album_source needs to be done after indexes are created or it is
9149 // very slow. It could be populated during CreateAnalytics but it is checked
9150 // and filled as part of scanning anyway so simply force full rescan.
9151 MigrateSources();
9153 if (version < 73)
9155 // add bBoxedSet to album table
9156 m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
9157 // add iDiscTotal to album table
9158 m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
9159 // populate iDiscTotal from the data already in the song table
9160 m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
9161 "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
9162 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
9163 // add strDiscSubtitles to song table
9164 m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
9166 if (version < 74)
9168 //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
9169 m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
9170 "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
9171 "strReleaseGroupMBID TEXT, "
9172 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
9173 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
9174 "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
9175 "bCompilation INTEGER NOT NULL DEFAULT '0', "
9176 "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
9177 "strReview TEXT, strImage TEXT, strLabel TEXT, "
9178 "strType TEXT, "
9179 "fRating FLOAT NOT NULL DEFAULT 0, "
9180 "iVotes INTEGER NOT NULL DEFAULT 0, "
9181 "iUserrating INTEGER NOT NULL DEFAULT 0, "
9182 "lastScraped VARCHAR(20) DEFAULT NULL, "
9183 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
9184 "strReleaseType TEXT, "
9185 "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
9186 "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
9187 // Prepare as MySQL has different CAST datatypes
9188 m_pDS->exec(
9189 PrepareSQL("INSERT INTO album_new "
9190 "(idalbum, strAlbum, "
9191 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
9192 "strArtistDisp, strArtistSort, strGenres, "
9193 "strReleaseDate, strOrigReleaseDate, "
9194 "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
9195 "strReview, strImage, strLabel, strType, "
9196 "fRating, iVotes, iUserrating, "
9197 "lastScraped, bScrapedMBID, strReleaseType, "
9198 "iDiscTotal, idInfoSetting) "
9199 "SELECT "
9200 "idAlbum, strAlbum, "
9201 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
9202 "strArtistDisp, strArtistSort, strGenres, "
9203 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9204 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9205 // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
9206 "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
9207 "bCompilation, strMoods, strStyles, strThemes, "
9208 "strReview, strImage, strLabel, strType, "
9209 "fRating, iVotes, iUserrating, "
9210 "lastScraped, bScrapedMBID, strReleaseType, "
9211 "iDiscTotal, idInfoSetting "
9212 "FROM album"));
9213 m_pDS->exec("DROP TABLE album");
9214 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
9216 //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
9217 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
9218 "idAlbum INTEGER, idPath INTEGER, "
9219 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
9220 "iTrack INTEGER, iDuration INTEGER, "
9221 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
9222 "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
9223 "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
9224 "lastplayed VARCHAR(20) DEFAULT NULL, "
9225 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
9226 "userrating INTEGER NOT NULL DEFAULT 0, "
9227 "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
9228 "dateAdded TEXT)");
9229 // Prepare as MySQL has different CAST datatypes
9230 m_pDS->exec(PrepareSQL("INSERT INTO song_new "
9231 "(idSong, "
9232 "idAlbum, idPath, "
9233 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9234 "iTrack, iDuration, "
9235 "strReleaseDate, strOrigReleaseDate, "
9236 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9237 "iTimesPlayed, iStartOffset, iEndOffset, "
9238 "lastplayed, "
9239 "rating, userrating, votes, "
9240 "comment, mood, strReplayGain, dateAdded) "
9241 "SELECT "
9242 "idSong, "
9243 "idAlbum, idPath, "
9244 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9245 "iTrack, iDuration, "
9246 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9247 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9248 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9249 "iTimesPlayed, iStartOffset, iEndOffset, "
9250 "lastplayed, "
9251 "rating, userrating, votes, "
9252 "comment, mood, strReplayGain, dateAdded "
9253 "FROM song"));
9254 m_pDS->exec("DROP TABLE song");
9255 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
9257 if (version < 75)
9259 m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
9260 m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
9261 m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
9263 if (version < 77)
9265 m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
9267 if (version < 78)
9269 std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime();
9271 // Add removed_link table
9272 m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
9273 // Add lastcleaned and artistlinksupdated to versiontagscan table
9274 m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
9275 m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
9276 m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
9277 // Adjust lastscanned if original local time value is after current UTC
9278 if (GetLibraryLastUpdated() > strUTCNow)
9279 SetLibraryLastUpdated();
9280 m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
9281 "genresupdated = lastscanned, "
9282 "artistlinksupdated = lastscanned");
9284 // Add dateNew, dateModified to song table
9285 m_pDS->exec("ALTER TABLE song ADD dateNew TEXT");
9286 m_pDS->exec("ALTER TABLE song ADD dateModified TEXT");
9287 // Set new to dateAdded and modified to lastplayed as estimates
9288 // Limit those local time values to now UTC, and modified is after new
9289 m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
9290 m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'", strUTCNow.c_str(),
9291 strUTCNow.c_str()));
9292 m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
9293 m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
9294 strUTCNow.c_str(), strUTCNow.c_str()));
9295 m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
9297 // Add dateAdded, dateNew, dateModified to album table
9298 m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT");
9299 m_pDS->exec("ALTER TABLE album ADD dateNew TEXT");
9300 m_pDS->exec("ALTER TABLE album ADD dateModified TEXT");
9301 // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
9302 // Limit modified value to now UTC and after new
9303 // Indices have been dropped making subquery very slow, so create temp index
9304 m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
9305 m_pDS->exec("UPDATE album SET dateAdded = "
9306 "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
9307 m_pDS->exec("UPDATE album SET dateNew = "
9308 "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
9309 m_pDS->exec("UPDATE album SET dateModified = dateNew");
9310 m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
9311 m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
9312 strUTCNow.c_str(), strUTCNow.c_str()));
9313 //Remove temp index, full analytics for database created later
9314 m_pDS->exec("DROP INDEX idxSong3 ON song");
9316 // Add dateAdded, dateNew, dateModified to artist table
9317 m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT");
9318 m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT");
9319 m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT");
9320 // dateAdded has NULL values until files rescanned by user
9321 // Set new and modified to now UTC as not worth complexity of estimating from song dates
9322 m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str()));
9323 m_pDS->exec("UPDATE artist SET dateModified = dateNew");
9325 if (version < 79)
9327 m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
9329 if (version < 80)
9331 m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
9332 // update duration for all current albums
9333 m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
9334 "WHERE song.idAlbum = album.idAlbum) "
9335 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
9337 if (version < 82)
9339 // Update artist table combining fanart URL data into strImage field
9340 // Clear empty URL data <fanart /> and <thumb />
9341 m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
9342 m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
9343 //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
9344 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
9345 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
9346 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
9347 "aspect=\"fanart\" preview')");
9348 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
9349 // Truncate the fanart when total URLs exceeds this
9350 bool bisMySQL = StringUtils::EqualsNoCase(
9351 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
9352 "mysql");
9353 if (bisMySQL)
9355 std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist "
9356 "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
9357 if (m_pDS->query(strSQL))
9359 while (!m_pDS->eof())
9361 int idArtist = m_pDS->fv("idArtist").get_asInt();
9362 std::string strFanart = m_pDS->fv("strFanart").get_asString();
9363 std::string strImage = m_pDS->fv("strImage").get_asString();
9364 size_t space = 65535;
9365 // Trim strImage to allow arbitrary half space for fanart
9366 if (!TrimImageURLs(strImage, space / 2))
9367 strImage.clear(); // </thumb> not found, empty field
9368 space = space - strImage.length();
9369 // Trim fanart to fit remaining space
9370 if (!TrimImageURLs(strFanart, space))
9371 strFanart.clear(); // </thumb> not found, empty field
9373 strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
9374 "WHERE idArtist = %i",
9375 strFanart.c_str(), strImage.c_str(), idArtist);
9376 m_pDS2->exec(strSQL); // Use other dataset to update while looping result set
9378 m_pDS->next();
9380 m_pDS->close();
9384 // Remove strFanart column from artist table
9385 m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
9386 "strArtist varchar(256), strMusicBrainzArtistID text, "
9387 "strSortName text, "
9388 "strType text, strGender text, strDisambiguation text, "
9389 "strBorn text, strFormed text, strGenres text, strMoods text, "
9390 "strStyles text, strInstruments text, strBiography text, "
9391 "strDied text, strDisbanded text, strYearsActive text, "
9392 "strImage text, "
9393 "lastScraped varchar(20) default NULL, "
9394 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
9395 "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
9396 "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
9397 // Concatenate fanart URLs into strImage field
9398 // Prepare SQL to convert CONCAT to || in SQLite
9399 m_pDS->exec(PrepareSQL("INSERT INTO artist_new "
9400 "(idArtist, strArtist, strMusicBrainzArtistID, "
9401 "strSortName, strType, strGender, strDisambiguation, "
9402 "strBorn, strFormed, strGenres, strMoods, "
9403 "strStyles , strInstruments , strBiography , "
9404 "strDied, strDisbanded, strYearsActive, "
9405 "strImage, "
9406 "lastScraped, bScrapedMBID, idInfoSetting, "
9407 "dateAdded, dateNew, dateModified) "
9408 "SELECT "
9409 "artist.idArtist, "
9410 "strArtist, strMusicBrainzArtistID, "
9411 "strSortName, strType, strGender, strDisambiguation, "
9412 "strBorn, strFormed, strGenres, strMoods, "
9413 "strStyles, strInstruments, strBiography, "
9414 "strDied, strDisbanded, strYearsActive, "
9415 "CONCAT(strImage, strFanart), "
9416 "lastScraped, bScrapedMBID, idInfoSetting, "
9417 "dateAdded, dateNew, dateModified "
9418 "FROM artist"));
9419 m_pDS->exec("DROP TABLE artist");
9420 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
9423 if (version < 83)
9424 m_pDS->exec("ALTER TABLE song ADD strVideoURL TEXT");
9426 // Set the version of tag scanning required.
9427 // Not every schema change requires the tags to be rescanned, set to the highest schema version
9428 // that needs this. Forced rescanning (of music files that have not changed since they were
9429 // previously scanned) also accommodates any changes to the way tags are processed
9430 // e.g. read tags that were not processed by previous versions.
9431 // The original db version when the tags were scanned, and the minimal db version needed are
9432 // later used to determine if a forced rescan should be prompted
9434 // The last schema change needing forced rescanning was 73.
9435 // This is because Kodi can now read and process extra tags involved in the creation of box sets
9437 SetMusicNeedsTagScan(73);
9439 // After all updates, store the original db version.
9440 // This indicates the version of tag processing that was used to populate db
9441 SetMusicTagScanVersion(version);
9444 int CMusicDatabase::GetSchemaVersion() const
9446 return 83;
9449 int CMusicDatabase::GetMusicNeedsTagScan()
9453 if (nullptr == m_pDB)
9454 return -1;
9455 if (nullptr == m_pDS)
9456 return -1;
9458 std::string sql = "SELECT * FROM versiontagscan";
9459 if (!m_pDS->query(sql))
9460 return -1;
9462 if (m_pDS->num_rows() != 1)
9464 m_pDS->close();
9465 return -1;
9468 int idVersion = m_pDS->fv("idVersion").get_asInt();
9469 int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt();
9470 m_pDS->close();
9471 if (idVersion < iNeedsScan)
9472 return idVersion;
9473 else
9474 return 0;
9476 catch (...)
9478 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
9480 return -1;
9483 void CMusicDatabase::SetMusicNeedsTagScan(int version)
9485 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version));
9488 void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */)
9490 if (version == 0)
9491 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
9492 else
9493 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version));
9496 std::string CMusicDatabase::GetLibraryLastUpdated()
9498 return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
9501 void CMusicDatabase::SetLibraryLastUpdated()
9503 CDateTime dateUpdated = CDateTime::GetUTCDateTime();
9504 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
9505 dateUpdated.GetAsDBDateTime().c_str()));
9508 std::string CMusicDatabase::GetLibraryLastCleaned()
9510 return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
9513 void CMusicDatabase::SetLibraryLastCleaned()
9515 std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
9516 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'", strUpdated.c_str()));
9519 std::string CMusicDatabase::GetArtistLinksUpdated()
9521 return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
9524 void CMusicDatabase::SetArtistLinksUpdated()
9526 std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
9527 m_pDS->exec(
9528 PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'", strUpdated.c_str()));
9531 std::string CMusicDatabase::GetGenresLastAdded()
9533 return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
9536 std::string CMusicDatabase::GetSongsLastAdded()
9538 return GetSingleValue("SELECT MAX(dateNew) FROM song");
9541 std::string CMusicDatabase::GetAlbumsLastAdded()
9543 return GetSingleValue("SELECT MAX(dateNew) FROM album");
9546 std::string CMusicDatabase::GetArtistsLastAdded()
9548 return GetSingleValue("SELECT MAX(dateNew) FROM artist");
9551 std::string CMusicDatabase::GetSongsLastModified()
9553 return GetSingleValue("SELECT MAX(dateModified) FROM song");
9556 std::string CMusicDatabase::GetAlbumsLastModified()
9558 return GetSingleValue("SELECT MAX(dateModified) FROM album");
9561 std::string CMusicDatabase::GetArtistsLastModified()
9563 return GetSingleValue("SELECT MAX(dateModified) FROM artist");
9566 unsigned int CMusicDatabase::GetRandomSongIDs(const Filter& filter,
9567 std::vector<std::pair<int, int>>& songIDs)
9571 if (nullptr == m_pDB)
9572 return 0;
9573 if (nullptr == m_pDS)
9574 return 0;
9576 std::string strSQL = "SELECT idSong FROM songview ";
9577 if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
9578 return false;
9579 strSQL += PrepareSQL(" ORDER BY RANDOM()");
9581 if (!m_pDS->query(strSQL))
9582 return 0;
9583 songIDs.clear();
9584 if (m_pDS->num_rows() == 0)
9586 m_pDS->close();
9587 return 0;
9589 songIDs.reserve(m_pDS->num_rows());
9590 while (!m_pDS->eof())
9592 songIDs.push_back(std::make_pair<int, int>(1, m_pDS->fv(song_idSong).get_asInt()));
9593 m_pDS->next();
9594 } // cleanup
9595 m_pDS->close();
9596 return static_cast<unsigned int>(songIDs.size());
9598 catch (...)
9600 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
9602 return 0;
9605 int CMusicDatabase::GetSongsCount(const Filter& filter)
9609 if (nullptr == m_pDB)
9610 return 0;
9611 if (nullptr == m_pDS)
9612 return 0;
9614 std::string strSQL = "select count(idSong) as NumSongs from songview ";
9615 if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
9616 return false;
9618 if (!m_pDS->query(strSQL))
9619 return false;
9620 if (m_pDS->num_rows() == 0)
9622 m_pDS->close();
9623 return 0;
9626 int iNumSongs = m_pDS->fv("NumSongs").get_asInt();
9627 // cleanup
9628 m_pDS->close();
9629 return iNumSongs;
9631 catch (...)
9633 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
9635 return 0;
9638 bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath)
9640 basePath.clear();
9641 std::vector<std::pair<std::string, int>> paths;
9642 if (!GetAlbumPaths(idAlbum, paths))
9643 return false;
9645 for (const auto& pathpair : paths)
9647 if (basePath.empty())
9648 basePath = pathpair.first.c_str();
9649 else
9650 URIUtils::GetCommonPath(basePath, pathpair.first.c_str());
9652 return true;
9655 bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths)
9657 paths.clear();
9658 std::string strSQL;
9661 if (nullptr == m_pDB)
9662 return false;
9663 if (nullptr == m_pDS2)
9664 return false;
9666 // Get the unique paths of songs on the album, providing there are no songs from
9667 // other albums with the same path. This returns
9668 // a) <album> if is contains all the songs and no others, or
9669 // b) <album>/cd1, <album>/cd2 etc. for disc sets
9670 // but does *not* return any path when albums are mixed together. That could be because of
9671 // deliberate file organisation, or (more likely) because of a tagging error in album name
9672 // or Musicbrainzalbumid. Thus it avoids finding some generic music path.
9673 strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
9674 "JOIN path ON song.idPath = path.idPath "
9675 "WHERE song.idAlbum = %ld "
9676 "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
9677 "WHERE idPath = song.idPath) = 1",
9678 idAlbum);
9680 if (!m_pDS2->query(strSQL))
9681 return false;
9682 if (m_pDS2->num_rows() == 0)
9684 // Album does not have a unique path, files are mixed
9685 m_pDS2->close();
9686 return false;
9689 while (!m_pDS2->eof())
9691 paths.emplace_back(m_pDS2->fv("strPath").get_asString(),
9692 m_pDS2->fv("song.idPath").get_asInt());
9693 m_pDS2->next();
9695 // Cleanup recordset data
9696 m_pDS2->close();
9697 return true;
9699 catch (...)
9701 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
9704 return false;
9707 int CMusicDatabase::GetDiscnumberForPathID(int idPath)
9709 std::string strSQL;
9710 int result = -1;
9713 if (nullptr == m_pDB)
9714 return -1;
9715 if (nullptr == m_pDS2)
9716 return -1;
9718 strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
9719 "WHERE idPath = %i",
9720 idPath);
9722 if (!m_pDS2->query(strSQL))
9723 return -1;
9724 if (m_pDS2->num_rows() == 1)
9725 { // Songs with this path have a unique disc number
9726 result = m_pDS2->fv("discnum").get_asInt();
9728 // Cleanup recordset data
9729 m_pDS2->close();
9731 catch (...)
9733 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
9735 return result;
9738 // Get old "artist path" - where artist.nfo and art was located v17 and below.
9739 // It is the path common to all albums by an (album) artist, but ensure it is unique
9740 // to that artist and not shared with other artists. Previously this caused incorrect nfo
9741 // and art to be applied to multiple artists.
9742 bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string& basePath)
9744 basePath.clear();
9747 if (nullptr == m_pDB)
9748 return false;
9749 if (nullptr == m_pDS2)
9750 return false;
9752 // find all albums from this artist, and all the paths to the songs from those albums
9753 std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist "
9754 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9755 "JOIN path ON song.idPath = path.idPath "
9756 "WHERE album_artist.idArtist = %ld "
9757 "GROUP BY song.idPath",
9758 idArtist);
9760 // run query
9761 if (!m_pDS2->query(strSQL))
9762 return false;
9763 int iRowsFound = m_pDS2->num_rows();
9764 if (iRowsFound == 0)
9766 // Artist is not an album artist, no path to find
9767 m_pDS2->close();
9768 return false;
9770 else if (iRowsFound == 1)
9772 // Special case for single path - assume that we're in an artist/album/songs filesystem
9773 URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath);
9774 m_pDS2->close();
9776 else
9778 // find the common path (if any) to these albums
9779 while (!m_pDS2->eof())
9781 std::string path = m_pDS2->fv("strPath").get_asString();
9782 if (basePath.empty())
9783 basePath = path;
9784 else
9785 URIUtils::GetCommonPath(basePath, path);
9787 m_pDS2->next();
9789 m_pDS2->close();
9792 // Check any path found is unique to that album artist, and do *not* return any path
9793 // that is shared with other album artists. That could be because of collaborations
9794 // i.e. albums with more than one album artist, or because there are albums by the
9795 // artist on multiple music sources, or elsewhere in the folder hierarchy.
9796 // Avoid returning some generic music path.
9797 if (!basePath.empty())
9799 strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
9800 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9801 "JOIN path ON song.idPath = path.idPath "
9802 "WHERE album_artist.idArtist <> %ld "
9803 "AND strPath LIKE '%s%%'",
9804 idArtist, basePath.c_str());
9805 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9806 if (!strValue.empty())
9808 int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9809 if (countartists == 0)
9810 return true;
9814 catch (...)
9816 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
9818 basePath.clear();
9819 return false;
9822 bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string& path)
9824 // Get path for artist in the artists folder
9825 path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
9826 CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
9827 if (path.empty())
9828 return false; // No Artists folder not set;
9829 // Get unique artist folder name
9830 std::string strFolder;
9831 if (GetArtistFolderName(artist, strFolder))
9833 path = URIUtils::AddFileToFolder(path, strFolder);
9834 return true;
9836 path.clear();
9837 return false;
9840 bool CMusicDatabase::GetAlbumFolder(const CAlbum& album,
9841 const std::string& strAlbumPath,
9842 std::string& strFolder)
9844 strFolder.clear();
9845 // Get a name for the album folder that is unique for the artist to use when
9846 // exporting albums to separate nfo files in a folder under an artist folder
9848 // When given an album path (common to all the music files containing *only*
9849 // that album) check if that folder name is *unique* looking at folders on
9850 // all levels of the music file paths for the artist
9851 if (!strAlbumPath.empty())
9853 // Get last folder from full path
9854 std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath);
9855 if (!folders.empty())
9857 strFolder = folders.back();
9858 // The same folder name could be used on different paths for albums by the
9859 // same first artist. The albums could be totally different or also have
9860 // the same name (but different mbid). Be over cautious and look for the
9861 // name any where in the music file paths
9862 std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
9863 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9864 "JOIN path on path.idPath = song.idPath "
9865 "WHERE album_artist.iOrder = 0 "
9866 "AND album_artist.idArtist = %ld "
9867 "AND path.strPath LIKE '%%\\%s\\%%'",
9868 album.artistCredits[0].GetArtistId(), strFolder.c_str());
9870 if (!m_pDS2->query(strSQL))
9871 return false;
9872 int iRowsFound = m_pDS2->num_rows();
9873 m_pDS2->close();
9874 if (iRowsFound == 1)
9875 return true;
9878 // Create a valid unique folder name from album title
9879 // @todo: Does UFT8 matter or need normalizing?
9880 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9881 strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT);
9882 StringUtils::Replace(strFolder, " _ ", "_");
9884 // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
9885 // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
9886 // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
9887 // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
9888 std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
9889 "JOIN album ON album_artist.idAlbum = album.idAlbum "
9890 "WHERE album_artist.iOrder = 0 "
9891 "AND album_artist.idArtist = %ld "
9892 "AND album.strAlbum LIKE '%s' ",
9893 album.artistCredits[0].GetArtistId(), album.strAlbum.c_str());
9894 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9895 if (strValue.empty())
9896 return false;
9897 int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9898 if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty())
9899 { // Only one of the duplicate albums can be without mbid
9900 strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4);
9902 return !strFolder.empty();
9905 bool CMusicDatabase::GetArtistFolderName(const CArtist& artist, std::string& strFolder)
9907 return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder);
9910 bool CMusicDatabase::GetArtistFolderName(const std::string& strArtist,
9911 const std::string& strMusicBrainzArtistID,
9912 std::string& strFolder)
9914 // Create a valid unique folder name for artist
9915 // @todo: Does UFT8 matter or need normalizing?
9916 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9917 strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT);
9918 StringUtils::Replace(strFolder, " _ ", "_");
9920 // Ensure <artist name> is unique e.g. 2 x John Williams.
9921 // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
9922 // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
9923 // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
9924 std::string strSQL =
9925 PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
9926 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9927 if (strValue.empty())
9928 return false;
9929 int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9930 if (countartist > 1)
9931 strFolder += "_" + strMusicBrainzArtistID.substr(0, 4);
9932 return !strFolder.empty();
9935 int CMusicDatabase::AddSource(const std::string& strName,
9936 const std::string& strMultipath,
9937 const std::vector<std::string>& vecPaths,
9938 int id /*= -1*/)
9940 std::string strSQL;
9943 if (nullptr == m_pDB)
9944 return -1;
9945 if (nullptr == m_pDS)
9946 return -1;
9948 // Check if source name already exists
9949 int idSource = GetSourceByName(strName);
9950 if (idSource < 0)
9952 BeginTransaction();
9953 // Add new source and source paths
9954 if (id > 0)
9955 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9956 "VALUES(%i, '%s', '%s')",
9957 id, strName.c_str(), strMultipath.c_str());
9958 else
9959 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9960 "VALUES(NULL, '%s', '%s')",
9961 strName.c_str(), strMultipath.c_str());
9962 m_pDS->exec(strSQL);
9964 idSource = static_cast<int>(m_pDS->lastinsertid());
9966 int idPath = 1;
9967 for (const auto& path : vecPaths)
9969 strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
9970 "VALUES(%i,%i,'%s')",
9971 idSource, idPath, path.c_str());
9972 m_pDS->exec(strSQL);
9973 ++idPath;
9976 // Find albums by song path, building WHERE for multiple source paths
9977 // (providing source has a path)
9978 if (vecPaths.size() > 0)
9980 std::vector<int> albumIds;
9981 Filter extFilter;
9982 strSQL = "SELECT DISTINCT idAlbum FROM song ";
9983 extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
9984 for (const auto& path : vecPaths)
9985 extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false);
9986 if (!BuildSQL(strSQL, extFilter, strSQL))
9987 return -1;
9989 if (!m_pDS->query(strSQL))
9990 return -1;
9992 while (!m_pDS->eof())
9994 albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
9995 m_pDS->next();
9997 m_pDS->close();
9999 // Add album_source for related albums
10000 for (auto idAlbum : albumIds)
10002 strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) "
10003 "VALUES('%i', '%i')",
10004 idSource, idAlbum);
10005 m_pDS->exec(strSQL);
10008 CommitTransaction();
10010 return idSource;
10012 catch (...)
10014 CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
10015 RollbackTransaction();
10018 return -1;
10021 int CMusicDatabase::UpdateSource(const std::string& strOldName,
10022 const std::string& strName,
10023 const std::string& strMultipath,
10024 const std::vector<std::string>& vecPaths)
10026 int idSource = -1;
10027 std::string strSourceMultipath;
10028 std::string strSQL;
10031 if (nullptr == m_pDB)
10032 return -1;
10033 if (nullptr == m_pDS)
10034 return -1;
10036 // Get details of named old source
10037 if (!strOldName.empty())
10039 strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
10040 strOldName.c_str());
10041 if (!m_pDS->query(strSQL))
10042 return -1;
10043 if (m_pDS->num_rows() > 0)
10045 idSource = m_pDS->fv("idSource").get_asInt();
10046 strSourceMultipath = m_pDS->fv("strMultipath").get_asString();
10048 m_pDS->close();
10050 if (idSource < 0)
10052 // Source not found, add new one
10053 return AddSource(strName, strMultipath, vecPaths);
10056 // Nothing changed? (that we hold in db, other source details could be modified)
10057 bool pathschanged = strMultipath.compare(strSourceMultipath) != 0;
10058 if (!pathschanged && strOldName.compare(strName) == 0)
10059 return idSource;
10061 if (!pathschanged)
10063 // Name changed? Could be that none of the values held in db changed
10064 if (strOldName.compare(strName) != 0)
10066 strSQL = PrepareSQL("UPDATE source SET strName = '%s' "
10067 "WHERE idSource = %i",
10068 strName.c_str(), idSource);
10069 m_pDS->exec(strSQL);
10071 return idSource;
10073 else
10075 // Change paths (and name) by deleting and re-adding, but keep same ID
10076 strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource);
10077 m_pDS->exec(strSQL);
10078 return AddSource(strName, strMultipath, vecPaths, idSource);
10081 catch (...)
10083 CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
10084 RollbackTransaction();
10087 return -1;
10090 bool CMusicDatabase::RemoveSource(const std::string& strName)
10092 // Related album_source and source_path rows removed by trigger
10093 SetLibraryLastCleaned();
10094 return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str()));
10097 int CMusicDatabase::GetSourceFromPath(const std::string& strPath1)
10099 std::string strSQL;
10100 int idSource = -1;
10103 std::string strPath(strPath1);
10104 if (!URIUtils::HasSlashAtEnd(strPath))
10105 URIUtils::AddSlashAtEnd(strPath);
10107 if (nullptr == m_pDB)
10108 return -1;
10109 if (nullptr == m_pDS)
10110 return -1;
10112 // Check if path is a source matching on multipath
10113 strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str());
10114 if (!m_pDS->query(strSQL))
10115 return -1;
10116 if (m_pDS->num_rows() > 0)
10117 idSource = m_pDS->fv("idSource").get_asInt();
10118 m_pDS->close();
10119 if (idSource > 0)
10120 return idSource;
10122 // Check if path is a source path (of many) or a subfolder of a single source
10123 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10124 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10125 strPath.c_str());
10126 if (!m_pDS->query(strSQL))
10127 return -1;
10128 if (m_pDS->num_rows() == 1)
10129 idSource = m_pDS->fv("idSource").get_asInt();
10130 m_pDS->close();
10131 return idSource;
10133 catch (...)
10135 CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath1);
10138 return -1;
10141 bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource)
10143 std::string strSQL;
10144 strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) "
10145 "VALUES(%i, %i)",
10146 idAlbum, idSource);
10147 return ExecuteQuery(strSQL);
10150 bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath)
10152 std::string strSQL;
10153 std::vector<int> sourceIds;
10156 if (nullptr == m_pDB)
10157 return false;
10158 if (nullptr == m_pDS)
10159 return false;
10161 if (!strPath.empty())
10163 // Find sources related to album using album path
10164 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10165 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10166 strPath.c_str());
10167 if (!m_pDS->query(strSQL))
10168 return false;
10169 while (!m_pDS->eof())
10171 sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
10172 m_pDS->next();
10174 m_pDS->close();
10176 else
10178 // Find sources using song paths, check each source path individually
10179 if (nullptr == m_pDS2)
10180 return false;
10181 strSQL = "SELECT idSource, strPath FROM source_path";
10182 if (!m_pDS->query(strSQL))
10183 return false;
10184 while (!m_pDS->eof())
10186 std::string sourcepath = m_pDS->fv("strPath").get_asString();
10187 strSQL = PrepareSQL("SELECT 1 FROM song "
10188 "JOIN path ON song.idPath = path.idPath "
10189 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
10190 sourcepath.c_str());
10191 if (!m_pDS2->query(strSQL))
10192 return false;
10193 if (m_pDS2->num_rows() > 0)
10194 sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
10195 m_pDS2->close();
10197 m_pDS->next();
10199 m_pDS->close();
10202 //Add album sources
10203 for (auto idSource : sourceIds)
10205 AddAlbumSource(idAlbum, idSource);
10208 return true;
10210 catch (...)
10212 CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath);
10215 return false;
10218 bool CMusicDatabase::DeleteAlbumSources(int idAlbum)
10220 return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum));
10223 bool CMusicDatabase::CheckSources(VECSOURCES& sources)
10225 if (sources.empty())
10227 // Source table empty too?
10228 return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
10231 // Check number of entries matches
10232 size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
10233 if (total != sources.size())
10234 return false;
10236 // Check individual sources match
10239 if (nullptr == m_pDB)
10240 return false;
10241 if (nullptr == m_pDS)
10242 return false;
10244 std::string strSQL;
10245 for (const auto& source : sources)
10247 // Check each source by name
10248 strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source "
10249 "WHERE strName LIKE '%s'",
10250 source.strName.c_str());
10251 m_pDS->query(strSQL);
10252 if (!m_pDS->query(strSQL))
10253 return false;
10254 if (m_pDS->num_rows() != 1)
10256 // Missing source, or name duplication
10257 m_pDS->close();
10258 return false;
10260 else
10262 // Check details. Encoded URLs of source.strPath matched to strMultipath
10263 // field, no need to look at individual paths of source_path table
10264 if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0)
10266 // Paths not match
10267 m_pDS->close();
10268 return false;
10270 m_pDS->close();
10273 return true;
10275 catch (...)
10277 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10279 return false;
10282 bool CMusicDatabase::MigrateSources()
10284 //Fetch music sources from xml
10285 VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10287 std::string strSQL;
10290 // Fill source and source paths tables
10291 for (const auto& source : sources)
10293 // AddSource(source.strName, source.strPath, source.vecPaths);
10294 // Add new source
10295 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
10296 "VALUES(NULL, '%s', '%s')",
10297 source.strName.c_str(), source.strPath.c_str());
10298 m_pDS->exec(strSQL);
10299 int idSource = static_cast<int>(m_pDS->lastinsertid());
10301 // Add new source paths
10302 int idPath = 1;
10303 for (const auto& path : source.vecPaths)
10305 strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
10306 "VALUES(%i,%i,'%s')",
10307 idSource, idPath, path.c_str());
10308 m_pDS->exec(strSQL);
10309 ++idPath;
10313 return true;
10315 catch (...)
10317 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
10319 return false;
10322 bool CMusicDatabase::UpdateSources()
10324 //Check library and xml sources match
10325 VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10326 if (CheckSources(sources))
10327 return true;
10331 // Empty sources table (related link tables removed by trigger);
10332 ExecuteQuery("DELETE FROM source");
10334 // Fill source table, and album sources
10335 for (const auto& source : sources)
10336 AddSource(source.strName, source.strPath, source.vecPaths);
10338 return true;
10340 catch (...)
10342 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10344 return false;
10347 bool CMusicDatabase::GetSources(CFileItemList& items)
10351 if (nullptr == m_pDB)
10352 return false;
10353 if (nullptr == m_pDS)
10354 return false;
10356 // Get music sources and individual source paths (may not be scanned or have albums etc.)
10357 std::string strSQL =
10358 "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
10359 "FROM source JOIN source_path ON source.idSource = source_path.idSource "
10360 "ORDER BY source.idSource, source_path.idPath";
10362 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
10363 if (!m_pDS->query(strSQL))
10364 return false;
10365 int iRowsFound = m_pDS->num_rows();
10366 if (iRowsFound == 0)
10368 m_pDS->close();
10369 return true;
10372 // Get data from returned rows
10373 // Item has source ID in MusicInfotag, multipath in path, and individual paths in property
10374 CVariant sourcePaths(CVariant::VariantTypeArray);
10375 int idSource = -1;
10376 while (!m_pDS->eof())
10378 if (idSource != m_pDS->fv("source.idSource").get_asInt())
10379 { // New source
10380 if (idSource > 0 && !sourcePaths.empty())
10382 //Store paths for previous source in item list
10383 items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
10384 sourcePaths.clear();
10386 idSource = m_pDS->fv("source.idSource").get_asInt();
10387 CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
10388 pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source");
10389 // Set tag URL for "file" property in AudioLibary processing
10390 pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString());
10391 // Set item path as source URL encoded multipath too
10392 pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString());
10394 pItem->m_bIsFolder = true;
10395 items.Add(pItem);
10397 // Get path data
10398 sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString());
10400 m_pDS->next();
10402 if (!sourcePaths.empty())
10404 //Store paths for final source
10405 items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
10406 sourcePaths.clear();
10409 // cleanup
10410 m_pDS->close();
10412 return true;
10414 catch (...)
10416 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10418 return false;
10421 bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item)
10423 if (nullptr == m_pDB)
10424 return false;
10425 if (nullptr == m_pDS)
10426 return false;
10430 std::string strSQL;
10431 strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
10432 "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
10433 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
10434 "WHERE artist.idArtist = %i "
10435 "ORDER BY album_source.idSource",
10436 idArtist);
10437 if (!m_pDS->query(strSQL))
10438 return false;
10439 if (m_pDS->num_rows() == 0)
10441 // Artist does have any source via albums may not be an album artist.
10442 // Check via songs fetch sources from compilations or where they are guest artist
10443 m_pDS->close();
10444 strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
10445 "JOIN song ON song_artist.idSong = song.idSong "
10446 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
10447 "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
10448 "ORDER BY album_source.idSource",
10449 idArtist);
10450 if (!m_pDS->query(strSQL))
10451 return false;
10452 if (m_pDS->num_rows() == 0)
10454 //No sources, but query successful
10455 m_pDS->close();
10456 return true;
10460 CVariant artistSources(CVariant::VariantTypeArray);
10461 while (!m_pDS->eof())
10463 artistSources.push_back(m_pDS->fv("idSource").get_asInt());
10464 m_pDS->next();
10466 m_pDS->close();
10468 item->SetProperty("sourceid", artistSources);
10469 return true;
10471 catch (...)
10473 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
10475 return false;
10478 bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item)
10480 if (nullptr == m_pDB)
10481 return false;
10482 if (nullptr == m_pDS)
10483 return false;
10487 std::string strSQL;
10488 strSQL = PrepareSQL("SELECT idSource FROM album_source "
10489 "WHERE album_source.idAlbum = %i "
10490 "ORDER BY idSource",
10491 idAlbum);
10492 if (!m_pDS->query(strSQL))
10493 return false;
10494 CVariant albumSources(CVariant::VariantTypeArray);
10495 if (m_pDS->num_rows() > 0)
10497 while (!m_pDS->eof())
10499 albumSources.push_back(m_pDS->fv("idSource").get_asInt());
10500 m_pDS->next();
10502 m_pDS->close();
10504 else
10506 //! @todo: handle singles, or don't waste time checking songs
10507 // Album does have any sources, may be a single??
10508 // Check via song paths, check each source path individually
10509 // usually fewer source paths than songs
10510 m_pDS->close();
10512 if (nullptr == m_pDS2)
10513 return false;
10514 strSQL = "SELECT idSource, strPath FROM source_path";
10515 if (!m_pDS->query(strSQL))
10516 return false;
10517 while (!m_pDS->eof())
10519 std::string sourcepath = m_pDS->fv("strPath").get_asString();
10520 strSQL = PrepareSQL("SELECT 1 FROM song "
10521 "JOIN path ON song.idPath = path.idPath "
10522 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
10523 idAlbum, sourcepath.c_str());
10524 if (!m_pDS2->query(strSQL))
10525 return false;
10526 if (m_pDS2->num_rows() > 0)
10527 albumSources.push_back(m_pDS->fv("idSource").get_asInt());
10528 m_pDS2->close();
10530 m_pDS->next();
10532 m_pDS->close();
10536 item->SetProperty("sourceid", albumSources);
10537 return true;
10539 catch (...)
10541 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
10543 return false;
10546 bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item)
10548 if (nullptr == m_pDB)
10549 return false;
10550 if (nullptr == m_pDS)
10551 return false;
10555 std::string strSQL;
10556 strSQL = PrepareSQL("SELECT idSource FROM song "
10557 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
10558 "WHERE song.idSong = %i "
10559 "ORDER BY idSource",
10560 idSong);
10561 if (!m_pDS->query(strSQL))
10562 return false;
10563 if (m_pDS->num_rows() == 0 && !strPath1.empty())
10565 // Check via song path instead
10566 m_pDS->close();
10567 std::string strPath(strPath1);
10568 if (!URIUtils::HasSlashAtEnd(strPath))
10569 URIUtils::AddSlashAtEnd(strPath);
10571 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10572 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10573 strPath.c_str());
10574 if (!m_pDS->query(strSQL))
10575 return false;
10577 CVariant songSources(CVariant::VariantTypeArray);
10578 while (!m_pDS->eof())
10580 songSources.push_back(m_pDS->fv("idSource").get_asInt());
10581 m_pDS->next();
10583 m_pDS->close();
10585 item->SetProperty("sourceid", songSources);
10586 return true;
10588 catch (...)
10590 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
10592 return false;
10595 int CMusicDatabase::GetSourceByName(const std::string& strSource)
10599 if (nullptr == m_pDB)
10600 return false;
10601 if (nullptr == m_pDS)
10602 return false;
10604 std::string strSQL;
10605 strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str());
10606 // run query
10607 if (!m_pDS->query(strSQL))
10608 return false;
10609 int iRowsFound = m_pDS->num_rows();
10610 if (iRowsFound != 1)
10612 m_pDS->close();
10613 return -1;
10615 return m_pDS->fv("idSource").get_asInt();
10617 catch (...)
10619 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10621 return -1;
10624 std::string CMusicDatabase::GetSourceById(int id)
10626 return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id));
10629 int CMusicDatabase::GetArtistByName(const std::string& strArtist)
10633 if (nullptr == m_pDB)
10634 return false;
10635 if (nullptr == m_pDS)
10636 return false;
10638 std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'",
10639 strArtist.c_str());
10641 // run query
10642 if (!m_pDS->query(strSQL))
10643 return false;
10644 int iRowsFound = m_pDS->num_rows();
10645 if (iRowsFound != 1)
10647 m_pDS->close();
10648 return -1;
10650 int lResult = m_pDS->fv("artist.idArtist").get_asInt();
10651 m_pDS->close();
10652 return lResult;
10654 catch (...)
10656 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10658 return -1;
10661 int CMusicDatabase::GetArtistByMatch(const CArtist& artist)
10663 std::string strSQL;
10666 if (nullptr == m_pDB || nullptr == m_pDS)
10667 return false;
10668 // Match on MusicBrainz ID, definitively unique
10669 if (!artist.strMusicBrainzArtistID.empty())
10670 strSQL = PrepareSQL("SELECT idArtist FROM artist "
10671 "WHERE strMusicBrainzArtistID = '%s'",
10672 artist.strMusicBrainzArtistID.c_str());
10673 else
10674 // No MusicBrainz ID, artist by name with no mbid
10675 strSQL = PrepareSQL("SELECT idArtist FROM artist "
10676 "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
10677 artist.strArtist.c_str());
10678 if (!m_pDS->query(strSQL))
10679 return false;
10680 int iRowsFound = m_pDS->num_rows();
10681 if (iRowsFound != 1)
10683 m_pDS->close();
10684 // Match on artist name, relax mbid restriction
10685 return GetArtistByName(artist.strArtist);
10687 int lResult = m_pDS->fv("idArtist").get_asInt();
10688 m_pDS->close();
10689 return lResult;
10691 catch (...)
10693 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
10695 return -1;
10698 bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist& artist)
10702 if (nullptr == m_pDB)
10703 return false;
10704 if (nullptr == m_pDS)
10705 return false;
10707 std::string strSQL = PrepareSQL(
10708 "SELECT artistview.* FROM song_artist "
10709 "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
10710 "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
10711 idSong);
10712 if (!m_pDS->query(strSQL))
10713 return false;
10714 int iRowsFound = m_pDS->num_rows();
10715 if (iRowsFound != 1)
10717 m_pDS->close();
10718 return false;
10721 artist = GetArtistFromDataset(m_pDS.get());
10723 m_pDS->close();
10724 return true;
10726 catch (...)
10728 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10730 return false;
10733 bool CMusicDatabase::IsSongArtist(int idSong, int idArtist)
10735 std::string strSQL = PrepareSQL("SELECT 1 FROM song_artist "
10736 "WHERE song_artist.idSong= %i AND "
10737 "song_artist.idArtist = %i AND song_artist.idRole = 1",
10738 idSong, idArtist);
10739 return GetSingleValue(strSQL).empty();
10742 bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist)
10744 std::string strSQL =
10745 PrepareSQL("SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
10746 "WHERE song.idSong = %i AND album_artist.idArtist = %i",
10747 idSong, idArtist);
10748 return GetSingleValue(strSQL).empty();
10751 bool CMusicDatabase::IsAlbumBoxset(int idAlbum)
10753 std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum);
10754 int isBoxSet = GetSingleValueInt(strSQL);
10755 return (isBoxSet == 1 ? true : false);
10758 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist)
10762 if (nullptr == m_pDB)
10763 return false;
10764 if (nullptr == m_pDS)
10765 return false;
10767 std::string strSQL;
10768 if (strArtist.empty())
10769 strSQL =
10770 PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str());
10771 else
10772 strSQL = PrepareSQL("SELECT idAlbum FROM album "
10773 "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
10774 strAlbum.c_str(), strArtist.c_str());
10775 // run query
10776 if (!m_pDS->query(strSQL))
10777 return false;
10778 int iRowsFound = m_pDS->num_rows();
10779 if (iRowsFound != 1)
10781 m_pDS->close();
10782 return -1;
10784 return m_pDS->fv("idAlbum").get_asInt();
10786 catch (...)
10788 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10790 return -1;
10793 bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum,
10794 const std::string& strArtist,
10795 int& idAlbum,
10796 std::string& strReview)
10799 Get the first album that matches with the title and artist display name.
10800 Artist(s) and album title may not be sufficient to uniquely identify a match since library can
10801 store multiple releases, and occasionally artists even have different albums with same name.
10802 Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
10806 if (nullptr == m_pDB)
10807 return false;
10808 if (nullptr == m_pDS)
10809 return false;
10811 std::string strSQL;
10812 if (strArtist.empty())
10813 strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
10814 strAlbum.c_str());
10815 else
10816 strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album "
10817 "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
10818 strAlbum.c_str(), strArtist.c_str());
10819 // run query
10820 if (!m_pDS->query(strSQL))
10821 return false;
10822 int iRowsFound = m_pDS->num_rows();
10823 if (iRowsFound > 0)
10825 idAlbum = m_pDS->fv("idAlbum").get_asInt();
10826 strReview = m_pDS->fv("strReview").get_asString();
10827 return true;
10830 catch (...)
10832 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10834 return false;
10837 bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items)
10841 if (nullptr == m_pDB)
10842 return false;
10843 if (nullptr == m_pDS)
10844 return false;
10846 std::string strSQL;
10847 strSQL = PrepareSQL("SELECT albumview.* FROM albumview "
10848 "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
10849 "WHERE album_artist.strArtist LIKE '%s'",
10850 strArtist.c_str());
10852 if (!m_pDS->query(strSQL))
10853 return false;
10855 while (!m_pDS->eof())
10857 CAlbum album = GetAlbumFromDataset(m_pDS.get());
10858 std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
10859 CFileItemPtr pItem(new CFileItem(path, album));
10860 std::string label =
10861 StringUtils::Format("{} ({})", album.strAlbum, pItem->GetMusicInfoTag()->GetYear());
10862 pItem->SetLabel(label);
10863 items.Add(pItem);
10864 m_pDS->next();
10866 m_pDS->close(); // cleanup recordset data
10867 return true;
10869 catch (...)
10871 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10873 return false;
10876 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum,
10877 const std::vector<std::string>& artist)
10879 return GetAlbumByName(
10880 strAlbum,
10881 StringUtils::Join(
10882 artist,
10883 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
10886 int CMusicDatabase::GetAlbumByMatch(const CAlbum& album)
10888 std::string strSQL;
10891 if (nullptr == m_pDB || nullptr == m_pDS)
10892 return false;
10893 // Match on MusicBrainz ID, definitively unique
10894 if (!album.strMusicBrainzAlbumID.empty())
10895 strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'",
10896 album.strMusicBrainzAlbumID.c_str());
10897 else
10898 // No mbid, match on album title and album artist descriptive string, ignore those with mbid
10899 strSQL = PrepareSQL("SELECT idAlbum FROM album "
10900 "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
10901 "AND strMusicBrainzAlbumID IS NULL",
10902 album.GetAlbumArtistString().c_str(), album.strAlbum.c_str());
10903 m_pDS->query(strSQL);
10904 if (!m_pDS->query(strSQL))
10905 return false;
10906 int iRowsFound = m_pDS->num_rows();
10907 if (iRowsFound != 1)
10909 m_pDS->close();
10910 // Match on album title and album artist descriptive string, relax mbid restriction
10911 return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString());
10913 int lResult = m_pDS->fv("idAlbum").get_asInt();
10914 m_pDS->close();
10915 return lResult;
10917 catch (...)
10919 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
10921 return -1;
10924 std::string CMusicDatabase::GetGenreById(int id)
10926 return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id));
10929 std::string CMusicDatabase::GetArtistById(int id)
10931 return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id));
10934 std::string CMusicDatabase::GetRoleById(int id)
10936 return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id));
10939 bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/)
10941 // Propagate artist sort names into concatenated artist sort name string for songs and albums
10942 // Avoid updating records where sort same as strArtistDisp
10943 std::string strSQL;
10945 // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
10946 // (not handled by PrepareSQL)
10947 bool bisMySQL = StringUtils::EqualsNoCase(
10948 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
10950 BeginMultipleExecute();
10951 if (bisMySQL)
10952 strSQL = "(SELECT GROUP_CONCAT("
10953 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10954 "ELSE artist.strSortName END "
10955 "ORDER BY album_artist.idAlbum, album_artist.iOrder "
10956 "SEPARATOR '; ') as val "
10957 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10958 "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
10959 else
10960 strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10961 "FROM(SELECT album_artist.idAlbum, "
10962 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10963 "ELSE artist.strSortName END as val "
10964 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10965 "WHERE album_artist.idAlbum = album.idAlbum "
10966 "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
10968 strSQL = "UPDATE album SET strArtistSort = " + strSQL +
10969 "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
10970 "AND strArtistDisp <> " +
10971 strSQL;
10972 if (idArtist > 0)
10973 strSQL +=
10974 PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
10975 "AND album_artist.idAlbum = album.idAlbum)",
10976 idArtist);
10977 ExecuteQuery(strSQL);
10978 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
10980 if (bisMySQL)
10981 strSQL = "(SELECT GROUP_CONCAT("
10982 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10983 "ELSE artist.strSortName END "
10984 "ORDER BY song_artist.idSong, song_artist.iOrder "
10985 "SEPARATOR '; ') as val "
10986 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10987 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
10988 else
10989 strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10990 "FROM(SELECT song_artist.idSong, "
10991 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10992 "ELSE artist.strSortName END as val "
10993 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10994 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
10995 "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
10997 strSQL = "UPDATE song SET strArtistSort = " + strSQL +
10998 "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
10999 "AND strArtistDisp <> " +
11000 strSQL;
11001 if (idArtist > 0)
11002 strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
11003 "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)",
11004 idArtist);
11005 ExecuteQuery(strSQL);
11006 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
11008 if (CommitMultipleExecute())
11009 return true;
11010 else
11011 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
11012 return false;
11015 std::string CMusicDatabase::GetAlbumById(int id)
11017 return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id));
11020 int CMusicDatabase::GetGenreByName(const std::string& strGenre)
11024 if (nullptr == m_pDB)
11025 return false;
11026 if (nullptr == m_pDS)
11027 return false;
11029 std::string strSQL;
11030 strSQL = PrepareSQL("SELECT idGenre FROM genre "
11031 "WHERE genre.strGenre LIKE '%s'",
11032 strGenre.c_str());
11033 // run query
11034 if (!m_pDS->query(strSQL))
11035 return false;
11036 int iRowsFound = m_pDS->num_rows();
11037 if (iRowsFound != 1)
11039 m_pDS->close();
11040 return -1;
11042 return m_pDS->fv("genre.idGenre").get_asInt();
11044 catch (...)
11046 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
11048 return -1;
11051 bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources)
11053 std::string strSQL;
11056 if (nullptr == m_pDB)
11057 return false;
11058 if (nullptr == m_pDS)
11059 return false;
11061 strSQL = "SELECT %s FROM genre ";
11062 Filter extFilter;
11063 extFilter.AppendField("genre.idGenre");
11064 extFilter.AppendField("genre.strGenre");
11065 if (bSources)
11067 strSQL = "SELECT DISTINCT %s FROM genre ";
11068 extFilter.AppendField("album_source.idSource");
11069 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
11070 extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
11071 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
11072 extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
11073 extFilter.AppendOrder("genre.strGenre");
11074 extFilter.AppendOrder("album_source.idSource");
11076 extFilter.AppendWhere("genre.strGenre != ''");
11078 std::string strSQLExtra;
11079 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
11080 return false;
11082 strSQL = PrepareSQL(strSQL, extFilter.fields.c_str()) + strSQLExtra;
11084 // run query
11085 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
11087 if (!m_pDS->query(strSQL))
11088 return false;
11089 int iRowsFound = m_pDS->num_rows();
11090 if (iRowsFound == 0)
11092 m_pDS->close();
11093 return true;
11096 if (!bSources)
11097 items.Reserve(iRowsFound);
11099 // Get data from returned rows
11100 // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
11101 CVariant genreSources(CVariant::VariantTypeArray);
11102 int idGenre = -1;
11103 while (!m_pDS->eof())
11105 if (idGenre != m_pDS->fv("genre.idGenre").get_asInt())
11106 { // New genre
11107 if (idGenre > 0 && bSources)
11109 //Store sources for previous genre in item list
11110 items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
11111 genreSources.clear();
11113 idGenre = m_pDS->fv("genre.idGenre").get_asInt();
11114 std::string strGenre = m_pDS->fv("genre.strGenre").get_asString();
11115 CFileItemPtr pItem(new CFileItem(strGenre));
11116 pItem->GetMusicInfoTag()->SetTitle(strGenre);
11117 pItem->GetMusicInfoTag()->SetGenre(strGenre);
11118 pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre");
11119 pItem->SetPath(StringUtils::Format("musicdb://genres/{}/", idGenre));
11120 pItem->m_bIsFolder = true;
11121 items.Add(pItem);
11123 // Get source data
11124 if (bSources)
11126 int sourceid = m_pDS->fv("album_source.idSource").get_asInt();
11127 if (sourceid > 0)
11128 genreSources.push_back(sourceid);
11130 m_pDS->next();
11132 if (bSources)
11134 //Store sources for final genre
11135 items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
11138 // cleanup
11139 m_pDS->close();
11141 return true;
11143 catch (...)
11145 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
11147 return false;
11150 std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc)
11152 // Get disc node title from ids allowing for "*all"
11153 std::string disctitle;
11154 std::string albumtitle;
11155 if (idAlbum > 0)
11156 albumtitle = GetAlbumById(idAlbum);
11157 if (idDisc > 0)
11159 disctitle = GetSingleValue("song", "strDiscSubtitle",
11160 PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc));
11161 if (disctitle.empty())
11162 disctitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc.
11163 if (albumtitle.empty())
11164 albumtitle = disctitle;
11165 else
11166 albumtitle = albumtitle + " - " + disctitle;
11168 return albumtitle;
11171 int CMusicDatabase::GetBoxsetsCount()
11173 return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
11176 int CMusicDatabase::GetAlbumDiscsCount(int idAlbum)
11178 std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum);
11179 return GetSingleValueInt(strSQL);
11182 int CMusicDatabase::GetCompilationAlbumsCount()
11184 return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
11187 int CMusicDatabase::GetSinglesCount()
11189 CDatabase::Filter filter(
11190 PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
11191 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
11192 return GetSongsCount(filter);
11195 int CMusicDatabase::GetArtistCountForRole(int role)
11197 std::string strSQL = PrepareSQL(
11198 "SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role);
11199 return GetSingleValueInt(strSQL);
11202 int CMusicDatabase::GetArtistCountForRole(const std::string& strRole)
11204 std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist "
11205 "JOIN role ON song_artist.idRole = role.idRole "
11206 "WHERE role.strRole LIKE '%s'",
11207 strRole.c_str());
11208 return GetSingleValueInt(strSQL);
11211 bool CMusicDatabase::SetPathHash(const std::string& path, const std::string& hash)
11215 if (nullptr == m_pDB)
11216 return false;
11217 if (nullptr == m_pDS)
11218 return false;
11220 if (hash.empty())
11221 { // this is an empty folder - we need only add it to the path table
11222 // if the path actually exists
11223 if (!CDirectory::Exists(path))
11224 return false;
11226 int idPath = AddPath(path);
11227 if (idPath < 0)
11228 return false;
11230 std::string strSQL =
11231 PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash.c_str(), idPath);
11232 m_pDS->exec(strSQL);
11234 return true;
11236 catch (...)
11238 CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash);
11241 return false;
11244 bool CMusicDatabase::GetPathHash(const std::string& path, std::string& hash)
11248 if (nullptr == m_pDB)
11249 return false;
11250 if (nullptr == m_pDS)
11251 return false;
11253 std::string strSQL = PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
11254 m_pDS->query(strSQL);
11255 if (m_pDS->num_rows() == 0)
11256 return false;
11257 hash = m_pDS->fv("strHash").get_asString();
11258 return true;
11260 catch (...)
11262 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
11265 return false;
11268 bool CMusicDatabase::RemoveSongsFromPath(const std::string& path1, MAPSONGS& songmap, bool exact)
11270 // We need to remove all songs from this path, as their tags are going
11271 // to be re-read. We need to remove all songs from the song table + all links to them
11272 // from the song link tables (as otherwise if a song is added back
11273 // to the table with the same idSong, these tables can't be cleaned up properly later)
11275 //! @todo SQLite probably doesn't allow this, but can we rely on that??
11277 // We don't need to remove orphaned albums at this point as in AddAlbum() we check
11278 // first whether the album has already been read during this scan, and if it hasn't
11279 // we check whether it's in the table and update accordingly at that point, removing the entries from
11280 // the album link tables. The only failure point for this is albums
11281 // that span multiple folders, where just the files in one folder have been changed. In this case
11282 // any linked fields that are only in the files that haven't changed will be removed. Clearly
11283 // the primary albumartist still matches (as that's what we looked up based on) so is this really
11284 // an issue? I don't think it is, as those artists will still have links to the album via the songs
11285 // which is generally what we rely on, so the only failure point is albumartist lookup. In this
11286 // case, it will return only things in the album_artist table from the newly updated songs (and
11287 // only if they have additional artists). I think the effect of this is minimal at best, as ALL
11288 // songs in the album should have the same albumartist!
11290 // we also remove the path at this point as it will be added later on if the
11291 // path still exists.
11292 // After scanning we then remove the orphaned artists, genres and thumbs.
11294 // Note: when used to remove all songs from a path and its subpath (exact=false), this
11295 // does miss archived songs.
11296 std::string path(path1);
11297 SetLibraryLastUpdated();
11300 if (!URIUtils::HasSlashAtEnd(path))
11301 URIUtils::AddSlashAtEnd(path);
11303 if (nullptr == m_pDB)
11304 return false;
11305 if (nullptr == m_pDS)
11306 return false;
11308 // Filename is not unique for a path as songs from a cuesheet have same filename.
11309 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
11310 // in a folder and some edited and rescanned.
11311 // Hence order by filename so these songs can be gathered together.
11312 std::string where;
11313 if (exact)
11314 where = PrepareSQL(" WHERE strPath='%s'", path.c_str());
11315 else
11316 where = PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()),
11317 path.c_str());
11318 std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName";
11319 if (!m_pDS->query(sql))
11320 return false;
11321 int iRowsFound = m_pDS->num_rows();
11322 if (iRowsFound > 0)
11324 // Each file is potentially mapped to a list of songs, gather these and save as list
11325 VECSONGS songs;
11326 std::string filename;
11327 std::vector<std::string> songIds;
11328 while (!m_pDS->eof())
11330 CSong song = GetSongFromDataset();
11331 if (!filename.empty() && filename != song.strFileName)
11333 // Save songs for previous filename
11334 songmap.insert(std::make_pair(filename, songs));
11335 songs.clear();
11337 song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb");
11338 songs.emplace_back(song);
11339 songIds.push_back(PrepareSQL("%i", song.idSong));
11340 filename = song.strFileName;
11342 m_pDS->next();
11344 m_pDS->close();
11345 songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
11347 //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
11348 for (const auto& id : songIds)
11349 AnnounceRemove(MediaTypeSong, atoi(id.c_str()));
11351 // Delete all songs, and anything linked to them via triggers
11352 std::string strIDs = StringUtils::Join(songIds, ",");
11353 sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")";
11354 m_pDS->exec(sql);
11356 // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
11357 sql = "delete from path" + where;
11358 m_pDS->exec(sql);
11359 return iRowsFound > 0;
11361 catch (...)
11363 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
11365 return false;
11368 void CMusicDatabase::CheckArtistLinksChanged()
11370 std::string strSQL = "SELECT COUNT(1) FROM removed_link ";
11371 int iLinks = GetSingleValueInt(strSQL, m_pDS);
11372 if (iLinks > 0)
11374 SetArtistLinksUpdated(); // Store datetime artist links last updated
11375 DeleteRemovedLinks(); // Clean-up artist links
11379 bool CMusicDatabase::GetPaths(std::set<std::string>& paths)
11383 if (nullptr == m_pDB)
11384 return false;
11385 if (nullptr == m_pDS)
11386 return false;
11388 paths.clear();
11390 // find all paths
11391 if (!m_pDS->query("SELECT strPath FROM path"))
11392 return false;
11393 int iRowsFound = m_pDS->num_rows();
11394 if (iRowsFound == 0)
11396 m_pDS->close();
11397 return true;
11399 while (!m_pDS->eof())
11401 paths.insert(m_pDS->fv("strPath").get_asString());
11402 m_pDS->next();
11404 m_pDS->close();
11405 return true;
11407 catch (...)
11409 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
11411 return false;
11414 bool CMusicDatabase::SetSongUserrating(const std::string& filePath, int userrating)
11418 if (filePath.empty())
11419 return false;
11420 if (nullptr == m_pDB)
11421 return false;
11422 if (nullptr == m_pDS)
11423 return false;
11425 int songID = GetSongIDFromPath(filePath);
11426 if (-1 == songID)
11427 return false;
11429 return SetSongUserrating(songID, userrating);
11431 catch (...)
11433 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, userrating);
11435 return false;
11438 bool CMusicDatabase::SetSongUserrating(int idSong, int userrating)
11442 if (nullptr == m_pDB)
11443 return false;
11444 if (nullptr == m_pDS)
11445 return false;
11447 std::string sql =
11448 PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong);
11449 m_pDS->exec(sql);
11450 return true;
11452 catch (...)
11454 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idSong, userrating);
11456 return false;
11459 bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating)
11463 if (nullptr == m_pDB)
11464 return false;
11465 if (nullptr == m_pDS)
11466 return false;
11468 if (-1 == idAlbum)
11469 return false;
11470 std::string sql =
11471 PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum);
11472 m_pDS->exec(sql);
11473 return true;
11475 catch (...)
11477 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idAlbum, userrating);
11479 return false;
11482 bool CMusicDatabase::SetSongVotes(const std::string& filePath, int votes)
11486 if (filePath.empty())
11487 return false;
11488 if (nullptr == m_pDB)
11489 return false;
11490 if (nullptr == m_pDS)
11491 return false;
11493 int songID = GetSongIDFromPath(filePath);
11494 if (-1 == songID)
11495 return false;
11497 std::string sql = PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID);
11499 m_pDS->exec(sql);
11500 return true;
11502 catch (...)
11504 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, votes);
11506 return false;
11509 int CMusicDatabase::GetSongIDFromPath(const std::string& filePath)
11511 // grab the where string to identify the song id
11512 CURL url(filePath);
11513 if (url.IsProtocol("musicdb"))
11515 std::string strFile = URIUtils::GetFileName(filePath);
11516 URIUtils::RemoveExtension(strFile);
11517 return atoi(strFile.c_str());
11519 // hit the db
11522 if (nullptr == m_pDB)
11523 return -1;
11524 if (nullptr == m_pDS)
11525 return -1;
11527 std::string strPath, strFileName;
11528 SplitPath(filePath, strPath, strFileName);
11529 URIUtils::AddSlashAtEnd(strPath);
11531 std::string sql = PrepareSQL("SELECT idSong FROM song JOIN path ON song.idPath = path.idPath "
11532 "WHERE song.strFileName='%s' AND path.strPath='%s'",
11533 strFileName.c_str(), strPath.c_str());
11534 if (!m_pDS->query(sql))
11535 return -1;
11537 if (m_pDS->num_rows() == 0)
11539 m_pDS->close();
11540 return -1;
11543 int songID = m_pDS->fv("idSong").get_asInt();
11544 m_pDS->close();
11545 return songID;
11547 catch (...)
11549 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
11551 return -1;
11554 bool CMusicDatabase::CommitTransaction()
11556 if (CDatabase::CommitTransaction())
11557 { // number of items in the db has likely changed, so reset the infomanager cache
11558 CGUIComponent* gui = CServiceBroker::GetGUI();
11559 if (gui)
11561 gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(
11562 LIBRARY_HAS_MUSIC, GetSongsCount() > 0);
11563 return true;
11566 return false;
11569 bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper)
11571 if (nullptr == m_pDB)
11572 return false;
11573 if (nullptr == m_pDS)
11574 return false;
11575 std::string strSQL;
11576 int idSetting = -1;
11579 CONTENT_TYPE content = CONTENT_NONE;
11581 // Build where clause from virtual path
11582 Filter extFilter;
11583 CMusicDbUrl musicUrl;
11584 SortDescription sorting;
11585 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
11586 return false;
11588 std::string itemType = musicUrl.GetType();
11589 if (StringUtils::EqualsNoCase(itemType, "artists"))
11591 content = CONTENT_ARTISTS;
11593 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11595 content = CONTENT_ALBUMS;
11597 else
11598 return false; //Only artists and albums have info settings
11600 std::string strSQLWhere;
11601 if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere))
11602 return false;
11604 // Replace view names with table names
11605 StringUtils::Replace(strSQLWhere, "artistview", "artist");
11606 StringUtils::Replace(strSQLWhere, "albumview", "album");
11608 BeginTransaction();
11609 // Clear current scraper settings (0 => default scraper used)
11610 if (content == CONTENT_ARTISTS)
11611 strSQL = "UPDATE artist SET idInfoSetting = %i ";
11612 else
11613 strSQL = "UPDATE album SET idInfoSetting = %i ";
11614 strSQL = PrepareSQL(strSQL, 0) + strSQLWhere;
11615 m_pDS->exec(strSQL);
11617 //Remove orphaned settings
11618 CleanupInfoSettings();
11620 if (scraper)
11622 // Add new info setting
11623 strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
11624 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
11625 m_pDS->exec(strSQL);
11626 idSetting = static_cast<int>(m_pDS->lastinsertid());
11628 if (content == CONTENT_ARTISTS)
11629 strSQL = "UPDATE artist SET idInfoSetting = %i ";
11630 else
11631 strSQL = "UPDATE album SET idInfoSetting = %i ";
11632 strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere;
11633 m_pDS->exec(strSQL);
11635 CommitTransaction();
11636 return true;
11638 catch (...)
11640 RollbackTransaction();
11641 CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, strBaseDir, strSQL);
11643 return false;
11646 bool CMusicDatabase::SetScraper(int id,
11647 const CONTENT_TYPE& content,
11648 const ADDON::ScraperPtr& scraper)
11650 if (nullptr == m_pDB)
11651 return false;
11652 if (nullptr == m_pDS)
11653 return false;
11654 std::string strSQL;
11655 int idSetting = -1;
11658 BeginTransaction();
11659 // Fetch current info settings for item, 0 => default is used
11660 if (content == CONTENT_ARTISTS)
11661 strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
11662 else
11663 strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
11664 strSQL = PrepareSQL(strSQL, id);
11665 m_pDS->query(strSQL);
11666 if (m_pDS->num_rows() > 0)
11667 idSetting = m_pDS->fv("idInfoSetting").get_asInt();
11668 m_pDS->close();
11670 if (idSetting < 1)
11671 { // Add new info setting
11672 strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
11673 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
11674 m_pDS->exec(strSQL);
11675 idSetting = static_cast<int>(m_pDS->lastinsertid());
11677 if (content == CONTENT_ARTISTS)
11678 strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
11679 else
11680 strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
11681 strSQL = PrepareSQL(strSQL, idSetting, id);
11682 m_pDS->exec(strSQL);
11684 else
11685 { // Update info setting
11686 strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' "
11687 "WHERE idSetting = %i";
11688 strSQL =
11689 PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting);
11690 m_pDS->exec(strSQL);
11692 CommitTransaction();
11693 return true;
11695 catch (...)
11697 RollbackTransaction();
11698 CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, id, strSQL);
11700 return false;
11703 bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper)
11705 std::string scraperUUID;
11706 std::string strSettings;
11709 if (nullptr == m_pDB)
11710 return false;
11711 if (nullptr == m_pDS)
11712 return false;
11714 std::string strSQL;
11715 strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
11716 if (content == CONTENT_ARTISTS)
11717 strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting "
11718 "WHERE artist.idArtist = %i";
11719 else
11720 strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting "
11721 "WHERE album.idAlbum = %i";
11722 strSQL = PrepareSQL(strSQL, id);
11723 m_pDS->query(strSQL);
11724 if (!m_pDS->eof())
11725 { // try and ascertain scraper
11726 scraperUUID = m_pDS->fv("strScraperPath").get_asString();
11727 strSettings = m_pDS->fv("strSettings").get_asString();
11729 // Use pre configured or default scraper
11730 ADDON::AddonPtr addon;
11731 if (!scraperUUID.empty() &&
11732 CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon,
11733 ADDON::OnlyEnabled::CHOICE_YES) &&
11734 addon)
11736 scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
11737 if (scraper)
11738 // Set settings
11739 scraper->SetPathSettings(content, strSettings);
11742 m_pDS->close();
11744 if (!scraper)
11745 { // use default music scraper instead
11746 ADDON::AddonPtr addon;
11747 if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
11748 ADDON::ScraperTypeFromContent(content), addon))
11750 scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
11751 return scraper != NULL;
11753 else
11754 return false;
11757 return true;
11759 catch (...)
11761 CLog::Log(LOGERROR, "{} -({}, {} {}) failed", __FUNCTION__, id, scraperUUID, strSettings);
11763 return false;
11766 bool CMusicDatabase::ScraperInUse(const std::string& scraperID) const
11770 if (nullptr == m_pDB)
11771 return false;
11772 if (nullptr == m_pDS)
11773 return false;
11775 std::string sql =
11776 PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID.c_str());
11777 if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
11779 m_pDS->close();
11780 return false;
11782 bool found = m_pDS->fv(0).get_asInt() > 0;
11783 m_pDS->close();
11784 return found;
11786 catch (...)
11788 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID);
11790 return false;
11793 bool CMusicDatabase::GetItems(const std::string& strBaseDir,
11794 CFileItemList& items,
11795 const Filter& filter /* = Filter() */,
11796 const SortDescription& sortDescription /* = SortDescription() */)
11798 CMusicDbUrl musicUrl;
11799 if (!musicUrl.FromString(strBaseDir))
11800 return false;
11802 return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription);
11805 bool CMusicDatabase::GetItems(const std::string& strBaseDir,
11806 const std::string& itemType,
11807 CFileItemList& items,
11808 const Filter& filter /* = Filter() */,
11809 const SortDescription& sortDescription /* = SortDescription() */)
11811 if (StringUtils::EqualsNoCase(itemType, "genres"))
11812 return GetGenresNav(strBaseDir, items, filter);
11813 else if (StringUtils::EqualsNoCase(itemType, "sources"))
11814 return GetSourcesNav(strBaseDir, items, filter);
11815 else if (StringUtils::EqualsNoCase(itemType, "years"))
11816 return GetYearsNav(strBaseDir, items, filter);
11817 else if (StringUtils::EqualsNoCase(itemType, "roles"))
11818 return GetRolesNav(strBaseDir, items, filter);
11819 else if (StringUtils::EqualsNoCase(itemType, "artists"))
11820 return GetArtistsNav(strBaseDir, items,
11821 !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
11822 CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS),
11823 -1, -1, -1, filter, sortDescription);
11824 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11825 return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription);
11826 else if (StringUtils::EqualsNoCase(itemType, "discs"))
11827 return GetDiscsByWhere(strBaseDir, filter, items, sortDescription);
11828 else if (StringUtils::EqualsNoCase(itemType, "songs"))
11829 return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true);
11831 return false;
11834 std::string CMusicDatabase::GetItemById(const std::string& itemType, int id)
11836 if (StringUtils::EqualsNoCase(itemType, "genres"))
11837 return GetGenreById(id);
11838 else if (StringUtils::EqualsNoCase(itemType, "sources"))
11839 return GetSourceById(id);
11840 else if (StringUtils::EqualsNoCase(itemType, "years"))
11841 return std::to_string(id);
11842 else if (StringUtils::EqualsNoCase(itemType, "artists"))
11843 return GetArtistById(id);
11844 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11845 return GetAlbumById(id);
11846 else if (StringUtils::EqualsNoCase(itemType, "roles"))
11847 return GetRoleById(id);
11849 return "";
11852 void CMusicDatabase::ExportToXML(const CLibExportSettings& settings,
11853 CGUIDialogProgress* progressDialog /*= nullptr*/)
11855 if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) &&
11856 !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) &&
11857 !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) &&
11858 !settings.IsItemExported(ELIBEXPORT_ALBUMS) && !settings.IsItemExported(ELIBEXPORT_SONGS))
11859 return;
11861 // Exporting albums either art or NFO (or both) selected
11862 if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && settings.m_skipnfo &&
11863 !settings.m_artwork && settings.IsItemExported(ELIBEXPORT_ALBUMS))
11864 return;
11866 std::string strFolder;
11867 if (settings.IsSingleFile() || settings.IsSeparateFiles())
11869 // Exporting to single file or separate files in a specified location
11870 if (settings.m_strPath.empty())
11871 return;
11873 strFolder = settings.m_strPath;
11874 if (!URIUtils::HasSlashAtEnd(strFolder))
11875 URIUtils::AddSlashAtEnd(strFolder);
11876 strFolder = URIUtils::GetDirectory(strFolder);
11877 if (strFolder.empty())
11878 return;
11880 else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists()))
11882 // Exporting artist folders only, or artist NFO or art to library folders
11883 // need Artist Information Folder defined.
11884 // (Album NFO and art goes to music folders)
11885 strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
11886 CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
11887 if (strFolder.empty())
11888 return;
11892 bool artistfoldersonly;
11893 artistfoldersonly = settings.IsArtistFoldersOnly() ||
11894 ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
11895 settings.m_skipnfo && !settings.m_artwork);
11897 int iFailCount = 0;
11900 if (nullptr == m_pDB)
11901 return;
11902 if (nullptr == m_pDS)
11903 return;
11904 if (nullptr == m_pDS2)
11905 return;
11907 // Create our xml document
11908 CXBMCTinyXML xmlDoc;
11909 TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11910 xmlDoc.InsertEndChild(decl);
11911 TiXmlNode* pMain = NULL;
11912 if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly)
11913 pMain = &xmlDoc;
11914 else if (settings.IsSingleFile())
11916 TiXmlElement xmlMainElement("musicdb");
11917 pMain = xmlDoc.InsertEndChild(xmlMainElement);
11920 if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly)
11922 // Find albums to export
11923 std::vector<int> albumIds;
11924 std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
11925 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str());
11926 if (!settings.m_unscraped)
11927 strSQL += "AND lastScraped IS NOT NULL";
11928 CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
11929 m_pDS->query(strSQL);
11931 int total = m_pDS->num_rows();
11932 int current = 0;
11934 albumIds.reserve(total);
11935 while (!m_pDS->eof())
11937 albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
11938 m_pDS->next();
11940 m_pDS->close();
11942 for (const auto& albumId : albumIds)
11944 CAlbum album;
11945 GetAlbum(albumId, album);
11946 std::string strAlbumPath;
11947 std::string strPath;
11948 // Get album path, empty unless all album songs are under a unique folder, and
11949 // there are no songs from another album in the same folder.
11950 if (!GetAlbumPath(albumId, strAlbumPath))
11951 strAlbumPath.clear();
11952 if (settings.IsSingleFile())
11954 // Save album to xml, including album path
11955 album.Save(pMain, "album", strAlbumPath);
11957 else
11958 { // Separate files and artwork
11959 bool pathfound = false;
11960 if (settings.IsToLibFolders())
11961 { // Save album.nfo and artwork with music files.
11962 // Most albums are under a unique folder, but if songs from various albums are mixed then
11963 // avoid overwriting by not allow NFO and art to be exported
11964 if (strAlbumPath.empty())
11965 CLog::Log(LOGDEBUG,
11966 "CMusicDatabase::{} - Not exporting album {} as unique path not found",
11967 __FUNCTION__, album.strAlbum);
11968 else if (!CDirectory::Exists(strAlbumPath))
11969 CLog::Log(
11970 LOGDEBUG,
11971 "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist",
11972 __FUNCTION__, album.strAlbum, strAlbumPath);
11973 else
11975 strPath = strAlbumPath;
11976 pathfound = true;
11979 else
11980 { // Save album.nfo and artwork to subfolder on export path
11981 // strPath = strFolder/<albumartist name>/<albumname>
11982 // where <albumname> is either the same name as the album folder
11983 // containing the music files (if unique) or is created using the album name
11984 std::string strAlbumArtist;
11985 pathfound = GetArtistFolderName(album.GetAlbumArtist()[0],
11986 album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist);
11987 if (pathfound)
11989 strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist);
11990 pathfound = CDirectory::Exists(strPath);
11991 if (!pathfound)
11992 pathfound = CDirectory::Create(strPath);
11994 if (!pathfound)
11995 CLog::Log(LOGDEBUG,
11996 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
11997 __FUNCTION__, album.strAlbum, strPath);
11998 else
12000 std::string strAlbumFolder;
12001 pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder);
12002 if (pathfound)
12004 strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder);
12005 pathfound = CDirectory::Exists(strPath);
12006 if (!pathfound)
12007 pathfound = CDirectory::Create(strPath);
12009 if (!pathfound)
12010 CLog::Log(LOGDEBUG,
12011 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
12012 __FUNCTION__, album.strAlbum, strPath);
12015 if (pathfound)
12017 if (!settings.m_skipnfo)
12019 // Save album to NFO, including album path
12020 album.Save(pMain, "album", strAlbumPath);
12021 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo");
12022 if (settings.m_overwrite || !CFile::Exists(nfoFile))
12024 if (!xmlDoc.SaveFile(nfoFile))
12026 CLog::Log(LOGERROR, "CMusicDatabase::{}: Album nfo export failed! ('{}')",
12027 __FUNCTION__, nfoFile);
12028 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
12029 g_localizeStrings.Get(20302),
12030 CURL::GetRedacted(nfoFile));
12031 iFailCount++;
12035 if (settings.m_artwork)
12037 // Save art in album folder
12038 // Note thumb resolution may be lower than original when overwriting
12039 std::map<std::string, std::string> artwork;
12040 std::string savedArtfile;
12041 if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork))
12043 for (const auto& art : artwork)
12045 if (art.first == "thumb")
12046 savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
12047 else
12048 savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
12049 CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
12050 settings.m_overwrite);
12054 xmlDoc.Clear();
12055 xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
12059 if ((current % 50) == 0 && progressDialog)
12061 progressDialog->SetLine(1, CVariant{album.strAlbum});
12062 progressDialog->SetPercentage(current * 100 / total);
12063 if (progressDialog->IsCanceled())
12064 return;
12066 current++;
12070 // Export song playback history to single file only
12071 if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS))
12073 if (!ExportSongHistory(pMain, progressDialog))
12074 return;
12077 if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty())
12079 // Find artists to export
12080 std::vector<int> artistIds;
12081 Filter filter;
12083 if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS))
12084 filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist "
12085 "WHERE album_artist.idArtist = artist.idArtist)",
12086 false);
12087 if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS))
12089 if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
12090 filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist "
12091 "WHERE song_artist.idArtist = artist.idArtist )",
12092 false);
12093 else
12094 filter.AppendWhere(
12095 "EXISTS (SELECT 1 FROM song_artist "
12096 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)",
12097 false);
12099 else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
12100 filter.AppendWhere(
12101 "EXISTS (SELECT 1 FROM song_artist "
12102 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)",
12103 false);
12105 if (!settings.m_unscraped && !artistfoldersonly)
12106 filter.AppendWhere("lastScraped IS NOT NULL", true);
12108 std::string strSQL = "SELECT idArtist FROM artist";
12109 BuildSQL(strSQL, filter, strSQL);
12110 CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
12112 m_pDS->query(strSQL);
12113 int total = m_pDS->num_rows();
12114 int current = 0;
12115 artistIds.reserve(total);
12116 while (!m_pDS->eof())
12118 artistIds.push_back(m_pDS->fv("idArtist").get_asInt());
12119 m_pDS->next();
12121 m_pDS->close();
12123 for (const auto& artistId : artistIds)
12125 CArtist artist;
12126 // Include discography when not folders only
12127 GetArtist(artistId, artist, !artistfoldersonly);
12128 std::string strPath;
12129 std::map<std::string, std::string> artwork;
12130 if (settings.IsSingleFile())
12132 // Save artist to xml, and old path (common to music files) if it has one
12133 GetOldArtistPath(artist.idArtist, strPath);
12134 artist.Save(pMain, "artist", strPath);
12136 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
12137 { // append to the XML
12138 TiXmlElement additionalNode("art");
12139 for (const auto& i : artwork)
12140 XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
12141 pMain->LastChild()->InsertEndChild(additionalNode);
12144 else
12145 { // Separate files: artist.nfo and artwork in strFolder/<artist name>
12146 // Get unique folder allowing for duplicate names e.g. 2 x John Williams
12147 bool pathfound = GetArtistFolderName(artist, strPath);
12148 if (pathfound)
12150 strPath = URIUtils::AddFileToFolder(strFolder, strPath);
12151 pathfound = CDirectory::Exists(strPath);
12152 if (!pathfound)
12153 pathfound = CDirectory::Create(strPath);
12155 if (!pathfound)
12156 CLog::Log(LOGDEBUG,
12157 "CMusicDatabase::{} - Not exporting artist {} as could not create {}",
12158 __FUNCTION__, artist.strArtist, strPath);
12159 else
12161 if (!artistfoldersonly)
12163 if (!settings.m_skipnfo)
12165 artist.Save(pMain, "artist", strPath);
12166 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo");
12167 if (settings.m_overwrite || !CFile::Exists(nfoFile))
12169 if (!xmlDoc.SaveFile(nfoFile))
12171 CLog::Log(LOGERROR, "CMusicDatabase::{}: Artist nfo export failed! ('{}')",
12172 __FUNCTION__, nfoFile);
12173 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
12174 g_localizeStrings.Get(20302),
12175 CURL::GetRedacted(nfoFile));
12176 iFailCount++;
12180 if (settings.m_artwork)
12182 std::string savedArtfile;
12183 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
12185 for (const auto& art : artwork)
12187 if (art.first == "thumb")
12188 savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
12189 else
12190 savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
12191 CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
12192 settings.m_overwrite);
12196 xmlDoc.Clear();
12197 xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
12201 if ((current % 50) == 0 && progressDialog)
12203 progressDialog->SetLine(1, CVariant{artist.strArtist});
12204 progressDialog->SetPercentage(current * 100 / total);
12205 if (progressDialog->IsCanceled())
12206 return;
12208 current++;
12212 if (settings.IsSingleFile())
12214 std::string xmlFile = URIUtils::AddFileToFolder(
12215 strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
12216 if (CFile::Exists(xmlFile))
12217 xmlFile = URIUtils::AddFileToFolder(
12218 strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
12219 xmlDoc.SaveFile(xmlFile);
12221 CVariant data;
12222 data["file"] = xmlFile;
12223 if (iFailCount > 0)
12224 data["failcount"] = iFailCount;
12225 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport",
12226 data);
12229 catch (...)
12231 CLog::Log(LOGERROR, "CMusicDatabase::{} failed", __FUNCTION__);
12232 iFailCount++;
12235 if (progressDialog)
12236 progressDialog->Close();
12238 if (iFailCount > 0 && progressDialog)
12239 HELPERS::ShowOKDialogLines(
12240 CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)});
12243 bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog)
12247 // Export songs with some playback history
12248 std::string strSQL =
12249 "SELECT idSong, song.idAlbum, "
12250 "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, "
12251 "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, "
12252 "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating "
12253 "FROM song JOIN album on album.idAlbum = song.idAlbum "
12254 "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0";
12256 CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL);
12257 m_pDS->query(strSQL);
12259 int total = m_pDS->num_rows();
12260 int current = 0;
12261 while (!m_pDS->eof())
12263 TiXmlElement songElement("song");
12264 TiXmlNode* song = pNode->InsertEndChild(songElement);
12266 XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt());
12267 XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString());
12268 XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString());
12269 XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt());
12270 XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString());
12271 XMLUtils::SetString(song, "musicbrainztrackid",
12272 m_pDS->fv("strMusicBrainzTrackID").get_asString());
12273 XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt());
12274 XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString());
12275 XMLUtils::SetString(song, "musicbrainzalbumid",
12276 m_pDS->fv("strMusicBrainzAlbumID").get_asString());
12277 XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString());
12278 XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt());
12279 XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString());
12280 auto* rating = XMLUtils::SetString(
12281 song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat()));
12282 if (rating)
12283 rating->ToElement()->SetAttribute("max", 10);
12284 XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt());
12285 auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt());
12286 if (userrating)
12287 userrating->ToElement()->SetAttribute("max", 10);
12289 if ((current % 100) == 0 && progressDialog)
12291 progressDialog->SetLine(1, CVariant{m_pDS->fv("strAlbum").get_asString()});
12292 progressDialog->SetPercentage(current * 100 / total);
12293 if (progressDialog->IsCanceled())
12295 m_pDS->close();
12296 return false;
12299 current++;
12301 m_pDS->next();
12303 m_pDS->close();
12304 return true;
12306 catch (...)
12308 CLog::Log(LOGERROR, "{0} failed", __FUNCTION__);
12310 return false;
12313 void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog)
12317 if (nullptr == m_pDB)
12318 return;
12319 if (nullptr == m_pDS)
12320 return;
12322 CXBMCTinyXML xmlDoc;
12323 if (!xmlDoc.LoadFile(xmlFile) && progressDialog)
12325 HELPERS::ShowOKDialogLines(CVariant{20197}, CVariant{38354}); //"Unable to read xml file"
12326 return;
12329 TiXmlElement* root = xmlDoc.RootElement();
12330 if (!root)
12331 return;
12333 TiXmlElement* entry = root->FirstChildElement();
12334 int current = 0;
12335 int total = 0;
12336 int songtotal = 0;
12337 // Count the number of artists, albums and songs
12338 while (entry)
12340 if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 ||
12341 StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
12342 total++;
12343 else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
12344 songtotal++;
12346 entry = entry->NextSiblingElement();
12349 BeginTransaction();
12350 entry = root->FirstChildElement();
12351 while (entry)
12353 std::string strTitle;
12354 if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0)
12356 CArtist importedArtist;
12357 importedArtist.Load(entry);
12358 strTitle = importedArtist.strArtist;
12360 // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
12361 int idArtist = GetArtistByMatch(importedArtist);
12362 if (idArtist > -1)
12364 CArtist artist;
12365 GetArtist(idArtist, artist, true); // include discography
12366 artist.MergeScrapedArtist(importedArtist, true);
12367 UpdateArtist(artist);
12369 else
12370 CLog::Log(LOGDEBUG, "{} - Not import additional artist data as {} not found",
12371 __FUNCTION__, importedArtist.strArtist);
12372 current++;
12374 else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
12376 CAlbum importedAlbum;
12377 importedAlbum.Load(entry);
12378 strTitle = importedAlbum.strAlbum;
12379 // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
12380 int idAlbum = GetAlbumByMatch(importedAlbum);
12381 if (idAlbum > -1)
12383 CAlbum album;
12384 GetAlbum(idAlbum, album, true);
12385 album.MergeScrapedAlbum(importedAlbum, true);
12386 UpdateAlbum(album); //Will replace song artists if present in xml
12388 else
12389 CLog::Log(LOGDEBUG, "{} - Not import additional album data as {} not found", __FUNCTION__,
12390 importedAlbum.strAlbum);
12392 current++;
12394 entry = entry->NextSiblingElement();
12395 if (progressDialog && total)
12397 progressDialog->SetPercentage(current * 100 / total);
12398 progressDialog->SetLine(2, CVariant{std::move(strTitle)});
12399 progressDialog->Progress();
12400 if (progressDialog->IsCanceled())
12402 RollbackTransaction();
12403 return;
12407 CommitTransaction();
12409 // Import song playback history <song> entries found
12410 if (songtotal > 0)
12411 if (!ImportSongHistory(xmlFile, songtotal, progressDialog))
12412 return;
12414 CGUIComponent* gui = CServiceBroker::GetGUI();
12415 if (gui)
12416 gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
12418 catch (...)
12420 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
12421 RollbackTransaction();
12423 if (progressDialog)
12424 progressDialog->Close();
12427 bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile,
12428 const int total,
12429 CGUIDialogProgress* progressDialog)
12431 bool bHistSongExists = false;
12434 CXBMCTinyXML xmlDoc;
12435 if (!xmlDoc.LoadFile(xmlFile))
12436 return false;
12438 TiXmlElement* root = xmlDoc.RootElement();
12439 if (!root)
12440 return false;
12442 TiXmlElement* entry = root->FirstChildElement();
12443 int current = 0;
12445 if (progressDialog)
12447 progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history"
12448 progressDialog->SetLine(2, CVariant{""});
12451 // As can be many songs do in db, not song at a time which would be slow
12452 // Convert xml entries into a SQL bulk insert statement
12453 std::string strSQL;
12454 entry = root->FirstChildElement();
12455 while (entry)
12457 std::string strArtistDisp;
12458 std::string strTitle;
12459 int iTrack;
12460 std::string strFilename;
12461 std::string strMusicBrainzTrackID;
12462 std::string strAlbum;
12463 std::string strMusicBrainzAlbumID;
12464 std::string strAlbumArtistDisp;
12465 int iTimesplayed;
12466 std::string lastplayed;
12467 int iUserrating = 0;
12468 float fRating = 0.0;
12469 int iVotes;
12470 std::string strSQLSong;
12471 if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
12473 XMLUtils::GetString(entry, "artistdesc", strArtistDisp);
12474 XMLUtils::GetString(entry, "title", strTitle);
12475 XMLUtils::GetInt(entry, "track", iTrack);
12476 XMLUtils::GetString(entry, "filename", strFilename);
12477 XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID);
12478 XMLUtils::GetString(entry, "albumtitle", strAlbum);
12479 XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID);
12480 XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp);
12481 XMLUtils::GetInt(entry, "timesplayed", iTimesplayed);
12482 XMLUtils::GetString(entry, "lastplayed", lastplayed);
12483 const TiXmlElement* rElement = entry->FirstChildElement("rating");
12484 if (rElement)
12486 float rating = 0;
12487 float max_rating = 10;
12488 XMLUtils::GetFloat(entry, "rating", rating);
12489 if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
12490 rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
12491 if (rating > 10.f)
12492 rating = 10.f;
12493 fRating = rating;
12495 XMLUtils::GetInt(entry, "votes", iVotes);
12496 const TiXmlElement* userrating = entry->FirstChildElement("userrating");
12497 if (userrating)
12499 float rating = 0;
12500 float max_rating = 10;
12501 XMLUtils::GetFloat(entry, "userrating", rating);
12502 if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS &&
12503 max_rating >= 1)
12504 rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
12505 if (rating > 10.f)
12506 rating = 10.f;
12507 iUserrating = MathUtils::round_int(static_cast<double>(rating));
12510 strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack);
12511 strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(),
12512 strFilename.c_str());
12513 if (strMusicBrainzTrackID.empty())
12514 strSQLSong += PrepareSQL("NULL, ");
12515 else
12516 strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str());
12517 strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str());
12518 if (strMusicBrainzAlbumID.empty())
12519 strSQLSong += PrepareSQL("NULL, ");
12520 else
12521 strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str());
12522 strSQLSong += PrepareSQL("%d, ", iTimesplayed);
12523 if (lastplayed.empty())
12524 strSQLSong += PrepareSQL("NULL, ");
12525 else
12526 strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str());
12527 strSQLSong +=
12528 PrepareSQL("%.1f, %d, %d, -1, -1)", static_cast<double>(fRating), iVotes, iUserrating);
12530 if (current > 0)
12531 strSQLSong = ", " + strSQLSong;
12532 strSQL += strSQLSong;
12533 current++;
12536 entry = entry->NextSiblingElement();
12538 if ((current % 100) == 0 && progressDialog)
12540 progressDialog->SetPercentage(current * 100 / total);
12541 progressDialog->SetLine(3, CVariant{std::move(strTitle)});
12542 progressDialog->Progress();
12543 if (progressDialog->IsCanceled())
12544 return false;
12548 CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__,
12549 total);
12550 /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of
12551 song table using correlated subqueries to a temp table. An updatable join
12552 to temp table would work in MySQL but SQLite not support updatable joins.
12554 m_pDS->exec("CREATE TABLE HistSong ("
12555 "idSongSrc INTEGER primary key, "
12556 "strAlbum varchar(256), "
12557 "strMusicBrainzAlbumID text, "
12558 "strAlbumArtistDisp text, "
12559 "strArtistDisp text, strTitle varchar(512), "
12560 "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, "
12561 "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, "
12562 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
12563 "userrating INTEGER NOT NULL DEFAULT 0, "
12564 "idAlbum INTEGER, idSong INTEGER)");
12565 bHistSongExists = true;
12567 strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, "
12568 "strFileName, strMusicBrainzTrackID, "
12569 "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, "
12570 " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " +
12571 strSQL;
12572 m_pDS->exec(strSQL);
12574 if (progressDialog)
12576 progressDialog->SetLine(2, CVariant{38351}); //"Matching data"
12577 progressDialog->SetLine(3, CVariant{""});
12578 progressDialog->Progress();
12579 if (progressDialog->IsCanceled())
12581 m_pDS->exec("DROP TABLE HistSong");
12582 return false;
12586 BeginTransaction();
12587 // Match albums first on mbid then artist string and album title, setting idAlbum
12588 // mbid is unique so subquery can only return one result at most
12589 strSQL = "UPDATE HistSong "
12590 "SET idAlbum = (SELECT album.idAlbum FROM album "
12591 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) "
12592 "WHERE EXISTS(SELECT 1 FROM album "
12593 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0";
12594 m_pDS->exec(strSQL);
12596 // Can only be one album with same title and artist(s) and no mbid.
12597 // But could have 2 releases one with and one without mbid, match up those without mbid
12598 strSQL = "UPDATE HistSong "
12599 "SET idAlbum = (SELECT album.idAlbum FROM album "
12600 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12601 "AND HistSong.strAlbum = album.strAlbum "
12602 "AND album.strMusicBrainzAlbumID IS NULL "
12603 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
12604 "WHERE EXISTS(SELECT 1 FROM album "
12605 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12606 "AND HistSong.strAlbum = album.strAlbum "
12607 "AND album.strMusicBrainzAlbumID IS NULL "
12608 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
12609 "AND idAlbum < 0";
12610 m_pDS->exec(strSQL);
12612 // Try match rest by title and artist(s), prioritise one without mbid
12613 // Target could have multiple releases - with mbid (non-matching) or one without mbid
12614 strSQL = "UPDATE HistSong "
12615 "SET idAlbum = (SELECT album.idAlbum FROM album "
12616 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12617 "AND HistSong.strAlbum = album.strAlbum "
12618 "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
12619 "WHERE EXISTS(SELECT 1 FROM album "
12620 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12621 "AND HistSong.strAlbum = album.strAlbum) "
12622 "AND idAlbum < 0";
12623 m_pDS->exec(strSQL);
12624 if (progressDialog)
12626 progressDialog->Progress();
12627 if (progressDialog->IsCanceled())
12629 RollbackTransaction();
12630 m_pDS->exec("DROP TABLE HistSong");
12631 return false;
12635 // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong
12636 strSQL = "UPDATE HistSong "
12637 "SET idSong = (SELECT idsong FROM song "
12638 "WHERE HistSong.idAlbum = song.idAlbum AND "
12639 "HistSong.iTrack = song.iTrack AND "
12640 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) "
12641 "WHERE EXISTS(SELECT 1 FROM song "
12642 "WHERE HistSong.idAlbum = song.idAlbum AND "
12643 "HistSong.iTrack = song.iTrack AND "
12644 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0";
12645 m_pDS->exec(strSQL);
12647 // An album can have more than one song with same track and title (although idAlbum, track and
12648 // title is often unique), but not using filename as an identifier to allow for import of song
12649 // history for renamed files. It is about song playback not file playback.
12650 // Pick the first
12651 strSQL = "UPDATE HistSong "
12652 "SET idSong = (SELECT idsong FROM song "
12653 "WHERE HistSong.idAlbum = song.idAlbum AND "
12654 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
12655 "WHERE EXISTS(SELECT 1 FROM song "
12656 "WHERE HistSong.idAlbum = song.idAlbum AND "
12657 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0";
12658 m_pDS->exec(strSQL);
12660 CommitTransaction();
12661 if (progressDialog)
12663 progressDialog->Progress();
12664 if (progressDialog->IsCanceled())
12666 m_pDS->exec("DROP TABLE HistSong");
12667 return false;
12671 // Create an index to speed up the updates
12672 m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)");
12674 // Log how many songs matched
12675 int unmatched = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS);
12676 CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__,
12677 total - unmatched, total);
12679 if (progressDialog)
12681 progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history"
12682 progressDialog->Progress();
12683 if (progressDialog->IsCanceled())
12685 m_pDS->exec("DROP TABLE HistSong"); // Drops index too
12686 return false;
12690 /* Update song table using the song ids we have matched.
12691 Use correlated subqueries as SQLite does not support updatable joins.
12692 MySQL requires HistSong table not to be defined temporary for this.
12695 BeginTransaction();
12696 // Times played and last played date(when count is greater)
12697 strSQL = "UPDATE song SET iTimesPlayed = "
12698 "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), "
12699 "lastplayed = "
12700 "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) "
12701 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12702 "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)";
12703 m_pDS->exec(strSQL);
12705 // User rating
12706 strSQL = "UPDATE song SET userrating = "
12707 "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) "
12708 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12709 "HistSong.idSong = song.idSong AND HistSong.userrating > 0)";
12710 m_pDS->exec(strSQL);
12712 // Rating and votes
12713 strSQL = "UPDATE song SET rating = "
12714 "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), "
12715 "votes = "
12716 "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) "
12717 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12718 "HistSong.idSong = song.idSong AND HistSong.rating > 0)";
12719 m_pDS->exec(strSQL);
12721 if (progressDialog)
12723 progressDialog->Progress();
12724 if (progressDialog->IsCanceled())
12726 RollbackTransaction();
12727 m_pDS->exec("DROP TABLE HistSong");
12728 return false;
12731 CommitTransaction();
12733 // Tidy up temp table (index also removed)
12734 m_pDS->exec("DROP TABLE HistSong");
12735 // Compact db to recover space as had to add/drop actual table
12736 if (progressDialog)
12738 progressDialog->SetLine(2, CVariant{331});
12739 progressDialog->Progress();
12741 Compress(false);
12743 // Write event log entry
12744 // "Importing song history {1} of {2} songs matched", total - unmatched, total)
12745 std::string strLine =
12746 StringUtils::Format(g_localizeStrings.Get(38353), total - unmatched, total);
12748 auto eventLog = CServiceBroker::GetEventLog();
12749 if (eventLog)
12750 eventLog->Add(EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information)));
12752 return true;
12754 catch (...)
12756 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
12757 RollbackTransaction();
12758 if (bHistSongExists)
12759 m_pDS->exec("DROP TABLE HistSong");
12761 return false;
12764 void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist)
12766 const std::string itemSeparator =
12767 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
12769 item.SetProperty("artist_sortname", artist.strSortName);
12770 item.SetProperty("artist_type", artist.strType);
12771 item.SetProperty("artist_gender", artist.strGender);
12772 item.SetProperty("artist_disambiguation", artist.strDisambiguation);
12773 item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator));
12774 item.SetProperty("artist_instrument_array", artist.instruments);
12775 item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator));
12776 item.SetProperty("artist_style_array", artist.styles);
12777 item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator));
12778 item.SetProperty("artist_mood_array", artist.moods);
12779 item.SetProperty("artist_born", artist.strBorn);
12780 item.SetProperty("artist_formed", artist.strFormed);
12781 item.SetProperty("artist_description", artist.strBiography);
12782 item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator));
12783 item.SetProperty("artist_genre_array", artist.genre);
12784 item.SetProperty("artist_died", artist.strDied);
12785 item.SetProperty("artist_disbanded", artist.strDisbanded);
12786 item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator));
12787 item.SetProperty("artist_yearsactive_array", artist.yearsActive);
12790 void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album)
12792 const std::string itemSeparator =
12793 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
12795 item.SetProperty("album_description", album.strReview);
12796 item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator));
12797 item.SetProperty("album_theme_array", album.themes);
12798 item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator));
12799 item.SetProperty("album_mood_array", album.moods);
12800 item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator));
12801 item.SetProperty("album_style_array", album.styles);
12802 item.SetProperty("album_type", album.strType);
12803 item.SetProperty("album_label", album.strLabel);
12804 item.SetProperty("album_artist", album.GetAlbumArtistString());
12805 item.SetProperty("album_artist_array", album.GetAlbumArtist());
12806 item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator));
12807 item.SetProperty("album_genre_array", album.genre);
12808 item.SetProperty("album_title", album.strAlbum);
12809 if (album.fRating > 0)
12810 item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating));
12811 if (album.iUserrating > 0)
12812 item.SetProperty("album_userrating", album.iUserrating);
12813 if (album.iVotes > 0)
12814 item.SetProperty("album_votes", album.iVotes);
12816 item.SetProperty("album_isboxset", album.bBoxedSet);
12817 item.SetProperty("album_totaldiscs", album.iTotalDiscs);
12818 item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType));
12819 item.SetProperty("album_duration",
12820 StringUtils::SecondsToTimeString(album.iAlbumDuration,
12821 static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS)));
12824 void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item)
12826 if (!item.HasMusicInfoTag())
12827 return;
12828 // May already have song artist ids as item property set when data read from
12829 // db, but check property is valid array (scripts could set item properties
12830 // incorrectly), otherwise try to fetch artist by name.
12831 int idArtist = -1;
12832 if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray())
12834 CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array();
12835 idArtist = static_cast<int>(varid->asInteger());
12837 else
12838 idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString());
12839 if (idArtist > -1)
12841 CArtist artist;
12842 if (GetArtist(idArtist, artist))
12843 SetPropertiesFromArtist(item, artist);
12845 int idAlbum = item.GetMusicInfoTag()->GetAlbumId();
12846 if (idAlbum <= 0)
12847 idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(),
12848 item.GetMusicInfoTag()->GetArtistString());
12849 if (idAlbum > -1)
12851 CAlbum album;
12852 if (GetAlbum(idAlbum, album, false))
12853 SetPropertiesFromAlbum(item, album);
12857 void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType)
12859 std::string strSQL;
12862 if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong)
12863 return;
12864 if (nullptr == m_pDB)
12865 return;
12866 if (nullptr == m_pDS)
12867 return;
12869 // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
12870 // e.g. when artwork for item is changed from info dialog but not item details.
12871 // Use SQL UPDATE that does not change record data.
12872 if (mediaType == MediaTypeArtist)
12873 strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId);
12874 else if (mediaType == MediaTypeAlbum)
12875 strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId);
12876 else // MediaTypeSong
12877 strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId);
12878 m_pDS->exec(strSQL);
12880 catch (...)
12882 CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__,
12883 mediaId, mediaType, strSQL);
12887 void CMusicDatabase::SetArtForItem(int mediaId,
12888 const std::string& mediaType,
12889 const std::map<std::string, std::string>& art)
12891 for (const auto& i : art)
12892 SetArtForItem(mediaId, mediaType, i.first, i.second);
12895 void CMusicDatabase::SetArtForItem(int mediaId,
12896 const std::string& mediaType,
12897 const std::string& artType,
12898 const std::string& url)
12902 if (nullptr == m_pDB)
12903 return;
12904 if (nullptr == m_pDS)
12905 return;
12907 // don't set <foo>.<bar> art types - these are derivative types from parent items
12908 if (artType.find('.') != std::string::npos)
12909 return;
12911 std::string sql = PrepareSQL("SELECT art_id FROM art "
12912 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
12913 mediaId, mediaType.c_str(), artType.c_str());
12914 m_pDS->query(sql);
12915 if (!m_pDS->eof())
12916 { // update
12917 int artId = m_pDS->fv(0).get_asInt();
12918 m_pDS->close();
12919 sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
12920 m_pDS->exec(sql);
12922 else
12923 { // insert
12924 m_pDS->close();
12925 sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
12926 "VALUES (%d, '%s', '%s', '%s')",
12927 mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
12928 m_pDS->exec(sql);
12931 catch (...)
12933 CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType,
12934 artType, url);
12938 bool CMusicDatabase::GetArtForItem(
12939 int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader>& art)
12941 std::string strSQL;
12944 if (!(songId > 0 || albumId > 0 || artistId > 0))
12945 return false;
12946 if (nullptr == m_pDB)
12947 return false;
12948 if (nullptr == m_pDS2)
12949 return false; // using dataset 2 as we're likely called in loops on dataset 1
12951 Filter filter;
12952 if (songId > 0)
12953 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong));
12954 if (albumId > 0)
12955 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum),
12956 false);
12957 if (artistId > 0)
12958 filter.AppendWhere(
12959 PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false);
12961 strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder "
12962 "FROM art";
12963 if (!BuildSQL(strSQL, filter, strSQL))
12964 return false;
12966 if (!(artistId > 0))
12968 // Artist ID unknown, so lookup album artist for albums and songs
12969 std::string strSQL2;
12970 if (albumId > 0)
12972 //Album ID known, so use it to look up album artist(s)
12973 strSQL2 = PrepareSQL(
12974 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12975 "url, album_artist.iOrder as iorder FROM art "
12976 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12977 "WHERE album_artist.idAlbum = %i ",
12978 MediaTypeArtist, albumId);
12979 if (bPrimaryArtist)
12980 strSQL2 += "AND album_artist.iOrder = 0";
12982 strSQL = strSQL + " UNION " + strSQL2;
12984 if (songId > 0)
12986 if (albumId < 0)
12988 //Album ID unknown, so get from song to look up album artist(s)
12989 strSQL2 = PrepareSQL(
12990 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12991 "url, album_artist.iOrder as iorder FROM art "
12992 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12993 "JOIN song ON song.idAlbum = album_artist.idAlbum "
12994 "WHERE song.idSong = %i ",
12995 MediaTypeArtist, songId);
12996 if (bPrimaryArtist)
12997 strSQL2 += "AND album_artist.iOrder = 0";
12999 strSQL = strSQL + " UNION " + strSQL2;
13002 // Artist ID unknown, so lookup artist for songs (could be different from album artist)
13003 strSQL2 = PrepareSQL(
13004 "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
13005 "url, song_artist.iOrder as iorder FROM art "
13006 "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
13007 "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
13008 MediaTypeArtist, songId, ROLE_ARTIST);
13009 if (bPrimaryArtist)
13010 strSQL2 += "AND song_artist.iOrder = 0";
13012 strSQL = strSQL + " UNION " + strSQL2;
13015 if (songId > 0 && albumId < 0)
13017 //Album ID unknown, so get from song to look up album art
13018 std::string strSQL2;
13019 strSQL2 = PrepareSQL("SELECT art_id, media_id, media_type, type, '' as prefix, "
13020 "url, 0 as iorder FROM art "
13021 "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
13022 "WHERE song.idSong = %i ",
13023 MediaTypeAlbum, songId);
13024 strSQL = strSQL + " UNION " + strSQL2;
13027 m_pDS2->query(strSQL);
13028 while (!m_pDS2->eof())
13030 ArtForThumbLoader artitem;
13031 artitem.artType = m_pDS2->fv("type").get_asString();
13032 artitem.mediaType = m_pDS2->fv("media_type").get_asString();
13033 artitem.prefix = m_pDS2->fv("prefix").get_asString();
13034 artitem.url = m_pDS2->fv("url").get_asString();
13035 int iOrder = m_pDS2->fv("iorder").get_asInt();
13036 // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
13037 if (iOrder > 0)
13038 artitem.prefix += m_pDS2->fv("iorder").get_asString();
13040 art.emplace_back(artitem);
13041 m_pDS2->next();
13043 m_pDS2->close();
13044 return !art.empty();
13046 catch (...)
13048 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strSQL);
13050 return false;
13053 bool CMusicDatabase::GetArtForItem(int mediaId,
13054 const std::string& mediaType,
13055 std::map<std::string, std::string>& art)
13059 if (nullptr == m_pDB)
13060 return false;
13061 if (nullptr == m_pDS2)
13062 return false; // using dataset 2 as we're likely called in loops on dataset 1
13064 std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'",
13065 mediaId, mediaType.c_str());
13066 m_pDS2->query(sql);
13067 while (!m_pDS2->eof())
13069 art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
13070 m_pDS2->next();
13072 m_pDS2->close();
13073 return !art.empty();
13075 catch (...)
13077 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId);
13079 return false;
13082 std::string CMusicDatabase::GetArtForItem(int mediaId,
13083 const std::string& mediaType,
13084 const std::string& artType)
13086 std::string query = PrepareSQL("SELECT url FROM art "
13087 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
13088 mediaId, mediaType.c_str(), artType.c_str());
13089 return GetSingleValue(query, m_pDS2);
13092 bool CMusicDatabase::RemoveArtForItem(int mediaId,
13093 const MediaType& mediaType,
13094 const std::string& artType)
13096 return ExecuteQuery(PrepareSQL("DELETE FROM art "
13097 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
13098 mediaId, mediaType.c_str(), artType.c_str()));
13101 bool CMusicDatabase::RemoveArtForItem(int mediaId,
13102 const MediaType& mediaType,
13103 const std::set<std::string>& artTypes)
13105 bool result = true;
13106 for (const auto& i : artTypes)
13107 result &= RemoveArtForItem(mediaId, mediaType, i);
13109 return result;
13112 bool CMusicDatabase::GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes)
13116 if (nullptr == m_pDB)
13117 return false;
13118 if (nullptr == m_pDS)
13119 return false;
13121 std::string strSQL =
13122 PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
13124 if (!m_pDS->query(strSQL))
13125 return false;
13126 int iRowsFound = m_pDS->num_rows();
13127 if (iRowsFound == 0)
13129 m_pDS->close();
13130 return false;
13133 while (!m_pDS->eof())
13135 artTypes.emplace_back(m_pDS->fv(0).get_asString());
13136 m_pDS->next();
13138 m_pDS->close();
13139 return true;
13141 catch (...)
13143 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType);
13145 return false;
13148 std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId,
13149 const MediaType& mediaType)
13151 CScraperUrl thumbURL;
13152 if (mediaType == MediaTypeArtist)
13154 CArtist artist;
13155 if (GetArtist(mediaId, artist))
13156 thumbURL = artist.thumbURL;
13158 else if (mediaType == MediaTypeAlbum)
13160 CAlbum album;
13161 if (GetAlbum(mediaId, album))
13162 thumbURL = album.thumbURL;
13165 std::vector<std::string> result;
13166 for (const auto& urlEntry : thumbURL.GetUrls())
13168 std::string artType = urlEntry.m_aspect;
13169 if (artType.empty())
13170 artType = "thumb";
13171 if (std::find(result.begin(), result.end(), artType) == result.end())
13172 result.push_back(artType);
13174 return result;
13177 std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem(
13178 int mediaId, const MediaType& mediaType, const std::string& artType)
13180 CScraperUrl thumbURL;
13181 if (mediaType == MediaTypeArtist)
13183 CArtist artist;
13184 if (GetArtist(mediaId, artist))
13185 thumbURL = artist.thumbURL;
13187 else if (mediaType == MediaTypeAlbum)
13189 CAlbum album;
13190 if (GetAlbum(mediaId, album))
13191 thumbURL = album.thumbURL;
13194 std::vector<CScraperUrl::SUrlEntry> result;
13195 for (auto urlEntry : thumbURL.GetUrls())
13197 if (urlEntry.m_aspect.empty())
13198 urlEntry.m_aspect = "thumb";
13199 if (artType.empty() || urlEntry.m_aspect == artType)
13200 result.push_back(urlEntry);
13202 return result;
13205 int CMusicDatabase::GetOrderFilter(const std::string& type,
13206 const SortDescription& sorting,
13207 Filter& filter)
13209 // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
13210 int iFieldsAdded = 0;
13211 filter.fields.clear(); // remove "*"
13212 std::vector<std::string> orderfields;
13213 std::string DESC;
13215 if (sorting.sortOrder == SortOrderDescending)
13216 DESC = " DESC";
13218 if (sorting.sortBy == SortByRandom)
13219 orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
13220 else
13222 FieldList fields;
13223 SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields);
13224 for (const auto& it : fields)
13226 std::string strField;
13227 if (it == FieldYear)
13228 strField = "iYear";
13229 else
13230 strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect);
13231 if (!strField.empty())
13232 orderfields.emplace_back(strField);
13236 // Convert field names into order by statement elements
13237 for (auto& name : orderfields)
13239 //Add field for adjusted name sorting using sort name and ignoring articles
13240 std::string sortSQL;
13241 if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist"))
13243 if (StringUtils::EndsWith(name, "strArtists"))
13244 sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort");
13245 else
13246 sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName");
13247 if (!sortSQL.empty())
13249 name = "artistsortname";
13250 filter.AppendField(sortSQL); // Add artistsortname as scalar query field
13251 iFieldsAdded++;
13253 // Natural number case-insensitive sort
13254 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
13256 else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle"))
13258 sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, "");
13259 if (!sortSQL.empty())
13261 name = "titlesortname";
13262 filter.AppendField(sortSQL); // Add sortname as scalar query field
13263 iFieldsAdded++;
13265 // Natural number case-insensitive sort
13266 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
13268 else if (StringUtils::EndsWith(name, "strGenres"))
13269 // Natural number case-insensitive sort
13270 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
13271 else
13272 filter.AppendOrder(name + DESC);
13274 return iFieldsAdded;
13277 bool CMusicDatabase::GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting)
13279 if (!musicUrl.IsValid())
13280 return false;
13282 std::string type = musicUrl.GetType();
13283 const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
13285 // Check for playlist rules first, they may contain role criteria
13286 bool hasRoleRules = false;
13288 auto option = options.find("xsp");
13289 if (option != options.end())
13291 PLAYLIST::CSmartPlaylist xsp;
13292 if (!xsp.LoadFromJson(option->second.asString()))
13293 return false;
13295 std::set<std::string> playlists;
13296 std::string xspWhere;
13297 xspWhere = xsp.GetWhereClause(*this, playlists);
13298 hasRoleRules = xsp.GetType() == "artists" &&
13299 xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos;
13301 // Check if the filter playlist matches the item type
13302 // Allow for grouping name like "originalyears" and type "years"
13303 if (xsp.GetType() == type ||
13304 (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed()))
13306 filter.AppendWhere(xspWhere);
13308 if (xsp.GetLimit() > 0)
13309 sorting.limitEnd = xsp.GetLimit();
13310 if (xsp.GetOrder() != SortByNone)
13311 sorting.sortBy = xsp.GetOrder();
13312 sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
13313 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
13314 CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
13315 sorting.sortAttributes = SortAttributeIgnoreArticle;
13319 //Process role options, common to artist and album type filtering
13320 int idRole = 1; // Default restrict song_artist to "artists" only, no other roles.
13321 option = options.find("roleid");
13322 if (option != options.end())
13323 idRole = static_cast<int>(option->second.asInteger());
13324 else
13326 option = options.find("role");
13327 if (option != options.end())
13329 if (option->second.asString() == "all" || option->second.asString() == "%")
13330 idRole = -1000; //All roles
13331 else
13332 idRole = GetRoleByName(option->second.asString());
13335 if (hasRoleRules)
13337 // Get Role from role rule(s) here.
13338 // But that requires much change, so for now get all roles as better than none
13339 idRole = -1000; //All roles
13342 std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role
13343 if (idRole > 0)
13344 strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole);
13346 int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1;
13347 int idDisc = -1;
13348 int idSource = -1;
13349 bool albumArtistsOnly = false;
13350 bool useOriginalYear = false;
13351 std::string artistname;
13353 // Process useoriginalyear option, setting overridden by option
13354 useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
13355 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
13356 option = options.find("useoriginalyear");
13357 if (option != options.end())
13358 useOriginalYear = option->second.asBoolean();
13360 // Process albumartistsonly option
13361 option = options.find("albumartistsonly");
13362 if (option != options.end())
13363 albumArtistsOnly = option->second.asBoolean();
13365 // Process genre option
13366 option = options.find("genreid");
13367 if (option != options.end())
13368 idGenre = static_cast<int>(option->second.asInteger());
13369 else
13371 option = options.find("genre");
13372 if (option != options.end())
13373 idGenre = GetGenreByName(option->second.asString());
13376 // Process source option
13377 option = options.find("sourceid");
13378 if (option != options.end())
13379 idSource = static_cast<int>(option->second.asInteger());
13380 else
13382 option = options.find("source");
13383 if (option != options.end())
13384 idSource = GetSourceByName(option->second.asString());
13387 // Process album option
13388 option = options.find("albumid");
13389 if (option != options.end())
13390 idAlbum = static_cast<int>(option->second.asInteger());
13391 else
13393 option = options.find("album");
13394 if (option != options.end())
13395 idAlbum = GetAlbumByName(option->second.asString());
13398 // Process artist option
13399 option = options.find("artistid");
13400 if (option != options.end())
13401 idArtist = static_cast<int>(option->second.asInteger());
13402 else
13404 option = options.find("artist");
13405 if (option != options.end())
13407 idArtist = GetArtistByName(option->second.asString());
13408 if (idArtist == -1)
13409 { // not found with that name, or more than one found as artist name is not unique
13410 artistname = option->second.asString();
13415 // Process song option
13416 option = options.find("songid");
13417 if (option != options.end())
13418 idSong = static_cast<int>(option->second.asInteger());
13420 if (type == "artists")
13422 if (!hasRoleRules)
13423 { // Not an "artists" smart playlist with roles rules, so get filter from options
13424 if (idArtist > 0)
13425 filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist));
13426 else if (idAlbum > 0)
13427 filter.AppendWhere(
13428 PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
13429 "WHERE album_artist.idAlbum = %i)",
13430 idAlbum));
13431 else if (idSong > 0)
13433 filter.AppendWhere(
13434 PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
13435 "WHERE song_artist.idSong = %i %s)",
13436 idSong, strRoleSQL.c_str()));
13438 else
13439 { /*
13440 Process idRole, idGenre, idSource and albumArtistsOnly options
13442 For artists these rules are combined because they apply via album and song
13443 and so we need to ensure all criteria are met via the same album or song.
13444 1) Some artists may be only album artists, so for all artists (with linked
13445 albums or songs) we need to check both album_artist and song_artist tables.
13446 2) Role is determined from song_artist table, so even if looking for album artists
13447 only we find those that also have a specific role e.g. which album artist is a
13448 composer of songs in that album, from entries in the song_artist table.
13449 a) Role < -1 is used to indicate that all roles are wanted.
13450 b) When not album artists only and a specific role wanted then only the song_artist
13451 table is checked.
13452 c) When album artists only and role = 1 (an "artist") then only the album_artist
13453 table is checked.
13455 std::string albumArtistSQL, songArtistSQL;
13456 ExistsSubQuery albumArtistSub("album_artist",
13457 "album_artist.idArtist = artistview.idArtist");
13458 // Prepare album artist subquery SQL
13459 if (idSource > 0)
13461 if (idRole == 1 && idGenre < 0)
13463 albumArtistSub.AppendJoin(
13464 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
13465 albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
13467 else
13469 albumArtistSub.AppendWhere(
13470 PrepareSQL("EXISTS(SELECT 1 FROM album_source "
13471 "WHERE album_source.idSource = %i "
13472 "AND album_source.idAlbum = album_artist.idAlbum)",
13473 idSource));
13476 if (idRole <= 1 && idGenre > 0)
13477 { // Check genre of songs of album using nested subquery
13478 std::string strGenre =
13479 PrepareSQL("EXISTS(SELECT 1 FROM song "
13480 "JOIN song_genre ON song_genre.idSong = song.idSong "
13481 "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)",
13482 idGenre);
13483 albumArtistSub.AppendWhere(strGenre);
13486 // Prepare song artist subquery SQL
13487 ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
13488 if (idRole > 0)
13489 songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
13490 if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1)
13492 songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
13493 "JOIN song_genre ON song_genre.idSong = song.idSong "
13494 "WHERE song.idSong = song_artist.idSong "
13495 "AND song_genre.idGenre = %i "
13496 "AND EXISTS(SELECT 1 FROM album_source "
13497 "WHERE album_source.idSource = %i "
13498 "AND album_source.idAlbum = song.idAlbum))",
13499 idGenre, idSource));
13501 else
13503 if (idGenre > 0)
13505 songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
13506 songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
13508 if (idSource > 0 && !albumArtistsOnly)
13510 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13511 songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
13512 songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
13514 if (idRole > 1 && albumArtistsOnly)
13515 { // Album artists only with role, check AND in album_artist for album of song
13516 // using nested subquery correlated with album_artist
13517 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13518 songArtistSub.param = "song_artist.idArtist = album_artist.idArtist";
13519 songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum");
13523 // Build filter clause from subqueries
13524 if (idRole > 1 && albumArtistsOnly)
13525 { // Album artists only with role, check AND in album_artist for album of song
13526 // using nested subquery correlated with album_artist
13527 songArtistSub.BuildSQL(songArtistSQL);
13528 albumArtistSub.AppendWhere(songArtistSQL);
13529 albumArtistSub.BuildSQL(albumArtistSQL);
13530 filter.AppendWhere(albumArtistSQL);
13532 else
13534 songArtistSub.BuildSQL(songArtistSQL);
13535 albumArtistSub.BuildSQL(albumArtistSQL);
13536 if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
13537 { // Artist contributing to songs, any role, check OR album artist too
13538 // as artists can be just album artists but not song artists
13539 filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
13541 else if (idRole > 1)
13543 // Artist contributes that role (not albmartistsonly as already handled)
13544 filter.AppendWhere(songArtistSQL);
13546 else // idRole = 1 and albumArtistsOnly
13547 { // Only look at album artists, not albums where artist features on songs
13548 filter.AppendWhere(albumArtistSQL);
13553 // remove the null string
13554 filter.AppendWhere("artistview.strArtist != ''");
13556 else if (type == "albums")
13558 option = options.find("year");
13559 if (option != options.end())
13561 if (!useOriginalYear)
13562 filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
13563 option->second.asString().c_str()));
13564 else
13565 filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
13566 option->second.asString().c_str()));
13568 option = options.find("compilation");
13569 if (option != options.end())
13570 filter.AppendWhere(
13571 PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
13573 option = options.find("boxset");
13574 if (option != options.end())
13575 filter.AppendWhere(
13576 PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
13578 if (idSource > 0)
13579 filter.AppendWhere(PrepareSQL(
13580 "EXISTS(SELECT 1 FROM album_source "
13581 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
13582 idSource));
13584 // Process artist, role and genre options together as song subquery to filter those
13585 // albums that have songs with both that artist and genre
13586 std::string albumArtistSQL, songArtistSQL, genreSQL;
13587 ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum");
13588 genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
13589 genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
13590 ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
13591 ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
13592 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13594 if (idArtist > 0)
13596 songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist));
13597 albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist));
13599 else if (!artistname.empty())
13600 { // Artist name is not unique, so could get albums or songs from more than one.
13601 songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
13602 songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
13604 albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
13605 albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
13607 if (idRole > 0)
13608 songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
13609 if (idGenre > 0)
13611 songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
13612 songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
13615 if (idArtist > 0 || !artistname.empty())
13617 if (idRole <= 1 && idGenre > 0)
13618 { // Check genre of songs of album using nested subquery
13619 genreSub.BuildSQL(genreSQL);
13620 albumArtistSub.AppendWhere(genreSQL);
13622 if (idRole > 1 && albumArtistsOnly)
13623 { // Album artists only with role, check AND in album_artist for same song
13624 // using nested subquery correlated with album_artist
13625 songArtistSub.param = "song.idAlbum = album_artist.idAlbum";
13626 songArtistSub.BuildSQL(songArtistSQL);
13627 albumArtistSub.AppendWhere(songArtistSQL);
13628 albumArtistSub.BuildSQL(albumArtistSQL);
13629 filter.AppendWhere(albumArtistSQL);
13631 else
13633 songArtistSub.BuildSQL(songArtistSQL);
13634 albumArtistSub.BuildSQL(albumArtistSQL);
13635 if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
13636 { // Artist contributing to songs, any role, check OR album artist too
13637 // as artists can be just album artists but not song artists
13638 filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
13640 else if (idRole > 1)
13641 { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
13642 filter.AppendWhere(songArtistSQL);
13644 else // idRole = 1 and albumArtistsOnly
13645 { // Only look at album artists, not albums where artist features on songs
13646 // This may want to be a separate option so you can choose to see all the albums where that artist
13647 // appears on one or more songs without having to list all song artists in the artists node.
13648 filter.AppendWhere(albumArtistSQL);
13652 else
13653 { // No artist given
13654 if (idGenre > 0)
13655 { // Have genre option but not artist
13656 genreSub.param = "song.idAlbum = albumview.idAlbum";
13657 genreSub.BuildSQL(genreSQL);
13658 filter.AppendWhere(genreSQL);
13660 // Exclude any single albums (aka empty tagged albums)
13661 // This causes "albums" media filter artist selection to only offer album artists
13662 option = options.find("show_singles");
13663 if (option == options.end() || !option->second.asBoolean())
13664 filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'",
13665 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
13668 else if (type == "discs")
13670 if (idAlbum > 0)
13671 filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum));
13672 else
13674 option = options.find("year");
13675 if (option != options.end())
13677 if (!useOriginalYear)
13678 filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
13679 option->second.asString().c_str()));
13680 else
13681 filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
13682 option->second.asString().c_str()));
13685 option = options.find("compilation");
13686 if (option != options.end())
13687 filter.AppendWhere(
13688 PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
13690 option = options.find("boxset");
13691 if (option != options.end())
13692 filter.AppendWhere(
13693 PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
13695 if (idSource > 0)
13696 filter.AppendWhere(PrepareSQL(
13697 "EXISTS(SELECT 1 FROM album_source "
13698 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
13699 idSource));
13701 option = options.find("discid");
13702 if (option != options.end())
13703 filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger()));
13705 option = options.find("disctitle");
13706 if (option != options.end())
13707 filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str()));
13709 if (idGenre > 0)
13710 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
13711 "song.idSong AND song_genre.idGenre = %i)",
13712 idGenre));
13714 std::string songArtistClause, albumArtistClause;
13715 if (idArtist > 0)
13717 songArtistClause =
13718 PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
13719 "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
13720 idArtist, strRoleSQL.c_str());
13721 albumArtistClause =
13722 PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
13723 "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
13724 idArtist);
13726 else if (!artistname.empty())
13727 { // Artist name is not unique, so could get songs from more than one.
13728 songArtistClause = PrepareSQL(
13729 "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
13730 "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
13731 artistname.c_str(), strRoleSQL.c_str());
13732 albumArtistClause =
13733 PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
13734 "album_artist.idArtist "
13735 "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
13736 artistname.c_str());
13739 // Process artist name or id option
13740 if (!songArtistClause.empty())
13742 if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
13743 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13744 else if (idRole > 1)
13746 if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
13747 filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
13748 else // songs where artist contributes that role.
13749 filter.AppendWhere(songArtistClause);
13751 else
13753 if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
13754 filter.AppendWhere(albumArtistClause);
13755 else // Artist is song artist or album artist
13756 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13760 else if (type == "songs" || type == "singles")
13762 option = options.find("singles");
13763 if (option != options.end())
13764 filter.AppendWhere(PrepareSQL(
13765 "songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
13766 option->second.asBoolean() ? "" : "NOT ",
13767 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
13769 // When have idAlbum skip year, compilation, boxset criteria as already applied via album
13770 if (idAlbum < 0)
13772 option = options.find("year");
13773 if (option != options.end())
13775 if (!useOriginalYear)
13776 filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
13777 option->second.asString().c_str()));
13778 else
13779 filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
13780 option->second.asString().c_str()));
13782 option = options.find("compilation");
13783 if (option != options.end())
13784 filter.AppendWhere(
13785 PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
13787 option = options.find("boxset");
13788 if (option != options.end())
13789 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
13790 "songview.idAlbum AND bBoxedSet = %i)",
13791 option->second.asBoolean() ? 1 : 0));
13794 option = options.find("discid");
13795 if (option != options.end())
13796 idDisc = static_cast<int>(option->second.asInteger());
13798 option = options.find("disctitle");
13799 if (option != options.end())
13800 filter.AppendWhere(
13801 PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str()));
13803 if (idSong > 0)
13804 filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong));
13806 if (idAlbum > 0)
13807 filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum));
13809 if (idDisc > 0)
13810 filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc));
13812 if (idGenre > 0)
13813 filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre "
13814 "WHERE song_genre.idGenre = %i)",
13815 idGenre));
13817 if (idSource > 0)
13818 filter.AppendWhere(PrepareSQL(
13819 "EXISTS(SELECT 1 FROM album_source "
13820 "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)",
13821 idSource));
13823 std::string songArtistClause, albumArtistClause;
13824 if (idArtist > 0)
13826 songArtistClause =
13827 PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
13828 "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
13829 idArtist, strRoleSQL.c_str());
13830 albumArtistClause = PrepareSQL(
13831 "EXISTS (SELECT 1 FROM album_artist "
13832 "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
13833 idArtist);
13835 else if (!artistname.empty())
13836 { // Artist name is not unique, so could get songs from more than one.
13837 songArtistClause = PrepareSQL(
13838 "EXISTS (SELECT 1 FROM song_artist "
13839 "JOIN artist ON artist.idArtist = song_artist.idArtist "
13840 "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
13841 artistname.c_str(), strRoleSQL.c_str());
13842 albumArtistClause = PrepareSQL(
13843 "EXISTS (SELECT 1 FROM album_artist "
13844 "JOIN artist ON artist.idArtist = album_artist.idArtist "
13845 "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
13846 artistname.c_str());
13849 // Process artist name or id option
13850 if (!songArtistClause.empty())
13852 if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
13853 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13854 else if (idRole > 1)
13856 if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
13857 filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
13858 else // songs where artist contributes that role.
13859 filter.AppendWhere(songArtistClause);
13861 else
13863 if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
13864 filter.AppendWhere(albumArtistClause);
13865 else // Artist is song artist or album artist
13866 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13871 option = options.find("filter");
13872 if (option != options.end())
13874 PLAYLIST::CSmartPlaylist xspFilter;
13875 if (!xspFilter.LoadFromJson(option->second.asString()))
13876 return false;
13878 // check if the filter playlist matches the item type
13879 if (xspFilter.GetType() == type)
13881 std::set<std::string> playlists;
13882 filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
13884 // remove the filter if it doesn't match the item type
13885 else
13886 musicUrl.RemoveOption("filter");
13889 return true;
13892 std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath)
13894 if (strFileNameAndPath.empty())
13895 return std::string();
13897 CDateTime dateMedia;
13898 int code;
13899 code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded;
13900 // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
13901 if (code == 1)
13902 dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath);
13903 //2 using the newer datetime of the file's mtime and ctime
13904 else if (code == 2)
13905 dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath);
13906 //3 using the older datetime of the file's mtime and ctime
13907 else if (code == 3)
13908 dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath);
13909 //0 using the current datetime if none of the above matches or one returns an invalid datetime
13910 if (!dateMedia.IsValid())
13911 dateMedia = CDateTime::GetCurrentDateTime();
13913 return dateMedia.GetAsDBDateTime();
13916 bool CMusicDatabase::AddAudioBook(const CFileItem& item)
13918 auto const& artists = item.GetMusicInfoTag()->GetArtist();
13919 std::string strSQL = PrepareSQL(
13920 "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
13921 "VALUES (NULL,'%s','%s',%i,'%s','%s')",
13922 item.GetMusicInfoTag()->GetAlbum().c_str(), artists.empty() ? "" : artists[0].c_str(), 0,
13923 item.GetDynPath().c_str(), CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
13924 return ExecuteQuery(strSQL);
13927 bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark)
13929 std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook "
13930 "WHERE file='%s'",
13931 item.GetDynPath().c_str());
13932 if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13934 if (!AddAudioBook(item))
13935 return false;
13938 strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i "
13939 "WHERE file='%s'",
13940 bookmark, item.GetDynPath().c_str());
13942 return ExecuteQuery(strSQL);
13945 bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark)
13947 std::string strSQL =
13948 PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'", item.GetDynPath().c_str());
13949 if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13950 return false;
13952 bookmark = m_pDS->fv(0).get_asInt();
13953 return true;