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 "currentcover.h"
25 #include "covers.h"
26 #include "config.h"
27 #include "widgets/icons.h"
28 #include "support/utils.h"
29 #include "support/globalstatic.h"
30 #include "online/onlineservice.h"
31 #include <QFile>
32 #include <QTimer>
33 
34 #if !defined Q_OS_WIN && !defined Q_OS_MAC
themes(const QString & theme,QStringList & iconThemes)35 static void themes(const QString &theme, QStringList &iconThemes)
36 {
37     if (iconThemes.contains(theme)) {
38         return;
39     }
40     QString lower=theme.toLower();
41     iconThemes << theme;
42     if (lower!=theme) {
43         iconThemes << lower;
44     }
45     QStringList paths=QIcon::themeSearchPaths();
46     QString key("Inherits=");
47     for (const QString &p: paths) {
48         QString index(p+"/"+theme+"/index.theme");
49         QFile f(index);
50         if (f.open(QIODevice::ReadOnly|QIODevice::Text)) {
51             while (!f.atEnd()) {
52                 QString line=QString::fromUtf8(f.readLine()).trimmed().simplified();
53                 if (line.startsWith(key)) {
54                     QStringList inherited=line.mid(key.length()).split(",", QString::SkipEmptyParts);
55                     for (const QString &i: inherited) {
56                         themes(i, iconThemes);
57                     }
58                     return;
59                 }
60             }
61         }
62     }
63 }
64 
initIconThemes()65 void CurrentCover::initIconThemes()
66 {
67     if (iconThemes.isEmpty()) {
68         themes(QIcon::themeName(), iconThemes);
69     }
70 }
71 
findIcon(const QStringList & names)72 QString CurrentCover::findIcon(const QStringList &names)
73 {
74     initIconThemes();
75     QList<int> sizes=QList<int>() << 256 << 128 << 64 << 48 << 32 << 24 << 22;
76     QStringList paths=QIcon::themeSearchPaths();
77     QStringList sections=QStringList() << "apps" << "categories" << "devices" << "mimetypes" << "mimes";
78     for (const QString &theme: iconThemes) {
79         for (const QString &n: names) {
80             for (const QString &p: paths) {
81                 QMap<int, QString> files;
82                 for (int s: sizes) {
83                     for (const QString &sect: sections) {
84                         QString f(p+"/"+theme+"/"+QString::number(s)+"x"+QString::number(s)+"/"+sect+"/"+n+".png");
85                         if (QFile::exists(f)) {
86                             files.insert(s, f);
87                             break;
88                         }
89                         f=QString(p+"/"+theme+"/"+QString::number(s)+"x"+QString::number(s)+"/"+sect+"/"+n+".svg");
90                         if (QFile::exists(f)) {
91                             files.insert(s, f);
92                             break;
93                         }
94                         f=QString(p+"/"+theme+"/"+sect+"/"+QString::number(s)+"/"+n+".svg");
95                         if (QFile::exists(f)) {
96                             files.insert(s, f);
97                             break;
98                         }
99                     }
100                 }
101 
102                 if (!files.isEmpty()) {
103                     for (int s: sizes) {
104                         if (files.contains(s)) {
105                             return files[s];
106                         }
107                     }
108                 }
109             }
110         }
111     }
112     return QString();
113 }
114 #endif
115 
GLOBAL_STATIC(CurrentCover,instance)116 GLOBAL_STATIC(CurrentCover, instance)
117 
118 CurrentCover::CurrentCover()
119     : QObject(nullptr)
120     , enabled(false)
121     , valid(false)
122     , timer(nullptr)
123 {
124     img=stdImage(current);
125     coverFileName=noCoverFileName;
126 }
127 
~CurrentCover()128 CurrentCover::~CurrentCover()
129 {
130 }
131 
stdImage(const Song & s)132 const QImage & CurrentCover::stdImage(const Song &s)
133 {
134     bool podcast=s.isFromOnlineService() && OnlineService::isPodcasts(s.onlineService());
135     bool stream=s.isStandardStream() || OnlineService::showLogoAsCover(s);
136 
137     QImage &img=podcast
138                 ? noPodcastCover
139                 : stream
140                     ? noStreamCover
141                     : noCover;
142 
143     if (img.isNull()) {
144         int iconSize=Icon::stdSize(Utils::scaleForDpi(128));
145         const QIcon &icon=podcast
146                 ? Icons::self()->podcastIcon
147                 : stream
148                     ? Icons::self()->streamIcon
149                     : Icons::self()->albumIcon(iconSize);
150         img=icon.pixmap(iconSize, iconSize).toImage();
151 
152         QString &file=podcast
153                       ? noPodcastCoverFileName
154                       : stream
155                           ? noStreamCoverFileName
156                           : noCoverFileName;
157 
158         if (file.isEmpty()) {
159             if (podcast) {
160                 QString iconFile=QString(CANTATA_SYS_ICONS_DIR+"podcasts.png");
161                 if (QFile::exists(iconFile)) {
162                     file=iconFile;
163                 }
164             } else if (stream) {
165                 QString iconFile=QString(CANTATA_SYS_ICONS_DIR+"stream.png");
166                 if (QFile::exists(iconFile)) {
167                     file=iconFile;
168                 }
169             }
170         }
171         #if !defined Q_OS_WIN && !defined Q_OS_MAC
172         if (file.isEmpty()) {
173             QStringList iconNames=podcast
174                     ? QStringList{ QStringLiteral("x-media-podcast"), QStringLiteral("news-feed"), QStringLiteral("application-rss+xml"), QStringLiteral("application-atom+xml") }
175                     : stream
176                         ? QStringList{ QStringLiteral("applications-internet") }
177                         : QStringList{ QStringLiteral("media-optical"), QStringLiteral("media-optical-audio") };
178             file=findIcon(iconNames);
179         }
180         #endif
181     }
182     return img;
183 }
184 
setEnabled(bool e)185 void CurrentCover::setEnabled(bool e)
186 {
187     if (enabled==e) {
188         return;
189     }
190 
191     enabled=e;
192     if (enabled) {
193         img=stdImage(Song());
194         connect(Covers::self(), SIGNAL(cover(const Song &, const QImage &, const QString &)), this, SLOT(coverRetrieved(const Song &, const QImage &, const QString &)));
195         connect(Covers::self(), SIGNAL(coverUpdated(const Song &, const QImage &, const QString &)), this, SLOT(coverRetrieved(const Song &, const QImage &, const QString &)));
196     } else {
197         disconnect(Covers::self(), SIGNAL(cover(const Song &, const QImage &, const QString &)), this, SLOT(coverRetrieved(const Song &, const QImage &, const QString &)));
198         disconnect(Covers::self(), SIGNAL(coverUpdated(const Song &, const QImage &, const QString &)), this, SLOT(coverRetrieved(const Song &, const QImage &, const QString &)));
199         current=Song();
200     }
201 }
202 
update(const Song & s)203 void CurrentCover::update(const Song &s)
204 {
205     if (!enabled) {
206         return;
207     }
208 
209     if (s.albumArtist()!=current.albumArtist() || s.album!=current.album || s.isStream()!=current.isStream() ||
210         s.onlineService()!=current.onlineService()) {
211         current=s;
212         if (timer) {
213             timer->stop();
214         }
215         if (!s.isUnknownAlbum() && ((!s.albumArtist().isEmpty() && !s.album.isEmpty() && !current.isStandardStream()) || OnlineService::showLogoAsCover(s))) {
216             Covers::Image cImg=Covers::self()->requestImage(s, true);
217             valid=!cImg.img.isNull();
218             if (valid) {
219                 coverFileName=cImg.fileName;
220                 img=cImg.img;
221                 emit coverFile(cImg.fileName);
222                 if (current.isFromOnlineService()) {
223                     if (coverFileName.startsWith(CANTATA_SYS_ICONS_DIR)) {
224                         emit coverImage(QImage());
225                     } else {
226                         emit coverImage(cImg.img);
227                     }
228                 } else {
229                     emit coverImage(cImg.img);
230                 }
231             } else {
232                 // We need to set the image here, so that TrayItem gets the correct 'noCover' image
233                 // ...but if Covers does eventually download a cover, we dont want valid->noCover->valid
234                 img=stdImage(current);
235                 // Start a timer - in case cover retrieval fails...
236                 if (!timer) {
237                     timer=new QTimer(this);
238                     timer->setSingleShot(true);
239                     connect(timer, SIGNAL(timeout()), this, SLOT(setDefault()));
240                 }
241                 timer->start(750);
242             }
243         } else {
244             valid=true;
245             img=stdImage(current);
246             setDefault();
247         }
248     }
249 }
250 
coverRetrieved(const Song & s,const QImage & newImage,const QString & file)251 void CurrentCover::coverRetrieved(const Song &s, const QImage &newImage, const QString &file)
252 {
253     if (!s.isArtistImageRequest() && s.albumArtist()==current.albumArtist() && s.album==current.album) {
254         if (timer) {
255             timer->stop();
256         }
257         valid=!newImage.isNull();
258         if (valid) {
259             coverFileName=file;
260             img=newImage;
261             emit coverFile(file);
262             emit coverImage(newImage);
263         } else {
264             setDefault();
265         }
266     }
267 }
268 
setDefault()269 void CurrentCover::setDefault()
270 {
271     bool podcast=current.isFromOnlineService() && OnlineService::isPodcasts(current.onlineService());
272     bool stream=current.isStandardStream() || OnlineService::showLogoAsCover(current);
273 
274     coverFileName=podcast
275                   ? noPodcastCoverFileName
276                   : stream
277                       ? noStreamCoverFileName
278                       : noCoverFileName;
279 
280     emit coverFile(coverFileName);
281     emit coverImage(QImage());
282 }
283 
284 #include "moc_currentcover.cpp"
285