[filesystem][SpecialProtocol] Removed assert from GetPath
[xbmc.git] / xbmc / music / MusicDatabase.cpp
bloba1bd42b418a0b74566eb58d4b6c4c60d863be22e
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 "GUIInfoManager.h"
15 #include "LangInfo.h"
16 #include "ServiceBroker.h"
17 #include "Song.h"
18 #include "TextureCache.h"
19 #include "URL.h"
20 #include "Util.h"
21 #include "addons/Addon.h"
22 #include "addons/AddonManager.h"
23 #include "addons/AddonSystemSettings.h"
24 #include "addons/Scraper.h"
25 #include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h"
26 #include "dbwrappers/dataset.h"
27 #include "dialogs/GUIDialogKaiToast.h"
28 #include "dialogs/GUIDialogProgress.h"
29 #include "dialogs/GUIDialogSelect.h"
30 #include "events/EventLog.h"
31 #include "events/NotificationEvent.h"
32 #include "filesystem/Directory.h"
33 #include "filesystem/DirectoryCache.h"
34 #include "filesystem/File.h"
35 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
36 #include "guilib/GUIComponent.h"
37 #include "guilib/GUIWindowManager.h"
38 #include "guilib/LocalizeStrings.h"
39 #include "guilib/guiinfo/GUIInfoLabels.h"
40 #include "interfaces/AnnouncementManager.h"
41 #include "messaging/helpers/DialogHelper.h"
42 #include "messaging/helpers/DialogOKHelper.h"
43 #include "music/MusicDbUrl.h"
44 #include "music/MusicLibraryQueue.h"
45 #include "music/tags/MusicInfoTag.h"
46 #include "network/Network.h"
47 #include "network/cddb.h"
48 #include "playlists/SmartPlayList.h"
49 #include "profiles/ProfileManager.h"
50 #include "settings/AdvancedSettings.h"
51 #include "settings/MediaSourceSettings.h"
52 #include "settings/Settings.h"
53 #include "settings/SettingsComponent.h"
54 #include "storage/MediaManager.h"
55 #include "utils/FileUtils.h"
56 #include "utils/LegacyPathTranslation.h"
57 #include "utils/MathUtils.h"
58 #include "utils/Random.h"
59 #include "utils/StringUtils.h"
60 #include "utils/URIUtils.h"
61 #include "utils/XMLUtils.h"
62 #include "utils/log.h"
64 #include <inttypes.h>
66 using namespace XFILE;
67 using namespace MUSICDATABASEDIRECTORY;
68 using namespace KODI::MESSAGING;
69 using namespace MUSIC_INFO;
71 using ADDON::AddonPtr;
72 using KODI::MESSAGING::HELPERS::DialogResponse;
74 #define RECENTLY_PLAYED_LIMIT 25
75 #define MIN_FULL_SEARCH_LENGTH 3
77 #ifdef HAS_DVD_DRIVE
78 using namespace CDDB;
79 using namespace MEDIA_DETECT;
80 #endif
82 static void AnnounceRemove(const std::string& content, int id)
84 CVariant data;
85 data["type"] = content;
86 data["id"] = id;
87 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
88 data["transaction"] = true;
89 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data);
92 static void AnnounceUpdate(const std::string& content, int id, bool added = false)
94 CVariant data;
95 data["type"] = content;
96 data["id"] = id;
97 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
98 data["transaction"] = true;
99 if (added)
100 data["added"] = true;
101 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data);
104 CMusicDatabase::CMusicDatabase(void)
106 m_translateBlankArtist = true;
109 CMusicDatabase::~CMusicDatabase(void)
111 EmptyCache();
114 bool CMusicDatabase::Open()
116 return CDatabase::Open(
117 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic);
120 void CMusicDatabase::CreateTables()
122 CLog::Log(LOGINFO, "create artist table");
123 m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, "
124 " strArtist varchar(256), strMusicBrainzArtistID text, "
125 " strSortName text, "
126 " strType text, strGender text, strDisambiguation text, "
127 " strBorn text, strFormed text, strGenres text, strMoods text, "
128 " strStyles text, strInstruments text, strBiography text, "
129 " strDied text, strDisbanded text, strYearsActive text, "
130 " strImage text, "
131 " lastScraped varchar(20) default NULL, "
132 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
133 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
134 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
135 // Create missing artist tag artist [Missing].
136 std::string strSQL =
137 PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
138 "VALUES( %i, '%s', '%s', '%s' )",
139 BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_NAME.c_str(),
140 BLANKARTIST_FAKEMUSICBRAINZID.c_str());
141 m_pDS->exec(strSQL);
143 CLog::Log(LOGINFO, "create album table");
144 m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, "
145 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
146 " strReleaseGroupMBID text, "
147 " strArtistDisp text, strArtistSort text, strGenres text, "
148 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
149 " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
150 " bCompilation integer not null default '0', "
151 " strMoods text, strStyles text, strThemes text, "
152 " strReview text, strImage text, strLabel text, "
153 " strType text, "
154 " strReleaseStatus TEXT, "
155 " fRating FLOAT NOT NULL DEFAULT 0, "
156 " iVotes INTEGER NOT NULL DEFAULT 0, "
157 " iUserrating INTEGER NOT NULL DEFAULT 0, "
158 " lastScraped varchar(20) default NULL, "
159 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
160 " strReleaseType text, "
161 " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
162 " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
163 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
164 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
166 CLog::Log(LOGINFO, "create audiobook table");
167 m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
168 " strBook varchar(256), strAuthor text,"
169 " bookmark integer, file text,"
170 " dateAdded varchar (20) default NULL)");
172 CLog::Log(LOGINFO, "create album_artist table");
173 m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, "
174 "strArtist text)");
176 CLog::Log(LOGINFO, "create album_source table");
177 m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
179 CLog::Log(LOGINFO, "create genre table");
180 m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
182 CLog::Log(LOGINFO, "create path table");
183 m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
185 CLog::Log(LOGINFO, "create source table");
186 m_pDS->exec(
187 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
189 CLog::Log(LOGINFO, "create source_path table");
190 m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
192 CLog::Log(LOGINFO, "create song table");
193 m_pDS->exec("CREATE TABLE song (idSong integer primary key, "
194 " idAlbum integer, idPath integer, "
195 " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
196 " iTrack integer, iDuration integer, "
197 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
198 " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
199 " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
200 " lastplayed varchar(20) default NULL, "
201 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
202 " userrating INTEGER NOT NULL DEFAULT 0, "
203 " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
204 " iBitRate INTEGER NOT NULL DEFAULT 0, "
205 " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
206 " strReplayGain text, "
207 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
208 CLog::Log(LOGINFO, "create song_artist table");
209 m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder "
210 "integer, strArtist text)");
211 CLog::Log(LOGINFO, "create song_genre table");
212 m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
214 CLog::Log(LOGINFO, "create role table");
215 m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
216 m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role
218 CLog::Log(LOGINFO, "create infosetting table");
219 m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, "
220 "strScraperPath TEXT, strSettings TEXT)");
222 CLog::Log(LOGINFO, "create discography table");
223 m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
224 "strReleaseGroupMBID TEXT)");
226 CLog::Log(LOGINFO, "create art table");
227 m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, "
228 "media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
230 CLog::Log(LOGINFO, "create versiontagscan table");
231 m_pDS->exec("CREATE TABLE versiontagscan "
232 "(idVersion INTEGER, iNeedsScan INTEGER, "
233 "lastscanned VARCHAR(20), "
234 "lastcleaned VARCHAR(20), "
235 "artistlinksupdated VARCHAR(20), "
236 "genresupdated VARCHAR(20))");
237 m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
238 GetSchemaVersion()));
240 CLog::Log(LOGINFO, "create removed_link table");
241 m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
244 void CMusicDatabase::CreateAnalytics()
246 CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__);
247 m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
248 m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
249 m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
250 m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
252 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
253 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
255 m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
257 m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
258 m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
259 m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
261 m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))");
263 m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
264 m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
266 m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
268 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
269 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
271 m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))");
272 m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
273 m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
274 m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
275 m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
276 //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
277 m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
279 m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
280 m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
281 m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
282 m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
284 m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
285 m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
287 m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))");
289 m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
291 m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
293 CLog::Log(LOGINFO, "create triggers");
294 m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
295 " DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
296 " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
297 " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
298 " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
299 " END");
300 m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
301 " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
302 " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
303 " DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
304 " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
305 " END");
306 m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
307 " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
308 " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
309 " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
310 " END");
311 m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
312 " DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
313 " DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
314 " END");
316 /* Maintain date new and last modified for songs, albums and artists using triggers
317 MySQL triggers cannot modify a table that is already being used by the statement that invoked
318 the trigger (to avoid recursion), but can set NEW column values before insert or update.
319 Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
320 Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
321 // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
323 bool bisMySQL = StringUtils::EqualsNoCase(
324 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
326 if (!bisMySQL)
327 { // SQLite trigger syntax - AFTER INSERT/UPDATE
328 m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
329 " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
330 " AND NEW.dateNew IS NULL;"
331 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
332 " END");
333 m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
334 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
335 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
336 " END");
337 m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
338 " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
339 " AND NEW.dateNew IS NULL;"
340 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
341 " END");
342 m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
343 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
344 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
345 " END");
346 m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
347 " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
348 " AND NEW.dateNew IS NULL;"
349 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
350 " END");
351 m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
352 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
353 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
354 " END");
356 m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
357 " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
358 " END");
360 else
361 { // MySQL trigger syntax - BEFORE INSERT/UPDATE
362 m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
363 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
364 " SET NEW.dateModified = now();"
365 " END");
366 m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
367 " SET NEW.dateModified = now()");
369 m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
370 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
371 " SET NEW.dateModified = now();"
372 " END");
373 m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
374 " SET NEW.dateModified = now()");
376 m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
377 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
378 " SET NEW.dateModified = now();"
379 " END");
380 m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
381 " SET NEW.dateModified = now()");
383 m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
384 " UPDATE versiontagscan SET genresupdated = now()");
387 // Triggers to maintain recent changes to album and song artist links in removed_link table
388 m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
389 "DELETE FROM removed_link "
390 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
391 "END");
392 m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
393 "DELETE FROM removed_link "
394 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
395 "END");
396 CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
398 // Create native functions stored in DB (MySQL/MariaDB only)
399 CreateNativeDBFunctions();
401 // we create views last to ensure all indexes are rolled in
402 CreateViews();
405 void CMusicDatabase::CreateRemovedLinkTriggers()
407 // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
408 m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
409 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
410 " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
411 " END");
412 m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
413 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
414 " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
415 " END");
419 void CMusicDatabase::CreateViews()
421 CLog::Log(LOGINFO, "create song view");
422 m_pDS->exec("CREATE VIEW songview AS SELECT "
423 " song.idSong AS idSong, "
424 " song.strArtistDisp AS strArtists,"
425 " song.strArtistSort AS strArtistSort,"
426 " song.strGenres AS strGenres,"
427 " strTitle, "
428 " iTrack, iDuration, "
429 " song.strReleaseDate as strReleaseDate, "
430 " song.strOrigReleaseDate as strOrigReleaseDate, "
431 " song.strDiscSubtitle as strDiscSubtitle, "
432 " strFileName, "
433 " strMusicBrainzTrackID, "
434 " iTimesPlayed, iStartOffset, iEndOffset, "
435 " lastplayed, "
436 " song.rating, "
437 " song.userrating, "
438 " song.votes, "
439 " comment, "
440 " song.idAlbum AS idAlbum, "
441 " strAlbum, "
442 " strPath, "
443 " album.strReleaseStatus as strReleaseStatus,"
444 " album.bCompilation AS bCompilation,"
445 " album.bBoxedSet AS bBoxedSet, "
446 " album.strArtistDisp AS strAlbumArtists,"
447 " album.strArtistSort AS strAlbumArtistSort,"
448 " album.strReleaseType AS strAlbumReleaseType,"
449 " song.mood as mood,"
450 " song.strReplayGain, "
451 " iBPM, "
452 " iBitRate, "
453 " iSampleRate, "
454 " iChannels, "
455 " album.iAlbumDuration AS iAlbumDuration, "
456 " album.iDiscTotal as iDiscTotal, "
457 " song.dateAdded as dateAdded, "
458 " song.dateNew AS dateNew, "
459 " song.dateModified AS dateModified "
460 "FROM song"
461 " JOIN album ON"
462 " song.idAlbum=album.idAlbum"
463 " JOIN path ON"
464 " song.idPath=path.idPath");
466 CLog::Log(LOGINFO, "create album view");
467 m_pDS->exec("CREATE VIEW albumview AS SELECT "
468 "album.idAlbum AS idAlbum, "
469 "strAlbum, "
470 "strMusicBrainzAlbumID, "
471 "strReleaseGroupMBID, "
472 "album.strArtistDisp AS strArtists, "
473 "album.strArtistSort AS strArtistSort, "
474 "album.strGenres AS strGenres, "
475 "album.strReleaseDate as strReleaseDate, "
476 "album.strOrigReleaseDate as strOrigReleaseDate, "
477 "album.bBoxedSet AS bBoxedSet, "
478 "album.strMoods AS strMoods, "
479 "album.strStyles AS strStyles, "
480 "strThemes, "
481 "strReview, "
482 "strLabel, "
483 "strType, "
484 "strReleaseStatus, "
485 "album.strImage as strImage, "
486 "album.fRating, "
487 "album.iUserrating, "
488 "album.iVotes, "
489 "bCompilation, "
490 "bScrapedMBID,"
491 "lastScraped,"
492 "dateAdded, dateNew, dateModified, "
493 "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song "
494 "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
495 "strReleaseType, "
496 "iDiscTotal, "
497 "(SELECT MAX(song.lastplayed) FROM song "
498 "WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
499 "iAlbumDuration "
500 "FROM album");
502 CLog::Log(LOGINFO, "create artist view");
503 m_pDS->exec("CREATE VIEW artistview AS SELECT"
504 " idArtist, strArtist, strSortName, "
505 " strMusicBrainzArtistID, "
506 " strType, strGender, strDisambiguation, "
507 " strBorn, strFormed, strGenres,"
508 " strMoods, strStyles, strInstruments, "
509 " strBiography, strDied, strDisbanded, "
510 " strYearsActive, strImage, "
511 " bScrapedMBID, lastScraped, "
512 " dateAdded, dateNew, dateModified "
513 "FROM artist");
515 CLog::Log(LOGINFO, "create albumartist view");
516 m_pDS->exec("CREATE VIEW albumartistview AS SELECT"
517 " album_artist.idAlbum AS idAlbum, "
518 " album_artist.idArtist AS idArtist, "
519 " 0 AS idRole, "
520 " 'AlbumArtist' AS strRole, "
521 " artist.strArtist AS strArtist, "
522 " artist.strSortName AS strSortName,"
523 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
524 " album_artist.iOrder AS iOrder "
525 "FROM album_artist "
526 "JOIN artist ON "
527 " album_artist.idArtist = artist.idArtist");
529 CLog::Log(LOGINFO, "create songartist view");
530 m_pDS->exec("CREATE VIEW songartistview AS SELECT"
531 " song_artist.idSong AS idSong, "
532 " song_artist.idArtist AS idArtist, "
533 " song_artist.idRole AS idRole, "
534 " role.strRole AS strRole, "
535 " artist.strArtist AS strArtist, "
536 " artist.strSortName AS strSortName,"
537 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
538 " song_artist.iOrder AS iOrder "
539 "FROM song_artist "
540 "JOIN artist ON "
541 " song_artist.idArtist = artist.idArtist "
542 "JOIN role ON "
543 " song_artist.idRole = role.idRole");
546 void CMusicDatabase::CreateNativeDBFunctions()
548 // Create native functions in MySQL/MariaDB database only
549 if (!StringUtils::EqualsNoCase(
550 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
551 "mysql"))
552 return;
553 CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions");
554 /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
555 default utf8_general_ci collation that SQLite does via a collation sequence callback
556 function to StringUtils::AlphaNumericCompare
557 !@todo: the video needs these defined too for sorting in DB, then creation can be made common
559 // clang-format off
560 // udfFirstNumberPos finds the position of the first digit in a string
561 m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
562 m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
563 "RETURNS int \n"
564 "LANGUAGE SQL \n"
565 "DETERMINISTIC \n"
566 "NO SQL \n"
567 "SQL SECURITY INVOKER \n"
568 "BEGIN \n"
569 " DECLARE position int; \n"
570 " DECLARE tmppos int; \n"
571 " SET position = 5000; \n"
572 " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
573 " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
574 " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
575 " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
576 " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
577 " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
578 " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
579 " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
580 " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
581 " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
582 " IF(position = 5000) THEN RETURN 0; END IF;\n"
583 " RETURN position; \n"
584 "END\n");
586 // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input
587 m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
588 m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
589 "RETURNS varchar(1024)\n"
590 "LANGUAGE SQL\n"
591 "DETERMINISTIC\n"
592 "NO SQL\n"
593 "SQL SECURITY INVOKER\n"
594 "BEGIN\n"
595 " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
596 " DECLARE i int;\n"
597 " DECLARE symbolCharsLen int;\n"
598 " DECLARE symbol char(1);\n"
599 " SET sortString = instring;\n"
600 " SET i = 1;\n"
601 " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
602 " WHILE(i <= symbolCharsLen) DO\n"
603 " SET symbol = SUBSTRING(symbolChars, i, 1);\n"
604 " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
605 " SET i = i + 1;\n"
606 " END WHILE;\n"
607 " RETURN sortString;\n"
608 "END\n");
610 // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
611 m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
612 m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
613 "sameOrderChars char(25))\n"
614 "RETURNS varchar(1024)\n"
615 "LANGUAGE SQL\n"
616 "DETERMINISTIC\n"
617 "NO SQL\n"
618 "SQL SECURITY INVOKER\n"
619 "BEGIN\n"
620 " DECLARE sortString varchar(1024);\n"
621 " DECLARE shiftedString varchar(1024);\n"
622 " DECLARE inLength int;\n"
623 " DECLARE shiftedLength int;\n"
624 " DECLARE totalSympadLength int;\n"
625 " DECLARE symbolshifted512 varchar(1024);\n"
626 " DECLARE numStartIndex int; \n"
627 " DECLARE numEndIndex int; \n"
628 " DECLARE padLength int; \n"
629 " DECLARE totalPadLength int; \n"
630 " DECLARE i int; \n"
631 " DECLARE sameOrderCharsLen int;\n"
632 " SET totalPadLength = 0; \n"
633 " SET instring = TRIM(instring);\n"
634 " SET inLength = CHAR_LENGTH(inString);\n"
635 " SET sortString = instring; \n"
636 " SET numStartIndex = udfFirstNumberPos(instring); \n"
637 " SET numEndIndex = 0; \n"
638 " SET i = 1; \n"
639 " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
640 " WHILE(i <= sameOrderCharsLen) DO \n"
641 " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
642 " SET i = i + 1; \n"
643 " END WHILE; \n"
644 " WHILE(numStartIndex <> 0) DO \n"
645 " SET numStartIndex = numStartIndex + numEndIndex; \n"
646 " SET numEndIndex = numStartIndex; \n"
647 " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
648 " SET numEndIndex = numEndIndex + 1; \n"
649 " END WHILE; \n"
650 " SET numEndIndex = numEndIndex - 1; \n"
651 " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
652 " IF padLength < 0 THEN \n"
653 " SET padLength = 0; \n"
654 " END IF; \n"
655 " IF inLength + totalPadLength + padlength > 1024 THEN \n"
656 " -- Padding more digits would be too long, pad this one just enough \n"
657 " SET padLength = 1024 - inLength - totalPadLength; \n"
658 " SET numStartIndex = 0; \n"
659 " END IF; \n"
660 " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
661 " SET totalPadLength = totalPadLength + padLength; \n"
662 " IF numStartIndex <> 0 THEN \n"
663 " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
664 " END IF; \n"
665 " END WHILE; \n"
666 " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
667 " -- when there is space as this could double string length. Note '\\' needs escaping \n"
668 " SET numStartIndex = 1; \n"
669 " SET numEndIndex = inLength + totalPadLength; \n"
670 " IF numEndIndex < 1024 THEN \n"
671 " SET shiftedLength = 0; \n"
672 " SET totalSympadLength = 0; \n"
673 " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
674 " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
675 " SET numStartIndex = numStartIndex + 512; \n"
676 " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
677 " IF totalSympadLength = 0 THEN \n"
678 " SET shiftedString = symbolshifted512; \n"
679 " ELSE \n"
680 " IF totalSympadLength + shiftedLength > 1024 THEN \n"
681 " SET shiftedLength = 1024 - totalSympadLength; \n"
682 " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
683 " END IF; \n"
684 " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
685 " END IF; \n"
686 " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
687 " END WHILE; \n"
688 " SET sortString = shiftedString; \n"
689 " END IF; \n"
690 " RETURN sortString; \n"
691 "END\n");
692 // clang-format on
695 void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath,
696 std::string& strPath,
697 std::string& strFileName)
699 URIUtils::Split(strFileNameAndPath, strPath, strFileName);
700 // Keep protocol options as part of the path
701 if (URIUtils::IsURL(strFileNameAndPath))
703 CURL url(strFileNameAndPath);
704 if (!url.GetProtocolOptions().empty())
705 strPath += "|" + url.GetProtocolOptions();
709 bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource)
711 BeginTransaction();
712 SetLibraryLastUpdated();
714 album.idAlbum = AddAlbum(album.strAlbum, //
715 album.strMusicBrainzAlbumID, //
716 album.strReleaseGroupMBID, //
717 album.GetAlbumArtistString(), //
718 album.GetAlbumArtistSort(), //
719 album.GetGenreString(), //
720 album.strReleaseDate, //
721 album.strOrigReleaseDate, //
722 album.bBoxedSet, //
723 album.strLabel, //
724 album.strType, //
725 album.strReleaseStatus, //
726 album.bCompilation, //
727 album.releaseType);
729 // Add the album artists
730 // Album must have at least one artist so set artist to [Missing]
731 if (album.artistCredits.empty())
732 AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
733 for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
734 ++artistCredit)
736 artistCredit->idArtist =
737 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
738 artistCredit->GetSortName());
739 AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
740 static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
743 // Add songs
744 for (auto song = album.songs.begin(); song != album.songs.end(); ++song)
746 song->idAlbum = album.idAlbum;
748 song->idSong = AddSong(song->idSong, //
749 song->dateNew, //
750 song->idAlbum, //
751 song->strTitle, //
752 song->strMusicBrainzTrackID, //
753 song->strFileName, //
754 song->strComment, //
755 song->strMood, //
756 song->strThumb, //
757 song->GetArtistString(), //
758 song->GetArtistSort(), //
759 song->genre, //
760 song->iTrack, //
761 song->iDuration, //
762 song->strReleaseDate, //
763 song->strOrigReleaseDate, //
764 song->strDiscSubtitle, //
765 song->iTimesPlayed, //
766 song->iStartOffset, song->iEndOffset, //
767 song->lastPlayed, //
768 song->rating, //
769 song->userrating, //
770 song->votes, //
771 song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels, //
772 song->replayGain);
774 // Song must have at least one artist so set artist to [Missing]
775 if (song->artistCredits.empty())
776 AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
778 for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end();
779 ++artistCredit)
781 artistCredit->idArtist =
782 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
783 artistCredit->GetSortName());
784 AddSongArtist(
785 artistCredit->idArtist, song->idSong, ROLE_ARTIST,
786 artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
787 static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit)));
789 // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
790 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
791 AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort());
794 // Set album duration as total of all songs on album.
795 // Folder layout may mean AddAlbum call has added more songs to an existing album
796 std::string strSQL;
797 strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum);
798 int albumDuration = GetSingleValueInt(strSQL);
799 m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration,
800 album.idAlbum));
802 // Add album sources
803 if (idSource > 0)
804 AddAlbumSource(album.idAlbum, idSource);
805 else
807 // Use album path, or failing that song paths to determine sources for the album
808 AddAlbumSources(album.idAlbum, album.strPath);
811 for (const auto& albumArt : album.art)
812 SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second);
814 // Set album disc total
815 m_pDS->exec(
816 PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
817 "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
818 album.idAlbum));
819 // Set a non-compilation album as a boxset if it has three or more distinct disc titles
820 if (!album.bBoxedSet && !album.bCompilation)
822 strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
823 album.idAlbum);
824 int numTitles = GetSingleValueInt(strSQL);
825 if (numTitles >= 3)
827 strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum);
828 m_pDS->exec(strSQL);
831 m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
832 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
833 album.idAlbum));
834 m_pDS->exec(
835 PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
836 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
837 album.idAlbum));
839 std::string albumdateadded =
840 GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum));
841 m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
842 albumdateadded.c_str(), album.idAlbum));
844 /* Update artist dateAdded values for artists involved in album as song or album artists.
845 Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
846 derived from song dateadded values which are usually file dates (or the last scan).
847 It is used to indicate those artists with recent media.
848 For artists that are neither album nor song artists (other roles only) dateadded will be null.
850 std::vector<std::string> artistIDs;
851 // Get distinct song and album artist IDs for this album
852 GetArtistsByAlbum(album.idAlbum, artistIDs);
853 std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")";
854 strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' "
855 "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
856 albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str());
857 m_pDS->exec(strSQL);
859 CommitTransaction();
860 return true;
863 bool CMusicDatabase::UpdateAlbum(CAlbum& album)
865 BeginTransaction();
866 SetLibraryLastUpdated();
868 const std::string itemSeparator =
869 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
871 if (album.bBoxedSet)
873 bool isBoxset = IsAlbumBoxset(album.idAlbum);
874 if (!isBoxset)
875 { // not a boxset already so make sure we have enough discs & they all have titles
876 bool canBeBoxset = false;
877 std::string strSQL;
878 strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum);
879 int numDiscs = GetSingleValueInt(strSQL);
880 if (numDiscs >= 2)
882 canBeBoxset = true;
883 int discValue;
884 for (discValue = 1; discValue <= numDiscs; discValue++)
886 strSQL =
887 PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
888 "song.iTrack >> 16 = %i",
889 album.idAlbum, discValue);
890 std::string currentTitle = GetSingleValue(strSQL);
891 if (currentTitle.empty())
893 currentTitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discValue);
894 strSQL =
895 PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
896 "song.iTrack >> 16 = %i",
897 currentTitle.c_str(), album.idAlbum, discValue);
898 ExecuteQuery(strSQL);
902 if (!canBeBoxset && album.bBoxedSet)
904 CLog::Log(LOGINFO, "{} : Album with id [{}] does not meet the requirements for a boxset.",
905 __FUNCTION__, album.idAlbum);
906 album.bBoxedSet = false;
910 UpdateAlbum(album.idAlbum, album.strAlbum, album.strMusicBrainzAlbumID, //
911 album.strReleaseGroupMBID, //
912 album.GetAlbumArtistString(), album.GetAlbumArtistSort(), //
913 album.GetGenreString(), //
914 StringUtils::Join(album.moods, itemSeparator), //
915 StringUtils::Join(album.styles, itemSeparator), //
916 StringUtils::Join(album.themes, itemSeparator), //
917 album.strReview, //
918 album.thumbURL.GetData(), //
919 album.strLabel, //
920 album.strType, //
921 album.strReleaseStatus, //
922 album.fRating, album.iUserrating, album.iVotes, //
923 album.strReleaseDate, //
924 album.strOrigReleaseDate, //
925 album.bBoxedSet, //
926 album.bCompilation, //
927 album.releaseType, //
928 album.bScrapedMBID);
930 if (!album.bArtistSongMerge)
932 // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
933 for (const auto& artistCredit : album.artistCredits)
934 UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID());
936 else
938 // Replace the album artists with those scraped or set by JSON
939 DeleteAlbumArtistsByAlbum(album.idAlbum);
940 // Album must have at least one artist so set artist to [Missing]
941 if (album.artistCredits.empty())
942 AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
943 for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
944 ++artistCredit)
946 artistCredit->idArtist =
947 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
948 artistCredit->GetSortName(), true);
949 AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
950 static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
952 /* Replace the songs with those scraped or imported, but if new songs is empty
953 (such as when called from JSON) do not remove the original ones
954 Also updates nested data e.g. song artists, song genres and contributors
955 Do not check for artist link changes, that is done later for all songs and album
957 int albumDuration = 0;
958 for (auto& song : album.songs)
960 UpdateSong(song);
961 albumDuration += song.iDuration;
963 if (albumDuration > 0)
964 m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
965 albumDuration, album.idAlbum));
968 if (!album.art.empty())
969 SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
971 CheckArtistLinksChanged();
973 CommitTransaction();
974 return true;
977 void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal)
979 // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
980 int iDate;
981 iDate = StringUtils::DateStringToYYYYMMDD(strRelease);
982 if (iDate < 0)
983 strRelease.clear();
984 iDate = StringUtils::DateStringToYYYYMMDD(strOriginal);
985 if (iDate < 0)
986 strOriginal.clear();
987 // Avoid missing release or original values unless both invalid or empty
988 if (!strRelease.empty() && strOriginal.empty())
989 strOriginal = strRelease;
990 else if (strRelease.empty() && !strOriginal.empty())
991 strRelease = strOriginal;
994 int CMusicDatabase::AddSong(const int idSong,
995 const CDateTime& dtDateNew,
996 const int idAlbum,
997 const std::string& strTitle,
998 const std::string& strMusicBrainzTrackID,
999 const std::string& strPathAndFileName,
1000 const std::string& strComment,
1001 const std::string& strMood,
1002 const std::string& strThumb,
1003 const std::string& artistDisp,
1004 const std::string& artistSort,
1005 const std::vector<std::string>& genres,
1006 int iTrack,
1007 int iDuration,
1008 const std::string& strReleaseDate,
1009 const std::string& strOrigReleaseDate,
1010 std::string& strDiscSubtitle,
1011 const int iTimesPlayed,
1012 int iStartOffset,
1013 int iEndOffset,
1014 const CDateTime& dtLastPlayed,
1015 float rating,
1016 int userrating,
1017 int votes,
1018 int iBPM,
1019 int iBitRate,
1020 int iSampleRate,
1021 int iChannels,
1022 const ReplayGain& replayGain)
1024 int idNew = -1;
1025 std::string strSQL;
1028 // We need at least the title
1029 if (strTitle.empty())
1030 return -1;
1032 if (nullptr == m_pDB)
1033 return -1;
1034 if (nullptr == m_pDS)
1035 return -1;
1037 std::string strPath, strFileName;
1038 SplitPath(strPathAndFileName, strPath, strFileName);
1039 int idPath = AddPath(strPath);
1041 if (idSong <= 1)
1043 if (!strMusicBrainzTrackID.empty())
1044 strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1045 "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
1046 idAlbum, iTrack, strMusicBrainzTrackID.c_str());
1047 else
1048 strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1049 "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
1050 "AND strMusicBrainzTrackID IS NULL",
1051 idAlbum, strFileName.c_str(), strTitle.c_str(), iTrack);
1053 if (!m_pDS->query(strSQL))
1054 return -1;
1056 if (m_pDS->num_rows() == 0)
1058 m_pDS->close();
1060 // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
1061 bool isBoxset = IsAlbumBoxset(idAlbum);
1062 if (isBoxset && strDiscSubtitle.empty())
1064 int discno = iTrack >> 16;
1065 strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discno);
1068 // Validate ISO8601 dates and ensure none missing
1069 std::string strRelease = strReleaseDate;
1070 std::string strOriginal = strOrigReleaseDate;
1071 NormaliseSongDates(strRelease, strOriginal);
1073 // Get dateAdded from music file timestamp
1074 std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1076 strSQL = "INSERT INTO song ("
1077 "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
1078 "strTitle, iTrack, iDuration, "
1079 "strReleaseDate, strOrigReleaseDate, iBPM, "
1080 "iBitrate, iSampleRate, iChannels, "
1081 "strDiscSubtitle, strFileName, dateAdded, "
1082 "strMusicBrainzTrackID, strArtistSort, "
1083 "iTimesPlayed, iStartOffset, iEndOffset, "
1084 "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
1086 if (idSong <= 0)
1087 // Song ID is autoincremented and dateNew set by trigger
1088 strSQL += PrepareSQL("VALUES (NULL, NULL, ");
1089 else
1090 //Reuse song Id and original date when the Id added
1091 strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str());
1093 strSQL +=
1094 PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
1095 idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration,
1096 strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate,
1097 iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str());
1099 if (strMusicBrainzTrackID.empty())
1100 strSQL += PrepareSQL(",NULL");
1101 else
1102 strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str());
1103 if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1104 strSQL += PrepareSQL(",NULL");
1105 else
1106 strSQL += PrepareSQL(",'%s'", artistSort.c_str());
1108 if (dtLastPlayed.IsValid())
1109 strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", //
1110 iTimesPlayed, iStartOffset, iEndOffset,
1111 dtLastPlayed.GetAsDBDateTime().c_str(), //
1112 static_cast<double>(rating), userrating, votes, //
1113 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1114 else
1115 strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", //
1116 iTimesPlayed, iStartOffset, iEndOffset, //
1117 static_cast<double>(rating), userrating, votes, //
1118 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1119 m_pDS->exec(strSQL);
1120 if (idSong <= 0)
1121 idNew = (int)m_pDS->lastinsertid();
1122 else
1123 idNew = idSong;
1125 else
1127 idNew = m_pDS->fv("idSong").get_asInt();
1128 m_pDS->close();
1129 UpdateSong(idNew, //
1130 strTitle, //
1131 strMusicBrainzTrackID, //
1132 strPathAndFileName, //
1133 strComment, //
1134 strMood, //
1135 strThumb, //
1136 artistDisp, //
1137 artistSort, //
1138 genres, //
1139 iTrack, //
1140 iDuration, //
1141 strReleaseDate, //
1142 strOrigReleaseDate, //
1143 strDiscSubtitle, //
1144 iTimesPlayed, //
1145 iStartOffset, iEndOffset, //
1146 dtLastPlayed, //
1147 rating, userrating, votes, //
1148 replayGain, //
1149 iBPM, iBitRate, iSampleRate, iChannels);
1151 if (!strThumb.empty())
1152 SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb);
1154 // Song genres added, and genre string updated to use the standardised genre names
1155 AddSongGenres(idNew, genres);
1157 AnnounceUpdate(MediaTypeSong, idNew, true);
1159 catch (...)
1161 CLog::Log(LOGERROR, "musicdatabase:unable to addsong ({})", strSQL);
1163 return idNew;
1166 bool CMusicDatabase::GetSong(int idSong, CSong& song)
1170 song.Clear();
1172 if (nullptr == m_pDB)
1173 return false;
1174 if (nullptr == m_pDS)
1175 return false;
1177 std::string strSQL =
1178 PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
1179 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1180 " WHERE songview.idSong = %i "
1181 " ORDER BY songartistview.idRole, songartistview.iOrder",
1182 idSong);
1184 if (!m_pDS->query(strSQL))
1185 return false;
1186 int iRowsFound = m_pDS->num_rows();
1187 if (iRowsFound == 0)
1189 m_pDS->close();
1190 return false;
1193 int songArtistOffset = song_enumCount;
1195 song = GetSongFromDataset(m_pDS->get_sql_record());
1196 while (!m_pDS->eof())
1198 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1200 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1201 if (idSongArtistRole == ROLE_ARTIST)
1202 song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
1203 else
1204 song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1206 m_pDS->next();
1208 m_pDS->close(); // cleanup recordset data
1209 return true;
1211 catch (...)
1213 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
1216 return false;
1219 bool CMusicDatabase::UpdateSong(CSong& song, bool bArtists /*= true*/, bool bArtistLinks /*= true*/)
1221 int result = UpdateSong(song.idSong,
1222 song.strTitle, //
1223 song.strMusicBrainzTrackID, //
1224 song.strFileName, //
1225 song.strComment, //
1226 song.strMood, //
1227 song.strThumb, //
1228 song.GetArtistString(), //
1229 song.GetArtistSort(), //
1230 song.genre, //
1231 song.iTrack, //
1232 song.iDuration, //
1233 song.strReleaseDate, //
1234 song.strOrigReleaseDate, //
1235 song.strDiscSubtitle, //
1236 song.iTimesPlayed, //
1237 song.iStartOffset, song.iEndOffset, //
1238 song.lastPlayed, //
1239 song.rating, song.userrating, song.votes, //
1240 song.replayGain, //
1241 song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels);
1242 if (result < 0)
1243 return false;
1245 // Replace Song genres and update genre string using the standardised genre names
1246 AddSongGenres(song.idSong, song.genre);
1247 if (bArtists)
1249 //Replace song artists and contributors
1250 DeleteSongArtistsBySong(song.idSong);
1251 // Song must have at least one artist so set artist to [Missing]
1252 if (song.artistCredits.empty())
1253 AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
1254 for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end();
1255 ++artistCredit)
1257 artistCredit->idArtist =
1258 AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
1259 artistCredit->GetSortName());
1260 AddSongArtist(artistCredit->idArtist, song.idSong, ROLE_ARTIST, artistCredit->GetArtist(),
1261 static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit)));
1263 // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
1264 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
1265 AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort());
1267 if (bArtistLinks)
1268 CheckArtistLinksChanged();
1271 return true;
1274 int CMusicDatabase::UpdateSong(int idSong,
1275 const std::string& strTitle,
1276 const std::string& strMusicBrainzTrackID,
1277 const std::string& strPathAndFileName,
1278 const std::string& strComment,
1279 const std::string& strMood,
1280 const std::string& strThumb,
1281 const std::string& artistDisp,
1282 const std::string& artistSort,
1283 const std::vector<std::string>& genres,
1284 int iTrack,
1285 int iDuration,
1286 const std::string& strReleaseDate,
1287 const std::string& strOrigReleaseDate,
1288 const std::string& strDiscSubtitle,
1289 int iTimesPlayed,
1290 int iStartOffset,
1291 int iEndOffset,
1292 const CDateTime& dtLastPlayed,
1293 float rating,
1294 int userrating,
1295 int votes,
1296 const ReplayGain& replayGain,
1297 int iBPM,
1298 int iBitRate,
1299 int iSampleRate,
1300 int iChannels)
1302 if (idSong < 0)
1303 return -1;
1305 std::string strSQL;
1306 std::string strPath, strFileName;
1307 SplitPath(strPathAndFileName, strPath, strFileName);
1308 int idPath = AddPath(strPath);
1310 // Validate ISO8601 dates and ensure none missing
1311 std::string strRelease = strReleaseDate;
1312 std::string strOriginal = strOrigReleaseDate;
1313 NormaliseSongDates(strRelease, strOriginal);
1315 std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1317 strSQL = PrepareSQL(
1318 "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
1319 " strTitle = '%s', iTrack = %i, iDuration = %i, "
1320 "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
1321 "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
1322 "dateAdded = '%s'",
1323 idPath, artistDisp.c_str(),
1324 StringUtils::Join(
1325 genres,
1326 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)
1327 .c_str(),
1328 strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(),
1329 strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels,
1330 strDateMedia.c_str());
1331 if (strMusicBrainzTrackID.empty())
1332 strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL");
1333 else
1334 strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str());
1335 if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1336 strSQL += PrepareSQL(", strArtistSort = NULL");
1337 else
1338 strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str());
1340 strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
1341 "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
1342 iStartOffset, iEndOffset, static_cast<double>(rating), userrating, votes,
1343 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1345 if (dtLastPlayed.IsValid())
1346 strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1347 dtLastPlayed.GetAsDBDateTime().c_str());
1348 else if (iTimesPlayed > 0)
1349 strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1350 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
1351 else
1352 strSQL += ", iTimesPlayed = 0, lastplayed = NULL ";
1353 strSQL += PrepareSQL("WHERE idSong = %i", idSong);
1355 bool status = ExecuteQuery(strSQL);
1357 if (status)
1358 AnnounceUpdate(MediaTypeSong, idSong);
1359 return idSong;
1362 int CMusicDatabase::AddAlbum(const std::string& strAlbum,
1363 const std::string& strMusicBrainzAlbumID,
1364 const std::string& strReleaseGroupMBID,
1365 const std::string& strArtist,
1366 const std::string& strArtistSort,
1367 const std::string& strGenre,
1368 const std::string& strReleaseDate,
1369 const std::string& strOrigReleaseDate,
1370 bool bBoxedSet,
1371 const std::string& strRecordLabel,
1372 const std::string& strType,
1373 const std::string& strReleaseStatus,
1374 bool bCompilation,
1375 CAlbum::ReleaseType releaseType)
1377 std::string strSQL;
1380 if (nullptr == m_pDB)
1381 return -1;
1382 if (nullptr == m_pDS)
1383 return -1;
1385 if (!strMusicBrainzAlbumID.empty())
1386 strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
1387 strMusicBrainzAlbumID.c_str());
1388 else
1389 strSQL = PrepareSQL("SELECT * FROM album "
1390 "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
1391 "AND strMusicBrainzAlbumID IS NULL",
1392 strArtist.c_str(), strAlbum.c_str());
1393 m_pDS->query(strSQL);
1394 std::string strCheckFlag = strType;
1395 StringUtils::ToLower(strCheckFlag);
1396 if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type
1397 bBoxedSet = true;
1398 if (m_pDS->num_rows() == 0)
1400 m_pDS->close();
1401 // Does not exist, add it
1402 strSQL =
1403 PrepareSQL("INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
1404 "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
1405 "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, "
1406 "strMusicBrainzAlbumID, "
1407 "strReleaseGroupMBID, strArtistSort) "
1408 "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
1409 strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
1410 strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet, //
1411 strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), //
1412 bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str());
1414 if (strMusicBrainzAlbumID.empty())
1415 strSQL += PrepareSQL(", NULL");
1416 else
1417 strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str());
1418 if (strReleaseGroupMBID.empty())
1419 strSQL += PrepareSQL(", NULL");
1420 else
1421 strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str());
1422 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1423 strSQL += PrepareSQL(", NULL");
1424 else
1425 strSQL += PrepareSQL(", '%s'", strArtistSort.c_str());
1426 strSQL += ")";
1427 m_pDS->exec(strSQL);
1429 return (int)m_pDS->lastinsertid();
1431 else
1433 /* Exists in our database and being re-scanned from tags, so we should update it as the details
1434 may have changed.
1436 Note that for multi-folder albums this will mean the last folder scanned will have the information
1437 stored for it. Most values here should be the same across all songs anyway, but it does mean
1438 that if there's any inconsistencies then only the last folders information will be taken.
1440 We make sure we clear out the link tables (album artists, album sources) and we reset
1441 the last scraped time to make sure that online metadata is re-fetched. */
1442 int idAlbum = m_pDS->fv("idAlbum").get_asInt();
1443 m_pDS->close();
1445 strSQL = "UPDATE album SET ";
1446 if (!strMusicBrainzAlbumID.empty())
1447 strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", //
1448 strAlbum.c_str(), strArtist.c_str());
1449 if (strReleaseGroupMBID.empty())
1450 strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,");
1451 else
1452 strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str());
1453 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1454 strSQL += PrepareSQL(" strArtistSort = NULL");
1455 else
1456 strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str());
1458 strSQL +=
1459 PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1460 "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
1461 "bCompilation=%i, strReleaseType = '%s', "
1462 "lastScraped = NULL "
1463 "WHERE idAlbum=%i",
1464 strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
1465 bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(),
1466 bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(), //
1467 idAlbum);
1468 m_pDS->exec(strSQL);
1469 DeleteAlbumArtistsByAlbum(idAlbum);
1470 DeleteAlbumSources(idAlbum);
1471 return idAlbum;
1474 catch (...)
1476 CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
1479 return -1;
1482 int CMusicDatabase::UpdateAlbum(int idAlbum,
1483 const std::string& strAlbum,
1484 const std::string& strMusicBrainzAlbumID,
1485 const std::string& strReleaseGroupMBID,
1486 const std::string& strArtist,
1487 const std::string& strArtistSort,
1488 const std::string& strGenre,
1489 const std::string& strMoods,
1490 const std::string& strStyles,
1491 const std::string& strThemes,
1492 const std::string& strReview,
1493 const std::string& strImage,
1494 const std::string& strLabel,
1495 const std::string& strType,
1496 const std::string& strReleaseStatus,
1497 float fRating,
1498 int iUserrating,
1499 int iVotes,
1500 const std::string& strReleaseDate,
1501 const std::string& strOrigReleaseDate,
1502 bool bBoxedSet,
1503 bool bCompilation,
1504 CAlbum::ReleaseType releaseType,
1505 bool bScrapedMBID)
1507 if (idAlbum < 0)
1508 return -1;
1510 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1511 // Truncate value cleaning up xml when URLs exceeds this
1512 std::string strImageURLs = strImage;
1513 if (StringUtils::EqualsNoCase(
1514 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1515 "mysql"))
1516 TrimImageURLs(strImageURLs, 65535);
1518 std::string strSQL;
1519 strSQL = PrepareSQL("UPDATE album SET "
1520 " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
1521 " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
1522 " strReview = '%s', strImage = '%s', strLabel = '%s', "
1523 " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
1524 " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1525 " bBoxedSet = %i, bCompilation = %i,"
1526 " strReleaseType = '%s', strReleaseStatus = '%s', "
1527 " lastScraped = '%s', bScrapedMBID = %i",
1528 strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
1529 strMoods.c_str(), strStyles.c_str(), strThemes.c_str(), //
1530 strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(), //
1531 strType.c_str(), static_cast<double>(fRating), iUserrating, iVotes, //
1532 strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
1533 bBoxedSet, bCompilation, //
1534 CAlbum::ReleaseTypeToString(releaseType).c_str(), strReleaseStatus.c_str(), //
1535 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), bScrapedMBID);
1536 if (strMusicBrainzAlbumID.empty())
1537 strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL");
1538 else
1539 strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str());
1540 if (strReleaseGroupMBID.empty())
1541 strSQL += PrepareSQL(", strReleaseGroupMBID = NULL");
1542 else
1543 strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str());
1544 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1545 strSQL += PrepareSQL(", strArtistSort = NULL");
1546 else
1547 strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str());
1549 strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum);
1551 bool status = ExecuteQuery(strSQL);
1552 if (status)
1553 AnnounceUpdate(MediaTypeAlbum, idAlbum);
1554 return idAlbum;
1557 bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */)
1561 if (nullptr == m_pDB)
1562 return false;
1563 if (nullptr == m_pDS)
1564 return false;
1566 if (idAlbum == -1)
1567 return false; // not in the database
1569 //Get album, song and album song info data using separate queries/datasets because we can have
1570 //multiple roles per artist for songs and that makes a single combined join impractical
1571 //Get album data
1572 std::string sql;
1573 sql = PrepareSQL("SELECT albumview.*,albumartistview.* "
1574 " FROM albumview "
1575 " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
1576 " WHERE albumview.idAlbum = %ld "
1577 " ORDER BY albumartistview.iOrder",
1578 idAlbum);
1580 CLog::Log(LOGDEBUG, "{}", sql);
1581 if (!m_pDS->query(sql))
1582 return false;
1583 if (m_pDS->num_rows() == 0)
1585 m_pDS->close();
1586 return false;
1589 int albumArtistOffset = album_enumCount;
1591 album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0,
1592 true); // true to grab and parse the imageURL
1593 while (!m_pDS->eof())
1595 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1597 // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
1598 // so there is only one row in the result set for each artist credit.
1599 album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
1601 m_pDS->next();
1603 m_pDS->close(); // cleanup recordset data
1605 //Get song data
1606 if (getSongs)
1608 sql = PrepareSQL("SELECT songview.*, songartistview.*"
1609 " FROM songview "
1610 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1611 " WHERE songview.idAlbum = %ld "
1612 " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder",
1613 idAlbum);
1615 CLog::Log(LOGDEBUG, "{}", sql);
1616 if (!m_pDS->query(sql))
1617 return false;
1618 if (m_pDS->num_rows() == 0) //Album with no songs
1620 m_pDS->close();
1621 return false;
1624 int songArtistOffset = song_enumCount;
1625 std::set<int> songs;
1626 while (!m_pDS->eof())
1628 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1630 int idSong = record->at(song_idSong).get_asInt(); //Same as songartist.idSong by join
1631 if (songs.find(idSong) == songs.end())
1633 album.songs.emplace_back(GetSongFromDataset(record));
1634 songs.insert(idSong);
1637 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1638 //By query order song is the last one appended to the album song vector.
1639 if (idSongArtistRole == ROLE_ARTIST)
1640 album.songs.back().artistCredits.emplace_back(
1641 GetArtistCreditFromDataset(record, songArtistOffset));
1642 else
1643 album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1645 m_pDS->next();
1647 m_pDS->close(); // cleanup recordset data
1650 return true;
1652 catch (...)
1654 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
1657 return false;
1660 bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum)
1662 std::string strSQL =
1663 PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum);
1664 return ExecuteQuery(strSQL);
1667 bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum)
1669 std::string strSQL =
1670 PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum);
1671 return GetSingleValue(strSQL).empty();
1674 int CMusicDatabase::AddGenre(std::string& strGenre)
1676 std::string strSQL;
1679 StringUtils::Trim(strGenre);
1681 if (strGenre.empty())
1682 strGenre = g_localizeStrings.Get(13205); // Unknown
1684 if (nullptr == m_pDB)
1685 return -1;
1686 if (nullptr == m_pDS)
1687 return -1;
1689 auto it = m_genreCache.find(strGenre);
1690 if (it != m_genreCache.end())
1691 return it->second;
1694 strSQL = PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'",
1695 strGenre.c_str());
1696 m_pDS->query(strSQL);
1697 if (m_pDS->num_rows() == 0)
1699 m_pDS->close();
1700 // doesn't exists, add it
1701 strSQL = PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )",
1702 strGenre.c_str());
1703 m_pDS->exec(strSQL);
1705 int idGenre = (int)m_pDS->lastinsertid();
1706 m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1707 return idGenre;
1709 else
1711 int idGenre = m_pDS->fv("idGenre").get_asInt();
1712 strGenre = m_pDS->fv("strGenre").get_asString();
1713 m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1714 m_pDS->close();
1715 return idGenre;
1718 catch (...)
1720 CLog::Log(LOGERROR, "musicdatabase:unable to addgenre ({})", strSQL);
1723 return -1;
1726 bool CMusicDatabase::UpdateArtist(const CArtist& artist)
1728 SetLibraryLastUpdated();
1730 const std::string itemSeparator =
1731 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
1733 UpdateArtist(artist.idArtist, //
1734 artist.strArtist, //
1735 artist.strSortName, //
1736 artist.strMusicBrainzArtistID, //
1737 artist.bScrapedMBID, //
1738 artist.strType, //
1739 artist.strGender, //
1740 artist.strDisambiguation, //
1741 artist.strBorn, //
1742 artist.strFormed, //
1743 StringUtils::Join(artist.genre, itemSeparator), //
1744 StringUtils::Join(artist.moods, itemSeparator), //
1745 StringUtils::Join(artist.styles, itemSeparator), //
1746 StringUtils::Join(artist.instruments, itemSeparator), //
1747 artist.strBiography, //
1748 artist.strDied, //
1749 artist.strDisbanded, //
1750 StringUtils::Join(artist.yearsActive, itemSeparator).c_str(), //
1751 artist.thumbURL.GetData());
1753 DeleteArtistDiscography(artist.idArtist);
1754 for (const auto& disc : artist.discography)
1756 AddArtistDiscography(artist.idArtist, disc);
1759 // Set current artwork (held in art table)
1760 if (!artist.art.empty())
1761 SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
1763 return true;
1766 int CMusicDatabase::AddArtist(const std::string& strArtist,
1767 const std::string& strMusicBrainzArtistID,
1768 const std::string& strSortName,
1769 bool bScrapedMBID /* = false*/)
1771 std::string strSQL;
1772 int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID);
1773 if (idArtist < 0 || strSortName.empty())
1774 return idArtist;
1776 /* Artist sort name always taken as the first value provided that is different from name, so only
1777 update when current sort name is blank. If a new sortname the same as name is provided then
1778 clear any sortname currently held.
1783 if (nullptr == m_pDB)
1784 return -1;
1785 if (nullptr == m_pDS)
1786 return -1;
1788 strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist);
1789 m_pDS->query(strSQL);
1790 if (m_pDS->num_rows() != 1)
1792 m_pDS->close();
1793 return -1;
1795 std::string strArtistName, strArtistSort;
1796 strArtistName = m_pDS->fv("strArtist").get_asString();
1797 strArtistSort = m_pDS->fv("strSortName").get_asString();
1798 m_pDS->close();
1800 if (!strArtistSort.empty())
1802 if (strSortName.compare(strArtistName) == 0)
1803 m_pDS->exec(
1804 PrepareSQL("UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist));
1806 else if (strSortName.compare(strArtistName) != 0)
1807 m_pDS->exec(PrepareSQL("UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
1808 strSortName.c_str(), idArtist));
1810 return idArtist;
1813 catch (...)
1815 CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname ({})", strSQL);
1818 return -1;
1821 int CMusicDatabase::AddArtist(const std::string& strArtist,
1822 const std::string& strMusicBrainzArtistID,
1823 bool bScrapedMBID /* = false*/)
1825 std::string strSQL;
1828 if (nullptr == m_pDB)
1829 return -1;
1830 if (nullptr == m_pDS)
1831 return -1;
1833 // 1) MusicBrainz
1834 if (!strMusicBrainzArtistID.empty())
1836 // 1.a) Match on a MusicBrainz ID
1837 strSQL =
1838 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
1839 strMusicBrainzArtistID.c_str());
1840 m_pDS->query(strSQL);
1841 if (m_pDS->num_rows() > 0)
1843 int idArtist = m_pDS->fv("idArtist").get_asInt();
1844 bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0;
1845 m_pDS->close();
1846 if (update)
1848 strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' "
1849 "WHERE idArtist = %i",
1850 strArtist.c_str(), idArtist);
1851 m_pDS->exec(strSQL);
1852 m_pDS->close();
1854 return idArtist;
1856 m_pDS->close();
1859 // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
1860 // and update that if it exists.
1861 strSQL = PrepareSQL("SELECT idArtist FROM artist "
1862 "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
1863 strArtist.c_str());
1864 m_pDS->query(strSQL);
1865 if (m_pDS->num_rows() > 0)
1867 int idArtist = m_pDS->fv("idArtist").get_asInt();
1868 m_pDS->close();
1869 // 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
1870 strSQL =
1871 PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
1872 "bScrapedMBID = %i WHERE idArtist = %i",
1873 strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID, idArtist);
1874 m_pDS->exec(strSQL);
1875 return idArtist;
1878 // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
1879 // With MusicBrainz IDs this could return multiple artists and is non-determinstic
1880 // Always pick the first artist ID returned by the DB to return.
1882 else
1884 strSQL =
1885 PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
1887 m_pDS->query(strSQL);
1888 if (m_pDS->num_rows() > 0)
1890 int idArtist = m_pDS->fv("idArtist").get_asInt();
1891 m_pDS->close();
1892 return idArtist;
1894 m_pDS->close();
1897 // 3) No artist exists at all - add it, flagging when has scraped mbid
1898 if (strMusicBrainzArtistID.empty())
1899 strSQL = PrepareSQL("INSERT INTO artist "
1900 "(idArtist, strArtist, strMusicBrainzArtistID) "
1901 "VALUES( NULL, '%s', NULL)",
1902 strArtist.c_str());
1903 else
1904 strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
1905 "bScrapedMBID) "
1906 "VALUES( NULL, '%s', '%s', %i )",
1907 strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID);
1909 m_pDS->exec(strSQL);
1910 int idArtist = (int)m_pDS->lastinsertid();
1911 return idArtist;
1913 catch (...)
1915 CLog::Log(LOGERROR, "musicdatabase:unable to addartist ({})", strSQL);
1918 return -1;
1921 int CMusicDatabase::UpdateArtist(int idArtist,
1922 const std::string& strArtist,
1923 const std::string& strSortName,
1924 const std::string& strMusicBrainzArtistID,
1925 const bool bScrapedMBID,
1926 const std::string& strType,
1927 const std::string& strGender,
1928 const std::string& strDisambiguation,
1929 const std::string& strBorn,
1930 const std::string& strFormed,
1931 const std::string& strGenres,
1932 const std::string& strMoods,
1933 const std::string& strStyles,
1934 const std::string& strInstruments,
1935 const std::string& strBiography,
1936 const std::string& strDied,
1937 const std::string& strDisbanded,
1938 const std::string& strYearsActive,
1939 const std::string& strImage)
1941 if (idArtist < 0)
1942 return -1;
1944 // Check another artist with this mbid not already exist (an alias for example)
1945 bool useMBIDNull = strMusicBrainzArtistID.empty();
1946 bool isScrapedMBID = bScrapedMBID;
1947 std::string artistname;
1948 int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
1949 if (idArtistMbid > 0 && idArtistMbid != idArtist)
1951 CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
1952 __FUNCTION__, strMusicBrainzArtistID, artistname, idArtistMbid, strArtist, idArtist);
1953 useMBIDNull = true;
1954 isScrapedMBID = false;
1957 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1958 // Truncate value cleaning up xml when URLs exceeds this
1959 std::string strImageURLs = strImage;
1960 if (StringUtils::EqualsNoCase(
1961 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1962 "mysql"))
1963 TrimImageURLs(strImageURLs, 65535);
1965 std::string strSQL;
1966 strSQL = PrepareSQL("UPDATE artist SET "
1967 " strArtist = '%s', "
1968 " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
1969 " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
1970 " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
1971 " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
1972 " strYearsActive = '%s', strImage = '%s', "
1973 " lastScraped = '%s', bScrapedMBID = %i",
1974 strArtist.c_str(),
1975 /* strSortName.c_str(),*/
1976 /* strMusicBrainzArtistID.c_str(), */
1977 strType.c_str(), strGender.c_str(), strDisambiguation.c_str(), //
1978 strBorn.c_str(), strFormed.c_str(), strGenres.c_str(), //
1979 strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(), //
1980 strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(), //
1981 strYearsActive.c_str(), strImageURLs.c_str(), //
1982 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID);
1983 if (useMBIDNull)
1984 strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL");
1985 else
1986 strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str());
1987 if (strSortName.empty())
1988 strSQL += PrepareSQL(", strSortName = NULL");
1989 else
1990 strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str());
1992 strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist);
1994 bool status = ExecuteQuery(strSQL);
1995 if (status)
1996 AnnounceUpdate(MediaTypeArtist, idArtist);
1997 return idArtist;
2000 bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist,
2001 const std::string& strMusicBrainzArtistID)
2003 if (strMusicBrainzArtistID.empty() || idArtist < 0)
2004 return false;
2006 // Check another artist with this mbid not already exist (an alias for example)
2007 std::string artistname;
2008 int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
2009 if (idArtistMbid > 0 && idArtistMbid != idArtist)
2011 CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__,
2012 strMusicBrainzArtistID, artistname, idArtistMbid);
2013 return false;
2016 // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
2017 std::string strSQL;
2018 strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
2019 "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
2020 strMusicBrainzArtistID.c_str(), idArtist);
2022 bool status = ExecuteQuery(strSQL);
2023 if (status)
2025 AnnounceUpdate(MediaTypeArtist, idArtist);
2026 return true;
2028 return false;
2031 bool CMusicDatabase::GetArtist(int idArtist, CArtist& artist, bool fetchAll /* = false */)
2035 auto start = std::chrono::steady_clock::now();
2036 if (nullptr == m_pDB)
2037 return false;
2038 if (nullptr == m_pDS)
2039 return false;
2041 if (idArtist == -1)
2042 return false; // not in the database
2044 std::string strSQL;
2045 if (fetchAll)
2046 strSQL = PrepareSQL("SELECT * FROM artistview "
2047 "LEFT JOIN discography ON artistview.idArtist = discography.idArtist "
2048 "WHERE artistview.idArtist = %i",
2049 idArtist);
2050 else
2051 strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist);
2053 if (!m_pDS->query(strSQL))
2054 return false;
2055 if (m_pDS->num_rows() == 0)
2057 m_pDS->close();
2058 return false;
2061 int discographyOffset = artist_enumCount;
2063 artist.discography.clear();
2064 artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs
2065 if (fetchAll)
2067 while (!m_pDS->eof())
2069 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
2070 CDiscoAlbum discoAlbum;
2071 discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString();
2072 discoAlbum.strYear = record->at(discographyOffset + 2).get_asString();
2073 discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString();
2074 artist.discography.emplace_back(discoAlbum);
2075 m_pDS->next();
2078 m_pDS->close(); // cleanup recordset data
2080 auto end = std::chrono::steady_clock::now();
2081 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
2083 CLog::Log(LOGDEBUG, LOGDATABASE, "{0}({1}) - took {2} ms", __FUNCTION__, strSQL,
2084 duration.count());
2086 return true;
2088 catch (...)
2090 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2093 return false;
2096 bool CMusicDatabase::GetArtistExists(int idArtist)
2100 if (nullptr == m_pDB)
2101 return false;
2102 if (nullptr == m_pDS)
2103 return false;
2105 std::string strSQL =
2106 PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist);
2108 if (!m_pDS->query(strSQL))
2109 return false;
2110 if (m_pDS->num_rows() == 0)
2112 m_pDS->close();
2113 return false;
2115 m_pDS->close(); // cleanup recordset data
2116 return true;
2118 catch (...)
2120 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2123 return false;
2126 int CMusicDatabase::GetLastArtist()
2128 std::string strSQL = "SELECT MAX(idArtist) FROM artist";
2129 std::string lastArtist = GetSingleValue(strSQL);
2130 if (lastArtist.empty())
2131 return -1;
2133 return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10));
2136 int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID,
2137 std::string& artistname)
2139 if (strMusicBrainzArtistID.empty())
2140 return -1;
2142 std::string strSQL;
2145 if (nullptr == m_pDB || nullptr == m_pDS2)
2146 return -1;
2147 // Match on MusicBrainz ID, definitively unique
2148 strSQL =
2149 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
2150 strMusicBrainzArtistID.c_str());
2151 if (!m_pDS2->query(strSQL))
2152 return -1;
2153 int idArtist = -1;
2154 if (m_pDS2->num_rows() > 0)
2156 idArtist = m_pDS2->fv("idArtist").get_asInt();
2157 artistname = m_pDS2->fv("strArtist").get_asString();
2159 m_pDS2->close();
2160 return idArtist;
2162 catch (...)
2164 CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__, strSQL);
2166 return -1;
2169 bool CMusicDatabase::HasArtistBeenScraped(int idArtist)
2171 std::string strSQL = PrepareSQL(
2172 "SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist);
2173 return GetSingleValue(strSQL).empty();
2176 bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist)
2178 std::string strSQL =
2179 PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist);
2180 return ExecuteQuery(strSQL);
2183 int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum)
2185 std::string strSQL = PrepareSQL("INSERT INTO discography "
2186 "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
2187 "VALUES(%i, '%s', '%s', '%s')",
2188 idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(),
2189 discoAlbum.strReleaseGroupMBID.c_str());
2190 return ExecuteQuery(strSQL);
2193 bool CMusicDatabase::DeleteArtistDiscography(int idArtist)
2195 std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist);
2196 return ExecuteQuery(strSQL);
2199 bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items)
2203 if (nullptr == m_pDB)
2204 return false;
2205 if (nullptr == m_pDS)
2206 return false;
2208 /* Combine entries from discography and album tables
2209 Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using
2210 correlated subqueries to a temp table. An updatable join to temp table would work in MySQL
2211 but SQLite not support updatable joins.
2213 m_pDS->exec("CREATE TABLE tempDisco "
2214 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2215 m_pDS->exec("CREATE TABLE tempAlbum "
2216 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2218 std::string strSQL;
2219 strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
2220 "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
2221 "strReleaseGroupMBID, NULL "
2222 "FROM discography WHERE idArtist = %i",
2223 idArtist);
2224 m_pDS->exec(strSQL);
2226 strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
2227 "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
2228 "strReleaseGroupMBID, album.idAlbum "
2229 "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
2230 "WHERE idArtist = %i",
2231 idArtist);
2232 m_pDS->exec(strSQL);
2234 // Match albums on release group mbid, if multi-releases then first used
2235 // Only use albums credited to this artist
2236 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
2237 "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
2238 m_pDS->exec(strSQL);
2239 //Delete matched albums
2240 strSQL = "DELETE FROM tempAlbum "
2241 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2242 m_pDS->exec(strSQL);
2244 // Match remaining to albums by artist on title and year
2245 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2246 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
2247 "AND tempAlbum.strYear = tempDisco.strYear) "
2248 "WHERE tempDisco.idAlbum is NULL";
2249 m_pDS->exec(strSQL);
2250 //Delete matched albums
2251 strSQL = "DELETE FROM tempAlbum "
2252 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2253 m_pDS->exec(strSQL);
2255 // Match remaining to albums by artist on title only
2256 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2257 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
2258 "WHERE tempDisco.idAlbum is NULL";
2259 m_pDS->exec(strSQL);
2260 // Use year from album table, when matched by title only as it could be different
2261 strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
2262 "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
2263 "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
2264 m_pDS->exec(strSQL);
2265 //Delete matched albums
2266 strSQL = "DELETE FROM tempAlbum "
2267 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2268 m_pDS->exec(strSQL);
2270 // Combine distinctly with any remaining unmatched albums by artist
2271 strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
2272 "UNION "
2273 "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
2274 "ORDER BY strYear, strAlbum, idAlbum";
2276 if (!m_pDS->query(strSQL))
2277 return false;
2278 int iRowsFound = m_pDS->num_rows();
2279 if (iRowsFound == 0)
2281 m_pDS->close();
2282 return true;
2285 while (!m_pDS->eof())
2287 int idAlbum = m_pDS->fv("idAlbum").get_asInt();
2288 if (idAlbum == 0)
2289 idAlbum = -1;
2290 std::string strAlbum = m_pDS->fv("strAlbum").get_asString();
2291 if (!strAlbum.empty())
2293 CFileItemPtr pItem(new CFileItem(strAlbum));
2294 pItem->SetLabel2(m_pDS->fv("strYear").get_asString());
2295 pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum);
2296 items.Add(pItem);
2298 m_pDS->next();
2301 // cleanup
2302 m_pDS->close();
2303 m_pDS->exec("DROP TABLE tempDisco");
2304 m_pDS->exec("DROP TABLE tempAlbum");
2306 return true;
2308 catch (...)
2310 m_pDS->exec("DROP TABLE tempDisco");
2311 m_pDS->exec("DROP TABLE tempAlbum");
2312 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
2314 return false;
2317 int CMusicDatabase::AddRole(const std::string& strRole)
2319 int idRole = -1;
2320 std::string strSQL;
2324 if (nullptr == m_pDB)
2325 return -1;
2326 if (nullptr == m_pDS)
2327 return -1;
2328 strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str());
2329 m_pDS->query(strSQL);
2330 if (m_pDS->num_rows() > 0)
2331 idRole = m_pDS->fv("idRole").get_asInt();
2332 m_pDS->close();
2334 if (idRole < 0)
2336 strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str());
2337 m_pDS->exec(strSQL);
2338 idRole = static_cast<int>(m_pDS->lastinsertid());
2339 m_pDS->close();
2342 catch (...)
2344 CLog::Log(LOGERROR, "musicdatabase:unable to AddRole ({})", strSQL);
2346 return idRole;
2349 bool CMusicDatabase::AddSongArtist(
2350 int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder)
2352 int idRole = AddRole(strRole);
2353 return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder);
2356 bool CMusicDatabase::AddSongArtist(
2357 int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder)
2359 std::string strSQL;
2360 strSQL = PrepareSQL("REPLACE INTO song_artist (idArtist, idSong, idRole, strArtist, iOrder) "
2361 "VALUES(%i, %i, %i,'%s', %i)",
2362 idArtist, idSong, idRole, strArtist.c_str(), iOrder);
2363 return ExecuteQuery(strSQL);
2366 int CMusicDatabase::AddSongContributor(int idSong,
2367 const std::string& strRole,
2368 const std::string& strArtist,
2369 const std::string& strSort)
2371 if (strArtist.empty())
2372 return -1;
2374 std::string strSQL;
2377 if (nullptr == m_pDB)
2378 return -1;
2379 if (nullptr == m_pDS)
2380 return -1;
2382 int idArtist = -1;
2383 // Add artist. As we only have name (no MBID) first try to identify artist from song
2384 // as they may have already been added with a different role (including MBID).
2385 strSQL =
2386 PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ",
2387 idSong, strArtist.c_str());
2388 m_pDS->query(strSQL);
2389 if (m_pDS->num_rows() > 0)
2390 idArtist = m_pDS->fv("idArtist").get_asInt();
2391 m_pDS->close();
2393 if (idArtist < 0)
2394 idArtist = AddArtist(strArtist, "", strSort);
2396 // Add to song_artist table
2397 AddSongArtist(idArtist, idSong, strRole, strArtist, 0);
2399 return idArtist;
2401 catch (...)
2403 CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor ({})", strSQL);
2406 return -1;
2409 void CMusicDatabase::AddSongContributors(int idSong,
2410 const VECMUSICROLES& contributors,
2411 const std::string& strSort)
2413 std::vector<std::string> composerSort;
2414 size_t countComposer = 0;
2415 if (!strSort.empty())
2417 composerSort = StringUtils::Split(
2418 strSort,
2419 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2422 for (const auto& credit : contributors)
2424 std::string strSortName;
2425 //Identify composer sort name if we have it
2426 if (countComposer < composerSort.size())
2428 if (credit.GetRoleDesc().compare("Composer") == 0)
2430 strSortName = composerSort[countComposer];
2431 countComposer++;
2434 AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName);
2438 int CMusicDatabase::GetRoleByName(const std::string& strRole)
2442 if (nullptr == m_pDB)
2443 return false;
2444 if (nullptr == m_pDS)
2445 return false;
2447 std::string strSQL;
2448 strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str());
2449 // run query
2450 if (!m_pDS->query(strSQL))
2451 return false;
2452 int iRowsFound = m_pDS->num_rows();
2453 if (iRowsFound != 1)
2455 m_pDS->close();
2456 return -1;
2458 return m_pDS->fv("idRole").get_asInt();
2460 catch (...)
2462 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
2464 return -1;
2467 bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item)
2471 std::string strSQL =
2472 PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole "
2473 "FROM song_artist JOIN role ON song_artist.idRole = role.idRole "
2474 "WHERE idArtist = %i ORDER BY song_artist.idRole ASC",
2475 idArtist);
2476 if (!m_pDS->query(strSQL))
2477 return false;
2478 if (m_pDS->num_rows() == 0)
2480 m_pDS->close();
2481 return true;
2484 CVariant artistRoles(CVariant::VariantTypeArray);
2486 while (!m_pDS->eof())
2488 CVariant roleObj;
2489 roleObj["role"] = m_pDS->fv("strRole").get_asString();
2490 roleObj["roleid"] = m_pDS->fv("idrole").get_asInt();
2491 artistRoles.push_back(roleObj);
2492 m_pDS->next();
2494 m_pDS->close();
2496 item->SetProperty("roles", artistRoles);
2497 return true;
2499 catch (...)
2501 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2503 return false;
2506 bool CMusicDatabase::DeleteSongArtistsBySong(int idSong)
2508 return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong));
2511 bool CMusicDatabase::AddAlbumArtist(int idArtist,
2512 int idAlbum,
2513 const std::string& strArtist,
2514 int iOrder)
2516 std::string strSQL;
2517 strSQL = PrepareSQL("REPLACE INTO album_artist (idArtist, idAlbum, strArtist, iOrder) "
2518 "VALUES(%i,%i,'%s',%i)",
2519 idArtist, idAlbum, strArtist.c_str(), iOrder);
2520 return ExecuteQuery(strSQL);
2523 bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum)
2525 return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum));
2528 bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres)
2530 if (idSong == -1)
2531 return true;
2533 std::string strSQL;
2536 // Clear current entries for song
2537 strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong);
2538 if (!ExecuteQuery(strSQL))
2539 return false;
2540 unsigned int index = 0;
2541 std::vector<std::string> modgenres = genres;
2542 for (auto& strGenre : modgenres)
2544 int idGenre = AddGenre(strGenre); // Genre string trimmed and matched case-insensitively
2545 strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
2546 idGenre, idSong, index++);
2547 if (!ExecuteQuery(strSQL))
2548 return false;
2550 // Update concatenated genre string from the standardised genre values
2551 std::string strGenres = StringUtils::Join(
2552 modgenres,
2553 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2554 strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", //
2555 strGenres.c_str(), idSong);
2556 if (!ExecuteQuery(strSQL))
2557 return false;
2559 return true;
2561 catch (...)
2563 CLog::Log(LOGERROR, "{}({}) {} failed", __FUNCTION__, idSong, strSQL);
2565 return false;
2568 bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int>& albums)
2572 std::string strSQL;
2573 strSQL = PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist);
2574 if (!m_pDS->query(strSQL))
2575 return false;
2576 if (m_pDS->num_rows() == 0)
2578 m_pDS->close();
2579 return false;
2582 while (!m_pDS->eof())
2584 albums.push_back(m_pDS->fv("idAlbum").get_asInt());
2585 m_pDS->next();
2587 m_pDS->close();
2588 return true;
2590 catch (...)
2592 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2594 return false;
2597 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item)
2601 std::string strSQL;
2603 strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum);
2605 if (!m_pDS->query(strSQL))
2606 return false;
2607 if (m_pDS->num_rows() == 0)
2609 m_pDS->close();
2610 return false;
2613 // Get album artist credits
2614 VECARTISTCREDITS artistCredits;
2615 while (!m_pDS->eof())
2617 artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0));
2618 m_pDS->next();
2620 m_pDS->close();
2622 // Populate item with song albumartist credits
2623 std::vector<std::string> musicBrainzID;
2624 std::vector<std::string> albumartists;
2625 CVariant artistidObj(CVariant::VariantTypeArray);
2626 for (const auto& artistCredit : artistCredits)
2628 artistidObj.push_back(artistCredit.GetArtistId());
2629 albumartists.emplace_back(artistCredit.GetArtist());
2630 if (!artistCredit.GetMusicBrainzArtistID().empty())
2631 musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID());
2633 item->GetMusicInfoTag()->SetAlbumArtist(albumartists);
2634 item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID);
2635 // Add song albumartistIds as separate property as not part of CMusicInfoTag
2636 item->SetProperty("albumartistid", artistidObj);
2638 return true;
2640 catch (...)
2642 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
2644 return false;
2647 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs)
2651 std::string strSQL;
2652 // Get distinct song and album artist IDs for this album, no other roles
2653 // Allow for artists that are only album artists and not song artists
2654 strSQL = PrepareSQL(
2655 "SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
2656 "UNION \n"
2657 "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
2658 "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
2659 idAlbum, idAlbum);
2661 if (!m_pDS->query(strSQL))
2662 return false;
2663 if (m_pDS->num_rows() == 0)
2665 m_pDS->close();
2666 return false;
2668 while (!m_pDS->eof())
2670 // Get ID as string so can easily join to make "IN" clause
2671 artistIDs.push_back(m_pDS->fv("idArtist").get_asString());
2672 m_pDS->next();
2674 m_pDS->close();
2675 return true;
2677 catch (...)
2679 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
2681 return false;
2684 bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int>& songs)
2688 std::string strSQL;
2689 //Restrict to Artists only, no other roles
2690 strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", //
2691 idArtist);
2692 if (!m_pDS->query(strSQL))
2693 return false;
2694 if (m_pDS->num_rows() == 0)
2696 m_pDS->close();
2697 return false;
2700 while (!m_pDS->eof())
2702 songs.push_back(m_pDS->fv("idSong").get_asInt());
2703 m_pDS->next();
2705 m_pDS->close();
2706 return true;
2708 catch (...)
2710 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2712 return false;
2715 bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int>& artists)
2719 std::string strSQL;
2720 //Restrict to Artists only, no other roles
2721 strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", //
2722 idSong);
2723 if (!m_pDS->query(strSQL))
2724 return false;
2725 if (m_pDS->num_rows() == 0)
2727 m_pDS->close();
2728 return false;
2731 while (!m_pDS->eof())
2733 artists.push_back(m_pDS->fv("idArtist").get_asInt());
2734 m_pDS->next();
2736 m_pDS->close();
2737 return true;
2739 catch (...)
2741 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
2743 return false;
2746 bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item)
2750 std::string strSQL;
2751 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
2752 "FROM album_artist "
2753 "JOIN song ON album_artist.idAlbum = song.idAlbum "
2754 "JOIN song_genre ON song.idSong = song_genre.idSong "
2755 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2756 "WHERE album_artist.idArtist = %i "
2757 "ORDER BY song_genre.idGenre",
2758 idArtist);
2759 if (!m_pDS->query(strSQL))
2760 return false;
2761 if (m_pDS->num_rows() == 0)
2763 // Artist does have any song genres via albums may not be an album artist.
2764 // Check via songs artist to fetch song genres from compilations or where they are guest artist
2765 m_pDS->close();
2766 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
2767 "FROM song_artist "
2768 "JOIN song_genre ON song_artist.idSong = song_genre.idSong "
2769 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2770 "WHERE song_artist.idArtist = %i "
2771 "ORDER BY song_genre.idGenre",
2772 idArtist);
2773 if (!m_pDS->query(strSQL))
2774 return false;
2775 if (m_pDS->num_rows() == 0)
2777 //No song genres, but query successful
2778 m_pDS->close();
2779 return true;
2783 CVariant artistSongGenres(CVariant::VariantTypeArray);
2785 while (!m_pDS->eof())
2787 CVariant genreObj;
2788 genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2789 genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2790 artistSongGenres.push_back(genreObj);
2791 m_pDS->next();
2793 m_pDS->close();
2795 item->SetProperty("songgenres", artistSongGenres);
2796 return true;
2798 catch (...)
2800 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2802 return false;
2805 bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item)
2809 std::string strSQL;
2810 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2811 "song JOIN song_genre ON song.idSong = song_genre.idSong "
2812 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2813 "WHERE song.idAlbum = %i "
2814 "ORDER BY song_genre.idSong, song_genre.iOrder",
2815 idAlbum);
2816 if (!m_pDS->query(strSQL))
2817 return false;
2818 if (m_pDS->num_rows() == 0)
2820 //No song genres, but query successful
2821 m_pDS->close();
2822 return true;
2825 CVariant albumSongGenres(CVariant::VariantTypeArray);
2827 while (!m_pDS->eof())
2829 CVariant genreObj;
2830 genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2831 genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2832 albumSongGenres.push_back(genreObj);
2833 m_pDS->next();
2835 m_pDS->close();
2837 item->SetProperty("songgenres", albumSongGenres);
2838 return true;
2840 catch (...)
2842 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
2844 return false;
2847 bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres)
2851 std::string strSQL = PrepareSQL("SELECT idGenre FROM song_genre "
2852 "WHERE idSong = %i ORDER BY iOrder ASC",
2853 idSong);
2854 if (!m_pDS->query(strSQL))
2855 return false;
2856 if (m_pDS->num_rows() == 0)
2858 m_pDS->close();
2859 return true;
2862 while (!m_pDS->eof())
2864 genres.push_back(m_pDS->fv("idGenre").get_asInt());
2865 m_pDS->next();
2867 m_pDS->close();
2869 return true;
2871 catch (...)
2873 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
2875 return false;
2878 bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item)
2882 int countalbum =
2883 GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist));
2884 CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean);
2885 IsAlbumArtistObj = (countalbum > 0);
2886 item->SetProperty("isalbumartist", IsAlbumArtistObj);
2887 return true;
2889 catch (...)
2891 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
2893 return false;
2897 int CMusicDatabase::AddPath(const std::string& strPath1)
2899 std::string strSQL;
2902 std::string strPath(strPath1);
2903 if (!URIUtils::HasSlashAtEnd(strPath))
2904 URIUtils::AddSlashAtEnd(strPath);
2906 if (nullptr == m_pDB)
2907 return -1;
2908 if (nullptr == m_pDS)
2909 return -1;
2911 auto it = m_pathCache.find(strPath);
2912 if (it != m_pathCache.end())
2913 return it->second;
2915 strSQL = PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath.c_str());
2916 m_pDS->query(strSQL);
2917 if (m_pDS->num_rows() == 0)
2919 m_pDS->close();
2920 // doesn't exists, add it
2921 strSQL = PrepareSQL("INSERT INTO path (idPath, strPath) "
2922 "VALUES(NULL, '%s')",
2923 strPath.c_str());
2924 m_pDS->exec(strSQL);
2926 int idPath = (int)m_pDS->lastinsertid();
2927 m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
2928 return idPath;
2930 else
2932 int idPath = m_pDS->fv("idPath").get_asInt();
2933 m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
2934 m_pDS->close();
2935 return idPath;
2938 catch (...)
2940 CLog::Log(LOGERROR, "musicdatabase:unable to addpath ({})", strSQL);
2943 return -1;
2946 CSong CMusicDatabase::GetSongFromDataset()
2948 return GetSongFromDataset(m_pDS->get_sql_record());
2951 CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record,
2952 int offset /* = 0 */)
2954 CSong song;
2955 song.idSong = record->at(offset + song_idSong).get_asInt();
2956 // Note this function does not populate artist credits, this must be done separately.
2957 // However artist names are held as a descriptive string
2958 song.strArtistDesc = record->at(offset + song_strArtists).get_asString();
2959 song.strArtistSort = record->at(offset + song_strArtistSort).get_asString();
2960 // Get the full genre string
2961 song.genre = StringUtils::Split(
2962 record->at(offset + song_strGenres).get_asString(),
2963 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2964 // and the rest...
2965 song.strAlbum = record->at(offset + song_strAlbum).get_asString();
2966 song.idAlbum = record->at(offset + song_idAlbum).get_asInt();
2967 song.iTrack = record->at(offset + song_iTrack).get_asInt();
2968 song.iDuration = record->at(offset + song_iDuration).get_asInt();
2969 song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString();
2970 song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString();
2971 song.strTitle = record->at(offset + song_strTitle).get_asString();
2972 song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt();
2973 song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString());
2974 song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString());
2975 song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString());
2976 song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString());
2977 song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt();
2978 song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt();
2979 song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString();
2980 song.rating = record->at(offset + song_rating).get_asFloat();
2981 song.userrating = record->at(offset + song_userrating).get_asInt();
2982 song.votes = record->at(offset + song_votes).get_asInt();
2983 song.strComment = record->at(offset + song_comment).get_asString();
2984 song.strMood = record->at(offset + song_mood).get_asString();
2985 song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1;
2986 song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString();
2987 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
2988 song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString());
2989 // Get filename with full path
2990 song.strFileName =
2991 URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(),
2992 record->at(offset + song_strFileName).get_asString());
2993 song.iBPM = record->at(offset + song_iBPM).get_asInt();
2994 song.iBitRate = record->at(offset + song_iBitRate).get_asInt();
2995 song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt();
2996 song.iChannels = record->at(offset + song_iChannels).get_asInt();
2997 return song;
3000 void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl)
3002 GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl);
3005 void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record,
3006 CFileItem* item,
3007 const CMusicDbUrl& baseUrl)
3009 // get the artist string from songview (not the song_artist and artist tables)
3010 item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString());
3011 // get the artist sort name string from songview (not the song_artist and artist tables)
3012 item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString());
3013 // and the full genre string
3014 item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString());
3015 // and the rest...
3016 item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString());
3017 item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt());
3018 item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt());
3019 item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt());
3020 item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong);
3021 item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString());
3022 item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString());
3023 item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString());
3024 item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString());
3025 item->SetLabel(record->at(song_strTitle).get_asString());
3026 item->SetStartOffset(record->at(song_iStartOffset).get_asInt64());
3027 item->SetProperty("item_start", item->GetStartOffset());
3028 item->SetEndOffset(record->at(song_iEndOffset).get_asInt64());
3029 item->GetMusicInfoTag()->SetMusicBrainzTrackID(
3030 record->at(song_strMusicBrainzTrackID).get_asString());
3031 item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat());
3032 item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt());
3033 item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt());
3034 item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString());
3035 item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString());
3036 item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt());
3037 item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString());
3038 item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString());
3039 item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString());
3040 item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString());
3041 std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(),
3042 record->at(song_strFileName).get_asString());
3043 item->GetMusicInfoTag()->SetURL(strRealPath);
3044 item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1);
3045 item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1);
3046 // get the album artist string from songview (not the album_artist and artist tables)
3047 item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString());
3048 item->GetMusicInfoTag()->SetAlbumReleaseType(
3049 CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString()));
3050 item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt());
3051 item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt());
3052 item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt());
3053 item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt());
3054 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
3055 ReplayGain replaygain;
3056 replaygain.Set(record->at(song_strReplayGain).get_asString());
3057 item->GetMusicInfoTag()->SetReplayGain(replaygain);
3058 item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt());
3060 item->GetMusicInfoTag()->SetLoaded(true);
3061 // Get filename with full path
3062 if (!baseUrl.IsValid())
3063 item->SetPath(strRealPath);
3064 else
3066 CMusicDbUrl itemUrl = baseUrl;
3067 std::string strFileName = record->at(song_strFileName).get_asString();
3068 std::string strExt = URIUtils::GetExtension(strFileName);
3069 std::string path = StringUtils::Format("{}{}", record->at(song_idSong).get_asInt(), strExt);
3070 itemUrl.AppendPath(path);
3071 item->SetPath(itemUrl.ToString());
3072 item->SetDynPath(strRealPath);
3076 void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item)
3078 // Populate fileitem with artists from vector of artist credits
3079 std::vector<std::string> musicBrainzID;
3080 std::vector<std::string> songartists;
3081 CVariant artistidObj(CVariant::VariantTypeArray);
3083 // When "missing tag" artist, it is the only artist when present.
3084 if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID)
3086 artistidObj.push_back((int)BLANKARTIST_ID);
3087 songartists.push_back(StringUtils::Empty);
3089 else
3091 for (const auto& artistCredit : artistCredits)
3093 artistidObj.push_back(artistCredit.GetArtistId());
3094 songartists.push_back(artistCredit.GetArtist());
3095 if (!artistCredit.GetMusicBrainzArtistID().empty())
3096 musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
3099 // Also sets ArtistDesc if empty from song.strArtist field
3100 item->GetMusicInfoTag()->SetArtist(songartists);
3101 item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID);
3102 // Add album artistIds as separate property as not part of CMusicInfoTag
3103 item->SetProperty("artistid", artistidObj);
3106 CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS,
3107 int offset /* = 0 */,
3108 bool imageURL /* = false*/)
3110 return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL);
3113 CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record,
3114 int offset /* = 0 */,
3115 bool imageURL /* = false*/)
3117 const std::string itemSeparator =
3118 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
3120 CAlbum album;
3121 album.idAlbum = record->at(offset + album_idAlbum).get_asInt();
3122 album.strAlbum = record->at(offset + album_strAlbum).get_asString();
3123 if (album.strAlbum.empty())
3124 album.strAlbum = g_localizeStrings.Get(1050);
3125 album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString();
3126 album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString();
3127 album.strArtistDesc = record->at(offset + album_strArtists).get_asString();
3128 album.strArtistSort = record->at(offset + album_strArtistSort).get_asString();
3129 album.genre =
3130 StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator);
3131 album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString();
3132 album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString();
3133 album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1;
3134 if (imageURL)
3135 album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString());
3136 album.fRating = record->at(offset + album_fRating).get_asFloat();
3137 album.iUserrating = record->at(offset + album_iUserrating).get_asInt();
3138 album.iVotes = record->at(offset + album_iVotes).get_asInt();
3139 album.strReview = record->at(offset + album_strReview).get_asString();
3140 album.styles =
3141 StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator);
3142 album.moods =
3143 StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator);
3144 album.themes =
3145 StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator);
3146 album.strLabel = record->at(offset + album_strLabel).get_asString();
3147 album.strType = record->at(offset + album_strType).get_asString();
3148 album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString();
3149 album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1;
3150 album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1;
3151 album.strLastScraped = record->at(offset + album_lastScraped).get_asString();
3152 album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt();
3153 album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString());
3154 album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt();
3155 album.SetDateAdded(record->at(offset + album_dateAdded).get_asString());
3156 album.SetDateNew(record->at(offset + album_dateNew).get_asString());
3157 album.SetDateUpdated(record->at(offset + album_dateModified).get_asString());
3158 album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString());
3159 album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt();
3160 return album;
3163 CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record,
3164 int offset /* = 0 */)
3166 CArtistCredit artistCredit;
3167 artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt();
3168 if (artistCredit.idArtist == BLANKARTIST_ID)
3169 artistCredit.m_strArtist = StringUtils::Empty;
3170 else
3172 artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString();
3173 artistCredit.m_strMusicBrainzArtistID =
3174 record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString();
3176 return artistCredit;
3179 CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record,
3180 int offset /* = 0 */)
3182 CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(),
3183 record->at(offset + artistCredit_strRole).get_asString(),
3184 record->at(offset + artistCredit_strArtist).get_asString(),
3185 record->at(offset + artistCredit_idArtist).get_asInt());
3186 return ArtistRole;
3189 CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS,
3190 int offset /* = 0 */,
3191 bool needThumb /* = true */)
3193 return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb);
3196 CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record,
3197 int offset /* = 0 */,
3198 bool needThumb /* = true */)
3200 const std::string itemSeparator =
3201 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
3203 CArtist artist;
3204 artist.idArtist = record->at(offset + artist_idArtist).get_asInt();
3205 if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist)
3206 artist.strArtist = g_localizeStrings.Get(38042); //Missing artist tag in current language
3207 else
3208 artist.strArtist = record->at(offset + artist_strArtist).get_asString();
3209 artist.strSortName = record->at(offset + artist_strSortName).get_asString();
3210 artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString();
3211 artist.strType = record->at(offset + artist_strType).get_asString();
3212 artist.strGender = record->at(offset + artist_strGender).get_asString();
3213 artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString();
3214 artist.genre =
3215 StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator);
3216 artist.strBiography = record->at(offset + artist_strBiography).get_asString();
3217 artist.styles =
3218 StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator);
3219 artist.moods =
3220 StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator);
3221 artist.strBorn = record->at(offset + artist_strBorn).get_asString();
3222 artist.strFormed = record->at(offset + artist_strFormed).get_asString();
3223 artist.strDied = record->at(offset + artist_strDied).get_asString();
3224 artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString();
3225 artist.yearsActive =
3226 StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator);
3227 artist.instruments =
3228 StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator);
3229 artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1;
3230 artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString();
3231 artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString());
3232 artist.SetDateNew(record->at(offset + artist_dateNew).get_asString());
3233 artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString());
3235 if (needThumb)
3237 artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString());
3240 return artist;
3243 bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath,
3244 CSong& song,
3245 int64_t startOffset)
3247 song.Clear();
3248 CURL url(strFileNameAndPath);
3250 if (url.IsProtocol("musicdb"))
3252 std::string strFile = URIUtils::GetFileName(strFileNameAndPath);
3253 URIUtils::RemoveExtension(strFile);
3254 return GetSong(atoi(strFile.c_str()), song);
3257 if (nullptr == m_pDB)
3258 return false;
3259 if (nullptr == m_pDS)
3260 return false;
3262 std::string strPath, strFileName;
3263 SplitPath(strFileNameAndPath, strPath, strFileName);
3264 URIUtils::AddSlashAtEnd(strPath);
3266 std::string strSQL = PrepareSQL("SELECT idSong FROM songview "
3267 "WHERE strFileName='%s' AND strPath='%s'",
3268 strFileName.c_str(), strPath.c_str());
3269 if (startOffset)
3270 strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset);
3272 int idSong = GetSingleValueInt(strSQL);
3273 if (idSong > 0)
3274 return GetSong(idSong, song);
3276 return false;
3279 int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath)
3283 if (nullptr == m_pDB)
3284 return false;
3285 if (nullptr == m_pDS)
3286 return false;
3288 std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song "
3289 "JOIN path ON song.idPath = path.idPath "
3290 "WHERE path.strPath='%s'",
3291 strPath.c_str());
3292 // run query
3293 if (!m_pDS->query(strSQL))
3294 return false;
3295 int iRowsFound = m_pDS->num_rows();
3297 int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned
3298 if (iRowsFound == 1)
3299 idAlbum = m_pDS->fv(0).get_asInt();
3301 m_pDS->close();
3303 return idAlbum;
3305 catch (...)
3307 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
3310 return -1;
3313 int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist,
3314 const std::string& strAlbum,
3315 const std::string& strTitle)
3319 std::string strSQL =
3320 PrepareSQL("SELECT idSong FROM songview "
3321 "WHERE strArtists LIKE '%s' AND strAlbum LIKE '%s' AND strTitle LIKE '%s'",
3322 strArtist.c_str(), strAlbum.c_str(), strTitle.c_str());
3324 if (!m_pDS->query(strSQL))
3325 return false;
3326 int iRowsFound = m_pDS->num_rows();
3327 if (iRowsFound == 0)
3329 m_pDS->close();
3330 return -1;
3332 int lResult = m_pDS->fv(0).get_asInt();
3333 m_pDS->close(); // cleanup recordset data
3334 return lResult;
3336 catch (...)
3338 CLog::Log(LOGERROR, "{} ({},{},{}) failed", __FUNCTION__, strArtist, strAlbum, strTitle);
3341 return -1;
3344 bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList& artists)
3348 if (nullptr == m_pDB)
3349 return false;
3350 if (nullptr == m_pDS)
3351 return false;
3353 std::string strVariousArtists = g_localizeStrings.Get(340).c_str();
3354 std::string strSQL;
3355 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3356 strSQL = PrepareSQL("SELECT * FROM artist "
3357 "WHERE (strArtist LIKE '%s%%' OR strArtist LIKE '%% %s%%') "
3358 "AND strArtist <> '%s' ",
3359 search.c_str(), search.c_str(), strVariousArtists.c_str());
3360 else
3361 strSQL = PrepareSQL("SELECT * FROM artist "
3362 "WHERE strArtist LIKE '%s%%' AND strArtist <> '%s' ",
3363 search.c_str(), strVariousArtists.c_str());
3365 if (!m_pDS->query(strSQL))
3366 return false;
3367 if (m_pDS->num_rows() == 0)
3369 m_pDS->close();
3370 return false;
3373 const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist
3374 while (!m_pDS->eof())
3376 std::string path = StringUtils::Format("musicdb://artists/{}/", m_pDS->fv(0).get_asInt());
3377 CFileItemPtr pItem(new CFileItem(path, true));
3378 std::string label = StringUtils::Format("[{}] {}", artistLabel, m_pDS->fv(1).get_asString());
3379 pItem->SetLabel(label);
3380 // sort label is stored in the title tag
3381 label = StringUtils::Format("A {}", m_pDS->fv(1).get_asString());
3382 pItem->GetMusicInfoTag()->SetTitle(label);
3383 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist);
3384 artists.Add(pItem);
3385 m_pDS->next();
3388 m_pDS->close(); // cleanup recordset data
3389 return true;
3391 catch (...)
3393 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3396 return false;
3399 bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items)
3403 if (nullptr == m_pDB)
3404 return false;
3405 if (nullptr == m_pDS)
3406 return false;
3408 CMusicDbUrl baseUrl;
3409 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3410 return false;
3412 std::string strSQL = "SELECT * FROM songview "
3413 "WHERE iTimesPlayed>0 "
3414 "ORDER BY iTimesPlayed DESC "
3415 "LIMIT 100";
3417 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3418 if (!m_pDS->query(strSQL))
3419 return false;
3420 int iRowsFound = m_pDS->num_rows();
3421 if (iRowsFound == 0)
3423 m_pDS->close();
3424 return true;
3426 items.Reserve(iRowsFound);
3427 while (!m_pDS->eof())
3429 CFileItemPtr item(new CFileItem);
3430 GetFileItemFromDataset(item.get(), baseUrl);
3431 items.Add(item);
3432 m_pDS->next();
3435 m_pDS->close(); // cleanup recordset data
3436 return true;
3438 catch (...)
3440 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3443 return false;
3446 bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums)
3450 albums.erase(albums.begin(), albums.end());
3451 if (nullptr == m_pDB)
3452 return false;
3453 if (nullptr == m_pDS)
3454 return false;
3456 // Get data from album and album_artist tables to fully populate albums
3457 std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview "
3458 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3459 "WHERE albumartistview.idAlbum IN "
3460 "(SELECT albumview.idAlbum FROM albumview "
3461 "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
3462 "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
3463 "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
3465 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3466 if (!m_pDS->query(strSQL))
3467 return false;
3468 int iRowsFound = m_pDS->num_rows();
3469 if (iRowsFound == 0)
3471 m_pDS->close();
3472 return true;
3475 int albumArtistOffset = album_enumCount;
3476 int albumId = -1;
3477 while (!m_pDS->eof())
3479 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3481 if (albumId != record->at(album_idAlbum).get_asInt())
3482 { // New album
3483 albumId = record->at(album_idAlbum).get_asInt();
3484 albums.push_back(GetAlbumFromDataset(record));
3486 // Get album artists
3487 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3489 m_pDS->next();
3492 m_pDS->close(); // cleanup recordset data
3493 return true;
3495 catch (...)
3497 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3500 return false;
3503 bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items)
3507 if (nullptr == m_pDB)
3508 return false;
3509 if (nullptr == m_pDS)
3510 return false;
3512 CMusicDbUrl baseUrl;
3513 if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir))
3514 return false;
3516 std::string strSQL = StringUtils::Format(
3517 "SELECT songview.*, albumview.* FROM songview"
3518 "JOIN albumview ON (songview.idAlbum = albumview.idAlbum) "
3519 "JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song "
3520 "WHERE song.iTimesPlayed > 0 "
3521 "GROUP BY idAlbum "
3522 "ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit "
3523 "ON (songview.idAlbum = _albumlimit.idAlbum) "
3524 "ORDER BY _albumlimit.iTimesPlayedSum DESC");
3525 CLog::Log(LOGDEBUG, "GetTop100AlbumSongs() query: {}", strSQL);
3526 if (!m_pDS->query(strSQL))
3527 return false;
3529 int iRowsFound = m_pDS->num_rows();
3530 if (iRowsFound == 0)
3532 m_pDS->close();
3533 return true;
3536 // get data from returned rows
3537 items.Reserve(iRowsFound);
3538 while (!m_pDS->eof())
3540 CFileItemPtr item(new CFileItem);
3541 GetFileItemFromDataset(item.get(), baseUrl);
3542 items.Add(item);
3543 m_pDS->next();
3546 // cleanup
3547 m_pDS->close();
3548 return true;
3550 catch (...)
3552 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3554 return false;
3557 bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums)
3561 albums.erase(albums.begin(), albums.end());
3562 if (nullptr == m_pDB)
3563 return false;
3564 if (nullptr == m_pDS)
3565 return false;
3567 auto start = std::chrono::steady_clock::now();
3569 // Get data from album and album_artist tables to fully populate albums
3570 std::string strSQL =
3571 PrepareSQL("SELECT albumview.*, albumartistview.* "
3572 "FROM (SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3573 "AND albumview.strReleaseType = '%s' "
3574 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3575 "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
3576 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3577 "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
3578 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT);
3580 auto queryStart = std::chrono::steady_clock::now();
3581 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3582 if (!m_pDS->query(strSQL))
3583 return false;
3585 auto queryEnd = std::chrono::steady_clock::now();
3586 auto queryDuration =
3587 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
3589 int iRowsFound = m_pDS->num_rows();
3590 if (iRowsFound == 0)
3592 m_pDS->close();
3593 return true;
3596 int albumArtistOffset = album_enumCount;
3597 int albumId = -1;
3598 while (!m_pDS->eof())
3600 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3602 if (albumId != record->at(album_idAlbum).get_asInt())
3603 { // New album
3604 albumId = record->at(album_idAlbum).get_asInt();
3605 albums.push_back(GetAlbumFromDataset(record));
3607 // Get album artists
3608 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3610 m_pDS->next();
3612 m_pDS->close(); // cleanup recordset data
3614 auto end = std::chrono::steady_clock::now();
3615 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
3617 CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
3618 duration.count(), queryDuration.count());
3620 return true;
3622 catch (...)
3624 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3627 return false;
3630 bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir,
3631 CFileItemList& items)
3635 if (nullptr == m_pDB)
3636 return false;
3637 if (nullptr == m_pDS)
3638 return false;
3640 CMusicDbUrl baseUrl;
3641 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3642 return false;
3644 std::string strSQL =
3645 PrepareSQL("SELECT songview.*, songartistview.* "
3646 "FROM (SELECT idAlbum, lastPlayed FROM albumview "
3647 "WHERE albumview.lastplayed IS NOT NULL "
3648 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3649 "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
3650 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3651 "ORDER BY playedalbums.lastplayed DESC, "
3652 "songartistview.idsong, songartistview.idRole, songartistview.iOrder",
3653 CServiceBroker::GetSettingsComponent()
3654 ->GetAdvancedSettings()
3655 ->m_iMusicLibraryRecentlyAddedItems);
3656 CLog::Log(LOGDEBUG, "GetRecentlyPlayedAlbumSongs() query: {}", strSQL);
3657 if (!m_pDS->query(strSQL))
3658 return false;
3660 int iRowsFound = m_pDS->num_rows();
3661 if (iRowsFound == 0)
3663 m_pDS->close();
3664 return true;
3667 // Needs a separate query to determine number of songs to set items size.
3668 // Get songs from returned rows. Join means there is a row for every song artist
3669 // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
3670 int songArtistOffset = song_enumCount;
3671 int songId = -1;
3672 VECARTISTCREDITS artistCredits;
3673 while (!m_pDS->eof())
3675 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3677 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3678 if (songId != record->at(song_idSong).get_asInt())
3679 { //New song
3680 if (songId > 0 && !artistCredits.empty())
3682 //Store artist credits for previous song
3683 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3684 artistCredits.clear();
3686 songId = record->at(song_idSong).get_asInt();
3687 CFileItemPtr item(new CFileItem);
3688 GetFileItemFromDataset(record, item.get(), baseUrl);
3689 items.Add(item);
3691 // Get song artist credits and contributors
3692 if (idSongArtistRole == ROLE_ARTIST)
3693 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3694 else
3695 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3696 GetArtistRoleFromDataset(record, songArtistOffset));
3698 m_pDS->next();
3700 if (!artistCredits.empty())
3702 //Store artist credits for final song
3703 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3704 artistCredits.clear();
3707 // cleanup
3708 m_pDS->close();
3709 return true;
3711 catch (...)
3713 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3715 return false;
3718 bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit)
3722 albums.erase(albums.begin(), albums.end());
3723 if (nullptr == m_pDB)
3724 return false;
3725 if (nullptr == m_pDS)
3726 return false;
3728 // Get data from album and album_artist tables to fully populate albums
3729 // Determine the recently added albums from dateAdded (usually derived from music file
3730 // timestamps, nothing to do with when albums added to library)
3731 std::string strSQL =
3732 PrepareSQL("SELECT albumview.*, albumartistview.* "
3733 "FROM (SELECT idAlbum FROM album WHERE strAlbum != '' "
3734 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3735 "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
3736 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3737 "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
3738 limit ? limit
3739 : CServiceBroker::GetSettingsComponent()
3740 ->GetAdvancedSettings()
3741 ->m_iMusicLibraryRecentlyAddedItems);
3743 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3744 if (!m_pDS->query(strSQL))
3745 return false;
3746 int iRowsFound = m_pDS->num_rows();
3747 if (iRowsFound == 0)
3749 m_pDS->close();
3750 return true;
3753 int albumArtistOffset = album_enumCount;
3754 int albumId = -1;
3755 while (!m_pDS->eof())
3757 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3759 if (albumId != record->at(album_idAlbum).get_asInt())
3760 { // New album
3761 albumId = record->at(album_idAlbum).get_asInt();
3762 albums.push_back(GetAlbumFromDataset(record));
3764 // Get album artists
3765 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3767 m_pDS->next();
3769 m_pDS->close(); // cleanup recordset data
3770 return true;
3772 catch (...)
3774 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3777 return false;
3780 bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir,
3781 CFileItemList& items,
3782 unsigned int limit)
3786 if (nullptr == m_pDB)
3787 return false;
3788 if (nullptr == m_pDS)
3789 return false;
3791 CMusicDbUrl baseUrl;
3792 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3793 return false;
3795 // Get data from song and song_artist tables to fully populate songs
3796 // Determine the recently added albums from dateAdded (usually derived from music file
3797 // timestamps, nothing to do with when albums added to library)
3798 std::string strSQL;
3799 strSQL = PrepareSQL("SELECT songview.*, songartistview.* "
3800 "FROM (SELECT idAlbum, dateAdded FROM album "
3801 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3802 "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
3803 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3804 "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
3805 "songview.idSong, songartistview.idRole, songartistview.iOrder ",
3806 limit ? limit
3807 : CServiceBroker::GetSettingsComponent()
3808 ->GetAdvancedSettings()
3809 ->m_iMusicLibraryRecentlyAddedItems);
3810 CLog::Log(LOGDEBUG, "GetRecentlyAddedAlbumSongs() query: {}", strSQL);
3811 if (!m_pDS->query(strSQL))
3812 return false;
3814 int iRowsFound = m_pDS->num_rows();
3815 if (iRowsFound == 0)
3817 m_pDS->close();
3818 return true;
3821 // Needs a separate query to determine number of songs to set items size.
3822 // Get songs from returned rows. Join means there is a row for every song artist
3823 int songArtistOffset = song_enumCount;
3824 int songId = -1;
3825 VECARTISTCREDITS artistCredits;
3826 while (!m_pDS->eof())
3828 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3830 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3831 if (songId != record->at(song_idSong).get_asInt())
3832 { //New song
3833 if (songId > 0 && !artistCredits.empty())
3835 //Store artist credits for previous song
3836 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3837 artistCredits.clear();
3839 songId = record->at(song_idSong).get_asInt();
3840 CFileItemPtr item(new CFileItem);
3841 GetFileItemFromDataset(record, item.get(), baseUrl);
3842 items.Add(item);
3844 // Get song artist credits and contributors
3845 if (idSongArtistRole == ROLE_ARTIST)
3846 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3847 else
3848 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3849 GetArtistRoleFromDataset(record, songArtistOffset));
3851 m_pDS->next();
3853 if (!artistCredits.empty())
3855 //Store artist credits for final song
3856 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3857 artistCredits.clear();
3860 // cleanup
3861 m_pDS->close();
3862 return true;
3864 catch (...)
3866 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
3868 return false;
3871 void CMusicDatabase::IncrementPlayCount(const CFileItem& item)
3875 if (nullptr == m_pDB)
3876 return;
3877 if (nullptr == m_pDS)
3878 return;
3880 int idSong = GetSongIDFromPath(item.GetPath());
3881 std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
3882 std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
3883 "WHERE idSong=%i",
3884 strDateNow.c_str(), idSong);
3885 m_pDS->exec(sql);
3887 catch (...)
3889 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, item.GetPath());
3893 bool CMusicDatabase::GetSongsByPath(const std::string& strPath1,
3894 MAPSONGS& songmap,
3895 bool bAppendToMap)
3897 std::string strPath(strPath1);
3900 if (!URIUtils::HasSlashAtEnd(strPath))
3901 URIUtils::AddSlashAtEnd(strPath);
3903 if (!bAppendToMap)
3904 songmap.clear();
3906 if (nullptr == m_pDB)
3907 return false;
3908 if (nullptr == m_pDS)
3909 return false;
3911 // Filename is not unique for a path as songs from a cuesheet have same filename.
3912 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
3913 // in a folder and some edited and rescanned.
3914 // Hence order by filename so these songs can be gathered together.
3915 std::string strSQL = PrepareSQL("SELECT * FROM songview "
3916 "WHERE strPath='%s' ORDER BY strFileName",
3917 strPath.c_str());
3918 if (!m_pDS->query(strSQL))
3919 return false;
3920 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
3921 int iRowsFound = m_pDS->num_rows();
3922 if (iRowsFound == 0)
3924 m_pDS->close();
3925 return false;
3928 // Each file is potentially mapped to a list of songs, gather these and save as list
3929 VECSONGS songs;
3930 std::string filename;
3931 while (!m_pDS->eof())
3933 CSong song = GetSongFromDataset();
3934 if (!filename.empty() && filename != song.strFileName)
3936 // Save songs for previous filename
3937 songmap.insert(std::make_pair(filename, songs));
3938 songs.clear();
3940 filename = song.strFileName;
3941 songs.emplace_back(song);
3942 m_pDS->next();
3944 m_pDS->close(); // cleanup recordset data
3945 songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
3946 return true;
3948 catch (...)
3950 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
3953 return false;
3956 void CMusicDatabase::EmptyCache()
3958 m_genreCache.erase(m_genreCache.begin(), m_genreCache.end());
3959 m_pathCache.erase(m_pathCache.begin(), m_pathCache.end());
3962 bool CMusicDatabase::Search(const std::string& search, CFileItemList& items)
3964 auto start = std::chrono::steady_clock::now();
3965 // first grab all the artists that match
3966 SearchArtists(search, items);
3967 auto end = std::chrono::steady_clock::now();
3968 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
3969 CLog::Log(LOGDEBUG, "{} Artist search in {} ms", __FUNCTION__, duration.count());
3971 start = std::chrono::steady_clock::now();
3972 // then albums that match
3973 SearchAlbums(search, items);
3974 end = std::chrono::steady_clock::now();
3975 duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
3976 CLog::Log(LOGDEBUG, "{} Album search in {} ms", __FUNCTION__, duration.count());
3978 start = std::chrono::steady_clock::now();
3979 // and finally songs
3980 SearchSongs(search, items);
3981 end = std::chrono::steady_clock::now();
3982 duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
3983 CLog::Log(LOGDEBUG, "{} Songs search in {} ms", __FUNCTION__, duration.count());
3985 return true;
3988 bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList& items)
3992 if (nullptr == m_pDB)
3993 return false;
3994 if (nullptr == m_pDS)
3995 return false;
3997 CMusicDbUrl baseUrl;
3998 if (!baseUrl.FromString("musicdb://songs/"))
3999 return false;
4001 std::string strSQL;
4002 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
4003 strSQL = PrepareSQL("SELECT * FROM songview "
4004 "WHERE strTitle LIKE '%s%%' or strTitle LIKE '%% %s%%' LIMIT 1000",
4005 search.c_str(), search.c_str());
4006 else
4007 strSQL = PrepareSQL("SELECT * FROM songview "
4008 "WHERE strTitle LIKE '%s%%' LIMIT 1000",
4009 search.c_str());
4011 if (!m_pDS->query(strSQL))
4012 return false;
4013 if (m_pDS->num_rows() == 0)
4014 return false;
4016 while (!m_pDS->eof())
4018 CFileItemPtr item(new CFileItem);
4019 GetFileItemFromDataset(item.get(), baseUrl);
4020 items.Add(item);
4021 m_pDS->next();
4024 m_pDS->close();
4025 return true;
4027 catch (...)
4029 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
4032 return false;
4035 bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList& albums)
4039 if (nullptr == m_pDB)
4040 return false;
4041 if (nullptr == m_pDS)
4042 return false;
4044 std::string strSQL;
4045 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
4046 strSQL = PrepareSQL("SELECT * FROM albumview "
4047 "WHERE strAlbum LIKE '%s%%' OR strAlbum LIKE '%% %s%%'",
4048 search.c_str(), search.c_str());
4049 else
4050 strSQL = PrepareSQL("SELECT * FROM albumview "
4051 "WHERE strAlbum LIKE '%s%%'",
4052 search.c_str());
4054 if (!m_pDS->query(strSQL))
4055 return false;
4057 const std::string& albumLabel(g_localizeStrings.Get(558)); // Album
4058 while (!m_pDS->eof())
4060 CAlbum album = GetAlbumFromDataset(m_pDS.get());
4061 std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
4062 CFileItemPtr pItem(new CFileItem(path, album));
4063 std::string label = StringUtils::Format("[{}] {}", albumLabel, album.strAlbum);
4064 pItem->SetLabel(label);
4065 // sort label is stored in the title tag
4066 label = StringUtils::Format("B {}", album.strAlbum);
4067 pItem->GetMusicInfoTag()->SetTitle(label);
4068 albums.Add(pItem);
4069 m_pDS->next();
4071 m_pDS->close(); // cleanup recordset data
4072 return true;
4074 catch (...)
4076 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
4078 return false;
4081 bool CMusicDatabase::CleanupSongsByIds(const std::string& strSongIds)
4085 if (nullptr == m_pDB)
4086 return false;
4087 if (nullptr == m_pDS)
4088 return false;
4089 // ok, now find all idSong's
4090 std::string strSQL = PrepareSQL("SELECT * FROM song JOIN path ON song.idPath = path.idPath "
4091 "WHERE song.idSong IN %s",
4092 strSongIds.c_str());
4093 if (!m_pDS->query(strSQL))
4094 return false;
4095 int iRowsFound = m_pDS->num_rows();
4096 if (iRowsFound == 0)
4098 m_pDS->close();
4099 return true;
4101 std::vector<std::string> songsToDelete;
4102 while (!m_pDS->eof())
4103 { // get the full song path
4104 std::string strFileName = URIUtils::AddFileToFolder(
4105 m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString());
4107 // Special case for streams inside an audio decoder package file.
4108 // The last dir in the path is the audio file that
4109 // contains the stream, so test if its there
4110 if (StringUtils::EndsWith(URIUtils::GetExtension(strFileName),
4111 KODI_ADDON_AUDIODECODER_TRACK_EXT))
4113 strFileName = URIUtils::GetDirectory(strFileName);
4114 // we are dropping back to a file, so remove the slash at end
4115 URIUtils::RemoveSlashAtEnd(strFileName);
4118 if (!CFile::Exists(strFileName, false))
4119 { // file no longer exists, so add to deletion list
4120 songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString());
4122 m_pDS->next();
4124 m_pDS->close();
4126 if (!songsToDelete.empty())
4128 std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")";
4129 // ok, now delete these songs + all references to them from the linked tables
4130 strSQL = "delete from song where idSong in " + strSongsToDelete;
4131 m_pDS->exec(strSQL);
4132 m_pDS->close();
4134 return true;
4136 catch (...)
4138 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
4140 return false;
4143 bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/)
4147 int total;
4148 // Count total number of songs
4149 total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS);
4150 // No songs to clean
4151 if (total == 0)
4152 return true;
4154 // run through all songs and get all unique path ids
4155 int iLIMIT = 1000;
4156 for (int i = 0;; i += iLIMIT)
4158 std::string strSQL = PrepareSQL("SELECT song.idSong FROM song "
4159 "ORDER BY song.idSong LIMIT %i OFFSET %i",
4160 iLIMIT, i);
4161 if (!m_pDS->query(strSQL))
4162 return false;
4163 int iRowsFound = m_pDS->num_rows();
4164 // keep going until no rows are left!
4165 if (iRowsFound == 0)
4167 m_pDS->close();
4168 return true;
4171 std::vector<std::string> songIds;
4172 while (!m_pDS->eof())
4174 songIds.push_back(m_pDS->fv("song.idSong").get_asString());
4175 m_pDS->next();
4177 m_pDS->close();
4178 std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")";
4179 CLog::Log(LOGDEBUG, "Checking songs from song ID list: {}", strSongIds);
4180 if (progressDialog)
4182 int percentage = i * 100 / total;
4183 if (percentage > progressDialog->GetPercentage())
4185 progressDialog->SetPercentage(percentage);
4186 progressDialog->Progress();
4188 if (progressDialog->IsCanceled())
4190 m_pDS->close();
4191 return false;
4194 if (!CleanupSongsByIds(strSongIds))
4195 return false;
4197 return true;
4199 catch (...)
4201 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()");
4203 return false;
4206 bool CMusicDatabase::CleanupAlbums()
4210 // This must be run AFTER songs have been cleaned up
4211 // delete albums with no reference to songs
4212 std::string strSQL = "SELECT * FROM album "
4213 "WHERE album.idAlbum NOT IN (SELECT idAlbum FROM song)";
4214 if (!m_pDS->query(strSQL))
4215 return false;
4216 int iRowsFound = m_pDS->num_rows();
4217 if (iRowsFound == 0)
4219 m_pDS->close();
4220 return true;
4223 std::vector<std::string> albumIds;
4224 while (!m_pDS->eof())
4226 albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString());
4227 m_pDS->next();
4229 m_pDS->close();
4231 std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")";
4232 // ok, now we can delete them and the references in the linked tables
4233 strSQL = "delete from album where idAlbum in " + strAlbumIds;
4234 m_pDS->exec(strSQL);
4235 return true;
4237 catch (...)
4239 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()");
4241 return false;
4244 bool CMusicDatabase::CleanupPaths()
4248 // needs to be done AFTER the songs and albums have been cleaned up.
4249 // we can happily delete any path that has no reference to a song
4250 // but we must keep all paths that have been scanned that may contain songs in subpaths
4252 // first create a temporary table of song paths
4253 m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
4254 m_pDS->exec("INSERT INTO songpaths "
4255 "SELECT idPath, strPath FROM path "
4256 "WHERE idPath IN (SELECT idPath FROM song)\n");
4258 // grab all paths that aren't immediately connected with a song
4259 std::string sql = "SELECT * FROM path WHERE idPath NOT IN (SELECT idPath FROM song)";
4260 if (!m_pDS->query(sql))
4261 return false;
4262 int iRowsFound = m_pDS->num_rows();
4263 if (iRowsFound == 0)
4265 m_pDS->close();
4266 return true;
4268 // and construct a list to delete
4269 std::vector<std::string> pathIds;
4270 while (!m_pDS->eof())
4272 // anything that isn't a parent path of a song path is to be deleted
4273 std::string path = m_pDS->fv("strPath").get_asString();
4274 sql = PrepareSQL("SELECT COUNT(idPath) FROM songpaths WHERE SUBSTR(strPath,1,%i)='%s'",
4275 StringUtils::utf8_strlen(path.c_str()), path.c_str());
4276 if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0)
4277 pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete
4278 m_pDS2->close();
4279 m_pDS->next();
4281 m_pDS->close();
4283 if (!pathIds.empty())
4285 // do the deletion, and drop our temp table
4286 std::string deleteSQL =
4287 "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")";
4288 m_pDS->exec(deleteSQL);
4290 m_pDS->exec("drop table songpaths");
4291 return true;
4293 catch (...)
4295 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
4297 return false;
4300 bool CMusicDatabase::InsideScannedPath(const std::string& path)
4302 std::string sql = PrepareSQL("SELECT idPath FROM path WHERE SUBSTR(strPath,1,%i)='%s' LIMIT 1",
4303 path.size(), path.c_str());
4304 return !GetSingleValue(sql).empty();
4307 bool CMusicDatabase::CleanupArtists()
4311 // (nested queries by Bobbin007)
4312 // must be executed AFTER the song, album and their artist link tables are cleaned.
4313 // Don't delete [Missing] the missing artist tag artist
4315 // Create temp table to avoid 1442 trigger hell on mysql
4316 m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
4317 m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
4318 m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
4319 m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID));
4320 // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
4321 // To avoid MySQL hanging or timeout create a table of unique ids with primary key
4322 m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
4323 m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
4324 m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
4325 // Tidy up temp tables
4326 m_pDS->exec("DROP TABLE tmp_delartists");
4327 m_pDS->exec("DROP TABLE tmp_keep");
4329 return true;
4331 catch (...)
4333 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
4335 return false;
4338 bool CMusicDatabase::CleanupGenres()
4342 // Cleanup orphaned song genres (ie those that don't belong to a song entry)
4343 // (nested queries by Bobbin007)
4344 // Must be executed AFTER the song, and song_genre have been cleaned.
4345 std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
4346 m_pDS->exec(strSQL);
4347 return true;
4349 catch (...)
4351 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
4353 return false;
4356 bool CMusicDatabase::CleanupInfoSettings()
4360 // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
4361 // Must be executed AFTER the album and artist tables have been cleaned.
4362 std::string strSQL = "DELETE FROM infosetting "
4363 "WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
4364 "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
4365 m_pDS->exec(strSQL);
4366 return true;
4368 catch (...)
4370 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
4372 return false;
4375 bool CMusicDatabase::CleanupRoles()
4379 // Cleanup orphaned roles (ie those that don't belong to a song entry)
4380 // Must be executed AFTER the song, and song_artist tables have been cleaned.
4381 // Do not remove default role (ROLE_ARTIST)
4382 std::string strSQL = "DELETE FROM role "
4383 "WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
4384 m_pDS->exec(strSQL);
4385 return true;
4387 catch (...)
4389 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
4391 return false;
4394 bool CMusicDatabase::DeleteRemovedLinks()
4398 std::string strSQL = "DELETE FROM removed_link";
4399 m_pDS->exec(strSQL);
4400 return true;
4402 catch (...)
4404 CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks");
4406 return false;
4409 bool CMusicDatabase::CleanupOrphanedItems()
4411 // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
4412 // remove_links not cleared here - done in CheckArtistLinksChanged()
4413 if (nullptr == m_pDB)
4414 return false;
4415 if (nullptr == m_pDS)
4416 return false;
4417 SetLibraryLastUpdated();
4418 if (!CleanupAlbums())
4419 return false;
4420 if (!CleanupArtists())
4421 return false;
4422 if (!CleanupGenres())
4423 return false;
4424 if (!CleanupRoles())
4425 return false;
4426 if (!CleanupInfoSettings())
4427 return false;
4428 return true;
4431 int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/)
4433 if (nullptr == m_pDB)
4434 return ERROR_DATABASE;
4435 if (nullptr == m_pDS)
4436 return ERROR_DATABASE;
4438 int ret;
4439 std::chrono::seconds duration;
4440 auto time = std::chrono::steady_clock::now();
4441 CLog::Log(LOGINFO, "{}: Starting musicdatabase cleanup ..", __FUNCTION__);
4442 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted");
4444 SetLibraryLastCleaned();
4446 // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link
4447 m_pDS->exec("DROP TRIGGER tgrDeleteSongArtist");
4448 m_pDS->exec("DROP TRIGGER tgrDeleteAlbumArtist");
4450 // first cleanup any songs with invalid paths
4451 if (progressDialog)
4453 progressDialog->SetLine(1, CVariant{318});
4454 progressDialog->SetLine(2, CVariant{330});
4455 progressDialog->SetPercentage(0);
4456 progressDialog->Progress();
4458 if (!CleanupSongs(progressDialog))
4460 ret = ERROR_REORG_SONGS;
4461 goto error;
4463 // then the albums that are not linked to a song or to album, or whose path is removed
4464 if (progressDialog)
4466 progressDialog->SetLine(1, CVariant{326});
4467 progressDialog->SetPercentage(20);
4468 progressDialog->Progress();
4469 if (progressDialog->IsCanceled())
4471 ret = ERROR_CANCEL;
4472 goto error;
4475 if (!CleanupAlbums())
4477 ret = ERROR_REORG_ALBUM;
4478 goto error;
4480 // now the paths
4481 if (progressDialog)
4483 progressDialog->SetLine(1, CVariant{324});
4484 progressDialog->SetPercentage(40);
4485 progressDialog->Progress();
4486 if (progressDialog->IsCanceled())
4488 ret = ERROR_CANCEL;
4489 goto error;
4492 if (!CleanupPaths())
4494 ret = ERROR_REORG_PATH;
4495 goto error;
4497 // and finally artists + genres
4498 if (progressDialog)
4500 progressDialog->SetLine(1, CVariant{320});
4501 progressDialog->SetPercentage(60);
4502 progressDialog->Progress();
4503 if (progressDialog->IsCanceled())
4505 ret = ERROR_CANCEL;
4506 goto error;
4509 if (!CleanupArtists())
4511 ret = ERROR_REORG_ARTIST;
4512 goto error;
4514 //Genres, roles and info settings progress in one step
4515 if (progressDialog)
4517 progressDialog->SetLine(1, CVariant{322});
4518 progressDialog->SetPercentage(80);
4519 progressDialog->Progress();
4520 if (progressDialog->IsCanceled())
4522 ret = ERROR_CANCEL;
4523 goto error;
4526 if (!CleanupGenres())
4528 ret = ERROR_REORG_OTHER;
4529 goto error;
4531 if (!CleanupRoles())
4533 ret = ERROR_REORG_OTHER;
4534 goto error;
4536 if (!CleanupInfoSettings())
4538 ret = ERROR_REORG_OTHER;
4539 goto error;
4541 if (!DeleteRemovedLinks())
4543 ret = ERROR_REORG_OTHER;
4544 goto error;
4547 // commit transaction
4548 if (progressDialog)
4550 progressDialog->SetLine(1, CVariant{328});
4551 progressDialog->SetPercentage(90);
4552 progressDialog->Progress();
4553 if (progressDialog->IsCanceled())
4555 ret = ERROR_CANCEL;
4556 goto error;
4559 if (!CommitTransaction())
4561 ret = ERROR_WRITING_CHANGES;
4562 goto error;
4565 // Recreate DELETE triggers on song_artist and album_artist
4566 CreateRemovedLinkTriggers();
4568 // and compress the database
4569 if (progressDialog)
4571 progressDialog->SetLine(1, CVariant{331});
4572 progressDialog->SetPercentage(100);
4573 progressDialog->Close();
4576 duration =
4577 std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - time);
4578 CLog::Log(LOGINFO, "{}: Cleaning musicdatabase done. Operation took {}s", __FUNCTION__,
4579 duration.count());
4580 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4582 if (!Compress(false))
4584 return ERROR_COMPRESSING;
4586 return ERROR_OK;
4588 error:
4589 RollbackTransaction();
4590 // Recreate DELETE triggers on song_artist and album_artist
4591 CreateRemovedLinkTriggers();
4592 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4593 return ret;
4596 bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space)
4598 if (strImage.length() > space)
4600 strImage = strImage.substr(0, space);
4601 // Tidy to last </thumb> tag
4602 size_t iPos = strImage.rfind("</thumb>");
4603 if (iPos == std::string::npos)
4604 return false;
4605 strImage = strImage.substr(0, iPos + 8);
4607 return true;
4610 bool CMusicDatabase::LookupCDDBInfo(bool bRequery /*=false*/)
4612 #ifdef HAS_DVD_DRIVE
4613 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
4614 CSettings::SETTING_AUDIOCDS_USECDDB))
4615 return false;
4617 // check network connectivity
4618 if (!CServiceBroker::GetNetwork().IsAvailable())
4619 return false;
4621 // Get information for the inserted disc
4622 CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
4623 if (pCdInfo == NULL)
4624 return false;
4626 // If the disc has no tracks, we are finished here.
4627 int nTracks = pCdInfo->GetTrackCount();
4628 if (nTracks <= 0)
4629 return false;
4631 // Delete old info if any
4632 if (bRequery)
4634 std::string strFile = StringUtils::Format("{:x}.cddb", pCdInfo->GetCddbDiscId());
4635 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4638 // Prepare cddb
4639 Xcddb cddb;
4640 cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4642 // Do we have to look for cddb information
4643 if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo))
4645 CGUIDialogProgress* pDialogProgress =
4646 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
4647 WINDOW_DIALOG_PROGRESS);
4648 CGUIDialogSelect* pDlgSelect =
4649 CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
4650 WINDOW_DIALOG_SELECT);
4652 if (!pDialogProgress)
4653 return false;
4654 if (!pDlgSelect)
4655 return false;
4657 // Show progress dialog if we have to connect to freedb.org
4658 pDialogProgress->SetHeading(CVariant{255}); //CDDB
4659 pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info
4660 pDialogProgress->SetLine(1, CVariant{256});
4661 pDialogProgress->SetLine(2, CVariant{""});
4662 pDialogProgress->ShowProgressBar(false);
4663 pDialogProgress->Open();
4665 // get cddb information
4666 if (!cddb.queryCDinfo(pCdInfo))
4668 pDialogProgress->Close();
4669 int lasterror = cddb.getLastError();
4671 // Have we found more then on match in cddb for this disc,...
4672 if (lasterror == E_WAIT_FOR_INPUT)
4674 // ...yes, show the matches found in a select dialog
4675 // and let the user choose an entry.
4676 pDlgSelect->Reset();
4677 pDlgSelect->SetHeading(CVariant{255});
4678 int i = 1;
4679 while (true)
4681 std::string strTitle = cddb.getInexactTitle(i);
4682 if (strTitle == "")
4683 break;
4685 const std::string& strArtist = cddb.getInexactArtist(i);
4686 if (!strArtist.empty())
4687 strTitle += " - " + strArtist;
4689 pDlgSelect->Add(strTitle);
4690 i++;
4692 pDlgSelect->Open();
4694 // Has the user selected a match...
4695 int iSelectedCD = pDlgSelect->GetSelectedItem();
4696 if (iSelectedCD >= 0)
4698 // ...query cddb for the inexact match
4699 if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD))
4700 pCdInfo->SetNoCDDBInfo();
4702 else
4703 pCdInfo->SetNoCDDBInfo();
4705 else if (lasterror == E_NO_MATCH_FOUND)
4707 pCdInfo->SetNoCDDBInfo();
4709 else
4711 pCdInfo->SetNoCDDBInfo();
4712 // ..no, an error occurred, display it to the user
4713 std::string strErrorText =
4714 StringUtils::Format("[{}] {}", cddb.getLastError(), cddb.getLastErrorText());
4715 HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)},
4716 CVariant{0});
4718 } // if ( !cddb.queryCDinfo( pCdInfo ) )
4719 else
4720 pDialogProgress->Close();
4723 // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
4725 return pCdInfo->HasCDDBInfo();
4726 #else
4727 return false;
4728 #endif
4731 void CMusicDatabase::DeleteCDDBInfo()
4733 #ifdef HAS_DVD_DRIVE
4734 CFileItemList items;
4735 if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb",
4736 DIR_FLAG_NO_FILE_DIRS))
4738 HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426});
4739 return;
4741 // Show a selectdialog that the user can select the album to delete
4742 CGUIDialogSelect* pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
4743 WINDOW_DIALOG_SELECT);
4744 if (pDlg)
4746 pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
4747 pDlg->Reset();
4749 std::map<uint32_t, std::string> mapCDDBIds;
4750 for (int i = 0; i < items.Size(); ++i)
4752 if (items[i]->m_bIsFolder)
4753 continue;
4755 std::string strFile = URIUtils::GetFileName(items[i]->GetPath());
4756 strFile.erase(strFile.size() - 5, 5);
4757 uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16);
4758 Xcddb cddb;
4759 cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4761 if (!cddb.queryCache(lDiscId))
4762 continue;
4764 std::string strDiskTitle, strDiskArtist;
4765 cddb.getDiskTitle(strDiskTitle);
4766 cddb.getDiskArtist(strDiskArtist);
4768 std::string str;
4769 if (strDiskArtist.empty())
4770 str = strDiskTitle;
4771 else
4772 str = strDiskTitle + " - " + strDiskArtist;
4774 pDlg->Add(str);
4775 mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str));
4778 pDlg->Sort();
4779 pDlg->Open();
4781 // and wait till user selects one
4782 int iSelectedAlbum = pDlg->GetSelectedItem();
4783 if (iSelectedAlbum < 0)
4785 mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4786 return;
4789 std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel();
4790 for (const auto& i : mapCDDBIds)
4792 if (i.second == strSelectedAlbum)
4794 std::string strFile = StringUtils::Format("{:x}.cddb", (unsigned int)i.first);
4795 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4796 break;
4799 mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4801 #endif
4804 void CMusicDatabase::Clean()
4806 // If we are scanning for music info in the background,
4807 // other writing access to the database is prohibited.
4808 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
4810 HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057});
4811 return;
4814 if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES)
4816 CMusicDatabase musicdatabase;
4817 if (musicdatabase.Open())
4819 int iReturnString = musicdatabase.Cleanup();
4820 musicdatabase.Close();
4822 if (iReturnString != ERROR_OK)
4824 HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString});
4830 bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir,
4831 CFileItemList& items,
4832 const Filter& filter /* = Filter() */,
4833 bool countOnly /* = false */)
4837 if (nullptr == m_pDB)
4838 return false;
4839 if (nullptr == m_pDS)
4840 return false;
4842 // get primary genres for songs - could be simplified to just SELECT * FROM genre?
4843 std::string strSQL = "SELECT %s FROM genre ";
4845 Filter extFilter = filter;
4846 CMusicDbUrl musicUrl;
4847 SortDescription sorting;
4848 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4849 return false;
4851 // if there are extra WHERE conditions we might need access
4852 // to songview or albumview for these conditions
4853 if (!extFilter.where.empty())
4855 if (extFilter.where.find("artistview") != std::string::npos)
4857 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4858 extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4859 extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
4860 extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
4862 else if (extFilter.where.find("songview") != std::string::npos)
4864 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4865 extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4867 else if (extFilter.where.find("albumview") != std::string::npos)
4869 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4870 extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
4871 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
4873 extFilter.AppendGroup("genre.idGenre");
4875 extFilter.AppendWhere("genre.strGenre != ''");
4877 if (countOnly)
4879 extFilter.fields = "COUNT(DISTINCT genre.idGenre)";
4880 extFilter.group.clear();
4881 extFilter.order.clear();
4884 std::string strSQLExtra;
4885 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
4886 return false;
4888 strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
4889 ? extFilter.fields.c_str()
4890 : "genre.*") +
4891 strSQLExtra;
4893 // run query
4894 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
4896 if (!m_pDS->query(strSQL))
4897 return false;
4898 int iRowsFound = m_pDS->num_rows();
4899 if (iRowsFound == 0)
4901 m_pDS->close();
4902 return true;
4905 if (countOnly)
4907 CFileItemPtr pItem(new CFileItem());
4908 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4909 items.Add(pItem);
4911 m_pDS->close();
4912 return true;
4915 // get data from returned rows
4916 while (!m_pDS->eof())
4918 CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString()));
4919 pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString());
4920 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre");
4922 CMusicDbUrl itemUrl = musicUrl;
4923 std::string strDir = StringUtils::Format("{}/", m_pDS->fv("genre.idGenre").get_asInt());
4924 itemUrl.AppendPath(strDir);
4925 pItem->SetPath(itemUrl.ToString());
4927 pItem->m_bIsFolder = true;
4928 items.Add(pItem);
4930 m_pDS->next();
4933 // cleanup
4934 m_pDS->close();
4936 return true;
4938 catch (...)
4940 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
4942 return false;
4945 bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir,
4946 CFileItemList& items,
4947 const Filter& filter /*= Filter()*/,
4948 bool countOnly /*= false*/)
4952 if (nullptr == m_pDB)
4953 return false;
4954 if (nullptr == m_pDS)
4955 return false;
4957 // Get sources for selection list when add/edit filter or smartplaylist rule
4958 std::string strSQL = "SELECT %s FROM source ";
4960 Filter extFilter = filter;
4961 CMusicDbUrl musicUrl;
4962 SortDescription sorting;
4963 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4964 return false;
4966 // if there are extra WHERE conditions we might need access
4967 // to songview or albumview for these conditions
4968 if (!extFilter.where.empty())
4970 if (extFilter.where.find("artistview") != std::string::npos)
4972 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4973 extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
4974 extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
4976 else if (extFilter.where.find("songview") != std::string::npos)
4978 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4979 extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
4981 else if (extFilter.where.find("albumview") != std::string::npos)
4983 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4984 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
4986 extFilter.AppendGroup("source.idSource");
4988 else
4989 { // Get only sources that have been scanned into music library
4990 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4991 extFilter.AppendGroup("source.idSource");
4994 if (countOnly)
4996 extFilter.fields = "COUNT(DISTINCT source.idSource)";
4997 extFilter.group.clear();
4998 extFilter.order.clear();
5001 std::string strSQLExtra;
5002 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5003 return false;
5005 strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
5006 ? extFilter.fields.c_str()
5007 : "source.*") +
5008 strSQLExtra;
5010 // run query
5011 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5013 if (!m_pDS->query(strSQL))
5014 return false;
5015 int iRowsFound = m_pDS->num_rows();
5016 if (iRowsFound == 0)
5018 m_pDS->close();
5019 return true;
5022 if (countOnly)
5024 CFileItemPtr pItem(new CFileItem());
5025 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
5026 items.Add(pItem);
5028 m_pDS->close();
5029 return true;
5032 // get data from returned rows
5033 while (!m_pDS->eof())
5035 CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
5036 pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString());
5037 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source");
5039 CMusicDbUrl itemUrl = musicUrl;
5040 std::string strDir = StringUtils::Format("{}/", m_pDS->fv("source.idSource").get_asInt());
5041 itemUrl.AppendPath(strDir);
5042 itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt());
5043 pItem->SetPath(itemUrl.ToString());
5045 pItem->m_bIsFolder = true;
5046 items.Add(pItem);
5048 m_pDS->next();
5051 // cleanup
5052 m_pDS->close();
5054 return true;
5056 catch (...)
5058 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5060 return false;
5063 bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir,
5064 CFileItemList& items,
5065 const Filter& filter /* = Filter() */)
5069 if (nullptr == m_pDB)
5070 return false;
5071 if (nullptr == m_pDS)
5072 return false;
5074 Filter extFilter = filter;
5075 CMusicDbUrl musicUrl;
5076 SortDescription sorting;
5077 std::string strSQL;
5078 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5079 return false;
5081 bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5082 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
5084 useOriginalYears =
5085 useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/");
5087 if (!useOriginalYears)
5088 { // Get years from year part of release date
5089 strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
5090 extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
5092 else
5093 { // Get years from year part of original date
5094 strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
5095 extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
5097 if (!BuildSQL(strSQL, extFilter, strSQL))
5098 return false;
5100 // run query
5101 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5102 if (!m_pDS->query(strSQL))
5103 return false;
5104 int iRowsFound = m_pDS->num_rows();
5105 if (iRowsFound == 0)
5107 m_pDS->close();
5108 return true;
5111 // get data from returned rows
5112 while (!m_pDS->eof())
5114 CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
5115 pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt());
5116 if (useOriginalYears)
5117 pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
5118 else
5119 pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year");
5121 CMusicDbUrl itemUrl = musicUrl;
5122 std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
5123 itemUrl.AppendPath(strDir);
5124 if (useOriginalYears)
5125 itemUrl.AddOption("useoriginalyear", true);
5126 pItem->SetPath(itemUrl.ToString());
5128 pItem->m_bIsFolder = true;
5129 items.Add(pItem);
5131 m_pDS->next();
5134 // cleanup
5135 m_pDS->close();
5137 return true;
5139 catch (...)
5141 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5143 return false;
5146 bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir,
5147 CFileItemList& items,
5148 const Filter& filter /* = Filter() */)
5152 if (nullptr == m_pDB)
5153 return false;
5154 if (nullptr == m_pDS)
5155 return false;
5157 Filter extFilter = filter;
5158 CMusicDbUrl musicUrl;
5159 SortDescription sorting;
5160 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5161 return false;
5163 // get roles with artists having that role
5164 std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role "
5165 "JOIN song_artist ON song_artist.idRole = role.idRole ";
5167 if (!BuildSQL(strSQL, extFilter, strSQL))
5168 return false;
5170 // run query
5171 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5172 if (!m_pDS->query(strSQL))
5173 return false;
5174 int iRowsFound = m_pDS->num_rows();
5175 if (iRowsFound == 0)
5177 m_pDS->close();
5178 return true;
5181 // get data from returned rows
5182 while (!m_pDS->eof())
5184 std::string labelValue = m_pDS->fv("role.strRole").get_asString();
5185 CFileItemPtr pItem(new CFileItem(labelValue));
5186 pItem->GetMusicInfoTag()->SetTitle(labelValue);
5187 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role");
5188 CMusicDbUrl itemUrl = musicUrl;
5189 std::string strDir = StringUtils::Format("{}/", m_pDS->fv("role.idRole").get_asInt());
5190 itemUrl.AppendPath(strDir);
5191 itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt());
5192 pItem->SetPath(itemUrl.ToString());
5194 pItem->m_bIsFolder = true;
5195 items.Add(pItem);
5197 m_pDS->next();
5200 // cleanup
5201 m_pDS->close();
5203 return true;
5205 catch (...)
5207 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5209 return false;
5212 bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year)
5214 CMusicDbUrl musicUrl;
5215 if (!musicUrl.FromString(strBaseDir))
5216 return false;
5218 musicUrl.AddOption("year", year);
5219 musicUrl.AddOption("show_singles", true); // allow singles to be listed
5221 Filter filter;
5222 return GetAlbumsByWhere(musicUrl.ToString(), filter, items);
5225 bool CMusicDatabase::GetCommonNav(const std::string& strBaseDir,
5226 const std::string& table,
5227 const std::string& labelField,
5228 CFileItemList& items,
5229 const Filter& filter /* = Filter() */,
5230 bool countOnly /* = false */)
5232 if (nullptr == m_pDB)
5233 return false;
5234 if (nullptr == m_pDS)
5235 return false;
5237 if (table.empty() || labelField.empty())
5238 return false;
5242 Filter extFilter = filter;
5243 std::string strSQL = "SELECT %s FROM " + table + " ";
5244 extFilter.AppendGroup(labelField);
5245 extFilter.AppendWhere(labelField + " != ''");
5247 if (countOnly)
5249 extFilter.fields = "COUNT(DISTINCT " + labelField + ")";
5250 extFilter.group.clear();
5251 extFilter.order.clear();
5254 // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
5255 // e.g. LIKE '%symphony%' would be taken as a %s format argument
5256 strSQL = PrepareSQL(strSQL,
5257 !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str());
5259 CMusicDbUrl musicUrl;
5260 if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl))
5261 return false;
5263 // run query
5264 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5265 if (!m_pDS->query(strSQL))
5266 return false;
5268 int iRowsFound = m_pDS->num_rows();
5269 if (iRowsFound <= 0)
5271 m_pDS->close();
5272 return false;
5275 if (countOnly)
5277 CFileItemPtr pItem(new CFileItem());
5278 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
5279 items.Add(pItem);
5281 m_pDS->close();
5282 return true;
5285 // get data from returned rows
5286 while (!m_pDS->eof())
5288 std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString();
5289 CFileItemPtr pItem(new CFileItem(labelValue));
5291 CMusicDbUrl itemUrl = musicUrl;
5292 std::string strDir = StringUtils::Format("{}/", labelValue);
5293 itemUrl.AppendPath(strDir);
5294 pItem->SetPath(itemUrl.ToString());
5296 pItem->m_bIsFolder = true;
5297 items.Add(pItem);
5299 m_pDS->next();
5302 // cleanup
5303 m_pDS->close();
5305 return true;
5307 catch (...)
5309 m_pDS->close();
5310 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5313 return false;
5316 bool CMusicDatabase::GetAlbumTypesNav(const std::string& strBaseDir,
5317 CFileItemList& items,
5318 const Filter& filter /* = Filter() */,
5319 bool countOnly /* = false */)
5321 return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly);
5324 bool CMusicDatabase::GetMusicLabelsNav(const std::string& strBaseDir,
5325 CFileItemList& items,
5326 const Filter& filter /* = Filter() */,
5327 bool countOnly /* = false */)
5329 return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly);
5332 bool CMusicDatabase::GetArtistsNav(const std::string& strBaseDir,
5333 CFileItemList& items,
5334 bool albumArtistsOnly /* = false */,
5335 int idGenre /* = -1 */,
5336 int idAlbum /* = -1 */,
5337 int idSong /* = -1 */,
5338 const Filter& filter /* = Filter() */,
5339 const SortDescription& sortDescription /* = SortDescription() */,
5340 bool countOnly /* = false */)
5342 if (nullptr == m_pDB)
5343 return false;
5344 if (nullptr == m_pDS)
5345 return false;
5348 CMusicDbUrl musicUrl;
5349 if (!musicUrl.FromString(strBaseDir))
5350 return false;
5352 if (idGenre > 0)
5353 musicUrl.AddOption("genreid", idGenre);
5354 else if (idAlbum > 0)
5355 musicUrl.AddOption("albumid", idAlbum);
5356 else if (idSong > 0)
5357 musicUrl.AddOption("songid", idSong);
5359 // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
5360 // when local option already present in music URL thus allowing it to be an option in custom nodes
5361 if (!musicUrl.HasOption("albumartistsonly"))
5362 musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
5364 bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5366 return result;
5368 catch (...)
5370 m_pDS->close();
5371 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5373 return false;
5376 bool CMusicDatabase::GetArtistsByWhere(
5377 const std::string& strBaseDir,
5378 const Filter& filter,
5379 CFileItemList& items,
5380 const SortDescription& sortDescription /* = SortDescription() */,
5381 bool countOnly /* = false */)
5383 if (nullptr == m_pDB)
5384 return false;
5385 if (nullptr == m_pDS)
5386 return false;
5390 auto start = std::chrono::steady_clock::now();
5391 int total = -1;
5393 Filter extFilter = filter;
5394 CMusicDbUrl musicUrl;
5395 SortDescription sorting = sortDescription;
5396 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5397 return false;
5399 bool extended = false;
5400 bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5402 // if there are extra WHERE conditions (from media filter dialog) we might
5403 // need access to songview or albumview for these conditions
5404 if (!extFilter.where.empty())
5406 if (extFilter.where.find("songview") != std::string::npos)
5408 extended = true;
5409 extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist "
5410 "JOIN songview ON songview.idSong = song_artist.idSong");
5412 else if (extFilter.where.find("albumview") != std::string::npos)
5414 extended = true;
5415 extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist "
5416 "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
5418 if (extended)
5419 extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
5422 std::string strSQLExtra;
5423 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5424 return false;
5426 // Count number of artsits that satisfy selection criteria (no limit built)
5427 // Count done in full query fetch when unlimited
5428 if (countOnly || limitedInSQL)
5430 if (extended)
5432 // Count distinct without group by
5433 Filter countFilter = extFilter;
5434 countFilter.group.clear();
5435 std::string strSQLWhere;
5436 if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5437 return false;
5438 total = GetSingleValueInt(
5439 "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS);
5441 else
5442 total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS);
5444 if (countOnly)
5446 CFileItemPtr pItem(new CFileItem());
5447 pItem->SetProperty("total", total);
5448 items.Add(pItem);
5450 m_pDS->close();
5451 return true;
5454 // Apply any limiting directly in SQL and so sort as well
5455 if (limitedInSQL)
5457 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5460 // Apply sort in SQL
5461 const std::shared_ptr<CSettings> settings =
5462 CServiceBroker::GetSettingsComponent()->GetSettings();
5463 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5464 sorting.sortAttributes =
5465 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5466 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5467 GetOrderFilter(MediaTypeArtist, sorting, extFilter);
5469 strSQLExtra.clear();
5470 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5471 return false;
5473 std::string strSQL;
5474 std::string strFields = "artistview.*";
5475 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5476 strFields = "artistview.*, " + extFilter.fields;
5477 strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra;
5479 // run query
5480 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5481 auto queryStart = std::chrono::steady_clock::now();
5482 if (!m_pDS->query(strSQL))
5483 return false;
5484 int iRowsFound = m_pDS->num_rows();
5485 if (iRowsFound == 0)
5487 m_pDS->close();
5488 return true;
5491 auto queryEnd = std::chrono::steady_clock::now();
5492 auto queryDuration =
5493 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
5495 // Store the total number of artists as a property
5496 if (total < iRowsFound)
5497 total = iRowsFound;
5498 items.SetProperty("total", total);
5500 DatabaseResults results;
5501 results.reserve(iRowsFound);
5502 // Populate results field vector from dataset
5503 FieldList fields;
5504 if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results))
5505 return false;
5506 // Store item list sort order
5507 items.SetSortMethod(sortDescription.sortBy);
5508 items.SetSortOrder(sortDescription.sortOrder);
5510 // Get Artists from returned rows
5511 items.Reserve(results.size());
5512 const dbiplus::query_data& data = m_pDS->get_result_set().records;
5513 for (const auto& i : results)
5515 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5516 const dbiplus::sql_record* const record = data.at(targetRow);
5520 CArtist artist = GetArtistFromDataset(record, false);
5521 CFileItemPtr pItem(new CFileItem(artist));
5523 CMusicDbUrl itemUrl = musicUrl;
5524 std::string path = StringUtils::Format("{}/", artist.idArtist);
5525 itemUrl.AppendPath(path);
5526 pItem->SetPath(itemUrl.ToString());
5528 pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist);
5529 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5530 pItem->SetProperty("icon_never_overlay", true);
5531 pItem->SetArt("icon", "DefaultArtist.png");
5533 SetPropertiesFromArtist(*pItem, artist);
5534 items.Add(pItem);
5536 catch (...)
5538 m_pDS->close();
5539 CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
5540 items.Size());
5543 // cleanup
5544 m_pDS->close();
5546 auto end = std::chrono::steady_clock::now();
5547 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
5549 CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1} ms query took {2} ms",
5550 __FUNCTION__, duration.count(), queryDuration.count());
5552 return true;
5554 catch (...)
5556 m_pDS->close();
5557 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5559 return false;
5562 bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum& album)
5566 if (nullptr == m_pDB)
5567 return false;
5568 if (nullptr == m_pDS)
5569 return false;
5571 std::string strSQL = PrepareSQL("SELECT albumview.* FROM song "
5572 "JOIN albumview on song.idAlbum = albumview.idAlbum "
5573 "WHERE song.idSong='%i'",
5574 idSong);
5575 if (!m_pDS->query(strSQL))
5576 return false;
5577 int iRowsFound = m_pDS->num_rows();
5578 if (iRowsFound != 1)
5580 m_pDS->close();
5581 return false;
5584 album = GetAlbumFromDataset(m_pDS.get());
5586 m_pDS->close();
5587 return true;
5589 catch (...)
5591 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
5593 return false;
5596 bool CMusicDatabase::GetAlbumsNav(const std::string& strBaseDir,
5597 CFileItemList& items,
5598 int idGenre /* = -1 */,
5599 int idArtist /* = -1 */,
5600 const Filter& filter /* = Filter() */,
5601 const SortDescription& sortDescription /* = SortDescription() */,
5602 bool countOnly /* = false */)
5604 CMusicDbUrl musicUrl;
5605 if (!musicUrl.FromString(strBaseDir))
5606 return false;
5608 // where clause
5609 if (idGenre > 0)
5610 musicUrl.AddOption("genreid", idGenre);
5612 if (idArtist > 0)
5613 musicUrl.AddOption("artistid", idArtist);
5615 return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5618 bool CMusicDatabase::GetAlbumsByWhere(
5619 const std::string& baseDir,
5620 const Filter& filter,
5621 CFileItemList& items,
5622 const SortDescription& sortDescription /* = SortDescription() */,
5623 bool countOnly /* = false */)
5625 if (m_pDB == nullptr || m_pDS == nullptr)
5626 return false;
5630 auto start = std::chrono::steady_clock::now();
5631 int total = -1;
5633 Filter extFilter = filter;
5634 CMusicDbUrl musicUrl;
5635 SortDescription sorting = sortDescription;
5636 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5637 return false;
5639 bool extended = false;
5640 bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5642 // If there are extra WHERE conditions (from media filter dialog) we might
5643 // need access to songview for these conditions
5644 if (extFilter.where.find("songview") != std::string::npos)
5646 extended = true;
5647 extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
5648 extFilter.AppendGroup("albumview.idAlbum");
5651 std::string strSQLExtra;
5652 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5653 return false;
5655 // Count number of albums that satisfy selection criteria (no limit built)
5656 // Count done in full query fetch when unlimited
5657 if (countOnly || limitedInSQL)
5659 if (extended)
5661 // Count distinct without group by
5662 Filter countFilter = extFilter;
5663 countFilter.group.clear();
5664 std::string strSQLWhere;
5665 if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5666 return false;
5667 total = GetSingleValueInt(
5668 "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS);
5670 else
5671 total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
5673 if (countOnly)
5675 CFileItemPtr pItem(new CFileItem());
5676 pItem->SetProperty("total", total);
5677 items.Add(pItem);
5679 m_pDS->close();
5680 return true;
5683 // Apply any limiting directly in SQL
5684 if (limitedInSQL)
5686 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5689 // Apply sort in SQL
5690 const std::shared_ptr<CSettings> settings =
5691 CServiceBroker::GetSettingsComponent()->GetSettings();
5692 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5693 sorting.sortAttributes =
5694 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5695 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5696 GetOrderFilter(MediaTypeAlbum, sorting, extFilter);
5697 // Modify order to use correct calculated year field
5698 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5699 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
5700 StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
5701 else
5702 StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5704 strSQLExtra.clear();
5705 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5706 return false;
5708 std::string strSQL;
5709 std::string strFields = "albumview.*";
5710 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5711 strFields = "albumview.*, " + extFilter.fields;
5712 strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra;
5714 // run query
5715 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5716 auto querytime = std::chrono::steady_clock::now();
5717 if (!m_pDS->query(strSQL))
5718 return false;
5719 int iRowsFound = m_pDS->num_rows();
5720 if (iRowsFound == 0)
5722 m_pDS->close();
5723 return true;
5726 auto queryEnd = std::chrono::steady_clock::now();
5727 auto queryDuration =
5728 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - querytime);
5730 // Store the total number of albums as a property
5731 if (total < iRowsFound)
5732 total = iRowsFound;
5733 items.SetProperty("total", total);
5735 DatabaseResults results;
5736 results.reserve(iRowsFound);
5737 // Populate results field vector from dataset
5738 FieldList fields;
5739 if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results))
5740 return false;
5741 // Store item list sort order
5742 items.SetSortMethod(sorting.sortBy);
5743 items.SetSortOrder(sorting.sortOrder);
5745 // Get albums from returned rows
5746 items.Reserve(results.size());
5747 const dbiplus::query_data& data = m_pDS->get_result_set().records;
5748 for (const auto& i : results)
5750 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5751 const dbiplus::sql_record* const record = data.at(targetRow);
5755 CMusicDbUrl itemUrl = musicUrl;
5756 std::string path = StringUtils::Format("{}/", record->at(album_idAlbum).get_asInt());
5757 itemUrl.AppendPath(path);
5759 CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record)));
5760 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5761 pItem->SetProperty("icon_never_overlay", true);
5762 pItem->SetArt("icon", "DefaultAlbumCover.png");
5763 items.Add(pItem);
5765 catch (...)
5767 m_pDS->close();
5768 CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
5769 items.Size());
5772 // cleanup
5773 m_pDS->close();
5775 auto end = std::chrono::steady_clock::now();
5776 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
5778 CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
5779 duration.count(), queryDuration.count());
5781 return true;
5783 catch (...)
5785 m_pDS->close();
5786 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
5788 return false;
5791 bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir,
5792 CFileItemList& items,
5793 int idAlbum,
5794 const Filter& filter,
5795 const SortDescription& sortDescription,
5796 bool countOnly)
5798 CMusicDbUrl musicUrl;
5799 if (!musicUrl.FromString(strBaseDir))
5800 return false;
5802 if (idAlbum > 0)
5803 musicUrl.AddOption("albumid", idAlbum);
5805 return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5808 bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir,
5809 const Filter& filter,
5810 CFileItemList& items,
5811 const SortDescription& sortDescription,
5812 bool countOnly)
5814 CMusicDbUrl musicUrl;
5815 if (!musicUrl.FromString(baseDir))
5816 return false;
5817 return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5820 bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl,
5821 const Filter& filter,
5822 CFileItemList& items,
5823 const SortDescription& sortDescription,
5824 bool countOnly)
5826 if (m_pDB == nullptr || m_pDS == nullptr)
5827 return false;
5831 auto start = std::chrono::steady_clock::now();
5832 int total = -1;
5833 std::string strSQL;
5835 Filter extFilter = filter;
5836 SortDescription sorting = sortDescription;
5838 if (!GetFilter(musicUrl, extFilter, sorting))
5839 return false;
5841 extFilter.AppendGroup("albumview.idAlbum, iDisc");
5843 // If there are extra songview WHERE conditions adjust to song or albumview
5844 // fields, and join Path table for strPath
5845 // ! @todo: convert songview fields into to song or albumview fields
5846 // But not sure we ever get songview fields in filter - REMOVE??
5847 if (extFilter.where.find("songview.strPath") != std::string::npos)
5849 extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
5852 std::string strSQLExtra;
5853 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5854 return false;
5856 // Apply any limiting directly in SQL if there is either no special sorting or random sort
5857 // When limited, random sort is also applied in SQL
5858 bool limitedInSQL = extFilter.limit.empty() &&
5859 (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) &&
5860 (sorting.limitStart > 0 || sorting.limitEnd > 0);
5862 if (countOnly || limitedInSQL)
5864 // Count number of discs that satisfy selection criteria
5865 // (when fetching all records get total from row count of results dataset)
5866 // Count not allow for same non-null title discs to be grouped together
5867 strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
5868 "albumview.idAlbum " +
5869 strSQLExtra;
5870 strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc ";
5871 total = GetSingleValueInt(strSQL, m_pDS);
5873 if (countOnly)
5875 items.SetProperty("total", total);
5876 return true;
5878 // Apply limits and random sort order directly in SQL
5879 if (limitedInSQL)
5881 if (sorting.sortBy == SortByRandom)
5882 strSQLExtra += PrepareSQL(" ORDER BY RANDOM()");
5883 strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
5885 else
5886 strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
5888 strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
5889 "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " +
5890 strSQLExtra;
5892 // run query
5893 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
5894 auto queryStart = std::chrono::steady_clock::now();
5895 if (!m_pDS->query(strSQL))
5896 return false;
5897 int iRowsFound = m_pDS->num_rows();
5898 if (iRowsFound == 0)
5900 m_pDS->close();
5901 return true;
5904 auto queryEnd = std::chrono::steady_clock::now();
5905 auto queryDuration =
5906 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
5908 // store the total value of items as a property
5909 if (total < iRowsFound)
5910 total = iRowsFound;
5911 items.SetProperty("total", total);
5913 DatabaseResults results;
5914 results.reserve(iRowsFound);
5916 // Avoid sorting with limits, just fetch results from dataset
5917 // Limit when SortByNone already applied in SQL,
5918 // Need guaranteed ordering for dataset processing to group by disc title
5919 // so apply sort later to fileitems list rather than dataset
5920 sorting.sortBy = SortByNone;
5921 if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results))
5922 return false;
5924 // Get data from returned rows, note possibly multiple albums although usually only one
5925 items.Reserve(total);
5926 int albumOffset = 2;
5927 CAlbum album;
5928 bool useTitle = true; // Assume we want to match by disc title later unless we have no titles
5929 std::string oldDiscTitle;
5930 const dbiplus::query_data& data = m_pDS->get_result_set().records;
5931 for (const auto& i : results)
5933 unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger());
5934 const dbiplus::sql_record* const record = data.at(targetRow);
5937 if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt())
5938 { // New album
5939 useTitle = true;
5940 album = GetAlbumFromDataset(record, albumOffset);
5943 int discnum = record->at(0).get_asInt();
5944 std::string strDiscSubtitle = record->at(1).get_asString();
5945 if (strDiscSubtitle.empty())
5946 { // Make (fake) disc title from disc number, group by disc number as no real title to match
5947 strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum);
5948 useTitle = false;
5950 else if (oldDiscTitle == strDiscSubtitle)
5951 { // disc title already added to list, fetch the next disc
5952 continue;
5954 oldDiscTitle = strDiscSubtitle;
5956 CMusicDbUrl itemUrl = musicUrl;
5957 std::string path = StringUtils::Format("{}/", discnum);
5958 itemUrl.AppendPath(path);
5960 // When disc titles are provided group discs together by title not number.
5961 // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
5962 if (useTitle)
5963 itemUrl.AddOption("disctitle", strDiscSubtitle.c_str());
5964 else
5965 itemUrl.AddOption("discid", discnum);
5966 CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album));
5967 pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order??
5968 pItem->GetMusicInfoTag()->SetDiscNumber(discnum);
5969 pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle);
5970 pItem->SetLabel(strDiscSubtitle);
5971 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5972 pItem->SetProperty("icon_never_overlay", true);
5973 pItem->SetArt("icon", "DefaultAlbumCover.png");
5974 items.Add(pItem);
5976 catch (...)
5978 m_pDS->close();
5979 CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
5980 items.Size());
5984 // cleanup
5985 m_pDS->close();
5987 // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
5988 // that is when have join with songartistview and sorting other than random with limit
5989 if (sorting.sortBy != SortByNone && !(limitedInSQL && sorting.sortBy == SortByRandom))
5990 items.Sort(sorting);
5992 auto end = std::chrono::steady_clock::now();
5993 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
5995 CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms", __FUNCTION__,
5996 duration.count(), queryDuration.count());
5998 return true;
6000 catch (...)
6002 m_pDS->close();
6003 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
6006 return false;
6008 int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */)
6010 int iDiscTotal = -1;
6011 CFileItemList itemscount;
6012 if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true))
6013 iDiscTotal = itemscount.GetProperty("total").asInteger32();
6014 return iDiscTotal;
6017 bool CMusicDatabase::GetSongsFullByWhere(
6018 const std::string& baseDir,
6019 const Filter& filter,
6020 CFileItemList& items,
6021 const SortDescription& sortDescription /* = SortDescription() */,
6022 bool artistData /* = false*/)
6024 if (m_pDB == nullptr || m_pDS == nullptr)
6025 return false;
6029 auto start = std::chrono::steady_clock::now();
6030 int total = -1;
6032 Filter extFilter = filter;
6033 CMusicDbUrl musicUrl;
6034 SortDescription sorting = sortDescription;
6035 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6036 return false;
6038 bool extended = false;
6039 bool limitedInSQL =
6040 extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0);
6042 // If there are extra WHERE conditions (from media filter dialog) we might
6043 // need access to albumview for these conditions
6044 if (extFilter.where.find("albumview") != std::string::npos)
6046 extended = true;
6047 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
6050 // Build songview <where> for count
6051 std::string strSQLExtra;
6052 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6053 return false;
6055 // Count (without group by) number of songs that satisfy selection criteria
6056 // Much quicker to use song table, not songview, when filtering only on song fields
6057 if (extended ||
6058 (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos ||
6059 extFilter.where.find("strPath") != std::string::npos ||
6060 extFilter.where.find("bCompilation") != std::string::npos ||
6061 extFilter.where.find("bBoxedset") != std::string::npos)))
6062 total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS);
6063 else
6065 std::string strSQLsong = strSQLExtra;
6066 StringUtils::Replace(strSQLsong, "songview", "song");
6067 total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS);
6070 if (extended)
6071 extFilter.AppendGroup("songview.idSong");
6073 // Apply any limiting directly in SQL
6074 if (limitedInSQL)
6076 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
6079 // Apply sort in SQL
6080 const std::shared_ptr<CSettings> settings =
6081 CServiceBroker::GetSettingsComponent()->GetSettings();
6082 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
6083 sorting.sortAttributes =
6084 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
6085 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
6086 GetOrderFilter(MediaTypeSong, sorting, extFilter);
6087 // Modify order to use correct calculated year field
6088 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
6089 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
6090 StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
6091 else
6092 StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
6094 std::string strFields = "songview.*";
6095 if (!artistData || limitedInSQL)
6097 // Build songview <where> + <order by> + <limits>
6098 strSQLExtra.clear();
6099 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6100 return false;
6102 else
6103 strFields = "songview.*, songartistview.*";
6104 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
6105 strFields = strFields + ", " + extFilter.fields;
6107 std::string strSQL;
6108 if (artistData)
6109 { // Get data from song and song_artist tables to fully populate songs with artists
6110 // All songs now have at least one artist so inner join sufficient
6111 // Build songartistview JOIN part of query
6112 Filter joinFilter;
6113 std::string strSQLJoin;
6114 joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
6115 if (sortDescription.sortBy == SortByRandom)
6116 joinFilter.AppendOrder("songartistview.idSong");
6117 else
6118 joinFilter.order = extFilter.order;
6119 if (limitedInSQL)
6121 StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong");
6122 StringUtils::Replace(joinFilter.order, "songview.", "sv.");
6124 else
6125 joinFilter.where = extFilter.where;
6126 joinFilter.AppendOrder("songartistview.idRole");
6127 joinFilter.AppendOrder("songartistview.iOrder");
6128 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6129 return false;
6131 if (limitedInSQL)
6133 // When have artist data (all roles) and LIMIT on songs use inline view
6134 // SELECT sv.*, songartistview.* FROM
6135 // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
6136 // <order by sv fields>, songartistview.idRole, songartistview.iOrder
6137 // Apply where clause, limits and order to songview, then join to songartistview this gives
6138 // multiple records per song in result set
6139 strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
6140 strSQL = "(" + strSQL + ") AS sv ";
6141 strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin;
6143 else
6144 strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin;
6146 else
6147 strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
6149 CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
6150 auto queryStart = std::chrono::steady_clock::now();
6151 // run query
6152 if (!m_pDS->query(strSQL))
6153 return false;
6155 int iRowsFound = m_pDS->num_rows();
6156 if (iRowsFound == 0)
6158 m_pDS->close();
6159 return true;
6162 auto queryEnd = std::chrono::steady_clock::now();
6163 auto queryDuration =
6164 std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
6166 // Store the total number of songs as a property
6167 items.SetProperty("total", total);
6169 DatabaseResults results;
6170 results.reserve(iRowsFound);
6171 // Populate results field vector from dataset
6172 FieldList fields;
6173 if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results))
6174 return false;
6175 // Store item list sort order
6176 items.SetSortMethod(sorting.sortBy);
6177 items.SetSortOrder(sorting.sortOrder);
6179 // Get songs from returned rows. If join songartistview then there is a row for every artist
6180 items.Reserve(total);
6181 int songArtistOffset = song_enumCount;
6182 int songId = -1;
6183 VECARTISTCREDITS artistCredits;
6184 const dbiplus::query_data& data = m_pDS->get_result_set().records;
6185 int count = 0;
6186 for (const auto& i : results)
6188 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
6189 const dbiplus::sql_record* const record = data.at(targetRow);
6193 if (songId != record->at(song_idSong).get_asInt())
6194 { //New song
6195 if (songId > 0 && !artistCredits.empty())
6197 //Store artist credits for previous song
6198 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
6199 artistCredits.clear();
6201 songId = record->at(song_idSong).get_asInt();
6202 CFileItemPtr item(new CFileItem);
6203 GetFileItemFromDataset(record, item.get(), musicUrl);
6204 // HACK for sorting by database returned order
6205 item->m_iprogramCount = ++count;
6206 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
6207 item->SetProperty("icon_never_overlay", true);
6208 item->SetArt("icon", "DefaultAudio.png");
6209 items.Add(item);
6211 // Get song artist credits and contributors
6212 if (artistData)
6214 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
6215 if (idSongArtistRole == ROLE_ARTIST)
6216 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
6217 else
6218 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
6219 GetArtistRoleFromDataset(record, songArtistOffset));
6222 catch (...)
6224 m_pDS->close();
6225 CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
6226 return (items.Size() > 0);
6229 if (!artistCredits.empty())
6231 //Store artist credits for final song
6232 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
6233 artistCredits.clear();
6235 // cleanup
6236 m_pDS->close();
6238 // Ensure random order of item list when results set sorted by idSong for artist processing
6239 // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
6240 // navigation. Order is read later from view state and list sorting is then triggered by
6241 // CGUIMediaWindow::Update in both cases.
6242 // So sorting here is currently redundant, but the consistent place to do it.
6243 // !@ todo: do sorting once, preferably in SQL
6244 if (sorting.sortBy == SortByRandom && artistData)
6245 items.Sort(sorting);
6247 auto end = std::chrono::steady_clock::now();
6248 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
6250 CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__,
6251 duration.count(), queryDuration.count());
6253 return true;
6255 catch (...)
6257 // cleanup
6258 m_pDS->close();
6259 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
6261 return false;
6264 bool CMusicDatabase::GetSongsByWhere(
6265 const std::string& baseDir,
6266 const Filter& filter,
6267 CFileItemList& items,
6268 const SortDescription& sortDescription /* = SortDescription() */)
6270 if (m_pDB == nullptr || m_pDS == nullptr)
6271 return false;
6275 int total = -1;
6277 std::string strSQL = "SELECT %s FROM songview ";
6279 Filter extFilter = filter;
6280 CMusicDbUrl musicUrl;
6281 SortDescription sorting = sortDescription;
6282 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6283 return false;
6285 // if there are extra WHERE conditions we might need access
6286 // to songview for these conditions
6287 if (extFilter.where.find("albumview") != std::string::npos)
6289 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
6290 extFilter.AppendGroup("songview.idSong");
6293 std::string strSQLExtra;
6294 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6295 return false;
6297 // Apply the limiting directly here if there's no special sorting but limiting
6298 if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
6299 (sorting.limitStart > 0 || sorting.limitEnd > 0))
6301 total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS);
6302 strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
6305 strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0
6306 ? filter.fields.c_str()
6307 : "songview.*") +
6308 strSQLExtra;
6310 CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
6311 // run query
6312 if (!m_pDS->query(strSQL))
6313 return false;
6315 int iRowsFound = m_pDS->num_rows();
6316 if (iRowsFound == 0)
6318 m_pDS->close();
6319 return true;
6322 // store the total value of items as a property
6323 if (total < iRowsFound)
6324 total = iRowsFound;
6325 items.SetProperty("total", total);
6327 DatabaseResults results;
6328 results.reserve(iRowsFound);
6329 if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results))
6330 return false;
6332 // get data from returned rows
6333 items.Reserve(results.size());
6334 const dbiplus::query_data& data = m_pDS->get_result_set().records;
6335 int count = 0;
6336 for (const auto& i : results)
6338 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
6339 const dbiplus::sql_record* const record = data.at(targetRow);
6343 CFileItemPtr item(new CFileItem);
6344 GetFileItemFromDataset(record, item.get(), musicUrl);
6345 // HACK for sorting by database returned order
6346 item->m_iprogramCount = ++count;
6347 items.Add(item);
6349 catch (...)
6351 m_pDS->close();
6352 CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
6353 return (items.Size() > 0);
6357 // cleanup
6358 m_pDS->close();
6359 return true;
6361 catch (...)
6363 // cleanup
6364 m_pDS->close();
6365 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
6367 return false;
6370 bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year)
6372 CMusicDbUrl musicUrl;
6373 if (!musicUrl.FromString(baseDir))
6374 return false;
6376 musicUrl.AddOption("year", year);
6378 Filter filter;
6379 return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true);
6382 bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir,
6383 CFileItemList& items,
6384 int idGenre,
6385 int idArtist,
6386 int idAlbum,
6387 const SortDescription& sortDescription /* = SortDescription() */)
6389 CMusicDbUrl musicUrl;
6390 if (!musicUrl.FromString(strBaseDir))
6391 return false;
6393 if (idAlbum > 0)
6394 musicUrl.AddOption("albumid", idAlbum);
6396 if (idGenre > 0)
6397 musicUrl.AddOption("genreid", idGenre);
6399 if (idArtist > 0)
6400 musicUrl.AddOption("artistid", idArtist);
6402 Filter filter;
6403 return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true);
6406 // clang-format off
6407 typedef struct
6409 std::string fieldJSON; // Field name in JSON schema
6410 std::string formatJSON; // Format in JSON schema
6411 bool bSimple; // Fetch field directly to JSON output
6412 std::string fieldDB; // Name of field in db query
6413 std::string SQL; // SQL for scalar subqueries or field alias
6414 } translateJSONField;
6416 static const translateJSONField JSONtoDBArtist[] = {
6417 // Table and single value join fields
6418 { "artist", "string", true, "strArtist", "" }, // Label field at top
6419 { "sortname", "string", true, "strSortname", "" },
6420 { "instrument", "array", true, "strInstruments", "" },
6421 { "description", "string", true, "strBiography", "" },
6422 { "genre", "array", true, "strGenres", "" },
6423 { "mood", "array", true, "strMoods", "" },
6424 { "style", "array", true, "strStyles", "" },
6425 { "yearsactive", "array", true, "strYearsActive", "" },
6426 { "born", "string", true, "strBorn", "" },
6427 { "formed", "string", true, "strFormed", "" },
6428 { "died", "string", true, "strDied", "" },
6429 { "disbanded", "string", true, "strDisbanded", "" },
6430 { "type", "string", true, "strType", "" },
6431 { "gender", "string", true, "strGender", "" },
6432 { "disambiguation", "string", true, "strDisambiguation", "" },
6433 { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
6434 { "dateadded", "string", true, "dateAdded", "" },
6435 { "datenew", "string", true, "dateNew", "" },
6436 { "datemodified", "string", true, "dateModified", "" },
6438 // JOIN fields (multivalue), same order as _JoinToArtistFields
6439 { "", "", false, "isSong", "" },
6440 { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" },
6441 { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" },
6442 { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" },
6443 { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" },
6444 { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" },
6445 { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" },
6446 { "art", "", false, "idArt", "art.art_id AS idArt" },
6447 { "", "", false, "artType", "art.type AS artType" },
6448 { "", "", false, "artURL", "art.url AS artURL" },
6449 { "", "", false, "idRole", "song_artist.idRole" },
6450 { "roles", "", false, "strRole", "role.strRole" },
6451 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
6452 // Derived from joined tables
6453 { "isalbumartist", "bool", false, "", "" },
6454 { "thumbnail", "string", false, "", "" },
6455 { "fanart", "string", false, "", "" }
6457 Sources and genre are related via album, and so the dataset only contains source and genre
6458 pairs that exist, rather than all the genres being repeated for every source. We can not only
6459 look at genres for the first source, and genre can be out of order.
6462 // clang-format on
6464 static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField);
6466 bool CMusicDatabase::GetArtistsByWhereJSON(
6467 const std::set<std::string>& fields,
6468 const std::string& baseDir,
6469 CVariant& result,
6470 int& total,
6471 const SortDescription& sortDescription /* = SortDescription() */)
6473 if (nullptr == m_pDB)
6474 return false;
6475 if (nullptr == m_pDS)
6476 return false;
6480 total = -1;
6482 size_t resultcount = 0;
6483 Filter extFilter;
6484 CMusicDbUrl musicUrl;
6485 SortDescription sorting = sortDescription;
6486 //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
6487 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6488 return false;
6490 // Replace view names in filter with table names
6491 StringUtils::Replace(extFilter.where, "artistview", "artist");
6492 StringUtils::Replace(extFilter.where, "albumview", "album");
6494 std::string strSQLExtra;
6495 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6496 return false;
6498 // Count number of artists that satisfy selection criteria
6499 //(includes xsp limits from filter, but not sort limits)
6500 total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS);
6501 resultcount = static_cast<size_t>(total);
6503 // Process albumartistsonly option
6504 const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
6505 bool albumArtistsOnly(false);
6506 auto option = options.find("albumartistsonly");
6507 if (option != options.end())
6508 albumArtistsOnly = option->second.asBoolean();
6509 // Process role options
6510 int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles.
6511 option = options.find("roleid");
6512 if (option != options.end())
6513 roleidfilter = static_cast<int>(option->second.asInteger());
6514 else
6516 option = options.find("role");
6517 if (option != options.end())
6519 if (option->second.asString() == "all" || option->second.asString() == "%")
6520 roleidfilter = -1000; //All roles
6521 else
6522 roleidfilter = GetRoleByName(option->second.asString());
6526 // Get order by (and any scalar query artist fields)
6527 int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter);
6528 // Replace artistview field names in order by artist table field names
6529 StringUtils::Replace(extFilter.order, "artistview", "artist");
6530 StringUtils::Replace(extFilter.fields, "artistview", "artist");
6532 // Grab and adjust artist sort field that may have been added to filter
6533 // These need to be added to the end of the artist table field list
6534 std::string artistsortSQL = extFilter.fields;
6535 extFilter.fields.clear();
6537 std::string strSQL;
6539 // Setup fields to query, and album field number mapping
6540 // Find first join field (isSong) in JSONtoDBArtist for offset
6541 int index_firstjoin = -1;
6542 for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++)
6544 if (JSONtoDBArtist[i].fieldDB == "isSong")
6546 index_firstjoin = i;
6547 break;
6550 Filter joinFilter;
6551 Filter albumArtistFilter;
6552 Filter songArtistFilter;
6553 DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount));
6554 extFilter.AppendField("artist.idArtist"); // ID "artistid" in JSON
6555 std::vector<int> dbfieldindex;
6556 // JSON "label" field is strArtist which is also output as "artist", query field once output twice
6557 extFilter.AppendField(JSONtoDBArtist[0].fieldDB);
6558 dbfieldindex.emplace_back(0); // Output "artist"
6560 // Check each optional artist db field that could be retrieved (not "artist")
6561 for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++)
6563 bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end();
6564 if (JSONtoDBArtist[i].bSimple)
6566 // Check for non-join fields in order too.
6567 // Query these in inline view (but not output) so can ref in outer order
6568 bool foundOrderby(false);
6569 if (!foundJSON)
6570 foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos;
6571 if (foundOrderby || foundJSON)
6573 // Store indexes of requested artist table and scalar subquery fields
6574 // to be output, and -1 when not output to JSON
6575 if (!foundJSON)
6576 dbfieldindex.emplace_back(-1);
6577 else
6578 dbfieldindex.emplace_back(i);
6579 // Field from scaler subquery
6580 if (!JSONtoDBArtist[i].SQL.empty())
6581 extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL));
6582 else
6583 // Field from artist table
6584 extFilter.AppendField(JSONtoDBArtist[i].fieldDB);
6587 else if (foundJSON)
6588 // Field from join or derived from joined fields
6589 joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true);
6592 // Append calculated artistsort field that may have been added to filter
6593 // Field used only for ORDER BY, not output to JSON
6594 extFilter.AppendField(artistsortSQL);
6595 for (int i = 0; i < iAddedFields; i++)
6596 dbfieldindex.emplace_back(-2); // columns in dataset
6598 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6599 strSQLExtra = "";
6600 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6601 return false;
6603 // Add any LIMIT clause to strSQLExtra
6604 if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
6606 strSQLExtra +=
6607 DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
6608 resultcount = std::min(
6609 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
6610 resultcount);
6613 // Setup multivalue JOINs, GROUP BY and ORDER BY
6614 bool bJoinAlbumArtist(false);
6615 bool bJoinSongArtist(false);
6616 if (sortDescription.sortBy != SortByRandom)
6618 // Repeat inline view order (that always includes idArtist) on join query
6619 std::string order = extFilter.order;
6620 StringUtils::Replace(order, "artist.", "a1.");
6621 joinFilter.AppendOrder(order);
6623 else
6624 joinFilter.AppendOrder("a1.idArtist");
6625 joinFilter.AppendGroup("a1.idArtist");
6626 // Album artists and song artists
6627 if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) ||
6628 joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6629 joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) ||
6630 joinLayout.GetFetch(joinToArtist_strRole))
6632 bJoinAlbumArtist = true;
6633 albumArtistFilter.AppendField("album_artist.idArtist AS id");
6634 if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole))
6636 bJoinSongArtist = true;
6637 songArtistFilter.AppendField("song_artist.idArtist AS id");
6638 songArtistFilter.AppendField("1 AS isSong");
6639 albumArtistFilter.AppendField("0 AS isSong");
6640 joinLayout.SetField(joinToArtist_isSong,
6641 JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6642 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6643 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6646 else if (joinLayout.GetFetch(joinToArtist_isalbumartist))
6648 // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
6649 // so no need for join to album_artist table. Set fetching fetch false so that
6650 // joinLayout.HasFilterFields() is false
6651 joinLayout.SetFetch(joinToArtist_isalbumartist, false);
6654 // Sources
6655 if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6656 { // Left join as source may have been removed but leaving lib entries
6657 albumArtistFilter.AppendJoin(
6658 "LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
6659 albumArtistFilter.AppendField(
6660 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL);
6661 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6662 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6663 if (bJoinSongArtist)
6665 songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
6666 songArtistFilter.AppendJoin(
6667 "LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
6668 songArtistFilter.AppendField(
6669 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6670 songArtistFilter.AppendField(
6671 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL);
6672 albumArtistFilter.AppendField(
6673 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6674 joinLayout.SetField(joinToArtist_idSourceSong,
6675 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6676 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6677 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6679 else
6681 joinLayout.SetField(joinToArtist_idSourceAlbum,
6682 JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true);
6686 // Songgenres - id and genres always both
6687 if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
6688 { // All albums have songs, but left join genre as songs may not have genre
6689 albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
6690 albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
6691 albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6692 albumArtistFilter.AppendField(
6693 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL);
6694 albumArtistFilter.AppendField(
6695 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6696 joinLayout.SetField(joinToArtist_strSongGenreAlbum,
6697 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6698 joinFilter.AppendGroup(
6699 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6700 joinFilter.AppendOrder(
6701 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6702 if (bJoinSongArtist)
6703 { // Left join genre as songs may not have genre
6704 songArtistFilter.AppendJoin(
6705 "LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
6706 songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6707 songArtistFilter.AppendField(
6708 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6709 songArtistFilter.AppendField(
6710 "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6711 songArtistFilter.AppendField(
6712 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL);
6713 songArtistFilter.AppendField(
6714 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL);
6715 albumArtistFilter.AppendField(
6716 "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6717 albumArtistFilter.AppendField(
6718 "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6719 joinLayout.SetField(joinToArtist_idSongGenreSong,
6720 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6721 joinLayout.SetField(
6722 joinToArtist_strSongGenreSong,
6723 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6724 joinFilter.AppendGroup(
6725 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6726 joinFilter.AppendOrder(
6727 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6729 else
6730 { // Define field alias names in join layout
6731 joinLayout.SetField(joinToArtist_idSongGenreAlbum,
6732 JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL,
6733 true);
6734 joinLayout.SetField(joinToArtist_strSongGenreAlbum,
6735 JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6739 // Roles
6740 if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole))
6741 // Only looking at album and song artists not other roles (default),
6742 // so filter dataset rows likewise.
6743 songArtistFilter.AppendWhere("song_artist.idRole = 1");
6744 else if (joinLayout.GetFetch(joinToArtist_strRole) || // "roles" field
6745 (bJoinSongArtist && (joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6746 joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))))
6747 { // Rows from many roles so fetch roleid for "roles", source and genre processing
6748 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL);
6749 // Add fake column to album_artist query
6750 albumArtistFilter.AppendField("-1 AS " +
6751 JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6752 joinLayout.SetField(joinToArtist_idRole,
6753 JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6754 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6755 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6757 if (joinLayout.GetFetch(joinToArtist_strRole))
6758 { // Fetch role desc
6759 songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
6760 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL);
6761 // Add fake column to album_artist query
6762 albumArtistFilter.AppendField("'albumartist' AS " +
6763 JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB);
6766 // Build source, genre and roles part of query
6767 if (bJoinAlbumArtist)
6769 if (bJoinSongArtist)
6771 // Combine song and album artist filter as UNION and add to join filter as an inline view
6772 std::string strAlbumSQL;
6773 if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL))
6774 return false;
6775 strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL;
6776 std::string strSongSQL;
6777 if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL))
6778 return false;
6779 strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL;
6781 joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL +
6782 ") AS albumSong ON id = a1.idArtist");
6784 else
6785 { //Only join album_artist, so move filter elements to join filter
6786 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
6787 joinFilter.AppendJoin(albumArtistFilter.join);
6791 //Art
6792 bool bJoinArt(false);
6793 bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) ||
6794 joinLayout.GetOutput(joinToArtist_thumbnail) ||
6795 joinLayout.GetOutput(joinToArtist_fanart);
6796 if (bJoinArt)
6797 { // Left join as artist may not have any art
6798 joinFilter.AppendJoin(
6799 "LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
6800 joinLayout.SetField(joinToArtist_idArt,
6801 JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL,
6802 joinLayout.GetOutput(joinToArtist_idArt));
6803 joinLayout.SetField(joinToArtist_artType,
6804 JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL);
6805 joinLayout.SetField(joinToArtist_artURL,
6806 JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL);
6807 joinFilter.AppendGroup("art.art_id");
6808 joinFilter.AppendOrder("arttype");
6809 if (!joinLayout.GetOutput(joinToArtist_idArt))
6811 if (!joinLayout.GetOutput(joinToArtist_thumbnail))
6812 // Fanart only
6813 joinFilter.AppendJoin("AND art.type = 'fanart'");
6814 else if (!joinLayout.GetOutput(joinToArtist_fanart))
6815 // Thumb only
6816 joinFilter.AppendJoin("AND art.type = 'thumb'");
6819 else if (bJoinSongArtist)
6820 joinFilter.group.clear(); // UNION only so no GROUP BY needed
6822 // Build JOIN part of query (if we have one)
6823 std::string strSQLJoin;
6824 if (joinLayout.HasFilterFields())
6825 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6826 return false;
6828 // Adjust where in the results record the join fields are allowing for the
6829 // inline view fields (Quicker than finding field by name every time)
6830 // idArtist + other artist fields
6831 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
6833 // Build full query
6834 // When have multiple value joins e.g. song genres, use inline view
6835 // SELECT a1.*, <join fields> FROM
6836 // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1
6837 // <joins> <group by> <order by> + <joins order by>
6838 // Don't use prepareSQL - confuses arttype = 'thumb' filter
6840 strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra;
6841 if (joinLayout.HasFilterFields())
6843 strSQL = "(" + strSQL + ") AS a1 ";
6844 strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
6847 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
6848 // run query
6849 auto start = std::chrono::steady_clock::now();
6851 if (!m_pDS->query(strSQL))
6852 return false;
6854 auto end = std::chrono::steady_clock::now();
6855 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
6857 CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
6859 int iRowsFound = m_pDS->num_rows();
6860 if (iRowsFound <= 0)
6862 m_pDS->close();
6863 return true;
6866 // Get artists from returned rows. Joins means there can be many rows per artist
6867 int artistId = -1;
6868 int sourceId = -1;
6869 int genreId = -1;
6870 int roleId = -1;
6871 int artId = -1;
6872 std::vector<int> genreidlist;
6873 std::vector<int> sourceidlist;
6874 std::vector<int> roleidlist;
6875 bool bArtDone(false);
6876 bool bHaveArtist(false);
6877 bool bIsAlbumArtist(true);
6878 bool bGenreFoundViaAlbum(false);
6879 CVariant artistObj;
6880 result["artists"].reserve(resultcount);
6881 while (!m_pDS->eof() || bHaveArtist)
6883 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
6885 if (m_pDS->eof() || artistId != record->at(0).get_asInt())
6887 // Store previous or last artist
6888 if (bHaveArtist)
6890 // Convert any empty MBid array into an array with one empty element [""]
6891 // to match the number of artist ID (way other mbid arrays handled)
6892 if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty())
6893 artistObj["musicbrainzartistid"].append("");
6895 result["artists"].append(artistObj);
6896 bHaveArtist = false;
6897 artistObj.clear();
6899 if (artistObj.empty())
6901 // Initialise fields, ensure those with possible null values are set to correct empty variant type
6902 if (joinLayout.GetOutput(joinToArtist_idSourceAlbum))
6903 artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
6904 if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum))
6905 artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
6906 if (joinLayout.GetOutput(joinToArtist_idArt))
6907 artistObj["art"] = CVariant(CVariant::VariantTypeObject);
6908 if (joinLayout.GetOutput(joinToArtist_thumbnail))
6909 artistObj["thumbnail"] = "";
6910 if (joinLayout.GetOutput(joinToArtist_fanart))
6911 artistObj["fanart"] = "";
6913 sourceId = -1;
6914 roleId = -1;
6915 genreId = -1;
6916 artId = -1;
6917 genreidlist.clear();
6918 bGenreFoundViaAlbum = false;
6919 sourceidlist.clear();
6920 roleidlist.clear();
6921 bArtDone = false;
6923 if (m_pDS->eof())
6924 continue; // Having saved the last artist stop
6926 // New artist
6927 artistId = record->at(0).get_asInt();
6928 bHaveArtist = true;
6929 artistObj["artistid"] = artistId;
6930 artistObj["label"] = record->at(1).get_asString();
6931 artistObj["artist"] = record->at(1).get_asString(); // Always have "artist"
6932 bIsAlbumArtist = true; //Album artist by default
6933 if (joinLayout.GetOutput(joinToArtist_isalbumartist))
6935 // Not album artist when fetching song artists too and first row for artist isSong=true
6936 if (bJoinSongArtist)
6937 bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
6938 artistObj["isalbumartist"] = bIsAlbumArtist;
6940 for (size_t i = 0; i < dbfieldindex.size(); i++)
6941 if (dbfieldindex[i] > -1)
6943 if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer")
6944 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
6945 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float")
6946 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
6947 record->at(1 + i).get_asFloat();
6948 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array")
6949 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
6950 record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
6951 ->GetAdvancedSettings()
6952 ->m_musicItemSeparator);
6953 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean")
6954 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
6955 else
6956 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
6957 record->at(1 + i).get_asString();
6960 if (bJoinAlbumArtist)
6962 bool bAlbumArtistRow(true);
6963 int idRoleRow = -1;
6964 if (bJoinSongArtist)
6966 bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
6967 if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 &&
6968 !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull())
6970 idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt();
6974 // Sources - gathered via both album_artist and song_artist (with role = 1)
6975 if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6977 if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 &&
6978 !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() &&
6979 sourceId !=
6980 record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) ||
6981 (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 &&
6982 !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() &&
6983 sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt()))
6985 bArtDone = bArtDone || (sourceId > 0); // Not first source, skip art repeats
6986 bool found(false);
6987 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt();
6988 if (!bAlbumArtistRow)
6990 // Skip other roles (when fetching them)
6991 if (idRoleRow > 1)
6993 found = true;
6995 else
6997 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt();
6998 // Song artist row may repeat sources found via album artist
6999 // Already have that source?
7000 for (const auto& i : sourceidlist)
7001 if (i == sourceId)
7003 found = true;
7004 break;
7008 if (!found)
7010 sourceidlist.emplace_back(sourceId);
7011 artistObj["sourceid"].append(sourceId);
7015 // Songgenres - via album artist takes precedence
7017 Sources and genre are related via album, and so the dataset only contains source
7018 and genre pairs that exist, rather than all the genres being repeated for every
7019 source. We can not only look at genres for the first source, and genre can be
7020 found out of order.
7021 Also song artist row may repeat genres found via album artist
7023 if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
7025 std::string strGenre;
7026 bool newgenre(false);
7027 if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 &&
7028 !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() &&
7029 genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt())
7031 bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
7032 newgenre = true;
7033 genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt();
7034 strGenre =
7035 record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString();
7037 else if (!bAlbumArtistRow && !bGenreFoundViaAlbum &&
7038 joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 &&
7039 !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() &&
7040 genreId !=
7041 record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt())
7043 bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
7044 newgenre = idRoleRow <= 1; // Skip other roles (when fetching them)
7045 genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt();
7046 strGenre =
7047 record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString();
7049 if (newgenre)
7051 // Already have that genre?
7052 bool found(false);
7053 for (const auto& i : genreidlist)
7054 if (i == genreId)
7056 found = true;
7057 break;
7059 if (!found)
7061 bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow;
7062 genreidlist.emplace_back(genreId);
7063 CVariant genreObj;
7064 genreObj["genreid"] = genreId;
7065 genreObj["title"] = strGenre;
7066 artistObj["songgenres"].append(genreObj);
7070 // Roles - gathered via song_artist roleid rows
7071 if (joinLayout.GetFetch(joinToArtist_idRole))
7073 if (!bAlbumArtistRow && roleId != idRoleRow)
7075 bArtDone = bArtDone || (roleId > 0); // Not first role, skip art repeats
7076 roleId = idRoleRow;
7077 if (joinLayout.GetOutput(joinToArtist_strRole))
7079 // Already have that role?
7080 bool found(false);
7081 for (const auto& i : roleidlist)
7082 if (i == roleId)
7084 found = true;
7085 break;
7087 if (!found)
7089 roleidlist.emplace_back(roleId);
7090 CVariant roleObj;
7091 roleObj["roleid"] = roleId;
7092 roleObj["role"] =
7093 record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString();
7094 artistObj["roles"].append(roleObj);
7100 // Art
7101 if (bJoinArt && !bArtDone &&
7102 !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() &&
7103 record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 &&
7104 artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt())
7106 artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt();
7107 if (joinLayout.GetOutput(joinToArtist_idArt))
7109 artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] =
7110 CTextureUtils::GetWrappedImageURL(
7111 record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
7113 if (joinLayout.GetOutput(joinToArtist_thumbnail) &&
7114 record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb")
7116 artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL(
7117 record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
7119 if (joinLayout.GetOutput(joinToArtist_fanart) &&
7120 record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart")
7122 artistObj["fanart"] = CTextureUtils::GetWrappedImageURL(
7123 record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
7127 m_pDS->next();
7129 m_pDS->close(); // cleanup recordset data
7131 // Ensure random order of output when results set is sorted to process multi-value joins
7132 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7133 KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array());
7135 return true;
7137 catch (...)
7139 m_pDS->close();
7140 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
7142 return false;
7145 // clang-format off
7146 static const translateJSONField JSONtoDBAlbum[] = {
7147 // albumview (inc scalar subquery fields use in filter rules)
7148 { "title", "string", true, "strAlbum", "" }, // Label field at top
7149 { "description", "string", true, "strReview", "" },
7150 { "genre", "array", true, "strGenres", "" },
7151 { "theme", "array", true, "strThemes", "" },
7152 { "mood", "array", true, "strMoods", "" },
7153 { "style", "array", true, "strStyles", "" },
7154 { "type", "string", true, "strType", "" },
7155 { "albumlabel", "string", true, "strLabel", "" },
7156 { "rating", "float", true, "fRating", "" },
7157 { "votes", "integer", true, "iVotes", "" },
7158 { "userrating", "unsigned", true, "iUserrating", "" },
7159 { "isboxset", "boolean", true, "bBoxedSet", "" },
7160 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" },
7161 { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table
7162 { "compilation", "boolean", true, "bCompilation", "" },
7163 { "releasetype", "string", true, "strReleaseType", "" },
7164 { "totaldiscs", "integer", true, "iDiscTotal", "" },
7165 { "sortartist", "string", true, "strArtistSort", "" },
7166 { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" },
7167 { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view
7168 { "dateadded", "string", true, "dateAdded", "" },
7169 { "datenew", "string", true, "dateNew", "" },
7170 { "datemodified", "string", true, "dateModified", "" },
7171 { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view
7172 { "originaldate", "string", true, "strOrigReleaseDate", "" },
7173 { "releasedate", "string", true, "strReleaseDate", "" },
7174 { "albumstatus", "string", true, "strReleaseStatus", "" },
7175 { "albumduration", "integer", true, "iAlbumDuration", "" },
7176 // Scalar subquery fields
7177 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7178 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
7179 { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
7180 "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
7181 // Single value JOIN fields
7182 { "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
7183 // JOIN fields (multivalue), same order as _JoinToAlbumFields
7184 { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" },
7185 { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" },
7186 { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" },
7188 Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
7189 and separate queries to allow for fallback strategy.
7191 Using albmview, rather than album table, as view has scalar subqueries for
7192 playcount and lastplayed already defined. Needed as MySQL does
7193 not support use of scalar subquery field alias names in where clauses (they
7194 have to be repeated) and these fields can be used by filter rules.
7195 Using this view is no slower than the album table as these scalar fields are
7196 only calculated (slowing query) when field is in field list.
7199 // clang-format on
7201 static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField);
7203 bool CMusicDatabase::GetAlbumsByWhereJSON(
7204 const std::set<std::string>& fields,
7205 const std::string& baseDir,
7206 CVariant& result,
7207 int& total,
7208 const SortDescription& sortDescription /* = SortDescription() */)
7211 if (nullptr == m_pDB)
7212 return false;
7213 if (nullptr == m_pDS)
7214 return false;
7218 total = -1;
7220 size_t resultcount = 0;
7221 Filter extFilter;
7222 CMusicDbUrl musicUrl;
7223 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7224 // passed in at the start of the function
7225 SortDescription sorting = sortDescription;
7226 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
7227 return false;
7229 // Replace view names in filter with table names
7230 StringUtils::Replace(extFilter.where, "artistview", "artist");
7232 std::string strSQLExtra;
7233 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7234 return false;
7236 // Count number of albums that satisfy selection criteria
7237 // (includes xsp limits from filter, but not sort limits)
7238 // Use albumview as filter rules in where clause may use scalar query fields
7239 total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
7240 resultcount = static_cast<size_t>(total);
7242 // Get order by (and any scalar query artist fields
7243 int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter);
7245 // Grab calculated artist/title sort fields that may have been added to filter
7246 // These need to be added to the end of the album table field list
7247 std::string calcsortfieldsSQL = extFilter.fields;
7248 extFilter.fields.clear();
7250 std::string strSQL;
7252 // Setup fields to query, and album field number mapping
7253 // Find idArtist in JSONtoDBAlbum, offset of first join field
7254 int index_idArtist = -1;
7255 for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++)
7257 if (JSONtoDBAlbum[i].fieldDB == "idArtist")
7259 index_idArtist = i;
7260 break;
7263 Filter joinFilter;
7264 DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount));
7265 extFilter.AppendField("albumview.idAlbum"); // ID "albumid" in JSON
7266 std::vector<int> dbfieldindex;
7267 // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
7268 extFilter.AppendField(JSONtoDBAlbum[0].fieldDB);
7269 if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end())
7270 dbfieldindex.emplace_back(0); // Output "title"
7271 else
7272 dbfieldindex.emplace_back(-1); // fetch but not output
7274 // Check each optional album db field that could be retrieved (not label)
7275 for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++)
7277 bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end();
7278 if (JSONtoDBAlbum[i].bSimple)
7280 // Check for non-join fields in order too.
7281 // Query these in inline view (but not output) so can ref in outer order
7282 bool foundOrderby(false);
7283 if (!foundJSON)
7284 foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos;
7285 if (foundOrderby || foundJSON)
7287 // Store indexes of requested album table and scalar subquery fields
7288 // to be output, and -1 when not output to JSON
7289 if (!foundJSON)
7290 dbfieldindex.emplace_back(-1);
7291 else
7292 dbfieldindex.emplace_back(i);
7293 if (!JSONtoDBAlbum[i].SQL.empty())
7294 // Field from scaler subquery
7295 extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL));
7296 else
7297 // Field from album table
7298 extFilter.AppendField(JSONtoDBAlbum[i].fieldDB);
7301 else if (foundJSON)
7302 // Field from join found in JSON request
7303 joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true);
7306 // Append calculated artist/title sort fields that may have been added to filter
7307 // Field used only for ORDER BY, not output to JSON
7308 extFilter.AppendField(calcsortfieldsSQL);
7309 for (int i = 0; i < iAddedFields; i++)
7310 dbfieldindex.emplace_back(-1); // columns in dataset
7312 // JOIN art tables if needed (fields output and/or in sort)
7313 if (extFilter.fields.find("art.") != std::string::npos)
7314 { // Left join as not all albums have art, but only have one thumb at most
7315 extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
7316 "AND art.media_type = 'album' AND art.type = 'thumb'");
7319 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7320 strSQLExtra = "";
7321 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7322 return false;
7324 // Add any LIMIT clause to strSQLExtra
7325 if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
7327 strSQLExtra +=
7328 DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
7329 resultcount = std::min(
7330 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
7331 resultcount);
7334 // Setup multivalue JOINs, GROUP BY and ORDER BY
7335 bool bJoinAlbumArtist(false);
7336 if (sortDescription.sortBy != SortByRandom)
7338 // Repeat inline view order (that always includes idAlbum) on join query
7339 std::string order = extFilter.order;
7340 StringUtils::Replace(order, "albumview.", "a1.");
7341 joinFilter.AppendOrder(order);
7343 else
7344 joinFilter.AppendOrder("a1.idAlbum");
7345 joinFilter.AppendGroup("a1.idAlbum");
7346 // Album artists
7347 if (joinLayout.GetFetch(joinToAlbum_idArtist) || joinLayout.GetFetch(joinToAlbum_strArtist) ||
7348 joinLayout.GetFetch(joinToAlbum_strArtistMBID))
7349 { // All albums have at least one artist so inner join sufficient
7350 bJoinAlbumArtist = true;
7351 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
7352 joinFilter.AppendGroup("album_artist.idArtist");
7353 joinFilter.AppendOrder("album_artist.iOrder");
7354 // Ensure idArtist is queried
7355 if (!joinLayout.GetFetch(joinToAlbum_idArtist))
7356 joinLayout.SetField(joinToAlbum_idArtist,
7357 JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL);
7359 // artist table needed for strArtist or MBID
7360 // (album_artist.strArtist can be an alias or spelling variation)
7361 if (joinLayout.GetFetch(joinToAlbum_strArtist) ||
7362 joinLayout.GetFetch(joinToAlbum_strArtistMBID))
7363 joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
7365 // Build JOIN part of query (if we have one)
7366 std::string strSQLJoin;
7367 if (joinLayout.HasFilterFields())
7368 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
7369 return false;
7371 // Adjust where in the results record the join fields are allowing for the
7372 // inline view fields (Quicker than finding field by name every time)
7373 // idAlbum + other album fields
7374 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
7376 // Build full query
7377 // When have multiple value joins (artists or song genres) use inline view
7378 // SELECT a1.*, <join fields> FROM
7379 // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1
7380 // <joins> <group by> <order by> <joins order by>
7381 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
7383 strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra;
7384 if (joinLayout.HasFilterFields())
7386 strSQL = "(" + strSQL + ") AS a1 ";
7387 strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
7390 // Modify query to use correct year field
7391 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
7392 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
7393 StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate");
7394 else
7395 StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate");
7397 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
7398 // run query
7399 auto start = std::chrono::steady_clock::now();
7401 if (!m_pDS->query(strSQL))
7402 return false;
7404 auto end = std::chrono::steady_clock::now();
7405 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
7407 CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
7409 int iRowsFound = m_pDS->num_rows();
7410 if (iRowsFound <= 0)
7412 m_pDS->close();
7413 return true;
7416 // Get albums from returned rows. Joins means there can be many rows per album
7417 int albumId = -1;
7418 int artistId = -1;
7419 CVariant albumObj;
7420 result["albums"].reserve(resultcount);
7421 while (!m_pDS->eof() || !albumObj.empty())
7423 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
7425 if (m_pDS->eof() || albumId != record->at(0).get_asInt())
7427 // Store previous or last album
7428 if (!albumObj.empty())
7430 // Split sources string into int array
7431 if (albumObj.isMember("sourceid"))
7433 std::vector<std::string> sources =
7434 StringUtils::Split(albumObj["sourceid"].asString(), ";");
7435 albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
7436 for (size_t i = 0; i < sources.size(); i++)
7437 albumObj["sourceid"].append(atoi(sources[i].c_str()));
7439 result["albums"].append(albumObj);
7440 albumObj.clear();
7441 artistId = -1;
7443 if (m_pDS->eof())
7444 continue; // Having saved last album stop
7446 // New album
7447 albumId = record->at(0).get_asInt();
7448 albumObj["albumid"] = albumId;
7449 albumObj["label"] = record->at(1).get_asString();
7450 for (size_t i = 0; i < dbfieldindex.size(); i++)
7451 if (dbfieldindex[i] > -1)
7453 if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres")
7455 // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
7456 std::vector<std::string> values =
7457 StringUtils::Split(record->at(1 + i).get_asString(), ",");
7458 if (values.size() % 2 == 0) // Must contain an even number of entries
7460 for (size_t j = 0; j + 1 < values.size(); j += 2)
7462 int idGenre = atoi(values[j].c_str());
7463 if (idGenre > 0)
7465 CVariant genreObj;
7466 genreObj["genreid"] = idGenre;
7467 genreObj["title"] = values[j + 1];
7468 albumObj["songgenres"].append(genreObj);
7472 // Ensure albums with null songgenres get empty array
7473 if (!albumObj.isMember("songgenres"))
7474 albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
7476 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer")
7477 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7478 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned")
7479 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
7480 std::max(record->at(1 + i).get_asInt(), 0);
7481 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float")
7482 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
7483 std::max(record->at(1 + i).get_asFloat(), 0.f);
7484 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array")
7485 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
7486 record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
7487 ->GetAdvancedSettings()
7488 ->m_musicItemSeparator);
7489 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean")
7490 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7491 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image")
7493 std::string url = record->at(1 + i).get_asString();
7494 if (!url.empty())
7495 url = CTextureUtils::GetWrappedImageURL(url);
7496 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url;
7498 else
7499 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
7502 if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1)
7504 if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt())
7506 artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt();
7507 if (joinLayout.GetOutput(joinToAlbum_idArtist))
7508 albumObj["artistid"].append(artistId);
7509 if (artistId == BLANKARTIST_ID)
7511 if (joinLayout.GetOutput(joinToAlbum_strArtist))
7512 albumObj["artist"].append(StringUtils::Empty);
7513 if (joinLayout.GetOutput(joinToAlbum_strArtistMBID))
7514 albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
7516 else
7518 if (joinLayout.GetOutput(joinToAlbum_strArtist) &&
7519 joinLayout.GetRecNo(joinToAlbum_strArtist) > -1)
7520 albumObj["artist"].append(
7521 record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString());
7522 if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) &&
7523 joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1)
7524 albumObj["musicbrainzalbumartistid"].append(
7525 record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString());
7529 m_pDS->next();
7531 m_pDS->close(); // cleanup recordset data
7533 // Ensure random order of output when results set is sorted to process multi-value joins
7534 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7535 KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array());
7537 return true;
7539 catch (...)
7541 m_pDS->close();
7542 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
7544 return false;
7547 // clang-format off
7548 static const translateJSONField JSONtoDBSong[] = {
7549 // table and single value join fields
7550 { "title", "string", true, "strTitle", "" }, // Label field at top
7551 { "albumid", "integer", true, "song.idAlbum", "" },
7552 { "", "", true, "song.iTrack", "" },
7553 { "displayartist", "string", true, "song.strArtistDisp", "" },
7554 { "sortartist", "string", true, "song.strArtistSort", "" },
7555 { "genre", "array", true, "song.strGenres", "" },
7556 { "duration", "integer", true, "iDuration", "" },
7557 { "comment", "string", true, "comment", "" },
7558 { "", "string", true, "strFileName", "" },
7559 { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" },
7560 { "playcount", "integer", true, "iTimesPlayed", "" },
7561 { "lastplayed", "string", true, "lastPlayed", "" },
7562 { "rating", "float", true, "rating", "" },
7563 { "votes", "integer", true, "votes", "" },
7564 { "userrating", "unsigned", true, "song.userrating", "" },
7565 { "mood", "array", true, "mood", "" },
7566 { "dateadded", "string", true, "song.dateAdded", "" },
7567 { "datenew", "string", true, "song.dateNew", "" },
7568 { "datemodified", "string", true, "song.dateModified", "" },
7569 { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" },
7570 { "", "string", true, "strPath", "path.strPath AS strPath" },
7571 { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" },
7572 { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" },
7573 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
7574 { "disctitle", "string", true, "song.strDiscSubtitle", "" },
7575 { "bpm", "integer", true, "iBPM", "" },
7576 { "originaldate", "string" , true, "song.strOrigReleaseDate","" },
7577 { "releasedate", "string" , true, "song.strReleaseDate", "" },
7578 { "bitrate", "integer", true, "iBitRate", "" },
7579 { "samplerate", "integer", true, "iSampleRate", "" },
7580 { "channels", "integer", true, "iChannels", "" },
7582 // JOIN fields (multivalue), same order as _JoinToSongFields
7583 { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" },
7584 { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" },
7585 { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
7586 { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" },
7587 { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" },
7588 { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" },
7589 { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" },
7590 { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" },
7591 { "", "", false, "idRole", "song_artist.idRole" },
7592 { "", "", false, "strRole", "role.strRole" },
7593 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
7594 { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
7595 { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" },
7597 { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" },
7598 { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" },
7599 { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" },
7600 { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" },
7601 { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" },
7603 // Scalar subquery fields
7604 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7605 { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" },
7606 { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" },
7607 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
7609 Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
7610 thumbloader and separate queries to allow for fallback strategy
7611 "lyrics"?? Can be set for an item (by addons) but not held in db so
7612 AudioLibrary.GetSongs() never fills this field despite being in schema
7614 FROM ( SELECT * FROM song
7615 JOIN album ON album.idAlbum = song.idAlbum
7616 JOIN path ON path.idPath = song.idPath) AS sv
7617 JOIN album_artist ON album_artist.idAlbum = song.idAlbum
7618 JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
7619 JOIN song_artist ON song_artist.idSong = song.idSong
7620 JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist
7621 JOIN role ON song_artist.idRole = role.idRole
7622 LEFT JOIN song_genre ON song.idSong = song_genre.idSong
7626 // clang-format on
7628 static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField);
7630 bool CMusicDatabase::GetSongsByWhereJSON(
7631 const std::set<std::string>& fields,
7632 const std::string& baseDir,
7633 CVariant& result,
7634 int& total,
7635 const SortDescription& sortDescription /* = SortDescription() */)
7638 if (nullptr == m_pDB)
7639 return false;
7640 if (nullptr == m_pDS)
7641 return false;
7645 total = -1;
7647 size_t resultcount = 0;
7648 Filter extFilter;
7649 CMusicDbUrl musicUrl;
7650 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7651 // passed into the function
7652 SortDescription sorting = sortDescription;
7653 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
7654 return false;
7656 // Replace view names in filter with table names
7657 StringUtils::Replace(extFilter.where, "artistview", "artist");
7658 StringUtils::Replace(extFilter.where, "albumview", "album");
7659 StringUtils::Replace(extFilter.where, "songview.strPath", "strPath");
7660 StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum");
7661 StringUtils::Replace(extFilter.where, "songview", "song");
7662 StringUtils::Replace(extFilter.where, "songartistview", "song_artist");
7664 // JOIN album and path tables needed by filter rules in where clause
7665 if (extFilter.where.find("album.") != std::string::npos ||
7666 extFilter.where.find("strAlbum") != std::string::npos)
7667 { // All songs have one album so inner join sufficient
7668 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7670 if (extFilter.where.find("strPath") != std::string::npos)
7671 { // All songs have one path so inner join sufficient
7672 extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7675 // Build JOINs and WHERE needed by filter for counting songs
7676 std::string strSQLExtra;
7677 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7678 return false;
7680 // Count number of songs that satisfy selection criteria
7681 // (includes xsp limits from filter, but not sort limits)
7682 total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS);
7683 resultcount = static_cast<size_t>(total);
7685 int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter);
7686 // Replace songview field names in order by with song, album path table field names
7687 // Field names in album same as song:
7688 // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
7689 StringUtils::Replace(extFilter.order, "songview.strPath", "strPath");
7690 StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum");
7691 StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation");
7692 StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp");
7693 StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp");
7694 StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort");
7695 StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType");
7696 StringUtils::Replace(extFilter.order, "songview", "song");
7697 StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort");
7698 StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp");
7699 StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum");
7700 StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle");
7702 // Grab calculated artist/title sort fields that may have been added to filter
7703 // These need to be added to the end of the song table field list
7704 std::string calcsortfieldsSQL = extFilter.fields;
7705 extFilter.fields.clear();
7707 std::string strSQL;
7709 // Setup fields to query, and song field number mapping
7710 // Find idAlbumArtist in JSONtoDBSong, offset of first join field
7711 int index_idAlbumArtist = -1;
7712 for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++)
7714 if (JSONtoDBSong[i].fieldDB == "idAlbumArtist")
7716 index_idAlbumArtist = i;
7717 break;
7720 Filter joinFilter;
7721 DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount));
7722 extFilter.AppendField("song.idSong"); // ID "songid" in JSON
7723 std::vector<int> dbfieldindex;
7724 // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
7725 extFilter.AppendField(JSONtoDBSong[0].fieldDB);
7726 if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end())
7727 dbfieldindex.emplace_back(0); // Output "title"
7728 else
7729 dbfieldindex.emplace_back(-1); // Fetch but not output
7730 std::vector<std::string> rolefieldlist;
7731 std::vector<int> roleidlist;
7732 // Check each optional db field that could be retrieved (not label)
7733 for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++)
7735 bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end();
7736 if (JSONtoDBSong[i].bSimple)
7738 // Check for non-join fields in order too.
7739 // Query these in inline view (but not output) so can ref in outer order
7740 bool foundOrderby(false);
7741 if (!foundJSON)
7742 foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos;
7743 if (foundOrderby || foundJSON)
7745 // Store indexes of requested album table and scalar subquery fields
7746 // to be output, and -1 when not output to JSON
7747 if (!foundJSON)
7748 dbfieldindex.emplace_back(-1);
7749 else
7750 dbfieldindex.emplace_back(i);
7751 if (!JSONtoDBSong[i].SQL.empty())
7752 // Field from scaler subquery
7753 extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL));
7754 else
7755 // Field from song table
7756 extFilter.AppendField(JSONtoDBSong[i].fieldDB);
7759 else if (foundJSON)
7760 { // Field from join found in JSON request
7761 if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_"))
7763 joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true);
7765 else
7766 { // "contributors", "displaycomposer" etc.
7767 rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON);
7771 // Append calculated artist/title sort fields that may have been added to filter
7772 // Field used only for ORDER BY, not output to JSON
7773 extFilter.AppendField(calcsortfieldsSQL);
7774 for (int i = 0; i < iAddedFields; i++)
7775 dbfieldindex.emplace_back(-1); // columns in dataset
7777 // Build matching list of role id for "displaycomposer", "displayconductor",
7778 // "displayorchestra", "displaylyricist"
7779 if (!rolefieldlist.empty())
7781 for (const auto& name : rolefieldlist)
7783 int idRole = -1;
7784 if (StringUtils::StartsWith(name, "display"))
7785 idRole = GetRoleByName(name.substr(7));
7786 roleidlist.emplace_back(idRole);
7790 // JOIN album and path tables needed for field output and/or in sort
7791 // if not already there for filter
7792 if ((extFilter.fields.find("album.") != std::string::npos ||
7793 extFilter.fields.find("strAlbum") != std::string::npos) &&
7794 extFilter.join.find("JOIN album") == std::string::npos)
7795 { // All songs have one album so inner join sufficient
7796 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7798 if (extFilter.fields.find("path.") != std::string::npos &&
7799 extFilter.join.find("JOIN path") == std::string::npos)
7800 { // All songs have one path so inner join sufficient
7801 extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7804 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7805 strSQLExtra = "";
7806 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7807 return false;
7809 // Add any LIMIT clause to strSQLExtra
7810 if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
7812 strSQLExtra +=
7813 DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
7814 resultcount = std::min(
7815 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
7816 resultcount);
7819 // Setup multivalue JOINs, GROUP BY and ORDER BY
7820 bool bJoinSongArtist(false);
7821 bool bJoinAlbumArtist(false);
7822 bool bJoinRole(false);
7823 if (sortDescription.sortBy != SortByRandom)
7825 // Repeat inline view order (that always includes idSong) on join query
7826 std::string order = extFilter.order;
7827 order = extFilter.order;
7828 StringUtils::Replace(order, "album.", "sv.");
7829 StringUtils::Replace(order, "song.", "sv.");
7830 joinFilter.AppendOrder(order);
7832 else
7833 joinFilter.AppendOrder("sv.idSong");
7834 joinFilter.AppendGroup("sv.idSong");
7836 // Album artists
7837 if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) ||
7838 joinLayout.GetFetch(joinToSongs_strAlbumArtist) ||
7839 joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID))
7840 { // All songs have at least one album artist so inner join sufficient
7841 bJoinAlbumArtist = true;
7842 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
7843 joinFilter.AppendGroup("album_artist.idArtist");
7844 joinFilter.AppendOrder("album_artist.iOrder");
7845 // Ensure idAlbumArtist is queried for processing repeats
7846 if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist))
7848 joinLayout.SetField(joinToSongs_idAlbumArtist,
7849 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL);
7851 // Ensure song.IdAlbum is field of the inline view for join
7852 if (fields.find("albumid") == fields.end())
7854 extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
7855 dbfieldindex.emplace_back(-1);
7857 // artist table needed for strArtist or MBID
7858 // (album_artist.strArtist can be an alias or spelling variation)
7859 if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) ||
7860 joinLayout.GetFetch(joinToSongs_strAlbumArtist))
7861 joinFilter.AppendJoin(
7862 "JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
7866 Song artists
7867 JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
7868 "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
7870 if (joinLayout.GetFetch(joinToSongs_idArtist) || joinLayout.GetFetch(joinToSongs_strArtist) ||
7871 joinLayout.GetFetch(joinToSongs_strArtistMBID) || !rolefieldlist.empty())
7872 { // All songs have at least one artist (idRole = 1) so inner join sufficient
7873 bJoinSongArtist = true;
7874 if (rolefieldlist.empty())
7875 { // song artists only, no other roles needed
7876 joinFilter.AppendJoin(
7877 "JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
7878 joinFilter.AppendGroup("song_artist.idArtist");
7879 joinFilter.AppendOrder("song_artist.iOrder");
7881 else
7883 // Ensure idRole is queried
7884 if (!joinLayout.GetFetch(joinToSongs_idRole))
7886 joinLayout.SetField(joinToSongs_idRole,
7887 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL);
7889 // Ensure strArtist is queried
7890 if (!joinLayout.GetFetch(joinToSongs_strArtist))
7892 joinLayout.SetField(joinToSongs_strArtist,
7893 JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL);
7895 if (fields.find("contributors") != fields.end())
7896 { // all roles
7897 bJoinRole = true;
7898 // Ensure strRole is queried from role table
7899 joinLayout.SetField(joinToSongs_strRole, "role.strRole");
7900 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
7901 joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
7902 joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
7903 joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
7905 else
7906 { // Get just roles for "displaycomposer", "displayconductor" etc.
7907 std::string where;
7908 for (size_t i = 0; i < roleidlist.size(); i++)
7910 int idRole = roleidlist[i];
7911 if (idRole <= 1)
7912 continue;
7913 if (where.empty())
7914 // Always get song artists too (role = 1) so can do inner join
7915 where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole);
7916 else
7917 where += PrepareSQL(" OR song_artist.idRole = %i", idRole);
7919 where = " (" + where + ")";
7920 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where);
7921 joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
7922 joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
7925 // Ensure idArtist is queried for processing repeats
7926 if (!joinLayout.GetFetch(joinToSongs_idArtist))
7928 joinLayout.SetField(joinToSongs_idArtist,
7929 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL);
7931 // artist table needed for strArtist or MBID
7932 // (song_artist.strArtist can be an alias or spelling variation)
7933 if (joinLayout.GetFetch(joinToSongs_strArtistMBID) ||
7934 joinLayout.GetFetch(joinToSongs_strArtist))
7935 joinFilter.AppendJoin(
7936 "JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
7939 // Genre ids
7940 if (joinLayout.GetFetch(joinToSongs_idGenre))
7941 { // song genre ids (strGenre demormalised in song table)
7942 // Left join as songs may not have genre
7943 joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
7944 joinFilter.AppendGroup("song_genre.idGenre");
7945 joinFilter.AppendOrder("song_genre.iOrder");
7948 // Build JOIN part of query (if we have one)
7949 std::string strSQLJoin;
7950 if (joinLayout.HasFilterFields())
7951 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
7952 return false;
7954 // Adjust where in the results record the join fields are allowing for the
7955 // inline view fields (Quicker than finding field by name every time)
7956 // idSong + other song fields
7957 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
7959 // Build full query
7960 // When have multiple value joins use inline view
7961 // SELECT sv.*, <join fields> FROM
7962 // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv
7963 // <joins> <group by>
7964 // <order by> + <joins order by>
7965 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
7966 strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra;
7967 if (joinLayout.HasFilterFields())
7969 strSQL = "(" + strSQL + ") AS sv ";
7970 strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
7973 // Modify query to use correct year field
7974 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
7975 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
7976 StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate");
7977 else
7978 StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate");
7980 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
7982 // Run query
7983 auto start = std::chrono::steady_clock::now();
7985 if (!m_pDS->query(strSQL))
7986 return false;
7988 auto end = std::chrono::steady_clock::now();
7989 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
7991 CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
7993 int iRowsFound = m_pDS->num_rows();
7994 if (iRowsFound <= 0)
7996 m_pDS->close();
7997 return true;
8000 // Get song from returned rows. Joins mean there can be many rows per song
8001 int songId = -1;
8002 int albumartistId = -1;
8003 int artistId = -1;
8004 int roleId = -1;
8005 bool bSongGenreDone(false);
8006 bool bSongArtistDone(false);
8007 bool bHaveSong(false);
8008 CVariant songObj;
8009 result["songs"].reserve(resultcount);
8010 while (!m_pDS->eof() || bHaveSong)
8012 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
8014 if (m_pDS->eof() || songId != record->at(0).get_asInt())
8016 // Store previous or last song
8017 if (bHaveSong)
8019 // Check empty role fields get returned, and format
8020 if (!rolefieldlist.empty())
8022 for (const auto& displayXXX : rolefieldlist)
8024 if (!StringUtils::StartsWith(displayXXX, "display"))
8026 // "contributors"
8027 if (!songObj.isMember(displayXXX))
8028 songObj[displayXXX] = CVariant(CVariant::VariantTypeArray);
8030 else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray())
8032 // Convert "displaycomposer", "displayconductor", "displayorchestra",
8033 // and "displaylyricist" arrays into strings
8034 std::vector<std::string> names;
8035 for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array();
8036 field != songObj[displayXXX].end_array(); ++field)
8037 names.emplace_back(field->asString());
8039 std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent()
8040 ->GetAdvancedSettings()
8041 ->m_musicItemSeparator);
8042 songObj[displayXXX] = role;
8044 else
8045 songObj[displayXXX] = "";
8048 result["songs"].append(songObj);
8049 bHaveSong = false;
8050 songObj.clear();
8052 if (songObj.empty())
8054 // Initialise fields, ensure those with possible null values are set to correct empty variant type
8055 if (joinLayout.GetOutput(joinToSongs_idGenre))
8056 songObj["genreid"] =
8057 CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array
8059 albumartistId = -1;
8060 artistId = -1;
8061 roleId = -1;
8062 bSongGenreDone = false;
8063 bSongArtistDone = false;
8065 if (m_pDS->eof())
8066 continue; // Having saved the last song stop
8068 // New song
8069 songId = record->at(0).get_asInt();
8070 bHaveSong = true;
8071 songObj["songid"] = songId;
8072 songObj["label"] = record->at(1).get_asString();
8073 for (size_t i = 0; i < dbfieldindex.size(); i++)
8074 if (dbfieldindex[i] > -1)
8076 if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer")
8077 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
8078 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned")
8079 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
8080 std::max(record->at(1 + i).get_asInt(), 0);
8081 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float")
8082 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
8083 std::max(record->at(1 + i).get_asFloat(), 0.f);
8084 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array")
8085 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
8086 record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
8087 ->GetAdvancedSettings()
8088 ->m_musicItemSeparator);
8089 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean")
8090 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
8091 else
8092 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
8095 // Split sources string into int array
8096 if (songObj.isMember("sourceid"))
8098 std::vector<std::string> sources =
8099 StringUtils::Split(songObj["sourceid"].asString(), ";");
8100 songObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
8101 for (size_t i = 0; i < sources.size(); i++)
8102 songObj["sourceid"].append(atoi(sources[i].c_str()));
8106 if (bJoinAlbumArtist)
8108 if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt())
8110 bSongGenreDone =
8111 bSongGenreDone || (albumartistId > 0); // Not first album artist, skip genre
8112 bSongArtistDone =
8113 bSongArtistDone || (albumartistId > 0); // Not first album artist, skip song artists
8114 albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt();
8115 if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
8116 songObj["albumartistid"].append(albumartistId);
8117 if (albumartistId == BLANKARTIST_ID)
8119 if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
8120 songObj["albumartist"].append(StringUtils::Empty);
8121 if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
8122 songObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
8124 else
8126 if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
8127 songObj["albumartistid"].append(albumartistId);
8128 if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
8129 songObj["albumartist"].append(
8130 record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString());
8131 if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
8132 songObj["musicbrainzalbumartistid"].append(
8133 record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString());
8137 if (bJoinSongArtist && !bSongArtistDone)
8139 if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt())
8141 bSongGenreDone = bSongGenreDone || (artistId > 0); // Not first artist, skip genre
8142 roleId = -1; // Allow for many artists same role
8143 artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
8144 if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 ||
8145 record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1)
8147 if (joinLayout.GetOutput(joinToSongs_idArtist))
8148 songObj["artistid"].append(artistId);
8149 if (artistId == BLANKARTIST_ID)
8151 if (joinLayout.GetOutput(joinToSongs_strArtist))
8152 songObj["artist"].append(StringUtils::Empty);
8153 if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
8154 songObj["musicbrainzartistid"].append(StringUtils::Empty);
8156 else
8158 if (joinLayout.GetOutput(joinToSongs_strArtist))
8159 songObj["artist"].append(
8160 record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
8161 if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
8162 songObj["musicbrainzartistid"].append(
8163 record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString());
8167 if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 &&
8168 roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt())
8170 bSongGenreDone = bSongGenreDone || (roleId > 0); // Not first role, skip genre
8171 roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt();
8172 if (roleId > 1)
8174 if (bJoinRole)
8175 { //Contributors
8176 CVariant contributor;
8177 contributor["name"] =
8178 record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString();
8179 contributor["role"] =
8180 record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString();
8181 contributor["roleid"] = roleId;
8182 contributor["artistid"] =
8183 record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
8184 songObj["contributors"].append(contributor);
8186 // "displaycomposer", "displayconductor" etc.
8187 for (size_t i = 0; i < roleidlist.size(); i++)
8189 if (roleidlist[i] == roleId)
8191 songObj[rolefieldlist[i]].append(
8192 record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
8193 continue;
8199 if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 &&
8200 !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull())
8202 songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt());
8204 m_pDS->next();
8206 m_pDS->close(); // cleanup recordset data
8208 // Ensure random order of output when results set is sorted to process multi-value joins
8209 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
8210 KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array());
8212 return true;
8214 catch (...)
8216 m_pDS->close();
8217 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
8219 return false;
8222 std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField)
8225 Make SQL clause from ignore article list.
8226 Group tokens the same length together, for example :
8227 WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
8228 THEN SUBSTR(strArtist, 5)
8229 WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
8230 THEN SUBSTR(strArtist, 4)
8232 std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
8233 std::string sortclause;
8234 size_t tokenlength = 0;
8235 std::string strWhen;
8236 for (const auto& token : sortTokens)
8238 if (token.length() != tokenlength)
8240 if (!strWhen.empty())
8242 if (!sortclause.empty())
8243 sortclause += " ";
8244 std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
8245 sortclause += "WHEN " + strWhen + strThen;
8246 strWhen.clear();
8248 tokenlength = token.length();
8250 std::string tokenclause = token;
8251 //Escape any ' or % in the token
8252 StringUtils::Replace(tokenclause, "'", "''");
8253 StringUtils::Replace(tokenclause, "%", "%%");
8254 // Single %, _ and ' so avoid using PrepareSQL
8255 tokenclause = strField + " LIKE '" + tokenclause + "%'";
8256 if (token.find('_') != std::string::npos)
8257 tokenclause += " ESCAPE '_'";
8258 if (!strWhen.empty())
8259 strWhen += " OR ";
8260 strWhen += tokenclause;
8262 if (!strWhen.empty())
8264 if (!sortclause.empty())
8265 sortclause += " ";
8266 std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
8267 sortclause += "WHEN " + strWhen + strThen;
8269 return sortclause;
8272 std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias,
8273 const SortAttribute& sortAttributes,
8274 const std::string& strField,
8275 const std::string& strSortField)
8278 Build SQL for sort name scalar subquery from sort attributes and ignore article list.
8279 For example :
8280 CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
8281 WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
8282 WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
8283 ELSE strField
8284 END AS strAlias
8287 std::string sortSQL;
8288 if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName)
8289 sortSQL =
8290 PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str());
8291 if (sortAttributes & SortAttributeIgnoreArticle)
8293 if (!sortSQL.empty())
8294 sortSQL += " ";
8295 // Make SQL from ignore article list, grouping tokens the same length together
8296 sortSQL += GetIgnoreArticleSQL(strField);
8298 if (!sortSQL.empty())
8300 sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc.
8301 sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str());
8304 return sortSQL;
8307 std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField,
8308 const SortOrder& sortOrder)
8311 Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
8312 and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
8313 Natural number order is not significant for where clause comparison and use of calculated fields
8314 means there is no advantage in defining as column default in table create than per query (which
8315 also makes looking at the db with other tools difficult).
8317 MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
8318 "ascii folding" case insensitive collation. Natural sorting is provided via native functions
8319 stored in the db.
8321 std::string DESC;
8322 if (sortOrder == SortOrderDescending)
8323 DESC = " DESC";
8324 std::string strSort;
8326 if (StringUtils::EqualsNoCase(
8327 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
8328 "mysql"))
8329 strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str());
8330 else
8331 strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str());
8332 return strSort;
8335 void CMusicDatabase::UpdateTables(int version)
8337 CLog::Log(LOGINFO, "{} - updating tables", __FUNCTION__);
8338 if (version < 34)
8340 m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
8341 m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
8342 m_pDS->exec(
8343 "CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, "
8344 "strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration "
8345 "integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, "
8346 "iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, "
8347 "lastplayed varchar(20) default NULL, rating char default '0', comment text)\n");
8348 m_pDS->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, "
8349 "iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, "
8350 "iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) "
8351 "SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, "
8352 "dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, "
8353 "iEndOffset, idThumb, lastplayed, rating, comment FROM song");
8355 m_pDS->exec("DROP TABLE song");
8356 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8358 m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
8361 if (version < 36)
8363 // translate legacy musicdb:// paths
8364 if (m_pDS->query("SELECT strPath FROM content"))
8366 std::vector<std::string> contentPaths;
8367 while (!m_pDS->eof())
8369 contentPaths.push_back(m_pDS->fv(0).get_asString());
8370 m_pDS->next();
8372 m_pDS->close();
8374 for (const auto& originalPath : contentPaths)
8376 std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath);
8377 m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(),
8378 originalPath.c_str()));
8383 if (version < 39)
8385 m_pDS->exec("CREATE TABLE album_new "
8386 "(idAlbum integer primary key, "
8387 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8388 " strArtists text, strGenres text, "
8389 " iYear integer, idThumb integer, "
8390 " bCompilation integer not null default '0', "
8391 " strMoods text, strStyles text, strThemes text, "
8392 " strReview text, strImage text, strLabel text, "
8393 " strType text, "
8394 " iRating integer, "
8395 " lastScraped varchar(20) default NULL, "
8396 " dateAdded varchar (20) default NULL)");
8397 m_pDS->exec("INSERT INTO album_new "
8398 "(idAlbum, "
8399 " strAlbum, strMusicBrainzAlbumID, "
8400 " strArtists, strGenres, "
8401 " iYear, idThumb, "
8402 " bCompilation, "
8403 " strMoods, strStyles, strThemes, "
8404 " strReview, strImage, strLabel, "
8405 " strType, "
8406 " iRating) "
8407 " SELECT "
8408 " album.idAlbum, "
8409 " strAlbum, strMusicBrainzAlbumID, "
8410 " strArtists, strGenres, "
8411 " album.iYear, idThumb, "
8412 " bCompilation, "
8413 " strMoods, strStyles, strThemes, "
8414 " strReview, strImage, strLabel, "
8415 " strType, iRating "
8416 " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
8417 m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE "
8418 "albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
8419 m_pDS->exec(PrepareSQL(
8420 "UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)",
8421 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
8422 m_pDS->exec("DROP TABLE album");
8423 m_pDS->exec("DROP TABLE albuminfo");
8424 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8426 if (version < 40)
8428 m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
8429 " strArtist varchar(256), strMusicBrainzArtistID text, "
8430 " strBorn text, strFormed text, strGenres text, strMoods text, "
8431 " strStyles text, strInstruments text, strBiography text, "
8432 " strDied text, strDisbanded text, strYearsActive text, "
8433 " strImage text, strFanart text, "
8434 " lastScraped varchar(20) default NULL, "
8435 " dateAdded varchar (20) default NULL)");
8436 m_pDS->exec("INSERT INTO artist_new "
8437 "(idArtist, strArtist, strMusicBrainzArtistID, "
8438 " strBorn, strFormed, strGenres, strMoods, "
8439 " strStyles , strInstruments , strBiography , "
8440 " strDied, strDisbanded, strYearsActive, "
8441 " strImage, strFanart) "
8442 " SELECT "
8443 " artist.idArtist, "
8444 " strArtist, strMusicBrainzArtistID, "
8445 " strBorn, strFormed, strGenres, strMoods, "
8446 " strStyles, strInstruments, strBiography, "
8447 " strDied, strDisbanded, strYearsActive, "
8448 " strImage, strFanart "
8449 " FROM artist "
8450 " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
8451 m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT "
8452 "idArtist FROM artistinfo)",
8453 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
8454 m_pDS->exec("DROP TABLE artist");
8455 m_pDS->exec("DROP TABLE artistinfo");
8456 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8458 if (version < 42)
8460 m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n");
8461 m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n");
8462 // populate these
8463 std::string sql = "select idArtist,strArtist from artist";
8464 m_pDS->query(sql);
8465 while (!m_pDS->eof())
8467 m_pDS2->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i",
8468 m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
8469 m_pDS2->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i",
8470 m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
8471 m_pDS->next();
8474 if (version < 48)
8475 { // null out columns that are no longer used
8476 m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
8477 m_pDS->exec("UPDATE album SET idThumb=NULL");
8479 if (version < 49)
8481 m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
8483 if (version < 50)
8485 // add a new column strReleaseType for albums
8486 m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n");
8488 // set strReleaseType based on album name
8489 m_pDS->exec(PrepareSQL(
8490 "UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''",
8491 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
8492 m_pDS->exec(
8493 PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''",
8494 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
8496 if (version < 51)
8498 m_pDS->exec("ALTER TABLE song ADD mood text\n");
8500 if (version < 53)
8502 m_pDS->exec("ALTER TABLE song ADD dateAdded text");
8504 if (version < 54)
8506 //Remove dateAdded from artist table
8507 m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
8508 " strArtist varchar(256), strMusicBrainzArtistID text, "
8509 " strBorn text, strFormed text, strGenres text, strMoods text, "
8510 " strStyles text, strInstruments text, strBiography text, "
8511 " strDied text, strDisbanded text, strYearsActive text, "
8512 " strImage text, strFanart text, "
8513 " lastScraped varchar(20) default NULL)");
8514 m_pDS->exec("INSERT INTO artist_new "
8515 "(idArtist, strArtist, strMusicBrainzArtistID, "
8516 " strBorn, strFormed, strGenres, strMoods, "
8517 " strStyles , strInstruments , strBiography , "
8518 " strDied, strDisbanded, strYearsActive, "
8519 " strImage, strFanart, lastScraped) "
8520 " SELECT "
8521 " idArtist, "
8522 " strArtist, strMusicBrainzArtistID, "
8523 " strBorn, strFormed, strGenres, strMoods, "
8524 " strStyles, strInstruments, strBiography, "
8525 " strDied, strDisbanded, strYearsActive, "
8526 " strImage, strFanart, lastScraped "
8527 " FROM artist");
8528 m_pDS->exec("DROP TABLE artist");
8529 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8531 //Remove dateAdded from album table
8532 m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8533 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8534 " strArtists text, strGenres text, "
8535 " iYear integer, idThumb integer, "
8536 " bCompilation integer not null default '0', "
8537 " strMoods text, strStyles text, strThemes text, "
8538 " strReview text, strImage text, strLabel text, "
8539 " strType text, "
8540 " iRating integer, "
8541 " lastScraped varchar(20) default NULL, "
8542 " strReleaseType text)");
8543 m_pDS->exec("INSERT INTO album_new "
8544 "(idAlbum, "
8545 " strAlbum, strMusicBrainzAlbumID, "
8546 " strArtists, strGenres, "
8547 " iYear, idThumb, "
8548 " bCompilation, "
8549 " strMoods, strStyles, strThemes, "
8550 " strReview, strImage, strLabel, "
8551 " strType, iRating, lastScraped, "
8552 " strReleaseType) "
8553 " SELECT "
8554 " album.idAlbum, "
8555 " strAlbum, strMusicBrainzAlbumID, "
8556 " strArtists, strGenres, "
8557 " iYear, idThumb, "
8558 " bCompilation, "
8559 " strMoods, strStyles, strThemes, "
8560 " strReview, strImage, strLabel, "
8561 " strType, iRating, lastScraped, "
8562 " strReleaseType"
8563 " FROM album");
8564 m_pDS->exec("DROP TABLE album");
8565 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8567 if (version < 55)
8569 m_pDS->exec("DROP TABLE karaokedata");
8571 if (version < 57)
8573 m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
8574 m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
8575 m_pDS->exec("UPDATE song SET userrating = rating * 2");
8576 m_pDS->exec("UPDATE song SET rating = 0");
8577 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8578 " idAlbum INTEGER, idPath INTEGER, "
8579 " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8580 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8581 " dwFileNameCRC TEXT, "
8582 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8583 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8584 " idThumb INTEGER, "
8585 " lastplayed VARCHAR(20) DEFAULT NULL, "
8586 " rating FLOAT DEFAULT 0, "
8587 " userrating INTEGER DEFAULT 0, "
8588 " comment TEXT, mood TEXT, dateAdded TEXT)");
8589 m_pDS->exec("INSERT INTO song_new "
8590 "(idSong, "
8591 " idAlbum, idPath, "
8592 " strArtists, strGenres, strTitle, "
8593 " iTrack, iDuration, iYear, "
8594 " dwFileNameCRC, "
8595 " strFileName, strMusicBrainzTrackID, "
8596 " iTimesPlayed, iStartOffset, iEndOffset, "
8597 " idThumb, "
8598 " lastplayed,"
8599 " rating, userrating, "
8600 " comment, mood, dateAdded)"
8601 " SELECT "
8602 " idSong, "
8603 " idAlbum, idPath, "
8604 " strArtists, strGenres, strTitle, "
8605 " iTrack, iDuration, iYear, "
8606 " dwFileNameCRC, "
8607 " strFileName, strMusicBrainzTrackID, "
8608 " iTimesPlayed, iStartOffset, iEndOffset, "
8609 " idThumb, "
8610 " lastplayed,"
8611 " rating, "
8612 " userrating, "
8613 " comment, mood, dateAdded"
8614 " FROM song");
8615 m_pDS->exec("DROP TABLE song");
8616 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8618 m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
8619 m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
8620 m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8621 " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8622 " strArtists TEXT, strGenres TEXT, "
8623 " iYear INTEGER, idThumb INTEGER, "
8624 " bCompilation INTEGER NOT NULL DEFAULT '0', "
8625 " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8626 " strReview TEXT, strImage TEXT, strLabel TEXT, "
8627 " strType TEXT, "
8628 " fRating FLOAT NOT NULL DEFAULT 0, "
8629 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8630 " lastScraped VARCHAR(20) DEFAULT NULL, "
8631 " strReleaseType TEXT)");
8632 m_pDS->exec("INSERT INTO album_new "
8633 "(idAlbum, "
8634 " strAlbum, strMusicBrainzAlbumID, "
8635 " strArtists, strGenres, "
8636 " iYear, idThumb, "
8637 " bCompilation, "
8638 " strMoods, strStyles, strThemes, "
8639 " strReview, strImage, strLabel, "
8640 " strType, "
8641 " fRating, "
8642 " iUserrating, "
8643 " lastScraped, "
8644 " strReleaseType)"
8645 " SELECT "
8646 " idAlbum, "
8647 " strAlbum, strMusicBrainzAlbumID, "
8648 " strArtists, strGenres, "
8649 " iYear, idThumb, "
8650 " bCompilation, "
8651 " strMoods, strStyles, strThemes, "
8652 " strReview, strImage, strLabel, "
8653 " strType, "
8654 " iRating, "
8655 " iUserrating, "
8656 " lastScraped, "
8657 " strReleaseType"
8658 " FROM album");
8659 m_pDS->exec("DROP TABLE album");
8660 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8662 m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
8663 m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
8665 if (version < 58)
8667 m_pDS->exec("UPDATE album SET fRating = fRating * 2");
8669 if (version < 59)
8671 m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
8672 m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
8674 //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
8675 m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, "
8676 "iOrder integer, strArtist text)");
8677 m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
8678 "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
8679 m_pDS->exec("DROP TABLE song_artist");
8680 m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
8682 //Remove strJoinPhrase, boolFeatured from album_artist table
8683 m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, "
8684 "strArtist text)");
8685 m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
8686 "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
8687 m_pDS->exec("DROP TABLE album_artist");
8688 m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
8690 if (version < 60)
8692 // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
8693 // do not have an artist tag to ensure all songs in the library have at least one artist.
8694 std::string strSQL;
8695 if (GetArtistExists(BLANKARTIST_ID))
8697 // When BLANKARTIST_ID (=1) is already in use, move the record
8699 { //No mbid index yet, so can have record for artist twice even with mbid
8700 strSQL = PrepareSQL("INSERT INTO artist SELECT null, "
8701 "strArtist, strMusicBrainzArtistID, "
8702 "strBorn, strFormed, strGenres, strMoods, "
8703 "strStyles, strInstruments, strBiography, "
8704 "strDied, strDisbanded, strYearsActive, "
8705 "strImage, strFanart, lastScraped "
8706 "FROM artist WHERE artist.idArtist = %i",
8707 BLANKARTIST_ID);
8708 m_pDS->exec(strSQL);
8709 int idArtist = (int)m_pDS->lastinsertid();
8710 //No triggers, so can delete artist without effecting other tables.
8711 strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
8712 m_pDS->exec(strSQL);
8714 // Update related tables with the new artist ID
8715 // Indices have been dropped making transactions very slow, so create appropriate temp indices
8716 m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
8717 m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
8718 m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
8719 m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
8720 strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
8721 BLANKARTIST_ID);
8722 m_pDS->exec(strSQL);
8723 strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
8724 BLANKARTIST_ID);
8725 m_pDS->exec(strSQL);
8726 strSQL =
8727 PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'",
8728 idArtist, BLANKARTIST_ID);
8729 m_pDS->exec(strSQL);
8730 strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist,
8731 BLANKARTIST_ID);
8732 m_pDS->exec(strSQL);
8733 // Drop temp indices
8734 m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist");
8735 m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
8736 m_pDS->exec("DROP INDEX idxDiscography ON discography");
8737 m_pDS->exec("DROP INDEX ix_art ON art");
8739 catch (...)
8741 CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed");
8745 // Create missing artist tag artist [Missing].
8746 // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
8747 strSQL = PrepareSQL(
8748 "INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
8749 BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
8750 m_pDS->exec(strSQL);
8752 // Indices have been dropped making transactions very slow, so create temp index
8753 m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
8754 m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
8756 // Ensure all songs have at least one artist, set those without to [Missing]
8757 strSQL = "SELECT count(idSong) FROM song "
8758 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8759 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
8760 int numsongs = GetSingleValueInt(strSQL);
8761 if (numsongs > 0)
8763 CLog::Log(LOGDEBUG, "{} songs have no artist, setting artist to [Missing]", numsongs);
8764 // Insert song_artist records for songs that don't have any
8767 strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
8768 "SELECT %i, idSong, %i, '%s', 0 FROM song "
8769 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8770 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
8771 BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST);
8772 ExecuteQuery(strSQL);
8774 catch (...)
8776 CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed");
8780 // Ensure all albums have at least one artist, set those without to [Missing]
8781 strSQL = "SELECT count(idAlbum) FROM album "
8782 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8783 "WHERE album_artist.idAlbum = album.idAlbum)";
8784 int numalbums = GetSingleValueInt(strSQL);
8785 if (numalbums > 0)
8787 CLog::Log(LOGDEBUG, "{} albums have no artist, setting artist to [Missing]", numalbums);
8788 // Insert album_artist records for albums that don't have any
8791 strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
8792 "SELECT %i, idAlbum, '%s', 0 FROM album "
8793 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8794 "WHERE album_artist.idAlbum = album.idAlbum)",
8795 BLANKARTIST_ID, BLANKARTIST_NAME.c_str());
8796 ExecuteQuery(strSQL);
8798 catch (...)
8800 CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed");
8803 //Remove temp indices, full analytics for database created later
8804 m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist");
8805 m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
8807 if (version < 61)
8809 // Create versiontagscan table
8810 m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
8811 m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
8813 if (version < 62)
8815 CLog::Log(LOGINFO, "create audiobook table");
8816 m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
8817 " strBook varchar(256), strAuthor text,"
8818 " bookmark integer, file text,"
8819 " dateAdded varchar (20) default NULL)");
8821 if (version < 63)
8823 // Add strSortName to Artist table
8824 m_pDS->exec("ALTER TABLE artist ADD strSortName text\n");
8826 //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
8827 m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8828 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8829 " strArtistDisp text, strArtistSort text, strGenres text, "
8830 " iYear integer, bCompilation integer not null default '0', "
8831 " strMoods text, strStyles text, strThemes text, "
8832 " strReview text, strImage text, strLabel text, "
8833 " strType text, "
8834 " fRating FLOAT NOT NULL DEFAULT 0, "
8835 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8836 " lastScraped varchar(20) default NULL, "
8837 " strReleaseType text, "
8838 " iVotes INTEGER NOT NULL DEFAULT 0)");
8839 m_pDS->exec("INSERT INTO album_new "
8840 "(idAlbum, "
8841 " strAlbum, strMusicBrainzAlbumID, "
8842 " strArtistDisp, strArtistSort, strGenres, "
8843 " iYear, bCompilation, "
8844 " strMoods, strStyles, strThemes, "
8845 " strReview, strImage, strLabel, "
8846 " strType, "
8847 " fRating, iUserrating, iVotes, "
8848 " lastScraped, "
8849 " strReleaseType)"
8850 " SELECT "
8851 " idAlbum, "
8852 " strAlbum, strMusicBrainzAlbumID, "
8853 " strArtists, NULL, strGenres, "
8854 " iYear, bCompilation, "
8855 " strMoods, strStyles, strThemes, "
8856 " strReview, strImage, strLabel, "
8857 " strType, "
8858 " fRating, iUserrating, iVotes, "
8859 " lastScraped, "
8860 " strReleaseType"
8861 " FROM album");
8862 m_pDS->exec("DROP TABLE album");
8863 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8865 //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
8866 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8867 " idAlbum INTEGER, idPath INTEGER, "
8868 " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8869 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8870 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8871 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8872 " lastplayed VARCHAR(20) DEFAULT NULL, "
8873 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
8874 " userrating INTEGER NOT NULL DEFAULT 0, "
8875 " comment TEXT, mood TEXT, dateAdded TEXT)");
8876 m_pDS->exec("INSERT INTO song_new "
8877 "(idSong, "
8878 " idAlbum, idPath, "
8879 " strArtistDisp, strArtistSort, strGenres, strTitle, "
8880 " iTrack, iDuration, iYear, "
8881 " strFileName, strMusicBrainzTrackID, "
8882 " iTimesPlayed, iStartOffset, iEndOffset, "
8883 " lastplayed,"
8884 " rating, userrating, votes, "
8885 " comment, mood, dateAdded)"
8886 " SELECT "
8887 " idSong, "
8888 " idAlbum, idPath, "
8889 " strArtists, NULL, strGenres, strTitle, "
8890 " iTrack, iDuration, iYear, "
8891 " strFileName, strMusicBrainzTrackID, "
8892 " iTimesPlayed, iStartOffset, iEndOffset, "
8893 " lastplayed,"
8894 " rating, userrating, votes, "
8895 " comment, mood, dateAdded"
8896 " FROM song");
8897 m_pDS->exec("DROP TABLE song");
8898 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8900 if (version < 65)
8902 // Remove cue table
8903 m_pDS->exec("DROP TABLE cue");
8904 // Add strReplayGain to song table
8905 m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
8907 if (version < 66)
8909 // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
8910 m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
8911 m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
8912 // Add a new column bScrapedMBID for artists
8913 m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
8915 if (version < 67)
8917 // Add infosetting table
8918 m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, "
8919 "strSettings TEXT)");
8920 // Add a new column for setting to album and artist tables
8921 m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
8922 m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
8924 // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
8925 m_pDS->exec(
8926 "CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
8927 "strScraperPath text, strSettings text)");
8930 m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
8931 "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, "
8932 "strScraperPath, strSettings "
8933 "FROM content WHERE strContent = 'artists' AND strPath LIKE "
8934 "'musicdb://artists/_%/' ORDER BY idItem");
8936 catch (...)
8938 CLog::Log(LOGERROR,
8939 "Migrating specific artist scraper settings has failed, settings not transferred");
8943 m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
8944 "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, "
8945 "strScraperPath, strSettings "
8946 "FROM content WHERE strContent = 'albums' AND strPath LIKE "
8947 "'musicdb://albums/_%/' ORDER BY idItem");
8949 catch (...)
8951 CLog::Log(LOGERROR,
8952 "Migrating specific album scraper settings has failed, settings not transferred");
8956 m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
8957 "SELECT id, strScraperPath, strSettings FROM content_temp");
8958 m_pDS->exec(
8959 "UPDATE artist SET idInfoSetting = "
8960 "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
8961 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = "
8962 "idArtist) ");
8963 m_pDS->exec("UPDATE album SET idInfoSetting = "
8964 "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
8965 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem "
8966 "= idAlbum) ");
8968 catch (...)
8970 CLog::Log(LOGERROR,
8971 "Migrating album and artist scraper settings has failed, settings not transferred");
8973 m_pDS->exec("DROP TABLE content_temp");
8975 // Remove content table
8976 m_pDS->exec("DROP TABLE content");
8977 // Remove albuminfosong table
8978 m_pDS->exec("DROP TABLE albuminfosong");
8980 if (version < 68)
8982 // Add a new columns strType, strGender, strDisambiguation for artists
8983 m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n");
8984 m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n");
8985 m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
8987 if (version < 69)
8989 // Remove album_genre table
8990 m_pDS->exec("DROP TABLE album_genre");
8992 if (version < 70)
8994 // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
8995 m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * "
8996 "40 / 3 \n");
8998 if (version < 71)
9000 // Add lastscanned to versiontagscan table
9001 m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
9002 CDateTime dateAdded = CDateTime::GetCurrentDateTime();
9003 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
9004 dateAdded.GetAsDBDateTime().c_str()));
9006 if (version < 72)
9008 // Create source table
9009 m_pDS->exec(
9010 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
9011 // Create source_path table
9012 m_pDS->exec(
9013 "CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
9014 // Create album_source table
9015 m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
9016 // Populate source and source_path tables from sources.xml
9017 // Filling album_source needs to be done after indexes are created or it is
9018 // very slow. It could be populated during CreateAnalytics but it is checked
9019 // and filled as part of scanning anyway so simply force full rescan.
9020 MigrateSources();
9022 if (version < 73)
9024 // add bBoxedSet to album table
9025 m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
9026 // add iDiscTotal to album table
9027 m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
9028 // populate iDiscTotal from the data already in the song table
9029 m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
9030 "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
9031 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
9032 // add strDiscSubtitles to song table
9033 m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
9035 if (version < 74)
9037 //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
9038 m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
9039 "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
9040 "strReleaseGroupMBID TEXT, "
9041 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
9042 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
9043 "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
9044 "bCompilation INTEGER NOT NULL DEFAULT '0', "
9045 "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
9046 "strReview TEXT, strImage TEXT, strLabel TEXT, "
9047 "strType TEXT, "
9048 "fRating FLOAT NOT NULL DEFAULT 0, "
9049 "iVotes INTEGER NOT NULL DEFAULT 0, "
9050 "iUserrating INTEGER NOT NULL DEFAULT 0, "
9051 "lastScraped VARCHAR(20) DEFAULT NULL, "
9052 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
9053 "strReleaseType TEXT, "
9054 "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
9055 "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
9056 // Prepare as MySQL has different CAST datatypes
9057 m_pDS->exec(
9058 PrepareSQL("INSERT INTO album_new "
9059 "(idalbum, strAlbum, "
9060 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
9061 "strArtistDisp, strArtistSort, strGenres, "
9062 "strReleaseDate, strOrigReleaseDate, "
9063 "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
9064 "strReview, strImage, strLabel, strType, "
9065 "fRating, iVotes, iUserrating, "
9066 "lastScraped, bScrapedMBID, strReleaseType, "
9067 "iDiscTotal, idInfoSetting) "
9068 "SELECT "
9069 "idAlbum, strAlbum, "
9070 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
9071 "strArtistDisp, strArtistSort, strGenres, "
9072 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9073 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9074 // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
9075 "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
9076 "bCompilation, strMoods, strStyles, strThemes, "
9077 "strReview, strImage, strLabel, strType, "
9078 "fRating, iVotes, iUserrating, "
9079 "lastScraped, bScrapedMBID, strReleaseType, "
9080 "iDiscTotal, idInfoSetting "
9081 "FROM album"));
9082 m_pDS->exec("DROP TABLE album");
9083 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
9085 //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
9086 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
9087 "idAlbum INTEGER, idPath INTEGER, "
9088 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
9089 "iTrack INTEGER, iDuration INTEGER, "
9090 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
9091 "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
9092 "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
9093 "lastplayed VARCHAR(20) DEFAULT NULL, "
9094 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
9095 "userrating INTEGER NOT NULL DEFAULT 0, "
9096 "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
9097 "dateAdded TEXT)");
9098 // Prepare as MySQL has different CAST datatypes
9099 m_pDS->exec(PrepareSQL("INSERT INTO song_new "
9100 "(idSong, "
9101 "idAlbum, idPath, "
9102 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9103 "iTrack, iDuration, "
9104 "strReleaseDate, strOrigReleaseDate, "
9105 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9106 "iTimesPlayed, iStartOffset, iEndOffset, "
9107 "lastplayed, "
9108 "rating, userrating, votes, "
9109 "comment, mood, strReplayGain, dateAdded) "
9110 "SELECT "
9111 "idSong, "
9112 "idAlbum, idPath, "
9113 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9114 "iTrack, iDuration, "
9115 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9116 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9117 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9118 "iTimesPlayed, iStartOffset, iEndOffset, "
9119 "lastplayed, "
9120 "rating, userrating, votes, "
9121 "comment, mood, strReplayGain, dateAdded "
9122 "FROM song"));
9123 m_pDS->exec("DROP TABLE song");
9124 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
9126 if (version < 75)
9128 m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
9129 m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
9130 m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
9132 if (version < 77)
9134 m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
9136 if (version < 78)
9138 std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime();
9140 // Add removed_link table
9141 m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
9142 // Add lastcleaned and artistlinksupdated to versiontagscan table
9143 m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
9144 m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
9145 m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
9146 // Adjust lastscanned if original local time value is after current UTC
9147 if (GetLibraryLastUpdated() > strUTCNow)
9148 SetLibraryLastUpdated();
9149 m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
9150 "genresupdated = lastscanned, "
9151 "artistlinksupdated = lastscanned");
9153 // Add dateNew, dateModified to song table
9154 m_pDS->exec("ALTER TABLE song ADD dateNew TEXT");
9155 m_pDS->exec("ALTER TABLE song ADD dateModified TEXT");
9156 // Set new to dateAdded and modified to lastplayed as estimates
9157 // Limit those local time values to now UTC, and modified is after new
9158 m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
9159 m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'", strUTCNow.c_str(),
9160 strUTCNow.c_str()));
9161 m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
9162 m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
9163 strUTCNow.c_str(), strUTCNow.c_str()));
9164 m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
9166 // Add dateAdded, dateNew, dateModified to album table
9167 m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT");
9168 m_pDS->exec("ALTER TABLE album ADD dateNew TEXT");
9169 m_pDS->exec("ALTER TABLE album ADD dateModified TEXT");
9170 // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
9171 // Limit modified value to now UTC and after new
9172 // Indices have been dropped making subquery very slow, so create temp index
9173 m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
9174 m_pDS->exec("UPDATE album SET dateAdded = "
9175 "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
9176 m_pDS->exec("UPDATE album SET dateNew = "
9177 "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
9178 m_pDS->exec("UPDATE album SET dateModified = dateNew");
9179 m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
9180 m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
9181 strUTCNow.c_str(), strUTCNow.c_str()));
9182 //Remove temp index, full analytics for database created later
9183 m_pDS->exec("DROP INDEX idxSong3 ON song");
9185 // Add dateAdded, dateNew, dateModified to artist table
9186 m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT");
9187 m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT");
9188 m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT");
9189 // dateAdded has NULL values until files rescanned by user
9190 // Set new and modified to now UTC as not worth complexity of estimating from song dates
9191 m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str()));
9192 m_pDS->exec("UPDATE artist SET dateModified = dateNew");
9194 if (version < 79)
9196 m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
9198 if (version < 80)
9200 m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
9201 // update duration for all current albums
9202 m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
9203 "WHERE song.idAlbum = album.idAlbum) "
9204 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
9206 if (version < 82)
9208 // Update artist table combining fanart URL data into strImage field
9209 // Clear empty URL data <fanart /> and <thumb />
9210 m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
9211 m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
9212 //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
9213 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
9214 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
9215 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
9216 "aspect=\"fanart\" preview')");
9217 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
9218 // Truncate the fanart when total URLs exceeds this
9219 bool bisMySQL = StringUtils::EqualsNoCase(
9220 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
9221 "mysql");
9222 if (bisMySQL)
9224 std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist "
9225 "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
9226 if (m_pDS->query(strSQL))
9228 while (!m_pDS->eof())
9230 int idArtist = m_pDS->fv("idArtist").get_asInt();
9231 std::string strFanart = m_pDS->fv("strFanart").get_asString();
9232 std::string strImage = m_pDS->fv("strImage").get_asString();
9233 size_t space = 65535;
9234 // Trim strImage to allow arbitrary half space for fanart
9235 if (!TrimImageURLs(strImage, space / 2))
9236 strImage.clear(); // </thumb> not found, empty field
9237 space = space - strImage.length();
9238 // Trim fanart to fit remaining space
9239 if (!TrimImageURLs(strFanart, space))
9240 strFanart.clear(); // </thumb> not found, empty field
9242 strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
9243 "WHERE idArtist = %i",
9244 strFanart.c_str(), strImage.c_str(), idArtist);
9245 m_pDS2->exec(strSQL); // Use other dataset to update while looping result set
9247 m_pDS->next();
9249 m_pDS->close();
9253 // Remove strFanart column from artist table
9254 m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
9255 "strArtist varchar(256), strMusicBrainzArtistID text, "
9256 "strSortName text, "
9257 "strType text, strGender text, strDisambiguation text, "
9258 "strBorn text, strFormed text, strGenres text, strMoods text, "
9259 "strStyles text, strInstruments text, strBiography text, "
9260 "strDied text, strDisbanded text, strYearsActive text, "
9261 "strImage text, "
9262 "lastScraped varchar(20) default NULL, "
9263 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
9264 "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
9265 "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
9266 // Concatenate fanart URLs into strImage field
9267 // Prepare SQL to convert CONCAT to || in SQLite
9268 m_pDS->exec(PrepareSQL("INSERT INTO artist_new "
9269 "(idArtist, strArtist, strMusicBrainzArtistID, "
9270 "strSortName, strType, strGender, strDisambiguation, "
9271 "strBorn, strFormed, strGenres, strMoods, "
9272 "strStyles , strInstruments , strBiography , "
9273 "strDied, strDisbanded, strYearsActive, "
9274 "strImage, "
9275 "lastScraped, bScrapedMBID, idInfoSetting, "
9276 "dateAdded, dateNew, dateModified) "
9277 "SELECT "
9278 "artist.idArtist, "
9279 "strArtist, strMusicBrainzArtistID, "
9280 "strSortName, strType, strGender, strDisambiguation, "
9281 "strBorn, strFormed, strGenres, strMoods, "
9282 "strStyles, strInstruments, strBiography, "
9283 "strDied, strDisbanded, strYearsActive, "
9284 "CONCAT(strImage, strFanart), "
9285 "lastScraped, bScrapedMBID, idInfoSetting, "
9286 "dateAdded, dateNew, dateModified "
9287 "FROM artist"));
9288 m_pDS->exec("DROP TABLE artist");
9289 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
9291 // Set the version of tag scanning required.
9292 // Not every schema change requires the tags to be rescanned, set to the highest schema version
9293 // that needs this. Forced rescanning (of music files that have not changed since they were
9294 // previously scanned) also accommodates any changes to the way tags are processed
9295 // e.g. read tags that were not processed by previous versions.
9296 // The original db version when the tags were scanned, and the minimal db version needed are
9297 // later used to determine if a forced rescan should be prompted
9299 // The last schema change needing forced rescanning was 73.
9300 // This is because Kodi can now read and process extra tags involved in the creation of box sets
9302 SetMusicNeedsTagScan(73);
9304 // After all updates, store the original db version.
9305 // This indicates the version of tag processing that was used to populate db
9306 SetMusicTagScanVersion(version);
9309 int CMusicDatabase::GetSchemaVersion() const
9311 return 82;
9314 int CMusicDatabase::GetMusicNeedsTagScan()
9318 if (nullptr == m_pDB)
9319 return -1;
9320 if (nullptr == m_pDS)
9321 return -1;
9323 std::string sql = "SELECT * FROM versiontagscan";
9324 if (!m_pDS->query(sql))
9325 return -1;
9327 if (m_pDS->num_rows() != 1)
9329 m_pDS->close();
9330 return -1;
9333 int idVersion = m_pDS->fv("idVersion").get_asInt();
9334 int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt();
9335 m_pDS->close();
9336 if (idVersion < iNeedsScan)
9337 return idVersion;
9338 else
9339 return 0;
9341 catch (...)
9343 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
9345 return -1;
9348 void CMusicDatabase::SetMusicNeedsTagScan(int version)
9350 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version));
9353 void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */)
9355 if (version == 0)
9356 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
9357 else
9358 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version));
9361 std::string CMusicDatabase::GetLibraryLastUpdated()
9363 return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
9366 void CMusicDatabase::SetLibraryLastUpdated()
9368 CDateTime dateUpdated = CDateTime::GetUTCDateTime();
9369 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
9370 dateUpdated.GetAsDBDateTime().c_str()));
9373 std::string CMusicDatabase::GetLibraryLastCleaned()
9375 return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
9378 void CMusicDatabase::SetLibraryLastCleaned()
9380 std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
9381 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'", strUpdated.c_str()));
9384 std::string CMusicDatabase::GetArtistLinksUpdated()
9386 return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
9389 void CMusicDatabase::SetArtistLinksUpdated()
9391 std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
9392 m_pDS->exec(
9393 PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'", strUpdated.c_str()));
9396 std::string CMusicDatabase::GetGenresLastAdded()
9398 return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
9401 std::string CMusicDatabase::GetSongsLastAdded()
9403 return GetSingleValue("SELECT MAX(dateNew) FROM song");
9406 std::string CMusicDatabase::GetAlbumsLastAdded()
9408 return GetSingleValue("SELECT MAX(dateNew) FROM album");
9411 std::string CMusicDatabase::GetArtistsLastAdded()
9413 return GetSingleValue("SELECT MAX(dateNew) FROM artist");
9416 std::string CMusicDatabase::GetSongsLastModified()
9418 return GetSingleValue("SELECT MAX(dateModified) FROM song");
9421 std::string CMusicDatabase::GetAlbumsLastModified()
9423 return GetSingleValue("SELECT MAX(dateModified) FROM album");
9426 std::string CMusicDatabase::GetArtistsLastModified()
9428 return GetSingleValue("SELECT MAX(dateModified) FROM artist");
9431 unsigned int CMusicDatabase::GetRandomSongIDs(const Filter& filter,
9432 std::vector<std::pair<int, int>>& songIDs)
9436 if (nullptr == m_pDB)
9437 return 0;
9438 if (nullptr == m_pDS)
9439 return 0;
9441 std::string strSQL = "SELECT idSong FROM songview ";
9442 if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
9443 return false;
9444 strSQL += PrepareSQL(" ORDER BY RANDOM()");
9446 if (!m_pDS->query(strSQL))
9447 return 0;
9448 songIDs.clear();
9449 if (m_pDS->num_rows() == 0)
9451 m_pDS->close();
9452 return 0;
9454 songIDs.reserve(m_pDS->num_rows());
9455 while (!m_pDS->eof())
9457 songIDs.push_back(std::make_pair<int, int>(1, m_pDS->fv(song_idSong).get_asInt()));
9458 m_pDS->next();
9459 } // cleanup
9460 m_pDS->close();
9461 return static_cast<unsigned int>(songIDs.size());
9463 catch (...)
9465 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
9467 return 0;
9470 int CMusicDatabase::GetSongsCount(const Filter& filter)
9474 if (nullptr == m_pDB)
9475 return 0;
9476 if (nullptr == m_pDS)
9477 return 0;
9479 std::string strSQL = "select count(idSong) as NumSongs from songview ";
9480 if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
9481 return false;
9483 if (!m_pDS->query(strSQL))
9484 return false;
9485 if (m_pDS->num_rows() == 0)
9487 m_pDS->close();
9488 return 0;
9491 int iNumSongs = m_pDS->fv("NumSongs").get_asInt();
9492 // cleanup
9493 m_pDS->close();
9494 return iNumSongs;
9496 catch (...)
9498 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
9500 return 0;
9503 bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath)
9505 basePath.clear();
9506 std::vector<std::pair<std::string, int>> paths;
9507 if (!GetAlbumPaths(idAlbum, paths))
9508 return false;
9510 for (const auto& pathpair : paths)
9512 if (basePath.empty())
9513 basePath = pathpair.first.c_str();
9514 else
9515 URIUtils::GetCommonPath(basePath, pathpair.first.c_str());
9517 return true;
9520 bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths)
9522 paths.clear();
9523 std::string strSQL;
9526 if (nullptr == m_pDB)
9527 return false;
9528 if (nullptr == m_pDS2)
9529 return false;
9531 // Get the unique paths of songs on the album, providing there are no songs from
9532 // other albums with the same path. This returns
9533 // a) <album> if is contains all the songs and no others, or
9534 // b) <album>/cd1, <album>/cd2 etc. for disc sets
9535 // but does *not* return any path when albums are mixed together. That could be because of
9536 // deliberate file organisation, or (more likely) because of a tagging error in album name
9537 // or Musicbrainzalbumid. Thus it avoids finding some generic music path.
9538 strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
9539 "JOIN path ON song.idPath = path.idPath "
9540 "WHERE song.idAlbum = %ld "
9541 "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
9542 "WHERE idPath = song.idPath) = 1",
9543 idAlbum);
9545 if (!m_pDS2->query(strSQL))
9546 return false;
9547 if (m_pDS2->num_rows() == 0)
9549 // Album does not have a unique path, files are mixed
9550 m_pDS2->close();
9551 return false;
9554 while (!m_pDS2->eof())
9556 paths.emplace_back(m_pDS2->fv("strPath").get_asString(),
9557 m_pDS2->fv("song.idPath").get_asInt());
9558 m_pDS2->next();
9560 // Cleanup recordset data
9561 m_pDS2->close();
9562 return true;
9564 catch (...)
9566 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
9569 return false;
9572 int CMusicDatabase::GetDiscnumberForPathID(int idPath)
9574 std::string strSQL;
9575 int result = -1;
9578 if (nullptr == m_pDB)
9579 return -1;
9580 if (nullptr == m_pDS2)
9581 return -1;
9583 strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
9584 "WHERE idPath = %i",
9585 idPath);
9587 if (!m_pDS2->query(strSQL))
9588 return -1;
9589 if (m_pDS2->num_rows() == 1)
9590 { // Songs with this path have a unique disc number
9591 result = m_pDS2->fv("discnum").get_asInt();
9593 // Cleanup recordset data
9594 m_pDS2->close();
9596 catch (...)
9598 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
9600 return result;
9603 // Get old "artist path" - where artist.nfo and art was located v17 and below.
9604 // It is the path common to all albums by an (album) artist, but ensure it is unique
9605 // to that artist and not shared with other artists. Previously this caused incorrect nfo
9606 // and art to be applied to multiple artists.
9607 bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string& basePath)
9609 basePath.clear();
9612 if (nullptr == m_pDB)
9613 return false;
9614 if (nullptr == m_pDS2)
9615 return false;
9617 // find all albums from this artist, and all the paths to the songs from those albums
9618 std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist "
9619 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9620 "JOIN path ON song.idPath = path.idPath "
9621 "WHERE album_artist.idArtist = %ld "
9622 "GROUP BY song.idPath",
9623 idArtist);
9625 // run query
9626 if (!m_pDS2->query(strSQL))
9627 return false;
9628 int iRowsFound = m_pDS2->num_rows();
9629 if (iRowsFound == 0)
9631 // Artist is not an album artist, no path to find
9632 m_pDS2->close();
9633 return false;
9635 else if (iRowsFound == 1)
9637 // Special case for single path - assume that we're in an artist/album/songs filesystem
9638 URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath);
9639 m_pDS2->close();
9641 else
9643 // find the common path (if any) to these albums
9644 while (!m_pDS2->eof())
9646 std::string path = m_pDS2->fv("strPath").get_asString();
9647 if (basePath.empty())
9648 basePath = path;
9649 else
9650 URIUtils::GetCommonPath(basePath, path);
9652 m_pDS2->next();
9654 m_pDS2->close();
9657 // Check any path found is unique to that album artist, and do *not* return any path
9658 // that is shared with other album artists. That could be because of collaborations
9659 // i.e. albums with more than one album artist, or because there are albums by the
9660 // artist on multiple music sources, or elsewhere in the folder hierarchy.
9661 // Avoid returning some generic music path.
9662 if (!basePath.empty())
9664 strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
9665 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9666 "JOIN path ON song.idPath = path.idPath "
9667 "WHERE album_artist.idArtist <> %ld "
9668 "AND strPath LIKE '%s%%'",
9669 idArtist, basePath.c_str());
9670 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9671 if (!strValue.empty())
9673 int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9674 if (countartists == 0)
9675 return true;
9679 catch (...)
9681 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
9683 basePath.clear();
9684 return false;
9687 bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string& path)
9689 // Get path for artist in the artists folder
9690 path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
9691 CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
9692 if (path.empty())
9693 return false; // No Artists folder not set;
9694 // Get unique artist folder name
9695 std::string strFolder;
9696 if (GetArtistFolderName(artist, strFolder))
9698 path = URIUtils::AddFileToFolder(path, strFolder);
9699 return true;
9701 path.clear();
9702 return false;
9705 bool CMusicDatabase::GetAlbumFolder(const CAlbum& album,
9706 const std::string& strAlbumPath,
9707 std::string& strFolder)
9709 strFolder.clear();
9710 // Get a name for the album folder that is unique for the artist to use when
9711 // exporting albums to separate nfo files in a folder under an artist folder
9713 // When given an album path (common to all the music files containing *only*
9714 // that album) check if that folder name is *unique* looking at folders on
9715 // all levels of the music file paths for the artist
9716 if (!strAlbumPath.empty())
9718 // Get last folder from full path
9719 std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath);
9720 if (!folders.empty())
9722 strFolder = folders.back();
9723 // The same folder name could be used on different paths for albums by the
9724 // same first artist. The albums could be totally different or also have
9725 // the same name (but different mbid). Be over cautious and look for the
9726 // name any where in the music file paths
9727 std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
9728 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9729 "JOIN path on path.idPath = song.idPath "
9730 "WHERE album_artist.iOrder = 0 "
9731 "AND album_artist.idArtist = %ld "
9732 "AND path.strPath LIKE '%%\\%s\\%%'",
9733 album.artistCredits[0].GetArtistId(), strFolder.c_str());
9735 if (!m_pDS2->query(strSQL))
9736 return false;
9737 int iRowsFound = m_pDS2->num_rows();
9738 m_pDS2->close();
9739 if (iRowsFound == 1)
9740 return true;
9743 // Create a valid unique folder name from album title
9744 // @todo: Does UFT8 matter or need normalizing?
9745 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9746 strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT);
9747 StringUtils::Replace(strFolder, " _ ", "_");
9749 // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
9750 // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
9751 // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
9752 // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
9753 std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
9754 "JOIN album ON album_artist.idAlbum = album.idAlbum "
9755 "WHERE album_artist.iOrder = 0 "
9756 "AND album_artist.idArtist = %ld "
9757 "AND album.strAlbum LIKE '%s' ",
9758 album.artistCredits[0].GetArtistId(), album.strAlbum.c_str());
9759 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9760 if (strValue.empty())
9761 return false;
9762 int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9763 if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty())
9764 { // Only one of the duplicate albums can be without mbid
9765 strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4);
9767 return !strFolder.empty();
9770 bool CMusicDatabase::GetArtistFolderName(const CArtist& artist, std::string& strFolder)
9772 return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder);
9775 bool CMusicDatabase::GetArtistFolderName(const std::string& strArtist,
9776 const std::string& strMusicBrainzArtistID,
9777 std::string& strFolder)
9779 // Create a valid unique folder name for artist
9780 // @todo: Does UFT8 matter or need normalizing?
9781 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9782 strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT);
9783 StringUtils::Replace(strFolder, " _ ", "_");
9785 // Ensure <artist name> is unique e.g. 2 x John Williams.
9786 // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
9787 // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
9788 // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
9789 std::string strSQL =
9790 PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
9791 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9792 if (strValue.empty())
9793 return false;
9794 int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9795 if (countartist > 1)
9796 strFolder += "_" + strMusicBrainzArtistID.substr(0, 4);
9797 return !strFolder.empty();
9800 int CMusicDatabase::AddSource(const std::string& strName,
9801 const std::string& strMultipath,
9802 const std::vector<std::string>& vecPaths,
9803 int id /*= -1*/)
9805 std::string strSQL;
9808 if (nullptr == m_pDB)
9809 return -1;
9810 if (nullptr == m_pDS)
9811 return -1;
9813 // Check if source name already exists
9814 int idSource = GetSourceByName(strName);
9815 if (idSource < 0)
9817 BeginTransaction();
9818 // Add new source and source paths
9819 if (id > 0)
9820 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9821 "VALUES(%i, '%s', '%s')",
9822 id, strName.c_str(), strMultipath.c_str());
9823 else
9824 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9825 "VALUES(NULL, '%s', '%s')",
9826 strName.c_str(), strMultipath.c_str());
9827 m_pDS->exec(strSQL);
9829 idSource = static_cast<int>(m_pDS->lastinsertid());
9831 int idPath = 1;
9832 for (const auto& path : vecPaths)
9834 strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
9835 "VALUES(%i,%i,'%s')",
9836 idSource, idPath, path.c_str());
9837 m_pDS->exec(strSQL);
9838 ++idPath;
9841 // Find albums by song path, building WHERE for multiple source paths
9842 // (providing source has a path)
9843 if (vecPaths.size() > 0)
9845 std::vector<int> albumIds;
9846 Filter extFilter;
9847 strSQL = "SELECT DISTINCT idAlbum FROM song ";
9848 extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
9849 for (const auto& path : vecPaths)
9850 extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false);
9851 if (!BuildSQL(strSQL, extFilter, strSQL))
9852 return -1;
9854 if (!m_pDS->query(strSQL))
9855 return -1;
9857 while (!m_pDS->eof())
9859 albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
9860 m_pDS->next();
9862 m_pDS->close();
9864 // Add album_source for related albums
9865 for (auto idAlbum : albumIds)
9867 strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) "
9868 "VALUES('%i', '%i')",
9869 idSource, idAlbum);
9870 m_pDS->exec(strSQL);
9873 CommitTransaction();
9875 return idSource;
9877 catch (...)
9879 CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
9880 RollbackTransaction();
9883 return -1;
9886 int CMusicDatabase::UpdateSource(const std::string& strOldName,
9887 const std::string& strName,
9888 const std::string& strMultipath,
9889 const std::vector<std::string>& vecPaths)
9891 int idSource = -1;
9892 std::string strSourceMultipath;
9893 std::string strSQL;
9896 if (nullptr == m_pDB)
9897 return -1;
9898 if (nullptr == m_pDS)
9899 return -1;
9901 // Get details of named old source
9902 if (!strOldName.empty())
9904 strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
9905 strOldName.c_str());
9906 if (!m_pDS->query(strSQL))
9907 return -1;
9908 if (m_pDS->num_rows() > 0)
9910 idSource = m_pDS->fv("idSource").get_asInt();
9911 strSourceMultipath = m_pDS->fv("strMultipath").get_asString();
9913 m_pDS->close();
9915 if (idSource < 0)
9917 // Source not found, add new one
9918 return AddSource(strName, strMultipath, vecPaths);
9921 // Nothing changed? (that we hold in db, other source details could be modified)
9922 bool pathschanged = strMultipath.compare(strSourceMultipath) != 0;
9923 if (!pathschanged && strOldName.compare(strName) == 0)
9924 return idSource;
9926 if (!pathschanged)
9928 // Name changed? Could be that none of the values held in db changed
9929 if (strOldName.compare(strName) != 0)
9931 strSQL = PrepareSQL("UPDATE source SET strName = '%s' "
9932 "WHERE idSource = %i",
9933 strName.c_str(), idSource);
9934 m_pDS->exec(strSQL);
9936 return idSource;
9938 else
9940 // Change paths (and name) by deleting and re-adding, but keep same ID
9941 strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource);
9942 m_pDS->exec(strSQL);
9943 return AddSource(strName, strMultipath, vecPaths, idSource);
9946 catch (...)
9948 CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
9949 RollbackTransaction();
9952 return -1;
9955 bool CMusicDatabase::RemoveSource(const std::string& strName)
9957 // Related album_source and source_path rows removed by trigger
9958 SetLibraryLastCleaned();
9959 return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str()));
9962 int CMusicDatabase::GetSourceFromPath(const std::string& strPath1)
9964 std::string strSQL;
9965 int idSource = -1;
9968 std::string strPath(strPath1);
9969 if (!URIUtils::HasSlashAtEnd(strPath))
9970 URIUtils::AddSlashAtEnd(strPath);
9972 if (nullptr == m_pDB)
9973 return -1;
9974 if (nullptr == m_pDS)
9975 return -1;
9977 // Check if path is a source matching on multipath
9978 strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str());
9979 if (!m_pDS->query(strSQL))
9980 return -1;
9981 if (m_pDS->num_rows() > 0)
9982 idSource = m_pDS->fv("idSource").get_asInt();
9983 m_pDS->close();
9984 if (idSource > 0)
9985 return idSource;
9987 // Check if path is a source path (of many) or a subfolder of a single source
9988 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9989 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
9990 strPath.c_str());
9991 if (!m_pDS->query(strSQL))
9992 return -1;
9993 if (m_pDS->num_rows() == 1)
9994 idSource = m_pDS->fv("idSource").get_asInt();
9995 m_pDS->close();
9996 return idSource;
9998 catch (...)
10000 CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath1);
10003 return -1;
10006 bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource)
10008 std::string strSQL;
10009 strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) "
10010 "VALUES(%i, %i)",
10011 idAlbum, idSource);
10012 return ExecuteQuery(strSQL);
10015 bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath)
10017 std::string strSQL;
10018 std::vector<int> sourceIds;
10021 if (nullptr == m_pDB)
10022 return false;
10023 if (nullptr == m_pDS)
10024 return false;
10026 if (!strPath.empty())
10028 // Find sources related to album using album path
10029 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10030 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10031 strPath.c_str());
10032 if (!m_pDS->query(strSQL))
10033 return false;
10034 while (!m_pDS->eof())
10036 sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
10037 m_pDS->next();
10039 m_pDS->close();
10041 else
10043 // Find sources using song paths, check each source path individually
10044 if (nullptr == m_pDS2)
10045 return false;
10046 strSQL = "SELECT idSource, strPath FROM source_path";
10047 if (!m_pDS->query(strSQL))
10048 return false;
10049 while (!m_pDS->eof())
10051 std::string sourcepath = m_pDS->fv("strPath").get_asString();
10052 strSQL = PrepareSQL("SELECT 1 FROM song "
10053 "JOIN path ON song.idPath = path.idPath "
10054 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
10055 sourcepath.c_str());
10056 if (!m_pDS2->query(strSQL))
10057 return false;
10058 if (m_pDS2->num_rows() > 0)
10059 sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
10060 m_pDS2->close();
10062 m_pDS->next();
10064 m_pDS->close();
10067 //Add album sources
10068 for (auto idSource : sourceIds)
10070 AddAlbumSource(idAlbum, idSource);
10073 return true;
10075 catch (...)
10077 CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath);
10080 return false;
10083 bool CMusicDatabase::DeleteAlbumSources(int idAlbum)
10085 return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum));
10088 bool CMusicDatabase::CheckSources(VECSOURCES& sources)
10090 if (sources.empty())
10092 // Source table empty too?
10093 return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
10096 // Check number of entries matches
10097 size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
10098 if (total != sources.size())
10099 return false;
10101 // Check individual sources match
10104 if (nullptr == m_pDB)
10105 return false;
10106 if (nullptr == m_pDS)
10107 return false;
10109 std::string strSQL;
10110 for (const auto& source : sources)
10112 // Check each source by name
10113 strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source "
10114 "WHERE strName LIKE '%s'",
10115 source.strName.c_str());
10116 m_pDS->query(strSQL);
10117 if (!m_pDS->query(strSQL))
10118 return false;
10119 if (m_pDS->num_rows() != 1)
10121 // Missing source, or name duplication
10122 m_pDS->close();
10123 return false;
10125 else
10127 // Check details. Encoded URLs of source.strPath matched to strMultipath
10128 // field, no need to look at individual paths of source_path table
10129 if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0)
10131 // Paths not match
10132 m_pDS->close();
10133 return false;
10135 m_pDS->close();
10138 return true;
10140 catch (...)
10142 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10144 return false;
10147 bool CMusicDatabase::MigrateSources()
10149 //Fetch music sources from xml
10150 VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10152 std::string strSQL;
10155 // Fill source and source paths tables
10156 for (const auto& source : sources)
10158 // AddSource(source.strName, source.strPath, source.vecPaths);
10159 // Add new source
10160 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
10161 "VALUES(NULL, '%s', '%s')",
10162 source.strName.c_str(), source.strPath.c_str());
10163 m_pDS->exec(strSQL);
10164 int idSource = static_cast<int>(m_pDS->lastinsertid());
10166 // Add new source paths
10167 int idPath = 1;
10168 for (const auto& path : source.vecPaths)
10170 strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
10171 "VALUES(%i,%i,'%s')",
10172 idSource, idPath, path.c_str());
10173 m_pDS->exec(strSQL);
10174 ++idPath;
10178 return true;
10180 catch (...)
10182 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
10184 return false;
10187 bool CMusicDatabase::UpdateSources()
10189 //Check library and xml sources match
10190 VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10191 if (CheckSources(sources))
10192 return true;
10196 // Empty sources table (related link tables removed by trigger);
10197 ExecuteQuery("DELETE FROM source");
10199 // Fill source table, and album sources
10200 for (const auto& source : sources)
10201 AddSource(source.strName, source.strPath, source.vecPaths);
10203 return true;
10205 catch (...)
10207 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10209 return false;
10212 bool CMusicDatabase::GetSources(CFileItemList& items)
10216 if (nullptr == m_pDB)
10217 return false;
10218 if (nullptr == m_pDS)
10219 return false;
10221 // Get music sources and individual source paths (may not be scanned or have albums etc.)
10222 std::string strSQL =
10223 "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
10224 "FROM source JOIN source_path ON source.idSource = source_path.idSource "
10225 "ORDER BY source.idSource, source_path.idPath";
10227 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
10228 if (!m_pDS->query(strSQL))
10229 return false;
10230 int iRowsFound = m_pDS->num_rows();
10231 if (iRowsFound == 0)
10233 m_pDS->close();
10234 return true;
10237 // Get data from returned rows
10238 // Item has source ID in MusicInfotag, multipath in path, and individual paths in property
10239 CVariant sourcePaths(CVariant::VariantTypeArray);
10240 int idSource = -1;
10241 while (!m_pDS->eof())
10243 if (idSource != m_pDS->fv("source.idSource").get_asInt())
10244 { // New source
10245 if (idSource > 0 && !sourcePaths.empty())
10247 //Store paths for previous source in item list
10248 items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
10249 sourcePaths.clear();
10251 idSource = m_pDS->fv("source.idSource").get_asInt();
10252 CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
10253 pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source");
10254 // Set tag URL for "file" property in AudioLibary processing
10255 pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString());
10256 // Set item path as source URL encoded multipath too
10257 pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString());
10259 pItem->m_bIsFolder = true;
10260 items.Add(pItem);
10262 // Get path data
10263 sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString());
10265 m_pDS->next();
10267 if (!sourcePaths.empty())
10269 //Store paths for final source
10270 items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
10271 sourcePaths.clear();
10274 // cleanup
10275 m_pDS->close();
10277 return true;
10279 catch (...)
10281 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10283 return false;
10286 bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item)
10288 if (nullptr == m_pDB)
10289 return false;
10290 if (nullptr == m_pDS)
10291 return false;
10295 std::string strSQL;
10296 strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
10297 "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
10298 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
10299 "WHERE artist.idArtist = %i "
10300 "ORDER BY album_source.idSource",
10301 idArtist);
10302 if (!m_pDS->query(strSQL))
10303 return false;
10304 if (m_pDS->num_rows() == 0)
10306 // Artist does have any source via albums may not be an album artist.
10307 // Check via songs fetch sources from compilations or where they are guest artist
10308 m_pDS->close();
10309 strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
10310 "JOIN song ON song_artist.idSong = song.idSong "
10311 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
10312 "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
10313 "ORDER BY album_source.idSource",
10314 idArtist);
10315 if (!m_pDS->query(strSQL))
10316 return false;
10317 if (m_pDS->num_rows() == 0)
10319 //No sources, but query successful
10320 m_pDS->close();
10321 return true;
10325 CVariant artistSources(CVariant::VariantTypeArray);
10326 while (!m_pDS->eof())
10328 artistSources.push_back(m_pDS->fv("idSource").get_asInt());
10329 m_pDS->next();
10331 m_pDS->close();
10333 item->SetProperty("sourceid", artistSources);
10334 return true;
10336 catch (...)
10338 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
10340 return false;
10343 bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item)
10345 if (nullptr == m_pDB)
10346 return false;
10347 if (nullptr == m_pDS)
10348 return false;
10352 std::string strSQL;
10353 strSQL = PrepareSQL("SELECT idSource FROM album_source "
10354 "WHERE album_source.idAlbum = %i "
10355 "ORDER BY idSource",
10356 idAlbum);
10357 if (!m_pDS->query(strSQL))
10358 return false;
10359 CVariant albumSources(CVariant::VariantTypeArray);
10360 if (m_pDS->num_rows() > 0)
10362 while (!m_pDS->eof())
10364 albumSources.push_back(m_pDS->fv("idSource").get_asInt());
10365 m_pDS->next();
10367 m_pDS->close();
10369 else
10371 //! @todo: handle singles, or don't waste time checking songs
10372 // Album does have any sources, may be a single??
10373 // Check via song paths, check each source path individually
10374 // usually fewer source paths than songs
10375 m_pDS->close();
10377 if (nullptr == m_pDS2)
10378 return false;
10379 strSQL = "SELECT idSource, strPath FROM source_path";
10380 if (!m_pDS->query(strSQL))
10381 return false;
10382 while (!m_pDS->eof())
10384 std::string sourcepath = m_pDS->fv("strPath").get_asString();
10385 strSQL = PrepareSQL("SELECT 1 FROM song "
10386 "JOIN path ON song.idPath = path.idPath "
10387 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
10388 idAlbum, sourcepath.c_str());
10389 if (!m_pDS2->query(strSQL))
10390 return false;
10391 if (m_pDS2->num_rows() > 0)
10392 albumSources.push_back(m_pDS->fv("idSource").get_asInt());
10393 m_pDS2->close();
10395 m_pDS->next();
10397 m_pDS->close();
10401 item->SetProperty("sourceid", albumSources);
10402 return true;
10404 catch (...)
10406 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
10408 return false;
10411 bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item)
10413 if (nullptr == m_pDB)
10414 return false;
10415 if (nullptr == m_pDS)
10416 return false;
10420 std::string strSQL;
10421 strSQL = PrepareSQL("SELECT idSource FROM song "
10422 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
10423 "WHERE song.idSong = %i "
10424 "ORDER BY idSource",
10425 idSong);
10426 if (!m_pDS->query(strSQL))
10427 return false;
10428 if (m_pDS->num_rows() == 0 && !strPath1.empty())
10430 // Check via song path instead
10431 m_pDS->close();
10432 std::string strPath(strPath1);
10433 if (!URIUtils::HasSlashAtEnd(strPath))
10434 URIUtils::AddSlashAtEnd(strPath);
10436 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10437 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10438 strPath.c_str());
10439 if (!m_pDS->query(strSQL))
10440 return false;
10442 CVariant songSources(CVariant::VariantTypeArray);
10443 while (!m_pDS->eof())
10445 songSources.push_back(m_pDS->fv("idSource").get_asInt());
10446 m_pDS->next();
10448 m_pDS->close();
10450 item->SetProperty("sourceid", songSources);
10451 return true;
10453 catch (...)
10455 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
10457 return false;
10460 int CMusicDatabase::GetSourceByName(const std::string& strSource)
10464 if (nullptr == m_pDB)
10465 return false;
10466 if (nullptr == m_pDS)
10467 return false;
10469 std::string strSQL;
10470 strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str());
10471 // run query
10472 if (!m_pDS->query(strSQL))
10473 return false;
10474 int iRowsFound = m_pDS->num_rows();
10475 if (iRowsFound != 1)
10477 m_pDS->close();
10478 return -1;
10480 return m_pDS->fv("idSource").get_asInt();
10482 catch (...)
10484 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10486 return -1;
10489 std::string CMusicDatabase::GetSourceById(int id)
10491 return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id));
10494 int CMusicDatabase::GetArtistByName(const std::string& strArtist)
10498 if (nullptr == m_pDB)
10499 return false;
10500 if (nullptr == m_pDS)
10501 return false;
10503 std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'",
10504 strArtist.c_str());
10506 // run query
10507 if (!m_pDS->query(strSQL))
10508 return false;
10509 int iRowsFound = m_pDS->num_rows();
10510 if (iRowsFound != 1)
10512 m_pDS->close();
10513 return -1;
10515 int lResult = m_pDS->fv("artist.idArtist").get_asInt();
10516 m_pDS->close();
10517 return lResult;
10519 catch (...)
10521 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10523 return -1;
10526 int CMusicDatabase::GetArtistByMatch(const CArtist& artist)
10528 std::string strSQL;
10531 if (nullptr == m_pDB || nullptr == m_pDS)
10532 return false;
10533 // Match on MusicBrainz ID, definitively unique
10534 if (!artist.strMusicBrainzArtistID.empty())
10535 strSQL = PrepareSQL("SELECT idArtist FROM artist "
10536 "WHERE strMusicBrainzArtistID = '%s'",
10537 artist.strMusicBrainzArtistID.c_str());
10538 else
10539 // No MusicBrainz ID, artist by name with no mbid
10540 strSQL = PrepareSQL("SELECT idArtist FROM artist "
10541 "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
10542 artist.strArtist.c_str());
10543 if (!m_pDS->query(strSQL))
10544 return false;
10545 int iRowsFound = m_pDS->num_rows();
10546 if (iRowsFound != 1)
10548 m_pDS->close();
10549 // Match on artist name, relax mbid restriction
10550 return GetArtistByName(artist.strArtist);
10552 int lResult = m_pDS->fv("idArtist").get_asInt();
10553 m_pDS->close();
10554 return lResult;
10556 catch (...)
10558 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
10560 return -1;
10563 bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist& artist)
10567 if (nullptr == m_pDB)
10568 return false;
10569 if (nullptr == m_pDS)
10570 return false;
10572 std::string strSQL = PrepareSQL(
10573 "SELECT artistview.* FROM song_artist "
10574 "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
10575 "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
10576 idSong);
10577 if (!m_pDS->query(strSQL))
10578 return false;
10579 int iRowsFound = m_pDS->num_rows();
10580 if (iRowsFound != 1)
10582 m_pDS->close();
10583 return false;
10586 artist = GetArtistFromDataset(m_pDS.get());
10588 m_pDS->close();
10589 return true;
10591 catch (...)
10593 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10595 return false;
10598 bool CMusicDatabase::IsSongArtist(int idSong, int idArtist)
10600 std::string strSQL = PrepareSQL("SELECT 1 FROM song_artist "
10601 "WHERE song_artist.idSong= %i AND "
10602 "song_artist.idArtist = %i AND song_artist.idRole = 1",
10603 idSong, idArtist);
10604 return GetSingleValue(strSQL).empty();
10607 bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist)
10609 std::string strSQL =
10610 PrepareSQL("SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
10611 "WHERE song.idSong = %i AND album_artist.idArtist = %i",
10612 idSong, idArtist);
10613 return GetSingleValue(strSQL).empty();
10616 bool CMusicDatabase::IsAlbumBoxset(int idAlbum)
10618 std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum);
10619 int isBoxSet = GetSingleValueInt(strSQL);
10620 return (isBoxSet == 1 ? true : false);
10623 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist)
10627 if (nullptr == m_pDB)
10628 return false;
10629 if (nullptr == m_pDS)
10630 return false;
10632 std::string strSQL;
10633 if (strArtist.empty())
10634 strSQL =
10635 PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str());
10636 else
10637 strSQL = PrepareSQL("SELECT idAlbum FROM album "
10638 "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
10639 strAlbum.c_str(), strArtist.c_str());
10640 // run query
10641 if (!m_pDS->query(strSQL))
10642 return false;
10643 int iRowsFound = m_pDS->num_rows();
10644 if (iRowsFound != 1)
10646 m_pDS->close();
10647 return -1;
10649 return m_pDS->fv("idAlbum").get_asInt();
10651 catch (...)
10653 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10655 return -1;
10658 bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum,
10659 const std::string& strArtist,
10660 int& idAlbum,
10661 std::string& strReview)
10664 Get the first album that matches with the title and artist display name.
10665 Artist(s) and album title may not be sufficient to uniquely identify a match since library can
10666 store multiple releases, and occasionally artists even have different albums with same name.
10667 Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
10671 if (nullptr == m_pDB)
10672 return false;
10673 if (nullptr == m_pDS)
10674 return false;
10676 std::string strSQL;
10677 if (strArtist.empty())
10678 strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
10679 strAlbum.c_str());
10680 else
10681 strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album "
10682 "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
10683 strAlbum.c_str(), strArtist.c_str());
10684 // run query
10685 if (!m_pDS->query(strSQL))
10686 return false;
10687 int iRowsFound = m_pDS->num_rows();
10688 if (iRowsFound > 0)
10690 idAlbum = m_pDS->fv("idAlbum").get_asInt();
10691 strReview = m_pDS->fv("strReview").get_asString();
10692 return true;
10695 catch (...)
10697 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10699 return false;
10702 bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items)
10706 if (nullptr == m_pDB)
10707 return false;
10708 if (nullptr == m_pDS)
10709 return false;
10711 std::string strSQL;
10712 strSQL = PrepareSQL("SELECT albumview.* FROM albumview "
10713 "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
10714 "WHERE album_artist.strArtist LIKE '%s'",
10715 strArtist.c_str());
10717 if (!m_pDS->query(strSQL))
10718 return false;
10720 while (!m_pDS->eof())
10722 CAlbum album = GetAlbumFromDataset(m_pDS.get());
10723 std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
10724 CFileItemPtr pItem(new CFileItem(path, album));
10725 std::string label =
10726 StringUtils::Format("{} ({})", album.strAlbum, pItem->GetMusicInfoTag()->GetYear());
10727 pItem->SetLabel(label);
10728 items.Add(pItem);
10729 m_pDS->next();
10731 m_pDS->close(); // cleanup recordset data
10732 return true;
10734 catch (...)
10736 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10738 return false;
10741 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum,
10742 const std::vector<std::string>& artist)
10744 return GetAlbumByName(
10745 strAlbum,
10746 StringUtils::Join(
10747 artist,
10748 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
10751 int CMusicDatabase::GetAlbumByMatch(const CAlbum& album)
10753 std::string strSQL;
10756 if (nullptr == m_pDB || nullptr == m_pDS)
10757 return false;
10758 // Match on MusicBrainz ID, definitively unique
10759 if (!album.strMusicBrainzAlbumID.empty())
10760 strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'",
10761 album.strMusicBrainzAlbumID.c_str());
10762 else
10763 // No mbid, match on album title and album artist descriptive string, ignore those with mbid
10764 strSQL = PrepareSQL("SELECT idAlbum FROM album "
10765 "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
10766 "AND strMusicBrainzAlbumID IS NULL",
10767 album.GetAlbumArtistString().c_str(), album.strAlbum.c_str());
10768 m_pDS->query(strSQL);
10769 if (!m_pDS->query(strSQL))
10770 return false;
10771 int iRowsFound = m_pDS->num_rows();
10772 if (iRowsFound != 1)
10774 m_pDS->close();
10775 // Match on album title and album artist descriptive string, relax mbid restriction
10776 return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString());
10778 int lResult = m_pDS->fv("idAlbum").get_asInt();
10779 m_pDS->close();
10780 return lResult;
10782 catch (...)
10784 CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
10786 return -1;
10789 std::string CMusicDatabase::GetGenreById(int id)
10791 return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id));
10794 std::string CMusicDatabase::GetArtistById(int id)
10796 return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id));
10799 std::string CMusicDatabase::GetRoleById(int id)
10801 return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id));
10804 bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/)
10806 // Propagate artist sort names into concatenated artist sort name string for songs and albums
10807 // Avoid updating records where sort same as strArtistDisp
10808 std::string strSQL;
10810 // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
10811 // (not handled by PrepareSQL)
10812 bool bisMySQL = StringUtils::EqualsNoCase(
10813 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
10815 BeginMultipleExecute();
10816 if (bisMySQL)
10817 strSQL = "(SELECT GROUP_CONCAT("
10818 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10819 "ELSE artist.strSortName END "
10820 "ORDER BY album_artist.idAlbum, album_artist.iOrder "
10821 "SEPARATOR '; ') as val "
10822 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10823 "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
10824 else
10825 strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10826 "FROM(SELECT album_artist.idAlbum, "
10827 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10828 "ELSE artist.strSortName END as val "
10829 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10830 "WHERE album_artist.idAlbum = album.idAlbum "
10831 "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
10833 strSQL = "UPDATE album SET strArtistSort = " + strSQL +
10834 "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
10835 "AND strArtistDisp <> " +
10836 strSQL;
10837 if (idArtist > 0)
10838 strSQL +=
10839 PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
10840 "AND album_artist.idAlbum = album.idAlbum)",
10841 idArtist);
10842 ExecuteQuery(strSQL);
10843 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
10845 if (bisMySQL)
10846 strSQL = "(SELECT GROUP_CONCAT("
10847 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10848 "ELSE artist.strSortName END "
10849 "ORDER BY song_artist.idSong, song_artist.iOrder "
10850 "SEPARATOR '; ') as val "
10851 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10852 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
10853 else
10854 strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10855 "FROM(SELECT song_artist.idSong, "
10856 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10857 "ELSE artist.strSortName END as val "
10858 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10859 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
10860 "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
10862 strSQL = "UPDATE song SET strArtistSort = " + strSQL +
10863 "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
10864 "AND strArtistDisp <> " +
10865 strSQL;
10866 if (idArtist > 0)
10867 strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
10868 "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)",
10869 idArtist);
10870 ExecuteQuery(strSQL);
10871 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
10873 if (CommitMultipleExecute())
10874 return true;
10875 else
10876 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10877 return false;
10880 std::string CMusicDatabase::GetAlbumById(int id)
10882 return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id));
10885 int CMusicDatabase::GetGenreByName(const std::string& strGenre)
10889 if (nullptr == m_pDB)
10890 return false;
10891 if (nullptr == m_pDS)
10892 return false;
10894 std::string strSQL;
10895 strSQL = PrepareSQL("SELECT idGenre FROM genre "
10896 "WHERE genre.strGenre LIKE '%s'",
10897 strGenre.c_str());
10898 // run query
10899 if (!m_pDS->query(strSQL))
10900 return false;
10901 int iRowsFound = m_pDS->num_rows();
10902 if (iRowsFound != 1)
10904 m_pDS->close();
10905 return -1;
10907 return m_pDS->fv("genre.idGenre").get_asInt();
10909 catch (...)
10911 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
10913 return -1;
10916 bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources)
10918 std::string strSQL;
10921 if (nullptr == m_pDB)
10922 return false;
10923 if (nullptr == m_pDS)
10924 return false;
10926 strSQL = "SELECT %s FROM genre ";
10927 Filter extFilter;
10928 extFilter.AppendField("genre.idGenre");
10929 extFilter.AppendField("genre.strGenre");
10930 if (bSources)
10932 strSQL = "SELECT DISTINCT %s FROM genre ";
10933 extFilter.AppendField("album_source.idSource");
10934 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
10935 extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
10936 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
10937 extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
10938 extFilter.AppendOrder("genre.strGenre");
10939 extFilter.AppendOrder("album_source.idSource");
10941 extFilter.AppendWhere("genre.strGenre != ''");
10943 std::string strSQLExtra;
10944 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
10945 return false;
10947 strSQL = PrepareSQL(strSQL, extFilter.fields.c_str()) + strSQLExtra;
10949 // run query
10950 CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
10952 if (!m_pDS->query(strSQL))
10953 return false;
10954 int iRowsFound = m_pDS->num_rows();
10955 if (iRowsFound == 0)
10957 m_pDS->close();
10958 return true;
10961 if (!bSources)
10962 items.Reserve(iRowsFound);
10964 // Get data from returned rows
10965 // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
10966 CVariant genreSources(CVariant::VariantTypeArray);
10967 int idGenre = -1;
10968 while (!m_pDS->eof())
10970 if (idGenre != m_pDS->fv("genre.idGenre").get_asInt())
10971 { // New genre
10972 if (idGenre > 0 && bSources)
10974 //Store sources for previous genre in item list
10975 items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
10976 genreSources.clear();
10978 idGenre = m_pDS->fv("genre.idGenre").get_asInt();
10979 std::string strGenre = m_pDS->fv("genre.strGenre").get_asString();
10980 CFileItemPtr pItem(new CFileItem(strGenre));
10981 pItem->GetMusicInfoTag()->SetTitle(strGenre);
10982 pItem->GetMusicInfoTag()->SetGenre(strGenre);
10983 pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre");
10984 pItem->SetPath(StringUtils::Format("musicdb://genres/{}/", idGenre));
10985 pItem->m_bIsFolder = true;
10986 items.Add(pItem);
10988 // Get source data
10989 if (bSources)
10991 int sourceid = m_pDS->fv("album_source.idSource").get_asInt();
10992 if (sourceid > 0)
10993 genreSources.push_back(sourceid);
10995 m_pDS->next();
10997 if (bSources)
10999 //Store sources for final genre
11000 items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
11003 // cleanup
11004 m_pDS->close();
11006 return true;
11008 catch (...)
11010 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
11012 return false;
11015 std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc)
11017 // Get disc node title from ids allowing for "*all"
11018 std::string disctitle;
11019 std::string albumtitle;
11020 if (idAlbum > 0)
11021 albumtitle = GetAlbumById(idAlbum);
11022 if (idDisc > 0)
11024 disctitle = GetSingleValue("song", "strDiscSubtitle",
11025 PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc));
11026 if (disctitle.empty())
11027 disctitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc.
11028 if (albumtitle.empty())
11029 albumtitle = disctitle;
11030 else
11031 albumtitle = albumtitle + " - " + disctitle;
11033 return albumtitle;
11036 int CMusicDatabase::GetBoxsetsCount()
11038 return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
11041 int CMusicDatabase::GetAlbumDiscsCount(int idAlbum)
11043 std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum);
11044 return GetSingleValueInt(strSQL);
11047 int CMusicDatabase::GetCompilationAlbumsCount()
11049 return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
11052 int CMusicDatabase::GetSinglesCount()
11054 CDatabase::Filter filter(
11055 PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
11056 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
11057 return GetSongsCount(filter);
11060 int CMusicDatabase::GetArtistCountForRole(int role)
11062 std::string strSQL = PrepareSQL(
11063 "SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role);
11064 return GetSingleValueInt(strSQL);
11067 int CMusicDatabase::GetArtistCountForRole(const std::string& strRole)
11069 std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist "
11070 "JOIN role ON song_artist.idRole = role.idRole "
11071 "WHERE role.strRole LIKE '%s'",
11072 strRole.c_str());
11073 return GetSingleValueInt(strSQL);
11076 bool CMusicDatabase::SetPathHash(const std::string& path, const std::string& hash)
11080 if (nullptr == m_pDB)
11081 return false;
11082 if (nullptr == m_pDS)
11083 return false;
11085 if (hash.empty())
11086 { // this is an empty folder - we need only add it to the path table
11087 // if the path actually exists
11088 if (!CDirectory::Exists(path))
11089 return false;
11091 int idPath = AddPath(path);
11092 if (idPath < 0)
11093 return false;
11095 std::string strSQL =
11096 PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash.c_str(), idPath);
11097 m_pDS->exec(strSQL);
11099 return true;
11101 catch (...)
11103 CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash);
11106 return false;
11109 bool CMusicDatabase::GetPathHash(const std::string& path, std::string& hash)
11113 if (nullptr == m_pDB)
11114 return false;
11115 if (nullptr == m_pDS)
11116 return false;
11118 std::string strSQL = PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
11119 m_pDS->query(strSQL);
11120 if (m_pDS->num_rows() == 0)
11121 return false;
11122 hash = m_pDS->fv("strHash").get_asString();
11123 return true;
11125 catch (...)
11127 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
11130 return false;
11133 bool CMusicDatabase::RemoveSongsFromPath(const std::string& path1, MAPSONGS& songmap, bool exact)
11135 // We need to remove all songs from this path, as their tags are going
11136 // to be re-read. We need to remove all songs from the song table + all links to them
11137 // from the song link tables (as otherwise if a song is added back
11138 // to the table with the same idSong, these tables can't be cleaned up properly later)
11140 //! @todo SQLite probably doesn't allow this, but can we rely on that??
11142 // We don't need to remove orphaned albums at this point as in AddAlbum() we check
11143 // first whether the album has already been read during this scan, and if it hasn't
11144 // we check whether it's in the table and update accordingly at that point, removing the entries from
11145 // the album link tables. The only failure point for this is albums
11146 // that span multiple folders, where just the files in one folder have been changed. In this case
11147 // any linked fields that are only in the files that haven't changed will be removed. Clearly
11148 // the primary albumartist still matches (as that's what we looked up based on) so is this really
11149 // an issue? I don't think it is, as those artists will still have links to the album via the songs
11150 // which is generally what we rely on, so the only failure point is albumartist lookup. In this
11151 // case, it will return only things in the album_artist table from the newly updated songs (and
11152 // only if they have additional artists). I think the effect of this is minimal at best, as ALL
11153 // songs in the album should have the same albumartist!
11155 // we also remove the path at this point as it will be added later on if the
11156 // path still exists.
11157 // After scanning we then remove the orphaned artists, genres and thumbs.
11159 // Note: when used to remove all songs from a path and its subpath (exact=false), this
11160 // does miss archived songs.
11161 std::string path(path1);
11162 SetLibraryLastUpdated();
11165 if (!URIUtils::HasSlashAtEnd(path))
11166 URIUtils::AddSlashAtEnd(path);
11168 if (nullptr == m_pDB)
11169 return false;
11170 if (nullptr == m_pDS)
11171 return false;
11173 // Filename is not unique for a path as songs from a cuesheet have same filename.
11174 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
11175 // in a folder and some edited and rescanned.
11176 // Hence order by filename so these songs can be gathered together.
11177 std::string where;
11178 if (exact)
11179 where = PrepareSQL(" WHERE strPath='%s'", path.c_str());
11180 else
11181 where = PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()),
11182 path.c_str());
11183 std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName";
11184 if (!m_pDS->query(sql))
11185 return false;
11186 int iRowsFound = m_pDS->num_rows();
11187 if (iRowsFound > 0)
11189 // Each file is potentially mapped to a list of songs, gather these and save as list
11190 VECSONGS songs;
11191 std::string filename;
11192 std::vector<std::string> songIds;
11193 while (!m_pDS->eof())
11195 CSong song = GetSongFromDataset();
11196 if (!filename.empty() && filename != song.strFileName)
11198 // Save songs for previous filename
11199 songmap.insert(std::make_pair(filename, songs));
11200 songs.clear();
11202 song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb");
11203 songs.emplace_back(song);
11204 songIds.push_back(PrepareSQL("%i", song.idSong));
11205 filename = song.strFileName;
11207 m_pDS->next();
11209 m_pDS->close();
11210 songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
11212 //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
11213 for (const auto& id : songIds)
11214 AnnounceRemove(MediaTypeSong, atoi(id.c_str()));
11216 // Delete all songs, and anything linked to them via triggers
11217 std::string strIDs = StringUtils::Join(songIds, ",");
11218 sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")";
11219 m_pDS->exec(sql);
11221 // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
11222 sql = "delete from path" + where;
11223 m_pDS->exec(sql);
11224 return iRowsFound > 0;
11226 catch (...)
11228 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
11230 return false;
11233 void CMusicDatabase::CheckArtistLinksChanged()
11235 std::string strSQL = "SELECT COUNT(1) FROM removed_link ";
11236 int iLinks = GetSingleValueInt(strSQL, m_pDS);
11237 if (iLinks > 0)
11239 SetArtistLinksUpdated(); // Store datetime artist links last updated
11240 DeleteRemovedLinks(); // Clean-up artist links
11244 bool CMusicDatabase::GetPaths(std::set<std::string>& paths)
11248 if (nullptr == m_pDB)
11249 return false;
11250 if (nullptr == m_pDS)
11251 return false;
11253 paths.clear();
11255 // find all paths
11256 if (!m_pDS->query("SELECT strPath FROM path"))
11257 return false;
11258 int iRowsFound = m_pDS->num_rows();
11259 if (iRowsFound == 0)
11261 m_pDS->close();
11262 return true;
11264 while (!m_pDS->eof())
11266 paths.insert(m_pDS->fv("strPath").get_asString());
11267 m_pDS->next();
11269 m_pDS->close();
11270 return true;
11272 catch (...)
11274 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
11276 return false;
11279 bool CMusicDatabase::SetSongUserrating(const std::string& filePath, int userrating)
11283 if (filePath.empty())
11284 return false;
11285 if (nullptr == m_pDB)
11286 return false;
11287 if (nullptr == m_pDS)
11288 return false;
11290 int songID = GetSongIDFromPath(filePath);
11291 if (-1 == songID)
11292 return false;
11294 return SetSongUserrating(songID, userrating);
11296 catch (...)
11298 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, userrating);
11300 return false;
11303 bool CMusicDatabase::SetSongUserrating(int idSong, int userrating)
11307 if (nullptr == m_pDB)
11308 return false;
11309 if (nullptr == m_pDS)
11310 return false;
11312 std::string sql =
11313 PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong);
11314 m_pDS->exec(sql);
11315 return true;
11317 catch (...)
11319 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idSong, userrating);
11321 return false;
11324 bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating)
11328 if (nullptr == m_pDB)
11329 return false;
11330 if (nullptr == m_pDS)
11331 return false;
11333 if (-1 == idAlbum)
11334 return false;
11335 std::string sql =
11336 PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum);
11337 m_pDS->exec(sql);
11338 return true;
11340 catch (...)
11342 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idAlbum, userrating);
11344 return false;
11347 bool CMusicDatabase::SetSongVotes(const std::string& filePath, int votes)
11351 if (filePath.empty())
11352 return false;
11353 if (nullptr == m_pDB)
11354 return false;
11355 if (nullptr == m_pDS)
11356 return false;
11358 int songID = GetSongIDFromPath(filePath);
11359 if (-1 == songID)
11360 return false;
11362 std::string sql = PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID);
11364 m_pDS->exec(sql);
11365 return true;
11367 catch (...)
11369 CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, votes);
11371 return false;
11374 int CMusicDatabase::GetSongIDFromPath(const std::string& filePath)
11376 // grab the where string to identify the song id
11377 CURL url(filePath);
11378 if (url.IsProtocol("musicdb"))
11380 std::string strFile = URIUtils::GetFileName(filePath);
11381 URIUtils::RemoveExtension(strFile);
11382 return atoi(strFile.c_str());
11384 // hit the db
11387 if (nullptr == m_pDB)
11388 return -1;
11389 if (nullptr == m_pDS)
11390 return -1;
11392 std::string strPath, strFileName;
11393 SplitPath(filePath, strPath, strFileName);
11394 URIUtils::AddSlashAtEnd(strPath);
11396 std::string sql = PrepareSQL("SELECT idSong FROM song JOIN path ON song.idPath = path.idPath "
11397 "WHERE song.strFileName='%s' AND path.strPath='%s'",
11398 strFileName.c_str(), strPath.c_str());
11399 if (!m_pDS->query(sql))
11400 return -1;
11402 if (m_pDS->num_rows() == 0)
11404 m_pDS->close();
11405 return -1;
11408 int songID = m_pDS->fv("idSong").get_asInt();
11409 m_pDS->close();
11410 return songID;
11412 catch (...)
11414 CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
11416 return -1;
11419 bool CMusicDatabase::CommitTransaction()
11421 if (CDatabase::CommitTransaction())
11422 { // number of items in the db has likely changed, so reset the infomanager cache
11423 CGUIComponent* gui = CServiceBroker::GetGUI();
11424 if (gui)
11426 gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(
11427 LIBRARY_HAS_MUSIC, GetSongsCount() > 0);
11428 return true;
11431 return false;
11434 bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper)
11436 if (nullptr == m_pDB)
11437 return false;
11438 if (nullptr == m_pDS)
11439 return false;
11440 std::string strSQL;
11441 int idSetting = -1;
11444 CONTENT_TYPE content = CONTENT_NONE;
11446 // Build where clause from virtual path
11447 Filter extFilter;
11448 CMusicDbUrl musicUrl;
11449 SortDescription sorting;
11450 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
11451 return false;
11453 std::string itemType = musicUrl.GetType();
11454 if (StringUtils::EqualsNoCase(itemType, "artists"))
11456 content = CONTENT_ARTISTS;
11458 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11460 content = CONTENT_ALBUMS;
11462 else
11463 return false; //Only artists and albums have info settings
11465 std::string strSQLWhere;
11466 if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere))
11467 return false;
11469 // Replace view names with table names
11470 StringUtils::Replace(strSQLWhere, "artistview", "artist");
11471 StringUtils::Replace(strSQLWhere, "albumview", "album");
11473 BeginTransaction();
11474 // Clear current scraper settings (0 => default scraper used)
11475 if (content == CONTENT_ARTISTS)
11476 strSQL = "UPDATE artist SET idInfoSetting = %i ";
11477 else
11478 strSQL = "UPDATE album SET idInfoSetting = %i ";
11479 strSQL = PrepareSQL(strSQL, 0) + strSQLWhere;
11480 m_pDS->exec(strSQL);
11482 //Remove orphaned settings
11483 CleanupInfoSettings();
11485 if (scraper)
11487 // Add new info setting
11488 strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
11489 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
11490 m_pDS->exec(strSQL);
11491 idSetting = static_cast<int>(m_pDS->lastinsertid());
11493 if (content == CONTENT_ARTISTS)
11494 strSQL = "UPDATE artist SET idInfoSetting = %i ";
11495 else
11496 strSQL = "UPDATE album SET idInfoSetting = %i ";
11497 strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere;
11498 m_pDS->exec(strSQL);
11500 CommitTransaction();
11501 return true;
11503 catch (...)
11505 RollbackTransaction();
11506 CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, strBaseDir, strSQL);
11508 return false;
11511 bool CMusicDatabase::SetScraper(int id,
11512 const CONTENT_TYPE& content,
11513 const ADDON::ScraperPtr& scraper)
11515 if (nullptr == m_pDB)
11516 return false;
11517 if (nullptr == m_pDS)
11518 return false;
11519 std::string strSQL;
11520 int idSetting = -1;
11523 BeginTransaction();
11524 // Fetch current info settings for item, 0 => default is used
11525 if (content == CONTENT_ARTISTS)
11526 strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
11527 else
11528 strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
11529 strSQL = PrepareSQL(strSQL, id);
11530 m_pDS->query(strSQL);
11531 if (m_pDS->num_rows() > 0)
11532 idSetting = m_pDS->fv("idInfoSetting").get_asInt();
11533 m_pDS->close();
11535 if (idSetting < 1)
11536 { // Add new info setting
11537 strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
11538 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
11539 m_pDS->exec(strSQL);
11540 idSetting = static_cast<int>(m_pDS->lastinsertid());
11542 if (content == CONTENT_ARTISTS)
11543 strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
11544 else
11545 strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
11546 strSQL = PrepareSQL(strSQL, idSetting, id);
11547 m_pDS->exec(strSQL);
11549 else
11550 { // Update info setting
11551 strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' "
11552 "WHERE idSetting = %i";
11553 strSQL =
11554 PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting);
11555 m_pDS->exec(strSQL);
11557 CommitTransaction();
11558 return true;
11560 catch (...)
11562 RollbackTransaction();
11563 CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, id, strSQL);
11565 return false;
11568 bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper)
11570 std::string scraperUUID;
11571 std::string strSettings;
11574 if (nullptr == m_pDB)
11575 return false;
11576 if (nullptr == m_pDS)
11577 return false;
11579 std::string strSQL;
11580 strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
11581 if (content == CONTENT_ARTISTS)
11582 strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting "
11583 "WHERE artist.idArtist = %i";
11584 else
11585 strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting "
11586 "WHERE album.idAlbum = %i";
11587 strSQL = PrepareSQL(strSQL, id);
11588 m_pDS->query(strSQL);
11589 if (!m_pDS->eof())
11590 { // try and ascertain scraper
11591 scraperUUID = m_pDS->fv("strScraperPath").get_asString();
11592 strSettings = m_pDS->fv("strSettings").get_asString();
11594 // Use pre configured or default scraper
11595 ADDON::AddonPtr addon;
11596 if (!scraperUUID.empty() &&
11597 CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon,
11598 ADDON::OnlyEnabled::CHOICE_YES) &&
11599 addon)
11601 scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
11602 if (scraper)
11603 // Set settings
11604 scraper->SetPathSettings(content, strSettings);
11607 m_pDS->close();
11609 if (!scraper)
11610 { // use default music scraper instead
11611 ADDON::AddonPtr addon;
11612 if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
11613 ADDON::ScraperTypeFromContent(content), addon))
11615 scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
11616 return scraper != NULL;
11618 else
11619 return false;
11622 return true;
11624 catch (...)
11626 CLog::Log(LOGERROR, "{} -({}, {} {}) failed", __FUNCTION__, id, scraperUUID, strSettings);
11628 return false;
11631 bool CMusicDatabase::ScraperInUse(const std::string& scraperID) const
11635 if (nullptr == m_pDB)
11636 return false;
11637 if (nullptr == m_pDS)
11638 return false;
11640 std::string sql =
11641 PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID.c_str());
11642 if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
11644 m_pDS->close();
11645 return false;
11647 bool found = m_pDS->fv(0).get_asInt() > 0;
11648 m_pDS->close();
11649 return found;
11651 catch (...)
11653 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID);
11655 return false;
11658 bool CMusicDatabase::GetItems(const std::string& strBaseDir,
11659 CFileItemList& items,
11660 const Filter& filter /* = Filter() */,
11661 const SortDescription& sortDescription /* = SortDescription() */)
11663 CMusicDbUrl musicUrl;
11664 if (!musicUrl.FromString(strBaseDir))
11665 return false;
11667 return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription);
11670 bool CMusicDatabase::GetItems(const std::string& strBaseDir,
11671 const std::string& itemType,
11672 CFileItemList& items,
11673 const Filter& filter /* = Filter() */,
11674 const SortDescription& sortDescription /* = SortDescription() */)
11676 if (StringUtils::EqualsNoCase(itemType, "genres"))
11677 return GetGenresNav(strBaseDir, items, filter);
11678 else if (StringUtils::EqualsNoCase(itemType, "sources"))
11679 return GetSourcesNav(strBaseDir, items, filter);
11680 else if (StringUtils::EqualsNoCase(itemType, "years"))
11681 return GetYearsNav(strBaseDir, items, filter);
11682 else if (StringUtils::EqualsNoCase(itemType, "roles"))
11683 return GetRolesNav(strBaseDir, items, filter);
11684 else if (StringUtils::EqualsNoCase(itemType, "artists"))
11685 return GetArtistsNav(strBaseDir, items,
11686 !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
11687 CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS),
11688 -1, -1, -1, filter, sortDescription);
11689 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11690 return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription);
11691 else if (StringUtils::EqualsNoCase(itemType, "discs"))
11692 return GetDiscsByWhere(strBaseDir, filter, items, sortDescription);
11693 else if (StringUtils::EqualsNoCase(itemType, "songs"))
11694 return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true);
11696 return false;
11699 std::string CMusicDatabase::GetItemById(const std::string& itemType, int id)
11701 if (StringUtils::EqualsNoCase(itemType, "genres"))
11702 return GetGenreById(id);
11703 else if (StringUtils::EqualsNoCase(itemType, "sources"))
11704 return GetSourceById(id);
11705 else if (StringUtils::EqualsNoCase(itemType, "years"))
11706 return std::to_string(id);
11707 else if (StringUtils::EqualsNoCase(itemType, "artists"))
11708 return GetArtistById(id);
11709 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11710 return GetAlbumById(id);
11711 else if (StringUtils::EqualsNoCase(itemType, "roles"))
11712 return GetRoleById(id);
11714 return "";
11717 void CMusicDatabase::ExportToXML(const CLibExportSettings& settings,
11718 CGUIDialogProgress* progressDialog /*= nullptr*/)
11720 if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) &&
11721 !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) &&
11722 !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) &&
11723 !settings.IsItemExported(ELIBEXPORT_ALBUMS) && !settings.IsItemExported(ELIBEXPORT_SONGS))
11724 return;
11726 // Exporting albums either art or NFO (or both) selected
11727 if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && settings.m_skipnfo &&
11728 !settings.m_artwork && settings.IsItemExported(ELIBEXPORT_ALBUMS))
11729 return;
11731 std::string strFolder;
11732 if (settings.IsSingleFile() || settings.IsSeparateFiles())
11734 // Exporting to single file or separate files in a specified location
11735 if (settings.m_strPath.empty())
11736 return;
11738 strFolder = settings.m_strPath;
11739 if (!URIUtils::HasSlashAtEnd(strFolder))
11740 URIUtils::AddSlashAtEnd(strFolder);
11741 strFolder = URIUtils::GetDirectory(strFolder);
11742 if (strFolder.empty())
11743 return;
11745 else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists()))
11747 // Exporting artist folders only, or artist NFO or art to library folders
11748 // need Artist Information Folder defined.
11749 // (Album NFO and art goes to music folders)
11750 strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
11751 CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
11752 if (strFolder.empty())
11753 return;
11757 bool artistfoldersonly;
11758 artistfoldersonly = settings.IsArtistFoldersOnly() ||
11759 ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
11760 settings.m_skipnfo && !settings.m_artwork);
11762 int iFailCount = 0;
11765 if (nullptr == m_pDB)
11766 return;
11767 if (nullptr == m_pDS)
11768 return;
11769 if (nullptr == m_pDS2)
11770 return;
11772 // Create our xml document
11773 CXBMCTinyXML xmlDoc;
11774 TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11775 xmlDoc.InsertEndChild(decl);
11776 TiXmlNode* pMain = NULL;
11777 if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly)
11778 pMain = &xmlDoc;
11779 else if (settings.IsSingleFile())
11781 TiXmlElement xmlMainElement("musicdb");
11782 pMain = xmlDoc.InsertEndChild(xmlMainElement);
11785 if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly)
11787 // Find albums to export
11788 std::vector<int> albumIds;
11789 std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
11790 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str());
11791 if (!settings.m_unscraped)
11792 strSQL += "AND lastScraped IS NOT NULL";
11793 CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
11794 m_pDS->query(strSQL);
11796 int total = m_pDS->num_rows();
11797 int current = 0;
11799 albumIds.reserve(total);
11800 while (!m_pDS->eof())
11802 albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
11803 m_pDS->next();
11805 m_pDS->close();
11807 for (const auto& albumId : albumIds)
11809 CAlbum album;
11810 GetAlbum(albumId, album);
11811 std::string strAlbumPath;
11812 std::string strPath;
11813 // Get album path, empty unless all album songs are under a unique folder, and
11814 // there are no songs from another album in the same folder.
11815 if (!GetAlbumPath(albumId, strAlbumPath))
11816 strAlbumPath.clear();
11817 if (settings.IsSingleFile())
11819 // Save album to xml, including album path
11820 album.Save(pMain, "album", strAlbumPath);
11822 else
11823 { // Separate files and artwork
11824 bool pathfound = false;
11825 if (settings.IsToLibFolders())
11826 { // Save album.nfo and artwork with music files.
11827 // Most albums are under a unique folder, but if songs from various albums are mixed then
11828 // avoid overwriting by not allow NFO and art to be exported
11829 if (strAlbumPath.empty())
11830 CLog::Log(LOGDEBUG,
11831 "CMusicDatabase::{} - Not exporting album {} as unique path not found",
11832 __FUNCTION__, album.strAlbum);
11833 else if (!CDirectory::Exists(strAlbumPath))
11834 CLog::Log(
11835 LOGDEBUG,
11836 "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist",
11837 __FUNCTION__, album.strAlbum, strAlbumPath);
11838 else
11840 strPath = strAlbumPath;
11841 pathfound = true;
11844 else
11845 { // Save album.nfo and artwork to subfolder on export path
11846 // strPath = strFolder/<albumartist name>/<albumname>
11847 // where <albumname> is either the same name as the album folder
11848 // containing the music files (if unique) or is created using the album name
11849 std::string strAlbumArtist;
11850 pathfound = GetArtistFolderName(album.GetAlbumArtist()[0],
11851 album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist);
11852 if (pathfound)
11854 strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist);
11855 pathfound = CDirectory::Exists(strPath);
11856 if (!pathfound)
11857 pathfound = CDirectory::Create(strPath);
11859 if (!pathfound)
11860 CLog::Log(LOGDEBUG,
11861 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
11862 __FUNCTION__, album.strAlbum, strPath);
11863 else
11865 std::string strAlbumFolder;
11866 pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder);
11867 if (pathfound)
11869 strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder);
11870 pathfound = CDirectory::Exists(strPath);
11871 if (!pathfound)
11872 pathfound = CDirectory::Create(strPath);
11874 if (!pathfound)
11875 CLog::Log(LOGDEBUG,
11876 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
11877 __FUNCTION__, album.strAlbum, strPath);
11880 if (pathfound)
11882 if (!settings.m_skipnfo)
11884 // Save album to NFO, including album path
11885 album.Save(pMain, "album", strAlbumPath);
11886 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo");
11887 if (settings.m_overwrite || !CFile::Exists(nfoFile))
11889 if (!xmlDoc.SaveFile(nfoFile))
11891 CLog::Log(LOGERROR, "CMusicDatabase::{}: Album nfo export failed! ('{}')",
11892 __FUNCTION__, nfoFile);
11893 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
11894 g_localizeStrings.Get(20302), nfoFile);
11895 iFailCount++;
11899 if (settings.m_artwork)
11901 // Save art in album folder
11902 // Note thumb resolution may be lower than original when overwriting
11903 std::map<std::string, std::string> artwork;
11904 std::string savedArtfile;
11905 if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork))
11907 for (const auto& art : artwork)
11909 if (art.first == "thumb")
11910 savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
11911 else
11912 savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
11913 CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
11914 settings.m_overwrite);
11918 xmlDoc.Clear();
11919 xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
11923 if ((current % 50) == 0 && progressDialog)
11925 progressDialog->SetLine(1, CVariant{album.strAlbum});
11926 progressDialog->SetPercentage(current * 100 / total);
11927 if (progressDialog->IsCanceled())
11928 return;
11930 current++;
11934 // Export song playback history to single file only
11935 if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS))
11937 if (!ExportSongHistory(pMain, progressDialog))
11938 return;
11941 if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty())
11943 // Find artists to export
11944 std::vector<int> artistIds;
11945 Filter filter;
11947 if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS))
11948 filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist "
11949 "WHERE album_artist.idArtist = artist.idArtist)",
11950 false);
11951 if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS))
11953 if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
11954 filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist "
11955 "WHERE song_artist.idArtist = artist.idArtist )",
11956 false);
11957 else
11958 filter.AppendWhere(
11959 "EXISTS (SELECT 1 FROM song_artist "
11960 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)",
11961 false);
11963 else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
11964 filter.AppendWhere(
11965 "EXISTS (SELECT 1 FROM song_artist "
11966 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)",
11967 false);
11969 if (!settings.m_unscraped && !artistfoldersonly)
11970 filter.AppendWhere("lastScraped IS NOT NULL", true);
11972 std::string strSQL = "SELECT idArtist FROM artist";
11973 BuildSQL(strSQL, filter, strSQL);
11974 CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
11976 m_pDS->query(strSQL);
11977 int total = m_pDS->num_rows();
11978 int current = 0;
11979 artistIds.reserve(total);
11980 while (!m_pDS->eof())
11982 artistIds.push_back(m_pDS->fv("idArtist").get_asInt());
11983 m_pDS->next();
11985 m_pDS->close();
11987 for (const auto& artistId : artistIds)
11989 CArtist artist;
11990 // Include discography when not folders only
11991 GetArtist(artistId, artist, !artistfoldersonly);
11992 std::string strPath;
11993 std::map<std::string, std::string> artwork;
11994 if (settings.IsSingleFile())
11996 // Save artist to xml, and old path (common to music files) if it has one
11997 GetOldArtistPath(artist.idArtist, strPath);
11998 artist.Save(pMain, "artist", strPath);
12000 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
12001 { // append to the XML
12002 TiXmlElement additionalNode("art");
12003 for (const auto& i : artwork)
12004 XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
12005 pMain->LastChild()->InsertEndChild(additionalNode);
12008 else
12009 { // Separate files: artist.nfo and artwork in strFolder/<artist name>
12010 // Get unique folder allowing for duplicate names e.g. 2 x John Williams
12011 bool pathfound = GetArtistFolderName(artist, strPath);
12012 if (pathfound)
12014 strPath = URIUtils::AddFileToFolder(strFolder, strPath);
12015 pathfound = CDirectory::Exists(strPath);
12016 if (!pathfound)
12017 pathfound = CDirectory::Create(strPath);
12019 if (!pathfound)
12020 CLog::Log(LOGDEBUG,
12021 "CMusicDatabase::{} - Not exporting artist {} as could not create {}",
12022 __FUNCTION__, artist.strArtist, strPath);
12023 else
12025 if (!artistfoldersonly)
12027 if (!settings.m_skipnfo)
12029 artist.Save(pMain, "artist", strPath);
12030 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo");
12031 if (settings.m_overwrite || !CFile::Exists(nfoFile))
12033 if (!xmlDoc.SaveFile(nfoFile))
12035 CLog::Log(LOGERROR, "CMusicDatabase::{}: Artist nfo export failed! ('{}')",
12036 __FUNCTION__, nfoFile);
12037 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
12038 g_localizeStrings.Get(20302), nfoFile);
12039 iFailCount++;
12043 if (settings.m_artwork)
12045 std::string savedArtfile;
12046 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
12048 for (const auto& art : artwork)
12050 if (art.first == "thumb")
12051 savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
12052 else
12053 savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
12054 CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
12055 settings.m_overwrite);
12059 xmlDoc.Clear();
12060 xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
12064 if ((current % 50) == 0 && progressDialog)
12066 progressDialog->SetLine(1, CVariant{artist.strArtist});
12067 progressDialog->SetPercentage(current * 100 / total);
12068 if (progressDialog->IsCanceled())
12069 return;
12071 current++;
12075 if (settings.IsSingleFile())
12077 std::string xmlFile = URIUtils::AddFileToFolder(
12078 strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
12079 if (CFile::Exists(xmlFile))
12080 xmlFile = URIUtils::AddFileToFolder(
12081 strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
12082 xmlDoc.SaveFile(xmlFile);
12084 CVariant data;
12085 data["file"] = xmlFile;
12086 if (iFailCount > 0)
12087 data["failcount"] = iFailCount;
12088 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport",
12089 data);
12092 catch (...)
12094 CLog::Log(LOGERROR, "CMusicDatabase::{} failed", __FUNCTION__);
12095 iFailCount++;
12098 if (progressDialog)
12099 progressDialog->Close();
12101 if (iFailCount > 0 && progressDialog)
12102 HELPERS::ShowOKDialogLines(
12103 CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)});
12106 bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog)
12110 // Export songs with some playback history
12111 std::string strSQL =
12112 "SELECT idSong, song.idAlbum, "
12113 "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, "
12114 "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, "
12115 "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating "
12116 "FROM song JOIN album on album.idAlbum = song.idAlbum "
12117 "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0";
12119 CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL);
12120 m_pDS->query(strSQL);
12122 int total = m_pDS->num_rows();
12123 int current = 0;
12124 while (!m_pDS->eof())
12126 TiXmlElement songElement("song");
12127 TiXmlNode* song = pNode->InsertEndChild(songElement);
12129 XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt());
12130 XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString());
12131 XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString());
12132 XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt());
12133 XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString());
12134 XMLUtils::SetString(song, "musicbrainztrackid",
12135 m_pDS->fv("strMusicBrainzTrackID").get_asString());
12136 XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt());
12137 XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString());
12138 XMLUtils::SetString(song, "musicbrainzalbumid",
12139 m_pDS->fv("strMusicBrainzAlbumID").get_asString());
12140 XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString());
12141 XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt());
12142 XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString());
12143 auto* rating = XMLUtils::SetString(
12144 song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat()));
12145 if (rating)
12146 rating->ToElement()->SetAttribute("max", 10);
12147 XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt());
12148 auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt());
12149 if (userrating)
12150 userrating->ToElement()->SetAttribute("max", 10);
12152 if ((current % 100) == 0 && progressDialog)
12154 progressDialog->SetLine(1, CVariant{m_pDS->fv("strAlbum").get_asString()});
12155 progressDialog->SetPercentage(current * 100 / total);
12156 if (progressDialog->IsCanceled())
12158 m_pDS->close();
12159 return false;
12162 current++;
12164 m_pDS->next();
12166 m_pDS->close();
12167 return true;
12169 catch (...)
12171 CLog::Log(LOGERROR, "{0} failed", __FUNCTION__);
12173 return false;
12176 void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog)
12180 if (nullptr == m_pDB)
12181 return;
12182 if (nullptr == m_pDS)
12183 return;
12185 CXBMCTinyXML xmlDoc;
12186 if (!xmlDoc.LoadFile(xmlFile) && progressDialog)
12188 HELPERS::ShowOKDialogLines(CVariant{20197}, CVariant{38354}); //"Unable to read xml file"
12189 return;
12192 TiXmlElement* root = xmlDoc.RootElement();
12193 if (!root)
12194 return;
12196 TiXmlElement* entry = root->FirstChildElement();
12197 int current = 0;
12198 int total = 0;
12199 int songtotal = 0;
12200 // Count the number of artists, albums and songs
12201 while (entry)
12203 if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 ||
12204 StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
12205 total++;
12206 else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
12207 songtotal++;
12209 entry = entry->NextSiblingElement();
12212 BeginTransaction();
12213 entry = root->FirstChildElement();
12214 while (entry)
12216 std::string strTitle;
12217 if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0)
12219 CArtist importedArtist;
12220 importedArtist.Load(entry);
12221 strTitle = importedArtist.strArtist;
12223 // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
12224 int idArtist = GetArtistByMatch(importedArtist);
12225 if (idArtist > -1)
12227 CArtist artist;
12228 GetArtist(idArtist, artist, true); // include discography
12229 artist.MergeScrapedArtist(importedArtist, true);
12230 UpdateArtist(artist);
12232 else
12233 CLog::Log(LOGDEBUG, "{} - Not import additional artist data as {} not found",
12234 __FUNCTION__, importedArtist.strArtist);
12235 current++;
12237 else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
12239 CAlbum importedAlbum;
12240 importedAlbum.Load(entry);
12241 strTitle = importedAlbum.strAlbum;
12242 // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
12243 int idAlbum = GetAlbumByMatch(importedAlbum);
12244 if (idAlbum > -1)
12246 CAlbum album;
12247 GetAlbum(idAlbum, album, true);
12248 album.MergeScrapedAlbum(importedAlbum, true);
12249 UpdateAlbum(album); //Will replace song artists if present in xml
12251 else
12252 CLog::Log(LOGDEBUG, "{} - Not import additional album data as {} not found", __FUNCTION__,
12253 importedAlbum.strAlbum);
12255 current++;
12257 entry = entry->NextSiblingElement();
12258 if (progressDialog && total)
12260 progressDialog->SetPercentage(current * 100 / total);
12261 progressDialog->SetLine(2, CVariant{std::move(strTitle)});
12262 progressDialog->Progress();
12263 if (progressDialog->IsCanceled())
12265 RollbackTransaction();
12266 return;
12270 CommitTransaction();
12272 // Import song playback history <song> entries found
12273 if (songtotal > 0)
12274 if (!ImportSongHistory(xmlFile, songtotal, progressDialog))
12275 return;
12277 CGUIComponent* gui = CServiceBroker::GetGUI();
12278 if (gui)
12279 gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
12281 catch (...)
12283 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
12284 RollbackTransaction();
12286 if (progressDialog)
12287 progressDialog->Close();
12290 bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile,
12291 const int total,
12292 CGUIDialogProgress* progressDialog)
12294 bool bHistSongExists = false;
12297 CXBMCTinyXML xmlDoc;
12298 if (!xmlDoc.LoadFile(xmlFile))
12299 return false;
12301 TiXmlElement* root = xmlDoc.RootElement();
12302 if (!root)
12303 return false;
12305 TiXmlElement* entry = root->FirstChildElement();
12306 int current = 0;
12308 if (progressDialog)
12310 progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history"
12311 progressDialog->SetLine(2, CVariant{""});
12314 // As can be many songs do in db, not song at a time which would be slow
12315 // Convert xml entries into a SQL bulk insert statement
12316 std::string strSQL;
12317 entry = root->FirstChildElement();
12318 while (entry)
12320 std::string strArtistDisp;
12321 std::string strTitle;
12322 int iTrack;
12323 std::string strFilename;
12324 std::string strMusicBrainzTrackID;
12325 std::string strAlbum;
12326 std::string strMusicBrainzAlbumID;
12327 std::string strAlbumArtistDisp;
12328 int iTimesplayed;
12329 std::string lastplayed;
12330 int iUserrating = 0;
12331 float fRating = 0.0;
12332 int iVotes;
12333 std::string strSQLSong;
12334 if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
12336 XMLUtils::GetString(entry, "artistdesc", strArtistDisp);
12337 XMLUtils::GetString(entry, "title", strTitle);
12338 XMLUtils::GetInt(entry, "track", iTrack);
12339 XMLUtils::GetString(entry, "filename", strFilename);
12340 XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID);
12341 XMLUtils::GetString(entry, "albumtitle", strAlbum);
12342 XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID);
12343 XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp);
12344 XMLUtils::GetInt(entry, "timesplayed", iTimesplayed);
12345 XMLUtils::GetString(entry, "lastplayed", lastplayed);
12346 const TiXmlElement* rElement = entry->FirstChildElement("rating");
12347 if (rElement)
12349 float rating = 0;
12350 float max_rating = 10;
12351 XMLUtils::GetFloat(entry, "rating", rating);
12352 if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
12353 rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
12354 if (rating > 10.f)
12355 rating = 10.f;
12356 fRating = rating;
12358 XMLUtils::GetInt(entry, "votes", iVotes);
12359 const TiXmlElement* userrating = entry->FirstChildElement("userrating");
12360 if (userrating)
12362 float rating = 0;
12363 float max_rating = 10;
12364 XMLUtils::GetFloat(entry, "userrating", rating);
12365 if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS &&
12366 max_rating >= 1)
12367 rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
12368 if (rating > 10.f)
12369 rating = 10.f;
12370 iUserrating = MathUtils::round_int(static_cast<double>(rating));
12373 strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack);
12374 strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(),
12375 strFilename.c_str());
12376 if (strMusicBrainzTrackID.empty())
12377 strSQLSong += PrepareSQL("NULL, ");
12378 else
12379 strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str());
12380 strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str());
12381 if (strMusicBrainzAlbumID.empty())
12382 strSQLSong += PrepareSQL("NULL, ");
12383 else
12384 strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str());
12385 strSQLSong += PrepareSQL("%d, ", iTimesplayed);
12386 if (lastplayed.empty())
12387 strSQLSong += PrepareSQL("NULL, ");
12388 else
12389 strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str());
12390 strSQLSong +=
12391 PrepareSQL("%.1f, %d, %d, -1, -1)", static_cast<double>(fRating), iVotes, iUserrating);
12393 if (current > 0)
12394 strSQLSong = ", " + strSQLSong;
12395 strSQL += strSQLSong;
12396 current++;
12399 entry = entry->NextSiblingElement();
12401 if ((current % 100) == 0 && progressDialog)
12403 progressDialog->SetPercentage(current * 100 / total);
12404 progressDialog->SetLine(3, CVariant{std::move(strTitle)});
12405 progressDialog->Progress();
12406 if (progressDialog->IsCanceled())
12407 return false;
12411 CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__,
12412 total);
12413 /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of
12414 song table using correlated subqueries to a temp table. An updatable join
12415 to temp table would work in MySQL but SQLite not support updatable joins.
12417 m_pDS->exec("CREATE TABLE HistSong ("
12418 "idSongSrc INTEGER primary key, "
12419 "strAlbum varchar(256), "
12420 "strMusicBrainzAlbumID text, "
12421 "strAlbumArtistDisp text, "
12422 "strArtistDisp text, strTitle varchar(512), "
12423 "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, "
12424 "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, "
12425 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
12426 "userrating INTEGER NOT NULL DEFAULT 0, "
12427 "idAlbum INTEGER, idSong INTEGER)");
12428 bHistSongExists = true;
12430 strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, "
12431 "strFileName, strMusicBrainzTrackID, "
12432 "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, "
12433 " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " +
12434 strSQL;
12435 m_pDS->exec(strSQL);
12437 if (progressDialog)
12439 progressDialog->SetLine(2, CVariant{38351}); //"Matching data"
12440 progressDialog->SetLine(3, CVariant{""});
12441 progressDialog->Progress();
12442 if (progressDialog->IsCanceled())
12444 m_pDS->exec("DROP TABLE HistSong");
12445 return false;
12449 BeginTransaction();
12450 // Match albums first on mbid then artist string and album title, setting idAlbum
12451 // mbid is unique so subquery can only return one result at most
12452 strSQL = "UPDATE HistSong "
12453 "SET idAlbum = (SELECT album.idAlbum FROM album "
12454 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) "
12455 "WHERE EXISTS(SELECT 1 FROM album "
12456 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0";
12457 m_pDS->exec(strSQL);
12459 // Can only be one album with same title and artist(s) and no mbid.
12460 // But could have 2 releases one with and one without mbid, match up those without mbid
12461 strSQL = "UPDATE HistSong "
12462 "SET idAlbum = (SELECT album.idAlbum FROM album "
12463 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12464 "AND HistSong.strAlbum = album.strAlbum "
12465 "AND album.strMusicBrainzAlbumID IS NULL "
12466 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
12467 "WHERE EXISTS(SELECT 1 FROM album "
12468 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12469 "AND HistSong.strAlbum = album.strAlbum "
12470 "AND album.strMusicBrainzAlbumID IS NULL "
12471 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
12472 "AND idAlbum < 0";
12473 m_pDS->exec(strSQL);
12475 // Try match rest by title and artist(s), prioritise one without mbid
12476 // Target could have multiple releases - with mbid (non-matching) or one without mbid
12477 strSQL = "UPDATE HistSong "
12478 "SET idAlbum = (SELECT album.idAlbum FROM album "
12479 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12480 "AND HistSong.strAlbum = album.strAlbum "
12481 "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
12482 "WHERE EXISTS(SELECT 1 FROM album "
12483 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12484 "AND HistSong.strAlbum = album.strAlbum) "
12485 "AND idAlbum < 0";
12486 m_pDS->exec(strSQL);
12487 if (progressDialog)
12489 progressDialog->Progress();
12490 if (progressDialog->IsCanceled())
12492 RollbackTransaction();
12493 m_pDS->exec("DROP TABLE HistSong");
12494 return false;
12498 // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong
12499 strSQL = "UPDATE HistSong "
12500 "SET idSong = (SELECT idsong FROM song "
12501 "WHERE HistSong.idAlbum = song.idAlbum AND "
12502 "HistSong.iTrack = song.iTrack AND "
12503 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) "
12504 "WHERE EXISTS(SELECT 1 FROM song "
12505 "WHERE HistSong.idAlbum = song.idAlbum AND "
12506 "HistSong.iTrack = song.iTrack AND "
12507 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0";
12508 m_pDS->exec(strSQL);
12510 // An album can have more than one song with same track and title (although idAlbum, track and
12511 // title is often unique), but not using filename as an identifier to allow for import of song
12512 // history for renamed files. It is about song playback not file playback.
12513 // Pick the first
12514 strSQL = "UPDATE HistSong "
12515 "SET idSong = (SELECT idsong FROM song "
12516 "WHERE HistSong.idAlbum = song.idAlbum AND "
12517 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
12518 "WHERE EXISTS(SELECT 1 FROM song "
12519 "WHERE HistSong.idAlbum = song.idAlbum AND "
12520 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0";
12521 m_pDS->exec(strSQL);
12523 CommitTransaction();
12524 if (progressDialog)
12526 progressDialog->Progress();
12527 if (progressDialog->IsCanceled())
12529 m_pDS->exec("DROP TABLE HistSong");
12530 return false;
12534 // Create an index to speed up the updates
12535 m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)");
12537 // Log how many songs matched
12538 int unmatched = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS);
12539 CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__,
12540 total - unmatched, total);
12542 if (progressDialog)
12544 progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history"
12545 progressDialog->Progress();
12546 if (progressDialog->IsCanceled())
12548 m_pDS->exec("DROP TABLE HistSong"); // Drops index too
12549 return false;
12553 /* Update song table using the song ids we have matched.
12554 Use correlated subqueries as SQLite does not support updatable joins.
12555 MySQL requires HistSong table not to be defined temporary for this.
12558 BeginTransaction();
12559 // Times played and last played date(when count is greater)
12560 strSQL = "UPDATE song SET iTimesPlayed = "
12561 "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), "
12562 "lastplayed = "
12563 "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) "
12564 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12565 "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)";
12566 m_pDS->exec(strSQL);
12568 // User rating
12569 strSQL = "UPDATE song SET userrating = "
12570 "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) "
12571 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12572 "HistSong.idSong = song.idSong AND HistSong.userrating > 0)";
12573 m_pDS->exec(strSQL);
12575 // Rating and votes
12576 strSQL = "UPDATE song SET rating = "
12577 "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), "
12578 "votes = "
12579 "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) "
12580 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12581 "HistSong.idSong = song.idSong AND HistSong.rating > 0)";
12582 m_pDS->exec(strSQL);
12584 if (progressDialog)
12586 progressDialog->Progress();
12587 if (progressDialog->IsCanceled())
12589 RollbackTransaction();
12590 m_pDS->exec("DROP TABLE HistSong");
12591 return false;
12594 CommitTransaction();
12596 // Tidy up temp table (index also removed)
12597 m_pDS->exec("DROP TABLE HistSong");
12598 // Compact db to recover space as had to add/drop actual table
12599 if (progressDialog)
12601 progressDialog->SetLine(2, CVariant{331});
12602 progressDialog->Progress();
12604 Compress(false);
12606 // Write event log entry
12607 // "Importing song history {1} of {2} songs matched", total - unmatched, total)
12608 std::string strLine =
12609 StringUtils::Format(g_localizeStrings.Get(38353), total - unmatched, total);
12611 auto eventLog = CServiceBroker::GetEventLog();
12612 if (eventLog)
12613 eventLog->Add(EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information)));
12615 return true;
12617 catch (...)
12619 CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
12620 RollbackTransaction();
12621 if (bHistSongExists)
12622 m_pDS->exec("DROP TABLE HistSong");
12624 return false;
12627 void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist)
12629 const std::string itemSeparator =
12630 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
12632 item.SetProperty("artist_sortname", artist.strSortName);
12633 item.SetProperty("artist_type", artist.strType);
12634 item.SetProperty("artist_gender", artist.strGender);
12635 item.SetProperty("artist_disambiguation", artist.strDisambiguation);
12636 item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator));
12637 item.SetProperty("artist_instrument_array", artist.instruments);
12638 item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator));
12639 item.SetProperty("artist_style_array", artist.styles);
12640 item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator));
12641 item.SetProperty("artist_mood_array", artist.moods);
12642 item.SetProperty("artist_born", artist.strBorn);
12643 item.SetProperty("artist_formed", artist.strFormed);
12644 item.SetProperty("artist_description", artist.strBiography);
12645 item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator));
12646 item.SetProperty("artist_genre_array", artist.genre);
12647 item.SetProperty("artist_died", artist.strDied);
12648 item.SetProperty("artist_disbanded", artist.strDisbanded);
12649 item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator));
12650 item.SetProperty("artist_yearsactive_array", artist.yearsActive);
12653 void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album)
12655 const std::string itemSeparator =
12656 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
12658 item.SetProperty("album_description", album.strReview);
12659 item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator));
12660 item.SetProperty("album_theme_array", album.themes);
12661 item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator));
12662 item.SetProperty("album_mood_array", album.moods);
12663 item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator));
12664 item.SetProperty("album_style_array", album.styles);
12665 item.SetProperty("album_type", album.strType);
12666 item.SetProperty("album_label", album.strLabel);
12667 item.SetProperty("album_artist", album.GetAlbumArtistString());
12668 item.SetProperty("album_artist_array", album.GetAlbumArtist());
12669 item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator));
12670 item.SetProperty("album_genre_array", album.genre);
12671 item.SetProperty("album_title", album.strAlbum);
12672 if (album.fRating > 0)
12673 item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating));
12674 if (album.iUserrating > 0)
12675 item.SetProperty("album_userrating", album.iUserrating);
12676 if (album.iVotes > 0)
12677 item.SetProperty("album_votes", album.iVotes);
12679 item.SetProperty("album_isboxset", album.bBoxedSet);
12680 item.SetProperty("album_totaldiscs", album.iTotalDiscs);
12681 item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType));
12682 item.SetProperty("album_duration",
12683 StringUtils::SecondsToTimeString(album.iAlbumDuration,
12684 static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS)));
12687 void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item)
12689 if (!item.HasMusicInfoTag())
12690 return;
12691 // May already have song artist ids as item property set when data read from
12692 // db, but check property is valid array (scripts could set item properties
12693 // incorrectly), otherwise try to fetch artist by name.
12694 int idArtist = -1;
12695 if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray())
12697 CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array();
12698 idArtist = static_cast<int>(varid->asInteger());
12700 else
12701 idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString());
12702 if (idArtist > -1)
12704 CArtist artist;
12705 if (GetArtist(idArtist, artist))
12706 SetPropertiesFromArtist(item, artist);
12708 int idAlbum = item.GetMusicInfoTag()->GetAlbumId();
12709 if (idAlbum <= 0)
12710 idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(),
12711 item.GetMusicInfoTag()->GetArtistString());
12712 if (idAlbum > -1)
12714 CAlbum album;
12715 if (GetAlbum(idAlbum, album, false))
12716 SetPropertiesFromAlbum(item, album);
12720 void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType)
12722 std::string strSQL;
12725 if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong)
12726 return;
12727 if (nullptr == m_pDB)
12728 return;
12729 if (nullptr == m_pDS)
12730 return;
12732 // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
12733 // e.g. when artwork for item is changed from info dialog but not item details.
12734 // Use SQL UPDATE that does not change record data.
12735 if (mediaType == MediaTypeArtist)
12736 strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId);
12737 else if (mediaType == MediaTypeAlbum)
12738 strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId);
12739 else // MediaTypeSong
12740 strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId);
12741 m_pDS->exec(strSQL);
12743 catch (...)
12745 CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__,
12746 mediaId, mediaType, strSQL);
12750 void CMusicDatabase::SetArtForItem(int mediaId,
12751 const std::string& mediaType,
12752 const std::map<std::string, std::string>& art)
12754 for (const auto& i : art)
12755 SetArtForItem(mediaId, mediaType, i.first, i.second);
12758 void CMusicDatabase::SetArtForItem(int mediaId,
12759 const std::string& mediaType,
12760 const std::string& artType,
12761 const std::string& url)
12765 if (nullptr == m_pDB)
12766 return;
12767 if (nullptr == m_pDS)
12768 return;
12770 // don't set <foo>.<bar> art types - these are derivative types from parent items
12771 if (artType.find('.') != std::string::npos)
12772 return;
12774 std::string sql = PrepareSQL("SELECT art_id FROM art "
12775 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
12776 mediaId, mediaType.c_str(), artType.c_str());
12777 m_pDS->query(sql);
12778 if (!m_pDS->eof())
12779 { // update
12780 int artId = m_pDS->fv(0).get_asInt();
12781 m_pDS->close();
12782 sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
12783 m_pDS->exec(sql);
12785 else
12786 { // insert
12787 m_pDS->close();
12788 sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
12789 "VALUES (%d, '%s', '%s', '%s')",
12790 mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
12791 m_pDS->exec(sql);
12794 catch (...)
12796 CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType,
12797 artType, url);
12801 bool CMusicDatabase::GetArtForItem(
12802 int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader>& art)
12804 std::string strSQL;
12807 if (!(songId > 0 || albumId > 0 || artistId > 0))
12808 return false;
12809 if (nullptr == m_pDB)
12810 return false;
12811 if (nullptr == m_pDS2)
12812 return false; // using dataset 2 as we're likely called in loops on dataset 1
12814 Filter filter;
12815 if (songId > 0)
12816 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong));
12817 if (albumId > 0)
12818 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum),
12819 false);
12820 if (artistId > 0)
12821 filter.AppendWhere(
12822 PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false);
12824 strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder "
12825 "FROM art";
12826 if (!BuildSQL(strSQL, filter, strSQL))
12827 return false;
12829 if (!(artistId > 0))
12831 // Artist ID unknown, so lookup album artist for albums and songs
12832 std::string strSQL2;
12833 if (albumId > 0)
12835 //Album ID known, so use it to look up album artist(s)
12836 strSQL2 = PrepareSQL(
12837 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12838 "url, album_artist.iOrder as iorder FROM art "
12839 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12840 "WHERE album_artist.idAlbum = %i ",
12841 MediaTypeArtist, albumId);
12842 if (bPrimaryArtist)
12843 strSQL2 += "AND album_artist.iOrder = 0";
12845 strSQL = strSQL + " UNION " + strSQL2;
12847 if (songId > 0)
12849 if (albumId < 0)
12851 //Album ID unknown, so get from song to look up album artist(s)
12852 strSQL2 = PrepareSQL(
12853 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12854 "url, album_artist.iOrder as iorder FROM art "
12855 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12856 "JOIN song ON song.idAlbum = album_artist.idAlbum "
12857 "WHERE song.idSong = %i ",
12858 MediaTypeArtist, songId);
12859 if (bPrimaryArtist)
12860 strSQL2 += "AND album_artist.iOrder = 0";
12862 strSQL = strSQL + " UNION " + strSQL2;
12865 // Artist ID unknown, so lookup artist for songs (could be different from album artist)
12866 strSQL2 = PrepareSQL(
12867 "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
12868 "url, song_artist.iOrder as iorder FROM art "
12869 "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
12870 "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
12871 MediaTypeArtist, songId, ROLE_ARTIST);
12872 if (bPrimaryArtist)
12873 strSQL2 += "AND song_artist.iOrder = 0";
12875 strSQL = strSQL + " UNION " + strSQL2;
12878 if (songId > 0 && albumId < 0)
12880 //Album ID unknown, so get from song to look up album art
12881 std::string strSQL2;
12882 strSQL2 = PrepareSQL("SELECT art_id, media_id, media_type, type, '' as prefix, "
12883 "url, 0 as iorder FROM art "
12884 "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
12885 "WHERE song.idSong = %i ",
12886 MediaTypeAlbum, songId);
12887 strSQL = strSQL + " UNION " + strSQL2;
12890 m_pDS2->query(strSQL);
12891 while (!m_pDS2->eof())
12893 ArtForThumbLoader artitem;
12894 artitem.artType = m_pDS2->fv("type").get_asString();
12895 artitem.mediaType = m_pDS2->fv("media_type").get_asString();
12896 artitem.prefix = m_pDS2->fv("prefix").get_asString();
12897 artitem.url = m_pDS2->fv("url").get_asString();
12898 int iOrder = m_pDS2->fv("iorder").get_asInt();
12899 // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
12900 if (iOrder > 0)
12901 artitem.prefix += m_pDS2->fv("iorder").get_asString();
12903 art.emplace_back(artitem);
12904 m_pDS2->next();
12906 m_pDS2->close();
12907 return !art.empty();
12909 catch (...)
12911 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strSQL);
12913 return false;
12916 bool CMusicDatabase::GetArtForItem(int mediaId,
12917 const std::string& mediaType,
12918 std::map<std::string, std::string>& art)
12922 if (nullptr == m_pDB)
12923 return false;
12924 if (nullptr == m_pDS2)
12925 return false; // using dataset 2 as we're likely called in loops on dataset 1
12927 std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'",
12928 mediaId, mediaType.c_str());
12929 m_pDS2->query(sql);
12930 while (!m_pDS2->eof())
12932 art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
12933 m_pDS2->next();
12935 m_pDS2->close();
12936 return !art.empty();
12938 catch (...)
12940 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId);
12942 return false;
12945 std::string CMusicDatabase::GetArtForItem(int mediaId,
12946 const std::string& mediaType,
12947 const std::string& artType)
12949 std::string query = PrepareSQL("SELECT url FROM art "
12950 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
12951 mediaId, mediaType.c_str(), artType.c_str());
12952 return GetSingleValue(query, m_pDS2);
12955 bool CMusicDatabase::RemoveArtForItem(int mediaId,
12956 const MediaType& mediaType,
12957 const std::string& artType)
12959 return ExecuteQuery(PrepareSQL("DELETE FROM art "
12960 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
12961 mediaId, mediaType.c_str(), artType.c_str()));
12964 bool CMusicDatabase::RemoveArtForItem(int mediaId,
12965 const MediaType& mediaType,
12966 const std::set<std::string>& artTypes)
12968 bool result = true;
12969 for (const auto& i : artTypes)
12970 result &= RemoveArtForItem(mediaId, mediaType, i);
12972 return result;
12975 bool CMusicDatabase::GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes)
12979 if (nullptr == m_pDB)
12980 return false;
12981 if (nullptr == m_pDS)
12982 return false;
12984 std::string strSQL =
12985 PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
12987 if (!m_pDS->query(strSQL))
12988 return false;
12989 int iRowsFound = m_pDS->num_rows();
12990 if (iRowsFound == 0)
12992 m_pDS->close();
12993 return false;
12996 while (!m_pDS->eof())
12998 artTypes.emplace_back(m_pDS->fv(0).get_asString());
12999 m_pDS->next();
13001 m_pDS->close();
13002 return true;
13004 catch (...)
13006 CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType);
13008 return false;
13011 std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId,
13012 const MediaType& mediaType)
13014 CScraperUrl thumbURL;
13015 if (mediaType == MediaTypeArtist)
13017 CArtist artist;
13018 if (GetArtist(mediaId, artist))
13019 thumbURL = artist.thumbURL;
13021 else if (mediaType == MediaTypeAlbum)
13023 CAlbum album;
13024 if (GetAlbum(mediaId, album))
13025 thumbURL = album.thumbURL;
13028 std::vector<std::string> result;
13029 for (const auto& urlEntry : thumbURL.GetUrls())
13031 std::string artType = urlEntry.m_aspect;
13032 if (artType.empty())
13033 artType = "thumb";
13034 if (std::find(result.begin(), result.end(), artType) == result.end())
13035 result.push_back(artType);
13037 return result;
13040 std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem(
13041 int mediaId, const MediaType& mediaType, const std::string& artType)
13043 CScraperUrl thumbURL;
13044 if (mediaType == MediaTypeArtist)
13046 CArtist artist;
13047 if (GetArtist(mediaId, artist))
13048 thumbURL = artist.thumbURL;
13050 else if (mediaType == MediaTypeAlbum)
13052 CAlbum album;
13053 if (GetAlbum(mediaId, album))
13054 thumbURL = album.thumbURL;
13057 std::vector<CScraperUrl::SUrlEntry> result;
13058 for (auto urlEntry : thumbURL.GetUrls())
13060 if (urlEntry.m_aspect.empty())
13061 urlEntry.m_aspect = "thumb";
13062 if (artType.empty() || urlEntry.m_aspect == artType)
13063 result.push_back(urlEntry);
13065 return result;
13068 int CMusicDatabase::GetOrderFilter(const std::string& type,
13069 const SortDescription& sorting,
13070 Filter& filter)
13072 // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
13073 int iFieldsAdded = 0;
13074 filter.fields.clear(); // remove "*"
13075 std::vector<std::string> orderfields;
13076 std::string DESC;
13078 if (sorting.sortOrder == SortOrderDescending)
13079 DESC = " DESC";
13081 if (sorting.sortBy == SortByRandom)
13082 orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
13083 else
13085 FieldList fields;
13086 SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields);
13087 for (const auto& it : fields)
13089 std::string strField;
13090 if (it == FieldYear)
13091 strField = "iYear";
13092 else
13093 strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect);
13094 if (!strField.empty())
13095 orderfields.emplace_back(strField);
13099 // Convert field names into order by statement elements
13100 for (auto& name : orderfields)
13102 //Add field for adjusted name sorting using sort name and ignoring articles
13103 std::string sortSQL;
13104 if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist"))
13106 if (StringUtils::EndsWith(name, "strArtists"))
13107 sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort");
13108 else
13109 sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName");
13110 if (!sortSQL.empty())
13112 name = "artistsortname";
13113 filter.AppendField(sortSQL); // Add artistsortname as scalar query field
13114 iFieldsAdded++;
13116 // Natural number case-insensitive sort
13117 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
13119 else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle"))
13121 sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, "");
13122 if (!sortSQL.empty())
13124 name = "titlesortname";
13125 filter.AppendField(sortSQL); // Add sortname as scalar query field
13126 iFieldsAdded++;
13128 // Natural number case-insensitive sort
13129 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
13131 else if (StringUtils::EndsWith(name, "strGenres"))
13132 // Natural number case-insensitive sort
13133 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
13134 else
13135 filter.AppendOrder(name + DESC);
13137 return iFieldsAdded;
13140 bool CMusicDatabase::GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting)
13142 if (!musicUrl.IsValid())
13143 return false;
13145 std::string type = musicUrl.GetType();
13146 const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
13148 // Check for playlist rules first, they may contain role criteria
13149 bool hasRoleRules = false;
13151 auto option = options.find("xsp");
13152 if (option != options.end())
13154 CSmartPlaylist xsp;
13155 if (!xsp.LoadFromJson(option->second.asString()))
13156 return false;
13158 std::set<std::string> playlists;
13159 std::string xspWhere;
13160 xspWhere = xsp.GetWhereClause(*this, playlists);
13161 hasRoleRules = xsp.GetType() == "artists" &&
13162 xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos;
13164 // Check if the filter playlist matches the item type
13165 // Allow for grouping name like "originalyears" and type "years"
13166 if (xsp.GetType() == type ||
13167 (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed()))
13169 filter.AppendWhere(xspWhere);
13171 if (xsp.GetLimit() > 0)
13172 sorting.limitEnd = xsp.GetLimit();
13173 if (xsp.GetOrder() != SortByNone)
13174 sorting.sortBy = xsp.GetOrder();
13175 sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
13176 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
13177 CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
13178 sorting.sortAttributes = SortAttributeIgnoreArticle;
13182 //Process role options, common to artist and album type filtering
13183 int idRole = 1; // Default restrict song_artist to "artists" only, no other roles.
13184 option = options.find("roleid");
13185 if (option != options.end())
13186 idRole = static_cast<int>(option->second.asInteger());
13187 else
13189 option = options.find("role");
13190 if (option != options.end())
13192 if (option->second.asString() == "all" || option->second.asString() == "%")
13193 idRole = -1000; //All roles
13194 else
13195 idRole = GetRoleByName(option->second.asString());
13198 if (hasRoleRules)
13200 // Get Role from role rule(s) here.
13201 // But that requires much change, so for now get all roles as better than none
13202 idRole = -1000; //All roles
13205 std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role
13206 if (idRole > 0)
13207 strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole);
13209 int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1;
13210 int idDisc = -1;
13211 int idSource = -1;
13212 bool albumArtistsOnly = false;
13213 bool useOriginalYear = false;
13214 std::string artistname;
13216 // Process useoriginalyear option, setting overridden by option
13217 useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
13218 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
13219 option = options.find("useoriginalyear");
13220 if (option != options.end())
13221 useOriginalYear = option->second.asBoolean();
13223 // Process albumartistsonly option
13224 option = options.find("albumartistsonly");
13225 if (option != options.end())
13226 albumArtistsOnly = option->second.asBoolean();
13228 // Process genre option
13229 option = options.find("genreid");
13230 if (option != options.end())
13231 idGenre = static_cast<int>(option->second.asInteger());
13232 else
13234 option = options.find("genre");
13235 if (option != options.end())
13236 idGenre = GetGenreByName(option->second.asString());
13239 // Process source option
13240 option = options.find("sourceid");
13241 if (option != options.end())
13242 idSource = static_cast<int>(option->second.asInteger());
13243 else
13245 option = options.find("source");
13246 if (option != options.end())
13247 idSource = GetSourceByName(option->second.asString());
13250 // Process album option
13251 option = options.find("albumid");
13252 if (option != options.end())
13253 idAlbum = static_cast<int>(option->second.asInteger());
13254 else
13256 option = options.find("album");
13257 if (option != options.end())
13258 idAlbum = GetAlbumByName(option->second.asString());
13261 // Process artist option
13262 option = options.find("artistid");
13263 if (option != options.end())
13264 idArtist = static_cast<int>(option->second.asInteger());
13265 else
13267 option = options.find("artist");
13268 if (option != options.end())
13270 idArtist = GetArtistByName(option->second.asString());
13271 if (idArtist == -1)
13272 { // not found with that name, or more than one found as artist name is not unique
13273 artistname = option->second.asString();
13278 // Process song option
13279 option = options.find("songid");
13280 if (option != options.end())
13281 idSong = static_cast<int>(option->second.asInteger());
13283 if (type == "artists")
13285 if (!hasRoleRules)
13286 { // Not an "artists" smart playlist with roles rules, so get filter from options
13287 if (idArtist > 0)
13288 filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist));
13289 else if (idAlbum > 0)
13290 filter.AppendWhere(
13291 PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
13292 "WHERE album_artist.idAlbum = %i)",
13293 idAlbum));
13294 else if (idSong > 0)
13296 filter.AppendWhere(
13297 PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
13298 "WHERE song_artist.idSong = %i %s)",
13299 idSong, strRoleSQL.c_str()));
13301 else
13302 { /*
13303 Process idRole, idGenre, idSource and albumArtistsOnly options
13305 For artists these rules are combined because they apply via album and song
13306 and so we need to ensure all criteria are met via the same album or song.
13307 1) Some artists may be only album artists, so for all artists (with linked
13308 albums or songs) we need to check both album_artist and song_artist tables.
13309 2) Role is determined from song_artist table, so even if looking for album artists
13310 only we find those that also have a specific role e.g. which album artist is a
13311 composer of songs in that album, from entries in the song_artist table.
13312 a) Role < -1 is used to indicate that all roles are wanted.
13313 b) When not album artists only and a specific role wanted then only the song_artist
13314 table is checked.
13315 c) When album artists only and role = 1 (an "artist") then only the album_artist
13316 table is checked.
13318 std::string albumArtistSQL, songArtistSQL;
13319 ExistsSubQuery albumArtistSub("album_artist",
13320 "album_artist.idArtist = artistview.idArtist");
13321 // Prepare album artist subquery SQL
13322 if (idSource > 0)
13324 if (idRole == 1 && idGenre < 0)
13326 albumArtistSub.AppendJoin(
13327 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
13328 albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
13330 else
13332 albumArtistSub.AppendWhere(
13333 PrepareSQL("EXISTS(SELECT 1 FROM album_source "
13334 "WHERE album_source.idSource = %i "
13335 "AND album_source.idAlbum = album_artist.idAlbum)",
13336 idSource));
13339 if (idRole <= 1 && idGenre > 0)
13340 { // Check genre of songs of album using nested subquery
13341 std::string strGenre =
13342 PrepareSQL("EXISTS(SELECT 1 FROM song "
13343 "JOIN song_genre ON song_genre.idSong = song.idSong "
13344 "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)",
13345 idGenre);
13346 albumArtistSub.AppendWhere(strGenre);
13349 // Prepare song artist subquery SQL
13350 ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
13351 if (idRole > 0)
13352 songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
13353 if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1)
13355 songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
13356 "JOIN song_genre ON song_genre.idSong = song.idSong "
13357 "WHERE song.idSong = song_artist.idSong "
13358 "AND song_genre.idGenre = %i "
13359 "AND EXISTS(SELECT 1 FROM album_source "
13360 "WHERE album_source.idSource = %i "
13361 "AND album_source.idAlbum = song.idAlbum))",
13362 idGenre, idSource));
13364 else
13366 if (idGenre > 0)
13368 songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
13369 songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
13371 if (idSource > 0 && !albumArtistsOnly)
13373 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13374 songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
13375 songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
13377 if (idRole > 1 && albumArtistsOnly)
13378 { // Album artists only with role, check AND in album_artist for album of song
13379 // using nested subquery correlated with album_artist
13380 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13381 songArtistSub.param = "song_artist.idArtist = album_artist.idArtist";
13382 songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum");
13386 // Build filter clause from subqueries
13387 if (idRole > 1 && albumArtistsOnly)
13388 { // Album artists only with role, check AND in album_artist for album of song
13389 // using nested subquery correlated with album_artist
13390 songArtistSub.BuildSQL(songArtistSQL);
13391 albumArtistSub.AppendWhere(songArtistSQL);
13392 albumArtistSub.BuildSQL(albumArtistSQL);
13393 filter.AppendWhere(albumArtistSQL);
13395 else
13397 songArtistSub.BuildSQL(songArtistSQL);
13398 albumArtistSub.BuildSQL(albumArtistSQL);
13399 if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
13400 { // Artist contributing to songs, any role, check OR album artist too
13401 // as artists can be just album artists but not song artists
13402 filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
13404 else if (idRole > 1)
13406 // Artist contributes that role (not albmartistsonly as already handled)
13407 filter.AppendWhere(songArtistSQL);
13409 else // idRole = 1 and albumArtistsOnly
13410 { // Only look at album artists, not albums where artist features on songs
13411 filter.AppendWhere(albumArtistSQL);
13416 // remove the null string
13417 filter.AppendWhere("artistview.strArtist != ''");
13419 else if (type == "albums")
13421 option = options.find("year");
13422 if (option != options.end())
13424 if (!useOriginalYear)
13425 filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
13426 option->second.asString().c_str()));
13427 else
13428 filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
13429 option->second.asString().c_str()));
13431 option = options.find("compilation");
13432 if (option != options.end())
13433 filter.AppendWhere(
13434 PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
13436 option = options.find("boxset");
13437 if (option != options.end())
13438 filter.AppendWhere(
13439 PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
13441 if (idSource > 0)
13442 filter.AppendWhere(PrepareSQL(
13443 "EXISTS(SELECT 1 FROM album_source "
13444 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
13445 idSource));
13447 // Process artist, role and genre options together as song subquery to filter those
13448 // albums that have songs with both that artist and genre
13449 std::string albumArtistSQL, songArtistSQL, genreSQL;
13450 ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum");
13451 genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
13452 genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
13453 ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
13454 ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
13455 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13457 if (idArtist > 0)
13459 songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist));
13460 albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist));
13462 else if (!artistname.empty())
13463 { // Artist name is not unique, so could get albums or songs from more than one.
13464 songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
13465 songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
13467 albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
13468 albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
13470 if (idRole > 0)
13471 songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
13472 if (idGenre > 0)
13474 songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
13475 songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
13478 if (idArtist > 0 || !artistname.empty())
13480 if (idRole <= 1 && idGenre > 0)
13481 { // Check genre of songs of album using nested subquery
13482 genreSub.BuildSQL(genreSQL);
13483 albumArtistSub.AppendWhere(genreSQL);
13485 if (idRole > 1 && albumArtistsOnly)
13486 { // Album artists only with role, check AND in album_artist for same song
13487 // using nested subquery correlated with album_artist
13488 songArtistSub.param = "song.idAlbum = album_artist.idAlbum";
13489 songArtistSub.BuildSQL(songArtistSQL);
13490 albumArtistSub.AppendWhere(songArtistSQL);
13491 albumArtistSub.BuildSQL(albumArtistSQL);
13492 filter.AppendWhere(albumArtistSQL);
13494 else
13496 songArtistSub.BuildSQL(songArtistSQL);
13497 albumArtistSub.BuildSQL(albumArtistSQL);
13498 if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
13499 { // Artist contributing to songs, any role, check OR album artist too
13500 // as artists can be just album artists but not song artists
13501 filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
13503 else if (idRole > 1)
13504 { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
13505 filter.AppendWhere(songArtistSQL);
13507 else // idRole = 1 and albumArtistsOnly
13508 { // Only look at album artists, not albums where artist features on songs
13509 // This may want to be a separate option so you can choose to see all the albums where that artist
13510 // appears on one or more songs without having to list all song artists in the artists node.
13511 filter.AppendWhere(albumArtistSQL);
13515 else
13516 { // No artist given
13517 if (idGenre > 0)
13518 { // Have genre option but not artist
13519 genreSub.param = "song.idAlbum = albumview.idAlbum";
13520 genreSub.BuildSQL(genreSQL);
13521 filter.AppendWhere(genreSQL);
13523 // Exclude any single albums (aka empty tagged albums)
13524 // This causes "albums" media filter artist selection to only offer album artists
13525 option = options.find("show_singles");
13526 if (option == options.end() || !option->second.asBoolean())
13527 filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'",
13528 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
13531 else if (type == "discs")
13533 if (idAlbum > 0)
13534 filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum));
13535 else
13537 option = options.find("year");
13538 if (option != options.end())
13540 if (!useOriginalYear)
13541 filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
13542 option->second.asString().c_str()));
13543 else
13544 filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
13545 option->second.asString().c_str()));
13548 option = options.find("compilation");
13549 if (option != options.end())
13550 filter.AppendWhere(
13551 PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
13553 option = options.find("boxset");
13554 if (option != options.end())
13555 filter.AppendWhere(
13556 PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
13558 if (idSource > 0)
13559 filter.AppendWhere(PrepareSQL(
13560 "EXISTS(SELECT 1 FROM album_source "
13561 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
13562 idSource));
13564 option = options.find("discid");
13565 if (option != options.end())
13566 filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger()));
13568 option = options.find("disctitle");
13569 if (option != options.end())
13570 filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str()));
13572 if (idGenre > 0)
13573 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
13574 "song.idSong AND song_genre.idGenre = %i)",
13575 idGenre));
13577 std::string songArtistClause, albumArtistClause;
13578 if (idArtist > 0)
13580 songArtistClause =
13581 PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
13582 "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
13583 idArtist, strRoleSQL.c_str());
13584 albumArtistClause =
13585 PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
13586 "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
13587 idArtist);
13589 else if (!artistname.empty())
13590 { // Artist name is not unique, so could get songs from more than one.
13591 songArtistClause = PrepareSQL(
13592 "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
13593 "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
13594 artistname.c_str(), strRoleSQL.c_str());
13595 albumArtistClause =
13596 PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
13597 "album_artist.idArtist "
13598 "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
13599 artistname.c_str());
13602 // Process artist name or id option
13603 if (!songArtistClause.empty())
13605 if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
13606 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13607 else if (idRole > 1)
13609 if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
13610 filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
13611 else // songs where artist contributes that role.
13612 filter.AppendWhere(songArtistClause);
13614 else
13616 if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
13617 filter.AppendWhere(albumArtistClause);
13618 else // Artist is song artist or album artist
13619 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13623 else if (type == "songs" || type == "singles")
13625 option = options.find("singles");
13626 if (option != options.end())
13627 filter.AppendWhere(PrepareSQL(
13628 "songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
13629 option->second.asBoolean() ? "" : "NOT ",
13630 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
13632 // When have idAlbum skip year, compilation, boxset criteria as already applied via album
13633 if (idAlbum < 0)
13635 option = options.find("year");
13636 if (option != options.end())
13638 if (!useOriginalYear)
13639 filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
13640 option->second.asString().c_str()));
13641 else
13642 filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
13643 option->second.asString().c_str()));
13645 option = options.find("compilation");
13646 if (option != options.end())
13647 filter.AppendWhere(
13648 PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
13650 option = options.find("boxset");
13651 if (option != options.end())
13652 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
13653 "songview.idAlbum AND bBoxedSet = %i)",
13654 option->second.asBoolean() ? 1 : 0));
13657 option = options.find("discid");
13658 if (option != options.end())
13659 idDisc = static_cast<int>(option->second.asInteger());
13661 option = options.find("disctitle");
13662 if (option != options.end())
13663 filter.AppendWhere(
13664 PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str()));
13666 if (idSong > 0)
13667 filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong));
13669 if (idAlbum > 0)
13670 filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum));
13672 if (idDisc > 0)
13673 filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc));
13675 if (idGenre > 0)
13676 filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre "
13677 "WHERE song_genre.idGenre = %i)",
13678 idGenre));
13680 if (idSource > 0)
13681 filter.AppendWhere(PrepareSQL(
13682 "EXISTS(SELECT 1 FROM album_source "
13683 "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)",
13684 idSource));
13686 std::string songArtistClause, albumArtistClause;
13687 if (idArtist > 0)
13689 songArtistClause =
13690 PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
13691 "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
13692 idArtist, strRoleSQL.c_str());
13693 albumArtistClause = PrepareSQL(
13694 "EXISTS (SELECT 1 FROM album_artist "
13695 "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
13696 idArtist);
13698 else if (!artistname.empty())
13699 { // Artist name is not unique, so could get songs from more than one.
13700 songArtistClause = PrepareSQL(
13701 "EXISTS (SELECT 1 FROM song_artist "
13702 "JOIN artist ON artist.idArtist = song_artist.idArtist "
13703 "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
13704 artistname.c_str(), strRoleSQL.c_str());
13705 albumArtistClause = PrepareSQL(
13706 "EXISTS (SELECT 1 FROM album_artist "
13707 "JOIN artist ON artist.idArtist = album_artist.idArtist "
13708 "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
13709 artistname.c_str());
13712 // Process artist name or id option
13713 if (!songArtistClause.empty())
13715 if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
13716 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13717 else if (idRole > 1)
13719 if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
13720 filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
13721 else // songs where artist contributes that role.
13722 filter.AppendWhere(songArtistClause);
13724 else
13726 if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
13727 filter.AppendWhere(albumArtistClause);
13728 else // Artist is song artist or album artist
13729 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
13734 option = options.find("filter");
13735 if (option != options.end())
13737 CSmartPlaylist xspFilter;
13738 if (!xspFilter.LoadFromJson(option->second.asString()))
13739 return false;
13741 // check if the filter playlist matches the item type
13742 if (xspFilter.GetType() == type)
13744 std::set<std::string> playlists;
13745 filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
13747 // remove the filter if it doesn't match the item type
13748 else
13749 musicUrl.RemoveOption("filter");
13752 return true;
13755 std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath)
13757 if (strFileNameAndPath.empty())
13758 return std::string();
13760 CDateTime dateMedia;
13761 int code;
13762 code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded;
13763 // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
13764 if (code == 1)
13765 dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath);
13766 //2 using the newer datetime of the file's mtime and ctime
13767 else if (code == 2)
13768 dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath);
13769 //3 using the older datetime of the file's mtime and ctime
13770 else if (code == 3)
13771 dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath);
13772 //0 using the current datetime if none of the above matches or one returns an invalid datetime
13773 if (!dateMedia.IsValid())
13774 dateMedia = CDateTime::GetCurrentDateTime();
13776 return dateMedia.GetAsDBDateTime();
13779 bool CMusicDatabase::AddAudioBook(const CFileItem& item)
13781 auto const& artists = item.GetMusicInfoTag()->GetArtist();
13782 std::string strSQL = PrepareSQL(
13783 "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
13784 "VALUES (NULL,'%s','%s',%i,'%s','%s')",
13785 item.GetMusicInfoTag()->GetAlbum().c_str(), artists.empty() ? "" : artists[0].c_str(), 0,
13786 item.GetDynPath().c_str(), CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
13787 return ExecuteQuery(strSQL);
13790 bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark)
13792 std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook "
13793 "WHERE file='%s'",
13794 item.GetDynPath().c_str());
13795 if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13797 if (!AddAudioBook(item))
13798 return false;
13801 strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i "
13802 "WHERE file='%s'",
13803 bookmark, item.GetDynPath().c_str());
13805 return ExecuteQuery(strSQL);
13808 bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark)
13810 std::string strSQL =
13811 PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'", item.GetDynPath().c_str());
13812 if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13813 return false;
13815 bookmark = m_pDS->fv(0).get_asInt();
13816 return true;