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