1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 * ----
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "librarypage.h"
25 #include "mpd-interface/mpdconnection.h"
26 #include "mpd-interface/mpdstats.h"
27 #include "covers.h"
28 #include "settings.h"
29 #include "stdactions.h"
30 #include "customactions.h"
31 #include "support/messagebox.h"
32 #include "support/actioncollection.h"
33 #include "models/mpdlibrarymodel.h"
34 #include "widgets/menubutton.h"
35 #include "widgets/genrecombo.h"
36 #include <QRandomGenerator>
37
LibraryPage(QWidget * p)38 LibraryPage::LibraryPage(QWidget *p)
39 : SinglePageWidget(p)
40 {
41 genreCombo=new GenreCombo(this);
42 connect(StdActions::self()->addRandomAlbumToPlayQueueAction, SIGNAL(triggered()), SLOT(addRandomAlbum()));
43 connect(MPDConnection::self(), SIGNAL(updatingLibrary(time_t)), view, SLOT(updating()));
44 connect(MPDConnection::self(), SIGNAL(updatedLibrary()), view, SLOT(updated()));
45 connect(MPDConnection::self(), SIGNAL(updatingDatabase()), view, SLOT(updating()));
46 connect(MPDConnection::self(), SIGNAL(updatedDatabase()), view, SLOT(updated()));
47 connect(view, SIGNAL(itemsSelected(bool)), this, SLOT(controlActions()));
48 connect(view, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(itemDoubleClicked(const QModelIndex &)));
49 view->setModel(MpdLibraryModel::self());
50 connect(MpdLibraryModel::self(), SIGNAL(modelReset()), this, SLOT(modelReset()));
51
52 view->allowCategorized();
53 // Settings...
54 Configuration config(metaObject()->className());
55 view->setMode(ItemView::Mode_DetailedTree);
56 MpdLibraryModel::self()->load(config);
57
58 config.beginGroup(SqlLibraryModel::groupingStr(MpdLibraryModel::self()->topLevel()));
59 view->load(config);
60 view->setSearchToolTip(tr("<p>Enter a string to search artist, album, title, etc. To filter based on year, add <i>#year-range</i> to search string - e.g.</p><ul>"
61 "<li><b><i>#2000</i></b> return tracks from 2000</li>"
62 "<li><b><i>#1980-1989</i></b> return tracks from the 80's</li>"
63 "<li><b><i>Blah #2000</i></b> to search for string <i>Blah</i> and only return tracks from 2000</li>"
64 "</ul></p>"));
65
66 showArtistImagesAction=new QAction(tr("Show Artist Images"), this);
67 showArtistImagesAction->setCheckable(true);
68 libraryAlbumSortAction=createMenuGroup(tr("Sort Albums"), QList<MenuItem>() << MenuItem(tr("Name"), LibraryDb::AS_AlArYr)
69 << MenuItem(tr("Year"), LibraryDb::AS_YrAlAr),
70 MpdLibraryModel::self()->libraryAlbumSort(), this, SLOT(libraryAlbumSortChanged()));
71 albumAlbumSortAction=createMenuGroup(tr("Sort Albums"), QList<MenuItem>() << MenuItem(tr("Album, Artist, Year"), LibraryDb::AS_AlArYr)
72 << MenuItem(tr("Album, Year, Artist"), LibraryDb::AS_AlYrAr)
73 << MenuItem(tr("Artist, Album, Year"), LibraryDb::AS_ArAlYr)
74 << MenuItem(tr("Artist, Year, Album"), LibraryDb::AS_ArYrAl)
75 << MenuItem(tr("Year, Album, Artist"), LibraryDb::AS_YrAlAr)
76 << MenuItem(tr("Year, Artist, Album"), LibraryDb::AS_YrArAl)
77 << MenuItem(tr("Modified Date"), LibraryDb::AS_Modified),
78 MpdLibraryModel::self()->albumAlbumSort(), this, SLOT(albumAlbumSortChanged()));
79
80 MenuButton *menu=new MenuButton(this);
81 viewAction=createViewMenu(QList<ItemView::Mode>() << ItemView::Mode_BasicTree << ItemView::Mode_SimpleTree
82 << ItemView::Mode_DetailedTree << ItemView::Mode_List
83 << ItemView::Mode_IconTop << ItemView::Mode_Categorized);
84 menu->addAction(viewAction);
85
86 menu->addAction(createMenuGroup(tr("Group By"), QList<MenuItem>() << MenuItem(tr("Genre"), SqlLibraryModel::T_Genre)
87 << MenuItem(tr("Artist"), SqlLibraryModel::T_Artist)
88 << MenuItem(tr("Album"), SqlLibraryModel::T_Album),
89 MpdLibraryModel::self()->topLevel(), this, SLOT(groupByChanged())));
90 genreCombo->setVisible(SqlLibraryModel::T_Genre!=MpdLibraryModel::self()->topLevel());
91
92 menu->addAction(libraryAlbumSortAction);
93 menu->addAction(albumAlbumSortAction);
94 showArtistImagesAction->setChecked(MpdLibraryModel::self()->useArtistImages());
95 menu->addAction(showArtistImagesAction);
96 connect(showArtistImagesAction, SIGNAL(toggled(bool)), this, SLOT(showArtistImagesChanged(bool)));
97 showArtistImagesAction->setVisible(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel() && ItemView::Mode_IconTop!=view->viewMode());
98 albumAlbumSortAction->setVisible(SqlLibraryModel::T_Album==MpdLibraryModel::self()->topLevel());
99 libraryAlbumSortAction->setVisible(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel());
100 genreCombo->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
101 init(ReplacePlayQueue|AppendToPlayQueue, QList<QWidget *>() << menu << genreCombo);
102 connect(genreCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(doSearch()));
103 view->addAction(StdActions::self()->addToStoredPlaylistAction);
104 view->addAction(CustomActions::self());
105 #ifdef TAGLIB_FOUND
106 #ifdef ENABLE_DEVICES_SUPPORT
107 view->addAction(StdActions::self()->copyToDeviceAction);
108 #endif
109 view->addAction(StdActions::self()->organiseFilesAction);
110 view->addAction(StdActions::self()->editTagsAction);
111 #ifdef ENABLE_REPLAYGAIN_SUPPORT
112 view->addAction(StdActions::self()->replaygainAction);
113 #endif
114 view->addAction(StdActions::self()->setCoverAction);
115 #ifdef ENABLE_DEVICES_SUPPORT
116 view->addSeparator();
117 view->addAction(StdActions::self()->deleteSongsAction);
118 #endif
119 #endif // TAGLIB_FOUND
120 connect(view, SIGNAL(updateToPlayQueue(QModelIndex,bool)), this, SLOT(updateToPlayQueue(QModelIndex,bool)));
121 view->setOpenAfterSearch(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel());
122 view->setInfoText(tr("No music? Looks like your MPD is not configured correctly."));
123
124 for (QAction *act: viewAction->menu()->actions()) {
125 if (ItemView::Mode_Categorized==act->property(constValProp).toInt()) {
126 act->setVisible(SqlLibraryModel::T_Album==MpdLibraryModel::self()->topLevel());
127 break;
128 }
129 }
130 }
131
~LibraryPage()132 LibraryPage::~LibraryPage()
133 {
134 Configuration config(metaObject()->className());
135 MpdLibraryModel::self()->save(config);
136 config.beginGroup(SqlLibraryModel::groupingStr(MpdLibraryModel::self()->topLevel()));
137 view->save(config);
138 }
139
clear()140 void LibraryPage::clear()
141 {
142 MpdLibraryModel::self()->clear();
143 view->goToTop();
144 }
145
nameKey(const QString & artist,const QString & album)146 static inline QString nameKey(const QString &artist, const QString &album)
147 {
148 return '{'+artist+"}{"+album+'}';
149 }
150
selectedFiles(bool allowPlaylists) const151 QStringList LibraryPage::selectedFiles(bool allowPlaylists) const
152 {
153 QModelIndexList selected = view->selectedIndexes();
154 if (selected.isEmpty()) {
155 return QStringList();
156 }
157 return MpdLibraryModel::self()->filenames(selected, allowPlaylists);
158 }
159
selectedSongs(bool allowPlaylists) const160 QList<Song> LibraryPage::selectedSongs(bool allowPlaylists) const
161 {
162 QModelIndexList selected = view->selectedIndexes();
163 if (selected.isEmpty()) {
164 return QList<Song>();
165 }
166 return MpdLibraryModel::self()->songs(selected, allowPlaylists);
167 }
168
coverRequest() const169 Song LibraryPage::coverRequest() const
170 {
171 QModelIndexList selected = view->selectedIndexes(false); // Dont need sorted selection here...
172
173 if (1==selected.count()) {
174 QList<Song> songs=MpdLibraryModel::self()->songs(QModelIndexList() << selected.first(), false);
175 if (!songs.isEmpty()) {
176 Song s=songs.at(0);
177
178 if (SqlLibraryModel::T_Artist==static_cast<SqlLibraryModel::Item *>(selected.first().internalPointer())->getType()) {
179 if (s.useComposer()) {
180 s.setComposerImageRequest();
181 } else {
182 s.setArtistImageRequest();
183 }
184 }
185 return s;
186 }
187 }
188 return Song();
189 }
190
191 #ifdef ENABLE_DEVICES_SUPPORT
addSelectionToDevice(const QString & udi)192 void LibraryPage::addSelectionToDevice(const QString &udi)
193 {
194 QList<Song> songs=selectedSongs();
195
196 if (!songs.isEmpty()) {
197 emit addToDevice(QString(), udi, songs);
198 view->clearSelection();
199 }
200 }
201
deleteSongs()202 void LibraryPage::deleteSongs()
203 {
204 QList<Song> songs=selectedSongs();
205
206 if (!songs.isEmpty()) {
207 if (MessageBox::Yes==MessageBox::warningYesNo(this, tr("Are you sure you wish to delete the selected songs?\n\nThis cannot be undone."),
208 tr("Delete Songs"), StdGuiItem::del(), StdGuiItem::cancel())) {
209 emit deleteSongs(QString(), songs);
210 }
211 view->clearSelection();
212 }
213 }
214 #endif
215
showSongs(const QList<Song> & songs)216 void LibraryPage::showSongs(const QList<Song> &songs)
217 {
218 // Filter out non-mpd file songs...
219 QList<Song> sngs;
220 for (const Song &s: songs) {
221 if (!s.file.isEmpty() && !s.hasProtocolOrIsAbsolute()) {
222 sngs.append(s);
223 }
224 }
225
226 if (sngs.isEmpty()) {
227 return;
228 }
229
230 view->clearSearchText();
231
232 bool first=true;
233 for (const Song &s: sngs) {
234 QModelIndex idx=MpdLibraryModel::self()->findSongIndex(s);
235 if (idx.isValid()) {
236 if (ItemView::Mode_SimpleTree==view->viewMode() || ItemView::Mode_DetailedTree==view->viewMode() || first) {
237 view->showIndex(idx, first);
238 }
239 if (first) {
240 first=false;
241 }
242 if (ItemView::Mode_SimpleTree!=view->viewMode() && ItemView::Mode_DetailedTree!=view->viewMode()) {
243 return;
244 }
245 }
246 }
247 }
248
showArtist(const QString & artist)249 void LibraryPage::showArtist(const QString &artist)
250 {
251 view->clearSearchText();
252 QModelIndex idx=MpdLibraryModel::self()->findArtistIndex(artist);
253 if (idx.isValid()) {
254 view->showIndex(idx, true);
255 if (ItemView::Mode_SimpleTree==view->viewMode() || ItemView::Mode_DetailedTree==view->viewMode()) {
256 while (idx.isValid()) {
257 view->setExpanded(idx);
258 idx=idx.parent();
259 }
260 }
261 }
262 }
263
showAlbum(const QString & artist,const QString & album)264 void LibraryPage::showAlbum(const QString &artist, const QString &album)
265 {
266 view->clearSearchText();
267 QModelIndex idx=MpdLibraryModel::self()->findAlbumIndex(artist, album);
268 if (idx.isValid()) {
269 view->showIndex(idx, true);
270 if (ItemView::Mode_SimpleTree==view->viewMode() || ItemView::Mode_DetailedTree==view->viewMode()) {
271 while (idx.isValid()) {
272 view->setExpanded(idx);
273 idx=idx.parent();
274 }
275 }
276 }
277 }
278
itemDoubleClicked(const QModelIndex &)279 void LibraryPage::itemDoubleClicked(const QModelIndex &)
280 {
281 const QModelIndexList selected = view->selectedIndexes(false); // Dont need sorted selection here...
282 if (1!=selected.size()) {
283 return; //doubleclick should only have one selected item
284 }
285 SqlLibraryModel::Item *item = static_cast<SqlLibraryModel::Item *>(selected.at(0).internalPointer());
286 if (SqlLibraryModel::T_Track==item->getType()) {
287 addSelectionToPlaylist();
288 }
289 }
290
setView(int v)291 void LibraryPage::setView(int v)
292 {
293 SinglePageWidget::setView(v);
294 showArtistImagesAction->setVisible(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel() && ItemView::Mode_IconTop!=view->viewMode());
295 }
296
modelReset()297 void LibraryPage::modelReset()
298 {
299 genreCombo->update(MpdLibraryModel::self()->getGenres());
300 int count = MpdLibraryModel::self()->trackCount();
301 view->setMinSearchDebounce(count <= 12500 ? 1000u : count <= 18000 ? 1500u : 2000u);
302 }
303
groupByChanged()304 void LibraryPage::groupByChanged()
305 {
306 QAction *act=qobject_cast<QAction *>(sender());
307 if (!act) {
308 return;
309 }
310 int mode=act->property(constValProp).toInt();
311
312 Configuration config(metaObject()->className());
313 config.beginGroup(SqlLibraryModel::groupingStr(MpdLibraryModel::self()->topLevel()));
314 view->save(config);
315
316 MpdLibraryModel::self()->setTopLevel((SqlLibraryModel::Type)mode);
317 albumAlbumSortAction->setVisible(SqlLibraryModel::T_Album==MpdLibraryModel::self()->topLevel());
318 libraryAlbumSortAction->setVisible(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel());
319 showArtistImagesAction->setVisible(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel() && ItemView::Mode_IconTop!=view->viewMode());
320 genreCombo->setVisible(SqlLibraryModel::T_Genre!=MpdLibraryModel::self()->topLevel());
321
322 config.endGroup();
323 config.beginGroup(SqlLibraryModel::groupingStr(MpdLibraryModel::self()->topLevel()));
324 if (!config.hasEntry(ItemView::constViewModeKey)) {
325 view->setMode(SqlLibraryModel::T_Album==mode ? ItemView::Mode_IconTop : ItemView::Mode_DetailedTree);
326 }
327 view->load(config);
328 for (QAction *act: viewAction->menu()->actions()) {
329 int viewMode = act->property(constValProp).toInt();
330 if (viewMode==view->viewMode()) {
331 act->setChecked(true);
332 }
333 if (ItemView::Mode_Categorized==viewMode) {
334 act->setVisible(SqlLibraryModel::T_Album==MpdLibraryModel::self()->topLevel());
335 }
336 if (ItemView::Mode_IconTop==viewMode) {
337 act->setVisible(SqlLibraryModel::T_Genre!=MpdLibraryModel::self()->topLevel());
338 }
339 }
340 view->setOpenAfterSearch(SqlLibraryModel::T_Album!=MpdLibraryModel::self()->topLevel());
341 }
342
libraryAlbumSortChanged()343 void LibraryPage::libraryAlbumSortChanged()
344 {
345 QAction *act=qobject_cast<QAction *>(sender());
346 if (act) {
347 MpdLibraryModel::self()->setLibraryAlbumSort((LibraryDb::AlbumSort)act->property(constValProp).toInt());
348 }
349 }
350
albumAlbumSortChanged()351 void LibraryPage::albumAlbumSortChanged()
352 {
353 QAction *act=qobject_cast<QAction *>(sender());
354 if (act) {
355 MpdLibraryModel::self()->setAlbumAlbumSort((LibraryDb::AlbumSort)act->property(constValProp).toInt());
356 }
357 }
358
showArtistImagesChanged(bool u)359 void LibraryPage::showArtistImagesChanged(bool u)
360 {
361 MpdLibraryModel::self()->setUseArtistImages(u);
362 }
363
updateToPlayQueue(const QModelIndex & idx,bool replace)364 void LibraryPage::updateToPlayQueue(const QModelIndex &idx, bool replace)
365 {
366 QStringList files=MpdLibraryModel::self()->filenames(QModelIndexList() << idx, true);
367 if (!files.isEmpty()) {
368 emit add(files, replace ? MPDConnection::ReplaceAndplay : MPDConnection::Append, 0, false);
369 }
370 }
371
addRandomAlbum()372 void LibraryPage::addRandomAlbum()
373 {
374 if (!isVisible()) {
375 return;
376 }
377
378 QStringList genres;
379 QStringList artists;
380 QList<SqlLibraryModel::AlbumItem *> albums;
381 QModelIndexList selected=view->selectedIndexes(false); // Dont need sorted selection here...
382 for (const QModelIndex &idx: selected) {
383 SqlLibraryModel::Item *item=static_cast<SqlLibraryModel::Item *>(idx.internalPointer());
384 switch (item->getType()) {
385 case SqlLibraryModel::T_Genre:
386 genres.append(item->getId());
387 break;
388 case SqlLibraryModel::T_Artist:
389 artists.append(item->getId());
390 break;
391 case SqlLibraryModel::T_Album:
392 // Can only have albums selected if set to group by albums
393 // ...controlActions ensures this!
394 albums.append(static_cast<SqlLibraryModel::AlbumItem *>(item));
395 break;
396 default:
397 break;
398 }
399 }
400
401 LibraryDb::Album album;
402 if (!albums.isEmpty()) {
403 // We have albums selected, so choose a random one of these...
404 SqlLibraryModel::AlbumItem *al=albums.at(QRandomGenerator::global()->bounded(albums.size()));
405 album.artist=al->getArtistId();
406 album.id=al->getId();
407 } else {
408 // If all items selected, then just choose random of all albums
409 switch(MpdLibraryModel::self()->topLevel()) {
410 case SqlLibraryModel::T_Genre:
411 if (genres.size()==MpdLibraryModel::self()->rowCount(QModelIndex())) {
412 genres=QStringList();
413 }
414 break;
415 case SqlLibraryModel::T_Artist:
416 if (artists.size()==MpdLibraryModel::self()->rowCount(QModelIndex())) {
417 artists=QStringList();
418 }
419 break;
420 case SqlLibraryModel::T_Album:
421 genres=artists=QStringList();
422 break;
423 default:
424 break;
425 }
426 album=MpdLibraryModel::self()->getRandomAlbum(genres, artists);
427 }
428
429 if (album.artist.isEmpty() || album.id.isEmpty()) {
430 return;
431 }
432 QList<Song> songs=MpdLibraryModel::self()->getAlbumTracks(album.artist, album.id);
433 if (!songs.isEmpty()) {
434 QStringList files;
435 for (const Song &s: songs) {
436 files.append(s.file);
437 }
438 emit add(files, /*replace ? MPDConnection::ReplaceAndplay : */MPDConnection::Append, 0, false);
439 }
440 }
441
doSearch()442 void LibraryPage::doSearch()
443 {
444 MpdLibraryModel::self()->search(view->searchText(),
445 genreCombo->isHidden() || genreCombo->currentIndex()<=0 ? QString() : genreCombo->currentText());
446 }
447
controlActions()448 void LibraryPage::controlActions()
449 {
450 QModelIndexList selected=view->selectedIndexes(false); // Dont need sorted selection here...
451 bool enable=selected.count()>0;
452
453 CustomActions::self()->setEnabled(enable);
454 StdActions::self()->enableAddToPlayQueue(enable);
455 StdActions::self()->addToStoredPlaylistAction->setEnabled(enable);
456 #ifdef TAGLIB_FOUND
457 StdActions::self()->organiseFilesAction->setEnabled(enable && MPDConnection::self()->getDetails().dirReadable);
458 StdActions::self()->editTagsAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled());
459 #ifdef ENABLE_REPLAYGAIN_SUPPORT
460 StdActions::self()->replaygainAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled());
461 #endif
462 #ifdef ENABLE_DEVICES_SUPPORT
463 StdActions::self()->deleteSongsAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled());
464 StdActions::self()->copyToDeviceAction->setEnabled(StdActions::self()->organiseFilesAction->isEnabled());
465 #endif
466 #endif // TAGLIB_FOUND
467
468 if (1==selected.count()) {
469 SqlLibraryModel::Item *item=static_cast<SqlLibraryModel::Item *>(selected.at(0).internalPointer());
470 SqlLibraryModel::Type type=item->getType();
471 StdActions::self()->setCoverAction->setEnabled((SqlLibraryModel::T_Artist==type/* && !static_cast<MusicLibraryItemArtist *>(item)->isComposer()*/) ||
472 SqlLibraryModel::T_Album==type);
473 } else {
474 StdActions::self()->setCoverAction->setEnabled(false);
475 }
476
477 bool allowRandomAlbum=isVisible() && !selected.isEmpty();
478 if (allowRandomAlbum) {
479 bool groupingAlbums=SqlLibraryModel::T_Album==MpdLibraryModel::self()->topLevel();
480 for (const QModelIndex &idx: selected) {
481 if (SqlLibraryModel::T_Track==static_cast<SqlLibraryModel::Item *>(idx.internalPointer())->getType() ||
482 (!groupingAlbums && SqlLibraryModel::T_Album==static_cast<SqlLibraryModel::Item *>(idx.internalPointer())->getType())) {
483 allowRandomAlbum=false;
484 break;
485 }
486 }
487 }
488 StdActions::self()->addRandomAlbumToPlayQueueAction->setVisible(allowRandomAlbum);
489 }
490
491 #include "moc_librarypage.cpp"
492