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 "FileItemList.h"
15 #include "GUIInfoManager.h"
17 #include "ServiceBroker.h"
19 #include "TextureCache.h"
22 #include "addons/Addon.h"
23 #include "addons/AddonManager.h"
24 #include "addons/AddonSystemSettings.h"
25 #include "addons/Scraper.h"
26 #include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h"
27 #include "dbwrappers/dataset.h"
28 #include "dialogs/GUIDialogKaiToast.h"
29 #include "dialogs/GUIDialogProgress.h"
30 #include "dialogs/GUIDialogSelect.h"
31 #include "events/EventLog.h"
32 #include "events/NotificationEvent.h"
33 #include "filesystem/Directory.h"
34 #include "filesystem/DirectoryCache.h"
35 #include "filesystem/File.h"
36 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
37 #include "guilib/GUIComponent.h"
38 #include "guilib/GUIWindowManager.h"
39 #include "guilib/LocalizeStrings.h"
40 #include "guilib/guiinfo/GUIInfoLabels.h"
41 #include "interfaces/AnnouncementManager.h"
42 #include "messaging/helpers/DialogHelper.h"
43 #include "messaging/helpers/DialogOKHelper.h"
44 #include "music/MusicDbUrl.h"
45 #include "music/MusicLibraryQueue.h"
46 #include "music/tags/MusicInfoTag.h"
47 #include "network/Network.h"
48 #include "network/cddb.h"
49 #include "playlists/SmartPlayList.h"
50 #include "profiles/ProfileManager.h"
51 #include "settings/AdvancedSettings.h"
52 #include "settings/MediaSourceSettings.h"
53 #include "settings/Settings.h"
54 #include "settings/SettingsComponent.h"
55 #include "storage/MediaManager.h"
56 #include "utils/FileUtils.h"
57 #include "utils/LegacyPathTranslation.h"
58 #include "utils/MathUtils.h"
59 #include "utils/Random.h"
60 #include "utils/StringUtils.h"
61 #include "utils/URIUtils.h"
62 #include "utils/XMLUtils.h"
63 #include "utils/log.h"
68 using namespace XFILE
;
69 using namespace MUSICDATABASEDIRECTORY
;
70 using namespace KODI::MESSAGING
;
71 using namespace MUSIC_INFO
;
73 using ADDON::AddonPtr
;
74 using KODI::MESSAGING::HELPERS::DialogResponse
;
76 #define RECENTLY_PLAYED_LIMIT 25
77 #define MIN_FULL_SEARCH_LENGTH 3
79 #ifdef HAS_OPTICAL_DRIVE
81 using namespace MEDIA_DETECT
;
84 static void AnnounceRemove(const std::string
& content
, int id
)
87 data
["type"] = content
;
89 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
90 data
["transaction"] = true;
91 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnRemove", data
);
94 static void AnnounceUpdate(const std::string
& content
, int id
, bool added
= false)
97 data
["type"] = content
;
99 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
100 data
["transaction"] = true;
102 data
["added"] = true;
103 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnUpdate", data
);
106 CMusicDatabase::CMusicDatabase(void)
108 m_translateBlankArtist
= true;
111 CMusicDatabase::~CMusicDatabase(void)
116 bool CMusicDatabase::Open()
118 return CDatabase::Open(
119 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
);
122 void CMusicDatabase::CreateTables()
124 CLog::Log(LOGINFO
, "create artist table");
125 m_pDS
->exec("CREATE TABLE artist ( idArtist integer primary key, "
126 " strArtist varchar(256), strMusicBrainzArtistID text, "
127 " strSortName text, "
128 " strType text, strGender text, strDisambiguation text, "
129 " strBorn text, strFormed text, strGenres text, strMoods text, "
130 " strStyles text, strInstruments text, strBiography text, "
131 " strDied text, strDisbanded text, strYearsActive text, "
133 " lastScraped varchar(20) default NULL, "
134 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
135 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
136 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
137 // Create missing artist tag artist [Missing].
139 PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
140 "VALUES( %i, '%s', '%s', '%s' )",
141 BLANKARTIST_ID
, BLANKARTIST_NAME
.c_str(), BLANKARTIST_NAME
.c_str(),
142 BLANKARTIST_FAKEMUSICBRAINZID
.c_str());
145 CLog::Log(LOGINFO
, "create album table");
146 m_pDS
->exec("CREATE TABLE album (idAlbum integer primary key, "
147 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
148 " strReleaseGroupMBID text, "
149 " strArtistDisp text, strArtistSort text, strGenres text, "
150 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
151 " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
152 " bCompilation integer not null default '0', "
153 " strMoods text, strStyles text, strThemes text, "
154 " strReview text, strImage text, strLabel text, "
156 " strReleaseStatus TEXT, "
157 " fRating FLOAT NOT NULL DEFAULT 0, "
158 " iVotes INTEGER NOT NULL DEFAULT 0, "
159 " iUserrating INTEGER NOT NULL DEFAULT 0, "
160 " lastScraped varchar(20) default NULL, "
161 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
162 " strReleaseType text, "
163 " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
164 " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
165 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
166 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
168 CLog::Log(LOGINFO
, "create audiobook table");
169 m_pDS
->exec("CREATE TABLE audiobook (idBook integer primary key, "
170 " strBook varchar(256), strAuthor text,"
171 " bookmark integer, file text,"
172 " dateAdded varchar (20) default NULL)");
174 CLog::Log(LOGINFO
, "create album_artist table");
175 m_pDS
->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, "
178 CLog::Log(LOGINFO
, "create album_source table");
179 m_pDS
->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
181 CLog::Log(LOGINFO
, "create genre table");
182 m_pDS
->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
184 CLog::Log(LOGINFO
, "create path table");
185 m_pDS
->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
187 CLog::Log(LOGINFO
, "create source table");
189 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
191 CLog::Log(LOGINFO
, "create source_path table");
192 m_pDS
->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
194 CLog::Log(LOGINFO
, "create song table");
195 m_pDS
->exec("CREATE TABLE song (idSong integer primary key, "
196 " idAlbum integer, idPath integer, "
197 " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
198 " iTrack integer, iDuration integer, "
199 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
200 " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
201 " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
202 " lastplayed varchar(20) default NULL, "
203 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
204 " userrating INTEGER NOT NULL DEFAULT 0, "
205 " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
206 " iBitRate INTEGER NOT NULL DEFAULT 0, "
207 " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
208 " strVideoURL TEXT, "
209 " strReplayGain text, "
210 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
211 CLog::Log(LOGINFO
, "create song_artist table");
212 m_pDS
->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder "
213 "integer, strArtist text)");
214 CLog::Log(LOGINFO
, "create song_genre table");
215 m_pDS
->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
217 CLog::Log(LOGINFO
, "create role table");
218 m_pDS
->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
219 m_pDS
->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role
221 CLog::Log(LOGINFO
, "create infosetting table");
222 m_pDS
->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, "
223 "strScraperPath TEXT, strSettings TEXT)");
225 CLog::Log(LOGINFO
, "create discography table");
226 m_pDS
->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
227 "strReleaseGroupMBID TEXT)");
229 CLog::Log(LOGINFO
, "create art table");
230 m_pDS
->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, "
231 "media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
233 CLog::Log(LOGINFO
, "create versiontagscan table");
234 m_pDS
->exec("CREATE TABLE versiontagscan "
235 "(idVersion INTEGER, iNeedsScan INTEGER, "
236 "lastscanned VARCHAR(20), "
237 "lastcleaned VARCHAR(20), "
238 "artistlinksupdated VARCHAR(20), "
239 "genresupdated VARCHAR(20))");
240 m_pDS
->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
241 GetSchemaVersion()));
243 CLog::Log(LOGINFO
, "create removed_link table");
244 m_pDS
->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
247 void CMusicDatabase::CreateAnalytics()
249 CLog::Log(LOGINFO
, "{} - creating indices", __FUNCTION__
);
250 m_pDS
->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
251 m_pDS
->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
252 m_pDS
->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
253 m_pDS
->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
255 m_pDS
->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
256 m_pDS
->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
258 m_pDS
->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
260 m_pDS
->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
261 m_pDS
->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
262 m_pDS
->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
264 m_pDS
->exec("CREATE INDEX idxPath ON path(strPath(255))");
266 m_pDS
->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
267 m_pDS
->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
269 m_pDS
->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
271 m_pDS
->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
272 m_pDS
->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
274 m_pDS
->exec("CREATE INDEX idxSong ON song(strTitle(255))");
275 m_pDS
->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
276 m_pDS
->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
277 m_pDS
->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
278 m_pDS
->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
279 //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
280 m_pDS
->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
282 m_pDS
->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
283 m_pDS
->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
284 m_pDS
->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
285 m_pDS
->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
287 m_pDS
->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
288 m_pDS
->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
290 m_pDS
->exec("CREATE INDEX idxRole on role(strRole(255))");
292 m_pDS
->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
294 m_pDS
->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
296 CLog::Log(LOGINFO
, "create triggers");
297 m_pDS
->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
298 " DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
299 " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
300 " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
301 " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
303 m_pDS
->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
304 " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
305 " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
306 " DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
307 " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
309 m_pDS
->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
310 " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
311 " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
312 " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
314 m_pDS
->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
315 " DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
316 " DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
319 /* Maintain date new and last modified for songs, albums and artists using triggers
320 MySQL triggers cannot modify a table that is already being used by the statement that invoked
321 the trigger (to avoid recursion), but can set NEW column values before insert or update.
322 Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
323 Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
324 // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
326 bool bisMySQL
= StringUtils::EqualsNoCase(
327 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
, "mysql");
330 { // SQLite trigger syntax - AFTER INSERT/UPDATE
331 m_pDS
->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
332 " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
333 " AND NEW.dateNew IS NULL;"
334 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
336 m_pDS
->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
337 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
338 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
340 m_pDS
->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
341 " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
342 " AND NEW.dateNew IS NULL;"
343 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
345 m_pDS
->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
346 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
347 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
349 m_pDS
->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
350 " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
351 " AND NEW.dateNew IS NULL;"
352 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
354 m_pDS
->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
355 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
356 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
359 m_pDS
->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
360 " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
364 { // MySQL trigger syntax - BEFORE INSERT/UPDATE
365 m_pDS
->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
366 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
367 " SET NEW.dateModified = now();"
369 m_pDS
->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
370 " SET NEW.dateModified = now()");
372 m_pDS
->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
373 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
374 " SET NEW.dateModified = now();"
376 m_pDS
->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
377 " SET NEW.dateModified = now()");
379 m_pDS
->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
380 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
381 " SET NEW.dateModified = now();"
383 m_pDS
->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
384 " SET NEW.dateModified = now()");
386 m_pDS
->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
387 " UPDATE versiontagscan SET genresupdated = now()");
390 // Triggers to maintain recent changes to album and song artist links in removed_link table
391 m_pDS
->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
392 "DELETE FROM removed_link "
393 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
395 m_pDS
->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
396 "DELETE FROM removed_link "
397 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
399 CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
401 // Create native functions stored in DB (MySQL/MariaDB only)
402 CreateNativeDBFunctions();
404 // we create views last to ensure all indexes are rolled in
408 void CMusicDatabase::CreateRemovedLinkTriggers()
410 // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
411 m_pDS
->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
412 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
413 " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
415 m_pDS
->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
416 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
417 " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
422 void CMusicDatabase::CreateViews()
424 CLog::Log(LOGINFO
, "create song view");
425 m_pDS
->exec("CREATE VIEW songview AS SELECT "
426 " song.idSong AS idSong, "
427 " song.strArtistDisp AS strArtists,"
428 " song.strArtistSort AS strArtistSort,"
429 " song.strGenres AS strGenres,"
431 " iTrack, iDuration, "
432 " song.strReleaseDate as strReleaseDate, "
433 " song.strOrigReleaseDate as strOrigReleaseDate, "
434 " song.strDiscSubtitle as strDiscSubtitle, "
436 " strMusicBrainzTrackID, "
437 " iTimesPlayed, iStartOffset, iEndOffset, "
443 " song.idAlbum AS idAlbum, "
446 " album.strReleaseStatus as strReleaseStatus,"
447 " album.bCompilation AS bCompilation,"
448 " album.bBoxedSet AS bBoxedSet, "
449 " album.strArtistDisp AS strAlbumArtists,"
450 " album.strArtistSort AS strAlbumArtistSort,"
451 " album.strReleaseType AS strAlbumReleaseType,"
452 " song.mood as mood,"
453 " song.strReplayGain, "
458 " song.strVideoURL as strVideoURL, "
459 " album.iAlbumDuration AS iAlbumDuration, "
460 " album.iDiscTotal as iDiscTotal, "
461 " song.dateAdded as dateAdded, "
462 " song.dateNew AS dateNew, "
463 " song.dateModified AS dateModified "
466 " song.idAlbum=album.idAlbum"
468 " song.idPath=path.idPath");
470 CLog::Log(LOGINFO
, "create album view");
471 m_pDS
->exec("CREATE VIEW albumview AS SELECT "
472 "album.idAlbum AS idAlbum, "
474 "strMusicBrainzAlbumID, "
475 "strReleaseGroupMBID, "
476 "album.strArtistDisp AS strArtists, "
477 "album.strArtistSort AS strArtistSort, "
478 "album.strGenres AS strGenres, "
479 "album.strReleaseDate as strReleaseDate, "
480 "album.strOrigReleaseDate as strOrigReleaseDate, "
481 "album.bBoxedSet AS bBoxedSet, "
482 "album.strMoods AS strMoods, "
483 "album.strStyles AS strStyles, "
489 "album.strImage as strImage, "
491 "album.iUserrating, "
496 "dateAdded, dateNew, dateModified, "
497 "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song "
498 "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
501 "(SELECT MAX(song.lastplayed) FROM song "
502 "WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
506 CLog::Log(LOGINFO
, "create artist view");
507 m_pDS
->exec("CREATE VIEW artistview AS SELECT"
508 " idArtist, strArtist, strSortName, "
509 " strMusicBrainzArtistID, "
510 " strType, strGender, strDisambiguation, "
511 " strBorn, strFormed, strGenres,"
512 " strMoods, strStyles, strInstruments, "
513 " strBiography, strDied, strDisbanded, "
514 " strYearsActive, strImage, "
515 " bScrapedMBID, lastScraped, "
516 " dateAdded, dateNew, dateModified "
519 CLog::Log(LOGINFO
, "create albumartist view");
520 m_pDS
->exec("CREATE VIEW albumartistview AS SELECT"
521 " album_artist.idAlbum AS idAlbum, "
522 " album_artist.idArtist AS idArtist, "
524 " 'AlbumArtist' AS strRole, "
525 " artist.strArtist AS strArtist, "
526 " artist.strSortName AS strSortName,"
527 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
528 " album_artist.iOrder AS iOrder "
531 " album_artist.idArtist = artist.idArtist");
533 CLog::Log(LOGINFO
, "create songartist view");
534 m_pDS
->exec("CREATE VIEW songartistview AS SELECT"
535 " song_artist.idSong AS idSong, "
536 " song_artist.idArtist AS idArtist, "
537 " song_artist.idRole AS idRole, "
538 " role.strRole AS strRole, "
539 " artist.strArtist AS strArtist, "
540 " artist.strSortName AS strSortName,"
541 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
542 " song_artist.iOrder AS iOrder "
545 " song_artist.idArtist = artist.idArtist "
547 " song_artist.idRole = role.idRole");
550 void CMusicDatabase::CreateNativeDBFunctions()
552 // Create native functions in MySQL/MariaDB database only
553 if (!StringUtils::EqualsNoCase(
554 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
,
557 CLog::Log(LOGINFO
, "Create native MySQL/MariaDB functions");
558 /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
559 default utf8_general_ci collation that SQLite does via a collation sequence callback
560 function to StringUtils::AlphaNumericCompare
561 !@todo: the video needs these defined too for sorting in DB, then creation can be made common
564 // udfFirstNumberPos finds the position of the first digit in a string
565 m_pDS
->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
566 m_pDS
->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
571 "SQL SECURITY INVOKER \n"
573 " DECLARE position int; \n"
574 " DECLARE tmppos int; \n"
575 " SET position = 5000; \n"
576 " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
577 " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
578 " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
579 " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
580 " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
581 " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
582 " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
583 " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
584 " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
585 " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
586 " IF(position = 5000) THEN RETURN 0; END IF;\n"
587 " RETURN position; \n"
590 // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input
591 m_pDS
->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
592 m_pDS
->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
593 "RETURNS varchar(1024)\n"
597 "SQL SECURITY INVOKER\n"
599 " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
601 " DECLARE symbolCharsLen int;\n"
602 " DECLARE symbol char(1);\n"
603 " SET sortString = instring;\n"
605 " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
606 " WHILE(i <= symbolCharsLen) DO\n"
607 " SET symbol = SUBSTRING(symbolChars, i, 1);\n"
608 " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
611 " RETURN sortString;\n"
614 // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
615 m_pDS
->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
616 m_pDS
->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
617 "sameOrderChars char(25))\n"
618 "RETURNS varchar(1024)\n"
622 "SQL SECURITY INVOKER\n"
624 " DECLARE sortString varchar(1024);\n"
625 " DECLARE shiftedString varchar(1024);\n"
626 " DECLARE inLength int;\n"
627 " DECLARE shiftedLength int;\n"
628 " DECLARE totalSympadLength int;\n"
629 " DECLARE symbolshifted512 varchar(1024);\n"
630 " DECLARE numStartIndex int; \n"
631 " DECLARE numEndIndex int; \n"
632 " DECLARE padLength int; \n"
633 " DECLARE totalPadLength int; \n"
635 " DECLARE sameOrderCharsLen int;\n"
636 " SET totalPadLength = 0; \n"
637 " SET instring = TRIM(instring);\n"
638 " SET inLength = CHAR_LENGTH(inString);\n"
639 " SET sortString = instring; \n"
640 " SET numStartIndex = udfFirstNumberPos(instring); \n"
641 " SET numEndIndex = 0; \n"
643 " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
644 " WHILE(i <= sameOrderCharsLen) DO \n"
645 " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
648 " WHILE(numStartIndex <> 0) DO \n"
649 " SET numStartIndex = numStartIndex + numEndIndex; \n"
650 " SET numEndIndex = numStartIndex; \n"
651 " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
652 " SET numEndIndex = numEndIndex + 1; \n"
654 " SET numEndIndex = numEndIndex - 1; \n"
655 " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
656 " IF padLength < 0 THEN \n"
657 " SET padLength = 0; \n"
659 " IF inLength + totalPadLength + padlength > 1024 THEN \n"
660 " -- Padding more digits would be too long, pad this one just enough \n"
661 " SET padLength = 1024 - inLength - totalPadLength; \n"
662 " SET numStartIndex = 0; \n"
664 " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
665 " SET totalPadLength = totalPadLength + padLength; \n"
666 " IF numStartIndex <> 0 THEN \n"
667 " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
670 " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
671 " -- when there is space as this could double string length. Note '\\' needs escaping \n"
672 " SET numStartIndex = 1; \n"
673 " SET numEndIndex = inLength + totalPadLength; \n"
674 " IF numEndIndex < 1024 THEN \n"
675 " SET shiftedLength = 0; \n"
676 " SET totalSympadLength = 0; \n"
677 " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
678 " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
679 " SET numStartIndex = numStartIndex + 512; \n"
680 " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
681 " IF totalSympadLength = 0 THEN \n"
682 " SET shiftedString = symbolshifted512; \n"
684 " IF totalSympadLength + shiftedLength > 1024 THEN \n"
685 " SET shiftedLength = 1024 - totalSympadLength; \n"
686 " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
688 " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
690 " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
692 " SET sortString = shiftedString; \n"
694 " RETURN sortString; \n"
699 void CMusicDatabase::SplitPath(const std::string
& strFileNameAndPath
,
700 std::string
& strPath
,
701 std::string
& strFileName
)
703 URIUtils::Split(strFileNameAndPath
, strPath
, strFileName
);
704 // Keep protocol options as part of the path
705 if (URIUtils::IsURL(strFileNameAndPath
))
707 CURL
url(strFileNameAndPath
);
708 if (!url
.GetProtocolOptions().empty())
709 strPath
+= "|" + url
.GetProtocolOptions();
713 bool CMusicDatabase::AddAlbum(CAlbum
& album
, int idSource
)
716 SetLibraryLastUpdated();
718 album
.idAlbum
= AddAlbum(album
.strAlbum
, //
719 album
.strMusicBrainzAlbumID
, //
720 album
.strReleaseGroupMBID
, //
721 album
.GetAlbumArtistString(), //
722 album
.GetAlbumArtistSort(), //
723 album
.GetGenreString(), //
724 album
.strReleaseDate
, //
725 album
.strOrigReleaseDate
, //
729 album
.strReleaseStatus
, //
730 album
.bCompilation
, //
733 // Add the album artists
734 // Album must have at least one artist so set artist to [Missing]
735 if (album
.artistCredits
.empty())
736 AddAlbumArtist(BLANKARTIST_ID
, album
.idAlbum
, BLANKARTIST_NAME
, 0);
737 for (auto artistCredit
= album
.artistCredits
.begin(); artistCredit
!= album
.artistCredits
.end();
740 artistCredit
->idArtist
=
741 AddArtist(artistCredit
->GetArtist(), artistCredit
->GetMusicBrainzArtistID(),
742 artistCredit
->GetSortName());
743 AddAlbumArtist(artistCredit
->idArtist
, album
.idAlbum
, artistCredit
->GetArtist(),
744 static_cast<int>(std::distance(album
.artistCredits
.begin(), artistCredit
)));
748 for (auto song
= album
.songs
.begin(); song
!= album
.songs
.end(); ++song
)
750 song
->idAlbum
= album
.idAlbum
;
752 song
->idSong
= AddSong(song
->idSong
, //
756 song
->strMusicBrainzTrackID
, //
757 song
->strFileName
, //
761 song
->GetArtistString(), //
762 song
->GetArtistSort(), //
766 song
->strReleaseDate
, //
767 song
->strOrigReleaseDate
, //
768 song
->strDiscSubtitle
, //
769 song
->iTimesPlayed
, //
770 song
->iStartOffset
, song
->iEndOffset
, //
775 song
->iBPM
, song
->iBitRate
, song
->iSampleRate
, song
->iChannels
, //
776 song
->songVideoURL
, //
779 // Song must have at least one artist so set artist to [Missing]
780 if (song
->artistCredits
.empty())
781 AddSongArtist(BLANKARTIST_ID
, song
->idSong
, ROLE_ARTIST
, BLANKARTIST_NAME
, 0);
783 for (auto artistCredit
= song
->artistCredits
.begin(); artistCredit
!= song
->artistCredits
.end();
786 artistCredit
->idArtist
=
787 AddArtist(artistCredit
->GetArtist(), artistCredit
->GetMusicBrainzArtistID(),
788 artistCredit
->GetSortName());
790 artistCredit
->idArtist
, song
->idSong
, ROLE_ARTIST
,
791 artistCredit
->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
792 static_cast<int>(std::distance(song
->artistCredits
.begin(), artistCredit
)));
794 // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
795 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
796 AddSongContributors(song
->idSong
, song
->GetContributors(), song
->GetComposerSort());
799 // Set album duration as total of all songs on album.
800 // Folder layout may mean AddAlbum call has added more songs to an existing album
802 strSQL
= PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album
.idAlbum
);
803 int albumDuration
= GetSingleValueInt(strSQL
);
804 m_pDS
->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration
,
809 AddAlbumSource(album
.idAlbum
, idSource
);
812 // Use album path, or failing that song paths to determine sources for the album
813 AddAlbumSources(album
.idAlbum
, album
.strPath
);
816 for (const auto& albumArt
: album
.art
)
817 SetArtForItem(album
.idAlbum
, MediaTypeAlbum
, albumArt
.first
, albumArt
.second
);
819 // Set album disc total
821 PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
822 "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
824 // Set a non-compilation album as a boxset if it has three or more distinct disc titles
825 if (!album
.bBoxedSet
&& !album
.bCompilation
)
827 strSQL
= PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
829 int numTitles
= GetSingleValueInt(strSQL
);
832 strSQL
= PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album
.idAlbum
);
836 m_pDS
->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
837 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
840 PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
841 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
844 std::string albumdateadded
=
845 GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album
.idAlbum
));
846 m_pDS
->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
847 albumdateadded
.c_str(), album
.idAlbum
));
849 /* Update artist dateAdded values for artists involved in album as song or album artists.
850 Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
851 derived from song dateadded values which are usually file dates (or the last scan).
852 It is used to indicate those artists with recent media.
853 For artists that are neither album nor song artists (other roles only) dateadded will be null.
855 std::vector
<std::string
> artistIDs
;
856 // Get distinct song and album artist IDs for this album
857 GetArtistsByAlbum(album
.idAlbum
, artistIDs
);
858 std::string strIDs
= "(" + StringUtils::Join(artistIDs
, ",") + ")";
859 strSQL
= PrepareSQL("UPDATE artist SET dateAdded = '%s' "
860 "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
861 albumdateadded
.c_str(), strIDs
.c_str(), albumdateadded
.c_str());
868 bool CMusicDatabase::UpdateAlbum(CAlbum
& album
)
871 SetLibraryLastUpdated();
873 const std::string itemSeparator
=
874 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
878 bool isBoxset
= IsAlbumBoxset(album
.idAlbum
);
880 { // not a boxset already so make sure we have enough discs & they all have titles
881 bool canBeBoxset
= false;
883 strSQL
= PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album
.idAlbum
);
884 int numDiscs
= GetSingleValueInt(strSQL
);
889 for (discValue
= 1; discValue
<= numDiscs
; discValue
++)
892 PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
893 "song.iTrack >> 16 = %i",
894 album
.idAlbum
, discValue
);
895 std::string currentTitle
= GetSingleValue(strSQL
);
896 if (currentTitle
.empty())
898 currentTitle
= StringUtils::Format("{} {}", g_localizeStrings
.Get(427), discValue
);
900 PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
901 "song.iTrack >> 16 = %i",
902 currentTitle
.c_str(), album
.idAlbum
, discValue
);
903 ExecuteQuery(strSQL
);
907 if (!canBeBoxset
&& album
.bBoxedSet
)
909 CLog::Log(LOGINFO
, "{} : Album with id [{}] does not meet the requirements for a boxset.",
910 __FUNCTION__
, album
.idAlbum
);
911 album
.bBoxedSet
= false;
915 UpdateAlbum(album
.idAlbum
, album
.strAlbum
, album
.strMusicBrainzAlbumID
, //
916 album
.strReleaseGroupMBID
, //
917 album
.GetAlbumArtistString(), album
.GetAlbumArtistSort(), //
918 album
.GetGenreString(), //
919 StringUtils::Join(album
.moods
, itemSeparator
), //
920 StringUtils::Join(album
.styles
, itemSeparator
), //
921 StringUtils::Join(album
.themes
, itemSeparator
), //
923 album
.thumbURL
.GetData(), //
926 album
.strReleaseStatus
, //
927 album
.fRating
, album
.iUserrating
, album
.iVotes
, //
928 album
.strReleaseDate
, //
929 album
.strOrigReleaseDate
, //
931 album
.bCompilation
, //
932 album
.releaseType
, //
935 if (!album
.bArtistSongMerge
)
937 // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
938 for (const auto& artistCredit
: album
.artistCredits
)
939 UpdateArtistScrapedMBID(artistCredit
.GetArtistId(), artistCredit
.GetMusicBrainzArtistID());
943 // Replace the album artists with those scraped or set by JSON
944 DeleteAlbumArtistsByAlbum(album
.idAlbum
);
945 // Album must have at least one artist so set artist to [Missing]
946 if (album
.artistCredits
.empty())
947 AddAlbumArtist(BLANKARTIST_ID
, album
.idAlbum
, BLANKARTIST_NAME
, 0);
948 for (auto artistCredit
= album
.artistCredits
.begin(); artistCredit
!= album
.artistCredits
.end();
951 artistCredit
->idArtist
=
952 AddArtist(artistCredit
->GetArtist(), artistCredit
->GetMusicBrainzArtistID(),
953 artistCredit
->GetSortName(), true);
954 AddAlbumArtist(artistCredit
->idArtist
, album
.idAlbum
, artistCredit
->GetArtist(),
955 static_cast<int>(std::distance(album
.artistCredits
.begin(), artistCredit
)));
957 /* Replace the songs with those scraped or imported, but if new songs is empty
958 (such as when called from JSON) do not remove the original ones
959 Also updates nested data e.g. song artists, song genres and contributors
960 Do not check for artist link changes, that is done later for all songs and album
962 int albumDuration
= 0;
963 for (auto& song
: album
.songs
)
966 albumDuration
+= song
.iDuration
;
968 if (albumDuration
> 0)
969 m_pDS
->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
970 albumDuration
, album
.idAlbum
));
973 if (!album
.art
.empty())
974 SetArtForItem(album
.idAlbum
, MediaTypeAlbum
, album
.art
);
976 CheckArtistLinksChanged();
982 void CMusicDatabase::NormaliseSongDates(std::string
& strRelease
, std::string
& strOriginal
)
984 // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
986 iDate
= StringUtils::DateStringToYYYYMMDD(strRelease
);
989 iDate
= StringUtils::DateStringToYYYYMMDD(strOriginal
);
992 // Avoid missing release or original values unless both invalid or empty
993 if (!strRelease
.empty() && strOriginal
.empty())
994 strOriginal
= strRelease
;
995 else if (strRelease
.empty() && !strOriginal
.empty())
996 strRelease
= strOriginal
;
999 int CMusicDatabase::AddSong(const int idSong
,
1000 const CDateTime
& dtDateNew
,
1002 const std::string
& strTitle
,
1003 const std::string
& strMusicBrainzTrackID
,
1004 const std::string
& strPathAndFileName
,
1005 const std::string
& strComment
,
1006 const std::string
& strMood
,
1007 const std::string
& strThumb
,
1008 const std::string
& artistDisp
,
1009 const std::string
& artistSort
,
1010 const std::vector
<std::string
>& genres
,
1013 const std::string
& strReleaseDate
,
1014 const std::string
& strOrigReleaseDate
,
1015 std::string
& strDiscSubtitle
,
1016 const int iTimesPlayed
,
1019 const CDateTime
& dtLastPlayed
,
1027 const std::string
& songVideoURL
,
1028 const ReplayGain
& replayGain
)
1034 // We need at least the title
1035 if (strTitle
.empty())
1038 if (nullptr == m_pDB
)
1040 if (nullptr == m_pDS
)
1043 std::string strPath
, strFileName
;
1044 SplitPath(strPathAndFileName
, strPath
, strFileName
);
1045 int idPath
= AddPath(strPath
);
1049 if (!strMusicBrainzTrackID
.empty())
1050 strSQL
= PrepareSQL("SELECT idSong FROM song WHERE "
1051 "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
1052 idAlbum
, iTrack
, strMusicBrainzTrackID
.c_str());
1054 strSQL
= PrepareSQL("SELECT idSong FROM song WHERE "
1055 "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
1056 "AND strMusicBrainzTrackID IS NULL",
1057 idAlbum
, strFileName
.c_str(), strTitle
.c_str(), iTrack
);
1059 if (!m_pDS
->query(strSQL
))
1062 if (m_pDS
->num_rows() == 0)
1066 // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
1067 bool isBoxset
= IsAlbumBoxset(idAlbum
);
1068 if (isBoxset
&& strDiscSubtitle
.empty())
1070 int discno
= iTrack
>> 16;
1071 strDiscSubtitle
= StringUtils::Format("{} {}", g_localizeStrings
.Get(427), discno
);
1074 // Validate ISO8601 dates and ensure none missing
1075 std::string strRelease
= strReleaseDate
;
1076 std::string strOriginal
= strOrigReleaseDate
;
1077 NormaliseSongDates(strRelease
, strOriginal
);
1079 // Get dateAdded from music file timestamp
1080 std::string strDateMedia
= GetMediaDateFromFile(strPathAndFileName
);
1082 strSQL
= "INSERT INTO song ("
1083 "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
1084 "strTitle, iTrack, iDuration, "
1085 "strReleaseDate, strOrigReleaseDate, iBPM, "
1086 "iBitrate, iSampleRate, iChannels, "
1087 "strDiscSubtitle, strFileName, dateAdded, "
1088 "strMusicBrainzTrackID, strArtistSort, "
1089 "iTimesPlayed, iStartOffset, iEndOffset, "
1090 "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
1093 // Song ID is autoincremented and dateNew set by trigger
1094 strSQL
+= PrepareSQL("VALUES (NULL, NULL, ");
1096 //Reuse song Id and original date when the Id added
1097 strSQL
+= PrepareSQL("VALUES (%i, '%s', ", idSong
, dtDateNew
.GetAsDBDateTime().c_str());
1100 PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
1101 idAlbum
, idPath
, artistDisp
.c_str(), strTitle
.c_str(), iTrack
, iDuration
,
1102 strRelease
.c_str(), strOriginal
.c_str(), iBPM
, iBitRate
, iSampleRate
,
1103 iChannels
, strDiscSubtitle
.c_str(), strFileName
.c_str(), strDateMedia
.c_str());
1105 if (strMusicBrainzTrackID
.empty())
1106 strSQL
+= PrepareSQL(",NULL");
1108 strSQL
+= PrepareSQL(",'%s'", strMusicBrainzTrackID
.c_str());
1109 if (artistSort
.empty() || artistSort
.compare(artistDisp
) == 0)
1110 strSQL
+= PrepareSQL(",NULL");
1112 strSQL
+= PrepareSQL(",'%s'", artistSort
.c_str());
1114 if (dtLastPlayed
.IsValid())
1115 strSQL
+= PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", //
1116 iTimesPlayed
, iStartOffset
, iEndOffset
,
1117 dtLastPlayed
.GetAsDBDateTime().c_str(), //
1118 static_cast<double>(rating
), userrating
, votes
, //
1119 strComment
.c_str(), strMood
.c_str(), replayGain
.Get().c_str());
1121 strSQL
+= PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", //
1122 iTimesPlayed
, iStartOffset
, iEndOffset
, //
1123 static_cast<double>(rating
), userrating
, votes
, //
1124 strComment
.c_str(), strMood
.c_str(), replayGain
.Get().c_str());
1125 m_pDS
->exec(strSQL
);
1127 idNew
= (int)m_pDS
->lastinsertid();
1133 idNew
= m_pDS
->fv("idSong").get_asInt();
1135 UpdateSong(idNew
, //
1137 strMusicBrainzTrackID
, //
1138 strPathAndFileName
, //
1148 strOrigReleaseDate
, //
1151 iStartOffset
, iEndOffset
, //
1153 rating
, userrating
, votes
, //
1155 iBPM
, iBitRate
, iSampleRate
, iChannels
, songVideoURL
);
1157 if (!strThumb
.empty())
1158 SetArtForItem(idNew
, MediaTypeSong
, "thumb", strThumb
);
1160 // Song genres added, and genre string updated to use the standardised genre names
1161 AddSongGenres(idNew
, genres
);
1163 AnnounceUpdate(MediaTypeSong
, idNew
, true);
1167 CLog::Log(LOGERROR
, "musicdatabase:unable to addsong ({})", strSQL
);
1172 bool CMusicDatabase::GetSong(int idSong
, CSong
& song
)
1178 if (nullptr == m_pDB
)
1180 if (nullptr == m_pDS
)
1183 std::string strSQL
=
1184 PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
1185 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1186 " WHERE songview.idSong = %i "
1187 " ORDER BY songartistview.idRole, songartistview.iOrder",
1190 if (!m_pDS
->query(strSQL
))
1192 int iRowsFound
= m_pDS
->num_rows();
1193 if (iRowsFound
== 0)
1199 int songArtistOffset
= song_enumCount
;
1201 song
= GetSongFromDataset(m_pDS
->get_sql_record());
1202 while (!m_pDS
->eof())
1204 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
1206 int idSongArtistRole
= record
->at(songArtistOffset
+ artistCredit_idRole
).get_asInt();
1207 if (idSongArtistRole
== ROLE_ARTIST
)
1208 song
.artistCredits
.emplace_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
1210 song
.AppendArtistRole(GetArtistRoleFromDataset(record
, songArtistOffset
));
1214 m_pDS
->close(); // cleanup recordset data
1219 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
1225 bool CMusicDatabase::UpdateSong(CSong
& song
, bool bArtists
/*= true*/, bool bArtistLinks
/*= true*/)
1227 int result
= UpdateSong(song
.idSong
,
1229 song
.strMusicBrainzTrackID
, //
1230 song
.strFileName
, //
1234 song
.GetArtistString(), //
1235 song
.GetArtistSort(), //
1239 song
.strReleaseDate
, //
1240 song
.strOrigReleaseDate
, //
1241 song
.strDiscSubtitle
, //
1242 song
.iTimesPlayed
, //
1243 song
.iStartOffset
, song
.iEndOffset
, //
1245 song
.rating
, song
.userrating
, song
.votes
, //
1247 song
.iBPM
, song
.iBitRate
, song
.iSampleRate
, song
.iChannels
, //
1252 // Replace Song genres and update genre string using the standardised genre names
1253 AddSongGenres(song
.idSong
, song
.genre
);
1256 //Replace song artists and contributors
1257 DeleteSongArtistsBySong(song
.idSong
);
1258 // Song must have at least one artist so set artist to [Missing]
1259 if (song
.artistCredits
.empty())
1260 AddSongArtist(BLANKARTIST_ID
, song
.idSong
, ROLE_ARTIST
, BLANKARTIST_NAME
, 0);
1261 for (auto artistCredit
= song
.artistCredits
.begin(); artistCredit
!= song
.artistCredits
.end();
1264 artistCredit
->idArtist
=
1265 AddArtist(artistCredit
->GetArtist(), artistCredit
->GetMusicBrainzArtistID(),
1266 artistCredit
->GetSortName());
1267 AddSongArtist(artistCredit
->idArtist
, song
.idSong
, ROLE_ARTIST
, artistCredit
->GetArtist(),
1268 static_cast<int>(std::distance(song
.artistCredits
.begin(), artistCredit
)));
1270 // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
1271 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
1272 AddSongContributors(song
.idSong
, song
.GetContributors(), song
.GetComposerSort());
1275 CheckArtistLinksChanged();
1281 int CMusicDatabase::UpdateSong(int idSong
,
1282 const std::string
& strTitle
,
1283 const std::string
& strMusicBrainzTrackID
,
1284 const std::string
& strPathAndFileName
,
1285 const std::string
& strComment
,
1286 const std::string
& strMood
,
1287 const std::string
& strThumb
,
1288 const std::string
& artistDisp
,
1289 const std::string
& artistSort
,
1290 const std::vector
<std::string
>& genres
,
1293 const std::string
& strReleaseDate
,
1294 const std::string
& strOrigReleaseDate
,
1295 const std::string
& strDiscSubtitle
,
1299 const CDateTime
& dtLastPlayed
,
1303 const ReplayGain
& replayGain
,
1308 const std::string
& songVideoURL
)
1314 std::string strPath
, strFileName
;
1315 SplitPath(strPathAndFileName
, strPath
, strFileName
);
1316 int idPath
= AddPath(strPath
);
1318 // Validate ISO8601 dates and ensure none missing
1319 std::string strRelease
= strReleaseDate
;
1320 std::string strOriginal
= strOrigReleaseDate
;
1321 NormaliseSongDates(strRelease
, strOriginal
);
1323 std::string strDateMedia
= GetMediaDateFromFile(strPathAndFileName
);
1325 strSQL
= PrepareSQL(
1326 "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
1327 " strTitle = '%s', iTrack = %i, iDuration = %i, "
1328 "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
1329 "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
1330 "dateAdded = '%s', strVideoURL = '%s'",
1331 idPath
, artistDisp
.c_str(),
1334 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
)
1336 strTitle
.c_str(), iTrack
, iDuration
, strRelease
.c_str(), strOriginal
.c_str(),
1337 strDiscSubtitle
.c_str(), strFileName
.c_str(), iBPM
, iBitRate
, iSampleRate
, iChannels
,
1338 strDateMedia
.c_str(), songVideoURL
.c_str());
1339 if (strMusicBrainzTrackID
.empty())
1340 strSQL
+= PrepareSQL(", strMusicBrainzTrackID = NULL");
1342 strSQL
+= PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID
.c_str());
1343 if (artistSort
.empty() || artistSort
.compare(artistDisp
) == 0)
1344 strSQL
+= PrepareSQL(", strArtistSort = NULL");
1346 strSQL
+= PrepareSQL(", strArtistSort = '%s'", artistSort
.c_str());
1348 strSQL
+= PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
1349 "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
1350 iStartOffset
, iEndOffset
, static_cast<double>(rating
), userrating
, votes
,
1351 strComment
.c_str(), strMood
.c_str(), replayGain
.Get().c_str());
1353 if (dtLastPlayed
.IsValid())
1354 strSQL
+= PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed
,
1355 dtLastPlayed
.GetAsDBDateTime().c_str());
1356 else if (iTimesPlayed
> 0)
1357 strSQL
+= PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed
,
1358 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
1360 strSQL
+= ", iTimesPlayed = 0, lastplayed = NULL ";
1361 strSQL
+= PrepareSQL("WHERE idSong = %i", idSong
);
1363 bool status
= ExecuteQuery(strSQL
);
1366 AnnounceUpdate(MediaTypeSong
, idSong
);
1370 int CMusicDatabase::AddAlbum(const std::string
& strAlbum
,
1371 const std::string
& strMusicBrainzAlbumID
,
1372 const std::string
& strReleaseGroupMBID
,
1373 const std::string
& strArtist
,
1374 const std::string
& strArtistSort
,
1375 const std::string
& strGenre
,
1376 const std::string
& strReleaseDate
,
1377 const std::string
& strOrigReleaseDate
,
1379 const std::string
& strRecordLabel
,
1380 const std::string
& strType
,
1381 const std::string
& strReleaseStatus
,
1383 CAlbum::ReleaseType releaseType
)
1388 if (nullptr == m_pDB
)
1390 if (nullptr == m_pDS
)
1393 if (!strMusicBrainzAlbumID
.empty())
1394 strSQL
= PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
1395 strMusicBrainzAlbumID
.c_str());
1397 strSQL
= PrepareSQL("SELECT * FROM album "
1398 "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
1399 "AND strMusicBrainzAlbumID IS NULL",
1400 strArtist
.c_str(), strAlbum
.c_str());
1401 m_pDS
->query(strSQL
);
1402 std::string strCheckFlag
= strType
;
1403 StringUtils::ToLower(strCheckFlag
);
1404 if (strCheckFlag
.find("boxset") != std::string::npos
) //boxset flagged in album type
1406 if (m_pDS
->num_rows() == 0)
1409 // Does not exist, add it
1411 PrepareSQL("INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
1412 "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
1413 "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, "
1414 "strMusicBrainzAlbumID, "
1415 "strReleaseGroupMBID, strArtistSort) "
1416 "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
1417 strAlbum
.c_str(), strArtist
.c_str(), strGenre
.c_str(), //
1418 strReleaseDate
.c_str(), strOrigReleaseDate
.c_str(), bBoxedSet
, //
1419 strRecordLabel
.c_str(), strType
.c_str(), strReleaseStatus
.c_str(), //
1420 bCompilation
, CAlbum::ReleaseTypeToString(releaseType
).c_str());
1422 if (strMusicBrainzAlbumID
.empty())
1423 strSQL
+= PrepareSQL(", NULL");
1425 strSQL
+= PrepareSQL(",'%s'", strMusicBrainzAlbumID
.c_str());
1426 if (strReleaseGroupMBID
.empty())
1427 strSQL
+= PrepareSQL(", NULL");
1429 strSQL
+= PrepareSQL(",'%s'", strReleaseGroupMBID
.c_str());
1430 if (strArtistSort
.empty() || strArtistSort
.compare(strArtist
) == 0)
1431 strSQL
+= PrepareSQL(", NULL");
1433 strSQL
+= PrepareSQL(", '%s'", strArtistSort
.c_str());
1435 m_pDS
->exec(strSQL
);
1437 return (int)m_pDS
->lastinsertid();
1441 /* Exists in our database and being re-scanned from tags, so we should update it as the details
1444 Note that for multi-folder albums this will mean the last folder scanned will have the information
1445 stored for it. Most values here should be the same across all songs anyway, but it does mean
1446 that if there's any inconsistencies then only the last folders information will be taken.
1448 We make sure we clear out the link tables (album artists, album sources) and we reset
1449 the last scraped time to make sure that online metadata is re-fetched. */
1450 int idAlbum
= m_pDS
->fv("idAlbum").get_asInt();
1453 strSQL
= "UPDATE album SET ";
1454 if (!strMusicBrainzAlbumID
.empty())
1455 strSQL
+= PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", //
1456 strAlbum
.c_str(), strArtist
.c_str());
1457 if (strReleaseGroupMBID
.empty())
1458 strSQL
+= PrepareSQL(" strReleaseGroupMBID = NULL,");
1460 strSQL
+= PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID
.c_str());
1461 if (strArtistSort
.empty() || strArtistSort
.compare(strArtist
) == 0)
1462 strSQL
+= PrepareSQL(" strArtistSort = NULL");
1464 strSQL
+= PrepareSQL(" strArtistSort = '%s'", strArtistSort
.c_str());
1467 PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1468 "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
1469 "bCompilation=%i, strReleaseType = '%s', "
1470 "lastScraped = NULL "
1472 strGenre
.c_str(), strReleaseDate
.c_str(), strOrigReleaseDate
.c_str(), //
1473 bBoxedSet
, strRecordLabel
.c_str(), strType
.c_str(), strReleaseStatus
.c_str(),
1474 bCompilation
, CAlbum::ReleaseTypeToString(releaseType
).c_str(), //
1476 m_pDS
->exec(strSQL
);
1477 DeleteAlbumArtistsByAlbum(idAlbum
);
1478 DeleteAlbumSources(idAlbum
);
1484 CLog::Log(LOGERROR
, "{} failed with query ({})", __FUNCTION__
, strSQL
);
1490 int CMusicDatabase::UpdateAlbum(int idAlbum
,
1491 const std::string
& strAlbum
,
1492 const std::string
& strMusicBrainzAlbumID
,
1493 const std::string
& strReleaseGroupMBID
,
1494 const std::string
& strArtist
,
1495 const std::string
& strArtistSort
,
1496 const std::string
& strGenre
,
1497 const std::string
& strMoods
,
1498 const std::string
& strStyles
,
1499 const std::string
& strThemes
,
1500 const std::string
& strReview
,
1501 const std::string
& strImage
,
1502 const std::string
& strLabel
,
1503 const std::string
& strType
,
1504 const std::string
& strReleaseStatus
,
1508 const std::string
& strReleaseDate
,
1509 const std::string
& strOrigReleaseDate
,
1512 CAlbum::ReleaseType releaseType
,
1518 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1519 // Truncate value cleaning up xml when URLs exceeds this
1520 std::string strImageURLs
= strImage
;
1521 if (StringUtils::EqualsNoCase(
1522 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
,
1524 TrimImageURLs(strImageURLs
, 65535);
1527 strSQL
= PrepareSQL("UPDATE album SET "
1528 " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
1529 " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
1530 " strReview = '%s', strImage = '%s', strLabel = '%s', "
1531 " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
1532 " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1533 " bBoxedSet = %i, bCompilation = %i,"
1534 " strReleaseType = '%s', strReleaseStatus = '%s', "
1535 " lastScraped = '%s', bScrapedMBID = %i",
1536 strAlbum
.c_str(), strArtist
.c_str(), strGenre
.c_str(), //
1537 strMoods
.c_str(), strStyles
.c_str(), strThemes
.c_str(), //
1538 strReview
.c_str(), strImageURLs
.c_str(), strLabel
.c_str(), //
1539 strType
.c_str(), static_cast<double>(fRating
), iUserrating
, iVotes
, //
1540 strReleaseDate
.c_str(), strOrigReleaseDate
.c_str(), //
1541 bBoxedSet
, bCompilation
, //
1542 CAlbum::ReleaseTypeToString(releaseType
).c_str(), strReleaseStatus
.c_str(), //
1543 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), bScrapedMBID
);
1544 if (strMusicBrainzAlbumID
.empty())
1545 strSQL
+= PrepareSQL(", strMusicBrainzAlbumID = NULL");
1547 strSQL
+= PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID
.c_str());
1548 if (strReleaseGroupMBID
.empty())
1549 strSQL
+= PrepareSQL(", strReleaseGroupMBID = NULL");
1551 strSQL
+= PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID
.c_str());
1552 if (strArtistSort
.empty() || strArtistSort
.compare(strArtist
) == 0)
1553 strSQL
+= PrepareSQL(", strArtistSort = NULL");
1555 strSQL
+= PrepareSQL(", strArtistSort = '%s'", strArtistSort
.c_str());
1557 strSQL
+= PrepareSQL(" WHERE idAlbum = %i", idAlbum
);
1559 bool status
= ExecuteQuery(strSQL
);
1561 AnnounceUpdate(MediaTypeAlbum
, idAlbum
);
1565 bool CMusicDatabase::GetAlbum(int idAlbum
, CAlbum
& album
, bool getSongs
/* = true */)
1569 if (nullptr == m_pDB
)
1571 if (nullptr == m_pDS
)
1575 return false; // not in the database
1577 //Get album, song and album song info data using separate queries/datasets because we can have
1578 //multiple roles per artist for songs and that makes a single combined join impractical
1581 sql
= PrepareSQL("SELECT albumview.*,albumartistview.* "
1583 " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
1584 " WHERE albumview.idAlbum = %ld "
1585 " ORDER BY albumartistview.iOrder",
1588 CLog::Log(LOGDEBUG
, "{}", sql
);
1589 if (!m_pDS
->query(sql
))
1591 if (m_pDS
->num_rows() == 0)
1597 int albumArtistOffset
= album_enumCount
;
1599 album
= GetAlbumFromDataset(m_pDS
->get_sql_record(), 0,
1600 true); // true to grab and parse the imageURL
1601 while (!m_pDS
->eof())
1603 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
1605 // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
1606 // so there is only one row in the result set for each artist credit.
1607 album
.artistCredits
.push_back(GetArtistCreditFromDataset(record
, albumArtistOffset
));
1611 m_pDS
->close(); // cleanup recordset data
1616 sql
= PrepareSQL("SELECT songview.*, songartistview.*"
1618 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1619 " WHERE songview.idAlbum = %ld "
1620 " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder",
1623 CLog::Log(LOGDEBUG
, "{}", sql
);
1624 if (!m_pDS
->query(sql
))
1626 if (m_pDS
->num_rows() == 0) //Album with no songs
1632 int songArtistOffset
= song_enumCount
;
1633 std::set
<int> songs
;
1634 while (!m_pDS
->eof())
1636 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
1638 int idSong
= record
->at(song_idSong
).get_asInt(); //Same as songartist.idSong by join
1639 if (songs
.find(idSong
) == songs
.end())
1641 album
.songs
.emplace_back(GetSongFromDataset(record
));
1642 songs
.insert(idSong
);
1645 int idSongArtistRole
= record
->at(songArtistOffset
+ artistCredit_idRole
).get_asInt();
1646 //By query order song is the last one appended to the album song vector.
1647 if (idSongArtistRole
== ROLE_ARTIST
)
1648 album
.songs
.back().artistCredits
.emplace_back(
1649 GetArtistCreditFromDataset(record
, songArtistOffset
));
1651 album
.songs
.back().AppendArtistRole(GetArtistRoleFromDataset(record
, songArtistOffset
));
1655 m_pDS
->close(); // cleanup recordset data
1662 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
1668 bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum
)
1670 std::string strSQL
=
1671 PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum
);
1672 return ExecuteQuery(strSQL
);
1675 bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum
)
1677 std::string strSQL
=
1678 PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum
);
1679 return GetSingleValue(strSQL
).empty();
1682 int CMusicDatabase::AddGenre(std::string
& strGenre
)
1687 StringUtils::Trim(strGenre
);
1689 if (strGenre
.empty())
1690 strGenre
= g_localizeStrings
.Get(13205); // Unknown
1692 if (nullptr == m_pDB
)
1694 if (nullptr == m_pDS
)
1697 auto it
= m_genreCache
.find(strGenre
);
1698 if (it
!= m_genreCache
.end())
1702 strSQL
= PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'",
1704 m_pDS
->query(strSQL
);
1705 if (m_pDS
->num_rows() == 0)
1708 // doesn't exists, add it
1709 strSQL
= PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )",
1711 m_pDS
->exec(strSQL
);
1713 int idGenre
= (int)m_pDS
->lastinsertid();
1714 m_genreCache
.insert(std::pair
<std::string
, int>(strGenre
, idGenre
));
1719 int idGenre
= m_pDS
->fv("idGenre").get_asInt();
1720 strGenre
= m_pDS
->fv("strGenre").get_asString();
1721 m_genreCache
.insert(std::pair
<std::string
, int>(strGenre
, idGenre
));
1728 CLog::Log(LOGERROR
, "musicdatabase:unable to addgenre ({})", strSQL
);
1734 bool CMusicDatabase::UpdateArtist(const CArtist
& artist
)
1736 SetLibraryLastUpdated();
1738 const std::string itemSeparator
=
1739 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
1741 UpdateArtist(artist
.idArtist
, //
1742 artist
.strArtist
, //
1743 artist
.strSortName
, //
1744 artist
.strMusicBrainzArtistID
, //
1745 artist
.bScrapedMBID
, //
1747 artist
.strGender
, //
1748 artist
.strDisambiguation
, //
1750 artist
.strFormed
, //
1751 StringUtils::Join(artist
.genre
, itemSeparator
), //
1752 StringUtils::Join(artist
.moods
, itemSeparator
), //
1753 StringUtils::Join(artist
.styles
, itemSeparator
), //
1754 StringUtils::Join(artist
.instruments
, itemSeparator
), //
1755 artist
.strBiography
, //
1757 artist
.strDisbanded
, //
1758 StringUtils::Join(artist
.yearsActive
, itemSeparator
).c_str(), //
1759 artist
.thumbURL
.GetData());
1761 DeleteArtistDiscography(artist
.idArtist
);
1762 for (const auto& disc
: artist
.discography
)
1764 AddArtistDiscography(artist
.idArtist
, disc
);
1767 if (!DeleteArtistVideoLinks(artist
.idArtist
))
1768 CLog::Log(LOGERROR
, "MusicDatabase: Error deleting ArtistVideoLinks");
1770 AddArtistVideoLinks(artist
);
1772 // Set current artwork (held in art table)
1773 if (!artist
.art
.empty())
1774 SetArtForItem(artist
.idArtist
, MediaTypeArtist
, artist
.art
);
1779 int CMusicDatabase::AddArtist(const std::string
& strArtist
,
1780 const std::string
& strMusicBrainzArtistID
,
1781 const std::string
& strSortName
,
1782 bool bScrapedMBID
/* = false*/)
1785 int idArtist
= AddArtist(strArtist
, strMusicBrainzArtistID
, bScrapedMBID
);
1786 if (idArtist
< 0 || strSortName
.empty())
1789 /* Artist sort name always taken as the first value provided that is different from name, so only
1790 update when current sort name is blank. If a new sortname the same as name is provided then
1791 clear any sortname currently held.
1796 if (nullptr == m_pDB
)
1798 if (nullptr == m_pDS
)
1801 strSQL
= PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist
);
1802 m_pDS
->query(strSQL
);
1803 if (m_pDS
->num_rows() != 1)
1808 std::string strArtistName
, strArtistSort
;
1809 strArtistName
= m_pDS
->fv("strArtist").get_asString();
1810 strArtistSort
= m_pDS
->fv("strSortName").get_asString();
1813 if (!strArtistSort
.empty())
1815 if (strSortName
.compare(strArtistName
) == 0)
1817 PrepareSQL("UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist
));
1819 else if (strSortName
.compare(strArtistName
) != 0)
1820 m_pDS
->exec(PrepareSQL("UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
1821 strSortName
.c_str(), idArtist
));
1828 CLog::Log(LOGERROR
, "musicdatabase:unable to addartist with sortname ({})", strSQL
);
1834 int CMusicDatabase::AddArtist(const std::string
& strArtist
,
1835 const std::string
& strMusicBrainzArtistID
,
1836 bool bScrapedMBID
/* = false*/)
1841 if (nullptr == m_pDB
)
1843 if (nullptr == m_pDS
)
1847 if (!strMusicBrainzArtistID
.empty())
1849 // 1.a) Match on a MusicBrainz ID
1851 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
1852 strMusicBrainzArtistID
.c_str());
1853 m_pDS
->query(strSQL
);
1854 if (m_pDS
->num_rows() > 0)
1856 int idArtist
= m_pDS
->fv("idArtist").get_asInt();
1857 bool update
= m_pDS
->fv("strArtist").get_asString().compare(strMusicBrainzArtistID
) == 0;
1861 strSQL
= PrepareSQL("UPDATE artist SET strArtist = '%s' "
1862 "WHERE idArtist = %i",
1863 strArtist
.c_str(), idArtist
);
1864 m_pDS
->exec(strSQL
);
1872 // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
1873 // and update that if it exists.
1874 strSQL
= PrepareSQL("SELECT idArtist FROM artist "
1875 "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
1877 m_pDS
->query(strSQL
);
1878 if (m_pDS
->num_rows() > 0)
1880 int idArtist
= m_pDS
->fv("idArtist").get_asInt();
1882 // 1.b.a) We found an artist by name but with no MusicBrainz ID set, update it and assume it is our artist, flag when mbid scraped
1884 PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
1885 "bScrapedMBID = %i WHERE idArtist = %i",
1886 strArtist
.c_str(), strMusicBrainzArtistID
.c_str(), bScrapedMBID
, idArtist
);
1887 m_pDS
->exec(strSQL
);
1891 // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
1892 // With MusicBrainz IDs this could return multiple artists and is non-determinstic
1893 // Always pick the first artist ID returned by the DB to return.
1898 PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'", strArtist
.c_str());
1900 m_pDS
->query(strSQL
);
1901 if (m_pDS
->num_rows() > 0)
1903 int idArtist
= m_pDS
->fv("idArtist").get_asInt();
1910 // 3) No artist exists at all - add it, flagging when has scraped mbid
1911 if (strMusicBrainzArtistID
.empty())
1912 strSQL
= PrepareSQL("INSERT INTO artist "
1913 "(idArtist, strArtist, strMusicBrainzArtistID) "
1914 "VALUES( NULL, '%s', NULL)",
1917 strSQL
= PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
1919 "VALUES( NULL, '%s', '%s', %i )",
1920 strArtist
.c_str(), strMusicBrainzArtistID
.c_str(), bScrapedMBID
);
1922 m_pDS
->exec(strSQL
);
1923 int idArtist
= (int)m_pDS
->lastinsertid();
1928 CLog::Log(LOGERROR
, "musicdatabase:unable to addartist ({})", strSQL
);
1934 int CMusicDatabase::UpdateArtist(int idArtist
,
1935 const std::string
& strArtist
,
1936 const std::string
& strSortName
,
1937 const std::string
& strMusicBrainzArtistID
,
1938 const bool bScrapedMBID
,
1939 const std::string
& strType
,
1940 const std::string
& strGender
,
1941 const std::string
& strDisambiguation
,
1942 const std::string
& strBorn
,
1943 const std::string
& strFormed
,
1944 const std::string
& strGenres
,
1945 const std::string
& strMoods
,
1946 const std::string
& strStyles
,
1947 const std::string
& strInstruments
,
1948 const std::string
& strBiography
,
1949 const std::string
& strDied
,
1950 const std::string
& strDisbanded
,
1951 const std::string
& strYearsActive
,
1952 const std::string
& strImage
)
1957 // Check another artist with this mbid not already exist (an alias for example)
1958 bool useMBIDNull
= strMusicBrainzArtistID
.empty();
1959 bool isScrapedMBID
= bScrapedMBID
;
1960 std::string artistname
;
1961 int idArtistMbid
= GetArtistFromMBID(strMusicBrainzArtistID
, artistname
);
1962 if (idArtistMbid
> 0 && idArtistMbid
!= idArtist
)
1964 CLog::Log(LOGDEBUG
, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
1965 __FUNCTION__
, strMusicBrainzArtistID
, artistname
, idArtistMbid
, strArtist
, idArtist
);
1967 isScrapedMBID
= false;
1970 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1971 // Truncate value cleaning up xml when URLs exceeds this
1972 std::string strImageURLs
= strImage
;
1973 if (StringUtils::EqualsNoCase(
1974 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
,
1976 TrimImageURLs(strImageURLs
, 65535);
1979 strSQL
= PrepareSQL("UPDATE artist SET "
1980 " strArtist = '%s', "
1981 " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
1982 " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
1983 " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
1984 " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
1985 " strYearsActive = '%s', strImage = '%s', "
1986 " lastScraped = '%s', bScrapedMBID = %i",
1988 /* strSortName.c_str(),*/
1989 /* strMusicBrainzArtistID.c_str(), */
1990 strType
.c_str(), strGender
.c_str(), strDisambiguation
.c_str(), //
1991 strBorn
.c_str(), strFormed
.c_str(), strGenres
.c_str(), //
1992 strMoods
.c_str(), strStyles
.c_str(), strInstruments
.c_str(), //
1993 strBiography
.c_str(), strDied
.c_str(), strDisbanded
.c_str(), //
1994 strYearsActive
.c_str(), strImageURLs
.c_str(), //
1995 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID
);
1997 strSQL
+= PrepareSQL(", strMusicBrainzArtistID = NULL");
1999 strSQL
+= PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID
.c_str());
2000 if (strSortName
.empty())
2001 strSQL
+= PrepareSQL(", strSortName = NULL");
2003 strSQL
+= PrepareSQL(", strSortName = '%s'", strSortName
.c_str());
2005 strSQL
+= PrepareSQL(" WHERE idArtist = %i", idArtist
);
2007 bool status
= ExecuteQuery(strSQL
);
2009 AnnounceUpdate(MediaTypeArtist
, idArtist
);
2013 bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist
,
2014 const std::string
& strMusicBrainzArtistID
)
2016 if (strMusicBrainzArtistID
.empty() || idArtist
< 0)
2019 // Check another artist with this mbid not already exist (an alias for example)
2020 std::string artistname
;
2021 int idArtistMbid
= GetArtistFromMBID(strMusicBrainzArtistID
, artistname
);
2022 if (idArtistMbid
> 0 && idArtistMbid
!= idArtist
)
2024 CLog::Log(LOGDEBUG
, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__
,
2025 strMusicBrainzArtistID
, artistname
, idArtistMbid
);
2029 // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
2031 strSQL
= PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
2032 "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
2033 strMusicBrainzArtistID
.c_str(), idArtist
);
2035 bool status
= ExecuteQuery(strSQL
);
2038 AnnounceUpdate(MediaTypeArtist
, idArtist
);
2044 bool CMusicDatabase::GetArtist(int idArtist
, CArtist
& artist
, bool fetchAll
/* = false */)
2048 auto start
= std::chrono::steady_clock::now();
2049 if (nullptr == m_pDB
)
2051 if (nullptr == m_pDS
)
2053 if (nullptr == m_pDS2
)
2057 return false; // not in the database
2061 strSQL
= PrepareSQL("SELECT * FROM artistview "
2062 "LEFT JOIN discography ON artistview.idArtist = discography.idArtist "
2063 "WHERE artistview.idArtist = %i",
2066 strSQL
= PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist
);
2068 if (!m_pDS
->query(strSQL
))
2070 if (m_pDS
->num_rows() == 0)
2075 std::string debugSQL
= strSQL
+ " - ";
2076 int discographyOffset
= artist_enumCount
;
2078 artist
.discography
.clear();
2079 artist
= GetArtistFromDataset(m_pDS
->get_sql_record(), 0, true); // inc scraped art URLs
2082 while (!m_pDS
->eof())
2084 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
2085 CDiscoAlbum discoAlbum
;
2086 discoAlbum
.strAlbum
= record
->at(discographyOffset
+ 1).get_asString();
2087 discoAlbum
.strYear
= record
->at(discographyOffset
+ 2).get_asString();
2088 discoAlbum
.strReleaseGroupMBID
= record
->at(discographyOffset
+ 3).get_asString();
2089 artist
.discography
.emplace_back(discoAlbum
);
2093 m_pDS
->close(); // cleanup recordset data
2095 artist
.videolinks
.clear();
2098 strSQL
= PrepareSQL("SELECT idSong, strTitle, strMusicBrainzTrackID, strVideoURL, url "
2099 "FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
2100 "LEFT JOIN art ON art.media_id = song.idSong AND art.type = 'videothumb' "
2101 "WHERE album_artist.idArtist = %i AND "
2102 "song.strVideoURL is not NULL GROUP by song.strVideoURL ORDER BY idSong",
2105 m_pDS
->query(strSQL
);
2106 while (!m_pDS
->eof())
2108 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
2109 ArtistVideoLinks videoLink
;
2110 videoLink
.title
= record
->at(1).get_asString();
2111 videoLink
.mbTrackID
= record
->at(2).get_asString();
2112 videoLink
.videoURL
= record
->at(3).get_asString();
2113 videoLink
.thumbURL
= record
->at(4).get_asString();
2115 artist
.videolinks
.emplace_back(std::move(videoLink
));
2121 auto end
= std::chrono::steady_clock::now();
2122 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
2124 CLog::LogF(LOGDEBUG
, "{} - took {} ms", debugSQL
, duration
.count());
2129 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2135 bool CMusicDatabase::GetArtistExists(int idArtist
)
2139 if (nullptr == m_pDB
)
2141 if (nullptr == m_pDS
)
2144 std::string strSQL
=
2145 PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist
);
2147 if (!m_pDS
->query(strSQL
))
2149 if (m_pDS
->num_rows() == 0)
2154 m_pDS
->close(); // cleanup recordset data
2159 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2165 int CMusicDatabase::GetLastArtist()
2167 std::string strSQL
= "SELECT MAX(idArtist) FROM artist";
2168 std::string lastArtist
= GetSingleValue(strSQL
);
2169 if (lastArtist
.empty())
2172 return static_cast<int>(strtol(lastArtist
.c_str(), NULL
, 10));
2175 int CMusicDatabase::GetArtistFromMBID(const std::string
& strMusicBrainzArtistID
,
2176 std::string
& artistname
)
2178 if (strMusicBrainzArtistID
.empty())
2184 if (nullptr == m_pDB
|| nullptr == m_pDS2
)
2186 // Match on MusicBrainz ID, definitively unique
2188 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
2189 strMusicBrainzArtistID
.c_str());
2190 if (!m_pDS2
->query(strSQL
))
2193 if (m_pDS2
->num_rows() > 0)
2195 idArtist
= m_pDS2
->fv("idArtist").get_asInt();
2196 artistname
= m_pDS2
->fv("strArtist").get_asString();
2203 CLog::Log(LOGERROR
, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__
, strSQL
);
2208 bool CMusicDatabase::HasArtistBeenScraped(int idArtist
)
2210 std::string strSQL
= PrepareSQL(
2211 "SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist
);
2212 return GetSingleValue(strSQL
).empty();
2215 bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist
)
2217 std::string strSQL
=
2218 PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist
);
2219 return ExecuteQuery(strSQL
);
2222 bool CMusicDatabase::AddArtistVideoLinks(const CArtist
& artist
)
2224 auto start
= std::chrono::steady_clock::now();
2229 if (nullptr == m_pDB
|| nullptr == m_pDS
|| nullptr == m_pDS2
)
2232 for (const auto& videoURL
: artist
.videolinks
)
2234 dbSong
= videoURL
.title
;
2235 std::string strSQL
= PrepareSQL(
2236 "SELECT idSong, strTitle FROM song WHERE strMusicBrainzTrackID = '%s' OR (EXISTS "
2237 "(SELECT 1 FROM album_artist WHERE album_artist.idAlbum = song.idAlbum AND "
2238 "album_artist.idArtist = '%i' AND song.strTitle LIKE '%%%s%%'))",
2239 videoURL
.mbTrackID
.c_str(), artist
.idArtist
, videoURL
.title
.c_str());
2241 if (!m_pDS
->query(strSQL
))
2243 if (m_pDS
->num_rows() == 0)
2246 while (!m_pDS
->eof())
2248 const int songId
= m_pDS
->fv(0).get_asInt();
2249 std::string strSQL2
= PrepareSQL("UPDATE song SET strVideoURL='%s' WHERE idSong = %i",
2250 videoURL
.videoURL
.c_str(), songId
);
2251 CLog::Log(LOGDEBUG
, "Adding videolink for song {} with id {}", dbSong
, songId
);
2252 m_pDS2
->exec(strSQL2
);
2254 if (!videoURL
.thumbURL
.empty())
2255 { // already have a videothumb for this song ?
2256 strSQL2
= PrepareSQL("SELECT art_id FROM art "
2257 "WHERE media_id=%i AND media_type='%s' AND type='videothumb'",
2258 songId
, MediaTypeSong
);
2259 m_pDS2
->query(strSQL2
);
2261 { // update existing thumb
2262 const int artId
= m_pDS2
->fv(0).get_asInt();
2264 strSQL2
= PrepareSQL("UPDATE art SET url='%s' where art_id=%d",
2265 videoURL
.thumbURL
.c_str(), artId
);
2266 m_pDS2
->exec(strSQL2
);
2269 { // insert new thumb
2271 strSQL2
= PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
2272 "VALUES (%d, '%s', '%s', '%s')",
2273 songId
, MediaTypeSong
, "videothumb", videoURL
.thumbURL
.c_str());
2274 m_pDS2
->exec(strSQL2
);
2282 auto end
= std::chrono::steady_clock::now();
2283 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
2284 CLog::LogF(LOGDEBUG
, "Time to store videolinks {}ms ", duration
.count());
2289 CLog::Log(LOGERROR
, "MusicDatabase: Unable to add videolink for song ({})", dbSong
);
2294 bool CMusicDatabase::DeleteArtistVideoLinks(const int idArtist
)
2296 std::string strSQL
= PrepareSQL("UPDATE song SET strVideoURL = NULL WHERE idAlbum IN "
2297 "(SELECT idAlbum FROM album_artist WHERE idArtist = %i)",
2299 if (!ExecuteQuery(strSQL
))
2301 strSQL
= PrepareSQL(
2302 "DELETE FROM art WHERE art.type = 'videothumb' AND art.media_id IN (SELECT idSong FROM song "
2303 "JOIN album_artist ON song.idAlbum = album_artist.idAlbum WHERE album_artist.idArtist = %i)",
2305 return ExecuteQuery(strSQL
);
2308 int CMusicDatabase::AddArtistDiscography(int idArtist
, const CDiscoAlbum
& discoAlbum
)
2310 std::string strSQL
= PrepareSQL("INSERT INTO discography "
2311 "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
2312 "VALUES(%i, '%s', '%s', '%s')",
2313 idArtist
, discoAlbum
.strAlbum
.c_str(), discoAlbum
.strYear
.c_str(),
2314 discoAlbum
.strReleaseGroupMBID
.c_str());
2315 return ExecuteQuery(strSQL
);
2318 bool CMusicDatabase::DeleteArtistDiscography(int idArtist
)
2320 std::string strSQL
= PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist
);
2321 return ExecuteQuery(strSQL
);
2324 bool CMusicDatabase::GetArtistDiscography(int idArtist
, CFileItemList
& items
)
2328 if (nullptr == m_pDB
)
2330 if (nullptr == m_pDS
)
2333 /* Combine entries from discography and album tables
2334 Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using
2335 correlated subqueries to a temp table. An updatable join to temp table would work in MySQL
2336 but SQLite not support updatable joins.
2338 m_pDS
->exec("CREATE TABLE tempDisco "
2339 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2340 m_pDS
->exec("CREATE TABLE tempAlbum "
2341 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2344 strSQL
= PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
2345 "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
2346 "strReleaseGroupMBID, NULL "
2347 "FROM discography WHERE idArtist = %i",
2349 m_pDS
->exec(strSQL
);
2351 strSQL
= PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
2352 "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
2353 "strReleaseGroupMBID, album.idAlbum "
2354 "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
2355 "WHERE idArtist = %i",
2357 m_pDS
->exec(strSQL
);
2359 // Match albums on release group mbid, if multi-releases then first used
2360 // Only use albums credited to this artist
2361 strSQL
= "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
2362 "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
2363 m_pDS
->exec(strSQL
);
2364 //Delete matched albums
2365 strSQL
= "DELETE FROM tempAlbum "
2366 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2367 m_pDS
->exec(strSQL
);
2369 // Match remaining to albums by artist on title and year
2370 strSQL
= "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2371 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
2372 "AND tempAlbum.strYear = tempDisco.strYear) "
2373 "WHERE tempDisco.idAlbum is NULL";
2374 m_pDS
->exec(strSQL
);
2375 //Delete matched albums
2376 strSQL
= "DELETE FROM tempAlbum "
2377 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2378 m_pDS
->exec(strSQL
);
2380 // Match remaining to albums by artist on title only
2381 strSQL
= "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2382 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
2383 "WHERE tempDisco.idAlbum is NULL";
2384 m_pDS
->exec(strSQL
);
2385 // Use year from album table, when matched by title only as it could be different
2386 strSQL
= "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
2387 "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
2388 "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
2389 m_pDS
->exec(strSQL
);
2390 //Delete matched albums
2391 strSQL
= "DELETE FROM tempAlbum "
2392 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2393 m_pDS
->exec(strSQL
);
2395 // Combine distinctly with any remaining unmatched albums by artist
2396 strSQL
= "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
2398 "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
2399 "ORDER BY strYear, strAlbum, idAlbum";
2401 if (!m_pDS
->query(strSQL
))
2403 int iRowsFound
= m_pDS
->num_rows();
2404 if (iRowsFound
== 0)
2410 while (!m_pDS
->eof())
2412 int idAlbum
= m_pDS
->fv("idAlbum").get_asInt();
2415 std::string strAlbum
= m_pDS
->fv("strAlbum").get_asString();
2416 if (!strAlbum
.empty())
2418 CFileItemPtr
pItem(new CFileItem(strAlbum
));
2419 pItem
->SetLabel2(m_pDS
->fv("strYear").get_asString());
2420 pItem
->GetMusicInfoTag()->SetDatabaseId(idAlbum
, MediaTypeAlbum
);
2428 m_pDS
->exec("DROP TABLE tempDisco");
2429 m_pDS
->exec("DROP TABLE tempAlbum");
2435 m_pDS
->exec("DROP TABLE tempDisco");
2436 m_pDS
->exec("DROP TABLE tempAlbum");
2437 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
2442 int CMusicDatabase::AddRole(const std::string
& strRole
)
2449 if (nullptr == m_pDB
)
2451 if (nullptr == m_pDS
)
2453 strSQL
= PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole
.c_str());
2454 m_pDS
->query(strSQL
);
2455 if (m_pDS
->num_rows() > 0)
2456 idRole
= m_pDS
->fv("idRole").get_asInt();
2461 strSQL
= PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole
.c_str());
2462 m_pDS
->exec(strSQL
);
2463 idRole
= static_cast<int>(m_pDS
->lastinsertid());
2469 CLog::Log(LOGERROR
, "musicdatabase:unable to AddRole ({})", strSQL
);
2474 bool CMusicDatabase::AddSongArtist(
2475 int idArtist
, int idSong
, const std::string
& strRole
, const std::string
& strArtist
, int iOrder
)
2477 int idRole
= AddRole(strRole
);
2478 return AddSongArtist(idArtist
, idSong
, idRole
, strArtist
, iOrder
);
2481 bool CMusicDatabase::AddSongArtist(
2482 int idArtist
, int idSong
, int idRole
, const std::string
& strArtist
, int iOrder
)
2485 strSQL
= PrepareSQL("REPLACE INTO song_artist (idArtist, idSong, idRole, strArtist, iOrder) "
2486 "VALUES(%i, %i, %i,'%s', %i)",
2487 idArtist
, idSong
, idRole
, strArtist
.c_str(), iOrder
);
2488 return ExecuteQuery(strSQL
);
2491 int CMusicDatabase::AddSongContributor(int idSong
,
2492 const std::string
& strRole
,
2493 const std::string
& strArtist
,
2494 const std::string
& strSort
)
2496 if (strArtist
.empty())
2502 if (nullptr == m_pDB
)
2504 if (nullptr == m_pDS
)
2508 // Add artist. As we only have name (no MBID) first try to identify artist from song
2509 // as they may have already been added with a different role (including MBID).
2511 PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ",
2512 idSong
, strArtist
.c_str());
2513 m_pDS
->query(strSQL
);
2514 if (m_pDS
->num_rows() > 0)
2515 idArtist
= m_pDS
->fv("idArtist").get_asInt();
2519 idArtist
= AddArtist(strArtist
, "", strSort
);
2521 // Add to song_artist table
2522 AddSongArtist(idArtist
, idSong
, strRole
, strArtist
, 0);
2528 CLog::Log(LOGERROR
, "musicdatabase:unable to AddSongContributor ({})", strSQL
);
2534 void CMusicDatabase::AddSongContributors(int idSong
,
2535 const VECMUSICROLES
& contributors
,
2536 const std::string
& strSort
)
2538 std::vector
<std::string
> composerSort
;
2539 size_t countComposer
= 0;
2540 if (!strSort
.empty())
2542 composerSort
= StringUtils::Split(
2544 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
);
2547 for (const auto& credit
: contributors
)
2549 std::string strSortName
;
2550 //Identify composer sort name if we have it
2551 if (countComposer
< composerSort
.size())
2553 if (credit
.GetRoleDesc().compare("Composer") == 0)
2555 strSortName
= composerSort
[countComposer
];
2559 AddSongContributor(idSong
, credit
.GetRoleDesc(), credit
.GetArtist(), strSortName
);
2563 int CMusicDatabase::GetRoleByName(const std::string
& strRole
)
2567 if (nullptr == m_pDB
)
2569 if (nullptr == m_pDS
)
2573 strSQL
= PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole
.c_str());
2575 if (!m_pDS
->query(strSQL
))
2577 int iRowsFound
= m_pDS
->num_rows();
2578 if (iRowsFound
!= 1)
2583 return m_pDS
->fv("idRole").get_asInt();
2587 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
2592 bool CMusicDatabase::GetRolesByArtist(int idArtist
, CFileItem
* item
)
2596 std::string strSQL
=
2597 PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole "
2598 "FROM song_artist JOIN role ON song_artist.idRole = role.idRole "
2599 "WHERE idArtist = %i ORDER BY song_artist.idRole ASC",
2601 if (!m_pDS
->query(strSQL
))
2603 if (m_pDS
->num_rows() == 0)
2609 CVariant
artistRoles(CVariant::VariantTypeArray
);
2611 while (!m_pDS
->eof())
2614 roleObj
["role"] = m_pDS
->fv("strRole").get_asString();
2615 roleObj
["roleid"] = m_pDS
->fv("idrole").get_asInt();
2616 artistRoles
.push_back(roleObj
);
2621 item
->SetProperty("roles", artistRoles
);
2626 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2631 bool CMusicDatabase::DeleteSongArtistsBySong(int idSong
)
2633 return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong
));
2636 bool CMusicDatabase::AddAlbumArtist(int idArtist
,
2638 const std::string
& strArtist
,
2642 strSQL
= PrepareSQL("REPLACE INTO album_artist (idArtist, idAlbum, strArtist, iOrder) "
2643 "VALUES(%i,%i,'%s',%i)",
2644 idArtist
, idAlbum
, strArtist
.c_str(), iOrder
);
2645 return ExecuteQuery(strSQL
);
2648 bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum
)
2650 return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum
));
2653 bool CMusicDatabase::AddSongGenres(int idSong
, const std::vector
<std::string
>& genres
)
2661 // Clear current entries for song
2662 strSQL
= PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong
);
2663 if (!ExecuteQuery(strSQL
))
2665 unsigned int index
= 0;
2666 std::vector
<std::string
> modgenres
= genres
;
2667 for (auto& strGenre
: modgenres
)
2669 int idGenre
= AddGenre(strGenre
); // Genre string trimmed and matched case-insensitively
2670 strSQL
= PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
2671 idGenre
, idSong
, index
++);
2672 if (!ExecuteQuery(strSQL
))
2675 // Update concatenated genre string from the standardised genre values
2676 std::string strGenres
= StringUtils::Join(
2678 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
);
2679 strSQL
= PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", //
2680 strGenres
.c_str(), idSong
);
2681 if (!ExecuteQuery(strSQL
))
2688 CLog::Log(LOGERROR
, "{}({}) {} failed", __FUNCTION__
, idSong
, strSQL
);
2693 bool CMusicDatabase::GetAlbumsByArtist(int idArtist
, std::vector
<int>& albums
)
2698 strSQL
= PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist
);
2699 if (!m_pDS
->query(strSQL
))
2701 if (m_pDS
->num_rows() == 0)
2707 while (!m_pDS
->eof())
2709 albums
.push_back(m_pDS
->fv("idAlbum").get_asInt());
2717 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2722 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum
, CFileItem
* item
)
2728 strSQL
= PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum
);
2730 if (!m_pDS
->query(strSQL
))
2732 if (m_pDS
->num_rows() == 0)
2738 // Get album artist credits
2739 VECARTISTCREDITS artistCredits
;
2740 while (!m_pDS
->eof())
2742 artistCredits
.emplace_back(GetArtistCreditFromDataset(m_pDS
->get_sql_record(), 0));
2747 // Populate item with song albumartist credits
2748 std::vector
<std::string
> musicBrainzID
;
2749 std::vector
<std::string
> albumartists
;
2750 CVariant
artistidObj(CVariant::VariantTypeArray
);
2751 for (const auto& artistCredit
: artistCredits
)
2753 artistidObj
.push_back(artistCredit
.GetArtistId());
2754 albumartists
.emplace_back(artistCredit
.GetArtist());
2755 if (!artistCredit
.GetMusicBrainzArtistID().empty())
2756 musicBrainzID
.emplace_back(artistCredit
.GetMusicBrainzArtistID());
2758 item
->GetMusicInfoTag()->SetAlbumArtist(albumartists
);
2759 item
->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID
);
2760 // Add song albumartistIds as separate property as not part of CMusicInfoTag
2761 item
->SetProperty("albumartistid", artistidObj
);
2767 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
2772 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum
, std::vector
<std::string
>& artistIDs
)
2777 // Get distinct song and album artist IDs for this album, no other roles
2778 // Allow for artists that are only album artists and not song artists
2779 strSQL
= PrepareSQL(
2780 "SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
2782 "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
2783 "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
2786 if (!m_pDS
->query(strSQL
))
2788 if (m_pDS
->num_rows() == 0)
2793 while (!m_pDS
->eof())
2795 // Get ID as string so can easily join to make "IN" clause
2796 artistIDs
.push_back(m_pDS
->fv("idArtist").get_asString());
2804 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
2809 bool CMusicDatabase::GetSongsByArtist(int idArtist
, std::vector
<int>& songs
)
2814 //Restrict to Artists only, no other roles
2815 strSQL
= PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", //
2817 if (!m_pDS
->query(strSQL
))
2819 if (m_pDS
->num_rows() == 0)
2825 while (!m_pDS
->eof())
2827 songs
.push_back(m_pDS
->fv("idSong").get_asInt());
2835 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2840 bool CMusicDatabase::GetArtistsBySong(int idSong
, std::vector
<int>& artists
)
2845 //Restrict to Artists only, no other roles
2846 strSQL
= PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", //
2848 if (!m_pDS
->query(strSQL
))
2850 if (m_pDS
->num_rows() == 0)
2856 while (!m_pDS
->eof())
2858 artists
.push_back(m_pDS
->fv("idArtist").get_asInt());
2866 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
2871 bool CMusicDatabase::GetGenresByArtist(int idArtist
, CFileItem
* item
)
2876 strSQL
= PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
2877 "FROM album_artist "
2878 "JOIN song ON album_artist.idAlbum = song.idAlbum "
2879 "JOIN song_genre ON song.idSong = song_genre.idSong "
2880 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2881 "WHERE album_artist.idArtist = %i "
2882 "ORDER BY song_genre.idGenre",
2884 if (!m_pDS
->query(strSQL
))
2886 if (m_pDS
->num_rows() == 0)
2888 // Artist does have any song genres via albums may not be an album artist.
2889 // Check via songs artist to fetch song genres from compilations or where they are guest artist
2891 strSQL
= PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
2893 "JOIN song_genre ON song_artist.idSong = song_genre.idSong "
2894 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2895 "WHERE song_artist.idArtist = %i "
2896 "ORDER BY song_genre.idGenre",
2898 if (!m_pDS
->query(strSQL
))
2900 if (m_pDS
->num_rows() == 0)
2902 //No song genres, but query successful
2908 CVariant
artistSongGenres(CVariant::VariantTypeArray
);
2910 while (!m_pDS
->eof())
2913 genreObj
["title"] = m_pDS
->fv("strGenre").get_asString();
2914 genreObj
["genreid"] = m_pDS
->fv("idGenre").get_asInt();
2915 artistSongGenres
.push_back(genreObj
);
2920 item
->SetProperty("songgenres", artistSongGenres
);
2925 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
2930 bool CMusicDatabase::GetGenresByAlbum(int idAlbum
, CFileItem
* item
)
2935 strSQL
= PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2936 "song JOIN song_genre ON song.idSong = song_genre.idSong "
2937 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2938 "WHERE song.idAlbum = %i "
2939 "ORDER BY song_genre.idSong, song_genre.iOrder",
2941 if (!m_pDS
->query(strSQL
))
2943 if (m_pDS
->num_rows() == 0)
2945 //No song genres, but query successful
2950 CVariant
albumSongGenres(CVariant::VariantTypeArray
);
2952 while (!m_pDS
->eof())
2955 genreObj
["title"] = m_pDS
->fv("strGenre").get_asString();
2956 genreObj
["genreid"] = m_pDS
->fv("idGenre").get_asInt();
2957 albumSongGenres
.push_back(genreObj
);
2962 item
->SetProperty("songgenres", albumSongGenres
);
2967 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
2972 bool CMusicDatabase::GetGenresBySong(int idSong
, std::vector
<int>& genres
)
2976 std::string strSQL
= PrepareSQL("SELECT idGenre FROM song_genre "
2977 "WHERE idSong = %i ORDER BY iOrder ASC",
2979 if (!m_pDS
->query(strSQL
))
2981 if (m_pDS
->num_rows() == 0)
2987 while (!m_pDS
->eof())
2989 genres
.push_back(m_pDS
->fv("idGenre").get_asInt());
2998 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
3003 bool CMusicDatabase::GetIsAlbumArtist(int idArtist
, CFileItem
* item
)
3008 GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist
));
3009 CVariant
IsAlbumArtistObj(CVariant::VariantTypeBoolean
);
3010 IsAlbumArtistObj
= (countalbum
> 0);
3011 item
->SetProperty("isalbumartist", IsAlbumArtistObj
);
3016 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
3022 int CMusicDatabase::AddPath(const std::string
& strPath1
)
3027 std::string
strPath(strPath1
);
3028 if (!URIUtils::HasSlashAtEnd(strPath
))
3029 URIUtils::AddSlashAtEnd(strPath
);
3031 if (nullptr == m_pDB
)
3033 if (nullptr == m_pDS
)
3036 auto it
= m_pathCache
.find(strPath
);
3037 if (it
!= m_pathCache
.end())
3040 strSQL
= PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath
.c_str());
3041 m_pDS
->query(strSQL
);
3042 if (m_pDS
->num_rows() == 0)
3045 // doesn't exists, add it
3046 strSQL
= PrepareSQL("INSERT INTO path (idPath, strPath) "
3047 "VALUES(NULL, '%s')",
3049 m_pDS
->exec(strSQL
);
3051 int idPath
= (int)m_pDS
->lastinsertid();
3052 m_pathCache
.insert(std::pair
<std::string
, int>(strPath
, idPath
));
3057 int idPath
= m_pDS
->fv("idPath").get_asInt();
3058 m_pathCache
.insert(std::pair
<std::string
, int>(strPath
, idPath
));
3065 CLog::Log(LOGERROR
, "musicdatabase:unable to addpath ({})", strSQL
);
3071 CSong
CMusicDatabase::GetSongFromDataset()
3073 return GetSongFromDataset(m_pDS
->get_sql_record());
3076 CSong
CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record
* const record
,
3077 int offset
/* = 0 */)
3080 song
.idSong
= record
->at(offset
+ song_idSong
).get_asInt();
3081 // Note this function does not populate artist credits, this must be done separately.
3082 // However artist names are held as a descriptive string
3083 song
.strArtistDesc
= record
->at(offset
+ song_strArtists
).get_asString();
3084 song
.strArtistSort
= record
->at(offset
+ song_strArtistSort
).get_asString();
3085 // Get the full genre string
3086 song
.genre
= StringUtils::Split(
3087 record
->at(offset
+ song_strGenres
).get_asString(),
3088 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
);
3090 song
.strAlbum
= record
->at(offset
+ song_strAlbum
).get_asString();
3091 song
.idAlbum
= record
->at(offset
+ song_idAlbum
).get_asInt();
3092 song
.iTrack
= record
->at(offset
+ song_iTrack
).get_asInt();
3093 song
.iDuration
= record
->at(offset
+ song_iDuration
).get_asInt();
3094 song
.strReleaseDate
= record
->at(offset
+ song_strReleaseDate
).get_asString();
3095 song
.strOrigReleaseDate
= record
->at(offset
+ song_strOrigReleaseDate
).get_asString();
3096 song
.strTitle
= record
->at(offset
+ song_strTitle
).get_asString();
3097 song
.iTimesPlayed
= record
->at(offset
+ song_iTimesPlayed
).get_asInt();
3098 song
.lastPlayed
.SetFromDBDateTime(record
->at(offset
+ song_lastplayed
).get_asString());
3099 song
.dateAdded
.SetFromDBDateTime(record
->at(offset
+ song_dateAdded
).get_asString());
3100 song
.dateNew
.SetFromDBDateTime(record
->at(offset
+ song_dateNew
).get_asString());
3101 song
.dateUpdated
.SetFromDBDateTime(record
->at(offset
+ song_dateModified
).get_asString());
3102 song
.iStartOffset
= record
->at(offset
+ song_iStartOffset
).get_asInt();
3103 song
.iEndOffset
= record
->at(offset
+ song_iEndOffset
).get_asInt();
3104 song
.strMusicBrainzTrackID
= record
->at(offset
+ song_strMusicBrainzTrackID
).get_asString();
3105 song
.rating
= record
->at(offset
+ song_rating
).get_asFloat();
3106 song
.userrating
= record
->at(offset
+ song_userrating
).get_asInt();
3107 song
.votes
= record
->at(offset
+ song_votes
).get_asInt();
3108 song
.strComment
= record
->at(offset
+ song_comment
).get_asString();
3109 song
.strMood
= record
->at(offset
+ song_mood
).get_asString();
3110 song
.bCompilation
= record
->at(offset
+ song_bCompilation
).get_asInt() == 1;
3111 song
.strDiscSubtitle
= record
->at(offset
+ song_strDiscSubtitle
).get_asString();
3112 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
3113 song
.replayGain
.Set(record
->at(offset
+ song_strReplayGain
).get_asString());
3114 // Get filename with full path
3116 URIUtils::AddFileToFolder(record
->at(offset
+ song_strPath
).get_asString(),
3117 record
->at(offset
+ song_strFileName
).get_asString());
3118 song
.iBPM
= record
->at(offset
+ song_iBPM
).get_asInt();
3119 song
.iBitRate
= record
->at(offset
+ song_iBitRate
).get_asInt();
3120 song
.iSampleRate
= record
->at(offset
+ song_iSampleRate
).get_asInt();
3121 song
.iChannels
= record
->at(offset
+ song_iChannels
).get_asInt();
3122 song
.songVideoURL
= record
->at(offset
+ song_songVideoURL
).get_asString();
3126 void CMusicDatabase::GetFileItemFromDataset(CFileItem
* item
, const CMusicDbUrl
& baseUrl
)
3128 GetFileItemFromDataset(m_pDS
->get_sql_record(), item
, baseUrl
);
3131 void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record
* const record
,
3133 const CMusicDbUrl
& baseUrl
)
3135 // get the artist string from songview (not the song_artist and artist tables)
3136 item
->GetMusicInfoTag()->SetArtistDesc(record
->at(song_strArtists
).get_asString());
3137 // get the artist sort name string from songview (not the song_artist and artist tables)
3138 item
->GetMusicInfoTag()->SetArtistSort(record
->at(song_strArtistSort
).get_asString());
3139 // and the full genre string
3140 item
->GetMusicInfoTag()->SetGenre(record
->at(song_strGenres
).get_asString());
3142 item
->GetMusicInfoTag()->SetAlbum(record
->at(song_strAlbum
).get_asString());
3143 item
->GetMusicInfoTag()->SetAlbumId(record
->at(song_idAlbum
).get_asInt());
3144 item
->GetMusicInfoTag()->SetTrackAndDiscNumber(record
->at(song_iTrack
).get_asInt());
3145 item
->GetMusicInfoTag()->SetDuration(record
->at(song_iDuration
).get_asInt());
3146 item
->GetMusicInfoTag()->SetDatabaseId(record
->at(song_idSong
).get_asInt(), MediaTypeSong
);
3147 item
->GetMusicInfoTag()->SetOriginalDate(record
->at(song_strOrigReleaseDate
).get_asString());
3148 item
->GetMusicInfoTag()->SetReleaseDate(record
->at(song_strReleaseDate
).get_asString());
3149 item
->GetMusicInfoTag()->SetTitle(record
->at(song_strTitle
).get_asString());
3150 item
->GetMusicInfoTag()->SetDiscSubtitle(record
->at(song_strDiscSubtitle
).get_asString());
3151 item
->SetLabel(record
->at(song_strTitle
).get_asString());
3152 item
->SetStartOffset(record
->at(song_iStartOffset
).get_asInt64());
3153 item
->SetProperty("item_start", item
->GetStartOffset());
3154 item
->SetEndOffset(record
->at(song_iEndOffset
).get_asInt64());
3155 item
->GetMusicInfoTag()->SetMusicBrainzTrackID(
3156 record
->at(song_strMusicBrainzTrackID
).get_asString());
3157 item
->GetMusicInfoTag()->SetRating(record
->at(song_rating
).get_asFloat());
3158 item
->GetMusicInfoTag()->SetUserrating(record
->at(song_userrating
).get_asInt());
3159 item
->GetMusicInfoTag()->SetVotes(record
->at(song_votes
).get_asInt());
3160 item
->GetMusicInfoTag()->SetComment(record
->at(song_comment
).get_asString());
3161 item
->GetMusicInfoTag()->SetMood(record
->at(song_mood
).get_asString());
3162 item
->GetMusicInfoTag()->SetPlayCount(record
->at(song_iTimesPlayed
).get_asInt());
3163 item
->GetMusicInfoTag()->SetLastPlayed(record
->at(song_lastplayed
).get_asString());
3164 item
->GetMusicInfoTag()->SetDateAdded(record
->at(song_dateAdded
).get_asString());
3165 item
->GetMusicInfoTag()->SetDateNew(record
->at(song_dateNew
).get_asString());
3166 item
->GetMusicInfoTag()->SetDateUpdated(record
->at(song_dateModified
).get_asString());
3167 std::string strRealPath
= URIUtils::AddFileToFolder(record
->at(song_strPath
).get_asString(),
3168 record
->at(song_strFileName
).get_asString());
3169 item
->GetMusicInfoTag()->SetURL(strRealPath
);
3170 item
->GetMusicInfoTag()->SetCompilation(record
->at(song_bCompilation
).get_asInt() == 1);
3171 item
->GetMusicInfoTag()->SetBoxset(record
->at(song_bBoxedSet
).get_asInt() == 1);
3172 // get the album artist string from songview (not the album_artist and artist tables)
3173 item
->GetMusicInfoTag()->SetAlbumArtist(record
->at(song_strAlbumArtists
).get_asString());
3174 item
->GetMusicInfoTag()->SetAlbumReleaseType(
3175 CAlbum::ReleaseTypeFromString(record
->at(song_strAlbumReleaseType
).get_asString()));
3176 item
->GetMusicInfoTag()->SetBPM(record
->at(song_iBPM
).get_asInt());
3177 item
->GetMusicInfoTag()->SetBitRate(record
->at(song_iBitRate
).get_asInt());
3178 item
->GetMusicInfoTag()->SetSampleRate(record
->at(song_iSampleRate
).get_asInt());
3179 item
->GetMusicInfoTag()->SetNoOfChannels(record
->at(song_iChannels
).get_asInt());
3180 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
3181 ReplayGain replaygain
;
3182 replaygain
.Set(record
->at(song_strReplayGain
).get_asString());
3183 item
->GetMusicInfoTag()->SetReplayGain(replaygain
);
3184 item
->GetMusicInfoTag()->SetTotalDiscs(record
->at(song_iDiscTotal
).get_asInt());
3185 item
->GetMusicInfoTag()->SetSongVideoURL(record
->at(song_songVideoURL
).get_asString());
3187 item
->GetMusicInfoTag()->SetLoaded(true);
3188 // Get filename with full path
3189 if (!baseUrl
.IsValid())
3190 item
->SetPath(strRealPath
);
3193 CMusicDbUrl itemUrl
= baseUrl
;
3194 std::string strFileName
= record
->at(song_strFileName
).get_asString();
3195 std::string strExt
= URIUtils::GetExtension(strFileName
);
3196 std::string path
= StringUtils::Format("{}{}", record
->at(song_idSong
).get_asInt(), strExt
);
3197 itemUrl
.AppendPath(path
);
3198 item
->SetPath(itemUrl
.ToString());
3199 item
->SetDynPath(strRealPath
);
3203 void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS
& artistCredits
, CFileItem
* item
)
3205 // Populate fileitem with artists from vector of artist credits
3206 std::vector
<std::string
> musicBrainzID
;
3207 std::vector
<std::string
> songartists
;
3208 CVariant
artistidObj(CVariant::VariantTypeArray
);
3210 // When "missing tag" artist, it is the only artist when present.
3211 if (artistCredits
.begin()->GetArtistId() == BLANKARTIST_ID
)
3213 artistidObj
.push_back((int)BLANKARTIST_ID
);
3214 songartists
.push_back(StringUtils::Empty
);
3218 for (const auto& artistCredit
: artistCredits
)
3220 artistidObj
.push_back(artistCredit
.GetArtistId());
3221 songartists
.push_back(artistCredit
.GetArtist());
3222 if (!artistCredit
.GetMusicBrainzArtistID().empty())
3223 musicBrainzID
.push_back(artistCredit
.GetMusicBrainzArtistID());
3226 // Also sets ArtistDesc if empty from song.strArtist field
3227 item
->GetMusicInfoTag()->SetArtist(songartists
);
3228 item
->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID
);
3229 // Add album artistIds as separate property as not part of CMusicInfoTag
3230 item
->SetProperty("artistid", artistidObj
);
3233 CAlbum
CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset
* pDS
,
3234 int offset
/* = 0 */,
3235 bool imageURL
/* = false*/)
3237 return GetAlbumFromDataset(pDS
->get_sql_record(), offset
, imageURL
);
3240 CAlbum
CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record
* const record
,
3241 int offset
/* = 0 */,
3242 bool imageURL
/* = false*/)
3244 const std::string itemSeparator
=
3245 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
3248 album
.idAlbum
= record
->at(offset
+ album_idAlbum
).get_asInt();
3249 album
.strAlbum
= record
->at(offset
+ album_strAlbum
).get_asString();
3250 if (album
.strAlbum
.empty())
3251 album
.strAlbum
= g_localizeStrings
.Get(1050);
3252 album
.strMusicBrainzAlbumID
= record
->at(offset
+ album_strMusicBrainzAlbumID
).get_asString();
3253 album
.strReleaseGroupMBID
= record
->at(offset
+ album_strReleaseGroupMBID
).get_asString();
3254 album
.strArtistDesc
= record
->at(offset
+ album_strArtists
).get_asString();
3255 album
.strArtistSort
= record
->at(offset
+ album_strArtistSort
).get_asString();
3257 StringUtils::Split(record
->at(offset
+ album_strGenres
).get_asString(), itemSeparator
);
3258 album
.strReleaseDate
= record
->at(offset
+ album_strReleaseDate
).get_asString();
3259 album
.strOrigReleaseDate
= record
->at(offset
+ album_strOrigReleaseDate
).get_asString();
3260 album
.bBoxedSet
= record
->at(offset
+ album_bBoxedSet
).get_asInt() == 1;
3262 album
.thumbURL
.ParseFromData(record
->at(offset
+ album_strThumbURL
).get_asString());
3263 album
.fRating
= record
->at(offset
+ album_fRating
).get_asFloat();
3264 album
.iUserrating
= record
->at(offset
+ album_iUserrating
).get_asInt();
3265 album
.iVotes
= record
->at(offset
+ album_iVotes
).get_asInt();
3266 album
.strReview
= record
->at(offset
+ album_strReview
).get_asString();
3268 StringUtils::Split(record
->at(offset
+ album_strStyles
).get_asString(), itemSeparator
);
3270 StringUtils::Split(record
->at(offset
+ album_strMoods
).get_asString(), itemSeparator
);
3272 StringUtils::Split(record
->at(offset
+ album_strThemes
).get_asString(), itemSeparator
);
3273 album
.strLabel
= record
->at(offset
+ album_strLabel
).get_asString();
3274 album
.strType
= record
->at(offset
+ album_strType
).get_asString();
3275 album
.strReleaseStatus
= record
->at(offset
+ album_strReleaseStatus
).get_asString();
3276 album
.bCompilation
= record
->at(offset
+ album_bCompilation
).get_asInt() == 1;
3277 album
.bScrapedMBID
= record
->at(offset
+ album_bScrapedMBID
).get_asInt() == 1;
3278 album
.strLastScraped
= record
->at(offset
+ album_lastScraped
).get_asString();
3279 album
.iTimesPlayed
= record
->at(offset
+ album_iTimesPlayed
).get_asInt();
3280 album
.SetReleaseType(record
->at(offset
+ album_strReleaseType
).get_asString());
3281 album
.iTotalDiscs
= record
->at(offset
+ album_iTotalDiscs
).get_asInt();
3282 album
.SetDateAdded(record
->at(offset
+ album_dateAdded
).get_asString());
3283 album
.SetDateNew(record
->at(offset
+ album_dateNew
).get_asString());
3284 album
.SetDateUpdated(record
->at(offset
+ album_dateModified
).get_asString());
3285 album
.SetLastPlayed(record
->at(offset
+ album_dtLastPlayed
).get_asString());
3286 album
.iAlbumDuration
= record
->at(offset
+ album_iAlbumDuration
).get_asInt();
3290 CArtistCredit
CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record
* const record
,
3291 int offset
/* = 0 */)
3293 CArtistCredit artistCredit
;
3294 artistCredit
.idArtist
= record
->at(offset
+ artistCredit_idArtist
).get_asInt();
3295 if (artistCredit
.idArtist
== BLANKARTIST_ID
)
3296 artistCredit
.m_strArtist
= StringUtils::Empty
;
3299 artistCredit
.m_strArtist
= record
->at(offset
+ artistCredit_strArtist
).get_asString();
3300 artistCredit
.m_strMusicBrainzArtistID
=
3301 record
->at(offset
+ artistCredit_strMusicBrainzArtistID
).get_asString();
3303 return artistCredit
;
3306 CMusicRole
CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record
* const record
,
3307 int offset
/* = 0 */)
3309 CMusicRole
ArtistRole(record
->at(offset
+ artistCredit_idRole
).get_asInt(),
3310 record
->at(offset
+ artistCredit_strRole
).get_asString(),
3311 record
->at(offset
+ artistCredit_strArtist
).get_asString(),
3312 record
->at(offset
+ artistCredit_idArtist
).get_asInt());
3316 CArtist
CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset
* pDS
,
3317 int offset
/* = 0 */,
3318 bool needThumb
/* = true */)
3320 return GetArtistFromDataset(pDS
->get_sql_record(), offset
, needThumb
);
3323 CArtist
CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record
* const record
,
3324 int offset
/* = 0 */,
3325 bool needThumb
/* = true */)
3327 const std::string itemSeparator
=
3328 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
3331 artist
.idArtist
= record
->at(offset
+ artist_idArtist
).get_asInt();
3332 if (artist
.idArtist
== BLANKARTIST_ID
&& m_translateBlankArtist
)
3333 artist
.strArtist
= g_localizeStrings
.Get(38042); //Missing artist tag in current language
3335 artist
.strArtist
= record
->at(offset
+ artist_strArtist
).get_asString();
3336 artist
.strSortName
= record
->at(offset
+ artist_strSortName
).get_asString();
3337 artist
.strMusicBrainzArtistID
= record
->at(offset
+ artist_strMusicBrainzArtistID
).get_asString();
3338 artist
.strType
= record
->at(offset
+ artist_strType
).get_asString();
3339 artist
.strGender
= record
->at(offset
+ artist_strGender
).get_asString();
3340 artist
.strDisambiguation
= record
->at(offset
+ artist_strDisambiguation
).get_asString();
3342 StringUtils::Split(record
->at(offset
+ artist_strGenres
).get_asString(), itemSeparator
);
3343 artist
.strBiography
= record
->at(offset
+ artist_strBiography
).get_asString();
3345 StringUtils::Split(record
->at(offset
+ artist_strStyles
).get_asString(), itemSeparator
);
3347 StringUtils::Split(record
->at(offset
+ artist_strMoods
).get_asString(), itemSeparator
);
3348 artist
.strBorn
= record
->at(offset
+ artist_strBorn
).get_asString();
3349 artist
.strFormed
= record
->at(offset
+ artist_strFormed
).get_asString();
3350 artist
.strDied
= record
->at(offset
+ artist_strDied
).get_asString();
3351 artist
.strDisbanded
= record
->at(offset
+ artist_strDisbanded
).get_asString();
3352 artist
.yearsActive
=
3353 StringUtils::Split(record
->at(offset
+ artist_strYearsActive
).get_asString(), itemSeparator
);
3354 artist
.instruments
=
3355 StringUtils::Split(record
->at(offset
+ artist_strInstruments
).get_asString(), itemSeparator
);
3356 artist
.bScrapedMBID
= record
->at(offset
+ artist_bScrapedMBID
).get_asInt() == 1;
3357 artist
.strLastScraped
= record
->at(offset
+ artist_lastScraped
).get_asString();
3358 artist
.SetDateAdded(record
->at(offset
+ artist_dateAdded
).get_asString());
3359 artist
.SetDateNew(record
->at(offset
+ artist_dateNew
).get_asString());
3360 artist
.SetDateUpdated(record
->at(offset
+ artist_dateModified
).get_asString());
3364 artist
.thumbURL
.ParseFromData(record
->at(artist_strImage
).get_asString());
3370 bool CMusicDatabase::GetSongByFileName(const std::string
& strFileNameAndPath
,
3372 int64_t startOffset
)
3375 CURL
url(strFileNameAndPath
);
3377 if (url
.IsProtocol("musicdb"))
3379 std::string strFile
= URIUtils::GetFileName(strFileNameAndPath
);
3380 URIUtils::RemoveExtension(strFile
);
3381 return GetSong(atoi(strFile
.c_str()), song
);
3384 if (nullptr == m_pDB
)
3386 if (nullptr == m_pDS
)
3389 std::string strPath
, strFileName
;
3390 SplitPath(strFileNameAndPath
, strPath
, strFileName
);
3391 URIUtils::AddSlashAtEnd(strPath
);
3393 std::string strSQL
= PrepareSQL("SELECT idSong FROM songview "
3394 "WHERE strFileName='%s' AND strPath='%s'",
3395 strFileName
.c_str(), strPath
.c_str());
3397 strSQL
+= PrepareSQL(" AND iStartOffset=%" PRIi64
, startOffset
);
3399 int idSong
= GetSingleValueInt(strSQL
);
3401 return GetSong(idSong
, song
);
3406 int CMusicDatabase::GetAlbumIdByPath(const std::string
& strPath
)
3410 if (nullptr == m_pDB
)
3412 if (nullptr == m_pDS
)
3415 std::string strSQL
= PrepareSQL("SELECT DISTINCT idAlbum FROM song "
3416 "JOIN path ON song.idPath = path.idPath "
3417 "WHERE path.strPath='%s'",
3420 if (!m_pDS
->query(strSQL
))
3422 int iRowsFound
= m_pDS
->num_rows();
3424 int idAlbum
= -1; // If no album is found, or more than one album is found then -1 is returned
3425 if (iRowsFound
== 1)
3426 idAlbum
= m_pDS
->fv(0).get_asInt();
3434 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, strPath
);
3440 int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string
& strArtist
,
3441 const std::string
& strAlbum
,
3442 const std::string
& strTitle
)
3446 std::string strSQL
=
3447 PrepareSQL("SELECT idSong FROM songview "
3448 "WHERE strArtists LIKE '%s' AND strAlbum LIKE '%s' AND strTitle LIKE '%s'",
3449 strArtist
.c_str(), strAlbum
.c_str(), strTitle
.c_str());
3451 if (!m_pDS
->query(strSQL
))
3453 int iRowsFound
= m_pDS
->num_rows();
3454 if (iRowsFound
== 0)
3459 int lResult
= m_pDS
->fv(0).get_asInt();
3460 m_pDS
->close(); // cleanup recordset data
3465 CLog::Log(LOGERROR
, "{} ({},{},{}) failed", __FUNCTION__
, strArtist
, strAlbum
, strTitle
);
3471 bool CMusicDatabase::SearchArtists(const std::string
& search
, CFileItemList
& artists
)
3475 if (nullptr == m_pDB
)
3477 if (nullptr == m_pDS
)
3480 std::string strVariousArtists
= g_localizeStrings
.Get(340).c_str();
3482 if (search
.size() >= MIN_FULL_SEARCH_LENGTH
)
3483 strSQL
= PrepareSQL("SELECT * FROM artist "
3484 "WHERE (strArtist LIKE '%s%%' OR strArtist LIKE '%% %s%%') "
3485 "AND strArtist <> '%s' ",
3486 search
.c_str(), search
.c_str(), strVariousArtists
.c_str());
3488 strSQL
= PrepareSQL("SELECT * FROM artist "
3489 "WHERE strArtist LIKE '%s%%' AND strArtist <> '%s' ",
3490 search
.c_str(), strVariousArtists
.c_str());
3492 if (!m_pDS
->query(strSQL
))
3494 if (m_pDS
->num_rows() == 0)
3500 const std::string
& artistLabel(g_localizeStrings
.Get(557)); // Artist
3501 while (!m_pDS
->eof())
3503 std::string path
= StringUtils::Format("musicdb://artists/{}/", m_pDS
->fv(0).get_asInt());
3504 CFileItemPtr
pItem(new CFileItem(path
, true));
3505 std::string label
= StringUtils::Format("[{}] {}", artistLabel
, m_pDS
->fv(1).get_asString());
3506 pItem
->SetLabel(label
);
3507 // sort label is stored in the title tag
3508 label
= StringUtils::Format("A {}", m_pDS
->fv(1).get_asString());
3509 pItem
->GetMusicInfoTag()->SetTitle(label
);
3510 pItem
->GetMusicInfoTag()->SetDatabaseId(m_pDS
->fv(0).get_asInt(), MediaTypeArtist
);
3515 m_pDS
->close(); // cleanup recordset data
3520 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3526 bool CMusicDatabase::GetTop100(const std::string
& strBaseDir
, CFileItemList
& items
)
3530 if (nullptr == m_pDB
)
3532 if (nullptr == m_pDS
)
3535 CMusicDbUrl baseUrl
;
3536 if (!strBaseDir
.empty() && !baseUrl
.FromString(strBaseDir
))
3539 std::string strSQL
= "SELECT * FROM songview "
3540 "WHERE iTimesPlayed>0 "
3541 "ORDER BY iTimesPlayed DESC "
3544 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3545 if (!m_pDS
->query(strSQL
))
3547 int iRowsFound
= m_pDS
->num_rows();
3548 if (iRowsFound
== 0)
3553 items
.Reserve(iRowsFound
);
3554 while (!m_pDS
->eof())
3556 CFileItemPtr
item(new CFileItem
);
3557 GetFileItemFromDataset(item
.get(), baseUrl
);
3562 m_pDS
->close(); // cleanup recordset data
3567 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3573 bool CMusicDatabase::GetTop100Albums(VECALBUMS
& albums
)
3577 albums
.erase(albums
.begin(), albums
.end());
3578 if (nullptr == m_pDB
)
3580 if (nullptr == m_pDS
)
3583 // Get data from album and album_artist tables to fully populate albums
3584 std::string strSQL
= "SELECT albumview.*, albumartistview.* FROM albumview "
3585 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3586 "WHERE albumartistview.idAlbum IN "
3587 "(SELECT albumview.idAlbum FROM albumview "
3588 "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
3589 "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
3590 "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
3592 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3593 if (!m_pDS
->query(strSQL
))
3595 int iRowsFound
= m_pDS
->num_rows();
3596 if (iRowsFound
== 0)
3602 int albumArtistOffset
= album_enumCount
;
3604 while (!m_pDS
->eof())
3606 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
3608 if (albumId
!= record
->at(album_idAlbum
).get_asInt())
3610 albumId
= record
->at(album_idAlbum
).get_asInt();
3611 albums
.push_back(GetAlbumFromDataset(record
));
3613 // Get album artists
3614 albums
.back().artistCredits
.push_back(GetArtistCreditFromDataset(record
, albumArtistOffset
));
3619 m_pDS
->close(); // cleanup recordset data
3624 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3630 bool CMusicDatabase::GetTop100AlbumSongs(const std::string
& strBaseDir
, CFileItemList
& items
)
3634 if (nullptr == m_pDB
)
3636 if (nullptr == m_pDS
)
3639 CMusicDbUrl baseUrl
;
3640 if (!strBaseDir
.empty() && baseUrl
.FromString(strBaseDir
))
3643 std::string strSQL
= StringUtils::Format(
3644 "SELECT songview.*, albumview.* FROM songview"
3645 "JOIN albumview ON (songview.idAlbum = albumview.idAlbum) "
3646 "JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song "
3647 "WHERE song.iTimesPlayed > 0 "
3649 "ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit "
3650 "ON (songview.idAlbum = _albumlimit.idAlbum) "
3651 "ORDER BY _albumlimit.iTimesPlayedSum DESC");
3652 CLog::Log(LOGDEBUG
, "GetTop100AlbumSongs() query: {}", strSQL
);
3653 if (!m_pDS
->query(strSQL
))
3656 int iRowsFound
= m_pDS
->num_rows();
3657 if (iRowsFound
== 0)
3663 // get data from returned rows
3664 items
.Reserve(iRowsFound
);
3665 while (!m_pDS
->eof())
3667 CFileItemPtr
item(new CFileItem
);
3668 GetFileItemFromDataset(item
.get(), baseUrl
);
3679 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3684 bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS
& albums
)
3688 albums
.erase(albums
.begin(), albums
.end());
3689 if (nullptr == m_pDB
)
3691 if (nullptr == m_pDS
)
3694 auto start
= std::chrono::steady_clock::now();
3696 // Get data from album and album_artist tables to fully populate albums
3697 std::string strSQL
=
3698 PrepareSQL("SELECT albumview.*, albumartistview.* "
3699 "FROM (SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3700 "AND albumview.strReleaseType = '%s' "
3701 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3702 "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
3703 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3704 "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
3705 CAlbum::ReleaseTypeToString(CAlbum::Album
).c_str(), RECENTLY_PLAYED_LIMIT
);
3707 auto queryStart
= std::chrono::steady_clock::now();
3708 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3709 if (!m_pDS
->query(strSQL
))
3712 auto queryEnd
= std::chrono::steady_clock::now();
3713 auto queryDuration
=
3714 std::chrono::duration_cast
<std::chrono::milliseconds
>(queryEnd
- queryStart
);
3716 int iRowsFound
= m_pDS
->num_rows();
3717 if (iRowsFound
== 0)
3723 int albumArtistOffset
= album_enumCount
;
3725 while (!m_pDS
->eof())
3727 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
3729 if (albumId
!= record
->at(album_idAlbum
).get_asInt())
3731 albumId
= record
->at(album_idAlbum
).get_asInt();
3732 albums
.push_back(GetAlbumFromDataset(record
));
3734 // Get album artists
3735 albums
.back().artistCredits
.push_back(GetArtistCreditFromDataset(record
, albumArtistOffset
));
3739 m_pDS
->close(); // cleanup recordset data
3741 auto end
= std::chrono::steady_clock::now();
3742 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
3744 CLog::Log(LOGDEBUG
, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__
,
3745 duration
.count(), queryDuration
.count());
3751 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3757 bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string
& strBaseDir
,
3758 CFileItemList
& items
)
3762 if (nullptr == m_pDB
)
3764 if (nullptr == m_pDS
)
3767 CMusicDbUrl baseUrl
;
3768 if (!strBaseDir
.empty() && !baseUrl
.FromString(strBaseDir
))
3771 std::string strSQL
=
3772 PrepareSQL("SELECT songview.*, songartistview.* "
3773 "FROM (SELECT idAlbum, lastPlayed FROM albumview "
3774 "WHERE albumview.lastplayed IS NOT NULL "
3775 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3776 "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
3777 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3778 "ORDER BY playedalbums.lastplayed DESC, "
3779 "songartistview.idsong, songartistview.idRole, songartistview.iOrder",
3780 CServiceBroker::GetSettingsComponent()
3781 ->GetAdvancedSettings()
3782 ->m_iMusicLibraryRecentlyAddedItems
);
3783 CLog::Log(LOGDEBUG
, "GetRecentlyPlayedAlbumSongs() query: {}", strSQL
);
3784 if (!m_pDS
->query(strSQL
))
3787 int iRowsFound
= m_pDS
->num_rows();
3788 if (iRowsFound
== 0)
3794 // Needs a separate query to determine number of songs to set items size.
3795 // Get songs from returned rows. Join means there is a row for every song artist
3796 // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
3797 int songArtistOffset
= song_enumCount
;
3799 VECARTISTCREDITS artistCredits
;
3800 while (!m_pDS
->eof())
3802 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
3804 int idSongArtistRole
= record
->at(songArtistOffset
+ artistCredit_idRole
).get_asInt();
3805 if (songId
!= record
->at(song_idSong
).get_asInt())
3807 if (songId
> 0 && !artistCredits
.empty())
3809 //Store artist credits for previous song
3810 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
3811 artistCredits
.clear();
3813 songId
= record
->at(song_idSong
).get_asInt();
3814 CFileItemPtr
item(new CFileItem
);
3815 GetFileItemFromDataset(record
, item
.get(), baseUrl
);
3818 // Get song artist credits and contributors
3819 if (idSongArtistRole
== ROLE_ARTIST
)
3820 artistCredits
.push_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
3822 items
[items
.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3823 GetArtistRoleFromDataset(record
, songArtistOffset
));
3827 if (!artistCredits
.empty())
3829 //Store artist credits for final song
3830 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
3831 artistCredits
.clear();
3840 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3845 bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS
& albums
, unsigned int limit
)
3849 albums
.erase(albums
.begin(), albums
.end());
3850 if (nullptr == m_pDB
)
3852 if (nullptr == m_pDS
)
3855 // Get data from album and album_artist tables to fully populate albums
3856 // Determine the recently added albums from dateAdded (usually derived from music file
3857 // timestamps, nothing to do with when albums added to library)
3858 std::string strSQL
=
3859 PrepareSQL("SELECT albumview.*, albumartistview.* "
3860 "FROM (SELECT idAlbum FROM album WHERE strAlbum != '' "
3861 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3862 "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
3863 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3864 "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
3866 : CServiceBroker::GetSettingsComponent()
3867 ->GetAdvancedSettings()
3868 ->m_iMusicLibraryRecentlyAddedItems
);
3870 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
3871 if (!m_pDS
->query(strSQL
))
3873 int iRowsFound
= m_pDS
->num_rows();
3874 if (iRowsFound
== 0)
3880 int albumArtistOffset
= album_enumCount
;
3882 while (!m_pDS
->eof())
3884 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
3886 if (albumId
!= record
->at(album_idAlbum
).get_asInt())
3888 albumId
= record
->at(album_idAlbum
).get_asInt();
3889 albums
.push_back(GetAlbumFromDataset(record
));
3891 // Get album artists
3892 albums
.back().artistCredits
.push_back(GetArtistCreditFromDataset(record
, albumArtistOffset
));
3896 m_pDS
->close(); // cleanup recordset data
3901 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3907 bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string
& strBaseDir
,
3908 CFileItemList
& items
,
3913 if (nullptr == m_pDB
)
3915 if (nullptr == m_pDS
)
3918 CMusicDbUrl baseUrl
;
3919 if (!strBaseDir
.empty() && !baseUrl
.FromString(strBaseDir
))
3922 // Get data from song and song_artist tables to fully populate songs
3923 // Determine the recently added albums from dateAdded (usually derived from music file
3924 // timestamps, nothing to do with when albums added to library)
3926 strSQL
= PrepareSQL("SELECT songview.*, songartistview.* "
3927 "FROM (SELECT idAlbum, dateAdded FROM album "
3928 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3929 "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
3930 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3931 "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
3932 "songview.idSong, songartistview.idRole, songartistview.iOrder ",
3934 : CServiceBroker::GetSettingsComponent()
3935 ->GetAdvancedSettings()
3936 ->m_iMusicLibraryRecentlyAddedItems
);
3937 CLog::Log(LOGDEBUG
, "GetRecentlyAddedAlbumSongs() query: {}", strSQL
);
3938 if (!m_pDS
->query(strSQL
))
3941 int iRowsFound
= m_pDS
->num_rows();
3942 if (iRowsFound
== 0)
3948 // Needs a separate query to determine number of songs to set items size.
3949 // Get songs from returned rows. Join means there is a row for every song artist
3950 int songArtistOffset
= song_enumCount
;
3952 VECARTISTCREDITS artistCredits
;
3953 while (!m_pDS
->eof())
3955 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
3957 int idSongArtistRole
= record
->at(songArtistOffset
+ artistCredit_idRole
).get_asInt();
3958 if (songId
!= record
->at(song_idSong
).get_asInt())
3960 if (songId
> 0 && !artistCredits
.empty())
3962 //Store artist credits for previous song
3963 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
3964 artistCredits
.clear();
3966 songId
= record
->at(song_idSong
).get_asInt();
3967 CFileItemPtr
item(new CFileItem
);
3968 GetFileItemFromDataset(record
, item
.get(), baseUrl
);
3971 // Get song artist credits and contributors
3972 if (idSongArtistRole
== ROLE_ARTIST
)
3973 artistCredits
.push_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
3975 items
[items
.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
3976 GetArtistRoleFromDataset(record
, songArtistOffset
));
3980 if (!artistCredits
.empty())
3982 //Store artist credits for final song
3983 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
3984 artistCredits
.clear();
3993 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
3998 void CMusicDatabase::IncrementPlayCount(const CFileItem
& item
)
4002 if (nullptr == m_pDB
)
4004 if (nullptr == m_pDS
)
4007 int idSong
= GetSongIDFromPath(item
.GetPath());
4008 std::string strDateNow
= CDateTime::GetCurrentDateTime().GetAsDBDateTime();
4009 std::string sql
= PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
4011 strDateNow
.c_str(), idSong
);
4016 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, item
.GetPath());
4020 bool CMusicDatabase::GetSongsByPath(const std::string
& strPath1
,
4024 std::string
strPath(strPath1
);
4027 if (!URIUtils::HasSlashAtEnd(strPath
))
4028 URIUtils::AddSlashAtEnd(strPath
);
4033 if (nullptr == m_pDB
)
4035 if (nullptr == m_pDS
)
4038 // Filename is not unique for a path as songs from a cuesheet have same filename.
4039 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
4040 // in a folder and some edited and rescanned.
4041 // Hence order by filename so these songs can be gathered together.
4042 std::string strSQL
= PrepareSQL("SELECT * FROM songview "
4043 "WHERE strPath='%s' ORDER BY strFileName",
4045 if (!m_pDS
->query(strSQL
))
4047 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
4048 int iRowsFound
= m_pDS
->num_rows();
4049 if (iRowsFound
== 0)
4055 // Each file is potentially mapped to a list of songs, gather these and save as list
4057 std::string filename
;
4058 while (!m_pDS
->eof())
4060 CSong song
= GetSongFromDataset();
4061 if (!filename
.empty() && filename
!= song
.strFileName
)
4063 // Save songs for previous filename
4064 songmap
.insert(std::make_pair(filename
, songs
));
4067 filename
= song
.strFileName
;
4068 songs
.emplace_back(song
);
4071 m_pDS
->close(); // cleanup recordset data
4072 songmap
.insert(std::make_pair(filename
, songs
)); // Save songs for last filename
4077 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, strPath
);
4083 void CMusicDatabase::EmptyCache()
4085 m_genreCache
.erase(m_genreCache
.begin(), m_genreCache
.end());
4086 m_pathCache
.erase(m_pathCache
.begin(), m_pathCache
.end());
4089 bool CMusicDatabase::Search(const std::string
& search
, CFileItemList
& items
)
4091 auto start
= std::chrono::steady_clock::now();
4092 // first grab all the artists that match
4093 SearchArtists(search
, items
);
4094 auto end
= std::chrono::steady_clock::now();
4095 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
4096 CLog::Log(LOGDEBUG
, "{} Artist search in {} ms", __FUNCTION__
, duration
.count());
4098 start
= std::chrono::steady_clock::now();
4099 // then albums that match
4100 SearchAlbums(search
, items
);
4101 end
= std::chrono::steady_clock::now();
4102 duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
4103 CLog::Log(LOGDEBUG
, "{} Album search in {} ms", __FUNCTION__
, duration
.count());
4105 start
= std::chrono::steady_clock::now();
4106 // and finally songs
4107 SearchSongs(search
, items
);
4108 end
= std::chrono::steady_clock::now();
4109 duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
4110 CLog::Log(LOGDEBUG
, "{} Songs search in {} ms", __FUNCTION__
, duration
.count());
4115 bool CMusicDatabase::SearchSongs(const std::string
& search
, CFileItemList
& items
)
4119 if (nullptr == m_pDB
)
4121 if (nullptr == m_pDS
)
4124 CMusicDbUrl baseUrl
;
4125 if (!baseUrl
.FromString("musicdb://songs/"))
4129 if (search
.size() >= MIN_FULL_SEARCH_LENGTH
)
4130 strSQL
= PrepareSQL("SELECT * FROM songview "
4131 "WHERE strTitle LIKE '%s%%' or strTitle LIKE '%% %s%%' LIMIT 1000",
4132 search
.c_str(), search
.c_str());
4134 strSQL
= PrepareSQL("SELECT * FROM songview "
4135 "WHERE strTitle LIKE '%s%%' LIMIT 1000",
4138 if (!m_pDS
->query(strSQL
))
4140 if (m_pDS
->num_rows() == 0)
4143 while (!m_pDS
->eof())
4145 CFileItemPtr
item(new CFileItem
);
4146 GetFileItemFromDataset(item
.get(), baseUrl
);
4156 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
4162 bool CMusicDatabase::SearchAlbums(const std::string
& search
, CFileItemList
& albums
)
4166 if (nullptr == m_pDB
)
4168 if (nullptr == m_pDS
)
4172 if (search
.size() >= MIN_FULL_SEARCH_LENGTH
)
4173 strSQL
= PrepareSQL("SELECT * FROM albumview "
4174 "WHERE strAlbum LIKE '%s%%' OR strAlbum LIKE '%% %s%%'",
4175 search
.c_str(), search
.c_str());
4177 strSQL
= PrepareSQL("SELECT * FROM albumview "
4178 "WHERE strAlbum LIKE '%s%%'",
4181 if (!m_pDS
->query(strSQL
))
4184 const std::string
& albumLabel(g_localizeStrings
.Get(558)); // Album
4185 while (!m_pDS
->eof())
4187 CAlbum album
= GetAlbumFromDataset(m_pDS
.get());
4188 std::string path
= StringUtils::Format("musicdb://albums/{}/", album
.idAlbum
);
4189 CFileItemPtr
pItem(new CFileItem(path
, album
));
4190 std::string label
= StringUtils::Format("[{}] {}", albumLabel
, album
.strAlbum
);
4191 pItem
->SetLabel(label
);
4192 // sort label is stored in the title tag
4193 label
= StringUtils::Format("B {}", album
.strAlbum
);
4194 pItem
->GetMusicInfoTag()->SetTitle(label
);
4198 m_pDS
->close(); // cleanup recordset data
4203 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
4208 bool CMusicDatabase::CleanupSongsByIds(const std::string
& strSongIds
)
4212 if (nullptr == m_pDB
)
4214 if (nullptr == m_pDS
)
4216 // ok, now find all idSong's
4217 std::string strSQL
= PrepareSQL("SELECT * FROM song JOIN path ON song.idPath = path.idPath "
4218 "WHERE song.idSong IN %s",
4219 strSongIds
.c_str());
4220 if (!m_pDS
->query(strSQL
))
4222 int iRowsFound
= m_pDS
->num_rows();
4223 if (iRowsFound
== 0)
4228 std::vector
<std::string
> songsToDelete
;
4229 while (!m_pDS
->eof())
4230 { // get the full song path
4231 std::string strFileName
= URIUtils::AddFileToFolder(
4232 m_pDS
->fv("path.strPath").get_asString(), m_pDS
->fv("song.strFileName").get_asString());
4234 // Special case for streams inside an audio decoder package file.
4235 // The last dir in the path is the audio file that
4236 // contains the stream, so test if its there
4237 if (StringUtils::EndsWith(URIUtils::GetExtension(strFileName
),
4238 KODI_ADDON_AUDIODECODER_TRACK_EXT
))
4240 strFileName
= URIUtils::GetDirectory(strFileName
);
4241 // we are dropping back to a file, so remove the slash at end
4242 URIUtils::RemoveSlashAtEnd(strFileName
);
4245 if (!CFile::Exists(strFileName
, false))
4246 { // file no longer exists, so add to deletion list
4247 songsToDelete
.push_back(m_pDS
->fv("song.idSong").get_asString());
4253 if (!songsToDelete
.empty())
4255 std::string strSongsToDelete
= "(" + StringUtils::Join(songsToDelete
, ",") + ")";
4256 // ok, now delete these songs + all references to them from the linked tables
4257 strSQL
= "delete from song where idSong in " + strSongsToDelete
;
4258 m_pDS
->exec(strSQL
);
4265 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
4270 bool CMusicDatabase::CleanupSongs(CGUIDialogProgress
* progressDialog
/*= nullptr*/)
4275 // Count total number of songs
4276 total
= GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS
);
4277 // No songs to clean
4281 // run through all songs and get all unique path ids
4283 for (int i
= 0;; i
+= iLIMIT
)
4285 std::string strSQL
= PrepareSQL("SELECT song.idSong FROM song "
4286 "ORDER BY song.idSong LIMIT %i OFFSET %i",
4288 if (!m_pDS
->query(strSQL
))
4290 int iRowsFound
= m_pDS
->num_rows();
4291 // keep going until no rows are left!
4292 if (iRowsFound
== 0)
4298 std::vector
<std::string
> songIds
;
4299 while (!m_pDS
->eof())
4301 songIds
.push_back(m_pDS
->fv("song.idSong").get_asString());
4305 std::string strSongIds
= "(" + StringUtils::Join(songIds
, ",") + ")";
4306 CLog::Log(LOGDEBUG
, "Checking songs from song ID list: {}", strSongIds
);
4309 int percentage
= i
* 100 / total
;
4310 if (percentage
> progressDialog
->GetPercentage())
4312 progressDialog
->SetPercentage(percentage
);
4313 progressDialog
->Progress();
4315 if (progressDialog
->IsCanceled())
4321 if (!CleanupSongsByIds(strSongIds
))
4328 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupSongs()");
4333 bool CMusicDatabase::CleanupAlbums()
4337 // This must be run AFTER songs have been cleaned up
4338 // delete albums with no reference to songs
4339 std::string strSQL
= "SELECT * FROM album "
4340 "WHERE album.idAlbum NOT IN (SELECT idAlbum FROM song)";
4341 if (!m_pDS
->query(strSQL
))
4343 int iRowsFound
= m_pDS
->num_rows();
4344 if (iRowsFound
== 0)
4350 std::vector
<std::string
> albumIds
;
4351 while (!m_pDS
->eof())
4353 albumIds
.push_back(m_pDS
->fv("album.idAlbum").get_asString());
4358 std::string strAlbumIds
= "(" + StringUtils::Join(albumIds
, ",") + ")";
4359 // ok, now we can delete them and the references in the linked tables
4360 strSQL
= "delete from album where idAlbum in " + strAlbumIds
;
4361 m_pDS
->exec(strSQL
);
4366 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupAlbums()");
4371 bool CMusicDatabase::CleanupPaths()
4375 // needs to be done AFTER the songs and albums have been cleaned up.
4376 // we can happily delete any path that has no reference to a song
4377 // but we must keep all paths that have been scanned that may contain songs in subpaths
4379 // first create a temporary table of song paths
4380 m_pDS
->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
4381 m_pDS
->exec("INSERT INTO songpaths "
4382 "SELECT idPath, strPath FROM path "
4383 "WHERE idPath IN (SELECT idPath FROM song)\n");
4385 // grab all paths that aren't immediately connected with a song
4386 std::string sql
= "SELECT * FROM path WHERE idPath NOT IN (SELECT idPath FROM song)";
4387 if (!m_pDS
->query(sql
))
4389 int iRowsFound
= m_pDS
->num_rows();
4390 if (iRowsFound
== 0)
4395 // and construct a list to delete
4396 std::vector
<std::string
> pathIds
;
4397 while (!m_pDS
->eof())
4399 // anything that isn't a parent path of a song path is to be deleted
4400 std::string path
= m_pDS
->fv("strPath").get_asString();
4401 sql
= PrepareSQL("SELECT COUNT(idPath) FROM songpaths WHERE SUBSTR(strPath,1,%i)='%s'",
4402 StringUtils::utf8_strlen(path
.c_str()), path
.c_str());
4403 if (m_pDS2
->query(sql
) && m_pDS2
->num_rows() == 1 && m_pDS2
->fv(0).get_asInt() == 0)
4404 pathIds
.push_back(m_pDS
->fv("idPath").get_asString()); // nothing found, so delete
4410 if (!pathIds
.empty())
4412 // do the deletion, and drop our temp table
4413 std::string deleteSQL
=
4414 "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds
, ",") + ")";
4415 m_pDS
->exec(deleteSQL
);
4417 m_pDS
->exec("drop table songpaths");
4422 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
4427 bool CMusicDatabase::InsideScannedPath(const std::string
& path
)
4429 std::string sql
= PrepareSQL("SELECT idPath FROM path WHERE SUBSTR(strPath,1,%i)='%s' LIMIT 1",
4430 path
.size(), path
.c_str());
4431 return !GetSingleValue(sql
).empty();
4434 bool CMusicDatabase::CleanupArtists()
4438 // (nested queries by Bobbin007)
4439 // must be executed AFTER the song, album and their artist link tables are cleaned.
4440 // Don't delete [Missing] the missing artist tag artist
4442 // Create temp table to avoid 1442 trigger hell on mysql
4443 m_pDS
->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
4444 m_pDS
->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
4445 m_pDS
->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
4446 m_pDS
->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID
));
4447 // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
4448 // To avoid MySQL hanging or timeout create a table of unique ids with primary key
4449 m_pDS
->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
4450 m_pDS
->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
4451 m_pDS
->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
4452 // Tidy up temp tables
4453 m_pDS
->exec("DROP TABLE tmp_delartists");
4454 m_pDS
->exec("DROP TABLE tmp_keep");
4460 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
4465 bool CMusicDatabase::CleanupGenres()
4469 // Cleanup orphaned song genres (ie those that don't belong to a song entry)
4470 // (nested queries by Bobbin007)
4471 // Must be executed AFTER the song, and song_genre have been cleaned.
4472 std::string strSQL
= "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
4473 m_pDS
->exec(strSQL
);
4478 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
4483 bool CMusicDatabase::CleanupInfoSettings()
4487 // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
4488 // Must be executed AFTER the album and artist tables have been cleaned.
4489 std::string strSQL
= "DELETE FROM infosetting "
4490 "WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
4491 "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
4492 m_pDS
->exec(strSQL
);
4497 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
4502 bool CMusicDatabase::CleanupRoles()
4506 // Cleanup orphaned roles (ie those that don't belong to a song entry)
4507 // Must be executed AFTER the song, and song_artist tables have been cleaned.
4508 // Do not remove default role (ROLE_ARTIST)
4509 std::string strSQL
= "DELETE FROM role "
4510 "WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
4511 m_pDS
->exec(strSQL
);
4516 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
4521 bool CMusicDatabase::DeleteRemovedLinks()
4525 std::string strSQL
= "DELETE FROM removed_link";
4526 m_pDS
->exec(strSQL
);
4531 CLog::Log(LOGERROR
, "Exception in CMusicDatabase::DeleteRemovedLinks");
4536 bool CMusicDatabase::CleanupOrphanedItems()
4538 // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
4539 // remove_links not cleared here - done in CheckArtistLinksChanged()
4540 if (nullptr == m_pDB
)
4542 if (nullptr == m_pDS
)
4544 SetLibraryLastUpdated();
4545 if (!CleanupAlbums())
4547 if (!CleanupArtists())
4549 if (!CleanupGenres())
4551 if (!CleanupRoles())
4553 if (!CleanupInfoSettings())
4558 int CMusicDatabase::Cleanup(CGUIDialogProgress
* progressDialog
/*= nullptr*/)
4560 if (nullptr == m_pDB
)
4561 return ERROR_DATABASE
;
4562 if (nullptr == m_pDS
)
4563 return ERROR_DATABASE
;
4566 std::chrono::seconds duration
;
4567 auto time
= std::chrono::steady_clock::now();
4568 CLog::Log(LOGINFO
, "{}: Starting musicdatabase cleanup ..", __FUNCTION__
);
4569 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnCleanStarted");
4571 SetLibraryLastCleaned();
4573 // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link
4574 // Check that triggers actually exist first as interrupting the clean causes them to not be
4577 m_pDS
->exec("DROP TRIGGER IF EXISTS tgrDeleteSongArtist");
4578 m_pDS
->exec("DROP TRIGGER IF EXISTS tgrDeleteAlbumArtist");
4580 // first cleanup any songs with invalid paths
4583 progressDialog
->SetLine(1, CVariant
{318});
4584 progressDialog
->SetLine(2, CVariant
{330});
4585 progressDialog
->SetPercentage(0);
4586 progressDialog
->Progress();
4588 if (!CleanupSongs(progressDialog
))
4590 ret
= ERROR_REORG_SONGS
;
4593 // then the albums that are not linked to a song or to album, or whose path is removed
4596 progressDialog
->SetLine(1, CVariant
{326});
4597 progressDialog
->SetPercentage(20);
4598 progressDialog
->Progress();
4599 if (progressDialog
->IsCanceled())
4605 if (!CleanupAlbums())
4607 ret
= ERROR_REORG_ALBUM
;
4613 progressDialog
->SetLine(1, CVariant
{324});
4614 progressDialog
->SetPercentage(40);
4615 progressDialog
->Progress();
4616 if (progressDialog
->IsCanceled())
4622 if (!CleanupPaths())
4624 ret
= ERROR_REORG_PATH
;
4627 // and finally artists + genres
4630 progressDialog
->SetLine(1, CVariant
{320});
4631 progressDialog
->SetPercentage(60);
4632 progressDialog
->Progress();
4633 if (progressDialog
->IsCanceled())
4639 if (!CleanupArtists())
4641 ret
= ERROR_REORG_ARTIST
;
4644 //Genres, roles and info settings progress in one step
4647 progressDialog
->SetLine(1, CVariant
{322});
4648 progressDialog
->SetPercentage(80);
4649 progressDialog
->Progress();
4650 if (progressDialog
->IsCanceled())
4656 if (!CleanupGenres())
4658 ret
= ERROR_REORG_OTHER
;
4661 if (!CleanupRoles())
4663 ret
= ERROR_REORG_OTHER
;
4666 if (!CleanupInfoSettings())
4668 ret
= ERROR_REORG_OTHER
;
4671 if (!DeleteRemovedLinks())
4673 ret
= ERROR_REORG_OTHER
;
4677 // commit transaction
4680 progressDialog
->SetLine(1, CVariant
{328});
4681 progressDialog
->SetPercentage(90);
4682 progressDialog
->Progress();
4683 if (progressDialog
->IsCanceled())
4689 if (!CommitTransaction())
4691 ret
= ERROR_WRITING_CHANGES
;
4695 // Recreate DELETE triggers on song_artist and album_artist
4696 CreateRemovedLinkTriggers();
4698 // and compress the database
4701 progressDialog
->SetLine(1, CVariant
{331});
4702 progressDialog
->SetPercentage(100);
4703 progressDialog
->Close();
4707 std::chrono::duration_cast
<std::chrono::seconds
>(std::chrono::steady_clock::now() - time
);
4708 CLog::Log(LOGINFO
, "{}: Cleaning musicdatabase done. Operation took {}s", __FUNCTION__
,
4710 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnCleanFinished");
4712 if (!Compress(false))
4714 return ERROR_COMPRESSING
;
4719 RollbackTransaction();
4720 // Recreate DELETE triggers on song_artist and album_artist
4721 CreateRemovedLinkTriggers();
4722 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnCleanFinished");
4726 bool CMusicDatabase::TrimImageURLs(std::string
& strImage
, const size_t space
)
4728 if (strImage
.length() > space
)
4730 strImage
.resize(space
);
4731 // Tidy to last </thumb> tag
4732 size_t iPos
= strImage
.rfind("</thumb>");
4733 if (iPos
== std::string::npos
)
4735 strImage
.resize(iPos
+ 8);
4740 bool CMusicDatabase::LookupCDDBInfo(bool bRequery
/*=false*/)
4742 #ifdef HAS_OPTICAL_DRIVE
4743 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
4744 CSettings::SETTING_AUDIOCDS_USECDDB
))
4747 // check network connectivity
4748 if (!CServiceBroker::GetNetwork().IsAvailable())
4751 // Get information for the inserted disc
4752 CCdInfo
* pCdInfo
= CServiceBroker::GetMediaManager().GetCdInfo();
4753 if (pCdInfo
== NULL
)
4756 // If the disc has no tracks, we are finished here.
4757 int nTracks
= pCdInfo
->GetTrackCount();
4761 // Delete old info if any
4764 std::string strFile
= StringUtils::Format("{:x}.cddb", pCdInfo
->GetCddbDiscId());
4765 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager
.GetCDDBFolder(), strFile
));
4770 cddb
.setCacheDir(m_profileManager
.GetCDDBFolder());
4772 // Do we have to look for cddb information
4773 if (pCdInfo
->HasCDDBInfo() && !cddb
.isCDCached(pCdInfo
))
4775 CGUIDialogProgress
* pDialogProgress
=
4776 CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogProgress
>(
4777 WINDOW_DIALOG_PROGRESS
);
4778 CGUIDialogSelect
* pDlgSelect
=
4779 CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogSelect
>(
4780 WINDOW_DIALOG_SELECT
);
4782 if (!pDialogProgress
)
4787 // Show progress dialog if we have to connect to freedb.org
4788 pDialogProgress
->SetHeading(CVariant
{255}); //CDDB
4789 pDialogProgress
->SetLine(0, CVariant
{""}); // Querying freedb for CDDB info
4790 pDialogProgress
->SetLine(1, CVariant
{256});
4791 pDialogProgress
->SetLine(2, CVariant
{""});
4792 pDialogProgress
->ShowProgressBar(false);
4793 pDialogProgress
->Open();
4795 // get cddb information
4796 if (!cddb
.queryCDinfo(pCdInfo
))
4798 pDialogProgress
->Close();
4799 int lasterror
= cddb
.getLastError();
4801 // Have we found more then on match in cddb for this disc,...
4802 if (lasterror
== E_WAIT_FOR_INPUT
)
4804 // ...yes, show the matches found in a select dialog
4805 // and let the user choose an entry.
4806 pDlgSelect
->Reset();
4807 pDlgSelect
->SetHeading(CVariant
{255});
4811 std::string strTitle
= cddb
.getInexactTitle(i
);
4815 const std::string
& strArtist
= cddb
.getInexactArtist(i
);
4816 if (!strArtist
.empty())
4817 strTitle
+= " - " + strArtist
;
4819 pDlgSelect
->Add(strTitle
);
4824 // Has the user selected a match...
4825 int iSelectedCD
= pDlgSelect
->GetSelectedItem();
4826 if (iSelectedCD
>= 0)
4828 // ...query cddb for the inexact match
4829 if (!cddb
.queryCDinfo(pCdInfo
, 1 + iSelectedCD
))
4830 pCdInfo
->SetNoCDDBInfo();
4833 pCdInfo
->SetNoCDDBInfo();
4835 else if (lasterror
== E_NO_MATCH_FOUND
)
4837 pCdInfo
->SetNoCDDBInfo();
4841 pCdInfo
->SetNoCDDBInfo();
4842 // ..no, an error occurred, display it to the user
4843 std::string strErrorText
=
4844 StringUtils::Format("[{}] {}", cddb
.getLastError(), cddb
.getLastErrorText());
4845 HELPERS::ShowOKDialogLines(CVariant
{255}, CVariant
{257}, CVariant
{std::move(strErrorText
)},
4848 } // if ( !cddb.queryCDinfo( pCdInfo ) )
4850 pDialogProgress
->Close();
4853 // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
4855 return pCdInfo
->HasCDDBInfo();
4861 void CMusicDatabase::DeleteCDDBInfo()
4863 #ifdef HAS_OPTICAL_DRIVE
4864 CFileItemList items
;
4865 if (!CDirectory::GetDirectory(m_profileManager
.GetCDDBFolder(), items
, ".cddb",
4866 DIR_FLAG_NO_FILE_DIRS
))
4868 HELPERS::ShowOKDialogText(CVariant
{313}, CVariant
{426});
4871 // Show a selectdialog that the user can select the album to delete
4872 CGUIDialogSelect
* pDlg
= CServiceBroker::GetGUI()->GetWindowManager().GetWindow
<CGUIDialogSelect
>(
4873 WINDOW_DIALOG_SELECT
);
4876 pDlg
->SetHeading(CVariant
{g_localizeStrings
.Get(181)});
4879 std::map
<uint32_t, std::string
> mapCDDBIds
;
4880 for (int i
= 0; i
< items
.Size(); ++i
)
4882 if (items
[i
]->m_bIsFolder
)
4885 std::string strFile
= URIUtils::GetFileName(items
[i
]->GetPath());
4886 strFile
.erase(strFile
.size() - 5, 5);
4887 uint32_t lDiscId
= strtoul(strFile
.c_str(), NULL
, 16);
4889 cddb
.setCacheDir(m_profileManager
.GetCDDBFolder());
4891 if (!cddb
.queryCache(lDiscId
))
4894 std::string strDiskTitle
, strDiskArtist
;
4895 cddb
.getDiskTitle(strDiskTitle
);
4896 cddb
.getDiskArtist(strDiskArtist
);
4899 if (strDiskArtist
.empty())
4902 str
= strDiskTitle
+ " - " + strDiskArtist
;
4905 mapCDDBIds
.insert(std::pair
<uint32_t, std::string
>(lDiscId
, str
));
4911 // and wait till user selects one
4912 int iSelectedAlbum
= pDlg
->GetSelectedItem();
4913 if (iSelectedAlbum
< 0)
4915 mapCDDBIds
.erase(mapCDDBIds
.begin(), mapCDDBIds
.end());
4919 std::string strSelectedAlbum
= pDlg
->GetSelectedFileItem()->GetLabel();
4920 for (const auto& i
: mapCDDBIds
)
4922 if (i
.second
== strSelectedAlbum
)
4924 std::string strFile
= StringUtils::Format("{:x}.cddb", (unsigned int)i
.first
);
4925 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager
.GetCDDBFolder(), strFile
));
4929 mapCDDBIds
.erase(mapCDDBIds
.begin(), mapCDDBIds
.end());
4934 void CMusicDatabase::Clean()
4936 // If we are scanning for music info in the background,
4937 // other writing access to the database is prohibited.
4938 if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
4940 HELPERS::ShowOKDialogText(CVariant
{189}, CVariant
{14057});
4944 if (HELPERS::ShowYesNoDialogText(CVariant
{313}, CVariant
{333}) == DialogResponse::CHOICE_YES
)
4946 CMusicDatabase musicdatabase
;
4947 if (musicdatabase
.Open())
4949 int iReturnString
= musicdatabase
.Cleanup();
4950 musicdatabase
.Close();
4952 if (iReturnString
!= ERROR_OK
)
4954 HELPERS::ShowOKDialogText(CVariant
{313}, CVariant
{iReturnString
});
4960 bool CMusicDatabase::GetGenresNav(const std::string
& strBaseDir
,
4961 CFileItemList
& items
,
4962 const Filter
& filter
/* = Filter() */,
4963 bool countOnly
/* = false */)
4967 if (nullptr == m_pDB
)
4969 if (nullptr == m_pDS
)
4972 // get primary genres for songs - could be simplified to just SELECT * FROM genre?
4973 std::string strSQL
= "SELECT %s FROM genre ";
4975 Filter extFilter
= filter
;
4976 CMusicDbUrl musicUrl
;
4977 SortDescription sorting
;
4978 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
4981 // if there are extra WHERE conditions we might need access
4982 // to songview or albumview for these conditions
4983 if (!extFilter
.where
.empty())
4985 if (extFilter
.where
.find("artistview") != std::string::npos
)
4987 extFilter
.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4988 extFilter
.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4989 extFilter
.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
4990 extFilter
.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
4992 else if (extFilter
.where
.find("songview") != std::string::npos
)
4994 extFilter
.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4995 extFilter
.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4997 else if (extFilter
.where
.find("albumview") != std::string::npos
)
4999 extFilter
.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
5000 extFilter
.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
5001 extFilter
.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
5003 extFilter
.AppendGroup("genre.idGenre");
5005 extFilter
.AppendWhere("genre.strGenre != ''");
5009 extFilter
.fields
= "COUNT(DISTINCT genre.idGenre)";
5010 extFilter
.group
.clear();
5011 extFilter
.order
.clear();
5014 std::string strSQLExtra
;
5015 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5018 strSQL
= PrepareSQL(strSQL
, !extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0
5019 ? extFilter
.fields
.c_str()
5024 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5026 if (!m_pDS
->query(strSQL
))
5028 int iRowsFound
= m_pDS
->num_rows();
5029 if (iRowsFound
== 0)
5037 CFileItemPtr
pItem(new CFileItem());
5038 pItem
->SetProperty("total", iRowsFound
== 1 ? m_pDS
->fv(0).get_asInt() : iRowsFound
);
5045 // get data from returned rows
5046 while (!m_pDS
->eof())
5048 CFileItemPtr
pItem(new CFileItem(m_pDS
->fv("genre.strGenre").get_asString()));
5049 pItem
->GetMusicInfoTag()->SetGenre(m_pDS
->fv("genre.strGenre").get_asString());
5050 pItem
->GetMusicInfoTag()->SetDatabaseId(m_pDS
->fv("genre.idGenre").get_asInt(), "genre");
5052 CMusicDbUrl itemUrl
= musicUrl
;
5053 std::string strDir
= StringUtils::Format("{}/", m_pDS
->fv("genre.idGenre").get_asInt());
5054 itemUrl
.AppendPath(strDir
);
5055 pItem
->SetPath(itemUrl
.ToString());
5057 pItem
->m_bIsFolder
= true;
5070 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5075 bool CMusicDatabase::GetSourcesNav(const std::string
& strBaseDir
,
5076 CFileItemList
& items
,
5077 const Filter
& filter
/*= Filter()*/,
5078 bool countOnly
/*= false*/)
5082 if (nullptr == m_pDB
)
5084 if (nullptr == m_pDS
)
5087 // Get sources for selection list when add/edit filter or smartplaylist rule
5088 std::string strSQL
= "SELECT %s FROM source ";
5090 Filter extFilter
= filter
;
5091 CMusicDbUrl musicUrl
;
5092 SortDescription sorting
;
5093 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
5096 // if there are extra WHERE conditions we might need access
5097 // to songview or albumview for these conditions
5098 if (!extFilter
.where
.empty())
5100 if (extFilter
.where
.find("artistview") != std::string::npos
)
5102 extFilter
.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5103 extFilter
.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
5104 extFilter
.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
5106 else if (extFilter
.where
.find("songview") != std::string::npos
)
5108 extFilter
.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5109 extFilter
.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
5111 else if (extFilter
.where
.find("albumview") != std::string::npos
)
5113 extFilter
.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5114 extFilter
.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
5116 extFilter
.AppendGroup("source.idSource");
5119 { // Get only sources that have been scanned into music library
5120 extFilter
.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
5121 extFilter
.AppendGroup("source.idSource");
5126 extFilter
.fields
= "COUNT(DISTINCT source.idSource)";
5127 extFilter
.group
.clear();
5128 extFilter
.order
.clear();
5131 std::string strSQLExtra
;
5132 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5135 strSQL
= PrepareSQL(strSQL
, !extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0
5136 ? extFilter
.fields
.c_str()
5141 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5143 if (!m_pDS
->query(strSQL
))
5145 int iRowsFound
= m_pDS
->num_rows();
5146 if (iRowsFound
== 0)
5154 CFileItemPtr
pItem(new CFileItem());
5155 pItem
->SetProperty("total", iRowsFound
== 1 ? m_pDS
->fv(0).get_asInt() : iRowsFound
);
5162 // get data from returned rows
5163 while (!m_pDS
->eof())
5165 CFileItemPtr
pItem(new CFileItem(m_pDS
->fv("source.strName").get_asString()));
5166 pItem
->GetMusicInfoTag()->SetTitle(m_pDS
->fv("source.strName").get_asString());
5167 pItem
->GetMusicInfoTag()->SetDatabaseId(m_pDS
->fv("source.idSource").get_asInt(), "source");
5169 CMusicDbUrl itemUrl
= musicUrl
;
5170 std::string strDir
= StringUtils::Format("{}/", m_pDS
->fv("source.idSource").get_asInt());
5171 itemUrl
.AppendPath(strDir
);
5172 itemUrl
.AddOption("sourceid", m_pDS
->fv("source.idSource").get_asInt());
5173 pItem
->SetPath(itemUrl
.ToString());
5175 pItem
->m_bIsFolder
= true;
5188 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5193 bool CMusicDatabase::GetYearsNav(const std::string
& strBaseDir
,
5194 CFileItemList
& items
,
5195 const Filter
& filter
/* = Filter() */)
5199 if (nullptr == m_pDB
)
5201 if (nullptr == m_pDS
)
5204 Filter extFilter
= filter
;
5205 CMusicDbUrl musicUrl
;
5206 SortDescription sorting
;
5208 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
5211 bool useOriginalYears
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5212 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
);
5215 useOriginalYears
|| StringUtils::StartsWith(strBaseDir
, "musicdb://originalyears/");
5217 if (!useOriginalYears
)
5218 { // Get years from year part of release date
5219 strSQL
= "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
5220 extFilter
.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
5223 { // Get years from year part of original date
5224 strSQL
= "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
5225 extFilter
.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
5227 if (!BuildSQL(strSQL
, extFilter
, strSQL
))
5231 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5232 if (!m_pDS
->query(strSQL
))
5234 int iRowsFound
= m_pDS
->num_rows();
5235 if (iRowsFound
== 0)
5241 // get data from returned rows
5242 while (!m_pDS
->eof())
5244 CFileItemPtr
pItem(new CFileItem(m_pDS
->fv(0).get_asString()));
5245 pItem
->GetMusicInfoTag()->SetYear(m_pDS
->fv(0).get_asInt());
5246 if (useOriginalYears
)
5247 pItem
->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
5249 pItem
->GetMusicInfoTag()->SetDatabaseId(-1, "year");
5251 CMusicDbUrl itemUrl
= musicUrl
;
5252 std::string strDir
= StringUtils::Format("{}/", m_pDS
->fv(0).get_asInt());
5253 itemUrl
.AppendPath(strDir
);
5254 if (useOriginalYears
)
5255 itemUrl
.AddOption("useoriginalyear", true);
5256 pItem
->SetPath(itemUrl
.ToString());
5258 pItem
->m_bIsFolder
= true;
5271 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5276 bool CMusicDatabase::GetRolesNav(const std::string
& strBaseDir
,
5277 CFileItemList
& items
,
5278 const Filter
& filter
/* = Filter() */)
5282 if (nullptr == m_pDB
)
5284 if (nullptr == m_pDS
)
5287 Filter extFilter
= filter
;
5288 CMusicDbUrl musicUrl
;
5289 SortDescription sorting
;
5290 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
5293 // get roles with artists having that role
5294 std::string strSQL
= "SELECT DISTINCT role.idRole, role.strRole FROM role "
5295 "JOIN song_artist ON song_artist.idRole = role.idRole ";
5297 if (!BuildSQL(strSQL
, extFilter
, strSQL
))
5301 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5302 if (!m_pDS
->query(strSQL
))
5304 int iRowsFound
= m_pDS
->num_rows();
5305 if (iRowsFound
== 0)
5311 // get data from returned rows
5312 while (!m_pDS
->eof())
5314 std::string labelValue
= m_pDS
->fv("role.strRole").get_asString();
5315 CFileItemPtr
pItem(new CFileItem(labelValue
));
5316 pItem
->GetMusicInfoTag()->SetTitle(labelValue
);
5317 pItem
->GetMusicInfoTag()->SetDatabaseId(m_pDS
->fv("role.idRole").get_asInt(), "role");
5318 CMusicDbUrl itemUrl
= musicUrl
;
5319 std::string strDir
= StringUtils::Format("{}/", m_pDS
->fv("role.idRole").get_asInt());
5320 itemUrl
.AppendPath(strDir
);
5321 itemUrl
.AddOption("roleid", m_pDS
->fv("role.idRole").get_asInt());
5322 pItem
->SetPath(itemUrl
.ToString());
5324 pItem
->m_bIsFolder
= true;
5337 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5342 bool CMusicDatabase::GetAlbumsByYear(const std::string
& strBaseDir
, CFileItemList
& items
, int year
)
5344 CMusicDbUrl musicUrl
;
5345 if (!musicUrl
.FromString(strBaseDir
))
5348 musicUrl
.AddOption("year", year
);
5349 musicUrl
.AddOption("show_singles", true); // allow singles to be listed
5352 return GetAlbumsByWhere(musicUrl
.ToString(), filter
, items
);
5355 bool CMusicDatabase::GetCommonNav(const std::string
& strBaseDir
,
5356 const std::string
& table
,
5357 const std::string
& labelField
,
5358 CFileItemList
& items
,
5359 const Filter
& filter
/* = Filter() */,
5360 bool countOnly
/* = false */)
5362 if (nullptr == m_pDB
)
5364 if (nullptr == m_pDS
)
5367 if (table
.empty() || labelField
.empty())
5372 Filter extFilter
= filter
;
5373 std::string strSQL
= "SELECT %s FROM " + table
+ " ";
5374 extFilter
.AppendGroup(labelField
);
5375 extFilter
.AppendWhere(labelField
+ " != ''");
5379 extFilter
.fields
= "COUNT(DISTINCT " + labelField
+ ")";
5380 extFilter
.group
.clear();
5381 extFilter
.order
.clear();
5384 // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
5385 // e.g. LIKE '%symphony%' would be taken as a %s format argument
5386 strSQL
= PrepareSQL(strSQL
,
5387 !extFilter
.fields
.empty() ? extFilter
.fields
.c_str() : labelField
.c_str());
5389 CMusicDbUrl musicUrl
;
5390 if (!BuildSQL(strBaseDir
, strSQL
, extFilter
, strSQL
, musicUrl
))
5394 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5395 if (!m_pDS
->query(strSQL
))
5398 int iRowsFound
= m_pDS
->num_rows();
5399 if (iRowsFound
<= 0)
5407 CFileItemPtr
pItem(new CFileItem());
5408 pItem
->SetProperty("total", iRowsFound
== 1 ? m_pDS
->fv(0).get_asInt() : iRowsFound
);
5415 // get data from returned rows
5416 while (!m_pDS
->eof())
5418 std::string labelValue
= m_pDS
->fv(labelField
.c_str()).get_asString();
5419 CFileItemPtr
pItem(new CFileItem(labelValue
));
5421 CMusicDbUrl itemUrl
= musicUrl
;
5422 std::string strDir
= StringUtils::Format("{}/", labelValue
);
5423 itemUrl
.AppendPath(strDir
);
5424 pItem
->SetPath(itemUrl
.ToString());
5426 pItem
->m_bIsFolder
= true;
5440 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5446 bool CMusicDatabase::GetAlbumTypesNav(const std::string
& strBaseDir
,
5447 CFileItemList
& items
,
5448 const Filter
& filter
/* = Filter() */,
5449 bool countOnly
/* = false */)
5451 return GetCommonNav(strBaseDir
, "albumview", "albumview.strType", items
, filter
, countOnly
);
5454 bool CMusicDatabase::GetMusicLabelsNav(const std::string
& strBaseDir
,
5455 CFileItemList
& items
,
5456 const Filter
& filter
/* = Filter() */,
5457 bool countOnly
/* = false */)
5459 return GetCommonNav(strBaseDir
, "albumview", "albumview.strLabel", items
, filter
, countOnly
);
5462 bool CMusicDatabase::GetArtistsNav(const std::string
& strBaseDir
,
5463 CFileItemList
& items
,
5464 bool albumArtistsOnly
/* = false */,
5465 int idGenre
/* = -1 */,
5466 int idAlbum
/* = -1 */,
5467 int idSong
/* = -1 */,
5468 const Filter
& filter
/* = Filter() */,
5469 const SortDescription
& sortDescription
/* = SortDescription() */,
5470 bool countOnly
/* = false */)
5472 if (nullptr == m_pDB
)
5474 if (nullptr == m_pDS
)
5478 CMusicDbUrl musicUrl
;
5479 if (!musicUrl
.FromString(strBaseDir
))
5483 musicUrl
.AddOption("genreid", idGenre
);
5484 else if (idAlbum
> 0)
5485 musicUrl
.AddOption("albumid", idAlbum
);
5486 else if (idSong
> 0)
5487 musicUrl
.AddOption("songid", idSong
);
5489 // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
5490 // when local option already present in music URL thus allowing it to be an option in custom nodes
5491 if (!musicUrl
.HasOption("albumartistsonly"))
5492 musicUrl
.AddOption("albumartistsonly", albumArtistsOnly
);
5494 bool result
= GetArtistsByWhere(musicUrl
.ToString(), filter
, items
, sortDescription
, countOnly
);
5501 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5506 bool CMusicDatabase::GetArtistsByWhere(
5507 const std::string
& strBaseDir
,
5508 const Filter
& filter
,
5509 CFileItemList
& items
,
5510 const SortDescription
& sortDescription
/* = SortDescription() */,
5511 bool countOnly
/* = false */)
5513 if (nullptr == m_pDB
)
5515 if (nullptr == m_pDS
)
5520 auto start
= std::chrono::steady_clock::now();
5523 Filter extFilter
= filter
;
5524 CMusicDbUrl musicUrl
;
5525 SortDescription sorting
= sortDescription
;
5526 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
5529 bool extended
= false;
5530 bool limitedInSQL
= extFilter
.limit
.empty() && (sorting
.limitStart
> 0 || sorting
.limitEnd
> 0);
5532 // if there are extra WHERE conditions (from media filter dialog) we might
5533 // need access to songview or albumview for these conditions
5534 if (!extFilter
.where
.empty())
5536 if (extFilter
.where
.find("songview") != std::string::npos
)
5539 extFilter
.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist "
5540 "JOIN songview ON songview.idSong = song_artist.idSong");
5542 else if (extFilter
.where
.find("albumview") != std::string::npos
)
5545 extFilter
.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist "
5546 "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
5549 extFilter
.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
5552 std::string strSQLExtra
;
5553 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5556 // Count number of artsits that satisfy selection criteria (no limit built)
5557 // Count done in full query fetch when unlimited
5558 if (countOnly
|| limitedInSQL
)
5562 // Count distinct without group by
5563 Filter countFilter
= extFilter
;
5564 countFilter
.group
.clear();
5565 std::string strSQLWhere
;
5566 if (!BuildSQL(strSQLWhere
, countFilter
, strSQLWhere
))
5568 total
= GetSingleValueInt(
5569 "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere
, m_pDS
);
5572 total
= GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra
, m_pDS
);
5576 CFileItemPtr
pItem(new CFileItem());
5577 pItem
->SetProperty("total", total
);
5584 // Apply any limiting directly in SQL and so sort as well
5587 extFilter
.limit
= DatabaseUtils::BuildLimitClauseOnly(sorting
.limitEnd
, sorting
.limitStart
);
5590 // Apply sort in SQL
5591 const std::shared_ptr
<CSettings
> settings
=
5592 CServiceBroker::GetSettingsComponent()->GetSettings();
5593 if (settings
->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME
))
5594 sorting
.sortAttributes
=
5595 static_cast<SortAttribute
>(sorting
.sortAttributes
| SortAttributeUseArtistSortName
);
5596 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5597 GetOrderFilter(MediaTypeArtist
, sorting
, extFilter
);
5599 strSQLExtra
.clear();
5600 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5604 std::string strFields
= "artistview.*";
5605 if (!extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0)
5606 strFields
= "artistview.*, " + extFilter
.fields
;
5607 strSQL
= "SELECT " + strFields
+ " FROM artistview " + strSQLExtra
;
5610 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5611 auto queryStart
= std::chrono::steady_clock::now();
5612 if (!m_pDS
->query(strSQL
))
5614 int iRowsFound
= m_pDS
->num_rows();
5615 if (iRowsFound
== 0)
5621 auto queryEnd
= std::chrono::steady_clock::now();
5622 auto queryDuration
=
5623 std::chrono::duration_cast
<std::chrono::milliseconds
>(queryEnd
- queryStart
);
5625 // Store the total number of artists as a property
5626 if (total
< iRowsFound
)
5628 items
.SetProperty("total", total
);
5630 DatabaseResults results
;
5631 results
.reserve(iRowsFound
);
5632 // Populate results field vector from dataset
5634 if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist
, fields
, m_pDS
, results
))
5636 // Store item list sort order
5637 items
.SetSortMethod(sortDescription
.sortBy
);
5638 items
.SetSortOrder(sortDescription
.sortOrder
);
5640 // Get Artists from returned rows
5641 items
.Reserve(results
.size());
5642 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
5643 for (const auto& i
: results
)
5645 unsigned int targetRow
= (unsigned int)i
.at(FieldRow
).asInteger();
5646 const dbiplus::sql_record
* const record
= data
.at(targetRow
);
5650 CArtist artist
= GetArtistFromDataset(record
, false);
5651 CFileItemPtr
pItem(new CFileItem(artist
));
5653 CMusicDbUrl itemUrl
= musicUrl
;
5654 std::string path
= StringUtils::Format("{}/", artist
.idArtist
);
5655 itemUrl
.AppendPath(path
);
5656 pItem
->SetPath(itemUrl
.ToString());
5658 pItem
->GetMusicInfoTag()->SetDatabaseId(artist
.idArtist
, MediaTypeArtist
);
5659 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5660 pItem
->SetProperty("icon_never_overlay", true);
5661 pItem
->SetArt("icon", "DefaultArtist.png");
5663 SetPropertiesFromArtist(*pItem
, artist
);
5669 CLog::Log(LOGERROR
, "{} - out of memory getting listing (got {})", __FUNCTION__
,
5676 auto end
= std::chrono::steady_clock::now();
5677 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
5679 CLog::Log(LOGDEBUG
, "{0}: Time to fill list with artists {1} ms query took {2} ms",
5680 __FUNCTION__
, duration
.count(), queryDuration
.count());
5687 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5692 bool CMusicDatabase::GetAlbumFromSong(int idSong
, CAlbum
& album
)
5696 if (nullptr == m_pDB
)
5698 if (nullptr == m_pDS
)
5701 std::string strSQL
= PrepareSQL("SELECT albumview.* FROM song "
5702 "JOIN albumview on song.idAlbum = albumview.idAlbum "
5703 "WHERE song.idSong='%i'",
5705 if (!m_pDS
->query(strSQL
))
5707 int iRowsFound
= m_pDS
->num_rows();
5708 if (iRowsFound
!= 1)
5714 album
= GetAlbumFromDataset(m_pDS
.get());
5721 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
5726 bool CMusicDatabase::GetAlbumsNav(const std::string
& strBaseDir
,
5727 CFileItemList
& items
,
5728 int idGenre
/* = -1 */,
5729 int idArtist
/* = -1 */,
5730 const Filter
& filter
/* = Filter() */,
5731 const SortDescription
& sortDescription
/* = SortDescription() */,
5732 bool countOnly
/* = false */)
5734 CMusicDbUrl musicUrl
;
5735 if (!musicUrl
.FromString(strBaseDir
))
5740 musicUrl
.AddOption("genreid", idGenre
);
5743 musicUrl
.AddOption("artistid", idArtist
);
5745 return GetAlbumsByWhere(musicUrl
.ToString(), filter
, items
, sortDescription
, countOnly
);
5748 bool CMusicDatabase::GetAlbumsByWhere(
5749 const std::string
& baseDir
,
5750 const Filter
& filter
,
5751 CFileItemList
& items
,
5752 const SortDescription
& sortDescription
/* = SortDescription() */,
5753 bool countOnly
/* = false */)
5755 if (m_pDB
== nullptr || m_pDS
== nullptr)
5760 auto start
= std::chrono::steady_clock::now();
5763 Filter extFilter
= filter
;
5764 CMusicDbUrl musicUrl
;
5765 SortDescription sorting
= sortDescription
;
5766 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
5769 bool extended
= false;
5770 bool limitedInSQL
= extFilter
.limit
.empty() && (sorting
.limitStart
> 0 || sorting
.limitEnd
> 0);
5772 // If there are extra WHERE conditions (from media filter dialog) we might
5773 // need access to songview for these conditions
5774 if (extFilter
.where
.find("songview") != std::string::npos
)
5777 extFilter
.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
5778 extFilter
.AppendGroup("albumview.idAlbum");
5781 std::string strSQLExtra
;
5782 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5785 // Count number of albums that satisfy selection criteria (no limit built)
5786 // Count done in full query fetch when unlimited
5787 if (countOnly
|| limitedInSQL
)
5791 // Count distinct without group by
5792 Filter countFilter
= extFilter
;
5793 countFilter
.group
.clear();
5794 std::string strSQLWhere
;
5795 if (!BuildSQL(strSQLWhere
, countFilter
, strSQLWhere
))
5797 total
= GetSingleValueInt(
5798 "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere
, m_pDS
);
5801 total
= GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra
, m_pDS
);
5805 CFileItemPtr
pItem(new CFileItem());
5806 pItem
->SetProperty("total", total
);
5813 // Apply any limiting directly in SQL
5816 extFilter
.limit
= DatabaseUtils::BuildLimitClauseOnly(sorting
.limitEnd
, sorting
.limitStart
);
5819 // Apply sort in SQL
5820 const std::shared_ptr
<CSettings
> settings
=
5821 CServiceBroker::GetSettingsComponent()->GetSettings();
5822 if (settings
->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME
))
5823 sorting
.sortAttributes
=
5824 static_cast<SortAttribute
>(sorting
.sortAttributes
| SortAttributeUseArtistSortName
);
5825 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5826 GetOrderFilter(MediaTypeAlbum
, sorting
, extFilter
);
5827 // Modify order to use correct calculated year field
5828 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5829 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
))
5830 StringUtils::Replace(extFilter
.order
, "iYear", "CAST(strReleaseDate AS INTEGER)");
5832 StringUtils::Replace(extFilter
.order
, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5834 strSQLExtra
.clear();
5835 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5839 std::string strFields
= "albumview.*";
5840 if (!extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0)
5841 strFields
= "albumview.*, " + extFilter
.fields
;
5842 strSQL
= "SELECT " + strFields
+ " FROM albumview " + strSQLExtra
;
5845 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
5846 auto querytime
= std::chrono::steady_clock::now();
5847 if (!m_pDS
->query(strSQL
))
5849 int iRowsFound
= m_pDS
->num_rows();
5850 if (iRowsFound
== 0)
5856 auto queryEnd
= std::chrono::steady_clock::now();
5857 auto queryDuration
=
5858 std::chrono::duration_cast
<std::chrono::milliseconds
>(queryEnd
- querytime
);
5860 // Store the total number of albums as a property
5861 if (total
< iRowsFound
)
5863 items
.SetProperty("total", total
);
5865 DatabaseResults results
;
5866 results
.reserve(iRowsFound
);
5867 // Populate results field vector from dataset
5869 if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum
, fields
, m_pDS
, results
))
5871 // Store item list sort order
5872 items
.SetSortMethod(sorting
.sortBy
);
5873 items
.SetSortOrder(sorting
.sortOrder
);
5875 // Get albums from returned rows
5876 items
.Reserve(results
.size());
5877 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
5878 for (const auto& i
: results
)
5880 unsigned int targetRow
= (unsigned int)i
.at(FieldRow
).asInteger();
5881 const dbiplus::sql_record
* const record
= data
.at(targetRow
);
5885 CMusicDbUrl itemUrl
= musicUrl
;
5886 std::string path
= StringUtils::Format("{}/", record
->at(album_idAlbum
).get_asInt());
5887 itemUrl
.AppendPath(path
);
5889 CFileItemPtr
pItem(new CFileItem(itemUrl
.ToString(), GetAlbumFromDataset(record
)));
5890 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5891 pItem
->SetProperty("icon_never_overlay", true);
5892 pItem
->SetArt("icon", "DefaultAlbumCover.png");
5898 CLog::Log(LOGERROR
, "{} - out of memory getting listing (got {})", __FUNCTION__
,
5905 auto end
= std::chrono::steady_clock::now();
5906 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
5908 CLog::Log(LOGDEBUG
, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__
,
5909 duration
.count(), queryDuration
.count());
5916 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, filter
.where
);
5921 bool CMusicDatabase::GetDiscsNav(const std::string
& strBaseDir
,
5922 CFileItemList
& items
,
5924 const Filter
& filter
,
5925 const SortDescription
& sortDescription
,
5928 CMusicDbUrl musicUrl
;
5929 if (!musicUrl
.FromString(strBaseDir
))
5933 musicUrl
.AddOption("albumid", idAlbum
);
5935 return GetDiscsByWhere(musicUrl
, filter
, items
, sortDescription
, countOnly
);
5938 bool CMusicDatabase::GetDiscsByWhere(const std::string
& baseDir
,
5939 const Filter
& filter
,
5940 CFileItemList
& items
,
5941 const SortDescription
& sortDescription
,
5944 CMusicDbUrl musicUrl
;
5945 if (!musicUrl
.FromString(baseDir
))
5947 return GetDiscsByWhere(musicUrl
, filter
, items
, sortDescription
, countOnly
);
5950 bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl
& musicUrl
,
5951 const Filter
& filter
,
5952 CFileItemList
& items
,
5953 const SortDescription
& sortDescription
,
5956 if (m_pDB
== nullptr || m_pDS
== nullptr)
5961 auto start
= std::chrono::steady_clock::now();
5965 Filter extFilter
= filter
;
5966 SortDescription sorting
= sortDescription
;
5968 if (!GetFilter(musicUrl
, extFilter
, sorting
))
5971 extFilter
.AppendGroup("albumview.idAlbum, iDisc");
5973 // If there are extra songview WHERE conditions adjust to song or albumview
5974 // fields, and join Path table for strPath
5975 // ! @todo: convert songview fields into to song or albumview fields
5976 // But not sure we ever get songview fields in filter - REMOVE??
5977 if (extFilter
.where
.find("songview.strPath") != std::string::npos
)
5979 extFilter
.AppendJoin("JOIN path ON song.idPath = path.idPath");
5982 std::string strSQLExtra
;
5983 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
5986 // Apply any limiting directly in SQL if there is either no special sorting or random sort
5987 // When limited, random sort is also applied in SQL
5988 bool limitedInSQL
= extFilter
.limit
.empty() &&
5989 (sorting
.sortBy
== SortByNone
|| sorting
.sortBy
== SortByRandom
) &&
5990 (sorting
.limitStart
> 0 || sorting
.limitEnd
> 0);
5992 if (countOnly
|| limitedInSQL
)
5994 // Count number of discs that satisfy selection criteria
5995 // (when fetching all records get total from row count of results dataset)
5996 // Count not allow for same non-null title discs to be grouped together
5997 strSQL
= "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
5998 "albumview.idAlbum " +
6000 strSQL
= "SELECT COUNT(1) FROM (" + strSQL
+ ") AS albumdisc ";
6001 total
= GetSingleValueInt(strSQL
, m_pDS
);
6005 items
.SetProperty("total", total
);
6008 // Apply limits and random sort order directly in SQL
6011 if (sorting
.sortBy
== SortByRandom
)
6012 strSQLExtra
+= PrepareSQL(" ORDER BY RANDOM()");
6013 strSQLExtra
+= DatabaseUtils::BuildLimitClause(sorting
.limitEnd
, sorting
.limitStart
);
6016 strSQLExtra
+= PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
6018 strSQL
= "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
6019 "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " +
6023 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
6024 auto queryStart
= std::chrono::steady_clock::now();
6025 if (!m_pDS
->query(strSQL
))
6027 int iRowsFound
= m_pDS
->num_rows();
6028 if (iRowsFound
== 0)
6034 auto queryEnd
= std::chrono::steady_clock::now();
6035 auto queryDuration
=
6036 std::chrono::duration_cast
<std::chrono::milliseconds
>(queryEnd
- queryStart
);
6038 // store the total value of items as a property
6039 if (total
< iRowsFound
)
6041 items
.SetProperty("total", total
);
6043 DatabaseResults results
;
6044 results
.reserve(iRowsFound
);
6046 // Avoid sorting with limits, just fetch results from dataset
6047 // Limit when SortByNone already applied in SQL,
6048 // Need guaranteed ordering for dataset processing to group by disc title
6049 // so apply sort later to fileitems list rather than dataset
6050 sorting
.sortBy
= SortByNone
;
6051 if (!SortUtils::SortFromDataset(sorting
, MediaTypeAlbum
, m_pDS
, results
))
6054 // Get data from returned rows, note possibly multiple albums although usually only one
6055 items
.Reserve(total
);
6056 int albumOffset
= 2;
6058 bool useTitle
= true; // Assume we want to match by disc title later unless we have no titles
6059 std::string oldDiscTitle
;
6060 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
6061 for (const auto& i
: results
)
6063 unsigned int targetRow
= static_cast<unsigned int>(i
.at(FieldRow
).asInteger());
6064 const dbiplus::sql_record
* const record
= data
.at(targetRow
);
6067 if (album
.idAlbum
!= record
->at(albumOffset
+ album_idAlbum
).get_asInt())
6070 album
= GetAlbumFromDataset(record
, albumOffset
);
6073 int discnum
= record
->at(0).get_asInt();
6074 std::string strDiscSubtitle
= record
->at(1).get_asString();
6075 if (strDiscSubtitle
.empty())
6076 { // Make (fake) disc title from disc number, group by disc number as no real title to match
6077 strDiscSubtitle
= StringUtils::Format("{} {}", g_localizeStrings
.Get(427), discnum
);
6080 else if (oldDiscTitle
== strDiscSubtitle
)
6081 { // disc title already added to list, fetch the next disc
6084 oldDiscTitle
= strDiscSubtitle
;
6086 CMusicDbUrl itemUrl
= musicUrl
;
6087 std::string path
= StringUtils::Format("{}/", discnum
);
6088 itemUrl
.AppendPath(path
);
6090 // When disc titles are provided group discs together by title not number.
6091 // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
6093 itemUrl
.AddOption("disctitle", strDiscSubtitle
.c_str());
6095 itemUrl
.AddOption("discid", discnum
);
6096 CFileItemPtr
pItem(new CFileItem(itemUrl
.ToString(), album
));
6097 pItem
->SetLabel2(record
->at(0).get_asString()); // GUI show label2 for disc sort order??
6098 pItem
->GetMusicInfoTag()->SetDiscNumber(discnum
);
6099 pItem
->GetMusicInfoTag()->SetTitle(strDiscSubtitle
);
6100 pItem
->SetLabel(strDiscSubtitle
);
6101 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
6102 pItem
->SetProperty("icon_never_overlay", true);
6103 pItem
->SetArt("icon", "DefaultAlbumCover.png");
6109 CLog::Log(LOGERROR
, "{} - out of memory getting listing (got {})", __FUNCTION__
,
6117 // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
6118 // that is when have join with songartistview and sorting other than random with limit
6119 if (sorting
.sortBy
!= SortByNone
&& !(limitedInSQL
&& sorting
.sortBy
== SortByRandom
))
6120 items
.Sort(sorting
);
6122 auto end
= std::chrono::steady_clock::now();
6123 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
6125 CLog::Log(LOGDEBUG
, "{0}: Time to fill list with discs {1}ms query took {2}ms", __FUNCTION__
,
6126 duration
.count(), queryDuration
.count());
6133 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, filter
.where
);
6138 int CMusicDatabase::GetDiscsCount(const std::string
& baseDir
, const Filter
& filter
/* = Filter() */)
6140 int iDiscTotal
= -1;
6141 CFileItemList itemscount
;
6142 if (GetDiscsByWhere(baseDir
, filter
, itemscount
, SortDescription(), true))
6143 iDiscTotal
= itemscount
.GetProperty("total").asInteger32();
6147 bool CMusicDatabase::GetSongsFullByWhere(
6148 const std::string
& baseDir
,
6149 const Filter
& filter
,
6150 CFileItemList
& items
,
6151 const SortDescription
& sortDescription
/* = SortDescription() */,
6152 bool artistData
/* = false*/)
6154 if (m_pDB
== nullptr || m_pDS
== nullptr)
6159 auto start
= std::chrono::steady_clock::now();
6162 Filter extFilter
= filter
;
6163 CMusicDbUrl musicUrl
;
6164 SortDescription sorting
= sortDescription
;
6165 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
6168 bool extended
= false;
6170 extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0);
6172 // If there are extra WHERE conditions (from media filter dialog) we might
6173 // need access to albumview for these conditions
6174 if (extFilter
.where
.find("albumview") != std::string::npos
)
6177 extFilter
.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
6180 // Build songview <where> for count
6181 std::string strSQLExtra
;
6182 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
6185 // Count (without group by) number of songs that satisfy selection criteria
6186 // Much quicker to use song table, not songview, when filtering only on song fields
6188 (!extFilter
.where
.empty() && (extFilter
.where
.find("strAlbum") != std::string::npos
||
6189 extFilter
.where
.find("strPath") != std::string::npos
||
6190 extFilter
.where
.find("bCompilation") != std::string::npos
||
6191 extFilter
.where
.find("bBoxedset") != std::string::npos
)))
6192 total
= GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra
, m_pDS
);
6195 std::string strSQLsong
= strSQLExtra
;
6196 StringUtils::Replace(strSQLsong
, "songview", "song");
6197 total
= GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong
, m_pDS
);
6201 extFilter
.AppendGroup("songview.idSong");
6203 // Apply any limiting directly in SQL
6206 extFilter
.limit
= DatabaseUtils::BuildLimitClauseOnly(sorting
.limitEnd
, sorting
.limitStart
);
6209 // Apply sort in SQL
6210 const std::shared_ptr
<CSettings
> settings
=
6211 CServiceBroker::GetSettingsComponent()->GetSettings();
6212 if (settings
->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME
))
6213 sorting
.sortAttributes
=
6214 static_cast<SortAttribute
>(sorting
.sortAttributes
| SortAttributeUseArtistSortName
);
6215 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
6216 GetOrderFilter(MediaTypeSong
, sorting
, extFilter
);
6217 // Modify order to use correct calculated year field
6218 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
6219 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
))
6220 StringUtils::Replace(extFilter
.order
, "iYear", "CAST(strReleaseDate AS INTEGER)");
6222 StringUtils::Replace(extFilter
.order
, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
6224 std::string strFields
= "songview.*";
6225 if (!artistData
|| limitedInSQL
)
6227 // Build songview <where> + <order by> + <limits>
6228 strSQLExtra
.clear();
6229 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
6233 strFields
= "songview.*, songartistview.*";
6234 if (!extFilter
.fields
.empty() && extFilter
.fields
.compare("*") != 0)
6235 strFields
= strFields
+ ", " + extFilter
.fields
;
6239 { // Get data from song and song_artist tables to fully populate songs with artists
6240 // All songs now have at least one artist so inner join sufficient
6241 // Build songartistview JOIN part of query
6243 std::string strSQLJoin
;
6244 joinFilter
.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
6245 if (sortDescription
.sortBy
== SortByRandom
)
6246 joinFilter
.AppendOrder("songartistview.idSong");
6248 joinFilter
.order
= extFilter
.order
;
6251 StringUtils::Replace(joinFilter
.join
, "songview.idSong", "sv.idSong");
6252 StringUtils::Replace(joinFilter
.order
, "songview.", "sv.");
6255 joinFilter
.where
= extFilter
.where
;
6256 joinFilter
.AppendOrder("songartistview.idRole");
6257 joinFilter
.AppendOrder("songartistview.iOrder");
6258 if (!BuildSQL(strSQLJoin
, joinFilter
, strSQLJoin
))
6263 // When have artist data (all roles) and LIMIT on songs use inline view
6264 // SELECT sv.*, songartistview.* FROM
6265 // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
6266 // <order by sv fields>, songartistview.idRole, songartistview.iOrder
6267 // Apply where clause, limits and order to songview, then join to songartistview this gives
6268 // multiple records per song in result set
6269 strSQL
= "SELECT " + strFields
+ " FROM songview " + strSQLExtra
;
6270 strSQL
= "(" + strSQL
+ ") AS sv ";
6271 strSQL
= "SELECT sv.*, songartistview.* FROM " + strSQL
+ strSQLJoin
;
6274 strSQL
= "SELECT " + strFields
+ " FROM songview " + strSQLJoin
;
6277 strSQL
= "SELECT " + strFields
+ " FROM songview " + strSQLExtra
;
6279 CLog::Log(LOGDEBUG
, "{} query = {}", __FUNCTION__
, strSQL
);
6280 auto queryStart
= std::chrono::steady_clock::now();
6282 if (!m_pDS
->query(strSQL
))
6285 int iRowsFound
= m_pDS
->num_rows();
6286 if (iRowsFound
== 0)
6292 auto queryEnd
= std::chrono::steady_clock::now();
6293 auto queryDuration
=
6294 std::chrono::duration_cast
<std::chrono::milliseconds
>(queryEnd
- queryStart
);
6296 // Store the total number of songs as a property
6297 items
.SetProperty("total", total
);
6299 DatabaseResults results
;
6300 results
.reserve(iRowsFound
);
6301 // Populate results field vector from dataset
6303 if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong
, fields
, m_pDS
, results
))
6305 // Store item list sort order
6306 items
.SetSortMethod(sorting
.sortBy
);
6307 items
.SetSortOrder(sorting
.sortOrder
);
6309 // Get songs from returned rows. If join songartistview then there is a row for every artist
6310 items
.Reserve(total
);
6311 int songArtistOffset
= song_enumCount
;
6313 VECARTISTCREDITS artistCredits
;
6314 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
6316 for (const auto& i
: results
)
6318 unsigned int targetRow
= (unsigned int)i
.at(FieldRow
).asInteger();
6319 const dbiplus::sql_record
* const record
= data
.at(targetRow
);
6323 if (songId
!= record
->at(song_idSong
).get_asInt())
6325 if (songId
> 0 && !artistCredits
.empty())
6327 //Store artist credits for previous song
6328 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
6329 artistCredits
.clear();
6331 songId
= record
->at(song_idSong
).get_asInt();
6332 CFileItemPtr
item(new CFileItem
);
6333 GetFileItemFromDataset(record
, item
.get(), musicUrl
);
6334 // HACK for sorting by database returned order
6335 item
->m_iprogramCount
= ++count
;
6336 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
6337 item
->SetProperty("icon_never_overlay", true);
6338 item
->SetArt("icon", "DefaultAudio.png");
6341 // Get song artist credits and contributors
6344 int idSongArtistRole
= record
->at(songArtistOffset
+ artistCredit_idRole
).get_asInt();
6345 if (idSongArtistRole
== ROLE_ARTIST
)
6346 artistCredits
.push_back(GetArtistCreditFromDataset(record
, songArtistOffset
));
6348 items
[items
.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
6349 GetArtistRoleFromDataset(record
, songArtistOffset
));
6355 CLog::Log(LOGERROR
, "{}: out of memory loading query: {}", __FUNCTION__
, filter
.where
);
6356 return (items
.Size() > 0);
6359 if (!artistCredits
.empty())
6361 //Store artist credits for final song
6362 GetFileItemFromArtistCredits(artistCredits
, items
[items
.Size() - 1].get());
6363 artistCredits
.clear();
6368 // Ensure random order of item list when results set sorted by idSong for artist processing
6369 // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
6370 // navigation. Order is read later from view state and list sorting is then triggered by
6371 // CGUIMediaWindow::Update in both cases.
6372 // So sorting here is currently redundant, but the consistent place to do it.
6373 // !@ todo: do sorting once, preferably in SQL
6374 if (sorting
.sortBy
== SortByRandom
&& artistData
)
6375 items
.Sort(sorting
);
6377 auto end
= std::chrono::steady_clock::now();
6378 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
6380 CLog::Log(LOGDEBUG
, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__
,
6381 duration
.count(), queryDuration
.count());
6389 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
6394 bool CMusicDatabase::GetSongsByWhere(
6395 const std::string
& baseDir
,
6396 const Filter
& filter
,
6397 CFileItemList
& items
,
6398 const SortDescription
& sortDescription
/* = SortDescription() */)
6400 if (m_pDB
== nullptr || m_pDS
== nullptr)
6407 std::string strSQL
= "SELECT %s FROM songview ";
6409 Filter extFilter
= filter
;
6410 CMusicDbUrl musicUrl
;
6411 SortDescription sorting
= sortDescription
;
6412 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
6415 // if there are extra WHERE conditions we might need access
6416 // to songview for these conditions
6417 if (extFilter
.where
.find("albumview") != std::string::npos
)
6419 extFilter
.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
6420 extFilter
.AppendGroup("songview.idSong");
6423 std::string strSQLExtra
;
6424 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
6427 // Apply the limiting directly here if there's no special sorting but limiting
6428 if (extFilter
.limit
.empty() && sorting
.sortBy
== SortByNone
&&
6429 (sorting
.limitStart
> 0 || sorting
.limitEnd
> 0))
6431 total
= GetSingleValueInt(PrepareSQL(strSQL
, "COUNT(1)") + strSQLExtra
, m_pDS
);
6432 strSQLExtra
+= DatabaseUtils::BuildLimitClause(sorting
.limitEnd
, sorting
.limitStart
);
6435 strSQL
= PrepareSQL(strSQL
, !filter
.fields
.empty() && filter
.fields
.compare("*") != 0
6436 ? filter
.fields
.c_str()
6440 CLog::Log(LOGDEBUG
, "{} query = {}", __FUNCTION__
, strSQL
);
6442 if (!m_pDS
->query(strSQL
))
6445 int iRowsFound
= m_pDS
->num_rows();
6446 if (iRowsFound
== 0)
6452 // store the total value of items as a property
6453 if (total
< iRowsFound
)
6455 items
.SetProperty("total", total
);
6457 DatabaseResults results
;
6458 results
.reserve(iRowsFound
);
6459 if (!SortUtils::SortFromDataset(sorting
, MediaTypeSong
, m_pDS
, results
))
6462 // get data from returned rows
6463 items
.Reserve(results
.size());
6464 const dbiplus::query_data
& data
= m_pDS
->get_result_set().records
;
6466 for (const auto& i
: results
)
6468 unsigned int targetRow
= (unsigned int)i
.at(FieldRow
).asInteger();
6469 const dbiplus::sql_record
* const record
= data
.at(targetRow
);
6473 CFileItemPtr
item(new CFileItem
);
6474 GetFileItemFromDataset(record
, item
.get(), musicUrl
);
6475 // HACK for sorting by database returned order
6476 item
->m_iprogramCount
= ++count
;
6482 CLog::Log(LOGERROR
, "{}: out of memory loading query: {}", __FUNCTION__
, filter
.where
);
6483 return (items
.Size() > 0);
6495 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
6500 bool CMusicDatabase::GetSongsByYear(const std::string
& baseDir
, CFileItemList
& items
, int year
)
6502 CMusicDbUrl musicUrl
;
6503 if (!musicUrl
.FromString(baseDir
))
6506 musicUrl
.AddOption("year", year
);
6509 return GetSongsFullByWhere(baseDir
, filter
, items
, SortDescription(), true);
6512 bool CMusicDatabase::GetSongsNav(const std::string
& strBaseDir
,
6513 CFileItemList
& items
,
6517 const SortDescription
& sortDescription
/* = SortDescription() */)
6519 CMusicDbUrl musicUrl
;
6520 if (!musicUrl
.FromString(strBaseDir
))
6524 musicUrl
.AddOption("albumid", idAlbum
);
6527 musicUrl
.AddOption("genreid", idGenre
);
6530 musicUrl
.AddOption("artistid", idArtist
);
6533 return GetSongsFullByWhere(musicUrl
.ToString(), filter
, items
, sortDescription
, true);
6539 std::string fieldJSON
; // Field name in JSON schema
6540 std::string formatJSON
; // Format in JSON schema
6541 bool bSimple
; // Fetch field directly to JSON output
6542 std::string fieldDB
; // Name of field in db query
6543 std::string SQL
; // SQL for scalar subqueries or field alias
6544 } translateJSONField
;
6546 static const translateJSONField JSONtoDBArtist
[] = {
6547 // Table and single value join fields
6548 { "artist", "string", true, "strArtist", "" }, // Label field at top
6549 { "sortname", "string", true, "strSortname", "" },
6550 { "instrument", "array", true, "strInstruments", "" },
6551 { "description", "string", true, "strBiography", "" },
6552 { "genre", "array", true, "strGenres", "" },
6553 { "mood", "array", true, "strMoods", "" },
6554 { "style", "array", true, "strStyles", "" },
6555 { "yearsactive", "array", true, "strYearsActive", "" },
6556 { "born", "string", true, "strBorn", "" },
6557 { "formed", "string", true, "strFormed", "" },
6558 { "died", "string", true, "strDied", "" },
6559 { "disbanded", "string", true, "strDisbanded", "" },
6560 { "type", "string", true, "strType", "" },
6561 { "gender", "string", true, "strGender", "" },
6562 { "disambiguation", "string", true, "strDisambiguation", "" },
6563 { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
6564 { "dateadded", "string", true, "dateAdded", "" },
6565 { "datenew", "string", true, "dateNew", "" },
6566 { "datemodified", "string", true, "dateModified", "" },
6568 // JOIN fields (multivalue), same order as _JoinToArtistFields
6569 { "", "", false, "isSong", "" },
6570 { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" },
6571 { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" },
6572 { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" },
6573 { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" },
6574 { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" },
6575 { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" },
6576 { "art", "", false, "idArt", "art.art_id AS idArt" },
6577 { "", "", false, "artType", "art.type AS artType" },
6578 { "", "", false, "artURL", "art.url AS artURL" },
6579 { "", "", false, "idRole", "song_artist.idRole" },
6580 { "roles", "", false, "strRole", "role.strRole" },
6581 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
6582 // Derived from joined tables
6583 { "isalbumartist", "bool", false, "", "" },
6584 { "thumbnail", "string", false, "", "" },
6585 { "fanart", "string", false, "", "" }
6587 Sources and genre are related via album, and so the dataset only contains source and genre
6588 pairs that exist, rather than all the genres being repeated for every source. We can not only
6589 look at genres for the first source, and genre can be out of order.
6594 static const size_t NUM_ARTIST_FIELDS
= sizeof(JSONtoDBArtist
) / sizeof(translateJSONField
);
6596 bool CMusicDatabase::GetArtistsByWhereJSON(
6597 const std::set
<std::string
>& fields
,
6598 const std::string
& baseDir
,
6601 const SortDescription
& sortDescription
/* = SortDescription() */)
6603 if (nullptr == m_pDB
)
6605 if (nullptr == m_pDS
)
6612 size_t resultcount
= 0;
6614 CMusicDbUrl musicUrl
;
6615 SortDescription sorting
= sortDescription
;
6616 //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
6617 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
6620 // Replace view names in filter with table names
6621 StringUtils::Replace(extFilter
.where
, "artistview", "artist");
6622 StringUtils::Replace(extFilter
.where
, "albumview", "album");
6624 std::string strSQLExtra
;
6625 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
6628 // Count number of artists that satisfy selection criteria
6629 //(includes xsp limits from filter, but not sort limits)
6630 total
= GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra
, m_pDS
);
6631 resultcount
= static_cast<size_t>(total
);
6633 // Process albumartistsonly option
6634 const CUrlOptions::UrlOptions
& options
= musicUrl
.GetOptions();
6635 bool albumArtistsOnly(false);
6636 auto option
= options
.find("albumartistsonly");
6637 if (option
!= options
.end())
6638 albumArtistsOnly
= option
->second
.asBoolean();
6639 // Process role options
6640 int roleidfilter
= 1; // Default restrict song_artist to "artists" only, no other roles.
6641 option
= options
.find("roleid");
6642 if (option
!= options
.end())
6643 roleidfilter
= static_cast<int>(option
->second
.asInteger());
6646 option
= options
.find("role");
6647 if (option
!= options
.end())
6649 if (option
->second
.asString() == "all" || option
->second
.asString() == "%")
6650 roleidfilter
= -1000; //All roles
6652 roleidfilter
= GetRoleByName(option
->second
.asString());
6656 // Get order by (and any scalar query artist fields)
6657 int iAddedFields
= GetOrderFilter(MediaTypeArtist
, sortDescription
, extFilter
);
6658 // Replace artistview field names in order by artist table field names
6659 StringUtils::Replace(extFilter
.order
, "artistview", "artist");
6660 StringUtils::Replace(extFilter
.fields
, "artistview", "artist");
6662 // Grab and adjust artist sort field that may have been added to filter
6663 // These need to be added to the end of the artist table field list
6664 std::string artistsortSQL
= extFilter
.fields
;
6665 extFilter
.fields
.clear();
6669 // Setup fields to query, and album field number mapping
6670 // Find first join field (isSong) in JSONtoDBArtist for offset
6671 int index_firstjoin
= -1;
6672 for (unsigned int i
= 0; i
< NUM_ARTIST_FIELDS
; i
++)
6674 if (JSONtoDBArtist
[i
].fieldDB
== "isSong")
6676 index_firstjoin
= i
;
6681 Filter albumArtistFilter
;
6682 Filter songArtistFilter
;
6683 DatasetLayout
joinLayout(static_cast<size_t>(joinToArtist_enumCount
));
6684 extFilter
.AppendField("artist.idArtist"); // ID "artistid" in JSON
6685 std::vector
<int> dbfieldindex
;
6686 // JSON "label" field is strArtist which is also output as "artist", query field once output twice
6687 extFilter
.AppendField(JSONtoDBArtist
[0].fieldDB
);
6688 dbfieldindex
.emplace_back(0); // Output "artist"
6690 // Check each optional artist db field that could be retrieved (not "artist")
6691 for (unsigned int i
= 1; i
< NUM_ARTIST_FIELDS
; i
++)
6693 bool foundJSON
= fields
.find(JSONtoDBArtist
[i
].fieldJSON
) != fields
.end();
6694 if (JSONtoDBArtist
[i
].bSimple
)
6696 // Check for non-join fields in order too.
6697 // Query these in inline view (but not output) so can ref in outer order
6698 bool foundOrderby(false);
6700 foundOrderby
= extFilter
.order
.find(JSONtoDBArtist
[i
].fieldDB
) != std::string::npos
;
6701 if (foundOrderby
|| foundJSON
)
6703 // Store indexes of requested artist table and scalar subquery fields
6704 // to be output, and -1 when not output to JSON
6706 dbfieldindex
.emplace_back(-1);
6708 dbfieldindex
.emplace_back(i
);
6709 // Field from scaler subquery
6710 if (!JSONtoDBArtist
[i
].SQL
.empty())
6711 extFilter
.AppendField(PrepareSQL(JSONtoDBArtist
[i
].SQL
));
6713 // Field from artist table
6714 extFilter
.AppendField(JSONtoDBArtist
[i
].fieldDB
);
6718 // Field from join or derived from joined fields
6719 joinLayout
.SetField(i
- index_firstjoin
, JSONtoDBArtist
[i
].fieldDB
, true);
6722 // Append calculated artistsort field that may have been added to filter
6723 // Field used only for ORDER BY, not output to JSON
6724 extFilter
.AppendField(artistsortSQL
);
6725 for (int i
= 0; i
< iAddedFields
; i
++)
6726 dbfieldindex
.emplace_back(-2); // columns in dataset
6728 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6730 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
6733 // Add any LIMIT clause to strSQLExtra
6734 if (extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0))
6737 DatabaseUtils::BuildLimitClause(sortDescription
.limitEnd
, sortDescription
.limitStart
);
6738 resultcount
= std::min(
6739 DatabaseUtils::GetLimitCount(sortDescription
.limitEnd
, sortDescription
.limitStart
),
6743 // Setup multivalue JOINs, GROUP BY and ORDER BY
6744 bool bJoinAlbumArtist(false);
6745 bool bJoinSongArtist(false);
6746 if (sortDescription
.sortBy
!= SortByRandom
)
6748 // Repeat inline view order (that always includes idArtist) on join query
6749 std::string order
= extFilter
.order
;
6750 StringUtils::Replace(order
, "artist.", "a1.");
6751 joinFilter
.AppendOrder(order
);
6754 joinFilter
.AppendOrder("a1.idArtist");
6755 joinFilter
.AppendGroup("a1.idArtist");
6756 // Album artists and song artists
6757 if ((joinLayout
.GetFetch(joinToArtist_isalbumartist
) && !albumArtistsOnly
) ||
6758 joinLayout
.GetFetch(joinToArtist_idSourceAlbum
) ||
6759 joinLayout
.GetFetch(joinToArtist_idSongGenreAlbum
) ||
6760 joinLayout
.GetFetch(joinToArtist_strRole
))
6762 bJoinAlbumArtist
= true;
6763 albumArtistFilter
.AppendField("album_artist.idArtist AS id");
6764 if (!albumArtistsOnly
|| joinLayout
.GetFetch(joinToArtist_strRole
))
6766 bJoinSongArtist
= true;
6767 songArtistFilter
.AppendField("song_artist.idArtist AS id");
6768 songArtistFilter
.AppendField("1 AS isSong");
6769 albumArtistFilter
.AppendField("0 AS isSong");
6770 joinLayout
.SetField(joinToArtist_isSong
,
6771 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_isSong
].fieldDB
);
6772 joinFilter
.AppendGroup(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_isSong
].fieldDB
);
6773 joinFilter
.AppendOrder(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_isSong
].fieldDB
);
6776 else if (joinLayout
.GetFetch(joinToArtist_isalbumartist
))
6778 // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
6779 // so no need for join to album_artist table. Set fetching fetch false so that
6780 // joinLayout.HasFilterFields() is false
6781 joinLayout
.SetFetch(joinToArtist_isalbumartist
, false);
6785 if (joinLayout
.GetFetch(joinToArtist_idSourceAlbum
))
6786 { // Left join as source may have been removed but leaving lib entries
6787 albumArtistFilter
.AppendJoin(
6788 "LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
6789 albumArtistFilter
.AppendField(
6790 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceAlbum
].SQL
);
6791 joinFilter
.AppendGroup(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceAlbum
].fieldDB
);
6792 joinFilter
.AppendOrder(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceAlbum
].fieldDB
);
6793 if (bJoinSongArtist
)
6795 songArtistFilter
.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
6796 songArtistFilter
.AppendJoin(
6797 "LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
6798 songArtistFilter
.AppendField(
6799 "-1 AS " + JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceAlbum
].fieldDB
);
6800 songArtistFilter
.AppendField(
6801 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceSong
].SQL
);
6802 albumArtistFilter
.AppendField(
6803 "-1 AS " + JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceSong
].fieldDB
);
6804 joinLayout
.SetField(joinToArtist_idSourceSong
,
6805 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceSong
].fieldDB
);
6806 joinFilter
.AppendGroup(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceSong
].fieldDB
);
6807 joinFilter
.AppendOrder(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceSong
].fieldDB
);
6811 joinLayout
.SetField(joinToArtist_idSourceAlbum
,
6812 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSourceAlbum
].SQL
, true);
6816 // Songgenres - id and genres always both
6817 if (joinLayout
.GetFetch(joinToArtist_idSongGenreAlbum
))
6818 { // All albums have songs, but left join genre as songs may not have genre
6819 albumArtistFilter
.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
6820 albumArtistFilter
.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
6821 albumArtistFilter
.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6822 albumArtistFilter
.AppendField(
6823 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreAlbum
].SQL
);
6824 albumArtistFilter
.AppendField(
6825 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreAlbum
].SQL
);
6826 joinLayout
.SetField(joinToArtist_strSongGenreAlbum
,
6827 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreAlbum
].fieldDB
);
6828 joinFilter
.AppendGroup(
6829 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreAlbum
].fieldDB
);
6830 joinFilter
.AppendOrder(
6831 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreAlbum
].fieldDB
);
6832 if (bJoinSongArtist
)
6833 { // Left join genre as songs may not have genre
6834 songArtistFilter
.AppendJoin(
6835 "LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
6836 songArtistFilter
.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6837 songArtistFilter
.AppendField(
6838 "-1 AS " + JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreAlbum
].fieldDB
);
6839 songArtistFilter
.AppendField(
6840 "'' AS " + JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreAlbum
].fieldDB
);
6841 songArtistFilter
.AppendField(
6842 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreSong
].SQL
);
6843 songArtistFilter
.AppendField(
6844 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreSong
].SQL
);
6845 albumArtistFilter
.AppendField(
6846 "-1 AS " + JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreSong
].fieldDB
);
6847 albumArtistFilter
.AppendField(
6848 "'' AS " + JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreSong
].fieldDB
);
6849 joinLayout
.SetField(joinToArtist_idSongGenreSong
,
6850 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreSong
].fieldDB
);
6851 joinLayout
.SetField(
6852 joinToArtist_strSongGenreSong
,
6853 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreSong
].fieldDB
);
6854 joinFilter
.AppendGroup(
6855 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreSong
].fieldDB
);
6856 joinFilter
.AppendOrder(
6857 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreSong
].fieldDB
);
6860 { // Define field alias names in join layout
6861 joinLayout
.SetField(joinToArtist_idSongGenreAlbum
,
6862 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idSongGenreAlbum
].SQL
,
6864 joinLayout
.SetField(joinToArtist_strSongGenreAlbum
,
6865 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strSongGenreAlbum
].SQL
);
6870 if (roleidfilter
== 1 && !joinLayout
.GetFetch(joinToArtist_strRole
))
6871 // Only looking at album and song artists not other roles (default),
6872 // so filter dataset rows likewise.
6873 songArtistFilter
.AppendWhere("song_artist.idRole = 1");
6874 else if (joinLayout
.GetFetch(joinToArtist_strRole
) || // "roles" field
6875 (bJoinSongArtist
&& (joinLayout
.GetFetch(joinToArtist_idSourceAlbum
) ||
6876 joinLayout
.GetFetch(joinToArtist_idSongGenreAlbum
))))
6877 { // Rows from many roles so fetch roleid for "roles", source and genre processing
6878 songArtistFilter
.AppendField(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idRole
].SQL
);
6879 // Add fake column to album_artist query
6880 albumArtistFilter
.AppendField("-1 AS " +
6881 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idRole
].fieldDB
);
6882 joinLayout
.SetField(joinToArtist_idRole
,
6883 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idRole
].fieldDB
);
6884 joinFilter
.AppendGroup(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idRole
].fieldDB
);
6885 joinFilter
.AppendOrder(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idRole
].fieldDB
);
6887 if (joinLayout
.GetFetch(joinToArtist_strRole
))
6888 { // Fetch role desc
6889 songArtistFilter
.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
6890 songArtistFilter
.AppendField(JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strRole
].SQL
);
6891 // Add fake column to album_artist query
6892 albumArtistFilter
.AppendField("'albumartist' AS " +
6893 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_strRole
].fieldDB
);
6896 // Build source, genre and roles part of query
6897 if (bJoinAlbumArtist
)
6899 if (bJoinSongArtist
)
6901 // Combine song and album artist filter as UNION and add to join filter as an inline view
6902 std::string strAlbumSQL
;
6903 if (!BuildSQL(strAlbumSQL
, albumArtistFilter
, strAlbumSQL
))
6905 strAlbumSQL
= "SELECT " + albumArtistFilter
.fields
+ " FROM album_artist " + strAlbumSQL
;
6906 std::string strSongSQL
;
6907 if (!BuildSQL(strSongSQL
, songArtistFilter
, strSongSQL
))
6909 strSongSQL
= "SELECT " + songArtistFilter
.fields
+ " FROM song_artist " + strSongSQL
;
6911 joinFilter
.AppendJoin("JOIN (" + strAlbumSQL
+ " UNION " + strSongSQL
+
6912 ") AS albumSong ON id = a1.idArtist");
6915 { //Only join album_artist, so move filter elements to join filter
6916 joinFilter
.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
6917 joinFilter
.AppendJoin(albumArtistFilter
.join
);
6922 bool bJoinArt(false);
6923 bJoinArt
= joinLayout
.GetOutput(joinToArtist_idArt
) ||
6924 joinLayout
.GetOutput(joinToArtist_thumbnail
) ||
6925 joinLayout
.GetOutput(joinToArtist_fanart
);
6927 { // Left join as artist may not have any art
6928 joinFilter
.AppendJoin(
6929 "LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
6930 joinLayout
.SetField(joinToArtist_idArt
,
6931 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_idArt
].SQL
,
6932 joinLayout
.GetOutput(joinToArtist_idArt
));
6933 joinLayout
.SetField(joinToArtist_artType
,
6934 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_artType
].SQL
);
6935 joinLayout
.SetField(joinToArtist_artURL
,
6936 JSONtoDBArtist
[index_firstjoin
+ joinToArtist_artURL
].SQL
);
6937 joinFilter
.AppendGroup("art.art_id");
6938 joinFilter
.AppendOrder("arttype");
6939 if (!joinLayout
.GetOutput(joinToArtist_idArt
))
6941 if (!joinLayout
.GetOutput(joinToArtist_thumbnail
))
6943 joinFilter
.AppendJoin("AND art.type = 'fanart'");
6944 else if (!joinLayout
.GetOutput(joinToArtist_fanart
))
6946 joinFilter
.AppendJoin("AND art.type = 'thumb'");
6949 else if (bJoinSongArtist
)
6950 joinFilter
.group
.clear(); // UNION only so no GROUP BY needed
6952 // Build JOIN part of query (if we have one)
6953 std::string strSQLJoin
;
6954 if (joinLayout
.HasFilterFields())
6955 if (!BuildSQL(strSQLJoin
, joinFilter
, strSQLJoin
))
6958 // Adjust where in the results record the join fields are allowing for the
6959 // inline view fields (Quicker than finding field by name every time)
6960 // idArtist + other artist fields
6961 joinLayout
.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex
.size()));
6964 // When have multiple value joins e.g. song genres, use inline view
6965 // SELECT a1.*, <join fields> FROM
6966 // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1
6967 // <joins> <group by> <order by> + <joins order by>
6968 // Don't use prepareSQL - confuses arttype = 'thumb' filter
6970 strSQL
= "SELECT " + extFilter
.fields
+ " FROM artist " + strSQLExtra
;
6971 if (joinLayout
.HasFilterFields())
6973 strSQL
= "(" + strSQL
+ ") AS a1 ";
6974 strSQL
= "SELECT a1.*, " + joinLayout
.GetFields() + " FROM " + strSQL
+ strSQLJoin
;
6977 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
6979 auto start
= std::chrono::steady_clock::now();
6981 if (!m_pDS
->query(strSQL
))
6984 auto end
= std::chrono::steady_clock::now();
6985 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
6987 CLog::Log(LOGDEBUG
, "{} - query took {} ms", __FUNCTION__
, duration
.count());
6989 int iRowsFound
= m_pDS
->num_rows();
6990 if (iRowsFound
<= 0)
6996 // Get artists from returned rows. Joins means there can be many rows per artist
7002 std::vector
<int> genreidlist
;
7003 std::vector
<int> sourceidlist
;
7004 std::vector
<int> roleidlist
;
7005 bool bArtDone(false);
7006 bool bHaveArtist(false);
7007 bool bIsAlbumArtist(true);
7008 bool bGenreFoundViaAlbum(false);
7010 result
["artists"].reserve(resultcount
);
7011 while (!m_pDS
->eof() || bHaveArtist
)
7013 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
7015 if (m_pDS
->eof() || artistId
!= record
->at(0).get_asInt())
7017 // Store previous or last artist
7020 // Convert any empty MBid array into an array with one empty element [""]
7021 // to match the number of artist ID (way other mbid arrays handled)
7022 if (artistObj
.isMember("musicbrainzartistid") && artistObj
["musicbrainzartistid"].empty())
7023 artistObj
["musicbrainzartistid"].append("");
7025 result
["artists"].append(artistObj
);
7026 bHaveArtist
= false;
7029 if (artistObj
.empty())
7031 // Initialise fields, ensure those with possible null values are set to correct empty variant type
7032 if (joinLayout
.GetOutput(joinToArtist_idSourceAlbum
))
7033 artistObj
["sourceid"] = CVariant(CVariant::VariantTypeArray
);
7034 if (joinLayout
.GetOutput(joinToArtist_idSongGenreAlbum
))
7035 artistObj
["songgenres"] = CVariant(CVariant::VariantTypeArray
);
7036 if (joinLayout
.GetOutput(joinToArtist_idArt
))
7037 artistObj
["art"] = CVariant(CVariant::VariantTypeObject
);
7038 if (joinLayout
.GetOutput(joinToArtist_thumbnail
))
7039 artistObj
["thumbnail"] = "";
7040 if (joinLayout
.GetOutput(joinToArtist_fanart
))
7041 artistObj
["fanart"] = "";
7047 genreidlist
.clear();
7048 bGenreFoundViaAlbum
= false;
7049 sourceidlist
.clear();
7054 continue; // Having saved the last artist stop
7057 artistId
= record
->at(0).get_asInt();
7059 artistObj
["artistid"] = artistId
;
7060 artistObj
["label"] = record
->at(1).get_asString();
7061 artistObj
["artist"] = record
->at(1).get_asString(); // Always have "artist"
7062 bIsAlbumArtist
= true; //Album artist by default
7063 if (joinLayout
.GetOutput(joinToArtist_isalbumartist
))
7065 // Not album artist when fetching song artists too and first row for artist isSong=true
7066 if (bJoinSongArtist
)
7067 bIsAlbumArtist
= !record
->at(joinLayout
.GetRecNo(joinToArtist_isSong
)).get_asBool();
7068 artistObj
["isalbumartist"] = bIsAlbumArtist
;
7070 for (size_t i
= 0; i
< dbfieldindex
.size(); i
++)
7071 if (dbfieldindex
[i
] > -1)
7073 if (JSONtoDBArtist
[dbfieldindex
[i
]].formatJSON
== "integer")
7074 artistObj
[JSONtoDBArtist
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asInt();
7075 else if (JSONtoDBArtist
[dbfieldindex
[i
]].formatJSON
== "float")
7076 artistObj
[JSONtoDBArtist
[dbfieldindex
[i
]].fieldJSON
] =
7077 record
->at(1 + i
).get_asFloat();
7078 else if (JSONtoDBArtist
[dbfieldindex
[i
]].formatJSON
== "array")
7079 artistObj
[JSONtoDBArtist
[dbfieldindex
[i
]].fieldJSON
] = StringUtils::Split(
7080 record
->at(1 + i
).get_asString(), CServiceBroker::GetSettingsComponent()
7081 ->GetAdvancedSettings()
7082 ->m_musicItemSeparator
);
7083 else if (JSONtoDBArtist
[dbfieldindex
[i
]].formatJSON
== "boolean")
7084 artistObj
[JSONtoDBArtist
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asBool();
7086 artistObj
[JSONtoDBArtist
[dbfieldindex
[i
]].fieldJSON
] =
7087 record
->at(1 + i
).get_asString();
7090 if (bJoinAlbumArtist
)
7092 bool bAlbumArtistRow(true);
7094 if (bJoinSongArtist
)
7096 bAlbumArtistRow
= !record
->at(joinLayout
.GetRecNo(joinToArtist_isSong
)).get_asBool();
7097 if (joinLayout
.GetRecNo(joinToArtist_idRole
) > -1 &&
7098 !record
->at(joinLayout
.GetRecNo(joinToArtist_idRole
)).get_isNull())
7100 idRoleRow
= record
->at(joinLayout
.GetRecNo(joinToArtist_idRole
)).get_asInt();
7104 // Sources - gathered via both album_artist and song_artist (with role = 1)
7105 if (joinLayout
.GetFetch(joinToArtist_idSourceAlbum
))
7107 if ((bAlbumArtistRow
&& joinLayout
.GetRecNo(joinToArtist_idSourceAlbum
) > -1 &&
7108 !record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceAlbum
)).get_isNull() &&
7110 record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceAlbum
)).get_asInt()) ||
7111 (!bAlbumArtistRow
&& joinLayout
.GetRecNo(joinToArtist_idSourceSong
) > -1 &&
7112 !record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceSong
)).get_isNull() &&
7113 sourceId
!= record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceSong
)).get_asInt()))
7115 bArtDone
= bArtDone
|| (sourceId
> 0); // Not first source, skip art repeats
7117 sourceId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceAlbum
)).get_asInt();
7118 if (!bAlbumArtistRow
)
7120 // Skip other roles (when fetching them)
7127 sourceId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idSourceSong
)).get_asInt();
7128 // Song artist row may repeat sources found via album artist
7129 // Already have that source?
7130 for (const auto& i
: sourceidlist
)
7140 sourceidlist
.emplace_back(sourceId
);
7141 artistObj
["sourceid"].append(sourceId
);
7145 // Songgenres - via album artist takes precedence
7147 Sources and genre are related via album, and so the dataset only contains source
7148 and genre pairs that exist, rather than all the genres being repeated for every
7149 source. We can not only look at genres for the first source, and genre can be
7151 Also song artist row may repeat genres found via album artist
7153 if (joinLayout
.GetFetch(joinToArtist_idSongGenreAlbum
))
7155 std::string strGenre
;
7156 bool newgenre(false);
7157 if (bAlbumArtistRow
&& joinLayout
.GetRecNo(joinToArtist_idSongGenreAlbum
) > -1 &&
7158 !record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreAlbum
)).get_isNull() &&
7159 genreId
!= record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreAlbum
)).get_asInt())
7161 bArtDone
= bArtDone
|| (genreId
> 0); // Not first genre, skip art repeats
7163 genreId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreAlbum
)).get_asInt();
7165 record
->at(joinLayout
.GetRecNo(joinToArtist_strSongGenreAlbum
)).get_asString();
7167 else if (!bAlbumArtistRow
&& !bGenreFoundViaAlbum
&&
7168 joinLayout
.GetRecNo(joinToArtist_idSongGenreSong
) > -1 &&
7169 !record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreSong
)).get_isNull() &&
7171 record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreSong
)).get_asInt())
7173 bArtDone
= bArtDone
|| (genreId
> 0); // Not first genre, skip art repeats
7174 newgenre
= idRoleRow
<= 1; // Skip other roles (when fetching them)
7175 genreId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idSongGenreSong
)).get_asInt();
7177 record
->at(joinLayout
.GetRecNo(joinToArtist_strSongGenreSong
)).get_asString();
7181 // Already have that genre?
7183 for (const auto& i
: genreidlist
)
7191 bGenreFoundViaAlbum
= bGenreFoundViaAlbum
|| bAlbumArtistRow
;
7192 genreidlist
.emplace_back(genreId
);
7194 genreObj
["genreid"] = genreId
;
7195 genreObj
["title"] = strGenre
;
7196 artistObj
["songgenres"].append(genreObj
);
7200 // Roles - gathered via song_artist roleid rows
7201 if (joinLayout
.GetFetch(joinToArtist_idRole
))
7203 if (!bAlbumArtistRow
&& roleId
!= idRoleRow
)
7205 bArtDone
= bArtDone
|| (roleId
> 0); // Not first role, skip art repeats
7207 if (joinLayout
.GetOutput(joinToArtist_strRole
))
7209 // Already have that role?
7211 for (const auto& i
: roleidlist
)
7219 roleidlist
.emplace_back(roleId
);
7221 roleObj
["roleid"] = roleId
;
7223 record
->at(joinLayout
.GetRecNo(joinToArtist_strRole
)).get_asString();
7224 artistObj
["roles"].append(roleObj
);
7231 if (bJoinArt
&& !bArtDone
&&
7232 !record
->at(joinLayout
.GetRecNo(joinToArtist_idArt
)).get_isNull() &&
7233 record
->at(joinLayout
.GetRecNo(joinToArtist_idArt
)).get_asInt() > 0 &&
7234 artId
!= record
->at(joinLayout
.GetRecNo(joinToArtist_idArt
)).get_asInt())
7236 artId
= record
->at(joinLayout
.GetRecNo(joinToArtist_idArt
)).get_asInt();
7237 if (joinLayout
.GetOutput(joinToArtist_idArt
))
7239 artistObj
["art"][record
->at(joinLayout
.GetRecNo(joinToArtist_artType
)).get_asString()] =
7240 CTextureUtils::GetWrappedImageURL(
7241 record
->at(joinLayout
.GetRecNo(joinToArtist_artURL
)).get_asString());
7243 if (joinLayout
.GetOutput(joinToArtist_thumbnail
) &&
7244 record
->at(joinLayout
.GetRecNo(joinToArtist_artType
)).get_asString() == "thumb")
7246 artistObj
["thumbnail"] = CTextureUtils::GetWrappedImageURL(
7247 record
->at(joinLayout
.GetRecNo(joinToArtist_artURL
)).get_asString());
7249 if (joinLayout
.GetOutput(joinToArtist_fanart
) &&
7250 record
->at(joinLayout
.GetRecNo(joinToArtist_artType
)).get_asString() == "fanart")
7252 artistObj
["fanart"] = CTextureUtils::GetWrappedImageURL(
7253 record
->at(joinLayout
.GetRecNo(joinToArtist_artURL
)).get_asString());
7259 m_pDS
->close(); // cleanup recordset data
7261 // Ensure random order of output when results set is sorted to process multi-value joins
7262 if (sortDescription
.sortBy
== SortByRandom
&& joinLayout
.HasFilterFields())
7263 KODI::UTILS::RandomShuffle(result
["artists"].begin_array(), result
["artists"].end_array());
7270 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
7276 static const translateJSONField JSONtoDBAlbum
[] = {
7277 // albumview (inc scalar subquery fields use in filter rules)
7278 { "title", "string", true, "strAlbum", "" }, // Label field at top
7279 { "description", "string", true, "strReview", "" },
7280 { "genre", "array", true, "strGenres", "" },
7281 { "theme", "array", true, "strThemes", "" },
7282 { "mood", "array", true, "strMoods", "" },
7283 { "style", "array", true, "strStyles", "" },
7284 { "type", "string", true, "strType", "" },
7285 { "albumlabel", "string", true, "strLabel", "" },
7286 { "rating", "float", true, "fRating", "" },
7287 { "votes", "integer", true, "iVotes", "" },
7288 { "userrating", "unsigned", true, "iUserrating", "" },
7289 { "isboxset", "boolean", true, "bBoxedSet", "" },
7290 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" },
7291 { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table
7292 { "compilation", "boolean", true, "bCompilation", "" },
7293 { "releasetype", "string", true, "strReleaseType", "" },
7294 { "totaldiscs", "integer", true, "iDiscTotal", "" },
7295 { "sortartist", "string", true, "strArtistSort", "" },
7296 { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" },
7297 { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view
7298 { "dateadded", "string", true, "dateAdded", "" },
7299 { "datenew", "string", true, "dateNew", "" },
7300 { "datemodified", "string", true, "dateModified", "" },
7301 { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view
7302 { "originaldate", "string", true, "strOrigReleaseDate", "" },
7303 { "releasedate", "string", true, "strReleaseDate", "" },
7304 { "albumstatus", "string", true, "strReleaseStatus", "" },
7305 { "albumduration", "integer", true, "iAlbumDuration", "" },
7306 // Scalar subquery fields
7307 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7308 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
7309 { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
7310 "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
7311 // Single value JOIN fields
7312 { "thumbnail", "image", true, "thumbnail", "art.url AS thumbnail" }, // or (SELECT art.url FROM art WHERE art.media_id = album.idAlbum AND art.media_type = "album" AND art.type = "thumb") as url
7313 // JOIN fields (multivalue), same order as _JoinToAlbumFields
7314 { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" },
7315 { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" },
7316 { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" },
7318 Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
7319 and separate queries to allow for fallback strategy.
7321 Using albmview, rather than album table, as view has scalar subqueries for
7322 playcount and lastplayed already defined. Needed as MySQL does
7323 not support use of scalar subquery field alias names in where clauses (they
7324 have to be repeated) and these fields can be used by filter rules.
7325 Using this view is no slower than the album table as these scalar fields are
7326 only calculated (slowing query) when field is in field list.
7331 static const size_t NUM_ALBUM_FIELDS
= sizeof(JSONtoDBAlbum
) / sizeof(translateJSONField
);
7333 bool CMusicDatabase::GetAlbumsByWhereJSON(
7334 const std::set
<std::string
>& fields
,
7335 const std::string
& baseDir
,
7338 const SortDescription
& sortDescription
/* = SortDescription() */)
7341 if (nullptr == m_pDB
)
7343 if (nullptr == m_pDS
)
7350 size_t resultcount
= 0;
7352 CMusicDbUrl musicUrl
;
7353 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7354 // passed in at the start of the function
7355 SortDescription sorting
= sortDescription
;
7356 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
7359 // Replace view names in filter with table names
7360 StringUtils::Replace(extFilter
.where
, "artistview", "artist");
7362 std::string strSQLExtra
;
7363 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
7366 // Count number of albums that satisfy selection criteria
7367 // (includes xsp limits from filter, but not sort limits)
7368 // Use albumview as filter rules in where clause may use scalar query fields
7369 total
= GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra
, m_pDS
);
7370 resultcount
= static_cast<size_t>(total
);
7372 // Get order by (and any scalar query artist fields
7373 int iAddedFields
= GetOrderFilter(MediaTypeAlbum
, sortDescription
, extFilter
);
7375 // Grab calculated artist/title sort fields that may have been added to filter
7376 // These need to be added to the end of the album table field list
7377 std::string calcsortfieldsSQL
= extFilter
.fields
;
7378 extFilter
.fields
.clear();
7382 // Setup fields to query, and album field number mapping
7383 // Find idArtist in JSONtoDBAlbum, offset of first join field
7384 int index_idArtist
= -1;
7385 for (unsigned int i
= 0; i
< NUM_ALBUM_FIELDS
; i
++)
7387 if (JSONtoDBAlbum
[i
].fieldDB
== "idArtist")
7394 DatasetLayout
joinLayout(static_cast<size_t>(joinToAlbum_enumCount
));
7395 extFilter
.AppendField("albumview.idAlbum"); // ID "albumid" in JSON
7396 std::vector
<int> dbfieldindex
;
7397 // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
7398 extFilter
.AppendField(JSONtoDBAlbum
[0].fieldDB
);
7399 if (fields
.find(JSONtoDBAlbum
[0].fieldJSON
) != fields
.end())
7400 dbfieldindex
.emplace_back(0); // Output "title"
7402 dbfieldindex
.emplace_back(-1); // fetch but not output
7404 // Check each optional album db field that could be retrieved (not label)
7405 for (unsigned int i
= 1; i
< NUM_ALBUM_FIELDS
; i
++)
7407 bool foundJSON
= fields
.find(JSONtoDBAlbum
[i
].fieldJSON
) != fields
.end();
7408 if (JSONtoDBAlbum
[i
].bSimple
)
7410 // Check for non-join fields in order too.
7411 // Query these in inline view (but not output) so can ref in outer order
7412 bool foundOrderby(false);
7414 foundOrderby
= extFilter
.order
.find(JSONtoDBAlbum
[i
].fieldDB
) != std::string::npos
;
7415 if (foundOrderby
|| foundJSON
)
7417 // Store indexes of requested album table and scalar subquery fields
7418 // to be output, and -1 when not output to JSON
7420 dbfieldindex
.emplace_back(-1);
7422 dbfieldindex
.emplace_back(i
);
7423 if (!JSONtoDBAlbum
[i
].SQL
.empty())
7424 // Field from scaler subquery
7425 extFilter
.AppendField(PrepareSQL(JSONtoDBAlbum
[i
].SQL
));
7427 // Field from album table
7428 extFilter
.AppendField(JSONtoDBAlbum
[i
].fieldDB
);
7432 // Field from join found in JSON request
7433 joinLayout
.SetField(i
- index_idArtist
, JSONtoDBAlbum
[i
].SQL
, true);
7436 // Append calculated artist/title sort fields that may have been added to filter
7437 // Field used only for ORDER BY, not output to JSON
7438 extFilter
.AppendField(calcsortfieldsSQL
);
7439 for (int i
= 0; i
< iAddedFields
; i
++)
7440 dbfieldindex
.emplace_back(-1); // columns in dataset
7442 // JOIN art tables if needed (fields output and/or in sort)
7443 if (extFilter
.fields
.find("art.") != std::string::npos
)
7444 { // Left join as not all albums have art, but only have one thumb at most
7445 extFilter
.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
7446 "AND art.media_type = 'album' AND art.type = 'thumb'");
7449 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7451 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
7454 // Add any LIMIT clause to strSQLExtra
7455 if (extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0))
7458 DatabaseUtils::BuildLimitClause(sortDescription
.limitEnd
, sortDescription
.limitStart
);
7459 resultcount
= std::min(
7460 DatabaseUtils::GetLimitCount(sortDescription
.limitEnd
, sortDescription
.limitStart
),
7464 // Setup multivalue JOINs, GROUP BY and ORDER BY
7465 bool bJoinAlbumArtist(false);
7466 if (sortDescription
.sortBy
!= SortByRandom
)
7468 // Repeat inline view order (that always includes idAlbum) on join query
7469 std::string order
= extFilter
.order
;
7470 StringUtils::Replace(order
, "albumview.", "a1.");
7471 joinFilter
.AppendOrder(order
);
7474 joinFilter
.AppendOrder("a1.idAlbum");
7475 joinFilter
.AppendGroup("a1.idAlbum");
7477 if (joinLayout
.GetFetch(joinToAlbum_idArtist
) || joinLayout
.GetFetch(joinToAlbum_strArtist
) ||
7478 joinLayout
.GetFetch(joinToAlbum_strArtistMBID
))
7479 { // All albums have at least one artist so inner join sufficient
7480 bJoinAlbumArtist
= true;
7481 joinFilter
.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
7482 joinFilter
.AppendGroup("album_artist.idArtist");
7483 joinFilter
.AppendOrder("album_artist.iOrder");
7484 // Ensure idArtist is queried
7485 if (!joinLayout
.GetFetch(joinToAlbum_idArtist
))
7486 joinLayout
.SetField(joinToAlbum_idArtist
,
7487 JSONtoDBAlbum
[index_idArtist
+ joinToAlbum_idArtist
].SQL
);
7489 // artist table needed for strArtist or MBID
7490 // (album_artist.strArtist can be an alias or spelling variation)
7491 if (joinLayout
.GetFetch(joinToAlbum_strArtist
) ||
7492 joinLayout
.GetFetch(joinToAlbum_strArtistMBID
))
7493 joinFilter
.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
7495 // Build JOIN part of query (if we have one)
7496 std::string strSQLJoin
;
7497 if (joinLayout
.HasFilterFields())
7498 if (!BuildSQL(strSQLJoin
, joinFilter
, strSQLJoin
))
7501 // Adjust where in the results record the join fields are allowing for the
7502 // inline view fields (Quicker than finding field by name every time)
7503 // idAlbum + other album fields
7504 joinLayout
.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex
.size()));
7507 // When have multiple value joins (artists or song genres) use inline view
7508 // SELECT a1.*, <join fields> FROM
7509 // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1
7510 // <joins> <group by> <order by> <joins order by>
7511 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
7513 strSQL
= "SELECT " + extFilter
.fields
+ " FROM albumview " + strSQLExtra
;
7514 if (joinLayout
.HasFilterFields())
7516 strSQL
= "(" + strSQL
+ ") AS a1 ";
7517 strSQL
= "SELECT a1.*, " + joinLayout
.GetFields() + " FROM " + strSQL
+ strSQLJoin
;
7520 // Modify query to use correct year field
7521 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
7522 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
))
7523 StringUtils::Replace(strSQL
, "<datefield>", "strReleaseDate");
7525 StringUtils::Replace(strSQL
, "<datefield>", "strOrigReleaseDate");
7527 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
7529 auto start
= std::chrono::steady_clock::now();
7531 if (!m_pDS
->query(strSQL
))
7534 auto end
= std::chrono::steady_clock::now();
7535 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
7537 CLog::Log(LOGDEBUG
, "{} - query took {} ms", __FUNCTION__
, duration
.count());
7539 int iRowsFound
= m_pDS
->num_rows();
7540 if (iRowsFound
<= 0)
7546 // Get albums from returned rows. Joins means there can be many rows per album
7550 result
["albums"].reserve(resultcount
);
7551 while (!m_pDS
->eof() || !albumObj
.empty())
7553 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
7555 if (m_pDS
->eof() || albumId
!= record
->at(0).get_asInt())
7557 // Store previous or last album
7558 if (!albumObj
.empty())
7560 // Split sources string into int array
7561 if (albumObj
.isMember("sourceid"))
7563 std::vector
<std::string
> sources
=
7564 StringUtils::Split(albumObj
["sourceid"].asString(), ";");
7565 albumObj
["sourceid"] = CVariant(CVariant::VariantTypeArray
);
7566 for (size_t i
= 0; i
< sources
.size(); i
++)
7567 albumObj
["sourceid"].append(atoi(sources
[i
].c_str()));
7569 result
["albums"].append(albumObj
);
7574 continue; // Having saved last album stop
7577 albumId
= record
->at(0).get_asInt();
7578 albumObj
["albumid"] = albumId
;
7579 albumObj
["label"] = record
->at(1).get_asString();
7580 for (size_t i
= 0; i
< dbfieldindex
.size(); i
++)
7581 if (dbfieldindex
[i
] > -1)
7583 if (JSONtoDBAlbum
[dbfieldindex
[i
]].fieldDB
== "songgenres")
7585 // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
7586 std::vector
<std::string
> values
=
7587 StringUtils::Split(record
->at(1 + i
).get_asString(), ",");
7588 if (values
.size() % 2 == 0) // Must contain an even number of entries
7590 for (size_t j
= 0; j
+ 1 < values
.size(); j
+= 2)
7592 int idGenre
= atoi(values
[j
].c_str());
7596 genreObj
["genreid"] = idGenre
;
7597 genreObj
["title"] = values
[j
+ 1];
7598 albumObj
["songgenres"].append(genreObj
);
7602 // Ensure albums with null songgenres get empty array
7603 if (!albumObj
.isMember("songgenres"))
7604 albumObj
["songgenres"] = CVariant(CVariant::VariantTypeArray
);
7606 else if (JSONtoDBAlbum
[dbfieldindex
[i
]].formatJSON
== "integer")
7607 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asInt();
7608 else if (JSONtoDBAlbum
[dbfieldindex
[i
]].formatJSON
== "unsigned")
7609 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] =
7610 std::max(record
->at(1 + i
).get_asInt(), 0);
7611 else if (JSONtoDBAlbum
[dbfieldindex
[i
]].formatJSON
== "float")
7612 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] =
7613 std::max(record
->at(1 + i
).get_asFloat(), 0.f
);
7614 else if (JSONtoDBAlbum
[dbfieldindex
[i
]].formatJSON
== "array")
7615 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] = StringUtils::Split(
7616 record
->at(1 + i
).get_asString(), CServiceBroker::GetSettingsComponent()
7617 ->GetAdvancedSettings()
7618 ->m_musicItemSeparator
);
7619 else if (JSONtoDBAlbum
[dbfieldindex
[i
]].formatJSON
== "boolean")
7620 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asBool();
7621 else if (JSONtoDBAlbum
[dbfieldindex
[i
]].formatJSON
== "image")
7623 std::string url
= record
->at(1 + i
).get_asString();
7625 url
= CTextureUtils::GetWrappedImageURL(url
);
7626 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] = url
;
7629 albumObj
[JSONtoDBAlbum
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asString();
7632 if (bJoinAlbumArtist
&& joinLayout
.GetRecNo(joinToAlbum_idArtist
) > -1)
7634 if (artistId
!= record
->at(joinLayout
.GetRecNo(joinToAlbum_idArtist
)).get_asInt())
7636 artistId
= record
->at(joinLayout
.GetRecNo(joinToAlbum_idArtist
)).get_asInt();
7637 if (joinLayout
.GetOutput(joinToAlbum_idArtist
))
7638 albumObj
["artistid"].append(artistId
);
7639 if (artistId
== BLANKARTIST_ID
)
7641 if (joinLayout
.GetOutput(joinToAlbum_strArtist
))
7642 albumObj
["artist"].append(StringUtils::Empty
);
7643 if (joinLayout
.GetOutput(joinToAlbum_strArtistMBID
))
7644 albumObj
["musicbrainzalbumartistid"].append(StringUtils::Empty
);
7648 if (joinLayout
.GetOutput(joinToAlbum_strArtist
) &&
7649 joinLayout
.GetRecNo(joinToAlbum_strArtist
) > -1)
7650 albumObj
["artist"].append(
7651 record
->at(joinLayout
.GetRecNo(joinToAlbum_strArtist
)).get_asString());
7652 if (joinLayout
.GetOutput(joinToAlbum_strArtistMBID
) &&
7653 joinLayout
.GetRecNo(joinToAlbum_strArtistMBID
) > -1)
7654 albumObj
["musicbrainzalbumartistid"].append(
7655 record
->at(joinLayout
.GetRecNo(joinToAlbum_strArtistMBID
)).get_asString());
7661 m_pDS
->close(); // cleanup recordset data
7663 // Ensure random order of output when results set is sorted to process multi-value joins
7664 if (sortDescription
.sortBy
== SortByRandom
&& joinLayout
.HasFilterFields())
7665 KODI::UTILS::RandomShuffle(result
["albums"].begin_array(), result
["albums"].end_array());
7672 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
7678 static const translateJSONField JSONtoDBSong
[] = {
7679 // table and single value join fields
7680 { "title", "string", true, "strTitle", "" }, // Label field at top
7681 { "albumid", "integer", true, "song.idAlbum", "" },
7682 { "", "", true, "song.iTrack", "" },
7683 { "displayartist", "string", true, "song.strArtistDisp", "" },
7684 { "sortartist", "string", true, "song.strArtistSort", "" },
7685 { "genre", "array", true, "song.strGenres", "" },
7686 { "duration", "integer", true, "iDuration", "" },
7687 { "comment", "string", true, "comment", "" },
7688 { "", "string", true, "strFileName", "" },
7689 { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" },
7690 { "playcount", "integer", true, "iTimesPlayed", "" },
7691 { "lastplayed", "string", true, "lastPlayed", "" },
7692 { "rating", "float", true, "rating", "" },
7693 { "votes", "integer", true, "votes", "" },
7694 { "userrating", "unsigned", true, "song.userrating", "" },
7695 { "mood", "array", true, "mood", "" },
7696 { "dateadded", "string", true, "song.dateAdded", "" },
7697 { "datenew", "string", true, "song.dateNew", "" },
7698 { "datemodified", "string", true, "song.dateModified", "" },
7699 { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" },
7700 { "", "string", true, "strPath", "path.strPath AS strPath" },
7701 { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" },
7702 { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" },
7703 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
7704 { "disctitle", "string", true, "song.strDiscSubtitle", "" },
7705 { "bpm", "integer", true, "iBPM", "" },
7706 { "originaldate", "string" , true, "song.strOrigReleaseDate","" },
7707 { "releasedate", "string" , true, "song.strReleaseDate", "" },
7708 { "bitrate", "integer", true, "iBitRate", "" },
7709 { "samplerate", "integer", true, "iSampleRate", "" },
7710 { "channels", "integer", true, "iChannels", "" },
7711 { "songvideourl", "string", true, "strVideoURL", "" },
7713 // JOIN fields (multivalue), same order as _JoinToSongFields
7714 { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" },
7715 { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" },
7716 { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
7717 { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" },
7718 { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" },
7719 { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" },
7720 { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" },
7721 { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" },
7722 { "", "", false, "idRole", "song_artist.idRole" },
7723 { "", "", false, "strRole", "role.strRole" },
7724 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
7725 { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
7726 { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" },
7728 { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" },
7729 { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" },
7730 { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" },
7731 { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" },
7732 { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" },
7734 // Scalar subquery fields
7735 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7736 { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" },
7737 { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" },
7738 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
7740 Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
7741 thumbloader and separate queries to allow for fallback strategy
7742 "lyrics"?? Can be set for an item (by addons) but not held in db so
7743 AudioLibrary.GetSongs() never fills this field despite being in schema
7745 FROM ( SELECT * FROM song
7746 JOIN album ON album.idAlbum = song.idAlbum
7747 JOIN path ON path.idPath = song.idPath) AS sv
7748 JOIN album_artist ON album_artist.idAlbum = song.idAlbum
7749 JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
7750 JOIN song_artist ON song_artist.idSong = song.idSong
7751 JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist
7752 JOIN role ON song_artist.idRole = role.idRole
7753 LEFT JOIN song_genre ON song.idSong = song_genre.idSong
7759 static const size_t NUM_SONG_FIELDS
= sizeof(JSONtoDBSong
) / sizeof(translateJSONField
);
7761 bool CMusicDatabase::GetSongsByWhereJSON(
7762 const std::set
<std::string
>& fields
,
7763 const std::string
& baseDir
,
7766 const SortDescription
& sortDescription
/* = SortDescription() */)
7769 if (nullptr == m_pDB
)
7771 if (nullptr == m_pDS
)
7778 size_t resultcount
= 0;
7780 CMusicDbUrl musicUrl
;
7781 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7782 // passed into the function
7783 SortDescription sorting
= sortDescription
;
7784 if (!musicUrl
.FromString(baseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
7787 // Replace view names in filter with table names
7788 StringUtils::Replace(extFilter
.where
, "artistview", "artist");
7789 StringUtils::Replace(extFilter
.where
, "albumview", "album");
7790 StringUtils::Replace(extFilter
.where
, "songview.strPath", "strPath");
7791 StringUtils::Replace(extFilter
.where
, "songview.strAlbum", "strAlbum");
7792 StringUtils::Replace(extFilter
.where
, "songview", "song");
7793 StringUtils::Replace(extFilter
.where
, "songartistview", "song_artist");
7795 // JOIN album and path tables needed by filter rules in where clause
7796 if (extFilter
.where
.find("album.") != std::string::npos
||
7797 extFilter
.where
.find("strAlbum") != std::string::npos
)
7798 { // All songs have one album so inner join sufficient
7799 extFilter
.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7801 if (extFilter
.where
.find("strPath") != std::string::npos
)
7802 { // All songs have one path so inner join sufficient
7803 extFilter
.AppendJoin("JOIN path ON path.idPath = song.idPath");
7806 // Build JOINs and WHERE needed by filter for counting songs
7807 std::string strSQLExtra
;
7808 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
7811 // Count number of songs that satisfy selection criteria
7812 // (includes xsp limits from filter, but not sort limits)
7813 total
= GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra
, m_pDS
);
7814 resultcount
= static_cast<size_t>(total
);
7816 int iAddedFields
= GetOrderFilter(MediaTypeSong
, sortDescription
, extFilter
);
7817 // Replace songview field names in order by with song, album path table field names
7818 // Field names in album same as song:
7819 // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
7820 StringUtils::Replace(extFilter
.order
, "songview.strPath", "strPath");
7821 StringUtils::Replace(extFilter
.order
, "songview.strAlbum", "strAlbum");
7822 StringUtils::Replace(extFilter
.order
, "songview.bCompilation", "album.bCompilation");
7823 StringUtils::Replace(extFilter
.order
, "songview.strArtists", "song.strArtistDisp");
7824 StringUtils::Replace(extFilter
.order
, "songview.strAlbumArtists", "album.strArtistDisp");
7825 StringUtils::Replace(extFilter
.order
, "songview.strAlbumArtistSort", "album.strArtistSort");
7826 StringUtils::Replace(extFilter
.order
, "songview.strAlbumReleaseType", "strReleaseType");
7827 StringUtils::Replace(extFilter
.order
, "songview", "song");
7828 StringUtils::Replace(extFilter
.fields
, " strArtistSort", " song.strArtistSort");
7829 StringUtils::Replace(extFilter
.fields
, "songview.strArtists", "song.strArtistDisp");
7830 StringUtils::Replace(extFilter
.fields
, "songview.strAlbum", "strAlbum");
7831 StringUtils::Replace(extFilter
.fields
, "songview.strTitle", "strTitle");
7833 // Grab calculated artist/title sort fields that may have been added to filter
7834 // These need to be added to the end of the song table field list
7835 std::string calcsortfieldsSQL
= extFilter
.fields
;
7836 extFilter
.fields
.clear();
7840 // Setup fields to query, and song field number mapping
7841 // Find idAlbumArtist in JSONtoDBSong, offset of first join field
7842 int index_idAlbumArtist
= -1;
7843 for (unsigned int i
= 0; i
< NUM_SONG_FIELDS
; i
++)
7845 if (JSONtoDBSong
[i
].fieldDB
== "idAlbumArtist")
7847 index_idAlbumArtist
= i
;
7852 DatasetLayout
joinLayout(static_cast<size_t>(joinToSongs_enumCount
));
7853 extFilter
.AppendField("song.idSong"); // ID "songid" in JSON
7854 std::vector
<int> dbfieldindex
;
7855 // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
7856 extFilter
.AppendField(JSONtoDBSong
[0].fieldDB
);
7857 if (fields
.find(JSONtoDBSong
[0].fieldJSON
) != fields
.end())
7858 dbfieldindex
.emplace_back(0); // Output "title"
7860 dbfieldindex
.emplace_back(-1); // Fetch but not output
7861 std::vector
<std::string
> rolefieldlist
;
7862 std::vector
<int> roleidlist
;
7863 // Check each optional db field that could be retrieved (not label)
7864 for (unsigned int i
= 1; i
< NUM_SONG_FIELDS
; i
++)
7866 bool foundJSON
= fields
.find(JSONtoDBSong
[i
].fieldJSON
) != fields
.end();
7867 if (JSONtoDBSong
[i
].bSimple
)
7869 // Check for non-join fields in order too.
7870 // Query these in inline view (but not output) so can ref in outer order
7871 bool foundOrderby(false);
7873 foundOrderby
= extFilter
.order
.find(JSONtoDBSong
[i
].fieldDB
) != std::string::npos
;
7874 if (foundOrderby
|| foundJSON
)
7876 // Store indexes of requested album table and scalar subquery fields
7877 // to be output, and -1 when not output to JSON
7879 dbfieldindex
.emplace_back(-1);
7881 dbfieldindex
.emplace_back(i
);
7882 if (!JSONtoDBSong
[i
].SQL
.empty())
7883 // Field from scaler subquery
7884 extFilter
.AppendField(PrepareSQL(JSONtoDBSong
[i
].SQL
));
7886 // Field from song table
7887 extFilter
.AppendField(JSONtoDBSong
[i
].fieldDB
);
7891 { // Field from join found in JSON request
7892 if (!StringUtils::StartsWith(JSONtoDBSong
[i
].fieldDB
, "Role_"))
7894 joinLayout
.SetField(i
- index_idAlbumArtist
, JSONtoDBSong
[i
].SQL
, true);
7897 { // "contributors", "displaycomposer" etc.
7898 rolefieldlist
.emplace_back(JSONtoDBSong
[i
].fieldJSON
);
7902 // Append calculated artist/title sort fields that may have been added to filter
7903 // Field used only for ORDER BY, not output to JSON
7904 extFilter
.AppendField(calcsortfieldsSQL
);
7905 for (int i
= 0; i
< iAddedFields
; i
++)
7906 dbfieldindex
.emplace_back(-1); // columns in dataset
7908 // Build matching list of role id for "displaycomposer", "displayconductor",
7909 // "displayorchestra", "displaylyricist"
7910 if (!rolefieldlist
.empty())
7912 for (const auto& name
: rolefieldlist
)
7915 if (StringUtils::StartsWith(name
, "display"))
7916 idRole
= GetRoleByName(name
.substr(7));
7917 roleidlist
.emplace_back(idRole
);
7921 // JOIN album and path tables needed for field output and/or in sort
7922 // if not already there for filter
7923 if ((extFilter
.fields
.find("album.") != std::string::npos
||
7924 extFilter
.fields
.find("strAlbum") != std::string::npos
) &&
7925 extFilter
.join
.find("JOIN album") == std::string::npos
)
7926 { // All songs have one album so inner join sufficient
7927 extFilter
.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7929 if (extFilter
.fields
.find("path.") != std::string::npos
&&
7930 extFilter
.join
.find("JOIN path") == std::string::npos
)
7931 { // All songs have one path so inner join sufficient
7932 extFilter
.AppendJoin("JOIN path ON path.idPath = song.idPath");
7935 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7937 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
7940 // Add any LIMIT clause to strSQLExtra
7941 if (extFilter
.limit
.empty() && (sortDescription
.limitStart
> 0 || sortDescription
.limitEnd
> 0))
7944 DatabaseUtils::BuildLimitClause(sortDescription
.limitEnd
, sortDescription
.limitStart
);
7945 resultcount
= std::min(
7946 DatabaseUtils::GetLimitCount(sortDescription
.limitEnd
, sortDescription
.limitStart
),
7950 // Setup multivalue JOINs, GROUP BY and ORDER BY
7951 bool bJoinSongArtist(false);
7952 bool bJoinAlbumArtist(false);
7953 bool bJoinRole(false);
7954 if (sortDescription
.sortBy
!= SortByRandom
)
7956 // Repeat inline view order (that always includes idSong) on join query
7957 std::string order
= extFilter
.order
;
7958 order
= extFilter
.order
;
7959 StringUtils::Replace(order
, "album.", "sv.");
7960 StringUtils::Replace(order
, "song.", "sv.");
7961 joinFilter
.AppendOrder(order
);
7964 joinFilter
.AppendOrder("sv.idSong");
7965 joinFilter
.AppendGroup("sv.idSong");
7968 if (joinLayout
.GetFetch(joinToSongs_idAlbumArtist
) ||
7969 joinLayout
.GetFetch(joinToSongs_strAlbumArtist
) ||
7970 joinLayout
.GetFetch(joinToSongs_strAlbumArtistMBID
))
7971 { // All songs have at least one album artist so inner join sufficient
7972 bJoinAlbumArtist
= true;
7973 joinFilter
.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
7974 joinFilter
.AppendGroup("album_artist.idArtist");
7975 joinFilter
.AppendOrder("album_artist.iOrder");
7976 // Ensure idAlbumArtist is queried for processing repeats
7977 if (!joinLayout
.GetFetch(joinToSongs_idAlbumArtist
))
7979 joinLayout
.SetField(joinToSongs_idAlbumArtist
,
7980 JSONtoDBSong
[index_idAlbumArtist
+ joinToSongs_idAlbumArtist
].SQL
);
7982 // Ensure song.IdAlbum is field of the inline view for join
7983 if (fields
.find("albumid") == fields
.end())
7985 extFilter
.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
7986 dbfieldindex
.emplace_back(-1);
7988 // artist table needed for strArtist or MBID
7989 // (album_artist.strArtist can be an alias or spelling variation)
7990 if (joinLayout
.GetFetch(joinToSongs_strAlbumArtistMBID
) ||
7991 joinLayout
.GetFetch(joinToSongs_strAlbumArtist
))
7992 joinFilter
.AppendJoin(
7993 "JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
7998 JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
7999 "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
8001 if (joinLayout
.GetFetch(joinToSongs_idArtist
) || joinLayout
.GetFetch(joinToSongs_strArtist
) ||
8002 joinLayout
.GetFetch(joinToSongs_strArtistMBID
) || !rolefieldlist
.empty())
8003 { // All songs have at least one artist (idRole = 1) so inner join sufficient
8004 bJoinSongArtist
= true;
8005 if (rolefieldlist
.empty())
8006 { // song artists only, no other roles needed
8007 joinFilter
.AppendJoin(
8008 "JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
8009 joinFilter
.AppendGroup("song_artist.idArtist");
8010 joinFilter
.AppendOrder("song_artist.iOrder");
8014 // Ensure idRole is queried
8015 if (!joinLayout
.GetFetch(joinToSongs_idRole
))
8017 joinLayout
.SetField(joinToSongs_idRole
,
8018 JSONtoDBSong
[index_idAlbumArtist
+ joinToSongs_idRole
].SQL
);
8020 // Ensure strArtist is queried
8021 if (!joinLayout
.GetFetch(joinToSongs_strArtist
))
8023 joinLayout
.SetField(joinToSongs_strArtist
,
8024 JSONtoDBSong
[index_idAlbumArtist
+ joinToSongs_strArtist
].SQL
);
8026 if (fields
.find("contributors") != fields
.end())
8029 // Ensure strRole is queried from role table
8030 joinLayout
.SetField(joinToSongs_strRole
, "role.strRole");
8031 joinFilter
.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
8032 joinFilter
.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
8033 joinFilter
.AppendGroup("song_artist.idArtist, song_artist.idRole");
8034 joinFilter
.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
8037 { // Get just roles for "displaycomposer", "displayconductor" etc.
8039 for (size_t i
= 0; i
< roleidlist
.size(); i
++)
8041 int idRole
= roleidlist
[i
];
8045 // Always get song artists too (role = 1) so can do inner join
8046 where
= PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole
);
8048 where
+= PrepareSQL(" OR song_artist.idRole = %i", idRole
);
8050 where
= " (" + where
+ ")";
8051 joinFilter
.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where
);
8052 joinFilter
.AppendGroup("song_artist.idArtist, song_artist.idRole");
8053 joinFilter
.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
8056 // Ensure idArtist is queried for processing repeats
8057 if (!joinLayout
.GetFetch(joinToSongs_idArtist
))
8059 joinLayout
.SetField(joinToSongs_idArtist
,
8060 JSONtoDBSong
[index_idAlbumArtist
+ joinToSongs_idArtist
].SQL
);
8062 // artist table needed for strArtist or MBID
8063 // (song_artist.strArtist can be an alias or spelling variation)
8064 if (joinLayout
.GetFetch(joinToSongs_strArtistMBID
) ||
8065 joinLayout
.GetFetch(joinToSongs_strArtist
))
8066 joinFilter
.AppendJoin(
8067 "JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
8071 if (joinLayout
.GetFetch(joinToSongs_idGenre
))
8072 { // song genre ids (strGenre demormalised in song table)
8073 // Left join as songs may not have genre
8074 joinFilter
.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
8075 joinFilter
.AppendGroup("song_genre.idGenre");
8076 joinFilter
.AppendOrder("song_genre.iOrder");
8079 // Build JOIN part of query (if we have one)
8080 std::string strSQLJoin
;
8081 if (joinLayout
.HasFilterFields())
8082 if (!BuildSQL(strSQLJoin
, joinFilter
, strSQLJoin
))
8085 // Adjust where in the results record the join fields are allowing for the
8086 // inline view fields (Quicker than finding field by name every time)
8087 // idSong + other song fields
8088 joinLayout
.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex
.size()));
8091 // When have multiple value joins use inline view
8092 // SELECT sv.*, <join fields> FROM
8093 // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv
8094 // <joins> <group by>
8095 // <order by> + <joins order by>
8096 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
8097 strSQL
= "SELECT " + extFilter
.fields
+ " FROM song " + strSQLExtra
;
8098 if (joinLayout
.HasFilterFields())
8100 strSQL
= "(" + strSQL
+ ") AS sv ";
8101 strSQL
= "SELECT sv.*, " + joinLayout
.GetFields() + " FROM " + strSQL
+ strSQLJoin
;
8104 // Modify query to use correct year field
8105 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
8106 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
))
8107 StringUtils::Replace(strSQL
, "<datefield>", "song.strReleaseDate");
8109 StringUtils::Replace(strSQL
, "<datefield>", "song.strOrigReleaseDate");
8111 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
8114 auto start
= std::chrono::steady_clock::now();
8116 if (!m_pDS
->query(strSQL
))
8119 auto end
= std::chrono::steady_clock::now();
8120 auto duration
= std::chrono::duration_cast
<std::chrono::milliseconds
>(end
- start
);
8122 CLog::Log(LOGDEBUG
, "{} - query took {} ms", __FUNCTION__
, duration
.count());
8124 int iRowsFound
= m_pDS
->num_rows();
8125 if (iRowsFound
<= 0)
8131 // Get song from returned rows. Joins mean there can be many rows per song
8133 int albumartistId
= -1;
8136 bool bSongGenreDone(false);
8137 bool bSongArtistDone(false);
8138 bool bHaveSong(false);
8140 result
["songs"].reserve(resultcount
);
8141 while (!m_pDS
->eof() || bHaveSong
)
8143 const dbiplus::sql_record
* const record
= m_pDS
->get_sql_record();
8145 if (m_pDS
->eof() || songId
!= record
->at(0).get_asInt())
8147 // Store previous or last song
8150 // Check empty role fields get returned, and format
8151 if (!rolefieldlist
.empty())
8153 for (const auto& displayXXX
: rolefieldlist
)
8155 if (!StringUtils::StartsWith(displayXXX
, "display"))
8158 if (!songObj
.isMember(displayXXX
))
8159 songObj
[displayXXX
] = CVariant(CVariant::VariantTypeArray
);
8161 else if (songObj
.isMember(displayXXX
) && songObj
[displayXXX
].isArray())
8163 // Convert "displaycomposer", "displayconductor", "displayorchestra",
8164 // and "displaylyricist" arrays into strings
8165 std::vector
<std::string
> names
;
8166 for (CVariant::const_iterator_array field
= songObj
[displayXXX
].begin_array();
8167 field
!= songObj
[displayXXX
].end_array(); ++field
)
8168 names
.emplace_back(field
->asString());
8170 std::string role
= StringUtils::Join(names
, CServiceBroker::GetSettingsComponent()
8171 ->GetAdvancedSettings()
8172 ->m_musicItemSeparator
);
8173 songObj
[displayXXX
] = role
;
8176 songObj
[displayXXX
] = "";
8179 result
["songs"].append(songObj
);
8183 if (songObj
.empty())
8185 // Initialise fields, ensure those with possible null values are set to correct empty variant type
8186 if (joinLayout
.GetOutput(joinToSongs_idGenre
))
8187 songObj
["genreid"] =
8188 CVariant(CVariant::VariantTypeArray
); //"genre" set [] by split of array
8193 bSongGenreDone
= false;
8194 bSongArtistDone
= false;
8197 continue; // Having saved the last song stop
8200 songId
= record
->at(0).get_asInt();
8202 songObj
["songid"] = songId
;
8203 songObj
["label"] = record
->at(1).get_asString();
8204 for (size_t i
= 0; i
< dbfieldindex
.size(); i
++)
8205 if (dbfieldindex
[i
] > -1)
8207 if (JSONtoDBSong
[dbfieldindex
[i
]].formatJSON
== "integer")
8208 songObj
[JSONtoDBSong
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asInt();
8209 else if (JSONtoDBSong
[dbfieldindex
[i
]].formatJSON
== "unsigned")
8210 songObj
[JSONtoDBSong
[dbfieldindex
[i
]].fieldJSON
] =
8211 std::max(record
->at(1 + i
).get_asInt(), 0);
8212 else if (JSONtoDBSong
[dbfieldindex
[i
]].formatJSON
== "float")
8213 songObj
[JSONtoDBSong
[dbfieldindex
[i
]].fieldJSON
] =
8214 std::max(record
->at(1 + i
).get_asFloat(), 0.f
);
8215 else if (JSONtoDBSong
[dbfieldindex
[i
]].formatJSON
== "array")
8216 songObj
[JSONtoDBSong
[dbfieldindex
[i
]].fieldJSON
] = StringUtils::Split(
8217 record
->at(1 + i
).get_asString(), CServiceBroker::GetSettingsComponent()
8218 ->GetAdvancedSettings()
8219 ->m_musicItemSeparator
);
8220 else if (JSONtoDBSong
[dbfieldindex
[i
]].formatJSON
== "boolean")
8221 songObj
[JSONtoDBSong
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asBool();
8223 songObj
[JSONtoDBSong
[dbfieldindex
[i
]].fieldJSON
] = record
->at(1 + i
).get_asString();
8226 // Split sources string into int array
8227 if (songObj
.isMember("sourceid"))
8229 std::vector
<std::string
> sources
=
8230 StringUtils::Split(songObj
["sourceid"].asString(), ";");
8231 songObj
["sourceid"] = CVariant(CVariant::VariantTypeArray
);
8232 for (size_t i
= 0; i
< sources
.size(); i
++)
8233 songObj
["sourceid"].append(atoi(sources
[i
].c_str()));
8237 if (bJoinAlbumArtist
)
8239 if (albumartistId
!= record
->at(joinLayout
.GetRecNo(joinToSongs_idAlbumArtist
)).get_asInt())
8242 bSongGenreDone
|| (albumartistId
> 0); // Not first album artist, skip genre
8244 bSongArtistDone
|| (albumartistId
> 0); // Not first album artist, skip song artists
8245 albumartistId
= record
->at(joinLayout
.GetRecNo(joinToSongs_idAlbumArtist
)).get_asInt();
8246 if (joinLayout
.GetOutput(joinToSongs_idAlbumArtist
))
8247 songObj
["albumartistid"].append(albumartistId
);
8248 if (albumartistId
== BLANKARTIST_ID
)
8250 if (joinLayout
.GetOutput(joinToSongs_strAlbumArtist
))
8251 songObj
["albumartist"].append(StringUtils::Empty
);
8252 if (joinLayout
.GetOutput(joinToSongs_strAlbumArtistMBID
))
8253 songObj
["musicbrainzalbumartistid"].append(StringUtils::Empty
);
8257 if (joinLayout
.GetOutput(joinToSongs_idAlbumArtist
))
8258 songObj
["albumartistid"].append(albumartistId
);
8259 if (joinLayout
.GetOutput(joinToSongs_strAlbumArtist
))
8260 songObj
["albumartist"].append(
8261 record
->at(joinLayout
.GetRecNo(joinToSongs_strAlbumArtist
)).get_asString());
8262 if (joinLayout
.GetOutput(joinToSongs_strAlbumArtistMBID
))
8263 songObj
["musicbrainzalbumartistid"].append(
8264 record
->at(joinLayout
.GetRecNo(joinToSongs_strAlbumArtistMBID
)).get_asString());
8268 if (bJoinSongArtist
&& !bSongArtistDone
)
8270 if (artistId
!= record
->at(joinLayout
.GetRecNo(joinToSongs_idArtist
)).get_asInt())
8272 bSongGenreDone
= bSongGenreDone
|| (artistId
> 0); // Not first artist, skip genre
8273 roleId
= -1; // Allow for many artists same role
8274 artistId
= record
->at(joinLayout
.GetRecNo(joinToSongs_idArtist
)).get_asInt();
8275 if (joinLayout
.GetRecNo(joinToSongs_idRole
) < 0 ||
8276 record
->at(joinLayout
.GetRecNo(joinToSongs_idRole
)).get_asInt() == 1)
8278 if (joinLayout
.GetOutput(joinToSongs_idArtist
))
8279 songObj
["artistid"].append(artistId
);
8280 if (artistId
== BLANKARTIST_ID
)
8282 if (joinLayout
.GetOutput(joinToSongs_strArtist
))
8283 songObj
["artist"].append(StringUtils::Empty
);
8284 if (joinLayout
.GetOutput(joinToSongs_strArtistMBID
))
8285 songObj
["musicbrainzartistid"].append(StringUtils::Empty
);
8289 if (joinLayout
.GetOutput(joinToSongs_strArtist
))
8290 songObj
["artist"].append(
8291 record
->at(joinLayout
.GetRecNo(joinToSongs_strArtist
)).get_asString());
8292 if (joinLayout
.GetOutput(joinToSongs_strArtistMBID
))
8293 songObj
["musicbrainzartistid"].append(
8294 record
->at(joinLayout
.GetRecNo(joinToSongs_strArtistMBID
)).get_asString());
8298 if (joinLayout
.GetRecNo(joinToSongs_idRole
) > 0 &&
8299 roleId
!= record
->at(joinLayout
.GetRecNo(joinToSongs_idRole
)).get_asInt())
8301 bSongGenreDone
= bSongGenreDone
|| (roleId
> 0); // Not first role, skip genre
8302 roleId
= record
->at(joinLayout
.GetRecNo(joinToSongs_idRole
)).get_asInt();
8307 CVariant contributor
;
8308 contributor
["name"] =
8309 record
->at(joinLayout
.GetRecNo(joinToSongs_strArtist
)).get_asString();
8310 contributor
["role"] =
8311 record
->at(joinLayout
.GetRecNo(joinToSongs_strRole
)).get_asString();
8312 contributor
["roleid"] = roleId
;
8313 contributor
["artistid"] =
8314 record
->at(joinLayout
.GetRecNo(joinToSongs_idArtist
)).get_asInt();
8315 songObj
["contributors"].append(contributor
);
8317 // "displaycomposer", "displayconductor" etc.
8318 for (size_t i
= 0; i
< roleidlist
.size(); i
++)
8320 if (roleidlist
[i
] == roleId
)
8322 songObj
[rolefieldlist
[i
]].append(
8323 record
->at(joinLayout
.GetRecNo(joinToSongs_strArtist
)).get_asString());
8330 if (!bSongGenreDone
&& joinLayout
.GetRecNo(joinToSongs_idGenre
) > -1 &&
8331 !record
->at(joinLayout
.GetRecNo(joinToSongs_idGenre
)).get_isNull())
8333 songObj
["genreid"].append(record
->at(joinLayout
.GetRecNo(joinToSongs_idGenre
)).get_asInt());
8337 m_pDS
->close(); // cleanup recordset data
8339 // Ensure random order of output when results set is sorted to process multi-value joins
8340 if (sortDescription
.sortBy
== SortByRandom
&& joinLayout
.HasFilterFields())
8341 KODI::UTILS::RandomShuffle(result
["songs"].begin_array(), result
["songs"].end_array());
8348 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
8353 std::string
CMusicDatabase::GetIgnoreArticleSQL(const std::string
& strField
)
8356 Make SQL clause from ignore article list.
8357 Group tokens the same length together, for example :
8358 WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
8359 THEN SUBSTR(strArtist, 5)
8360 WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
8361 THEN SUBSTR(strArtist, 4)
8363 std::set
<std::string
> sortTokens
= g_langInfo
.GetSortTokens();
8364 std::string sortclause
;
8365 size_t tokenlength
= 0;
8366 std::string strWhen
;
8367 for (const auto& token
: sortTokens
)
8369 if (token
.length() != tokenlength
)
8371 if (!strWhen
.empty())
8373 if (!sortclause
.empty())
8375 std::string strThen
= PrepareSQL(" THEN SUBSTR(%s, %i)", strField
.c_str(), tokenlength
+ 1);
8376 sortclause
+= "WHEN " + strWhen
+ strThen
;
8379 tokenlength
= token
.length();
8381 std::string tokenclause
= token
;
8382 //Escape any ' or % in the token
8383 StringUtils::Replace(tokenclause
, "'", "''");
8384 StringUtils::Replace(tokenclause
, "%", "%%");
8385 // Single %, _ and ' so avoid using PrepareSQL
8386 tokenclause
= strField
+ " LIKE '" + tokenclause
+ "%'";
8387 if (token
.find('_') != std::string::npos
)
8388 tokenclause
+= " ESCAPE '_'";
8389 if (!strWhen
.empty())
8391 strWhen
+= tokenclause
;
8393 if (!strWhen
.empty())
8395 if (!sortclause
.empty())
8397 std::string strThen
= PrepareSQL(" THEN SUBSTR(%s, %i)", strField
.c_str(), tokenlength
+ 1);
8398 sortclause
+= "WHEN " + strWhen
+ strThen
;
8403 std::string
CMusicDatabase::SortnameBuildSQL(const std::string
& strAlias
,
8404 const SortAttribute
& sortAttributes
,
8405 const std::string
& strField
,
8406 const std::string
& strSortField
)
8409 Build SQL for sort name scalar subquery from sort attributes and ignore article list.
8411 CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
8412 WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
8413 WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
8418 std::string sortSQL
;
8419 if (!strSortField
.empty() && sortAttributes
& SortAttributeUseArtistSortName
)
8421 PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField
.c_str(), strSortField
.c_str());
8422 if (sortAttributes
& SortAttributeIgnoreArticle
)
8424 if (!sortSQL
.empty())
8426 // Make SQL from ignore article list, grouping tokens the same length together
8427 sortSQL
+= GetIgnoreArticleSQL(strField
);
8429 if (!sortSQL
.empty())
8431 sortSQL
= "CASE " + sortSQL
; // Not prepare as may contain ' and % etc.
8432 sortSQL
+= PrepareSQL(" ELSE %s END AS %s", strField
.c_str(), strAlias
.c_str());
8438 std::string
CMusicDatabase::AlphanumericSortSQL(const std::string
& strField
,
8439 const SortOrder
& sortOrder
)
8442 Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
8443 and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
8444 Natural number order is not significant for where clause comparison and use of calculated fields
8445 means there is no advantage in defining as column default in table create than per query (which
8446 also makes looking at the db with other tools difficult).
8448 MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
8449 "ascii folding" case insensitive collation. Natural sorting is provided via native functions
8453 if (sortOrder
== SortOrderDescending
)
8455 std::string strSort
;
8457 if (StringUtils::EqualsNoCase(
8458 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
,
8460 strSort
= PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField
.c_str(), DESC
.c_str());
8462 strSort
= PrepareSQL("%s COLLATE ALPHANUM%s", strField
.c_str(), DESC
.c_str());
8466 void CMusicDatabase::UpdateTables(int version
)
8468 CLog::Log(LOGINFO
, "{} - updating tables", __FUNCTION__
);
8471 m_pDS
->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
8472 m_pDS
->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
8474 "CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, "
8475 "strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration "
8476 "integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, "
8477 "iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, "
8478 "lastplayed varchar(20) default NULL, rating char default '0', comment text)\n");
8479 m_pDS
->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, "
8480 "iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, "
8481 "iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) "
8482 "SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, "
8483 "dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, "
8484 "iEndOffset, idThumb, lastplayed, rating, comment FROM song");
8486 m_pDS
->exec("DROP TABLE song");
8487 m_pDS
->exec("ALTER TABLE song_new RENAME TO song");
8489 m_pDS
->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
8494 // translate legacy musicdb:// paths
8495 if (m_pDS
->query("SELECT strPath FROM content"))
8497 std::vector
<std::string
> contentPaths
;
8498 while (!m_pDS
->eof())
8500 contentPaths
.push_back(m_pDS
->fv(0).get_asString());
8505 for (const auto& originalPath
: contentPaths
)
8507 std::string path
= CLegacyPathTranslation::TranslateMusicDbPath(originalPath
);
8508 m_pDS
->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path
.c_str(),
8509 originalPath
.c_str()));
8516 m_pDS
->exec("CREATE TABLE album_new "
8517 "(idAlbum integer primary key, "
8518 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8519 " strArtists text, strGenres text, "
8520 " iYear integer, idThumb integer, "
8521 " bCompilation integer not null default '0', "
8522 " strMoods text, strStyles text, strThemes text, "
8523 " strReview text, strImage text, strLabel text, "
8525 " iRating integer, "
8526 " lastScraped varchar(20) default NULL, "
8527 " dateAdded varchar (20) default NULL)");
8528 m_pDS
->exec("INSERT INTO album_new "
8530 " strAlbum, strMusicBrainzAlbumID, "
8531 " strArtists, strGenres, "
8534 " strMoods, strStyles, strThemes, "
8535 " strReview, strImage, strLabel, "
8540 " strAlbum, strMusicBrainzAlbumID, "
8541 " strArtists, strGenres, "
8542 " album.iYear, idThumb, "
8544 " strMoods, strStyles, strThemes, "
8545 " strReview, strImage, strLabel, "
8546 " strType, iRating "
8547 " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
8548 m_pDS
->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE "
8549 "albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
8550 m_pDS
->exec(PrepareSQL(
8551 "UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)",
8552 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
8553 m_pDS
->exec("DROP TABLE album");
8554 m_pDS
->exec("DROP TABLE albuminfo");
8555 m_pDS
->exec("ALTER TABLE album_new RENAME TO album");
8559 m_pDS
->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
8560 " strArtist varchar(256), strMusicBrainzArtistID text, "
8561 " strBorn text, strFormed text, strGenres text, strMoods text, "
8562 " strStyles text, strInstruments text, strBiography text, "
8563 " strDied text, strDisbanded text, strYearsActive text, "
8564 " strImage text, strFanart text, "
8565 " lastScraped varchar(20) default NULL, "
8566 " dateAdded varchar (20) default NULL)");
8567 m_pDS
->exec("INSERT INTO artist_new "
8568 "(idArtist, strArtist, strMusicBrainzArtistID, "
8569 " strBorn, strFormed, strGenres, strMoods, "
8570 " strStyles , strInstruments , strBiography , "
8571 " strDied, strDisbanded, strYearsActive, "
8572 " strImage, strFanart) "
8574 " artist.idArtist, "
8575 " strArtist, strMusicBrainzArtistID, "
8576 " strBorn, strFormed, strGenres, strMoods, "
8577 " strStyles, strInstruments, strBiography, "
8578 " strDied, strDisbanded, strYearsActive, "
8579 " strImage, strFanart "
8581 " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
8582 m_pDS
->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT "
8583 "idArtist FROM artistinfo)",
8584 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
8585 m_pDS
->exec("DROP TABLE artist");
8586 m_pDS
->exec("DROP TABLE artistinfo");
8587 m_pDS
->exec("ALTER TABLE artist_new RENAME TO artist");
8591 m_pDS
->exec("ALTER TABLE album_artist ADD strArtist text\n");
8592 m_pDS
->exec("ALTER TABLE song_artist ADD strArtist text\n");
8594 std::string sql
= "select idArtist,strArtist from artist";
8596 while (!m_pDS
->eof())
8598 m_pDS2
->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i",
8599 m_pDS
->fv(1).get_asString().c_str(), m_pDS
->fv(0).get_asInt()));
8600 m_pDS2
->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i",
8601 m_pDS
->fv(1).get_asString().c_str(), m_pDS
->fv(0).get_asInt()));
8606 { // null out columns that are no longer used
8607 m_pDS
->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
8608 m_pDS
->exec("UPDATE album SET idThumb=NULL");
8612 m_pDS
->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
8616 // add a new column strReleaseType for albums
8617 m_pDS
->exec("ALTER TABLE album ADD strReleaseType text\n");
8619 // set strReleaseType based on album name
8620 m_pDS
->exec(PrepareSQL(
8621 "UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''",
8622 CAlbum::ReleaseTypeToString(CAlbum::Album
).c_str()));
8624 PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''",
8625 CAlbum::ReleaseTypeToString(CAlbum::Single
).c_str()));
8629 m_pDS
->exec("ALTER TABLE song ADD mood text\n");
8633 m_pDS
->exec("ALTER TABLE song ADD dateAdded text");
8637 //Remove dateAdded from artist table
8638 m_pDS
->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
8639 " strArtist varchar(256), strMusicBrainzArtistID text, "
8640 " strBorn text, strFormed text, strGenres text, strMoods text, "
8641 " strStyles text, strInstruments text, strBiography text, "
8642 " strDied text, strDisbanded text, strYearsActive text, "
8643 " strImage text, strFanart text, "
8644 " lastScraped varchar(20) default NULL)");
8645 m_pDS
->exec("INSERT INTO artist_new "
8646 "(idArtist, strArtist, strMusicBrainzArtistID, "
8647 " strBorn, strFormed, strGenres, strMoods, "
8648 " strStyles , strInstruments , strBiography , "
8649 " strDied, strDisbanded, strYearsActive, "
8650 " strImage, strFanart, lastScraped) "
8653 " strArtist, strMusicBrainzArtistID, "
8654 " strBorn, strFormed, strGenres, strMoods, "
8655 " strStyles, strInstruments, strBiography, "
8656 " strDied, strDisbanded, strYearsActive, "
8657 " strImage, strFanart, lastScraped "
8659 m_pDS
->exec("DROP TABLE artist");
8660 m_pDS
->exec("ALTER TABLE artist_new RENAME TO artist");
8662 //Remove dateAdded from album table
8663 m_pDS
->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8664 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8665 " strArtists text, strGenres text, "
8666 " iYear integer, idThumb integer, "
8667 " bCompilation integer not null default '0', "
8668 " strMoods text, strStyles text, strThemes text, "
8669 " strReview text, strImage text, strLabel text, "
8671 " iRating integer, "
8672 " lastScraped varchar(20) default NULL, "
8673 " strReleaseType text)");
8674 m_pDS
->exec("INSERT INTO album_new "
8676 " strAlbum, strMusicBrainzAlbumID, "
8677 " strArtists, strGenres, "
8680 " strMoods, strStyles, strThemes, "
8681 " strReview, strImage, strLabel, "
8682 " strType, iRating, lastScraped, "
8686 " strAlbum, strMusicBrainzAlbumID, "
8687 " strArtists, strGenres, "
8690 " strMoods, strStyles, strThemes, "
8691 " strReview, strImage, strLabel, "
8692 " strType, iRating, lastScraped, "
8695 m_pDS
->exec("DROP TABLE album");
8696 m_pDS
->exec("ALTER TABLE album_new RENAME TO album");
8700 m_pDS
->exec("DROP TABLE karaokedata");
8704 m_pDS
->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
8705 m_pDS
->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
8706 m_pDS
->exec("UPDATE song SET userrating = rating * 2");
8707 m_pDS
->exec("UPDATE song SET rating = 0");
8708 m_pDS
->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8709 " idAlbum INTEGER, idPath INTEGER, "
8710 " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8711 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8712 " dwFileNameCRC TEXT, "
8713 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8714 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8715 " idThumb INTEGER, "
8716 " lastplayed VARCHAR(20) DEFAULT NULL, "
8717 " rating FLOAT DEFAULT 0, "
8718 " userrating INTEGER DEFAULT 0, "
8719 " comment TEXT, mood TEXT, dateAdded TEXT)");
8720 m_pDS
->exec("INSERT INTO song_new "
8722 " idAlbum, idPath, "
8723 " strArtists, strGenres, strTitle, "
8724 " iTrack, iDuration, iYear, "
8726 " strFileName, strMusicBrainzTrackID, "
8727 " iTimesPlayed, iStartOffset, iEndOffset, "
8730 " rating, userrating, "
8731 " comment, mood, dateAdded)"
8734 " idAlbum, idPath, "
8735 " strArtists, strGenres, strTitle, "
8736 " iTrack, iDuration, iYear, "
8738 " strFileName, strMusicBrainzTrackID, "
8739 " iTimesPlayed, iStartOffset, iEndOffset, "
8744 " comment, mood, dateAdded"
8746 m_pDS
->exec("DROP TABLE song");
8747 m_pDS
->exec("ALTER TABLE song_new RENAME TO song");
8749 m_pDS
->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
8750 m_pDS
->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
8751 m_pDS
->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8752 " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8753 " strArtists TEXT, strGenres TEXT, "
8754 " iYear INTEGER, idThumb INTEGER, "
8755 " bCompilation INTEGER NOT NULL DEFAULT '0', "
8756 " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8757 " strReview TEXT, strImage TEXT, strLabel TEXT, "
8759 " fRating FLOAT NOT NULL DEFAULT 0, "
8760 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8761 " lastScraped VARCHAR(20) DEFAULT NULL, "
8762 " strReleaseType TEXT)");
8763 m_pDS
->exec("INSERT INTO album_new "
8765 " strAlbum, strMusicBrainzAlbumID, "
8766 " strArtists, strGenres, "
8769 " strMoods, strStyles, strThemes, "
8770 " strReview, strImage, strLabel, "
8778 " strAlbum, strMusicBrainzAlbumID, "
8779 " strArtists, strGenres, "
8782 " strMoods, strStyles, strThemes, "
8783 " strReview, strImage, strLabel, "
8790 m_pDS
->exec("DROP TABLE album");
8791 m_pDS
->exec("ALTER TABLE album_new RENAME TO album");
8793 m_pDS
->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
8794 m_pDS
->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
8798 m_pDS
->exec("UPDATE album SET fRating = fRating * 2");
8802 m_pDS
->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
8803 m_pDS
->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
8805 //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
8806 m_pDS
->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, "
8807 "iOrder integer, strArtist text)");
8808 m_pDS
->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
8809 "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
8810 m_pDS
->exec("DROP TABLE song_artist");
8811 m_pDS
->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
8813 //Remove strJoinPhrase, boolFeatured from album_artist table
8814 m_pDS
->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, "
8816 m_pDS
->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
8817 "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
8818 m_pDS
->exec("DROP TABLE album_artist");
8819 m_pDS
->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
8823 // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
8824 // do not have an artist tag to ensure all songs in the library have at least one artist.
8826 if (GetArtistExists(BLANKARTIST_ID
))
8828 // When BLANKARTIST_ID (=1) is already in use, move the record
8830 { //No mbid index yet, so can have record for artist twice even with mbid
8831 strSQL
= PrepareSQL("INSERT INTO artist SELECT null, "
8832 "strArtist, strMusicBrainzArtistID, "
8833 "strBorn, strFormed, strGenres, strMoods, "
8834 "strStyles, strInstruments, strBiography, "
8835 "strDied, strDisbanded, strYearsActive, "
8836 "strImage, strFanart, lastScraped "
8837 "FROM artist WHERE artist.idArtist = %i",
8839 m_pDS
->exec(strSQL
);
8840 int idArtist
= (int)m_pDS
->lastinsertid();
8841 //No triggers, so can delete artist without effecting other tables.
8842 strSQL
= PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID
);
8843 m_pDS
->exec(strSQL
);
8845 // Update related tables with the new artist ID
8846 // Indices have been dropped making transactions very slow, so create appropriate temp indices
8847 m_pDS
->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
8848 m_pDS
->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
8849 m_pDS
->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
8850 m_pDS
->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
8851 strSQL
= PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist
,
8853 m_pDS
->exec(strSQL
);
8854 strSQL
= PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist
,
8856 m_pDS
->exec(strSQL
);
8858 PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'",
8859 idArtist
, BLANKARTIST_ID
);
8860 m_pDS
->exec(strSQL
);
8861 strSQL
= PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist
,
8863 m_pDS
->exec(strSQL
);
8864 // Drop temp indices
8865 m_pDS
->exec("DROP INDEX idxSongArtist2 ON song_artist");
8866 m_pDS
->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
8867 m_pDS
->exec("DROP INDEX idxDiscography ON discography");
8868 m_pDS
->exec("DROP INDEX ix_art ON art");
8872 CLog::Log(LOGERROR
, "Moving existing artist to add missing tag artist has failed");
8876 // Create missing artist tag artist [Missing].
8877 // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
8878 strSQL
= PrepareSQL(
8879 "INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
8880 BLANKARTIST_ID
, BLANKARTIST_NAME
.c_str(), BLANKARTIST_FAKEMUSICBRAINZID
.c_str());
8881 m_pDS
->exec(strSQL
);
8883 // Indices have been dropped making transactions very slow, so create temp index
8884 m_pDS
->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
8885 m_pDS
->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
8887 // Ensure all songs have at least one artist, set those without to [Missing]
8888 strSQL
= "SELECT count(idSong) FROM song "
8889 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8890 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
8891 int numsongs
= GetSingleValueInt(strSQL
);
8894 CLog::Log(LOGDEBUG
, "{} songs have no artist, setting artist to [Missing]", numsongs
);
8895 // Insert song_artist records for songs that don't have any
8898 strSQL
= PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
8899 "SELECT %i, idSong, %i, '%s', 0 FROM song "
8900 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8901 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
8902 BLANKARTIST_ID
, ROLE_ARTIST
, BLANKARTIST_NAME
.c_str(), ROLE_ARTIST
);
8903 ExecuteQuery(strSQL
);
8907 CLog::Log(LOGERROR
, "Setting missing artist for songs without an artist has failed");
8911 // Ensure all albums have at least one artist, set those without to [Missing]
8912 strSQL
= "SELECT count(idAlbum) FROM album "
8913 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8914 "WHERE album_artist.idAlbum = album.idAlbum)";
8915 int numalbums
= GetSingleValueInt(strSQL
);
8918 CLog::Log(LOGDEBUG
, "{} albums have no artist, setting artist to [Missing]", numalbums
);
8919 // Insert album_artist records for albums that don't have any
8922 strSQL
= PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
8923 "SELECT %i, idAlbum, '%s', 0 FROM album "
8924 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8925 "WHERE album_artist.idAlbum = album.idAlbum)",
8926 BLANKARTIST_ID
, BLANKARTIST_NAME
.c_str());
8927 ExecuteQuery(strSQL
);
8931 CLog::Log(LOGERROR
, "Setting artist missing for albums without an artist has failed");
8934 //Remove temp indices, full analytics for database created later
8935 m_pDS
->exec("DROP INDEX idxSongArtist1 ON song_artist");
8936 m_pDS
->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
8940 // Create versiontagscan table
8941 m_pDS
->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
8942 m_pDS
->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
8946 CLog::Log(LOGINFO
, "create audiobook table");
8947 m_pDS
->exec("CREATE TABLE audiobook (idBook integer primary key, "
8948 " strBook varchar(256), strAuthor text,"
8949 " bookmark integer, file text,"
8950 " dateAdded varchar (20) default NULL)");
8954 // Add strSortName to Artist table
8955 m_pDS
->exec("ALTER TABLE artist ADD strSortName text\n");
8957 //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
8958 m_pDS
->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8959 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8960 " strArtistDisp text, strArtistSort text, strGenres text, "
8961 " iYear integer, bCompilation integer not null default '0', "
8962 " strMoods text, strStyles text, strThemes text, "
8963 " strReview text, strImage text, strLabel text, "
8965 " fRating FLOAT NOT NULL DEFAULT 0, "
8966 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8967 " lastScraped varchar(20) default NULL, "
8968 " strReleaseType text, "
8969 " iVotes INTEGER NOT NULL DEFAULT 0)");
8970 m_pDS
->exec("INSERT INTO album_new "
8972 " strAlbum, strMusicBrainzAlbumID, "
8973 " strArtistDisp, strArtistSort, strGenres, "
8974 " iYear, bCompilation, "
8975 " strMoods, strStyles, strThemes, "
8976 " strReview, strImage, strLabel, "
8978 " fRating, iUserrating, iVotes, "
8983 " strAlbum, strMusicBrainzAlbumID, "
8984 " strArtists, NULL, strGenres, "
8985 " iYear, bCompilation, "
8986 " strMoods, strStyles, strThemes, "
8987 " strReview, strImage, strLabel, "
8989 " fRating, iUserrating, iVotes, "
8993 m_pDS
->exec("DROP TABLE album");
8994 m_pDS
->exec("ALTER TABLE album_new RENAME TO album");
8996 //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
8997 m_pDS
->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8998 " idAlbum INTEGER, idPath INTEGER, "
8999 " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
9000 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
9001 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
9002 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
9003 " lastplayed VARCHAR(20) DEFAULT NULL, "
9004 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
9005 " userrating INTEGER NOT NULL DEFAULT 0, "
9006 " comment TEXT, mood TEXT, dateAdded TEXT)");
9007 m_pDS
->exec("INSERT INTO song_new "
9009 " idAlbum, idPath, "
9010 " strArtistDisp, strArtistSort, strGenres, strTitle, "
9011 " iTrack, iDuration, iYear, "
9012 " strFileName, strMusicBrainzTrackID, "
9013 " iTimesPlayed, iStartOffset, iEndOffset, "
9015 " rating, userrating, votes, "
9016 " comment, mood, dateAdded)"
9019 " idAlbum, idPath, "
9020 " strArtists, NULL, strGenres, strTitle, "
9021 " iTrack, iDuration, iYear, "
9022 " strFileName, strMusicBrainzTrackID, "
9023 " iTimesPlayed, iStartOffset, iEndOffset, "
9025 " rating, userrating, votes, "
9026 " comment, mood, dateAdded"
9028 m_pDS
->exec("DROP TABLE song");
9029 m_pDS
->exec("ALTER TABLE song_new RENAME TO song");
9034 m_pDS
->exec("DROP TABLE cue");
9035 // Add strReplayGain to song table
9036 m_pDS
->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
9040 // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
9041 m_pDS
->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
9042 m_pDS
->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
9043 // Add a new column bScrapedMBID for artists
9044 m_pDS
->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
9048 // Add infosetting table
9049 m_pDS
->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, "
9050 "strSettings TEXT)");
9051 // Add a new column for setting to album and artist tables
9052 m_pDS
->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
9053 m_pDS
->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
9055 // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
9057 "CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
9058 "strScraperPath text, strSettings text)");
9061 m_pDS
->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
9062 "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, "
9063 "strScraperPath, strSettings "
9064 "FROM content WHERE strContent = 'artists' AND strPath LIKE "
9065 "'musicdb://artists/_%/' ORDER BY idItem");
9070 "Migrating specific artist scraper settings has failed, settings not transferred");
9074 m_pDS
->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
9075 "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, "
9076 "strScraperPath, strSettings "
9077 "FROM content WHERE strContent = 'albums' AND strPath LIKE "
9078 "'musicdb://albums/_%/' ORDER BY idItem");
9083 "Migrating specific album scraper settings has failed, settings not transferred");
9087 m_pDS
->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
9088 "SELECT id, strScraperPath, strSettings FROM content_temp");
9090 "UPDATE artist SET idInfoSetting = "
9091 "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
9092 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = "
9094 m_pDS
->exec("UPDATE album SET idInfoSetting = "
9095 "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
9096 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem "
9102 "Migrating album and artist scraper settings has failed, settings not transferred");
9104 m_pDS
->exec("DROP TABLE content_temp");
9106 // Remove content table
9107 m_pDS
->exec("DROP TABLE content");
9108 // Remove albuminfosong table
9109 m_pDS
->exec("DROP TABLE albuminfosong");
9113 // Add a new columns strType, strGender, strDisambiguation for artists
9114 m_pDS
->exec("ALTER TABLE artist ADD strType TEXT \n");
9115 m_pDS
->exec("ALTER TABLE artist ADD strGender TEXT \n");
9116 m_pDS
->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
9120 // Remove album_genre table
9121 m_pDS
->exec("DROP TABLE album_genre");
9125 // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
9126 m_pDS
->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * "
9131 // Add lastscanned to versiontagscan table
9132 m_pDS
->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
9133 CDateTime dateAdded
= CDateTime::GetCurrentDateTime();
9134 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
9135 dateAdded
.GetAsDBDateTime().c_str()));
9139 // Create source table
9141 "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
9142 // Create source_path table
9144 "CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
9145 // Create album_source table
9146 m_pDS
->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
9147 // Populate source and source_path tables from sources.xml
9148 // Filling album_source needs to be done after indexes are created or it is
9149 // very slow. It could be populated during CreateAnalytics but it is checked
9150 // and filled as part of scanning anyway so simply force full rescan.
9155 // add bBoxedSet to album table
9156 m_pDS
->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
9157 // add iDiscTotal to album table
9158 m_pDS
->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
9159 // populate iDiscTotal from the data already in the song table
9160 m_pDS
->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
9161 "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
9162 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
9163 // add strDiscSubtitles to song table
9164 m_pDS
->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
9168 //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
9169 m_pDS
->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
9170 "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
9171 "strReleaseGroupMBID TEXT, "
9172 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
9173 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
9174 "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
9175 "bCompilation INTEGER NOT NULL DEFAULT '0', "
9176 "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
9177 "strReview TEXT, strImage TEXT, strLabel TEXT, "
9179 "fRating FLOAT NOT NULL DEFAULT 0, "
9180 "iVotes INTEGER NOT NULL DEFAULT 0, "
9181 "iUserrating INTEGER NOT NULL DEFAULT 0, "
9182 "lastScraped VARCHAR(20) DEFAULT NULL, "
9183 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
9184 "strReleaseType TEXT, "
9185 "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
9186 "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
9187 // Prepare as MySQL has different CAST datatypes
9189 PrepareSQL("INSERT INTO album_new "
9190 "(idalbum, strAlbum, "
9191 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
9192 "strArtistDisp, strArtistSort, strGenres, "
9193 "strReleaseDate, strOrigReleaseDate, "
9194 "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
9195 "strReview, strImage, strLabel, strType, "
9196 "fRating, iVotes, iUserrating, "
9197 "lastScraped, bScrapedMBID, strReleaseType, "
9198 "iDiscTotal, idInfoSetting) "
9200 "idAlbum, strAlbum, "
9201 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
9202 "strArtistDisp, strArtistSort, strGenres, "
9203 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9204 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9205 // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
9206 "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
9207 "bCompilation, strMoods, strStyles, strThemes, "
9208 "strReview, strImage, strLabel, strType, "
9209 "fRating, iVotes, iUserrating, "
9210 "lastScraped, bScrapedMBID, strReleaseType, "
9211 "iDiscTotal, idInfoSetting "
9213 m_pDS
->exec("DROP TABLE album");
9214 m_pDS
->exec("ALTER TABLE album_new RENAME TO album");
9216 //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
9217 m_pDS
->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
9218 "idAlbum INTEGER, idPath INTEGER, "
9219 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
9220 "iTrack INTEGER, iDuration INTEGER, "
9221 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
9222 "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
9223 "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
9224 "lastplayed VARCHAR(20) DEFAULT NULL, "
9225 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
9226 "userrating INTEGER NOT NULL DEFAULT 0, "
9227 "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
9229 // Prepare as MySQL has different CAST datatypes
9230 m_pDS
->exec(PrepareSQL("INSERT INTO song_new "
9233 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9234 "iTrack, iDuration, "
9235 "strReleaseDate, strOrigReleaseDate, "
9236 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9237 "iTimesPlayed, iStartOffset, iEndOffset, "
9239 "rating, userrating, votes, "
9240 "comment, mood, strReplayGain, dateAdded) "
9244 "strArtistDisp, strArtistSort, strGenres, strTitle, "
9245 "iTrack, iDuration, "
9246 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9247 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
9248 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
9249 "iTimesPlayed, iStartOffset, iEndOffset, "
9251 "rating, userrating, votes, "
9252 "comment, mood, strReplayGain, dateAdded "
9254 m_pDS
->exec("DROP TABLE song");
9255 m_pDS
->exec("ALTER TABLE song_new RENAME TO song");
9259 m_pDS
->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
9260 m_pDS
->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
9261 m_pDS
->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
9265 m_pDS
->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
9269 std::string strUTCNow
= CDateTime::GetUTCDateTime().GetAsDBDateTime();
9271 // Add removed_link table
9272 m_pDS
->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
9273 // Add lastcleaned and artistlinksupdated to versiontagscan table
9274 m_pDS
->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
9275 m_pDS
->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
9276 m_pDS
->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
9277 // Adjust lastscanned if original local time value is after current UTC
9278 if (GetLibraryLastUpdated() > strUTCNow
)
9279 SetLibraryLastUpdated();
9280 m_pDS
->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
9281 "genresupdated = lastscanned, "
9282 "artistlinksupdated = lastscanned");
9284 // Add dateNew, dateModified to song table
9285 m_pDS
->exec("ALTER TABLE song ADD dateNew TEXT");
9286 m_pDS
->exec("ALTER TABLE song ADD dateModified TEXT");
9287 // Set new to dateAdded and modified to lastplayed as estimates
9288 // Limit those local time values to now UTC, and modified is after new
9289 m_pDS
->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
9290 m_pDS
->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'", strUTCNow
.c_str(),
9291 strUTCNow
.c_str()));
9292 m_pDS
->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
9293 m_pDS
->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
9294 strUTCNow
.c_str(), strUTCNow
.c_str()));
9295 m_pDS
->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
9297 // Add dateAdded, dateNew, dateModified to album table
9298 m_pDS
->exec("ALTER TABLE album ADD dateAdded TEXT");
9299 m_pDS
->exec("ALTER TABLE album ADD dateNew TEXT");
9300 m_pDS
->exec("ALTER TABLE album ADD dateModified TEXT");
9301 // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
9302 // Limit modified value to now UTC and after new
9303 // Indices have been dropped making subquery very slow, so create temp index
9304 m_pDS
->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
9305 m_pDS
->exec("UPDATE album SET dateAdded = "
9306 "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
9307 m_pDS
->exec("UPDATE album SET dateNew = "
9308 "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
9309 m_pDS
->exec("UPDATE album SET dateModified = dateNew");
9310 m_pDS
->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
9311 m_pDS
->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
9312 strUTCNow
.c_str(), strUTCNow
.c_str()));
9313 //Remove temp index, full analytics for database created later
9314 m_pDS
->exec("DROP INDEX idxSong3 ON song");
9316 // Add dateAdded, dateNew, dateModified to artist table
9317 m_pDS
->exec("ALTER TABLE artist ADD dateAdded TEXT");
9318 m_pDS
->exec("ALTER TABLE artist ADD dateNew TEXT");
9319 m_pDS
->exec("ALTER TABLE artist ADD dateModified TEXT");
9320 // dateAdded has NULL values until files rescanned by user
9321 // Set new and modified to now UTC as not worth complexity of estimating from song dates
9322 m_pDS
->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow
.c_str()));
9323 m_pDS
->exec("UPDATE artist SET dateModified = dateNew");
9327 m_pDS
->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
9331 m_pDS
->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
9332 // update duration for all current albums
9333 m_pDS
->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
9334 "WHERE song.idAlbum = album.idAlbum) "
9335 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
9339 // Update artist table combining fanart URL data into strImage field
9340 // Clear empty URL data <fanart /> and <thumb />
9341 m_pDS
->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
9342 m_pDS
->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
9343 //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
9344 m_pDS
->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
9345 m_pDS
->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
9346 m_pDS
->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
9347 "aspect=\"fanart\" preview')");
9348 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
9349 // Truncate the fanart when total URLs exceeds this
9350 bool bisMySQL
= StringUtils::EqualsNoCase(
9351 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
,
9355 std::string strSQL
= "SELECT idArtist, strFanart, strImage FROM artist "
9356 "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
9357 if (m_pDS
->query(strSQL
))
9359 while (!m_pDS
->eof())
9361 int idArtist
= m_pDS
->fv("idArtist").get_asInt();
9362 std::string strFanart
= m_pDS
->fv("strFanart").get_asString();
9363 std::string strImage
= m_pDS
->fv("strImage").get_asString();
9364 size_t space
= 65535;
9365 // Trim strImage to allow arbitrary half space for fanart
9366 if (!TrimImageURLs(strImage
, space
/ 2))
9367 strImage
.clear(); // </thumb> not found, empty field
9368 space
= space
- strImage
.length();
9369 // Trim fanart to fit remaining space
9370 if (!TrimImageURLs(strFanart
, space
))
9371 strFanart
.clear(); // </thumb> not found, empty field
9373 strSQL
= PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
9374 "WHERE idArtist = %i",
9375 strFanart
.c_str(), strImage
.c_str(), idArtist
);
9376 m_pDS2
->exec(strSQL
); // Use other dataset to update while looping result set
9384 // Remove strFanart column from artist table
9385 m_pDS
->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
9386 "strArtist varchar(256), strMusicBrainzArtistID text, "
9387 "strSortName text, "
9388 "strType text, strGender text, strDisambiguation text, "
9389 "strBorn text, strFormed text, strGenres text, strMoods text, "
9390 "strStyles text, strInstruments text, strBiography text, "
9391 "strDied text, strDisbanded text, strYearsActive text, "
9393 "lastScraped varchar(20) default NULL, "
9394 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
9395 "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
9396 "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
9397 // Concatenate fanart URLs into strImage field
9398 // Prepare SQL to convert CONCAT to || in SQLite
9399 m_pDS
->exec(PrepareSQL("INSERT INTO artist_new "
9400 "(idArtist, strArtist, strMusicBrainzArtistID, "
9401 "strSortName, strType, strGender, strDisambiguation, "
9402 "strBorn, strFormed, strGenres, strMoods, "
9403 "strStyles , strInstruments , strBiography , "
9404 "strDied, strDisbanded, strYearsActive, "
9406 "lastScraped, bScrapedMBID, idInfoSetting, "
9407 "dateAdded, dateNew, dateModified) "
9410 "strArtist, strMusicBrainzArtistID, "
9411 "strSortName, strType, strGender, strDisambiguation, "
9412 "strBorn, strFormed, strGenres, strMoods, "
9413 "strStyles, strInstruments, strBiography, "
9414 "strDied, strDisbanded, strYearsActive, "
9415 "CONCAT(strImage, strFanart), "
9416 "lastScraped, bScrapedMBID, idInfoSetting, "
9417 "dateAdded, dateNew, dateModified "
9419 m_pDS
->exec("DROP TABLE artist");
9420 m_pDS
->exec("ALTER TABLE artist_new RENAME TO artist");
9424 m_pDS
->exec("ALTER TABLE song ADD strVideoURL TEXT");
9426 // Set the version of tag scanning required.
9427 // Not every schema change requires the tags to be rescanned, set to the highest schema version
9428 // that needs this. Forced rescanning (of music files that have not changed since they were
9429 // previously scanned) also accommodates any changes to the way tags are processed
9430 // e.g. read tags that were not processed by previous versions.
9431 // The original db version when the tags were scanned, and the minimal db version needed are
9432 // later used to determine if a forced rescan should be prompted
9434 // The last schema change needing forced rescanning was 73.
9435 // This is because Kodi can now read and process extra tags involved in the creation of box sets
9437 SetMusicNeedsTagScan(73);
9439 // After all updates, store the original db version.
9440 // This indicates the version of tag processing that was used to populate db
9441 SetMusicTagScanVersion(version
);
9444 int CMusicDatabase::GetSchemaVersion() const
9449 int CMusicDatabase::GetMusicNeedsTagScan()
9453 if (nullptr == m_pDB
)
9455 if (nullptr == m_pDS
)
9458 std::string sql
= "SELECT * FROM versiontagscan";
9459 if (!m_pDS
->query(sql
))
9462 if (m_pDS
->num_rows() != 1)
9468 int idVersion
= m_pDS
->fv("idVersion").get_asInt();
9469 int iNeedsScan
= m_pDS
->fv("iNeedsScan").get_asInt();
9471 if (idVersion
< iNeedsScan
)
9478 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
9483 void CMusicDatabase::SetMusicNeedsTagScan(int version
)
9485 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version
));
9488 void CMusicDatabase::SetMusicTagScanVersion(int version
/* = 0 */)
9491 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
9493 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version
));
9496 std::string
CMusicDatabase::GetLibraryLastUpdated()
9498 return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
9501 void CMusicDatabase::SetLibraryLastUpdated()
9503 CDateTime dateUpdated
= CDateTime::GetUTCDateTime();
9504 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
9505 dateUpdated
.GetAsDBDateTime().c_str()));
9508 std::string
CMusicDatabase::GetLibraryLastCleaned()
9510 return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
9513 void CMusicDatabase::SetLibraryLastCleaned()
9515 std::string strUpdated
= CDateTime::GetUTCDateTime().GetAsDBDateTime();
9516 m_pDS
->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'", strUpdated
.c_str()));
9519 std::string
CMusicDatabase::GetArtistLinksUpdated()
9521 return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
9524 void CMusicDatabase::SetArtistLinksUpdated()
9526 std::string strUpdated
= CDateTime::GetUTCDateTime().GetAsDBDateTime();
9528 PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'", strUpdated
.c_str()));
9531 std::string
CMusicDatabase::GetGenresLastAdded()
9533 return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
9536 std::string
CMusicDatabase::GetSongsLastAdded()
9538 return GetSingleValue("SELECT MAX(dateNew) FROM song");
9541 std::string
CMusicDatabase::GetAlbumsLastAdded()
9543 return GetSingleValue("SELECT MAX(dateNew) FROM album");
9546 std::string
CMusicDatabase::GetArtistsLastAdded()
9548 return GetSingleValue("SELECT MAX(dateNew) FROM artist");
9551 std::string
CMusicDatabase::GetSongsLastModified()
9553 return GetSingleValue("SELECT MAX(dateModified) FROM song");
9556 std::string
CMusicDatabase::GetAlbumsLastModified()
9558 return GetSingleValue("SELECT MAX(dateModified) FROM album");
9561 std::string
CMusicDatabase::GetArtistsLastModified()
9563 return GetSingleValue("SELECT MAX(dateModified) FROM artist");
9566 unsigned int CMusicDatabase::GetRandomSongIDs(const Filter
& filter
,
9567 std::vector
<std::pair
<int, int>>& songIDs
)
9571 if (nullptr == m_pDB
)
9573 if (nullptr == m_pDS
)
9576 std::string strSQL
= "SELECT idSong FROM songview ";
9577 if (!CDatabase::BuildSQL(strSQL
, filter
, strSQL
))
9579 strSQL
+= PrepareSQL(" ORDER BY RANDOM()");
9581 if (!m_pDS
->query(strSQL
))
9584 if (m_pDS
->num_rows() == 0)
9589 songIDs
.reserve(m_pDS
->num_rows());
9590 while (!m_pDS
->eof())
9592 songIDs
.push_back(std::make_pair
<int, int>(1, m_pDS
->fv(song_idSong
).get_asInt()));
9596 return static_cast<unsigned int>(songIDs
.size());
9600 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
9605 int CMusicDatabase::GetSongsCount(const Filter
& filter
)
9609 if (nullptr == m_pDB
)
9611 if (nullptr == m_pDS
)
9614 std::string strSQL
= "select count(idSong) as NumSongs from songview ";
9615 if (!CDatabase::BuildSQL(strSQL
, filter
, strSQL
))
9618 if (!m_pDS
->query(strSQL
))
9620 if (m_pDS
->num_rows() == 0)
9626 int iNumSongs
= m_pDS
->fv("NumSongs").get_asInt();
9633 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, filter
.where
);
9638 bool CMusicDatabase::GetAlbumPath(int idAlbum
, std::string
& basePath
)
9641 std::vector
<std::pair
<std::string
, int>> paths
;
9642 if (!GetAlbumPaths(idAlbum
, paths
))
9645 for (const auto& pathpair
: paths
)
9647 if (basePath
.empty())
9648 basePath
= pathpair
.first
.c_str();
9650 URIUtils::GetCommonPath(basePath
, pathpair
.first
.c_str());
9655 bool CMusicDatabase::GetAlbumPaths(int idAlbum
, std::vector
<std::pair
<std::string
, int>>& paths
)
9661 if (nullptr == m_pDB
)
9663 if (nullptr == m_pDS2
)
9666 // Get the unique paths of songs on the album, providing there are no songs from
9667 // other albums with the same path. This returns
9668 // a) <album> if is contains all the songs and no others, or
9669 // b) <album>/cd1, <album>/cd2 etc. for disc sets
9670 // but does *not* return any path when albums are mixed together. That could be because of
9671 // deliberate file organisation, or (more likely) because of a tagging error in album name
9672 // or Musicbrainzalbumid. Thus it avoids finding some generic music path.
9673 strSQL
= PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
9674 "JOIN path ON song.idPath = path.idPath "
9675 "WHERE song.idAlbum = %ld "
9676 "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
9677 "WHERE idPath = song.idPath) = 1",
9680 if (!m_pDS2
->query(strSQL
))
9682 if (m_pDS2
->num_rows() == 0)
9684 // Album does not have a unique path, files are mixed
9689 while (!m_pDS2
->eof())
9691 paths
.emplace_back(m_pDS2
->fv("strPath").get_asString(),
9692 m_pDS2
->fv("song.idPath").get_asInt());
9695 // Cleanup recordset data
9701 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
9707 int CMusicDatabase::GetDiscnumberForPathID(int idPath
)
9713 if (nullptr == m_pDB
)
9715 if (nullptr == m_pDS2
)
9718 strSQL
= PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
9719 "WHERE idPath = %i",
9722 if (!m_pDS2
->query(strSQL
))
9724 if (m_pDS2
->num_rows() == 1)
9725 { // Songs with this path have a unique disc number
9726 result
= m_pDS2
->fv("discnum").get_asInt();
9728 // Cleanup recordset data
9733 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
9738 // Get old "artist path" - where artist.nfo and art was located v17 and below.
9739 // It is the path common to all albums by an (album) artist, but ensure it is unique
9740 // to that artist and not shared with other artists. Previously this caused incorrect nfo
9741 // and art to be applied to multiple artists.
9742 bool CMusicDatabase::GetOldArtistPath(int idArtist
, std::string
& basePath
)
9747 if (nullptr == m_pDB
)
9749 if (nullptr == m_pDS2
)
9752 // find all albums from this artist, and all the paths to the songs from those albums
9753 std::string strSQL
= PrepareSQL("SELECT strPath FROM album_artist "
9754 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9755 "JOIN path ON song.idPath = path.idPath "
9756 "WHERE album_artist.idArtist = %ld "
9757 "GROUP BY song.idPath",
9761 if (!m_pDS2
->query(strSQL
))
9763 int iRowsFound
= m_pDS2
->num_rows();
9764 if (iRowsFound
== 0)
9766 // Artist is not an album artist, no path to find
9770 else if (iRowsFound
== 1)
9772 // Special case for single path - assume that we're in an artist/album/songs filesystem
9773 URIUtils::GetParentPath(m_pDS2
->fv("strPath").get_asString(), basePath
);
9778 // find the common path (if any) to these albums
9779 while (!m_pDS2
->eof())
9781 std::string path
= m_pDS2
->fv("strPath").get_asString();
9782 if (basePath
.empty())
9785 URIUtils::GetCommonPath(basePath
, path
);
9792 // Check any path found is unique to that album artist, and do *not* return any path
9793 // that is shared with other album artists. That could be because of collaborations
9794 // i.e. albums with more than one album artist, or because there are albums by the
9795 // artist on multiple music sources, or elsewhere in the folder hierarchy.
9796 // Avoid returning some generic music path.
9797 if (!basePath
.empty())
9799 strSQL
= PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
9800 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9801 "JOIN path ON song.idPath = path.idPath "
9802 "WHERE album_artist.idArtist <> %ld "
9803 "AND strPath LIKE '%s%%'",
9804 idArtist
, basePath
.c_str());
9805 std::string strValue
= GetSingleValue(strSQL
, m_pDS2
);
9806 if (!strValue
.empty())
9808 int countartists
= static_cast<int>(strtol(strValue
.c_str(), NULL
, 10));
9809 if (countartists
== 0)
9816 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
9822 bool CMusicDatabase::GetArtistPath(const CArtist
& artist
, std::string
& path
)
9824 // Get path for artist in the artists folder
9825 path
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
9826 CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER
);
9828 return false; // No Artists folder not set;
9829 // Get unique artist folder name
9830 std::string strFolder
;
9831 if (GetArtistFolderName(artist
, strFolder
))
9833 path
= URIUtils::AddFileToFolder(path
, strFolder
);
9840 bool CMusicDatabase::GetAlbumFolder(const CAlbum
& album
,
9841 const std::string
& strAlbumPath
,
9842 std::string
& strFolder
)
9845 // Get a name for the album folder that is unique for the artist to use when
9846 // exporting albums to separate nfo files in a folder under an artist folder
9848 // When given an album path (common to all the music files containing *only*
9849 // that album) check if that folder name is *unique* looking at folders on
9850 // all levels of the music file paths for the artist
9851 if (!strAlbumPath
.empty())
9853 // Get last folder from full path
9854 std::vector
<std::string
> folders
= URIUtils::SplitPath(strAlbumPath
);
9855 if (!folders
.empty())
9857 strFolder
= folders
.back();
9858 // The same folder name could be used on different paths for albums by the
9859 // same first artist. The albums could be totally different or also have
9860 // the same name (but different mbid). Be over cautious and look for the
9861 // name any where in the music file paths
9862 std::string strSQL
= PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
9863 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9864 "JOIN path on path.idPath = song.idPath "
9865 "WHERE album_artist.iOrder = 0 "
9866 "AND album_artist.idArtist = %ld "
9867 "AND path.strPath LIKE '%%\\%s\\%%'",
9868 album
.artistCredits
[0].GetArtistId(), strFolder
.c_str());
9870 if (!m_pDS2
->query(strSQL
))
9872 int iRowsFound
= m_pDS2
->num_rows();
9874 if (iRowsFound
== 1)
9878 // Create a valid unique folder name from album title
9879 // @todo: Does UFT8 matter or need normalizing?
9880 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9881 strFolder
= CUtil::MakeLegalFileName(album
.strAlbum
, LEGAL_WIN32_COMPAT
);
9882 StringUtils::Replace(strFolder
, " _ ", "_");
9884 // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
9885 // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
9886 // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
9887 // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
9888 std::string strSQL
= PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
9889 "JOIN album ON album_artist.idAlbum = album.idAlbum "
9890 "WHERE album_artist.iOrder = 0 "
9891 "AND album_artist.idArtist = %ld "
9892 "AND album.strAlbum LIKE '%s' ",
9893 album
.artistCredits
[0].GetArtistId(), album
.strAlbum
.c_str());
9894 std::string strValue
= GetSingleValue(strSQL
, m_pDS2
);
9895 if (strValue
.empty())
9897 int countalbum
= static_cast<int>(strtol(strValue
.c_str(), NULL
, 10));
9898 if (countalbum
> 1 && !album
.strMusicBrainzAlbumID
.empty())
9899 { // Only one of the duplicate albums can be without mbid
9900 strFolder
+= "_" + album
.strMusicBrainzAlbumID
.substr(0, 4);
9902 return !strFolder
.empty();
9905 bool CMusicDatabase::GetArtistFolderName(const CArtist
& artist
, std::string
& strFolder
)
9907 return GetArtistFolderName(artist
.strArtist
, artist
.strMusicBrainzArtistID
, strFolder
);
9910 bool CMusicDatabase::GetArtistFolderName(const std::string
& strArtist
,
9911 const std::string
& strMusicBrainzArtistID
,
9912 std::string
& strFolder
)
9914 // Create a valid unique folder name for artist
9915 // @todo: Does UFT8 matter or need normalizing?
9916 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9917 strFolder
= CUtil::MakeLegalFileName(strArtist
, LEGAL_WIN32_COMPAT
);
9918 StringUtils::Replace(strFolder
, " _ ", "_");
9920 // Ensure <artist name> is unique e.g. 2 x John Williams.
9921 // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
9922 // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
9923 // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
9924 std::string strSQL
=
9925 PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist
.c_str());
9926 std::string strValue
= GetSingleValue(strSQL
, m_pDS2
);
9927 if (strValue
.empty())
9929 int countartist
= static_cast<int>(strtol(strValue
.c_str(), NULL
, 10));
9930 if (countartist
> 1)
9931 strFolder
+= "_" + strMusicBrainzArtistID
.substr(0, 4);
9932 return !strFolder
.empty();
9935 int CMusicDatabase::AddSource(const std::string
& strName
,
9936 const std::string
& strMultipath
,
9937 const std::vector
<std::string
>& vecPaths
,
9943 if (nullptr == m_pDB
)
9945 if (nullptr == m_pDS
)
9948 // Check if source name already exists
9949 int idSource
= GetSourceByName(strName
);
9953 // Add new source and source paths
9955 strSQL
= PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9956 "VALUES(%i, '%s', '%s')",
9957 id
, strName
.c_str(), strMultipath
.c_str());
9959 strSQL
= PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
9960 "VALUES(NULL, '%s', '%s')",
9961 strName
.c_str(), strMultipath
.c_str());
9962 m_pDS
->exec(strSQL
);
9964 idSource
= static_cast<int>(m_pDS
->lastinsertid());
9967 for (const auto& path
: vecPaths
)
9969 strSQL
= PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
9970 "VALUES(%i,%i,'%s')",
9971 idSource
, idPath
, path
.c_str());
9972 m_pDS
->exec(strSQL
);
9976 // Find albums by song path, building WHERE for multiple source paths
9977 // (providing source has a path)
9978 if (vecPaths
.size() > 0)
9980 std::vector
<int> albumIds
;
9982 strSQL
= "SELECT DISTINCT idAlbum FROM song ";
9983 extFilter
.AppendJoin("JOIN path ON song.idPath = path.idPath");
9984 for (const auto& path
: vecPaths
)
9985 extFilter
.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path
.c_str()), false);
9986 if (!BuildSQL(strSQL
, extFilter
, strSQL
))
9989 if (!m_pDS
->query(strSQL
))
9992 while (!m_pDS
->eof())
9994 albumIds
.push_back(m_pDS
->fv("idAlbum").get_asInt());
9999 // Add album_source for related albums
10000 for (auto idAlbum
: albumIds
)
10002 strSQL
= PrepareSQL("INSERT INTO album_source (idSource, idAlbum) "
10003 "VALUES('%i', '%i')",
10004 idSource
, idAlbum
);
10005 m_pDS
->exec(strSQL
);
10008 CommitTransaction();
10014 CLog::Log(LOGERROR
, "{} failed with query ({})", __FUNCTION__
, strSQL
);
10015 RollbackTransaction();
10021 int CMusicDatabase::UpdateSource(const std::string
& strOldName
,
10022 const std::string
& strName
,
10023 const std::string
& strMultipath
,
10024 const std::vector
<std::string
>& vecPaths
)
10027 std::string strSourceMultipath
;
10028 std::string strSQL
;
10031 if (nullptr == m_pDB
)
10033 if (nullptr == m_pDS
)
10036 // Get details of named old source
10037 if (!strOldName
.empty())
10039 strSQL
= PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
10040 strOldName
.c_str());
10041 if (!m_pDS
->query(strSQL
))
10043 if (m_pDS
->num_rows() > 0)
10045 idSource
= m_pDS
->fv("idSource").get_asInt();
10046 strSourceMultipath
= m_pDS
->fv("strMultipath").get_asString();
10052 // Source not found, add new one
10053 return AddSource(strName
, strMultipath
, vecPaths
);
10056 // Nothing changed? (that we hold in db, other source details could be modified)
10057 bool pathschanged
= strMultipath
.compare(strSourceMultipath
) != 0;
10058 if (!pathschanged
&& strOldName
.compare(strName
) == 0)
10063 // Name changed? Could be that none of the values held in db changed
10064 if (strOldName
.compare(strName
) != 0)
10066 strSQL
= PrepareSQL("UPDATE source SET strName = '%s' "
10067 "WHERE idSource = %i",
10068 strName
.c_str(), idSource
);
10069 m_pDS
->exec(strSQL
);
10075 // Change paths (and name) by deleting and re-adding, but keep same ID
10076 strSQL
= PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource
);
10077 m_pDS
->exec(strSQL
);
10078 return AddSource(strName
, strMultipath
, vecPaths
, idSource
);
10083 CLog::Log(LOGERROR
, "{} failed with query ({})", __FUNCTION__
, strSQL
);
10084 RollbackTransaction();
10090 bool CMusicDatabase::RemoveSource(const std::string
& strName
)
10092 // Related album_source and source_path rows removed by trigger
10093 SetLibraryLastCleaned();
10094 return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName
.c_str()));
10097 int CMusicDatabase::GetSourceFromPath(const std::string
& strPath1
)
10099 std::string strSQL
;
10103 std::string
strPath(strPath1
);
10104 if (!URIUtils::HasSlashAtEnd(strPath
))
10105 URIUtils::AddSlashAtEnd(strPath
);
10107 if (nullptr == m_pDB
)
10109 if (nullptr == m_pDS
)
10112 // Check if path is a source matching on multipath
10113 strSQL
= PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath
.c_str());
10114 if (!m_pDS
->query(strSQL
))
10116 if (m_pDS
->num_rows() > 0)
10117 idSource
= m_pDS
->fv("idSource").get_asInt();
10122 // Check if path is a source path (of many) or a subfolder of a single source
10123 strSQL
= PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10124 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10126 if (!m_pDS
->query(strSQL
))
10128 if (m_pDS
->num_rows() == 1)
10129 idSource
= m_pDS
->fv("idSource").get_asInt();
10135 CLog::Log(LOGERROR
, "{} path: {} ({}) failed", __FUNCTION__
, strSQL
, strPath1
);
10141 bool CMusicDatabase::AddAlbumSource(int idAlbum
, int idSource
)
10143 std::string strSQL
;
10144 strSQL
= PrepareSQL("INSERT INTO album_source (idAlbum, idSource) "
10146 idAlbum
, idSource
);
10147 return ExecuteQuery(strSQL
);
10150 bool CMusicDatabase::AddAlbumSources(int idAlbum
, const std::string
& strPath
)
10152 std::string strSQL
;
10153 std::vector
<int> sourceIds
;
10156 if (nullptr == m_pDB
)
10158 if (nullptr == m_pDS
)
10161 if (!strPath
.empty())
10163 // Find sources related to album using album path
10164 strSQL
= PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10165 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10167 if (!m_pDS
->query(strSQL
))
10169 while (!m_pDS
->eof())
10171 sourceIds
.push_back(m_pDS
->fv("idSource").get_asInt());
10178 // Find sources using song paths, check each source path individually
10179 if (nullptr == m_pDS2
)
10181 strSQL
= "SELECT idSource, strPath FROM source_path";
10182 if (!m_pDS
->query(strSQL
))
10184 while (!m_pDS
->eof())
10186 std::string sourcepath
= m_pDS
->fv("strPath").get_asString();
10187 strSQL
= PrepareSQL("SELECT 1 FROM song "
10188 "JOIN path ON song.idPath = path.idPath "
10189 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
10190 sourcepath
.c_str());
10191 if (!m_pDS2
->query(strSQL
))
10193 if (m_pDS2
->num_rows() > 0)
10194 sourceIds
.push_back(m_pDS
->fv("idSource").get_asInt());
10202 //Add album sources
10203 for (auto idSource
: sourceIds
)
10205 AddAlbumSource(idAlbum
, idSource
);
10212 CLog::Log(LOGERROR
, "{} path: {} ({}) failed", __FUNCTION__
, strSQL
, strPath
);
10218 bool CMusicDatabase::DeleteAlbumSources(int idAlbum
)
10220 return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum
));
10223 bool CMusicDatabase::CheckSources(VECSOURCES
& sources
)
10225 if (sources
.empty())
10227 // Source table empty too?
10228 return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
10231 // Check number of entries matches
10232 size_t total
= static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
10233 if (total
!= sources
.size())
10236 // Check individual sources match
10239 if (nullptr == m_pDB
)
10241 if (nullptr == m_pDS
)
10244 std::string strSQL
;
10245 for (const auto& source
: sources
)
10247 // Check each source by name
10248 strSQL
= PrepareSQL("SELECT idSource, strMultipath FROM source "
10249 "WHERE strName LIKE '%s'",
10250 source
.strName
.c_str());
10251 m_pDS
->query(strSQL
);
10252 if (!m_pDS
->query(strSQL
))
10254 if (m_pDS
->num_rows() != 1)
10256 // Missing source, or name duplication
10262 // Check details. Encoded URLs of source.strPath matched to strMultipath
10263 // field, no need to look at individual paths of source_path table
10264 if (source
.strPath
.compare(m_pDS
->fv("strMultipath").get_asString()) != 0)
10277 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10282 bool CMusicDatabase::MigrateSources()
10284 //Fetch music sources from xml
10285 VECSOURCES
sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10287 std::string strSQL
;
10290 // Fill source and source paths tables
10291 for (const auto& source
: sources
)
10293 // AddSource(source.strName, source.strPath, source.vecPaths);
10295 strSQL
= PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
10296 "VALUES(NULL, '%s', '%s')",
10297 source
.strName
.c_str(), source
.strPath
.c_str());
10298 m_pDS
->exec(strSQL
);
10299 int idSource
= static_cast<int>(m_pDS
->lastinsertid());
10301 // Add new source paths
10303 for (const auto& path
: source
.vecPaths
)
10305 strSQL
= PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
10306 "VALUES(%i,%i,'%s')",
10307 idSource
, idPath
, path
.c_str());
10308 m_pDS
->exec(strSQL
);
10317 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, strSQL
);
10322 bool CMusicDatabase::UpdateSources()
10324 //Check library and xml sources match
10325 VECSOURCES
sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
10326 if (CheckSources(sources
))
10331 // Empty sources table (related link tables removed by trigger);
10332 ExecuteQuery("DELETE FROM source");
10334 // Fill source table, and album sources
10335 for (const auto& source
: sources
)
10336 AddSource(source
.strName
, source
.strPath
, source
.vecPaths
);
10342 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10347 bool CMusicDatabase::GetSources(CFileItemList
& items
)
10351 if (nullptr == m_pDB
)
10353 if (nullptr == m_pDS
)
10356 // Get music sources and individual source paths (may not be scanned or have albums etc.)
10357 std::string strSQL
=
10358 "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
10359 "FROM source JOIN source_path ON source.idSource = source_path.idSource "
10360 "ORDER BY source.idSource, source_path.idPath";
10362 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
10363 if (!m_pDS
->query(strSQL
))
10365 int iRowsFound
= m_pDS
->num_rows();
10366 if (iRowsFound
== 0)
10372 // Get data from returned rows
10373 // Item has source ID in MusicInfotag, multipath in path, and individual paths in property
10374 CVariant
sourcePaths(CVariant::VariantTypeArray
);
10376 while (!m_pDS
->eof())
10378 if (idSource
!= m_pDS
->fv("source.idSource").get_asInt())
10380 if (idSource
> 0 && !sourcePaths
.empty())
10382 //Store paths for previous source in item list
10383 items
[items
.Size() - 1].get()->SetProperty("paths", sourcePaths
);
10384 sourcePaths
.clear();
10386 idSource
= m_pDS
->fv("source.idSource").get_asInt();
10387 CFileItemPtr
pItem(new CFileItem(m_pDS
->fv("source.strName").get_asString()));
10388 pItem
->GetMusicInfoTag()->SetDatabaseId(idSource
, "source");
10389 // Set tag URL for "file" property in AudioLibary processing
10390 pItem
->GetMusicInfoTag()->SetURL(m_pDS
->fv("source.strMultipath").get_asString());
10391 // Set item path as source URL encoded multipath too
10392 pItem
->SetPath(m_pDS
->fv("source.strMultiPath").get_asString());
10394 pItem
->m_bIsFolder
= true;
10398 sourcePaths
.push_back(m_pDS
->fv("source_path.strPath").get_asString());
10402 if (!sourcePaths
.empty())
10404 //Store paths for final source
10405 items
[items
.Size() - 1].get()->SetProperty("paths", sourcePaths
);
10406 sourcePaths
.clear();
10416 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10421 bool CMusicDatabase::GetSourcesByArtist(int idArtist
, CFileItem
* item
)
10423 if (nullptr == m_pDB
)
10425 if (nullptr == m_pDS
)
10430 std::string strSQL
;
10431 strSQL
= PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
10432 "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
10433 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
10434 "WHERE artist.idArtist = %i "
10435 "ORDER BY album_source.idSource",
10437 if (!m_pDS
->query(strSQL
))
10439 if (m_pDS
->num_rows() == 0)
10441 // Artist does have any source via albums may not be an album artist.
10442 // Check via songs fetch sources from compilations or where they are guest artist
10444 strSQL
= PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
10445 "JOIN song ON song_artist.idSong = song.idSong "
10446 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
10447 "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
10448 "ORDER BY album_source.idSource",
10450 if (!m_pDS
->query(strSQL
))
10452 if (m_pDS
->num_rows() == 0)
10454 //No sources, but query successful
10460 CVariant
artistSources(CVariant::VariantTypeArray
);
10461 while (!m_pDS
->eof())
10463 artistSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10468 item
->SetProperty("sourceid", artistSources
);
10473 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idArtist
);
10478 bool CMusicDatabase::GetSourcesByAlbum(int idAlbum
, CFileItem
* item
)
10480 if (nullptr == m_pDB
)
10482 if (nullptr == m_pDS
)
10487 std::string strSQL
;
10488 strSQL
= PrepareSQL("SELECT idSource FROM album_source "
10489 "WHERE album_source.idAlbum = %i "
10490 "ORDER BY idSource",
10492 if (!m_pDS
->query(strSQL
))
10494 CVariant
albumSources(CVariant::VariantTypeArray
);
10495 if (m_pDS
->num_rows() > 0)
10497 while (!m_pDS
->eof())
10499 albumSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10506 //! @todo: handle singles, or don't waste time checking songs
10507 // Album does have any sources, may be a single??
10508 // Check via song paths, check each source path individually
10509 // usually fewer source paths than songs
10512 if (nullptr == m_pDS2
)
10514 strSQL
= "SELECT idSource, strPath FROM source_path";
10515 if (!m_pDS
->query(strSQL
))
10517 while (!m_pDS
->eof())
10519 std::string sourcepath
= m_pDS
->fv("strPath").get_asString();
10520 strSQL
= PrepareSQL("SELECT 1 FROM song "
10521 "JOIN path ON song.idPath = path.idPath "
10522 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
10523 idAlbum
, sourcepath
.c_str());
10524 if (!m_pDS2
->query(strSQL
))
10526 if (m_pDS2
->num_rows() > 0)
10527 albumSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10536 item
->SetProperty("sourceid", albumSources
);
10541 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idAlbum
);
10546 bool CMusicDatabase::GetSourcesBySong(int idSong
, const std::string
& strPath1
, CFileItem
* item
)
10548 if (nullptr == m_pDB
)
10550 if (nullptr == m_pDS
)
10555 std::string strSQL
;
10556 strSQL
= PrepareSQL("SELECT idSource FROM song "
10557 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
10558 "WHERE song.idSong = %i "
10559 "ORDER BY idSource",
10561 if (!m_pDS
->query(strSQL
))
10563 if (m_pDS
->num_rows() == 0 && !strPath1
.empty())
10565 // Check via song path instead
10567 std::string
strPath(strPath1
);
10568 if (!URIUtils::HasSlashAtEnd(strPath
))
10569 URIUtils::AddSlashAtEnd(strPath
);
10571 strSQL
= PrepareSQL("SELECT DISTINCT idSource FROM source_path "
10572 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
10574 if (!m_pDS
->query(strSQL
))
10577 CVariant
songSources(CVariant::VariantTypeArray
);
10578 while (!m_pDS
->eof())
10580 songSources
.push_back(m_pDS
->fv("idSource").get_asInt());
10585 item
->SetProperty("sourceid", songSources
);
10590 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, idSong
);
10595 int CMusicDatabase::GetSourceByName(const std::string
& strSource
)
10599 if (nullptr == m_pDB
)
10601 if (nullptr == m_pDS
)
10604 std::string strSQL
;
10605 strSQL
= PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource
.c_str());
10607 if (!m_pDS
->query(strSQL
))
10609 int iRowsFound
= m_pDS
->num_rows();
10610 if (iRowsFound
!= 1)
10615 return m_pDS
->fv("idSource").get_asInt();
10619 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10624 std::string
CMusicDatabase::GetSourceById(int id
)
10626 return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id
));
10629 int CMusicDatabase::GetArtistByName(const std::string
& strArtist
)
10633 if (nullptr == m_pDB
)
10635 if (nullptr == m_pDS
)
10638 std::string strSQL
= PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'",
10639 strArtist
.c_str());
10642 if (!m_pDS
->query(strSQL
))
10644 int iRowsFound
= m_pDS
->num_rows();
10645 if (iRowsFound
!= 1)
10650 int lResult
= m_pDS
->fv("artist.idArtist").get_asInt();
10656 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10661 int CMusicDatabase::GetArtistByMatch(const CArtist
& artist
)
10663 std::string strSQL
;
10666 if (nullptr == m_pDB
|| nullptr == m_pDS
)
10668 // Match on MusicBrainz ID, definitively unique
10669 if (!artist
.strMusicBrainzArtistID
.empty())
10670 strSQL
= PrepareSQL("SELECT idArtist FROM artist "
10671 "WHERE strMusicBrainzArtistID = '%s'",
10672 artist
.strMusicBrainzArtistID
.c_str());
10674 // No MusicBrainz ID, artist by name with no mbid
10675 strSQL
= PrepareSQL("SELECT idArtist FROM artist "
10676 "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
10677 artist
.strArtist
.c_str());
10678 if (!m_pDS
->query(strSQL
))
10680 int iRowsFound
= m_pDS
->num_rows();
10681 if (iRowsFound
!= 1)
10684 // Match on artist name, relax mbid restriction
10685 return GetArtistByName(artist
.strArtist
);
10687 int lResult
= m_pDS
->fv("idArtist").get_asInt();
10693 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
10698 bool CMusicDatabase::GetArtistFromSong(int idSong
, CArtist
& artist
)
10702 if (nullptr == m_pDB
)
10704 if (nullptr == m_pDS
)
10707 std::string strSQL
= PrepareSQL(
10708 "SELECT artistview.* FROM song_artist "
10709 "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
10710 "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
10712 if (!m_pDS
->query(strSQL
))
10714 int iRowsFound
= m_pDS
->num_rows();
10715 if (iRowsFound
!= 1)
10721 artist
= GetArtistFromDataset(m_pDS
.get());
10728 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10733 bool CMusicDatabase::IsSongArtist(int idSong
, int idArtist
)
10735 std::string strSQL
= PrepareSQL("SELECT 1 FROM song_artist "
10736 "WHERE song_artist.idSong= %i AND "
10737 "song_artist.idArtist = %i AND song_artist.idRole = 1",
10739 return GetSingleValue(strSQL
).empty();
10742 bool CMusicDatabase::IsSongAlbumArtist(int idSong
, int idArtist
)
10744 std::string strSQL
=
10745 PrepareSQL("SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
10746 "WHERE song.idSong = %i AND album_artist.idArtist = %i",
10748 return GetSingleValue(strSQL
).empty();
10751 bool CMusicDatabase::IsAlbumBoxset(int idAlbum
)
10753 std::string strSQL
= PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum
);
10754 int isBoxSet
= GetSingleValueInt(strSQL
);
10755 return (isBoxSet
== 1 ? true : false);
10758 int CMusicDatabase::GetAlbumByName(const std::string
& strAlbum
, const std::string
& strArtist
)
10762 if (nullptr == m_pDB
)
10764 if (nullptr == m_pDS
)
10767 std::string strSQL
;
10768 if (strArtist
.empty())
10770 PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum
.c_str());
10772 strSQL
= PrepareSQL("SELECT idAlbum FROM album "
10773 "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
10774 strAlbum
.c_str(), strArtist
.c_str());
10776 if (!m_pDS
->query(strSQL
))
10778 int iRowsFound
= m_pDS
->num_rows();
10779 if (iRowsFound
!= 1)
10784 return m_pDS
->fv("idAlbum").get_asInt();
10788 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10793 bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string
& strAlbum
,
10794 const std::string
& strArtist
,
10796 std::string
& strReview
)
10799 Get the first album that matches with the title and artist display name.
10800 Artist(s) and album title may not be sufficient to uniquely identify a match since library can
10801 store multiple releases, and occasionally artists even have different albums with same name.
10802 Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
10806 if (nullptr == m_pDB
)
10808 if (nullptr == m_pDS
)
10811 std::string strSQL
;
10812 if (strArtist
.empty())
10813 strSQL
= PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
10816 strSQL
= PrepareSQL("SELECT idAlbum, strReview FROM album "
10817 "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
10818 strAlbum
.c_str(), strArtist
.c_str());
10820 if (!m_pDS
->query(strSQL
))
10822 int iRowsFound
= m_pDS
->num_rows();
10823 if (iRowsFound
> 0)
10825 idAlbum
= m_pDS
->fv("idAlbum").get_asInt();
10826 strReview
= m_pDS
->fv("strReview").get_asString();
10832 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10837 bool CMusicDatabase::SearchAlbumsByArtistName(const std::string
& strArtist
, CFileItemList
& items
)
10841 if (nullptr == m_pDB
)
10843 if (nullptr == m_pDS
)
10846 std::string strSQL
;
10847 strSQL
= PrepareSQL("SELECT albumview.* FROM albumview "
10848 "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
10849 "WHERE album_artist.strArtist LIKE '%s'",
10850 strArtist
.c_str());
10852 if (!m_pDS
->query(strSQL
))
10855 while (!m_pDS
->eof())
10857 CAlbum album
= GetAlbumFromDataset(m_pDS
.get());
10858 std::string path
= StringUtils::Format("musicdb://albums/{}/", album
.idAlbum
);
10859 CFileItemPtr
pItem(new CFileItem(path
, album
));
10860 std::string label
=
10861 StringUtils::Format("{} ({})", album
.strAlbum
, pItem
->GetMusicInfoTag()->GetYear());
10862 pItem
->SetLabel(label
);
10866 m_pDS
->close(); // cleanup recordset data
10871 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
10876 int CMusicDatabase::GetAlbumByName(const std::string
& strAlbum
,
10877 const std::vector
<std::string
>& artist
)
10879 return GetAlbumByName(
10883 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
));
10886 int CMusicDatabase::GetAlbumByMatch(const CAlbum
& album
)
10888 std::string strSQL
;
10891 if (nullptr == m_pDB
|| nullptr == m_pDS
)
10893 // Match on MusicBrainz ID, definitively unique
10894 if (!album
.strMusicBrainzAlbumID
.empty())
10895 strSQL
= PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'",
10896 album
.strMusicBrainzAlbumID
.c_str());
10898 // No mbid, match on album title and album artist descriptive string, ignore those with mbid
10899 strSQL
= PrepareSQL("SELECT idAlbum FROM album "
10900 "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
10901 "AND strMusicBrainzAlbumID IS NULL",
10902 album
.GetAlbumArtistString().c_str(), album
.strAlbum
.c_str());
10903 m_pDS
->query(strSQL
);
10904 if (!m_pDS
->query(strSQL
))
10906 int iRowsFound
= m_pDS
->num_rows();
10907 if (iRowsFound
!= 1)
10910 // Match on album title and album artist descriptive string, relax mbid restriction
10911 return GetAlbumByName(album
.strAlbum
, album
.GetAlbumArtistString());
10913 int lResult
= m_pDS
->fv("idAlbum").get_asInt();
10919 CLog::Log(LOGERROR
, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__
, strSQL
);
10924 std::string
CMusicDatabase::GetGenreById(int id
)
10926 return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id
));
10929 std::string
CMusicDatabase::GetArtistById(int id
)
10931 return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id
));
10934 std::string
CMusicDatabase::GetRoleById(int id
)
10936 return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id
));
10939 bool CMusicDatabase::UpdateArtistSortNames(int idArtist
/*=-1*/)
10941 // Propagate artist sort names into concatenated artist sort name string for songs and albums
10942 // Avoid updating records where sort same as strArtistDisp
10943 std::string strSQL
;
10945 // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
10946 // (not handled by PrepareSQL)
10947 bool bisMySQL
= StringUtils::EqualsNoCase(
10948 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic
.type
, "mysql");
10950 BeginMultipleExecute();
10952 strSQL
= "(SELECT GROUP_CONCAT("
10953 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10954 "ELSE artist.strSortName END "
10955 "ORDER BY album_artist.idAlbum, album_artist.iOrder "
10956 "SEPARATOR '; ') as val "
10957 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10958 "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
10960 strSQL
= "(SELECT GROUP_CONCAT(val, '; ') "
10961 "FROM(SELECT album_artist.idAlbum, "
10962 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10963 "ELSE artist.strSortName END as val "
10964 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10965 "WHERE album_artist.idAlbum = album.idAlbum "
10966 "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
10968 strSQL
= "UPDATE album SET strArtistSort = " + strSQL
+
10969 "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
10970 "AND strArtistDisp <> " +
10974 PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
10975 "AND album_artist.idAlbum = album.idAlbum)",
10977 ExecuteQuery(strSQL
);
10978 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
10981 strSQL
= "(SELECT GROUP_CONCAT("
10982 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10983 "ELSE artist.strSortName END "
10984 "ORDER BY song_artist.idSong, song_artist.iOrder "
10985 "SEPARATOR '; ') as val "
10986 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10987 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
10989 strSQL
= "(SELECT GROUP_CONCAT(val, '; ') "
10990 "FROM(SELECT song_artist.idSong, "
10991 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10992 "ELSE artist.strSortName END as val "
10993 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10994 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
10995 "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
10997 strSQL
= "UPDATE song SET strArtistSort = " + strSQL
+
10998 "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
10999 "AND strArtistDisp <> " +
11002 strSQL
+= PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
11003 "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)",
11005 ExecuteQuery(strSQL
);
11006 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
11008 if (CommitMultipleExecute())
11011 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
11015 std::string
CMusicDatabase::GetAlbumById(int id
)
11017 return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id
));
11020 int CMusicDatabase::GetGenreByName(const std::string
& strGenre
)
11024 if (nullptr == m_pDB
)
11026 if (nullptr == m_pDS
)
11029 std::string strSQL
;
11030 strSQL
= PrepareSQL("SELECT idGenre FROM genre "
11031 "WHERE genre.strGenre LIKE '%s'",
11034 if (!m_pDS
->query(strSQL
))
11036 int iRowsFound
= m_pDS
->num_rows();
11037 if (iRowsFound
!= 1)
11042 return m_pDS
->fv("genre.idGenre").get_asInt();
11046 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
11051 bool CMusicDatabase::GetGenresJSON(CFileItemList
& items
, bool bSources
)
11053 std::string strSQL
;
11056 if (nullptr == m_pDB
)
11058 if (nullptr == m_pDS
)
11061 strSQL
= "SELECT %s FROM genre ";
11063 extFilter
.AppendField("genre.idGenre");
11064 extFilter
.AppendField("genre.strGenre");
11067 strSQL
= "SELECT DISTINCT %s FROM genre ";
11068 extFilter
.AppendField("album_source.idSource");
11069 extFilter
.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
11070 extFilter
.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
11071 extFilter
.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
11072 extFilter
.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
11073 extFilter
.AppendOrder("genre.strGenre");
11074 extFilter
.AppendOrder("album_source.idSource");
11076 extFilter
.AppendWhere("genre.strGenre != ''");
11078 std::string strSQLExtra
;
11079 if (!BuildSQL(strSQLExtra
, extFilter
, strSQLExtra
))
11082 strSQL
= PrepareSQL(strSQL
, extFilter
.fields
.c_str()) + strSQLExtra
;
11085 CLog::Log(LOGDEBUG
, "{} query: {}", __FUNCTION__
, strSQL
);
11087 if (!m_pDS
->query(strSQL
))
11089 int iRowsFound
= m_pDS
->num_rows();
11090 if (iRowsFound
== 0)
11097 items
.Reserve(iRowsFound
);
11099 // Get data from returned rows
11100 // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
11101 CVariant
genreSources(CVariant::VariantTypeArray
);
11103 while (!m_pDS
->eof())
11105 if (idGenre
!= m_pDS
->fv("genre.idGenre").get_asInt())
11107 if (idGenre
> 0 && bSources
)
11109 //Store sources for previous genre in item list
11110 items
[items
.Size() - 1].get()->SetProperty("sourceid", genreSources
);
11111 genreSources
.clear();
11113 idGenre
= m_pDS
->fv("genre.idGenre").get_asInt();
11114 std::string strGenre
= m_pDS
->fv("genre.strGenre").get_asString();
11115 CFileItemPtr
pItem(new CFileItem(strGenre
));
11116 pItem
->GetMusicInfoTag()->SetTitle(strGenre
);
11117 pItem
->GetMusicInfoTag()->SetGenre(strGenre
);
11118 pItem
->GetMusicInfoTag()->SetDatabaseId(idGenre
, "genre");
11119 pItem
->SetPath(StringUtils::Format("musicdb://genres/{}/", idGenre
));
11120 pItem
->m_bIsFolder
= true;
11126 int sourceid
= m_pDS
->fv("album_source.idSource").get_asInt();
11128 genreSources
.push_back(sourceid
);
11134 //Store sources for final genre
11135 items
[items
.Size() - 1].get()->SetProperty("sourceid", genreSources
);
11145 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, strSQL
);
11150 std::string
CMusicDatabase::GetAlbumDiscTitle(int idAlbum
, int idDisc
)
11152 // Get disc node title from ids allowing for "*all"
11153 std::string disctitle
;
11154 std::string albumtitle
;
11156 albumtitle
= GetAlbumById(idAlbum
);
11159 disctitle
= GetSingleValue("song", "strDiscSubtitle",
11160 PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum
, idDisc
));
11161 if (disctitle
.empty())
11162 disctitle
= StringUtils::Format("{} {}", g_localizeStrings
.Get(427), idDisc
); // "Disc 1" etc.
11163 if (albumtitle
.empty())
11164 albumtitle
= disctitle
;
11166 albumtitle
= albumtitle
+ " - " + disctitle
;
11171 int CMusicDatabase::GetBoxsetsCount()
11173 return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
11176 int CMusicDatabase::GetAlbumDiscsCount(int idAlbum
)
11178 std::string strSQL
= PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum
);
11179 return GetSingleValueInt(strSQL
);
11182 int CMusicDatabase::GetCompilationAlbumsCount()
11184 return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
11187 int CMusicDatabase::GetSinglesCount()
11189 CDatabase::Filter
filter(
11190 PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
11191 CAlbum::ReleaseTypeToString(CAlbum::Single
).c_str()));
11192 return GetSongsCount(filter
);
11195 int CMusicDatabase::GetArtistCountForRole(int role
)
11197 std::string strSQL
= PrepareSQL(
11198 "SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role
);
11199 return GetSingleValueInt(strSQL
);
11202 int CMusicDatabase::GetArtistCountForRole(const std::string
& strRole
)
11204 std::string strSQL
= PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist "
11205 "JOIN role ON song_artist.idRole = role.idRole "
11206 "WHERE role.strRole LIKE '%s'",
11208 return GetSingleValueInt(strSQL
);
11211 bool CMusicDatabase::SetPathHash(const std::string
& path
, const std::string
& hash
)
11215 if (nullptr == m_pDB
)
11217 if (nullptr == m_pDS
)
11221 { // this is an empty folder - we need only add it to the path table
11222 // if the path actually exists
11223 if (!CDirectory::Exists(path
))
11226 int idPath
= AddPath(path
);
11230 std::string strSQL
=
11231 PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash
.c_str(), idPath
);
11232 m_pDS
->exec(strSQL
);
11238 CLog::Log(LOGERROR
, "{} ({}, {}) failed", __FUNCTION__
, path
, hash
);
11244 bool CMusicDatabase::GetPathHash(const std::string
& path
, std::string
& hash
)
11248 if (nullptr == m_pDB
)
11250 if (nullptr == m_pDS
)
11253 std::string strSQL
= PrepareSQL("select strHash from path where strPath='%s'", path
.c_str());
11254 m_pDS
->query(strSQL
);
11255 if (m_pDS
->num_rows() == 0)
11257 hash
= m_pDS
->fv("strHash").get_asString();
11262 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, path
);
11268 bool CMusicDatabase::RemoveSongsFromPath(const std::string
& path1
, MAPSONGS
& songmap
, bool exact
)
11270 // We need to remove all songs from this path, as their tags are going
11271 // to be re-read. We need to remove all songs from the song table + all links to them
11272 // from the song link tables (as otherwise if a song is added back
11273 // to the table with the same idSong, these tables can't be cleaned up properly later)
11275 //! @todo SQLite probably doesn't allow this, but can we rely on that??
11277 // We don't need to remove orphaned albums at this point as in AddAlbum() we check
11278 // first whether the album has already been read during this scan, and if it hasn't
11279 // we check whether it's in the table and update accordingly at that point, removing the entries from
11280 // the album link tables. The only failure point for this is albums
11281 // that span multiple folders, where just the files in one folder have been changed. In this case
11282 // any linked fields that are only in the files that haven't changed will be removed. Clearly
11283 // the primary albumartist still matches (as that's what we looked up based on) so is this really
11284 // an issue? I don't think it is, as those artists will still have links to the album via the songs
11285 // which is generally what we rely on, so the only failure point is albumartist lookup. In this
11286 // case, it will return only things in the album_artist table from the newly updated songs (and
11287 // only if they have additional artists). I think the effect of this is minimal at best, as ALL
11288 // songs in the album should have the same albumartist!
11290 // we also remove the path at this point as it will be added later on if the
11291 // path still exists.
11292 // After scanning we then remove the orphaned artists, genres and thumbs.
11294 // Note: when used to remove all songs from a path and its subpath (exact=false), this
11295 // does miss archived songs.
11296 std::string
path(path1
);
11297 SetLibraryLastUpdated();
11300 if (!URIUtils::HasSlashAtEnd(path
))
11301 URIUtils::AddSlashAtEnd(path
);
11303 if (nullptr == m_pDB
)
11305 if (nullptr == m_pDS
)
11308 // Filename is not unique for a path as songs from a cuesheet have same filename.
11309 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
11310 // in a folder and some edited and rescanned.
11311 // Hence order by filename so these songs can be gathered together.
11314 where
= PrepareSQL(" WHERE strPath='%s'", path
.c_str());
11316 where
= PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path
.c_str()),
11318 std::string sql
= "SELECT * FROM songview" + where
+ " ORDER BY strFileName";
11319 if (!m_pDS
->query(sql
))
11321 int iRowsFound
= m_pDS
->num_rows();
11322 if (iRowsFound
> 0)
11324 // Each file is potentially mapped to a list of songs, gather these and save as list
11326 std::string filename
;
11327 std::vector
<std::string
> songIds
;
11328 while (!m_pDS
->eof())
11330 CSong song
= GetSongFromDataset();
11331 if (!filename
.empty() && filename
!= song
.strFileName
)
11333 // Save songs for previous filename
11334 songmap
.insert(std::make_pair(filename
, songs
));
11337 song
.strThumb
= GetArtForItem(song
.idSong
, MediaTypeSong
, "thumb");
11338 songs
.emplace_back(song
);
11339 songIds
.push_back(PrepareSQL("%i", song
.idSong
));
11340 filename
= song
.strFileName
;
11345 songmap
.insert(std::make_pair(filename
, songs
)); // Save songs for last filename
11347 //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
11348 for (const auto& id
: songIds
)
11349 AnnounceRemove(MediaTypeSong
, atoi(id
.c_str()));
11351 // Delete all songs, and anything linked to them via triggers
11352 std::string strIDs
= StringUtils::Join(songIds
, ",");
11353 sql
= "DELETE FROM song WHERE idSong in (" + strIDs
+ ")";
11356 // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
11357 sql
= "delete from path" + where
;
11359 return iRowsFound
> 0;
11363 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, path
);
11368 void CMusicDatabase::CheckArtistLinksChanged()
11370 std::string strSQL
= "SELECT COUNT(1) FROM removed_link ";
11371 int iLinks
= GetSingleValueInt(strSQL
, m_pDS
);
11374 SetArtistLinksUpdated(); // Store datetime artist links last updated
11375 DeleteRemovedLinks(); // Clean-up artist links
11379 bool CMusicDatabase::GetPaths(std::set
<std::string
>& paths
)
11383 if (nullptr == m_pDB
)
11385 if (nullptr == m_pDS
)
11391 if (!m_pDS
->query("SELECT strPath FROM path"))
11393 int iRowsFound
= m_pDS
->num_rows();
11394 if (iRowsFound
== 0)
11399 while (!m_pDS
->eof())
11401 paths
.insert(m_pDS
->fv("strPath").get_asString());
11409 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
11414 bool CMusicDatabase::SetSongUserrating(const std::string
& filePath
, int userrating
)
11418 if (filePath
.empty())
11420 if (nullptr == m_pDB
)
11422 if (nullptr == m_pDS
)
11425 int songID
= GetSongIDFromPath(filePath
);
11429 return SetSongUserrating(songID
, userrating
);
11433 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, filePath
, userrating
);
11438 bool CMusicDatabase::SetSongUserrating(int idSong
, int userrating
)
11442 if (nullptr == m_pDB
)
11444 if (nullptr == m_pDS
)
11448 PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating
, idSong
);
11454 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, idSong
, userrating
);
11459 bool CMusicDatabase::SetAlbumUserrating(const int idAlbum
, int userrating
)
11463 if (nullptr == m_pDB
)
11465 if (nullptr == m_pDS
)
11471 PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating
, idAlbum
);
11477 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, idAlbum
, userrating
);
11482 bool CMusicDatabase::SetSongVotes(const std::string
& filePath
, int votes
)
11486 if (filePath
.empty())
11488 if (nullptr == m_pDB
)
11490 if (nullptr == m_pDS
)
11493 int songID
= GetSongIDFromPath(filePath
);
11497 std::string sql
= PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes
, songID
);
11504 CLog::Log(LOGERROR
, "{} ({},{}) failed", __FUNCTION__
, filePath
, votes
);
11509 int CMusicDatabase::GetSongIDFromPath(const std::string
& filePath
)
11511 // grab the where string to identify the song id
11512 CURL
url(filePath
);
11513 if (url
.IsProtocol("musicdb"))
11515 std::string strFile
= URIUtils::GetFileName(filePath
);
11516 URIUtils::RemoveExtension(strFile
);
11517 return atoi(strFile
.c_str());
11522 if (nullptr == m_pDB
)
11524 if (nullptr == m_pDS
)
11527 std::string strPath
, strFileName
;
11528 SplitPath(filePath
, strPath
, strFileName
);
11529 URIUtils::AddSlashAtEnd(strPath
);
11531 std::string sql
= PrepareSQL("SELECT idSong FROM song JOIN path ON song.idPath = path.idPath "
11532 "WHERE song.strFileName='%s' AND path.strPath='%s'",
11533 strFileName
.c_str(), strPath
.c_str());
11534 if (!m_pDS
->query(sql
))
11537 if (m_pDS
->num_rows() == 0)
11543 int songID
= m_pDS
->fv("idSong").get_asInt();
11549 CLog::Log(LOGERROR
, "{} ({}) failed", __FUNCTION__
, filePath
);
11554 bool CMusicDatabase::CommitTransaction()
11556 if (CDatabase::CommitTransaction())
11557 { // number of items in the db has likely changed, so reset the infomanager cache
11558 CGUIComponent
* gui
= CServiceBroker::GetGUI();
11561 gui
->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(
11562 LIBRARY_HAS_MUSIC
, GetSongsCount() > 0);
11569 bool CMusicDatabase::SetScraperAll(const std::string
& strBaseDir
, const ADDON::ScraperPtr
& scraper
)
11571 if (nullptr == m_pDB
)
11573 if (nullptr == m_pDS
)
11575 std::string strSQL
;
11576 int idSetting
= -1;
11579 CONTENT_TYPE content
= CONTENT_NONE
;
11581 // Build where clause from virtual path
11583 CMusicDbUrl musicUrl
;
11584 SortDescription sorting
;
11585 if (!musicUrl
.FromString(strBaseDir
) || !GetFilter(musicUrl
, extFilter
, sorting
))
11588 std::string itemType
= musicUrl
.GetType();
11589 if (StringUtils::EqualsNoCase(itemType
, "artists"))
11591 content
= CONTENT_ARTISTS
;
11593 else if (StringUtils::EqualsNoCase(itemType
, "albums"))
11595 content
= CONTENT_ALBUMS
;
11598 return false; //Only artists and albums have info settings
11600 std::string strSQLWhere
;
11601 if (!BuildSQL(strSQLWhere
, extFilter
, strSQLWhere
))
11604 // Replace view names with table names
11605 StringUtils::Replace(strSQLWhere
, "artistview", "artist");
11606 StringUtils::Replace(strSQLWhere
, "albumview", "album");
11608 BeginTransaction();
11609 // Clear current scraper settings (0 => default scraper used)
11610 if (content
== CONTENT_ARTISTS
)
11611 strSQL
= "UPDATE artist SET idInfoSetting = %i ";
11613 strSQL
= "UPDATE album SET idInfoSetting = %i ";
11614 strSQL
= PrepareSQL(strSQL
, 0) + strSQLWhere
;
11615 m_pDS
->exec(strSQL
);
11617 //Remove orphaned settings
11618 CleanupInfoSettings();
11622 // Add new info setting
11623 strSQL
= "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
11624 strSQL
= PrepareSQL(strSQL
, scraper
->ID().c_str(), scraper
->GetPathSettings().c_str());
11625 m_pDS
->exec(strSQL
);
11626 idSetting
= static_cast<int>(m_pDS
->lastinsertid());
11628 if (content
== CONTENT_ARTISTS
)
11629 strSQL
= "UPDATE artist SET idInfoSetting = %i ";
11631 strSQL
= "UPDATE album SET idInfoSetting = %i ";
11632 strSQL
= PrepareSQL(strSQL
, idSetting
) + strSQLWhere
;
11633 m_pDS
->exec(strSQL
);
11635 CommitTransaction();
11640 RollbackTransaction();
11641 CLog::Log(LOGERROR
, "{} - ({}, {}) failed", __FUNCTION__
, strBaseDir
, strSQL
);
11646 bool CMusicDatabase::SetScraper(int id
,
11647 const CONTENT_TYPE
& content
,
11648 const ADDON::ScraperPtr
& scraper
)
11650 if (nullptr == m_pDB
)
11652 if (nullptr == m_pDS
)
11654 std::string strSQL
;
11655 int idSetting
= -1;
11658 BeginTransaction();
11659 // Fetch current info settings for item, 0 => default is used
11660 if (content
== CONTENT_ARTISTS
)
11661 strSQL
= "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
11663 strSQL
= "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
11664 strSQL
= PrepareSQL(strSQL
, id
);
11665 m_pDS
->query(strSQL
);
11666 if (m_pDS
->num_rows() > 0)
11667 idSetting
= m_pDS
->fv("idInfoSetting").get_asInt();
11671 { // Add new info setting
11672 strSQL
= "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
11673 strSQL
= PrepareSQL(strSQL
, scraper
->ID().c_str(), scraper
->GetPathSettings().c_str());
11674 m_pDS
->exec(strSQL
);
11675 idSetting
= static_cast<int>(m_pDS
->lastinsertid());
11677 if (content
== CONTENT_ARTISTS
)
11678 strSQL
= "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
11680 strSQL
= "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
11681 strSQL
= PrepareSQL(strSQL
, idSetting
, id
);
11682 m_pDS
->exec(strSQL
);
11685 { // Update info setting
11686 strSQL
= "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' "
11687 "WHERE idSetting = %i";
11689 PrepareSQL(strSQL
, scraper
->ID().c_str(), scraper
->GetPathSettings().c_str(), idSetting
);
11690 m_pDS
->exec(strSQL
);
11692 CommitTransaction();
11697 RollbackTransaction();
11698 CLog::Log(LOGERROR
, "{} - ({}, {}) failed", __FUNCTION__
, id
, strSQL
);
11703 bool CMusicDatabase::GetScraper(int id
, const CONTENT_TYPE
& content
, ADDON::ScraperPtr
& scraper
)
11705 std::string scraperUUID
;
11706 std::string strSettings
;
11709 if (nullptr == m_pDB
)
11711 if (nullptr == m_pDS
)
11714 std::string strSQL
;
11715 strSQL
= "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
11716 if (content
== CONTENT_ARTISTS
)
11717 strSQL
= strSQL
+ "artist ON artist.idInfoSetting = infosetting.idSetting "
11718 "WHERE artist.idArtist = %i";
11720 strSQL
= strSQL
+ "album ON album.idInfoSetting = infosetting.idSetting "
11721 "WHERE album.idAlbum = %i";
11722 strSQL
= PrepareSQL(strSQL
, id
);
11723 m_pDS
->query(strSQL
);
11725 { // try and ascertain scraper
11726 scraperUUID
= m_pDS
->fv("strScraperPath").get_asString();
11727 strSettings
= m_pDS
->fv("strSettings").get_asString();
11729 // Use pre configured or default scraper
11730 ADDON::AddonPtr addon
;
11731 if (!scraperUUID
.empty() &&
11732 CServiceBroker::GetAddonMgr().GetAddon(scraperUUID
, addon
,
11733 ADDON::OnlyEnabled::CHOICE_YES
) &&
11736 scraper
= std::dynamic_pointer_cast
<ADDON::CScraper
>(addon
);
11739 scraper
->SetPathSettings(content
, strSettings
);
11745 { // use default music scraper instead
11746 ADDON::AddonPtr addon
;
11747 if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
11748 ADDON::ScraperTypeFromContent(content
), addon
))
11750 scraper
= std::dynamic_pointer_cast
<ADDON::CScraper
>(addon
);
11751 return scraper
!= NULL
;
11761 CLog::Log(LOGERROR
, "{} -({}, {} {}) failed", __FUNCTION__
, id
, scraperUUID
, strSettings
);
11766 bool CMusicDatabase::ScraperInUse(const std::string
& scraperID
) const
11770 if (nullptr == m_pDB
)
11772 if (nullptr == m_pDS
)
11776 PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID
.c_str());
11777 if (!m_pDS
->query(sql
) || m_pDS
->num_rows() == 0)
11782 bool found
= m_pDS
->fv(0).get_asInt() > 0;
11788 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, scraperID
);
11793 bool CMusicDatabase::GetItems(const std::string
& strBaseDir
,
11794 CFileItemList
& items
,
11795 const Filter
& filter
/* = Filter() */,
11796 const SortDescription
& sortDescription
/* = SortDescription() */)
11798 CMusicDbUrl musicUrl
;
11799 if (!musicUrl
.FromString(strBaseDir
))
11802 return GetItems(strBaseDir
, musicUrl
.GetType(), items
, filter
, sortDescription
);
11805 bool CMusicDatabase::GetItems(const std::string
& strBaseDir
,
11806 const std::string
& itemType
,
11807 CFileItemList
& items
,
11808 const Filter
& filter
/* = Filter() */,
11809 const SortDescription
& sortDescription
/* = SortDescription() */)
11811 if (StringUtils::EqualsNoCase(itemType
, "genres"))
11812 return GetGenresNav(strBaseDir
, items
, filter
);
11813 else if (StringUtils::EqualsNoCase(itemType
, "sources"))
11814 return GetSourcesNav(strBaseDir
, items
, filter
);
11815 else if (StringUtils::EqualsNoCase(itemType
, "years"))
11816 return GetYearsNav(strBaseDir
, items
, filter
);
11817 else if (StringUtils::EqualsNoCase(itemType
, "roles"))
11818 return GetRolesNav(strBaseDir
, items
, filter
);
11819 else if (StringUtils::EqualsNoCase(itemType
, "artists"))
11820 return GetArtistsNav(strBaseDir
, items
,
11821 !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
11822 CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS
),
11823 -1, -1, -1, filter
, sortDescription
);
11824 else if (StringUtils::EqualsNoCase(itemType
, "albums"))
11825 return GetAlbumsByWhere(strBaseDir
, filter
, items
, sortDescription
);
11826 else if (StringUtils::EqualsNoCase(itemType
, "discs"))
11827 return GetDiscsByWhere(strBaseDir
, filter
, items
, sortDescription
);
11828 else if (StringUtils::EqualsNoCase(itemType
, "songs"))
11829 return GetSongsFullByWhere(strBaseDir
, filter
, items
, sortDescription
, true);
11834 std::string
CMusicDatabase::GetItemById(const std::string
& itemType
, int id
)
11836 if (StringUtils::EqualsNoCase(itemType
, "genres"))
11837 return GetGenreById(id
);
11838 else if (StringUtils::EqualsNoCase(itemType
, "sources"))
11839 return GetSourceById(id
);
11840 else if (StringUtils::EqualsNoCase(itemType
, "years"))
11841 return std::to_string(id
);
11842 else if (StringUtils::EqualsNoCase(itemType
, "artists"))
11843 return GetArtistById(id
);
11844 else if (StringUtils::EqualsNoCase(itemType
, "albums"))
11845 return GetAlbumById(id
);
11846 else if (StringUtils::EqualsNoCase(itemType
, "roles"))
11847 return GetRoleById(id
);
11852 void CMusicDatabase::ExportToXML(const CLibExportSettings
& settings
,
11853 CGUIDialogProgress
* progressDialog
/*= nullptr*/)
11855 if (!settings
.IsItemExported(ELIBEXPORT_ALBUMARTISTS
) &&
11856 !settings
.IsItemExported(ELIBEXPORT_SONGARTISTS
) &&
11857 !settings
.IsItemExported(ELIBEXPORT_OTHERARTISTS
) &&
11858 !settings
.IsItemExported(ELIBEXPORT_ALBUMS
) && !settings
.IsItemExported(ELIBEXPORT_SONGS
))
11861 // Exporting albums either art or NFO (or both) selected
11862 if ((settings
.IsToLibFolders() || settings
.IsSeparateFiles()) && settings
.m_skipnfo
&&
11863 !settings
.m_artwork
&& settings
.IsItemExported(ELIBEXPORT_ALBUMS
))
11866 std::string strFolder
;
11867 if (settings
.IsSingleFile() || settings
.IsSeparateFiles())
11869 // Exporting to single file or separate files in a specified location
11870 if (settings
.m_strPath
.empty())
11873 strFolder
= settings
.m_strPath
;
11874 if (!URIUtils::HasSlashAtEnd(strFolder
))
11875 URIUtils::AddSlashAtEnd(strFolder
);
11876 strFolder
= URIUtils::GetDirectory(strFolder
);
11877 if (strFolder
.empty())
11880 else if (settings
.IsArtistFoldersOnly() || (settings
.IsToLibFolders() && settings
.IsArtists()))
11882 // Exporting artist folders only, or artist NFO or art to library folders
11883 // need Artist Information Folder defined.
11884 // (Album NFO and art goes to music folders)
11885 strFolder
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
11886 CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER
);
11887 if (strFolder
.empty())
11892 bool artistfoldersonly
;
11893 artistfoldersonly
= settings
.IsArtistFoldersOnly() ||
11894 ((settings
.IsToLibFolders() || settings
.IsSeparateFiles()) &&
11895 settings
.m_skipnfo
&& !settings
.m_artwork
);
11897 int iFailCount
= 0;
11900 if (nullptr == m_pDB
)
11902 if (nullptr == m_pDS
)
11904 if (nullptr == m_pDS2
)
11907 // Create our xml document
11908 CXBMCTinyXML xmlDoc
;
11909 TiXmlDeclaration
decl("1.0", "UTF-8", "yes");
11910 xmlDoc
.InsertEndChild(decl
);
11911 TiXmlNode
* pMain
= NULL
;
11912 if ((settings
.IsToLibFolders() || settings
.IsSeparateFiles()) && !artistfoldersonly
)
11914 else if (settings
.IsSingleFile())
11916 TiXmlElement
xmlMainElement("musicdb");
11917 pMain
= xmlDoc
.InsertEndChild(xmlMainElement
);
11920 if (settings
.IsItemExported(ELIBEXPORT_ALBUMS
) && !artistfoldersonly
)
11922 // Find albums to export
11923 std::vector
<int> albumIds
;
11924 std::string strSQL
= PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
11925 CAlbum::ReleaseTypeToString(CAlbum::Album
).c_str());
11926 if (!settings
.m_unscraped
)
11927 strSQL
+= "AND lastScraped IS NOT NULL";
11928 CLog::Log(LOGDEBUG
, "CMusicDatabase::{} - {}", __FUNCTION__
, strSQL
);
11929 m_pDS
->query(strSQL
);
11931 int total
= m_pDS
->num_rows();
11934 albumIds
.reserve(total
);
11935 while (!m_pDS
->eof())
11937 albumIds
.push_back(m_pDS
->fv("idAlbum").get_asInt());
11942 for (const auto& albumId
: albumIds
)
11945 GetAlbum(albumId
, album
);
11946 std::string strAlbumPath
;
11947 std::string strPath
;
11948 // Get album path, empty unless all album songs are under a unique folder, and
11949 // there are no songs from another album in the same folder.
11950 if (!GetAlbumPath(albumId
, strAlbumPath
))
11951 strAlbumPath
.clear();
11952 if (settings
.IsSingleFile())
11954 // Save album to xml, including album path
11955 album
.Save(pMain
, "album", strAlbumPath
);
11958 { // Separate files and artwork
11959 bool pathfound
= false;
11960 if (settings
.IsToLibFolders())
11961 { // Save album.nfo and artwork with music files.
11962 // Most albums are under a unique folder, but if songs from various albums are mixed then
11963 // avoid overwriting by not allow NFO and art to be exported
11964 if (strAlbumPath
.empty())
11965 CLog::Log(LOGDEBUG
,
11966 "CMusicDatabase::{} - Not exporting album {} as unique path not found",
11967 __FUNCTION__
, album
.strAlbum
);
11968 else if (!CDirectory::Exists(strAlbumPath
))
11971 "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist",
11972 __FUNCTION__
, album
.strAlbum
, strAlbumPath
);
11975 strPath
= strAlbumPath
;
11980 { // Save album.nfo and artwork to subfolder on export path
11981 // strPath = strFolder/<albumartist name>/<albumname>
11982 // where <albumname> is either the same name as the album folder
11983 // containing the music files (if unique) or is created using the album name
11984 std::string strAlbumArtist
;
11985 pathfound
= GetArtistFolderName(album
.GetAlbumArtist()[0],
11986 album
.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist
);
11989 strPath
= URIUtils::AddFileToFolder(strFolder
, strAlbumArtist
);
11990 pathfound
= CDirectory::Exists(strPath
);
11992 pathfound
= CDirectory::Create(strPath
);
11995 CLog::Log(LOGDEBUG
,
11996 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
11997 __FUNCTION__
, album
.strAlbum
, strPath
);
12000 std::string strAlbumFolder
;
12001 pathfound
= GetAlbumFolder(album
, strAlbumPath
, strAlbumFolder
);
12004 strPath
= URIUtils::AddFileToFolder(strPath
, strAlbumFolder
);
12005 pathfound
= CDirectory::Exists(strPath
);
12007 pathfound
= CDirectory::Create(strPath
);
12010 CLog::Log(LOGDEBUG
,
12011 "CMusicDatabase::{} - Not exporting album {} as could not create {}",
12012 __FUNCTION__
, album
.strAlbum
, strPath
);
12017 if (!settings
.m_skipnfo
)
12019 // Save album to NFO, including album path
12020 album
.Save(pMain
, "album", strAlbumPath
);
12021 std::string nfoFile
= URIUtils::AddFileToFolder(strPath
, "album.nfo");
12022 if (settings
.m_overwrite
|| !CFile::Exists(nfoFile
))
12024 if (!xmlDoc
.SaveFile(nfoFile
))
12026 CLog::Log(LOGERROR
, "CMusicDatabase::{}: Album nfo export failed! ('{}')",
12027 __FUNCTION__
, nfoFile
);
12028 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error
,
12029 g_localizeStrings
.Get(20302),
12030 CURL::GetRedacted(nfoFile
));
12035 if (settings
.m_artwork
)
12037 // Save art in album folder
12038 // Note thumb resolution may be lower than original when overwriting
12039 std::map
<std::string
, std::string
> artwork
;
12040 std::string savedArtfile
;
12041 if (GetArtForItem(album
.idAlbum
, MediaTypeAlbum
, artwork
))
12043 for (const auto& art
: artwork
)
12045 if (art
.first
== "thumb")
12046 savedArtfile
= URIUtils::AddFileToFolder(strPath
, "folder");
12048 savedArtfile
= URIUtils::AddFileToFolder(strPath
, art
.first
);
12049 CServiceBroker::GetTextureCache()->Export(art
.second
, savedArtfile
,
12050 settings
.m_overwrite
);
12055 xmlDoc
.InsertEndChild(decl
); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
12059 if ((current
% 50) == 0 && progressDialog
)
12061 progressDialog
->SetLine(1, CVariant
{album
.strAlbum
});
12062 progressDialog
->SetPercentage(current
* 100 / total
);
12063 if (progressDialog
->IsCanceled())
12070 // Export song playback history to single file only
12071 if (settings
.IsSingleFile() && settings
.IsItemExported(ELIBEXPORT_SONGS
))
12073 if (!ExportSongHistory(pMain
, progressDialog
))
12077 if ((settings
.IsArtists() || artistfoldersonly
) && !strFolder
.empty())
12079 // Find artists to export
12080 std::vector
<int> artistIds
;
12083 if (settings
.IsItemExported(ELIBEXPORT_ALBUMARTISTS
))
12084 filter
.AppendWhere("EXISTS(SELECT 1 FROM album_artist "
12085 "WHERE album_artist.idArtist = artist.idArtist)",
12087 if (settings
.IsItemExported(ELIBEXPORT_SONGARTISTS
))
12089 if (settings
.IsItemExported(ELIBEXPORT_OTHERARTISTS
))
12090 filter
.AppendWhere("EXISTS (SELECT 1 FROM song_artist "
12091 "WHERE song_artist.idArtist = artist.idArtist )",
12094 filter
.AppendWhere(
12095 "EXISTS (SELECT 1 FROM song_artist "
12096 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)",
12099 else if (settings
.IsItemExported(ELIBEXPORT_OTHERARTISTS
))
12100 filter
.AppendWhere(
12101 "EXISTS (SELECT 1 FROM song_artist "
12102 "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)",
12105 if (!settings
.m_unscraped
&& !artistfoldersonly
)
12106 filter
.AppendWhere("lastScraped IS NOT NULL", true);
12108 std::string strSQL
= "SELECT idArtist FROM artist";
12109 BuildSQL(strSQL
, filter
, strSQL
);
12110 CLog::Log(LOGDEBUG
, "CMusicDatabase::{} - {}", __FUNCTION__
, strSQL
);
12112 m_pDS
->query(strSQL
);
12113 int total
= m_pDS
->num_rows();
12115 artistIds
.reserve(total
);
12116 while (!m_pDS
->eof())
12118 artistIds
.push_back(m_pDS
->fv("idArtist").get_asInt());
12123 for (const auto& artistId
: artistIds
)
12126 // Include discography when not folders only
12127 GetArtist(artistId
, artist
, !artistfoldersonly
);
12128 std::string strPath
;
12129 std::map
<std::string
, std::string
> artwork
;
12130 if (settings
.IsSingleFile())
12132 // Save artist to xml, and old path (common to music files) if it has one
12133 GetOldArtistPath(artist
.idArtist
, strPath
);
12134 artist
.Save(pMain
, "artist", strPath
);
12136 if (GetArtForItem(artist
.idArtist
, MediaTypeArtist
, artwork
))
12137 { // append to the XML
12138 TiXmlElement
additionalNode("art");
12139 for (const auto& i
: artwork
)
12140 XMLUtils::SetString(&additionalNode
, i
.first
.c_str(), i
.second
);
12141 pMain
->LastChild()->InsertEndChild(additionalNode
);
12145 { // Separate files: artist.nfo and artwork in strFolder/<artist name>
12146 // Get unique folder allowing for duplicate names e.g. 2 x John Williams
12147 bool pathfound
= GetArtistFolderName(artist
, strPath
);
12150 strPath
= URIUtils::AddFileToFolder(strFolder
, strPath
);
12151 pathfound
= CDirectory::Exists(strPath
);
12153 pathfound
= CDirectory::Create(strPath
);
12156 CLog::Log(LOGDEBUG
,
12157 "CMusicDatabase::{} - Not exporting artist {} as could not create {}",
12158 __FUNCTION__
, artist
.strArtist
, strPath
);
12161 if (!artistfoldersonly
)
12163 if (!settings
.m_skipnfo
)
12165 artist
.Save(pMain
, "artist", strPath
);
12166 std::string nfoFile
= URIUtils::AddFileToFolder(strPath
, "artist.nfo");
12167 if (settings
.m_overwrite
|| !CFile::Exists(nfoFile
))
12169 if (!xmlDoc
.SaveFile(nfoFile
))
12171 CLog::Log(LOGERROR
, "CMusicDatabase::{}: Artist nfo export failed! ('{}')",
12172 __FUNCTION__
, nfoFile
);
12173 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error
,
12174 g_localizeStrings
.Get(20302),
12175 CURL::GetRedacted(nfoFile
));
12180 if (settings
.m_artwork
)
12182 std::string savedArtfile
;
12183 if (GetArtForItem(artist
.idArtist
, MediaTypeArtist
, artwork
))
12185 for (const auto& art
: artwork
)
12187 if (art
.first
== "thumb")
12188 savedArtfile
= URIUtils::AddFileToFolder(strPath
, "folder");
12190 savedArtfile
= URIUtils::AddFileToFolder(strPath
, art
.first
);
12191 CServiceBroker::GetTextureCache()->Export(art
.second
, savedArtfile
,
12192 settings
.m_overwrite
);
12197 xmlDoc
.InsertEndChild(decl
); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
12201 if ((current
% 50) == 0 && progressDialog
)
12203 progressDialog
->SetLine(1, CVariant
{artist
.strArtist
});
12204 progressDialog
->SetPercentage(current
* 100 / total
);
12205 if (progressDialog
->IsCanceled())
12212 if (settings
.IsSingleFile())
12214 std::string xmlFile
= URIUtils::AddFileToFolder(
12215 strFolder
, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
12216 if (CFile::Exists(xmlFile
))
12217 xmlFile
= URIUtils::AddFileToFolder(
12218 strFolder
, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
12219 xmlDoc
.SaveFile(xmlFile
);
12222 data
["file"] = xmlFile
;
12223 if (iFailCount
> 0)
12224 data
["failcount"] = iFailCount
;
12225 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary
, "OnExport",
12231 CLog::Log(LOGERROR
, "CMusicDatabase::{} failed", __FUNCTION__
);
12235 if (progressDialog
)
12236 progressDialog
->Close();
12238 if (iFailCount
> 0 && progressDialog
)
12239 HELPERS::ShowOKDialogLines(
12240 CVariant
{20196}, CVariant
{StringUtils::Format(g_localizeStrings
.Get(15011), iFailCount
)});
12243 bool CMusicDatabase::ExportSongHistory(TiXmlNode
* pNode
, CGUIDialogProgress
* progressDialog
)
12247 // Export songs with some playback history
12248 std::string strSQL
=
12249 "SELECT idSong, song.idAlbum, "
12250 "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, "
12251 "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, "
12252 "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating "
12253 "FROM song JOIN album on album.idAlbum = song.idAlbum "
12254 "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0";
12256 CLog::Log(LOGDEBUG
, "{0} - {1}", __FUNCTION__
, strSQL
);
12257 m_pDS
->query(strSQL
);
12259 int total
= m_pDS
->num_rows();
12261 while (!m_pDS
->eof())
12263 TiXmlElement
songElement("song");
12264 TiXmlNode
* song
= pNode
->InsertEndChild(songElement
);
12266 XMLUtils::SetInt(song
, "idsong", m_pDS
->fv("idSong").get_asInt());
12267 XMLUtils::SetString(song
, "artistdesc", m_pDS
->fv("strArtistDisp").get_asString());
12268 XMLUtils::SetString(song
, "title", m_pDS
->fv("strTitle").get_asString());
12269 XMLUtils::SetInt(song
, "track", m_pDS
->fv("iTrack").get_asInt());
12270 XMLUtils::SetString(song
, "filename", m_pDS
->fv("strFilename").get_asString());
12271 XMLUtils::SetString(song
, "musicbrainztrackid",
12272 m_pDS
->fv("strMusicBrainzTrackID").get_asString());
12273 XMLUtils::SetInt(song
, "idalbum", m_pDS
->fv("idAlbum").get_asInt());
12274 XMLUtils::SetString(song
, "albumtitle", m_pDS
->fv("strAlbum").get_asString());
12275 XMLUtils::SetString(song
, "musicbrainzalbumid",
12276 m_pDS
->fv("strMusicBrainzAlbumID").get_asString());
12277 XMLUtils::SetString(song
, "albumartistdesc", m_pDS
->fv("strAlbumArtistDisp").get_asString());
12278 XMLUtils::SetInt(song
, "timesplayed", m_pDS
->fv("iTimesplayed").get_asInt());
12279 XMLUtils::SetString(song
, "lastplayed", m_pDS
->fv("lastplayed").get_asString());
12280 auto* rating
= XMLUtils::SetString(
12281 song
, "rating", StringUtils::FormatNumber(m_pDS
->fv("rating").get_asFloat()));
12283 rating
->ToElement()->SetAttribute("max", 10);
12284 XMLUtils::SetInt(song
, "votes", m_pDS
->fv("votes").get_asInt());
12285 auto* userrating
= XMLUtils::SetInt(song
, "userrating", m_pDS
->fv("userrating").get_asInt());
12287 userrating
->ToElement()->SetAttribute("max", 10);
12289 if ((current
% 100) == 0 && progressDialog
)
12291 progressDialog
->SetLine(1, CVariant
{m_pDS
->fv("strAlbum").get_asString()});
12292 progressDialog
->SetPercentage(current
* 100 / total
);
12293 if (progressDialog
->IsCanceled())
12308 CLog::Log(LOGERROR
, "{0} failed", __FUNCTION__
);
12313 void CMusicDatabase::ImportFromXML(const std::string
& xmlFile
, CGUIDialogProgress
* progressDialog
)
12317 if (nullptr == m_pDB
)
12319 if (nullptr == m_pDS
)
12322 CXBMCTinyXML xmlDoc
;
12323 if (!xmlDoc
.LoadFile(xmlFile
) && progressDialog
)
12325 HELPERS::ShowOKDialogLines(CVariant
{20197}, CVariant
{38354}); //"Unable to read xml file"
12329 TiXmlElement
* root
= xmlDoc
.RootElement();
12333 TiXmlElement
* entry
= root
->FirstChildElement();
12337 // Count the number of artists, albums and songs
12340 if (StringUtils::CompareNoCase(entry
->Value(), "artist", 6) == 0 ||
12341 StringUtils::CompareNoCase(entry
->Value(), "album", 5) == 0)
12343 else if (StringUtils::CompareNoCase(entry
->Value(), "song", 4) == 0)
12346 entry
= entry
->NextSiblingElement();
12349 BeginTransaction();
12350 entry
= root
->FirstChildElement();
12353 std::string strTitle
;
12354 if (StringUtils::CompareNoCase(entry
->Value(), "artist", 6) == 0)
12356 CArtist importedArtist
;
12357 importedArtist
.Load(entry
);
12358 strTitle
= importedArtist
.strArtist
;
12360 // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
12361 int idArtist
= GetArtistByMatch(importedArtist
);
12365 GetArtist(idArtist
, artist
, true); // include discography
12366 artist
.MergeScrapedArtist(importedArtist
, true);
12367 UpdateArtist(artist
);
12370 CLog::Log(LOGDEBUG
, "{} - Not import additional artist data as {} not found",
12371 __FUNCTION__
, importedArtist
.strArtist
);
12374 else if (StringUtils::CompareNoCase(entry
->Value(), "album", 5) == 0)
12376 CAlbum importedAlbum
;
12377 importedAlbum
.Load(entry
);
12378 strTitle
= importedAlbum
.strAlbum
;
12379 // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
12380 int idAlbum
= GetAlbumByMatch(importedAlbum
);
12384 GetAlbum(idAlbum
, album
, true);
12385 album
.MergeScrapedAlbum(importedAlbum
, true);
12386 UpdateAlbum(album
); //Will replace song artists if present in xml
12389 CLog::Log(LOGDEBUG
, "{} - Not import additional album data as {} not found", __FUNCTION__
,
12390 importedAlbum
.strAlbum
);
12394 entry
= entry
->NextSiblingElement();
12395 if (progressDialog
&& total
)
12397 progressDialog
->SetPercentage(current
* 100 / total
);
12398 progressDialog
->SetLine(2, CVariant
{std::move(strTitle
)});
12399 progressDialog
->Progress();
12400 if (progressDialog
->IsCanceled())
12402 RollbackTransaction();
12407 CommitTransaction();
12409 // Import song playback history <song> entries found
12411 if (!ImportSongHistory(xmlFile
, songtotal
, progressDialog
))
12414 CGUIComponent
* gui
= CServiceBroker::GetGUI();
12416 gui
->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
12420 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
12421 RollbackTransaction();
12423 if (progressDialog
)
12424 progressDialog
->Close();
12427 bool CMusicDatabase::ImportSongHistory(const std::string
& xmlFile
,
12429 CGUIDialogProgress
* progressDialog
)
12431 bool bHistSongExists
= false;
12434 CXBMCTinyXML xmlDoc
;
12435 if (!xmlDoc
.LoadFile(xmlFile
))
12438 TiXmlElement
* root
= xmlDoc
.RootElement();
12442 TiXmlElement
* entry
= root
->FirstChildElement();
12445 if (progressDialog
)
12447 progressDialog
->SetLine(1, CVariant
{38350}); //"Importing song playback history"
12448 progressDialog
->SetLine(2, CVariant
{""});
12451 // As can be many songs do in db, not song at a time which would be slow
12452 // Convert xml entries into a SQL bulk insert statement
12453 std::string strSQL
;
12454 entry
= root
->FirstChildElement();
12457 std::string strArtistDisp
;
12458 std::string strTitle
;
12460 std::string strFilename
;
12461 std::string strMusicBrainzTrackID
;
12462 std::string strAlbum
;
12463 std::string strMusicBrainzAlbumID
;
12464 std::string strAlbumArtistDisp
;
12466 std::string lastplayed
;
12467 int iUserrating
= 0;
12468 float fRating
= 0.0;
12470 std::string strSQLSong
;
12471 if (StringUtils::CompareNoCase(entry
->Value(), "song", 4) == 0)
12473 XMLUtils::GetString(entry
, "artistdesc", strArtistDisp
);
12474 XMLUtils::GetString(entry
, "title", strTitle
);
12475 XMLUtils::GetInt(entry
, "track", iTrack
);
12476 XMLUtils::GetString(entry
, "filename", strFilename
);
12477 XMLUtils::GetString(entry
, "musicbrainztrackid", strMusicBrainzTrackID
);
12478 XMLUtils::GetString(entry
, "albumtitle", strAlbum
);
12479 XMLUtils::GetString(entry
, "musicbrainzalbumid", strMusicBrainzAlbumID
);
12480 XMLUtils::GetString(entry
, "albumartistdesc", strAlbumArtistDisp
);
12481 XMLUtils::GetInt(entry
, "timesplayed", iTimesplayed
);
12482 XMLUtils::GetString(entry
, "lastplayed", lastplayed
);
12483 const TiXmlElement
* rElement
= entry
->FirstChildElement("rating");
12487 float max_rating
= 10;
12488 XMLUtils::GetFloat(entry
, "rating", rating
);
12489 if (rElement
->QueryFloatAttribute("max", &max_rating
) == TIXML_SUCCESS
&& max_rating
>= 1)
12490 rating
*= (10.f
/ max_rating
); // Normalise the value to between 0 and 10
12495 XMLUtils::GetInt(entry
, "votes", iVotes
);
12496 const TiXmlElement
* userrating
= entry
->FirstChildElement("userrating");
12500 float max_rating
= 10;
12501 XMLUtils::GetFloat(entry
, "userrating", rating
);
12502 if (userrating
->QueryFloatAttribute("max", &max_rating
) == TIXML_SUCCESS
&&
12504 rating
*= (10.f
/ max_rating
); // Normalise the value to between 0 and 10
12507 iUserrating
= MathUtils::round_int(static_cast<double>(rating
));
12510 strSQLSong
= PrepareSQL("(%d, %d, ", current
+ 1, iTrack
);
12511 strSQLSong
+= PrepareSQL("'%s', '%s', '%s', ", strArtistDisp
.c_str(), strTitle
.c_str(),
12512 strFilename
.c_str());
12513 if (strMusicBrainzTrackID
.empty())
12514 strSQLSong
+= PrepareSQL("NULL, ");
12516 strSQLSong
+= PrepareSQL("'%s', ", strMusicBrainzTrackID
.c_str());
12517 strSQLSong
+= PrepareSQL("'%s', '%s', ", strAlbum
.c_str(), strAlbumArtistDisp
.c_str());
12518 if (strMusicBrainzAlbumID
.empty())
12519 strSQLSong
+= PrepareSQL("NULL, ");
12521 strSQLSong
+= PrepareSQL("'%s', ", strMusicBrainzAlbumID
.c_str());
12522 strSQLSong
+= PrepareSQL("%d, ", iTimesplayed
);
12523 if (lastplayed
.empty())
12524 strSQLSong
+= PrepareSQL("NULL, ");
12526 strSQLSong
+= PrepareSQL("'%s', ", lastplayed
.c_str());
12528 PrepareSQL("%.1f, %d, %d, -1, -1)", static_cast<double>(fRating
), iVotes
, iUserrating
);
12531 strSQLSong
= ", " + strSQLSong
;
12532 strSQL
+= strSQLSong
;
12536 entry
= entry
->NextSiblingElement();
12538 if ((current
% 100) == 0 && progressDialog
)
12540 progressDialog
->SetPercentage(current
* 100 / total
);
12541 progressDialog
->SetLine(3, CVariant
{std::move(strTitle
)});
12542 progressDialog
->Progress();
12543 if (progressDialog
->IsCanceled())
12548 CLog::Log(LOGINFO
, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__
,
12550 /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of
12551 song table using correlated subqueries to a temp table. An updatable join
12552 to temp table would work in MySQL but SQLite not support updatable joins.
12554 m_pDS
->exec("CREATE TABLE HistSong ("
12555 "idSongSrc INTEGER primary key, "
12556 "strAlbum varchar(256), "
12557 "strMusicBrainzAlbumID text, "
12558 "strAlbumArtistDisp text, "
12559 "strArtistDisp text, strTitle varchar(512), "
12560 "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, "
12561 "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, "
12562 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
12563 "userrating INTEGER NOT NULL DEFAULT 0, "
12564 "idAlbum INTEGER, idSong INTEGER)");
12565 bHistSongExists
= true;
12567 strSQL
= "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, "
12568 "strFileName, strMusicBrainzTrackID, "
12569 "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, "
12570 " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " +
12572 m_pDS
->exec(strSQL
);
12574 if (progressDialog
)
12576 progressDialog
->SetLine(2, CVariant
{38351}); //"Matching data"
12577 progressDialog
->SetLine(3, CVariant
{""});
12578 progressDialog
->Progress();
12579 if (progressDialog
->IsCanceled())
12581 m_pDS
->exec("DROP TABLE HistSong");
12586 BeginTransaction();
12587 // Match albums first on mbid then artist string and album title, setting idAlbum
12588 // mbid is unique so subquery can only return one result at most
12589 strSQL
= "UPDATE HistSong "
12590 "SET idAlbum = (SELECT album.idAlbum FROM album "
12591 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) "
12592 "WHERE EXISTS(SELECT 1 FROM album "
12593 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0";
12594 m_pDS
->exec(strSQL
);
12596 // Can only be one album with same title and artist(s) and no mbid.
12597 // But could have 2 releases one with and one without mbid, match up those without mbid
12598 strSQL
= "UPDATE HistSong "
12599 "SET idAlbum = (SELECT album.idAlbum FROM album "
12600 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12601 "AND HistSong.strAlbum = album.strAlbum "
12602 "AND album.strMusicBrainzAlbumID IS NULL "
12603 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
12604 "WHERE EXISTS(SELECT 1 FROM album "
12605 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12606 "AND HistSong.strAlbum = album.strAlbum "
12607 "AND album.strMusicBrainzAlbumID IS NULL "
12608 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
12610 m_pDS
->exec(strSQL
);
12612 // Try match rest by title and artist(s), prioritise one without mbid
12613 // Target could have multiple releases - with mbid (non-matching) or one without mbid
12614 strSQL
= "UPDATE HistSong "
12615 "SET idAlbum = (SELECT album.idAlbum FROM album "
12616 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12617 "AND HistSong.strAlbum = album.strAlbum "
12618 "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
12619 "WHERE EXISTS(SELECT 1 FROM album "
12620 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
12621 "AND HistSong.strAlbum = album.strAlbum) "
12623 m_pDS
->exec(strSQL
);
12624 if (progressDialog
)
12626 progressDialog
->Progress();
12627 if (progressDialog
->IsCanceled())
12629 RollbackTransaction();
12630 m_pDS
->exec("DROP TABLE HistSong");
12635 // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong
12636 strSQL
= "UPDATE HistSong "
12637 "SET idSong = (SELECT idsong FROM song "
12638 "WHERE HistSong.idAlbum = song.idAlbum AND "
12639 "HistSong.iTrack = song.iTrack AND "
12640 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) "
12641 "WHERE EXISTS(SELECT 1 FROM song "
12642 "WHERE HistSong.idAlbum = song.idAlbum AND "
12643 "HistSong.iTrack = song.iTrack AND "
12644 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0";
12645 m_pDS
->exec(strSQL
);
12647 // An album can have more than one song with same track and title (although idAlbum, track and
12648 // title is often unique), but not using filename as an identifier to allow for import of song
12649 // history for renamed files. It is about song playback not file playback.
12651 strSQL
= "UPDATE HistSong "
12652 "SET idSong = (SELECT idsong FROM song "
12653 "WHERE HistSong.idAlbum = song.idAlbum AND "
12654 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
12655 "WHERE EXISTS(SELECT 1 FROM song "
12656 "WHERE HistSong.idAlbum = song.idAlbum AND "
12657 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0";
12658 m_pDS
->exec(strSQL
);
12660 CommitTransaction();
12661 if (progressDialog
)
12663 progressDialog
->Progress();
12664 if (progressDialog
->IsCanceled())
12666 m_pDS
->exec("DROP TABLE HistSong");
12671 // Create an index to speed up the updates
12672 m_pDS
->exec("CREATE INDEX idxHistSong ON HistSong(idSong)");
12674 // Log how many songs matched
12675 int unmatched
= GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS
);
12676 CLog::Log(LOGINFO
, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__
,
12677 total
- unmatched
, total
);
12679 if (progressDialog
)
12681 progressDialog
->SetLine(2, CVariant
{38352}); //"Updating song playback history"
12682 progressDialog
->Progress();
12683 if (progressDialog
->IsCanceled())
12685 m_pDS
->exec("DROP TABLE HistSong"); // Drops index too
12690 /* Update song table using the song ids we have matched.
12691 Use correlated subqueries as SQLite does not support updatable joins.
12692 MySQL requires HistSong table not to be defined temporary for this.
12695 BeginTransaction();
12696 // Times played and last played date(when count is greater)
12697 strSQL
= "UPDATE song SET iTimesPlayed = "
12698 "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), "
12700 "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) "
12701 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12702 "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)";
12703 m_pDS
->exec(strSQL
);
12706 strSQL
= "UPDATE song SET userrating = "
12707 "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) "
12708 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12709 "HistSong.idSong = song.idSong AND HistSong.userrating > 0)";
12710 m_pDS
->exec(strSQL
);
12712 // Rating and votes
12713 strSQL
= "UPDATE song SET rating = "
12714 "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), "
12716 "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) "
12717 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
12718 "HistSong.idSong = song.idSong AND HistSong.rating > 0)";
12719 m_pDS
->exec(strSQL
);
12721 if (progressDialog
)
12723 progressDialog
->Progress();
12724 if (progressDialog
->IsCanceled())
12726 RollbackTransaction();
12727 m_pDS
->exec("DROP TABLE HistSong");
12731 CommitTransaction();
12733 // Tidy up temp table (index also removed)
12734 m_pDS
->exec("DROP TABLE HistSong");
12735 // Compact db to recover space as had to add/drop actual table
12736 if (progressDialog
)
12738 progressDialog
->SetLine(2, CVariant
{331});
12739 progressDialog
->Progress();
12743 // Write event log entry
12744 // "Importing song history {1} of {2} songs matched", total - unmatched, total)
12745 std::string strLine
=
12746 StringUtils::Format(g_localizeStrings
.Get(38353), total
- unmatched
, total
);
12748 auto eventLog
= CServiceBroker::GetEventLog();
12750 eventLog
->Add(EventPtr(new CNotificationEvent(20197, strLine
, EventLevel::Information
)));
12756 CLog::Log(LOGERROR
, "{} failed", __FUNCTION__
);
12757 RollbackTransaction();
12758 if (bHistSongExists
)
12759 m_pDS
->exec("DROP TABLE HistSong");
12764 void CMusicDatabase::SetPropertiesFromArtist(CFileItem
& item
, const CArtist
& artist
)
12766 const std::string itemSeparator
=
12767 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
12769 item
.SetProperty("artist_sortname", artist
.strSortName
);
12770 item
.SetProperty("artist_type", artist
.strType
);
12771 item
.SetProperty("artist_gender", artist
.strGender
);
12772 item
.SetProperty("artist_disambiguation", artist
.strDisambiguation
);
12773 item
.SetProperty("artist_instrument", StringUtils::Join(artist
.instruments
, itemSeparator
));
12774 item
.SetProperty("artist_instrument_array", artist
.instruments
);
12775 item
.SetProperty("artist_style", StringUtils::Join(artist
.styles
, itemSeparator
));
12776 item
.SetProperty("artist_style_array", artist
.styles
);
12777 item
.SetProperty("artist_mood", StringUtils::Join(artist
.moods
, itemSeparator
));
12778 item
.SetProperty("artist_mood_array", artist
.moods
);
12779 item
.SetProperty("artist_born", artist
.strBorn
);
12780 item
.SetProperty("artist_formed", artist
.strFormed
);
12781 item
.SetProperty("artist_description", artist
.strBiography
);
12782 item
.SetProperty("artist_genre", StringUtils::Join(artist
.genre
, itemSeparator
));
12783 item
.SetProperty("artist_genre_array", artist
.genre
);
12784 item
.SetProperty("artist_died", artist
.strDied
);
12785 item
.SetProperty("artist_disbanded", artist
.strDisbanded
);
12786 item
.SetProperty("artist_yearsactive", StringUtils::Join(artist
.yearsActive
, itemSeparator
));
12787 item
.SetProperty("artist_yearsactive_array", artist
.yearsActive
);
12790 void CMusicDatabase::SetPropertiesFromAlbum(CFileItem
& item
, const CAlbum
& album
)
12792 const std::string itemSeparator
=
12793 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator
;
12795 item
.SetProperty("album_description", album
.strReview
);
12796 item
.SetProperty("album_theme", StringUtils::Join(album
.themes
, itemSeparator
));
12797 item
.SetProperty("album_theme_array", album
.themes
);
12798 item
.SetProperty("album_mood", StringUtils::Join(album
.moods
, itemSeparator
));
12799 item
.SetProperty("album_mood_array", album
.moods
);
12800 item
.SetProperty("album_style", StringUtils::Join(album
.styles
, itemSeparator
));
12801 item
.SetProperty("album_style_array", album
.styles
);
12802 item
.SetProperty("album_type", album
.strType
);
12803 item
.SetProperty("album_label", album
.strLabel
);
12804 item
.SetProperty("album_artist", album
.GetAlbumArtistString());
12805 item
.SetProperty("album_artist_array", album
.GetAlbumArtist());
12806 item
.SetProperty("album_genre", StringUtils::Join(album
.genre
, itemSeparator
));
12807 item
.SetProperty("album_genre_array", album
.genre
);
12808 item
.SetProperty("album_title", album
.strAlbum
);
12809 if (album
.fRating
> 0)
12810 item
.SetProperty("album_rating", StringUtils::FormatNumber(album
.fRating
));
12811 if (album
.iUserrating
> 0)
12812 item
.SetProperty("album_userrating", album
.iUserrating
);
12813 if (album
.iVotes
> 0)
12814 item
.SetProperty("album_votes", album
.iVotes
);
12816 item
.SetProperty("album_isboxset", album
.bBoxedSet
);
12817 item
.SetProperty("album_totaldiscs", album
.iTotalDiscs
);
12818 item
.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album
.releaseType
));
12819 item
.SetProperty("album_duration",
12820 StringUtils::SecondsToTimeString(album
.iAlbumDuration
,
12821 static_cast<TIME_FORMAT
>(TIME_FORMAT_GUESS
)));
12824 void CMusicDatabase::SetPropertiesForFileItem(CFileItem
& item
)
12826 if (!item
.HasMusicInfoTag())
12828 // May already have song artist ids as item property set when data read from
12829 // db, but check property is valid array (scripts could set item properties
12830 // incorrectly), otherwise try to fetch artist by name.
12832 if (item
.HasProperty("artistid") && item
.GetProperty("artistid").isArray())
12834 CVariant::const_iterator_array varid
= item
.GetProperty("artistid").begin_array();
12835 idArtist
= static_cast<int>(varid
->asInteger());
12838 idArtist
= GetArtistByName(item
.GetMusicInfoTag()->GetArtistString());
12842 if (GetArtist(idArtist
, artist
))
12843 SetPropertiesFromArtist(item
, artist
);
12845 int idAlbum
= item
.GetMusicInfoTag()->GetAlbumId();
12847 idAlbum
= GetAlbumByName(item
.GetMusicInfoTag()->GetAlbum(),
12848 item
.GetMusicInfoTag()->GetArtistString());
12852 if (GetAlbum(idAlbum
, album
, false))
12853 SetPropertiesFromAlbum(item
, album
);
12857 void CMusicDatabase::SetItemUpdated(int mediaId
, const std::string
& mediaType
)
12859 std::string strSQL
;
12862 if (mediaType
!= MediaTypeArtist
&& mediaType
!= MediaTypeAlbum
&& mediaType
!= MediaTypeSong
)
12864 if (nullptr == m_pDB
)
12866 if (nullptr == m_pDS
)
12869 // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
12870 // e.g. when artwork for item is changed from info dialog but not item details.
12871 // Use SQL UPDATE that does not change record data.
12872 if (mediaType
== MediaTypeArtist
)
12873 strSQL
= PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId
);
12874 else if (mediaType
== MediaTypeAlbum
)
12875 strSQL
= PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId
);
12876 else // MediaTypeSong
12877 strSQL
= PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId
);
12878 m_pDS
->exec(strSQL
);
12882 CLog::Log(LOGERROR
, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__
,
12883 mediaId
, mediaType
, strSQL
);
12887 void CMusicDatabase::SetArtForItem(int mediaId
,
12888 const std::string
& mediaType
,
12889 const std::map
<std::string
, std::string
>& art
)
12891 for (const auto& i
: art
)
12892 SetArtForItem(mediaId
, mediaType
, i
.first
, i
.second
);
12895 void CMusicDatabase::SetArtForItem(int mediaId
,
12896 const std::string
& mediaType
,
12897 const std::string
& artType
,
12898 const std::string
& url
)
12902 if (nullptr == m_pDB
)
12904 if (nullptr == m_pDS
)
12907 // don't set <foo>.<bar> art types - these are derivative types from parent items
12908 if (artType
.find('.') != std::string::npos
)
12911 std::string sql
= PrepareSQL("SELECT art_id FROM art "
12912 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
12913 mediaId
, mediaType
.c_str(), artType
.c_str());
12917 int artId
= m_pDS
->fv(0).get_asInt();
12919 sql
= PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url
.c_str(), artId
);
12925 sql
= PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
12926 "VALUES (%d, '%s', '%s', '%s')",
12927 mediaId
, mediaType
.c_str(), artType
.c_str(), url
.c_str());
12933 CLog::Log(LOGERROR
, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__
, mediaId
, mediaType
,
12938 bool CMusicDatabase::GetArtForItem(
12939 int songId
, int albumId
, int artistId
, bool bPrimaryArtist
, std::vector
<ArtForThumbLoader
>& art
)
12941 std::string strSQL
;
12944 if (!(songId
> 0 || albumId
> 0 || artistId
> 0))
12946 if (nullptr == m_pDB
)
12948 if (nullptr == m_pDS2
)
12949 return false; // using dataset 2 as we're likely called in loops on dataset 1
12953 filter
.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId
, MediaTypeSong
));
12955 filter
.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId
, MediaTypeAlbum
),
12958 filter
.AppendWhere(
12959 PrepareSQL("media_id = %i AND media_type ='%s'", artistId
, MediaTypeArtist
), false);
12961 strSQL
= "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder "
12963 if (!BuildSQL(strSQL
, filter
, strSQL
))
12966 if (!(artistId
> 0))
12968 // Artist ID unknown, so lookup album artist for albums and songs
12969 std::string strSQL2
;
12972 //Album ID known, so use it to look up album artist(s)
12973 strSQL2
= PrepareSQL(
12974 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12975 "url, album_artist.iOrder as iorder FROM art "
12976 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12977 "WHERE album_artist.idAlbum = %i ",
12978 MediaTypeArtist
, albumId
);
12979 if (bPrimaryArtist
)
12980 strSQL2
+= "AND album_artist.iOrder = 0";
12982 strSQL
= strSQL
+ " UNION " + strSQL2
;
12988 //Album ID unknown, so get from song to look up album artist(s)
12989 strSQL2
= PrepareSQL(
12990 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12991 "url, album_artist.iOrder as iorder FROM art "
12992 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12993 "JOIN song ON song.idAlbum = album_artist.idAlbum "
12994 "WHERE song.idSong = %i ",
12995 MediaTypeArtist
, songId
);
12996 if (bPrimaryArtist
)
12997 strSQL2
+= "AND album_artist.iOrder = 0";
12999 strSQL
= strSQL
+ " UNION " + strSQL2
;
13002 // Artist ID unknown, so lookup artist for songs (could be different from album artist)
13003 strSQL2
= PrepareSQL(
13004 "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
13005 "url, song_artist.iOrder as iorder FROM art "
13006 "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
13007 "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
13008 MediaTypeArtist
, songId
, ROLE_ARTIST
);
13009 if (bPrimaryArtist
)
13010 strSQL2
+= "AND song_artist.iOrder = 0";
13012 strSQL
= strSQL
+ " UNION " + strSQL2
;
13015 if (songId
> 0 && albumId
< 0)
13017 //Album ID unknown, so get from song to look up album art
13018 std::string strSQL2
;
13019 strSQL2
= PrepareSQL("SELECT art_id, media_id, media_type, type, '' as prefix, "
13020 "url, 0 as iorder FROM art "
13021 "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
13022 "WHERE song.idSong = %i ",
13023 MediaTypeAlbum
, songId
);
13024 strSQL
= strSQL
+ " UNION " + strSQL2
;
13027 m_pDS2
->query(strSQL
);
13028 while (!m_pDS2
->eof())
13030 ArtForThumbLoader artitem
;
13031 artitem
.artType
= m_pDS2
->fv("type").get_asString();
13032 artitem
.mediaType
= m_pDS2
->fv("media_type").get_asString();
13033 artitem
.prefix
= m_pDS2
->fv("prefix").get_asString();
13034 artitem
.url
= m_pDS2
->fv("url").get_asString();
13035 int iOrder
= m_pDS2
->fv("iorder").get_asInt();
13036 // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
13038 artitem
.prefix
+= m_pDS2
->fv("iorder").get_asString();
13040 art
.emplace_back(artitem
);
13044 return !art
.empty();
13048 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, strSQL
);
13053 bool CMusicDatabase::GetArtForItem(int mediaId
,
13054 const std::string
& mediaType
,
13055 std::map
<std::string
, std::string
>& art
)
13059 if (nullptr == m_pDB
)
13061 if (nullptr == m_pDS2
)
13062 return false; // using dataset 2 as we're likely called in loops on dataset 1
13064 std::string sql
= PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'",
13065 mediaId
, mediaType
.c_str());
13066 m_pDS2
->query(sql
);
13067 while (!m_pDS2
->eof())
13069 art
.insert(std::make_pair(m_pDS2
->fv(0).get_asString(), m_pDS2
->fv(1).get_asString()));
13073 return !art
.empty();
13077 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, mediaId
);
13082 std::string
CMusicDatabase::GetArtForItem(int mediaId
,
13083 const std::string
& mediaType
,
13084 const std::string
& artType
)
13086 std::string query
= PrepareSQL("SELECT url FROM art "
13087 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
13088 mediaId
, mediaType
.c_str(), artType
.c_str());
13089 return GetSingleValue(query
, m_pDS2
);
13092 bool CMusicDatabase::RemoveArtForItem(int mediaId
,
13093 const MediaType
& mediaType
,
13094 const std::string
& artType
)
13096 return ExecuteQuery(PrepareSQL("DELETE FROM art "
13097 "WHERE media_id=%i AND media_type='%s' AND type='%s'",
13098 mediaId
, mediaType
.c_str(), artType
.c_str()));
13101 bool CMusicDatabase::RemoveArtForItem(int mediaId
,
13102 const MediaType
& mediaType
,
13103 const std::set
<std::string
>& artTypes
)
13105 bool result
= true;
13106 for (const auto& i
: artTypes
)
13107 result
&= RemoveArtForItem(mediaId
, mediaType
, i
);
13112 bool CMusicDatabase::GetArtTypes(const MediaType
& mediaType
, std::vector
<std::string
>& artTypes
)
13116 if (nullptr == m_pDB
)
13118 if (nullptr == m_pDS
)
13121 std::string strSQL
=
13122 PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType
.c_str());
13124 if (!m_pDS
->query(strSQL
))
13126 int iRowsFound
= m_pDS
->num_rows();
13127 if (iRowsFound
== 0)
13133 while (!m_pDS
->eof())
13135 artTypes
.emplace_back(m_pDS
->fv(0).get_asString());
13143 CLog::Log(LOGERROR
, "{}({}) failed", __FUNCTION__
, mediaType
);
13148 std::vector
<std::string
> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId
,
13149 const MediaType
& mediaType
)
13151 CScraperUrl thumbURL
;
13152 if (mediaType
== MediaTypeArtist
)
13155 if (GetArtist(mediaId
, artist
))
13156 thumbURL
= artist
.thumbURL
;
13158 else if (mediaType
== MediaTypeAlbum
)
13161 if (GetAlbum(mediaId
, album
))
13162 thumbURL
= album
.thumbURL
;
13165 std::vector
<std::string
> result
;
13166 for (const auto& urlEntry
: thumbURL
.GetUrls())
13168 std::string artType
= urlEntry
.m_aspect
;
13169 if (artType
.empty())
13171 if (std::find(result
.begin(), result
.end(), artType
) == result
.end())
13172 result
.push_back(artType
);
13177 std::vector
<CScraperUrl::SUrlEntry
> CMusicDatabase::GetAvailableArtForItem(
13178 int mediaId
, const MediaType
& mediaType
, const std::string
& artType
)
13180 CScraperUrl thumbURL
;
13181 if (mediaType
== MediaTypeArtist
)
13184 if (GetArtist(mediaId
, artist
))
13185 thumbURL
= artist
.thumbURL
;
13187 else if (mediaType
== MediaTypeAlbum
)
13190 if (GetAlbum(mediaId
, album
))
13191 thumbURL
= album
.thumbURL
;
13194 std::vector
<CScraperUrl::SUrlEntry
> result
;
13195 for (auto urlEntry
: thumbURL
.GetUrls())
13197 if (urlEntry
.m_aspect
.empty())
13198 urlEntry
.m_aspect
= "thumb";
13199 if (artType
.empty() || urlEntry
.m_aspect
== artType
)
13200 result
.push_back(urlEntry
);
13205 int CMusicDatabase::GetOrderFilter(const std::string
& type
,
13206 const SortDescription
& sorting
,
13209 // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
13210 int iFieldsAdded
= 0;
13211 filter
.fields
.clear(); // remove "*"
13212 std::vector
<std::string
> orderfields
;
13215 if (sorting
.sortOrder
== SortOrderDescending
)
13218 if (sorting
.sortBy
== SortByRandom
)
13219 orderfields
.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
13223 SortUtils::GetFieldsForSQLSort(type
, sorting
.sortBy
, fields
);
13224 for (const auto& it
: fields
)
13226 std::string strField
;
13227 if (it
== FieldYear
)
13228 strField
= "iYear";
13230 strField
= DatabaseUtils::GetField(it
, type
, DatabaseQueryPartSelect
);
13231 if (!strField
.empty())
13232 orderfields
.emplace_back(strField
);
13236 // Convert field names into order by statement elements
13237 for (auto& name
: orderfields
)
13239 //Add field for adjusted name sorting using sort name and ignoring articles
13240 std::string sortSQL
;
13241 if (StringUtils::EndsWith(name
, "strArtists") || StringUtils::EndsWith(name
, "strArtist"))
13243 if (StringUtils::EndsWith(name
, "strArtists"))
13244 sortSQL
= SortnameBuildSQL("artistsortname", sorting
.sortAttributes
, name
, "strArtistSort");
13246 sortSQL
= SortnameBuildSQL("artistsortname", sorting
.sortAttributes
, name
, "strSortName");
13247 if (!sortSQL
.empty())
13249 name
= "artistsortname";
13250 filter
.AppendField(sortSQL
); // Add artistsortname as scalar query field
13253 // Natural number case-insensitive sort
13254 filter
.AppendOrder(AlphanumericSortSQL(name
, sorting
.sortOrder
));
13256 else if (StringUtils::EndsWith(name
, "strAlbum") || StringUtils::EndsWith(name
, "strTitle"))
13258 sortSQL
= SortnameBuildSQL("titlesortname", sorting
.sortAttributes
, name
, "");
13259 if (!sortSQL
.empty())
13261 name
= "titlesortname";
13262 filter
.AppendField(sortSQL
); // Add sortname as scalar query field
13265 // Natural number case-insensitive sort
13266 filter
.AppendOrder(AlphanumericSortSQL(name
, sorting
.sortOrder
));
13268 else if (StringUtils::EndsWith(name
, "strGenres"))
13269 // Natural number case-insensitive sort
13270 filter
.AppendOrder(AlphanumericSortSQL(name
, sorting
.sortOrder
));
13272 filter
.AppendOrder(name
+ DESC
);
13274 return iFieldsAdded
;
13277 bool CMusicDatabase::GetFilter(CDbUrl
& musicUrl
, Filter
& filter
, SortDescription
& sorting
)
13279 if (!musicUrl
.IsValid())
13282 std::string type
= musicUrl
.GetType();
13283 const CUrlOptions::UrlOptions
& options
= musicUrl
.GetOptions();
13285 // Check for playlist rules first, they may contain role criteria
13286 bool hasRoleRules
= false;
13288 auto option
= options
.find("xsp");
13289 if (option
!= options
.end())
13291 PLAYLIST::CSmartPlaylist xsp
;
13292 if (!xsp
.LoadFromJson(option
->second
.asString()))
13295 std::set
<std::string
> playlists
;
13296 std::string xspWhere
;
13297 xspWhere
= xsp
.GetWhereClause(*this, playlists
);
13298 hasRoleRules
= xsp
.GetType() == "artists" &&
13299 xspWhere
.find("song_artist.idRole = role.idRole") != xspWhere
.npos
;
13301 // Check if the filter playlist matches the item type
13302 // Allow for grouping name like "originalyears" and type "years"
13303 if (xsp
.GetType() == type
||
13304 (xsp
.GetGroup().find(type
) != std::string::npos
&& !xsp
.IsGroupMixed()))
13306 filter
.AppendWhere(xspWhere
);
13308 if (xsp
.GetLimit() > 0)
13309 sorting
.limitEnd
= xsp
.GetLimit();
13310 if (xsp
.GetOrder() != SortByNone
)
13311 sorting
.sortBy
= xsp
.GetOrder();
13312 sorting
.sortOrder
= xsp
.GetOrderAscending() ? SortOrderAscending
: SortOrderDescending
;
13313 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
13314 CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING
))
13315 sorting
.sortAttributes
= SortAttributeIgnoreArticle
;
13319 //Process role options, common to artist and album type filtering
13320 int idRole
= 1; // Default restrict song_artist to "artists" only, no other roles.
13321 option
= options
.find("roleid");
13322 if (option
!= options
.end())
13323 idRole
= static_cast<int>(option
->second
.asInteger());
13326 option
= options
.find("role");
13327 if (option
!= options
.end())
13329 if (option
->second
.asString() == "all" || option
->second
.asString() == "%")
13330 idRole
= -1000; //All roles
13332 idRole
= GetRoleByName(option
->second
.asString());
13337 // Get Role from role rule(s) here.
13338 // But that requires much change, so for now get all roles as better than none
13339 idRole
= -1000; //All roles
13342 std::string strRoleSQL
; //Role < 0 means all roles, otherwise filter by role
13344 strRoleSQL
= PrepareSQL(" AND song_artist.idRole = %i ", idRole
);
13346 int idArtist
= -1, idGenre
= -1, idAlbum
= -1, idSong
= -1;
13349 bool albumArtistsOnly
= false;
13350 bool useOriginalYear
= false;
13351 std::string artistname
;
13353 // Process useoriginalyear option, setting overridden by option
13354 useOriginalYear
= CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
13355 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE
);
13356 option
= options
.find("useoriginalyear");
13357 if (option
!= options
.end())
13358 useOriginalYear
= option
->second
.asBoolean();
13360 // Process albumartistsonly option
13361 option
= options
.find("albumartistsonly");
13362 if (option
!= options
.end())
13363 albumArtistsOnly
= option
->second
.asBoolean();
13365 // Process genre option
13366 option
= options
.find("genreid");
13367 if (option
!= options
.end())
13368 idGenre
= static_cast<int>(option
->second
.asInteger());
13371 option
= options
.find("genre");
13372 if (option
!= options
.end())
13373 idGenre
= GetGenreByName(option
->second
.asString());
13376 // Process source option
13377 option
= options
.find("sourceid");
13378 if (option
!= options
.end())
13379 idSource
= static_cast<int>(option
->second
.asInteger());
13382 option
= options
.find("source");
13383 if (option
!= options
.end())
13384 idSource
= GetSourceByName(option
->second
.asString());
13387 // Process album option
13388 option
= options
.find("albumid");
13389 if (option
!= options
.end())
13390 idAlbum
= static_cast<int>(option
->second
.asInteger());
13393 option
= options
.find("album");
13394 if (option
!= options
.end())
13395 idAlbum
= GetAlbumByName(option
->second
.asString());
13398 // Process artist option
13399 option
= options
.find("artistid");
13400 if (option
!= options
.end())
13401 idArtist
= static_cast<int>(option
->second
.asInteger());
13404 option
= options
.find("artist");
13405 if (option
!= options
.end())
13407 idArtist
= GetArtistByName(option
->second
.asString());
13408 if (idArtist
== -1)
13409 { // not found with that name, or more than one found as artist name is not unique
13410 artistname
= option
->second
.asString();
13415 // Process song option
13416 option
= options
.find("songid");
13417 if (option
!= options
.end())
13418 idSong
= static_cast<int>(option
->second
.asInteger());
13420 if (type
== "artists")
13423 { // Not an "artists" smart playlist with roles rules, so get filter from options
13425 filter
.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist
));
13426 else if (idAlbum
> 0)
13427 filter
.AppendWhere(
13428 PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
13429 "WHERE album_artist.idAlbum = %i)",
13431 else if (idSong
> 0)
13433 filter
.AppendWhere(
13434 PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
13435 "WHERE song_artist.idSong = %i %s)",
13436 idSong
, strRoleSQL
.c_str()));
13440 Process idRole, idGenre, idSource and albumArtistsOnly options
13442 For artists these rules are combined because they apply via album and song
13443 and so we need to ensure all criteria are met via the same album or song.
13444 1) Some artists may be only album artists, so for all artists (with linked
13445 albums or songs) we need to check both album_artist and song_artist tables.
13446 2) Role is determined from song_artist table, so even if looking for album artists
13447 only we find those that also have a specific role e.g. which album artist is a
13448 composer of songs in that album, from entries in the song_artist table.
13449 a) Role < -1 is used to indicate that all roles are wanted.
13450 b) When not album artists only and a specific role wanted then only the song_artist
13452 c) When album artists only and role = 1 (an "artist") then only the album_artist
13455 std::string albumArtistSQL
, songArtistSQL
;
13456 ExistsSubQuery
albumArtistSub("album_artist",
13457 "album_artist.idArtist = artistview.idArtist");
13458 // Prepare album artist subquery SQL
13461 if (idRole
== 1 && idGenre
< 0)
13463 albumArtistSub
.AppendJoin(
13464 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
13465 albumArtistSub
.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource
));
13469 albumArtistSub
.AppendWhere(
13470 PrepareSQL("EXISTS(SELECT 1 FROM album_source "
13471 "WHERE album_source.idSource = %i "
13472 "AND album_source.idAlbum = album_artist.idAlbum)",
13476 if (idRole
<= 1 && idGenre
> 0)
13477 { // Check genre of songs of album using nested subquery
13478 std::string strGenre
=
13479 PrepareSQL("EXISTS(SELECT 1 FROM song "
13480 "JOIN song_genre ON song_genre.idSong = song.idSong "
13481 "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)",
13483 albumArtistSub
.AppendWhere(strGenre
);
13486 // Prepare song artist subquery SQL
13487 ExistsSubQuery
songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
13489 songArtistSub
.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole
));
13490 if (idSource
> 0 && idGenre
> 0 && !albumArtistsOnly
&& idRole
>= 1)
13492 songArtistSub
.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
13493 "JOIN song_genre ON song_genre.idSong = song.idSong "
13494 "WHERE song.idSong = song_artist.idSong "
13495 "AND song_genre.idGenre = %i "
13496 "AND EXISTS(SELECT 1 FROM album_source "
13497 "WHERE album_source.idSource = %i "
13498 "AND album_source.idAlbum = song.idAlbum))",
13499 idGenre
, idSource
));
13505 songArtistSub
.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
13506 songArtistSub
.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre
));
13508 if (idSource
> 0 && !albumArtistsOnly
)
13510 songArtistSub
.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13511 songArtistSub
.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
13512 songArtistSub
.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource
));
13514 if (idRole
> 1 && albumArtistsOnly
)
13515 { // Album artists only with role, check AND in album_artist for album of song
13516 // using nested subquery correlated with album_artist
13517 songArtistSub
.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13518 songArtistSub
.param
= "song_artist.idArtist = album_artist.idArtist";
13519 songArtistSub
.AppendWhere("song.idAlbum = album_artist.idAlbum");
13523 // Build filter clause from subqueries
13524 if (idRole
> 1 && albumArtistsOnly
)
13525 { // Album artists only with role, check AND in album_artist for album of song
13526 // using nested subquery correlated with album_artist
13527 songArtistSub
.BuildSQL(songArtistSQL
);
13528 albumArtistSub
.AppendWhere(songArtistSQL
);
13529 albumArtistSub
.BuildSQL(albumArtistSQL
);
13530 filter
.AppendWhere(albumArtistSQL
);
13534 songArtistSub
.BuildSQL(songArtistSQL
);
13535 albumArtistSub
.BuildSQL(albumArtistSQL
);
13536 if (idRole
< 0 || (idRole
== 1 && !albumArtistsOnly
))
13537 { // Artist contributing to songs, any role, check OR album artist too
13538 // as artists can be just album artists but not song artists
13539 filter
.AppendWhere(songArtistSQL
+ " OR " + albumArtistSQL
);
13541 else if (idRole
> 1)
13543 // Artist contributes that role (not albmartistsonly as already handled)
13544 filter
.AppendWhere(songArtistSQL
);
13546 else // idRole = 1 and albumArtistsOnly
13547 { // Only look at album artists, not albums where artist features on songs
13548 filter
.AppendWhere(albumArtistSQL
);
13553 // remove the null string
13554 filter
.AppendWhere("artistview.strArtist != ''");
13556 else if (type
== "albums")
13558 option
= options
.find("year");
13559 if (option
!= options
.end())
13561 if (!useOriginalYear
)
13562 filter
.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
13563 option
->second
.asString().c_str()));
13565 filter
.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
13566 option
->second
.asString().c_str()));
13568 option
= options
.find("compilation");
13569 if (option
!= options
.end())
13570 filter
.AppendWhere(
13571 PrepareSQL("albumview.bCompilation = %i", option
->second
.asBoolean() ? 1 : 0));
13573 option
= options
.find("boxset");
13574 if (option
!= options
.end())
13575 filter
.AppendWhere(
13576 PrepareSQL("albumview.bBoxedSet = %i", option
->second
.asBoolean() ? 1 : 0));
13579 filter
.AppendWhere(PrepareSQL(
13580 "EXISTS(SELECT 1 FROM album_source "
13581 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
13584 // Process artist, role and genre options together as song subquery to filter those
13585 // albums that have songs with both that artist and genre
13586 std::string albumArtistSQL
, songArtistSQL
, genreSQL
;
13587 ExistsSubQuery
genreSub("song", "song.idAlbum = album_artist.idAlbum");
13588 genreSub
.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
13589 genreSub
.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre
));
13590 ExistsSubQuery
albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
13591 ExistsSubQuery
songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
13592 songArtistSub
.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
13596 songArtistSub
.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist
));
13597 albumArtistSub
.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist
));
13599 else if (!artistname
.empty())
13600 { // Artist name is not unique, so could get albums or songs from more than one.
13601 songArtistSub
.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
13602 songArtistSub
.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname
.c_str()));
13604 albumArtistSub
.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
13605 albumArtistSub
.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname
.c_str()));
13608 songArtistSub
.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole
));
13611 songArtistSub
.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
13612 songArtistSub
.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre
));
13615 if (idArtist
> 0 || !artistname
.empty())
13617 if (idRole
<= 1 && idGenre
> 0)
13618 { // Check genre of songs of album using nested subquery
13619 genreSub
.BuildSQL(genreSQL
);
13620 albumArtistSub
.AppendWhere(genreSQL
);
13622 if (idRole
> 1 && albumArtistsOnly
)
13623 { // Album artists only with role, check AND in album_artist for same song
13624 // using nested subquery correlated with album_artist
13625 songArtistSub
.param
= "song.idAlbum = album_artist.idAlbum";
13626 songArtistSub
.BuildSQL(songArtistSQL
);
13627 albumArtistSub
.AppendWhere(songArtistSQL
);
13628 albumArtistSub
.BuildSQL(albumArtistSQL
);
13629 filter
.AppendWhere(albumArtistSQL
);
13633 songArtistSub
.BuildSQL(songArtistSQL
);
13634 albumArtistSub
.BuildSQL(albumArtistSQL
);
13635 if (idRole
< 0 || (idRole
== 1 && !albumArtistsOnly
))
13636 { // Artist contributing to songs, any role, check OR album artist too
13637 // as artists can be just album artists but not song artists
13638 filter
.AppendWhere(songArtistSQL
+ " OR " + albumArtistSQL
);
13640 else if (idRole
> 1)
13641 { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
13642 filter
.AppendWhere(songArtistSQL
);
13644 else // idRole = 1 and albumArtistsOnly
13645 { // Only look at album artists, not albums where artist features on songs
13646 // This may want to be a separate option so you can choose to see all the albums where that artist
13647 // appears on one or more songs without having to list all song artists in the artists node.
13648 filter
.AppendWhere(albumArtistSQL
);
13653 { // No artist given
13655 { // Have genre option but not artist
13656 genreSub
.param
= "song.idAlbum = albumview.idAlbum";
13657 genreSub
.BuildSQL(genreSQL
);
13658 filter
.AppendWhere(genreSQL
);
13660 // Exclude any single albums (aka empty tagged albums)
13661 // This causes "albums" media filter artist selection to only offer album artists
13662 option
= options
.find("show_singles");
13663 if (option
== options
.end() || !option
->second
.asBoolean())
13664 filter
.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'",
13665 CAlbum::ReleaseTypeToString(CAlbum::Album
).c_str()));
13668 else if (type
== "discs")
13671 filter
.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum
));
13674 option
= options
.find("year");
13675 if (option
!= options
.end())
13677 if (!useOriginalYear
)
13678 filter
.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
13679 option
->second
.asString().c_str()));
13681 filter
.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
13682 option
->second
.asString().c_str()));
13685 option
= options
.find("compilation");
13686 if (option
!= options
.end())
13687 filter
.AppendWhere(
13688 PrepareSQL("albumview.bCompilation = %i", option
->second
.asBoolean() ? 1 : 0));
13690 option
= options
.find("boxset");
13691 if (option
!= options
.end())
13692 filter
.AppendWhere(
13693 PrepareSQL("albumview.bBoxedSet = %i", option
->second
.asBoolean() ? 1 : 0));
13696 filter
.AppendWhere(PrepareSQL(
13697 "EXISTS(SELECT 1 FROM album_source "
13698 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
13701 option
= options
.find("discid");
13702 if (option
!= options
.end())
13703 filter
.AppendWhere(PrepareSQL("iDisc = %i", option
->second
.asInteger()));
13705 option
= options
.find("disctitle");
13706 if (option
!= options
.end())
13707 filter
.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option
->second
.asString().c_str()));
13710 filter
.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
13711 "song.idSong AND song_genre.idGenre = %i)",
13714 std::string songArtistClause
, albumArtistClause
;
13718 PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
13719 "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
13720 idArtist
, strRoleSQL
.c_str());
13721 albumArtistClause
=
13722 PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
13723 "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
13726 else if (!artistname
.empty())
13727 { // Artist name is not unique, so could get songs from more than one.
13728 songArtistClause
= PrepareSQL(
13729 "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
13730 "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
13731 artistname
.c_str(), strRoleSQL
.c_str());
13732 albumArtistClause
=
13733 PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
13734 "album_artist.idArtist "
13735 "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
13736 artistname
.c_str());
13739 // Process artist name or id option
13740 if (!songArtistClause
.empty())
13742 if (idRole
< 0) // Artist contributes to songs, any roles OR is album artist
13743 filter
.AppendWhere("(" + songArtistClause
+ " OR " + albumArtistClause
+ ")");
13744 else if (idRole
> 1)
13746 if (albumArtistsOnly
) //Album artists only with role, check AND in album_artist for same song
13747 filter
.AppendWhere("(" + songArtistClause
+ " AND " + albumArtistClause
+ ")");
13748 else // songs where artist contributes that role.
13749 filter
.AppendWhere(songArtistClause
);
13753 if (albumArtistsOnly
) // Only look at album artists, not where artist features on songs
13754 filter
.AppendWhere(albumArtistClause
);
13755 else // Artist is song artist or album artist
13756 filter
.AppendWhere("(" + songArtistClause
+ " OR " + albumArtistClause
+ ")");
13760 else if (type
== "songs" || type
== "singles")
13762 option
= options
.find("singles");
13763 if (option
!= options
.end())
13764 filter
.AppendWhere(PrepareSQL(
13765 "songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
13766 option
->second
.asBoolean() ? "" : "NOT ",
13767 CAlbum::ReleaseTypeToString(CAlbum::Single
).c_str()));
13769 // When have idAlbum skip year, compilation, boxset criteria as already applied via album
13772 option
= options
.find("year");
13773 if (option
!= options
.end())
13775 if (!useOriginalYear
)
13776 filter
.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
13777 option
->second
.asString().c_str()));
13779 filter
.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
13780 option
->second
.asString().c_str()));
13782 option
= options
.find("compilation");
13783 if (option
!= options
.end())
13784 filter
.AppendWhere(
13785 PrepareSQL("songview.bCompilation = %i", option
->second
.asBoolean() ? 1 : 0));
13787 option
= options
.find("boxset");
13788 if (option
!= options
.end())
13789 filter
.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
13790 "songview.idAlbum AND bBoxedSet = %i)",
13791 option
->second
.asBoolean() ? 1 : 0));
13794 option
= options
.find("discid");
13795 if (option
!= options
.end())
13796 idDisc
= static_cast<int>(option
->second
.asInteger());
13798 option
= options
.find("disctitle");
13799 if (option
!= options
.end())
13800 filter
.AppendWhere(
13801 PrepareSQL("songview.strDiscSubtitle = '%s'", option
->second
.asString().c_str()));
13804 filter
.AppendWhere(PrepareSQL("songview.idSong = %i", idSong
));
13807 filter
.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum
));
13810 filter
.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc
));
13813 filter
.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre "
13814 "WHERE song_genre.idGenre = %i)",
13818 filter
.AppendWhere(PrepareSQL(
13819 "EXISTS(SELECT 1 FROM album_source "
13820 "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)",
13823 std::string songArtistClause
, albumArtistClause
;
13827 PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
13828 "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
13829 idArtist
, strRoleSQL
.c_str());
13830 albumArtistClause
= PrepareSQL(
13831 "EXISTS (SELECT 1 FROM album_artist "
13832 "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
13835 else if (!artistname
.empty())
13836 { // Artist name is not unique, so could get songs from more than one.
13837 songArtistClause
= PrepareSQL(
13838 "EXISTS (SELECT 1 FROM song_artist "
13839 "JOIN artist ON artist.idArtist = song_artist.idArtist "
13840 "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
13841 artistname
.c_str(), strRoleSQL
.c_str());
13842 albumArtistClause
= PrepareSQL(
13843 "EXISTS (SELECT 1 FROM album_artist "
13844 "JOIN artist ON artist.idArtist = album_artist.idArtist "
13845 "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
13846 artistname
.c_str());
13849 // Process artist name or id option
13850 if (!songArtistClause
.empty())
13852 if (idRole
< 0) // Artist contributes to songs, any roles OR is album artist
13853 filter
.AppendWhere("(" + songArtistClause
+ " OR " + albumArtistClause
+ ")");
13854 else if (idRole
> 1)
13856 if (albumArtistsOnly
) //Album artists only with role, check AND in album_artist for same song
13857 filter
.AppendWhere("(" + songArtistClause
+ " AND " + albumArtistClause
+ ")");
13858 else // songs where artist contributes that role.
13859 filter
.AppendWhere(songArtistClause
);
13863 if (albumArtistsOnly
) // Only look at album artists, not where artist features on songs
13864 filter
.AppendWhere(albumArtistClause
);
13865 else // Artist is song artist or album artist
13866 filter
.AppendWhere("(" + songArtistClause
+ " OR " + albumArtistClause
+ ")");
13871 option
= options
.find("filter");
13872 if (option
!= options
.end())
13874 PLAYLIST::CSmartPlaylist xspFilter
;
13875 if (!xspFilter
.LoadFromJson(option
->second
.asString()))
13878 // check if the filter playlist matches the item type
13879 if (xspFilter
.GetType() == type
)
13881 std::set
<std::string
> playlists
;
13882 filter
.AppendWhere(xspFilter
.GetWhereClause(*this, playlists
));
13884 // remove the filter if it doesn't match the item type
13886 musicUrl
.RemoveOption("filter");
13892 std::string
CMusicDatabase::GetMediaDateFromFile(const std::string
& strFileNameAndPath
)
13894 if (strFileNameAndPath
.empty())
13895 return std::string();
13897 CDateTime dateMedia
;
13899 code
= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded
;
13900 // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
13902 dateMedia
= CFileUtils::GetModificationDate(0, strFileNameAndPath
);
13903 //2 using the newer datetime of the file's mtime and ctime
13904 else if (code
== 2)
13905 dateMedia
= CFileUtils::GetModificationDate(1, strFileNameAndPath
);
13906 //3 using the older datetime of the file's mtime and ctime
13907 else if (code
== 3)
13908 dateMedia
= CFileUtils::GetModificationDate(2, strFileNameAndPath
);
13909 //0 using the current datetime if none of the above matches or one returns an invalid datetime
13910 if (!dateMedia
.IsValid())
13911 dateMedia
= CDateTime::GetCurrentDateTime();
13913 return dateMedia
.GetAsDBDateTime();
13916 bool CMusicDatabase::AddAudioBook(const CFileItem
& item
)
13918 auto const& artists
= item
.GetMusicInfoTag()->GetArtist();
13919 std::string strSQL
= PrepareSQL(
13920 "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
13921 "VALUES (NULL,'%s','%s',%i,'%s','%s')",
13922 item
.GetMusicInfoTag()->GetAlbum().c_str(), artists
.empty() ? "" : artists
[0].c_str(), 0,
13923 item
.GetDynPath().c_str(), CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
13924 return ExecuteQuery(strSQL
);
13927 bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem
& item
, int bookmark
)
13929 std::string strSQL
= PrepareSQL("SELECT bookmark FROM audiobook "
13931 item
.GetDynPath().c_str());
13932 if (!m_pDS
->query(strSQL
.c_str()) || m_pDS
->num_rows() == 0)
13934 if (!AddAudioBook(item
))
13938 strSQL
= PrepareSQL("UPDATE audiobook SET bookmark=%i "
13940 bookmark
, item
.GetDynPath().c_str());
13942 return ExecuteQuery(strSQL
);
13945 bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem
& item
, int& bookmark
)
13947 std::string strSQL
=
13948 PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'", item
.GetDynPath().c_str());
13949 if (!m_pDS
->query(strSQL
.c_str()) || m_pDS
->num_rows() == 0)
13952 bookmark
= m_pDS
->fv(0).get_asInt();