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 "core/application.h"
19 #include "core/logging.h"
20 #include "core/player.h"
21 #include "core/songloader.h"
22 #include "core/utilities.h"
23 #include "library/librarybackend.h"
24 #include "library/libraryplaylistitem.h"
25 #include "playlistbackend.h"
26 #include "playlistcontainer.h"
27 #include "playlistmanager.h"
28 #include "playlistparsers/playlistparser.h"
29 #include "playlistsaveoptionsdialog.h"
30 #include "playlistview.h"
31 #include "queue.h"
32 #include "smartplaylists/generator.h"
33
34 #include <QFileDialog>
35 #include <QFileInfo>
36 #include <QFuture>
37 #include <QMessageBox>
38 #include <QtConcurrentRun>
39 #include <QtDebug>
40
41 using smart_playlists::GeneratorPtr;
42
PlaylistManager(Application * app,QObject * parent)43 PlaylistManager::PlaylistManager(Application* app, QObject* parent)
44 : PlaylistManagerInterface(app, parent),
45 app_(app),
46 playlist_backend_(nullptr),
47 library_backend_(nullptr),
48 sequence_(nullptr),
49 parser_(nullptr),
50 playlist_container_(nullptr),
51 current_(-1),
52 active_(-1) {
53 connect(app_->player(), SIGNAL(Paused()), SLOT(SetActivePaused()));
54 connect(app_->player(), SIGNAL(Playing()), SLOT(SetActivePlaying()));
55 connect(app_->player(), SIGNAL(Stopped()), SLOT(SetActiveStopped()));
56 }
57
~PlaylistManager()58 PlaylistManager::~PlaylistManager() {
59 for (const Data& data : playlists_.values()) {
60 delete data.p;
61 }
62 }
63
Init(LibraryBackend * library_backend,PlaylistBackend * playlist_backend,PlaylistSequence * sequence,PlaylistContainer * playlist_container)64 void PlaylistManager::Init(LibraryBackend* library_backend,
65 PlaylistBackend* playlist_backend,
66 PlaylistSequence* sequence,
67 PlaylistContainer* playlist_container) {
68 library_backend_ = library_backend;
69 playlist_backend_ = playlist_backend;
70 sequence_ = sequence;
71 parser_ = new PlaylistParser(library_backend, this);
72 playlist_container_ = playlist_container;
73
74 connect(library_backend_, SIGNAL(SongsDiscovered(SongList)),
75 SLOT(SongsDiscovered(SongList)));
76 connect(library_backend_, SIGNAL(SongsStatisticsChanged(SongList)),
77 SLOT(SongsDiscovered(SongList)));
78 connect(library_backend_, SIGNAL(SongsRatingChanged(SongList)),
79 SLOT(SongsDiscovered(SongList)));
80
81 for (const PlaylistBackend::Playlist& p :
82 playlist_backend->GetAllOpenPlaylists()) {
83 AddPlaylist(p.id, p.name, p.special_type, p.ui_path, p.favorite);
84 }
85
86 // If no playlist exists then make a new one
87 if (playlists_.isEmpty()) New(tr("Playlist"));
88
89 emit PlaylistManagerInitialized();
90 }
91
GetAllPlaylists() const92 QList<Playlist*> PlaylistManager::GetAllPlaylists() const {
93 QList<Playlist*> result;
94
95 for (const Data& data : playlists_.values()) {
96 result.append(data.p);
97 }
98
99 return result;
100 }
101
selection(int id) const102 QItemSelection PlaylistManager::selection(int id) const {
103 QMap<int, Data>::const_iterator it = playlists_.find(id);
104 return it->selection;
105 }
106
AddPlaylist(int id,const QString & name,const QString & special_type,const QString & ui_path,bool favorite)107 Playlist* PlaylistManager::AddPlaylist(int id, const QString& name,
108 const QString& special_type,
109 const QString& ui_path, bool favorite) {
110 Playlist* ret = new Playlist(playlist_backend_, app_->task_manager(),
111 library_backend_, id, special_type, favorite);
112 ret->set_sequence(sequence_);
113 ret->set_ui_path(ui_path);
114
115 connect(ret, SIGNAL(CurrentSongChanged(Song)),
116 SIGNAL(CurrentSongChanged(Song)));
117 connect(ret, SIGNAL(PlaylistChanged()), SLOT(OneOfPlaylistsChanged()));
118 connect(ret, SIGNAL(PlaylistChanged()), SLOT(UpdateSummaryText()));
119 connect(ret, SIGNAL(EditingFinished(QModelIndex)),
120 SIGNAL(EditingFinished(QModelIndex)));
121 connect(ret, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
122 connect(ret, SIGNAL(PlayRequested(QModelIndex)),
123 SIGNAL(PlayRequested(QModelIndex)));
124 connect(playlist_container_->view(),
125 SIGNAL(ColumnAlignmentChanged(ColumnAlignmentMap)), ret,
126 SLOT(SetColumnAlignment(ColumnAlignmentMap)));
127
128 playlists_[id] = Data(ret, name);
129
130 emit PlaylistAdded(id, name, favorite);
131
132 if (current_ == -1) {
133 SetCurrentPlaylist(id);
134 }
135 if (active_ == -1) {
136 SetActivePlaylist(id);
137 }
138
139 return ret;
140 }
141
New(const QString & name,const SongList & songs,const QString & special_type)142 void PlaylistManager::New(const QString& name, const SongList& songs,
143 const QString& special_type) {
144 if (name.isNull()) return;
145
146 int id = playlist_backend_->CreatePlaylist(name, special_type);
147
148 if (id == -1) qFatal("Couldn't create playlist");
149
150 Playlist* playlist = AddPlaylist(id, name, special_type, QString(), false);
151 playlist->InsertSongsOrLibraryItems(songs);
152
153 SetCurrentPlaylist(id);
154
155 // If the name is just "Playlist", append the id
156 if (name == tr("Playlist")) {
157 Rename(id, QString("%1 %2").arg(name).arg(id));
158 }
159 }
160
Load(const QString & filename)161 void PlaylistManager::Load(const QString& filename) {
162 QFileInfo info(filename);
163
164 int id = playlist_backend_->CreatePlaylist(info.baseName(), QString());
165
166 if (id == -1) {
167 emit Error(tr("Couldn't create playlist"));
168 return;
169 }
170
171 Playlist* playlist =
172 AddPlaylist(id, info.baseName(), QString(), QString(), false);
173
174 QList<QUrl> urls;
175 playlist->InsertUrls(urls << QUrl::fromLocalFile(filename));
176 }
177
Save(int id,const QString & filename,Playlist::Path path_type)178 void PlaylistManager::Save(int id, const QString& filename,
179 Playlist::Path path_type) {
180 if (playlists_.contains(id)) {
181 parser_->Save(playlist(id)->GetAllSongs(), filename, path_type);
182 } else {
183 // Playlist is not in the playlist manager: probably save action was
184 // triggered
185 // from the left side bar and the playlist isn't loaded.
186 QFuture<QList<Song>> future = QtConcurrent::run(
187 playlist_backend_, &PlaylistBackend::GetPlaylistSongs, id);
188 NewClosure(future, this, SLOT(ItemsLoadedForSavePlaylist(
189 QFuture<SongList>, QString, Playlist::Path)),
190 future, filename, path_type);
191 }
192 }
193
ItemsLoadedForSavePlaylist(QFuture<SongList> future,const QString & filename,Playlist::Path path_type)194 void PlaylistManager::ItemsLoadedForSavePlaylist(QFuture<SongList> future,
195 const QString& filename,
196 Playlist::Path path_type) {
197 parser_->Save(future.result(), filename, path_type);
198 }
199
SaveWithUI(int id,const QString & playlist_name)200 void PlaylistManager::SaveWithUI(int id, const QString& playlist_name) {
201 QSettings settings;
202 settings.beginGroup(Playlist::kSettingsGroup);
203 QString filename = settings.value("last_save_playlist").toString();
204 QString extension = settings.value("last_save_extension",
205 parser()->default_extension()).toString();
206 QString filter =
207 settings.value("last_save_filter", parser()->default_filter()).toString();
208
209 QString suggested_filename = playlist_name;
210 suggested_filename.replace(QRegExp("\\W"), "");
211
212 qLog(Debug) << "Using extension:" << extension;
213
214 // We want to use the playlist tab name (with disallowed characters removed)
215 // as a default filename, but in the same directory as the last saved file.
216
217 // Strip off filename components until we find something that's a folder
218 forever {
219 QFileInfo fileinfo(filename);
220 if (filename.isEmpty() || fileinfo.isDir()) break;
221
222 filename = filename.section('/', 0, -2);
223 }
224
225 // Use the home directory as a fallback in case the path is empty.
226 if (filename.isEmpty()) filename = QDir::homePath();
227
228 // Add the suggested filename
229 filename += "/" + suggested_filename + "." + extension;
230 qLog(Debug) << "Suggested filename:" << filename;
231
232 filename = QFileDialog::getSaveFileName(
233 nullptr, tr("Save playlist", "Title of the playlist save dialog."),
234 filename, parser()->filters(), &filter);
235
236 if (filename.isNull()) {
237 return;
238 }
239
240 // Check if the file extension is valid. Fallback to the default if not.
241 QFileInfo info(filename);
242 ParserBase* parser = parser_->ParserForExtension(info.suffix());
243 if (!parser) {
244 qLog(Warning) << "Unknown file extension:" << info.suffix();
245 filename = info.absolutePath() + "/" + info.fileName() + "." +
246 parser_->default_extension();
247 info.setFile(filename);
248 filter = info.suffix();
249 }
250
251 int p = settings.value(Playlist::kPathType, Playlist::Path_Automatic).toInt();
252 Playlist::Path path = static_cast<Playlist::Path>(p);
253 if (path == Playlist::Path_Ask_User) {
254 PlaylistSaveOptionsDialog optionsDialog(nullptr);
255 optionsDialog.setModal(true);
256 if (optionsDialog.exec() != QDialog::Accepted) {
257 return;
258 }
259 path = optionsDialog.path_type();
260 }
261
262 settings.setValue("last_save_playlist", filename);
263 settings.setValue("last_save_filter", filter);
264 settings.setValue("last_save_extension", info.suffix());
265
266 Save(id == -1 ? current_id() : id, filename, path);
267 }
268
Rename(int id,const QString & new_name)269 void PlaylistManager::Rename(int id, const QString& new_name) {
270 Q_ASSERT(playlists_.contains(id));
271
272 playlist_backend_->RenamePlaylist(id, new_name);
273 playlists_[id].name = new_name;
274
275 emit PlaylistRenamed(id, new_name);
276 }
277
Favorite(int id,bool favorite)278 void PlaylistManager::Favorite(int id, bool favorite) {
279 if (playlists_.contains(id)) {
280 // If playlists_ contains this playlist, its means it's opened: star or
281 // unstar it.
282 playlist_backend_->FavoritePlaylist(id, favorite);
283 playlists_[id].p->set_favorite(favorite);
284 } else {
285 Q_ASSERT(!favorite);
286 // Otherwise it means user wants to remove this playlist from the left
287 // panel,
288 // while it's not visible in the playlist tabbar either, because it has been
289 // closed: delete it.
290 playlist_backend_->RemovePlaylist(id);
291 }
292 emit PlaylistFavorited(id, favorite);
293 }
294
Close(int id)295 bool PlaylistManager::Close(int id) {
296 // Won't allow removing the last playlist
297 if (playlists_.count() <= 1 || !playlists_.contains(id)) return false;
298
299 int next_id = -1;
300 for (int possible_next_id : playlists_.keys()) {
301 if (possible_next_id != id) {
302 next_id = possible_next_id;
303 break;
304 }
305 }
306 if (next_id == -1) return false;
307
308 if (id == active_) SetActivePlaylist(next_id);
309 if (id == current_) SetCurrentPlaylist(next_id);
310
311 Data data = playlists_.take(id);
312 emit PlaylistClosed(id);
313
314 if (!data.p->is_favorite()) {
315 playlist_backend_->RemovePlaylist(id);
316 emit PlaylistDeleted(id);
317 }
318 delete data.p;
319
320 return true;
321 }
322
Delete(int id)323 void PlaylistManager::Delete(int id) {
324 if (!Close(id)) {
325 return;
326 }
327
328 playlist_backend_->RemovePlaylist(id);
329 emit PlaylistDeleted(id);
330 }
331
OneOfPlaylistsChanged()332 void PlaylistManager::OneOfPlaylistsChanged() {
333 emit PlaylistChanged(qobject_cast<Playlist*>(sender()));
334 }
335
SetCurrentPlaylist(int id)336 void PlaylistManager::SetCurrentPlaylist(int id) {
337 Q_ASSERT(playlists_.contains(id));
338 current_ = id;
339 emit CurrentChanged(current());
340 UpdateSummaryText();
341 }
342
SetActivePlaylist(int id)343 void PlaylistManager::SetActivePlaylist(int id) {
344 Q_ASSERT(playlists_.contains(id));
345
346 // Kinda a hack: unset the current item from the old active playlist before
347 // setting the new one
348 if (active_ != -1 && active_ != id) active()->set_current_row(-1);
349
350 active_ = id;
351 emit ActiveChanged(active());
352
353 sequence_->SetUsingDynamicPlaylist(active()->is_dynamic());
354 }
355
SetActiveToCurrent()356 void PlaylistManager::SetActiveToCurrent() {
357 // Check if we need to update the active playlist.
358 // By calling SetActiveToCurrent, the playlist manager emits the signal
359 // "ActiveChanged". This signal causes the network remote module to
360 // send all playlists to the clients, even if no change happened.
361 if (current_id() != active_id()) {
362 SetActivePlaylist(current_id());
363 }
364 }
365
ClearCurrent()366 void PlaylistManager::ClearCurrent() { current()->Clear(); }
367
ShuffleCurrent()368 void PlaylistManager::ShuffleCurrent() { current()->Shuffle(); }
369
RemoveDuplicatesCurrent()370 void PlaylistManager::RemoveDuplicatesCurrent() {
371 current()->RemoveDuplicateSongs();
372 }
373
RemoveUnavailableCurrent()374 void PlaylistManager::RemoveUnavailableCurrent() {
375 current()->RemoveUnavailableSongs();
376 }
377
SetActivePlaying()378 void PlaylistManager::SetActivePlaying() { active()->Playing(); }
379
SetActivePaused()380 void PlaylistManager::SetActivePaused() { active()->Paused(); }
381
SetActiveStopped()382 void PlaylistManager::SetActiveStopped() { active()->Stopped(); }
383
SetActiveStreamMetadata(const QUrl & url,const Song & song)384 void PlaylistManager::SetActiveStreamMetadata(const QUrl& url,
385 const Song& song) {
386 active()->SetStreamMetadata(url, song);
387 }
388
RateCurrentSong(double rating)389 void PlaylistManager::RateCurrentSong(double rating) {
390 active()->RateSong(active()->current_index(), rating);
391 }
392
RateCurrentSong(int rating)393 void PlaylistManager::RateCurrentSong(int rating) {
394 RateCurrentSong(rating / 5.0);
395 }
396
ChangePlaylistOrder(const QList<int> & ids)397 void PlaylistManager::ChangePlaylistOrder(const QList<int>& ids) {
398 playlist_backend_->SetPlaylistOrder(ids);
399 }
400
Enque(int id,int i)401 void PlaylistManager::Enque(int id, int i) {
402 QModelIndexList dummyIndexList;
403
404 Q_ASSERT(playlists_.contains(id));
405
406 dummyIndexList.append(playlist(id)->index(i, 0));
407 playlist(id)->queue()->ToggleTracks(dummyIndexList);
408 }
409
UpdateSummaryText()410 void PlaylistManager::UpdateSummaryText() {
411 int tracks = current()->rowCount();
412 quint64 nanoseconds = 0;
413 int selected = 0;
414
415 // Get the length of the selected tracks
416 for (const QItemSelectionRange& range : playlists_[current_id()].selection) {
417 if (!range.isValid()) continue;
418
419 selected += range.bottom() - range.top() + 1;
420 for (int i = range.top(); i <= range.bottom(); ++i) {
421 qint64 length =
422 range.model()->index(i, Playlist::Column_Length).data().toLongLong();
423 if (length > 0) nanoseconds += length;
424 }
425 }
426
427 QString summary;
428 if (selected > 1) {
429 summary += tr("%1 selected of").arg(selected) + " ";
430 } else {
431 nanoseconds = current()->GetTotalLength();
432 }
433
434 // TODO: Make the plurals translatable
435 summary += tracks == 1 ? tr("1 track") : tr("%1 tracks").arg(tracks);
436
437 if (nanoseconds)
438 summary += " - [ " + Utilities::WordyTimeNanosec(nanoseconds) + " ]";
439
440 emit SummaryTextChanged(summary);
441 }
442
SelectionChanged(const QItemSelection & selection)443 void PlaylistManager::SelectionChanged(const QItemSelection& selection) {
444 playlists_[current_id()].selection = selection;
445 UpdateSummaryText();
446 }
447
SongsDiscovered(const SongList & songs)448 void PlaylistManager::SongsDiscovered(const SongList& songs) {
449 // Some songs might've changed in the library, let's update any playlist
450 // items we have that match those songs
451
452 for (const Song& song : songs) {
453 for (const Data& data : playlists_) {
454 PlaylistItemList items = data.p->library_items_by_id(song.id());
455 for (PlaylistItemPtr item : items) {
456 if (item->Metadata().directory_id() != song.directory_id()) continue;
457 static_cast<LibraryPlaylistItem*>(item.get())->SetMetadata(song);
458 data.p->ItemChanged(item);
459 }
460 }
461 }
462 }
463
PlaySmartPlaylist(GeneratorPtr generator,bool as_new,bool clear)464 void PlaylistManager::PlaySmartPlaylist(GeneratorPtr generator, bool as_new,
465 bool clear) {
466 if (as_new) {
467 New(generator->name());
468 }
469
470 if (clear) {
471 current()->Clear();
472 }
473
474 current()->InsertSmartPlaylist(generator);
475 }
476
477 // When Player has processed the new song chosen by the user...
SongChangeRequestProcessed(const QUrl & url,bool valid)478 void PlaylistManager::SongChangeRequestProcessed(const QUrl& url, bool valid) {
479 for (Playlist* playlist : GetAllPlaylists()) {
480 if (playlist->ApplyValidityOnCurrentSong(url, valid)) {
481 return;
482 }
483 }
484 }
485
InsertUrls(int id,const QList<QUrl> & urls,int pos,bool play_now,bool enqueue)486 void PlaylistManager::InsertUrls(int id, const QList<QUrl>& urls, int pos,
487 bool play_now, bool enqueue) {
488 Q_ASSERT(playlists_.contains(id));
489
490 playlists_[id].p->InsertUrls(urls, pos, play_now, enqueue);
491 }
492
InsertSongs(int id,const SongList & songs,int pos,bool play_now,bool enqueue)493 void PlaylistManager::InsertSongs(int id, const SongList& songs, int pos,
494 bool play_now, bool enqueue) {
495 Q_ASSERT(playlists_.contains(id));
496
497 playlists_[id].p->InsertSongs(songs, pos, play_now, enqueue);
498 }
499
RemoveItemsWithoutUndo(int id,const QList<int> & indices)500 void PlaylistManager::RemoveItemsWithoutUndo(int id,
501 const QList<int>& indices) {
502 Q_ASSERT(playlists_.contains(id));
503
504 playlists_[id].p->RemoveItemsWithoutUndo(indices);
505 }
506
RemoveCurrentSong()507 void PlaylistManager::RemoveCurrentSong() {
508 active()->removeRows(active()->current_index().row(), 1);
509 }
510
InvalidateDeletedSongs()511 void PlaylistManager::InvalidateDeletedSongs() {
512 for (Playlist* playlist : GetAllPlaylists()) {
513 playlist->InvalidateDeletedSongs();
514 }
515 }
516
RemoveDeletedSongs()517 void PlaylistManager::RemoveDeletedSongs() {
518 for (Playlist* playlist : GetAllPlaylists()) {
519 playlist->RemoveDeletedSongs();
520 }
521 }
522
GetNameForNewPlaylist(const SongList & songs)523 QString PlaylistManager::GetNameForNewPlaylist(const SongList& songs) {
524 if (songs.isEmpty()) {
525 return tr("Playlist");
526 }
527
528 QSet<QString> artists;
529 QSet<QString> albums;
530
531 for (const Song& song : songs) {
532 artists << (song.artist().isEmpty() ? tr("Unknown") : song.artist());
533 albums << (song.album().isEmpty() ? tr("Unknown") : song.album());
534
535 if (artists.size() > 1) {
536 break;
537 }
538 }
539
540 bool various_artists = artists.size() > 1;
541
542 QString result;
543 if (various_artists) {
544 result = tr("Various artists");
545 } else {
546 result = artists.values().first();
547 }
548
549 if (!various_artists && albums.size() == 1) {
550 result += " - " + albums.toList().first();
551 }
552
553 return result;
554 }
555
Open(int id)556 void PlaylistManager::Open(int id) {
557 if (playlists_.contains(id)) {
558 return;
559 }
560
561 const PlaylistBackend::Playlist& p = playlist_backend_->GetPlaylist(id);
562 if (p.id != id) {
563 return;
564 }
565
566 AddPlaylist(p.id, p.name, p.special_type, p.ui_path, p.favorite);
567 }
568
SetCurrentOrOpen(int id)569 void PlaylistManager::SetCurrentOrOpen(int id) {
570 Open(id);
571 SetCurrentPlaylist(id);
572 }
573
IsPlaylistOpen(int id)574 bool PlaylistManager::IsPlaylistOpen(int id) { return playlists_.contains(id); }
575