1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
8
9 #include "MusicDatabase.h"
10
11 #include "Album.h"
12 #include "Application.h"
13 #include "Artist.h"
14 #include "FileItem.h"
15 #include "GUIInfoManager.h"
16 #include "LangInfo.h"
17 #include "ServiceBroker.h"
18 #include "Song.h"
19 #include "TextureCache.h"
20 #include "URL.h"
21 #include "Util.h"
22 #include "addons/Addon.h"
23 #include "addons/AddonManager.h"
24 #include "addons/AddonSystemSettings.h"
25 #include "addons/Scraper.h"
26 #include "dbwrappers/dataset.h"
27 #include "dialogs/GUIDialogKaiToast.h"
28 #include "dialogs/GUIDialogProgress.h"
29 #include "dialogs/GUIDialogSelect.h"
30 #include "events/EventLog.h"
31 #include "events/NotificationEvent.h"
32 #include "filesystem/Directory.h"
33 #include "filesystem/DirectoryCache.h"
34 #include "filesystem/File.h"
35 #include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
36 #include "guilib/GUIComponent.h"
37 #include "guilib/GUIWindowManager.h"
38 #include "guilib/LocalizeStrings.h"
39 #include "guilib/guiinfo/GUIInfoLabels.h"
40 #include "interfaces/AnnouncementManager.h"
41 #include "messaging/helpers/DialogHelper.h"
42 #include "messaging/helpers/DialogOKHelper.h"
43 #include "music/tags/MusicInfoTag.h"
44 #include "network/Network.h"
45 #include "network/cddb.h"
46 #include "playlists/SmartPlayList.h"
47 #include "profiles/ProfileManager.h"
48 #include "settings/AdvancedSettings.h"
49 #include "settings/MediaSourceSettings.h"
50 #include "settings/Settings.h"
51 #include "settings/SettingsComponent.h"
52 #include "storage/MediaManager.h"
53 #include "threads/SystemClock.h"
54 #include "utils/FileUtils.h"
55 #include "utils/LegacyPathTranslation.h"
56 #include "utils/MathUtils.h"
57 #include "utils/Random.h"
58 #include "utils/StringUtils.h"
59 #include "utils/URIUtils.h"
60 #include "utils/XMLUtils.h"
61 #include "utils/log.h"
62
63 #include <inttypes.h>
64
65 using namespace XFILE;
66 using namespace MUSICDATABASEDIRECTORY;
67 using namespace KODI::MESSAGING;
68 using namespace MUSIC_INFO;
69
70 using ADDON::AddonPtr;
71 using KODI::MESSAGING::HELPERS::DialogResponse;
72
73 #define RECENTLY_PLAYED_LIMIT 25
74 #define MIN_FULL_SEARCH_LENGTH 3
75
76 #ifdef HAS_DVD_DRIVE
77 using namespace CDDB;
78 using namespace MEDIA_DETECT;
79 #endif
80
AnnounceRemove(const std::string & content,int id)81 static void AnnounceRemove(const std::string& content, int id)
82 {
83 CVariant data;
84 data["type"] = content;
85 data["id"] = id;
86 if (g_application.IsMusicScanning())
87 data["transaction"] = true;
88 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data);
89 }
90
AnnounceUpdate(const std::string & content,int id,bool added=false)91 static void AnnounceUpdate(const std::string& content, int id, bool added = false)
92 {
93 CVariant data;
94 data["type"] = content;
95 data["id"] = id;
96 if (g_application.IsMusicScanning())
97 data["transaction"] = true;
98 if (added)
99 data["added"] = true;
100 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data);
101 }
102
CMusicDatabase(void)103 CMusicDatabase::CMusicDatabase(void)
104 {
105 m_translateBlankArtist = true;
106 }
107
~CMusicDatabase(void)108 CMusicDatabase::~CMusicDatabase(void)
109 {
110 EmptyCache();
111 }
112
Open()113 bool CMusicDatabase::Open()
114 {
115 return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic);
116 }
117
CreateTables()118 void CMusicDatabase::CreateTables()
119 {
120 CLog::Log(LOGINFO, "create artist table");
121 m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, "
122 " strArtist varchar(256), strMusicBrainzArtistID text, "
123 " strSortName text, "
124 " strType text, strGender text, strDisambiguation text, "
125 " strBorn text, strFormed text, strGenres text, strMoods text, "
126 " strStyles text, strInstruments text, strBiography text, "
127 " strDied text, strDisbanded text, strYearsActive text, "
128 " strImage text, "
129 " lastScraped varchar(20) default NULL, "
130 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
131 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
132 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
133 // Create missing artist tag artist [Missing].
134 std::string strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
135 "VALUES( %i, '%s', '%s', '%s' )",
136 BLANKARTIST_ID, BLANKARTIST_NAME.c_str(),
137 BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
138 m_pDS->exec(strSQL);
139
140 CLog::Log(LOGINFO, "create album table");
141 m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, "
142 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
143 " strReleaseGroupMBID text, "
144 " strArtistDisp text, strArtistSort text, strGenres text, "
145 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
146 " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
147 " bCompilation integer not null default '0', "
148 " strMoods text, strStyles text, strThemes text, "
149 " strReview text, strImage text, strLabel text, "
150 " strType text, "
151 " strReleaseStatus TEXT, "
152 " fRating FLOAT NOT NULL DEFAULT 0, "
153 " iVotes INTEGER NOT NULL DEFAULT 0, "
154 " iUserrating INTEGER NOT NULL DEFAULT 0, "
155 " lastScraped varchar(20) default NULL, "
156 " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
157 " strReleaseType text, "
158 " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
159 " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
160 " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
161 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
162
163 CLog::Log(LOGINFO, "create audiobook table");
164 m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
165 " strBook varchar(256), strAuthor text,"
166 " bookmark integer, file text,"
167 " dateAdded varchar (20) default NULL)");
168
169 CLog::Log(LOGINFO, "create album_artist table");
170 m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, strArtist text)");
171
172 CLog::Log(LOGINFO, "create album_source table");
173 m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
174
175 CLog::Log(LOGINFO, "create genre table");
176 m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
177
178 CLog::Log(LOGINFO, "create path table");
179 m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
180
181 CLog::Log(LOGINFO, "create source table");
182 m_pDS->exec("CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
183
184 CLog::Log(LOGINFO, "create source_path table");
185 m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
186
187 CLog::Log(LOGINFO, "create song table");
188 m_pDS->exec("CREATE TABLE song (idSong integer primary key, "
189 " idAlbum integer, idPath integer, "
190 " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
191 " iTrack integer, iDuration integer, "
192 " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
193 " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
194 " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
195 " lastplayed varchar(20) default NULL, "
196 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
197 " userrating INTEGER NOT NULL DEFAULT 0, "
198 " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
199 " iBitRate INTEGER NOT NULL DEFAULT 0, "
200 " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
201 " strReplayGain text, "
202 " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
203 CLog::Log(LOGINFO, "create song_artist table");
204 m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder integer, strArtist text)");
205 CLog::Log(LOGINFO, "create song_genre table");
206 m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
207
208 CLog::Log(LOGINFO, "create role table");
209 m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
210 m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role
211
212 CLog::Log(LOGINFO, "create infosetting table");
213 m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, strSettings TEXT)");
214
215 CLog::Log(LOGINFO, "create discography table");
216 m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
217 "strReleaseGroupMBID TEXT)");
218
219 CLog::Log(LOGINFO, "create art table");
220 m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
221
222 CLog::Log(LOGINFO, "create versiontagscan table");
223 m_pDS->exec("CREATE TABLE versiontagscan (idVersion INTEGER, iNeedsScan INTEGER, "
224 "lastscanned VARCHAR(20), "
225 "lastcleaned VARCHAR(20), "
226 "artistlinksupdated VARCHAR(20), "
227 "genresupdated VARCHAR(20))");
228 m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
229 GetSchemaVersion()));
230
231 CLog::Log(LOGINFO, "create removed_link table");
232 m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
233
234 }
235
CreateAnalytics()236 void CMusicDatabase::CreateAnalytics()
237 {
238 CLog::Log(LOGINFO, "%s - creating indices", __FUNCTION__);
239 m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
240 m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
241 m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
242 m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
243
244 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
245 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
246
247 m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
248
249 m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
250 m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
251 m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
252
253 m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))");
254
255 m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
256 m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
257
258 m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
259
260 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
261 m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
262
263 m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))");
264 m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
265 m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
266 m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
267 m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
268 //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
269 m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
270
271 m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
272 m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
273 m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
274 m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
275
276 m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
277 m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
278
279 m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))");
280
281 m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
282
283 m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
284
285 CLog::Log(LOGINFO, "create triggers");
286 m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
287 " DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
288 " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
289 " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
290 " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
291 " END");
292 m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
293 " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
294 " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
295 " DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
296 " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
297 " END");
298 m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
299 " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
300 " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
301 " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
302 " END");
303 m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
304 " DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
305 " DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
306 " END");
307
308 /* Maintain date new and last modified for songs, albums and artists using triggers
309 MySQL triggers cannot modify a table that is already being used by the statement that invoked
310 the trigger (to avoid recursion), but can set NEW column values before insert or update.
311 Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
312 Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
313 // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
314 */
315 bool bisMySQL = StringUtils::EqualsNoCase(
316 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
317
318 if (!bisMySQL)
319 { // SQLite trigger syntax - AFTER INSERT/UPDATE
320 m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
321 " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
322 " AND NEW.dateNew IS NULL;"
323 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
324 " END");
325 m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
326 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
327 " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
328 " END");
329 m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
330 " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
331 " AND NEW.dateNew IS NULL;"
332 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
333 " END");
334 m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
335 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
336 " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
337 " END");
338 m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
339 " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
340 " AND NEW.dateNew IS NULL;"
341 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
342 " END");
343 m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
344 " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
345 " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
346 " END");
347
348 m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
349 " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
350 " END");
351 }
352 else
353 { // MySQL trigger syntax - BEFORE INSERT/UPDATE
354 m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
355 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
356 " SET NEW.dateModified = now();"
357 " END");
358 m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
359 " SET NEW.dateModified = now()");
360
361 m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
362 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
363 " SET NEW.dateModified = now();"
364 " END");
365 m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
366 " SET NEW.dateModified = now()");
367
368 m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
369 " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
370 " SET NEW.dateModified = now();"
371 " END");
372 m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
373 " SET NEW.dateModified = now()");
374
375 m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
376 " UPDATE versiontagscan SET genresupdated = now()");
377 }
378
379 // Triggers to maintain recent changes to album and song artist links in removed_link table
380 m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
381 "DELETE FROM removed_link "
382 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
383 "END");
384 m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
385 "DELETE FROM removed_link "
386 "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
387 "END");
388 CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
389
390 // Create native functions stored in DB (MySQL/MariaDB only)
391 CreateNativeDBFunctions();
392
393 // we create views last to ensure all indexes are rolled in
394 CreateViews();
395 }
396
CreateRemovedLinkTriggers()397 void CMusicDatabase::CreateRemovedLinkTriggers()
398 {
399 // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
400 m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
401 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
402 " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
403 " END");
404 m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
405 " INSERT INTO removed_link (idArtist, idMedia, idRole)"
406 " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
407 " END");
408 }
409
410
CreateViews()411 void CMusicDatabase::CreateViews()
412 {
413 CLog::Log(LOGINFO, "create song view");
414 m_pDS->exec("CREATE VIEW songview AS SELECT "
415 " song.idSong AS idSong, "
416 " song.strArtistDisp AS strArtists,"
417 " song.strArtistSort AS strArtistSort,"
418 " song.strGenres AS strGenres,"
419 " strTitle, "
420 " iTrack, iDuration, "
421 " song.strReleaseDate as strReleaseDate, "
422 " song.strOrigReleaseDate as strOrigReleaseDate, "
423 " song.strDiscSubtitle as strDiscSubtitle, "
424 " strFileName, "
425 " strMusicBrainzTrackID, "
426 " iTimesPlayed, iStartOffset, iEndOffset, "
427 " lastplayed, "
428 " song.rating, "
429 " song.userrating, "
430 " song.votes, "
431 " comment, "
432 " song.idAlbum AS idAlbum, "
433 " strAlbum, "
434 " strPath, "
435 " album.strReleaseStatus as strReleaseStatus,"
436 " album.bCompilation AS bCompilation,"
437 " album.bBoxedSet AS bBoxedSet, "
438 " album.strArtistDisp AS strAlbumArtists,"
439 " album.strArtistSort AS strAlbumArtistSort,"
440 " album.strReleaseType AS strAlbumReleaseType,"
441 " song.mood as mood,"
442 " song.strReplayGain, "
443 " iBPM, "
444 " iBitRate, "
445 " iSampleRate, "
446 " iChannels, "
447 " album.iAlbumDuration AS iAlbumDuration, "
448 " album.iDiscTotal as iDiscTotal, "
449 " song.dateAdded as dateAdded, "
450 " song.dateNew AS dateNew, "
451 " song.dateModified AS dateModified "
452 "FROM song"
453 " JOIN album ON"
454 " song.idAlbum=album.idAlbum"
455 " JOIN path ON"
456 " song.idPath=path.idPath");
457
458 CLog::Log(LOGINFO, "create album view");
459 m_pDS->exec("CREATE VIEW albumview AS SELECT "
460 " album.idAlbum AS idAlbum, "
461 " strAlbum, "
462 " strMusicBrainzAlbumID, "
463 " strReleaseGroupMBID, "
464 " album.strArtistDisp AS strArtists, "
465 " album.strArtistSort AS strArtistSort, "
466 " album.strGenres AS strGenres, "
467 " album.strReleaseDate as strReleaseDate, "
468 " album.strOrigReleaseDate as strOrigReleaseDate, "
469 " album.bBoxedSet AS bBoxedSet, "
470 " album.strMoods AS strMoods, "
471 " album.strStyles AS strStyles, "
472 " strThemes, "
473 " strReview, "
474 " strLabel, "
475 " strType, "
476 " strReleaseStatus, "
477 " album.strImage as strImage, "
478 " album.fRating, "
479 " album.iUserrating, "
480 " album.iVotes, "
481 " bCompilation, "
482 " bScrapedMBID,"
483 " lastScraped,"
484 " dateAdded, dateNew, dateModified, "
485 " (SELECT ROUND(AVG(song.iTimesPlayed)) FROM song WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
486 " strReleaseType, "
487 " iDiscTotal, "
488 " (SELECT MAX(song.lastplayed) FROM song WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
489 " iAlbumDuration "
490 "FROM album"
491 );
492
493 CLog::Log(LOGINFO, "create artist view");
494 m_pDS->exec("CREATE VIEW artistview AS SELECT"
495 " idArtist, strArtist, strSortName, "
496 " strMusicBrainzArtistID, "
497 " strType, strGender, strDisambiguation, "
498 " strBorn, strFormed, strGenres,"
499 " strMoods, strStyles, strInstruments, "
500 " strBiography, strDied, strDisbanded, "
501 " strYearsActive, strImage, "
502 " bScrapedMBID, lastScraped, "
503 " dateAdded, dateNew, dateModified "
504 "FROM artist");
505
506 CLog::Log(LOGINFO, "create albumartist view");
507 m_pDS->exec("CREATE VIEW albumartistview AS SELECT"
508 " album_artist.idAlbum AS idAlbum, "
509 " album_artist.idArtist AS idArtist, "
510 " 0 AS idRole, "
511 " 'AlbumArtist' AS strRole, "
512 " artist.strArtist AS strArtist, "
513 " artist.strSortName AS strSortName,"
514 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
515 " album_artist.iOrder AS iOrder "
516 "FROM album_artist "
517 "JOIN artist ON "
518 " album_artist.idArtist = artist.idArtist");
519
520 CLog::Log(LOGINFO, "create songartist view");
521 m_pDS->exec("CREATE VIEW songartistview AS SELECT"
522 " song_artist.idSong AS idSong, "
523 " song_artist.idArtist AS idArtist, "
524 " song_artist.idRole AS idRole, "
525 " role.strRole AS strRole, "
526 " artist.strArtist AS strArtist, "
527 " artist.strSortName AS strSortName,"
528 " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
529 " song_artist.iOrder AS iOrder "
530 "FROM song_artist "
531 "JOIN artist ON "
532 " song_artist.idArtist = artist.idArtist "
533 "JOIN role ON "
534 " song_artist.idRole = role.idRole");
535 }
536
CreateNativeDBFunctions()537 void CMusicDatabase::CreateNativeDBFunctions()
538 {
539 // Create native functions in MySQL/MariaDB database only
540 if (!StringUtils::EqualsNoCase(
541 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
542 "mysql"))
543 return;
544 CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions");
545 /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
546 default utf8_general_ci collation that SQLite does via a collation sequence callback
547 function to StringUtils::AlphaNumericCompare
548 !@todo: the video needs these defined too for sorting in DB, then creation can be made common
549 */
550 // clang-format off
551 // udfFirstNumberPos finds the position of the first digit in a string
552 m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
553 m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
554 "RETURNS int \n"
555 "LANGUAGE SQL \n"
556 "DETERMINISTIC \n"
557 "NO SQL \n"
558 "SQL SECURITY INVOKER \n"
559 "BEGIN \n"
560 " DECLARE position int; \n"
561 " DECLARE tmppos int; \n"
562 " SET position = 5000; \n"
563 " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
564 " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
565 " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
566 " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
567 " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
568 " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
569 " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
570 " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
571 " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
572 " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
573 " IF(position = 5000) THEN RETURN 0; END IF;\n"
574 " RETURN position; \n"
575 "END\n");
576
577 // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input
578 m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
579 m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
580 "RETURNS varchar(1024)\n"
581 "LANGUAGE SQL\n"
582 "DETERMINISTIC\n"
583 "NO SQL\n"
584 "SQL SECURITY INVOKER\n"
585 "BEGIN\n"
586 " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
587 " DECLARE i int;\n"
588 " DECLARE symbolCharsLen int;\n"
589 " DECLARE symbol char(1);\n"
590 " SET sortString = instring;\n"
591 " SET i = 1;\n"
592 " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
593 " WHILE(i <= symbolCharsLen) DO\n"
594 " SET symbol = SUBSTRING(symbolChars, i, 1);\n"
595 " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
596 " SET i = i + 1;\n"
597 " END WHILE;\n"
598 " RETURN sortString;\n"
599 "END\n");
600
601 // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
602 m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
603 m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
604 "sameOrderChars char(25))\n"
605 "RETURNS varchar(1024)\n"
606 "LANGUAGE SQL\n"
607 "DETERMINISTIC\n"
608 "NO SQL\n"
609 "SQL SECURITY INVOKER\n"
610 "BEGIN\n"
611 " DECLARE sortString varchar(1024);\n"
612 " DECLARE shiftedString varchar(1024);\n"
613 " DECLARE inLength int;\n"
614 " DECLARE shiftedLength int;\n"
615 " DECLARE totalSympadLength int;\n"
616 " DECLARE symbolshifted512 varchar(1024);\n"
617 " DECLARE numStartIndex int; \n"
618 " DECLARE numEndIndex int; \n"
619 " DECLARE padLength int; \n"
620 " DECLARE totalPadLength int; \n"
621 " DECLARE i int; \n"
622 " DECLARE sameOrderCharsLen int;\n"
623 " SET totalPadLength = 0; \n"
624 " SET instring = TRIM(instring);\n"
625 " SET inLength = CHAR_LENGTH(inString);\n"
626 " SET sortString = instring; \n"
627 " SET numStartIndex = udfFirstNumberPos(instring); \n"
628 " SET numEndIndex = 0; \n"
629 " SET i = 1; \n"
630 " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
631 " WHILE(i <= sameOrderCharsLen) DO \n"
632 " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
633 " SET i = i + 1; \n"
634 " END WHILE; \n"
635 " WHILE(numStartIndex <> 0) DO \n"
636 " SET numStartIndex = numStartIndex + numEndIndex; \n"
637 " SET numEndIndex = numStartIndex; \n"
638 " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
639 " SET numEndIndex = numEndIndex + 1; \n"
640 " END WHILE; \n"
641 " SET numEndIndex = numEndIndex - 1; \n"
642 " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
643 " IF padLength < 0 THEN \n"
644 " SET padLength = 0; \n"
645 " END IF; \n"
646 " IF inLength + totalPadLength + padlength > 1024 THEN \n"
647 " -- Padding more digits would be too long, pad this one just enough \n"
648 " SET padLength = 1024 - inLength - totalPadLength; \n"
649 " SET numStartIndex = 0; \n"
650 " END IF; \n"
651 " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
652 " SET totalPadLength = totalPadLength + padLength; \n"
653 " IF numStartIndex <> 0 THEN \n"
654 " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
655 " END IF; \n"
656 " END WHILE; \n"
657 " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
658 " -- when there is space as this could double string length. Note '\\' needs escaping \n"
659 " SET numStartIndex = 1; \n"
660 " SET numEndIndex = inLength + totalPadLength; \n"
661 " IF numEndIndex < 1024 THEN \n"
662 " SET shiftedLength = 0; \n"
663 " SET totalSympadLength = 0; \n"
664 " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
665 " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
666 " SET numStartIndex = numStartIndex + 512; \n"
667 " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
668 " IF totalSympadLength = 0 THEN \n"
669 " SET shiftedString = symbolshifted512; \n"
670 " ELSE \n"
671 " IF totalSympadLength + shiftedLength > 1024 THEN \n"
672 " SET shiftedLength = 1024 - totalSympadLength; \n"
673 " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
674 " END IF; \n"
675 " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
676 " END IF; \n"
677 " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
678 " END WHILE; \n"
679 " SET sortString = shiftedString; \n"
680 " END IF; \n"
681 " RETURN sortString; \n"
682 "END\n");
683 // clang-format on
684 }
685
SplitPath(const std::string & strFileNameAndPath,std::string & strPath,std::string & strFileName)686 void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName)
687 {
688 URIUtils::Split(strFileNameAndPath, strPath, strFileName);
689 // Keep protocol options as part of the path
690 if (URIUtils::IsURL(strFileNameAndPath))
691 {
692 CURL url(strFileNameAndPath);
693 if (!url.GetProtocolOptions().empty())
694 strPath += "|" + url.GetProtocolOptions();
695 }
696 }
697
AddAlbum(CAlbum & album,int idSource)698 bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource)
699 {
700 BeginTransaction();
701 SetLibraryLastUpdated();
702
703 album.idAlbum = AddAlbum(album.strAlbum,
704 album.strMusicBrainzAlbumID,
705 album.strReleaseGroupMBID,
706 album.GetAlbumArtistString(),
707 album.GetAlbumArtistSort(),
708 album.GetGenreString(),
709 album.strReleaseDate, album.strOrigReleaseDate,
710 album.bBoxedSet,
711 album.strLabel, album.strType, album.strReleaseStatus,
712 album.bCompilation, album.releaseType);
713
714 // Add the album artists
715 if (album.artistCredits.empty())
716 AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0); // Album must have at least one artist so set artist to [Missing]
717 for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
718 {
719 artistCredit->idArtist = AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(), artistCredit->GetSortName());
720 AddAlbumArtist(artistCredit->idArtist,
721 album.idAlbum,
722 artistCredit->GetArtist(),
723 static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
724 }
725
726 for (auto song = album.songs.begin(); song != album.songs.end(); ++song)
727 {
728 song->idAlbum = album.idAlbum;
729
730 song->idSong = AddSong(song->idSong, song->dateNew,
731 song->idAlbum,
732 song->strTitle, song->strMusicBrainzTrackID,
733 song->strFileName, song->strComment,
734 song->strMood, song->strThumb,
735 song->GetArtistString(),
736 song->GetArtistSort(),
737 song->genre,
738 song->iTrack, song->iDuration,
739 song->strReleaseDate, song->strOrigReleaseDate,
740 song->strDiscSubtitle,
741 song->iTimesPlayed, song->iStartOffset,
742 song->iEndOffset,
743 song->lastPlayed,
744 song->rating,
745 song->userrating,
746 song->votes,
747 song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels,
748 song->replayGain);
749
750 if (song->artistCredits.empty())
751 AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0); // Song must have at least one artist so set artist to [Missing]
752
753 for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end(); ++artistCredit)
754 {
755 artistCredit->idArtist = AddArtist(artistCredit->GetArtist(),
756 artistCredit->GetMusicBrainzArtistID(),
757 artistCredit->GetSortName());
758 AddSongArtist(artistCredit->idArtist,
759 song->idSong,
760 ROLE_ARTIST,
761 artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
762 static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit)));
763 }
764 // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
765 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
766 AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort());
767 }
768
769 // Set album duration as total of all songs on album.
770 // Folder layout may mean AddAlbum call has added more songs to an existing album
771 std::string strSQL;
772 strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum);
773 int albumDuration = GetSingleValueInt(strSQL);
774 m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration,
775 album.idAlbum));
776
777 // Add album sources
778 if (idSource > 0)
779 AddAlbumSource(album.idAlbum, idSource);
780 else
781 {
782 // Use album path, or failing that song paths to determine sources for the album
783 AddAlbumSources(album.idAlbum, album.strPath);
784 }
785
786 for (const auto &albumArt : album.art)
787 SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second);
788
789 // Set album disc total
790 m_pDS->exec(
791 PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
792 "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
793 album.idAlbum));
794 // Set a non-compilation album as a boxset if it has three or more distinct disc titles
795 if (!album.bBoxedSet && !album.bCompilation)
796 {
797 std::string strSQL;
798 strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
799 album.idAlbum);
800 int numTitles = GetSingleValueInt(strSQL);
801 if (numTitles >=3)
802 {
803 strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum);
804 m_pDS->exec(strSQL);
805 }
806 }
807 m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
808 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
809 album.idAlbum));
810 m_pDS->exec(PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
811 "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
812 album.idAlbum));
813
814 std::string albumdateadded =
815 GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum));
816 m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
817 albumdateadded.c_str(), album.idAlbum));
818
819 /* Update artist dateAdded values for artists involved in album as song or album artists.
820 Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
821 derived from song dateadded values which are usually file dates (or the last scan).
822 It is used to indicate those artists with recent media.
823 For artists that are neither album nor song artists (other roles only) dateadded will be null.
824 */
825 std::vector<std::string> artistIDs;
826 // Get distinct song and album artist IDs for this album
827 GetArtistsByAlbum(album.idAlbum, artistIDs);
828 std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")";
829 strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' "
830 "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
831 albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str());
832 m_pDS->exec(strSQL);
833
834 CommitTransaction();
835 return true;
836 }
837
UpdateAlbum(CAlbum & album)838 bool CMusicDatabase::UpdateAlbum(CAlbum& album)
839 {
840 BeginTransaction();
841 SetLibraryLastUpdated();
842
843 const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
844 bool isBoxset = false;
845 bool canBeBoxset = false;
846
847 if (album.bBoxedSet)
848 {
849 isBoxset = IsAlbumBoxset(album.idAlbum);
850 if (!isBoxset)
851 { // not a boxset already so make sure we have enough discs & they all have titles
852 std::string strSQL;
853 strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum);
854 int numDiscs = GetSingleValueInt(strSQL);
855 if (numDiscs >= 2)
856 {
857 canBeBoxset = true;
858 int discValue;
859 for (discValue = 1; discValue <= numDiscs; discValue++)
860 {
861 strSQL =
862 PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
863 "song.iTrack >> 16 = %i",
864 album.idAlbum, discValue);
865 std::string currentTitle = GetSingleValue(strSQL);
866 if (currentTitle.empty())
867 {
868 currentTitle = StringUtils::Format("%s %i", g_localizeStrings.Get(427), discValue);
869 strSQL =
870 PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
871 "song.iTrack >> 16 = %i",
872 currentTitle.c_str(), album.idAlbum, discValue);
873 ExecuteQuery(strSQL);
874 }
875 }
876 }
877 if (!canBeBoxset && album.bBoxedSet)
878 {
879 CLog::Log(LOGINFO, "%s : Album with id [%i] does not meet the requirements for a boxset.",
880 __FUNCTION__, album.idAlbum);
881 album.bBoxedSet = false;
882 }
883 }
884 }
885 UpdateAlbum(album.idAlbum,
886 album.strAlbum, album.strMusicBrainzAlbumID,
887 album.strReleaseGroupMBID,
888 album.GetAlbumArtistString(), album.GetAlbumArtistSort(),
889 album.GetGenreString(),
890 StringUtils::Join(album.moods, itemSeparator),
891 StringUtils::Join(album.styles, itemSeparator),
892 StringUtils::Join(album.themes, itemSeparator),
893 album.strReview,
894 album.thumbURL.GetData(),
895 album.strLabel, album.strType, album.strReleaseStatus,
896 album.fRating, album.iUserrating, album.iVotes,
897 album.strReleaseDate, album.strOrigReleaseDate,
898 album.bBoxedSet,
899 album.bCompilation, album.releaseType,
900 album.bScrapedMBID);
901
902 if (!album.bArtistSongMerge)
903 {
904 // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
905 for (const auto &artistCredit : album.artistCredits)
906 UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID());
907 }
908 else
909 {
910 // Replace the album artists with those scraped or set by JSON
911 DeleteAlbumArtistsByAlbum(album.idAlbum);
912 if (album.artistCredits.empty())
913 AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0); // Album must have at least one artist so set artist to [Missing]
914 for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
915 {
916 artistCredit->idArtist = AddArtist(artistCredit->GetArtist(),
917 artistCredit->GetMusicBrainzArtistID(), artistCredit->GetSortName(), true);
918 AddAlbumArtist(artistCredit->idArtist,
919 album.idAlbum,
920 artistCredit->GetArtist(),
921 static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
922 }
923 /* Replace the songs with those scraped or imported, but if new songs is empty
924 (such as when called from JSON) do not remove the original ones
925 Also updates nested data e.g. song artists, song genres and contributors
926 Do not check for artist link changes, that is done later for all songs and album
927 */
928 int albumDuration = 0;
929 for (auto &song : album.songs)
930 {
931 UpdateSong(song);
932 albumDuration += song.iDuration;
933 }
934 if (albumDuration > 0)
935 m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
936 albumDuration, album.idAlbum));
937 }
938
939 if (!album.art.empty())
940 SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
941
942 CheckArtistLinksChanged();
943
944 CommitTransaction();
945 return true;
946 }
947
NormaliseSongDates(std::string & strRelease,std::string & strOriginal)948 void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal)
949 {
950 // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
951 int iDate;
952 iDate = StringUtils::DateStringToYYYYMMDD(strRelease);
953 if (iDate < 0)
954 strRelease.clear();
955 iDate = StringUtils::DateStringToYYYYMMDD(strOriginal);
956 if (iDate < 0)
957 strOriginal.clear();
958 // Avoid missing release or original values unless both invalid or empty
959 if (!strRelease.empty() && strOriginal.empty())
960 strOriginal = strRelease;
961 else if (strRelease.empty() && !strOriginal.empty())
962 strRelease = strOriginal;
963 }
964
AddSong(const int idSong,const CDateTime & dtDateNew,const int idAlbum,const std::string & strTitle,const std::string & strMusicBrainzTrackID,const std::string & strPathAndFileName,const std::string & strComment,const std::string & strMood,const std::string & strThumb,const std::string & artistDisp,const std::string & artistSort,const std::vector<std::string> & genres,int iTrack,int iDuration,const std::string & strReleaseDate,const std::string & strOrigReleaseDate,std::string & strDiscSubtitle,const int iTimesPlayed,int iStartOffset,int iEndOffset,const CDateTime & dtLastPlayed,float rating,int userrating,int votes,int iBPM,int iBitRate,int iSampleRate,int iChannels,const ReplayGain & replayGain)965 int CMusicDatabase::AddSong(const int idSong, const CDateTime& dtDateNew,
966 const int idAlbum,
967 const std::string& strTitle, const std::string& strMusicBrainzTrackID,
968 const std::string& strPathAndFileName, const std::string& strComment,
969 const std::string& strMood, const std::string& strThumb,
970 const std::string &artistDisp, const std::string &artistSort,
971 const std::vector<std::string>& genres,
972 int iTrack, int iDuration,
973 const std::string& strReleaseDate,
974 const std::string& strOrigReleaseDate,
975 std::string& strDiscSubtitle,
976 const int iTimesPlayed, int iStartOffset, int iEndOffset,
977 const CDateTime& dtLastPlayed, float rating, int userrating, int votes,
978 int iBPM, int iBitRate, int iSampleRate, int iChannels,
979 const ReplayGain& replayGain)
980 {
981 int idNew = -1;
982 std::string strSQL;
983 try
984 {
985 // We need at least the title
986 if (strTitle.empty())
987 return -1;
988
989 if (nullptr == m_pDB)
990 return -1;
991 if (nullptr == m_pDS)
992 return -1;
993
994 std::string strPath, strFileName;
995 SplitPath(strPathAndFileName, strPath, strFileName);
996 int idPath = AddPath(strPath);
997
998 if (idSong <= 1)
999 {
1000 if (!strMusicBrainzTrackID.empty())
1001 strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1002 "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
1003 idAlbum,
1004 iTrack,
1005 strMusicBrainzTrackID.c_str());
1006 else
1007 strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
1008 "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
1009 "AND strMusicBrainzTrackID IS NULL",
1010 idAlbum,
1011 strFileName.c_str(),
1012 strTitle.c_str(),
1013 iTrack);
1014
1015 if (!m_pDS->query(strSQL))
1016 return -1;
1017 }
1018 if (m_pDS->num_rows() == 0)
1019 {
1020 m_pDS->close();
1021
1022 // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
1023 bool isBoxset = IsAlbumBoxset(idAlbum);
1024 if (isBoxset && strDiscSubtitle.empty())
1025 {
1026 int discno = iTrack >> 16;
1027 strDiscSubtitle = StringUtils::Format("%s %i", g_localizeStrings.Get(427), discno);
1028 }
1029
1030 // Validate ISO8601 dates and ensure none missing
1031 std::string strRelease = strReleaseDate;
1032 std::string strOriginal = strOrigReleaseDate;
1033 NormaliseSongDates(strRelease, strOriginal);
1034
1035 // Get dateAdded from music file timestamp
1036 std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1037
1038 strSQL = "INSERT INTO song ("
1039 "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
1040 "strTitle, iTrack, iDuration, "
1041 "strReleaseDate, strOrigReleaseDate, iBPM, "
1042 "iBitrate, iSampleRate, iChannels, "
1043 "strDiscSubtitle, strFileName, dateAdded, "
1044 "strMusicBrainzTrackID, strArtistSort, "
1045 "iTimesPlayed, iStartOffset, iEndOffset, "
1046 "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
1047
1048 if (idSong <= 0)
1049 // Song ID is autoincremented and dateNew set by trigger
1050 strSQL += PrepareSQL("VALUES (NULL, NULL, ");
1051 else
1052 //Reuse song Id and original date when the Id added
1053 strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str());
1054
1055 strSQL +=
1056 PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
1057 idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration,
1058 strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate,
1059 iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str());
1060
1061 if (strMusicBrainzTrackID.empty())
1062 strSQL += PrepareSQL(",NULL");
1063 else
1064 strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str());
1065 if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1066 strSQL += PrepareSQL(",NULL");
1067 else
1068 strSQL += PrepareSQL(",'%s'", artistSort.c_str());
1069
1070 if (dtLastPlayed.IsValid())
1071 strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", iTimesPlayed,
1072 iStartOffset, iEndOffset, dtLastPlayed.GetAsDBDateTime().c_str(),
1073 rating, userrating, votes, strComment.c_str(), strMood.c_str(),
1074 replayGain.Get().c_str());
1075 else
1076 strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", iTimesPlayed,
1077 iStartOffset, iEndOffset, rating, userrating, votes,
1078 strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
1079 m_pDS->exec(strSQL);
1080 if (idSong <= 0)
1081 idNew = (int)m_pDS->lastinsertid();
1082 else
1083 idNew = idSong;
1084 }
1085 else
1086 {
1087 idNew = m_pDS->fv("idSong").get_asInt();
1088 m_pDS->close();
1089 UpdateSong(idNew, strTitle, strMusicBrainzTrackID, strPathAndFileName, strComment, strMood,
1090 strThumb, artistDisp, artistSort, genres, iTrack, iDuration,
1091 strReleaseDate, strOrigReleaseDate, strDiscSubtitle, iTimesPlayed,
1092 iStartOffset, iEndOffset, dtLastPlayed, rating, userrating, votes, replayGain,
1093 iBPM, iBitRate, iSampleRate, iChannels);
1094 }
1095 if (!strThumb.empty())
1096 SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb);
1097
1098 // Song genres added, and genre string updated to use the standardised genre names
1099 AddSongGenres(idNew, genres);
1100
1101 AnnounceUpdate(MediaTypeSong, idNew, true);
1102 }
1103 catch (...)
1104 {
1105 CLog::Log(LOGERROR, "musicdatabase:unable to addsong (%s)", strSQL.c_str());
1106 }
1107 return idNew;
1108 }
1109
GetSong(int idSong,CSong & song)1110 bool CMusicDatabase::GetSong(int idSong, CSong& song)
1111 {
1112 try
1113 {
1114 song.Clear();
1115
1116 if (nullptr == m_pDB)
1117 return false;
1118 if (nullptr == m_pDS)
1119 return false;
1120
1121 std::string strSQL=PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
1122 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1123 " WHERE songview.idSong = %i "
1124 " ORDER BY songartistview.idRole, songartistview.iOrder", idSong);
1125
1126 if (!m_pDS->query(strSQL)) return false;
1127 int iRowsFound = m_pDS->num_rows();
1128 if (iRowsFound == 0)
1129 {
1130 m_pDS->close();
1131 return false;
1132 }
1133
1134 int songArtistOffset = song_enumCount;
1135
1136 song = GetSongFromDataset(m_pDS->get_sql_record());
1137 while (!m_pDS->eof())
1138 {
1139 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1140
1141 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1142 if (idSongArtistRole == ROLE_ARTIST)
1143 song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
1144 else
1145 song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1146
1147 m_pDS->next();
1148 }
1149 m_pDS->close(); // cleanup recordset data
1150 return true;
1151 }
1152 catch (...)
1153 {
1154 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
1155 }
1156
1157 return false;
1158 }
1159
UpdateSong(CSong & song,bool bArtists,bool bArtistLinks)1160 bool CMusicDatabase::UpdateSong(CSong& song,
1161 bool bArtists /*= true*/,
1162 bool bArtistLinks /*= true*/)
1163 {
1164 int result = UpdateSong(song.idSong,
1165 song.strTitle,
1166 song.strMusicBrainzTrackID,
1167 song.strFileName,
1168 song.strComment,
1169 song.strMood,
1170 song.strThumb,
1171 song.GetArtistString(),
1172 song.GetArtistSort(),
1173 song.genre,
1174 song.iTrack,
1175 song.iDuration,
1176 song.strReleaseDate,
1177 song.strOrigReleaseDate,
1178 song.strDiscSubtitle,
1179 song.iTimesPlayed,
1180 song.iStartOffset,
1181 song.iEndOffset,
1182 song.lastPlayed,
1183 song.rating,
1184 song.userrating,
1185 song.votes,
1186 song.replayGain,
1187 song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels);
1188 if (result < 0)
1189 return false;
1190
1191 // Replace Song genres and update genre string using the standardised genre names
1192 AddSongGenres(song.idSong, song.genre);
1193 if (bArtists)
1194 {
1195 //Replace song artists and contributors
1196 DeleteSongArtistsBySong(song.idSong);
1197 if (song.artistCredits.empty())
1198 AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0); // Song must have at least one artist so set artist to [Missing]
1199 for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end(); ++artistCredit)
1200 {
1201 artistCredit->idArtist = AddArtist(artistCredit->GetArtist(),
1202 artistCredit->GetMusicBrainzArtistID(), artistCredit->GetSortName());
1203 AddSongArtist(artistCredit->idArtist,
1204 song.idSong,
1205 ROLE_ARTIST,
1206 artistCredit->GetArtist(),
1207 static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit)));
1208 }
1209 // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
1210 // and use COMPOSERSORT tag data to provide sort names for artists that are composers
1211 AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort());
1212
1213 if (bArtistLinks)
1214 CheckArtistLinksChanged();
1215 }
1216
1217 return true;
1218 }
1219
UpdateSong(int idSong,const std::string & strTitle,const std::string & strMusicBrainzTrackID,const std::string & strPathAndFileName,const std::string & strComment,const std::string & strMood,const std::string & strThumb,const std::string & artistDisp,const std::string & artistSort,const std::vector<std::string> & genres,int iTrack,int iDuration,const std::string & strReleaseDate,const std::string & strOrigReleaseDate,const std::string & strDiscSubtitle,int iTimesPlayed,int iStartOffset,int iEndOffset,const CDateTime & dtLastPlayed,float rating,int userrating,int votes,const ReplayGain & replayGain,int iBPM,int iBitRate,int iSampleRate,int iChannels)1220 int CMusicDatabase::UpdateSong(int idSong,
1221 const std::string& strTitle, const std::string& strMusicBrainzTrackID,
1222 const std::string& strPathAndFileName, const std::string& strComment,
1223 const std::string& strMood, const std::string& strThumb,
1224 const std::string &artistDisp, const std::string &artistSort,
1225 const std::vector<std::string>& genres,
1226 int iTrack, int iDuration,
1227 const std::string& strReleaseDate,
1228 const std::string& strOrigReleaseDate,
1229 const std::string& strDiscSubtitle,
1230 int iTimesPlayed, int iStartOffset, int iEndOffset,
1231 const CDateTime& dtLastPlayed, float rating, int userrating, int votes,
1232 const ReplayGain& replayGain, int iBPM, int iBitRate,
1233 int iSampleRate,
1234 int iChannels)
1235 {
1236 if (idSong < 0)
1237 return -1;
1238
1239 std::string strSQL;
1240 std::string strPath, strFileName;
1241 SplitPath(strPathAndFileName, strPath, strFileName);
1242 int idPath = AddPath(strPath);
1243
1244 // Validate ISO8601 dates and ensure none missing
1245 std::string strRelease = strReleaseDate;
1246 std::string strOriginal = strOrigReleaseDate;
1247 NormaliseSongDates(strRelease, strOriginal);
1248
1249 std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
1250
1251 strSQL = PrepareSQL(
1252 "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
1253 " strTitle = '%s', iTrack = %i, iDuration = %i, "
1254 "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
1255 "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
1256 "dateAdded = '%s'",
1257 idPath, artistDisp.c_str(),
1258 StringUtils::Join(
1259 genres,
1260 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)
1261 .c_str(),
1262 strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(),
1263 strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels,
1264 strDateMedia.c_str());
1265 if (strMusicBrainzTrackID.empty())
1266 strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL");
1267 else
1268 strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str());
1269 if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
1270 strSQL += PrepareSQL(", strArtistSort = NULL");
1271 else
1272 strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str());
1273
1274 strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
1275 "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
1276 iStartOffset, iEndOffset, rating, userrating, votes, strComment.c_str(),
1277 strMood.c_str(), replayGain.Get().c_str());
1278
1279 if (dtLastPlayed.IsValid())
1280 strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1281 dtLastPlayed.GetAsDBDateTime().c_str());
1282 else if (iTimesPlayed > 0)
1283 strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
1284 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
1285 else
1286 strSQL += ", iTimesPlayed = 0, lastplayed = NULL ";
1287 strSQL += PrepareSQL("WHERE idSong = %i", idSong);
1288
1289 bool status = ExecuteQuery(strSQL);
1290
1291 if (status)
1292 AnnounceUpdate(MediaTypeSong, idSong);
1293 return idSong;
1294 }
1295
AddAlbum(const std::string & strAlbum,const std::string & strMusicBrainzAlbumID,const std::string & strReleaseGroupMBID,const std::string & strArtist,const std::string & strArtistSort,const std::string & strGenre,const std::string & strReleaseDate,const std::string & strOrigReleaseDate,bool bBoxedSet,const std::string & strRecordLabel,const std::string & strType,const std::string & strReleaseStatus,bool bCompilation,CAlbum::ReleaseType releaseType)1296 int CMusicDatabase::AddAlbum(const std::string& strAlbum, const std::string& strMusicBrainzAlbumID,
1297 const std::string& strReleaseGroupMBID,
1298 const std::string& strArtist, const std::string& strArtistSort,
1299 const std::string& strGenre,
1300 const std::string& strReleaseDate, const std::string& strOrigReleaseDate,
1301 bool bBoxedSet,
1302 const std::string& strRecordLabel, const std::string& strType,
1303 const std::string& strReleaseStatus,
1304 bool bCompilation, CAlbum::ReleaseType releaseType)
1305 {
1306 std::string strSQL;
1307 try
1308 {
1309 if (nullptr == m_pDB)
1310 return -1;
1311 if (nullptr == m_pDS)
1312 return -1;
1313
1314 if (!strMusicBrainzAlbumID.empty())
1315 strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
1316 strMusicBrainzAlbumID.c_str());
1317 else
1318 strSQL = PrepareSQL("SELECT * FROM album WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' AND strMusicBrainzAlbumID IS NULL",
1319 strArtist.c_str(),
1320 strAlbum.c_str());
1321 m_pDS->query(strSQL);
1322 std::string strCheckFlag = strType;
1323 StringUtils::ToLower(strCheckFlag);
1324 if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type
1325 bBoxedSet = true;
1326 if (m_pDS->num_rows() == 0)
1327 {
1328 m_pDS->close();
1329 // Does not exist, add it
1330 strSQL = PrepareSQL(
1331 "INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
1332 "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
1333 "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, "
1334 "strMusicBrainzAlbumID, "
1335 "strReleaseGroupMBID, strArtistSort) "
1336 "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
1337 strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(),
1338 strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet,
1339 strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), bCompilation,
1340 CAlbum::ReleaseTypeToString(releaseType).c_str());
1341
1342 if (strMusicBrainzAlbumID.empty())
1343 strSQL += PrepareSQL(", NULL");
1344 else
1345 strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str());
1346 if (strReleaseGroupMBID.empty())
1347 strSQL += PrepareSQL(", NULL");
1348 else
1349 strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str());
1350 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1351 strSQL += PrepareSQL(", NULL");
1352 else
1353 strSQL += PrepareSQL(", '%s'", strArtistSort.c_str());
1354 strSQL += ")";
1355 m_pDS->exec(strSQL);
1356
1357 return (int)m_pDS->lastinsertid();
1358 }
1359 else
1360 {
1361 /* Exists in our database and being re-scanned from tags, so we should update it as the details
1362 may have changed.
1363
1364 Note that for multi-folder albums this will mean the last folder scanned will have the information
1365 stored for it. Most values here should be the same across all songs anyway, but it does mean
1366 that if there's any inconsistencies then only the last folders information will be taken.
1367
1368 We make sure we clear out the link tables (album artists, album sources) and we reset
1369 the last scraped time to make sure that online metadata is re-fetched. */
1370 int idAlbum = m_pDS->fv("idAlbum").get_asInt();
1371 m_pDS->close();
1372
1373 strSQL = "UPDATE album SET ";
1374 if (!strMusicBrainzAlbumID.empty())
1375 strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", strAlbum.c_str(), strArtist.c_str());
1376 if (strReleaseGroupMBID.empty())
1377 strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,");
1378 else
1379 strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str());
1380 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1381 strSQL += PrepareSQL(" strArtistSort = NULL");
1382 else
1383 strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str());
1384
1385 strSQL +=
1386 PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1387 "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
1388 "bCompilation=%i, strReleaseType = '%s', "
1389 "lastScraped = NULL "
1390 "WHERE idAlbum=%i",
1391 strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(),
1392 bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(),
1393 bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(),
1394 idAlbum);
1395 m_pDS->exec(strSQL);
1396 DeleteAlbumArtistsByAlbum(idAlbum);
1397 DeleteAlbumSources(idAlbum);
1398 return idAlbum;
1399 }
1400 }
1401 catch (...)
1402 {
1403 CLog::Log(LOGERROR, "%s failed with query (%s)", __FUNCTION__, strSQL.c_str());
1404 }
1405
1406 return -1;
1407 }
1408
UpdateAlbum(int idAlbum,const std::string & strAlbum,const std::string & strMusicBrainzAlbumID,const std::string & strReleaseGroupMBID,const std::string & strArtist,const std::string & strArtistSort,const std::string & strGenre,const std::string & strMoods,const std::string & strStyles,const std::string & strThemes,const std::string & strReview,const std::string & strImage,const std::string & strLabel,const std::string & strType,const std::string & strReleaseStatus,float fRating,int iUserrating,int iVotes,const std::string & strReleaseDate,const std::string & strOrigReleaseDate,bool bBoxedSet,bool bCompilation,CAlbum::ReleaseType releaseType,bool bScrapedMBID)1409 int CMusicDatabase::UpdateAlbum(int idAlbum,
1410 const std::string& strAlbum,
1411 const std::string& strMusicBrainzAlbumID,
1412 const std::string& strReleaseGroupMBID,
1413 const std::string& strArtist,
1414 const std::string& strArtistSort,
1415 const std::string& strGenre,
1416 const std::string& strMoods,
1417 const std::string& strStyles,
1418 const std::string& strThemes,
1419 const std::string& strReview,
1420 const std::string& strImage,
1421 const std::string& strLabel,
1422 const std::string& strType,
1423 const std::string& strReleaseStatus,
1424 float fRating,
1425 int iUserrating,
1426 int iVotes,
1427 const std::string& strReleaseDate,
1428 const std::string& strOrigReleaseDate,
1429 bool bBoxedSet,
1430 bool bCompilation,
1431 CAlbum::ReleaseType releaseType,
1432 bool bScrapedMBID)
1433 {
1434 if (idAlbum < 0)
1435 return -1;
1436
1437 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1438 // Truncate value cleaning up xml when URLs exceeds this
1439 std::string strImageURLs = strImage;
1440 if (StringUtils::EqualsNoCase(
1441 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1442 "mysql"))
1443 TrimImageURLs(strImageURLs, 65535);
1444
1445 std::string strSQL;
1446 strSQL = PrepareSQL("UPDATE album SET "
1447 " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
1448 " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
1449 " strReview = '%s', strImage = '%s', strLabel = '%s', "
1450 " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
1451 " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
1452 " bBoxedSet = %i, bCompilation = %i,"
1453 " strReleaseType = '%s', strReleaseStatus = '%s', "
1454 " lastScraped = '%s', bScrapedMBID = %i",
1455 strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(),
1456 strMoods.c_str(), strStyles.c_str(), strThemes.c_str(),
1457 strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(),
1458 strType.c_str(), fRating, iUserrating, iVotes,
1459 strReleaseDate.c_str(), strOrigReleaseDate.c_str(),
1460 bBoxedSet, bCompilation,
1461 CAlbum::ReleaseTypeToString(releaseType).c_str(),
1462 strReleaseStatus.c_str(),
1463 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(),
1464 bScrapedMBID);
1465 if (strMusicBrainzAlbumID.empty())
1466 strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL");
1467 else
1468 strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str());
1469 if (strReleaseGroupMBID.empty())
1470 strSQL += PrepareSQL(", strReleaseGroupMBID = NULL");
1471 else
1472 strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str());
1473 if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
1474 strSQL += PrepareSQL(", strArtistSort = NULL");
1475 else
1476 strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str());
1477
1478 strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum);
1479
1480 bool status = ExecuteQuery(strSQL);
1481 if (status)
1482 AnnounceUpdate(MediaTypeAlbum, idAlbum);
1483 return idAlbum;
1484 }
1485
GetAlbum(int idAlbum,CAlbum & album,bool getSongs)1486 bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */)
1487 {
1488 try
1489 {
1490 if (nullptr == m_pDB)
1491 return false;
1492 if (nullptr == m_pDS)
1493 return false;
1494
1495 if (idAlbum == -1)
1496 return false; // not in the database
1497
1498 //Get album, song and album song info data using separate queries/datasets because we can have
1499 //multiple roles per artist for songs and that makes a single combined join impractical
1500 //Get album data
1501 std::string sql;
1502 sql = PrepareSQL("SELECT albumview.*,albumartistview.* "
1503 " FROM albumview "
1504 " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
1505 " WHERE albumview.idAlbum = %ld "
1506 " ORDER BY albumartistview.iOrder", idAlbum);
1507
1508 CLog::Log(LOGDEBUG, "%s", sql.c_str());
1509 if (!m_pDS->query(sql)) return false;
1510 if (m_pDS->num_rows() == 0)
1511 {
1512 m_pDS->close();
1513 return false;
1514 }
1515
1516 int albumArtistOffset = album_enumCount;
1517
1518 album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0, true); // true to grab and parse the imageURL
1519 while (!m_pDS->eof())
1520 {
1521 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1522
1523 // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
1524 // so there is only one row in the result set for each artist credit.
1525 album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
1526
1527 m_pDS->next();
1528 }
1529 m_pDS->close(); // cleanup recordset data
1530
1531 //Get song data
1532 if (getSongs)
1533 {
1534 sql = PrepareSQL("SELECT songview.*, songartistview.*"
1535 " FROM songview "
1536 " JOIN songartistview ON songview.idSong = songartistview.idSong "
1537 " WHERE songview.idAlbum = %ld "
1538 " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder", idAlbum);
1539
1540 CLog::Log(LOGDEBUG, "%s", sql.c_str());
1541 if (!m_pDS->query(sql)) return false;
1542 if (m_pDS->num_rows() == 0) //Album with no songs
1543 {
1544 m_pDS->close();
1545 return false;
1546 }
1547
1548 int songArtistOffset = song_enumCount;
1549 std::set<int> songs;
1550 while (!m_pDS->eof())
1551 {
1552 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1553
1554 int idSong = record->at(song_idSong).get_asInt(); //Same as songartist.idSong by join
1555 if (songs.find(idSong) == songs.end())
1556 {
1557 album.songs.emplace_back(GetSongFromDataset(record));
1558 songs.insert(idSong);
1559 }
1560
1561 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
1562 //By query order song is the last one appended to the album song vector.
1563 if (idSongArtistRole == ROLE_ARTIST)
1564 album.songs.back().artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
1565 else
1566 album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
1567
1568 m_pDS->next();
1569 }
1570 m_pDS->close(); // cleanup recordset data
1571 }
1572
1573 return true;
1574 }
1575 catch (...)
1576 {
1577 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
1578 }
1579
1580 return false;
1581 }
1582
ClearAlbumLastScrapedTime(int idAlbum)1583 bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum)
1584 {
1585 std::string strSQL = PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum);
1586 return ExecuteQuery(strSQL);
1587 }
1588
HasAlbumBeenScraped(int idAlbum)1589 bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum)
1590 {
1591 std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum);
1592 return GetSingleValue(strSQL).empty();
1593 }
1594
AddGenre(std::string & strGenre)1595 int CMusicDatabase::AddGenre(std::string& strGenre)
1596 {
1597 std::string strSQL;
1598 try
1599 {
1600 StringUtils::Trim(strGenre);
1601
1602 if (strGenre.empty())
1603 strGenre=g_localizeStrings.Get(13205); // Unknown
1604
1605 if (nullptr == m_pDB)
1606 return -1;
1607 if (nullptr == m_pDS)
1608 return -1;
1609
1610 auto it = m_genreCache.find(strGenre);
1611 if (it != m_genreCache.end())
1612 return it->second;
1613
1614
1615 strSQL=PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'", strGenre.c_str());
1616 m_pDS->query(strSQL);
1617 if (m_pDS->num_rows() == 0)
1618 {
1619 m_pDS->close();
1620 // doesn't exists, add it
1621 strSQL=PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )", strGenre.c_str());
1622 m_pDS->exec(strSQL);
1623
1624 int idGenre = (int)m_pDS->lastinsertid();
1625 m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1626 return idGenre;
1627 }
1628 else
1629 {
1630 int idGenre = m_pDS->fv("idGenre").get_asInt();
1631 strGenre = m_pDS->fv("strGenre").get_asString();
1632 m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
1633 m_pDS->close();
1634 return idGenre;
1635 }
1636 }
1637 catch (...)
1638 {
1639 CLog::Log(LOGERROR, "musicdatabase:unable to addgenre (%s)", strSQL.c_str());
1640 }
1641
1642 return -1;
1643 }
1644
UpdateArtist(const CArtist & artist)1645 bool CMusicDatabase::UpdateArtist(const CArtist& artist)
1646 {
1647 SetLibraryLastUpdated();
1648
1649 const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
1650
1651 UpdateArtist(artist.idArtist,
1652 artist.strArtist, artist.strSortName,
1653 artist.strMusicBrainzArtistID, artist.bScrapedMBID,
1654 artist.strType, artist.strGender, artist.strDisambiguation,
1655 artist.strBorn, artist.strFormed,
1656 StringUtils::Join(artist.genre, itemSeparator),
1657 StringUtils::Join(artist.moods, itemSeparator),
1658 StringUtils::Join(artist.styles, itemSeparator),
1659 StringUtils::Join(artist.instruments, itemSeparator),
1660 artist.strBiography, artist.strDied,
1661 artist.strDisbanded,
1662 StringUtils::Join(artist.yearsActive, itemSeparator).c_str(),
1663 artist.thumbURL.GetData());
1664
1665 DeleteArtistDiscography(artist.idArtist);
1666 for (const auto &disc : artist.discography)
1667 {
1668 AddArtistDiscography(artist.idArtist, disc);
1669 }
1670
1671 // Set current artwork (held in art table)
1672 if (!artist.art.empty())
1673 SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
1674
1675 return true;
1676 }
1677
AddArtist(const std::string & strArtist,const std::string & strMusicBrainzArtistID,const std::string & strSortName,bool bScrapedMBID)1678 int CMusicDatabase::AddArtist(const std::string& strArtist, const std::string& strMusicBrainzArtistID, const std::string& strSortName, bool bScrapedMBID /* = false*/)
1679 {
1680 std::string strSQL;
1681 int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID);
1682 if (idArtist < 0 || strSortName.empty())
1683 return idArtist;
1684
1685 /* Artist sort name always taken as the first value provided that is different from name, so only
1686 update when current sort name is blank. If a new sortname the same as name is provided then
1687 clear any sortname currently held.
1688 */
1689
1690 try
1691 {
1692 if (nullptr == m_pDB)
1693 return -1;
1694 if (nullptr == m_pDS)
1695 return -1;
1696
1697 strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist);
1698 m_pDS->query(strSQL);
1699 if (m_pDS->num_rows() != 1)
1700 {
1701 m_pDS->close();
1702 return -1;
1703 }
1704 std::string strArtistName, strArtistSort;
1705 strArtistName = m_pDS->fv("strArtist").get_asString();
1706 strArtistSort = m_pDS->fv("strSortName").get_asString();
1707 m_pDS->close();
1708
1709 if (!strArtistSort.empty())
1710 {
1711 if (strSortName.compare(strArtistName) == 0)
1712 m_pDS->exec(PrepareSQL(
1713 "UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist));
1714 }
1715 else if (strSortName.compare(strArtistName) != 0)
1716 m_pDS->exec(PrepareSQL(
1717 "UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
1718 strSortName.c_str(), idArtist));
1719
1720 return idArtist;
1721 }
1722
1723 catch (...)
1724 {
1725 CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname (%s)", strSQL.c_str());
1726 }
1727
1728 return -1;
1729 }
1730
AddArtist(const std::string & strArtist,const std::string & strMusicBrainzArtistID,bool bScrapedMBID)1731 int CMusicDatabase::AddArtist(const std::string& strArtist, const std::string& strMusicBrainzArtistID, bool bScrapedMBID /* = false*/)
1732 {
1733 std::string strSQL;
1734 try
1735 {
1736 if (nullptr == m_pDB)
1737 return -1;
1738 if (nullptr == m_pDS)
1739 return -1;
1740
1741 // 1) MusicBrainz
1742 if (!strMusicBrainzArtistID.empty())
1743 {
1744 // 1.a) Match on a MusicBrainz ID
1745 strSQL = PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
1746 strMusicBrainzArtistID.c_str());
1747 m_pDS->query(strSQL);
1748 if (m_pDS->num_rows() > 0)
1749 {
1750 int idArtist = m_pDS->fv("idArtist").get_asInt();
1751 bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0;
1752 m_pDS->close();
1753 if (update)
1754 {
1755 strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' "
1756 "WHERE idArtist = %i", strArtist.c_str(), idArtist);
1757 m_pDS->exec(strSQL);
1758 m_pDS->close();
1759 }
1760 return idArtist;
1761 }
1762 m_pDS->close();
1763
1764
1765 // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
1766 // and update that if it exists.
1767 strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL", strArtist.c_str());
1768 m_pDS->query(strSQL);
1769 if (m_pDS->num_rows() > 0)
1770 {
1771 int idArtist = m_pDS->fv("idArtist").get_asInt();
1772 m_pDS->close();
1773 // 1.b.a) We found an artist by name but with no MusicBrainz ID set, update it and assume it is our artist, flag when mbid scraped
1774 strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
1775 "bScrapedMBID = %i WHERE idArtist = %i",
1776 strArtist.c_str(),
1777 strMusicBrainzArtistID.c_str(),
1778 bScrapedMBID,
1779 idArtist);
1780 m_pDS->exec(strSQL);
1781 return idArtist;
1782 }
1783
1784 // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
1785 // With MusicBrainz IDs this could return multiple artists and is non-determinstic
1786 // Always pick the first artist ID returned by the DB to return.
1787 }
1788 else
1789 {
1790 strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'",
1791 strArtist.c_str());
1792
1793 m_pDS->query(strSQL);
1794 if (m_pDS->num_rows() > 0)
1795 {
1796 int idArtist = m_pDS->fv("idArtist").get_asInt();
1797 m_pDS->close();
1798 return idArtist;
1799 }
1800 m_pDS->close();
1801 }
1802
1803 // 3) No artist exists at all - add it, flagging when has scraped mbid
1804 if (strMusicBrainzArtistID.empty())
1805 strSQL = PrepareSQL("INSERT INTO artist "
1806 "(idArtist, strArtist, strMusicBrainzArtistID) "
1807 "VALUES( NULL, '%s', NULL)",
1808 strArtist.c_str());
1809 else
1810 strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
1811 "bScrapedMBID) "
1812 "VALUES( NULL, '%s', '%s', %i )",
1813 strArtist.c_str(),
1814 strMusicBrainzArtistID.c_str(), bScrapedMBID);
1815
1816 m_pDS->exec(strSQL);
1817 int idArtist = (int)m_pDS->lastinsertid();
1818 return idArtist;
1819 }
1820 catch (...)
1821 {
1822 CLog::Log(LOGERROR, "musicdatabase:unable to addartist (%s)", strSQL.c_str());
1823 }
1824
1825 return -1;
1826 }
1827
UpdateArtist(int idArtist,const std::string & strArtist,const std::string & strSortName,const std::string & strMusicBrainzArtistID,const bool bScrapedMBID,const std::string & strType,const std::string & strGender,const std::string & strDisambiguation,const std::string & strBorn,const std::string & strFormed,const std::string & strGenres,const std::string & strMoods,const std::string & strStyles,const std::string & strInstruments,const std::string & strBiography,const std::string & strDied,const std::string & strDisbanded,const std::string & strYearsActive,const std::string & strImage)1828 int CMusicDatabase::UpdateArtist(int idArtist,
1829 const std::string& strArtist, const std::string& strSortName,
1830 const std::string& strMusicBrainzArtistID, const bool bScrapedMBID,
1831 const std::string& strType, const std::string& strGender,
1832 const std::string& strDisambiguation,
1833 const std::string& strBorn, const std::string& strFormed,
1834 const std::string& strGenres, const std::string& strMoods,
1835 const std::string& strStyles, const std::string& strInstruments,
1836 const std::string& strBiography, const std::string& strDied,
1837 const std::string& strDisbanded, const std::string& strYearsActive,
1838 const std::string& strImage)
1839 {
1840 if (idArtist < 0)
1841 return -1;
1842
1843 // Check another artist with this mbid not already exist (an alias for example)
1844 bool useMBIDNull = strMusicBrainzArtistID.empty();
1845 bool isScrapedMBID = bScrapedMBID;
1846 std::string artistname;
1847 int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
1848 if (idArtistMbid > 0 && idArtistMbid != idArtist)
1849 {
1850 CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
1851 __FUNCTION__, strMusicBrainzArtistID.c_str(), artistname.c_str(), idArtistMbid,
1852 strArtist.c_str(), idArtist);
1853 useMBIDNull = true;
1854 isScrapedMBID = false;
1855 }
1856
1857 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
1858 // Truncate value cleaning up xml when URLs exceeds this
1859 std::string strImageURLs = strImage;
1860 if (StringUtils::EqualsNoCase(
1861 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
1862 "mysql"))
1863 TrimImageURLs(strImageURLs, 65535);
1864
1865 std::string strSQL;
1866 strSQL = PrepareSQL("UPDATE artist SET "
1867 " strArtist = '%s', "
1868 " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
1869 " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
1870 " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
1871 " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
1872 " strYearsActive = '%s', strImage = '%s', "
1873 " lastScraped = '%s', bScrapedMBID = %i",
1874 strArtist.c_str(),
1875 /* strSortName.c_str(),*/
1876 /* strMusicBrainzArtistID.c_str(), */
1877 strType.c_str(), strGender.c_str(), strDisambiguation.c_str(),
1878 strBorn.c_str(), strFormed.c_str(), strGenres.c_str(),
1879 strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(),
1880 strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(),
1881 strYearsActive.c_str(), strImageURLs.c_str(),
1882 CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID);
1883 if (useMBIDNull)
1884 strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL");
1885 else
1886 strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str());
1887 if (strSortName.empty())
1888 strSQL += PrepareSQL(", strSortName = NULL");
1889 else
1890 strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str());
1891
1892 strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist);
1893
1894 bool status = ExecuteQuery(strSQL);
1895 if (status)
1896 AnnounceUpdate(MediaTypeArtist, idArtist);
1897 return idArtist;
1898 }
1899
UpdateArtistScrapedMBID(int idArtist,const std::string & strMusicBrainzArtistID)1900 bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist, const std::string& strMusicBrainzArtistID)
1901 {
1902 if (strMusicBrainzArtistID.empty() || idArtist < 0)
1903 return false;
1904
1905 // Check another artist with this mbid not already exist (an alias for example)
1906 std::string artistname;
1907 int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
1908 if (idArtistMbid > 0 && idArtistMbid != idArtist)
1909 {
1910 CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__,
1911 strMusicBrainzArtistID.c_str(), artistname.c_str(), idArtistMbid);
1912 return false;
1913 }
1914
1915 // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
1916 std::string strSQL;
1917 strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
1918 "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
1919 strMusicBrainzArtistID.c_str(), idArtist);
1920
1921 bool status = ExecuteQuery(strSQL);
1922 if (status)
1923 {
1924 AnnounceUpdate(MediaTypeArtist, idArtist);
1925 return true;
1926 }
1927 return false;
1928 }
1929
GetArtist(int idArtist,CArtist & artist,bool fetchAll)1930 bool CMusicDatabase::GetArtist(int idArtist, CArtist &artist, bool fetchAll /* = false */)
1931 {
1932 try
1933 {
1934 unsigned int time = XbmcThreads::SystemClockMillis();
1935 if (nullptr == m_pDB)
1936 return false;
1937 if (nullptr == m_pDS)
1938 return false;
1939
1940 if (idArtist == -1)
1941 return false; // not in the database
1942
1943 std::string strSQL;
1944 if (fetchAll)
1945 strSQL = PrepareSQL("SELECT * FROM artistview LEFT JOIN discography ON artistview.idArtist = discography.idArtist WHERE artistview.idArtist = %i", idArtist);
1946 else
1947 strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist);
1948
1949 if (!m_pDS->query(strSQL)) return false;
1950 if (m_pDS->num_rows() == 0)
1951 {
1952 m_pDS->close();
1953 return false;
1954 }
1955
1956 int discographyOffset = artist_enumCount;
1957
1958 artist.discography.clear();
1959 artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs
1960 if (fetchAll)
1961 {
1962 while (!m_pDS->eof())
1963 {
1964 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
1965 CDiscoAlbum discoAlbum;
1966 discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString();
1967 discoAlbum.strYear = record->at(discographyOffset + 2).get_asString();
1968 discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString();
1969 m_pDS->next();
1970 }
1971 }
1972 m_pDS->close(); // cleanup recordset data
1973 CLog::Log(LOGDEBUG, LOGDATABASE, "{0}({1}) - took {2} ms", __FUNCTION__, strSQL, XbmcThreads::SystemClockMillis() - time);
1974 return true;
1975 }
1976 catch (...)
1977 {
1978 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
1979 }
1980
1981 return false;
1982 }
1983
GetArtistExists(int idArtist)1984 bool CMusicDatabase::GetArtistExists(int idArtist)
1985 {
1986 try
1987 {
1988 if (nullptr == m_pDB)
1989 return false;
1990 if (nullptr == m_pDS)
1991 return false;
1992
1993 std::string strSQL = PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist);
1994
1995 if (!m_pDS->query(strSQL)) return false;
1996 if (m_pDS->num_rows() == 0)
1997 {
1998 m_pDS->close();
1999 return false;
2000 }
2001 m_pDS->close(); // cleanup recordset data
2002 return true;
2003 }
2004 catch (...)
2005 {
2006 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2007 }
2008
2009 return false;
2010 }
2011
GetLastArtist()2012 int CMusicDatabase::GetLastArtist()
2013 {
2014 std::string strSQL = "SELECT MAX(idArtist) FROM artist";
2015 std::string lastArtist = GetSingleValue(strSQL);
2016 if (lastArtist.empty())
2017 return -1;
2018
2019 return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10));
2020 }
2021
GetArtistFromMBID(const std::string & strMusicBrainzArtistID,std::string & artistname)2022 int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID,
2023 std::string& artistname)
2024 {
2025 if (strMusicBrainzArtistID.empty())
2026 return -1;
2027
2028 std::string strSQL;
2029 try
2030 {
2031 if (nullptr == m_pDB || nullptr == m_pDS2)
2032 return -1;
2033 // Match on MusicBrainz ID, definitively unique
2034 strSQL =
2035 PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
2036 strMusicBrainzArtistID.c_str());
2037 if (!m_pDS2->query(strSQL))
2038 return -1;
2039 int idArtist = -1;
2040 if (m_pDS2->num_rows() > 0)
2041 {
2042 idArtist = m_pDS2->fv("idArtist").get_asInt();
2043 artistname = m_pDS2->fv("strArtist").get_asString();
2044 }
2045 m_pDS2->close();
2046 return idArtist;
2047 }
2048 catch (...)
2049 {
2050 CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__,
2051 strSQL.c_str());
2052 }
2053 return -1;
2054 }
2055
HasArtistBeenScraped(int idArtist)2056 bool CMusicDatabase::HasArtistBeenScraped(int idArtist)
2057 {
2058 std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist);
2059 return GetSingleValue(strSQL).empty();
2060 }
2061
ClearArtistLastScrapedTime(int idArtist)2062 bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist)
2063 {
2064 std::string strSQL = PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist);
2065 return ExecuteQuery(strSQL);
2066 }
2067
AddArtistDiscography(int idArtist,const CDiscoAlbum & discoAlbum)2068 int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum)
2069 {
2070 std::string strSQL = PrepareSQL("INSERT INTO discography "
2071 "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
2072 "VALUES(%i, '%s', '%s', '%s')",
2073 idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(),
2074 discoAlbum.strReleaseGroupMBID.c_str());
2075 return ExecuteQuery(strSQL);
2076 }
2077
DeleteArtistDiscography(int idArtist)2078 bool CMusicDatabase::DeleteArtistDiscography(int idArtist)
2079 {
2080 std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist);
2081 return ExecuteQuery(strSQL);
2082 }
2083
GetArtistDiscography(int idArtist,CFileItemList & items)2084 bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items)
2085 {
2086 try
2087 {
2088 if (nullptr == m_pDB)
2089 return false;
2090 if (nullptr == m_pDS)
2091 return false;
2092
2093 /* Combine entries from discography and album tables
2094 Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using
2095 correlated subqueries to a temp table. An updatable join to temp table would work in MySQL
2096 but SQLite not support updatable joins.
2097 */
2098 m_pDS->exec("CREATE TABLE tempDisco "
2099 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2100 m_pDS->exec("CREATE TABLE tempAlbum "
2101 "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
2102
2103 std::string strSQL;
2104 strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
2105 "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
2106 "strReleaseGroupMBID, NULL "
2107 "FROM discography WHERE idArtist = %i",
2108 idArtist);
2109 m_pDS->exec(strSQL);
2110
2111 strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
2112 "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
2113 "strReleaseGroupMBID, album.idAlbum "
2114 "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
2115 "WHERE idArtist = %i",
2116 idArtist);
2117 m_pDS->exec(strSQL);
2118
2119 // Match albums on release group mbid, if multi-releases then first used
2120 // Only use albums credited to this artist
2121 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
2122 "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
2123 m_pDS->exec(strSQL);
2124 //Delete matched albums
2125 strSQL = "DELETE FROM tempAlbum "
2126 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2127 m_pDS->exec(strSQL);
2128
2129 // Match remaining to albums by artist on title and year
2130 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2131 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
2132 "AND tempAlbum.strYear = tempDisco.strYear) "
2133 "WHERE tempDisco.idAlbum is NULL";
2134 m_pDS->exec(strSQL);
2135 //Delete matched albums
2136 strSQL = "DELETE FROM tempAlbum "
2137 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2138 m_pDS->exec(strSQL);
2139
2140 // Match remaining to albums by artist on title only
2141 strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
2142 "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
2143 "WHERE tempDisco.idAlbum is NULL";
2144 m_pDS->exec(strSQL);
2145 // Use year from album table, when matched by title only as it could be different
2146 strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
2147 "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
2148 "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
2149 m_pDS->exec(strSQL);
2150 //Delete matched albums
2151 strSQL = "DELETE FROM tempAlbum "
2152 "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
2153 m_pDS->exec(strSQL);
2154
2155 // Combine distinctly with any remaining unmatched albums by artist
2156 strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
2157 "UNION "
2158 "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
2159 "ORDER BY strYear, strAlbum, idAlbum";
2160
2161 if (!m_pDS->query(strSQL))
2162 return false;
2163 int iRowsFound = m_pDS->num_rows();
2164 if (iRowsFound == 0)
2165 {
2166 m_pDS->close();
2167 return true;
2168 }
2169
2170 while (!m_pDS->eof())
2171 {
2172 int idAlbum = m_pDS->fv("idAlbum").get_asInt();
2173 if (idAlbum == 0)
2174 idAlbum = -1;
2175 std::string strAlbum = m_pDS->fv("strAlbum").get_asString();
2176 if (!strAlbum.empty())
2177 {
2178 CFileItemPtr pItem(new CFileItem(strAlbum));
2179 pItem->SetLabel2(m_pDS->fv("strYear").get_asString());
2180 pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum);
2181 items.Add(pItem);
2182 }
2183 m_pDS->next();
2184 }
2185
2186 // cleanup
2187 m_pDS->close();
2188 m_pDS->exec("DROP TABLE tempDisco");
2189 m_pDS->exec("DROP TABLE tempAlbum");
2190
2191 return true;
2192 }
2193 catch (...)
2194 {
2195 m_pDS->exec("DROP TABLE tempDisco");
2196 m_pDS->exec("DROP TABLE tempAlbum");
2197 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
2198 }
2199 return false;
2200 }
2201
AddRole(const std::string & strRole)2202 int CMusicDatabase::AddRole(const std::string &strRole)
2203 {
2204 int idRole = -1;
2205 std::string strSQL;
2206
2207 try
2208 {
2209 if (nullptr == m_pDB)
2210 return -1;
2211 if (nullptr == m_pDS)
2212 return -1;
2213 strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str());
2214 m_pDS->query(strSQL);
2215 if (m_pDS->num_rows() > 0)
2216 idRole = m_pDS->fv("idRole").get_asInt();
2217 m_pDS->close();
2218
2219 if (idRole < 0)
2220 {
2221 strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str());
2222 m_pDS->exec(strSQL);
2223 idRole = static_cast<int>(m_pDS->lastinsertid());
2224 m_pDS->close();
2225 }
2226 }
2227 catch (...)
2228 {
2229 CLog::Log(LOGERROR, "musicdatabase:unable to AddRole (%s)", strSQL.c_str());
2230 }
2231 return idRole;
2232 }
2233
AddSongArtist(int idArtist,int idSong,const std::string & strRole,const std::string & strArtist,int iOrder)2234 bool CMusicDatabase::AddSongArtist(int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder)
2235 {
2236 int idRole = AddRole(strRole);
2237 return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder);
2238 }
2239
AddSongArtist(int idArtist,int idSong,int idRole,const std::string & strArtist,int iOrder)2240 bool CMusicDatabase::AddSongArtist(int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder)
2241 {
2242 std::string strSQL;
2243 strSQL = PrepareSQL("replace into song_artist (idArtist, idSong, idRole, strArtist, iOrder) values(%i,%i,%i,'%s',%i)",
2244 idArtist, idSong, idRole, strArtist.c_str(), iOrder);
2245 return ExecuteQuery(strSQL);
2246 }
2247
AddSongContributor(int idSong,const std::string & strRole,const std::string & strArtist,const std::string & strSort)2248 int CMusicDatabase::AddSongContributor(int idSong, const std::string& strRole, const std::string& strArtist, const std::string &strSort)
2249 {
2250 if (strArtist.empty())
2251 return -1;
2252
2253 std::string strSQL;
2254 try
2255 {
2256 if (nullptr == m_pDB)
2257 return -1;
2258 if (nullptr == m_pDS)
2259 return -1;
2260
2261 int idArtist = -1;
2262 // Add artist. As we only have name (no MBID) first try to identify artist from song
2263 // as they may have already been added with a different role (including MBID).
2264 strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ", idSong, strArtist.c_str());
2265 m_pDS->query(strSQL);
2266 if (m_pDS->num_rows() > 0)
2267 idArtist = m_pDS->fv("idArtist").get_asInt();
2268 m_pDS->close();
2269
2270 if (idArtist < 0)
2271 idArtist = AddArtist(strArtist, "", strSort);
2272
2273 // Add to song_artist table
2274 AddSongArtist(idArtist, idSong, strRole, strArtist, 0);
2275
2276 return idArtist;
2277 }
2278 catch (...)
2279 {
2280 CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor (%s)", strSQL.c_str());
2281 }
2282
2283 return -1;
2284 }
2285
AddSongContributors(int idSong,const VECMUSICROLES & contributors,const std::string & strSort)2286 void CMusicDatabase::AddSongContributors(int idSong, const VECMUSICROLES& contributors, const std::string &strSort)
2287 {
2288 std::vector<std::string> composerSort;
2289 size_t countComposer = 0;
2290 if (!strSort.empty())
2291 {
2292 composerSort = StringUtils::Split(strSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2293 }
2294
2295 for (const auto &credit : contributors)
2296 {
2297 std::string strSortName;
2298 //Identify composer sort name if we have it
2299 if (countComposer < composerSort.size())
2300 {
2301 if (credit.GetRoleDesc().compare("Composer") == 0)
2302 {
2303 strSortName = composerSort[countComposer];
2304 countComposer++;
2305 }
2306 }
2307 AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName);
2308 }
2309 }
2310
GetRoleByName(const std::string & strRole)2311 int CMusicDatabase::GetRoleByName(const std::string& strRole)
2312 {
2313 try
2314 {
2315 if (nullptr == m_pDB)
2316 return false;
2317 if (nullptr == m_pDS)
2318 return false;
2319
2320 std::string strSQL;
2321 strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str());
2322 // run query
2323 if (!m_pDS->query(strSQL)) return false;
2324 int iRowsFound = m_pDS->num_rows();
2325 if (iRowsFound != 1)
2326 {
2327 m_pDS->close();
2328 return -1;
2329 }
2330 return m_pDS->fv("idRole").get_asInt();
2331 }
2332 catch (...)
2333 {
2334 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
2335 }
2336 return -1;
2337
2338 }
2339
GetRolesByArtist(int idArtist,CFileItem * item)2340 bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item)
2341 {
2342 try
2343 {
2344 std::string strSQL = PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole FROM song_artist JOIN role ON "
2345 " song_artist.idRole = role.idRole WHERE idArtist = %i ORDER BY song_artist.idRole ASC", idArtist);
2346 if (!m_pDS->query(strSQL))
2347 return false;
2348 if (m_pDS->num_rows() == 0)
2349 {
2350 m_pDS->close();
2351 return true;
2352 }
2353
2354 CVariant artistRoles(CVariant::VariantTypeArray);
2355
2356 while (!m_pDS->eof())
2357 {
2358 CVariant roleObj;
2359 roleObj["role"] = m_pDS->fv("strRole").get_asString();
2360 roleObj["roleid"] = m_pDS->fv("idrole").get_asInt();
2361 artistRoles.push_back(roleObj);
2362 m_pDS->next();
2363 }
2364 m_pDS->close();
2365
2366 item->SetProperty("roles", artistRoles);
2367 return true;
2368 }
2369 catch (...)
2370 {
2371 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2372 }
2373 return false;
2374 }
2375
DeleteSongArtistsBySong(int idSong)2376 bool CMusicDatabase::DeleteSongArtistsBySong(int idSong)
2377 {
2378 return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong));
2379 }
2380
AddAlbumArtist(int idArtist,int idAlbum,const std::string & strArtist,int iOrder)2381 bool CMusicDatabase::AddAlbumArtist(int idArtist,
2382 int idAlbum,
2383 const std::string& strArtist,
2384 int iOrder)
2385 {
2386 std::string strSQL;
2387 strSQL = PrepareSQL("replace into album_artist (idArtist, idAlbum, strArtist, iOrder) values(%i,%i,'%s',%i)",
2388 idArtist, idAlbum, strArtist.c_str(), iOrder);
2389 return ExecuteQuery(strSQL);
2390 }
2391
DeleteAlbumArtistsByAlbum(int idAlbum)2392 bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum)
2393 {
2394 return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum));
2395 }
2396
AddSongGenres(int idSong,const std::vector<std::string> & genres)2397 bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres)
2398 {
2399 if (idSong == -1)
2400 return true;
2401
2402 std::string strSQL;
2403 try
2404 {
2405 // Clear current entries for song
2406 strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong);
2407 if (!ExecuteQuery(strSQL))
2408 return false;
2409 unsigned int index = 0;
2410 std::vector<std::string> modgenres = genres;
2411 for (auto &strGenre : modgenres)
2412 {
2413 int idGenre = AddGenre(strGenre); // Genre string trimed and matched case insensitively
2414 strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
2415 idGenre, idSong, index++);
2416 if (!ExecuteQuery(strSQL))
2417 return false;
2418 }
2419 // Update concatenated genre string from the standardised genre values
2420 std::string strGenres = StringUtils::Join(modgenres, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2421 strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", strGenres.c_str(), idSong);
2422 if (!ExecuteQuery(strSQL))
2423 return false;
2424
2425 return true;
2426 }
2427 catch (...)
2428 {
2429 CLog::Log(LOGERROR, "%s(%i) %s failed", __FUNCTION__, idSong, strSQL.c_str());
2430 }
2431 return false;
2432 }
2433
GetAlbumsByArtist(int idArtist,std::vector<int> & albums)2434 bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int> &albums)
2435 {
2436 try
2437 {
2438 std::string strSQL;
2439 strSQL = PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist);
2440 if (!m_pDS->query(strSQL))
2441 return false;
2442 if (m_pDS->num_rows() == 0)
2443 {
2444 m_pDS->close();
2445 return false;
2446 }
2447
2448 while (!m_pDS->eof())
2449 {
2450 albums.push_back(m_pDS->fv("idAlbum").get_asInt());
2451 m_pDS->next();
2452 }
2453 m_pDS->close();
2454 return true;
2455 }
2456 catch (...)
2457 {
2458 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2459 }
2460 return false;
2461 }
2462
GetArtistsByAlbum(int idAlbum,CFileItem * item)2463 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item)
2464 {
2465 try
2466 {
2467 std::string strSQL;
2468
2469 strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum);
2470
2471 if (!m_pDS->query(strSQL))
2472 return false;
2473 if (m_pDS->num_rows() == 0)
2474 {
2475 m_pDS->close();
2476 return false;
2477 }
2478
2479 // Get album artist credits
2480 VECARTISTCREDITS artistCredits;
2481 while (!m_pDS->eof())
2482 {
2483 artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0));
2484 m_pDS->next();
2485 }
2486 m_pDS->close();
2487
2488 // Populate item with song albumartist credits
2489 std::vector<std::string> musicBrainzID;
2490 std::vector<std::string> albumartists;
2491 CVariant artistidObj(CVariant::VariantTypeArray);
2492 for (const auto &artistCredit : artistCredits)
2493 {
2494 artistidObj.push_back(artistCredit.GetArtistId());
2495 albumartists.emplace_back(artistCredit.GetArtist());
2496 if (!artistCredit.GetMusicBrainzArtistID().empty())
2497 musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID());
2498 }
2499 item->GetMusicInfoTag()->SetAlbumArtist(albumartists);
2500 item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID);
2501 // Add song albumartistIds as separate property as not part of CMusicInfoTag
2502 item->SetProperty("albumartistid", artistidObj);
2503
2504 return true;
2505 }
2506 catch (...)
2507 {
2508 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
2509 }
2510 return false;
2511 }
2512
GetArtistsByAlbum(int idAlbum,std::vector<std::string> & artistIDs)2513 bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs)
2514 {
2515 try
2516 {
2517 std::string strSQL;
2518 // Get distinct song and album artist IDs for this album, no other roles
2519 // Allow for artists that are only album artists and not song artists
2520 strSQL = PrepareSQL("SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
2521 "UNION \n"
2522 "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
2523 "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
2524 idAlbum, idAlbum);
2525
2526 if (!m_pDS->query(strSQL))
2527 return false;
2528 if (m_pDS->num_rows() == 0)
2529 {
2530 m_pDS->close();
2531 return false;
2532 }
2533 while (!m_pDS->eof())
2534 {
2535 // Get ID as string so can easily join to make "IN" clause
2536 artistIDs.push_back(m_pDS->fv("idArtist").get_asString());
2537 m_pDS->next();
2538 }
2539 m_pDS->close();
2540 return true;
2541 }
2542 catch (...)
2543 {
2544 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
2545 }
2546 return false;
2547 };
2548
GetSongsByArtist(int idArtist,std::vector<int> & songs)2549 bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int> &songs)
2550 {
2551 try
2552 {
2553 std::string strSQL;
2554 //Restrict to Artists only, no other roles
2555 strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", idArtist);
2556 if (!m_pDS->query(strSQL))
2557 return false;
2558 if (m_pDS->num_rows() == 0)
2559 {
2560 m_pDS->close();
2561 return false;
2562 }
2563
2564 while (!m_pDS->eof())
2565 {
2566 songs.push_back(m_pDS->fv("idSong").get_asInt());
2567 m_pDS->next();
2568 }
2569 m_pDS->close();
2570 return true;
2571 }
2572 catch (...)
2573 {
2574 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2575 }
2576 return false;
2577 };
2578
GetArtistsBySong(int idSong,std::vector<int> & artists)2579 bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int> &artists)
2580 {
2581 try
2582 {
2583 std::string strSQL;
2584 //Restrict to Artists only, no other roles
2585 strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", idSong);
2586 if (!m_pDS->query(strSQL))
2587 return false;
2588 if (m_pDS->num_rows() == 0)
2589 {
2590 m_pDS->close();
2591 return false;
2592 }
2593
2594 while (!m_pDS->eof())
2595 {
2596 artists.push_back(m_pDS->fv("idArtist").get_asInt());
2597 m_pDS->next();
2598 }
2599 m_pDS->close();
2600 return true;
2601 }
2602 catch (...)
2603 {
2604 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
2605 }
2606 return false;
2607 }
2608
GetGenresByArtist(int idArtist,CFileItem * item)2609 bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item)
2610 {
2611 try
2612 {
2613 std::string strSQL;
2614 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2615 "album_artist JOIN song ON album_artist.idAlbum = song.idAlbum "
2616 "JOIN song_genre ON song.idSong = song_genre.idSong "
2617 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2618 "WHERE album_artist.idArtist = %i "
2619 "ORDER BY song_genre.idGenre", idArtist);
2620 if (!m_pDS->query(strSQL))
2621 return false;
2622 if (m_pDS->num_rows() == 0)
2623 {
2624 // Artist does have any song genres via albums may not be an album artist.
2625 // Check via songs artist to fetch song genres from compilations or where they are guest artist
2626 m_pDS->close();
2627 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2628 "song_artist JOIN song_genre ON song_artist.idSong = song_genre.idSong "
2629 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2630 "WHERE song_artist.idArtist = %i "
2631 "ORDER BY song_genre.idGenre", idArtist);
2632 if (!m_pDS->query(strSQL))
2633 return false;
2634 if (m_pDS->num_rows() == 0)
2635 {
2636 //No song genres, but query sucessfull
2637 m_pDS->close();
2638 return true;
2639 }
2640 }
2641
2642 CVariant artistSongGenres(CVariant::VariantTypeArray);
2643
2644 while (!m_pDS->eof())
2645 {
2646 CVariant genreObj;
2647 genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2648 genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2649 artistSongGenres.push_back(genreObj);
2650 m_pDS->next();
2651 }
2652 m_pDS->close();
2653
2654 item->SetProperty("songgenres", artistSongGenres);
2655 return true;
2656 }
2657 catch (...)
2658 {
2659 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2660 }
2661 return false;
2662 }
2663
GetGenresByAlbum(int idAlbum,CFileItem * item)2664 bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item)
2665 {
2666 try
2667 {
2668 std::string strSQL;
2669 strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
2670 "song JOIN song_genre ON song.idSong = song_genre.idSong "
2671 "JOIN genre ON song_genre.idGenre = genre.idGenre "
2672 "WHERE song.idAlbum = %i "
2673 "ORDER BY song_genre.idSong, song_genre.iOrder", idAlbum);
2674 if (!m_pDS->query(strSQL))
2675 return false;
2676 if (m_pDS->num_rows() == 0)
2677 {
2678 //No song genres, but query sucessfull
2679 m_pDS->close();
2680 return true;
2681 }
2682
2683 CVariant albumSongGenres(CVariant::VariantTypeArray);
2684
2685 while (!m_pDS->eof())
2686 {
2687 CVariant genreObj;
2688 genreObj["title"] = m_pDS->fv("strGenre").get_asString();
2689 genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
2690 albumSongGenres.push_back(genreObj);
2691 m_pDS->next();
2692 }
2693 m_pDS->close();
2694
2695 item->SetProperty("songgenres", albumSongGenres);
2696 return true;
2697 }
2698 catch (...)
2699 {
2700 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
2701 }
2702 return false;
2703 }
2704
GetGenresBySong(int idSong,std::vector<int> & genres)2705 bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres)
2706 {
2707 try
2708 {
2709 std::string strSQL = PrepareSQL("select idGenre from song_genre where idSong = %i ORDER BY iOrder ASC", idSong);
2710 if (!m_pDS->query(strSQL))
2711 return false;
2712 if (m_pDS->num_rows() == 0)
2713 {
2714 m_pDS->close();
2715 return true;
2716 }
2717
2718 while (!m_pDS->eof())
2719 {
2720 genres.push_back(m_pDS->fv("idGenre").get_asInt());
2721 m_pDS->next();
2722 }
2723 m_pDS->close();
2724
2725 return true;
2726 }
2727 catch (...)
2728 {
2729 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
2730 }
2731 return false;
2732 }
2733
GetIsAlbumArtist(int idArtist,CFileItem * item)2734 bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item)
2735 {
2736 try
2737 {
2738 int countalbum = GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist));
2739 CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean);
2740 IsAlbumArtistObj = (countalbum > 0);
2741 item->SetProperty("isalbumartist", IsAlbumArtistObj);
2742 return true;
2743 }
2744 catch (...)
2745 {
2746 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
2747 }
2748 return false;
2749 }
2750
2751
AddPath(const std::string & strPath1)2752 int CMusicDatabase::AddPath(const std::string& strPath1)
2753 {
2754 std::string strSQL;
2755 try
2756 {
2757 std::string strPath(strPath1);
2758 if (!URIUtils::HasSlashAtEnd(strPath))
2759 URIUtils::AddSlashAtEnd(strPath);
2760
2761 if (nullptr == m_pDB)
2762 return -1;
2763 if (nullptr == m_pDS)
2764 return -1;
2765
2766 auto it = m_pathCache.find(strPath);
2767 if (it != m_pathCache.end())
2768 return it->second;
2769
2770 strSQL=PrepareSQL( "select * from path where strPath='%s'", strPath.c_str());
2771 m_pDS->query(strSQL);
2772 if (m_pDS->num_rows() == 0)
2773 {
2774 m_pDS->close();
2775 // doesn't exists, add it
2776 strSQL=PrepareSQL("insert into path (idPath, strPath) values( NULL, '%s' )", strPath.c_str());
2777 m_pDS->exec(strSQL);
2778
2779 int idPath = (int)m_pDS->lastinsertid();
2780 m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
2781 return idPath;
2782 }
2783 else
2784 {
2785 int idPath = m_pDS->fv("idPath").get_asInt();
2786 m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
2787 m_pDS->close();
2788 return idPath;
2789 }
2790 }
2791 catch (...)
2792 {
2793 CLog::Log(LOGERROR, "musicdatabase:unable to addpath (%s)", strSQL.c_str());
2794 }
2795
2796 return -1;
2797 }
2798
GetSongFromDataset()2799 CSong CMusicDatabase::GetSongFromDataset()
2800 {
2801 return GetSongFromDataset(m_pDS->get_sql_record());
2802 }
2803
GetSongFromDataset(const dbiplus::sql_record * const record,int offset)2804 CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */)
2805 {
2806 CSong song;
2807 song.idSong = record->at(offset + song_idSong).get_asInt();
2808 // Note this function does not populate artist credits, this must be done separately.
2809 // However artist names are held as a descriptive string
2810 song.strArtistDesc = record->at(offset + song_strArtists).get_asString();
2811 song.strArtistSort = record->at(offset + song_strArtistSort).get_asString();
2812 // Get the full genre string
2813 song.genre = StringUtils::Split(record->at(offset + song_strGenres).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
2814 // and the rest...
2815 song.strAlbum = record->at(offset + song_strAlbum).get_asString();
2816 song.idAlbum = record->at(offset + song_idAlbum).get_asInt();
2817 song.iTrack = record->at(offset + song_iTrack).get_asInt() ;
2818 song.iDuration = record->at(offset + song_iDuration).get_asInt() ;
2819 song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString();
2820 song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString();
2821 song.strTitle = record->at(offset + song_strTitle).get_asString();
2822 song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt();
2823 song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString());
2824 song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString());
2825 song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString());
2826 song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString());
2827 song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt();
2828 song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt();
2829 song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString();
2830 song.rating = record->at(offset + song_rating).get_asFloat();
2831 song.userrating = record->at(offset + song_userrating).get_asInt();
2832 song.votes = record->at(offset + song_votes).get_asInt();
2833 song.strComment = record->at(offset + song_comment).get_asString();
2834 song.strMood = record->at(offset + song_mood).get_asString();
2835 song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1;
2836 song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString();
2837 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
2838 song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString());
2839 // Get filename with full path
2840 song.strFileName = URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(), record->at(offset + song_strFileName).get_asString());
2841 song.iBPM = record->at(offset + song_iBPM).get_asInt();
2842 song.iBitRate = record->at(offset + song_iBitRate).get_asInt();
2843 song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt();
2844 song.iChannels = record->at(offset + song_iChannels).get_asInt();
2845 return song;
2846 }
2847
GetFileItemFromDataset(CFileItem * item,const CMusicDbUrl & baseUrl)2848 void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl &baseUrl)
2849 {
2850 GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl);
2851 }
2852
GetFileItemFromDataset(const dbiplus::sql_record * const record,CFileItem * item,const CMusicDbUrl & baseUrl)2853 void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record, CFileItem* item, const CMusicDbUrl &baseUrl)
2854 {
2855 // get the artist string from songview (not the song_artist and artist tables)
2856 item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString());
2857 // get the artist sort name string from songview (not the song_artist and artist tables)
2858 item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString());
2859 // and the full genre string
2860 item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString());
2861 // and the rest...
2862 item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString());
2863 item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt());
2864 item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt());
2865 item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt());
2866 item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong);
2867 item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString());
2868 item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString());
2869 item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString());
2870 item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString());
2871 item->SetLabel(record->at(song_strTitle).get_asString());
2872 item->m_lStartOffset = record->at(song_iStartOffset).get_asInt64();
2873 item->SetProperty("item_start", item->m_lStartOffset);
2874 item->m_lEndOffset = record->at(song_iEndOffset).get_asInt64();
2875 item->GetMusicInfoTag()->SetMusicBrainzTrackID(record->at(song_strMusicBrainzTrackID).get_asString());
2876 item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat());
2877 item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt());
2878 item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt());
2879 item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString());
2880 item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString());
2881 item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt());
2882 item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString());
2883 item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString());
2884 item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString());
2885 item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString());
2886 std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(), record->at(song_strFileName).get_asString());
2887 item->GetMusicInfoTag()->SetURL(strRealPath);
2888 item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1);
2889 item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1);
2890 // get the album artist string from songview (not the album_artist and artist tables)
2891 item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString());
2892 item->GetMusicInfoTag()->SetAlbumReleaseType(CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString()));
2893 item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt());
2894 item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt());
2895 item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt());
2896 item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt());
2897 // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
2898 ReplayGain replaygain;
2899 replaygain.Set(record->at(song_strReplayGain).get_asString());
2900 item->GetMusicInfoTag()->SetReplayGain(replaygain);
2901 item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt());
2902
2903 item->GetMusicInfoTag()->SetLoaded(true);
2904 // Get filename with full path
2905 if (!baseUrl.IsValid())
2906 item->SetPath(strRealPath);
2907 else
2908 {
2909 CMusicDbUrl itemUrl = baseUrl;
2910 std::string strFileName = record->at(song_strFileName).get_asString();
2911 std::string strExt = URIUtils::GetExtension(strFileName);
2912 std::string path = StringUtils::Format("%i%s", record->at(song_idSong).get_asInt(), strExt.c_str());
2913 itemUrl.AppendPath(path);
2914 item->SetPath(itemUrl.ToString());
2915 item->SetDynPath(strRealPath);
2916 }
2917 }
2918
GetFileItemFromArtistCredits(VECARTISTCREDITS & artistCredits,CFileItem * item)2919 void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item)
2920 {
2921 // Populate fileitem with artists from vector of artist credits
2922 std::vector<std::string> musicBrainzID;
2923 std::vector<std::string> songartists;
2924 CVariant artistidObj(CVariant::VariantTypeArray);
2925
2926 // When "missing tag" artist, it is the only artist when present.
2927 if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID)
2928 {
2929 artistidObj.push_back((int)BLANKARTIST_ID);
2930 songartists.push_back(StringUtils::Empty);
2931 }
2932 else
2933 {
2934 for (const auto &artistCredit : artistCredits)
2935 {
2936 artistidObj.push_back(artistCredit.GetArtistId());
2937 songartists.push_back(artistCredit.GetArtist());
2938 if (!artistCredit.GetMusicBrainzArtistID().empty())
2939 musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
2940 }
2941 }
2942 item->GetMusicInfoTag()->SetArtist(songartists); // Also sets ArtistDesc if empty from song.strArtist field
2943 item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID);
2944 // Add album artistIds as separate property as not part of CMusicInfoTag
2945 item->SetProperty("artistid", artistidObj);
2946 }
2947
GetAlbumFromDataset(dbiplus::Dataset * pDS,int offset,bool imageURL)2948 CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS, int offset /* = 0 */, bool imageURL /* = false*/)
2949 {
2950 return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL);
2951 }
2952
GetAlbumFromDataset(const dbiplus::sql_record * const record,int offset,bool imageURL)2953 CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */, bool imageURL /* = false*/)
2954 {
2955 const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
2956
2957 CAlbum album;
2958 album.idAlbum = record->at(offset + album_idAlbum).get_asInt();
2959 album.strAlbum = record->at(offset + album_strAlbum).get_asString();
2960 if (album.strAlbum.empty())
2961 album.strAlbum = g_localizeStrings.Get(1050);
2962 album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString();
2963 album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString();
2964 album.strArtistDesc = record->at(offset + album_strArtists).get_asString();
2965 album.strArtistSort = record->at(offset + album_strArtistSort).get_asString();
2966 album.genre = StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator);
2967 album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString();
2968 album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString();
2969 album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1;
2970 if (imageURL)
2971 album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString());
2972 album.fRating = record->at(offset + album_fRating).get_asFloat();
2973 album.iUserrating = record->at(offset + album_iUserrating).get_asInt();
2974 album.iVotes = record->at(offset + album_iVotes).get_asInt();
2975 album.strReview = record->at(offset + album_strReview).get_asString();
2976 album.styles = StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator);
2977 album.moods = StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator);
2978 album.themes = StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator);
2979 album.strLabel = record->at(offset + album_strLabel).get_asString();
2980 album.strType = record->at(offset + album_strType).get_asString();
2981 album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString();
2982 album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1;
2983 album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1;
2984 album.strLastScraped = record->at(offset + album_lastScraped).get_asString();
2985 album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt();
2986 album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString());
2987 album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt();
2988 album.SetDateAdded(record->at(offset + album_dateAdded).get_asString());
2989 album.SetDateNew(record->at(offset + album_dateNew).get_asString());
2990 album.SetDateUpdated(record->at(offset + album_dateModified).get_asString());
2991 album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString());
2992 album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt();
2993 return album;
2994 }
2995
GetArtistCreditFromDataset(const dbiplus::sql_record * const record,int offset)2996 CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */)
2997 {
2998 CArtistCredit artistCredit;
2999 artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt();
3000 if (artistCredit.idArtist == BLANKARTIST_ID)
3001 artistCredit.m_strArtist = StringUtils::Empty;
3002 else
3003 {
3004 artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString();
3005 artistCredit.m_strMusicBrainzArtistID = record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString();
3006 }
3007 return artistCredit;
3008 }
3009
GetArtistRoleFromDataset(const dbiplus::sql_record * const record,int offset)3010 CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */)
3011 {
3012 CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(),
3013 record->at(offset + artistCredit_strRole).get_asString(),
3014 record->at(offset + artistCredit_strArtist).get_asString(),
3015 record->at(offset + artistCredit_idArtist).get_asInt());
3016 return ArtistRole;
3017 }
3018
GetArtistFromDataset(dbiplus::Dataset * pDS,int offset,bool needThumb)3019 CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS, int offset /* = 0 */, bool needThumb /* = true */)
3020 {
3021 return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb);
3022 }
3023
GetArtistFromDataset(const dbiplus::sql_record * const record,int offset,bool needThumb)3024 CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record, int offset /* = 0 */, bool needThumb /* = true */)
3025 {
3026 const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
3027
3028 CArtist artist;
3029 artist.idArtist = record->at(offset + artist_idArtist).get_asInt();
3030 if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist)
3031 artist.strArtist = g_localizeStrings.Get(38042); //Missing artist tag in current language
3032 else
3033 artist.strArtist = record->at(offset + artist_strArtist).get_asString();
3034 artist.strSortName = record->at(offset + artist_strSortName).get_asString();
3035 artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString();
3036 artist.strType = record->at(offset + artist_strType).get_asString();
3037 artist.strGender = record->at(offset + artist_strGender).get_asString();
3038 artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString();
3039 artist.genre = StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator);
3040 artist.strBiography = record->at(offset + artist_strBiography).get_asString();
3041 artist.styles = StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator);
3042 artist.moods = StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator);
3043 artist.strBorn = record->at(offset + artist_strBorn).get_asString();
3044 artist.strFormed = record->at(offset + artist_strFormed).get_asString();
3045 artist.strDied = record->at(offset + artist_strDied).get_asString();
3046 artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString();
3047 artist.yearsActive = StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator);
3048 artist.instruments = StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator);
3049 artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1;
3050 artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString();
3051 artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString());
3052 artist.SetDateNew(record->at(offset + artist_dateNew).get_asString());
3053 artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString());
3054
3055 if (needThumb)
3056 {
3057 artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString());
3058 }
3059
3060 return artist;
3061 }
3062
GetSongByFileName(const std::string & strFileNameAndPath,CSong & song,int64_t startOffset)3063 bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath, CSong& song, int64_t startOffset)
3064 {
3065 song.Clear();
3066 CURL url(strFileNameAndPath);
3067
3068 if (url.IsProtocol("musicdb"))
3069 {
3070 std::string strFile = URIUtils::GetFileName(strFileNameAndPath);
3071 URIUtils::RemoveExtension(strFile);
3072 return GetSong(atoi(strFile.c_str()), song);
3073 }
3074
3075 if (nullptr == m_pDB)
3076 return false;
3077 if (nullptr == m_pDS)
3078 return false;
3079
3080 std::string strPath, strFileName;
3081 SplitPath(strFileNameAndPath, strPath, strFileName);
3082 URIUtils::AddSlashAtEnd(strPath);
3083
3084 std::string strSQL = PrepareSQL("select idSong from songview "
3085 "where strFileName='%s' and strPath='%s'",
3086 strFileName.c_str(), strPath.c_str());
3087 if (startOffset)
3088 strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset);
3089
3090 int idSong = GetSingleValueInt(strSQL);
3091 if (idSong > 0)
3092 return GetSong(idSong, song);
3093
3094 return false;
3095 }
3096
GetAlbumIdByPath(const std::string & strPath)3097 int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath)
3098 {
3099 try
3100 {
3101 if (nullptr == m_pDB)
3102 return false;
3103 if (nullptr == m_pDS)
3104 return false;
3105
3106 std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song JOIN path ON song.idPath = path.idPath WHERE path.strPath='%s'", strPath.c_str());
3107 // run query
3108 if (!m_pDS->query(strSQL)) return false;
3109 int iRowsFound = m_pDS->num_rows();
3110
3111 int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned
3112 if (iRowsFound == 1)
3113 idAlbum = m_pDS->fv(0).get_asInt();
3114
3115 m_pDS->close();
3116
3117 return idAlbum;
3118 }
3119 catch (...)
3120 {
3121 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, strPath.c_str());
3122 }
3123
3124 return -1;
3125 }
3126
GetSongByArtistAndAlbumAndTitle(const std::string & strArtist,const std::string & strAlbum,const std::string & strTitle)3127 int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist, const std::string& strAlbum, const std::string& strTitle)
3128 {
3129 try
3130 {
3131 std::string strSQL=PrepareSQL("select idSong from songview "
3132 "where strArtists like '%s' and strAlbum like '%s' and "
3133 "strTitle like '%s'",strArtist.c_str(),strAlbum.c_str(),strTitle.c_str());
3134
3135 if (!m_pDS->query(strSQL)) return false;
3136 int iRowsFound = m_pDS->num_rows();
3137 if (iRowsFound == 0)
3138 {
3139 m_pDS->close();
3140 return -1;
3141 }
3142 int lResult = m_pDS->fv(0).get_asInt();
3143 m_pDS->close(); // cleanup recordset data
3144 return lResult;
3145 }
3146 catch (...)
3147 {
3148 CLog::Log(LOGERROR, "%s (%s,%s,%s) failed", __FUNCTION__, strArtist.c_str(),strAlbum.c_str(),strTitle.c_str());
3149 }
3150
3151 return -1;
3152 }
3153
SearchArtists(const std::string & search,CFileItemList & artists)3154 bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList &artists)
3155 {
3156 try
3157 {
3158 if (nullptr == m_pDB)
3159 return false;
3160 if (nullptr == m_pDS)
3161 return false;
3162
3163 std::string strVariousArtists = g_localizeStrings.Get(340).c_str();
3164 std::string strSQL;
3165 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3166 strSQL=PrepareSQL("select * from artist "
3167 "where (strArtist like '%s%%' or strArtist like '%% %s%%') and strArtist <> '%s' "
3168 , search.c_str(), search.c_str(), strVariousArtists.c_str() );
3169 else
3170 strSQL=PrepareSQL("select * from artist "
3171 "where strArtist like '%s%%' and strArtist <> '%s' "
3172 , search.c_str(), strVariousArtists.c_str() );
3173
3174 if (!m_pDS->query(strSQL)) return false;
3175 if (m_pDS->num_rows() == 0)
3176 {
3177 m_pDS->close();
3178 return false;
3179 }
3180
3181 const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist
3182 while (!m_pDS->eof())
3183 {
3184 std::string path = StringUtils::Format("musicdb://artists/%i/", m_pDS->fv(0).get_asInt());
3185 CFileItemPtr pItem(new CFileItem(path, true));
3186 std::string label = StringUtils::Format("[%s] %s", artistLabel.c_str(), m_pDS->fv(1).get_asString().c_str());
3187 pItem->SetLabel(label);
3188 label = StringUtils::Format("A %s", m_pDS->fv(1).get_asString().c_str()); // sort label is stored in the title tag
3189 pItem->GetMusicInfoTag()->SetTitle(label);
3190 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist);
3191 artists.Add(pItem);
3192 m_pDS->next();
3193 }
3194
3195 m_pDS->close(); // cleanup recordset data
3196 return true;
3197 }
3198 catch (...)
3199 {
3200 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3201 }
3202
3203 return false;
3204 }
3205
GetTop100(const std::string & strBaseDir,CFileItemList & items)3206 bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items)
3207 {
3208 try
3209 {
3210 if (nullptr == m_pDB)
3211 return false;
3212 if (nullptr == m_pDS)
3213 return false;
3214
3215 CMusicDbUrl baseUrl;
3216 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3217 return false;
3218
3219 std::string strSQL="select * from songview "
3220 "where iTimesPlayed>0 "
3221 "order by iTimesPlayed desc "
3222 "limit 100";
3223
3224 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3225 if (!m_pDS->query(strSQL)) return false;
3226 int iRowsFound = m_pDS->num_rows();
3227 if (iRowsFound == 0)
3228 {
3229 m_pDS->close();
3230 return true;
3231 }
3232 items.Reserve(iRowsFound);
3233 while (!m_pDS->eof())
3234 {
3235 CFileItemPtr item(new CFileItem);
3236 GetFileItemFromDataset(item.get(), baseUrl);
3237 items.Add(item);
3238 m_pDS->next();
3239 }
3240
3241 m_pDS->close(); // cleanup recordset data
3242 return true;
3243 }
3244 catch (...)
3245 {
3246 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3247 }
3248
3249 return false;
3250 }
3251
GetTop100Albums(VECALBUMS & albums)3252 bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums)
3253 {
3254 try
3255 {
3256 albums.erase(albums.begin(), albums.end());
3257 if (nullptr == m_pDB)
3258 return false;
3259 if (nullptr == m_pDS)
3260 return false;
3261
3262 // Get data from album and album_artist tables to fully populate albums
3263 std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview "
3264 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3265 "WHERE albumartistview.idAlbum in "
3266 "(SELECT albumview.idAlbum FROM albumview "
3267 "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
3268 "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
3269 "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
3270
3271 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3272 if (!m_pDS->query(strSQL)) return false;
3273 int iRowsFound = m_pDS->num_rows();
3274 if (iRowsFound == 0)
3275 {
3276 m_pDS->close();
3277 return true;
3278 }
3279
3280 int albumArtistOffset = album_enumCount;
3281 int albumId = -1;
3282 while (!m_pDS->eof())
3283 {
3284 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3285
3286 if (albumId != record->at(album_idAlbum).get_asInt())
3287 { // New album
3288 albumId = record->at(album_idAlbum).get_asInt();
3289 albums.push_back(GetAlbumFromDataset(record));
3290 }
3291 // Get album artists
3292 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3293
3294 m_pDS->next();
3295 }
3296
3297 m_pDS->close(); // cleanup recordset data
3298 return true;
3299 }
3300 catch (...)
3301 {
3302 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3303 }
3304
3305 return false;
3306 }
3307
GetTop100AlbumSongs(const std::string & strBaseDir,CFileItemList & items)3308 bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items)
3309 {
3310 try
3311 {
3312 if (nullptr == m_pDB)
3313 return false;
3314 if (nullptr == m_pDS)
3315 return false;
3316
3317 CMusicDbUrl baseUrl;
3318 if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir))
3319 return false;
3320
3321 std::string strSQL = StringUtils::Format("SELECT songview.*, albumview.* FROM songview JOIN albumview ON (songview.idAlbum = albumview.idAlbum) JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song WHERE song.iTimesPlayed > 0 GROUP BY idAlbum ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit ON (songview.idAlbum = _albumlimit.idAlbum) ORDER BY _albumlimit.iTimesPlayedSum DESC");
3322 CLog::Log(LOGDEBUG,"GetTop100AlbumSongs() query: %s", strSQL.c_str());
3323 if (!m_pDS->query(strSQL)) return false;
3324
3325 int iRowsFound = m_pDS->num_rows();
3326 if (iRowsFound == 0)
3327 {
3328 m_pDS->close();
3329 return true;
3330 }
3331
3332 // get data from returned rows
3333 items.Reserve(iRowsFound);
3334 while (!m_pDS->eof())
3335 {
3336 CFileItemPtr item(new CFileItem);
3337 GetFileItemFromDataset(item.get(), baseUrl);
3338 items.Add(item);
3339 m_pDS->next();
3340 }
3341
3342 // cleanup
3343 m_pDS->close();
3344 return true;
3345 }
3346 catch (...)
3347 {
3348 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3349 }
3350 return false;
3351 }
3352
GetRecentlyPlayedAlbums(VECALBUMS & albums)3353 bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums)
3354 {
3355 try
3356 {
3357 albums.erase(albums.begin(), albums.end());
3358 if (nullptr == m_pDB)
3359 return false;
3360 if (nullptr == m_pDS)
3361 return false;
3362
3363 unsigned int querytime = 0;
3364 unsigned int time = XbmcThreads::SystemClockMillis();
3365
3366 // Get data from album and album_artist tables to fully populate albums
3367 std::string strSQL = PrepareSQL("SELECT albumview.*, albumartistview.* FROM "
3368 "(SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3369 "AND albumview.strReleaseType = '%s' "
3370 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3371 "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
3372 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3373 "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
3374 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT);
3375
3376 querytime = XbmcThreads::SystemClockMillis();
3377 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3378 if (!m_pDS->query(strSQL)) return false;
3379 querytime = XbmcThreads::SystemClockMillis() - querytime;
3380 int iRowsFound = m_pDS->num_rows();
3381 if (iRowsFound == 0)
3382 {
3383 m_pDS->close();
3384 return true;
3385 }
3386
3387 int albumArtistOffset = album_enumCount;
3388 int albumId = -1;
3389 while (!m_pDS->eof())
3390 {
3391 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3392
3393 if (albumId != record->at(album_idAlbum).get_asInt())
3394 { // New album
3395 albumId = record->at(album_idAlbum).get_asInt();
3396 albums.push_back(GetAlbumFromDataset(record));
3397 }
3398 // Get album artists
3399 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3400
3401 m_pDS->next();
3402 }
3403 m_pDS->close(); // cleanup recordset data
3404
3405 CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms",
3406 __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
3407 return true;
3408 }
3409 catch (...)
3410 {
3411 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3412 }
3413
3414 return false;
3415 }
3416
GetRecentlyPlayedAlbumSongs(const std::string & strBaseDir,CFileItemList & items)3417 bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir, CFileItemList& items)
3418 {
3419 try
3420 {
3421 if (nullptr == m_pDB)
3422 return false;
3423 if (nullptr == m_pDS)
3424 return false;
3425
3426 CMusicDbUrl baseUrl;
3427 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3428 return false;
3429
3430 std::string strSQL = PrepareSQL("SELECT songview.*, songartistview.* FROM "
3431 "(SELECT idAlbum, lastPlayed FROM albumview WHERE albumview.lastplayed IS NOT NULL "
3432 "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
3433 "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
3434 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3435 "ORDER BY playedalbums.lastplayed DESC,songartistview.idsong, songartistview.idRole, songartistview.iOrder",
3436 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryRecentlyAddedItems);
3437 CLog::Log(LOGDEBUG,"GetRecentlyPlayedAlbumSongs() query: %s", strSQL.c_str());
3438 if (!m_pDS->query(strSQL)) return false;
3439
3440 int iRowsFound = m_pDS->num_rows();
3441 if (iRowsFound == 0)
3442 {
3443 m_pDS->close();
3444 return true;
3445 }
3446
3447 // Needs a separate query to determine number of songs to set items size.
3448 // Get songs from returned rows. Join means there is a row for every song artist
3449 // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
3450 int songArtistOffset = song_enumCount;
3451 int songId = -1;
3452 VECARTISTCREDITS artistCredits;
3453 while (!m_pDS->eof())
3454 {
3455 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3456
3457 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3458 if (songId != record->at(song_idSong).get_asInt())
3459 { //New song
3460 if (songId > 0 && !artistCredits.empty())
3461 {
3462 //Store artist credits for previous song
3463 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3464 artistCredits.clear();
3465 }
3466 songId = record->at(song_idSong).get_asInt();
3467 CFileItemPtr item(new CFileItem);
3468 GetFileItemFromDataset(record, item.get(), baseUrl);
3469 items.Add(item);
3470 }
3471 // Get song artist credits and contributors
3472 if (idSongArtistRole == ROLE_ARTIST)
3473 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3474 else
3475 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
3476
3477 m_pDS->next();
3478 }
3479 if (!artistCredits.empty())
3480 {
3481 //Store artist credits for final song
3482 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3483 artistCredits.clear();
3484 }
3485
3486 // cleanup
3487 m_pDS->close();
3488 return true;
3489 }
3490 catch (...)
3491 {
3492 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3493 }
3494 return false;
3495 }
3496
GetRecentlyAddedAlbums(VECALBUMS & albums,unsigned int limit)3497 bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit)
3498 {
3499 try
3500 {
3501 albums.erase(albums.begin(), albums.end());
3502 if (nullptr == m_pDB)
3503 return false;
3504 if (nullptr == m_pDS)
3505 return false;
3506
3507 // Get data from album and album_artist tables to fully populate albums
3508 // Determine the recently added albums from dateAdded (usually derived from music file
3509 // timestamps, nothing to do with when albums added to library)
3510 std::string strSQL = PrepareSQL("SELECT albumview.*, albumartistview.* FROM "
3511 "(SELECT idAlbum FROM album WHERE strAlbum != '' "
3512 "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3513 "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
3514 "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
3515 "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
3516 limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryRecentlyAddedItems);
3517
3518 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3519 if (!m_pDS->query(strSQL)) return false;
3520 int iRowsFound = m_pDS->num_rows();
3521 if (iRowsFound == 0)
3522 {
3523 m_pDS->close();
3524 return true;
3525 }
3526
3527 int albumArtistOffset = album_enumCount;
3528 int albumId = -1;
3529 while (!m_pDS->eof())
3530 {
3531 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3532
3533 if (albumId != record->at(album_idAlbum).get_asInt())
3534 { // New album
3535 albumId = record->at(album_idAlbum).get_asInt();
3536 albums.push_back(GetAlbumFromDataset(record));
3537 }
3538 // Get album artists
3539 albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
3540
3541 m_pDS->next();
3542 }
3543 m_pDS->close(); // cleanup recordset data
3544 return true;
3545 }
3546 catch (...)
3547 {
3548 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3549 }
3550
3551 return false;
3552 }
3553
GetRecentlyAddedAlbumSongs(const std::string & strBaseDir,CFileItemList & items,unsigned int limit)3554 bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir, CFileItemList& items, unsigned int limit)
3555 {
3556 try
3557 {
3558 if (nullptr == m_pDB)
3559 return false;
3560 if (nullptr == m_pDS)
3561 return false;
3562
3563 CMusicDbUrl baseUrl;
3564 if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
3565 return false;
3566
3567 // Get data from song and song_artist tables to fully populate songs
3568 // Determine the recently added albums from dateAdded (usually derived from music file
3569 // timestamps, nothing to do with when albums added to library)
3570 std::string strSQL;
3571 strSQL = PrepareSQL("SELECT songview.*, songartistview.* FROM "
3572 "(SELECT idAlbum, dateAdded FROM album ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
3573 "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
3574 "JOIN songartistview ON songview.idSong = songartistview.idSong "
3575 "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
3576 "songview.idSong, songartistview.idRole, songartistview.iOrder ",
3577 limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryRecentlyAddedItems);
3578 CLog::Log(LOGDEBUG,"GetRecentlyAddedAlbumSongs() query: %s", strSQL.c_str());
3579 if (!m_pDS->query(strSQL)) return false;
3580
3581 int iRowsFound = m_pDS->num_rows();
3582 if (iRowsFound == 0)
3583 {
3584 m_pDS->close();
3585 return true;
3586 }
3587
3588 // Needs a separate query to determine number of songs to set items size.
3589 // Get songs from returned rows. Join means there is a row for every song artist
3590 int songArtistOffset = song_enumCount;
3591 int songId = -1;
3592 VECARTISTCREDITS artistCredits;
3593 while (!m_pDS->eof())
3594 {
3595 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
3596
3597 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
3598 if (songId != record->at(song_idSong).get_asInt())
3599 { //New song
3600 if (songId > 0 && !artistCredits.empty())
3601 {
3602 //Store artist credits for previous song
3603 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3604 artistCredits.clear();
3605 }
3606 songId = record->at(song_idSong).get_asInt();
3607 CFileItemPtr item(new CFileItem);
3608 GetFileItemFromDataset(record, item.get(), baseUrl);
3609 items.Add(item);
3610 }
3611 // Get song artist credits and contributors
3612 if (idSongArtistRole == ROLE_ARTIST)
3613 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
3614 else
3615 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
3616
3617 m_pDS->next();
3618 }
3619 if (!artistCredits.empty())
3620 {
3621 //Store artist credits for final song
3622 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
3623 artistCredits.clear();
3624 }
3625
3626 // cleanup
3627 m_pDS->close();
3628 return true;
3629 }
3630 catch (...)
3631 {
3632 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3633 }
3634 return false;
3635 }
3636
IncrementPlayCount(const CFileItem & item)3637 void CMusicDatabase::IncrementPlayCount(const CFileItem& item)
3638 {
3639 try
3640 {
3641 if (nullptr == m_pDB)
3642 return;
3643 if (nullptr == m_pDS)
3644 return;
3645
3646 int idSong = GetSongIDFromPath(item.GetPath());
3647 std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
3648 std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
3649 "WHERE idSong=%i",
3650 strDateNow.c_str(), idSong);
3651 m_pDS->exec(sql);
3652 }
3653 catch (...)
3654 {
3655 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, item.GetPath().c_str());
3656 }
3657 }
3658
GetSongsByPath(const std::string & strPath1,MAPSONGS & songmap,bool bAppendToMap)3659 bool CMusicDatabase::GetSongsByPath(const std::string& strPath1, MAPSONGS& songmap, bool bAppendToMap)
3660 {
3661 std::string strPath(strPath1);
3662 try
3663 {
3664 if (!URIUtils::HasSlashAtEnd(strPath))
3665 URIUtils::AddSlashAtEnd(strPath);
3666
3667 if (!bAppendToMap)
3668 songmap.clear();
3669
3670 if (nullptr == m_pDB)
3671 return false;
3672 if (nullptr == m_pDS)
3673 return false;
3674
3675 // Filename is not unique for a path as songs from a cuesheet have same filename.
3676 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
3677 // in a folder and some edited and rescanned.
3678 // Hence order by filename so these songs can be gathered together.
3679 std::string strSQL = PrepareSQL(
3680 "SELECT * FROM songview WHERE strPath='%s' ORDER BY strFileName", strPath.c_str());
3681 if (!m_pDS->query(strSQL))
3682 return false;
3683 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
3684 int iRowsFound = m_pDS->num_rows();
3685 if (iRowsFound == 0)
3686 {
3687 m_pDS->close();
3688 return false;
3689 }
3690
3691 // Each file is potentially mapped to a list of songs, gather these and save as list
3692 VECSONGS songs;
3693 std::string filename;
3694 while (!m_pDS->eof())
3695 {
3696 CSong song = GetSongFromDataset();
3697 if (!filename.empty() && filename != song.strFileName)
3698 {
3699 // Save songs for previous filename
3700 songmap.insert(std::make_pair(filename, songs));
3701 songs.clear();
3702 }
3703 filename = song.strFileName;
3704 songs.emplace_back(song);
3705 m_pDS->next();
3706 }
3707 m_pDS->close(); // cleanup recordset data
3708 songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
3709 return true;
3710 }
3711 catch (...)
3712 {
3713 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, strPath.c_str());
3714 }
3715
3716 return false;
3717 }
3718
EmptyCache()3719 void CMusicDatabase::EmptyCache()
3720 {
3721 m_genreCache.erase(m_genreCache.begin(), m_genreCache.end());
3722 m_pathCache.erase(m_pathCache.begin(), m_pathCache.end());
3723 }
3724
Search(const std::string & search,CFileItemList & items)3725 bool CMusicDatabase::Search(const std::string& search, CFileItemList &items)
3726 {
3727 unsigned int time = XbmcThreads::SystemClockMillis();
3728 // first grab all the artists that match
3729 SearchArtists(search, items);
3730 CLog::Log(LOGDEBUG, "%s Artist search in %i ms",
3731 __FUNCTION__, XbmcThreads::SystemClockMillis() - time); time = XbmcThreads::SystemClockMillis();
3732
3733 // then albums that match
3734 SearchAlbums(search, items);
3735 CLog::Log(LOGDEBUG, "%s Album search in %i ms",
3736 __FUNCTION__, XbmcThreads::SystemClockMillis() - time); time = XbmcThreads::SystemClockMillis();
3737
3738 // and finally songs
3739 SearchSongs(search, items);
3740 CLog::Log(LOGDEBUG, "%s Songs search in %i ms",
3741 __FUNCTION__, XbmcThreads::SystemClockMillis() - time); time = XbmcThreads::SystemClockMillis();
3742 return true;
3743 }
3744
SearchSongs(const std::string & search,CFileItemList & items)3745 bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList &items)
3746 {
3747 try
3748 {
3749 if (nullptr == m_pDB)
3750 return false;
3751 if (nullptr == m_pDS)
3752 return false;
3753
3754 CMusicDbUrl baseUrl;
3755 if (!baseUrl.FromString("musicdb://songs/"))
3756 return false;
3757
3758 std::string strSQL;
3759 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3760 strSQL=PrepareSQL("select * from songview where strTitle like '%s%%' or strTitle like '%% %s%%' limit 1000", search.c_str(), search.c_str());
3761 else
3762 strSQL=PrepareSQL("select * from songview where strTitle like '%s%%' limit 1000", search.c_str());
3763
3764 if (!m_pDS->query(strSQL)) return false;
3765 if (m_pDS->num_rows() == 0) return false;
3766
3767 while (!m_pDS->eof())
3768 {
3769 CFileItemPtr item(new CFileItem);
3770 GetFileItemFromDataset(item.get(), baseUrl);
3771 items.Add(item);
3772 m_pDS->next();
3773 }
3774
3775 m_pDS->close();
3776 return true;
3777 }
3778 catch (...)
3779 {
3780 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3781 }
3782
3783 return false;
3784 }
3785
SearchAlbums(const std::string & search,CFileItemList & albums)3786 bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList &albums)
3787 {
3788 try
3789 {
3790 if (nullptr == m_pDB)
3791 return false;
3792 if (nullptr == m_pDS)
3793 return false;
3794
3795 std::string strSQL;
3796 if (search.size() >= MIN_FULL_SEARCH_LENGTH)
3797 strSQL=PrepareSQL("select * from albumview where strAlbum like '%s%%' or strAlbum like '%% %s%%'", search.c_str(), search.c_str());
3798 else
3799 strSQL=PrepareSQL("select * from albumview where strAlbum like '%s%%'", search.c_str());
3800
3801 if (!m_pDS->query(strSQL)) return false;
3802
3803 const std::string& albumLabel(g_localizeStrings.Get(558)); // Album
3804 while (!m_pDS->eof())
3805 {
3806 CAlbum album = GetAlbumFromDataset(m_pDS.get());
3807 std::string path = StringUtils::Format("musicdb://albums/%ld/", album.idAlbum);
3808 CFileItemPtr pItem(new CFileItem(path, album));
3809 std::string label = StringUtils::Format("[%s] %s", albumLabel.c_str(), album.strAlbum.c_str());
3810 pItem->SetLabel(label);
3811 label = StringUtils::Format("B %s", album.strAlbum.c_str()); // sort label is stored in the title tag
3812 pItem->GetMusicInfoTag()->SetTitle(label);
3813 albums.Add(pItem);
3814 m_pDS->next();
3815 }
3816 m_pDS->close(); // cleanup recordset data
3817 return true;
3818 }
3819 catch (...)
3820 {
3821 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
3822 }
3823 return false;
3824 }
3825
CleanupSongsByIds(const std::string & strSongIds)3826 bool CMusicDatabase::CleanupSongsByIds(const std::string &strSongIds)
3827 {
3828 try
3829 {
3830 if (nullptr == m_pDB)
3831 return false;
3832 if (nullptr == m_pDS)
3833 return false;
3834 // ok, now find all idSong's
3835 std::string strSQL=PrepareSQL("select * from song join path on song.idPath = path.idPath where song.idSong in %s", strSongIds.c_str());
3836 if (!m_pDS->query(strSQL)) return false;
3837 int iRowsFound = m_pDS->num_rows();
3838 if (iRowsFound == 0)
3839 {
3840 m_pDS->close();
3841 return true;
3842 }
3843 std::vector<std::string> songsToDelete;
3844 while (!m_pDS->eof())
3845 { // get the full song path
3846 std::string strFileName = URIUtils::AddFileToFolder(m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString());
3847
3848 // Special case for streams inside an ogg file. (oggstream)
3849 // The last dir in the path is the ogg file that
3850 // contains the stream, so test if its there
3851 if (URIUtils::HasExtension(strFileName, ".oggstream|.nsfstream"))
3852 {
3853 strFileName = URIUtils::GetDirectory(strFileName);
3854 // we are dropping back to a file, so remove the slash at end
3855 URIUtils::RemoveSlashAtEnd(strFileName);
3856 }
3857
3858 if (!CFile::Exists(strFileName, false))
3859 { // file no longer exists, so add to deletion list
3860 songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString());
3861 }
3862 m_pDS->next();
3863 }
3864 m_pDS->close();
3865
3866 if (!songsToDelete.empty())
3867 {
3868 std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")";
3869 // ok, now delete these songs + all references to them from the linked tables
3870 strSQL = "delete from song where idSong in " + strSongsToDelete;
3871 m_pDS->exec(strSQL);
3872 m_pDS->close();
3873 }
3874 return true;
3875 }
3876 catch (...)
3877 {
3878 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
3879 }
3880 return false;
3881 }
3882
CleanupSongs(CGUIDialogProgress * progressDialog)3883 bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/)
3884 {
3885 try
3886 {
3887 int total;
3888 // Count total number of songs
3889 total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS);
3890 // No songs to clean
3891 if (total == 0)
3892 return true;
3893
3894 // run through all songs and get all unique path ids
3895 int iLIMIT = 1000;
3896 for (int i=0;;i+=iLIMIT)
3897 {
3898 std::string strSQL=PrepareSQL("select song.idSong from song order by song.idSong limit %i offset %i",iLIMIT,i);
3899 if (!m_pDS->query(strSQL)) return false;
3900 int iRowsFound = m_pDS->num_rows();
3901 // keep going until no rows are left!
3902 if (iRowsFound == 0)
3903 {
3904 m_pDS->close();
3905 return true;
3906 }
3907
3908 std::vector<std::string> songIds;
3909 while (!m_pDS->eof())
3910 {
3911 songIds.push_back(m_pDS->fv("song.idSong").get_asString());
3912 m_pDS->next();
3913 }
3914 m_pDS->close();
3915 std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")";
3916 CLog::Log(LOGDEBUG,"Checking songs from song ID list: %s",strSongIds.c_str());
3917 if (progressDialog)
3918 {
3919 int percentage = i * 100 / total;
3920 if (percentage > progressDialog->GetPercentage())
3921 {
3922 progressDialog->SetPercentage(percentage);
3923 progressDialog->Progress();
3924 }
3925 if (progressDialog->IsCanceled())
3926 {
3927 m_pDS->close();
3928 return false;
3929 }
3930 }
3931 if (!CleanupSongsByIds(strSongIds)) return false;
3932 }
3933 return true;
3934 }
3935 catch(...)
3936 {
3937 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()");
3938 }
3939 return false;
3940 }
3941
CleanupAlbums()3942 bool CMusicDatabase::CleanupAlbums()
3943 {
3944 try
3945 {
3946 // This must be run AFTER songs have been cleaned up
3947 // delete albums with no reference to songs
3948 std::string strSQL = "select * from album where album.idAlbum not in (select idAlbum from song)";
3949 if (!m_pDS->query(strSQL)) return false;
3950 int iRowsFound = m_pDS->num_rows();
3951 if (iRowsFound == 0)
3952 {
3953 m_pDS->close();
3954 return true;
3955 }
3956
3957 std::vector<std::string> albumIds;
3958 while (!m_pDS->eof())
3959 {
3960 albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString());
3961 m_pDS->next();
3962 }
3963 m_pDS->close();
3964
3965 std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")";
3966 // ok, now we can delete them and the references in the linked tables
3967 strSQL = "delete from album where idAlbum in " + strAlbumIds;
3968 m_pDS->exec(strSQL);
3969 return true;
3970 }
3971 catch (...)
3972 {
3973 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()");
3974 }
3975 return false;
3976 }
3977
CleanupPaths()3978 bool CMusicDatabase::CleanupPaths()
3979 {
3980 try
3981 {
3982 // needs to be done AFTER the songs and albums have been cleaned up.
3983 // we can happily delete any path that has no reference to a song
3984 // but we must keep all paths that have been scanned that may contain songs in subpaths
3985
3986 // first create a temporary table of song paths
3987 m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
3988 m_pDS->exec("INSERT INTO songpaths select idPath,strPath from path where idPath in (select idPath from song)\n");
3989
3990 // grab all paths that aren't immediately connected with a song
3991 std::string sql = "select * from path where idPath not in (select idPath from song)";
3992 if (!m_pDS->query(sql)) return false;
3993 int iRowsFound = m_pDS->num_rows();
3994 if (iRowsFound == 0)
3995 {
3996 m_pDS->close();
3997 return true;
3998 }
3999 // and construct a list to delete
4000 std::vector<std::string> pathIds;
4001 while (!m_pDS->eof())
4002 {
4003 // anything that isn't a parent path of a song path is to be deleted
4004 std::string path = m_pDS->fv("strPath").get_asString();
4005 std::string sql = PrepareSQL("select count(idPath) from songpaths where SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()), path.c_str());
4006 if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0)
4007 pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete
4008 m_pDS2->close();
4009 m_pDS->next();
4010 }
4011 m_pDS->close();
4012
4013 if (!pathIds.empty())
4014 {
4015 // do the deletion, and drop our temp table
4016 std::string deleteSQL = "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")";
4017 m_pDS->exec(deleteSQL);
4018 }
4019 m_pDS->exec("drop table songpaths");
4020 return true;
4021 }
4022 catch (...)
4023 {
4024 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
4025 }
4026 return false;
4027 }
4028
InsideScannedPath(const std::string & path)4029 bool CMusicDatabase::InsideScannedPath(const std::string& path)
4030 {
4031 std::string sql = PrepareSQL("select idPath from path where SUBSTR(strPath,1,%i)='%s' LIMIT 1", path.size(), path.c_str());
4032 return !GetSingleValue(sql).empty();
4033 }
4034
CleanupArtists()4035 bool CMusicDatabase::CleanupArtists()
4036 {
4037 try
4038 {
4039 // (nested queries by Bobbin007)
4040 // must be executed AFTER the song, album and their artist link tables are cleaned.
4041 // Don't delete [Missing] the missing artist tag artist
4042
4043 // Create temp table to avoid 1442 trigger hell on mysql
4044 m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
4045 m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
4046 m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
4047 m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID));
4048 // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
4049 // To avoid MySQL hanging or timeout create a table of unique ids with primary key
4050 m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
4051 m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
4052 m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
4053 // Tidy up temp tables
4054 m_pDS->exec("DROP TABLE tmp_delartists");
4055 m_pDS->exec("DROP TABLE tmp_keep");
4056
4057 return true;
4058 }
4059 catch (...)
4060 {
4061 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
4062 }
4063 return false;
4064 }
4065
CleanupGenres()4066 bool CMusicDatabase::CleanupGenres()
4067 {
4068 try
4069 {
4070 // Cleanup orphaned song genres (ie those that don't belong to a song entry)
4071 // (nested queries by Bobbin007)
4072 // Must be executed AFTER the song, and song_genre have been cleaned.
4073 std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
4074 m_pDS->exec(strSQL);
4075 return true;
4076 }
4077 catch (...)
4078 {
4079 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
4080 }
4081 return false;
4082 }
4083
CleanupInfoSettings()4084 bool CMusicDatabase::CleanupInfoSettings()
4085 {
4086 try
4087 {
4088 // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
4089 // Must be executed AFTER the album and artist tables have been cleaned.
4090 std::string strSQL = "DELETE FROM infosetting WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
4091 "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
4092 m_pDS->exec(strSQL);
4093 return true;
4094 }
4095 catch (...)
4096 {
4097 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
4098 }
4099 return false;
4100 }
4101
CleanupRoles()4102 bool CMusicDatabase::CleanupRoles()
4103 {
4104 try
4105 {
4106 // Cleanup orphaned roles (ie those that don't belong to a song entry)
4107 // Must be executed AFTER the song, and song_artist tables have been cleaned.
4108 // Do not remove default role (ROLE_ARTIST)
4109 std::string strSQL = "DELETE FROM role WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
4110 m_pDS->exec(strSQL);
4111 return true;
4112 }
4113 catch (...)
4114 {
4115 CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
4116 }
4117 return false;
4118 }
4119
DeleteRemovedLinks()4120 bool CMusicDatabase::DeleteRemovedLinks()
4121 {
4122 try
4123 {
4124 std::string strSQL = "DELETE FROM removed_link";
4125 m_pDS->exec(strSQL);
4126 return true;
4127 }
4128 catch (...)
4129 {
4130 CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks");
4131 }
4132 return false;
4133 }
4134
CleanupOrphanedItems()4135 bool CMusicDatabase::CleanupOrphanedItems()
4136 {
4137 // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
4138 // remove_links not cleared here - done in CheckArtistLinksChanged()
4139 if (nullptr == m_pDB)
4140 return false;
4141 if (nullptr == m_pDS)
4142 return false;
4143 SetLibraryLastUpdated();
4144 if (!CleanupAlbums()) return false;
4145 if (!CleanupArtists()) return false;
4146 if (!CleanupGenres()) return false;
4147 if (!CleanupRoles()) return false;
4148 if (!CleanupInfoSettings()) return false;
4149 return true;
4150 }
4151
Cleanup(CGUIDialogProgress * progressDialog)4152 int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/)
4153 {
4154 if (nullptr == m_pDB)
4155 return ERROR_DATABASE;
4156 if (nullptr == m_pDS)
4157 return ERROR_DATABASE;
4158
4159 int ret = ERROR_OK;
4160 unsigned int time = XbmcThreads::SystemClockMillis();
4161 CLog::Log(LOGINFO, "%s: Starting musicdatabase cleanup ..", __FUNCTION__);
4162 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted");
4163
4164 SetLibraryLastCleaned();
4165
4166 // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link
4167 m_pDS->exec("DROP TRIGGER tgrDeleteSongArtist");
4168 m_pDS->exec("DROP TRIGGER tgrDeleteAlbumArtist");
4169
4170 // first cleanup any songs with invalid paths
4171 if (progressDialog)
4172 {
4173 progressDialog->SetLine(1, CVariant{318});
4174 progressDialog->SetLine(2, CVariant{330});
4175 progressDialog->SetPercentage(0);
4176 progressDialog->Progress();
4177 }
4178 if (!CleanupSongs(progressDialog))
4179 {
4180 ret = ERROR_REORG_SONGS;
4181 goto error;
4182 }
4183 // then the albums that are not linked to a song or to album, or whose path is removed
4184 if (progressDialog)
4185 {
4186 progressDialog->SetLine(1, CVariant{326});
4187 progressDialog->SetPercentage(20);
4188 progressDialog->Progress();
4189 if (progressDialog->IsCanceled())
4190 {
4191 ret = ERROR_CANCEL;
4192 goto error;
4193 }
4194 }
4195 if (!CleanupAlbums())
4196 {
4197 ret = ERROR_REORG_ALBUM;
4198 goto error;
4199 }
4200 // now the paths
4201 if (progressDialog)
4202 {
4203 progressDialog->SetLine(1, CVariant{324});
4204 progressDialog->SetPercentage(40);
4205 progressDialog->Progress();
4206 if (progressDialog->IsCanceled())
4207 {
4208 ret = ERROR_CANCEL;
4209 goto error;
4210 }
4211 }
4212 if (!CleanupPaths())
4213 {
4214 ret = ERROR_REORG_PATH;
4215 goto error;
4216 }
4217 // and finally artists + genres
4218 if (progressDialog)
4219 {
4220 progressDialog->SetLine(1, CVariant{320});
4221 progressDialog->SetPercentage(60);
4222 progressDialog->Progress();
4223 if (progressDialog->IsCanceled())
4224 {
4225 ret = ERROR_CANCEL;
4226 goto error;
4227 }
4228 }
4229 if (!CleanupArtists())
4230 {
4231 ret = ERROR_REORG_ARTIST;
4232 goto error;
4233 }
4234 //Genres, roles and info settings progess in one step
4235 if (progressDialog)
4236 {
4237 progressDialog->SetLine(1, CVariant{322});
4238 progressDialog->SetPercentage(80);
4239 progressDialog->Progress();
4240 if (progressDialog->IsCanceled())
4241 {
4242 ret = ERROR_CANCEL;
4243 goto error;
4244 }
4245 }
4246 if (!CleanupGenres())
4247 {
4248 ret = ERROR_REORG_OTHER;
4249 goto error;
4250 }
4251 if (!CleanupRoles())
4252 {
4253 ret = ERROR_REORG_OTHER;
4254 goto error;
4255 }
4256 if (!CleanupInfoSettings())
4257 {
4258 ret = ERROR_REORG_OTHER;
4259 goto error;
4260 }
4261 if (!DeleteRemovedLinks())
4262 {
4263 ret = ERROR_REORG_OTHER;
4264 goto error;
4265 }
4266
4267 // commit transaction
4268 if (progressDialog)
4269 {
4270 progressDialog->SetLine(1, CVariant{328});
4271 progressDialog->SetPercentage(90);
4272 progressDialog->Progress();
4273 if (progressDialog->IsCanceled())
4274 {
4275 ret = ERROR_CANCEL;
4276 goto error;
4277 }
4278 }
4279 if (!CommitTransaction())
4280 {
4281 ret = ERROR_WRITING_CHANGES;
4282 goto error;
4283 }
4284
4285 // Recreate DELETE triggers on song_artist and album_artist
4286 CreateRemovedLinkTriggers();
4287
4288 // and compress the database
4289 if (progressDialog)
4290 {
4291 progressDialog->SetLine(1, CVariant{331});
4292 progressDialog->SetPercentage(100);
4293 progressDialog->Close();
4294 }
4295 time = XbmcThreads::SystemClockMillis() - time;
4296 CLog::Log(LOGINFO, "%s: Cleaning musicdatabase done. Operation took %s", __FUNCTION__,
4297 StringUtils::SecondsToTimeString(time / 1000).c_str());
4298 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4299
4300 if (!Compress(false))
4301 {
4302 return ERROR_COMPRESSING;
4303 }
4304 return ERROR_OK;
4305
4306 error:
4307 RollbackTransaction();
4308 // Recreate DELETE triggers on song_artist and album_artist
4309 CreateRemovedLinkTriggers();
4310 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
4311 return ret;
4312 }
4313
TrimImageURLs(std::string & strImage,const size_t space)4314 bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space)
4315 {
4316 if (strImage.length() > space)
4317 {
4318 strImage = strImage.substr(0, space);
4319 // Tidy to last </thumb> tag
4320 size_t iPos = strImage.rfind("</thumb>");
4321 if (iPos == std::string::npos)
4322 return false;
4323 strImage = strImage.substr(0, iPos + 8);
4324 }
4325 return true;
4326 }
4327
LookupCDDBInfo(bool bRequery)4328 bool CMusicDatabase::LookupCDDBInfo(bool bRequery/*=false*/)
4329 {
4330 #ifdef HAS_DVD_DRIVE
4331 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOCDS_USECDDB))
4332 return false;
4333
4334 // check network connectivity
4335 if (!CServiceBroker::GetNetwork().IsAvailable())
4336 return false;
4337
4338 // Get information for the inserted disc
4339 CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
4340 if (pCdInfo == NULL)
4341 return false;
4342
4343 // If the disc has no tracks, we are finished here.
4344 int nTracks = pCdInfo->GetTrackCount();
4345 if (nTracks <= 0)
4346 return false;
4347
4348 // Delete old info if any
4349 if (bRequery)
4350 {
4351 std::string strFile = StringUtils::Format("%x.cddb", pCdInfo->GetCddbDiscId());
4352 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4353 }
4354
4355 // Prepare cddb
4356 Xcddb cddb;
4357 cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4358
4359 // Do we have to look for cddb information
4360 if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo))
4361 {
4362 CGUIDialogProgress* pDialogProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
4363 CGUIDialogSelect *pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
4364
4365 if (!pDialogProgress) return false;
4366 if (!pDlgSelect) return false;
4367
4368 // Show progress dialog if we have to connect to freedb.org
4369 pDialogProgress->SetHeading(CVariant{255}); //CDDB
4370 pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info
4371 pDialogProgress->SetLine(1, CVariant{256});
4372 pDialogProgress->SetLine(2, CVariant{""});
4373 pDialogProgress->ShowProgressBar(false);
4374 pDialogProgress->Open();
4375
4376 // get cddb information
4377 if (!cddb.queryCDinfo(pCdInfo))
4378 {
4379 pDialogProgress->Close();
4380 int lasterror = cddb.getLastError();
4381
4382 // Have we found more then on match in cddb for this disc,...
4383 if (lasterror == E_WAIT_FOR_INPUT)
4384 {
4385 // ...yes, show the matches found in a select dialog
4386 // and let the user choose an entry.
4387 pDlgSelect->Reset();
4388 pDlgSelect->SetHeading(CVariant{255});
4389 int i = 1;
4390 while (true)
4391 {
4392 std::string strTitle = cddb.getInexactTitle(i);
4393 if (strTitle == "") break;
4394
4395 const std::string& strArtist = cddb.getInexactArtist(i);
4396 if (!strArtist.empty())
4397 strTitle += " - " + strArtist;
4398
4399 pDlgSelect->Add(strTitle);
4400 i++;
4401 }
4402 pDlgSelect->Open();
4403
4404 // Has the user selected a match...
4405 int iSelectedCD = pDlgSelect->GetSelectedItem();
4406 if (iSelectedCD >= 0)
4407 {
4408 // ...query cddb for the inexact match
4409 if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD))
4410 pCdInfo->SetNoCDDBInfo();
4411 }
4412 else
4413 pCdInfo->SetNoCDDBInfo();
4414 }
4415 else if (lasterror == E_NO_MATCH_FOUND)
4416 {
4417 pCdInfo->SetNoCDDBInfo();
4418 }
4419 else
4420 {
4421 pCdInfo->SetNoCDDBInfo();
4422 // ..no, an error occurred, display it to the user
4423 std::string strErrorText = StringUtils::Format("[%d] %s", cddb.getLastError(), cddb.getLastErrorText());
4424 HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)}, CVariant{0});
4425 }
4426 } // if ( !cddb.queryCDinfo( pCdInfo ) )
4427 else
4428 pDialogProgress->Close();
4429 }
4430
4431 // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
4432
4433 return pCdInfo->HasCDDBInfo();
4434 #else
4435 return false;
4436 #endif
4437 }
4438
DeleteCDDBInfo()4439 void CMusicDatabase::DeleteCDDBInfo()
4440 {
4441 #ifdef HAS_DVD_DRIVE
4442 CFileItemList items;
4443 if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb", DIR_FLAG_NO_FILE_DIRS))
4444 {
4445 HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426});
4446 return ;
4447 }
4448 // Show a selectdialog that the user can select the album to delete
4449 CGUIDialogSelect *pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
4450 if (pDlg)
4451 {
4452 pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
4453 pDlg->Reset();
4454
4455 std::map<uint32_t, std::string> mapCDDBIds;
4456 for (int i = 0; i < items.Size(); ++i)
4457 {
4458 if (items[i]->m_bIsFolder)
4459 continue;
4460
4461 std::string strFile = URIUtils::GetFileName(items[i]->GetPath());
4462 strFile.erase(strFile.size() - 5, 5);
4463 uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16);
4464 Xcddb cddb;
4465 cddb.setCacheDir(m_profileManager.GetCDDBFolder());
4466
4467 if (!cddb.queryCache(lDiscId))
4468 continue;
4469
4470 std::string strDiskTitle, strDiskArtist;
4471 cddb.getDiskTitle(strDiskTitle);
4472 cddb.getDiskArtist(strDiskArtist);
4473
4474 std::string str;
4475 if (strDiskArtist.empty())
4476 str = strDiskTitle;
4477 else
4478 str = strDiskTitle + " - " + strDiskArtist;
4479
4480 pDlg->Add(str);
4481 mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str));
4482 }
4483
4484 pDlg->Sort();
4485 pDlg->Open();
4486
4487 // and wait till user selects one
4488 int iSelectedAlbum = pDlg->GetSelectedItem();
4489 if (iSelectedAlbum < 0)
4490 {
4491 mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4492 return ;
4493 }
4494
4495 std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel();
4496 for (const auto &i : mapCDDBIds)
4497 {
4498 if (i.second == strSelectedAlbum)
4499 {
4500 std::string strFile = StringUtils::Format("%x.cddb", (unsigned int) i.first);
4501 CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
4502 break;
4503 }
4504 }
4505 mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
4506 }
4507 #endif
4508 }
4509
Clean()4510 void CMusicDatabase::Clean()
4511 {
4512 // If we are scanning for music info in the background,
4513 // other writing access to the database is prohibited.
4514 if (g_application.IsMusicScanning())
4515 {
4516 HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057});
4517 return;
4518 }
4519
4520 if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::YES)
4521 {
4522 CMusicDatabase musicdatabase;
4523 if (musicdatabase.Open())
4524 {
4525 int iReturnString = musicdatabase.Cleanup();
4526 musicdatabase.Close();
4527
4528 if (iReturnString != ERROR_OK)
4529 {
4530 HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString});
4531 }
4532 }
4533 }
4534 }
4535
GetGenresNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)4536 bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir, CFileItemList& items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
4537 {
4538 try
4539 {
4540 if (nullptr == m_pDB)
4541 return false;
4542 if (nullptr == m_pDS)
4543 return false;
4544
4545 // get primary genres for songs - could be simplified to just SELECT * FROM genre?
4546 std::string strSQL = "SELECT %s FROM genre ";
4547
4548 Filter extFilter = filter;
4549 CMusicDbUrl musicUrl;
4550 SortDescription sorting;
4551 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4552 return false;
4553
4554 // if there are extra WHERE conditions we might need access
4555 // to songview or albumview for these conditions
4556 if (!extFilter.where.empty())
4557 {
4558 if (extFilter.where.find("artistview") != std::string::npos)
4559 {
4560 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4561 extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4562 extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
4563 extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
4564 }
4565 else if (extFilter.where.find("songview") != std::string::npos)
4566 {
4567 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4568 extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
4569 }
4570 else if (extFilter.where.find("albumview") != std::string::npos)
4571 {
4572 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
4573 extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
4574 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
4575 }
4576 extFilter.AppendGroup("genre.idGenre");
4577 }
4578 extFilter.AppendWhere("genre.strGenre != ''");
4579
4580 if (countOnly)
4581 {
4582 extFilter.fields = "COUNT(DISTINCT genre.idGenre)";
4583 extFilter.group.clear();
4584 extFilter.order.clear();
4585 }
4586
4587 std::string strSQLExtra;
4588 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
4589 return false;
4590
4591 strSQL = PrepareSQL(strSQL.c_str(), !extFilter.fields.empty() && extFilter.fields.compare("*") != 0 ? extFilter.fields.c_str() : "genre.*") + strSQLExtra;
4592
4593 // run query
4594 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4595
4596 if (!m_pDS->query(strSQL))
4597 return false;
4598 int iRowsFound = m_pDS->num_rows();
4599 if (iRowsFound == 0)
4600 {
4601 m_pDS->close();
4602 return true;
4603 }
4604
4605 if (countOnly)
4606 {
4607 CFileItemPtr pItem(new CFileItem());
4608 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4609 items.Add(pItem);
4610
4611 m_pDS->close();
4612 return true;
4613 }
4614
4615 // get data from returned rows
4616 while (!m_pDS->eof())
4617 {
4618 CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString()));
4619 pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString());
4620 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre");
4621
4622 CMusicDbUrl itemUrl = musicUrl;
4623 std::string strDir = StringUtils::Format("%i/", m_pDS->fv("genre.idGenre").get_asInt());
4624 itemUrl.AppendPath(strDir);
4625 pItem->SetPath(itemUrl.ToString());
4626
4627 pItem->m_bIsFolder = true;
4628 items.Add(pItem);
4629
4630 m_pDS->next();
4631 }
4632
4633 // cleanup
4634 m_pDS->close();
4635
4636 return true;
4637 }
4638 catch (...)
4639 {
4640 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4641 }
4642 return false;
4643 }
4644
GetSourcesNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)4645 bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir, CFileItemList& items, const Filter &filter /*= Filter()*/, bool countOnly /*= false*/)
4646 {
4647 try
4648 {
4649 if (nullptr == m_pDB)
4650 return false;
4651 if (nullptr == m_pDS)
4652 return false;
4653
4654 // Get sources for selection list when add/edit filter or smartplaylist rule
4655 std::string strSQL = "SELECT %s FROM source ";
4656
4657 Filter extFilter = filter;
4658 CMusicDbUrl musicUrl;
4659 SortDescription sorting;
4660 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4661 return false;
4662
4663 // if there are extra WHERE conditions we might need access
4664 // to songview or albumview for these conditions
4665 if (!extFilter.where.empty())
4666 {
4667 if (extFilter.where.find("artistview") != std::string::npos)
4668 {
4669 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4670 extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
4671 extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
4672 }
4673 else if (extFilter.where.find("songview") != std::string::npos)
4674 {
4675 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4676 extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
4677 }
4678 else if (extFilter.where.find("albumview") != std::string::npos)
4679 {
4680 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4681 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
4682 }
4683 extFilter.AppendGroup("source.idSource");
4684 }
4685 else
4686 { // Get only sources that have been scanned into music library
4687 extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
4688 extFilter.AppendGroup("source.idSource");
4689 }
4690
4691 if (countOnly)
4692 {
4693 extFilter.fields = "COUNT(DISTINCT source.idSource)";
4694 extFilter.group.clear();
4695 extFilter.order.clear();
4696 }
4697
4698 std::string strSQLExtra;
4699 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
4700 return false;
4701
4702 strSQL = PrepareSQL(strSQL.c_str(), !extFilter.fields.empty() && extFilter.fields.compare("*") != 0 ? extFilter.fields.c_str() : "source.*") + strSQLExtra;
4703
4704 // run query
4705 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4706
4707 if (!m_pDS->query(strSQL))
4708 return false;
4709 int iRowsFound = m_pDS->num_rows();
4710 if (iRowsFound == 0)
4711 {
4712 m_pDS->close();
4713 return true;
4714 }
4715
4716 if (countOnly)
4717 {
4718 CFileItemPtr pItem(new CFileItem());
4719 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4720 items.Add(pItem);
4721
4722 m_pDS->close();
4723 return true;
4724 }
4725
4726 // get data from returned rows
4727 while (!m_pDS->eof())
4728 {
4729 CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
4730 pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString());
4731 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source");
4732
4733 CMusicDbUrl itemUrl = musicUrl;
4734 std::string strDir = StringUtils::Format("%i/", m_pDS->fv("source.idSource").get_asInt());
4735 itemUrl.AppendPath(strDir);
4736 itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt());
4737 pItem->SetPath(itemUrl.ToString());
4738
4739 pItem->m_bIsFolder = true;
4740 items.Add(pItem);
4741
4742 m_pDS->next();
4743 }
4744
4745 // cleanup
4746 m_pDS->close();
4747
4748 return true;
4749 }
4750 catch (...)
4751 {
4752 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4753 }
4754 return false;
4755 }
4756
GetYearsNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter)4757 bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir, CFileItemList& items, const Filter& filter /* = Filter() */)
4758 {
4759 try
4760 {
4761 if (nullptr == m_pDB)
4762 return false;
4763 if (nullptr == m_pDS)
4764 return false;
4765
4766 Filter extFilter = filter;
4767 CMusicDbUrl musicUrl;
4768 SortDescription sorting;
4769 std::string strSQL;
4770 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4771 return false;
4772
4773 bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
4774 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
4775
4776 useOriginalYears =
4777 useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/");
4778
4779 if (!useOriginalYears)
4780 { // Get years from year part of release date
4781 strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
4782 extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
4783 }
4784 else
4785 { // Get years from year part of original date
4786 strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
4787 extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
4788 }
4789 if (!BuildSQL(strSQL, extFilter, strSQL))
4790 return false;
4791
4792 // run query
4793 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4794 if (!m_pDS->query(strSQL))
4795 return false;
4796 int iRowsFound = m_pDS->num_rows();
4797 if (iRowsFound == 0)
4798 {
4799 m_pDS->close();
4800 return true;
4801 }
4802
4803 // get data from returned rows
4804 while (!m_pDS->eof())
4805 {
4806 CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
4807 pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt());
4808 if (useOriginalYears)
4809 pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
4810 else
4811 pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year");
4812
4813 CMusicDbUrl itemUrl = musicUrl;
4814 std::string strDir = StringUtils::Format("%i/", m_pDS->fv(0).get_asInt());
4815 itemUrl.AppendPath(strDir);
4816 if (useOriginalYears)
4817 itemUrl.AddOption("useoriginalyear", true);
4818 pItem->SetPath(itemUrl.ToString());
4819
4820 pItem->m_bIsFolder = true;
4821 items.Add(pItem);
4822
4823 m_pDS->next();
4824 }
4825
4826 // cleanup
4827 m_pDS->close();
4828
4829 return true;
4830 }
4831 catch (...)
4832 {
4833 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4834 }
4835 return false;
4836 }
4837
GetRolesNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter)4838 bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir, CFileItemList& items, const Filter &filter /* = Filter() */)
4839 {
4840 try
4841 {
4842 if (nullptr == m_pDB)
4843 return false;
4844 if (nullptr == m_pDS)
4845 return false;
4846
4847 Filter extFilter = filter;
4848 CMusicDbUrl musicUrl;
4849 SortDescription sorting;
4850 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
4851 return false;
4852
4853 // get roles with artists having that role
4854 std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role "
4855 "JOIN song_artist ON song_artist.idRole = role.idRole ";
4856
4857 if (!BuildSQL(strSQL, extFilter, strSQL))
4858 return false;
4859
4860 // run query
4861 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4862 if (!m_pDS->query(strSQL))
4863 return false;
4864 int iRowsFound = m_pDS->num_rows();
4865 if (iRowsFound == 0)
4866 {
4867 m_pDS->close();
4868 return true;
4869 }
4870
4871 // get data from returned rows
4872 while (!m_pDS->eof())
4873 {
4874 std::string labelValue = m_pDS->fv("role.strRole").get_asString();
4875 CFileItemPtr pItem(new CFileItem(labelValue));
4876 pItem->GetMusicInfoTag()->SetTitle(labelValue);
4877 pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role");
4878 CMusicDbUrl itemUrl = musicUrl;
4879 std::string strDir = StringUtils::Format("%i/", m_pDS->fv("role.idRole").get_asInt());
4880 itemUrl.AppendPath(strDir);
4881 itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt());
4882 pItem->SetPath(itemUrl.ToString());
4883
4884 pItem->m_bIsFolder = true;
4885 items.Add(pItem);
4886
4887 m_pDS->next();
4888 }
4889
4890 // cleanup
4891 m_pDS->close();
4892
4893 return true;
4894 }
4895 catch (...)
4896 {
4897 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4898 }
4899 return false;
4900 }
4901
GetAlbumsByYear(const std::string & strBaseDir,CFileItemList & items,int year)4902 bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year)
4903 {
4904 CMusicDbUrl musicUrl;
4905 if (!musicUrl.FromString(strBaseDir))
4906 return false;
4907
4908 musicUrl.AddOption("year", year);
4909 musicUrl.AddOption("show_singles", true); // allow singles to be listed
4910
4911 Filter filter;
4912 return GetAlbumsByWhere(musicUrl.ToString(), filter, items);
4913 }
4914
GetCommonNav(const std::string & strBaseDir,const std::string & table,const std::string & labelField,CFileItemList & items,const Filter & filter,bool countOnly)4915 bool CMusicDatabase::GetCommonNav(const std::string &strBaseDir, const std::string &table, const std::string &labelField, CFileItemList &items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
4916 {
4917 if (nullptr == m_pDB)
4918 return false;
4919 if (nullptr == m_pDS)
4920 return false;
4921
4922 if (table.empty() || labelField.empty())
4923 return false;
4924
4925 try
4926 {
4927 Filter extFilter = filter;
4928 std::string strSQL = "SELECT %s FROM " + table + " ";
4929 extFilter.AppendGroup(labelField);
4930 extFilter.AppendWhere(labelField + " != ''");
4931
4932 if (countOnly)
4933 {
4934 extFilter.fields = "COUNT(DISTINCT " + labelField + ")";
4935 extFilter.group.clear();
4936 extFilter.order.clear();
4937 }
4938
4939 // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
4940 // e.g. LIKE '%symphony%' would be taken as a %s format argument
4941 strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str());
4942
4943 CMusicDbUrl musicUrl;
4944 if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl))
4945 return false;
4946
4947 // run query
4948 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
4949 if (!m_pDS->query(strSQL))
4950 return false;
4951
4952 int iRowsFound = m_pDS->num_rows();
4953 if (iRowsFound <= 0)
4954 {
4955 m_pDS->close();
4956 return false;
4957 }
4958
4959 if (countOnly)
4960 {
4961 CFileItemPtr pItem(new CFileItem());
4962 pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
4963 items.Add(pItem);
4964
4965 m_pDS->close();
4966 return true;
4967 }
4968
4969 // get data from returned rows
4970 while (!m_pDS->eof())
4971 {
4972 std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString();
4973 CFileItemPtr pItem(new CFileItem(labelValue));
4974
4975 CMusicDbUrl itemUrl = musicUrl;
4976 std::string strDir = StringUtils::Format("%s/", labelValue.c_str());
4977 itemUrl.AppendPath(strDir);
4978 pItem->SetPath(itemUrl.ToString());
4979
4980 pItem->m_bIsFolder = true;
4981 items.Add(pItem);
4982
4983 m_pDS->next();
4984 }
4985
4986 // cleanup
4987 m_pDS->close();
4988
4989 return true;
4990 }
4991 catch (...)
4992 {
4993 m_pDS->close();
4994 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
4995 }
4996
4997 return false;
4998 }
4999
GetAlbumTypesNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)5000 bool CMusicDatabase::GetAlbumTypesNav(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
5001 {
5002 return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly);
5003 }
5004
GetMusicLabelsNav(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,bool countOnly)5005 bool CMusicDatabase::GetMusicLabelsNav(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
5006 {
5007 return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly);
5008 }
5009
GetArtistsNav(const std::string & strBaseDir,CFileItemList & items,bool albumArtistsOnly,int idGenre,int idAlbum,int idSong,const Filter & filter,const SortDescription & sortDescription,bool countOnly)5010 bool CMusicDatabase::GetArtistsNav(const std::string& strBaseDir, CFileItemList& items, bool albumArtistsOnly /* = false */, int idGenre /* = -1 */, int idAlbum /* = -1 */, int idSong /* = -1 */, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */, bool countOnly /* = false */)
5011 {
5012 if (nullptr == m_pDB)
5013 return false;
5014 if (nullptr == m_pDS)
5015 return false;
5016 try
5017 {
5018 CMusicDbUrl musicUrl;
5019 if (!musicUrl.FromString(strBaseDir))
5020 return false;
5021
5022 if (idGenre > 0)
5023 musicUrl.AddOption("genreid", idGenre);
5024 else if (idAlbum > 0)
5025 musicUrl.AddOption("albumid", idAlbum);
5026 else if (idSong > 0)
5027 musicUrl.AddOption("songid", idSong);
5028
5029 // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
5030 // when local option already present in music URL thus allowing it to be an option in custom nodes
5031 if (!musicUrl.HasOption("albumartistsonly"))
5032 musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
5033
5034 bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5035
5036 return result;
5037 }
5038 catch (...)
5039 {
5040 m_pDS->close();
5041 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
5042 }
5043 return false;
5044 }
5045
GetArtistsByWhere(const std::string & strBaseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5046 bool CMusicDatabase::GetArtistsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription /* = SortDescription() */, bool countOnly /* = false */)
5047 {
5048 if (nullptr == m_pDB)
5049 return false;
5050 if (nullptr == m_pDS)
5051 return false;
5052
5053 try
5054 {
5055 unsigned int querytime = 0;
5056 unsigned int time = XbmcThreads::SystemClockMillis();
5057 int total = -1;
5058
5059 Filter extFilter = filter;
5060 CMusicDbUrl musicUrl;
5061 SortDescription sorting = sortDescription;
5062 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
5063 return false;
5064
5065 bool extended = false;
5066 bool limitedInSQL =
5067 extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5068
5069 // if there are extra WHERE conditions (from media filter dialog) we might
5070 // need access to songview or albumview for these conditions
5071 if (!extFilter.where.empty())
5072 {
5073 if (extFilter.where.find("songview") != std::string::npos)
5074 {
5075 extended = true;
5076 extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist JOIN songview ON songview.idSong = song_artist.idSong");
5077 }
5078 else if (extFilter.where.find("albumview") != std::string::npos)
5079 {
5080 extended = true;
5081 extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
5082 }
5083 if (extended)
5084 extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
5085 }
5086
5087 std::string strSQLExtra;
5088 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5089 return false;
5090
5091 // Count number of artsits that satisfy selection criteria (no limit built)
5092 // Count done in full query fetch when unlimited
5093 if (countOnly || limitedInSQL)
5094 {
5095 if (extended)
5096 {
5097 // Count distinct without group by
5098 Filter countFilter = extFilter;
5099 countFilter.group.clear();
5100 std::string strSQLWhere;
5101 if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5102 return false;
5103 total = GetSingleValueInt(
5104 "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS);
5105 }
5106 else
5107 total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS);
5108 }
5109 if (countOnly)
5110 {
5111 CFileItemPtr pItem(new CFileItem());
5112 pItem->SetProperty("total", total);
5113 items.Add(pItem);
5114
5115 m_pDS->close();
5116 return true;
5117 }
5118
5119 // Apply any limiting directly in SQL and so sort as well
5120 if (limitedInSQL)
5121 {
5122 extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5123 }
5124
5125 // Apply sort in SQL
5126 const std::shared_ptr<CSettings> settings =
5127 CServiceBroker::GetSettingsComponent()->GetSettings();
5128 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5129 sorting.sortAttributes =
5130 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5131 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5132 GetOrderFilter(MediaTypeArtist, sorting, extFilter);
5133
5134 strSQLExtra.clear();
5135 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5136 return false;
5137
5138 std::string strSQL;
5139 std::string strFields = "artistview.*";
5140 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5141 strFields = "artistview.*, " + extFilter.fields;
5142 strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra;
5143
5144 // run query
5145 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
5146 querytime = XbmcThreads::SystemClockMillis();
5147 if (!m_pDS->query(strSQL))
5148 return false;
5149 int iRowsFound = m_pDS->num_rows();
5150 if (iRowsFound == 0)
5151 {
5152 m_pDS->close();
5153 return true;
5154 }
5155 querytime = XbmcThreads::SystemClockMillis() - querytime;
5156
5157 // Store the total number of artists as a property
5158 if (total < iRowsFound)
5159 total = iRowsFound;
5160 items.SetProperty("total", total);
5161
5162 DatabaseResults results;
5163 results.reserve(iRowsFound);
5164 // Populate results field vector from dataset
5165 FieldList fields;
5166 if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results))
5167 return false;
5168 // Store item list sort order
5169 items.SetSortMethod(sortDescription.sortBy);
5170 items.SetSortOrder(sortDescription.sortOrder);
5171
5172 // Get Artists from returned rows
5173 items.Reserve(results.size());
5174 const dbiplus::query_data &data = m_pDS->get_result_set().records;
5175 for (const auto &i : results)
5176 {
5177 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5178 const dbiplus::sql_record* const record = data.at(targetRow);
5179
5180 try
5181 {
5182 CArtist artist = GetArtistFromDataset(record, false);
5183 CFileItemPtr pItem(new CFileItem(artist));
5184
5185 CMusicDbUrl itemUrl = musicUrl;
5186 std::string path = StringUtils::Format("%ld/", artist.idArtist);
5187 itemUrl.AppendPath(path);
5188 pItem->SetPath(itemUrl.ToString());
5189
5190 pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist);
5191 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5192 pItem->SetProperty("icon_never_overlay", true);
5193 pItem->SetArt("icon", "DefaultArtist.png");
5194
5195 SetPropertiesFromArtist(*pItem, artist);
5196 items.Add(pItem);
5197 }
5198 catch (...)
5199 {
5200 m_pDS->close();
5201 CLog::Log(LOGERROR, "%s - out of memory getting listing (got %i)", __FUNCTION__, items.Size());
5202 }
5203 }
5204 // cleanup
5205 m_pDS->close();
5206
5207 CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1}ms query took {2}ms",
5208 __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
5209 return true;
5210 }
5211 catch (...)
5212 {
5213 m_pDS->close();
5214 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
5215 }
5216 return false;
5217 }
5218
GetAlbumFromSong(int idSong,CAlbum & album)5219 bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum &album)
5220 {
5221 try
5222 {
5223 if (nullptr == m_pDB)
5224 return false;
5225 if (nullptr == m_pDS)
5226 return false;
5227
5228 std::string strSQL = PrepareSQL("select albumview.* from song join albumview on song.idAlbum = albumview.idAlbum where song.idSong='%i'", idSong);
5229 if (!m_pDS->query(strSQL)) return false;
5230 int iRowsFound = m_pDS->num_rows();
5231 if (iRowsFound != 1)
5232 {
5233 m_pDS->close();
5234 return false;
5235 }
5236
5237 album = GetAlbumFromDataset(m_pDS.get());
5238
5239 m_pDS->close();
5240 return true;
5241
5242 }
5243 catch (...)
5244 {
5245 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
5246 }
5247 return false;
5248 }
5249
GetAlbumsNav(const std::string & strBaseDir,CFileItemList & items,int idGenre,int idArtist,const Filter & filter,const SortDescription & sortDescription,bool countOnly)5250 bool CMusicDatabase::GetAlbumsNav(const std::string& strBaseDir, CFileItemList& items, int idGenre /* = -1 */, int idArtist /* = -1 */, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */, bool countOnly /* = false */)
5251 {
5252 CMusicDbUrl musicUrl;
5253 if (!musicUrl.FromString(strBaseDir))
5254 return false;
5255
5256 // where clause
5257 if (idGenre > 0)
5258 musicUrl.AddOption("genreid", idGenre);
5259
5260 if (idArtist > 0)
5261 musicUrl.AddOption("artistid", idArtist);
5262
5263 return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
5264 }
5265
GetAlbumsByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5266 bool CMusicDatabase::GetAlbumsByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, const SortDescription &sortDescription /* = SortDescription() */, bool countOnly /* = false */)
5267 {
5268 if (m_pDB == nullptr || m_pDS == nullptr)
5269 return false;
5270
5271 try
5272 {
5273 unsigned int querytime = 0;
5274 unsigned int time = XbmcThreads::SystemClockMillis();
5275 int total = -1;
5276
5277 Filter extFilter = filter;
5278 CMusicDbUrl musicUrl;
5279 SortDescription sorting = sortDescription;
5280 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5281 return false;
5282
5283 bool extended = false;
5284 bool limitedInSQL =
5285 extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
5286
5287 // If there are extra WHERE conditions (from media filter dialog) we might
5288 // need access to songview for these conditions
5289 if (extFilter.where.find("songview") != std::string::npos)
5290 {
5291 extended = true;
5292 extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
5293 extFilter.AppendGroup("albumview.idAlbum");
5294 }
5295
5296 std::string strSQLExtra;
5297 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5298 return false;
5299
5300 // Count number of albums that satisfy selection criteria (no limit built)
5301 // Count done in full query fetch when unlimited
5302 if (countOnly || limitedInSQL)
5303 {
5304 if (extended)
5305 {
5306 // Count distinct without group by
5307 Filter countFilter = extFilter;
5308 countFilter.group.clear();
5309 std::string strSQLWhere;
5310 if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
5311 return false;
5312 total = GetSingleValueInt(
5313 "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS);
5314 }
5315 else
5316 total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
5317 }
5318 if (countOnly)
5319 {
5320 CFileItemPtr pItem(new CFileItem());
5321 pItem->SetProperty("total", total);
5322 items.Add(pItem);
5323
5324 m_pDS->close();
5325 return true;
5326 }
5327
5328 // Apply any limiting directly in SQL
5329 if (limitedInSQL)
5330 {
5331 extFilter.limit =
5332 DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5333 }
5334
5335 // Apply sort in SQL
5336 const std::shared_ptr<CSettings> settings =
5337 CServiceBroker::GetSettingsComponent()->GetSettings();
5338 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5339 sorting.sortAttributes =
5340 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5341 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5342 GetOrderFilter(MediaTypeAlbum, sorting, extFilter);
5343 // Modify order to use correct calculated year field
5344 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5345 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
5346 StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
5347 else
5348 StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5349
5350 strSQLExtra.clear();
5351 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5352 return false;
5353
5354 std::string strSQL;
5355 std::string strFields = "albumview.*";
5356 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5357 strFields = "albumview.*, " + extFilter.fields;
5358 strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra;
5359
5360 // run query
5361 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
5362 querytime = XbmcThreads::SystemClockMillis();
5363 if (!m_pDS->query(strSQL))
5364 return false;
5365 int iRowsFound = m_pDS->num_rows();
5366 if (iRowsFound == 0)
5367 {
5368 m_pDS->close();
5369 return true;
5370 }
5371 querytime = XbmcThreads::SystemClockMillis() - querytime;
5372
5373 // Store the total number of albums as a property
5374 if (total < iRowsFound)
5375 total = iRowsFound;
5376 items.SetProperty("total", total);
5377
5378 DatabaseResults results;
5379 results.reserve(iRowsFound);
5380 // Populate results field vector from dataset
5381 FieldList fields;
5382 if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results))
5383 return false;
5384 // Store item list sort order
5385 items.SetSortMethod(sorting.sortBy);
5386 items.SetSortOrder(sorting.sortOrder);
5387
5388 // Get albums from returned rows
5389 items.Reserve(results.size());
5390 const dbiplus::query_data &data = m_pDS->get_result_set().records;
5391 for (const auto &i : results)
5392 {
5393 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5394 const dbiplus::sql_record* const record = data.at(targetRow);
5395
5396 try
5397 {
5398 CMusicDbUrl itemUrl = musicUrl;
5399 std::string path = StringUtils::Format("%i/", record->at(album_idAlbum).get_asInt());
5400 itemUrl.AppendPath(path);
5401
5402 CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record)));
5403 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5404 pItem->SetProperty("icon_never_overlay", true);
5405 pItem->SetArt("icon", "DefaultAlbumCover.png");
5406 items.Add(pItem);
5407 }
5408 catch (...)
5409 {
5410 m_pDS->close();
5411 CLog::Log(LOGERROR, "%s - out of memory getting listing (got %i)", __FUNCTION__, items.Size());
5412 }
5413 }
5414 // cleanup
5415 m_pDS->close();
5416
5417 CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms",
5418 __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
5419 return true;
5420 }
5421 catch (...)
5422 {
5423 m_pDS->close();
5424 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, filter.where.c_str());
5425 }
5426 return false;
5427 }
5428
GetDiscsNav(const std::string & strBaseDir,CFileItemList & items,int idAlbum,const Filter & filter,const SortDescription & sortDescription,bool countOnly)5429 bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir, CFileItemList& items, int idAlbum,
5430 const Filter& filter, const SortDescription& sortDescription, bool countOnly)
5431 {
5432 CMusicDbUrl musicUrl;
5433 if (!musicUrl.FromString(strBaseDir))
5434 return false;
5435
5436 if (idAlbum > 0)
5437 musicUrl.AddOption("albumid", idAlbum);
5438
5439 return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5440 }
5441
GetDiscsByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5442 bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir,
5443 const Filter& filter,
5444 CFileItemList& items,
5445 const SortDescription& sortDescription,
5446 bool countOnly)
5447 {
5448 CMusicDbUrl musicUrl;
5449 if (!musicUrl.FromString(baseDir))
5450 return false;
5451 return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
5452 }
5453
GetDiscsByWhere(CMusicDbUrl & musicUrl,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool countOnly)5454 bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl,
5455 const Filter& filter,
5456 CFileItemList& items,
5457 const SortDescription& sortDescription,
5458 bool countOnly)
5459 {
5460 if (m_pDB == nullptr || m_pDS == nullptr)
5461 return false;
5462
5463 try
5464 {
5465 unsigned int querytime = 0;
5466 unsigned int time = XbmcThreads::SystemClockMillis();
5467 int total = -1;
5468 std::string strSQL;
5469
5470 Filter extFilter = filter;
5471 SortDescription sorting = sortDescription;
5472
5473 if (!GetFilter(musicUrl, extFilter, sorting))
5474 return false;
5475
5476 extFilter.AppendGroup("albumview.idAlbum, iDisc");
5477
5478 // If there are extra songview WHERE conditions adjust to song or albumview
5479 // fields, and join Path table for strPath
5480 // ! @todo: convert songview fields into to song or albumview fields
5481 // But not sure we ever get songview fields in filter - REMOVE??
5482 if (extFilter.where.find("songview.strPath") != std::string::npos)
5483 {
5484 extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
5485 }
5486
5487 std::string strSQLExtra;
5488 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5489 return false;
5490
5491 // Apply any limiting directly in SQL if there is either no special sorting or random sort
5492 // When limited, random sort is also applied in SQL
5493 bool limitedInSQL = extFilter.limit.empty() &&
5494 (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) &&
5495 (sorting.limitStart > 0 || sorting.limitEnd > 0);
5496
5497 if (countOnly || limitedInSQL)
5498 {
5499 // Count number of discs that satisfy selection criteria
5500 // (when fetching all records get total from row count of results dataset)
5501 // Count not allow for same non-null title discs to be grouped together
5502 strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
5503 "albumview.idAlbum " +
5504 strSQLExtra;
5505 strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc ";
5506 total = GetSingleValueInt(strSQL, m_pDS);
5507 }
5508 if (countOnly)
5509 {
5510 items.SetProperty("total", total);
5511 return true;
5512 }
5513 // Apply limits and random sort order directly in SQL
5514 if (limitedInSQL)
5515 {
5516 if (sorting.sortBy == SortByRandom)
5517 strSQLExtra += PrepareSQL(" ORDER BY RANDOM()");
5518 strSQLExtra +=
5519 DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
5520 }
5521 else
5522 strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
5523
5524 strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
5525 "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " + strSQLExtra;
5526
5527 // run query
5528 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
5529 querytime = XbmcThreads::SystemClockMillis();
5530 if (!m_pDS->query(strSQL))
5531 return false;
5532 int iRowsFound = m_pDS->num_rows();
5533 if (iRowsFound == 0)
5534 {
5535 m_pDS->close();
5536 return true;
5537 }
5538 querytime = XbmcThreads::SystemClockMillis() - querytime;
5539
5540 // store the total value of items as a property
5541 if (total < iRowsFound)
5542 total = iRowsFound;
5543 items.SetProperty("total", total);
5544
5545 DatabaseResults results;
5546 results.reserve(iRowsFound);
5547
5548 // Avoid sorting with limits, just fetch results from dataset
5549 // Limit when SortByNone already applied in SQL,
5550 // Need guaranteed ordering for dataset processing to group by disc title
5551 // so apply sort later to fileitems list rather than dataset
5552 if (sorting.sortBy != SortByNone)
5553 sorting.sortBy = SortByNone;
5554 if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results))
5555 return false;
5556
5557 // Get data from returned rows, note possibly multiple albums although usually only one
5558 items.Reserve(total);
5559 int albumOffset = 2;
5560 CAlbum album;
5561 bool useTitle = true; // Assume we want to match by disc title later unless we have no titles
5562 std::string oldDiscTitle;
5563 const dbiplus::query_data& data = m_pDS->get_result_set().records;
5564 for (const auto& i : results)
5565 {
5566 unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger());
5567 const dbiplus::sql_record* const record = data.at(targetRow);
5568 try
5569 {
5570 if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt())
5571 { // New album
5572 useTitle = true;
5573 album = GetAlbumFromDataset(record, albumOffset);
5574 }
5575
5576 int discnum = record->at(0).get_asInt();
5577 std::string strDiscSubtitle = record->at(1).get_asString();
5578 if (strDiscSubtitle.empty())
5579 { // Make (fake) disc title from disc number, group by disc number as no real title to match
5580 strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum);
5581 useTitle = false;
5582 }
5583 else if (oldDiscTitle == strDiscSubtitle)
5584 { // disc title already added to list, fetch the next disc
5585 continue;
5586 }
5587 oldDiscTitle = strDiscSubtitle;
5588
5589 CMusicDbUrl itemUrl = musicUrl;
5590 std::string path = StringUtils::Format("%i/", discnum);
5591 itemUrl.AppendPath(path);
5592
5593 // When disc titles are provided group discs together by title not number.
5594 // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
5595 if (useTitle)
5596 itemUrl.AddOption("disctitle", strDiscSubtitle.c_str());
5597 else
5598 itemUrl.AddOption("discid", discnum);
5599 CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album));
5600 pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order??
5601 pItem->GetMusicInfoTag()->SetDiscNumber(discnum);
5602 pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle);
5603 pItem->SetLabel(strDiscSubtitle);
5604 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5605 pItem->SetProperty("icon_never_overlay", true);
5606 pItem->SetArt("icon", "DefaultAlbumCover.png");
5607 items.Add(pItem);
5608 }
5609 catch (...)
5610 {
5611 m_pDS->close();
5612 CLog::Log(LOGERROR, "%s - out of memory getting listing (got %i)", __FUNCTION__,
5613 items.Size());
5614 }
5615 }
5616
5617 // cleanup
5618 m_pDS->close();
5619
5620 // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
5621 // that is when have join with songartistview and sorting other than random with limit
5622 if (sorting.sortBy != SortByNone &&
5623 !(limitedInSQL && sorting.sortBy == SortByRandom))
5624 items.Sort(sorting);
5625
5626 CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms",
5627 __FUNCTION__, XbmcThreads::SystemClockMillis() - time, querytime);
5628 return true;
5629 }
5630 catch (...)
5631 {
5632 m_pDS->close();
5633 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, filter.where.c_str());
5634 }
5635
5636 return false;
5637 }
GetDiscsCount(const std::string & baseDir,const Filter & filter)5638 int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */)
5639 {
5640 int iDiscTotal = -1;
5641 CFileItemList itemscount;
5642 if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true))
5643 iDiscTotal = itemscount.GetProperty("total").asInteger32();
5644 return iDiscTotal;
5645 }
5646
GetSongsFullByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription,bool artistData)5647 bool CMusicDatabase::GetSongsFullByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, const SortDescription &sortDescription /* = SortDescription() */, bool artistData /* = false*/)
5648 {
5649 if (m_pDB == nullptr || m_pDS == nullptr)
5650 return false;
5651
5652 try
5653 {
5654 unsigned int querytime = 0;
5655 unsigned int time = XbmcThreads::SystemClockMillis();
5656 int total = -1;
5657
5658 Filter extFilter = filter;
5659 CMusicDbUrl musicUrl;
5660 SortDescription sorting = sortDescription;
5661 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5662 return false;
5663
5664 bool extended = false;
5665 bool limitedInSQL =
5666 extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0);
5667
5668 // If there are extra WHERE conditions (from media filter dialog) we might
5669 // need access to albumview for these conditions
5670 if (extFilter.where.find("albumview") != std::string::npos)
5671 {
5672 extended = true;
5673 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
5674 }
5675
5676 // Build songview <where> for count
5677 std::string strSQLExtra;
5678 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5679 return false;
5680
5681 // Count (without group by) number of songs that satisfy selection criteria
5682 // Much quicker to use song table, not songview, when filtering only on song fields
5683 if (extended ||
5684 (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos ||
5685 extFilter.where.find("strPath") != std::string::npos ||
5686 extFilter.where.find("bCompilation") != std::string::npos ||
5687 extFilter.where.find("bBoxedset") != std::string::npos)))
5688 total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS);
5689 else
5690 {
5691 std::string strSQLsong = strSQLExtra;
5692 StringUtils::Replace(strSQLsong, "songview", "song");
5693 total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS);
5694 }
5695
5696 if (extended)
5697 extFilter.AppendGroup("songview.idSong");
5698
5699 // Apply any limiting directly in SQL
5700 if (limitedInSQL)
5701 {
5702 extFilter.limit =
5703 DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
5704 }
5705
5706 // Apply sort in SQL
5707 const std::shared_ptr<CSettings> settings =
5708 CServiceBroker::GetSettingsComponent()->GetSettings();
5709 if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
5710 sorting.sortAttributes =
5711 static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
5712 // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
5713 GetOrderFilter(MediaTypeSong, sorting, extFilter);
5714 // Modify order to use correct calculated year field
5715 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
5716 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
5717 StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
5718 else
5719 StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
5720
5721 std::string strFields = "songview.*";
5722 if (!artistData || limitedInSQL)
5723 {
5724 // Build songview <where> + <order by> + <limits>
5725 strSQLExtra.clear();
5726 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5727 return false;
5728 }
5729 else
5730 strFields = "songview.*, songartistview.*";
5731 if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
5732 strFields = strFields + ", " + extFilter.fields;
5733
5734 std::string strSQL;
5735 if (artistData)
5736 { // Get data from song and song_artist tables to fully populate songs with artists
5737 // All songs now have at least one artist so inner join sufficient
5738 // Build songartistview JOIN part of query
5739 Filter joinFilter;
5740 std::string strSQLJoin;
5741 joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
5742 if (sortDescription.sortBy == SortByRandom)
5743 joinFilter.AppendOrder("songartistview.idSong");
5744 else
5745 joinFilter.order = extFilter.order;
5746 if (limitedInSQL)
5747 {
5748 StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong");
5749 StringUtils::Replace(joinFilter.order, "songview.", "sv.");
5750 }
5751 else
5752 joinFilter.where = extFilter.where;
5753 joinFilter.AppendOrder("songartistview.idRole");
5754 joinFilter.AppendOrder("songartistview.iOrder");
5755 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
5756 return false;
5757
5758 if (limitedInSQL)
5759 {
5760 // When have artist data (all roles) and LIMIT on songs use inline view
5761 // SELECT sv.*, songartistview.* FROM
5762 // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
5763 // <order by sv fields>, songartistview.idRole, songartistview.iOrder
5764 // Apply where clause, limits and order to songview, then join to songartistview this gives
5765 // multiple records per song in result set
5766 strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
5767 strSQL = "(" + strSQL + ") AS sv ";
5768 strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin;
5769 }
5770 else
5771 strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin;
5772 }
5773 else
5774 strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
5775
5776 CLog::Log(LOGDEBUG, "%s query = %s", __FUNCTION__, strSQL.c_str());
5777 querytime = XbmcThreads::SystemClockMillis();
5778 // run query
5779 if (!m_pDS->query(strSQL))
5780 return false;
5781
5782 int iRowsFound = m_pDS->num_rows();
5783 if (iRowsFound == 0)
5784 {
5785 m_pDS->close();
5786 return true;
5787 }
5788 querytime = XbmcThreads::SystemClockMillis() - querytime;
5789
5790 // Store the total number of songs as a property
5791 items.SetProperty("total", total);
5792
5793 DatabaseResults results;
5794 results.reserve(iRowsFound);
5795 // Populate results field vector from dataset
5796 FieldList fields;
5797 if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results))
5798 return false;
5799 // Store item list sort order
5800 items.SetSortMethod(sorting.sortBy);
5801 items.SetSortOrder(sorting.sortOrder);
5802
5803 // Get songs from returned rows. If join songartistview then there is a row for every artist
5804 items.Reserve(total);
5805 int songArtistOffset = song_enumCount;
5806 int songId = -1;
5807 VECARTISTCREDITS artistCredits;
5808 const dbiplus::query_data &data = m_pDS->get_result_set().records;
5809 int count = 0;
5810 for (const auto &i : results)
5811 {
5812 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5813 const dbiplus::sql_record* const record = data.at(targetRow);
5814
5815 try
5816 {
5817 if (songId != record->at(song_idSong).get_asInt())
5818 { //New song
5819 if (songId > 0 && !artistCredits.empty())
5820 {
5821 //Store artist credits for previous song
5822 GetFileItemFromArtistCredits(artistCredits, items[items.Size()-1].get());
5823 artistCredits.clear();
5824 }
5825 songId = record->at(song_idSong).get_asInt();
5826 CFileItemPtr item(new CFileItem);
5827 GetFileItemFromDataset(record, item.get(), musicUrl);
5828 // HACK for sorting by database returned order
5829 item->m_iprogramCount = ++count;
5830 // Set icon now to avoid slow per item processing in FillInDefaultIcon later
5831 item->SetProperty("icon_never_overlay", true);
5832 item->SetArt("icon", "DefaultAudio.png");
5833 items.Add(item);
5834 }
5835 // Get song artist credits and contributors
5836 if (artistData)
5837 {
5838 int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
5839 if (idSongArtistRole == ROLE_ARTIST)
5840 artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
5841 else
5842 items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
5843 }
5844 }
5845 catch (...)
5846 {
5847 m_pDS->close();
5848 CLog::Log(LOGERROR, "%s: out of memory loading query: %s", __FUNCTION__, filter.where.c_str());
5849 return (items.Size() > 0);
5850 }
5851 }
5852 if (!artistCredits.empty())
5853 {
5854 //Store artist credits for final song
5855 GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
5856 artistCredits.clear();
5857 }
5858 // cleanup
5859 m_pDS->close();
5860
5861 // Ensure random order of item list when results set sorted by idSong for artist processing
5862 // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
5863 // navigation. Order is read later from view state and list sorting is then triggered by
5864 // CGUIMediaWindow::Update in both cases.
5865 // So sorting here is currently redundant, but the consistent place to do it.
5866 // !@ todo: do sorting once, preferably in SQL
5867 if (sorting.sortBy == SortByRandom && artistData)
5868 items.Sort(sorting);
5869
5870 CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__,
5871 XbmcThreads::SystemClockMillis() - time, querytime);
5872 return true;
5873 }
5874 catch (...)
5875 {
5876 // cleanup
5877 m_pDS->close();
5878 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
5879 }
5880 return false;
5881 }
5882
GetSongsByWhere(const std::string & baseDir,const Filter & filter,CFileItemList & items,const SortDescription & sortDescription)5883 bool CMusicDatabase::GetSongsByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, const SortDescription &sortDescription /* = SortDescription() */)
5884 {
5885 if (m_pDB == nullptr || m_pDS == nullptr)
5886 return false;
5887
5888 try
5889 {
5890 int total = -1;
5891
5892 std::string strSQL = "SELECT %s FROM songview ";
5893
5894 Filter extFilter = filter;
5895 CMusicDbUrl musicUrl;
5896 SortDescription sorting = sortDescription;
5897 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
5898 return false;
5899
5900 // if there are extra WHERE conditions we might need access
5901 // to songview for these conditions
5902 if (extFilter.where.find("albumview") != std::string::npos)
5903 {
5904 extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
5905 extFilter.AppendGroup("songview.idSong");
5906 }
5907
5908 std::string strSQLExtra;
5909 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
5910 return false;
5911
5912 // Apply the limiting directly here if there's no special sorting but limiting
5913 if (extFilter.limit.empty() &&
5914 sorting.sortBy == SortByNone &&
5915 (sorting.limitStart > 0 || sorting.limitEnd > 0))
5916 {
5917 total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS);
5918 strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
5919 }
5920
5921 strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0 ? filter.fields.c_str() : "songview.*") + strSQLExtra;
5922
5923 CLog::Log(LOGDEBUG, "%s query = %s", __FUNCTION__, strSQL.c_str());
5924 // run query
5925 if (!m_pDS->query(strSQL))
5926 return false;
5927
5928 int iRowsFound = m_pDS->num_rows();
5929 if (iRowsFound == 0)
5930 {
5931 m_pDS->close();
5932 return true;
5933 }
5934
5935 // store the total value of items as a property
5936 if (total < iRowsFound)
5937 total = iRowsFound;
5938 items.SetProperty("total", total);
5939
5940 DatabaseResults results;
5941 results.reserve(iRowsFound);
5942 if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results))
5943 return false;
5944
5945 // get data from returned rows
5946 items.Reserve(results.size());
5947 const dbiplus::query_data &data = m_pDS->get_result_set().records;
5948 int count = 0;
5949 for (const auto &i : results)
5950 {
5951 unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
5952 const dbiplus::sql_record* const record = data.at(targetRow);
5953
5954 try
5955 {
5956 CFileItemPtr item(new CFileItem);
5957 GetFileItemFromDataset(record, item.get(), musicUrl);
5958 // HACK for sorting by database returned order
5959 item->m_iprogramCount = ++count;
5960 items.Add(item);
5961 }
5962 catch (...)
5963 {
5964 m_pDS->close();
5965 CLog::Log(LOGERROR, "%s: out of memory loading query: %s", __FUNCTION__, filter.where.c_str());
5966 return (items.Size() > 0);
5967 }
5968 }
5969
5970 // cleanup
5971 m_pDS->close();
5972 return true;
5973 }
5974 catch (...)
5975 {
5976 // cleanup
5977 m_pDS->close();
5978 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
5979 }
5980 return false;
5981 }
5982
GetSongsByYear(const std::string & baseDir,CFileItemList & items,int year)5983 bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year)
5984 {
5985 CMusicDbUrl musicUrl;
5986 if (!musicUrl.FromString(baseDir))
5987 return false;
5988
5989 musicUrl.AddOption("year", year);
5990
5991 Filter filter;
5992 return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true);
5993 }
5994
GetSongsNav(const std::string & strBaseDir,CFileItemList & items,int idGenre,int idArtist,int idAlbum,const SortDescription & sortDescription)5995 bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir, CFileItemList& items, int idGenre, int idArtist, int idAlbum, const SortDescription &sortDescription /* = SortDescription() */)
5996 {
5997 CMusicDbUrl musicUrl;
5998 if (!musicUrl.FromString(strBaseDir))
5999 return false;
6000
6001 if (idAlbum > 0)
6002 musicUrl.AddOption("albumid", idAlbum);
6003
6004 if (idGenre > 0)
6005 musicUrl.AddOption("genreid", idGenre);
6006
6007 if (idArtist > 0)
6008 musicUrl.AddOption("artistid", idArtist);
6009
6010 Filter filter;
6011 return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true);
6012 }
6013
6014 // clang-format off
6015 typedef struct
6016 {
6017 std::string fieldJSON; // Field name in JSON schema
6018 std::string formatJSON; // Format in JSON schema
6019 bool bSimple; // Fetch field directly to JSON output
6020 std::string fieldDB; // Name of field in db query
6021 std::string SQL; // SQL for scalar subqueries or field alias
6022 } translateJSONField;
6023
6024 static const translateJSONField JSONtoDBArtist[] = {
6025 // Table and single value join fields
6026 { "artist", "string", true, "strArtist", "" }, // Label field at top
6027 { "sortname", "string", true, "strSortname", "" },
6028 { "instrument", "array", true, "strInstruments", "" },
6029 { "description", "string", true, "strBiography", "" },
6030 { "genre", "array", true, "strGenres", "" },
6031 { "mood", "array", true, "strMoods", "" },
6032 { "style", "array", true, "strStyles", "" },
6033 { "yearsactive", "array", true, "strYearsActive", "" },
6034 { "born", "string", true, "strBorn", "" },
6035 { "formed", "string", true, "strFormed", "" },
6036 { "died", "string", true, "strDied", "" },
6037 { "disbanded", "string", true, "strDisbanded", "" },
6038 { "type", "string", true, "strType", "" },
6039 { "gender", "string", true, "strGender", "" },
6040 { "disambiguation", "string", true, "strDisambiguation", "" },
6041 { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
6042 { "dateadded", "string", true, "dateAdded", "" },
6043 { "datenew", "string", true, "dateNew", "" },
6044 { "datemodified", "string", true, "dateModified", "" },
6045
6046 // JOIN fields (multivalue), same order as _JoinToArtistFields
6047 { "", "", false, "isSong", "" },
6048 { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" },
6049 { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" },
6050 { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" },
6051 { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" },
6052 { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" },
6053 { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" },
6054 { "art", "", false, "idArt", "art.art_id AS idArt" },
6055 { "", "", false, "artType", "art.type AS artType" },
6056 { "", "", false, "artURL", "art.url AS artURL" },
6057 { "", "", false, "idRole", "song_artist.idRole" },
6058 { "roles", "", false, "strRole", "role.strRole" },
6059 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
6060 // Derived from joined tables
6061 { "isalbumartist", "bool", false, "", "" },
6062 { "thumbnail", "string", false, "", "" },
6063 { "fanart", "string", false, "", "" }
6064 // clang-format on
6065 /*
6066 Sources and genre are related via album, and so the dataset only contains source
6067 and genre pairs that exist, rather than all the genres being repeated for every
6068 source. We can not only look at genres for the first source, and genre can be
6069 out of order.
6070 */
6071
6072 };
6073
6074 static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField);
6075
GetArtistsByWhereJSON(const std::set<std::string> & fields,const std::string & baseDir,CVariant & result,int & total,const SortDescription & sortDescription)6076 bool CMusicDatabase::GetArtistsByWhereJSON(const std::set<std::string>& fields, const std::string &baseDir,
6077 CVariant& result, int& total, const SortDescription &sortDescription /* = SortDescription() */)
6078 {
6079 if (nullptr == m_pDB)
6080 return false;
6081 if (nullptr == m_pDS)
6082 return false;
6083
6084 try
6085 {
6086 total = -1;
6087
6088 size_t resultcount = 0;
6089 Filter extFilter;
6090 CMusicDbUrl musicUrl;
6091 SortDescription sorting = sortDescription;
6092 //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
6093 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6094 return false;
6095
6096 // Replace view names in filter with table names
6097 StringUtils::Replace(extFilter.where, "artistview", "artist");
6098 StringUtils::Replace(extFilter.where, "albumview", "album");
6099
6100 std::string strSQLExtra;
6101 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6102 return false;
6103
6104 // Count number of artists that satisfy selection criteria
6105 //(includes xsp limits from filter, but not sort limits)
6106 total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS);
6107 resultcount = static_cast<size_t>(total);
6108
6109 // Process albumartistsonly option
6110 const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
6111 bool albumArtistsOnly(false);
6112 auto option = options.find("albumartistsonly");
6113 if (option != options.end())
6114 albumArtistsOnly = option->second.asBoolean();
6115 // Process role options
6116 int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles.
6117 option = options.find("roleid");
6118 if (option != options.end())
6119 roleidfilter = static_cast<int>(option->second.asInteger());
6120 else
6121 {
6122 option = options.find("role");
6123 if (option != options.end())
6124 {
6125 if (option->second.asString() == "all" || option->second.asString() == "%")
6126 roleidfilter = -1000; //All roles
6127 else
6128 roleidfilter = GetRoleByName(option->second.asString());
6129 }
6130 }
6131
6132 // Get order by (and any scalar query artist fields)
6133 int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter);
6134 // Replace artistview field names in order by artist table field names
6135 StringUtils::Replace(extFilter.order, "artistview", "artist");
6136 StringUtils::Replace(extFilter.fields, "artistview", "artist");
6137
6138 // Grab and adjust artist sort field that may have been added to filter
6139 // These need to be added to the end of the artist table field list
6140 std::string artistsortSQL = extFilter.fields;
6141 extFilter.fields.clear();
6142
6143 std::string strSQL;
6144
6145 // Setup fields to query, and album field number mapping
6146 // Find first join field (isSong) in JSONtoDBArtist for offset
6147 int index_firstjoin = -1;
6148 for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++)
6149 {
6150 if (JSONtoDBArtist[i].fieldDB == "isSong")
6151 {
6152 index_firstjoin = i;
6153 break;
6154 }
6155 }
6156 Filter joinFilter;
6157 Filter albumArtistFilter;
6158 Filter songArtistFilter;
6159 DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount));
6160 extFilter.AppendField("artist.idArtist"); // ID "artistid" in JSON
6161 std::vector<int> dbfieldindex;
6162 // JSON "label" field is strArtist which is also output as "artist", query field once output twice
6163 extFilter.AppendField(JSONtoDBArtist[0].fieldDB);
6164 dbfieldindex.emplace_back(0); // Output "artist"
6165
6166 // Check each otional artist db field that could be retrieved (not "artist")
6167 for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++)
6168 {
6169 bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end();
6170 if (JSONtoDBArtist[i].bSimple)
6171 {
6172 // Check for non-join fields in order too.
6173 // Query these in inline view (but not output) so can ref in outer order
6174 bool foundOrderby(false);
6175 if (!foundJSON)
6176 foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos;
6177 if (foundOrderby || foundJSON)
6178 {
6179 // Store indexes of requested artist table and scalar subquery fields
6180 // to be output, and -1 when not output to JSON
6181 if (!foundJSON)
6182 dbfieldindex.emplace_back(-1);
6183 else
6184 dbfieldindex.emplace_back(i);
6185 // Field from scaler subquery
6186 if (!JSONtoDBArtist[i].SQL.empty())
6187 extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL));
6188 else
6189 // Field from artist table
6190 extFilter.AppendField(JSONtoDBArtist[i].fieldDB);
6191 }
6192 }
6193 else if (foundJSON)
6194 // Field from join or derived from joined fields
6195 joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true);
6196 }
6197
6198 // Append calculated artistsort field that may have been added to filter
6199 // Field used only for ORDER BY, not output to JSON
6200 extFilter.AppendField(artistsortSQL);
6201 for (int i = 0; i < iAddedFields; i++)
6202 dbfieldindex.emplace_back(-2); // columns in dataset
6203
6204 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6205 strSQLExtra = "";
6206 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6207 return false;
6208
6209 // Add any LIMIT clause to strSQLExtra
6210 if (extFilter.limit.empty() &&
6211 (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
6212 {
6213 strSQLExtra += DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
6214 resultcount = std::min(
6215 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
6216 resultcount);
6217 }
6218
6219 // Setup multivalue JOINs, GROUP BY and ORDER BY
6220 bool bJoinAlbumArtist(false);
6221 bool bJoinSongArtist(false);
6222 if (sortDescription.sortBy != SortByRandom)
6223 {
6224 // Repeat inline view order (that always includes idArtist) on join query
6225 std::string order = extFilter.order;
6226 StringUtils::Replace(order, "artist.", "a1.");
6227 joinFilter.AppendOrder(order);
6228 }
6229 else
6230 joinFilter.AppendOrder("a1.idArtist");
6231 joinFilter.AppendGroup("a1.idArtist");
6232 // Album artists and song artists
6233 if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) ||
6234 joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6235 joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) ||
6236 joinLayout.GetFetch(joinToArtist_strRole))
6237 {
6238 bJoinAlbumArtist = true;
6239 albumArtistFilter.AppendField("album_artist.idArtist AS id");
6240 if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole))
6241 {
6242 bJoinSongArtist = true;
6243 songArtistFilter.AppendField("song_artist.idArtist AS id");
6244 songArtistFilter.AppendField("1 AS isSong");
6245 albumArtistFilter.AppendField("0 AS isSong");
6246 joinLayout.SetField(joinToArtist_isSong, JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6247 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6248 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
6249 }
6250 }
6251 else if (joinLayout.GetFetch(joinToArtist_isalbumartist))
6252 {
6253 // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
6254 // so no need for join to album_artist table. Set fetching fetch false so that
6255 // joinLayout.HasFilterFields() is false
6256 joinLayout.SetFetch(joinToArtist_isalbumartist, false);
6257 }
6258
6259 // Sources
6260 if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6261 { // Left join as source may have been removed but leaving lib entries
6262 albumArtistFilter.AppendJoin("LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
6263 albumArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL);
6264 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6265 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6266 if (bJoinSongArtist)
6267 {
6268 songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
6269 songArtistFilter.AppendJoin("LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
6270 songArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
6271 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL);
6272 albumArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6273 joinLayout.SetField(joinToArtist_idSourceSong, JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6274 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6275 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
6276 }
6277 else
6278 {
6279 joinLayout.SetField(joinToArtist_idSourceAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true);
6280 }
6281 }
6282
6283 // Songgenres - id and genres always both
6284 if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
6285 { // All albums have songs, but left join genre as songs may not have genre
6286 albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
6287 albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
6288 albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6289 albumArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL);
6290 albumArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6291 joinLayout.SetField(joinToArtist_strSongGenreAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6292 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6293 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6294 if (bJoinSongArtist)
6295 { // Left join genre as songs may not have genre
6296 songArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
6297 songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
6298 songArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
6299 songArtistFilter.AppendField("'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
6300 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL);
6301 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL);
6302 albumArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6303 albumArtistFilter.AppendField("'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6304 joinLayout.SetField(joinToArtist_idSongGenreSong, JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6305 joinLayout.SetField(joinToArtist_strSongGenreSong, JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
6306 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6307 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
6308 }
6309 else
6310 { // Define field alias names in join layout
6311 joinLayout.SetField(joinToArtist_idSongGenreAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL, true);
6312 joinLayout.SetField(joinToArtist_strSongGenreAlbum, JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
6313 }
6314 }
6315
6316 // Roles
6317 if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole))
6318 // Only looking at album and song artists not other roles (default),
6319 // so filter dataset rows likewise.
6320 songArtistFilter.AppendWhere("song_artist.idRole = 1");
6321 else if (joinLayout.GetFetch(joinToArtist_strRole) || // "roles" field
6322 (bJoinSongArtist &&
6323 (joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
6324 joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))))
6325 { // Rows from many roles so fetch roleid for "roles", source and genre processing
6326 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL);
6327 // Add fake column to album_artist query
6328 albumArtistFilter.AppendField("-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6329 joinLayout.SetField(joinToArtist_idRole, JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6330 joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6331 joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
6332 }
6333 if (joinLayout.GetFetch(joinToArtist_strRole))
6334 { // Fetch role desc
6335 songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
6336 songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL);
6337 // Add fake column to album_artist query
6338 albumArtistFilter.AppendField("'albumartist' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB);
6339 }
6340
6341 // Build source, genre and roles part of query
6342 if (bJoinAlbumArtist)
6343 {
6344 if (bJoinSongArtist)
6345 {
6346 // Combine song and album artist filter as UNION and add to join filter as an inline view
6347 std::string strAlbumSQL;
6348 if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL))
6349 return false;
6350 strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL;
6351 std::string strSongSQL;
6352 if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL))
6353 return false;
6354 strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL;
6355
6356 joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL + ") AS albumSong ON id = a1.idArtist");
6357 }
6358 else
6359 { //Only join album_artist, so move filter elements to join filter
6360 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
6361 joinFilter.AppendJoin(albumArtistFilter.join);
6362 }
6363 }
6364
6365 //Art
6366 bool bJoinArt(false);
6367 bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) ||
6368 joinLayout.GetOutput(joinToArtist_thumbnail) ||
6369 joinLayout.GetOutput(joinToArtist_fanart);
6370 if (bJoinArt)
6371 { // Left join as artist may not have any art
6372 joinFilter.AppendJoin("LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
6373 joinLayout.SetField(joinToArtist_idArt, JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL,
6374 joinLayout.GetOutput(joinToArtist_idArt));
6375 joinLayout.SetField(joinToArtist_artType, JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL);
6376 joinLayout.SetField(joinToArtist_artURL, JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL);
6377 joinFilter.AppendGroup("art.art_id");
6378 joinFilter.AppendOrder("arttype");
6379 if (!joinLayout.GetOutput(joinToArtist_idArt))
6380 {
6381 if (!joinLayout.GetOutput(joinToArtist_thumbnail))
6382 // Fanart only
6383 joinFilter.AppendJoin("AND art.type = 'fanart'");
6384 else if (!joinLayout.GetOutput(joinToArtist_fanart))
6385 // Thumb only
6386 joinFilter.AppendJoin("AND art.type = 'thumb'");
6387 }
6388 }
6389 else if (bJoinSongArtist)
6390 joinFilter.group.clear(); // UNION only so no GROUP BY needed
6391
6392 // Build JOIN part of query (if we have one)
6393 std::string strSQLJoin;
6394 if (joinLayout.HasFilterFields())
6395 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6396 return false;
6397
6398 // Adjust where in the results record the join fields are allowing for the
6399 // inline view fields (Quicker than finding field by name every time)
6400 // idArtist + other artist fields
6401 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
6402
6403 // Build full query
6404 // When have multiple value joins e.g. song genres, use inline view
6405 // SELECT a1.*, <join fields> FROM
6406 // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1
6407 // <joins> <group by> <order by> + <joins order by>
6408 // Don't use prepareSQL - confuses arttype = 'thumb' filter
6409
6410 strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra;
6411 if (joinLayout.HasFilterFields())
6412 {
6413 strSQL = "(" + strSQL + ") AS a1 ";
6414 strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
6415 }
6416
6417 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
6418 // run query
6419 unsigned int time = XbmcThreads::SystemClockMillis();
6420 if (!m_pDS->query(strSQL))
6421 return false;
6422 CLog::Log(LOGDEBUG, "%s - query took %i ms",
6423 __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
6424
6425 int iRowsFound = m_pDS->num_rows();
6426 if (iRowsFound <= 0)
6427 {
6428 m_pDS->close();
6429 return true;
6430 }
6431
6432 // Get artists from returned rows. Joins means there can be many rows per artist
6433 int artistId = -1;
6434 int sourceId = -1;
6435 int genreId = -1;
6436 int roleId = -1;
6437 int artId = -1;
6438 std::vector<int> genreidlist;
6439 std::vector<int> sourceidlist;
6440 std::vector<int> roleidlist;
6441 bool bArtDone(false);
6442 bool bHaveArtist(false);
6443 bool bIsAlbumArtist(true);
6444 bool bGenreFoundViaAlbum(false);
6445 CVariant artistObj;
6446 result["artists"].reserve(resultcount);
6447 while (!m_pDS->eof() || bHaveArtist)
6448 {
6449 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
6450
6451 if (m_pDS->eof() || artistId != record->at(0).get_asInt())
6452 {
6453 // Store previous or last artist
6454 if (bHaveArtist)
6455 {
6456 // Convert any empty MBid array into an array with one empty element [""]
6457 // to match the number of artist ID (way other mbid arrays handled)
6458 if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty())
6459 artistObj["musicbrainzartistid"].append("");
6460
6461 result["artists"].append(artistObj);
6462 bHaveArtist = false;
6463 artistObj.clear();
6464 }
6465 if (artistObj.empty())
6466 {
6467 // Initialise fields, ensure those with possible null values are set to correct empty variant type
6468 if (joinLayout.GetOutput(joinToArtist_idSourceAlbum))
6469 artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
6470 if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum))
6471 artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
6472 if (joinLayout.GetOutput(joinToArtist_idArt))
6473 artistObj["art"] = CVariant(CVariant::VariantTypeObject);
6474 if (joinLayout.GetOutput(joinToArtist_thumbnail))
6475 artistObj["thumbnail"] = "";
6476 if (joinLayout.GetOutput(joinToArtist_fanart))
6477 artistObj["fanart"] = "";
6478
6479 sourceId = -1;
6480 roleId = -1;
6481 genreId = -1;
6482 artId = -1;
6483 genreidlist.clear();
6484 bGenreFoundViaAlbum = false;
6485 sourceidlist.clear();
6486 roleidlist.clear();
6487 bArtDone = false;
6488 }
6489 if (m_pDS->eof())
6490 continue; // Having saved the last artist stop
6491
6492 // New artist
6493 artistId = record->at(0).get_asInt();
6494 bHaveArtist = true;
6495 artistObj["artistid"] = artistId;
6496 artistObj["label"] = record->at(1).get_asString();
6497 artistObj["artist"] = record->at(1).get_asString(); // Always have "artist"
6498 bIsAlbumArtist = true; //Album artist by default
6499 if (joinLayout.GetOutput(joinToArtist_isalbumartist))
6500 {
6501 // Not album artist when fetching song artists too and first row for artist isSong=true
6502 if (bJoinSongArtist)
6503 bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
6504 artistObj["isalbumartist"] = bIsAlbumArtist;
6505 }
6506 for (size_t i = 0; i < dbfieldindex.size(); i++)
6507 if (dbfieldindex[i] > -1)
6508 {
6509 if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer")
6510 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
6511 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float")
6512 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asFloat();
6513 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array")
6514 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
6515 StringUtils::Split(record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
6516 else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean")
6517 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
6518 else
6519 artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
6520 }
6521 }
6522 if (bJoinAlbumArtist)
6523 {
6524 bool bAlbumArtistRow(true);
6525 int idRoleRow = -1;
6526 if (bJoinSongArtist)
6527 {
6528 bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
6529 if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 &&
6530 !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull())
6531 {
6532 idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt();
6533 }
6534 }
6535
6536 // Sources - gathered via both album_artist and song_artist (with role = 1)
6537 if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
6538 {
6539 if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 &&
6540 !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() &&
6541 sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) ||
6542 (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 &&
6543 !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() &&
6544 sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt()))
6545 {
6546 bArtDone = bArtDone || (sourceId > 0); // Not first source, skip art repeats
6547 bool found(false);
6548 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt();
6549 if (!bAlbumArtistRow)
6550 {
6551 // Skip other roles (when fetching them)
6552 if (idRoleRow > 1)
6553 {
6554 found = true;
6555 }
6556 else
6557 {
6558 sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt();
6559 // Song artist row may repeat sources found via album artist
6560 // Already have that source?
6561 for (const auto& i : sourceidlist)
6562 if (i == sourceId)
6563 {
6564 found = true;
6565 break;
6566 }
6567 }
6568 }
6569 if (!found)
6570 {
6571 sourceidlist.emplace_back(sourceId);
6572 artistObj["sourceid"].append(sourceId);
6573 }
6574 }
6575 }
6576 // Songgenres - via album artist takes precedence
6577 /*
6578 Sources and genre are related via album, and so the dataset only contains source
6579 and genre pairs that exist, rather than all the genres being repeated for every
6580 source. We can not only look at genres for the first source, and genre can be
6581 found out of order.
6582 Also song artist row may repeat genres found via album artist
6583 */
6584 if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
6585 {
6586 std::string strGenre;
6587 bool newgenre(false);
6588 if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 &&
6589 !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() &&
6590 genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt())
6591 {
6592 bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
6593 newgenre = true;
6594 genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt();
6595 strGenre = record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString();
6596 }
6597 else if (!bAlbumArtistRow && !bGenreFoundViaAlbum &&
6598 joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 &&
6599 !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() &&
6600 genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt())
6601 {
6602 bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
6603 newgenre = idRoleRow <= 1; // Skip other roles (when fetching them)
6604 genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt();
6605 strGenre = record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString();
6606 }
6607 bool found(false);
6608 if (newgenre)
6609 {
6610 // Already have that genre?
6611 for (const auto& i : genreidlist)
6612 if (i == genreId)
6613 {
6614 found = true;
6615 break;
6616 }
6617 if (!found)
6618 {
6619 bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow;
6620 genreidlist.emplace_back(genreId);
6621 CVariant genreObj;
6622 genreObj["genreid"] = genreId;
6623 genreObj["title"] = strGenre;
6624 artistObj["songgenres"].append(genreObj);
6625 }
6626 }
6627 }
6628 // Roles - gathered via song_artist roleid rows
6629 if (joinLayout.GetFetch(joinToArtist_idRole))
6630 {
6631 if (!bAlbumArtistRow && roleId != idRoleRow)
6632 {
6633 bArtDone = bArtDone || (roleId > 0); // Not first role, skip art repeats
6634 roleId = idRoleRow;
6635 if (joinLayout.GetOutput(joinToArtist_strRole))
6636 {
6637 // Already have that role?
6638 bool found(false);
6639 for (const auto& i : roleidlist)
6640 if (i == roleId)
6641 {
6642 found = true;
6643 break;
6644 }
6645 if (!found)
6646 {
6647 roleidlist.emplace_back(roleId);
6648 CVariant roleObj;
6649 roleObj["roleid"] = roleId;
6650 roleObj["role"] = record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString();
6651 artistObj["roles"].append(roleObj);
6652 }
6653 }
6654 }
6655 }
6656 }
6657 // Art
6658 if (bJoinArt && !bArtDone &&
6659 !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() &&
6660 record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 &&
6661 artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt())
6662 {
6663 artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt();
6664 if (joinLayout.GetOutput(joinToArtist_idArt))
6665 {
6666 artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] =
6667 CTextureUtils::GetWrappedImageURL(record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
6668 }
6669 if (joinLayout.GetOutput(joinToArtist_thumbnail) &&
6670 record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb")
6671 {
6672 artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL(record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
6673 }
6674 if (joinLayout.GetOutput(joinToArtist_fanart) &&
6675 record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart")
6676 {
6677 artistObj["fanart"] = CTextureUtils::GetWrappedImageURL(record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
6678 }
6679 }
6680
6681 m_pDS->next();
6682 }
6683 m_pDS->close(); // cleanup recordset data
6684
6685 // Ensure random order of output when results set is sorted to process multi-value joins
6686 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
6687 KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array());
6688
6689 return true;
6690 }
6691 catch (...)
6692 {
6693 m_pDS->close();
6694 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
6695 }
6696 return false;
6697 }
6698
6699 // clang-format off
6700 static const translateJSONField JSONtoDBAlbum[] = {
6701 // albumview (inc scalar subquery fields use in filter rules)
6702 { "title", "string", true, "strAlbum", "" }, // Label field at top
6703 { "description", "string", true, "strReview", "" },
6704 { "genre", "array", true, "strGenres", "" },
6705 { "theme", "array", true, "strThemes", "" },
6706 { "mood", "array", true, "strMoods", "" },
6707 { "style", "array", true, "strStyles", "" },
6708 { "type", "string", true, "strType", "" },
6709 { "albumlabel", "string", true, "strLabel", "" },
6710 { "rating", "float", true, "fRating", "" },
6711 { "votes", "integer", true, "iVotes", "" },
6712 { "userrating", "unsigned", true, "iUserrating", "" },
6713 { "isboxset", "boolean", true, "bBoxedSet", "" },
6714 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" },
6715 { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table
6716 { "compilation", "boolean", true, "bCompilation", "" },
6717 { "releasetype", "string", true, "strReleaseType", "" },
6718 { "totaldiscs", "integer", true, "iDiscTotal", "" },
6719 { "sortartist", "string", true, "strArtistSort", "" },
6720 { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" },
6721 { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view
6722 { "dateadded", "string", true, "dateAdded", "" },
6723 { "datenew", "string", true, "dateNew", "" },
6724 { "datemodified", "string", true, "dateModified", "" },
6725 { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view
6726 { "originaldate", "string", true, "strOrigReleaseDate", "" },
6727 { "releasedate", "string", true, "strReleaseDate", "" },
6728 { "albumstatus", "string", true, "strReleaseStatus", "" },
6729 { "albumduration", "integer", true, "iAlbumDuration", "" },
6730 // Scalar subquery fields
6731 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
6732 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
6733 { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
6734 "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
6735 // Single value JOIN fields
6736 { "thumbnail", "image", true, "thumbnail", "art.url AS thumbnail" }, // or (SELECT art.url FROM art WHERE art.media_id = album.idAlbum AND art.media_type = "album" AND art.type = "thumb") as url
6737 // JOIN fields (multivalue), same order as _JoinToAlbumFields
6738 { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" },
6739 { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" },
6740 { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" },
6741 /*
6742 Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
6743 and separate queries to allow for fallback strategy.
6744
6745 Using albmview, rather than album table, as view has scalar subqueries for
6746 playcount and lastplayed already defined. Needed as MySQL does
6747 not support use of scalar subquery field alias names in where clauses (they
6748 have to be repeated) and these fields can be used by filter rules.
6749 Using this view is no slower than the album table as these scalar fields are
6750 only calculated (slowing query) when field is in field list.
6751 */
6752 };
6753 // clang-format on
6754
6755 static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField);
6756
GetAlbumsByWhereJSON(const std::set<std::string> & fields,const std::string & baseDir,CVariant & result,int & total,const SortDescription & sortDescription)6757 bool CMusicDatabase::GetAlbumsByWhereJSON(const std::set<std::string>& fields, const std::string &baseDir,
6758 CVariant& result, int& total, const SortDescription &sortDescription /* = SortDescription() */)
6759 {
6760
6761 if (nullptr == m_pDB)
6762 return false;
6763 if (nullptr == m_pDS)
6764 return false;
6765
6766 try
6767 {
6768 total = -1;
6769
6770 size_t resultcount = 0;
6771 Filter extFilter;
6772 CMusicDbUrl musicUrl;
6773 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
6774 // passed in at the start of the function
6775 SortDescription sorting = sortDescription;
6776 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
6777 return false;
6778
6779 // Replace view names in filter with table names
6780 StringUtils::Replace(extFilter.where, "artistview", "artist");
6781
6782 std::string strSQLExtra;
6783 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6784 return false;
6785
6786 // Count number of albums that satisfy selection criteria
6787 // (includes xsp limits from filter, but not sort limits)
6788 // Use albumview as filter rules in where clause may use scalar query fields
6789 total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
6790 resultcount = static_cast<size_t>(total);
6791
6792 // Get order by (and any scalar query artist fields
6793 int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter);
6794
6795 // Grab calculated artist/title sort fields that may have been added to filter
6796 // These need to be added to the end of the album table field list
6797 std::string calcsortfieldsSQL = extFilter.fields;
6798 extFilter.fields.clear();
6799
6800 std::string strSQL;
6801
6802 // Setup fields to query, and album field number mapping
6803 // Find idArtist in JSONtoDBAlbum, offset of first join field
6804 int index_idArtist = -1;
6805 for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++)
6806 {
6807 if (JSONtoDBAlbum[i].fieldDB == "idArtist")
6808 {
6809 index_idArtist = i;
6810 break;
6811 }
6812 }
6813 Filter joinFilter;
6814 DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount));
6815 extFilter.AppendField("albumview.idAlbum"); // ID "albumid" in JSON
6816 std::vector<int> dbfieldindex;
6817 // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
6818 extFilter.AppendField(JSONtoDBAlbum[0].fieldDB);
6819 if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end())
6820 dbfieldindex.emplace_back(0); // Output "title"
6821 else
6822 dbfieldindex.emplace_back(-1); // fetch but not outout
6823
6824 // Check each optional album db field that could be retrieved (not label)
6825 for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++)
6826 {
6827 bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end();
6828 if (JSONtoDBAlbum[i].bSimple)
6829 {
6830 // Check for non-join fields in order too.
6831 // Query these in inline view (but not output) so can ref in outer order
6832 bool foundOrderby(false);
6833 if (!foundJSON)
6834 foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos;
6835 if (foundOrderby || foundJSON)
6836 {
6837 // Store indexes of requested album table and scalar subquery fields
6838 // to be output, and -1 when not output to JSON
6839 if (!foundJSON)
6840 dbfieldindex.emplace_back(-1);
6841 else
6842 dbfieldindex.emplace_back(i);
6843 if (!JSONtoDBAlbum[i].SQL.empty())
6844 // Field from scaler subquery
6845 extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL));
6846 else
6847 // Field from album table
6848 extFilter.AppendField(JSONtoDBAlbum[i].fieldDB);
6849 }
6850 }
6851 else if (foundJSON)
6852 // Field from join found in JSON request
6853 joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true);
6854 }
6855
6856 // Append calculated artist/title sort fields that may have been added to filter
6857 // Field used only for ORDER BY, not output to JSON
6858 extFilter.AppendField(calcsortfieldsSQL);
6859 for (int i = 0; i < iAddedFields; i++)
6860 dbfieldindex.emplace_back(-1); // columns in dataset
6861
6862 // JOIN art tables if needed (fields output and/or in sort)
6863 if (extFilter.fields.find("art.") != std::string::npos)
6864 { // Left join as not all albums have art, but only have one thumb at most
6865 extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
6866 "AND art.media_type = 'album' AND art.type = 'thumb'");
6867 }
6868
6869 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
6870 strSQLExtra = "";
6871 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
6872 return false;
6873
6874 // Add any LIMIT clause to strSQLExtra
6875 if (extFilter.limit.empty() &&
6876 (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
6877 {
6878 strSQLExtra += DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
6879 resultcount = std::min(
6880 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
6881 resultcount);
6882 }
6883
6884 // Setup multivalue JOINs, GROUP BY and ORDER BY
6885 bool bJoinAlbumArtist(false);
6886 if (sortDescription.sortBy != SortByRandom)
6887 {
6888 // Repeat inline view order (that always includes idAlbum) on join query
6889 std::string order = extFilter.order;
6890 StringUtils::Replace(order, "albumview.", "a1.");
6891 joinFilter.AppendOrder(order);
6892 }
6893 else
6894 joinFilter.AppendOrder("a1.idAlbum");
6895 joinFilter.AppendGroup("a1.idAlbum");
6896 // Album artists
6897 if (joinLayout.GetFetch(joinToAlbum_idArtist) ||
6898 joinLayout.GetFetch(joinToAlbum_strArtist) ||
6899 joinLayout.GetFetch(joinToAlbum_strArtistMBID))
6900 { // All albums have at least one artist so inner join sufficient
6901 bJoinAlbumArtist = true;
6902 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
6903 joinFilter.AppendGroup("album_artist.idArtist");
6904 joinFilter.AppendOrder("album_artist.iOrder");
6905 // Ensure idArtist is queried
6906 if (!joinLayout.GetFetch(joinToAlbum_idArtist))
6907 joinLayout.SetField(joinToAlbum_idArtist, JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL);
6908 }
6909 // artist table needed for strArtist or MBID
6910 // (album_artist.strArtist can be an alias or spelling variation)
6911 if (joinLayout.GetFetch(joinToAlbum_strArtist) || joinLayout.GetFetch(joinToAlbum_strArtistMBID))
6912 joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
6913
6914 // Build JOIN part of query (if we have one)
6915 std::string strSQLJoin;
6916 if (joinLayout.HasFilterFields())
6917 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
6918 return false;
6919
6920 // Adjust where in the results record the join fields are allowing for the
6921 // inline view fields (Quicker than finding field by name every time)
6922 // idAlbum + other album fields
6923 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
6924
6925 // Build full query
6926 // When have multiple value joins (artists or song genres) use inline view
6927 // SELECT a1.*, <join fields> FROM
6928 // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1
6929 // <joins> <group by> <order by> <joins order by>
6930 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
6931
6932 strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra;
6933 if (joinLayout.HasFilterFields())
6934 {
6935 strSQL = "(" + strSQL + ") AS a1 ";
6936 strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
6937 }
6938
6939 // Modify query to use correct year field
6940 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
6941 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
6942 StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate");
6943 else
6944 StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate");
6945
6946 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
6947 // run query
6948 unsigned int time = XbmcThreads::SystemClockMillis();
6949 if (!m_pDS->query(strSQL))
6950 return false;
6951 CLog::Log(LOGDEBUG, "%s - query took %i ms",
6952 __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
6953
6954 int iRowsFound = m_pDS->num_rows();
6955 if (iRowsFound <= 0)
6956 {
6957 m_pDS->close();
6958 return true;
6959 }
6960
6961 // Get albums from returned rows. Joins means there can be many rows per album
6962 int albumId = -1;
6963 int artistId = -1;
6964 CVariant albumObj;
6965 result["albums"].reserve(resultcount);
6966 while (!m_pDS->eof() || !albumObj.empty())
6967 {
6968 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
6969
6970 if (m_pDS->eof() || albumId != record->at(0).get_asInt())
6971 {
6972 // Store previous or last album
6973 if (!albumObj.empty())
6974 {
6975 // Split sources string into int array
6976 if (albumObj.isMember("sourceid"))
6977 {
6978 std::vector<std::string> sources = StringUtils::Split(albumObj["sourceid"].asString(), ";");
6979 albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
6980 for (size_t i = 0; i < sources.size(); i++)
6981 albumObj["sourceid"].append(atoi(sources[i].c_str()));
6982 }
6983 result["albums"].append(albumObj);
6984 albumObj.clear();
6985 artistId = -1;
6986 }
6987 if (m_pDS->eof())
6988 continue; // Having saved last album stop
6989
6990 // New album
6991 albumId = record->at(0).get_asInt();
6992 albumObj["albumid"] = albumId;
6993 albumObj["label"] = record->at(1).get_asString();
6994 for (size_t i = 0; i < dbfieldindex.size(); i++)
6995 if (dbfieldindex[i] > -1)
6996 {
6997 if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres")
6998 {
6999 // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
7000 std::vector<std::string> values =
7001 StringUtils::Split(record->at(1 + i).get_asString(), ",");
7002 if (values.size() % 2 == 0) // Must contain an even number of entries
7003 {
7004 for (size_t i = 0; i + 1 < values.size(); i += 2)
7005 {
7006 int idGenre = atoi(values[i].c_str());
7007 if (idGenre > 0)
7008 {
7009 CVariant genreObj;
7010 genreObj["genreid"] = idGenre;
7011 genreObj["title"] = values[i + 1];
7012 albumObj["songgenres"].append(genreObj);
7013 }
7014 }
7015 }
7016 // Ensure albums with null songgenres get empty array
7017 if (!albumObj.isMember("songgenres"))
7018 albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
7019 }
7020 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer")
7021 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7022 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned")
7023 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asInt(), 0);
7024 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float")
7025 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asFloat(), 0.f);
7026 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array")
7027 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split(record->at(1 + i).get_asString(),
7028 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
7029 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean")
7030 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7031 else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image")
7032 {
7033 std::string url = record->at(1 + i).get_asString();
7034 if (!url.empty())
7035 url = CTextureUtils::GetWrappedImageURL(url);
7036 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url;
7037 }
7038 else
7039 albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
7040 }
7041 }
7042 if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1)
7043 {
7044 if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt())
7045 {
7046 artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt();
7047 if (joinLayout.GetOutput(joinToAlbum_idArtist))
7048 albumObj["artistid"].append(artistId);
7049 if (artistId == BLANKARTIST_ID)
7050 {
7051 if (joinLayout.GetOutput(joinToAlbum_strArtist))
7052 albumObj["artist"].append(StringUtils::Empty);
7053 if (joinLayout.GetOutput(joinToAlbum_strArtistMBID))
7054 albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
7055 }
7056 else
7057 {
7058 if (joinLayout.GetOutput(joinToAlbum_strArtist) && joinLayout.GetRecNo(joinToAlbum_strArtist) > -1)
7059 albumObj["artist"].append(record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString());
7060 if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) && joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1)
7061 albumObj["musicbrainzalbumartistid"].append(record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString());
7062 }
7063 }
7064 }
7065 m_pDS->next();
7066 }
7067 m_pDS->close(); // cleanup recordset data
7068
7069 // Ensure random order of output when results set is sorted to process multi-value joins
7070 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7071 KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array());
7072
7073 return true;
7074 }
7075 catch (...)
7076 {
7077 m_pDS->close();
7078 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
7079 }
7080 return false;
7081 }
7082
7083 // clang-format off
7084 static const translateJSONField JSONtoDBSong[] = {
7085 // table and single value join fields
7086 { "title", "string", true, "strTitle", "" }, // Label field at top
7087 { "albumid", "integer", true, "song.idAlbum", "" },
7088 { "", "", true, "song.iTrack", "" },
7089 { "displayartist", "string", true, "song.strArtistDisp", "" },
7090 { "sortartist", "string", true, "song.strArtistSort", "" },
7091 { "genre", "array", true, "song.strGenres", "" },
7092 { "duration", "integer", true, "iDuration", "" },
7093 { "comment", "string", true, "comment", "" },
7094 { "", "string", true, "strFileName", "" },
7095 { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" },
7096 { "playcount", "integer", true, "iTimesPlayed", "" },
7097 { "lastplayed", "string", true, "lastPlayed", "" },
7098 { "rating", "float", true, "rating", "" },
7099 { "votes", "integer", true, "votes", "" },
7100 { "userrating", "unsigned", true, "song.userrating", "" },
7101 { "mood", "array", true, "mood", "" },
7102 { "dateadded", "string", true, "song.dateAdded", "" },
7103 { "datenew", "string", true, "song.dateNew", "" },
7104 { "datemodified", "string", true, "song.dateModified", "" },
7105 { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" },
7106 { "", "string", true, "strPath", "path.strPath AS strPath" },
7107 { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" },
7108 { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" },
7109 { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
7110 { "disctitle", "string", true, "song.strDiscSubtitle", "" },
7111 { "bpm", "integer", true, "iBPM", "" },
7112 { "originaldate", "string" , true, "song.strOrigReleaseDate","" },
7113 { "releasedate", "string" , true, "song.strReleaseDate", "" },
7114 { "bitrate", "integer", true, "iBitRate", "" },
7115 { "samplerate", "integer", true, "iSampleRate", "" },
7116 { "channels", "integer", true, "iChannels", "" },
7117
7118 // JOIN fields (multivalue), same order as _JoinToSongFields
7119 { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" },
7120 { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" },
7121 { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
7122 { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" },
7123 { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" },
7124 { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" },
7125 { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" },
7126 { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" },
7127 { "", "", false, "idRole", "song_artist.idRole" },
7128 { "", "", false, "strRole", "role.strRole" },
7129 { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
7130 { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
7131 { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" },
7132
7133 { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" },
7134 { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" },
7135 { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" },
7136 { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" },
7137 { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" },
7138
7139 // Scalar subquery fields
7140 { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
7141 { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" },
7142 { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" },
7143 { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
7144 /*
7145 Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
7146 thumbloader and separate queries to allow for fallback strategy
7147 "lyrics"?? Can be set for an item (by addons) but not held in db so
7148 AudioLibrary.GetSongs() never fills this field despite being in schema
7149
7150 FROM ( SELECT * FROM song
7151 JOIN album ON album.idAlbum = song.idAlbum
7152 JOIN path ON path.idPath = song.idPath) AS sv
7153 JOIN album_artist ON album_artist.idAlbum = song.idAlbum
7154 JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
7155 JOIN song_artist ON song_artist.idSong = song.idSong
7156 JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist
7157 JOIN role ON song_artist.idRole = role.idRole
7158 LEFT JOIN song_genre ON song.idSong = song_genre.idSong
7159
7160 */
7161 };
7162 // clang-format on
7163
7164 static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField);
7165
GetSongsByWhereJSON(const std::set<std::string> & fields,const std::string & baseDir,CVariant & result,int & total,const SortDescription & sortDescription)7166 bool CMusicDatabase::GetSongsByWhereJSON(const std::set<std::string>& fields, const std::string &baseDir,
7167 CVariant& result, int& total, const SortDescription &sortDescription /* = SortDescription() */)
7168 {
7169
7170 if (nullptr == m_pDB)
7171 return false;
7172 if (nullptr == m_pDS)
7173 return false;
7174
7175 try
7176 {
7177 total = -1;
7178
7179 size_t resultcount = 0;
7180 Filter extFilter;
7181 CMusicDbUrl musicUrl;
7182 // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
7183 // passed into the function
7184 SortDescription sorting = sortDescription;
7185 if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
7186 return false;
7187
7188 // Replace view names in filter with table names
7189 StringUtils::Replace(extFilter.where, "artistview", "artist");
7190 StringUtils::Replace(extFilter.where, "albumview", "album");
7191 StringUtils::Replace(extFilter.where, "songview.strPath", "strPath");
7192 StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum");
7193 StringUtils::Replace(extFilter.where, "songview", "song");
7194 StringUtils::Replace(extFilter.where, "songartistview", "song_artist");
7195
7196 // JOIN album and path tables needed by filter rules in where clause
7197 if (extFilter.where.find("album.") != std::string::npos ||
7198 extFilter.where.find("strAlbum") != std::string::npos)
7199 { // All songs have one album so inner join sufficient
7200 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7201 }
7202 if (extFilter.where.find("strPath") != std::string::npos)
7203 { // All songs have one path so inner join sufficient
7204 extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7205 }
7206
7207 // Build JOINs and WHERE needed by filter for counting songs
7208 std::string strSQLExtra;
7209 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7210 return false;
7211
7212 // Count number of songs that satisfy selection criteria
7213 // (includes xsp limits from filter, but not sort limits)
7214 total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS);
7215 resultcount = static_cast<size_t>(total);
7216
7217 int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter);
7218 // Replace songview field names in order by with song, album path table field names
7219 // Field names in album same as song:
7220 // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
7221 StringUtils::Replace(extFilter.order, "songview.strPath", "strPath");
7222 StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum");
7223 StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation");
7224 StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp");
7225 StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp");
7226 StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort");
7227 StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType");
7228 StringUtils::Replace(extFilter.order, "songview", "song");
7229 StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort");
7230 StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp");
7231 StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum");
7232 StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle");
7233
7234 // Grab calculated artist/title sort fields that may have been added to filter
7235 // These need to be added to the end of the song table field list
7236 std::string calcsortfieldsSQL = extFilter.fields;
7237 extFilter.fields.clear();
7238
7239 std::string strSQL;
7240
7241 // Setup fields to query, and song field number mapping
7242 // Find idAlbumArtist in JSONtoDBSong, offset of first join field
7243 int index_idAlbumArtist = -1;
7244 for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++)
7245 {
7246 if (JSONtoDBSong[i].fieldDB == "idAlbumArtist")
7247 {
7248 index_idAlbumArtist = i;
7249 break;
7250 }
7251 }
7252 Filter joinFilter;
7253 DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount));
7254 extFilter.AppendField("song.idSong"); // ID "songid" in JSON
7255 std::vector<int> dbfieldindex;
7256 // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
7257 extFilter.AppendField(JSONtoDBSong[0].fieldDB);
7258 if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end())
7259 dbfieldindex.emplace_back(0); // Output "title"
7260 else
7261 dbfieldindex.emplace_back(-1); // Fetch but not output
7262 std::vector<std::string> rolefieldlist;
7263 std::vector<int> roleidlist;
7264 // Check each optional db field that could be retrieved (not label)
7265 for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++)
7266 {
7267 bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end();
7268 if (JSONtoDBSong[i].bSimple)
7269 {
7270 // Check for non-join fields in order too.
7271 // Query these in inline view (but not output) so can ref in outer order
7272 bool foundOrderby(false);
7273 if (!foundJSON)
7274 foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos;
7275 if (foundOrderby || foundJSON)
7276 {
7277 // Store indexes of requested album table and scalar subquery fields
7278 // to be output, and -1 when not output to JSON
7279 if (!foundJSON)
7280 dbfieldindex.emplace_back(-1);
7281 else
7282 dbfieldindex.emplace_back(i);
7283 if (!JSONtoDBSong[i].SQL.empty())
7284 // Field from scaler subquery
7285 extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL));
7286 else
7287 // Field from song table
7288 extFilter.AppendField(JSONtoDBSong[i].fieldDB);
7289 }
7290 }
7291 else if (foundJSON)
7292 { // Field from join found in JSON request
7293 if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_"))
7294 {
7295 joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true);
7296 }
7297 else
7298 { // "contributors", "displaycomposer" etc.
7299 rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON);
7300 }
7301 }
7302 }
7303 // Append calculated artist/title sort fields that may have been added to filter
7304 // Field used only for ORDER BY, not output to JSON
7305 extFilter.AppendField(calcsortfieldsSQL);
7306 for (int i = 0; i < iAddedFields; i++)
7307 dbfieldindex.emplace_back(-1); // columns in dataset
7308
7309 // Build matching list of role id for "displaycomposer", "displayconductor",
7310 // "displayorchestra", "displaylyricist"
7311 if (!rolefieldlist.empty())
7312 {
7313 for (const auto& name : rolefieldlist)
7314 {
7315 int idRole = -1;
7316 if (StringUtils::StartsWith(name, "display"))
7317 idRole = GetRoleByName(name.substr(7));
7318 roleidlist.emplace_back(idRole);
7319 }
7320 }
7321
7322 // JOIN album and path tables needed for field output and/or in sort
7323 // if not already there for filter
7324 if ((extFilter.fields.find("album.") != std::string::npos ||
7325 extFilter.fields.find("strAlbum") != std::string::npos) &&
7326 extFilter.join.find("JOIN album") == std::string::npos)
7327 { // All songs have one album so inner join sufficient
7328 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
7329 }
7330 if (extFilter.fields.find("path.") != std::string::npos &&
7331 extFilter.join.find("JOIN path") == std::string::npos)
7332 { // All songs have one path so inner join sufficient
7333 extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
7334 }
7335
7336 // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
7337 strSQLExtra = "";
7338 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
7339 return false;
7340
7341 // Add any LIMIT clause to strSQLExtra
7342 if (extFilter.limit.empty() &&
7343 (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
7344 {
7345 strSQLExtra += DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
7346 resultcount = std::min(
7347 DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
7348 resultcount);
7349 }
7350
7351 // Setup multivalue JOINs, GROUP BY and ORDER BY
7352 bool bJoinSongArtist(false);
7353 bool bJoinAlbumArtist(false);
7354 bool bJoinRole(false);
7355 if (sortDescription.sortBy != SortByRandom)
7356 {
7357 // Repeat inline view order (that always includes idSong) on join query
7358 std::string order = extFilter.order;
7359 order = extFilter.order;
7360 StringUtils::Replace(order, "album.", "sv.");
7361 StringUtils::Replace(order, "song.", "sv.");
7362 joinFilter.AppendOrder(order);
7363 }
7364 else
7365 joinFilter.AppendOrder("sv.idSong");
7366 joinFilter.AppendGroup("sv.idSong");
7367
7368 // Album artists
7369 if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) ||
7370 joinLayout.GetFetch(joinToSongs_strAlbumArtist) ||
7371 joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID))
7372 { // All songs have at least one album artist so inner join sufficient
7373 bJoinAlbumArtist = true;
7374 joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
7375 joinFilter.AppendGroup("album_artist.idArtist");
7376 joinFilter.AppendOrder("album_artist.iOrder");
7377 // Ensure idAlbumArtist is queried for processing repeats
7378 if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist))
7379 {
7380 joinLayout.SetField(joinToSongs_idAlbumArtist,
7381 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL);
7382 }
7383 // Ensure song.IdAlbum is field of the inline view for join
7384 if (fields.find("albumid") == fields.end())
7385 {
7386 extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
7387 dbfieldindex.emplace_back(-1);
7388 }
7389 // artist table needed for strArtist or MBID
7390 // (album_artist.strArtist can be an alias or spelling variation)
7391 if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) || joinLayout.GetFetch(joinToSongs_strAlbumArtist))
7392 joinFilter.AppendJoin("JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
7393 }
7394
7395 /*
7396 Song artists
7397 JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
7398 "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
7399 */
7400 if (joinLayout.GetFetch(joinToSongs_idArtist) ||
7401 joinLayout.GetFetch(joinToSongs_strArtist) ||
7402 joinLayout.GetFetch(joinToSongs_strArtistMBID) ||
7403 !rolefieldlist.empty())
7404 { // All songs have at least one artist (idRole = 1) so inner join sufficient
7405 bJoinSongArtist = true;
7406 if (rolefieldlist.empty())
7407 { // song artists only, no other roles needed
7408 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
7409 joinFilter.AppendGroup("song_artist.idArtist");
7410 joinFilter.AppendOrder("song_artist.iOrder");
7411 }
7412 else
7413 {
7414 // Ensure idRole is queried
7415 if (!joinLayout.GetFetch(joinToSongs_idRole))
7416 {
7417 joinLayout.SetField(joinToSongs_idRole,
7418 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL);
7419 }
7420 // Ensure strArtist is queried
7421 if (!joinLayout.GetFetch(joinToSongs_strArtist))
7422 {
7423 joinLayout.SetField(joinToSongs_strArtist,
7424 JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL);
7425 }
7426 if (fields.find("contributors") != fields.end())
7427 { // all roles
7428 bJoinRole = true;
7429 // Ensure strRole is queried from role table
7430 joinLayout.SetField(joinToSongs_strRole, "role.strRole");
7431 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
7432 joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
7433 joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
7434 joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
7435 }
7436 else
7437 { // Get just roles for "displaycomposer", "displayconductor" etc.
7438 std::string where;
7439 for (size_t i = 0; i < roleidlist.size(); i++)
7440 {
7441 int idRole = roleidlist[i];
7442 if (idRole <= 1)
7443 continue;
7444 if (where.empty())
7445 // Always get song artists too (role = 1) so can do inner join
7446 where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole);
7447 else
7448 where += PrepareSQL(" OR song_artist.idRole = %i", idRole);
7449 }
7450 where = " (" + where + ")";
7451 joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where);
7452 joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
7453 joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
7454 }
7455 }
7456 // Ensure idArtist is queried for processing repeats
7457 if (!joinLayout.GetFetch(joinToSongs_idArtist))
7458 {
7459 joinLayout.SetField(joinToSongs_idArtist,
7460 JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL);
7461 }
7462 // artist table needed for strArtist or MBID
7463 // (song_artist.strArtist can be an alias or spelling variation)
7464 if (joinLayout.GetFetch(joinToSongs_strArtistMBID) || joinLayout.GetFetch(joinToSongs_strArtist))
7465 joinFilter.AppendJoin("JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
7466 }
7467
7468 // Genre ids
7469 if (joinLayout.GetFetch(joinToSongs_idGenre))
7470 { // song genre ids (strGenre demormalised in song table)
7471 // Left join as songs may not have genre
7472 joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
7473 joinFilter.AppendGroup("song_genre.idGenre");
7474 joinFilter.AppendOrder("song_genre.iOrder");
7475 }
7476
7477 // Build JOIN part of query (if we have one)
7478 std::string strSQLJoin;
7479 if (joinLayout.HasFilterFields())
7480 if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
7481 return false;
7482
7483 // Adjust where in the results record the join fields are allowing for the
7484 // inline view fields (Quicker than finding field by name every time)
7485 // idSong + other song fields
7486 joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
7487
7488 // Build full query
7489 // When have multiple value joins use inline view
7490 // SELECT sv.*, <join fields> FROM
7491 // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv
7492 // <joins> <group by>
7493 // <order by> + <joins order by>
7494 // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
7495 strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra;
7496 if (joinLayout.HasFilterFields())
7497 {
7498 strSQL = "("+ strSQL + ") AS sv ";
7499 strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
7500 }
7501
7502 // Modify query to use correct year field
7503 if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
7504 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
7505 StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate");
7506 else
7507 StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate");
7508
7509 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
7510
7511 // Run query
7512 unsigned int time = XbmcThreads::SystemClockMillis();
7513 if (!m_pDS->query(strSQL))
7514 return false;
7515 CLog::Log(LOGDEBUG, "%s - query took %i ms",
7516 __FUNCTION__, XbmcThreads::SystemClockMillis() - time);
7517
7518 int iRowsFound = m_pDS->num_rows();
7519 if (iRowsFound <= 0)
7520 {
7521 m_pDS->close();
7522 return true;
7523 }
7524
7525 // Get song from returned rows. Joins mean there can be many rows per song
7526 int songId = -1;
7527 int albumartistId = -1;
7528 int artistId = -1;
7529 int roleId = -1;
7530 bool bSongGenreDone(false);
7531 bool bSongArtistDone(false);
7532 bool bHaveSong(false);
7533 CVariant songObj;
7534 result["songs"].reserve(resultcount);
7535 while (!m_pDS->eof() || bHaveSong)
7536 {
7537 const dbiplus::sql_record* const record = m_pDS->get_sql_record();
7538
7539 if (m_pDS->eof() || songId != record->at(0).get_asInt())
7540 {
7541 // Store previous or last song
7542 if (bHaveSong)
7543 {
7544 // Check empty role fields get returned, and format
7545 if (!rolefieldlist.empty())
7546 {
7547 for (const auto& displayXXX : rolefieldlist)
7548 {
7549 if (!StringUtils::StartsWith(displayXXX, "display"))
7550 {
7551 // "contributors"
7552 if (!songObj.isMember(displayXXX))
7553 songObj[displayXXX] = CVariant(CVariant::VariantTypeArray);
7554 }
7555 else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray())
7556 {
7557 // Convert "displaycomposer", "displayconductor", "displayorchestra",
7558 // and "displaylyricist" arrays into strings
7559 std::vector<std::string> names;
7560 for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array();
7561 field != songObj[displayXXX].end_array(); field++)
7562 names.emplace_back(field->asString());
7563
7564 std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent()
7565 ->GetAdvancedSettings()
7566 ->m_musicItemSeparator);
7567 songObj[displayXXX] = role;
7568 }
7569 else
7570 songObj[displayXXX] = "";
7571 }
7572 }
7573 result["songs"].append(songObj);
7574 bHaveSong = false;
7575 songObj.clear();
7576 }
7577 if (songObj.empty())
7578 {
7579 // Initialise fields, ensure those with possible null values are set to correct empty variant type
7580 if (joinLayout.GetOutput(joinToSongs_idGenre))
7581 songObj["genreid"] = CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array
7582
7583 albumartistId = -1;
7584 artistId = -1;
7585 roleId = -1;
7586 bSongGenreDone = false;
7587 bSongArtistDone = false;
7588 }
7589 if (m_pDS->eof())
7590 continue; // Having saved the last song stop
7591
7592 // New song
7593 songId = record->at(0).get_asInt();
7594 bHaveSong = true;
7595 songObj["songid"] = songId;
7596 songObj["label"] = record->at(1).get_asString();
7597 for (size_t i = 0; i < dbfieldindex.size(); i++)
7598 if (dbfieldindex[i] > -1)
7599 {
7600 if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer")
7601 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
7602 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned")
7603 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asInt(), 0);
7604 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float")
7605 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = std::max(record->at(1 + i).get_asFloat(), 0.f);
7606 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array")
7607 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split(record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
7608 else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean")
7609 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
7610 else
7611 songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
7612 }
7613
7614 // Split sources string into int array
7615 if (songObj.isMember("sourceid"))
7616 {
7617 std::vector<std::string> sources = StringUtils::Split(songObj["sourceid"].asString(), ";");
7618 songObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
7619 for (size_t i = 0; i < sources.size(); i++)
7620 songObj["sourceid"].append(atoi(sources[i].c_str()));
7621 }
7622 }
7623
7624 if (bJoinAlbumArtist)
7625 {
7626 if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt())
7627 {
7628 bSongGenreDone = bSongGenreDone || (albumartistId > 0); // Not first album artist, skip genre
7629 bSongArtistDone = bSongArtistDone || (albumartistId > 0); // Not first album artist, skip song artists
7630 albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt();
7631 if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
7632 songObj["albumartistid"].append(albumartistId);
7633 if (albumartistId == BLANKARTIST_ID)
7634 {
7635 if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
7636 songObj["albumartist"].append(StringUtils::Empty);
7637 if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
7638 songObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
7639 }
7640 else
7641 {
7642 if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
7643 songObj["albumartistid"].append(albumartistId);
7644 if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
7645 songObj["albumartist"].append(record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString());
7646 if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
7647 songObj["musicbrainzalbumartistid"].append(record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString());
7648 }
7649 }
7650 }
7651 if (bJoinSongArtist && !bSongArtistDone)
7652 {
7653 if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt())
7654 {
7655 bSongGenreDone = bSongGenreDone || (artistId > 0); // Not first artist, skip genre
7656 roleId = -1; // Allow for many artists same role
7657 artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
7658 if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 ||
7659 record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1)
7660 {
7661 if (joinLayout.GetOutput(joinToSongs_idArtist))
7662 songObj["artistid"].append(artistId);
7663 if (artistId == BLANKARTIST_ID)
7664 {
7665 if (joinLayout.GetOutput(joinToSongs_strArtist))
7666 songObj["artist"].append(StringUtils::Empty);
7667 if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
7668 songObj["musicbrainzartistid"].append(StringUtils::Empty);
7669 }
7670 else
7671 {
7672 if (joinLayout.GetOutput(joinToSongs_strArtist))
7673 songObj["artist"].append(record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
7674 if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
7675 songObj["musicbrainzartistid"].append(record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString());
7676 }
7677 }
7678 }
7679 if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 &&
7680 roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt())
7681 {
7682 bSongGenreDone = bSongGenreDone || (roleId > 0); // Not first role, skip genre
7683 roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt();
7684 if (roleId > 1)
7685 {
7686 if (bJoinRole)
7687 { //Contributors
7688 CVariant contributor;
7689 contributor["name"] = record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString();
7690 contributor["role"] = record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString();
7691 contributor["roleid"] = roleId;
7692 contributor["artistid"] = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
7693 songObj["contributors"].append(contributor);
7694 }
7695 // "displaycomposer", "displayconductor" etc.
7696 for (size_t i = 0; i < roleidlist.size(); i++)
7697 {
7698 if (roleidlist[i] == roleId)
7699 {
7700 songObj[rolefieldlist[i]].append(record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
7701 continue;
7702 }
7703 }
7704 }
7705 }
7706 }
7707 if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 &&
7708 !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull())
7709 {
7710 songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt());
7711 }
7712 m_pDS->next();
7713 }
7714 m_pDS->close(); // cleanup recordset data
7715
7716 // Ensure random order of output when results set is sorted to process multi-value joins
7717 if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
7718 KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array());
7719
7720 return true;
7721 }
7722 catch (...)
7723 {
7724 m_pDS->close();
7725 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
7726 }
7727 return false;
7728 }
7729
GetIgnoreArticleSQL(const std::string & strField)7730 std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField)
7731 {
7732 /*
7733 Make SQL clause from ignore article list.
7734 Group tokens the same length together, for example :
7735 WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
7736 THEN SUBSTR(strArtist, 5)
7737 WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
7738 THEN SUBSTR(strArtist, 4)
7739 */
7740 std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
7741 std::string sortclause;
7742 size_t tokenlength = 0;
7743 std::string strWhen;
7744 for (const auto& token : sortTokens)
7745 {
7746 if (token.length() != tokenlength)
7747 {
7748 if (!strWhen.empty())
7749 {
7750 if (!sortclause.empty())
7751 sortclause += " ";
7752 std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
7753 sortclause += "WHEN " + strWhen + strThen;
7754 strWhen.clear();
7755 }
7756 tokenlength = token.length();
7757 }
7758 std::string tokenclause = token;
7759 //Escape any ' or % in the token
7760 StringUtils::Replace(tokenclause, "'", "''");
7761 StringUtils::Replace(tokenclause, "%", "%%");
7762 // Single %, _ and ' so avoid using PrepareSQL
7763 tokenclause = strField + " LIKE '" + tokenclause + "%'";
7764 if (token.find('_') != std::string::npos)
7765 tokenclause += " ESCAPE '_'";
7766 if (!strWhen.empty())
7767 strWhen += " OR ";
7768 strWhen += tokenclause;
7769 }
7770 if (!strWhen.empty())
7771 {
7772 if (!sortclause.empty())
7773 sortclause += " ";
7774 std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
7775 sortclause += "WHEN " + strWhen + strThen;
7776 }
7777 return sortclause;
7778 }
7779
SortnameBuildSQL(const std::string & strAlias,const SortAttribute & sortAttributes,const std::string & strField,const std::string & strSortField)7780 std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias,
7781 const SortAttribute& sortAttributes,
7782 const std::string& strField,
7783 const std::string& strSortField)
7784 {
7785 /*
7786 Build SQL for sort name scalar subquery from sort attributes and ignore article list.
7787 For example :
7788 CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
7789 WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
7790 WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
7791 ELSE strField
7792 END AS strAlias
7793 */
7794
7795 std::string sortSQL;
7796 if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName)
7797 sortSQL =
7798 PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str());
7799 if (sortAttributes & SortAttributeIgnoreArticle)
7800 {
7801 if (!sortSQL.empty())
7802 sortSQL += " ";
7803 // Make SQL from ignore article list, grouping tokens the same length together
7804 sortSQL += GetIgnoreArticleSQL(strField);
7805 }
7806 if (!sortSQL.empty())
7807 {
7808 sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc.
7809 sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str());
7810 }
7811
7812 return sortSQL;
7813 }
7814
AlphanumericSortSQL(const std::string & strField,const SortOrder & sortOrder)7815 std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField, const SortOrder& sortOrder)
7816 {
7817 /*
7818 Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
7819 and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
7820 Natural number order is not significant for where clause comparison and use of calculated fields
7821 means there is no advantage in defining as column defualt in table create than per query (which
7822 also makes looking at the db with other tools difficult).
7823
7824 MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
7825 "ascii folding" case insensitive collation. Natural sorting is provided via native functions
7826 stored in the db.
7827 */
7828 std::string DESC;
7829 if (sortOrder == SortOrderDescending)
7830 DESC = " DESC";
7831 std::string strSort;
7832
7833 if (StringUtils::EqualsNoCase(
7834 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
7835 "mysql"))
7836 strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str());
7837 else
7838 strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str());
7839 return strSort;
7840 }
7841
UpdateTables(int version)7842 void CMusicDatabase::UpdateTables(int version)
7843 {
7844 CLog::Log(LOGINFO, "%s - updating tables", __FUNCTION__);
7845 if (version < 34)
7846 {
7847 m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
7848 m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
7849 m_pDS->exec("CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, lastplayed varchar(20) default NULL, rating char default '0', comment text)\n");
7850 m_pDS->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment FROM song");
7851
7852 m_pDS->exec("DROP TABLE song");
7853 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
7854
7855 m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
7856 }
7857
7858 if (version < 36)
7859 {
7860 // translate legacy musicdb:// paths
7861 if (m_pDS->query("SELECT strPath FROM content"))
7862 {
7863 std::vector<std::string> contentPaths;
7864 while (!m_pDS->eof())
7865 {
7866 contentPaths.push_back(m_pDS->fv(0).get_asString());
7867 m_pDS->next();
7868 }
7869 m_pDS->close();
7870
7871 for (const auto &originalPath : contentPaths)
7872 {
7873 std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath);
7874 m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(), originalPath.c_str()));
7875 }
7876 }
7877 }
7878
7879 if (version < 39)
7880 {
7881 m_pDS->exec("CREATE TABLE album_new "
7882 "(idAlbum integer primary key, "
7883 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
7884 " strArtists text, strGenres text, "
7885 " iYear integer, idThumb integer, "
7886 " bCompilation integer not null default '0', "
7887 " strMoods text, strStyles text, strThemes text, "
7888 " strReview text, strImage text, strLabel text, "
7889 " strType text, "
7890 " iRating integer, "
7891 " lastScraped varchar(20) default NULL, "
7892 " dateAdded varchar (20) default NULL)");
7893 m_pDS->exec("INSERT INTO album_new "
7894 "(idAlbum, "
7895 " strAlbum, strMusicBrainzAlbumID, "
7896 " strArtists, strGenres, "
7897 " iYear, idThumb, "
7898 " bCompilation, "
7899 " strMoods, strStyles, strThemes, "
7900 " strReview, strImage, strLabel, "
7901 " strType, "
7902 " iRating) "
7903 " SELECT "
7904 " album.idAlbum, "
7905 " strAlbum, strMusicBrainzAlbumID, "
7906 " strArtists, strGenres, "
7907 " album.iYear, idThumb, "
7908 " bCompilation, "
7909 " strMoods, strStyles, strThemes, "
7910 " strReview, strImage, strLabel, "
7911 " strType, iRating "
7912 " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
7913 m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
7914 m_pDS->exec(PrepareSQL("UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)", CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
7915 m_pDS->exec("DROP TABLE album");
7916 m_pDS->exec("DROP TABLE albuminfo");
7917 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
7918 }
7919 if (version < 40)
7920 {
7921 m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
7922 " strArtist varchar(256), strMusicBrainzArtistID text, "
7923 " strBorn text, strFormed text, strGenres text, strMoods text, "
7924 " strStyles text, strInstruments text, strBiography text, "
7925 " strDied text, strDisbanded text, strYearsActive text, "
7926 " strImage text, strFanart text, "
7927 " lastScraped varchar(20) default NULL, "
7928 " dateAdded varchar (20) default NULL)");
7929 m_pDS->exec("INSERT INTO artist_new "
7930 "(idArtist, strArtist, strMusicBrainzArtistID, "
7931 " strBorn, strFormed, strGenres, strMoods, "
7932 " strStyles , strInstruments , strBiography , "
7933 " strDied, strDisbanded, strYearsActive, "
7934 " strImage, strFanart) "
7935 " SELECT "
7936 " artist.idArtist, "
7937 " strArtist, strMusicBrainzArtistID, "
7938 " strBorn, strFormed, strGenres, strMoods, "
7939 " strStyles, strInstruments, strBiography, "
7940 " strDied, strDisbanded, strYearsActive, "
7941 " strImage, strFanart "
7942 " FROM artist "
7943 " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
7944 m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT idArtist FROM artistinfo)", CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
7945 m_pDS->exec("DROP TABLE artist");
7946 m_pDS->exec("DROP TABLE artistinfo");
7947 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
7948 }
7949 if (version < 42)
7950 {
7951 m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n");
7952 m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n");
7953 // populate these
7954 std::string sql = "select idArtist,strArtist from artist";
7955 m_pDS->query(sql);
7956 while (!m_pDS->eof())
7957 {
7958 m_pDS2->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i", m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
7959 m_pDS2->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i", m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
7960 m_pDS->next();
7961 }
7962 }
7963 if (version < 48)
7964 { // null out columns that are no longer used
7965 m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
7966 m_pDS->exec("UPDATE album SET idThumb=NULL");
7967 }
7968 if (version < 49)
7969 {
7970 m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
7971 }
7972 if (version < 50)
7973 {
7974 // add a new column strReleaseType for albums
7975 m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n");
7976
7977 // set strReleaseType based on album name
7978 m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''", CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
7979 m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''", CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
7980 }
7981 if (version < 51)
7982 {
7983 m_pDS->exec("ALTER TABLE song ADD mood text\n");
7984 }
7985 if (version < 53)
7986 {
7987 m_pDS->exec("ALTER TABLE song ADD dateAdded text");
7988 }
7989 if (version < 54)
7990 {
7991 //Remove dateAdded from artist table
7992 m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
7993 " strArtist varchar(256), strMusicBrainzArtistID text, "
7994 " strBorn text, strFormed text, strGenres text, strMoods text, "
7995 " strStyles text, strInstruments text, strBiography text, "
7996 " strDied text, strDisbanded text, strYearsActive text, "
7997 " strImage text, strFanart text, "
7998 " lastScraped varchar(20) default NULL)");
7999 m_pDS->exec("INSERT INTO artist_new "
8000 "(idArtist, strArtist, strMusicBrainzArtistID, "
8001 " strBorn, strFormed, strGenres, strMoods, "
8002 " strStyles , strInstruments , strBiography , "
8003 " strDied, strDisbanded, strYearsActive, "
8004 " strImage, strFanart, lastScraped) "
8005 " SELECT "
8006 " idArtist, "
8007 " strArtist, strMusicBrainzArtistID, "
8008 " strBorn, strFormed, strGenres, strMoods, "
8009 " strStyles, strInstruments, strBiography, "
8010 " strDied, strDisbanded, strYearsActive, "
8011 " strImage, strFanart, lastScraped "
8012 " FROM artist");
8013 m_pDS->exec("DROP TABLE artist");
8014 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8015
8016 //Remove dateAdded from album table
8017 m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8018 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8019 " strArtists text, strGenres text, "
8020 " iYear integer, idThumb integer, "
8021 " bCompilation integer not null default '0', "
8022 " strMoods text, strStyles text, strThemes text, "
8023 " strReview text, strImage text, strLabel text, "
8024 " strType text, "
8025 " iRating integer, "
8026 " lastScraped varchar(20) default NULL, "
8027 " strReleaseType text)");
8028 m_pDS->exec("INSERT INTO album_new "
8029 "(idAlbum, "
8030 " strAlbum, strMusicBrainzAlbumID, "
8031 " strArtists, strGenres, "
8032 " iYear, idThumb, "
8033 " bCompilation, "
8034 " strMoods, strStyles, strThemes, "
8035 " strReview, strImage, strLabel, "
8036 " strType, iRating, lastScraped, "
8037 " strReleaseType) "
8038 " SELECT "
8039 " album.idAlbum, "
8040 " strAlbum, strMusicBrainzAlbumID, "
8041 " strArtists, strGenres, "
8042 " iYear, idThumb, "
8043 " bCompilation, "
8044 " strMoods, strStyles, strThemes, "
8045 " strReview, strImage, strLabel, "
8046 " strType, iRating, lastScraped, "
8047 " strReleaseType"
8048 " FROM album");
8049 m_pDS->exec("DROP TABLE album");
8050 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8051 }
8052 if (version < 55)
8053 {
8054 m_pDS->exec("DROP TABLE karaokedata");
8055 }
8056 if (version < 57)
8057 {
8058 m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
8059 m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
8060 m_pDS->exec("UPDATE song SET userrating = rating * 2");
8061 m_pDS->exec("UPDATE song SET rating = 0");
8062 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8063 " idAlbum INTEGER, idPath INTEGER, "
8064 " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8065 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8066 " dwFileNameCRC TEXT, "
8067 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8068 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8069 " idThumb INTEGER, "
8070 " lastplayed VARCHAR(20) DEFAULT NULL, "
8071 " rating FLOAT DEFAULT 0, "
8072 " userrating INTEGER DEFAULT 0, "
8073 " comment TEXT, mood TEXT, dateAdded TEXT)");
8074 m_pDS->exec("INSERT INTO song_new "
8075 "(idSong, "
8076 " idAlbum, idPath, "
8077 " strArtists, strGenres, strTitle, "
8078 " iTrack, iDuration, iYear, "
8079 " dwFileNameCRC, "
8080 " strFileName, strMusicBrainzTrackID, "
8081 " iTimesPlayed, iStartOffset, iEndOffset, "
8082 " idThumb, "
8083 " lastplayed,"
8084 " rating, userrating, "
8085 " comment, mood, dateAdded)"
8086 " SELECT "
8087 " idSong, "
8088 " idAlbum, idPath, "
8089 " strArtists, strGenres, strTitle, "
8090 " iTrack, iDuration, iYear, "
8091 " dwFileNameCRC, "
8092 " strFileName, strMusicBrainzTrackID, "
8093 " iTimesPlayed, iStartOffset, iEndOffset, "
8094 " idThumb, "
8095 " lastplayed,"
8096 " rating, "
8097 " userrating, "
8098 " comment, mood, dateAdded"
8099 " FROM song");
8100 m_pDS->exec("DROP TABLE song");
8101 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8102
8103 m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
8104 m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
8105 m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8106 " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8107 " strArtists TEXT, strGenres TEXT, "
8108 " iYear INTEGER, idThumb INTEGER, "
8109 " bCompilation INTEGER NOT NULL DEFAULT '0', "
8110 " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8111 " strReview TEXT, strImage TEXT, strLabel TEXT, "
8112 " strType TEXT, "
8113 " fRating FLOAT NOT NULL DEFAULT 0, "
8114 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8115 " lastScraped VARCHAR(20) DEFAULT NULL, "
8116 " strReleaseType TEXT)");
8117 m_pDS->exec("INSERT INTO album_new "
8118 "(idAlbum, "
8119 " strAlbum, strMusicBrainzAlbumID, "
8120 " strArtists, strGenres, "
8121 " iYear, idThumb, "
8122 " bCompilation, "
8123 " strMoods, strStyles, strThemes, "
8124 " strReview, strImage, strLabel, "
8125 " strType, "
8126 " fRating, "
8127 " iUserrating, "
8128 " lastScraped, "
8129 " strReleaseType)"
8130 " SELECT "
8131 " idAlbum, "
8132 " strAlbum, strMusicBrainzAlbumID, "
8133 " strArtists, strGenres, "
8134 " iYear, idThumb, "
8135 " bCompilation, "
8136 " strMoods, strStyles, strThemes, "
8137 " strReview, strImage, strLabel, "
8138 " strType, "
8139 " iRating, "
8140 " iUserrating, "
8141 " lastScraped, "
8142 " strReleaseType"
8143 " FROM album");
8144 m_pDS->exec("DROP TABLE album");
8145 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8146
8147 m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
8148 m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
8149 }
8150 if (version < 58)
8151 {
8152 m_pDS->exec("UPDATE album SET fRating = fRating * 2");
8153 }
8154 if (version < 59)
8155 {
8156 m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
8157 m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
8158
8159 //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
8160 m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, iOrder integer, strArtist text)");
8161 m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
8162 "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
8163 m_pDS->exec("DROP TABLE song_artist");
8164 m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
8165
8166 //Remove strJoinPhrase, boolFeatured from album_artist table
8167 m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, strArtist text)");
8168 m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
8169 "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
8170 m_pDS->exec("DROP TABLE album_artist");
8171 m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
8172 }
8173 if (version < 60)
8174 {
8175 // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
8176 // do not have an artist tag to ensure all songs in the library have at least one artist.
8177 std::string strSQL;
8178 if (GetArtistExists(BLANKARTIST_ID))
8179 {
8180 // When BLANKARTIST_ID (=1) is already in use, move the record
8181 try
8182 { //No mbid index yet, so can have record for artist twice even with mbid
8183 strSQL = PrepareSQL("INSERT INTO artist SELECT null, "
8184 "strArtist, strMusicBrainzArtistID, "
8185 "strBorn, strFormed, strGenres, strMoods, "
8186 "strStyles, strInstruments, strBiography, "
8187 "strDied, strDisbanded, strYearsActive, "
8188 "strImage, strFanart, lastScraped "
8189 "FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
8190 m_pDS->exec(strSQL);
8191 int idArtist = (int)m_pDS->lastinsertid();
8192 //No triggers, so can delete artist without effecting other tables.
8193 strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
8194 m_pDS->exec(strSQL);
8195
8196 // Update related tables with the new artist ID
8197 // Indices have been dropped making transactions very slow, so create appropriate temp indices
8198 m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
8199 m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
8200 m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
8201 m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
8202 strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist, BLANKARTIST_ID);
8203 m_pDS->exec(strSQL);
8204 strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist, BLANKARTIST_ID);
8205 m_pDS->exec(strSQL);
8206 strSQL = PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'", idArtist, BLANKARTIST_ID);
8207 m_pDS->exec(strSQL);
8208 strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist, BLANKARTIST_ID);
8209 m_pDS->exec(strSQL);
8210 // Drop temp indices
8211 m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist");
8212 m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
8213 m_pDS->exec("DROP INDEX idxDiscography ON discography");
8214 m_pDS->exec("DROP INDEX ix_art ON art");
8215 }
8216 catch (...)
8217 {
8218 CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed");
8219 }
8220 }
8221
8222 // Create missing artist tag artist [Missing].
8223 // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
8224 strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
8225 BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
8226 m_pDS->exec(strSQL);
8227
8228 // Indices have been dropped making transactions very slow, so create temp index
8229 m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
8230 m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
8231
8232 // Ensure all songs have at least one artist, set those without to [Missing]
8233 strSQL = "SELECT count(idSong) FROM song "
8234 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8235 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
8236 int numsongs = GetSingleValueInt(strSQL);
8237 if (numsongs > 0)
8238 {
8239 CLog::Log(LOGDEBUG, "%i songs have no artist, setting artist to [Missing]", numsongs);
8240 // Insert song_artist records for songs that don't have any
8241 try
8242 {
8243 strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
8244 "SELECT %i, idSong, %i, '%s', 0 FROM song "
8245 "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
8246 "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
8247 BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST);
8248 ExecuteQuery(strSQL);
8249 }
8250 catch (...)
8251 {
8252 CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed");
8253 }
8254 }
8255
8256 // Ensure all albums have at least one artist, set those without to [Missing]
8257 strSQL = "SELECT count(idAlbum) FROM album "
8258 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8259 "WHERE album_artist.idAlbum = album.idAlbum)";
8260 int numalbums = GetSingleValueInt(strSQL);
8261 if (numalbums > 0)
8262 {
8263 CLog::Log(LOGDEBUG, "%i albums have no artist, setting artist to [Missing]", numalbums);
8264 // Insert album_artist records for albums that don't have any
8265 try
8266 {
8267 strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
8268 "SELECT %i, idAlbum, '%s', 0 FROM album "
8269 "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
8270 "WHERE album_artist.idAlbum = album.idAlbum)",
8271 BLANKARTIST_ID, BLANKARTIST_NAME.c_str());
8272 ExecuteQuery(strSQL);
8273 }
8274 catch (...)
8275 {
8276 CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed");
8277 }
8278 }
8279 //Remove temp indices, full analytics for database created later
8280 m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist");
8281 m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
8282 }
8283 if (version < 61)
8284 {
8285 // Create versiontagscan table
8286 m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
8287 m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
8288 }
8289 if (version < 62)
8290 {
8291 CLog::Log(LOGINFO, "create audiobook table");
8292 m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
8293 " strBook varchar(256), strAuthor text,"
8294 " bookmark integer, file text,"
8295 " dateAdded varchar (20) default NULL)");
8296 }
8297 if (version < 63)
8298 {
8299 // Add strSortName to Artist table
8300 m_pDS->exec("ALTER TABLE artist ADD strSortName text\n");
8301
8302 //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
8303 m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
8304 " strAlbum varchar(256), strMusicBrainzAlbumID text, "
8305 " strArtistDisp text, strArtistSort text, strGenres text, "
8306 " iYear integer, bCompilation integer not null default '0', "
8307 " strMoods text, strStyles text, strThemes text, "
8308 " strReview text, strImage text, strLabel text, "
8309 " strType text, "
8310 " fRating FLOAT NOT NULL DEFAULT 0, "
8311 " iUserrating INTEGER NOT NULL DEFAULT 0, "
8312 " lastScraped varchar(20) default NULL, "
8313 " strReleaseType text, "
8314 " iVotes INTEGER NOT NULL DEFAULT 0)");
8315 m_pDS->exec("INSERT INTO album_new "
8316 "(idAlbum, "
8317 " strAlbum, strMusicBrainzAlbumID, "
8318 " strArtistDisp, strArtistSort, strGenres, "
8319 " iYear, bCompilation, "
8320 " strMoods, strStyles, strThemes, "
8321 " strReview, strImage, strLabel, "
8322 " strType, "
8323 " fRating, iUserrating, iVotes, "
8324 " lastScraped, "
8325 " strReleaseType)"
8326 " SELECT "
8327 " idAlbum, "
8328 " strAlbum, strMusicBrainzAlbumID, "
8329 " strArtists, NULL, strGenres, "
8330 " iYear, bCompilation, "
8331 " strMoods, strStyles, strThemes, "
8332 " strReview, strImage, strLabel, "
8333 " strType, "
8334 " fRating, iUserrating, iVotes, "
8335 " lastScraped, "
8336 " strReleaseType"
8337 " FROM album");
8338 m_pDS->exec("DROP TABLE album");
8339 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8340
8341 //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
8342 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8343 " idAlbum INTEGER, idPath INTEGER, "
8344 " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8345 " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
8346 " strFileName TEXT, strMusicBrainzTrackID TEXT, "
8347 " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8348 " lastplayed VARCHAR(20) DEFAULT NULL, "
8349 " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
8350 " userrating INTEGER NOT NULL DEFAULT 0, "
8351 " comment TEXT, mood TEXT, dateAdded TEXT)");
8352 m_pDS->exec("INSERT INTO song_new "
8353 "(idSong, "
8354 " idAlbum, idPath, "
8355 " strArtistDisp, strArtistSort, strGenres, strTitle, "
8356 " iTrack, iDuration, iYear, "
8357 " strFileName, strMusicBrainzTrackID, "
8358 " iTimesPlayed, iStartOffset, iEndOffset, "
8359 " lastplayed,"
8360 " rating, userrating, votes, "
8361 " comment, mood, dateAdded)"
8362 " SELECT "
8363 " idSong, "
8364 " idAlbum, idPath, "
8365 " strArtists, NULL, strGenres, strTitle, "
8366 " iTrack, iDuration, iYear, "
8367 " strFileName, strMusicBrainzTrackID, "
8368 " iTimesPlayed, iStartOffset, iEndOffset, "
8369 " lastplayed,"
8370 " rating, userrating, votes, "
8371 " comment, mood, dateAdded"
8372 " FROM song");
8373 m_pDS->exec("DROP TABLE song");
8374 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8375 }
8376 if (version < 65)
8377 {
8378 // Remove cue table
8379 m_pDS->exec("DROP TABLE cue");
8380 // Add strReplayGain to song table
8381 m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
8382 }
8383 if (version < 66)
8384 {
8385 // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
8386 m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
8387 m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
8388 // Add a new column bScrapedMBID for artists
8389 m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
8390 }
8391 if (version < 67)
8392 {
8393 // Add infosetting table
8394 m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, strSettings TEXT)");
8395 // Add a new column for setting to album and artist tables
8396 m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
8397 m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
8398
8399 // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
8400 m_pDS->exec("CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
8401 "strScraperPath text, strSettings text)");
8402 try
8403 {
8404 m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
8405 "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, strScraperPath, strSettings "
8406 "FROM content WHERE strContent = 'artists' AND strPath LIKE 'musicdb://artists/_%/' ORDER BY idItem"
8407 );
8408 }
8409 catch (...)
8410 {
8411 CLog::Log(LOGERROR, "Migrating specific artist scraper settings has failed, settings not transferred");
8412 }
8413 try
8414 {
8415 m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
8416 "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, strScraperPath, strSettings "
8417 "FROM content WHERE strContent = 'albums' AND strPath LIKE 'musicdb://albums/_%/' ORDER BY idItem"
8418 );
8419 }
8420 catch (...)
8421 {
8422 CLog::Log(LOGERROR, "Migrating specific album scraper settings has failed, settings not transferred");
8423 }
8424 try
8425 {
8426 m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
8427 "SELECT id, strScraperPath, strSettings FROM content_temp");
8428 m_pDS->exec("UPDATE artist SET idInfoSetting = "
8429 "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
8430 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) ");
8431 m_pDS->exec("UPDATE album SET idInfoSetting = "
8432 "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
8433 "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) ");
8434 }
8435 catch (...)
8436 {
8437 CLog::Log(LOGERROR, "Migrating album and artist scraper settings has failed, settings not transferred");
8438 }
8439 m_pDS->exec("DROP TABLE content_temp");
8440
8441 // Remove content table
8442 m_pDS->exec("DROP TABLE content");
8443 // Remove albuminfosong table
8444 m_pDS->exec("DROP TABLE albuminfosong");
8445 }
8446 if (version < 68)
8447 {
8448 // Add a new columns strType, strGender, strDisambiguation for artists
8449 m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n");
8450 m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n");
8451 m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
8452 }
8453 if (version < 69)
8454 {
8455 // Remove album_genre table
8456 m_pDS->exec("DROP TABLE album_genre");
8457 }
8458 if (version < 70)
8459 {
8460 // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
8461 m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * 40 / 3 \n");
8462 }
8463 if (version < 71)
8464 {
8465 // Add lastscanned to versiontagscan table
8466 m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
8467 CDateTime dateAdded = CDateTime::GetCurrentDateTime();
8468 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'", dateAdded.GetAsDBDateTime().c_str()));
8469 }
8470 if (version < 72)
8471 {
8472 // Create source table
8473 m_pDS->exec("CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
8474 // Create source_path table
8475 m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
8476 // Create album_source table
8477 m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
8478 // Populate source and source_path tables from sources.xml
8479 // Filling album_source needs to be done after indexes are created or it is
8480 // very slow. It could be populated during CreateAnalytics but it is checked
8481 // and filled as part of scanning anyway so simply force full rescan.
8482 MigrateSources();
8483 }
8484 if (version < 73)
8485 {
8486 // add bBoxedSet to album table
8487 m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
8488 // add iDiscTotal to album table
8489 m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
8490 // populate iDiscTotal from the data already in the song table
8491 m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
8492 "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
8493 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
8494 // add strDiscSubtitles to song table
8495 m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
8496 }
8497 if (version < 74)
8498 {
8499 //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
8500 m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
8501 "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
8502 "strReleaseGroupMBID TEXT, "
8503 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
8504 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
8505 "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
8506 "bCompilation INTEGER NOT NULL DEFAULT '0', "
8507 "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
8508 "strReview TEXT, strImage TEXT, strLabel TEXT, "
8509 "strType TEXT, "
8510 "fRating FLOAT NOT NULL DEFAULT 0, "
8511 "iVotes INTEGER NOT NULL DEFAULT 0, "
8512 "iUserrating INTEGER NOT NULL DEFAULT 0, "
8513 "lastScraped VARCHAR(20) DEFAULT NULL, "
8514 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
8515 "strReleaseType TEXT, "
8516 "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
8517 "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
8518 // Prepare as MySQL has different CAST datatypes
8519 m_pDS->exec(PrepareSQL("INSERT INTO album_new "
8520 "(idalbum, strAlbum, "
8521 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
8522 "strArtistDisp, strArtistSort, strGenres, "
8523 "strReleaseDate, strOrigReleaseDate, "
8524 "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
8525 "strReview, strImage, strLabel, strType, "
8526 "fRating, iVotes, iUserrating, "
8527 "lastScraped, bScrapedMBID, strReleaseType, "
8528 "iDiscTotal, idInfoSetting) "
8529 "SELECT "
8530 "idAlbum, strAlbum, "
8531 "strMusicBrainzAlbumID, strReleaseGroupMBID, "
8532 "strArtistDisp, strArtistSort, strGenres, "
8533 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8534 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8535 // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
8536 "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
8537 "bCompilation, strMoods, strStyles, strThemes, "
8538 "strReview, strImage, strLabel, strType, "
8539 "fRating, iVotes, iUserrating, "
8540 "lastScraped, bScrapedMBID, strReleaseType, "
8541 "iDiscTotal, idInfoSetting "
8542 "FROM album"));
8543 m_pDS->exec("DROP TABLE album");
8544 m_pDS->exec("ALTER TABLE album_new RENAME TO album");
8545
8546 //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
8547 m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
8548 "idAlbum INTEGER, idPath INTEGER, "
8549 "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
8550 "iTrack INTEGER, iDuration INTEGER, "
8551 "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
8552 "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
8553 "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
8554 "lastplayed VARCHAR(20) DEFAULT NULL, "
8555 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
8556 "userrating INTEGER NOT NULL DEFAULT 0, "
8557 "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
8558 "dateAdded TEXT)");
8559 // Prepare as MySQL has different CAST datatypes
8560 m_pDS->exec(PrepareSQL("INSERT INTO song_new "
8561 "(idSong, "
8562 "idAlbum, idPath, "
8563 "strArtistDisp, strArtistSort, strGenres, strTitle, "
8564 "iTrack, iDuration, "
8565 "strReleaseDate, strOrigReleaseDate, "
8566 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
8567 "iTimesPlayed, iStartOffset, iEndOffset, "
8568 "lastplayed, "
8569 "rating, userrating, votes, "
8570 "comment, mood, strReplayGain, dateAdded) "
8571 "SELECT "
8572 "idSong, "
8573 "idAlbum, idPath, "
8574 "strArtistDisp, strArtistSort, strGenres, strTitle, "
8575 "iTrack, iDuration, "
8576 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8577 "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
8578 "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
8579 "iTimesPlayed, iStartOffset, iEndOffset, "
8580 "lastplayed, "
8581 "rating, userrating, votes, "
8582 "comment, mood, strReplayGain, dateAdded "
8583 "FROM song"));
8584 m_pDS->exec("DROP TABLE song");
8585 m_pDS->exec("ALTER TABLE song_new RENAME TO song");
8586 }
8587 if (version < 75)
8588 {
8589 m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
8590 m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
8591 m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
8592 }
8593 if (version < 77)
8594 {
8595 m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
8596 }
8597 if (version < 78)
8598 {
8599 std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime();
8600
8601 // Add removed_link table
8602 m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
8603 // Add lastcleaned and artistlinksupdated to versiontagscan table
8604 m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
8605 m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
8606 m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
8607 // Adjust lastscanned if original local time value is after current UTC
8608 if (GetLibraryLastUpdated() > strUTCNow)
8609 SetLibraryLastUpdated();
8610 m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
8611 "genresupdated = lastscanned, "
8612 "artistlinksupdated = lastscanned");
8613
8614 // Add dateNew, dateModified to song table
8615 m_pDS->exec("ALTER TABLE song ADD dateNew TEXT");
8616 m_pDS->exec("ALTER TABLE song ADD dateModified TEXT");
8617 // Set new to dateAdded and modified to lastplayed as estimates
8618 // Limit those local time values to now UTC, and modified is after new
8619 m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
8620 m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'",
8621 strUTCNow.c_str(), strUTCNow.c_str()));
8622 m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
8623 m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
8624 strUTCNow.c_str(), strUTCNow.c_str()));
8625 m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
8626
8627 // Add dateAdded, dateNew, dateModified to album table
8628 m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT");
8629 m_pDS->exec("ALTER TABLE album ADD dateNew TEXT");
8630 m_pDS->exec("ALTER TABLE album ADD dateModified TEXT");
8631 // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
8632 // Limit modified value to now UTC and after new
8633 // Indices have been dropped making subquery very slow, so create temp index
8634 m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
8635 m_pDS->exec("UPDATE album SET dateAdded = "
8636 "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
8637 m_pDS->exec("UPDATE album SET dateNew = "
8638 "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
8639 m_pDS->exec("UPDATE album SET dateModified = dateNew");
8640 m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
8641 m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
8642 strUTCNow.c_str(), strUTCNow.c_str()));
8643 //Remove temp index, full analytics for database created later
8644 m_pDS->exec("DROP INDEX idxSong3 ON song");
8645
8646 // Add dateAdded, dateNew, dateModified to artist table
8647 m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT");
8648 m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT");
8649 m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT");
8650 // dateAdded has NULL values until files rescanned by user
8651 // Set new and modified to now UTC as not worth complexity of estimating from song dates
8652 m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str()));
8653 m_pDS->exec("UPDATE artist SET dateModified = dateNew");
8654 }
8655 if (version < 79)
8656 {
8657 m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
8658 }
8659 if (version < 80)
8660 {
8661 m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
8662 // update duration for all current albums
8663 m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
8664 "WHERE song.idAlbum = album.idAlbum) "
8665 "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
8666 }
8667 if (version < 82)
8668 {
8669 // Update artist table combining fanart URL data into strImage field
8670 // Clear empty URL data <fanart /> and <thumb />
8671 m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
8672 m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
8673 //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
8674 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
8675 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
8676 m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
8677 "aspect=\"fanart\" preview')");
8678 // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
8679 // Truncate the fanart when total URLs exceeds this
8680 bool bisMySQL = StringUtils::EqualsNoCase(
8681 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
8682 "mysql");
8683 if (bisMySQL)
8684 {
8685 std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist "
8686 "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
8687 if (m_pDS->query(strSQL))
8688 {
8689 while (!m_pDS->eof())
8690 {
8691 int idArtist = m_pDS->fv("idArtist").get_asInt();
8692 std::string strFanart = m_pDS->fv("strFanart").get_asString();
8693 std::string strImage = m_pDS->fv("strImage").get_asString();
8694 size_t space = 65535;
8695 // Trim strImage to allow arbitrary half space for fanart
8696 if (!TrimImageURLs(strImage, space / 2))
8697 strImage.clear(); // </thumb> not found, empty field
8698 space = space - strImage.length();
8699 // Trim fanart to fit remaining space
8700 if (!TrimImageURLs(strFanart, space))
8701 strFanart.clear(); // </thumb> not found, empty field
8702
8703 strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
8704 "WHERE idArtist = %i",
8705 strFanart.c_str(), strImage.c_str(), idArtist);
8706 m_pDS2->exec(strSQL); // Use other dataset to update while looping result set
8707
8708 m_pDS->next();
8709 }
8710 m_pDS->close();
8711 }
8712 }
8713
8714 // Remove strFanart column from artist table
8715 m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
8716 "strArtist varchar(256), strMusicBrainzArtistID text, "
8717 "strSortName text, "
8718 "strType text, strGender text, strDisambiguation text, "
8719 "strBorn text, strFormed text, strGenres text, strMoods text, "
8720 "strStyles text, strInstruments text, strBiography text, "
8721 "strDied text, strDisbanded text, strYearsActive text, "
8722 "strImage text, "
8723 "lastScraped varchar(20) default NULL, "
8724 "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
8725 "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
8726 "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
8727 // Concatentate fanart URLs into strImage field
8728 // Prepare SQL to convert CONCAT to || in SQLite
8729 m_pDS->exec(PrepareSQL("INSERT INTO artist_new "
8730 "(idArtist, strArtist, strMusicBrainzArtistID, "
8731 "strSortName, strType, strGender, strDisambiguation, "
8732 "strBorn, strFormed, strGenres, strMoods, "
8733 "strStyles , strInstruments , strBiography , "
8734 "strDied, strDisbanded, strYearsActive, "
8735 "strImage, "
8736 "lastScraped, bScrapedMBID, idInfoSetting, "
8737 "dateAdded, dateNew, dateModified) "
8738 "SELECT "
8739 "artist.idArtist, "
8740 "strArtist, strMusicBrainzArtistID, "
8741 "strSortName, strType, strGender, strDisambiguation, "
8742 "strBorn, strFormed, strGenres, strMoods, "
8743 "strStyles, strInstruments, strBiography, "
8744 "strDied, strDisbanded, strYearsActive, "
8745 "CONCAT(strImage, strFanart), "
8746 "lastScraped, bScrapedMBID, idInfoSetting, "
8747 "dateAdded, dateNew, dateModified "
8748 "FROM artist"));
8749 m_pDS->exec("DROP TABLE artist");
8750 m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
8751 }
8752 // Set the verion of tag scanning required.
8753 // Not every schema change requires the tags to be rescanned, set to the highest schema version
8754 // that needs this. Forced rescanning (of music files that have not changed since they were
8755 // previously scanned) also accommodates any changes to the way tags are processed
8756 // e.g. read tags that were not processed by previous versions.
8757 // The original db version when the tags were scanned, and the minimal db version needed are
8758 // later used to determine if a forced rescan should be prompted
8759
8760 // The last schema change needing forced rescanning was 73.
8761 // This is because Kodi can now read and process extra tags involved in the creation of box sets
8762
8763 SetMusicNeedsTagScan(73);
8764
8765 // After all updates, store the original db version.
8766 // This indicates the version of tag processing that was used to populate db
8767 SetMusicTagScanVersion(version);
8768 }
8769
GetSchemaVersion() const8770 int CMusicDatabase::GetSchemaVersion() const
8771 {
8772 return 82;
8773 }
8774
GetMusicNeedsTagScan()8775 int CMusicDatabase::GetMusicNeedsTagScan()
8776 {
8777 try
8778 {
8779 if (nullptr == m_pDB)
8780 return -1;
8781 if (nullptr == m_pDS)
8782 return -1;
8783
8784 std::string sql = "SELECT * FROM versiontagscan";
8785 if (!m_pDS->query(sql)) return -1;
8786
8787 if (m_pDS->num_rows() != 1)
8788 {
8789 m_pDS->close();
8790 return -1;
8791 }
8792
8793 int idVersion = m_pDS->fv("idVersion").get_asInt();
8794 int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt();
8795 m_pDS->close();
8796 if (idVersion < iNeedsScan)
8797 return idVersion;
8798 else
8799 return 0;
8800 }
8801 catch (...)
8802 {
8803 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
8804 }
8805 return -1;
8806 }
8807
SetMusicNeedsTagScan(int version)8808 void CMusicDatabase::SetMusicNeedsTagScan(int version)
8809 {
8810 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version));
8811 }
8812
SetMusicTagScanVersion(int version)8813 void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */)
8814 {
8815 if (version == 0)
8816 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
8817 else
8818 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version));
8819 }
8820
GetLibraryLastUpdated()8821 std::string CMusicDatabase::GetLibraryLastUpdated()
8822 {
8823 return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
8824 }
8825
SetLibraryLastUpdated()8826 void CMusicDatabase::SetLibraryLastUpdated()
8827 {
8828 CDateTime dateUpdated = CDateTime::GetUTCDateTime();
8829 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
8830 dateUpdated.GetAsDBDateTime().c_str()));
8831 }
8832
GetLibraryLastCleaned()8833 std::string CMusicDatabase::GetLibraryLastCleaned()
8834 {
8835 return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
8836 }
8837
SetLibraryLastCleaned()8838 void CMusicDatabase::SetLibraryLastCleaned()
8839 {
8840 std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
8841 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'",
8842 strUpdated.c_str()));
8843 }
8844
GetArtistLinksUpdated()8845 std::string CMusicDatabase::GetArtistLinksUpdated()
8846 {
8847 return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
8848 }
8849
SetArtistLinksUpdated()8850 void CMusicDatabase::SetArtistLinksUpdated()
8851 {
8852 std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
8853 m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'",
8854 strUpdated.c_str()));
8855 }
8856
GetGenresLastAdded()8857 std::string CMusicDatabase::GetGenresLastAdded()
8858 {
8859 return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
8860 }
8861
GetSongsLastAdded()8862 std::string CMusicDatabase::GetSongsLastAdded()
8863 {
8864 return GetSingleValue("SELECT MAX(dateNew) FROM song");
8865 }
8866
GetAlbumsLastAdded()8867 std::string CMusicDatabase::GetAlbumsLastAdded()
8868 {
8869 return GetSingleValue("SELECT MAX(dateNew) FROM album");
8870 }
8871
GetArtistsLastAdded()8872 std::string CMusicDatabase::GetArtistsLastAdded()
8873 {
8874 return GetSingleValue("SELECT MAX(dateNew) FROM artist");
8875 }
8876
GetSongsLastModified()8877 std::string CMusicDatabase::GetSongsLastModified()
8878 {
8879 return GetSingleValue("SELECT MAX(dateModified) FROM song");
8880 }
8881
GetAlbumsLastModified()8882 std::string CMusicDatabase::GetAlbumsLastModified()
8883 {
8884 return GetSingleValue("SELECT MAX(dateModified) FROM album");
8885 }
8886
GetArtistsLastModified()8887 std::string CMusicDatabase::GetArtistsLastModified()
8888 {
8889 return GetSingleValue("SELECT MAX(dateModified) FROM artist");
8890 }
8891
GetRandomSongIDs(const Filter & filter,std::vector<std::pair<int,int>> & songIDs)8892 unsigned int CMusicDatabase::GetRandomSongIDs(const Filter &filter, std::vector<std::pair<int,int> > &songIDs)
8893 {
8894 try
8895 {
8896 if (nullptr == m_pDB)
8897 return 0;
8898 if (nullptr == m_pDS)
8899 return 0;
8900
8901 std::string strSQL = "SELECT idSong FROM songview ";
8902 if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
8903 return false;
8904 strSQL += PrepareSQL(" ORDER BY RANDOM()");
8905
8906 if (!m_pDS->query(strSQL)) return 0;
8907 songIDs.clear();
8908 if (m_pDS->num_rows() == 0)
8909 {
8910 m_pDS->close();
8911 return 0;
8912 }
8913 songIDs.reserve(m_pDS->num_rows());
8914 while (!m_pDS->eof())
8915 {
8916 songIDs.push_back(std::make_pair<int,int>(1, m_pDS->fv(song_idSong).get_asInt()));
8917 m_pDS->next();
8918 } // cleanup
8919 m_pDS->close();
8920 return static_cast<unsigned int>(songIDs.size());
8921 }
8922 catch (...)
8923 {
8924 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
8925 }
8926 return 0;
8927 }
8928
GetSongsCount(const Filter & filter)8929 int CMusicDatabase::GetSongsCount(const Filter &filter)
8930 {
8931 try
8932 {
8933 if (nullptr == m_pDB)
8934 return 0;
8935 if (nullptr == m_pDS)
8936 return 0;
8937
8938 std::string strSQL = "select count(idSong) as NumSongs from songview ";
8939 if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
8940 return false;
8941
8942 if (!m_pDS->query(strSQL)) return false;
8943 if (m_pDS->num_rows() == 0)
8944 {
8945 m_pDS->close();
8946 return 0;
8947 }
8948
8949 int iNumSongs = m_pDS->fv("NumSongs").get_asInt();
8950 // cleanup
8951 m_pDS->close();
8952 return iNumSongs;
8953 }
8954 catch (...)
8955 {
8956 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, filter.where.c_str());
8957 }
8958 return 0;
8959 }
8960
GetAlbumPath(int idAlbum,std::string & basePath)8961 bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath)
8962 {
8963 basePath.clear();
8964 std::vector<std::pair<std::string, int>> paths;
8965 if (!GetAlbumPaths(idAlbum, paths))
8966 return false;
8967
8968 for (const auto& pathpair : paths)
8969 {
8970 if (basePath.empty())
8971 basePath = pathpair.first.c_str();
8972 else
8973 URIUtils::GetCommonPath(basePath, pathpair.first.c_str());
8974 }
8975 return true;
8976 }
8977
GetAlbumPaths(int idAlbum,std::vector<std::pair<std::string,int>> & paths)8978 bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths)
8979 {
8980 paths.clear();
8981 std::string strSQL;
8982 try
8983 {
8984 if (nullptr == m_pDB)
8985 return false;
8986 if (nullptr == m_pDS2)
8987 return false;
8988
8989 // Get the unique paths of songs on the album, providing there are no songs from
8990 // other albums with the same path. This returns
8991 // a) <album> if is contains all the songs and no others, or
8992 // b) <album>/cd1, <album>/cd2 etc. for disc sets
8993 // but does *not* return any path when albums are mixed together. That could be because of
8994 // deliberate file organisation, or (more likely) because of a tagging error in album name
8995 // or Musicbrainzalbumid. Thus it avoids finding somme generic music path.
8996 strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
8997 "JOIN path ON song.idPath = path.idPath "
8998 "WHERE song.idAlbum = %ld "
8999 "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
9000 "WHERE idPath = song.idPath) = 1", idAlbum);
9001
9002 if (!m_pDS2->query(strSQL))
9003 return false;
9004 if (m_pDS2->num_rows() == 0)
9005 {
9006 // Album does not have a unique path, files are mixed
9007 m_pDS2->close();
9008 return false;
9009 }
9010
9011 while (!m_pDS2->eof())
9012 {
9013 paths.emplace_back(m_pDS2->fv("strPath").get_asString(), m_pDS2->fv("song.idPath").get_asInt());
9014 m_pDS2->next();
9015 }
9016 // Cleanup recordset data
9017 m_pDS2->close();
9018 return true;
9019 }
9020 catch (...)
9021 {
9022 CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %s", __FUNCTION__, strSQL.c_str());
9023 }
9024
9025 return false;
9026 }
9027
GetDiscnumberForPathID(int idPath)9028 int CMusicDatabase::GetDiscnumberForPathID(int idPath)
9029 {
9030 std::string strSQL;
9031 int result = -1;
9032 try
9033 {
9034 if (nullptr == m_pDB)
9035 return -1;
9036 if (nullptr == m_pDS2)
9037 return -1;
9038
9039 strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
9040 "WHERE idPath = %i", idPath);
9041
9042 if (!m_pDS2->query(strSQL))
9043 return -1;
9044 if (m_pDS2->num_rows() == 1)
9045 { // Songs with this path have a unique disc number
9046 result = m_pDS2->fv("discnum").get_asInt();
9047 }
9048 // Cleanup recordset data
9049 m_pDS2->close();
9050 }
9051 catch (...)
9052 {
9053 CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %s", __FUNCTION__, strSQL.c_str());
9054 }
9055 return result;
9056 }
9057
9058 // Get old "artist path" - where artist.nfo and art was located v17 and below.
9059 // It is the path common to all albums by an (album) artist, but ensure it is unique
9060 // to that artist and not shared with other artists. Previously this caused incorrect nfo
9061 // and art to be applied to multiple artists.
GetOldArtistPath(int idArtist,std::string & basePath)9062 bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string &basePath)
9063 {
9064 basePath.clear();
9065 try
9066 {
9067 if (nullptr == m_pDB)
9068 return false;
9069 if (nullptr == m_pDS2)
9070 return false;
9071
9072 // find all albums from this artist, and all the paths to the songs from those albums
9073 std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist "
9074 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9075 "JOIN path ON song.idPath = path.idPath "
9076 "WHERE album_artist.idArtist = %ld "
9077 "GROUP BY song.idPath",
9078 idArtist);
9079
9080 // run query
9081 if (!m_pDS2->query(strSQL)) return false;
9082 int iRowsFound = m_pDS2->num_rows();
9083 if (iRowsFound == 0)
9084 {
9085 // Artist is not an album artist, no path to find
9086 m_pDS2->close();
9087 return false;
9088 }
9089 else if (iRowsFound == 1)
9090 {
9091 // Special case for single path - assume that we're in an artist/album/songs filesystem
9092 URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath);
9093 m_pDS2->close();
9094 }
9095 else
9096 {
9097 // find the common path (if any) to these albums
9098 while (!m_pDS2->eof())
9099 {
9100 std::string path = m_pDS2->fv("strPath").get_asString();
9101 if (basePath.empty())
9102 basePath = path;
9103 else
9104 URIUtils::GetCommonPath(basePath, path);
9105
9106 m_pDS2->next();
9107 }
9108 m_pDS2->close();
9109 }
9110
9111 // Check any path found is unique to that album artist, and do *not* return any path
9112 // that is shared with other album artists. That could be because of collaborations
9113 // i.e. albums with more than one album artist, or because there are albums by the
9114 // artist on multiple music sources, or elsewhere in the folder hierarchy.
9115 // Avoid returning some generic music path.
9116 if (!basePath.empty())
9117 {
9118 strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
9119 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9120 "JOIN path ON song.idPath = path.idPath "
9121 "WHERE album_artist.idArtist <> %ld "
9122 "AND strPath LIKE '%s%%'",
9123 idArtist, basePath.c_str());
9124 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9125 if (!strValue.empty())
9126 {
9127 int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9128 if (countartists == 0)
9129 return true;
9130 }
9131 }
9132 }
9133 catch (...)
9134 {
9135 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9136 }
9137 basePath.clear();
9138 return false;
9139 }
9140
GetArtistPath(const CArtist & artist,std::string & path)9141 bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string &path)
9142 {
9143 // Get path for artist in the artists folder
9144 path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
9145 if (path.empty())
9146 return false; // No Artists folder not set;
9147 // Get unique artist folder name
9148 std::string strFolder;
9149 if (GetArtistFolderName(artist, strFolder))
9150 {
9151 path = URIUtils::AddFileToFolder(path, strFolder);
9152 return true;
9153 }
9154 path.clear();
9155 return false;
9156 }
9157
GetAlbumFolder(const CAlbum & album,const std::string & strAlbumPath,std::string & strFolder)9158 bool CMusicDatabase::GetAlbumFolder(const CAlbum& album, const std::string &strAlbumPath, std::string &strFolder)
9159 {
9160 strFolder.clear();
9161 // Get a name for the album folder that is unique for the artist to use when
9162 // exporting albums to separate nfo files in a folder under an artist folder
9163
9164 // When given an album path (common to all the music files containing *only*
9165 // that album) check if that folder name is *unique* looking at folders on
9166 // all levels of the music file paths for the artist
9167 if (!strAlbumPath.empty())
9168 {
9169 // Get last folder from full path
9170 std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath);
9171 if (!folders.empty())
9172 {
9173 strFolder = folders.back();
9174 // The same folder name could be used on different paths for albums by the
9175 // same first artist. The albums could be totally different or also have
9176 // the same name (but different mbid). Be over cautious and look for the
9177 // name any where in the music file paths
9178 std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
9179 "JOIN song ON album_artist.idAlbum = song.idAlbum "
9180 "JOIN path on path.idPath = song.idPath "
9181 "WHERE album_artist.iOrder = 0 "
9182 "AND album_artist.idArtist = %ld "
9183 "AND path.strPath LIKE '%%\\%s\\%%'",
9184 album.artistCredits[0].GetArtistId(), strFolder.c_str());
9185
9186 if (!m_pDS2->query(strSQL))
9187 return false;
9188 int iRowsFound = m_pDS2->num_rows();
9189 m_pDS2->close();
9190 if (iRowsFound == 1)
9191 return true;
9192 }
9193 }
9194 // Create a valid unique folder name from album title
9195 // @todo: Does UFT8 matter or need normalizing?
9196 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9197 strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT);
9198 StringUtils::Replace(strFolder, " _ ", "_");
9199
9200 // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
9201 // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
9202 // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
9203 // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
9204 std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
9205 "JOIN album ON album_artist.idAlbum = album.idAlbum "
9206 "WHERE album_artist.iOrder = 0 "
9207 "AND album_artist.idArtist = %ld "
9208 "AND album.strAlbum LIKE '%s' ",
9209 album.artistCredits[0].GetArtistId(), album.strAlbum.c_str());
9210 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9211 if (strValue.empty())
9212 return false;
9213 int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9214 if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty())
9215 { // Only one of the duplicate albums can be without mbid
9216 strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4);
9217 }
9218 return !strFolder.empty();
9219 }
9220
GetArtistFolderName(const CArtist & artist,std::string & strFolder)9221 bool CMusicDatabase::GetArtistFolderName(const CArtist &artist, std::string &strFolder)
9222 {
9223 return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder);
9224 }
9225
GetArtistFolderName(const std::string & strArtist,const std::string & strMusicBrainzArtistID,std::string & strFolder)9226 bool CMusicDatabase::GetArtistFolderName(const std::string &strArtist, const std::string &strMusicBrainzArtistID,
9227 std::string &strFolder)
9228 {
9229 // Create a valid unique folder name for artist
9230 // @todo: Does UFT8 matter or need normalizing?
9231 // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
9232 strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT);
9233 StringUtils::Replace(strFolder, " _ ", "_");
9234
9235 // Ensure <artist name> is unique e.g. 2 x John Williams.
9236 // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
9237 // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
9238 // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
9239 std::string strSQL = PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
9240 std::string strValue = GetSingleValue(strSQL, m_pDS2);
9241 if (strValue.empty())
9242 return false;
9243 int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
9244 if (countartist > 1)
9245 strFolder += "_" + strMusicBrainzArtistID.substr(0, 4);
9246 return !strFolder.empty();
9247 }
9248
AddSource(const std::string & strName,const std::string & strMultipath,const std::vector<std::string> & vecPaths,int id)9249 int CMusicDatabase::AddSource(const std::string& strName, const std::string& strMultipath, const std::vector<std::string>& vecPaths, int id /*= -1*/)
9250 {
9251 int idSource = -1;
9252 std::string strSQL;
9253 try
9254 {
9255 if (nullptr == m_pDB)
9256 return -1;
9257 if (nullptr == m_pDS)
9258 return -1;
9259
9260 // Check if source name already exists
9261 idSource = GetSourceByName(strName);
9262 if (idSource < 0)
9263 {
9264 BeginTransaction();
9265 // Add new source and source paths
9266 if (id > 0)
9267 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) VALUES(%i, '%s', '%s')",
9268 id, strName.c_str(), strMultipath.c_str());
9269 else
9270 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) VALUES(NULL, '%s', '%s')",
9271 strName.c_str(), strMultipath.c_str());
9272 m_pDS->exec(strSQL);
9273
9274 idSource = static_cast<int>(m_pDS->lastinsertid());
9275
9276 int idPath = 1;
9277 for (const auto& path : vecPaths)
9278 {
9279 strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) values(%i,%i,'%s')",
9280 idSource, idPath, path.c_str());
9281 m_pDS->exec(strSQL);
9282 ++idPath;
9283 }
9284
9285 // Find albums by song path, building WHERE for multiple source paths
9286 // (providing source has a path)
9287 if (vecPaths.size() > 0)
9288 {
9289 std::vector<int> albumIds;
9290 Filter extFilter;
9291 strSQL = "SELECT DISTINCT idAlbum FROM song ";
9292 extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
9293 for (const auto& path : vecPaths)
9294 extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false);
9295 if (!BuildSQL(strSQL, extFilter, strSQL))
9296 return -1;
9297
9298 if (!m_pDS->query(strSQL))
9299 return -1;
9300
9301 while (!m_pDS->eof())
9302 {
9303 albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
9304 m_pDS->next();
9305 }
9306 m_pDS->close();
9307
9308 // Add album_source for related albums
9309 for (auto idAlbum : albumIds)
9310 {
9311 strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) VALUES('%i', '%i')",
9312 idSource, idAlbum);
9313 m_pDS->exec(strSQL);
9314 }
9315 }
9316 CommitTransaction();
9317 }
9318 return idSource;
9319 }
9320 catch (...)
9321 {
9322 CLog::Log(LOGERROR, "%s failed with query (%s)", __FUNCTION__, strSQL.c_str());
9323 RollbackTransaction();
9324 }
9325
9326 return -1;
9327 }
9328
UpdateSource(const std::string & strOldName,const std::string & strName,const std::string & strMultipath,const std::vector<std::string> & vecPaths)9329 int CMusicDatabase::UpdateSource(const std::string& strOldName, const std::string& strName, const std::string& strMultipath, const std::vector<std::string>& vecPaths)
9330 {
9331 int idSource = -1;
9332 std::string strSourceMultipath;
9333 std::string strSQL;
9334 try
9335 {
9336 if (nullptr == m_pDB)
9337 return -1;
9338 if (nullptr == m_pDS)
9339 return -1;
9340
9341 // Get details of named old source
9342 if (!strOldName.empty())
9343 {
9344 strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
9345 strOldName.c_str());
9346 if (!m_pDS->query(strSQL))
9347 return -1;
9348 if (m_pDS->num_rows() > 0)
9349 {
9350 idSource = m_pDS->fv("idSource").get_asInt();
9351 strSourceMultipath = m_pDS->fv("strMultipath").get_asString();
9352 }
9353 m_pDS->close();
9354 }
9355 if (idSource < 0)
9356 {
9357 // Source not found, add new one
9358 return AddSource(strName, strMultipath, vecPaths);
9359 }
9360
9361 // Nothing changed? (that we hold in db, other source details could be modified)
9362 bool pathschanged = strMultipath.compare(strSourceMultipath) != 0;
9363 if (!pathschanged && strOldName.compare(strName) == 0)
9364 return idSource;
9365
9366 if (!pathschanged)
9367 {
9368 // Name changed? Could be that none of the values held in db changed
9369 if (strOldName.compare(strName) != 0)
9370 {
9371 strSQL = PrepareSQL("UPDATE source SET strName = '%s' WHERE idSource = %i",
9372 strName.c_str(), idSource);
9373 m_pDS->exec(strSQL);
9374 }
9375 return idSource;
9376 }
9377 else
9378 {
9379 // Change paths (and name) by deleting and re-adding, but keep same ID
9380 strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource);
9381 m_pDS->exec(strSQL);
9382 return AddSource(strName, strMultipath, vecPaths, idSource);
9383 }
9384 }
9385 catch (...)
9386 {
9387 CLog::Log(LOGERROR, "%s failed with query (%s)", __FUNCTION__, strSQL.c_str());
9388 RollbackTransaction();
9389 }
9390
9391 return -1;
9392 }
9393
RemoveSource(const std::string & strName)9394 bool CMusicDatabase::RemoveSource(const std::string& strName)
9395 {
9396 // Related album_source and source_path rows removed by trigger
9397 SetLibraryLastCleaned();
9398 return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str()));
9399 }
9400
GetSourceFromPath(const std::string & strPath1)9401 int CMusicDatabase::GetSourceFromPath(const std::string& strPath1)
9402 {
9403 std::string strSQL;
9404 int idSource = -1;
9405 try
9406 {
9407 std::string strPath(strPath1);
9408 if (!URIUtils::HasSlashAtEnd(strPath))
9409 URIUtils::AddSlashAtEnd(strPath);
9410
9411 if (nullptr == m_pDB)
9412 return -1;
9413 if (nullptr == m_pDS)
9414 return -1;
9415
9416 // Check if path is a source matching on multipath
9417 strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str());
9418 if (!m_pDS->query(strSQL))
9419 return -1;
9420 if (m_pDS->num_rows() > 0)
9421 idSource = m_pDS->fv("idSource").get_asInt();
9422 m_pDS->close();
9423 if (idSource > 0)
9424 return idSource;
9425
9426 // Check if path is a source path (of many) or a subfolder of a single source
9427 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9428 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", strPath.c_str());
9429 if (!m_pDS->query(strSQL))
9430 return -1;
9431 if (m_pDS->num_rows() == 1)
9432 idSource = m_pDS->fv("idSource").get_asInt();
9433 m_pDS->close();
9434 return idSource;
9435
9436 }
9437 catch (...)
9438 {
9439 CLog::Log(LOGERROR, "%s path: %s (%s) failed", __FUNCTION__, strSQL.c_str(), strPath1.c_str());
9440 }
9441
9442 return -1;
9443 }
9444
AddAlbumSource(int idAlbum,int idSource)9445 bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource)
9446 {
9447 std::string strSQL;
9448 strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) values(%i, %i)",
9449 idAlbum, idSource);
9450 return ExecuteQuery(strSQL);
9451 }
9452
AddAlbumSources(int idAlbum,const std::string & strPath)9453 bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath)
9454 {
9455 std::string strSQL;
9456 std::vector<int> sourceIds;
9457 try
9458 {
9459 if (nullptr == m_pDB)
9460 return false;
9461 if (nullptr == m_pDS)
9462 return false;
9463
9464 if (!strPath.empty())
9465 {
9466 // Find sources related to album using album path
9467 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9468 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", strPath.c_str());
9469 if (!m_pDS->query(strSQL))
9470 return false;
9471 while (!m_pDS->eof())
9472 {
9473 sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
9474 m_pDS->next();
9475 }
9476 m_pDS->close();
9477 }
9478 else
9479 {
9480 // Find sources using song paths, check each source path individually
9481 if (nullptr == m_pDS2)
9482 return false;
9483 strSQL = "SELECT idSource, strPath FROM source_path";
9484 if (!m_pDS->query(strSQL))
9485 return false;
9486 while (!m_pDS->eof())
9487 {
9488 std::string sourcepath = m_pDS->fv("strPath").get_asString();
9489 strSQL = PrepareSQL("SELECT 1 FROM song "
9490 "JOIN path ON song.idPath = path.idPath "
9491 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'", sourcepath.c_str());
9492 if (!m_pDS2->query(strSQL))
9493 return false;
9494 if (m_pDS2->num_rows() > 0)
9495 sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
9496 m_pDS2->close();
9497
9498 m_pDS->next();
9499 }
9500 m_pDS->close();
9501 }
9502
9503 //Add album sources
9504 for (auto idSource : sourceIds)
9505 {
9506 AddAlbumSource(idAlbum, idSource);
9507 }
9508
9509 return true;
9510 }
9511 catch (...)
9512 {
9513 CLog::Log(LOGERROR, "%s path: %s (%s) failed", __FUNCTION__, strSQL.c_str(), strPath.c_str());
9514 }
9515
9516 return false;
9517 }
9518
DeleteAlbumSources(int idAlbum)9519 bool CMusicDatabase::DeleteAlbumSources(int idAlbum)
9520 {
9521 return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum));
9522 }
9523
CheckSources(VECSOURCES & sources)9524 bool CMusicDatabase::CheckSources(VECSOURCES& sources)
9525 {
9526 if (sources.empty())
9527 {
9528 // Source table empty too?
9529 return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
9530 }
9531
9532 // Check number of entries matches
9533 size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
9534 if (total != sources.size())
9535 return false;
9536
9537 // Check individual sources match
9538 try
9539 {
9540 if (nullptr == m_pDB)
9541 return false;
9542 if (nullptr == m_pDS)
9543 return false;
9544
9545 std::string strSQL;
9546 for (const auto& source : sources)
9547 {
9548 // Check each source by name
9549 strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source "
9550 "WHERE strName LIKE '%s'", source.strName.c_str());
9551 m_pDS->query(strSQL);
9552 if (!m_pDS->query(strSQL))
9553 return false;
9554 if (m_pDS->num_rows() != 1)
9555 {
9556 // Missing source, or name duplication
9557 m_pDS->close();
9558 return false;
9559 }
9560 else
9561 {
9562 // Check details. Encoded URLs of source.strPath matched to strMultipath
9563 // field, no need to look at individual paths of source_path table
9564 if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0)
9565 {
9566 // Paths not match
9567 m_pDS->close();
9568 return false;
9569 }
9570 m_pDS->close();
9571 }
9572 }
9573 return true;
9574 }
9575 catch (...)
9576 {
9577 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9578 }
9579 return false;
9580 }
9581
MigrateSources()9582 bool CMusicDatabase::MigrateSources()
9583 {
9584 //Fetch music sources from xml
9585 VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
9586
9587 std::string strSQL;
9588 try
9589 {
9590 // Fill source and source paths tables
9591 for (const auto& source : sources)
9592 {
9593 // AddSource(source.strName, source.strPath, source.vecPaths);
9594 // Add new source
9595 strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) VALUES(NULL, '%s', '%s')",
9596 source.strName.c_str(), source.strPath.c_str());
9597 m_pDS->exec(strSQL);
9598 int idSource = static_cast<int>(m_pDS->lastinsertid());
9599
9600 // Add new source paths
9601 int idPath = 1;
9602 for (const auto& path : source.vecPaths)
9603 {
9604 strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) values(%i,%i,'%s')",
9605 idSource, idPath, path.c_str());
9606 m_pDS->exec(strSQL);
9607 ++idPath;
9608 }
9609 }
9610
9611 return true;
9612 }
9613 catch (...)
9614 {
9615 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, strSQL.c_str());
9616 }
9617 return false;
9618 }
9619
UpdateSources()9620 bool CMusicDatabase::UpdateSources()
9621 {
9622 //Check library and xml sources match
9623 VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
9624 if (CheckSources(sources))
9625 return true;
9626
9627 try
9628 {
9629 // Empty sources table (related link tables removed by trigger);
9630 ExecuteQuery("DELETE FROM source");
9631
9632 // Fill source table, and album sources
9633 for (const auto& source : sources)
9634 AddSource(source.strName, source.strPath, source.vecPaths);
9635
9636 return true;
9637 }
9638 catch (...)
9639 {
9640 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9641 }
9642 return false;
9643 }
9644
GetSources(CFileItemList & items)9645 bool CMusicDatabase::GetSources(CFileItemList& items)
9646 {
9647 try
9648 {
9649 if (nullptr == m_pDB)
9650 return false;
9651 if (nullptr == m_pDS)
9652 return false;
9653
9654 // Get music sources and individual source paths (may not be scanned or have albums etc.)
9655 std::string strSQL = "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
9656 "FROM source JOIN source_path ON source.idSource = source_path.idSource "
9657 "ORDER BY source.idSource, source_path.idPath";
9658
9659 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
9660 if (!m_pDS->query(strSQL))
9661 return false;
9662 int iRowsFound = m_pDS->num_rows();
9663 if (iRowsFound == 0)
9664 {
9665 m_pDS->close();
9666 return true;
9667 }
9668
9669 // Get data from returned rows
9670 // Item has source ID in MusicInfotag, multipath in path, and indiviual paths in property
9671 CVariant sourcePaths(CVariant::VariantTypeArray);
9672 int idSource = -1;
9673 while (!m_pDS->eof())
9674 {
9675 if (idSource != m_pDS->fv("source.idSource").get_asInt())
9676 { // New source
9677 if (idSource > 0 && !sourcePaths.empty())
9678 {
9679 //Store paths for previous source in item list
9680 items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
9681 sourcePaths.clear();
9682 }
9683 idSource = m_pDS->fv("source.idSource").get_asInt();
9684 CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
9685 pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source");
9686 // Set tag URL for "file" property in AudioLibary processing
9687 pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString());
9688 // Set item path as source URL encoded multipath too
9689 pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString());
9690
9691 pItem->m_bIsFolder = true;
9692 items.Add(pItem);
9693 }
9694 // Get path data
9695 sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString());
9696
9697 m_pDS->next();
9698 }
9699 if (!sourcePaths.empty())
9700 {
9701 //Store paths for final source
9702 items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
9703 sourcePaths.clear();
9704 }
9705
9706 // cleanup
9707 m_pDS->close();
9708
9709 return true;
9710 }
9711 catch (...)
9712 {
9713 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9714 }
9715 return false;
9716 }
9717
GetSourcesByArtist(int idArtist,CFileItem * item)9718 bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item)
9719 {
9720 if (nullptr == m_pDB)
9721 return false;
9722 if (nullptr == m_pDS)
9723 return false;
9724
9725 try
9726 {
9727 std::string strSQL;
9728 strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
9729 "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
9730 "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
9731 "WHERE artist.idArtist = %i "
9732 "ORDER BY album_source.idSource", idArtist);
9733 if (!m_pDS->query(strSQL))
9734 return false;
9735 if (m_pDS->num_rows() == 0)
9736 {
9737 // Artist does have any source via albums may not be an album artist.
9738 // Check via songs fetch sources from compilations or where they are guest artist
9739 m_pDS->close();
9740 strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
9741 "JOIN song ON song_artist.idSong = song.idSong "
9742 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
9743 "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
9744 "ORDER BY album_source.idSource", idArtist);
9745 if (!m_pDS->query(strSQL))
9746 return false;
9747 if (m_pDS->num_rows() == 0)
9748 {
9749 //No sources, but query sucessfull
9750 m_pDS->close();
9751 return true;
9752 }
9753 }
9754
9755 CVariant artistSources(CVariant::VariantTypeArray);
9756 while (!m_pDS->eof())
9757 {
9758 artistSources.push_back(m_pDS->fv("idSource").get_asInt());
9759 m_pDS->next();
9760 }
9761 m_pDS->close();
9762
9763 item->SetProperty("sourceid", artistSources);
9764 return true;
9765 }
9766 catch (...)
9767 {
9768 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idArtist);
9769 }
9770 return false;
9771 }
9772
GetSourcesByAlbum(int idAlbum,CFileItem * item)9773 bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item)
9774 {
9775 if (nullptr == m_pDB)
9776 return false;
9777 if (nullptr == m_pDS)
9778 return false;
9779
9780 try
9781 {
9782 std::string strSQL;
9783 strSQL = PrepareSQL("SELECT idSource FROM album_source "
9784 "WHERE album_source.idAlbum = %i "
9785 "ORDER BY idSource", idAlbum);
9786 if (!m_pDS->query(strSQL))
9787 return false;
9788 CVariant albumSources(CVariant::VariantTypeArray);
9789 if (m_pDS->num_rows() > 0)
9790 {
9791 while (!m_pDS->eof())
9792 {
9793 albumSources.push_back(m_pDS->fv("idSource").get_asInt());
9794 m_pDS->next();
9795 }
9796 m_pDS->close();
9797 }
9798 else
9799 {
9800 //! @todo: handle singles, or don't waste time checking songs
9801 // Album does have any sources, may be a single??
9802 // Check via song paths, check each source path individually
9803 // usually fewer source paths than songs
9804 m_pDS->close();
9805
9806 if (nullptr == m_pDS2)
9807 return false;
9808 strSQL = "SELECT idSource, strPath FROM source_path";
9809 if (!m_pDS->query(strSQL))
9810 return false;
9811 while (!m_pDS->eof())
9812 {
9813 std::string sourcepath = m_pDS->fv("strPath").get_asString();
9814 strSQL = PrepareSQL("SELECT 1 FROM song "
9815 "JOIN path ON song.idPath = path.idPath "
9816 "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'", idAlbum, sourcepath.c_str());
9817 if (!m_pDS2->query(strSQL))
9818 return false;
9819 if (m_pDS2->num_rows() > 0)
9820 albumSources.push_back(m_pDS->fv("idSource").get_asInt());
9821 m_pDS2->close();
9822
9823 m_pDS->next();
9824 }
9825 m_pDS->close();
9826 }
9827
9828
9829 item->SetProperty("sourceid", albumSources);
9830 return true;
9831 }
9832 catch (...)
9833 {
9834 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idAlbum);
9835 }
9836 return false;
9837 }
9838
GetSourcesBySong(int idSong,const std::string & strPath1,CFileItem * item)9839 bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item)
9840 {
9841 if (nullptr == m_pDB)
9842 return false;
9843 if (nullptr == m_pDS)
9844 return false;
9845
9846 try
9847 {
9848 std::string strSQL;
9849 strSQL = PrepareSQL("SELECT idSource FROM song "
9850 "JOIN album_source ON album_source.idAlbum = song.idAlbum "
9851 "WHERE song.idSong = %i "
9852 "ORDER BY idSource", idSong);
9853 if (!m_pDS->query(strSQL))
9854 return false;
9855 if (m_pDS->num_rows() == 0 && !strPath1.empty())
9856 {
9857 // Check via song path instead
9858 m_pDS->close();
9859 std::string strPath(strPath1);
9860 if (!URIUtils::HasSlashAtEnd(strPath))
9861 URIUtils::AddSlashAtEnd(strPath);
9862
9863 strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
9864 "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath", strPath.c_str());
9865 if (!m_pDS->query(strSQL))
9866 return false;
9867 }
9868 CVariant songSources(CVariant::VariantTypeArray);
9869 while (!m_pDS->eof())
9870 {
9871 songSources.push_back(m_pDS->fv("idSource").get_asInt());
9872 m_pDS->next();
9873 }
9874 m_pDS->close();
9875
9876 item->SetProperty("sourceid", songSources);
9877 return true;
9878 }
9879 catch (...)
9880 {
9881 CLog::Log(LOGERROR, "%s(%i) failed", __FUNCTION__, idSong);
9882 }
9883 return false;
9884 }
9885
GetSourceByName(const std::string & strSource)9886 int CMusicDatabase::GetSourceByName(const std::string& strSource)
9887 {
9888 try
9889 {
9890 if (nullptr == m_pDB)
9891 return false;
9892 if (nullptr == m_pDS)
9893 return false;
9894
9895 std::string strSQL;
9896 strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str());
9897 // run query
9898 if (!m_pDS->query(strSQL)) return false;
9899 int iRowsFound = m_pDS->num_rows();
9900 if (iRowsFound != 1)
9901 {
9902 m_pDS->close();
9903 return -1;
9904 }
9905 return m_pDS->fv("idSource").get_asInt();
9906 }
9907 catch (...)
9908 {
9909 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9910 }
9911 return -1;
9912 }
9913
GetSourceById(int id)9914 std::string CMusicDatabase::GetSourceById(int id)
9915 {
9916 return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id));
9917 }
9918
GetArtistByName(const std::string & strArtist)9919 int CMusicDatabase::GetArtistByName(const std::string& strArtist)
9920 {
9921 try
9922 {
9923 if (nullptr == m_pDB)
9924 return false;
9925 if (nullptr == m_pDS)
9926 return false;
9927
9928 std::string strSQL=PrepareSQL("select idArtist from artist where artist.strArtist like '%s'", strArtist.c_str());
9929
9930 // run query
9931 if (!m_pDS->query(strSQL)) return false;
9932 int iRowsFound = m_pDS->num_rows();
9933 if (iRowsFound != 1)
9934 {
9935 m_pDS->close();
9936 return -1;
9937 }
9938 int lResult = m_pDS->fv("artist.idArtist").get_asInt();
9939 m_pDS->close();
9940 return lResult;
9941 }
9942 catch (...)
9943 {
9944 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
9945 }
9946 return -1;
9947 }
9948
GetArtistByMatch(const CArtist & artist)9949 int CMusicDatabase::GetArtistByMatch(const CArtist& artist)
9950 {
9951 std::string strSQL;
9952 try
9953 {
9954 if (nullptr == m_pDB || nullptr == m_pDS)
9955 return false;
9956 // Match on MusicBrainz ID, definitively unique
9957 if (!artist.strMusicBrainzArtistID.empty())
9958 strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
9959 artist.strMusicBrainzArtistID.c_str());
9960 else
9961 // No MusicBrainz ID, artist by name with no mbid
9962 strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
9963 artist.strArtist.c_str());
9964 if (!m_pDS->query(strSQL)) return false;
9965 int iRowsFound = m_pDS->num_rows();
9966 if (iRowsFound != 1)
9967 {
9968 m_pDS->close();
9969 // Match on artist name, relax mbid restriction
9970 return GetArtistByName(artist.strArtist);
9971 }
9972 int lResult = m_pDS->fv("idArtist").get_asInt();
9973 m_pDS->close();
9974 return lResult;
9975 }
9976 catch (...)
9977 {
9978 CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %", __FUNCTION__, strSQL.c_str());
9979 }
9980 return -1;
9981 }
9982
GetArtistFromSong(int idSong,CArtist & artist)9983 bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist &artist)
9984 {
9985 try
9986 {
9987 if (nullptr == m_pDB)
9988 return false;
9989 if (nullptr == m_pDS)
9990 return false;
9991
9992 std::string strSQL = PrepareSQL(
9993 "SELECT artistview.* FROM song_artist "
9994 "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
9995 "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
9996 idSong);
9997 if (!m_pDS->query(strSQL)) return false;
9998 int iRowsFound = m_pDS->num_rows();
9999 if (iRowsFound != 1)
10000 {
10001 m_pDS->close();
10002 return false;
10003 }
10004
10005 artist = GetArtistFromDataset(m_pDS.get());
10006
10007 m_pDS->close();
10008 return true;
10009
10010 }
10011 catch (...)
10012 {
10013 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10014 }
10015 return false;
10016 }
10017
IsSongArtist(int idSong,int idArtist)10018 bool CMusicDatabase::IsSongArtist(int idSong, int idArtist)
10019 {
10020 std::string strSQL = PrepareSQL(
10021 "SELECT 1 FROM song_artist "
10022 "WHERE song_artist.idSong= %i AND "
10023 "song_artist.idArtist = %i AND song_artist.idRole = 1",
10024 idSong, idArtist);
10025 return GetSingleValue(strSQL).empty();
10026 }
10027
IsSongAlbumArtist(int idSong,int idArtist)10028 bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist)
10029 {
10030 std::string strSQL = PrepareSQL(
10031 "SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
10032 "WHERE song.idSong = %i AND album_artist.idArtist = %i",
10033 idSong, idArtist);
10034 return GetSingleValue(strSQL).empty();
10035 }
10036
IsAlbumBoxset(int idAlbum)10037 bool CMusicDatabase::IsAlbumBoxset(int idAlbum)
10038 {
10039 std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum);
10040 int isBoxSet = GetSingleValueInt(strSQL);
10041 return (isBoxSet == 1 ? true : false);
10042 }
10043
GetAlbumByName(const std::string & strAlbum,const std::string & strArtist)10044 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist)
10045 {
10046 try
10047 {
10048 if (nullptr == m_pDB)
10049 return false;
10050 if (nullptr == m_pDS)
10051 return false;
10052
10053 std::string strSQL;
10054 if (strArtist.empty())
10055 strSQL=PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str());
10056 else
10057 strSQL=PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'", strAlbum.c_str(),strArtist.c_str());
10058 // run query
10059 if (!m_pDS->query(strSQL)) return false;
10060 int iRowsFound = m_pDS->num_rows();
10061 if (iRowsFound != 1)
10062 {
10063 m_pDS->close();
10064 return -1;
10065 }
10066 return m_pDS->fv("idAlbum").get_asInt();
10067 }
10068 catch (...)
10069 {
10070 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10071 }
10072 return -1;
10073 }
10074
GetMatchingMusicVideoAlbum(const std::string & strAlbum,const std::string & strArtist,int & idAlbum,std::string & strReview)10075 bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum,
10076 const std::string& strArtist,
10077 int& idAlbum,
10078 std::string& strReview)
10079 {
10080 /*
10081 Get the first album that matches with the title and artist display name.
10082 Artist(s) and album title may not be sufficient to uniquely identify a match since library can
10083 store multiple releases, and occasionally artists even have different albums with same name.
10084 Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
10085 */
10086 try
10087 {
10088 if (nullptr == m_pDB)
10089 return false;
10090 if (nullptr == m_pDS)
10091 return false;
10092
10093 std::string strSQL;
10094 if (strArtist.empty())
10095 strSQL =
10096 PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
10097 strAlbum.c_str());
10098 else
10099 strSQL =
10100 PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s' AND "
10101 "album.strArtistDisp LIKE '%s'",
10102 strAlbum.c_str(), strArtist.c_str());
10103 // run query
10104 if (!m_pDS->query(strSQL))
10105 return false;
10106 int iRowsFound = m_pDS->num_rows();
10107 if (iRowsFound > 0)
10108 {
10109 idAlbum = m_pDS->fv("idAlbum").get_asInt();
10110 strReview = m_pDS->fv("strReview").get_asString();
10111 return true;
10112 }
10113 }
10114 catch (...)
10115 {
10116 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10117 }
10118 return false;
10119 }
10120
SearchAlbumsByArtistName(const std::string & strArtist,CFileItemList & items)10121 bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items)
10122 {
10123 try
10124 {
10125 if (nullptr == m_pDB)
10126 return false;
10127 if (nullptr == m_pDS)
10128 return false;
10129
10130 std::string strSQL;
10131 strSQL = PrepareSQL("SELECT albumview.* FROM albumview "
10132 "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
10133 "WHERE album_artist.strArtist LIKE '%s'",
10134 strArtist.c_str());
10135
10136 if (!m_pDS->query(strSQL))
10137 return false;
10138
10139 while (!m_pDS->eof())
10140 {
10141 CAlbum album = GetAlbumFromDataset(m_pDS.get());
10142 std::string path = StringUtils::Format("musicdb://albums/%ld/", album.idAlbum);
10143 CFileItemPtr pItem(new CFileItem(path, album));
10144 std::string label =
10145 StringUtils::Format("%s (%i)", album.strAlbum, pItem->GetMusicInfoTag()->GetYear());
10146 pItem->SetLabel(label);
10147 items.Add(pItem);
10148 m_pDS->next();
10149 }
10150 m_pDS->close(); // cleanup recordset data
10151 return true;
10152 }
10153 catch (...)
10154 {
10155 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10156 }
10157 return false;
10158 }
10159
GetAlbumByName(const std::string & strAlbum,const std::vector<std::string> & artist)10160 int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::vector<std::string>& artist)
10161 {
10162 return GetAlbumByName(strAlbum, StringUtils::Join(artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
10163 }
10164
GetAlbumByMatch(const CAlbum & album)10165 int CMusicDatabase::GetAlbumByMatch(const CAlbum &album)
10166 {
10167 std::string strSQL;
10168 try
10169 {
10170 if (nullptr == m_pDB || nullptr == m_pDS)
10171 return false;
10172 // Match on MusicBrainz ID, definitively unique
10173 if (!album.strMusicBrainzAlbumID.empty())
10174 strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'", album.strMusicBrainzAlbumID.c_str());
10175 else
10176 // No mbid, match on album title and album artist descriptive string, ignore those with mbid
10177 strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' AND strMusicBrainzAlbumID IS NULL",
10178 album.GetAlbumArtistString().c_str(),
10179 album.strAlbum.c_str());
10180 m_pDS->query(strSQL);
10181 if (!m_pDS->query(strSQL)) return false;
10182 int iRowsFound = m_pDS->num_rows();
10183 if (iRowsFound != 1)
10184 {
10185 m_pDS->close();
10186 // Match on album title and album artist descriptive string, relax mbid restriction
10187 return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString());
10188 }
10189 int lResult = m_pDS->fv("idAlbum").get_asInt();
10190 m_pDS->close();
10191 return lResult;
10192 }
10193 catch (...)
10194 {
10195 CLog::Log(LOGERROR, "CMusicDatabase::%s - failed to execute %", __FUNCTION__, strSQL.c_str());
10196 }
10197 return -1;
10198 }
10199
GetGenreById(int id)10200 std::string CMusicDatabase::GetGenreById(int id)
10201 {
10202 return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id));
10203 }
10204
GetArtistById(int id)10205 std::string CMusicDatabase::GetArtistById(int id)
10206 {
10207 return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id));
10208 }
10209
GetRoleById(int id)10210 std::string CMusicDatabase::GetRoleById(int id)
10211 {
10212 return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id));
10213 }
10214
UpdateArtistSortNames(int idArtist)10215 bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/)
10216 {
10217 // Propagate artist sort names into concatenated artist sort name string for songs and albums
10218 // Avoid updating records where sort same as strArtistDisp
10219 std::string strSQL;
10220
10221 // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
10222 // (not handled by PrepareSQL)
10223 bool bisMySQL = StringUtils::EqualsNoCase(
10224 CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
10225
10226 BeginMultipleExecute();
10227 if (bisMySQL)
10228 strSQL = "(SELECT GROUP_CONCAT("
10229 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10230 "ELSE artist.strSortName END "
10231 "ORDER BY album_artist.idAlbum, album_artist.iOrder "
10232 "SEPARATOR '; ') as val "
10233 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10234 "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
10235 else
10236 strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10237 "FROM(SELECT album_artist.idAlbum, "
10238 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10239 "ELSE artist.strSortName END as val "
10240 "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
10241 "WHERE album_artist.idAlbum = album.idAlbum "
10242 "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
10243
10244 strSQL = "UPDATE album SET strArtistSort = " + strSQL +
10245 "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
10246 "AND strArtistDisp <> " + strSQL;
10247 if (idArtist > 0)
10248 strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
10249 "AND album_artist.idAlbum = album.idAlbum)", idArtist);
10250 ExecuteQuery(strSQL);
10251 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
10252
10253 if (bisMySQL)
10254 strSQL = "(SELECT GROUP_CONCAT("
10255 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10256 "ELSE artist.strSortName END "
10257 "ORDER BY song_artist.idSong, song_artist.iOrder "
10258 "SEPARATOR '; ') as val "
10259 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10260 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
10261 else
10262 strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
10263 "FROM(SELECT song_artist.idSong, "
10264 "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
10265 "ELSE artist.strSortName END as val "
10266 "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
10267 "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
10268 "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
10269
10270 strSQL = "UPDATE song SET strArtistSort = " + strSQL +
10271 "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
10272 "AND strArtistDisp <> " + strSQL;
10273 if (idArtist > 0)
10274 strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
10275 "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)", idArtist);
10276 ExecuteQuery(strSQL);
10277 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
10278
10279 if (CommitMultipleExecute())
10280 return true;
10281 else
10282 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10283 return false;
10284 }
10285
GetAlbumById(int id)10286 std::string CMusicDatabase::GetAlbumById(int id)
10287 {
10288 return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id));
10289 }
10290
GetGenreByName(const std::string & strGenre)10291 int CMusicDatabase::GetGenreByName(const std::string& strGenre)
10292 {
10293 try
10294 {
10295 if (nullptr == m_pDB)
10296 return false;
10297 if (nullptr == m_pDS)
10298 return false;
10299
10300 std::string strSQL;
10301 strSQL=PrepareSQL("select idGenre from genre where genre.strGenre like '%s'", strGenre.c_str());
10302 // run query
10303 if (!m_pDS->query(strSQL)) return false;
10304 int iRowsFound = m_pDS->num_rows();
10305 if (iRowsFound != 1)
10306 {
10307 m_pDS->close();
10308 return -1;
10309 }
10310 return m_pDS->fv("genre.idGenre").get_asInt();
10311 }
10312 catch (...)
10313 {
10314 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10315 }
10316 return -1;
10317 }
10318
GetGenresJSON(CFileItemList & items,bool bSources)10319 bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources)
10320 {
10321 std::string strSQL;
10322 try
10323 {
10324 if (nullptr == m_pDB)
10325 return false;
10326 if (nullptr == m_pDS)
10327 return false;
10328
10329 strSQL = "SELECT %s FROM genre ";
10330 Filter extFilter;
10331 extFilter.AppendField("genre.idGenre");
10332 extFilter.AppendField("genre.strGenre");
10333 if (bSources)
10334 {
10335 strSQL = "SELECT DISTINCT %s FROM genre ";
10336 extFilter.AppendField("album_source.idSource");
10337 extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
10338 extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
10339 extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
10340 extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
10341 extFilter.AppendOrder("genre.strGenre");
10342 extFilter.AppendOrder("album_source.idSource");
10343 }
10344 extFilter.AppendWhere("genre.strGenre != ''");
10345
10346 std::string strSQLExtra;
10347 if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
10348 return false;
10349
10350 strSQL = PrepareSQL(strSQL.c_str(), extFilter.fields.c_str()) + strSQLExtra;
10351
10352 // run query
10353 CLog::Log(LOGDEBUG, "%s query: %s", __FUNCTION__, strSQL.c_str());
10354
10355 if (!m_pDS->query(strSQL))
10356 return false;
10357 int iRowsFound = m_pDS->num_rows();
10358 if (iRowsFound == 0)
10359 {
10360 m_pDS->close();
10361 return true;
10362 }
10363
10364 if (!bSources)
10365 items.Reserve(iRowsFound);
10366
10367 // Get data from returned rows
10368 // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
10369 CVariant genreSources(CVariant::VariantTypeArray);
10370 int idGenre = -1;
10371 while (!m_pDS->eof())
10372 {
10373 if (idGenre != m_pDS->fv("genre.idGenre").get_asInt())
10374 { // New genre
10375 if (idGenre > 0 && bSources)
10376 {
10377 //Store sources for previous genre in item list
10378 items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
10379 genreSources.clear();
10380 }
10381 idGenre = m_pDS->fv("genre.idGenre").get_asInt();
10382 std::string strGenre = m_pDS->fv("genre.strGenre").get_asString();
10383 CFileItemPtr pItem(new CFileItem(strGenre));
10384 pItem->GetMusicInfoTag()->SetTitle(strGenre);
10385 pItem->GetMusicInfoTag()->SetGenre(strGenre);
10386 pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre");
10387 pItem->SetPath(StringUtils::Format("musicdb://genres/%i/", idGenre));
10388 pItem->m_bIsFolder = true;
10389 items.Add(pItem);
10390 }
10391 // Get source data
10392 if (bSources)
10393 {
10394 int sourceid = m_pDS->fv("album_source.idSource").get_asInt();
10395 if (sourceid > 0)
10396 genreSources.push_back(sourceid);
10397 }
10398 m_pDS->next();
10399 }
10400 if (bSources)
10401 {
10402 //Store sources for final genre
10403 items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
10404 }
10405
10406 // cleanup
10407 m_pDS->close();
10408
10409 return true;
10410 }
10411 catch (...)
10412 {
10413 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, strSQL.c_str());
10414 }
10415 return false;
10416 }
10417
GetAlbumDiscTitle(int idAlbum,int idDisc)10418 std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc)
10419 {
10420 // Get disc node title from ids allowing for "*all"
10421 std::string disctitle;
10422 std::string albumtitle;
10423 if (idAlbum > 0)
10424 albumtitle = GetAlbumById(idAlbum);
10425 if (idDisc > 0)
10426 {
10427 disctitle = GetSingleValue("song", "strDiscSubtitle",
10428 PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc));
10429 if (disctitle.empty())
10430 disctitle = StringUtils::Format("%s %i", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc.
10431 if (albumtitle.empty())
10432 albumtitle = disctitle;
10433 else
10434 albumtitle = albumtitle + " - " + disctitle;
10435 }
10436 return albumtitle;
10437 }
10438
GetBoxsetsCount()10439 int CMusicDatabase::GetBoxsetsCount()
10440 {
10441 return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
10442 }
10443
GetAlbumDiscsCount(int idAlbum)10444 int CMusicDatabase::GetAlbumDiscsCount(int idAlbum)
10445 {
10446 std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum);
10447 return GetSingleValueInt(strSQL);
10448 }
10449
GetCompilationAlbumsCount()10450 int CMusicDatabase::GetCompilationAlbumsCount()
10451 {
10452 return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
10453 }
10454
GetSinglesCount()10455 int CMusicDatabase::GetSinglesCount()
10456 {
10457 CDatabase::Filter filter(PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')", CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
10458 return GetSongsCount(filter);
10459 }
10460
GetArtistCountForRole(int role)10461 int CMusicDatabase::GetArtistCountForRole(int role)
10462 {
10463 std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role);
10464 return GetSingleValueInt(strSQL);
10465 }
10466
GetArtistCountForRole(const std::string & strRole)10467 int CMusicDatabase::GetArtistCountForRole(const std::string& strRole)
10468 {
10469 std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist JOIN role ON song_artist.idRole = role.idRole WHERE role.strRole LIKE '%s'", strRole.c_str());
10470 return GetSingleValueInt(strSQL);
10471 }
10472
SetPathHash(const std::string & path,const std::string & hash)10473 bool CMusicDatabase::SetPathHash(const std::string &path, const std::string &hash)
10474 {
10475 try
10476 {
10477 if (nullptr == m_pDB)
10478 return false;
10479 if (nullptr == m_pDS)
10480 return false;
10481
10482 if (hash.empty())
10483 { // this is an empty folder - we need only add it to the path table
10484 // if the path actually exists
10485 if (!CDirectory::Exists(path))
10486 return false;
10487 }
10488 int idPath = AddPath(path);
10489 if (idPath < 0) return false;
10490
10491 std::string strSQL=PrepareSQL("update path set strHash='%s' where idPath=%ld", hash.c_str(), idPath);
10492 m_pDS->exec(strSQL);
10493
10494 return true;
10495 }
10496 catch (...)
10497 {
10498 CLog::Log(LOGERROR, "%s (%s, %s) failed", __FUNCTION__, path.c_str(), hash.c_str());
10499 }
10500
10501 return false;
10502 }
10503
GetPathHash(const std::string & path,std::string & hash)10504 bool CMusicDatabase::GetPathHash(const std::string &path, std::string &hash)
10505 {
10506 try
10507 {
10508 if (nullptr == m_pDB)
10509 return false;
10510 if (nullptr == m_pDS)
10511 return false;
10512
10513 std::string strSQL=PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
10514 m_pDS->query(strSQL);
10515 if (m_pDS->num_rows() == 0)
10516 return false;
10517 hash = m_pDS->fv("strHash").get_asString();
10518 return true;
10519 }
10520 catch (...)
10521 {
10522 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, path.c_str());
10523 }
10524
10525 return false;
10526 }
10527
RemoveSongsFromPath(const std::string & path1,MAPSONGS & songmap,bool exact)10528 bool CMusicDatabase::RemoveSongsFromPath(const std::string &path1, MAPSONGS& songmap, bool exact)
10529 {
10530 // We need to remove all songs from this path, as their tags are going
10531 // to be re-read. We need to remove all songs from the song table + all links to them
10532 // from the song link tables (as otherwise if a song is added back
10533 // to the table with the same idSong, these tables can't be cleaned up properly later)
10534
10535 //! @todo SQLite probably doesn't allow this, but can we rely on that??
10536
10537 // We don't need to remove orphaned albums at this point as in AddAlbum() we check
10538 // first whether the album has already been read during this scan, and if it hasn't
10539 // we check whether it's in the table and update accordingly at that point, removing the entries from
10540 // the album link tables. The only failure point for this is albums
10541 // that span multiple folders, where just the files in one folder have been changed. In this case
10542 // any linked fields that are only in the files that haven't changed will be removed. Clearly
10543 // the primary albumartist still matches (as that's what we looked up based on) so is this really
10544 // an issue? I don't think it is, as those artists will still have links to the album via the songs
10545 // which is generally what we rely on, so the only failure point is albumartist lookup. In this
10546 // case, it will return only things in the album_artist table from the newly updated songs (and
10547 // only if they have additional artists). I think the effect of this is minimal at best, as ALL
10548 // songs in the album should have the same albumartist!
10549
10550 // we also remove the path at this point as it will be added later on if the
10551 // path still exists.
10552 // After scanning we then remove the orphaned artists, genres and thumbs.
10553
10554 // Note: when used to remove all songs from a path and its subpath (exact=false), this
10555 // does miss archived songs.
10556 std::string path(path1);
10557 SetLibraryLastUpdated();
10558 try
10559 {
10560 if (!URIUtils::HasSlashAtEnd(path))
10561 URIUtils::AddSlashAtEnd(path);
10562
10563 if (nullptr == m_pDB)
10564 return false;
10565 if (nullptr == m_pDS)
10566 return false;
10567
10568 // Filename is not unique for a path as songs from a cuesheet have same filename.
10569 // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
10570 // in a folder and some edited and rescanned.
10571 // Hence order by filename so these songs can be gathered together.
10572 std::string where;
10573 if (exact)
10574 where = PrepareSQL(" where strPath='%s'", path.c_str());
10575 else
10576 where = PrepareSQL(" where SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()), path.c_str());
10577 std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName";
10578 if (!m_pDS->query(sql)) return false;
10579 int iRowsFound = m_pDS->num_rows();
10580 if (iRowsFound > 0)
10581 {
10582 // Each file is potentially mapped to a list of songs, gather these and save as list
10583 VECSONGS songs;
10584 std::string filename;
10585 std::vector<std::string> songIds;
10586 while (!m_pDS->eof())
10587 {
10588 CSong song = GetSongFromDataset();
10589 if (!filename.empty() && filename != song.strFileName)
10590 {
10591 // Save songs for previous filename
10592 songmap.insert(std::make_pair(filename, songs));
10593 songs.clear();
10594 }
10595 song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb");
10596 songs.emplace_back(song);
10597 songIds.push_back(PrepareSQL("%i", song.idSong));
10598 filename = song.strFileName;
10599
10600 m_pDS->next();
10601 }
10602 m_pDS->close();
10603 songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
10604
10605 //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
10606 for (const auto& id : songIds)
10607 AnnounceRemove(MediaTypeSong, atoi(id.c_str()));
10608
10609 // Delete all songs, and anything linked to them via triggers
10610 std::string strIDs = StringUtils::Join(songIds, ",");
10611 sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")";
10612 m_pDS->exec(sql);
10613 }
10614 // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
10615 sql = "delete from path" + where;
10616 m_pDS->exec(sql);
10617 return iRowsFound > 0;
10618 }
10619 catch (...)
10620 {
10621 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, path.c_str());
10622 }
10623 return false;
10624 }
10625
CheckArtistLinksChanged()10626 void CMusicDatabase::CheckArtistLinksChanged()
10627 {
10628 std::string strSQL = "SELECT COUNT(1) FROM removed_link ";
10629 int iLinks = GetSingleValueInt(strSQL, m_pDS);
10630 if (iLinks > 0)
10631 {
10632 SetArtistLinksUpdated(); // Store datetime artist links last updated
10633 DeleteRemovedLinks(); // Clean-up artist links
10634 }
10635 }
10636
GetPaths(std::set<std::string> & paths)10637 bool CMusicDatabase::GetPaths(std::set<std::string> &paths)
10638 {
10639 try
10640 {
10641 if (nullptr == m_pDB)
10642 return false;
10643 if (nullptr == m_pDS)
10644 return false;
10645
10646 paths.clear();
10647
10648 // find all paths
10649 if (!m_pDS->query("select strPath from path")) return false;
10650 int iRowsFound = m_pDS->num_rows();
10651 if (iRowsFound == 0)
10652 {
10653 m_pDS->close();
10654 return true;
10655 }
10656 while (!m_pDS->eof())
10657 {
10658 paths.insert(m_pDS->fv("strPath").get_asString());
10659 m_pDS->next();
10660 }
10661 m_pDS->close();
10662 return true;
10663 }
10664 catch (...)
10665 {
10666 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
10667 }
10668 return false;
10669 }
10670
SetSongUserrating(const std::string & filePath,int userrating)10671 bool CMusicDatabase::SetSongUserrating(const std::string &filePath, int userrating)
10672 {
10673 try
10674 {
10675 if (filePath.empty()) return false;
10676 if (nullptr == m_pDB)
10677 return false;
10678 if (nullptr == m_pDS)
10679 return false;
10680
10681 int songID = GetSongIDFromPath(filePath);
10682 if (-1 == songID) return false;
10683
10684 return SetSongUserrating(songID, userrating);
10685 }
10686 catch (...)
10687 {
10688 CLog::Log(LOGERROR, "%s (%s,%i) failed", __FUNCTION__, filePath.c_str(), userrating);
10689 }
10690 return false;
10691 }
10692
SetSongUserrating(int idSong,int userrating)10693 bool CMusicDatabase::SetSongUserrating(int idSong, int userrating)
10694 {
10695 try
10696 {
10697 if (nullptr == m_pDB)
10698 return false;
10699 if (nullptr == m_pDS)
10700 return false;
10701
10702 std::string sql =
10703 PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong);
10704 m_pDS->exec(sql);
10705 return true;
10706 }
10707 catch (...)
10708 {
10709 CLog::Log(LOGERROR, "%s (%i,%i) failed", __FUNCTION__, idSong, userrating);
10710 }
10711 return false;
10712 }
10713
SetAlbumUserrating(const int idAlbum,int userrating)10714 bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating)
10715 {
10716 try
10717 {
10718 if (nullptr == m_pDB)
10719 return false;
10720 if (nullptr == m_pDS)
10721 return false;
10722
10723 if (-1 == idAlbum) return false;
10724 std::string sql =
10725 PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum);
10726 m_pDS->exec(sql);
10727 return true;
10728 }
10729 catch (...)
10730 {
10731 CLog::Log(LOGERROR, "%s (%i,%i) failed", __FUNCTION__, idAlbum, userrating);
10732 }
10733 return false;
10734 }
10735
SetSongVotes(const std::string & filePath,int votes)10736 bool CMusicDatabase::SetSongVotes(const std::string &filePath, int votes)
10737 {
10738 try
10739 {
10740 if (filePath.empty()) return false;
10741 if (nullptr == m_pDB)
10742 return false;
10743 if (nullptr == m_pDS)
10744 return false;
10745
10746 int songID = GetSongIDFromPath(filePath);
10747 if (-1 == songID) return false;
10748
10749 std::string sql =
10750 PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID);
10751
10752 m_pDS->exec(sql);
10753 return true;
10754 }
10755 catch (...)
10756 {
10757 CLog::Log(LOGERROR, "%s (%s,%i) failed", __FUNCTION__, filePath.c_str(), votes);
10758 }
10759 return false;
10760 }
10761
GetSongIDFromPath(const std::string & filePath)10762 int CMusicDatabase::GetSongIDFromPath(const std::string &filePath)
10763 {
10764 // grab the where string to identify the song id
10765 CURL url(filePath);
10766 if (url.IsProtocol("musicdb"))
10767 {
10768 std::string strFile=URIUtils::GetFileName(filePath);
10769 URIUtils::RemoveExtension(strFile);
10770 return atoi(strFile.c_str());
10771 }
10772 // hit the db
10773 try
10774 {
10775 if (nullptr == m_pDB)
10776 return -1;
10777 if (nullptr == m_pDS)
10778 return -1;
10779
10780 std::string strPath, strFileName;
10781 SplitPath(filePath, strPath, strFileName);
10782 URIUtils::AddSlashAtEnd(strPath);
10783
10784 std::string sql = PrepareSQL("select idSong from song join path on song.idPath = path.idPath where song.strFileName='%s' and path.strPath='%s'", strFileName.c_str(), strPath.c_str());
10785 if (!m_pDS->query(sql)) return -1;
10786
10787 if (m_pDS->num_rows() == 0)
10788 {
10789 m_pDS->close();
10790 return -1;
10791 }
10792
10793 int songID = m_pDS->fv("idSong").get_asInt();
10794 m_pDS->close();
10795 return songID;
10796 }
10797 catch (...)
10798 {
10799 CLog::Log(LOGERROR, "%s (%s) failed", __FUNCTION__, filePath.c_str());
10800 }
10801 return -1;
10802 }
10803
CommitTransaction()10804 bool CMusicDatabase::CommitTransaction()
10805 {
10806 if (CDatabase::CommitTransaction())
10807 { // number of items in the db has likely changed, so reset the infomanager cache
10808 CGUIComponent* gui = CServiceBroker::GetGUI();
10809 if (gui)
10810 {
10811 gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(LIBRARY_HAS_MUSIC, GetSongsCount() > 0);
10812 return true;
10813 }
10814 }
10815 return false;
10816 }
10817
SetScraperAll(const std::string & strBaseDir,const ADDON::ScraperPtr & scraper)10818 bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper)
10819 {
10820 if (nullptr == m_pDB)
10821 return false;
10822 if (nullptr == m_pDS)
10823 return false;
10824 std::string strSQL;
10825 int idSetting = -1;
10826 try
10827 {
10828 CONTENT_TYPE content = CONTENT_NONE;
10829
10830 // Build where clause from virtual path
10831 Filter extFilter;
10832 CMusicDbUrl musicUrl;
10833 SortDescription sorting;
10834 if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
10835 return false;
10836
10837 std::string itemType = musicUrl.GetType();
10838 if (StringUtils::EqualsNoCase(itemType, "artists"))
10839 {
10840 content = CONTENT_ARTISTS;
10841 }
10842 else if (StringUtils::EqualsNoCase(itemType, "albums"))
10843 {
10844 content = CONTENT_ALBUMS;
10845 }
10846 else
10847 return false; //Only artists and albums have info settings
10848
10849 std::string strSQLWhere;
10850 if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere))
10851 return false;
10852
10853 // Replace view names with table names
10854 StringUtils::Replace(strSQLWhere, "artistview", "artist");
10855 StringUtils::Replace(strSQLWhere, "albumview", "album");
10856
10857 BeginTransaction();
10858 // Clear current scraper settings (0 => default scraper used)
10859 if (content == CONTENT_ARTISTS)
10860 strSQL = "UPDATE artist SET idInfoSetting = %i ";
10861 else
10862 strSQL = "UPDATE album SET idInfoSetting = %i ";
10863 strSQL = PrepareSQL(strSQL, 0) + strSQLWhere;
10864 m_pDS->exec(strSQL);
10865
10866 //Remove orphaned settings
10867 CleanupInfoSettings();
10868
10869 if (scraper)
10870 {
10871 // Add new info setting
10872 strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
10873 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
10874 m_pDS->exec(strSQL);
10875 idSetting = static_cast<int>(m_pDS->lastinsertid());
10876
10877 if (content == CONTENT_ARTISTS)
10878 strSQL = "UPDATE artist SET idInfoSetting = %i ";
10879 else
10880 strSQL = "UPDATE album SET idInfoSetting = %i ";
10881 strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere;
10882 m_pDS->exec(strSQL);
10883 }
10884 CommitTransaction();
10885 return true;
10886 }
10887 catch (...)
10888 {
10889 RollbackTransaction();
10890 CLog::Log(LOGERROR, "%s - (%s, %s) failed", __FUNCTION__, strBaseDir.c_str(), strSQL.c_str());
10891 }
10892 return false;
10893 }
10894
SetScraper(int id,const CONTENT_TYPE & content,const ADDON::ScraperPtr & scraper)10895 bool CMusicDatabase::SetScraper(int id,
10896 const CONTENT_TYPE& content,
10897 const ADDON::ScraperPtr& scraper)
10898 {
10899 if (nullptr == m_pDB)
10900 return false;
10901 if (nullptr == m_pDS)
10902 return false;
10903 std::string strSQL;
10904 int idSetting = -1;
10905 try
10906 {
10907 BeginTransaction();
10908 // Fetch current info settings for item, 0 => default is used
10909 if (content == CONTENT_ARTISTS)
10910 strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
10911 else
10912 strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
10913 strSQL = PrepareSQL(strSQL, id);
10914 m_pDS->query(strSQL);
10915 if (m_pDS->num_rows() > 0)
10916 idSetting = m_pDS->fv("idInfoSetting").get_asInt();
10917 m_pDS->close();
10918
10919 if (idSetting < 1)
10920 { // Add new info setting
10921 strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
10922 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
10923 m_pDS->exec(strSQL);
10924 idSetting = static_cast<int>(m_pDS->lastinsertid());
10925
10926 if (content == CONTENT_ARTISTS)
10927 strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
10928 else
10929 strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
10930 strSQL = PrepareSQL(strSQL, idSetting, id);
10931 m_pDS->exec(strSQL);
10932 }
10933 else
10934 { // Update info setting
10935 strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' WHERE idSetting = %i";
10936 strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting);
10937 m_pDS->exec(strSQL);
10938 }
10939 CommitTransaction();
10940 return true;
10941 }
10942 catch (...)
10943 {
10944 RollbackTransaction();
10945 CLog::Log(LOGERROR, "%s - (%i, %s) failed", __FUNCTION__, id, strSQL.c_str());
10946 }
10947 return false;
10948 }
10949
GetScraper(int id,const CONTENT_TYPE & content,ADDON::ScraperPtr & scraper)10950 bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE &content, ADDON::ScraperPtr& scraper)
10951 {
10952 std::string scraperUUID;
10953 std::string strSettings;
10954 try
10955 {
10956 if (nullptr == m_pDB)
10957 return false;
10958 if (nullptr == m_pDS)
10959 return false;
10960
10961 std::string strSQL;
10962 strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
10963 if (content == CONTENT_ARTISTS)
10964 strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting WHERE artist.idArtist = %i";
10965 else
10966 strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting WHERE album.idAlbum = %i";
10967 strSQL = PrepareSQL(strSQL, id);
10968 m_pDS->query(strSQL);
10969 if (!m_pDS->eof())
10970 { // try and ascertain scraper
10971 scraperUUID = m_pDS->fv("strScraperPath").get_asString();
10972 strSettings = m_pDS->fv("strSettings").get_asString();
10973
10974 // Use pre configured or default scraper
10975 ADDON::AddonPtr addon;
10976 if (!scraperUUID.empty() &&
10977 CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon, ADDON::ADDON_UNKNOWN,
10978 ADDON::OnlyEnabled::YES) &&
10979 addon)
10980 {
10981 scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
10982 if (scraper)
10983 // Set settings
10984 scraper->SetPathSettings(content, strSettings);
10985 }
10986 }
10987 m_pDS->close();
10988
10989 if (!scraper)
10990 { // use default music scraper instead
10991 ADDON::AddonPtr addon;
10992 if(ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::ScraperTypeFromContent(content), addon))
10993 {
10994 scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
10995 return scraper != NULL;
10996 }
10997 else
10998 return false;
10999 }
11000
11001 return true;
11002 }
11003 catch (...)
11004 {
11005 CLog::Log(LOGERROR, "%s -(%i, %s %s) failed", __FUNCTION__, id, scraperUUID.c_str(), strSettings.c_str());
11006 }
11007 return false;
11008 }
11009
ScraperInUse(const std::string & scraperID) const11010 bool CMusicDatabase::ScraperInUse(const std::string &scraperID) const
11011 {
11012 try
11013 {
11014 if (nullptr == m_pDB)
11015 return false;
11016 if (nullptr == m_pDS)
11017 return false;
11018
11019 std::string sql = PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'",scraperID.c_str());
11020 if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
11021 {
11022 m_pDS->close();
11023 return false;
11024 }
11025 bool found = m_pDS->fv(0).get_asInt() > 0;
11026 m_pDS->close();
11027 return found;
11028 }
11029 catch (...)
11030 {
11031 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, scraperID.c_str());
11032 }
11033 return false;
11034 }
11035
GetItems(const std::string & strBaseDir,CFileItemList & items,const Filter & filter,const SortDescription & sortDescription)11036 bool CMusicDatabase::GetItems(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */)
11037 {
11038 CMusicDbUrl musicUrl;
11039 if (!musicUrl.FromString(strBaseDir))
11040 return false;
11041
11042 return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription);
11043 }
11044
GetItems(const std::string & strBaseDir,const std::string & itemType,CFileItemList & items,const Filter & filter,const SortDescription & sortDescription)11045 bool CMusicDatabase::GetItems(const std::string &strBaseDir, const std::string &itemType, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */)
11046 {
11047 if (StringUtils::EqualsNoCase(itemType, "genres"))
11048 return GetGenresNav(strBaseDir, items, filter);
11049 else if (StringUtils::EqualsNoCase(itemType, "sources"))
11050 return GetSourcesNav(strBaseDir, items, filter);
11051 else if (StringUtils::EqualsNoCase(itemType, "years"))
11052 return GetYearsNav(strBaseDir, items, filter);
11053 else if (StringUtils::EqualsNoCase(itemType, "roles"))
11054 return GetRolesNav(strBaseDir, items, filter);
11055 else if (StringUtils::EqualsNoCase(itemType, "artists"))
11056 return GetArtistsNav(strBaseDir, items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), -1, -1, -1, filter, sortDescription);
11057 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11058 return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription);
11059 else if (StringUtils::EqualsNoCase(itemType, "discs"))
11060 return GetDiscsByWhere(strBaseDir, filter, items, sortDescription);
11061 else if (StringUtils::EqualsNoCase(itemType, "songs"))
11062 return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true);
11063
11064 return false;
11065 }
11066
GetItemById(const std::string & itemType,int id)11067 std::string CMusicDatabase::GetItemById(const std::string &itemType, int id)
11068 {
11069 if (StringUtils::EqualsNoCase(itemType, "genres"))
11070 return GetGenreById(id);
11071 else if (StringUtils::EqualsNoCase(itemType, "sources"))
11072 return GetSourceById(id);
11073 else if (StringUtils::EqualsNoCase(itemType, "years"))
11074 return StringUtils::Format("%d", id);
11075 else if (StringUtils::EqualsNoCase(itemType, "artists"))
11076 return GetArtistById(id);
11077 else if (StringUtils::EqualsNoCase(itemType, "albums"))
11078 return GetAlbumById(id);
11079 else if (StringUtils::EqualsNoCase(itemType, "roles"))
11080 return GetRoleById(id);
11081
11082 return "";
11083 }
11084
ExportToXML(const CLibExportSettings & settings,CGUIDialogProgress * progressDialog)11085 void CMusicDatabase::ExportToXML(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog /*= nullptr*/)
11086 {
11087 if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) &&
11088 !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) &&
11089 !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) &&
11090 !settings.IsItemExported(ELIBEXPORT_ALBUMS) &&
11091 !settings.IsItemExported(ELIBEXPORT_SONGS))
11092 return;
11093
11094 // Exporting albums either art or NFO (or both) selected
11095 if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
11096 settings.m_skipnfo && !settings.m_artwork &&
11097 settings.IsItemExported(ELIBEXPORT_ALBUMS))
11098 return;
11099
11100 std::string strFolder;
11101 if (settings.IsSingleFile() || settings.IsSeparateFiles())
11102 {
11103 // Exporting to single file or separate files in a specified location
11104 if (settings.m_strPath.empty())
11105 return;
11106
11107 strFolder = settings.m_strPath;
11108 if (!URIUtils::HasSlashAtEnd(strFolder))
11109 URIUtils::AddSlashAtEnd(strFolder);
11110 strFolder = URIUtils::GetDirectory(strFolder);
11111 if (strFolder.empty())
11112 return;
11113 }
11114 else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists()))
11115 {
11116 // Exporting artist folders only, or artist NFO or art to library folders
11117 // need Artist Information Folder defined.
11118 // (Album NFO and art goes to music folders)
11119 strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
11120 if (strFolder.empty())
11121 return;
11122 }
11123
11124 //
11125 bool artistfoldersonly;
11126 artistfoldersonly = settings.IsArtistFoldersOnly() ||
11127 ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
11128 settings.m_skipnfo && !settings.m_artwork);
11129
11130 int iFailCount = 0;
11131 try
11132 {
11133 if (nullptr == m_pDB)
11134 return;
11135 if (nullptr == m_pDS)
11136 return;
11137 if (nullptr == m_pDS2)
11138 return;
11139
11140 // Create our xml document
11141 CXBMCTinyXML xmlDoc;
11142 TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11143 xmlDoc.InsertEndChild(decl);
11144 TiXmlNode *pMain = NULL;
11145 if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly)
11146 pMain = &xmlDoc;
11147 else if (settings.IsSingleFile())
11148 {
11149 TiXmlElement xmlMainElement("musicdb");
11150 pMain = xmlDoc.InsertEndChild(xmlMainElement);
11151 }
11152
11153 if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly)
11154 {
11155 // Find albums to export
11156 std::vector<int> albumIds;
11157 std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
11158 CAlbum::ReleaseTypeToString(CAlbum::Album).c_str());
11159 if (!settings.m_unscraped)
11160 strSQL += "AND lastScraped IS NOT NULL";
11161 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - %s", __FUNCTION__, strSQL.c_str());
11162 m_pDS->query(strSQL);
11163
11164 int total = m_pDS->num_rows();
11165 int current = 0;
11166
11167 albumIds.reserve(total);
11168 while (!m_pDS->eof())
11169 {
11170 albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
11171 m_pDS->next();
11172 }
11173 m_pDS->close();
11174
11175 for (const auto &albumId : albumIds)
11176 {
11177 CAlbum album;
11178 GetAlbum(albumId, album);
11179 std::string strAlbumPath;
11180 std::string strPath;
11181 // Get album path, empty unless all album songs are under a unique folder, and
11182 // there are no songs from another album in the same folder.
11183 if (!GetAlbumPath(albumId, strAlbumPath))
11184 strAlbumPath.clear();
11185 if (settings.IsSingleFile())
11186 {
11187 // Save album to xml, including album path
11188 album.Save(pMain, "album", strAlbumPath);
11189 }
11190 else
11191 { // Separate files and artwork
11192 bool pathfound = false;
11193 if (settings.IsToLibFolders())
11194 { // Save album.nfo and artwork with music files.
11195 // Most albums are under a unique folder, but if songs from various albums are mixed then
11196 // avoid overwriting by not allow NFO and art to be exported
11197 if (strAlbumPath.empty())
11198 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as unique path not found",
11199 __FUNCTION__, album.strAlbum.c_str());
11200 else if (!CDirectory::Exists(strAlbumPath))
11201 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as found path %s does not exist",
11202 __FUNCTION__, album.strAlbum.c_str(), strAlbumPath.c_str());
11203 else
11204 {
11205 strPath = strAlbumPath;
11206 pathfound = true;
11207 }
11208 }
11209 else
11210 { // Save album.nfo and artwork to subfolder on export path
11211 // strPath = strFolder/<albumartist name>/<albumname>
11212 // where <albumname> is either the same name as the album folder
11213 // containing the music files (if unique) or is created using the album name
11214 std::string strAlbumArtist;
11215 pathfound = GetArtistFolderName(album.GetAlbumArtist()[0], album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist);
11216 if (pathfound)
11217 {
11218 strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist);
11219 pathfound = CDirectory::Exists(strPath);
11220 if (!pathfound)
11221 pathfound = CDirectory::Create(strPath);
11222 }
11223 if (!pathfound)
11224 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as could not create %s",
11225 __FUNCTION__, album.strAlbum.c_str(), strPath.c_str());
11226 else
11227 {
11228 std::string strAlbumFolder;
11229 pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder);
11230 if (pathfound)
11231 {
11232 strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder);
11233 pathfound = CDirectory::Exists(strPath);
11234 if (!pathfound)
11235 pathfound = CDirectory::Create(strPath);
11236 }
11237 if (!pathfound)
11238 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting album %s as could not create %s",
11239 __FUNCTION__, album.strAlbum.c_str(), strPath.c_str());
11240 }
11241 }
11242 if (pathfound)
11243 {
11244 if (!settings.m_skipnfo)
11245 {
11246 // Save album to NFO, including album path
11247 album.Save(pMain, "album", strAlbumPath);
11248 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo");
11249 if (settings.m_overwrite || !CFile::Exists(nfoFile))
11250 {
11251 if (!xmlDoc.SaveFile(nfoFile))
11252 {
11253 CLog::Log(LOGERROR, "CMusicDatabase::%s: Album nfo export failed! ('%s')", __FUNCTION__, nfoFile.c_str());
11254 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(20302), nfoFile);
11255 iFailCount++;
11256 }
11257 }
11258 }
11259 if (settings.m_artwork)
11260 {
11261 // Save art in album folder
11262 // Note thumb resoluton may be lower than original when overwriting
11263 std::map<std::string, std::string> artwork;
11264 std::string savedArtfile;
11265 if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork))
11266 {
11267 for (const auto &art : artwork)
11268 {
11269 if (art.first == "thumb")
11270 savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
11271 else
11272 savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
11273 CTextureCache::GetInstance().Export(art.second, savedArtfile, settings.m_overwrite);
11274 }
11275 }
11276 }
11277 xmlDoc.Clear();
11278 TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11279 xmlDoc.InsertEndChild(decl);
11280 }
11281 }
11282
11283 if ((current % 50) == 0 && progressDialog)
11284 {
11285 progressDialog->SetLine(1, CVariant{ album.strAlbum });
11286 progressDialog->SetPercentage(current * 100 / total);
11287 if (progressDialog->IsCanceled())
11288 return;
11289 }
11290 current++;
11291 }
11292 }
11293
11294 // Export song playback history to single file only
11295 if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS))
11296 {
11297 if (!ExportSongHistory(pMain, progressDialog))
11298 return;
11299 }
11300
11301 if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty())
11302 {
11303 // Find artists to export
11304 std::vector<int> artistIds;
11305 std::string sql;
11306 Filter filter;
11307
11308 if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS))
11309 filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist WHERE album_artist.idArtist = artist.idArtist)", false);
11310 if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS))
11311 {
11312 if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
11313 filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = artist.idArtist )", false);
11314 else
11315 filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)", false);
11316 }
11317 else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
11318 filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)", false);
11319
11320 if (!settings.m_unscraped && !artistfoldersonly)
11321 filter.AppendWhere("lastScraped IS NOT NULL", true);
11322
11323 std::string strSQL = "SELECT idArtist FROM artist";
11324 BuildSQL(strSQL, filter, strSQL);
11325 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - %s", __FUNCTION__, strSQL.c_str());
11326
11327 m_pDS->query(strSQL);
11328 int total = m_pDS->num_rows();
11329 int current = 0;
11330 artistIds.reserve(total);
11331 while (!m_pDS->eof())
11332 {
11333 artistIds.push_back(m_pDS->fv("idArtist").get_asInt());
11334 m_pDS->next();
11335 }
11336 m_pDS->close();
11337
11338 for (const auto &artistId : artistIds)
11339 {
11340 CArtist artist;
11341 GetArtist(artistId, artist, !artistfoldersonly); // include discography when not folders only
11342 std::string strPath;
11343 std::map<std::string, std::string> artwork;
11344 if (settings.IsSingleFile())
11345 {
11346 // Save artist to xml, and old path (common to music files) if it has one
11347 GetOldArtistPath(artist.idArtist, strPath);
11348 artist.Save(pMain, "artist", strPath);
11349
11350 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
11351 { // append to the XML
11352 TiXmlElement additionalNode("art");
11353 for (const auto &i : artwork)
11354 XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
11355 pMain->LastChild()->InsertEndChild(additionalNode);
11356 }
11357 }
11358 else
11359 { // Separate files: artist.nfo and artwork in strFolder/<artist name>
11360 // Get unique folder allowing for duplicate names e.g. 2 x John Williams
11361 bool pathfound = GetArtistFolderName(artist, strPath);
11362 if (pathfound)
11363 {
11364 strPath = URIUtils::AddFileToFolder(strFolder, strPath);
11365 pathfound = CDirectory::Exists(strPath);
11366 if (!pathfound)
11367 pathfound = CDirectory::Create(strPath);
11368 }
11369 if (!pathfound)
11370 CLog::Log(LOGDEBUG, "CMusicDatabase::%s - Not exporting artist %s as could not create %s",
11371 __FUNCTION__, artist.strArtist.c_str(), strPath.c_str());
11372 else
11373 {
11374 if (!artistfoldersonly)
11375 {
11376 if (!settings.m_skipnfo)
11377 {
11378 artist.Save(pMain, "artist", strPath);
11379 std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo");
11380 if (settings.m_overwrite || !CFile::Exists(nfoFile))
11381 {
11382 if (!xmlDoc.SaveFile(nfoFile))
11383 {
11384 CLog::Log(LOGERROR, "CMusicDatabase::%s: Artist nfo export failed! ('%s')", __FUNCTION__, nfoFile.c_str());
11385 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(20302), nfoFile);
11386 iFailCount++;
11387 }
11388 }
11389 }
11390 if (settings.m_artwork)
11391 {
11392 std::string savedArtfile;
11393 if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
11394 {
11395 for (const auto &art : artwork)
11396 {
11397 if (art.first == "thumb")
11398 savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
11399 else
11400 savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
11401 CTextureCache::GetInstance().Export(art.second, savedArtfile, settings.m_overwrite);
11402 }
11403 }
11404 }
11405 xmlDoc.Clear();
11406 TiXmlDeclaration decl("1.0", "UTF-8", "yes");
11407 xmlDoc.InsertEndChild(decl);
11408 }
11409 }
11410 }
11411 if ((current % 50) == 0 && progressDialog)
11412 {
11413 progressDialog->SetLine(1, CVariant{ artist.strArtist });
11414 progressDialog->SetPercentage(current * 100 / total);
11415 if (progressDialog->IsCanceled())
11416 return;
11417 }
11418 current++;
11419 }
11420 }
11421
11422 if (settings.IsSingleFile())
11423 {
11424 std::string xmlFile = URIUtils::AddFileToFolder(strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
11425 if (CFile::Exists(xmlFile))
11426 xmlFile = URIUtils::AddFileToFolder(strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
11427 xmlDoc.SaveFile(xmlFile);
11428
11429 CVariant data;
11430 data["file"] = xmlFile;
11431 if (iFailCount > 0)
11432 data["failcount"] = iFailCount;
11433 CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport",
11434 data);
11435 }
11436 }
11437 catch (...)
11438 {
11439 CLog::Log(LOGERROR, "CMusicDatabase::%s failed", __FUNCTION__);
11440 iFailCount++;
11441 }
11442
11443 if (progressDialog)
11444 progressDialog->Close();
11445
11446 if (iFailCount > 0 && progressDialog)
11447 HELPERS::ShowOKDialogLines(CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011).c_str(), iFailCount)});
11448 }
11449
ExportSongHistory(TiXmlNode * pNode,CGUIDialogProgress * progressDialog)11450 bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog)
11451 {
11452 try
11453 {
11454 // Export songs with some playback history
11455 std::string strSQL = "SELECT idSong, song.idAlbum, "
11456 "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, "
11457 "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, "
11458 "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating "
11459 "FROM song JOIN album on album.idAlbum = song.idAlbum "
11460 "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0";
11461
11462 CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL.c_str());
11463 m_pDS->query(strSQL);
11464
11465 int total = m_pDS->num_rows();
11466 int current = 0;
11467 while (!m_pDS->eof())
11468 {
11469 TiXmlElement songElement("song");
11470 TiXmlNode* song = pNode->InsertEndChild(songElement);
11471
11472 XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt());
11473 XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString());
11474 XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString());
11475 XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt());
11476 XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString());
11477 XMLUtils::SetString(song, "musicbrainztrackid", m_pDS->fv("strMusicBrainzTrackID").get_asString());
11478 XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt());
11479 XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString());
11480 XMLUtils::SetString(song, "musicbrainzalbumid", m_pDS->fv("strMusicBrainzAlbumID").get_asString());
11481 XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString());
11482 XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt());
11483 XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString());
11484 auto* rating = XMLUtils::SetString(song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat()));
11485 if (rating)
11486 rating->ToElement()->SetAttribute("max", 10);
11487 XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt());
11488 auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt());
11489 if (userrating)
11490 userrating->ToElement()->SetAttribute("max", 10);
11491
11492 if ((current % 100) == 0 && progressDialog)
11493 {
11494 progressDialog->SetLine(1, CVariant{ m_pDS->fv("strAlbum").get_asString() });
11495 progressDialog->SetPercentage(current * 100 / total);
11496 if (progressDialog->IsCanceled())
11497 {
11498 m_pDS->close();
11499 return false;
11500 }
11501 }
11502 current++;
11503
11504 m_pDS->next();
11505 }
11506 m_pDS->close();
11507 return true;
11508 }
11509 catch (...)
11510 {
11511 CLog::Log(LOGERROR, "{0} failed", __FUNCTION__);
11512 }
11513 return false;
11514 }
11515
ImportFromXML(const std::string & xmlFile,CGUIDialogProgress * progressDialog)11516 void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog)
11517 {
11518 try
11519 {
11520 if (nullptr == m_pDB)
11521 return;
11522 if (nullptr == m_pDS)
11523 return;
11524
11525 CXBMCTinyXML xmlDoc;
11526 if (!xmlDoc.LoadFile(xmlFile) && progressDialog)
11527 {
11528 HELPERS::ShowOKDialogLines(CVariant{ 20197 }, CVariant{ 38354 }); //"Unable to read xml file"
11529 return;
11530 }
11531
11532 TiXmlElement *root = xmlDoc.RootElement();
11533 if (!root) return;
11534
11535 TiXmlElement *entry = root->FirstChildElement();
11536 int current = 0;
11537 int total = 0;
11538 int songtotal = 0;
11539 // Count the number of artists, albums and songs
11540 while (entry)
11541 {
11542 if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 ||
11543 StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
11544 total++;
11545 else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
11546 songtotal++;
11547
11548 entry = entry->NextSiblingElement();
11549 }
11550
11551 BeginTransaction();
11552 entry = root->FirstChildElement();
11553 while (entry)
11554 {
11555 std::string strTitle;
11556 if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0)
11557 {
11558 CArtist importedArtist;
11559 importedArtist.Load(entry);
11560 strTitle = importedArtist.strArtist;
11561
11562 // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
11563 int idArtist = GetArtistByMatch(importedArtist);
11564 if (idArtist > -1)
11565 {
11566 CArtist artist;
11567 GetArtist(idArtist, artist, true); // include discography
11568 artist.MergeScrapedArtist(importedArtist, true);
11569 UpdateArtist(artist);
11570 }
11571 else
11572 CLog::Log(LOGDEBUG, "%s - Not import additional artist data as %s not found", __FUNCTION__, importedArtist.strArtist.c_str());
11573 current++;
11574 }
11575 else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
11576 {
11577 CAlbum importedAlbum;
11578 importedAlbum.Load(entry);
11579 strTitle = importedAlbum.strAlbum;
11580 // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
11581 int idAlbum = GetAlbumByMatch(importedAlbum);
11582 if (idAlbum > -1)
11583 {
11584 CAlbum album;
11585 GetAlbum(idAlbum, album, true);
11586 album.MergeScrapedAlbum(importedAlbum, true);
11587 UpdateAlbum(album); //Will replace song artists if present in xml
11588 }
11589 else
11590 CLog::Log(LOGDEBUG, "%s - Not import additional album data as %s not found", __FUNCTION__, importedAlbum.strAlbum.c_str());
11591
11592 current++;
11593 }
11594 entry = entry ->NextSiblingElement();
11595 if (progressDialog && total)
11596 {
11597 progressDialog->SetPercentage(current * 100 / total);
11598 progressDialog->SetLine(2, CVariant{std::move(strTitle)});
11599 progressDialog->Progress();
11600 if (progressDialog->IsCanceled())
11601 {
11602 RollbackTransaction();
11603 return;
11604 }
11605 }
11606 }
11607 CommitTransaction();
11608
11609 // Import song playback history <song> entries found
11610 if (songtotal > 0)
11611 if (!ImportSongHistory(xmlFile, songtotal, progressDialog))
11612 return;
11613
11614 CGUIComponent* gui = CServiceBroker::GetGUI();
11615 if (gui)
11616 gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
11617 }
11618 catch (...)
11619 {
11620 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
11621 RollbackTransaction();
11622 }
11623 if (progressDialog)
11624 progressDialog->Close();
11625 }
11626
ImportSongHistory(const std::string & xmlFile,const int total,CGUIDialogProgress * progressDialog)11627 bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile, const int total, CGUIDialogProgress* progressDialog)
11628 {
11629 bool bHistSongExists = false;
11630 try
11631 {
11632 CXBMCTinyXML xmlDoc;
11633 if (!xmlDoc.LoadFile(xmlFile))
11634 return false;
11635
11636 TiXmlElement* root = xmlDoc.RootElement();
11637 if (!root)
11638 return false;
11639
11640 TiXmlElement* entry = root->FirstChildElement();
11641 int current = 0;
11642
11643 if (progressDialog)
11644 {
11645 progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history"
11646 progressDialog->SetLine(2, CVariant{ "" });
11647 }
11648
11649 // As can be many songs do in db, not song at a time which would be slow
11650 // Convert xml entries into a SQL bulk insert statement
11651 std::string strSQL;
11652 entry = root->FirstChildElement();
11653 while (entry)
11654 {
11655 std::string strArtistDisp;
11656 std::string strTitle;
11657 int iTrack;
11658 std::string strFilename;
11659 std::string strMusicBrainzTrackID;
11660 std::string strAlbum;
11661 std::string strMusicBrainzAlbumID;
11662 std::string strAlbumArtistDisp;
11663 int iTimesplayed;
11664 std::string lastplayed;
11665 int iUserrating = 0;
11666 float fRating = 0.0;
11667 int iVotes;
11668 std::string strSQLSong;
11669 if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
11670 {
11671 XMLUtils::GetString(entry, "artistdesc", strArtistDisp);
11672 XMLUtils::GetString(entry, "title", strTitle);
11673 XMLUtils::GetInt(entry, "track", iTrack);
11674 XMLUtils::GetString(entry, "filename", strFilename);
11675 XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID);
11676 XMLUtils::GetString(entry, "albumtitle", strAlbum);
11677 XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID);
11678 XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp);
11679 XMLUtils::GetInt(entry, "timesplayed", iTimesplayed);
11680 XMLUtils::GetString(entry, "lastplayed", lastplayed);
11681 const TiXmlElement* rElement = entry->FirstChildElement("rating");
11682 if (rElement)
11683 {
11684 float rating = 0;
11685 float max_rating = 10;
11686 XMLUtils::GetFloat(entry, "rating", rating);
11687 if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
11688 rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
11689 if (rating > 10.f)
11690 rating = 10.f;
11691 fRating = rating;
11692 }
11693 XMLUtils::GetInt(entry, "votes", iVotes);
11694 const TiXmlElement* userrating = entry->FirstChildElement("userrating");
11695 if (userrating)
11696 {
11697 float rating = 0;
11698 float max_rating = 10;
11699 XMLUtils::GetFloat(entry, "userrating", rating);
11700 if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
11701 rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
11702 if (rating > 10.f)
11703 rating = 10.f;
11704 iUserrating = MathUtils::round_int(rating);
11705 }
11706
11707 strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack);
11708 strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(), strFilename.c_str());
11709 if (strMusicBrainzTrackID.empty())
11710 strSQLSong += PrepareSQL("NULL, ");
11711 else
11712 strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str());
11713 strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str());
11714 if (strMusicBrainzAlbumID.empty())
11715 strSQLSong += PrepareSQL("NULL, ");
11716 else
11717 strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str());
11718 strSQLSong += PrepareSQL("%d, ", iTimesplayed);
11719 if (lastplayed.empty())
11720 strSQLSong += PrepareSQL("NULL, ");
11721 else
11722 strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str());
11723 strSQLSong += PrepareSQL("%.1f, %d, %d, -1, -1)", fRating, iVotes, iUserrating);
11724
11725 if (current > 0)
11726 strSQLSong = ", " + strSQLSong;
11727 strSQL += strSQLSong;
11728 current++;
11729 }
11730
11731 entry = entry->NextSiblingElement();
11732
11733 if ((current % 100) == 0 && progressDialog)
11734 {
11735 progressDialog->SetPercentage(current * 100 / total);
11736 progressDialog->SetLine(3, CVariant{ std::move(strTitle) });
11737 progressDialog->Progress();
11738 if (progressDialog->IsCanceled())
11739 return false;
11740 }
11741 }
11742
11743 CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__, total);
11744 /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of
11745 song table using correlated subqueries to a temp table. An updatable join
11746 to temp table would work in MySQL but SQLite not support updatable joins.
11747 */
11748 m_pDS->exec("CREATE TABLE HistSong ("
11749 "idSongSrc INTEGER primary key, "
11750 "strAlbum varchar(256), "
11751 "strMusicBrainzAlbumID text, "
11752 "strAlbumArtistDisp text, "
11753 "strArtistDisp text, strTitle varchar(512), "
11754 "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, "
11755 "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, "
11756 "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
11757 "userrating INTEGER NOT NULL DEFAULT 0, "
11758 "idAlbum INTEGER, idSong INTEGER)");
11759 bHistSongExists = true;
11760
11761 strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, "
11762 "strFileName, strMusicBrainzTrackID, "
11763 "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, "
11764 " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " + strSQL;
11765 m_pDS->exec(strSQL);
11766
11767 if (progressDialog)
11768 {
11769 progressDialog->SetLine(2, CVariant{38351}); //"Matching data"
11770 progressDialog->SetLine(3, CVariant{ "" });
11771 progressDialog->Progress();
11772 if (progressDialog->IsCanceled())
11773 {
11774 m_pDS->exec("DROP TABLE HistSong");
11775 return false;
11776 }
11777 }
11778
11779 BeginTransaction();
11780 // Match albums first on mbid then artist string and album title, setting idAlbum
11781 // mbid is unique so subquery can only return one result at most
11782 strSQL = "UPDATE HistSong "
11783 "SET idAlbum = (SELECT album.idAlbum FROM album "
11784 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) "
11785 "WHERE EXISTS(SELECT 1 FROM album "
11786 "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0";
11787 m_pDS->exec(strSQL);
11788
11789 // Can only be one album with same title and artist(s) and no mbid.
11790 // But could have 2 releases one with and one without mbid, match up those without mbid
11791 strSQL = "UPDATE HistSong "
11792 "SET idAlbum = (SELECT album.idAlbum FROM album "
11793 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11794 "AND HistSong.strAlbum = album.strAlbum "
11795 "AND album.strMusicBrainzAlbumID IS NULL "
11796 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
11797 "WHERE EXISTS(SELECT 1 FROM album "
11798 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11799 "AND HistSong.strAlbum = album.strAlbum "
11800 "AND album.strMusicBrainzAlbumID IS NULL "
11801 "AND HistSong.strMusicBrainzAlbumID IS NULL) "
11802 "AND idAlbum < 0";
11803 m_pDS->exec(strSQL);
11804
11805 // Try match rest by title and artist(s), prioritise one without mbid
11806 // Target could have multiple releases - with mbid (non-matching) or one without mbid
11807 strSQL = "UPDATE HistSong "
11808 "SET idAlbum = (SELECT album.idAlbum FROM album "
11809 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11810 "AND HistSong.strAlbum = album.strAlbum "
11811 "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
11812 "WHERE EXISTS(SELECT 1 FROM album "
11813 "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
11814 "AND HistSong.strAlbum = album.strAlbum) "
11815 "AND idAlbum < 0";
11816 m_pDS->exec(strSQL);
11817 if (progressDialog)
11818 {
11819 progressDialog->Progress();
11820 if (progressDialog->IsCanceled())
11821 {
11822 RollbackTransaction();
11823 m_pDS->exec("DROP TABLE HistSong");
11824 return false;
11825 }
11826 }
11827
11828 // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong
11829 strSQL = "UPDATE HistSong "
11830 "SET idSong = (SELECT idsong FROM song "
11831 "WHERE HistSong.idAlbum = song.idAlbum AND "
11832 "HistSong.iTrack = song.iTrack AND "
11833 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) "
11834 "WHERE EXISTS(SELECT 1 FROM song "
11835 "WHERE HistSong.idAlbum = song.idAlbum AND "
11836 "HistSong.iTrack = song.iTrack AND "
11837 "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0";
11838 m_pDS->exec(strSQL);
11839
11840 // An album can have more than one song with same track and title (although idAlbum, track and
11841 // title is often unique), but not using filename as an identifier to allow for import of song
11842 // history for renamed files. It is about song playback not file playback.
11843 // Pick the first
11844 strSQL = "UPDATE HistSong "
11845 "SET idSong = (SELECT idsong FROM song "
11846 "WHERE HistSong.idAlbum = song.idAlbum AND "
11847 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
11848 "WHERE EXISTS(SELECT 1 FROM song "
11849 "WHERE HistSong.idAlbum = song.idAlbum AND "
11850 "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0";
11851 m_pDS->exec(strSQL);
11852
11853 CommitTransaction();
11854 if (progressDialog)
11855 {
11856 progressDialog->Progress();
11857 if (progressDialog->IsCanceled())
11858 {
11859 m_pDS->exec("DROP TABLE HistSong");
11860 return false;
11861 }
11862 }
11863
11864 // Create an index to speed up the updates
11865 m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)");
11866
11867 // Log how many songs matched
11868 int unmatched = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS);
11869 CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__, total - unmatched, total);
11870
11871 if (progressDialog)
11872 {
11873 progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history"
11874 progressDialog->Progress();
11875 if (progressDialog->IsCanceled())
11876 {
11877 m_pDS->exec("DROP TABLE HistSong"); // Drops index too
11878 return false;
11879 }
11880 }
11881
11882 /* Update song table using the song ids we have matched.
11883 Use correlated subqueries as SQLite does not support updatable joins.
11884 MySQL requires HistSong table not to be defined temporary for this.
11885 */
11886
11887 BeginTransaction();
11888 // Times played and last played date(when count is greater)
11889 strSQL = "UPDATE song SET iTimesPlayed = "
11890 "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), "
11891 "lastplayed = "
11892 "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) "
11893 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
11894 "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)";
11895 m_pDS->exec(strSQL);
11896
11897 // User rating
11898 strSQL = "UPDATE song SET userrating = "
11899 "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) "
11900 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
11901 "HistSong.idSong = song.idSong AND HistSong.userrating > 0)";
11902 m_pDS->exec(strSQL);
11903
11904 // Rating and votes
11905 strSQL = "UPDATE song SET rating = "
11906 "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), "
11907 "votes = "
11908 "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) "
11909 "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
11910 "HistSong.idSong = song.idSong AND HistSong.rating > 0)";
11911 m_pDS->exec(strSQL);
11912
11913 if (progressDialog)
11914 {
11915 progressDialog->Progress();
11916 if (progressDialog->IsCanceled())
11917 {
11918 RollbackTransaction();
11919 m_pDS->exec("DROP TABLE HistSong");
11920 return false;
11921 }
11922 }
11923 CommitTransaction();
11924
11925 // Tidy up temp table (index also removed)
11926 m_pDS->exec("DROP TABLE HistSong");
11927 // Compact db to recover space as had to add/drop actual table
11928 if (progressDialog)
11929 {
11930 progressDialog->SetLine(2, CVariant{ 331 });
11931 progressDialog->Progress();
11932 }
11933 Compress(false);
11934
11935 // Write event log entry
11936 // "Importing song history {1} of {2} songs matched", total - unmatched, total)
11937 std::string strLine = StringUtils::Format(g_localizeStrings.Get(38353).c_str(), total - unmatched, total);
11938 CServiceBroker::GetEventLog().Add(
11939 EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information)));
11940
11941 return true;
11942 }
11943 catch (...)
11944 {
11945 CLog::Log(LOGERROR, "%s failed", __FUNCTION__);
11946 RollbackTransaction();
11947 if (bHistSongExists)
11948 m_pDS->exec("DROP TABLE HistSong");
11949 }
11950 return false;
11951 }
11952
SetPropertiesFromArtist(CFileItem & item,const CArtist & artist)11953 void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist)
11954 {
11955 const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
11956
11957 item.SetProperty("artist_sortname", artist.strSortName);
11958 item.SetProperty("artist_type", artist.strType);
11959 item.SetProperty("artist_gender", artist.strGender);
11960 item.SetProperty("artist_disambiguation", artist.strDisambiguation);
11961 item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator));
11962 item.SetProperty("artist_instrument_array", artist.instruments);
11963 item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator));
11964 item.SetProperty("artist_style_array", artist.styles);
11965 item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator));
11966 item.SetProperty("artist_mood_array", artist.moods);
11967 item.SetProperty("artist_born", artist.strBorn);
11968 item.SetProperty("artist_formed", artist.strFormed);
11969 item.SetProperty("artist_description", artist.strBiography);
11970 item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator));
11971 item.SetProperty("artist_genre_array", artist.genre);
11972 item.SetProperty("artist_died", artist.strDied);
11973 item.SetProperty("artist_disbanded", artist.strDisbanded);
11974 item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator));
11975 item.SetProperty("artist_yearsactive_array", artist.yearsActive);
11976 }
11977
SetPropertiesFromAlbum(CFileItem & item,const CAlbum & album)11978 void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album)
11979 {
11980 const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
11981
11982 item.SetProperty("album_description", album.strReview);
11983 item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator));
11984 item.SetProperty("album_theme_array", album.themes);
11985 item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator));
11986 item.SetProperty("album_mood_array", album.moods);
11987 item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator));
11988 item.SetProperty("album_style_array", album.styles);
11989 item.SetProperty("album_type", album.strType);
11990 item.SetProperty("album_label", album.strLabel);
11991 item.SetProperty("album_artist", album.GetAlbumArtistString());
11992 item.SetProperty("album_artist_array", album.GetAlbumArtist());
11993 item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator));
11994 item.SetProperty("album_genre_array", album.genre);
11995 item.SetProperty("album_title", album.strAlbum);
11996 if (album.fRating > 0)
11997 item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating));
11998 if (album.iUserrating > 0)
11999 item.SetProperty("album_userrating", album.iUserrating);
12000 if (album.iVotes > 0)
12001 item.SetProperty("album_votes", album.iVotes);
12002
12003 item.SetProperty("album_isboxset", album.bBoxedSet);
12004 item.SetProperty("album_totaldiscs", album.iTotalDiscs);
12005 item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType));
12006 item.SetProperty("album_duration",
12007 StringUtils::SecondsToTimeString(
12008 album.iAlbumDuration, static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS)));
12009 }
12010
SetPropertiesForFileItem(CFileItem & item)12011 void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item)
12012 {
12013 if (!item.HasMusicInfoTag())
12014 return;
12015 // May already have song artist ids as item property set when data read from
12016 // db, but check property is valid array (scripts could set item properties
12017 // incorrectly), otherwise try to fetch artist by name.
12018 int idArtist = -1;
12019 if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray())
12020 {
12021 CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array();
12022 idArtist = static_cast<int>(varid->asInteger());
12023 }
12024 else
12025 idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString());
12026 if (idArtist > -1)
12027 {
12028 CArtist artist;
12029 if (GetArtist(idArtist, artist))
12030 SetPropertiesFromArtist(item,artist);
12031 }
12032 int idAlbum = item.GetMusicInfoTag()->GetAlbumId();
12033 if (idAlbum <= 0)
12034 idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(),
12035 item.GetMusicInfoTag()->GetArtistString());
12036 if (idAlbum > -1)
12037 {
12038 CAlbum album;
12039 if (GetAlbum(idAlbum, album, false))
12040 SetPropertiesFromAlbum(item,album);
12041 }
12042 }
12043
SetItemUpdated(int mediaId,const std::string & mediaType)12044 void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType)
12045 {
12046 std::string strSQL;
12047 try
12048 {
12049 if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong)
12050 return;
12051 if (nullptr == m_pDB)
12052 return;
12053 if (nullptr == m_pDS)
12054 return;
12055
12056 // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
12057 // e.g. when artwork for item is changed from info dialog but not item details.
12058 // Use SQL UPDATE that does not change record data.
12059 if (mediaType == MediaTypeArtist)
12060 strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId);
12061 else if (mediaType == MediaTypeAlbum)
12062 strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId);
12063 else // MediaTypeSong
12064 strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId);
12065 m_pDS->exec(strSQL);
12066 }
12067 catch (...)
12068 {
12069 CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__,
12070 mediaId, mediaType.c_str(), strSQL.c_str());
12071 }
12072 }
12073
SetArtForItem(int mediaId,const std::string & mediaType,const std::map<std::string,std::string> & art)12074 void CMusicDatabase::SetArtForItem(int mediaId, const std::string &mediaType, const std::map<std::string, std::string> &art)
12075 {
12076 for (const auto &i : art)
12077 SetArtForItem(mediaId, mediaType, i.first, i.second);
12078 }
12079
SetArtForItem(int mediaId,const std::string & mediaType,const std::string & artType,const std::string & url)12080 void CMusicDatabase::SetArtForItem(int mediaId, const std::string &mediaType, const std::string &artType, const std::string &url)
12081 {
12082 try
12083 {
12084 if (nullptr == m_pDB)
12085 return;
12086 if (nullptr == m_pDS)
12087 return;
12088
12089 // don't set <foo>.<bar> art types - these are derivative types from parent items
12090 if (artType.find('.') != std::string::npos)
12091 return;
12092
12093 std::string sql = PrepareSQL("SELECT art_id FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str());
12094 m_pDS->query(sql);
12095 if (!m_pDS->eof())
12096 { // update
12097 int artId = m_pDS->fv(0).get_asInt();
12098 m_pDS->close();
12099 sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
12100 m_pDS->exec(sql);
12101 }
12102 else
12103 { // insert
12104 m_pDS->close();
12105 sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) VALUES (%d, '%s', '%s', '%s')", mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
12106 m_pDS->exec(sql);
12107 }
12108 }
12109 catch (...)
12110 {
12111 CLog::Log(LOGERROR, "%s(%d, '%s', '%s', '%s') failed", __FUNCTION__, mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
12112 }
12113 }
12114
GetArtForItem(int songId,int albumId,int artistId,bool bPrimaryArtist,std::vector<ArtForThumbLoader> & art)12115 bool CMusicDatabase::GetArtForItem(int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader> &art)
12116 {
12117 std::string strSQL;
12118 try
12119 {
12120 if (!(songId > 0 || albumId > 0 || artistId > 0)) return false;
12121 if (nullptr == m_pDB)
12122 return false;
12123 if (nullptr == m_pDS2)
12124 return false; // using dataset 2 as we're likely called in loops on dataset 1
12125
12126 Filter filter;
12127 if (songId > 0)
12128 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong));
12129 if (albumId > 0)
12130 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum), false);
12131 if (artistId > 0)
12132 filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false);
12133
12134 strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder FROM art";
12135 if (!BuildSQL(strSQL, filter, strSQL))
12136 return false;
12137
12138 if (!(artistId > 0))
12139 {
12140 // Artist ID unknown, so lookup album artist for albums and songs
12141 std::string strSQL2;
12142 if (albumId > 0)
12143 {
12144 //Album ID known, so use it to look up album artist(s)
12145 strSQL2 = PrepareSQL(
12146 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12147 "url, album_artist.iOrder as iorder FROM art "
12148 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12149 "WHERE album_artist.idAlbum = %i ",
12150 MediaTypeArtist, albumId);
12151 if (bPrimaryArtist)
12152 strSQL2 += "AND album_artist.iOrder = 0";
12153
12154 strSQL = strSQL + " UNION " + strSQL2;
12155 }
12156 if (songId > 0)
12157 {
12158 if (albumId < 0)
12159 {
12160 //Album ID unknown, so get from song to look up album artist(s)
12161 strSQL2 = PrepareSQL(
12162 "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
12163 "url, album_artist.iOrder as iorder FROM art "
12164 "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
12165 "JOIN song ON song.idAlbum = album_artist.idAlbum "
12166 "WHERE song.idSong = %i ",
12167 MediaTypeArtist, songId);
12168 if (bPrimaryArtist)
12169 strSQL2 += "AND album_artist.iOrder = 0";
12170
12171 strSQL = strSQL + " UNION " + strSQL2;
12172 }
12173
12174 // Artist ID unknown, so lookup artist for songs (could be different from album artist)
12175 strSQL2 = PrepareSQL(
12176 "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
12177 "url, song_artist.iOrder as iorder FROM art "
12178 "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
12179 "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
12180 MediaTypeArtist, songId, ROLE_ARTIST);
12181 if (bPrimaryArtist)
12182 strSQL2 += "AND song_artist.iOrder = 0";
12183
12184 strSQL = strSQL + " UNION " + strSQL2;
12185 }
12186 }
12187 if (songId > 0 && albumId < 0)
12188 {
12189 //Album ID unknown, so get from song to look up album art
12190 std::string strSQL2;
12191 strSQL2 = PrepareSQL(
12192 "SELECT art_id, media_id, media_type, type, '' as prefix, "
12193 "url, 0 as iorder FROM art "
12194 "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
12195 "WHERE song.idSong = %i ",
12196 MediaTypeAlbum, songId);
12197 strSQL = strSQL + " UNION " + strSQL2;
12198 }
12199
12200 m_pDS2->query(strSQL);
12201 while (!m_pDS2->eof())
12202 {
12203 ArtForThumbLoader artitem;
12204 artitem.artType = m_pDS2->fv("type").get_asString();
12205 artitem.mediaType = m_pDS2->fv("media_type").get_asString();
12206 artitem.prefix = m_pDS2->fv("prefix").get_asString();
12207 artitem.url = m_pDS2->fv("url").get_asString();
12208 int iOrder = m_pDS2->fv("iorder").get_asInt();
12209 // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
12210 if (iOrder > 0)
12211 artitem.prefix += m_pDS2->fv("iorder").get_asString();
12212
12213 art.emplace_back(artitem);
12214 m_pDS2->next();
12215 }
12216 m_pDS2->close();
12217 return !art.empty();
12218 }
12219 catch (...)
12220 {
12221 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, strSQL.c_str());
12222 }
12223 return false;
12224 }
12225
GetArtForItem(int mediaId,const std::string & mediaType,std::map<std::string,std::string> & art)12226 bool CMusicDatabase::GetArtForItem(int mediaId, const std::string &mediaType, std::map<std::string, std::string> &art)
12227 {
12228 try
12229 {
12230 if (nullptr == m_pDB)
12231 return false;
12232 if (nullptr == m_pDS2)
12233 return false; // using dataset 2 as we're likely called in loops on dataset 1
12234
12235 std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'", mediaId, mediaType.c_str());
12236 m_pDS2->query(sql);
12237 while (!m_pDS2->eof())
12238 {
12239 art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
12240 m_pDS2->next();
12241 }
12242 m_pDS2->close();
12243 return !art.empty();
12244 }
12245 catch (...)
12246 {
12247 CLog::Log(LOGERROR, "%s(%d) failed", __FUNCTION__, mediaId);
12248 }
12249 return false;
12250 }
12251
GetArtForItem(int mediaId,const std::string & mediaType,const std::string & artType)12252 std::string CMusicDatabase::GetArtForItem(int mediaId, const std::string &mediaType, const std::string &artType)
12253 {
12254 std::string query = PrepareSQL("SELECT url FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str());
12255 return GetSingleValue(query, m_pDS2);
12256 }
12257
RemoveArtForItem(int mediaId,const MediaType & mediaType,const std::string & artType)12258 bool CMusicDatabase::RemoveArtForItem(int mediaId, const MediaType & mediaType, const std::string & artType)
12259 {
12260 return ExecuteQuery(PrepareSQL("DELETE FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str()));
12261 }
12262
RemoveArtForItem(int mediaId,const MediaType & mediaType,const std::set<std::string> & artTypes)12263 bool CMusicDatabase::RemoveArtForItem(int mediaId, const MediaType & mediaType, const std::set<std::string>& artTypes)
12264 {
12265 bool result = true;
12266 for (const auto &i : artTypes)
12267 result &= RemoveArtForItem(mediaId, mediaType, i);
12268
12269 return result;
12270 }
12271
GetArtTypes(const MediaType & mediaType,std::vector<std::string> & artTypes)12272 bool CMusicDatabase::GetArtTypes(const MediaType &mediaType, std::vector<std::string> &artTypes)
12273 {
12274 try
12275 {
12276 if (nullptr == m_pDB)
12277 return false;
12278 if (nullptr == m_pDS)
12279 return false;
12280
12281 std::string strSQL = PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
12282
12283 if (!m_pDS->query(strSQL)) return false;
12284 int iRowsFound = m_pDS->num_rows();
12285 if (iRowsFound == 0)
12286 {
12287 m_pDS->close();
12288 return false;
12289 }
12290
12291 while (!m_pDS->eof())
12292 {
12293 artTypes.emplace_back(m_pDS->fv(0).get_asString());
12294 m_pDS->next();
12295 }
12296 m_pDS->close();
12297 return true;
12298 }
12299 catch (...)
12300 {
12301 CLog::Log(LOGERROR, "%s(%s) failed", __FUNCTION__, mediaType.c_str());
12302 }
12303 return false;
12304 }
12305
GetAvailableArtTypesForItem(int mediaId,const MediaType & mediaType)12306 std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId,
12307 const MediaType& mediaType)
12308 {
12309 CScraperUrl thumbURL;
12310 if (mediaType == MediaTypeArtist)
12311 {
12312 CArtist artist;
12313 if (GetArtist(mediaId, artist))
12314 thumbURL = artist.thumbURL;
12315 }
12316 else if (mediaType == MediaTypeAlbum)
12317 {
12318 CAlbum album;
12319 if (GetAlbum(mediaId, album))
12320 thumbURL = album.thumbURL;
12321 }
12322
12323 std::vector<std::string> result;
12324 for (const auto& urlEntry : thumbURL.GetUrls())
12325 {
12326 std::string artType = urlEntry.m_aspect;
12327 if (artType.empty())
12328 artType = "thumb";
12329 if (std::find(result.begin(), result.end(), artType) == result.end())
12330 result.push_back(artType);
12331 }
12332 return result;
12333 }
12334
GetAvailableArtForItem(int mediaId,const MediaType & mediaType,const std::string & artType)12335 std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem(
12336 int mediaId, const MediaType& mediaType, const std::string& artType)
12337 {
12338 CScraperUrl thumbURL;
12339 if (mediaType == MediaTypeArtist)
12340 {
12341 CArtist artist;
12342 if (GetArtist(mediaId, artist))
12343 thumbURL = artist.thumbURL;
12344 }
12345 else if (mediaType == MediaTypeAlbum)
12346 {
12347 CAlbum album;
12348 if (GetAlbum(mediaId, album))
12349 thumbURL = album.thumbURL;
12350 }
12351
12352 std::vector<CScraperUrl::SUrlEntry> result;
12353 for (auto urlEntry : thumbURL.GetUrls())
12354 {
12355 if (urlEntry.m_aspect.empty())
12356 urlEntry.m_aspect = "thumb";
12357 if (artType.empty() || urlEntry.m_aspect == artType)
12358 result.push_back(urlEntry);
12359 }
12360 return result;
12361 }
12362
GetOrderFilter(const std::string & type,const SortDescription & sorting,Filter & filter)12363 int CMusicDatabase::GetOrderFilter(const std::string& type,
12364 const SortDescription& sorting,
12365 Filter& filter)
12366 {
12367 // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
12368 int iFieldsAdded = 0;
12369 filter.fields.clear(); // remove "*"
12370 std::vector<std::string> orderfields;
12371 std::string DESC;
12372
12373 if (sorting.sortOrder == SortOrderDescending)
12374 DESC = " DESC";
12375
12376 if (sorting.sortBy == SortByRandom)
12377 orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
12378 else
12379 {
12380 FieldList fields;
12381 SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields);
12382 for (const auto& it : fields)
12383 {
12384 std::string strField;
12385 if (it == FieldYear)
12386 strField = "iYear";
12387 else
12388 strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect);
12389 if (!strField.empty())
12390 orderfields.emplace_back(strField);
12391 }
12392 }
12393
12394 // Convert field names into order by statement elements
12395 for (auto& name : orderfields)
12396 {
12397 //Add field for adjusted name sorting using sort name and ignoring articles
12398 std::string sortSQL;
12399 if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist"))
12400 {
12401 if (StringUtils::EndsWith(name, "strArtists"))
12402 sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort");
12403 else
12404 sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName");
12405 if (!sortSQL.empty())
12406 {
12407 name = "artistsortname";
12408 filter.AppendField(sortSQL); // Add artistsortname as scalar query field
12409 iFieldsAdded++;
12410 }
12411 // Natural number case insensitve sort
12412 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
12413 }
12414 else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle"))
12415 {
12416 sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, "");
12417 if (!sortSQL.empty())
12418 {
12419 name = "titlesortname";
12420 filter.AppendField(sortSQL); // Add sortname as scalar query field
12421 iFieldsAdded++;
12422 }
12423 // Natural number case insensitve sort
12424 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
12425 }
12426 else if (StringUtils::EndsWith(name, "strGenres"))
12427 // Natural number case insensitve sort
12428 filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
12429 else
12430 filter.AppendOrder(name + DESC);
12431 }
12432 return iFieldsAdded;
12433 }
12434
GetFilter(CDbUrl & musicUrl,Filter & filter,SortDescription & sorting)12435 bool CMusicDatabase::GetFilter(CDbUrl &musicUrl, Filter &filter, SortDescription &sorting)
12436 {
12437 if (!musicUrl.IsValid())
12438 return false;
12439
12440 std::string type = musicUrl.GetType();
12441 const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
12442
12443 // Check for playlist rules first, they may contain role criteria
12444 bool hasRoleRules = false;
12445
12446 auto option = options.find("xsp");
12447 if (option != options.end())
12448 {
12449 CSmartPlaylist xsp;
12450 if (!xsp.LoadFromJson(option->second.asString()))
12451 return false;
12452
12453 std::set<std::string> playlists;
12454 std::string xspWhere;
12455 xspWhere = xsp.GetWhereClause(*this, playlists);
12456 hasRoleRules = xsp.GetType() == "artists" && xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos;
12457
12458 // Check if the filter playlist matches the item type
12459 // Allow for grouping name like "originalyears" and type "years"
12460 if (xsp.GetType() == type ||
12461 (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed()))
12462 {
12463 filter.AppendWhere(xspWhere);
12464
12465 if (xsp.GetLimit() > 0)
12466 sorting.limitEnd = xsp.GetLimit();
12467 if (xsp.GetOrder() != SortByNone)
12468 sorting.sortBy = xsp.GetOrder();
12469 sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
12470 if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
12471 sorting.sortAttributes = SortAttributeIgnoreArticle;
12472 }
12473 }
12474
12475 //Process role options, common to artist and album type filtering
12476 int idRole = 1; // Default restrict song_artist to "artists" only, no other roles.
12477 option = options.find("roleid");
12478 if (option != options.end())
12479 idRole = static_cast<int>(option->second.asInteger());
12480 else
12481 {
12482 option = options.find("role");
12483 if (option != options.end())
12484 {
12485 if (option->second.asString() == "all" || option->second.asString() == "%")
12486 idRole = -1000; //All roles
12487 else
12488 idRole = GetRoleByName(option->second.asString());
12489 }
12490 }
12491 if (hasRoleRules)
12492 {
12493 // Get Role from role rule(s) here.
12494 // But that requires much change, so for now get all roles as better than none
12495 idRole = -1000; //All roles
12496 }
12497
12498 std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role
12499 if(idRole > 0) strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole);
12500
12501 int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1;
12502 int idDisc = -1;
12503 int idSource = -1;
12504 bool albumArtistsOnly = false;
12505 bool useOriginalYear = false;
12506 std::string artistname;
12507
12508 // Process useoriginalyear option, setting overridden by option
12509 useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
12510 CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
12511 option = options.find("useoriginalyear");
12512 if (option != options.end())
12513 useOriginalYear = option->second.asBoolean();
12514
12515 // Process albumartistsonly option
12516 option = options.find("albumartistsonly");
12517 if (option != options.end())
12518 albumArtistsOnly = option->second.asBoolean();
12519
12520 // Process genre option
12521 option = options.find("genreid");
12522 if (option != options.end())
12523 idGenre = static_cast<int>(option->second.asInteger());
12524 else
12525 {
12526 option = options.find("genre");
12527 if (option != options.end())
12528 idGenre = GetGenreByName(option->second.asString());
12529 }
12530
12531 // Process source option
12532 option = options.find("sourceid");
12533 if (option != options.end())
12534 idSource = static_cast<int>(option->second.asInteger());
12535 else
12536 {
12537 option = options.find("source");
12538 if (option != options.end())
12539 idSource = GetSourceByName(option->second.asString());
12540 }
12541
12542 // Process album option
12543 option = options.find("albumid");
12544 if (option != options.end())
12545 idAlbum = static_cast<int>(option->second.asInteger());
12546 else
12547 {
12548 option = options.find("album");
12549 if (option != options.end())
12550 idAlbum = GetAlbumByName(option->second.asString());
12551 }
12552
12553 // Process artist option
12554 option = options.find("artistid");
12555 if (option != options.end())
12556 idArtist = static_cast<int>(option->second.asInteger());
12557 else
12558 {
12559 option = options.find("artist");
12560 if (option != options.end())
12561 {
12562 idArtist = GetArtistByName(option->second.asString());
12563 if (idArtist == -1)
12564 {// not found with that name, or more than one found as artist name is not unique
12565 artistname = option->second.asString();
12566 }
12567 }
12568 }
12569
12570 // Process song option
12571 option = options.find("songid");
12572 if (option != options.end())
12573 idSong = static_cast<int>(option->second.asInteger());
12574
12575 if (type == "artists")
12576 {
12577 if (!hasRoleRules)
12578 { // Not an "artists" smart playlist with roles rules, so get filter from options
12579 if (idArtist > 0)
12580 filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist));
12581 else if (idAlbum > 0)
12582 filter.AppendWhere(PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
12583 "WHERE album_artist.idAlbum = %i)", idAlbum));
12584 else if (idSong > 0)
12585 {
12586 filter.AppendWhere(PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
12587 "WHERE song_artist.idSong = %i %s)", idSong, strRoleSQL.c_str()));
12588 }
12589 else
12590 { /*
12591 Process idRole, idGenre, idSource and albumArtistsOnly options
12592
12593 For artists these rules are combined because they apply via album and song
12594 and so we need to ensure all criteria are met via the same album or song.
12595 1) Some artists may be only album artists, so for all artists (with linked
12596 albums or songs) we need to check both album_artist and song_artist tables.
12597 2) Role is determined from song_artist table, so even if looking for album artists
12598 only we find those that also have a specific role e.g. which album artist is a
12599 composer of songs in that album, from entries in the song_artist table.
12600 a) Role < -1 is used to indicate that all roles are wanted.
12601 b) When not album artists only and a specific role wanted then only the song_artist
12602 table is checked.
12603 c) When album artists only and role = 1 (an "artist") then only the album_artist
12604 table is checked.
12605 */
12606 std::string albumArtistSQL, songArtistSQL;
12607 ExistsSubQuery albumArtistSub("album_artist", "album_artist.idArtist = artistview.idArtist");
12608 // Prepare album artist subquery SQL
12609 if (idSource > 0)
12610 {
12611 if (idRole == 1 && idGenre < 0)
12612 {
12613 albumArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
12614 albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
12615 }
12616 else
12617 {
12618 albumArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12619 "WHERE album_source.idSource = %i "
12620 "AND album_source.idAlbum = album_artist.idAlbum)", idSource));
12621 }
12622 }
12623 if (idRole <= 1 && idGenre > 0)
12624 { // Check genre of songs of album using nested subquery
12625 std::string strGenre = PrepareSQL("EXISTS(SELECT 1 FROM song "
12626 "JOIN song_genre ON song_genre.idSong = song.idSong "
12627 "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)", idGenre);
12628 albumArtistSub.AppendWhere(strGenre);
12629 }
12630
12631 // Prepare song artist subquery SQL
12632 ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
12633 if (idRole > 0)
12634 songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
12635 if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1)
12636 {
12637 songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
12638 "JOIN song_genre ON song_genre.idSong = song.idSong "
12639 "WHERE song.idSong = song_artist.idSong "
12640 "AND song_genre.idGenre = %i "
12641 "AND EXISTS(SELECT 1 FROM album_source "
12642 "WHERE album_source.idSource = %i "
12643 "AND album_source.idAlbum = song.idAlbum))", idGenre, idSource));
12644 }
12645 else
12646 {
12647 if (idGenre > 0)
12648 {
12649 songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
12650 songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
12651 }
12652 if (idSource > 0 && !albumArtistsOnly)
12653 {
12654 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
12655 songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
12656 songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
12657 }
12658 if (idRole > 1 && albumArtistsOnly)
12659 { // Album artists only with role, check AND in album_artist for album of song
12660 // using nested subquery correlated with album_artist
12661 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
12662 songArtistSub.param = "song_artist.idArtist = album_artist.idArtist";
12663 songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum");
12664 }
12665 }
12666
12667 // Build filter clause from subqueries
12668 if (idRole > 1 && albumArtistsOnly)
12669 { // Album artists only with role, check AND in album_artist for album of song
12670 // using nested subquery correlated with album_artist
12671 songArtistSub.BuildSQL(songArtistSQL);
12672 albumArtistSub.AppendWhere(songArtistSQL);
12673 albumArtistSub.BuildSQL(albumArtistSQL);
12674 filter.AppendWhere(albumArtistSQL);
12675 }
12676 else
12677 {
12678 songArtistSub.BuildSQL(songArtistSQL);
12679 albumArtistSub.BuildSQL(albumArtistSQL);
12680 if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
12681 { // Artist contributing to songs, any role, check OR album artist too
12682 // as artists can be just album artists but not song artists
12683 filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
12684 }
12685 else if (idRole > 1)
12686 {
12687 // Artist contributes that role (not albmartistsonly as already handled)
12688 filter.AppendWhere(songArtistSQL);
12689 }
12690 else // idRole = 1 and albumArtistsOnly
12691 { // Only look at album artists, not albums where artist features on songs
12692 filter.AppendWhere(albumArtistSQL);
12693 }
12694 }
12695 }
12696 }
12697 // remove the null string
12698 filter.AppendWhere("artistview.strArtist != ''");
12699 }
12700 else if (type == "albums")
12701 {
12702 option = options.find("year");
12703 if (option != options.end())
12704 {
12705 if (!useOriginalYear)
12706 filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
12707 option->second.asString().c_str()));
12708 else
12709 filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
12710 option->second.asString().c_str()));
12711 }
12712 option = options.find("compilation");
12713 if (option != options.end())
12714 filter.AppendWhere(PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
12715
12716 option = options.find("boxset");
12717 if (option != options.end())
12718 filter.AppendWhere(
12719 PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
12720
12721 if (idSource > 0)
12722 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12723 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)", idSource));
12724
12725 // Process artist, role and genre options together as song subquery to filter those
12726 // albums that have songs with both that artist and genre
12727 std::string albumArtistSQL, songArtistSQL, genreSQL;
12728 ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum");
12729 genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
12730 genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
12731 ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
12732 ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
12733 songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
12734
12735 if (idArtist > 0)
12736 {
12737 songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist));
12738 albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist));
12739 }
12740 else if (!artistname.empty())
12741 { // Artist name is not unique, so could get albums or songs from more than one.
12742 songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
12743 songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
12744
12745 albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
12746 albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
12747 }
12748 if (idRole > 0)
12749 songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
12750 if (idGenre > 0)
12751 {
12752 songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
12753 songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
12754 }
12755
12756 if (idArtist > 0 || !artistname.empty())
12757 {
12758 if (idRole <= 1 && idGenre > 0)
12759 { // Check genre of songs of album using nested subquery
12760 genreSub.BuildSQL(genreSQL);
12761 albumArtistSub.AppendWhere(genreSQL);
12762 }
12763 if (idRole > 1 && albumArtistsOnly)
12764 { // Album artists only with role, check AND in album_artist for same song
12765 // using nested subquery correlated with album_artist
12766 songArtistSub.param = "song.idAlbum = album_artist.idAlbum";
12767 songArtistSub.BuildSQL(songArtistSQL);
12768 albumArtistSub.AppendWhere(songArtistSQL);
12769 albumArtistSub.BuildSQL(albumArtistSQL);
12770 filter.AppendWhere(albumArtistSQL);
12771 }
12772 else
12773 {
12774 songArtistSub.BuildSQL(songArtistSQL);
12775 albumArtistSub.BuildSQL(albumArtistSQL);
12776 if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
12777 { // Artist contributing to songs, any role, check OR album artist too
12778 // as artists can be just album artists but not song artists
12779 filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
12780 }
12781 else if (idRole > 1)
12782 { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
12783 filter.AppendWhere(songArtistSQL);
12784 }
12785 else // idRole = 1 and albumArtistsOnly
12786 { // Only look at album artists, not albums where artist features on songs
12787 // This may want to be a separate option so you can choose to see all the albums where that artist
12788 // appears on one or more songs without having to list all song artists in the artists node.
12789 filter.AppendWhere(albumArtistSQL);
12790 }
12791 }
12792 }
12793 else
12794 { // No artist given
12795 if (idGenre > 0)
12796 { // Have genre option but not artist
12797 genreSub.param = "song.idAlbum = albumview.idAlbum";
12798 genreSub.BuildSQL(genreSQL);
12799 filter.AppendWhere(genreSQL);
12800 }
12801 // Exclude any single albums (aka empty tagged albums)
12802 // This causes "albums" media filter artist selection to only offer album artists
12803 option = options.find("show_singles");
12804 if (option == options.end() || !option->second.asBoolean())
12805 filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'", CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
12806 }
12807 }
12808 else if (type == "discs")
12809 {
12810 if (idAlbum > 0)
12811 filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum));
12812 else
12813 {
12814 option = options.find("year");
12815 if (option != options.end())
12816 {
12817 if (!useOriginalYear)
12818 filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
12819 option->second.asString().c_str()));
12820 else
12821 filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
12822 option->second.asString().c_str()));
12823 }
12824
12825 option = options.find("compilation");
12826 if (option != options.end())
12827 filter.AppendWhere(
12828 PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
12829
12830 option = options.find("boxset");
12831 if (option != options.end())
12832 filter.AppendWhere(
12833 PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
12834
12835 if (idSource > 0)
12836 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12837 "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)", idSource));
12838 }
12839 option = options.find("discid");
12840 if (option != options.end())
12841 filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger()));
12842
12843 option = options.find("disctitle");
12844 if (option != options.end())
12845 filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str()));
12846
12847 if (idGenre > 0)
12848 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
12849 "song.idSong AND song_genre.idGenre = %i)",
12850 idGenre));
12851
12852 std::string songArtistClause, albumArtistClause;
12853 if (idArtist > 0)
12854 {
12855 songArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
12856 "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
12857 idArtist, strRoleSQL.c_str());
12858 albumArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
12859 "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
12860 idArtist);
12861 }
12862 else if (!artistname.empty())
12863 { // Artist name is not unique, so could get songs from more than one.
12864 songArtistClause = PrepareSQL(
12865 "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
12866 "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
12867 artistname.c_str(), strRoleSQL.c_str());
12868 albumArtistClause =
12869 PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
12870 "album_artist.idArtist "
12871 "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
12872 artistname.c_str());
12873 }
12874
12875 // Process artist name or id option
12876 if (!songArtistClause.empty())
12877 {
12878 if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
12879 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12880 else if (idRole > 1)
12881 {
12882 if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
12883 filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
12884 else // songs where artist contributes that role.
12885 filter.AppendWhere(songArtistClause);
12886 }
12887 else
12888 {
12889 if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
12890 filter.AppendWhere(albumArtistClause);
12891 else // Artist is song artist or album artist
12892 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12893 }
12894 }
12895 }
12896 else if (type == "songs" || type == "singles")
12897 {
12898 option = options.find("singles");
12899 if (option != options.end())
12900 filter.AppendWhere(PrepareSQL("songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
12901 option->second.asBoolean() ? "" : "NOT ",
12902 CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
12903
12904 // When have idAlbum skip year, compilation, boxset criteria as already applied via album
12905 if (idAlbum < 0)
12906 {
12907 option = options.find("year");
12908 if (option != options.end())
12909 {
12910 if (!useOriginalYear)
12911 filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
12912 option->second.asString().c_str()));
12913 else
12914 filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
12915 option->second.asString().c_str()));
12916 }
12917 option = options.find("compilation");
12918 if (option != options.end())
12919 filter.AppendWhere(
12920 PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
12921
12922 option = options.find("boxset");
12923 if (option != options.end())
12924 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
12925 "songview.idAlbum AND bBoxedSet = %i)",
12926 option->second.asBoolean() ? 1 : 0));
12927 }
12928
12929 option = options.find("discid");
12930 if (option != options.end())
12931 idDisc = static_cast<int>(option->second.asInteger());
12932
12933 option = options.find("disctitle");
12934 if (option != options.end())
12935 filter.AppendWhere(
12936 PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str()));
12937
12938 if (idSong > 0)
12939 filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong));
12940
12941 if (idAlbum > 0)
12942 filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum));
12943
12944 if (idDisc > 0)
12945 filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc));
12946
12947 if (idGenre > 0)
12948 filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre WHERE song_genre.idGenre = %i)", idGenre));
12949
12950 if (idSource > 0)
12951 filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album_source "
12952 "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)", idSource));
12953
12954 std::string songArtistClause, albumArtistClause;
12955 if (idArtist > 0)
12956 {
12957 songArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
12958 "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
12959 idArtist, strRoleSQL.c_str());
12960 albumArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
12961 "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
12962 idArtist);
12963 }
12964 else if (!artistname.empty())
12965 { // Artist name is not unique, so could get songs from more than one.
12966 songArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
12967 "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
12968 artistname.c_str(), strRoleSQL.c_str());
12969 albumArtistClause = PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = album_artist.idArtist "
12970 "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
12971 artistname.c_str());
12972 }
12973
12974 // Process artist name or id option
12975 if (!songArtistClause.empty())
12976 {
12977 if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
12978 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12979 else if (idRole > 1)
12980 {
12981 if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
12982 filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
12983 else // songs where artist contributes that role.
12984 filter.AppendWhere(songArtistClause);
12985 }
12986 else
12987 {
12988 if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
12989 filter.AppendWhere(albumArtistClause);
12990 else // Artist is song artist or album artist
12991 filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
12992 }
12993 }
12994 }
12995
12996 option = options.find("filter");
12997 if (option != options.end())
12998 {
12999 CSmartPlaylist xspFilter;
13000 if (!xspFilter.LoadFromJson(option->second.asString()))
13001 return false;
13002
13003 // check if the filter playlist matches the item type
13004 if (xspFilter.GetType() == type)
13005 {
13006 std::set<std::string> playlists;
13007 filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
13008 }
13009 // remove the filter if it doesn't match the item type
13010 else
13011 musicUrl.RemoveOption("filter");
13012 }
13013
13014 return true;
13015 }
13016
GetMediaDateFromFile(const std::string & strFileNameAndPath)13017 std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath)
13018 {
13019 if (strFileNameAndPath.empty())
13020 return std::string();
13021
13022 CDateTime dateMedia;
13023 int code;
13024 code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded;
13025 // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
13026 if (code == 1)
13027 dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath);
13028 //2 using the newer datetime of the file's mtime and ctime
13029 else if (code == 2)
13030 dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath);
13031 //3 using the older datetime of the file's mtime and ctime
13032 else if (code == 3)
13033 dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath);
13034 //0 using the current datetime if none of the above matches or one returns an invalid datetime
13035 if (!dateMedia.IsValid())
13036 dateMedia = CDateTime::GetCurrentDateTime();
13037
13038 return dateMedia.GetAsDBDateTime();
13039 }
13040
AddAudioBook(const CFileItem & item)13041 bool CMusicDatabase::AddAudioBook(const CFileItem& item)
13042 {
13043 auto const& artists = item.GetMusicInfoTag()->GetArtist();
13044 std::string strSQL = PrepareSQL(
13045 "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
13046 "VALUES (NULL,'%s','%s',%i,'%s','%s')",
13047 item.GetMusicInfoTag()->GetAlbum().c_str(),
13048 artists.empty() ? "" : artists[0].c_str(),
13049 0,
13050 item.GetDynPath().c_str(),
13051 CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()
13052 );
13053 return ExecuteQuery(strSQL);
13054 }
13055
SetResumeBookmarkForAudioBook(const CFileItem & item,int bookmark)13056 bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark)
13057 {
13058 std::string strSQL = PrepareSQL("select bookmark from audiobook where file='%s'",
13059 item.GetDynPath().c_str());
13060 if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13061 {
13062 if (!AddAudioBook(item))
13063 return false;
13064 }
13065
13066 strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i WHERE file='%s'",
13067 bookmark, item.GetDynPath().c_str());
13068
13069 return ExecuteQuery(strSQL);
13070 }
13071
GetResumeBookmarkForAudioBook(const CFileItem & item,int & bookmark)13072 bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark)
13073 {
13074 std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'",
13075 item.GetDynPath().c_str());
13076 if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
13077 return false;
13078
13079 bookmark = m_pDS->fv(0).get_asInt();
13080 return true;
13081 }
13082