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