1 /***************************************************************************
2  *   Copyright (C) 2003-2005 Max Howell <max.howell@methylblue.com>        *
3  *             (C) 2003-2010 Mark Kretschmann <kretschmann@kde.org>        *
4  *             (C) 2005-2007 Alexandre Oliveira <aleprj@gmail.com>         *
5  *             (C) 2008 Dan Meltzer <parallelgrapefruit@gmail.com>         *
6  *             (C) 2008-2009 Jeff Mitchell <mitchell@kde.org>              *
7  *             (C) 2010 Ralf Engels <ralf-engels@gmx.de>                   *
8  *             (C) 2010 Sergey Ivanov <123kash@gmail.com>                  *
9  *                                                                         *
10  *   This program is free software; you can redistribute it and/or modify  *
11  *   it under the terms of the GNU General Public License as published by  *
12  *   the Free Software Foundation; either version 2 of the License, or     *
13  *   (at your option) any later version.                                   *
14  *                                                                         *
15  *   This program is distributed in the hope that it will be useful,       *
16  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
18  *   GNU General Public License for more details.                          *
19  *                                                                         *
20  *   You should have received a copy of the GNU General Public License     *
21  *   along with this program; if not, write to the                         *
22  *   Free Software Foundation, Inc.,                                       *
23  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
24  ***************************************************************************/
25 
26 #include "MetaTagLib.h"
27 
28 #include "FileType.h"
29 #include "TagsFromFileNameGuesser.h"
30 #include <config.h>
31 
32 #include <KEncodingProber>
33 
34 #include <QImage>
35 #include <QBuffer>
36 #include <QDir>
37 #include <QFile>
38 #include <QFileInfo>
39 #include <QCryptographicHash>
40 #include <QMutex>
41 #include <QMutexLocker>
42 #include <QString>
43 #include <QTime>
44 
45 #include "FileTypeResolver.h"
46 #include "MetaReplayGain.h"
47 #include "tag_helpers/TagHelper.h"
48 #include "tag_helpers/StringHelper.h"
49 
50 //Taglib:
51 #include <audioproperties.h>
52 
53 #ifdef TAGLIB_EXTRAS_FOUND
54 #pragma GCC diagnostic push
55 #pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
56 #include <audiblefiletyperesolver.h>
57 #include <realmediafiletyperesolver.h>
58 #pragma GCC diagnostic pop
59 #endif // TAGLIB_EXTRAS_FOUND
60 
61 
62 namespace Meta
63 {
64     namespace Tag
65     {
66         QMutex s_mutex;
67 
68         static void addRandomness( QCryptographicHash *md5 );
69 
70         /** Get a taglib fileref for a path */
71         static TagLib::FileRef getFileRef( const QString &path );
72 
73         /** Returns a byte vector that can be used to generate the unique id based on the tags. */
74         static TagLib::ByteVector generatedUniqueIdHelper( const TagLib::FileRef &fileref );
75 
76         static QString generateUniqueId( const QString &path );
77 
78     }
79 }
80 
81 TagLib::FileRef
getFileRef(const QString & path)82 Meta::Tag::getFileRef( const QString &path )
83 {
84 #ifdef Q_OS_WIN32
85     const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( path.utf16() );
86 #else
87 #ifdef COMPLEX_TAGLIB_FILENAME
88     const wchar_t *encodedName = reinterpret_cast< const wchar_t * >( path.utf16() );
89 #else
90     QByteArray fileName = QFile::encodeName( path );
91     const char *encodedName = fileName.constData(); // valid as long as fileName exists
92 #endif
93 #endif
94 
95     // Tests reveal the following:
96     //
97     // TagLib::AudioProperties   Relative Time Taken
98     //
99     //  No AudioProp Reading        1
100     //  Fast                        1.18
101     //  Average                     Untested
102     //  Accurate                    Untested
103 
104     return TagLib::FileRef( encodedName, true, TagLib::AudioProperties::Fast );
105 }
106 
107 
108 // ----------------------- unique id ------------------------
109 
110 
111 void
addRandomness(QCryptographicHash * md5)112 Meta::Tag::addRandomness( QCryptographicHash *md5 )
113 {
114     //md5 has size of file already added for some little extra randomness for the hash
115     qsrand( QTime::currentTime().msec() );
116     md5->addData( QString::number( qrand() ).toLatin1() );
117     md5->addData( QString::number( qrand() ).toLatin1() );
118     md5->addData( QString::number( qrand() ).toLatin1() );
119     md5->addData( QString::number( qrand() ).toLatin1() );
120     md5->addData( QString::number( qrand() ).toLatin1() );
121     md5->addData( QString::number( qrand() ).toLatin1() );
122     md5->addData( QString::number( qrand() ).toLatin1() );
123 }
124 
125 TagLib::ByteVector
generatedUniqueIdHelper(const TagLib::FileRef & fileref)126 Meta::Tag::generatedUniqueIdHelper( const TagLib::FileRef &fileref )
127 {
128     TagLib::ByteVector bv;
129 
130     TagHelper *tagHelper = selectHelper( fileref );
131 
132     if( tagHelper )
133     {
134         bv = tagHelper->render();
135         delete tagHelper;
136     }
137 
138     return bv;
139 }
140 
141 QString
generateUniqueId(const QString & path)142 Meta::Tag::generateUniqueId( const QString &path )
143 {
144     QCryptographicHash md5( QCryptographicHash::Md5 );
145     QFile qfile( path );
146     QByteArray size;
147     md5.addData( size.setNum( qfile.size() ) );
148 
149     TagLib::FileRef fileref = getFileRef( path );
150     TagLib::ByteVector bv = generatedUniqueIdHelper( fileref );
151     md5.addData( bv.data(), bv.size() );
152 
153     char databuf[16384];
154     int readlen = 0;
155 
156     if( qfile.open( QIODevice::ReadOnly ) )
157     {
158         if( ( readlen = qfile.read( databuf, 16384 ) ) > 0 )
159         {
160             md5.addData( databuf, readlen );
161             qfile.close();
162         }
163         else
164         {
165             qfile.close();
166             addRandomness( &md5 );
167         }
168     }
169     else
170         addRandomness( &md5 );
171 
172     return QString::fromLatin1( md5.result().toHex() );
173 }
174 
175 
176 // --------- file type resolver ----------
177 
178 /** Will ensure that we have our file type resolvers added */
ensureFileTypeResolvers()179 static void ensureFileTypeResolvers()
180 {
181     static bool alreadyAdded = false;
182     if( !alreadyAdded ) {
183         alreadyAdded = true;
184 
185 #ifdef TAGLIB_EXTRAS_FOUND
186         TagLib::FileRef::addFileTypeResolver(new AudibleFileTypeResolver);
187         TagLib::FileRef::addFileTypeResolver(new RealMediaFileTypeResolver);
188 #endif
189         TagLib::FileRef::addFileTypeResolver(new Meta::Tag::FileTypeResolver());
190     }
191 }
192 
193 // ----------------------- reading ------------------------
194 
195 Meta::FieldHash
readTags(const QString & path,bool)196 Meta::Tag::readTags( const QString &path, bool /*useCharsetDetector*/ )
197 {
198     Meta::FieldHash result;
199 
200     // we do not rely on taglib being thread safe especially when writing the same file from different threads.
201     QMutexLocker locker( &s_mutex );
202     ensureFileTypeResolvers();
203 
204     TagLib::FileRef fileref = getFileRef( path );
205 
206     if( fileref.isNull() )
207         return result;
208 
209     Meta::ReplayGainTagMap replayGainTags = Meta::readReplayGainTags( fileref );
210     if( replayGainTags.contains( Meta::ReplayGain_Track_Gain ) )
211         result.insert( Meta::valTrackGain, replayGainTags[Meta::ReplayGain_Track_Gain] );
212     if( replayGainTags.contains( Meta::ReplayGain_Track_Peak ) )
213         result.insert( Meta::valTrackGainPeak, replayGainTags[Meta::ReplayGain_Track_Peak] );
214 
215     // strangely: the album gain defaults to the track gain
216     if( replayGainTags.contains( Meta::ReplayGain_Album_Gain ) )
217         result.insert( Meta::valAlbumGain, replayGainTags[Meta::ReplayGain_Album_Gain] );
218     else if( replayGainTags.contains( Meta::ReplayGain_Track_Gain ) )
219         result.insert( Meta::valAlbumGain, replayGainTags[Meta::ReplayGain_Track_Gain] );
220     if( replayGainTags.contains( Meta::ReplayGain_Album_Peak ) )
221         result.insert( Meta::valAlbumGainPeak, replayGainTags[Meta::ReplayGain_Album_Peak] );
222     else if( replayGainTags.contains( Meta::ReplayGain_Track_Peak ) )
223         result.insert( Meta::valAlbumGainPeak, replayGainTags[Meta::ReplayGain_Track_Peak] );
224 
225     TagHelper *tagHelper = selectHelper( fileref );
226     if( tagHelper )
227     {
228         if( 0/* useCharsetDetector */ )
229         {
230             KEncodingProber prober;
231             if( prober.feed( tagHelper->testString() ) != KEncodingProber::NotMe )
232                 Meta::Tag::setCodecByName( prober.encoding() );
233         }
234 
235         result.insert( Meta::valFormat, tagHelper->fileType() );
236         result.unite( tagHelper->tags() );
237         delete tagHelper;
238     }
239 
240     TagLib::AudioProperties *properties = fileref.audioProperties();
241     if( properties )
242     {
243         if( !result.contains( Meta::valBitrate ) && properties->bitrate() )
244             result.insert( Meta::valBitrate, properties->bitrate() );
245         if( !result.contains( Meta::valLength ) && properties->length() )
246             result.insert( Meta::valLength, properties->length() * 1000 );
247         if( !result.contains( Meta::valSamplerate ) && properties->sampleRate() )
248             result.insert( Meta::valSamplerate, properties->sampleRate() );
249     }
250 
251     //If tags doesn't contains title and artist, try to guess It from file name
252     if( !result.contains( Meta::valTitle ) ||
253         result.value( Meta::valTitle ).toString().isEmpty() )
254         result.unite( TagGuesser::guessTags( path ) );
255 
256     //we didn't set a FileType till now, let's look it up via FileExtension
257     if( !result.contains( Meta::valFormat ) )
258     {
259         QString ext = path.mid( path.lastIndexOf( QLatin1Char('.') ) + 1 );
260         result.insert( Meta::valFormat, Amarok::FileTypeSupport::fileType( ext ) );
261     }
262 
263     QFileInfo fileInfo( path );
264     result.insert( Meta::valFilesize, fileInfo.size() );
265     result.insert( Meta::valModified, fileInfo.lastModified() );
266 
267     if( !result.contains( Meta::valUniqueId ) )
268         result.insert( Meta::valUniqueId, generateUniqueId( path ) );
269 
270     // compute bitrate if it is not already set and we know length
271     if( !result.contains( Meta::valBitrate ) && result.contains( Meta::valLength ) )
272         result.insert( Meta::valBitrate, ( fileInfo.size() * 8 * 1000 ) /
273                        ( result.value( Meta::valLength ).toInt() * 1024 ) );
274 
275     return result;
276 }
277 
278 QImage
embeddedCover(const QString & path)279 Meta::Tag::embeddedCover( const QString &path )
280 {
281     // we do not rely on taglib being thread safe especially when writing the same file from different threads.
282     QMutexLocker locker( &s_mutex );
283 
284     ensureFileTypeResolvers();
285     TagLib::FileRef fileref = getFileRef( path );
286     if( fileref.isNull() )
287         return QImage();
288 
289     QImage img;
290     TagHelper *tagHelper = selectHelper( fileref );
291     if( tagHelper )
292     {
293         img = tagHelper->embeddedCover();
294         delete tagHelper;
295     }
296     return img;
297 }
298 
299 void
writeTags(const QString & path,const FieldHash & changes,bool writeStatistics)300 Meta::Tag::writeTags( const QString &path, const FieldHash &changes, bool writeStatistics )
301 {
302     FieldHash data = changes;
303 
304     if( !writeStatistics )
305     {
306         data.remove( Meta::valFirstPlayed );
307         data.remove( Meta::valLastPlayed );
308         data.remove( Meta::valPlaycount );
309         data.remove( Meta::valScore );
310         data.remove( Meta::valRating );
311     }
312 
313     // we do not rely on taglib being thread safe especially when writing the same file from different threads.
314     QMutexLocker locker( &s_mutex );
315 
316     ensureFileTypeResolvers();
317     TagLib::FileRef fileref = getFileRef( path );
318     if( fileref.isNull() || data.isEmpty() )
319         return;
320 
321     QScopedPointer<TagHelper> tagHelper( selectHelper( fileref, true ) );
322     if( !tagHelper )
323         return;
324 
325     if( tagHelper->setTags( data ) )
326         fileref.save();
327 }
328 
329 void
setEmbeddedCover(const QString & path,const QImage & cover)330 Meta::Tag::setEmbeddedCover( const QString &path, const QImage &cover )
331 {
332     // we do not rely on taglib being thread safe especially when writing the same file from different threads.
333     QMutexLocker locker( &s_mutex );
334     ensureFileTypeResolvers();
335 
336     TagLib::FileRef fileref = getFileRef( path );
337 
338     if( fileref.isNull() )
339         return;
340 
341     TagHelper *tagHelper = selectHelper( fileref, true );
342     if( !tagHelper )
343         return;
344 
345     if( tagHelper->setEmbeddedCover( cover ) )
346         fileref.save();
347 
348     delete tagHelper;
349 }
350 
351 #undef Qt4QStringToTString
352