1 /*
2 * Strawberry Music Player
3 * This file was part of Clementine.
4 * Copyright 2010, David Sansome <me@davidsansome.com>
5 * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
6 *
7 * Strawberry is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Strawberry is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22 #include "config.h"
23
24 #include <optional>
25
26 #include <QtGlobal>
27 #include <QObject>
28 #include <QApplication>
29 #include <QThread>
30 #include <QMutex>
31 #include <QSet>
32 #include <QMap>
33 #include <QVector>
34 #include <QVariant>
35 #include <QByteArray>
36 #include <QString>
37 #include <QStringList>
38 #include <QUrl>
39 #include <QFileInfo>
40 #include <QDateTime>
41 #include <QRegularExpression>
42 #include <QSqlDatabase>
43 #include <QSqlQuery>
44 #include <QSqlError>
45
46 #include "core/logging.h"
47 #include "core/database.h"
48 #include "core/scopedtransaction.h"
49 #include "core/song.h"
50 #include "smartplaylists/smartplaylistsearch.h"
51
52 #include "directory.h"
53 #include "sqlrow.h"
54 #include "collectionbackend.h"
55 #include "collectionquery.h"
56 #include "collectiontask.h"
57
CollectionBackend(QObject * parent)58 CollectionBackend::CollectionBackend(QObject *parent)
59 : CollectionBackendInterface(parent),
60 db_(nullptr),
61 task_manager_(nullptr),
62 source_(Song::Source_Unknown),
63 original_thread_(nullptr) {
64
65 original_thread_ = thread();
66
67 }
68
Init(Database * db,TaskManager * task_manager,const Song::Source source,const QString & songs_table,const QString & fts_table,const QString & dirs_table,const QString & subdirs_table)69 void CollectionBackend::Init(Database *db, TaskManager *task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table, const QString &subdirs_table) {
70
71 db_ = db;
72 task_manager_ = task_manager;
73 source_ = source;
74 songs_table_ = songs_table;
75 dirs_table_ = dirs_table;
76 subdirs_table_ = subdirs_table;
77 fts_table_ = fts_table;
78
79 }
80
Close()81 void CollectionBackend::Close() {
82
83 if (db_) {
84 QMutexLocker l(db_->Mutex());
85 db_->Close();
86 }
87
88 }
89
ExitAsync()90 void CollectionBackend::ExitAsync() {
91 QMetaObject::invokeMethod(this, "Exit", Qt::QueuedConnection);
92 }
93
Exit()94 void CollectionBackend::Exit() {
95
96 Q_ASSERT(QThread::currentThread() == thread());
97
98 moveToThread(original_thread_);
99 emit ExitFinished();
100
101 }
102
ReportErrors(const CollectionQuery & query)103 void CollectionBackend::ReportErrors(const CollectionQuery &query) {
104
105 const QSqlError sql_error = query.lastError();
106 if (sql_error.isValid()) {
107 qLog(Error) << "Unable to execute collection SQL query: " << sql_error;
108 qLog(Error) << "Faulty SQL query: " << query.lastQuery();
109 qLog(Error) << "Bound SQL values: " << query.boundValues();
110 QString error;
111 error += "Unable to execute collection SQL query: " + sql_error.text() + "<br />";
112 error += "Faulty SQL query: " + query.lastQuery();
113 emit Error(error);
114 }
115
116 }
117
LoadDirectoriesAsync()118 void CollectionBackend::LoadDirectoriesAsync() {
119 QMetaObject::invokeMethod(this, "LoadDirectories", Qt::QueuedConnection);
120 }
121
UpdateTotalSongCountAsync()122 void CollectionBackend::UpdateTotalSongCountAsync() {
123 QMetaObject::invokeMethod(this, "UpdateTotalSongCount", Qt::QueuedConnection);
124 }
125
UpdateTotalArtistCountAsync()126 void CollectionBackend::UpdateTotalArtistCountAsync() {
127 QMetaObject::invokeMethod(this, "UpdateTotalArtistCount", Qt::QueuedConnection);
128 }
129
UpdateTotalAlbumCountAsync()130 void CollectionBackend::UpdateTotalAlbumCountAsync() {
131 QMetaObject::invokeMethod(this, "UpdateTotalAlbumCount", Qt::QueuedConnection);
132 }
133
IncrementPlayCountAsync(const int id)134 void CollectionBackend::IncrementPlayCountAsync(const int id) {
135 QMetaObject::invokeMethod(this, "IncrementPlayCount", Qt::QueuedConnection, Q_ARG(int, id));
136 }
137
IncrementSkipCountAsync(const int id,const float progress)138 void CollectionBackend::IncrementSkipCountAsync(const int id, const float progress) {
139 QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress));
140 }
141
ResetStatisticsAsync(const int id)142 void CollectionBackend::ResetStatisticsAsync(const int id) {
143 QMetaObject::invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id));
144 }
145
LoadDirectories()146 void CollectionBackend::LoadDirectories() {
147
148 DirectoryList dirs = GetAllDirectories();
149
150 QMutexLocker l(db_->Mutex());
151 QSqlDatabase db(db_->Connect());
152
153 for (const Directory &dir : dirs) {
154 emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db));
155 }
156
157 }
158
ChangeDirPath(const int id,const QString & old_path,const QString & new_path)159 void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, const QString &new_path) {
160
161 QMutexLocker l(db_->Mutex());
162 QSqlDatabase db(db_->Connect());
163 ScopedTransaction t(&db);
164
165 // Do the dirs table
166 {
167 SqlQuery q(db);
168 q.prepare(QString("UPDATE %1 SET path=:path WHERE ROWID=:id").arg(dirs_table_));
169 q.BindValue(":path", new_path);
170 q.BindValue(":id", id);
171 if (!q.Exec()) {
172 db_->ReportErrors(q);
173 return;
174 }
175 }
176
177 const QByteArray old_url = QUrl::fromLocalFile(old_path).toEncoded();
178 const QByteArray new_url = QUrl::fromLocalFile(new_path).toEncoded();
179
180 const int path_len = old_url.length();
181
182 // Do the subdirs table
183 {
184 SqlQuery q(db);
185 q.prepare(QString("UPDATE %1 SET path=:path || substr(path, %2) WHERE directory=:id").arg(subdirs_table_).arg(path_len));
186 q.BindValue(":path", new_url);
187 q.BindValue(":id", id);
188 if (!q.Exec()) {
189 db_->ReportErrors(q);
190 return;
191 }
192 }
193
194 // Do the songs table
195 {
196 SqlQuery q(db);
197 q.prepare(QString("UPDATE %1 SET url=:path || substr(url, %2) WHERE directory=:id").arg(songs_table_).arg(path_len));
198 q.BindValue(":path", new_url);
199 q.BindValue(":id", id);
200 if (!q.Exec()) {
201 db_->ReportErrors(q);
202 return;
203 }
204 }
205
206 t.Commit();
207
208 }
209
GetAllDirectories()210 DirectoryList CollectionBackend::GetAllDirectories() {
211
212 QMutexLocker l(db_->Mutex());
213 QSqlDatabase db(db_->Connect());
214
215 DirectoryList ret;
216
217 SqlQuery q(db);
218 q.prepare(QString("SELECT ROWID, path FROM %1").arg(dirs_table_));
219 if (!q.Exec()) {
220 db_->ReportErrors(q);
221 return ret;
222 }
223
224 while (q.next()) {
225 Directory dir;
226 dir.id = q.value(0).toInt();
227 dir.path = q.value(1).toString();
228
229 ret << dir;
230 }
231 return ret;
232
233 }
234
SubdirsInDirectory(const int id)235 SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) {
236
237 QMutexLocker l(db_->Mutex());
238 QSqlDatabase db = db_->Connect();
239 return SubdirsInDirectory(id, db);
240
241 }
242
SubdirsInDirectory(const int id,QSqlDatabase & db)243 SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) {
244
245 SqlQuery q(db);
246 q.prepare(QString("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_));
247 q.BindValue(":dir", id);
248 if (!q.Exec()) {
249 db_->ReportErrors(q);
250 return SubdirectoryList();
251 }
252
253 SubdirectoryList subdirs;
254 while (q.next()) {
255 Subdirectory subdir;
256 subdir.directory_id = id;
257 subdir.path = q.value(0).toString();
258 subdir.mtime = q.value(1).toLongLong();
259 subdirs << subdir;
260 }
261
262 return subdirs;
263
264 }
265
UpdateTotalSongCount()266 void CollectionBackend::UpdateTotalSongCount() {
267
268 QMutexLocker l(db_->Mutex());
269 QSqlDatabase db(db_->Connect());
270
271 SqlQuery q(db);
272 q.prepare(QString("SELECT COUNT(*) FROM %1 WHERE unavailable = 0").arg(songs_table_));
273 if (!q.Exec()) {
274 db_->ReportErrors(q);
275 return;
276 }
277 if (!q.next()) {
278 db_->ReportErrors(q);
279 return;
280 }
281
282 emit TotalSongCountUpdated(q.value(0).toInt());
283
284 }
285
UpdateTotalArtistCount()286 void CollectionBackend::UpdateTotalArtistCount() {
287
288 QMutexLocker l(db_->Mutex());
289 QSqlDatabase db(db_->Connect());
290
291 SqlQuery q(db);
292 q.prepare(QString("SELECT COUNT(DISTINCT artist) FROM %1 WHERE unavailable = 0").arg(songs_table_));
293 if (!q.Exec()) {
294 db_->ReportErrors(q);
295 return;
296 }
297 if (!q.next()) {
298 db_->ReportErrors(q);
299 return;
300 }
301
302 emit TotalArtistCountUpdated(q.value(0).toInt());
303
304 }
305
UpdateTotalAlbumCount()306 void CollectionBackend::UpdateTotalAlbumCount() {
307
308 QMutexLocker l(db_->Mutex());
309 QSqlDatabase db(db_->Connect());
310
311 SqlQuery q(db);
312 q.prepare(QString("SELECT COUNT(*) FROM (SELECT DISTINCT effective_albumartist, album FROM %1 WHERE unavailable = 0)").arg(songs_table_));
313 if (!q.Exec()) {
314 db_->ReportErrors(q);
315 return;
316 }
317 if (!q.next()) {
318 db_->ReportErrors(q);
319 return;
320 }
321
322 emit TotalAlbumCountUpdated(q.value(0).toInt());
323
324 }
325
AddDirectory(const QString & path)326 void CollectionBackend::AddDirectory(const QString &path) {
327
328 QString canonical_path = QFileInfo(path).canonicalFilePath();
329 QString db_path = canonical_path;
330
331 QMutexLocker l(db_->Mutex());
332 QSqlDatabase db(db_->Connect());
333
334 SqlQuery q(db);
335 q.prepare(QString("INSERT INTO %1 (path, subdirs) VALUES (:path, 1)").arg(dirs_table_));
336 q.BindValue(":path", db_path);
337 if (!q.Exec()) {
338 db_->ReportErrors(q);
339 return;
340 }
341
342 Directory dir;
343 dir.path = canonical_path;
344 dir.id = q.lastInsertId().toInt();
345
346 emit DirectoryDiscovered(dir, SubdirectoryList());
347
348 }
349
RemoveDirectory(const Directory & dir)350 void CollectionBackend::RemoveDirectory(const Directory &dir) {
351
352 QMutexLocker l(db_->Mutex());
353 QSqlDatabase db(db_->Connect());
354
355 // Remove songs first
356 DeleteSongs(FindSongsInDirectory(dir.id));
357
358 ScopedTransaction transaction(&db);
359
360 // Delete the subdirs that were in this directory
361 {
362 SqlQuery q(db);
363 q.prepare(QString("DELETE FROM %1 WHERE directory_id = :id").arg(subdirs_table_));
364 q.BindValue(":id", dir.id);
365 if (!q.Exec()) {
366 db_->ReportErrors(q);
367 return;
368 }
369 }
370
371 // Now remove the directory itself
372 {
373 SqlQuery q(db);
374 q.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(dirs_table_));
375 q.BindValue(":id", dir.id);
376 if (!q.Exec()) {
377 db_->ReportErrors(q);
378 return;
379 }
380 }
381
382 emit DirectoryDeleted(dir);
383
384 transaction.Commit();
385
386 }
387
FindSongsInDirectory(const int id)388 SongList CollectionBackend::FindSongsInDirectory(const int id) {
389
390 QMutexLocker l(db_->Mutex());
391 QSqlDatabase db(db_->Connect());
392
393 SqlQuery q(db);
394 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE directory_id = :directory_id").arg(songs_table_));
395 q.BindValue(":directory_id", id);
396 if (!q.Exec()) {
397 db_->ReportErrors(q);
398 return SongList();
399 }
400
401 SongList ret;
402 while (q.next()) {
403 Song song(source_);
404 song.InitFromQuery(q, true);
405 ret << song;
406 }
407 return ret;
408
409 }
410
SongsWithMissingFingerprint(const int id)411 SongList CollectionBackend::SongsWithMissingFingerprint(const int id) {
412
413 QMutexLocker l(db_->Mutex());
414 QSqlDatabase db(db_->Connect());
415
416 SqlQuery q(db);
417 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE directory_id = :directory_id AND unavailable = 0 AND (fingerprint IS NULL OR fingerprint = '')").arg(songs_table_));
418 q.BindValue(":directory_id", id);
419 if (!q.Exec()) {
420 db_->ReportErrors(q);
421 return SongList();
422 }
423
424 SongList ret;
425 while (q.next()) {
426 Song song(source_);
427 song.InitFromQuery(q, true);
428 ret << song;
429 }
430 return ret;
431
432 }
433
SongPathChanged(const Song & song,const QFileInfo & new_file,const std::optional<int> new_collection_directory_id)434 void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional<int> new_collection_directory_id) {
435
436 // Take a song and update its path
437 Song updated_song = song;
438 updated_song.set_source(source_);
439 updated_song.set_url(QUrl::fromLocalFile(new_file.absoluteFilePath()));
440 updated_song.set_basefilename(new_file.fileName());
441 updated_song.InitArtManual();
442 if (updated_song.is_collection_song() && new_collection_directory_id) {
443 updated_song.set_directory_id(new_collection_directory_id.value());
444 }
445
446 AddOrUpdateSongs(SongList() << updated_song);
447
448 }
449
AddOrUpdateSubdirs(const SubdirectoryList & subdirs)450 void CollectionBackend::AddOrUpdateSubdirs(const SubdirectoryList &subdirs) {
451
452 QMutexLocker l(db_->Mutex());
453 QSqlDatabase db(db_->Connect());
454
455 ScopedTransaction transaction(&db);
456 for (const Subdirectory &subdir : subdirs) {
457 if (subdir.mtime == 0) {
458 // Delete the subdirectory
459 SqlQuery q(db);
460 q.prepare(QString("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
461 q.BindValue(":id", subdir.directory_id);
462 q.BindValue(":path", subdir.path);
463 if (!q.Exec()) {
464 db_->ReportErrors(q);
465 return;
466 }
467 }
468 else {
469 // See if this subdirectory already exists in the database
470 bool exists = false;
471 {
472 SqlQuery q(db);
473 q.prepare(QString("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
474 q.BindValue(":id", subdir.directory_id);
475 q.BindValue(":path", subdir.path);
476 if (!q.Exec()) {
477 db_->ReportErrors(q);
478 return;
479 }
480 exists = q.next();
481 }
482
483 if (exists) {
484 SqlQuery q(db);
485 q.prepare(QString("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
486 q.BindValue(":mtime", subdir.mtime);
487 q.BindValue(":id", subdir.directory_id);
488 q.BindValue(":path", subdir.path);
489 if (!q.Exec()) {
490 db_->ReportErrors(q);
491 return;
492 }
493 }
494 else {
495 SqlQuery q(db);
496 q.prepare(QString("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
497 q.BindValue(":id", subdir.directory_id);
498 q.BindValue(":path", subdir.path);
499 q.BindValue(":mtime", subdir.mtime);
500 if (!q.Exec()) {
501 db_->ReportErrors(q);
502 return;
503 }
504 }
505 }
506 }
507
508 transaction.Commit();
509
510 }
511
AddOrUpdateSongsAsync(const SongList & songs)512 void CollectionBackend::AddOrUpdateSongsAsync(const SongList &songs) {
513 QMetaObject::invokeMethod(this, "AddOrUpdateSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
514 }
515
AddOrUpdateSongs(const SongList & songs)516 void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
517
518 QMutexLocker l(db_->Mutex());
519 QSqlDatabase db(db_->Connect());
520
521 ScopedTransaction transaction(&db);
522
523 SongList added_songs;
524 SongList deleted_songs;
525
526 for (const Song &song : songs) {
527
528 // Do a sanity check first - make sure the song's directory still exists
529 // This is to fix a possible race condition when a directory is removed while CollectionWatcher is scanning it.
530 if (!dirs_table_.isEmpty()) {
531 SqlQuery check_dir(db);
532 check_dir.prepare(QString("SELECT ROWID FROM %1 WHERE ROWID = :id").arg(dirs_table_));
533 check_dir.BindValue(":id", song.directory_id());
534 if (!check_dir.Exec()) {
535 db_->ReportErrors(check_dir);
536 return;
537 }
538
539 if (!check_dir.next()) continue;
540
541 }
542
543 if (song.id() != -1) { // This song exists in the DB.
544
545 // Get the previous song data first
546 Song old_song(GetSongById(song.id()));
547 if (!old_song.is_valid()) continue;
548
549 // Update
550 {
551 SqlQuery q(db);
552 q.prepare(QString("UPDATE %1 SET " + Song::kUpdateSpec + " WHERE ROWID = :id").arg(songs_table_));
553 song.BindToQuery(&q);
554 q.BindValue(":id", song.id());
555 if (!q.Exec()) {
556 db_->ReportErrors(q);
557 return;
558 }
559 }
560
561 {
562 SqlQuery q(db);
563 q.prepare(QString("UPDATE %1 SET " + Song::kFtsUpdateSpec + " WHERE ROWID = :id").arg(fts_table_));
564 song.BindToFtsQuery(&q);
565 q.BindValue(":id", song.id());
566 if (!q.Exec()) {
567 db_->ReportErrors(q);
568 return;
569 }
570 }
571
572 deleted_songs << old_song;
573 added_songs << song;
574
575 continue;
576
577 }
578 else if (!song.song_id().isEmpty()) { // Song has a unique id, check if the song exists.
579
580 // Get the previous song data first
581 Song old_song(GetSongBySongId(song.song_id()));
582
583 if (old_song.is_valid() && old_song.id() != -1) {
584
585 Song new_song = song;
586 new_song.set_id(old_song.id());
587
588 // Update
589 {
590 SqlQuery q(db);
591 q.prepare(QString("UPDATE %1 SET " + Song::kUpdateSpec + " WHERE ROWID = :id").arg(songs_table_));
592 new_song.BindToQuery(&q);
593 q.BindValue(":id", new_song.id());
594 if (!q.Exec()) {
595 db_->ReportErrors(q);
596 return;
597 }
598 }
599
600 {
601 SqlQuery q(db);
602 q.prepare(QString("UPDATE %1 SET " + Song::kFtsUpdateSpec + " WHERE ROWID = :id").arg(fts_table_));
603 new_song.BindToFtsQuery(&q);
604 q.BindValue(":id", new_song.id());
605 if (!q.Exec()) {
606 db_->ReportErrors(q);
607 return;
608 }
609 }
610
611 deleted_songs << old_song;
612 added_songs << new_song;
613
614 continue;
615
616 }
617
618 }
619
620 // Create new song
621
622 int id = -1;
623 { // Insert the row and create a new ID
624 SqlQuery q(db);
625 q.prepare(QString("INSERT INTO %1 (" + Song::kColumnSpec + ") VALUES (" + Song::kBindSpec + ")").arg(songs_table_));
626 song.BindToQuery(&q);
627 if (!q.Exec()) {
628 db_->ReportErrors(q);
629 return;
630 }
631 // Get the new ID
632 id = q.lastInsertId().toInt();
633 }
634
635 if (id == -1) return;
636
637 { // Add to the FTS index
638 SqlQuery q(db);
639 q.prepare(QString("INSERT INTO %1 (ROWID, " + Song::kFtsColumnSpec + ") VALUES (:id, " + Song::kFtsBindSpec + ")").arg(fts_table_));
640 q.BindValue(":id", id);
641 song.BindToFtsQuery(&q);
642 if (!q.Exec()) {
643 db_->ReportErrors(q);
644 return;
645 }
646 }
647
648 Song song_copy(song);
649 song_copy.set_id(id);
650 added_songs << song_copy;
651
652 }
653
654 transaction.Commit();
655
656 if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
657 if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
658
659 UpdateTotalSongCountAsync();
660 UpdateTotalArtistCountAsync();
661 UpdateTotalAlbumCountAsync();
662
663 }
664
UpdateSongsBySongIDAsync(const SongMap & new_songs)665 void CollectionBackend::UpdateSongsBySongIDAsync(const SongMap &new_songs) {
666 QMetaObject::invokeMethod(this, "UpdateSongsBySongID", Qt::QueuedConnection, Q_ARG(SongMap, new_songs));
667 }
668
UpdateSongsBySongID(const SongMap & new_songs)669 void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
670
671 QMutexLocker l(db_->Mutex());
672 QSqlDatabase db(db_->Connect());
673
674 CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_)));
675 ScopedTransaction transaction(&db);
676
677 SongList added_songs;
678 SongList deleted_songs;
679
680 SongMap old_songs;
681 {
682 CollectionQuery query(db, songs_table_, fts_table_);
683 if (!ExecCollectionQuery(&query, old_songs)) {
684 ReportErrors(query);
685 return;
686 }
687 }
688
689 // Add or update songs.
690 for (const Song &new_song : new_songs) {
691 if (old_songs.contains(new_song.song_id())) {
692
693 Song old_song = old_songs[new_song.song_id()];
694
695 if (!new_song.IsMetadataEqual(old_song)) { // Update existing song.
696
697 {
698 SqlQuery q(db);
699 q.prepare(QString("UPDATE %1 SET " + Song::kUpdateSpec + " WHERE ROWID = :id").arg(songs_table_));
700 new_song.BindToQuery(&q);
701 q.BindValue(":id", old_song.id());
702 if (!q.Exec()) {
703 db_->ReportErrors(q);
704 return;
705 }
706 }
707 {
708 SqlQuery q(db);
709 q.prepare(QString("UPDATE %1 SET " + Song::kFtsUpdateSpec + " WHERE ROWID = :id").arg(fts_table_));
710 new_song.BindToFtsQuery(&q);
711 q.BindValue(":id", old_song.id());
712 if (!q.Exec()) {
713 db_->ReportErrors(q);
714 return;
715 }
716 }
717
718 deleted_songs << old_song;
719 Song new_song_copy(new_song);
720 new_song_copy.set_id(old_song.id());
721 added_songs << new_song_copy;
722
723 }
724
725 }
726 else { // Add new song
727 int id = -1;
728 {
729 SqlQuery q(db);
730 q.prepare(QString("INSERT INTO %1 (" + Song::kColumnSpec + ") VALUES (" + Song::kBindSpec + ")").arg(songs_table_));
731 new_song.BindToQuery(&q);
732 if (!q.Exec()) {
733 db_->ReportErrors(q);
734 return;
735 }
736 // Get the new ID
737 id = q.lastInsertId().toInt();
738 }
739
740 if (id == -1) return;
741
742 { // Add to the FTS index
743 SqlQuery q(db);
744 q.prepare(QString("INSERT INTO %1 (ROWID, " + Song::kFtsColumnSpec + ") VALUES (:id, " + Song::kFtsBindSpec + ")").arg(fts_table_));
745 q.BindValue(":id", id);
746 new_song.BindToFtsQuery(&q);
747 if (!q.Exec()) {
748 db_->ReportErrors(q);
749 return;
750 }
751 }
752
753 Song new_song_copy(new_song);
754 new_song_copy.set_id(id);
755 added_songs << new_song_copy;
756 }
757 }
758
759 // Delete songs
760 for (const Song &old_song : old_songs) {
761 if (!new_songs.contains(old_song.song_id())) {
762 {
763 SqlQuery q(db);
764 q.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
765 q.BindValue(":id", old_song.id());
766 if (!q.Exec()) {
767 db_->ReportErrors(q);
768 return;
769 }
770 }
771 {
772 SqlQuery q(db);
773 q.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
774 q.BindValue(":id", old_song.id());
775 if (!q.Exec()) {
776 db_->ReportErrors(q);
777 return;
778 }
779 }
780 deleted_songs << old_song;
781 }
782 }
783
784 transaction.Commit();
785
786 if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
787 if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
788
789 UpdateTotalSongCountAsync();
790 UpdateTotalArtistCountAsync();
791 UpdateTotalAlbumCountAsync();
792
793 }
794
UpdateMTimesOnly(const SongList & songs)795 void CollectionBackend::UpdateMTimesOnly(const SongList &songs) {
796
797 QMutexLocker l(db_->Mutex());
798 QSqlDatabase db(db_->Connect());
799
800 SqlQuery q(db);
801 q.prepare(QString("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_));
802
803 ScopedTransaction transaction(&db);
804 for (const Song &song : songs) {
805 q.BindValue(":mtime", song.mtime());
806 q.BindValue(":id", song.id());
807 if (!q.Exec()) {
808 db_->ReportErrors(q);
809 return;
810 }
811 }
812 transaction.Commit();
813
814 }
815
DeleteSongs(const SongList & songs)816 void CollectionBackend::DeleteSongs(const SongList &songs) {
817
818 QMutexLocker l(db_->Mutex());
819 QSqlDatabase db(db_->Connect());
820
821 SqlQuery remove(db);
822 remove.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
823 SqlQuery remove_fts(db);
824 remove_fts.prepare(QString("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
825
826 ScopedTransaction transaction(&db);
827 for (const Song &song : songs) {
828 remove.BindValue(":id", song.id());
829 if (!remove.Exec()) {
830 db_->ReportErrors(remove);
831 return;
832 }
833
834 remove_fts.BindValue(":id", song.id());
835 if (!remove_fts.Exec()) {
836 db_->ReportErrors(remove_fts);
837 return;
838 }
839 }
840 transaction.Commit();
841
842 emit SongsDeleted(songs);
843
844 UpdateTotalSongCountAsync();
845 UpdateTotalArtistCountAsync();
846 UpdateTotalAlbumCountAsync();
847
848 }
849
MarkSongsUnavailable(const SongList & songs,const bool unavailable)850 void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool unavailable) {
851
852 QMutexLocker l(db_->Mutex());
853 QSqlDatabase db(db_->Connect());
854
855 SqlQuery remove(db);
856 remove.prepare(QString("UPDATE %1 SET unavailable = %2 WHERE ROWID = :id").arg(songs_table_).arg(static_cast<int>(unavailable)));
857
858 ScopedTransaction transaction(&db);
859 for (const Song &song : songs) {
860 remove.BindValue(":id", song.id());
861 if (!remove.Exec()) {
862 db_->ReportErrors(remove);
863 return;
864 }
865 }
866 transaction.Commit();
867
868 if (unavailable) {
869 emit SongsDeleted(songs);
870 }
871 else {
872 emit SongsDiscovered(songs);
873 }
874
875 UpdateTotalSongCountAsync();
876 UpdateTotalArtistCountAsync();
877 UpdateTotalAlbumCountAsync();
878
879 }
880
GetAll(const QString & column,const QueryOptions & opt)881 QStringList CollectionBackend::GetAll(const QString &column, const QueryOptions &opt) {
882
883 QMutexLocker l(db_->Mutex());
884 QSqlDatabase db(db_->Connect());
885
886 CollectionQuery query(db, songs_table_, fts_table_, opt);
887 query.SetColumnSpec("DISTINCT " + column);
888 query.AddCompilationRequirement(false);
889
890 if (!query.Exec()) {
891 ReportErrors(query);
892 return QStringList();
893 }
894
895 QStringList ret;
896 while (query.Next()) {
897 ret << query.Value(0).toString();
898 }
899 return ret;
900
901 }
902
GetAllArtists(const QueryOptions & opt)903 QStringList CollectionBackend::GetAllArtists(const QueryOptions &opt) {
904
905 return GetAll("artist", opt);
906 }
907
GetAllArtistsWithAlbums(const QueryOptions & opt)908 QStringList CollectionBackend::GetAllArtistsWithAlbums(const QueryOptions &opt) {
909
910 QMutexLocker l(db_->Mutex());
911 QSqlDatabase db(db_->Connect());
912
913 // Albums with 'albumartist' field set:
914 CollectionQuery query(db, songs_table_, fts_table_, opt);
915 query.SetColumnSpec("DISTINCT albumartist");
916 query.AddCompilationRequirement(false);
917 query.AddWhere("album", "", "!=");
918
919 // Albums with no 'albumartist' (extract 'artist'):
920 CollectionQuery query2(db, songs_table_, fts_table_, opt);
921 query2.SetColumnSpec("DISTINCT artist");
922 query2.AddCompilationRequirement(false);
923 query2.AddWhere("album", "", "!=");
924 query2.AddWhere("albumartist", "", "=");
925
926 if (!query.Exec()) {
927 ReportErrors(query);
928 return QStringList();
929 }
930 if (!query2.Exec()) {
931 ReportErrors(query2);
932 return QStringList();
933 }
934
935 QSet<QString> artists;
936 while (query.Next()) {
937 artists << query.Value(0).toString();
938 }
939
940 while (query2.Next()) {
941 artists << query2.Value(0).toString();
942 }
943
944 return QStringList(artists.values());
945
946 }
947
GetAllAlbums(const QueryOptions & opt)948 CollectionBackend::AlbumList CollectionBackend::GetAllAlbums(const QueryOptions &opt) {
949 return GetAlbums(QString(), false, opt);
950 }
951
GetAlbumsByArtist(const QString & artist,const QueryOptions & opt)952 CollectionBackend::AlbumList CollectionBackend::GetAlbumsByArtist(const QString &artist, const QueryOptions &opt) {
953 return GetAlbums(artist, false, opt);
954 }
955
GetArtistSongs(const QString & effective_albumartist,const QueryOptions & opt)956 SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist, const QueryOptions &opt) {
957
958 QSqlDatabase db(db_->Connect());
959 QMutexLocker l(db_->Mutex());
960
961 CollectionQuery query(db, songs_table_, fts_table_, opt);
962 query.AddCompilationRequirement(false);
963 query.AddWhere("effective_albumartist", effective_albumartist);
964
965 SongList songs;
966 if (!ExecCollectionQuery(&query, songs)) {
967 ReportErrors(query);
968 }
969
970 return songs;
971
972 }
973
GetAlbumSongs(const QString & effective_albumartist,const QString & album,const QueryOptions & opt)974 SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist, const QString &album, const QueryOptions &opt) {
975
976 QSqlDatabase db(db_->Connect());
977 QMutexLocker l(db_->Mutex());
978
979 CollectionQuery query(db, songs_table_, fts_table_, opt);
980 query.AddCompilationRequirement(false);
981 query.AddWhere("effective_albumartist", effective_albumartist);
982 query.AddWhere("album", album);
983
984 SongList songs;
985 if (!ExecCollectionQuery(&query, songs)) {
986 ReportErrors(query);
987 }
988
989 return songs;
990
991 }
992
GetSongsByAlbum(const QString & album,const QueryOptions & opt)993 SongList CollectionBackend::GetSongsByAlbum(const QString &album, const QueryOptions &opt) {
994
995 QSqlDatabase db(db_->Connect());
996 QMutexLocker l(db_->Mutex());
997
998 CollectionQuery query(db, songs_table_, fts_table_, opt);
999 query.AddCompilationRequirement(false);
1000 query.AddWhere("album", album);
1001
1002 SongList songs;
1003 if (!ExecCollectionQuery(&query, songs)) {
1004 ReportErrors(query);
1005 }
1006
1007 return songs;
1008
1009 }
1010
ExecCollectionQuery(CollectionQuery * query,SongList & songs)1011 bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongList &songs) {
1012
1013 query->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
1014
1015 if (!query->Exec()) return false;
1016
1017 while (query->Next()) {
1018 Song song(source_);
1019 song.InitFromQuery(*query, true);
1020 songs << song;
1021 }
1022 return true;
1023
1024 }
1025
ExecCollectionQuery(CollectionQuery * query,SongMap & songs)1026 bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongMap &songs) {
1027
1028 query->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
1029
1030 if (!query->Exec()) return false;
1031
1032 while (query->Next()) {
1033 Song song(source_);
1034 song.InitFromQuery(*query, true);
1035 songs.insert(song.song_id(), song);
1036 }
1037 return true;
1038
1039 }
1040
GetSongById(const int id)1041 Song CollectionBackend::GetSongById(const int id) {
1042
1043 QMutexLocker l(db_->Mutex());
1044 QSqlDatabase db(db_->Connect());
1045 return GetSongById(id, db);
1046
1047 }
1048
GetSongsById(const QList<int> & ids)1049 SongList CollectionBackend::GetSongsById(const QList<int> &ids) {
1050
1051 QMutexLocker l(db_->Mutex());
1052 QSqlDatabase db(db_->Connect());
1053
1054 QStringList str_ids;
1055 str_ids.reserve(ids.count());
1056 for (const int id : ids) {
1057 str_ids << QString::number(id);
1058 }
1059
1060 return GetSongsById(str_ids, db);
1061
1062 }
1063
GetSongsById(const QStringList & ids)1064 SongList CollectionBackend::GetSongsById(const QStringList &ids) {
1065
1066 QMutexLocker l(db_->Mutex());
1067 QSqlDatabase db(db_->Connect());
1068
1069 return GetSongsById(ids, db);
1070
1071 }
1072
GetSongsByForeignId(const QStringList & ids,const QString & table,const QString & column)1073 SongList CollectionBackend::GetSongsByForeignId(const QStringList &ids, const QString &table, const QString &column) {
1074
1075 QMutexLocker l(db_->Mutex());
1076 QSqlDatabase db(db_->Connect());
1077
1078 QString in = ids.join(",");
1079
1080 SqlQuery q(db);
1081 q.prepare(QString("SELECT %2.ROWID, " + Song::kColumnSpec + ", %2.%3 FROM %2, %1 WHERE %2.%3 IN (%4) AND %1.ROWID = %2.ROWID AND unavailable = 0").arg(songs_table_, table, column, in));
1082 if (!q.Exec()) {
1083 db_->ReportErrors(q);
1084 return SongList();
1085 }
1086
1087 QVector<Song> ret(ids.count());
1088 while (q.next()) {
1089 const QString foreign_id = q.value(static_cast<int>(Song::kColumns.count()) + 1).toString();
1090 const int index = ids.indexOf(foreign_id);
1091 if (index == -1) continue;
1092
1093 ret[index].InitFromQuery(q, true);
1094 }
1095 return ret.toList();
1096
1097 }
1098
GetSongById(const int id,QSqlDatabase & db)1099 Song CollectionBackend::GetSongById(const int id, QSqlDatabase &db) {
1100
1101 SongList list = GetSongsById(QStringList() << QString::number(id), db);
1102 if (list.isEmpty()) return Song();
1103 return list.first();
1104
1105 }
1106
GetSongsById(const QStringList & ids,QSqlDatabase & db)1107 SongList CollectionBackend::GetSongsById(const QStringList &ids, QSqlDatabase &db) {
1108
1109 QString in = ids.join(",");
1110
1111 SqlQuery q(db);
1112 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE ROWID IN (%2)").arg(songs_table_, in));
1113 if (!q.Exec()) {
1114 db_->ReportErrors(q);
1115 return SongList();
1116 }
1117
1118 SongList ret;
1119 while (q.next()) {
1120 Song song(source_);
1121 song.InitFromQuery(q, true);
1122 ret << song;
1123 }
1124 return ret;
1125
1126 }
1127
GetSongByUrl(const QUrl & url,const qint64 beginning)1128 Song CollectionBackend::GetSongByUrl(const QUrl &url, const qint64 beginning) {
1129
1130 QMutexLocker l(db_->Mutex());
1131 QSqlDatabase db(db_->Connect());
1132
1133 SqlQuery q(db);
1134 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND beginning = :beginning AND unavailable = 0").arg(songs_table_));
1135
1136 q.BindValue(":url1", url);
1137 q.BindValue(":url2", url.toString());
1138 q.BindValue(":url3", url.toString(QUrl::FullyEncoded));
1139 q.BindValue(":url4", url.toEncoded());
1140 q.BindValue(":beginning", beginning);
1141
1142 if (!q.Exec()) {
1143 db_->ReportErrors(q);
1144 return Song();
1145 }
1146
1147 if (!q.next()) {
1148 return Song();
1149 }
1150
1151 Song song(source_);
1152 song.InitFromQuery(q, true);
1153
1154 return song;
1155
1156 }
1157
GetSongsByUrl(const QUrl & url,const bool unavailable)1158 SongList CollectionBackend::GetSongsByUrl(const QUrl &url, const bool unavailable) {
1159
1160 QMutexLocker l(db_->Mutex());
1161 QSqlDatabase db(db_->Connect());
1162
1163 SqlQuery q(db);
1164 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = :unavailable").arg(songs_table_));
1165
1166 q.BindValue(":url1", url);
1167 q.BindValue(":url2", url.toString());
1168 q.BindValue(":url3", url.toString(QUrl::FullyEncoded));
1169 q.BindValue(":url4", url.toEncoded());
1170 q.BindValue(":unavailable", (unavailable ? 1 : 0));
1171
1172 SongList songs;
1173 if (q.Exec()) {
1174 while (q.next()) {
1175 Song song(source_);
1176 song.InitFromQuery(q, true);
1177 songs << song;
1178 }
1179 }
1180 else {
1181 db_->ReportErrors(q);
1182 }
1183
1184 return songs;
1185
1186 }
1187
1188
GetSongBySongId(const QString & song_id)1189 Song CollectionBackend::GetSongBySongId(const QString &song_id) {
1190
1191 QMutexLocker l(db_->Mutex());
1192 QSqlDatabase db(db_->Connect());
1193 return GetSongBySongId(song_id, db);
1194
1195 }
1196
GetSongsBySongId(const QStringList & song_ids)1197 SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids) {
1198
1199 QMutexLocker l(db_->Mutex());
1200 QSqlDatabase db(db_->Connect());
1201
1202 return GetSongsBySongId(song_ids, db);
1203
1204 }
1205
GetSongBySongId(const QString & song_id,QSqlDatabase & db)1206 Song CollectionBackend::GetSongBySongId(const QString &song_id, QSqlDatabase &db) {
1207
1208 SongList list = GetSongsBySongId(QStringList() << song_id, db);
1209 if (list.isEmpty()) return Song();
1210 return list.first();
1211
1212 }
1213
GetSongsBySongId(const QStringList & song_ids,QSqlDatabase & db)1214 SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids, QSqlDatabase &db) {
1215
1216 QStringList song_ids2;
1217 song_ids2.reserve(song_ids.count());
1218 for (const QString &song_id : song_ids) {
1219 song_ids2 << "'" + song_id + "'";
1220 }
1221 QString in = song_ids2.join(",");
1222
1223 SqlQuery q(db);
1224 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE SONG_ID IN (%2)").arg(songs_table_, in));
1225 if (!q.Exec()) {
1226 db_->ReportErrors(q);
1227 return SongList();
1228 }
1229
1230 SongList ret;
1231 while (q.next()) {
1232 Song song(source_);
1233 song.InitFromQuery(q, true);
1234 ret << song;
1235 }
1236
1237 return ret;
1238
1239 }
1240
GetSongsByFingerprint(const QString & fingerprint)1241 SongList CollectionBackend::GetSongsByFingerprint(const QString &fingerprint) {
1242
1243 QMutexLocker l(db_->Mutex());
1244 QSqlDatabase db(db_->Connect());
1245
1246 SqlQuery q(db);
1247 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE fingerprint = :fingerprint").arg(songs_table_));
1248 q.BindValue(":fingerprint", fingerprint);
1249 if (!q.Exec()) {
1250 db_->ReportErrors(q);
1251 return SongList();
1252 }
1253
1254 SongList songs;
1255 while (q.next()) {
1256 Song song(source_);
1257 song.InitFromQuery(q, true);
1258 songs << song;
1259 }
1260
1261 return songs;
1262
1263 }
1264
1265
GetCompilationAlbums(const QueryOptions & opt)1266 CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const QueryOptions &opt) {
1267 return GetAlbums(QString(), true, opt);
1268 }
1269
GetCompilationSongs(const QString & album,const QueryOptions & opt)1270 SongList CollectionBackend::GetCompilationSongs(const QString &album, const QueryOptions &opt) {
1271
1272 QMutexLocker l(db_->Mutex());
1273 QSqlDatabase db(db_->Connect());
1274
1275 CollectionQuery query(db, songs_table_, fts_table_, opt);
1276 query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
1277 query.AddCompilationRequirement(true);
1278 query.AddWhere("album", album);
1279
1280 if (!query.Exec()) {
1281 ReportErrors(query);
1282 return SongList();
1283 }
1284
1285 SongList ret;
1286 while (query.Next()) {
1287 Song song(source_);
1288 song.InitFromQuery(query, true);
1289 ret << song;
1290 }
1291 return ret;
1292
1293 }
1294
Source() const1295 Song::Source CollectionBackend::Source() const {
1296 return source_;
1297 }
1298
CompilationsNeedUpdating()1299 void CollectionBackend::CompilationsNeedUpdating() {
1300
1301 QMutexLocker l(db_->Mutex());
1302 QSqlDatabase db(db_->Connect());
1303
1304 // Look for albums that have songs by more than one 'effective album artist' in the same directory
1305
1306 SqlQuery q(db);
1307 q.prepare(QString("SELECT effective_albumartist, album, url, compilation_detected FROM %1 WHERE unavailable = 0 ORDER BY album").arg(songs_table_));
1308 if (!q.Exec()) {
1309 db_->ReportErrors(q);
1310 return;
1311 }
1312
1313 QMap<QString, CompilationInfo> compilation_info;
1314 while (q.next()) {
1315 QString artist = q.value(0).toString();
1316 QString album = q.value(1).toString();
1317 QUrl url = QUrl::fromEncoded(q.value(2).toString().toUtf8());
1318 bool compilation_detected = q.value(3).toBool();
1319
1320 // Ignore songs that don't have an album field set
1321 if (album.isEmpty()) continue;
1322
1323 // Find the directory the song is in
1324 QString directory = url.toString(QUrl::PreferLocalFile|QUrl::RemoveFilename);
1325
1326 CompilationInfo &info = compilation_info[directory + album];
1327 info.urls << url;
1328 if (!info.artists.contains(artist)) {
1329 info.artists << artist;
1330 }
1331 if (compilation_detected) info.has_compilation_detected++;
1332 else info.has_not_compilation_detected++;
1333 }
1334
1335 // Now mark the songs that we think are in compilations
1336 SongList deleted_songs;
1337 SongList added_songs;
1338
1339 ScopedTransaction transaction(&db);
1340
1341 QMap<QString, CompilationInfo>::const_iterator it = compilation_info.constBegin();
1342 for (; it != compilation_info.constEnd(); ++it) {
1343 const CompilationInfo &info = it.value();
1344
1345 // If there were more than one 'effective album artist' for this album directory, then it's a compilation.
1346
1347 for (const QUrl &url : info.urls) {
1348 if (info.artists.count() > 1) { // This directory+album is a compilation.
1349 if (info.has_not_compilation_detected > 0) { // Run updates if any of the songs is not marked as compilations.
1350 UpdateCompilations(db, deleted_songs, added_songs, url, true);
1351 }
1352 }
1353 else {
1354 if (info.has_compilation_detected > 0) {
1355 UpdateCompilations(db, deleted_songs, added_songs, url, false);
1356 }
1357 }
1358 }
1359 }
1360
1361 transaction.Commit();
1362
1363 if (!deleted_songs.isEmpty()) {
1364 emit SongsDeleted(deleted_songs);
1365 emit SongsDiscovered(added_songs);
1366 }
1367
1368 }
1369
UpdateCompilations(const QSqlDatabase & db,SongList & deleted_songs,SongList & added_songs,const QUrl & url,const bool compilation_detected)1370 bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &deleted_songs, SongList &added_songs, const QUrl &url, const bool compilation_detected) {
1371
1372 { // Get song, so we can tell the model its updated
1373 SqlQuery q(db);
1374 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(songs_table_));
1375 q.BindValue(":url1", url);
1376 q.BindValue(":url2", url.toString());
1377 q.BindValue(":url3", url.toString(QUrl::FullyEncoded));
1378 q.BindValue(":url4", url.toEncoded());
1379 if (q.Exec()) {
1380 while (q.next()) {
1381 Song song(source_);
1382 song.InitFromQuery(q, true);
1383 deleted_songs << song;
1384 song.set_compilation_detected(compilation_detected);
1385 added_songs << song;
1386 }
1387 }
1388 else {
1389 db_->ReportErrors(q);
1390 return false;
1391 }
1392 }
1393
1394 // Update the song
1395 SqlQuery q(db);
1396 q.prepare(QString("UPDATE %1 SET compilation_detected = :compilation_detected, compilation_effective = ((compilation OR :compilation_detected OR compilation_on) AND NOT compilation_off) + 0 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(songs_table_));
1397 q.BindValue(":compilation_detected", static_cast<int>(compilation_detected));
1398 q.BindValue(":url1", url);
1399 q.BindValue(":url2", url.toString());
1400 q.BindValue(":url3", url.toString(QUrl::FullyEncoded));
1401 q.BindValue(":url4", url.toEncoded());
1402 if (!q.Exec()) {
1403 db_->ReportErrors(q);
1404 return false;
1405 }
1406
1407 return true;
1408
1409 }
1410
GetAlbums(const QString & artist,const bool compilation_required,const QueryOptions & opt)1411 CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt) {
1412
1413 QMutexLocker l(db_->Mutex());
1414 QSqlDatabase db(db_->Connect());
1415
1416 CollectionQuery query(db, songs_table_, fts_table_, opt);
1417 query.SetColumnSpec("url, effective_albumartist, album, compilation_effective, art_automatic, art_manual, filetype, cue_path");
1418 query.SetOrderBy("effective_albumartist, album, url");
1419
1420 if (compilation_required) {
1421 query.AddCompilationRequirement(true);
1422 }
1423 else if (!artist.isEmpty()) {
1424 query.AddCompilationRequirement(false);
1425 query.AddWhere("effective_albumartist", artist);
1426 }
1427
1428 if (!query.Exec()) {
1429 ReportErrors(query);
1430 return AlbumList();
1431 }
1432
1433 QMap<QString, Album> albums;
1434 while (query.Next()) {
1435 bool is_compilation = query.Value(3).toBool();
1436
1437 Album info;
1438 QUrl url = QUrl::fromEncoded(query.Value(0).toByteArray());
1439 if (!is_compilation) {
1440 info.album_artist = query.Value(1).toString();
1441 }
1442 info.album = query.Value(2).toString();
1443
1444 QString art_automatic = query.Value(4).toString();
1445 if (art_automatic.contains(QRegularExpression("..+:.*"))) {
1446 info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8());
1447 }
1448 else {
1449 info.art_automatic = QUrl::fromLocalFile(art_automatic);
1450 }
1451
1452 QString art_manual = query.Value(5).toString();
1453 if (art_manual.contains(QRegularExpression("..+:.*"))) {
1454 info.art_manual = QUrl::fromEncoded(art_manual.toUtf8());
1455 }
1456 else {
1457 info.art_manual = QUrl::fromLocalFile(art_manual);
1458 }
1459
1460 info.filetype = Song::FileType(query.Value(6).toInt());
1461 QString filetype = Song::TextForFiletype(info.filetype);
1462 info.cue_path = query.Value(7).toString();
1463
1464 QString key;
1465 if (!info.album_artist.isEmpty()) {
1466 key.append(info.album_artist);
1467 }
1468 if (!info.album.isEmpty()) {
1469 if (!key.isEmpty()) key.append("-");
1470 key.append(info.album);
1471 }
1472 if (!filetype.isEmpty()) {
1473 key.append(filetype);
1474 }
1475
1476 if (key.isEmpty()) continue;
1477
1478 if (albums.contains(key)) {
1479 albums[key].urls.append(url);
1480 }
1481 else {
1482 info.urls << url;
1483 albums.insert(key, info);
1484 }
1485
1486 }
1487
1488 return albums.values();
1489
1490 }
1491
GetAlbumArt(const QString & effective_albumartist,const QString & album)1492 CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective_albumartist, const QString &album) {
1493
1494 QMutexLocker l(db_->Mutex());
1495 QSqlDatabase db(db_->Connect());
1496
1497 Album ret;
1498 ret.album = album;
1499 ret.album_artist = effective_albumartist;
1500
1501 CollectionQuery query(db, songs_table_, fts_table_, QueryOptions());
1502 query.SetColumnSpec("art_automatic, art_manual, url");
1503 if (!effective_albumartist.isEmpty()) {
1504 query.AddWhere("effective_albumartist", effective_albumartist);
1505 }
1506 query.AddWhere("album", album);
1507
1508 if (!query.Exec()) {
1509 ReportErrors(query);
1510 return ret;
1511 }
1512
1513 if (query.Next()) {
1514 ret.art_automatic = QUrl::fromEncoded(query.Value(0).toByteArray());
1515 ret.art_manual = QUrl::fromEncoded(query.Value(1).toByteArray());
1516 ret.urls << QUrl::fromEncoded(query.Value(2).toByteArray());
1517 }
1518
1519 return ret;
1520
1521 }
1522
UpdateManualAlbumArtAsync(const QString & effective_albumartist,const QString & album,const QUrl & cover_url,const bool clear_art_automatic)1523 void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) {
1524
1525 QMetaObject::invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url), Q_ARG(bool, clear_art_automatic));
1526
1527 }
1528
UpdateManualAlbumArt(const QString & effective_albumartist,const QString & album,const QUrl & cover_url,const bool clear_art_automatic)1529 void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) {
1530
1531 QMutexLocker l(db_->Mutex());
1532 QSqlDatabase db(db_->Connect());
1533
1534 // Get the songs before they're updated
1535 CollectionQuery query(db, songs_table_, fts_table_);
1536 query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
1537 query.AddWhere("effective_albumartist", effective_albumartist);
1538 query.AddWhere("album", album);
1539
1540 if (!query.Exec()) {
1541 ReportErrors(query);
1542 return;
1543 }
1544
1545 SongList deleted_songs;
1546 while (query.Next()) {
1547 Song song(source_);
1548 song.InitFromQuery(query, true);
1549 deleted_songs << song;
1550 }
1551
1552 // Update the songs
1553 QString sql(QString("UPDATE %1 SET art_manual = :cover").arg(songs_table_));
1554 if (clear_art_automatic) {
1555 sql += ", art_automatic = ''";
1556 }
1557 sql += " WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0";
1558
1559 SqlQuery q(db);
1560 q.prepare(sql);
1561 q.BindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : "");
1562 q.BindValue(":effective_albumartist", effective_albumartist);
1563 q.BindValue(":album", album);
1564
1565 if (!q.Exec()) {
1566 db_->ReportErrors(q);
1567 return;
1568 }
1569
1570 // Now get the updated songs
1571 if (!query.Exec()) {
1572 ReportErrors(query);
1573 return;
1574 }
1575
1576 SongList added_songs;
1577 while (query.Next()) {
1578 Song song(source_);
1579 song.InitFromQuery(query, true);
1580 added_songs << song;
1581 }
1582
1583 if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
1584 emit SongsDeleted(deleted_songs);
1585 emit SongsDiscovered(added_songs);
1586 }
1587
1588 }
1589
UpdateAutomaticAlbumArtAsync(const QString & effective_albumartist,const QString & album,const QUrl & cover_url)1590 void CollectionBackend::UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url) {
1591
1592 QMetaObject::invokeMethod(this, "UpdateAutomaticAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url));
1593
1594 }
1595
UpdateAutomaticAlbumArt(const QString & effective_albumartist,const QString & album,const QUrl & cover_url)1596 void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url) {
1597
1598 QMutexLocker l(db_->Mutex());
1599 QSqlDatabase db(db_->Connect());
1600
1601 // Get the songs before they're updated
1602 CollectionQuery query(db, songs_table_, fts_table_);
1603 query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
1604 query.AddWhere("effective_albumartist", effective_albumartist);
1605 query.AddWhere("album", album);
1606
1607 if (!query.Exec()) {
1608 ReportErrors(query);
1609 return;
1610 }
1611
1612 SongList deleted_songs;
1613 while (query.Next()) {
1614 Song song(source_);
1615 song.InitFromQuery(query, true);
1616 deleted_songs << song;
1617 }
1618
1619 // Update the songs
1620 QString sql(QString("UPDATE %1 SET art_automatic = :cover WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
1621
1622 SqlQuery q(db);
1623 q.prepare(sql);
1624 q.BindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : "");
1625 q.BindValue(":effective_albumartist", effective_albumartist);
1626 q.BindValue(":album", album);
1627
1628 if (!q.Exec()) {
1629 db_->ReportErrors(q);
1630 return;
1631 }
1632
1633 // Now get the updated songs
1634 if (!query.Exec()) {
1635 ReportErrors(query);
1636 return;
1637 }
1638
1639 SongList added_songs;
1640 while (query.Next()) {
1641 Song song(source_);
1642 song.InitFromQuery(query, true);
1643 added_songs << song;
1644 }
1645
1646 if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
1647 emit SongsDeleted(deleted_songs);
1648 emit SongsDiscovered(added_songs);
1649 }
1650
1651 }
1652
ForceCompilation(const QString & album,const QList<QString> & artists,const bool on)1653 void CollectionBackend::ForceCompilation(const QString &album, const QList<QString> &artists, const bool on) {
1654
1655 QMutexLocker l(db_->Mutex());
1656 QSqlDatabase db(db_->Connect());
1657 SongList deleted_songs, added_songs;
1658
1659 for (const QString &artist : artists) {
1660 // Get the songs before they're updated
1661 CollectionQuery query(db, songs_table_, fts_table_);
1662 query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
1663 query.AddWhere("album", album);
1664 if (!artist.isEmpty()) query.AddWhere("artist", artist);
1665
1666 if (!query.Exec()) {
1667 ReportErrors(query);
1668 return;
1669 }
1670
1671 while (query.Next()) {
1672 Song song(source_);
1673 song.InitFromQuery(query, true);
1674 deleted_songs << song;
1675 }
1676
1677 // Update the songs
1678 QString sql(QString("UPDATE %1 SET compilation_on = :compilation_on, compilation_off = :compilation_off, compilation_effective = ((compilation OR compilation_detected OR :compilation_on) AND NOT :compilation_off) + 0 WHERE album = :album AND unavailable = 0").arg(songs_table_));
1679 if (!artist.isEmpty()) sql += " AND artist = :artist";
1680
1681 SqlQuery q(db);
1682 q.prepare(sql);
1683 q.BindValue(":compilation_on", on ? 1 : 0);
1684 q.BindValue(":compilation_off", on ? 0 : 1);
1685 q.BindValue(":album", album);
1686 if (!artist.isEmpty()) q.BindValue(":artist", artist);
1687
1688 if (!q.Exec()) {
1689 db_->ReportErrors(q);
1690 return;
1691 }
1692
1693 // Now get the updated songs
1694 if (!query.Exec()) {
1695 ReportErrors(query);
1696 return;
1697 }
1698
1699 while (query.Next()) {
1700 Song song(source_);
1701 song.InitFromQuery(query, true);
1702 added_songs << song;
1703 }
1704 }
1705
1706 if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
1707 emit SongsDeleted(deleted_songs);
1708 emit SongsDiscovered(added_songs);
1709 }
1710
1711 }
1712
IncrementPlayCount(const int id)1713 void CollectionBackend::IncrementPlayCount(const int id) {
1714
1715 if (id == -1) return;
1716
1717 QMutexLocker l(db_->Mutex());
1718 QSqlDatabase db(db_->Connect());
1719
1720 SqlQuery q(db);
1721 q.prepare(QString("UPDATE %1 SET playcount = playcount + 1, lastplayed = :now WHERE ROWID = :id").arg(songs_table_));
1722 q.BindValue(":now", QDateTime::currentDateTime().toSecsSinceEpoch());
1723 q.BindValue(":id", id);
1724 if (!q.Exec()) {
1725 db_->ReportErrors(q);
1726 return;
1727 }
1728
1729 Song new_song = GetSongById(id, db);
1730 emit SongsStatisticsChanged(SongList() << new_song);
1731
1732 }
1733
IncrementSkipCount(const int id,const float progress)1734 void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
1735
1736 Q_UNUSED(progress);
1737
1738 if (id == -1) return;
1739
1740 QMutexLocker l(db_->Mutex());
1741 QSqlDatabase db(db_->Connect());
1742
1743 SqlQuery q(db);
1744 q.prepare(QString("UPDATE %1 SET skipcount = skipcount + 1 WHERE ROWID = :id").arg(songs_table_));
1745 q.BindValue(":id", id);
1746 if (!q.Exec()) {
1747 db_->ReportErrors(q);
1748 return;
1749 }
1750
1751 Song new_song = GetSongById(id, db);
1752 emit SongsStatisticsChanged(SongList() << new_song);
1753
1754 }
1755
ResetStatistics(const int id)1756 void CollectionBackend::ResetStatistics(const int id) {
1757
1758 if (id == -1) return;
1759
1760 QMutexLocker l(db_->Mutex());
1761 QSqlDatabase db(db_->Connect());
1762
1763 SqlQuery q(db);
1764 q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID = :id").arg(songs_table_));
1765 q.BindValue(":id", id);
1766 if (!q.Exec()) {
1767 db_->ReportErrors(q);
1768 return;
1769 }
1770
1771 Song new_song = GetSongById(id, db);
1772 emit SongsStatisticsChanged(SongList() << new_song);
1773
1774 }
1775
DeleteAll()1776 void CollectionBackend::DeleteAll() {
1777
1778 {
1779 QMutexLocker l(db_->Mutex());
1780 QSqlDatabase db(db_->Connect());
1781 ScopedTransaction t(&db);
1782
1783 {
1784 SqlQuery q(db);
1785 q.prepare("DELETE FROM " + songs_table_);
1786 if (!q.Exec()) {
1787 db_->ReportErrors(q);
1788 return;
1789 }
1790 }
1791
1792 {
1793 SqlQuery q(db);
1794 q.prepare("DELETE FROM " + fts_table_);
1795 if (!q.Exec()) {
1796 db_->ReportErrors(q);
1797 return;
1798 }
1799 }
1800
1801 t.Commit();
1802 }
1803
1804 emit DatabaseReset();
1805
1806 }
1807
SmartPlaylistsFindSongs(const SmartPlaylistSearch & search)1808 SongList CollectionBackend::SmartPlaylistsFindSongs(const SmartPlaylistSearch &search) {
1809
1810 QMutexLocker l(db_->Mutex());
1811 QSqlDatabase db(db_->Connect());
1812
1813 // Build the query
1814 QString sql = search.ToSql(songs_table());
1815
1816 // Run the query
1817 SongList ret;
1818 SqlQuery query(db);
1819 query.prepare(sql);
1820 if (!query.Exec()) {
1821 db_->ReportErrors(query);
1822 return ret;
1823 }
1824
1825 // Read the results
1826 while (query.next()) {
1827 Song song;
1828 song.InitFromQuery(query, true);
1829 ret << song;
1830 }
1831 return ret;
1832
1833 }
1834
SmartPlaylistsGetAllSongs()1835 SongList CollectionBackend::SmartPlaylistsGetAllSongs() {
1836
1837 // Get all the songs!
1838 return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
1839
1840 }
1841
GetSongsBy(const QString & artist,const QString & album,const QString & title)1842 SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &album, const QString &title) {
1843
1844 QMutexLocker l(db_->Mutex());
1845 QSqlDatabase db(db_->Connect());
1846
1847 SongList songs;
1848 SqlQuery q(db);
1849 if (album.isEmpty()) {
1850 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE artist = :artist COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(songs_table_));
1851 }
1852 else {
1853 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE artist = :artist COLLATE NOCASE AND album = :album COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(songs_table_));
1854 }
1855 q.BindValue(":artist", artist);
1856 if (!album.isEmpty()) q.BindValue(":album", album);
1857 q.BindValue(":title", title);
1858 if (!q.Exec()) {
1859 db_->ReportErrors(q);
1860 return SongList();
1861 }
1862 while (q.next()) {
1863 Song song(source_);
1864 song.InitFromQuery(q, true);
1865 songs << song;
1866 }
1867
1868 return songs;
1869
1870 }
1871
UpdateLastPlayed(const QString & artist,const QString & album,const QString & title,const qint64 lastplayed)1872 void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const qint64 lastplayed) {
1873
1874 SongList songs = GetSongsBy(artist, album, title);
1875 if (songs.isEmpty()) {
1876 qLog(Debug) << "Could not find a matching song in the database for" << artist << album << title;
1877 return;
1878 }
1879
1880 QMutexLocker l(db_->Mutex());
1881 QSqlDatabase db(db_->Connect());
1882
1883 for (const Song &song : songs) {
1884 if (song.lastplayed() >= lastplayed) {
1885 continue;
1886 }
1887 SqlQuery q(db);
1888 q.prepare(QString("UPDATE %1 SET lastplayed = :lastplayed WHERE ROWID = :id").arg(songs_table_));
1889 q.BindValue(":lastplayed", lastplayed);
1890 q.BindValue(":id", song.id());
1891 if (!q.Exec()) {
1892 db_->ReportErrors(q);
1893 continue;
1894 }
1895 }
1896
1897 emit SongsStatisticsChanged(SongList() << songs);
1898
1899 }
1900
UpdatePlayCount(const QString & artist,const QString & title,const int playcount)1901 void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount) {
1902
1903 SongList songs = GetSongsBy(artist, QString(), title);
1904 if (songs.isEmpty()) {
1905 qLog(Debug) << "Could not find a matching song in the database for" << artist << title;
1906 return;
1907 }
1908
1909 QMutexLocker l(db_->Mutex());
1910 QSqlDatabase db(db_->Connect());
1911
1912 for (const Song &song : songs) {
1913 SqlQuery q(db);
1914 q.prepare(QString("UPDATE %1 SET playcount = :playcount WHERE ROWID = :id").arg(songs_table_));
1915 q.BindValue(":playcount", playcount);
1916 q.BindValue(":id", song.id());
1917 if (!q.Exec()) {
1918 db_->ReportErrors(q);
1919 return;
1920 }
1921 }
1922
1923 emit SongsStatisticsChanged(SongList() << songs);
1924
1925 }
1926
UpdateSongRating(const int id,const double rating)1927 void CollectionBackend::UpdateSongRating(const int id, const double rating) {
1928
1929 if (id == -1) return;
1930
1931 QList<int> id_list;
1932 id_list << id;
1933 UpdateSongsRating(id_list, rating);
1934
1935 }
1936
UpdateSongsRating(const QList<int> & id_list,const double rating)1937 void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const double rating) {
1938
1939 if (id_list.isEmpty()) return;
1940
1941 QMutexLocker l(db_->Mutex());
1942 QSqlDatabase db(db_->Connect());
1943
1944 QStringList id_str_list;
1945 id_str_list.reserve(id_list.count());
1946 for (int i : id_list) {
1947 id_str_list << QString::number(i);
1948 }
1949 QString ids = id_str_list.join(",");
1950 SqlQuery q(db);
1951 q.prepare(QString("UPDATE %1 SET rating = :rating WHERE ROWID IN (%2)").arg(songs_table_, ids));
1952 q.BindValue(":rating", rating);
1953 if (!q.Exec()) {
1954 db_->ReportErrors(q);
1955 return;
1956 }
1957
1958 SongList new_song_list = GetSongsById(id_str_list, db);
1959
1960 emit SongsRatingChanged(new_song_list);
1961
1962 }
1963
UpdateSongRatingAsync(const int id,const double rating)1964 void CollectionBackend::UpdateSongRatingAsync(const int id, const double rating) {
1965 QMetaObject::invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(double, rating));
1966 }
1967
UpdateSongsRatingAsync(const QList<int> & ids,const double rating)1968 void CollectionBackend::UpdateSongsRatingAsync(const QList<int> &ids, const double rating) {
1969 QMetaObject::invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList<int>, ids), Q_ARG(double, rating));
1970 }
1971
UpdateLastSeen(const int directory_id,const int expire_unavailable_songs_days)1972 void CollectionBackend::UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days) {
1973
1974 {
1975 QMutexLocker l(db_->Mutex());
1976 QSqlDatabase db(db_->Connect());
1977
1978 SqlQuery q(db);
1979 q.prepare(QString("UPDATE %1 SET lastseen = :lastseen WHERE directory_id = :directory_id AND unavailable = 0").arg(songs_table_));
1980 q.BindValue(":lastseen", QDateTime::currentDateTime().toSecsSinceEpoch());
1981 q.BindValue(":directory_id", directory_id);
1982 if (!q.Exec()) {
1983 db_->ReportErrors(q);
1984 return;
1985 }
1986 }
1987
1988 if (expire_unavailable_songs_days > 0) ExpireSongs(directory_id, expire_unavailable_songs_days);
1989
1990 }
1991
ExpireSongs(const int directory_id,const int expire_unavailable_songs_days)1992 void CollectionBackend::ExpireSongs(const int directory_id, const int expire_unavailable_songs_days) {
1993
1994 SongList songs;
1995 {
1996 QMutexLocker l(db_->Mutex());
1997 QSqlDatabase db(db_->Connect());
1998 SqlQuery q(db);
1999 q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE directory_id = :directory_id AND unavailable = 1 AND lastseen > 0 AND lastseen < :time").arg(songs_table_));
2000 q.BindValue(":directory_id", directory_id);
2001 q.BindValue(":time", QDateTime::currentDateTime().toSecsSinceEpoch() - (expire_unavailable_songs_days * 86400));
2002 if (!q.Exec()) {
2003 db_->ReportErrors(q);
2004 return;
2005 }
2006 while (q.next()) {
2007 Song song(source_);
2008 song.InitFromQuery(q, true);
2009 songs << song;
2010 }
2011 }
2012
2013 if (!songs.isEmpty()) DeleteSongs(songs);
2014
2015 }
2016
2017