1 /* This file is part of Clementine.
2    Copyright 2010, David Sansome <me@davidsansome.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 #include "playlistbackend.h"
19 
20 #include <memory>
21 #include <functional>
22 
23 #include <QFile>
24 #include <QHash>
25 #include <QMutexLocker>
26 #include <QSqlQuery>
27 #include <QtDebug>
28 
29 #include "core/application.h"
30 #include "core/database.h"
31 #include "core/logging.h"
32 #include "core/scopedtransaction.h"
33 #include "core/song.h"
34 #include "library/librarybackend.h"
35 #include "library/sqlrow.h"
36 #include "playlist/songplaylistitem.h"
37 #include "playlistparsers/cueparser.h"
38 #include "smartplaylists/generator.h"
39 
40 using std::placeholders::_1;
41 using std::shared_ptr;
42 
43 using smart_playlists::GeneratorPtr;
44 
45 const int PlaylistBackend::kSongTableJoins = 4;
46 
PlaylistBackend(Application * app,QObject * parent)47 PlaylistBackend::PlaylistBackend(Application* app, QObject* parent)
48     : QObject(parent), app_(app), db_(app_->database()) {}
49 
GetAllPlaylists()50 PlaylistBackend::PlaylistList PlaylistBackend::GetAllPlaylists() {
51   return GetPlaylists(GetPlaylists_All);
52 }
53 
GetAllOpenPlaylists()54 PlaylistBackend::PlaylistList PlaylistBackend::GetAllOpenPlaylists() {
55   return GetPlaylists(GetPlaylists_OpenInUi);
56 }
57 
GetAllFavoritePlaylists()58 PlaylistBackend::PlaylistList PlaylistBackend::GetAllFavoritePlaylists() {
59   return GetPlaylists(GetPlaylists_Favorite);
60 }
61 
GetPlaylists(GetPlaylistsFlags flags)62 PlaylistBackend::PlaylistList PlaylistBackend::GetPlaylists(
63     GetPlaylistsFlags flags) {
64   QMutexLocker l(db_->Mutex());
65   QSqlDatabase db(db_->Connect());
66 
67   PlaylistList ret;
68 
69   QStringList condition_list;
70   if (flags & GetPlaylists_OpenInUi) {
71     condition_list << "ui_order != -1";
72   }
73   if (flags & GetPlaylists_Favorite) {
74     condition_list << "is_favorite != 0";
75   }
76   QString condition;
77   if (!condition_list.isEmpty()) {
78     condition = " WHERE " + condition_list.join(" OR ");
79   }
80 
81   QSqlQuery q(db);
82   q.prepare("SELECT ROWID, name, last_played, dynamic_playlist_type,"
83       "       dynamic_playlist_data, dynamic_playlist_backend,"
84       "       special_type, ui_path, is_favorite"
85       " FROM playlists"
86       " " +
87           condition + " ORDER BY ui_order");
88   q.exec();
89   if (db_->CheckErrors(q)) return ret;
90 
91   while (q.next()) {
92     Playlist p;
93     p.id = q.value(0).toInt();
94     p.name = q.value(1).toString();
95     p.last_played = q.value(2).toInt();
96     p.dynamic_type = q.value(3).toString();
97     p.dynamic_data = q.value(4).toByteArray();
98     p.dynamic_backend = q.value(5).toString();
99     p.special_type = q.value(6).toString();
100     p.ui_path = q.value(7).toString();
101     p.favorite = q.value(8).toBool();
102     ret << p;
103   }
104 
105   return ret;
106 }
107 
GetPlaylist(int id)108 PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(int id) {
109   QMutexLocker l(db_->Mutex());
110   QSqlDatabase db(db_->Connect());
111 
112   QSqlQuery q(db);
113   q.prepare("SELECT ROWID, name, last_played, dynamic_playlist_type,"
114       "       dynamic_playlist_data, dynamic_playlist_backend,"
115       "       special_type, ui_path, is_favorite"
116       " FROM playlists"
117       " WHERE ROWID=:id");
118   q.bindValue(":id", id);
119   q.exec();
120   if (db_->CheckErrors(q)) return Playlist();
121 
122   q.next();
123 
124   Playlist p;
125   p.id = q.value(0).toInt();
126   p.name = q.value(1).toString();
127   p.last_played = q.value(2).toInt();
128   p.dynamic_type = q.value(3).toString();
129   p.dynamic_data = q.value(4).toByteArray();
130   p.dynamic_backend = q.value(5).toString();
131   p.special_type = q.value(6).toString();
132   p.ui_path = q.value(7).toString();
133   p.favorite = q.value(8).toBool();
134 
135   return p;
136 }
137 
GetPlaylistRows(int playlist)138 QSqlQuery PlaylistBackend::GetPlaylistRows(int playlist) {
139   QMutexLocker l(db_->Mutex());
140   QSqlDatabase db(db_->Connect());
141 
142   QString query = "SELECT songs.ROWID, " + Song::JoinSpec("songs") +
143                   ","
144                   "       magnatune_songs.ROWID, " +
145                   Song::JoinSpec("magnatune_songs") +
146                   ","
147                   "       jamendo_songs.ROWID, " +
148                   Song::JoinSpec("jamendo_songs") +
149                   ","
150                   "       p.ROWID, " +
151                   Song::JoinSpec("p") +
152                   ","
153                   "       p.type, p.radio_service"
154                   " FROM playlist_items AS p"
155                   " LEFT JOIN songs"
156                   "    ON p.library_id = songs.ROWID"
157                   " LEFT JOIN magnatune_songs"
158                   "    ON p.library_id = magnatune_songs.ROWID"
159                   " LEFT JOIN jamendo.songs AS jamendo_songs"
160                   "    ON p.library_id = jamendo_songs.ROWID"
161                   " WHERE p.playlist = :playlist";
162   QSqlQuery q(db);
163   // Forward iterations only may be faster
164   q.setForwardOnly(true);
165   q.prepare(query);
166   q.bindValue(":playlist", playlist);
167   q.exec();
168 
169   return q;
170 }
171 
GetPlaylistItems(int playlist)172 QList<PlaylistItemPtr> PlaylistBackend::GetPlaylistItems(int playlist) {
173   QSqlQuery q = GetPlaylistRows(playlist);
174   // Note that as this only accesses the query, not the db, we don't need the
175   // mutex.
176   if (db_->CheckErrors(q)) return QList<PlaylistItemPtr>();
177 
178   // it's probable that we'll have a few songs associated with the
179   // same CUE so we're caching results of parsing CUEs
180   std::shared_ptr<NewSongFromQueryState> state_ptr(new NewSongFromQueryState());
181   QList<PlaylistItemPtr> playlistitems;
182   while (q.next()) {
183     playlistitems << NewPlaylistItemFromQuery(SqlRow(q), state_ptr);
184   }
185   return playlistitems;
186 }
187 
GetPlaylistSongs(int playlist)188 QList<Song> PlaylistBackend::GetPlaylistSongs(int playlist) {
189   QSqlQuery q = GetPlaylistRows(playlist);
190   // Note that as this only accesses the query, not the db, we don't need the
191   // mutex.
192   if (db_->CheckErrors(q)) return QList<Song>();
193 
194   // it's probable that we'll have a few songs associated with the
195   // same CUE so we're caching results of parsing CUEs
196   std::shared_ptr<NewSongFromQueryState> state_ptr(new NewSongFromQueryState());
197   QList<Song> songs;
198   while (q.next()) {
199     songs << NewSongFromQuery(SqlRow(q), state_ptr);
200   }
201   return songs;
202 }
203 
NewPlaylistItemFromQuery(const SqlRow & row,std::shared_ptr<NewSongFromQueryState> state)204 PlaylistItemPtr PlaylistBackend::NewPlaylistItemFromQuery(
205     const SqlRow& row, std::shared_ptr<NewSongFromQueryState> state) {
206   // The song tables get joined first, plus one each for the song ROWIDs
207   const int playlist_row = (Song::kColumns.count() + 1) * kSongTableJoins;
208 
209   PlaylistItemPtr item(
210       PlaylistItem::NewFromType(row.value(playlist_row).toString()));
211   if (item) {
212     item->InitFromQuery(row);
213     return RestoreCueData(item, state);
214   } else {
215     return item;
216   }
217 }
218 
NewSongFromQuery(const SqlRow & row,std::shared_ptr<NewSongFromQueryState> state)219 Song PlaylistBackend::NewSongFromQuery(
220     const SqlRow& row, std::shared_ptr<NewSongFromQueryState> state) {
221   return NewPlaylistItemFromQuery(row, state)->Metadata();
222 }
223 
224 // If song had a CUE and the CUE still exists, the metadata from it will
225 // be applied here.
RestoreCueData(PlaylistItemPtr item,std::shared_ptr<NewSongFromQueryState> state)226 PlaylistItemPtr PlaylistBackend::RestoreCueData(
227     PlaylistItemPtr item, std::shared_ptr<NewSongFromQueryState> state) {
228   // we need library to run a CueParser; also, this method applies only to
229   // file-type PlaylistItems
230   if (item->type() != "File") {
231     return item;
232   }
233   CueParser cue_parser(app_->library_backend());
234 
235   Song song = item->Metadata();
236   // we're only interested in .cue songs here
237   if (!song.has_cue()) {
238     return item;
239   }
240 
241   QString cue_path = song.cue_path();
242   // if .cue was deleted - reload the song
243   if (!QFile::exists(cue_path)) {
244     item->Reload();
245     return item;
246   }
247 
248   SongList song_list;
249   {
250     QMutexLocker locker(&state->mutex_);
251 
252     if (!state->cached_cues_.contains(cue_path)) {
253       QFile cue(cue_path);
254       cue.open(QIODevice::ReadOnly);
255 
256       song_list =
257           cue_parser.Load(&cue, cue_path, QDir(cue_path.section('/', 0, -2)));
258       state->cached_cues_[cue_path] = song_list;
259     } else {
260       song_list = state->cached_cues_[cue_path];
261     }
262   }
263 
264   for (const Song& from_list : song_list) {
265     if (from_list.url().toEncoded() == song.url().toEncoded() &&
266         from_list.beginning_nanosec() == song.beginning_nanosec()) {
267       // we found a matching section; replace the input
268       // item with a new one containing CUE metadata
269       return PlaylistItemPtr(new SongPlaylistItem(from_list));
270     }
271   }
272 
273   // there's no such section in the related .cue -> reload the song
274   item->Reload();
275   return item;
276 }
277 
SavePlaylistAsync(int playlist,const PlaylistItemList & items,int last_played,GeneratorPtr dynamic)278 void PlaylistBackend::SavePlaylistAsync(int playlist,
279                                         const PlaylistItemList& items,
280                                         int last_played, GeneratorPtr dynamic) {
281   metaObject()->invokeMethod(
282       this, "SavePlaylist", Qt::QueuedConnection, Q_ARG(int, playlist),
283       Q_ARG(PlaylistItemList, items), Q_ARG(int, last_played),
284       Q_ARG(smart_playlists::GeneratorPtr, dynamic));
285 }
286 
SavePlaylist(int playlist,const PlaylistItemList & items,int last_played,GeneratorPtr dynamic)287 void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList& items,
288                                    int last_played, GeneratorPtr dynamic) {
289   QMutexLocker l(db_->Mutex());
290   QSqlDatabase db(db_->Connect());
291 
292   qLog(Debug) << "Saving playlist" << playlist;
293 
294   QSqlQuery clear(db);
295   clear.prepare("DELETE FROM playlist_items WHERE playlist = :playlist");
296   QSqlQuery insert(db);
297   insert.prepare("INSERT INTO playlist_items"
298       " (playlist, type, library_id, radio_service, " +
299           Song::kColumnSpec +
300           ")"
301           " VALUES (:playlist, :type, :library_id, :radio_service, " +
302           Song::kBindSpec + ")");
303   QSqlQuery update(db);
304   update.prepare("UPDATE playlists SET "
305       "   last_played=:last_played,"
306       "   dynamic_playlist_type=:dynamic_type,"
307       "   dynamic_playlist_data=:dynamic_data,"
308       "   dynamic_playlist_backend=:dynamic_backend"
309       " WHERE ROWID=:playlist");
310 
311   ScopedTransaction transaction(&db);
312 
313   // Clear the existing items in the playlist
314   clear.bindValue(":playlist", playlist);
315   clear.exec();
316   if (db_->CheckErrors(clear)) return;
317 
318   // Save the new ones
319   for (PlaylistItemPtr item : items) {
320     insert.bindValue(":playlist", playlist);
321     item->BindToQuery(&insert);
322 
323     insert.exec();
324     db_->CheckErrors(insert);
325   }
326 
327   // Update the last played track number
328   update.bindValue(":last_played", last_played);
329   if (dynamic) {
330     update.bindValue(":dynamic_type", dynamic->type());
331     update.bindValue(":dynamic_data", dynamic->Save());
332     update.bindValue(":dynamic_backend", dynamic->library()->songs_table());
333   } else {
334     update.bindValue(":dynamic_type", QString());
335     update.bindValue(":dynamic_data", QByteArray());
336     update.bindValue(":dynamic_backend", QString());
337   }
338   update.bindValue(":playlist", playlist);
339   update.exec();
340   if (db_->CheckErrors(update)) return;
341 
342   transaction.Commit();
343 }
344 
CreatePlaylist(const QString & name,const QString & special_type)345 int PlaylistBackend::CreatePlaylist(const QString& name,
346                                     const QString& special_type) {
347   QMutexLocker l(db_->Mutex());
348   QSqlDatabase db(db_->Connect());
349 
350   QSqlQuery q(db);
351   q.prepare(
352       "INSERT INTO playlists (name, special_type)"
353       " VALUES (:name, :special_type)");
354   q.bindValue(":name", name);
355   q.bindValue(":special_type", special_type);
356   q.exec();
357   if (db_->CheckErrors(q)) return -1;
358 
359   return q.lastInsertId().toInt();
360 }
361 
RemovePlaylist(int id)362 void PlaylistBackend::RemovePlaylist(int id) {
363   QMutexLocker l(db_->Mutex());
364   QSqlDatabase db(db_->Connect());
365   QSqlQuery delete_playlist(db);
366   delete_playlist.prepare("DELETE FROM playlists WHERE ROWID=:id");
367   QSqlQuery delete_items(db);
368   delete_items.prepare("DELETE FROM playlist_items WHERE playlist=:id");
369 
370   delete_playlist.bindValue(":id", id);
371   delete_items.bindValue(":id", id);
372 
373   ScopedTransaction transaction(&db);
374 
375   delete_playlist.exec();
376   if (db_->CheckErrors(delete_playlist)) return;
377 
378   delete_items.exec();
379   if (db_->CheckErrors(delete_items)) return;
380 
381   transaction.Commit();
382 }
383 
RenamePlaylist(int id,const QString & new_name)384 void PlaylistBackend::RenamePlaylist(int id, const QString& new_name) {
385   QMutexLocker l(db_->Mutex());
386   QSqlDatabase db(db_->Connect());
387   QSqlQuery q(db);
388   q.prepare("UPDATE playlists SET name=:name WHERE ROWID=:id");
389   q.bindValue(":name", new_name);
390   q.bindValue(":id", id);
391 
392   q.exec();
393   db_->CheckErrors(q);
394 }
395 
FavoritePlaylist(int id,bool is_favorite)396 void PlaylistBackend::FavoritePlaylist(int id, bool is_favorite) {
397   QMutexLocker l(db_->Mutex());
398   QSqlDatabase db(db_->Connect());
399   QSqlQuery q(db);
400   q.prepare("UPDATE playlists SET is_favorite=:is_favorite WHERE ROWID=:id");
401   q.bindValue(":is_favorite", is_favorite ? 1 : 0);
402   q.bindValue(":id", id);
403 
404   q.exec();
405   db_->CheckErrors(q);
406 }
407 
SetPlaylistOrder(const QList<int> & ids)408 void PlaylistBackend::SetPlaylistOrder(const QList<int>& ids) {
409   QMutexLocker l(db_->Mutex());
410   QSqlDatabase db(db_->Connect());
411   ScopedTransaction transaction(&db);
412 
413   QSqlQuery q(db);
414   q.prepare("UPDATE playlists SET ui_order=-1");
415   q.exec();
416   if (db_->CheckErrors(q)) return;
417 
418   q.prepare("UPDATE playlists SET ui_order=:index WHERE ROWID=:id");
419   for (int i = 0; i < ids.count(); ++i) {
420     q.bindValue(":index", i);
421     q.bindValue(":id", ids[i]);
422     q.exec();
423     if (db_->CheckErrors(q)) return;
424   }
425 
426   transaction.Commit();
427 }
428 
SetPlaylistUiPath(int id,const QString & path)429 void PlaylistBackend::SetPlaylistUiPath(int id, const QString& path) {
430   QMutexLocker l(db_->Mutex());
431   QSqlDatabase db(db_->Connect());
432   QSqlQuery q(db);
433   q.prepare("UPDATE playlists SET ui_path=:path WHERE ROWID=:id");
434 
435   ScopedTransaction transaction(&db);
436 
437   q.bindValue(":path", path);
438   q.bindValue(":id", id);
439   q.exec();
440   if (db_->CheckErrors(q)) return;
441 
442   transaction.Commit();
443 }
444