1 /* $BEGIN_LICENSE
2 
3 This file is part of Musique.
4 Copyright 2013, Flavio Tordini <flavio.tordini@gmail.com>
5 
6 Musique is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10 
11 Musique is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with Musique.  If not, see <http://www.gnu.org/licenses/>.
18 
19 $END_LICENSE */
20 
21 #include "coverutils.h"
22 #include "finderitemdelegate.h"
23 #include "model/album.h"
24 
isAcceptableImage(const QImage & image)25 bool CoverUtils::isAcceptableImage(const QImage &image) {
26     const int minimumSize = FinderItemDelegate::ITEM_WIDTH;
27     const int width = image.size().width();
28     const int height = image.size().height();
29 
30     if (width < minimumSize || height < minimumSize) {
31         qDebug() << "Local cover too small" << image.size();
32         return false;
33     }
34 
35     float aspectRatio = (float)width / (float)height;
36     if (aspectRatio > 1.2 || aspectRatio < 0.8) {
37         qDebug() << "Local cover not square enough" << image.size();
38         return false;
39     }
40 
41     return true;
42 }
43 
maybeScaleImage(const QImage & image)44 QImage CoverUtils::maybeScaleImage(const QImage &image) {
45     static const int maximumSize = 300;
46     const int width = image.size().width();
47     const int height = image.size().height();
48     if (width > maximumSize || height > maximumSize) {
49         qDebug() << "Scaling local cover" << image.size();
50         return image.scaled(maximumSize, maximumSize, Qt::KeepAspectRatio,
51                             Qt::SmoothTransformation);
52     }
53     return image;
54 }
55 
saveImage(const QImage & image,Album * album)56 bool CoverUtils::saveImage(const QImage &image, Album *album) {
57     QImage scaledImage = maybeScaleImage(image);
58     QBuffer buffer;
59     scaledImage.save(&buffer, "JPG");
60     album->setPhoto(buffer.data());
61     return true;
62 }
63 
coverFromFile(const QString & dir,Album * album)64 bool CoverUtils::coverFromFile(const QString &dir, Album *album) {
65     static const QVector<QRegExp> coverREs = [] {
66         QLatin1String ext(".(jpe?g|gif|png|bmp)");
67         QVector<QRegExp> res;
68         res << QRegExp(".*cover.*" + ext, Qt::CaseInsensitive)
69             << QRegExp(".*front.*" + ext, Qt::CaseInsensitive)
70             << QRegExp(".*folder.*" + ext, Qt::CaseInsensitive);
71         return res;
72     }();
73 
74     const QFileInfoList flist =
75             QDir(dir).entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Readable);
76 
77     for (const QFileInfo &fileInfo : flist) {
78         const QString filename = fileInfo.fileName();
79         for (const QRegExp &re : coverREs) {
80             if (filename.contains(re)) {
81                 qDebug() << "Found local cover" << filename;
82                 QImage image(fileInfo.absoluteFilePath());
83                 if (isAcceptableImage(image)) return saveImage(image, album);
84                 break;
85             }
86         }
87     }
88 
89     return false;
90 }
91 
coverFromTags(const QString & filename,Album * album)92 bool CoverUtils::coverFromTags(const QString &filename, Album *album) {
93     const QString suffix = QFileInfo(filename).suffix().toLower();
94     if (suffix == "mp3") {
95         TagLib::MPEG::File f((TagLib::FileName)filename.toUtf8());
96         if (!f.isValid()) return false;
97         return coverFromMPEGTags(f.ID3v2Tag(), album);
98     } else if (suffix == "ogg" || suffix == "oga") {
99         TagLib::Ogg::Vorbis::File f((TagLib::FileName)filename.toUtf8());
100         if (!f.isValid()) return false;
101         return coverFromXiphComment(f.tag(), album);
102     } else if (suffix == "flac") {
103         TagLib::FLAC::File f((TagLib::FileName)filename.toUtf8());
104         bool res = false;
105         if (f.isValid()) res = coverFromMPEGTags(f.ID3v2Tag(), album);
106         if (!res) res = coverFromXiphComment(f.xiphComment(), album);
107         return res;
108     } else if (suffix == "aac" || suffix == "m4a" || suffix == "m4b" || suffix == "m4p" ||
109                suffix == "mp4") {
110         return coverFromMP4(filename, album);
111     }
112     return false;
113 }
114 
coverFromMPEGTags(TagLib::ID3v2::Tag * tag,Album * album)115 bool CoverUtils::coverFromMPEGTags(TagLib::ID3v2::Tag *tag, Album *album) {
116     if (!tag) return false;
117 
118     TagLib::ID3v2::FrameList list = tag->frameList("APIC");
119     if (list.isEmpty()) return false;
120 
121     TagLib::ID3v2::AttachedPictureFrame *frame =
122             static_cast<TagLib::ID3v2::AttachedPictureFrame *>(list.front());
123     if (!frame) return false;
124     const int frameSize = frame->picture().size();
125     if (frameSize <= 0) return false;
126 
127     QImage image;
128     image.loadFromData((const uchar *)frame->picture().data(), frame->picture().size());
129     if (!isAcceptableImage(image)) return false;
130 
131     return saveImage(image, album);
132 }
133 
coverFromXiphComment(TagLib::Ogg::XiphComment * xiphComment,Album * album)134 bool CoverUtils::coverFromXiphComment(TagLib::Ogg::XiphComment *xiphComment, Album *album) {
135     if (!xiphComment) return false;
136 
137     const TagLib::StringList &stringList = xiphComment->fieldListMap()["COVERART"];
138     if (stringList.isEmpty()) return false;
139     TagLib::ByteVector byteVector = stringList.front().data(TagLib::String::Latin1);
140 
141     QByteArray encodedData;
142     encodedData.setRawData(byteVector.data(), byteVector.size());
143 
144     QByteArray data = QByteArray::fromBase64(encodedData);
145 
146     QImage image;
147     image.loadFromData(data);
148     if (!isAcceptableImage(image)) return false;
149 
150     qDebug() << "Cover from Xiph!";
151 
152     return saveImage(image, album);
153 }
154 
coverFromMP4(const QString & filename,Album * album)155 bool CoverUtils::coverFromMP4(const QString &filename, Album *album) {
156     TagLib::MP4::File f((TagLib::FileName)filename.toUtf8());
157     if (!f.isValid()) return false;
158 
159     TagLib::MP4::Tag *tag = static_cast<TagLib::MP4::Tag *>(f.tag());
160     if (!tag) return false;
161 
162     TagLib::MP4::ItemListMap itemsListMap = tag->itemListMap();
163     TagLib::MP4::Item coverItem = itemsListMap["covr"];
164     TagLib::MP4::CoverArtList coverArtList = coverItem.toCoverArtList();
165     TagLib::MP4::CoverArt coverArt = coverArtList.front();
166 
167     QImage image;
168     image.loadFromData((const uchar *)coverArt.data().data(), coverArt.data().size());
169     if (!isAcceptableImage(image)) return false;
170 
171     qDebug() << "Cover from MP4!";
172 
173     return saveImage(image, album);
174 }
175