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