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