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