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