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 "albumview.h"
25 #include "artistview.h"
26 #include "gui/covers.h"
27 #include "network/networkaccessmanager.h"
28 #include "support/utils.h"
29 #include "qtiocompressor/qtiocompressor.h"
30 #include "contextengine.h"
31 #include "widgets/textbrowser.h"
32 #include "widgets/icons.h"
33 #include "support/actioncollection.h"
34 #include "support/action.h"
35 #include "support/configuration.h"
36 #include "models/mpdlibrarymodel.h"
37 #include "mpd-interface/cuefile.h"
38 #include <QScrollBar>
39 #include <QFile>
40 #include <QUrl>
41 #include <QUrlQuery>
42 #include <QMenu>
43 #include <QTimer>
44 #include <QCoreApplication>
45 #include <QDesktopServices>
46 #include <QDebug>
47 
48 const QLatin1String AlbumView::constCacheDir("albums/");
49 const QLatin1String AlbumView::constInfoExt(".html.gz");
50 
51 static const QLatin1String constScheme("cantata");
52 
cacheFileName(const QString & artist,const QString & album,const QString & lang,bool createDir)53 static QString cacheFileName(const QString &artist, const QString &album, const QString &lang, bool createDir)
54 {
55     return Utils::cacheDir(AlbumView::constCacheDir, createDir)+Covers::encodeName(artist)+QLatin1String(" - ")+Covers::encodeName(album)+"."+lang+AlbumView::constInfoExt;
56 }
57 
58 enum Parts {
59     Cover = 0x01,
60     Details = 0x02,
61     All = Cover+Details
62 };
63 
AlbumView(QWidget * p)64 AlbumView::AlbumView(QWidget *p)
65     : View(p)
66     , detailsReceived(0)
67 {
68     engine=ContextEngine::create(this);
69     #ifndef Q_OS_WIN
70     // Full width covers not working under windows. Issue #1252
71     fullWidthCoverAction = new Action(tr("Full Width Cover"), this);
72     fullWidthCoverAction->setCheckable(true);
73     connect(fullWidthCoverAction, SIGNAL(toggled(bool)), this, SLOT(setScaleImage(bool)));
74     fullWidthCoverAction->setChecked(Configuration(metaObject()->className()).get("fullWidthCover", false));
75     #endif
76     refreshAction = ActionCollection::get()->createAction("refreshalbum", tr("Refresh Album Information"), Icons::self()->refreshIcon);
77     connect(refreshAction, SIGNAL(triggered()), this, SLOT(refresh()));
78     connect(engine, SIGNAL(searchResult(QString,QString)), this, SLOT(searchResponse(QString,QString)));
79     connect(Covers::self(), SIGNAL(cover(Song,QImage,QString)), SLOT(coverRetrieved(Song,QImage,QString)));
80     connect(Covers::self(), SIGNAL(coverUpdated(Song,QImage,QString)), SLOT(coverUpdated(Song,QImage,QString)));
81     connect(text, SIGNAL(anchorClicked(QUrl)), SLOT(playSong(QUrl)));
82     text->setContextMenuPolicy(Qt::CustomContextMenu);
83     connect(text, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint)));
84     setStandardHeader(tr("Album"));
85     int imageSize=fontMetrics().height()*18;
86     setPicSize(QSize(imageSize, imageSize));
87     clear();
88     if (ArtistView::constCacheAge>0) {
89         clearCache();
90         QTimer *timer=new QTimer(this);
91         connect(timer, SIGNAL(timeout()), this, SLOT(clearCache()));
92         timer->start((int)((ArtistView::constCacheAge/2.0)*1000*24*60*60));
93     }
94 }
95 
~AlbumView()96 AlbumView::~AlbumView()
97 {
98     #ifndef Q_OS_WIN
99     Configuration(metaObject()->className()).set("fullWidthCover", fullWidthCoverAction->isChecked());
100     #endif
101 }
102 
showContextMenu(const QPoint & pos)103 void AlbumView::showContextMenu(const QPoint &pos)
104 {
105     QMenu *menu = text->createStandardContextMenu();
106     menu->addSeparator();
107     if (cancelJobAction->isEnabled()) {
108         menu->addAction(cancelJobAction);
109     } else {
110         menu->addAction(refreshAction);
111     }
112     #ifndef Q_OS_WIN
113     menu->addAction(fullWidthCoverAction);
114     #endif
115     menu->exec(text->mapToGlobal(pos));
116     delete menu;
117 }
118 
refresh()119 void AlbumView::refresh()
120 {
121     if (currentSong.isEmpty()) {
122         return;
123     }
124     for (const QString &lang: engine->getLangs()) {
125         QFile::remove(cacheFileName(Covers::fixArtist(currentSong.albumArtistOrComposer()), currentSong.album, engine->getPrefix(lang), false));
126     }
127     update(currentSong, true);
128 }
129 
update(const Song & song,bool force)130 void AlbumView::update(const Song &song, bool force)
131 {
132     QString streamName=song.isStandardStream() && song.album.isEmpty() ? song.name() : QString();
133     if (!streamName.isEmpty() && streamName!=currentSong.name()) {
134         abort();
135         currentSong=song;
136         clearDetails();
137         setHeader(streamName);
138         needToUpdate=false;
139         detailsReceived=All;
140         pic=createPicTag(QImage(), CANTATA_SYS_ICONS_DIR+QLatin1String("stream.png"));
141         updateDetails();
142         return;
143     }
144 
145     if (song.isEmpty() || song.albumArtistOrComposer().isEmpty() || song.album.isEmpty()) {
146         currentSong=song;
147         clearDetails();
148         abort();
149         return;
150     }
151 
152     if (force || song.albumArtistOrComposer()!=currentSong.albumArtistOrComposer() || song.album!=currentSong.album) {
153         currentSong=song;
154         currentArtist=currentSong.basicArtist();
155         abort();
156         if (!isVisible()) {
157             needToUpdate=true;
158             return;
159         }
160         clearDetails();
161         setHeader(song.album.isEmpty() ? stdHeader : song.album);
162         Covers::Image cImg=Covers::self()->requestImage(song, true);
163         if (!cImg.img.isNull()) {
164             detailsReceived|=Cover;
165             pic=createPicTag(cImg.img, cImg.fileName);
166         }
167         getTrackListing();
168         getDetails();
169 
170         if (All==detailsReceived) {
171             hideSpinner();
172         } else {
173             showSpinner();
174         }
175     } else if (song.title!=currentSong.title) {
176         currentSong=song;
177         getTrackListing();
178         updateDetails(true);
179     }
180 }
181 
playSong(const QUrl & url)182 void AlbumView::playSong(const QUrl &url)
183 {
184     if (url.scheme() == constScheme) {
185         emit playSong(url.path().mid(1)); // Remove leading /
186     } else if (CueFile::isCue(url.toString())) {
187         emit playSong(url.toString());
188     } else {
189         QDesktopServices::openUrl(url);
190     }
191 }
192 
getTrackListing()193 void AlbumView::getTrackListing()
194 {
195     if (currentSong.isNonMPD()) {
196         if (!pic.isEmpty()) {
197             updateDetails();
198         }
199         return;
200     }
201 
202     if (songs.isEmpty()) {
203         songs=MpdLibraryModel::self()->getAlbumTracks(currentSong);
204     }
205 
206     if (!songs.isEmpty()) {
207         trackList=View::subHeader(tr("Tracks"))+QLatin1String("<p><table>");
208         for (const Song &s: songs) {
209             if (CueFile::isCue(s.file)) {
210                 QUrl u(s.file);
211                 QUrlQuery q(u);
212 
213                 q.addQueryItem("artist", s.artist);
214                 q.addQueryItem("albumartist", s.albumartist);
215                 q.addQueryItem("album", s.album);
216                 q.addQueryItem("title", s.title);
217                 q.addQueryItem("disc", QString::number(s.disc));
218                 q.addQueryItem("track", QString::number(s.track));
219                 q.addQueryItem("time", QString::number(s.time));
220                 q.addQueryItem("year", QString::number(s.year));
221                 q.addQueryItem("origYear", QString::number(s.origYear));
222                 u.setQuery(q);
223 
224                 trackList+=QLatin1String("<tr><td align='right'>")+QString::number(s.track)+
225                            QLatin1String("</td><td><a href=\"")+u.toString()+QLatin1String("\">") +
226                            ((s.albumartist==currentSong.albumartist && s.album==currentSong.album && s.title==currentSong.title) ? "<b>"+s.displayTitle()+"</b>" : s.displayTitle())+
227                            QLatin1String("</a></td></tr>");
228             } else {
229                 trackList+=QLatin1String("<tr><td align='right'>")+QString::number(s.track)+
230                            QLatin1String("</td><td><a href=\"")+constScheme+QLatin1String(":///")+s.file+QLatin1String("\">") +
231                            (s.file==currentSong.file ? "<b>"+s.displayTitle()+"</b>" : s.displayTitle())+
232                            QLatin1String("</a></td></tr>");
233             }
234         }
235 
236         trackList+=QLatin1String("</table></p>");
237         updateDetails();
238     }
239 }
240 
getDetails()241 void AlbumView::getDetails()
242 {
243     engine->cancel();
244     for (const QString &lang: engine->getLangs()) {
245         QString prefix=engine->getPrefix(lang);
246         QString cachedFile=cacheFileName(Covers::fixArtist(currentSong.albumArtistOrComposer()), currentSong.album, prefix, false);
247         if (QFile::exists(cachedFile)) {
248             QFile f(cachedFile);
249             QtIOCompressor compressor(&f);
250             compressor.setStreamFormat(QtIOCompressor::GzipFormat);
251             if (compressor.open(QIODevice::ReadOnly)) {
252                 QByteArray data=compressor.readAll();
253 
254                 if (!data.isEmpty()) {
255                     searchResponse(QString::fromUtf8(data), QString());
256                     Utils::touchFile(cachedFile);
257                     return;
258                 }
259             }
260         }
261     }
262     engine->search(QStringList() << currentSong.albumArtistOrComposer() << currentSong.album, ContextEngine::Album);
263 }
264 
coverRetrieved(const Song & s,const QImage & img,const QString & file)265 void AlbumView::coverRetrieved(const Song &s, const QImage &img, const QString &file)
266 {
267     if (!s.isArtistImageRequest() && (s==currentSong && pic.isEmpty())) {
268         detailsReceived|=Cover;
269         if (All==detailsReceived) {
270             hideSpinner();
271         }
272         pic=createPicTag(img, file);
273         if (!pic.isEmpty()) {
274             updateDetails();
275         }
276     }
277 }
278 
coverUpdated(const Song & s,const QImage & img,const QString & file)279 void AlbumView::coverUpdated(const Song &s, const QImage &img, const QString &file)
280 {
281     if (!s.isArtistImageRequest() && s==currentSong) {
282         detailsReceived|=Cover;
283         if (All==detailsReceived) {
284             hideSpinner();
285         }
286         pic=createPicTag(img, file);
287         if (!pic.isEmpty()) {
288             updateDetails();
289         }
290     }
291 }
292 
searchResponse(const QString & resp,const QString & lang)293 void AlbumView::searchResponse(const QString &resp, const QString &lang)
294 {
295     detailsReceived|=Details;
296     if (All==detailsReceived) {
297         hideSpinner();
298     }
299 
300     if (!resp.isEmpty()) {
301         details=engine->translateLinks(resp);
302         if (!lang.isEmpty()) {
303             QFile f(cacheFileName(Covers::fixArtist(currentSong.albumArtistOrComposer()), currentSong.album, lang, true));
304             QtIOCompressor compressor(&f);
305             compressor.setStreamFormat(QtIOCompressor::GzipFormat);
306             if (compressor.open(QIODevice::WriteOnly)) {
307                 compressor.write(resp.toUtf8().constData());
308             }
309         }
310         updateDetails();
311     }
312 }
313 
updateDetails(bool preservePos)314 void AlbumView::updateDetails(bool preservePos)
315 {
316     int pos=preservePos ? text->verticalScrollBar()->value() : 0;
317     if (!details.isEmpty()) {
318         setHtml(pic+"<br>"+details+"<br>"+trackList);
319     } else {
320         setHtml(pic+trackList);
321     }
322     if (preservePos) {
323         text->verticalScrollBar()->setValue(pos);
324     }
325 }
326 
abort()327 void AlbumView::abort()
328 {
329     engine->cancel();
330     hideSpinner();
331 }
332 
clearCache()333 void AlbumView::clearCache()
334 {
335     Utils::clearOldCache(constCacheDir, ArtistView::constCacheAge);
336 }
337 
clearDetails()338 void AlbumView::clearDetails()
339 {
340     details.clear();
341     trackList.clear();
342     bio.clear();
343     pic.clear();
344     songs.clear();
345     clear();
346     engine->cancel();
347     detailsReceived=0;
348 }
349 
350 #include "moc_albumview.cpp"
351