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.
9 #include "MusicDatabase.h"
14 #include "GUIInfoManager.h"
16 #include "ServiceBroker.h"
18 #include "TextureCache.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"
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
79 using namespace MEDIA_DETECT
;
82 static void AnnounceRemove(const std::string
& content
, int id
)
85 data
["type"] = content
;
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)
95 data
["type"] = content
;
97 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
98 data
["transaction"] = true;
100 data
["added"] = true;
101 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnUpdate", data
);
104 CMusicDatabase::CMusicDatabase(void)
106 m_translateBlankArtist
= true;
109 CMusicDatabase::~CMusicDatabase(void)
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, "
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].
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());
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, "
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, "
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");
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';"
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';"
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';"
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;"
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");
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;"
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;"
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;"
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;"
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;"
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;"
356 m_pDS
->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
357 " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
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();"
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();"
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();"
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; "
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; "
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
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);"
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);"
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,"
428 " iTrack, iDuration, "
429 " song.strReleaseDate as strReleaseDate, "
430 " song.strOrigReleaseDate as strOrigReleaseDate, "
431 " song.strDiscSubtitle as strDiscSubtitle, "
433 " strMusicBrainzTrackID, "
434 " iTimesPlayed, iStartOffset, iEndOffset, "
440 " song.idAlbum AS idAlbum, "
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, "
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 "
462 " song.idAlbum=album.idAlbum"
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, "
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, "
485 "album.strImage as strImage, "
487 "album.iUserrating, "
492 "dateAdded, dateNew, dateModified, "
493 "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song "
494 "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
497 "(SELECT MAX(song.lastplayed) FROM song "
498 "WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
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 "
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, "
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 "
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 "
541 " song_artist.idArtist = artist.idArtist "
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
,
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
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"
567 "SQL SECURITY INVOKER \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"
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"
593 "SQL SECURITY INVOKER\n"
595 " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
597 " DECLARE symbolCharsLen int;\n"
598 " DECLARE symbol char(1);\n"
599 " SET sortString = instring;\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"
607 " RETURN sortString;\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"
618 "SQL SECURITY INVOKER\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"
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"
639 " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
640 " WHILE(i <= sameOrderCharsLen) DO \n"
641 " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \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"
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"
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"
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"
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"
680 " IF totalSympadLength + shiftedLength > 1024 THEN \n"
681 " SET shiftedLength = 1024 - totalSympadLength; \n"
682 " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
684 " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
686 " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
688 " SET sortString = shiftedString; \n"
690 " RETURN sortString; \n"
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
)
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
, //
725 album
.strReleaseStatus
, //
726 album
.bCompilation
, //
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();
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
)));
744 for (auto song
= album
.songs
.begin(); song
!= album
.songs
.end(); ++song
)
746 song
->idAlbum
= album
.idAlbum
;
748 song
->idSong
= AddSong(song
->idSong
, //
752 song
->strMusicBrainzTrackID
, //
753 song
->strFileName
, //
757 song
->GetArtistString(), //
758 song
->GetArtistSort(), //
762 song
->strReleaseDate
, //
763 song
->strOrigReleaseDate
, //
764 song
->strDiscSubtitle
, //
765 song
->iTimesPlayed
, //
766 song
->iStartOffset
, song
->iEndOffset
, //
771 song
->iBPM
, song
->iBitRate
, song
->iSampleRate
, song
->iChannels
, //
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();
781 artistCredit
->idArtist
=
782 AddArtist(artistCredit
->GetArtist(), artistCredit
->GetMusicBrainzArtistID(),
783 artistCredit
->GetSortName());
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
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
,
804 AddAlbumSource(album
.idAlbum
, idSource
);
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
816 PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
817 "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
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",
824 int numTitles
= GetSingleValueInt(strSQL
);
827 strSQL
= PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album
.idAlbum
);
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",
835 PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
836 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
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());
863 bool CMusicDatabase::UpdateAlbum(CAlbum
& album
)
866 SetLibraryLastUpdated();
868 const std::string itemSeparator
=
869 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
873 bool isBoxset
= IsAlbumBoxset(album
.idAlbum
);
875 { // not a boxset already so make sure we have enough discs & they all have titles
876 bool canBeBoxset
= false;
878 strSQL
= PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album
.idAlbum
);
879 int numDiscs
= GetSingleValueInt(strSQL
);
884 for (discValue
= 1; discValue
<= numDiscs
; discValue
++)
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
);
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
), //
918 album
.thumbURL
.GetData(), //
921 album
.strReleaseStatus
, //
922 album
.fRating
, album
.iUserrating
, album
.iVotes
, //
923 album
.strReleaseDate
, //
924 album
.strOrigReleaseDate
, //
926 album
.bCompilation
, //
927 album
.releaseType
, //
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());
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();
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
)
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();
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
981 iDate
= StringUtils::DateStringToYYYYMMDD(strRelease
);
984 iDate
= StringUtils::DateStringToYYYYMMDD(strOriginal
);
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
,
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
,
1008 const std::string
& strReleaseDate
,
1009 const std::string
& strOrigReleaseDate
,
1010 std::string
& strDiscSubtitle
,
1011 const int iTimesPlayed
,
1014 const CDateTime
& dtLastPlayed
,
1022 const ReplayGain
& replayGain
)
1028 // We need at least the title
1029 if (strTitle
.empty())
1032 if (nullptr == m_pDB
)
1034 if (nullptr == m_pDS
)
1037 std::string strPath
, strFileName
;
1038 SplitPath(strPathAndFileName
, strPath
, strFileName
);
1039 int idPath
= AddPath(strPath
);
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());
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
))
1056 if (m_pDS
->num_rows() == 0)
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) ";
1087 // Song ID is autoincremented and dateNew set by trigger
1088 strSQL
+= PrepareSQL("VALUES (NULL, NULL, ");
1090 //Reuse song Id and original date when the Id added
1091 strSQL
+= PrepareSQL("VALUES (%i, '%s', ", idSong
, dtDateNew
.GetAsDBDateTime().c_str());
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");
1102 strSQL
+= PrepareSQL(",'%s'", strMusicBrainzTrackID
.c_str());
1103 if (artistSort
.empty() || artistSort
.compare(artistDisp
) == 0)
1104 strSQL
+= PrepareSQL(",NULL");
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());
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
);
1121 idNew
= (int)m_pDS
->lastinsertid();
1127 idNew
= m_pDS
->fv("idSong").get_asInt();
1129 UpdateSong(idNew
, //
1131 strMusicBrainzTrackID
, //
1132 strPathAndFileName
, //
1142 strOrigReleaseDate
, //
1145 iStartOffset
, iEndOffset
, //
1147 rating
, userrating
, votes
, //
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);
1161 CLog::Log(LOGERROR
, "musicdatabase:unable to addsong ({})", strSQL
);
1166 bool CMusicDatabase::GetSong(int idSong
, CSong
& song
)
1172 if (nullptr == m_pDB
)
1174 if (nullptr == m_pDS
)
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",
1184 if (!m_pDS
->query(strSQL
))
1186 int iRowsFound
= m_pDS
->num_rows();
1187 if (iRowsFound
== 0)
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
));
1204 song
.AppendArtistRole(GetArtistRoleFromDataset(record
, songArtistOffset
));
1208 m_pDS
->close(); // cleanup recordset data
1213 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
1219 bool CMusicDatabase::UpdateSong(CSong
& song
, bool bArtists
/*= true*/, bool bArtistLinks
/*= true*/)
1221 int result
= UpdateSong(song
.idSong
,
1223 song
.strMusicBrainzTrackID
, //
1224 song
.strFileName
, //
1228 song
.GetArtistString(), //
1229 song
.GetArtistSort(), //
1233 song
.strReleaseDate
, //
1234 song
.strOrigReleaseDate
, //
1235 song
.strDiscSubtitle
, //
1236 song
.iTimesPlayed
, //
1237 song
.iStartOffset
, song
.iEndOffset
, //
1239 song
.rating
, song
.userrating
, song
.votes
, //
1241 song
.iBPM
, song
.iBitRate
, song
.iSampleRate
, song
.iChannels
);
1245 // Replace Song genres and update genre string using the standardised genre names
1246 AddSongGenres(song
.idSong
, song
.genre
);
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();
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());
1268 CheckArtistLinksChanged();
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
,
1286 const std::string
& strReleaseDate
,
1287 const std::string
& strOrigReleaseDate
,
1288 const std::string
& strDiscSubtitle
,
1292 const CDateTime
& dtLastPlayed
,
1296 const ReplayGain
& replayGain
,
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, "
1323 idPath
, artistDisp
.c_str(),
1326 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
)
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");
1334 strSQL
+= PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID
.c_str());
1335 if (artistSort
.empty() || artistSort
.compare(artistDisp
) == 0)
1336 strSQL
+= PrepareSQL(", strArtistSort = NULL");
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());
1352 strSQL
+= ", iTimesPlayed = 0, lastplayed = NULL ";
1353 strSQL
+= PrepareSQL("WHERE idSong = %i", idSong
);
1355 bool status
= ExecuteQuery(strSQL
);
1358 AnnounceUpdate(MediaTypeSong
, 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
,
1371 const std::string
& strRecordLabel
,
1372 const std::string
& strType
,
1373 const std::string
& strReleaseStatus
,
1375 CAlbum::ReleaseType releaseType
)
1380 if (nullptr == m_pDB
)
1382 if (nullptr == m_pDS
)
1385 if (!strMusicBrainzAlbumID
.empty())
1386 strSQL
= PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
1387 strMusicBrainzAlbumID
.c_str());
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
1398 if (m_pDS
->num_rows() == 0)
1401 // Does not exist, add it
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");
1417 strSQL
+= PrepareSQL(",'%s'", strMusicBrainzAlbumID
.c_str());
1418 if (strReleaseGroupMBID
.empty())
1419 strSQL
+= PrepareSQL(", NULL");
1421 strSQL
+= PrepareSQL(",'%s'", strReleaseGroupMBID
.c_str());
1422 if (strArtistSort
.empty() || strArtistSort
.compare(strArtist
) == 0)
1423 strSQL
+= PrepareSQL(", NULL");
1425 strSQL
+= PrepareSQL(", '%s'", strArtistSort
.c_str());
1427 m_pDS
->exec(strSQL
);
1429 return (int)m_pDS
->lastinsertid();
1433 /* Exists in our database and being re-scanned from tags, so we should update it as the details
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();
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,");
1452 strSQL
+= PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID
.c_str());
1453 if (strArtistSort
.empty() || strArtistSort
.compare(strArtist
) == 0)
1454 strSQL
+= PrepareSQL(" strArtistSort = NULL");
1456 strSQL
+= PrepareSQL(" strArtistSort = '%s'", strArtistSort
.c_str());
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 "
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(), //
1468 m_pDS
->exec(strSQL
);
1469 DeleteAlbumArtistsByAlbum(idAlbum
);
1470 DeleteAlbumSources(idAlbum
);
1476 CLog::Log(LOGERROR
, "{} failed with query ({})", __FUNCTION__
, strSQL
);
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
,
1500 const std::string
& strReleaseDate
,
1501 const std::string
& strOrigReleaseDate
,
1504 CAlbum::ReleaseType releaseType
,
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
,
1516 TrimImageURLs(strImageURLs
, 65535);
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");
1539 strSQL
+= PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID
.c_str());
1540 if (strReleaseGroupMBID
.empty())
1541 strSQL
+= PrepareSQL(", strReleaseGroupMBID = NULL");
1543 strSQL
+= PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID
.c_str());
1544 if (strArtistSort
.empty() || strArtistSort
.compare(strArtist
) == 0)
1545 strSQL
+= PrepareSQL(", strArtistSort = NULL");
1547 strSQL
+= PrepareSQL(", strArtistSort = '%s'", strArtistSort
.c_str());
1549 strSQL
+= PrepareSQL(" WHERE idAlbum = %i", idAlbum
);
1551 bool status
= ExecuteQuery(strSQL
);
1553 AnnounceUpdate(MediaTypeAlbum
, idAlbum
);
1557 bool CMusicDatabase::GetAlbum(int idAlbum
, CAlbum
& album
, bool getSongs
/* = true */)
1561 if (nullptr == m_pDB
)
1563 if (nullptr == m_pDS
)
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
1573 sql
= PrepareSQL("SELECT albumview.*,albumartistview.* "
1575 " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
1576 " WHERE albumview.idAlbum = %ld "
1577 " ORDER BY albumartistview.iOrder",
1580 CLog::Log(LOGDEBUG
, "{}", sql
);
1581 if (!m_pDS
->query(sql
))
1583 if (m_pDS
->num_rows() == 0)
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
));
1603 m_pDS
->close(); // cleanup recordset data
1608 sql
= PrepareSQL("SELECT songview.*, songartistview.*"
1610 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1611 " WHERE songview.idAlbum = %ld "
1612 " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder",
1615 CLog::Log(LOGDEBUG
, "{}", sql
);
1616 if (!m_pDS
->query(sql
))
1618 if (m_pDS
->num_rows() == 0) //Album with no songs
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
));
1643 album
.songs
.back().AppendArtistRole(GetArtistRoleFromDataset(record
, songArtistOffset
));
1647 m_pDS
->close(); // cleanup recordset data
1654 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
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
)
1679 StringUtils::Trim(strGenre
);
1681 if (strGenre
.empty())
1682 strGenre
= g_localizeStrings
.Get(13205); // Unknown
1684 if (nullptr == m_pDB
)
1686 if (nullptr == m_pDS
)
1689 auto it
= m_genreCache
.find(strGenre
);
1690 if (it
!= m_genreCache
.end())
1694 strSQL
= PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'",
1696 m_pDS
->query(strSQL
);
1697 if (m_pDS
->num_rows() == 0)
1700 // doesn't exists, add it
1701 strSQL
= PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )",
1703 m_pDS
->exec(strSQL
);
1705 int idGenre
= (int)m_pDS
->lastinsertid();
1706 m_genreCache
.insert(std::pair
<std::string
, int>(strGenre
, idGenre
));
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
));
1720 CLog::Log(LOGERROR
, "musicdatabase:unable to addgenre ({})", strSQL
);
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
, //
1739 artist
.strGender
, //
1740 artist
.strDisambiguation
, //
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
, //
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
);
1766 int CMusicDatabase::AddArtist(const std::string
& strArtist
,
1767 const std::string
& strMusicBrainzArtistID
,
1768 const std::string
& strSortName
,
1769 bool bScrapedMBID
/* = false*/)
1772 int idArtist
= AddArtist(strArtist
, strMusicBrainzArtistID
, bScrapedMBID
);
1773 if (idArtist
< 0 || strSortName
.empty())
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
)
1785 if (nullptr == m_pDS
)
1788 strSQL
= PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist
);
1789 m_pDS
->query(strSQL
);
1790 if (m_pDS
->num_rows() != 1)
1795 std::string strArtistName
, strArtistSort
;
1796 strArtistName
= m_pDS
->fv("strArtist").get_asString();
1797 strArtistSort
= m_pDS
->fv("strSortName").get_asString();
1800 if (!strArtistSort
.empty())
1802 if (strSortName
.compare(strArtistName
) == 0)
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
));
1815 CLog::Log(LOGERROR
, "musicdatabase:unable to addartist with sortname ({})", strSQL
);
1821 int CMusicDatabase::AddArtist(const std::string
& strArtist
,
1822 const std::string
& strMusicBrainzArtistID
,
1823 bool bScrapedMBID
/* = false*/)
1828 if (nullptr == m_pDB
)
1830 if (nullptr == m_pDS
)
1834 if (!strMusicBrainzArtistID
.empty())
1836 // 1.a) Match on a MusicBrainz ID
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;
1848 strSQL
= PrepareSQL("UPDATE artist SET strArtist = '%s' "
1849 "WHERE idArtist = %i",
1850 strArtist
.c_str(), idArtist
);
1851 m_pDS
->exec(strSQL
);
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",
1864 m_pDS
->query(strSQL
);
1865 if (m_pDS
->num_rows() > 0)
1867 int idArtist
= m_pDS
->fv("idArtist").get_asInt();
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
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
);
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.
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();
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)",
1904 strSQL
= PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
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();
1915 CLog::Log(LOGERROR
, "musicdatabase:unable to addartist ({})", strSQL
);
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
)
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
);
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
,
1963 TrimImageURLs(strImageURLs
, 65535);
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",
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
);
1984 strSQL
+= PrepareSQL(", strMusicBrainzArtistID = NULL");
1986 strSQL
+= PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID
.c_str());
1987 if (strSortName
.empty())
1988 strSQL
+= PrepareSQL(", strSortName = NULL");
1990 strSQL
+= PrepareSQL(", strSortName = '%s'", strSortName
.c_str());
1992 strSQL
+= PrepareSQL(" WHERE idArtist = %i", idArtist
);
1994 bool status
= ExecuteQuery(strSQL
);
1996 AnnounceUpdate(MediaTypeArtist
, idArtist
);
2000 bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist
,
2001 const std::string
& strMusicBrainzArtistID
)
2003 if (strMusicBrainzArtistID
.empty() || idArtist
< 0)
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
);
2016 // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
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
);
2025 AnnounceUpdate(MediaTypeArtist
, idArtist
);
2031 bool CMusicDatabase::GetArtist(int idArtist
, CArtist
& artist
, bool fetchAll
/* = false */)
2035 auto start
= std::chrono::steady_clock::now();
2036 if (nullptr == m_pDB
)
2038 if (nullptr == m_pDS
)
2042 return false; // not in the database
2046 strSQL
= PrepareSQL("SELECT * FROM artistview "
2047 "LEFT JOIN discography ON artistview.idArtist = discography.idArtist "
2048 "WHERE artistview.idArtist = %i",
2051 strSQL
= PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist
);
2053 if (!m_pDS
->query(strSQL
))
2055 if (m_pDS
->num_rows() == 0)
2061 int discographyOffset
= artist_enumCount
;
2063 artist
.discography
.clear();
2064 artist
= GetArtistFromDataset(m_pDS
->get_sql_record(), 0, true); // inc scraped art URLs
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
);
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
,
2090 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2096 bool CMusicDatabase::GetArtistExists(int idArtist
)
2100 if (nullptr == m_pDB
)
2102 if (nullptr == m_pDS
)
2105 std::string strSQL
=
2106 PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist
);
2108 if (!m_pDS
->query(strSQL
))
2110 if (m_pDS
->num_rows() == 0)
2115 m_pDS
->close(); // cleanup recordset data
2120 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2126 int CMusicDatabase::GetLastArtist()
2128 std::string strSQL
= "SELECT MAX(idArtist) FROM artist";
2129 std::string lastArtist
= GetSingleValue(strSQL
);
2130 if (lastArtist
.empty())
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())
2145 if (nullptr == m_pDB
|| nullptr == m_pDS2
)
2147 // Match on MusicBrainz ID, definitively unique
2149 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
2150 strMusicBrainzArtistID
.c_str());
2151 if (!m_pDS2
->query(strSQL
))
2154 if (m_pDS2
->num_rows() > 0)
2156 idArtist
= m_pDS2
->fv("idArtist").get_asInt();
2157 artistname
= m_pDS2
->fv("strArtist").get_asString();
2164 CLog::Log(LOGERROR
, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__
, strSQL
);
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
)
2205 if (nullptr == m_pDS
)
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)");
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",
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",
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 "
2273 "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
2274 "ORDER BY strYear, strAlbum, idAlbum";
2276 if (!m_pDS
->query(strSQL
))
2278 int iRowsFound
= m_pDS
->num_rows();
2279 if (iRowsFound
== 0)
2285 while (!m_pDS
->eof())
2287 int idAlbum
= m_pDS
->fv("idAlbum").get_asInt();
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
);
2303 m_pDS
->exec("DROP TABLE tempDisco");
2304 m_pDS
->exec("DROP TABLE tempAlbum");
2310 m_pDS
->exec("DROP TABLE tempDisco");
2311 m_pDS
->exec("DROP TABLE tempAlbum");
2312 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
2317 int CMusicDatabase::AddRole(const std::string
& strRole
)
2324 if (nullptr == m_pDB
)
2326 if (nullptr == m_pDS
)
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();
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());
2344 CLog::Log(LOGERROR
, "musicdatabase:unable to AddRole ({})", strSQL
);
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
)
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())
2377 if (nullptr == m_pDB
)
2379 if (nullptr == m_pDS
)
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).
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();
2394 idArtist
= AddArtist(strArtist
, "", strSort
);
2396 // Add to song_artist table
2397 AddSongArtist(idArtist
, idSong
, strRole
, strArtist
, 0);
2403 CLog::Log(LOGERROR
, "musicdatabase:unable to AddSongContributor ({})", strSQL
);
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(
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
];
2434 AddSongContributor(idSong
, credit
.GetRoleDesc(), credit
.GetArtist(), strSortName
);
2438 int CMusicDatabase::GetRoleByName(const std::string
& strRole
)
2442 if (nullptr == m_pDB
)
2444 if (nullptr == m_pDS
)
2448 strSQL
= PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole
.c_str());
2450 if (!m_pDS
->query(strSQL
))
2452 int iRowsFound
= m_pDS
->num_rows();
2453 if (iRowsFound
!= 1)
2458 return m_pDS
->fv("idRole").get_asInt();
2462 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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",
2476 if (!m_pDS
->query(strSQL
))
2478 if (m_pDS
->num_rows() == 0)
2484 CVariant
artistRoles(CVariant::VariantTypeArray
);
2486 while (!m_pDS
->eof())
2489 roleObj
["role"] = m_pDS
->fv("strRole").get_asString();
2490 roleObj
["roleid"] = m_pDS
->fv("idrole").get_asInt();
2491 artistRoles
.push_back(roleObj
);
2496 item
->SetProperty("roles", artistRoles
);
2501 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2506 bool CMusicDatabase::DeleteSongArtistsBySong(int idSong
)
2508 return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong
));
2511 bool CMusicDatabase::AddAlbumArtist(int idArtist
,
2513 const std::string
& strArtist
,
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
)
2536 // Clear current entries for song
2537 strSQL
= PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong
);
2538 if (!ExecuteQuery(strSQL
))
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
))
2550 // Update concatenated genre string from the standardised genre values
2551 std::string strGenres
= StringUtils::Join(
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
))
2563 CLog::Log(LOGERROR
, "{}({}) {} failed", __FUNCTION__
, idSong
, strSQL
);
2568 bool CMusicDatabase::GetAlbumsByArtist(int idArtist
, std::vector
<int>& albums
)
2573 strSQL
= PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist
);
2574 if (!m_pDS
->query(strSQL
))
2576 if (m_pDS
->num_rows() == 0)
2582 while (!m_pDS
->eof())
2584 albums
.push_back(m_pDS
->fv("idAlbum").get_asInt());
2592 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2597 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum
, CFileItem
* item
)
2603 strSQL
= PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum
);
2605 if (!m_pDS
->query(strSQL
))
2607 if (m_pDS
->num_rows() == 0)
2613 // Get album artist credits
2614 VECARTISTCREDITS artistCredits
;
2615 while (!m_pDS
->eof())
2617 artistCredits
.emplace_back(GetArtistCreditFromDataset(m_pDS
->get_sql_record(), 0));
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
);
2642 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
2647 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum
, std::vector
<std::string
>& artistIDs
)
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"
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 ",
2661 if (!m_pDS
->query(strSQL
))
2663 if (m_pDS
->num_rows() == 0)
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());
2679 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
2684 bool CMusicDatabase::GetSongsByArtist(int idArtist
, std::vector
<int>& songs
)
2689 //Restrict to Artists only, no other roles
2690 strSQL
= PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", //
2692 if (!m_pDS
->query(strSQL
))
2694 if (m_pDS
->num_rows() == 0)
2700 while (!m_pDS
->eof())
2702 songs
.push_back(m_pDS
->fv("idSong").get_asInt());
2710 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2715 bool CMusicDatabase::GetArtistsBySong(int idSong
, std::vector
<int>& artists
)
2720 //Restrict to Artists only, no other roles
2721 strSQL
= PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", //
2723 if (!m_pDS
->query(strSQL
))
2725 if (m_pDS
->num_rows() == 0)
2731 while (!m_pDS
->eof())
2733 artists
.push_back(m_pDS
->fv("idArtist").get_asInt());
2741 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
2746 bool CMusicDatabase::GetGenresByArtist(int idArtist
, CFileItem
* item
)
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",
2759 if (!m_pDS
->query(strSQL
))
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
2766 strSQL
= PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
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",
2773 if (!m_pDS
->query(strSQL
))
2775 if (m_pDS
->num_rows() == 0)
2777 //No song genres, but query successful
2783 CVariant
artistSongGenres(CVariant::VariantTypeArray
);
2785 while (!m_pDS
->eof())
2788 genreObj
["title"] = m_pDS
->fv("strGenre").get_asString();
2789 genreObj
["genreid"] = m_pDS
->fv("idGenre").get_asInt();
2790 artistSongGenres
.push_back(genreObj
);
2795 item
->SetProperty("songgenres", artistSongGenres
);
2800 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2805 bool CMusicDatabase::GetGenresByAlbum(int idAlbum
, CFileItem
* item
)
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",
2816 if (!m_pDS
->query(strSQL
))
2818 if (m_pDS
->num_rows() == 0)
2820 //No song genres, but query successful
2825 CVariant
albumSongGenres(CVariant::VariantTypeArray
);
2827 while (!m_pDS
->eof())
2830 genreObj
["title"] = m_pDS
->fv("strGenre").get_asString();
2831 genreObj
["genreid"] = m_pDS
->fv("idGenre").get_asInt();
2832 albumSongGenres
.push_back(genreObj
);
2837 item
->SetProperty("songgenres", albumSongGenres
);
2842 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
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",
2854 if (!m_pDS
->query(strSQL
))
2856 if (m_pDS
->num_rows() == 0)
2862 while (!m_pDS
->eof())
2864 genres
.push_back(m_pDS
->fv("idGenre").get_asInt());
2873 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
2878 bool CMusicDatabase::GetIsAlbumArtist(int idArtist
, CFileItem
* item
)
2883 GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist
));
2884 CVariant
IsAlbumArtistObj(CVariant::VariantTypeBoolean
);
2885 IsAlbumArtistObj
= (countalbum
> 0);
2886 item
->SetProperty("isalbumartist", IsAlbumArtistObj
);
2891 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2897 int CMusicDatabase::AddPath(const std::string
& strPath1
)
2902 std::string
strPath(strPath1
);
2903 if (!URIUtils::HasSlashAtEnd(strPath
))
2904 URIUtils::AddSlashAtEnd(strPath
);
2906 if (nullptr == m_pDB
)
2908 if (nullptr == m_pDS
)
2911 auto it
= m_pathCache
.find(strPath
);
2912 if (it
!= m_pathCache
.end())
2915 strSQL
= PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath
.c_str());
2916 m_pDS
->query(strSQL
);
2917 if (m_pDS
->num_rows() == 0)
2920 // doesn't exists, add it
2921 strSQL
= PrepareSQL("INSERT INTO path (idPath, strPath) "
2922 "VALUES(NULL, '%s')",
2924 m_pDS
->exec(strSQL
);
2926 int idPath
= (int)m_pDS
->lastinsertid();
2927 m_pathCache
.insert(std::pair
<std::string
, int>(strPath
, idPath
));
2932 int idPath
= m_pDS
->fv("idPath").get_asInt();
2933 m_pathCache
.insert(std::pair
<std::string
, int>(strPath
, idPath
));
2940 CLog::Log(LOGERROR
, "musicdatabase:unable to addpath ({})", strSQL
);
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 */)
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
);
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
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();
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
,
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());
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
);
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
);
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
;
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();
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;
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();
3141 StringUtils::Split(record
->at(offset
+ album_strStyles
).get_asString(), itemSeparator
);
3143 StringUtils::Split(record
->at(offset
+ album_strMoods
).get_asString(), itemSeparator
);
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();
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
;
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());
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
;
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
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();
3215 StringUtils::Split(record
->at(offset
+ artist_strGenres
).get_asString(), itemSeparator
);
3216 artist
.strBiography
= record
->at(offset
+ artist_strBiography
).get_asString();
3218 StringUtils::Split(record
->at(offset
+ artist_strStyles
).get_asString(), itemSeparator
);
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());
3237 artist
.thumbURL
.ParseFromData(record
->at(artist_strImage
).get_asString());
3243 bool CMusicDatabase::GetSongByFileName(const std::string
& strFileNameAndPath
,
3245 int64_t startOffset
)
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
)
3259 if (nullptr == m_pDS
)
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());
3270 strSQL
+= PrepareSQL(" AND iStartOffset=%" PRIi64
, startOffset
);
3272 int idSong
= GetSingleValueInt(strSQL
);
3274 return GetSong(idSong
, song
);
3279 int CMusicDatabase::GetAlbumIdByPath(const std::string
& strPath
)
3283 if (nullptr == m_pDB
)
3285 if (nullptr == m_pDS
)
3288 std::string strSQL
= PrepareSQL("SELECT DISTINCT idAlbum FROM song "
3289 "JOIN path ON song.idPath = path.idPath "
3290 "WHERE path.strPath='%s'",
3293 if (!m_pDS
->query(strSQL
))
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();
3307 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, strPath
);
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
))
3326 int iRowsFound
= m_pDS
->num_rows();
3327 if (iRowsFound
== 0)
3332 int lResult
= m_pDS
->fv(0).get_asInt();
3333 m_pDS
->close(); // cleanup recordset data
3338 CLog::Log(LOGERROR
, "{} ({},{},{}) failed", __FUNCTION__
, strArtist
, strAlbum
, strTitle
);
3344 bool CMusicDatabase::SearchArtists(const std::string
& search
, CFileItemList
& artists
)
3348 if (nullptr == m_pDB
)
3350 if (nullptr == m_pDS
)
3353 std::string strVariousArtists
= g_localizeStrings
.Get(340).c_str();
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());
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
))
3367 if (m_pDS
->num_rows() == 0)
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
);
3388 m_pDS
->close(); // cleanup recordset data
3393 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3399 bool CMusicDatabase::GetTop100(const std::string
& strBaseDir
, CFileItemList
& items
)
3403 if (nullptr == m_pDB
)
3405 if (nullptr == m_pDS
)
3408 CMusicDbUrl baseUrl
;
3409 if (!strBaseDir
.empty() && !baseUrl
.FromString(strBaseDir
))
3412 std::string strSQL
= "SELECT * FROM songview "
3413 "WHERE iTimesPlayed>0 "
3414 "ORDER BY iTimesPlayed DESC "
3417 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3418 if (!m_pDS
->query(strSQL
))
3420 int iRowsFound
= m_pDS
->num_rows();
3421 if (iRowsFound
== 0)
3426 items
.Reserve(iRowsFound
);
3427 while (!m_pDS
->eof())
3429 CFileItemPtr
item(new CFileItem
);
3430 GetFileItemFromDataset(item
.get(), baseUrl
);
3435 m_pDS
->close(); // cleanup recordset data
3440 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3446 bool CMusicDatabase::GetTop100Albums(VECALBUMS
& albums
)
3450 albums
.erase(albums
.begin(), albums
.end());
3451 if (nullptr == m_pDB
)
3453 if (nullptr == m_pDS
)
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
))
3468 int iRowsFound
= m_pDS
->num_rows();
3469 if (iRowsFound
== 0)
3475 int albumArtistOffset
= album_enumCount
;
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())
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
));
3492 m_pDS
->close(); // cleanup recordset data
3497 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3503 bool CMusicDatabase::GetTop100AlbumSongs(const std::string
& strBaseDir
, CFileItemList
& items
)
3507 if (nullptr == m_pDB
)
3509 if (nullptr == m_pDS
)
3512 CMusicDbUrl baseUrl
;
3513 if (!strBaseDir
.empty() && baseUrl
.FromString(strBaseDir
))
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 "
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
))
3529 int iRowsFound
= m_pDS
->num_rows();
3530 if (iRowsFound
== 0)
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
);
3552 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3557 bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS
& albums
)
3561 albums
.erase(albums
.begin(), albums
.end());
3562 if (nullptr == m_pDB
)
3564 if (nullptr == m_pDS
)
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
))
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)
3596 int albumArtistOffset
= album_enumCount
;
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())
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
));
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());
3624 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3630 bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string
& strBaseDir
,
3631 CFileItemList
& items
)
3635 if (nullptr == m_pDB
)
3637 if (nullptr == m_pDS
)
3640 CMusicDbUrl baseUrl
;
3641 if (!strBaseDir
.empty() && !baseUrl
.FromString(strBaseDir
))
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
))
3660 int iRowsFound
= m_pDS
->num_rows();
3661 if (iRowsFound
== 0)
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
;
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())
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
);
3691 // Get song artist credits and contributors
3692 if (idSongArtistRole
== ROLE_ARTIST
)
3693 artistCredits
.push_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
3695 items
[items
.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3696 GetArtistRoleFromDataset(record
, songArtistOffset
));
3700 if (!artistCredits
.empty())
3702 //Store artist credits for final song
3703 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
3704 artistCredits
.clear();
3713 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3718 bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS
& albums
, unsigned int limit
)
3722 albums
.erase(albums
.begin(), albums
.end());
3723 if (nullptr == m_pDB
)
3725 if (nullptr == m_pDS
)
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 ",
3739 : CServiceBroker::GetSettingsComponent()
3740 ->GetAdvancedSettings()
3741 ->m_iMusicLibraryRecentlyAddedItems
);
3743 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3744 if (!m_pDS
->query(strSQL
))
3746 int iRowsFound
= m_pDS
->num_rows();
3747 if (iRowsFound
== 0)
3753 int albumArtistOffset
= album_enumCount
;
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())
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
));
3769 m_pDS
->close(); // cleanup recordset data
3774 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3780 bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string
& strBaseDir
,
3781 CFileItemList
& items
,
3786 if (nullptr == m_pDB
)
3788 if (nullptr == m_pDS
)
3791 CMusicDbUrl baseUrl
;
3792 if (!strBaseDir
.empty() && !baseUrl
.FromString(strBaseDir
))
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)
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 ",
3807 : CServiceBroker::GetSettingsComponent()
3808 ->GetAdvancedSettings()
3809 ->m_iMusicLibraryRecentlyAddedItems
);
3810 CLog::Log(LOGDEBUG
, "GetRecentlyAddedAlbumSongs() query: {}", strSQL
);
3811 if (!m_pDS
->query(strSQL
))
3814 int iRowsFound
= m_pDS
->num_rows();
3815 if (iRowsFound
== 0)
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
;
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())
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
);
3844 // Get song artist credits and contributors
3845 if (idSongArtistRole
== ROLE_ARTIST
)
3846 artistCredits
.push_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
3848 items
[items
.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3849 GetArtistRoleFromDataset(record
, songArtistOffset
));
3853 if (!artistCredits
.empty())
3855 //Store artist credits for final song
3856 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
3857 artistCredits
.clear();
3866 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3871 void CMusicDatabase::IncrementPlayCount(const CFileItem
& item
)
3875 if (nullptr == m_pDB
)
3877 if (nullptr == m_pDS
)
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' "
3884 strDateNow
.c_str(), idSong
);
3889 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, item
.GetPath());
3893 bool CMusicDatabase::GetSongsByPath(const std::string
& strPath1
,
3897 std::string
strPath(strPath1
);
3900 if (!URIUtils::HasSlashAtEnd(strPath
))
3901 URIUtils::AddSlashAtEnd(strPath
);
3906 if (nullptr == m_pDB
)
3908 if (nullptr == m_pDS
)
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",
3918 if (!m_pDS
->query(strSQL
))
3920 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3921 int iRowsFound
= m_pDS
->num_rows();
3922 if (iRowsFound
== 0)
3928 // Each file is potentially mapped to a list of songs, gather these and save as list
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
));
3940 filename
= song
.strFileName
;
3941 songs
.emplace_back(song
);
3944 m_pDS
->close(); // cleanup recordset data
3945 songmap
.insert(std::make_pair(filename
, songs
)); // Save songs for last filename
3950 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, strPath
);
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());
3988 bool CMusicDatabase::SearchSongs(const std::string
& search
, CFileItemList
& items
)
3992 if (nullptr == m_pDB
)
3994 if (nullptr == m_pDS
)
3997 CMusicDbUrl baseUrl
;
3998 if (!baseUrl
.FromString("musicdb://songs/"))
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());
4007 strSQL
= PrepareSQL("SELECT * FROM songview "
4008 "WHERE strTitle LIKE '%s%%' LIMIT 1000",
4011 if (!m_pDS
->query(strSQL
))
4013 if (m_pDS
->num_rows() == 0)
4016 while (!m_pDS
->eof())
4018 CFileItemPtr
item(new CFileItem
);
4019 GetFileItemFromDataset(item
.get(), baseUrl
);
4029 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
4035 bool CMusicDatabase::SearchAlbums(const std::string
& search
, CFileItemList
& albums
)
4039 if (nullptr == m_pDB
)
4041 if (nullptr == m_pDS
)
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());
4050 strSQL
= PrepareSQL("SELECT * FROM albumview "
4051 "WHERE strAlbum LIKE '%s%%'",
4054 if (!m_pDS
->query(strSQL
))
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
);
4071 m_pDS
->close(); // cleanup recordset data
4076 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
4081 bool CMusicDatabase::CleanupSongsByIds(const std::string
& strSongIds
)
4085 if (nullptr == m_pDB
)
4087 if (nullptr == m_pDS
)
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
))
4095 int iRowsFound
= m_pDS
->num_rows();
4096 if (iRowsFound
== 0)
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());
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
);
4138 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
4143 bool CMusicDatabase::CleanupSongs(CGUIDialogProgress
* progressDialog
/*= nullptr*/)
4148 // Count total number of songs
4149 total
= GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS
);
4150 // No songs to clean
4154 // run through all songs and get all unique path ids
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",
4161 if (!m_pDS
->query(strSQL
))
4163 int iRowsFound
= m_pDS
->num_rows();
4164 // keep going until no rows are left!
4165 if (iRowsFound
== 0)
4171 std::vector
<std::string
> songIds
;
4172 while (!m_pDS
->eof())
4174 songIds
.push_back(m_pDS
->fv("song.idSong").get_asString());
4178 std::string strSongIds
= "(" + StringUtils::Join(songIds
, ",") + ")";
4179 CLog::Log(LOGDEBUG
, "Checking songs from song ID list: {}", strSongIds
);
4182 int percentage
= i
* 100 / total
;
4183 if (percentage
> progressDialog
->GetPercentage())
4185 progressDialog
->SetPercentage(percentage
);
4186 progressDialog
->Progress();
4188 if (progressDialog
->IsCanceled())
4194 if (!CleanupSongsByIds(strSongIds
))
4201 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupSongs()");
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
))
4216 int iRowsFound
= m_pDS
->num_rows();
4217 if (iRowsFound
== 0)
4223 std::vector
<std::string
> albumIds
;
4224 while (!m_pDS
->eof())
4226 albumIds
.push_back(m_pDS
->fv("album.idAlbum").get_asString());
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
);
4239 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupAlbums()");
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
))
4262 int iRowsFound
= m_pDS
->num_rows();
4263 if (iRowsFound
== 0)
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
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");
4295 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
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");
4333 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
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
);
4351 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
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
);
4370 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
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
);
4389 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
4394 bool CMusicDatabase::DeleteRemovedLinks()
4398 std::string strSQL
= "DELETE FROM removed_link";
4399 m_pDS
->exec(strSQL
);
4404 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::DeleteRemovedLinks");
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
)
4415 if (nullptr == m_pDS
)
4417 SetLibraryLastUpdated();
4418 if (!CleanupAlbums())
4420 if (!CleanupArtists())
4422 if (!CleanupGenres())
4424 if (!CleanupRoles())
4426 if (!CleanupInfoSettings())
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
;
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
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
;
4463 // then the albums that are not linked to a song or to album, or whose path is removed
4466 progressDialog
->SetLine(1, CVariant
{326});
4467 progressDialog
->SetPercentage(20);
4468 progressDialog
->Progress();
4469 if (progressDialog
->IsCanceled())
4475 if (!CleanupAlbums())
4477 ret
= ERROR_REORG_ALBUM
;
4483 progressDialog
->SetLine(1, CVariant
{324});
4484 progressDialog
->SetPercentage(40);
4485 progressDialog
->Progress();
4486 if (progressDialog
->IsCanceled())
4492 if (!CleanupPaths())
4494 ret
= ERROR_REORG_PATH
;
4497 // and finally artists + genres
4500 progressDialog
->SetLine(1, CVariant
{320});
4501 progressDialog
->SetPercentage(60);
4502 progressDialog
->Progress();
4503 if (progressDialog
->IsCanceled())
4509 if (!CleanupArtists())
4511 ret
= ERROR_REORG_ARTIST
;
4514 //Genres, roles and info settings progress in one step
4517 progressDialog
->SetLine(1, CVariant
{322});
4518 progressDialog
->SetPercentage(80);
4519 progressDialog
->Progress();
4520 if (progressDialog
->IsCanceled())
4526 if (!CleanupGenres())
4528 ret
= ERROR_REORG_OTHER
;
4531 if (!CleanupRoles())
4533 ret
= ERROR_REORG_OTHER
;
4536 if (!CleanupInfoSettings())
4538 ret
= ERROR_REORG_OTHER
;
4541 if (!DeleteRemovedLinks())
4543 ret
= ERROR_REORG_OTHER
;
4547 // commit transaction
4550 progressDialog
->SetLine(1, CVariant
{328});
4551 progressDialog
->SetPercentage(90);
4552 progressDialog
->Progress();
4553 if (progressDialog
->IsCanceled())
4559 if (!CommitTransaction())
4561 ret
= ERROR_WRITING_CHANGES
;
4565 // Recreate DELETE triggers on song_artist and album_artist
4566 CreateRemovedLinkTriggers();
4568 // and compress the database
4571 progressDialog
->SetLine(1, CVariant
{331});
4572 progressDialog
->SetPercentage(100);
4573 progressDialog
->Close();
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__
,
4580 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnCleanFinished");
4582 if (!Compress(false))
4584 return ERROR_COMPRESSING
;
4589 RollbackTransaction();
4590 // Recreate DELETE triggers on song_artist and album_artist
4591 CreateRemovedLinkTriggers();
4592 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnCleanFinished");
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
)
4605 strImage
= strImage
.substr(0, iPos
+ 8);
4610 bool CMusicDatabase::LookupCDDBInfo(bool bRequery
/*=false*/)
4612 #ifdef HAS_DVD_DRIVE
4613 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
4614 CSettings::SETTING_AUDIOCDS_USECDDB
))
4617 // check network connectivity
4618 if (!CServiceBroker::GetNetwork().IsAvailable())
4621 // Get information for the inserted disc
4622 CCdInfo
* pCdInfo
= CServiceBroker::GetMediaManager().GetCdInfo();
4623 if (pCdInfo
== NULL
)
4626 // If the disc has no tracks, we are finished here.
4627 int nTracks
= pCdInfo
->GetTrackCount();
4631 // Delete old info if any
4634 std::string strFile
= StringUtils::Format("{:x}.cddb", pCdInfo
->GetCddbDiscId());
4635 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager
.GetCDDBFolder(), strFile
));
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
)
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});
4681 std::string strTitle
= cddb
.getInexactTitle(i
);
4685 const std::string
& strArtist
= cddb
.getInexactArtist(i
);
4686 if (!strArtist
.empty())
4687 strTitle
+= " - " + strArtist
;
4689 pDlgSelect
->Add(strTitle
);
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();
4703 pCdInfo
->SetNoCDDBInfo();
4705 else if (lasterror
== E_NO_MATCH_FOUND
)
4707 pCdInfo
->SetNoCDDBInfo();
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
)},
4718 } // if ( !cddb.queryCDinfo( pCdInfo ) )
4720 pDialogProgress
->Close();
4723 // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
4725 return pCdInfo
->HasCDDBInfo();
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});
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
);
4746 pDlg
->SetHeading(CVariant
{g_localizeStrings
.Get(181)});
4749 std::map
<uint32_t, std::string
> mapCDDBIds
;
4750 for (int i
= 0; i
< items
.Size(); ++i
)
4752 if (items
[i
]->m_bIsFolder
)
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);
4759 cddb
.setCacheDir(m_profileManager
.GetCDDBFolder());
4761 if (!cddb
.queryCache(lDiscId
))
4764 std::string strDiskTitle
, strDiskArtist
;
4765 cddb
.getDiskTitle(strDiskTitle
);
4766 cddb
.getDiskArtist(strDiskArtist
);
4769 if (strDiskArtist
.empty())
4772 str
= strDiskTitle
+ " - " + strDiskArtist
;
4775 mapCDDBIds
.insert(std::pair
<uint32_t, std::string
>(lDiscId
, str
));
4781 // and wait till user selects one
4782 int iSelectedAlbum
= pDlg
->GetSelectedItem();
4783 if (iSelectedAlbum
< 0)
4785 mapCDDBIds
.erase(mapCDDBIds
.begin(), mapCDDBIds
.end());
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
));
4799 mapCDDBIds
.erase(mapCDDBIds
.begin(), mapCDDBIds
.end());
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});
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
)
4839 if (nullptr == m_pDS
)
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
))
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 != ''");
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
))
4888 strSQL
= PrepareSQL(strSQL
, !extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0
4889 ? extFilter
.fields
.c_str()
4894 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
4896 if (!m_pDS
->query(strSQL
))
4898 int iRowsFound
= m_pDS
->num_rows();
4899 if (iRowsFound
== 0)
4907 CFileItemPtr
pItem(new CFileItem());
4908 pItem
->SetProperty("total", iRowsFound
== 1 ? m_pDS
->fv(0).get_asInt() : iRowsFound
);
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;
4940 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
)
4954 if (nullptr == m_pDS
)
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
))
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");
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");
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
))
5005 strSQL
= PrepareSQL(strSQL
, !extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0
5006 ? extFilter
.fields
.c_str()
5011 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5013 if (!m_pDS
->query(strSQL
))
5015 int iRowsFound
= m_pDS
->num_rows();
5016 if (iRowsFound
== 0)
5024 CFileItemPtr
pItem(new CFileItem());
5025 pItem
->SetProperty("total", iRowsFound
== 1 ? m_pDS
->fv(0).get_asInt() : iRowsFound
);
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;
5058 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5063 bool CMusicDatabase::GetYearsNav(const std::string
& strBaseDir
,
5064 CFileItemList
& items
,
5065 const Filter
& filter
/* = Filter() */)
5069 if (nullptr == m_pDB
)
5071 if (nullptr == m_pDS
)
5074 Filter extFilter
= filter
;
5075 CMusicDbUrl musicUrl
;
5076 SortDescription sorting
;
5078 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
5081 bool useOriginalYears
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5082 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
);
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)");
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
))
5101 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5102 if (!m_pDS
->query(strSQL
))
5104 int iRowsFound
= m_pDS
->num_rows();
5105 if (iRowsFound
== 0)
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");
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;
5141 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5146 bool CMusicDatabase::GetRolesNav(const std::string
& strBaseDir
,
5147 CFileItemList
& items
,
5148 const Filter
& filter
/* = Filter() */)
5152 if (nullptr == m_pDB
)
5154 if (nullptr == m_pDS
)
5157 Filter extFilter
= filter
;
5158 CMusicDbUrl musicUrl
;
5159 SortDescription sorting
;
5160 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
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
))
5171 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5172 if (!m_pDS
->query(strSQL
))
5174 int iRowsFound
= m_pDS
->num_rows();
5175 if (iRowsFound
== 0)
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;
5207 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5212 bool CMusicDatabase::GetAlbumsByYear(const std::string
& strBaseDir
, CFileItemList
& items
, int year
)
5214 CMusicDbUrl musicUrl
;
5215 if (!musicUrl
.FromString(strBaseDir
))
5218 musicUrl
.AddOption("year", year
);
5219 musicUrl
.AddOption("show_singles", true); // allow singles to be listed
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
)
5234 if (nullptr == m_pDS
)
5237 if (table
.empty() || labelField
.empty())
5242 Filter extFilter
= filter
;
5243 std::string strSQL
= "SELECT %s FROM " + table
+ " ";
5244 extFilter
.AppendGroup(labelField
);
5245 extFilter
.AppendWhere(labelField
+ " != ''");
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
))
5264 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5265 if (!m_pDS
->query(strSQL
))
5268 int iRowsFound
= m_pDS
->num_rows();
5269 if (iRowsFound
<= 0)
5277 CFileItemPtr
pItem(new CFileItem());
5278 pItem
->SetProperty("total", iRowsFound
== 1 ? m_pDS
->fv(0).get_asInt() : iRowsFound
);
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;
5310 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
)
5344 if (nullptr == m_pDS
)
5348 CMusicDbUrl musicUrl
;
5349 if (!musicUrl
.FromString(strBaseDir
))
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
);
5371 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
)
5385 if (nullptr == m_pDS
)
5390 auto start
= std::chrono::steady_clock::now();
5393 Filter extFilter
= filter
;
5394 CMusicDbUrl musicUrl
;
5395 SortDescription sorting
= sortDescription
;
5396 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
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
)
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
)
5415 extFilter
.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist "
5416 "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
5419 extFilter
.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
5422 std::string strSQLExtra
;
5423 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
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
)
5432 // Count distinct without group by
5433 Filter countFilter
= extFilter
;
5434 countFilter
.group
.clear();
5435 std::string strSQLWhere
;
5436 if (!BuildSQL(strSQLWhere
, countFilter
, strSQLWhere
))
5438 total
= GetSingleValueInt(
5439 "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere
, m_pDS
);
5442 total
= GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra
, m_pDS
);
5446 CFileItemPtr
pItem(new CFileItem());
5447 pItem
->SetProperty("total", total
);
5454 // Apply any limiting directly in SQL and so sort as well
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
))
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
;
5480 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5481 auto queryStart
= std::chrono::steady_clock::now();
5482 if (!m_pDS
->query(strSQL
))
5484 int iRowsFound
= m_pDS
->num_rows();
5485 if (iRowsFound
== 0)
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
)
5498 items
.SetProperty("total", total
);
5500 DatabaseResults results
;
5501 results
.reserve(iRowsFound
);
5502 // Populate results field vector from dataset
5504 if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist
, fields
, m_pDS
, results
))
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
);
5539 CLog::Log(LOGERROR
, "{} - out of memory getting listing (got {})", __FUNCTION__
,
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());
5557 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5562 bool CMusicDatabase::GetAlbumFromSong(int idSong
, CAlbum
& album
)
5566 if (nullptr == m_pDB
)
5568 if (nullptr == m_pDS
)
5571 std::string strSQL
= PrepareSQL("SELECT albumview.* FROM song "
5572 "JOIN albumview on song.idAlbum = albumview.idAlbum "
5573 "WHERE song.idSong='%i'",
5575 if (!m_pDS
->query(strSQL
))
5577 int iRowsFound
= m_pDS
->num_rows();
5578 if (iRowsFound
!= 1)
5584 album
= GetAlbumFromDataset(m_pDS
.get());
5591 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
))
5610 musicUrl
.AddOption("genreid", idGenre
);
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)
5630 auto start
= std::chrono::steady_clock::now();
5633 Filter extFilter
= filter
;
5634 CMusicDbUrl musicUrl
;
5635 SortDescription sorting
= sortDescription
;
5636 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
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
)
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
))
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
)
5661 // Count distinct without group by
5662 Filter countFilter
= extFilter
;
5663 countFilter
.group
.clear();
5664 std::string strSQLWhere
;
5665 if (!BuildSQL(strSQLWhere
, countFilter
, strSQLWhere
))
5667 total
= GetSingleValueInt(
5668 "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere
, m_pDS
);
5671 total
= GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra
, m_pDS
);
5675 CFileItemPtr
pItem(new CFileItem());
5676 pItem
->SetProperty("total", total
);
5683 // Apply any limiting directly in SQL
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)");
5702 StringUtils::Replace(extFilter
.order
, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5704 strSQLExtra
.clear();
5705 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
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
;
5715 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5716 auto querytime
= std::chrono::steady_clock::now();
5717 if (!m_pDS
->query(strSQL
))
5719 int iRowsFound
= m_pDS
->num_rows();
5720 if (iRowsFound
== 0)
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
)
5733 items
.SetProperty("total", total
);
5735 DatabaseResults results
;
5736 results
.reserve(iRowsFound
);
5737 // Populate results field vector from dataset
5739 if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum
, fields
, m_pDS
, results
))
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");
5768 CLog::Log(LOGERROR
, "{} - out of memory getting listing (got {})", __FUNCTION__
,
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());
5786 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, filter
.where
);
5791 bool CMusicDatabase::GetDiscsNav(const std::string
& strBaseDir
,
5792 CFileItemList
& items
,
5794 const Filter
& filter
,
5795 const SortDescription
& sortDescription
,
5798 CMusicDbUrl musicUrl
;
5799 if (!musicUrl
.FromString(strBaseDir
))
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
,
5814 CMusicDbUrl musicUrl
;
5815 if (!musicUrl
.FromString(baseDir
))
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
,
5826 if (m_pDB
== nullptr || m_pDS
== nullptr)
5831 auto start
= std::chrono::steady_clock::now();
5835 Filter extFilter
= filter
;
5836 SortDescription sorting
= sortDescription
;
5838 if (!GetFilter(musicUrl
, extFilter
, sorting
))
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
))
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 " +
5870 strSQL
= "SELECT COUNT(1) FROM (" + strSQL
+ ") AS albumdisc ";
5871 total
= GetSingleValueInt(strSQL
, m_pDS
);
5875 items
.SetProperty("total", total
);
5878 // Apply limits and random sort order directly in SQL
5881 if (sorting
.sortBy
== SortByRandom
)
5882 strSQLExtra
+= PrepareSQL(" ORDER BY RANDOM()");
5883 strSQLExtra
+= DatabaseUtils::BuildLimitClause(sorting
.limitEnd
, sorting
.limitStart
);
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 " +
5893 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5894 auto queryStart
= std::chrono::steady_clock::now();
5895 if (!m_pDS
->query(strSQL
))
5897 int iRowsFound
= m_pDS
->num_rows();
5898 if (iRowsFound
== 0)
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
)
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
))
5924 // Get data from returned rows, note possibly multiple albums although usually only one
5925 items
.Reserve(total
);
5926 int albumOffset
= 2;
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())
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
);
5950 else if (oldDiscTitle
== strDiscSubtitle
)
5951 { // disc title already added to list, fetch the next disc
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
5963 itemUrl
.AddOption("disctitle", strDiscSubtitle
.c_str());
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");
5979 CLog::Log(LOGERROR
, "{} - out of memory getting listing (got {})", __FUNCTION__
,
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());
6003 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, filter
.where
);
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();
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)
6029 auto start
= std::chrono::steady_clock::now();
6032 Filter extFilter
= filter
;
6033 CMusicDbUrl musicUrl
;
6034 SortDescription sorting
= sortDescription
;
6035 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
6038 bool extended
= false;
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
)
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
))
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
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
);
6065 std::string strSQLsong
= strSQLExtra
;
6066 StringUtils::Replace(strSQLsong
, "songview", "song");
6067 total
= GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong
, m_pDS
);
6071 extFilter
.AppendGroup("songview.idSong");
6073 // Apply any limiting directly in SQL
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)");
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
))
6103 strFields
= "songview.*, songartistview.*";
6104 if (!extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0)
6105 strFields
= strFields
+ ", " + extFilter
.fields
;
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
6113 std::string strSQLJoin
;
6114 joinFilter
.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
6115 if (sortDescription
.sortBy
== SortByRandom
)
6116 joinFilter
.AppendOrder("songartistview.idSong");
6118 joinFilter
.order
= extFilter
.order
;
6121 StringUtils::Replace(joinFilter
.join
, "songview.idSong", "sv.idSong");
6122 StringUtils::Replace(joinFilter
.order
, "songview.", "sv.");
6125 joinFilter
.where
= extFilter
.where
;
6126 joinFilter
.AppendOrder("songartistview.idRole");
6127 joinFilter
.AppendOrder("songartistview.iOrder");
6128 if (!BuildSQL(strSQLJoin
, joinFilter
, strSQLJoin
))
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
;
6144 strSQL
= "SELECT " + strFields
+ " FROM songview " + strSQLJoin
;
6147 strSQL
= "SELECT " + strFields
+ " FROM songview " + strSQLExtra
;
6149 CLog::Log(LOGDEBUG
, "{} query = {}", __FUNCTION__
, strSQL
);
6150 auto queryStart
= std::chrono::steady_clock::now();
6152 if (!m_pDS
->query(strSQL
))
6155 int iRowsFound
= m_pDS
->num_rows();
6156 if (iRowsFound
== 0)
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
6173 if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong
, fields
, m_pDS
, results
))
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
;
6183 VECARTISTCREDITS artistCredits
;
6184 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
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())
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");
6211 // Get song artist credits and contributors
6214 int idSongArtistRole
= record
->at(songArtistOffset
+ artistCredit_idRole
).get_asInt();
6215 if (idSongArtistRole
== ROLE_ARTIST
)
6216 artistCredits
.push_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
6218 items
[items
.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
6219 GetArtistRoleFromDataset(record
, songArtistOffset
));
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();
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());
6259 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
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)
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
))
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
))
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()
6310 CLog::Log(LOGDEBUG
, "{} query = {}", __FUNCTION__
, strSQL
);
6312 if (!m_pDS
->query(strSQL
))
6315 int iRowsFound
= m_pDS
->num_rows();
6316 if (iRowsFound
== 0)
6322 // store the total value of items as a property
6323 if (total
< iRowsFound
)
6325 items
.SetProperty("total", total
);
6327 DatabaseResults results
;
6328 results
.reserve(iRowsFound
);
6329 if (!SortUtils::SortFromDataset(sorting
, MediaTypeSong
, m_pDS
, results
))
6332 // get data from returned rows
6333 items
.Reserve(results
.size());
6334 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
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
;
6352 CLog::Log(LOGERROR
, "{}: out of memory loading query: {}", __FUNCTION__
, filter
.where
);
6353 return (items
.Size() > 0);
6365 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
6370 bool CMusicDatabase::GetSongsByYear(const std::string
& baseDir
, CFileItemList
& items
, int year
)
6372 CMusicDbUrl musicUrl
;
6373 if (!musicUrl
.FromString(baseDir
))
6376 musicUrl
.AddOption("year", year
);
6379 return GetSongsFullByWhere(baseDir
, filter
, items
, SortDescription(), true);
6382 bool CMusicDatabase::GetSongsNav(const std::string
& strBaseDir
,
6383 CFileItemList
& items
,
6387 const SortDescription
& sortDescription
/* = SortDescription() */)
6389 CMusicDbUrl musicUrl
;
6390 if (!musicUrl
.FromString(strBaseDir
))
6394 musicUrl
.AddOption("albumid", idAlbum
);
6397 musicUrl
.AddOption("genreid", idGenre
);
6400 musicUrl
.AddOption("artistid", idArtist
);
6403 return GetSongsFullByWhere(musicUrl
.ToString(), filter
, items
, sortDescription
, true);
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.
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
,
6471 const SortDescription
& sortDescription
/* = SortDescription() */)
6473 if (nullptr == m_pDB
)
6475 if (nullptr == m_pDS
)
6482 size_t resultcount
= 0;
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
))
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
))
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());
6516 option
= options
.find("role");
6517 if (option
!= options
.end())
6519 if (option
->second
.asString() == "all" || option
->second
.asString() == "%")
6520 roleidfilter
= -1000; //All roles
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();
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
;
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);
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
6576 dbfieldindex
.emplace_back(-1);
6578 dbfieldindex
.emplace_back(i
);
6579 // Field from scaler subquery
6580 if (!JSONtoDBArtist
[i
].SQL
.empty())
6581 extFilter
.AppendField(PrepareSQL(JSONtoDBArtist
[i
].SQL
));
6583 // Field from artist table
6584 extFilter
.AppendField(JSONtoDBArtist
[i
].fieldDB
);
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
6600 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
6603 // Add any LIMIT clause to strSQLExtra
6604 if (extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0))
6607 DatabaseUtils::BuildLimitClause(sortDescription
.limitEnd
, sortDescription
.limitStart
);
6608 resultcount
= std::min(
6609 DatabaseUtils::GetLimitCount(sortDescription
.limitEnd
, sortDescription
.limitStart
),
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
);
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);
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
);
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
);
6730 { // Define field alias names in join layout
6731 joinLayout
.SetField(joinToArtist_idSongGenreAlbum
,
6732 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreAlbum
].SQL
,
6734 joinLayout
.SetField(joinToArtist_strSongGenreAlbum
,
6735 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreAlbum
].SQL
);
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
))
6775 strAlbumSQL
= "SELECT " + albumArtistFilter
.fields
+ " FROM album_artist " + strAlbumSQL
;
6776 std::string strSongSQL
;
6777 if (!BuildSQL(strSongSQL
, songArtistFilter
, strSongSQL
))
6779 strSongSQL
= "SELECT " + songArtistFilter
.fields
+ " FROM song_artist " + strSongSQL
;
6781 joinFilter
.AppendJoin("JOIN (" + strAlbumSQL
+ " UNION " + strSongSQL
+
6782 ") AS albumSong ON id = a1.idArtist");
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
);
6792 bool bJoinArt(false);
6793 bJoinArt
= joinLayout
.GetOutput(joinToArtist_idArt
) ||
6794 joinLayout
.GetOutput(joinToArtist_thumbnail
) ||
6795 joinLayout
.GetOutput(joinToArtist_fanart
);
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
))
6813 joinFilter
.AppendJoin("AND art.type = 'fanart'");
6814 else if (!joinLayout
.GetOutput(joinToArtist_fanart
))
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
))
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()));
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
);
6849 auto start
= std::chrono::steady_clock::now();
6851 if (!m_pDS
->query(strSQL
))
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)
6866 // Get artists from returned rows. Joins means there can be many rows per artist
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);
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
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;
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"] = "";
6917 genreidlist
.clear();
6918 bGenreFoundViaAlbum
= false;
6919 sourceidlist
.clear();
6924 continue; // Having saved the last artist stop
6927 artistId
= record
->at(0).get_asInt();
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();
6956 artistObj
[JSONtoDBArtist
[dbfieldindex
[i
]].fieldJSON
] =
6957 record
->at(1 + i
).get_asString();
6960 if (bJoinAlbumArtist
)
6962 bool bAlbumArtistRow(true);
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() &&
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
6987 sourceId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceAlbum
)).get_asInt();
6988 if (!bAlbumArtistRow
)
6990 // Skip other roles (when fetching them)
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
)
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
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
7033 genreId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreAlbum
)).get_asInt();
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() &&
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();
7047 record
->at(joinLayout
.GetRecNo(joinToArtist_strSongGenreSong
)).get_asString();
7051 // Already have that genre?
7053 for (const auto& i
: genreidlist
)
7061 bGenreFoundViaAlbum
= bGenreFoundViaAlbum
|| bAlbumArtistRow
;
7062 genreidlist
.emplace_back(genreId
);
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
7077 if (joinLayout
.GetOutput(joinToArtist_strRole
))
7079 // Already have that role?
7081 for (const auto& i
: roleidlist
)
7089 roleidlist
.emplace_back(roleId
);
7091 roleObj
["roleid"] = roleId
;
7093 record
->at(joinLayout
.GetRecNo(joinToArtist_strRole
)).get_asString();
7094 artistObj
["roles"].append(roleObj
);
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());
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());
7140 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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.
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
,
7208 const SortDescription
& sortDescription
/* = SortDescription() */)
7211 if (nullptr == m_pDB
)
7213 if (nullptr == m_pDS
)
7220 size_t resultcount
= 0;
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
))
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
))
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();
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")
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"
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);
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
7290 dbfieldindex
.emplace_back(-1);
7292 dbfieldindex
.emplace_back(i
);
7293 if (!JSONtoDBAlbum
[i
].SQL
.empty())
7294 // Field from scaler subquery
7295 extFilter
.AppendField(PrepareSQL(JSONtoDBAlbum
[i
].SQL
));
7297 // Field from album table
7298 extFilter
.AppendField(JSONtoDBAlbum
[i
].fieldDB
);
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
7321 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
7324 // Add any LIMIT clause to strSQLExtra
7325 if (extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0))
7328 DatabaseUtils::BuildLimitClause(sortDescription
.limitEnd
, sortDescription
.limitStart
);
7329 resultcount
= std::min(
7330 DatabaseUtils::GetLimitCount(sortDescription
.limitEnd
, sortDescription
.limitStart
),
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
);
7344 joinFilter
.AppendOrder("a1.idAlbum");
7345 joinFilter
.AppendGroup("a1.idAlbum");
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
))
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()));
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");
7395 StringUtils::Replace(strSQL
, "<datefield>", "strOrigReleaseDate");
7397 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
7399 auto start
= std::chrono::steady_clock::now();
7401 if (!m_pDS
->query(strSQL
))
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)
7416 // Get albums from returned rows. Joins means there can be many rows per album
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
);
7444 continue; // Having saved last album stop
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());
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();
7495 url
= CTextureUtils::GetWrappedImageURL(url
);
7496 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] = url
;
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
);
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());
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());
7542 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
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
,
7635 const SortDescription
& sortDescription
/* = SortDescription() */)
7638 if (nullptr == m_pDB
)
7640 if (nullptr == m_pDS
)
7647 size_t resultcount
= 0;
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
))
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
))
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();
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
;
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"
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);
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
7748 dbfieldindex
.emplace_back(-1);
7750 dbfieldindex
.emplace_back(i
);
7751 if (!JSONtoDBSong
[i
].SQL
.empty())
7752 // Field from scaler subquery
7753 extFilter
.AppendField(PrepareSQL(JSONtoDBSong
[i
].SQL
));
7755 // Field from song table
7756 extFilter
.AppendField(JSONtoDBSong
[i
].fieldDB
);
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);
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
)
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
7806 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
7809 // Add any LIMIT clause to strSQLExtra
7810 if (extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0))
7813 DatabaseUtils::BuildLimitClause(sortDescription
.limitEnd
, sortDescription
.limitStart
);
7814 resultcount
= std::min(
7815 DatabaseUtils::GetLimitCount(sortDescription
.limitEnd
, sortDescription
.limitStart
),
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
);
7833 joinFilter
.AppendOrder("sv.idSong");
7834 joinFilter
.AppendGroup("sv.idSong");
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");
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");
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())
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");
7906 { // Get just roles for "displaycomposer", "displayconductor" etc.
7908 for (size_t i
= 0; i
< roleidlist
.size(); i
++)
7910 int idRole
= roleidlist
[i
];
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
);
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");
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
))
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()));
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");
7978 StringUtils::Replace(strSQL
, "<datefield>", "song.strOrigReleaseDate");
7980 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
7983 auto start
= std::chrono::steady_clock::now();
7985 if (!m_pDS
->query(strSQL
))
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)
8000 // Get song from returned rows. Joins mean there can be many rows per song
8002 int albumartistId
= -1;
8005 bool bSongGenreDone(false);
8006 bool bSongArtistDone(false);
8007 bool bHaveSong(false);
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
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"))
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
;
8045 songObj
[displayXXX
] = "";
8048 result
["songs"].append(songObj
);
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
8062 bSongGenreDone
= false;
8063 bSongArtistDone
= false;
8066 continue; // Having saved the last song stop
8069 songId
= record
->at(0).get_asInt();
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();
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())
8111 bSongGenreDone
|| (albumartistId
> 0); // Not first album artist, skip genre
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
);
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
);
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();
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());
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());
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());
8217 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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())
8244 std::string strThen
= PrepareSQL(" THEN SUBSTR(%s, %i)", strField
.c_str(), tokenlength
+ 1);
8245 sortclause
+= "WHEN " + strWhen
+ strThen
;
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())
8260 strWhen
+= tokenclause
;
8262 if (!strWhen
.empty())
8264 if (!sortclause
.empty())
8266 std::string strThen
= PrepareSQL(" THEN SUBSTR(%s, %i)", strField
.c_str(), tokenlength
+ 1);
8267 sortclause
+= "WHEN " + strWhen
+ strThen
;
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.
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)
8287 std::string sortSQL
;
8288 if (!strSortField
.empty() && sortAttributes
& SortAttributeUseArtistSortName
)
8290 PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField
.c_str(), strSortField
.c_str());
8291 if (sortAttributes
& SortAttributeIgnoreArticle
)
8293 if (!sortSQL
.empty())
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());
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
8322 if (sortOrder
== SortOrderDescending
)
8324 std::string strSort
;
8326 if (StringUtils::EqualsNoCase(
8327 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
,
8329 strSort
= PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField
.c_str(), DESC
.c_str());
8331 strSort
= PrepareSQL("%s COLLATE ALPHANUM%s", strField
.c_str(), DESC
.c_str());
8335 void CMusicDatabase::UpdateTables(int version
)
8337 CLog::Log(LOGINFO
, "{} - updating tables", __FUNCTION__
);
8340 m_pDS
->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
8341 m_pDS
->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
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");
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());
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()));
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, "
8394 " iRating integer, "
8395 " lastScraped varchar(20) default NULL, "
8396 " dateAdded varchar (20) default NULL)");
8397 m_pDS
->exec("INSERT INTO album_new "
8399 " strAlbum, strMusicBrainzAlbumID, "
8400 " strArtists, strGenres, "
8403 " strMoods, strStyles, strThemes, "
8404 " strReview, strImage, strLabel, "
8409 " strAlbum, strMusicBrainzAlbumID, "
8410 " strArtists, strGenres, "
8411 " album.iYear, idThumb, "
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");
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) "
8443 " artist.idArtist, "
8444 " strArtist, strMusicBrainzArtistID, "
8445 " strBorn, strFormed, strGenres, strMoods, "
8446 " strStyles, strInstruments, strBiography, "
8447 " strDied, strDisbanded, strYearsActive, "
8448 " strImage, strFanart "
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");
8460 m_pDS
->exec("ALTER TABLE album_artist ADD strArtist text\n");
8461 m_pDS
->exec("ALTER TABLE song_artist ADD strArtist text\n");
8463 std::string sql
= "select idArtist,strArtist from artist";
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()));
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");
8481 m_pDS
->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
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()));
8493 PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''",
8494 CAlbum::ReleaseTypeToString(CAlbum::Single
).c_str()));
8498 m_pDS
->exec("ALTER TABLE song ADD mood text\n");
8502 m_pDS
->exec("ALTER TABLE song ADD dateAdded text");
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) "
8522 " strArtist, strMusicBrainzArtistID, "
8523 " strBorn, strFormed, strGenres, strMoods, "
8524 " strStyles, strInstruments, strBiography, "
8525 " strDied, strDisbanded, strYearsActive, "
8526 " strImage, strFanart, lastScraped "
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, "
8540 " iRating integer, "
8541 " lastScraped varchar(20) default NULL, "
8542 " strReleaseType text)");
8543 m_pDS
->exec("INSERT INTO album_new "
8545 " strAlbum, strMusicBrainzAlbumID, "
8546 " strArtists, strGenres, "
8549 " strMoods, strStyles, strThemes, "
8550 " strReview, strImage, strLabel, "
8551 " strType, iRating, lastScraped, "
8555 " strAlbum, strMusicBrainzAlbumID, "
8556 " strArtists, strGenres, "
8559 " strMoods, strStyles, strThemes, "
8560 " strReview, strImage, strLabel, "
8561 " strType, iRating, lastScraped, "
8564 m_pDS
->exec("DROP TABLE album");
8565 m_pDS
->exec("ALTER TABLE album_new RENAME TO album");
8569 m_pDS
->exec("DROP TABLE karaokedata");
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 "
8591 " idAlbum, idPath, "
8592 " strArtists, strGenres, strTitle, "
8593 " iTrack, iDuration, iYear, "
8595 " strFileName, strMusicBrainzTrackID, "
8596 " iTimesPlayed, iStartOffset, iEndOffset, "
8599 " rating, userrating, "
8600 " comment, mood, dateAdded)"
8603 " idAlbum, idPath, "
8604 " strArtists, strGenres, strTitle, "
8605 " iTrack, iDuration, iYear, "
8607 " strFileName, strMusicBrainzTrackID, "
8608 " iTimesPlayed, iStartOffset, iEndOffset, "
8613 " comment, mood, dateAdded"
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, "
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 "
8634 " strAlbum, strMusicBrainzAlbumID, "
8635 " strArtists, strGenres, "
8638 " strMoods, strStyles, strThemes, "
8639 " strReview, strImage, strLabel, "
8647 " strAlbum, strMusicBrainzAlbumID, "
8648 " strArtists, strGenres, "
8651 " strMoods, strStyles, strThemes, "
8652 " strReview, strImage, strLabel, "
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");
8667 m_pDS
->exec("UPDATE album SET fRating = fRating * 2");
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, "
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");
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.
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",
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
,
8722 m_pDS
->exec(strSQL
);
8723 strSQL
= PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist
,
8725 m_pDS
->exec(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
,
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");
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
);
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
);
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
);
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
);
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");
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)");
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)");
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, "
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 "
8841 " strAlbum, strMusicBrainzAlbumID, "
8842 " strArtistDisp, strArtistSort, strGenres, "
8843 " iYear, bCompilation, "
8844 " strMoods, strStyles, strThemes, "
8845 " strReview, strImage, strLabel, "
8847 " fRating, iUserrating, iVotes, "
8852 " strAlbum, strMusicBrainzAlbumID, "
8853 " strArtists, NULL, strGenres, "
8854 " iYear, bCompilation, "
8855 " strMoods, strStyles, strThemes, "
8856 " strReview, strImage, strLabel, "
8858 " fRating, iUserrating, iVotes, "
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 "
8878 " idAlbum, idPath, "
8879 " strArtistDisp, strArtistSort, strGenres, strTitle, "
8880 " iTrack, iDuration, iYear, "
8881 " strFileName, strMusicBrainzTrackID, "
8882 " iTimesPlayed, iStartOffset, iEndOffset, "
8884 " rating, userrating, votes, "
8885 " comment, mood, dateAdded)"
8888 " idAlbum, idPath, "
8889 " strArtists, NULL, strGenres, strTitle, "
8890 " iTrack, iDuration, iYear, "
8891 " strFileName, strMusicBrainzTrackID, "
8892 " iTimesPlayed, iStartOffset, iEndOffset, "
8894 " rating, userrating, votes, "
8895 " comment, mood, dateAdded"
8897 m_pDS
->exec("DROP TABLE song");
8898 m_pDS
->exec("ALTER TABLE song_new RENAME TO song");
8903 m_pDS
->exec("DROP TABLE cue");
8904 // Add strReplayGain to song table
8905 m_pDS
->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
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");
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
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");
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");
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");
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 = "
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 "
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");
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");
8989 // Remove album_genre table
8990 m_pDS
->exec("DROP TABLE album_genre");
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 * "
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()));
9008 // Create source table
9010 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
9011 // Create source_path table
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.
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");
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, "
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
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) "
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 "
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, "
9098 // Prepare as MySQL has different CAST datatypes
9099 m_pDS
->exec(PrepareSQL("INSERT INTO song_new "
9102 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9103 "iTrack, iDuration, "
9104 "strReleaseDate, strOrigReleaseDate, "
9105 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9106 "iTimesPlayed, iStartOffset, iEndOffset, "
9108 "rating, userrating, votes, "
9109 "comment, mood, strReplayGain, dateAdded) "
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, "
9120 "rating, userrating, votes, "
9121 "comment, mood, strReplayGain, dateAdded "
9123 m_pDS
->exec("DROP TABLE song");
9124 m_pDS
->exec("ALTER TABLE song_new RENAME TO song");
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");
9134 m_pDS
->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
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");
9196 m_pDS
->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
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)");
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
,
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
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, "
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, "
9275 "lastScraped, bScrapedMBID, idInfoSetting, "
9276 "dateAdded, dateNew, dateModified) "
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 "
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
9314 int CMusicDatabase::GetMusicNeedsTagScan()
9318 if (nullptr == m_pDB
)
9320 if (nullptr == m_pDS
)
9323 std::string sql
= "SELECT * FROM versiontagscan";
9324 if (!m_pDS
->query(sql
))
9327 if (m_pDS
->num_rows() != 1)
9333 int idVersion
= m_pDS
->fv("idVersion").get_asInt();
9334 int iNeedsScan
= m_pDS
->fv("iNeedsScan").get_asInt();
9336 if (idVersion
< iNeedsScan
)
9343 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
9348 void CMusicDatabase::SetMusicNeedsTagScan(int version
)
9350 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version
));
9353 void CMusicDatabase::SetMusicTagScanVersion(int version
/* = 0 */)
9356 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
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();
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
)
9438 if (nullptr == m_pDS
)
9441 std::string strSQL
= "SELECT idSong FROM songview ";
9442 if (!CDatabase::BuildSQL(strSQL
, filter
, strSQL
))
9444 strSQL
+= PrepareSQL(" ORDER BY RANDOM()");
9446 if (!m_pDS
->query(strSQL
))
9449 if (m_pDS
->num_rows() == 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()));
9461 return static_cast<unsigned int>(songIDs
.size());
9465 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
9470 int CMusicDatabase::GetSongsCount(const Filter
& filter
)
9474 if (nullptr == m_pDB
)
9476 if (nullptr == m_pDS
)
9479 std::string strSQL
= "select count(idSong) as NumSongs from songview ";
9480 if (!CDatabase::BuildSQL(strSQL
, filter
, strSQL
))
9483 if (!m_pDS
->query(strSQL
))
9485 if (m_pDS
->num_rows() == 0)
9491 int iNumSongs
= m_pDS
->fv("NumSongs").get_asInt();
9498 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
9503 bool CMusicDatabase::GetAlbumPath(int idAlbum
, std::string
& basePath
)
9506 std::vector
<std::pair
<std::string
, int>> paths
;
9507 if (!GetAlbumPaths(idAlbum
, paths
))
9510 for (const auto& pathpair
: paths
)
9512 if (basePath
.empty())
9513 basePath
= pathpair
.first
.c_str();
9515 URIUtils::GetCommonPath(basePath
, pathpair
.first
.c_str());
9520 bool CMusicDatabase::GetAlbumPaths(int idAlbum
, std::vector
<std::pair
<std::string
, int>>& paths
)
9526 if (nullptr == m_pDB
)
9528 if (nullptr == m_pDS2
)
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",
9545 if (!m_pDS2
->query(strSQL
))
9547 if (m_pDS2
->num_rows() == 0)
9549 // Album does not have a unique path, files are mixed
9554 while (!m_pDS2
->eof())
9556 paths
.emplace_back(m_pDS2
->fv("strPath").get_asString(),
9557 m_pDS2
->fv("song.idPath").get_asInt());
9560 // Cleanup recordset data
9566 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
9572 int CMusicDatabase::GetDiscnumberForPathID(int idPath
)
9578 if (nullptr == m_pDB
)
9580 if (nullptr == m_pDS2
)
9583 strSQL
= PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
9584 "WHERE idPath = %i",
9587 if (!m_pDS2
->query(strSQL
))
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
9598 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
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
)
9612 if (nullptr == m_pDB
)
9614 if (nullptr == m_pDS2
)
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",
9626 if (!m_pDS2
->query(strSQL
))
9628 int iRowsFound
= m_pDS2
->num_rows();
9629 if (iRowsFound
== 0)
9631 // Artist is not an album artist, no path to find
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
);
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())
9650 URIUtils::GetCommonPath(basePath
, path
);
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)
9681 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
);
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
);
9705 bool CMusicDatabase::GetAlbumFolder(const CAlbum
& album
,
9706 const std::string
& strAlbumPath
,
9707 std::string
& strFolder
)
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
))
9737 int iRowsFound
= m_pDS2
->num_rows();
9739 if (iRowsFound
== 1)
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())
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())
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
,
9808 if (nullptr == m_pDB
)
9810 if (nullptr == m_pDS
)
9813 // Check if source name already exists
9814 int idSource
= GetSourceByName(strName
);
9818 // Add new source and source paths
9820 strSQL
= PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9821 "VALUES(%i, '%s', '%s')",
9822 id
, strName
.c_str(), strMultipath
.c_str());
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());
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
);
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
;
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
))
9854 if (!m_pDS
->query(strSQL
))
9857 while (!m_pDS
->eof())
9859 albumIds
.push_back(m_pDS
->fv("idAlbum").get_asInt());
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')",
9870 m_pDS
->exec(strSQL
);
9873 CommitTransaction();
9879 CLog::Log(LOGERROR
, "{} failed with query ({})", __FUNCTION__
, strSQL
);
9880 RollbackTransaction();
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
)
9892 std::string strSourceMultipath
;
9896 if (nullptr == m_pDB
)
9898 if (nullptr == m_pDS
)
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
))
9908 if (m_pDS
->num_rows() > 0)
9910 idSource
= m_pDS
->fv("idSource").get_asInt();
9911 strSourceMultipath
= m_pDS
->fv("strMultipath").get_asString();
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)
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
);
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
);
9948 CLog::Log(LOGERROR
, "{} failed with query ({})", __FUNCTION__
, strSQL
);
9949 RollbackTransaction();
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
)
9968 std::string
strPath(strPath1
);
9969 if (!URIUtils::HasSlashAtEnd(strPath
))
9970 URIUtils::AddSlashAtEnd(strPath
);
9972 if (nullptr == m_pDB
)
9974 if (nullptr == m_pDS
)
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
))
9981 if (m_pDS
->num_rows() > 0)
9982 idSource
= m_pDS
->fv("idSource").get_asInt();
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",
9991 if (!m_pDS
->query(strSQL
))
9993 if (m_pDS
->num_rows() == 1)
9994 idSource
= m_pDS
->fv("idSource").get_asInt();
10000 CLog::Log(LOGERROR
, "{} path: {} ({}) failed", __FUNCTION__
, strSQL
, strPath1
);
10006 bool CMusicDatabase::AddAlbumSource(int idAlbum
, int idSource
)
10008 std::string strSQL
;
10009 strSQL
= PrepareSQL("INSERT INTO album_source (idAlbum, idSource) "
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
)
10023 if (nullptr == m_pDS
)
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",
10032 if (!m_pDS
->query(strSQL
))
10034 while (!m_pDS
->eof())
10036 sourceIds
.push_back(m_pDS
->fv("idSource").get_asInt());
10043 // Find sources using song paths, check each source path individually
10044 if (nullptr == m_pDS2
)
10046 strSQL
= "SELECT idSource, strPath FROM source_path";
10047 if (!m_pDS
->query(strSQL
))
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
))
10058 if (m_pDS2
->num_rows() > 0)
10059 sourceIds
.push_back(m_pDS
->fv("idSource").get_asInt());
10067 //Add album sources
10068 for (auto idSource
: sourceIds
)
10070 AddAlbumSource(idAlbum
, idSource
);
10077 CLog::Log(LOGERROR
, "{} path: {} ({}) failed", __FUNCTION__
, strSQL
, strPath
);
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())
10101 // Check individual sources match
10104 if (nullptr == m_pDB
)
10106 if (nullptr == m_pDS
)
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
))
10119 if (m_pDS
->num_rows() != 1)
10121 // Missing source, or name duplication
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)
10142 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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);
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
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
);
10182 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, strSQL
);
10187 bool CMusicDatabase::UpdateSources()
10189 //Check library and xml sources match
10190 VECSOURCES
sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10191 if (CheckSources(sources
))
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
);
10207 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10212 bool CMusicDatabase::GetSources(CFileItemList
& items
)
10216 if (nullptr == m_pDB
)
10218 if (nullptr == m_pDS
)
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
))
10230 int iRowsFound
= m_pDS
->num_rows();
10231 if (iRowsFound
== 0)
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
);
10241 while (!m_pDS
->eof())
10243 if (idSource
!= m_pDS
->fv("source.idSource").get_asInt())
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;
10263 sourcePaths
.push_back(m_pDS
->fv("source_path.strPath").get_asString());
10267 if (!sourcePaths
.empty())
10269 //Store paths for final source
10270 items
[items
.Size() - 1].get()->SetProperty("paths", sourcePaths
);
10271 sourcePaths
.clear();
10281 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10286 bool CMusicDatabase::GetSourcesByArtist(int idArtist
, CFileItem
* item
)
10288 if (nullptr == m_pDB
)
10290 if (nullptr == m_pDS
)
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",
10302 if (!m_pDS
->query(strSQL
))
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
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",
10315 if (!m_pDS
->query(strSQL
))
10317 if (m_pDS
->num_rows() == 0)
10319 //No sources, but query successful
10325 CVariant
artistSources(CVariant::VariantTypeArray
);
10326 while (!m_pDS
->eof())
10328 artistSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10333 item
->SetProperty("sourceid", artistSources
);
10338 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
10343 bool CMusicDatabase::GetSourcesByAlbum(int idAlbum
, CFileItem
* item
)
10345 if (nullptr == m_pDB
)
10347 if (nullptr == m_pDS
)
10352 std::string strSQL
;
10353 strSQL
= PrepareSQL("SELECT idSource FROM album_source "
10354 "WHERE album_source.idAlbum = %i "
10355 "ORDER BY idSource",
10357 if (!m_pDS
->query(strSQL
))
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());
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
10377 if (nullptr == m_pDS2
)
10379 strSQL
= "SELECT idSource, strPath FROM source_path";
10380 if (!m_pDS
->query(strSQL
))
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
))
10391 if (m_pDS2
->num_rows() > 0)
10392 albumSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10401 item
->SetProperty("sourceid", albumSources
);
10406 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
10411 bool CMusicDatabase::GetSourcesBySong(int idSong
, const std::string
& strPath1
, CFileItem
* item
)
10413 if (nullptr == m_pDB
)
10415 if (nullptr == m_pDS
)
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",
10426 if (!m_pDS
->query(strSQL
))
10428 if (m_pDS
->num_rows() == 0 && !strPath1
.empty())
10430 // Check via song path instead
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",
10439 if (!m_pDS
->query(strSQL
))
10442 CVariant
songSources(CVariant::VariantTypeArray
);
10443 while (!m_pDS
->eof())
10445 songSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10450 item
->SetProperty("sourceid", songSources
);
10455 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
10460 int CMusicDatabase::GetSourceByName(const std::string
& strSource
)
10464 if (nullptr == m_pDB
)
10466 if (nullptr == m_pDS
)
10469 std::string strSQL
;
10470 strSQL
= PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource
.c_str());
10472 if (!m_pDS
->query(strSQL
))
10474 int iRowsFound
= m_pDS
->num_rows();
10475 if (iRowsFound
!= 1)
10480 return m_pDS
->fv("idSource").get_asInt();
10484 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
)
10500 if (nullptr == m_pDS
)
10503 std::string strSQL
= PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'",
10504 strArtist
.c_str());
10507 if (!m_pDS
->query(strSQL
))
10509 int iRowsFound
= m_pDS
->num_rows();
10510 if (iRowsFound
!= 1)
10515 int lResult
= m_pDS
->fv("artist.idArtist").get_asInt();
10521 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10526 int CMusicDatabase::GetArtistByMatch(const CArtist
& artist
)
10528 std::string strSQL
;
10531 if (nullptr == m_pDB
|| nullptr == m_pDS
)
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());
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
))
10545 int iRowsFound
= m_pDS
->num_rows();
10546 if (iRowsFound
!= 1)
10549 // Match on artist name, relax mbid restriction
10550 return GetArtistByName(artist
.strArtist
);
10552 int lResult
= m_pDS
->fv("idArtist").get_asInt();
10558 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
10563 bool CMusicDatabase::GetArtistFromSong(int idSong
, CArtist
& artist
)
10567 if (nullptr == m_pDB
)
10569 if (nullptr == m_pDS
)
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",
10577 if (!m_pDS
->query(strSQL
))
10579 int iRowsFound
= m_pDS
->num_rows();
10580 if (iRowsFound
!= 1)
10586 artist
= GetArtistFromDataset(m_pDS
.get());
10593 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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",
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",
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
)
10629 if (nullptr == m_pDS
)
10632 std::string strSQL
;
10633 if (strArtist
.empty())
10635 PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum
.c_str());
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());
10641 if (!m_pDS
->query(strSQL
))
10643 int iRowsFound
= m_pDS
->num_rows();
10644 if (iRowsFound
!= 1)
10649 return m_pDS
->fv("idAlbum").get_asInt();
10653 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10658 bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string
& strAlbum
,
10659 const std::string
& strArtist
,
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
)
10673 if (nullptr == m_pDS
)
10676 std::string strSQL
;
10677 if (strArtist
.empty())
10678 strSQL
= PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
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());
10685 if (!m_pDS
->query(strSQL
))
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();
10697 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10702 bool CMusicDatabase::SearchAlbumsByArtistName(const std::string
& strArtist
, CFileItemList
& items
)
10706 if (nullptr == m_pDB
)
10708 if (nullptr == m_pDS
)
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
))
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
);
10731 m_pDS
->close(); // cleanup recordset data
10736 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10741 int CMusicDatabase::GetAlbumByName(const std::string
& strAlbum
,
10742 const std::vector
<std::string
>& artist
)
10744 return GetAlbumByName(
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
)
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());
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
))
10771 int iRowsFound
= m_pDS
->num_rows();
10772 if (iRowsFound
!= 1)
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();
10784 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
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();
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) ";
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 <> " +
10839 PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
10840 "AND album_artist.idAlbum = album.idAlbum)",
10842 ExecuteQuery(strSQL
);
10843 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
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) ";
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 <> " +
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)",
10870 ExecuteQuery(strSQL
);
10871 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
10873 if (CommitMultipleExecute())
10876 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
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
)
10891 if (nullptr == m_pDS
)
10894 std::string strSQL
;
10895 strSQL
= PrepareSQL("SELECT idGenre FROM genre "
10896 "WHERE genre.strGenre LIKE '%s'",
10899 if (!m_pDS
->query(strSQL
))
10901 int iRowsFound
= m_pDS
->num_rows();
10902 if (iRowsFound
!= 1)
10907 return m_pDS
->fv("genre.idGenre").get_asInt();
10911 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10916 bool CMusicDatabase::GetGenresJSON(CFileItemList
& items
, bool bSources
)
10918 std::string strSQL
;
10921 if (nullptr == m_pDB
)
10923 if (nullptr == m_pDS
)
10926 strSQL
= "SELECT %s FROM genre ";
10928 extFilter
.AppendField("genre.idGenre");
10929 extFilter
.AppendField("genre.strGenre");
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
))
10947 strSQL
= PrepareSQL(strSQL
, extFilter
.fields
.c_str()) + strSQLExtra
;
10950 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
10952 if (!m_pDS
->query(strSQL
))
10954 int iRowsFound
= m_pDS
->num_rows();
10955 if (iRowsFound
== 0)
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
);
10968 while (!m_pDS
->eof())
10970 if (idGenre
!= m_pDS
->fv("genre.idGenre").get_asInt())
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;
10991 int sourceid
= m_pDS
->fv("album_source.idSource").get_asInt();
10993 genreSources
.push_back(sourceid
);
10999 //Store sources for final genre
11000 items
[items
.Size() - 1].get()->SetProperty("sourceid", genreSources
);
11010 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, strSQL
);
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
;
11021 albumtitle
= GetAlbumById(idAlbum
);
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
;
11031 albumtitle
= albumtitle
+ " - " + disctitle
;
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'",
11073 return GetSingleValueInt(strSQL
);
11076 bool CMusicDatabase::SetPathHash(const std::string
& path
, const std::string
& hash
)
11080 if (nullptr == m_pDB
)
11082 if (nullptr == m_pDS
)
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
))
11091 int idPath
= AddPath(path
);
11095 std::string strSQL
=
11096 PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash
.c_str(), idPath
);
11097 m_pDS
->exec(strSQL
);
11103 CLog::Log(LOGERROR
, "{} ({}, {}) failed", __FUNCTION__
, path
, hash
);
11109 bool CMusicDatabase::GetPathHash(const std::string
& path
, std::string
& hash
)
11113 if (nullptr == m_pDB
)
11115 if (nullptr == m_pDS
)
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)
11122 hash
= m_pDS
->fv("strHash").get_asString();
11127 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, path
);
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
)
11170 if (nullptr == m_pDS
)
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.
11179 where
= PrepareSQL(" WHERE strPath='%s'", path
.c_str());
11181 where
= PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path
.c_str()),
11183 std::string sql
= "SELECT * FROM songview" + where
+ " ORDER BY strFileName";
11184 if (!m_pDS
->query(sql
))
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
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
));
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
;
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
+ ")";
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
;
11224 return iRowsFound
> 0;
11228 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, path
);
11233 void CMusicDatabase::CheckArtistLinksChanged()
11235 std::string strSQL
= "SELECT COUNT(1) FROM removed_link ";
11236 int iLinks
= GetSingleValueInt(strSQL
, m_pDS
);
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
)
11250 if (nullptr == m_pDS
)
11256 if (!m_pDS
->query("SELECT strPath FROM path"))
11258 int iRowsFound
= m_pDS
->num_rows();
11259 if (iRowsFound
== 0)
11264 while (!m_pDS
->eof())
11266 paths
.insert(m_pDS
->fv("strPath").get_asString());
11274 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
11279 bool CMusicDatabase::SetSongUserrating(const std::string
& filePath
, int userrating
)
11283 if (filePath
.empty())
11285 if (nullptr == m_pDB
)
11287 if (nullptr == m_pDS
)
11290 int songID
= GetSongIDFromPath(filePath
);
11294 return SetSongUserrating(songID
, userrating
);
11298 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, filePath
, userrating
);
11303 bool CMusicDatabase::SetSongUserrating(int idSong
, int userrating
)
11307 if (nullptr == m_pDB
)
11309 if (nullptr == m_pDS
)
11313 PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating
, idSong
);
11319 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, idSong
, userrating
);
11324 bool CMusicDatabase::SetAlbumUserrating(const int idAlbum
, int userrating
)
11328 if (nullptr == m_pDB
)
11330 if (nullptr == m_pDS
)
11336 PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating
, idAlbum
);
11342 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, idAlbum
, userrating
);
11347 bool CMusicDatabase::SetSongVotes(const std::string
& filePath
, int votes
)
11351 if (filePath
.empty())
11353 if (nullptr == m_pDB
)
11355 if (nullptr == m_pDS
)
11358 int songID
= GetSongIDFromPath(filePath
);
11362 std::string sql
= PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes
, songID
);
11369 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, filePath
, votes
);
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());
11387 if (nullptr == m_pDB
)
11389 if (nullptr == m_pDS
)
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
))
11402 if (m_pDS
->num_rows() == 0)
11408 int songID
= m_pDS
->fv("idSong").get_asInt();
11414 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, filePath
);
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();
11426 gui
->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(
11427 LIBRARY_HAS_MUSIC
, GetSongsCount() > 0);
11434 bool CMusicDatabase::SetScraperAll(const std::string
& strBaseDir
, const ADDON::ScraperPtr
& scraper
)
11436 if (nullptr == m_pDB
)
11438 if (nullptr == m_pDS
)
11440 std::string strSQL
;
11441 int idSetting
= -1;
11444 CONTENT_TYPE content
= CONTENT_NONE
;
11446 // Build where clause from virtual path
11448 CMusicDbUrl musicUrl
;
11449 SortDescription sorting
;
11450 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
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
;
11463 return false; //Only artists and albums have info settings
11465 std::string strSQLWhere
;
11466 if (!BuildSQL(strSQLWhere
, extFilter
, strSQLWhere
))
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 ";
11478 strSQL
= "UPDATE album SET idInfoSetting = %i ";
11479 strSQL
= PrepareSQL(strSQL
, 0) + strSQLWhere
;
11480 m_pDS
->exec(strSQL
);
11482 //Remove orphaned settings
11483 CleanupInfoSettings();
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 ";
11496 strSQL
= "UPDATE album SET idInfoSetting = %i ";
11497 strSQL
= PrepareSQL(strSQL
, idSetting
) + strSQLWhere
;
11498 m_pDS
->exec(strSQL
);
11500 CommitTransaction();
11505 RollbackTransaction();
11506 CLog::Log(LOGERROR
, "{} - ({}, {}) failed", __FUNCTION__
, strBaseDir
, strSQL
);
11511 bool CMusicDatabase::SetScraper(int id
,
11512 const CONTENT_TYPE
& content
,
11513 const ADDON::ScraperPtr
& scraper
)
11515 if (nullptr == m_pDB
)
11517 if (nullptr == m_pDS
)
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";
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();
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";
11545 strSQL
= "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
11546 strSQL
= PrepareSQL(strSQL
, idSetting
, id
);
11547 m_pDS
->exec(strSQL
);
11550 { // Update info setting
11551 strSQL
= "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' "
11552 "WHERE idSetting = %i";
11554 PrepareSQL(strSQL
, scraper
->ID().c_str(), scraper
->GetPathSettings().c_str(), idSetting
);
11555 m_pDS
->exec(strSQL
);
11557 CommitTransaction();
11562 RollbackTransaction();
11563 CLog::Log(LOGERROR
, "{} - ({}, {}) failed", __FUNCTION__
, id
, strSQL
);
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
)
11576 if (nullptr == m_pDS
)
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";
11585 strSQL
= strSQL
+ "album ON album.idInfoSetting = infosetting.idSetting "
11586 "WHERE album.idAlbum = %i";
11587 strSQL
= PrepareSQL(strSQL
, id
);
11588 m_pDS
->query(strSQL
);
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
) &&
11601 scraper
= std::dynamic_pointer_cast
<ADDON::CScraper
>(addon
);
11604 scraper
->SetPathSettings(content
, strSettings
);
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
;
11626 CLog::Log(LOGERROR
, "{} -({}, {} {}) failed", __FUNCTION__
, id
, scraperUUID
, strSettings
);
11631 bool CMusicDatabase::ScraperInUse(const std::string
& scraperID
) const
11635 if (nullptr == m_pDB
)
11637 if (nullptr == m_pDS
)
11641 PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID
.c_str());
11642 if (!m_pDS
->query(sql
) || m_pDS
->num_rows() == 0)
11647 bool found
= m_pDS
->fv(0).get_asInt() > 0;
11653 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, scraperID
);
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
))
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);
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
);
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
))
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
))
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())
11738 strFolder
= settings
.m_strPath
;
11739 if (!URIUtils::HasSlashAtEnd(strFolder
))
11740 URIUtils::AddSlashAtEnd(strFolder
);
11741 strFolder
= URIUtils::GetDirectory(strFolder
);
11742 if (strFolder
.empty())
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())
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
)
11767 if (nullptr == m_pDS
)
11769 if (nullptr == m_pDS2
)
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
)
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();
11799 albumIds
.reserve(total
);
11800 while (!m_pDS
->eof())
11802 albumIds
.push_back(m_pDS
->fv("idAlbum").get_asInt());
11807 for (const auto& albumId
: albumIds
)
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
);
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
))
11836 "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist",
11837 __FUNCTION__
, album
.strAlbum
, strAlbumPath
);
11840 strPath
= strAlbumPath
;
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
);
11854 strPath
= URIUtils::AddFileToFolder(strFolder
, strAlbumArtist
);
11855 pathfound
= CDirectory::Exists(strPath
);
11857 pathfound
= CDirectory::Create(strPath
);
11860 CLog::Log(LOGDEBUG
,
11861 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
11862 __FUNCTION__
, album
.strAlbum
, strPath
);
11865 std::string strAlbumFolder
;
11866 pathfound
= GetAlbumFolder(album
, strAlbumPath
, strAlbumFolder
);
11869 strPath
= URIUtils::AddFileToFolder(strPath
, strAlbumFolder
);
11870 pathfound
= CDirectory::Exists(strPath
);
11872 pathfound
= CDirectory::Create(strPath
);
11875 CLog::Log(LOGDEBUG
,
11876 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
11877 __FUNCTION__
, album
.strAlbum
, strPath
);
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
);
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");
11912 savedArtfile
= URIUtils::AddFileToFolder(strPath
, art
.first
);
11913 CServiceBroker::GetTextureCache()->Export(art
.second
, savedArtfile
,
11914 settings
.m_overwrite
);
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())
11934 // Export song playback history to single file only
11935 if (settings
.IsSingleFile() && settings
.IsItemExported(ELIBEXPORT_SONGS
))
11937 if (!ExportSongHistory(pMain
, progressDialog
))
11941 if ((settings
.IsArtists() || artistfoldersonly
) && !strFolder
.empty())
11943 // Find artists to export
11944 std::vector
<int> artistIds
;
11947 if (settings
.IsItemExported(ELIBEXPORT_ALBUMARTISTS
))
11948 filter
.AppendWhere("EXISTS(SELECT 1 FROM album_artist "
11949 "WHERE album_artist.idArtist = artist.idArtist)",
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 )",
11958 filter
.AppendWhere(
11959 "EXISTS (SELECT 1 FROM song_artist "
11960 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)",
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)",
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();
11979 artistIds
.reserve(total
);
11980 while (!m_pDS
->eof())
11982 artistIds
.push_back(m_pDS
->fv("idArtist").get_asInt());
11987 for (const auto& artistId
: artistIds
)
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
);
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
);
12014 strPath
= URIUtils::AddFileToFolder(strFolder
, strPath
);
12015 pathfound
= CDirectory::Exists(strPath
);
12017 pathfound
= CDirectory::Create(strPath
);
12020 CLog::Log(LOGDEBUG
,
12021 "CMusicDatabase::{} - Not exporting artist {} as could not create {}",
12022 __FUNCTION__
, artist
.strArtist
, strPath
);
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
);
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");
12053 savedArtfile
= URIUtils::AddFileToFolder(strPath
, art
.first
);
12054 CServiceBroker::GetTextureCache()->Export(art
.second
, savedArtfile
,
12055 settings
.m_overwrite
);
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())
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
);
12085 data
["file"] = xmlFile
;
12086 if (iFailCount
> 0)
12087 data
["failcount"] = iFailCount
;
12088 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnExport",
12094 CLog::Log(LOGERROR
, "CMusicDatabase::{} failed", __FUNCTION__
);
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();
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()));
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());
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())
12171 CLog::Log(LOGERROR
, "{0} failed", __FUNCTION__
);
12176 void CMusicDatabase::ImportFromXML(const std::string
& xmlFile
, CGUIDialogProgress
* progressDialog
)
12180 if (nullptr == m_pDB
)
12182 if (nullptr == m_pDS
)
12185 CXBMCTinyXML xmlDoc
;
12186 if (!xmlDoc
.LoadFile(xmlFile
) && progressDialog
)
12188 HELPERS::ShowOKDialogLines(CVariant
{20197}, CVariant
{38354}); //"Unable to read xml file"
12192 TiXmlElement
* root
= xmlDoc
.RootElement();
12196 TiXmlElement
* entry
= root
->FirstChildElement();
12200 // Count the number of artists, albums and songs
12203 if (StringUtils::CompareNoCase(entry
->Value(), "artist", 6) == 0 ||
12204 StringUtils::CompareNoCase(entry
->Value(), "album", 5) == 0)
12206 else if (StringUtils::CompareNoCase(entry
->Value(), "song", 4) == 0)
12209 entry
= entry
->NextSiblingElement();
12212 BeginTransaction();
12213 entry
= root
->FirstChildElement();
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
);
12228 GetArtist(idArtist
, artist
, true); // include discography
12229 artist
.MergeScrapedArtist(importedArtist
, true);
12230 UpdateArtist(artist
);
12233 CLog::Log(LOGDEBUG
, "{} - Not import additional artist data as {} not found",
12234 __FUNCTION__
, importedArtist
.strArtist
);
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
);
12247 GetAlbum(idAlbum
, album
, true);
12248 album
.MergeScrapedAlbum(importedAlbum
, true);
12249 UpdateAlbum(album
); //Will replace song artists if present in xml
12252 CLog::Log(LOGDEBUG
, "{} - Not import additional album data as {} not found", __FUNCTION__
,
12253 importedAlbum
.strAlbum
);
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();
12270 CommitTransaction();
12272 // Import song playback history <song> entries found
12274 if (!ImportSongHistory(xmlFile
, songtotal
, progressDialog
))
12277 CGUIComponent
* gui
= CServiceBroker::GetGUI();
12279 gui
->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
12283 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
12284 RollbackTransaction();
12286 if (progressDialog
)
12287 progressDialog
->Close();
12290 bool CMusicDatabase::ImportSongHistory(const std::string
& xmlFile
,
12292 CGUIDialogProgress
* progressDialog
)
12294 bool bHistSongExists
= false;
12297 CXBMCTinyXML xmlDoc
;
12298 if (!xmlDoc
.LoadFile(xmlFile
))
12301 TiXmlElement
* root
= xmlDoc
.RootElement();
12305 TiXmlElement
* entry
= root
->FirstChildElement();
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();
12320 std::string strArtistDisp
;
12321 std::string strTitle
;
12323 std::string strFilename
;
12324 std::string strMusicBrainzTrackID
;
12325 std::string strAlbum
;
12326 std::string strMusicBrainzAlbumID
;
12327 std::string strAlbumArtistDisp
;
12329 std::string lastplayed
;
12330 int iUserrating
= 0;
12331 float fRating
= 0.0;
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");
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
12358 XMLUtils::GetInt(entry
, "votes", iVotes
);
12359 const TiXmlElement
* userrating
= entry
->FirstChildElement("userrating");
12363 float max_rating
= 10;
12364 XMLUtils::GetFloat(entry
, "userrating", rating
);
12365 if (userrating
->QueryFloatAttribute("max", &max_rating
) == TIXML_SUCCESS
&&
12367 rating
*= (10.f
/ max_rating
); // Normalise the value to between 0 and 10
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, ");
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, ");
12384 strSQLSong
+= PrepareSQL("'%s', ", strMusicBrainzAlbumID
.c_str());
12385 strSQLSong
+= PrepareSQL("%d, ", iTimesplayed
);
12386 if (lastplayed
.empty())
12387 strSQLSong
+= PrepareSQL("NULL, ");
12389 strSQLSong
+= PrepareSQL("'%s', ", lastplayed
.c_str());
12391 PrepareSQL("%.1f, %d, %d, -1, -1)", static_cast<double>(fRating
), iVotes
, iUserrating
);
12394 strSQLSong
= ", " + strSQLSong
;
12395 strSQL
+= strSQLSong
;
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())
12411 CLog::Log(LOGINFO
, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__
,
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 " +
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");
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) "
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) "
12486 m_pDS
->exec(strSQL
);
12487 if (progressDialog
)
12489 progressDialog
->Progress();
12490 if (progressDialog
->IsCanceled())
12492 RollbackTransaction();
12493 m_pDS
->exec("DROP TABLE HistSong");
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.
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");
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
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), "
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
);
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), "
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");
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();
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();
12613 eventLog
->Add(EventPtr(new CNotificationEvent(20197, strLine
, EventLevel::Information
)));
12619 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
12620 RollbackTransaction();
12621 if (bHistSongExists
)
12622 m_pDS
->exec("DROP TABLE HistSong");
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())
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.
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());
12701 idArtist
= GetArtistByName(item
.GetMusicInfoTag()->GetArtistString());
12705 if (GetArtist(idArtist
, artist
))
12706 SetPropertiesFromArtist(item
, artist
);
12708 int idAlbum
= item
.GetMusicInfoTag()->GetAlbumId();
12710 idAlbum
= GetAlbumByName(item
.GetMusicInfoTag()->GetAlbum(),
12711 item
.GetMusicInfoTag()->GetArtistString());
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
)
12727 if (nullptr == m_pDB
)
12729 if (nullptr == m_pDS
)
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
);
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
)
12767 if (nullptr == m_pDS
)
12770 // don't set <foo>.<bar> art types - these are derivative types from parent items
12771 if (artType
.find('.') != std::string::npos
)
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());
12780 int artId
= m_pDS
->fv(0).get_asInt();
12782 sql
= PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url
.c_str(), artId
);
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());
12796 CLog::Log(LOGERROR
, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__
, mediaId
, mediaType
,
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))
12809 if (nullptr == m_pDB
)
12811 if (nullptr == m_pDS2
)
12812 return false; // using dataset 2 as we're likely called in loops on dataset 1
12816 filter
.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId
, MediaTypeSong
));
12818 filter
.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId
, MediaTypeAlbum
),
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 "
12826 if (!BuildSQL(strSQL
, filter
, strSQL
))
12829 if (!(artistId
> 0))
12831 // Artist ID unknown, so lookup album artist for albums and songs
12832 std::string strSQL2
;
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
;
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"
12901 artitem
.prefix
+= m_pDS2
->fv("iorder").get_asString();
12903 art
.emplace_back(artitem
);
12907 return !art
.empty();
12911 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, strSQL
);
12916 bool CMusicDatabase::GetArtForItem(int mediaId
,
12917 const std::string
& mediaType
,
12918 std::map
<std::string
, std::string
>& art
)
12922 if (nullptr == m_pDB
)
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()));
12936 return !art
.empty();
12940 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, mediaId
);
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
);
12975 bool CMusicDatabase::GetArtTypes(const MediaType
& mediaType
, std::vector
<std::string
>& artTypes
)
12979 if (nullptr == m_pDB
)
12981 if (nullptr == m_pDS
)
12984 std::string strSQL
=
12985 PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType
.c_str());
12987 if (!m_pDS
->query(strSQL
))
12989 int iRowsFound
= m_pDS
->num_rows();
12990 if (iRowsFound
== 0)
12996 while (!m_pDS
->eof())
12998 artTypes
.emplace_back(m_pDS
->fv(0).get_asString());
13006 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, mediaType
);
13011 std::vector
<std::string
> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId
,
13012 const MediaType
& mediaType
)
13014 CScraperUrl thumbURL
;
13015 if (mediaType
== MediaTypeArtist
)
13018 if (GetArtist(mediaId
, artist
))
13019 thumbURL
= artist
.thumbURL
;
13021 else if (mediaType
== MediaTypeAlbum
)
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())
13034 if (std::find(result
.begin(), result
.end(), artType
) == result
.end())
13035 result
.push_back(artType
);
13040 std::vector
<CScraperUrl::SUrlEntry
> CMusicDatabase::GetAvailableArtForItem(
13041 int mediaId
, const MediaType
& mediaType
, const std::string
& artType
)
13043 CScraperUrl thumbURL
;
13044 if (mediaType
== MediaTypeArtist
)
13047 if (GetArtist(mediaId
, artist
))
13048 thumbURL
= artist
.thumbURL
;
13050 else if (mediaType
== MediaTypeAlbum
)
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
);
13068 int CMusicDatabase::GetOrderFilter(const std::string
& type
,
13069 const SortDescription
& sorting
,
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
;
13078 if (sorting
.sortOrder
== SortOrderDescending
)
13081 if (sorting
.sortBy
== SortByRandom
)
13082 orderfields
.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
13086 SortUtils::GetFieldsForSQLSort(type
, sorting
.sortBy
, fields
);
13087 for (const auto& it
: fields
)
13089 std::string strField
;
13090 if (it
== FieldYear
)
13091 strField
= "iYear";
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");
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
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
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
));
13135 filter
.AppendOrder(name
+ DESC
);
13137 return iFieldsAdded
;
13140 bool CMusicDatabase::GetFilter(CDbUrl
& musicUrl
, Filter
& filter
, SortDescription
& sorting
)
13142 if (!musicUrl
.IsValid())
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()))
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());
13189 option
= options
.find("role");
13190 if (option
!= options
.end())
13192 if (option
->second
.asString() == "all" || option
->second
.asString() == "%")
13193 idRole
= -1000; //All roles
13195 idRole
= GetRoleByName(option
->second
.asString());
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
13207 strRoleSQL
= PrepareSQL(" AND song_artist.idRole = %i ", idRole
);
13209 int idArtist
= -1, idGenre
= -1, idAlbum
= -1, idSong
= -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());
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());
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());
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());
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")
13286 { // Not an "artists" smart playlist with roles rules, so get filter from options
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)",
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()));
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
13315 c) When album artists only and role = 1 (an "artist") then only the album_artist
13318 std::string albumArtistSQL
, songArtistSQL
;
13319 ExistsSubQuery
albumArtistSub("album_artist",
13320 "album_artist.idArtist = artistview.idArtist");
13321 // Prepare album artist subquery SQL
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
));
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)",
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)",
13346 albumArtistSub
.AppendWhere(strGenre
);
13349 // Prepare song artist subquery SQL
13350 ExistsSubQuery
songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
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
));
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
);
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()));
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));
13442 filter
.AppendWhere(PrepareSQL(
13443 "EXISTS(SELECT 1 FROM album_source "
13444 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
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");
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()));
13471 songArtistSub
.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole
));
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
);
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
);
13516 { // No artist given
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")
13534 filter
.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum
));
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()));
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));
13559 filter
.AppendWhere(PrepareSQL(
13560 "EXISTS(SELECT 1 FROM album_source "
13561 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
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()));
13573 filter
.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
13574 "song.idSong AND song_genre.idGenre = %i)",
13577 std::string songArtistClause
, albumArtistClause
;
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)",
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
);
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
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()));
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()));
13667 filter
.AppendWhere(PrepareSQL("songview.idSong = %i", idSong
));
13670 filter
.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum
));
13673 filter
.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc
));
13676 filter
.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre "
13677 "WHERE song_genre.idGenre = %i)",
13681 filter
.AppendWhere(PrepareSQL(
13682 "EXISTS(SELECT 1 FROM album_source "
13683 "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)",
13686 std::string songArtistClause
, albumArtistClause
;
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)",
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
);
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()))
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
13749 musicUrl
.RemoveOption("filter");
13755 std::string
CMusicDatabase::GetMediaDateFromFile(const std::string
& strFileNameAndPath
)
13757 if (strFileNameAndPath
.empty())
13758 return std::string();
13760 CDateTime dateMedia
;
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
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 "
13794 item
.GetDynPath().c_str());
13795 if (!m_pDS
->query(strSQL
.c_str()) || m_pDS
->num_rows() == 0)
13797 if (!AddAudioBook(item
))
13801 strSQL
= PrepareSQL("UPDATE audiobook SET bookmark=%i "
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)
13815 bookmark
= m_pDS
->fv(0).get_asInt();