Revert previous commit, was incorrect
[amarok.git] / src / metabundle.cpp
blob07da90d2d569401fb6ec064fee1a98563e65ba00
1 // Max Howell <max.howell@methylblue.com>, (C) 2004
2 // Alexandre Pereira de Oliveira <aleprj@gmail.com>, (C) 2005, 2006
3 // Gábor Lehel <illissius@gmail.com>, (C) 2005, 2006
4 // Shane King <kde@dontletsstart.com>, (C) 2006
5 // Peter C. Ndikuwera <pndiku@gmail.com>, (C) 2006
6 // License: GNU General Public License V2
9 #define DEBUG_PREFIX "MetaBundle"
11 #include <stdlib.h>
12 #include <unistd.h>
13 #include <stdio.h>
14 #include <time.h>
15 #include <sys/time.h>
16 #include <sys/types.h>
17 #include <fcntl.h>
19 #include "amarok.h"
20 #include "amarokconfig.h"
21 #include "debug.h"
22 #include "collectiondb.h"
23 #include <kapplication.h>
24 #include <kfilemetainfo.h>
25 #include <kio/global.h>
26 #include <kio/job.h>
27 #include <kio/jobclasses.h>
28 #include <kio/netaccess.h>
29 #include <kcodecs.h>
30 #include <KRandom>
31 #include <qdom.h>
32 #include <QFile> //decodePath()
33 //Added by qt3to4:
34 #include <Q3ValueList>
35 #include <QByteArray>
36 #include <attachedpictureframe.h>
37 #include <fileref.h>
38 #include <id3v1genres.h> //used to load genre list
39 #include <mpegfile.h>
40 #include <tag.h>
41 #include <tstring.h>
42 #include <tlist.h>
43 #include <apetag.h>
44 #include <id3v2tag.h>
45 #include <id3v1tag.h>
46 #include <mpcfile.h>
47 #include <oggfile.h>
48 #include <oggflacfile.h>
49 #include <vorbisfile.h>
50 #include <flacfile.h>
51 #include <textidentificationframe.h>
52 #include <uniquefileidentifierframe.h>
53 #include <xiphcomment.h>
55 #include "config-amarok.h"
56 #ifdef HAVE_MP4V2
57 #include "metadata/mp4/mp4file.h"
58 #include "metadata/mp4/mp4tag.h"
59 #else
60 #include "metadata/m4a/mp4file.h"
61 #include "metadata/m4a/mp4itunestag.h"
62 #endif
64 #include "lastfm.h"
65 #include "metabundle.h"
66 #include "podcastbundle.h"
69 MetaBundle::EmbeddedImage::EmbeddedImage( const TagLib::ByteVector& data, const TagLib::String& description )
70 : m_description( TStringToQString( description ) )
72 m_data = QByteArray( data.data(), data.size() );
75 const QByteArray &MetaBundle::EmbeddedImage::hash() const
77 if( m_hash.isEmpty() ) {
78 m_hash = KMD5( m_data ).hexDigest();
80 return m_hash;
83 bool MetaBundle::EmbeddedImage::save( const QDir& dir ) const
85 QFile file( dir.filePath( hash() ) );
87 if( file.open( QIODevice::WriteOnly | QIODevice::Unbuffered ) ) {
88 const Q_LONG s = file.write( m_data.data(), m_data.size() );
89 if( s >= 0 && Q_LONG( s ) == m_data.size() ) {
90 debug() << "EmbeddedImage::save " << file.fileName();
91 return true;
93 file.remove();
95 debug() << "EmbeddedImage::save failed! " << file.fileName();
96 return false;
99 /// These are untranslated and used for storing/retrieving XML playlist
100 const QString &MetaBundle::exactColumnName( int c ) //static
102 // construct static qstrings to avoid constructing them all the time
103 static QString columns[] = {
104 "Filename", "Title", "Artist", "AlbumArtist", "Composer", "Year", "Album", "DiscNumber", "Track", "BPM", "Genre", "Comment",
105 "Directory", "Type", "Length", "Bitrate", "SampleRate", "Score", "Rating", "PlayCount", "LastPlayed",
106 "Mood", "Filesize" };
107 static QString error( "ERROR" );
109 if ( c >= 0 && c < NUM_COLUMNS )
110 return columns[c];
111 else
112 return error;
115 const QString MetaBundle::prettyColumnName( int index ) //static
117 switch( index )
119 case Filename: return i18n( "Filename" );
120 case Title: return i18n( "Title" );
121 case Artist: return i18n( "Artist" );
122 case AlbumArtist:return i18n( "Album Artist");
123 case Composer: return i18n( "Composer" );
124 case Year: return i18n( "Year" );
125 case Album: return i18n( "Album" );
126 case DiscNumber: return i18n( "Disc Number" );
127 case Track: return i18n( "Track" );
128 case Bpm: return i18n( "BPM" );
129 case Genre: return i18n( "Genre" );
130 case Comment: return i18n( "Comment" );
131 case Directory: return i18n( "Directory" );
132 case Type: return i18n( "Type" );
133 case Length: return i18n( "Length" );
134 case Bitrate: return i18n( "Bitrate" );
135 case SampleRate: return i18n( "Sample Rate" );
136 case Score: return i18n( "Score" );
137 case Rating: return i18n( "Rating" );
138 case PlayCount: return i18n( "Play Count" );
139 case LastPlayed: return i18nc( "Column name", "Last Played" );
140 case Mood: return i18n( "Mood" );
141 case Filesize: return i18n( "File Size" );
143 return "This is a bug.";
146 int MetaBundle::columnIndex( const QString &name )
148 for( int i = 0; i < NUM_COLUMNS; ++i )
149 if( exactColumnName( i ).toLower() == name.toLower() )
150 return i;
151 return -1;
154 MetaBundle::MetaBundle()
155 : m_uniqueId( QString() )
156 , m_year( Undetermined )
157 , m_discNumber( Undetermined )
158 , m_track( Undetermined )
159 , m_bpm( Undetermined )
160 , m_bitrate( Undetermined )
161 , m_length( Undetermined )
162 , m_sampleRate( Undetermined )
163 , m_score( Undetermined )
164 , m_rating( Undetermined )
165 , m_playCount( Undetermined )
166 , m_lastPlay( abs( Undetermined ) )
167 , m_filesize( Undetermined )
168 , m_moodbar( 0 )
169 , m_type( other )
170 , m_exists( true )
171 , m_isValidMedia( true )
172 , m_isCompilation( false )
173 , m_notCompilation( false )
174 , m_safeToSave( false )
175 , m_waitingOnKIO( 0 )
176 , m_tempSavePath( QString() )
177 , m_origRenamedSavePath( QString() )
178 , m_tempSaveDigest( 0 )
179 , m_saveFileref( 0 )
180 , m_podcastBundle( 0 )
181 , m_lastFmBundle( 0 )
182 , m_isSearchDirty(true)
184 init();
187 MetaBundle::MetaBundle( const KUrl &url, bool noCache, TagLib::AudioProperties::ReadStyle readStyle, EmbeddedImageList* images )
188 : m_url( url )
189 , m_uniqueId( QString() )
190 , m_year( Undetermined )
191 , m_discNumber( Undetermined )
192 , m_track( Undetermined )
193 , m_bpm( Undetermined )
194 , m_bitrate( Undetermined )
195 , m_length( Undetermined )
196 , m_sampleRate( Undetermined )
197 , m_score( Undetermined )
198 , m_rating( Undetermined )
199 , m_playCount( Undetermined )
200 , m_lastPlay( abs( Undetermined ) )
201 , m_filesize( Undetermined )
202 , m_moodbar( 0 )
203 , m_type( other )
204 , m_exists( isFile() && QFile::exists( url.path() ) )
205 , m_isValidMedia( false )
206 , m_isCompilation( false )
207 , m_notCompilation( false )
208 , m_safeToSave( false )
209 , m_waitingOnKIO( 0 )
210 , m_tempSavePath( QString() )
211 , m_origRenamedSavePath( QString() )
212 , m_tempSaveDigest( 0 )
213 , m_saveFileref( 0 )
214 , m_podcastBundle( 0 )
215 , m_lastFmBundle( 0 )
216 , m_isSearchDirty(true)
218 if ( exists() )
220 if ( !noCache )
221 m_isValidMedia = CollectionDB::instance()->bundleForUrl( this );
223 if ( !isValidMedia() || ( !m_podcastBundle && m_length <= 0 ) )
224 readTags( readStyle, images );
226 else
228 // if it's a podcast we might get some info this way
229 CollectionDB::instance()->bundleForUrl( this );
230 m_bitrate = m_length = m_sampleRate = Unavailable;
234 //StreamProvider ctor
235 MetaBundle::MetaBundle( const QString& title,
236 const QString& streamUrl,
237 const int bitrate,
238 const QString& genre,
239 const QString& streamName,
240 const KUrl& url )
241 : m_url ( url )
242 , m_genre ( genre )
243 , m_streamName( streamName )
244 , m_streamUrl ( streamUrl )
245 , m_uniqueId( QString() )
246 , m_year( 0 )
247 , m_discNumber( 0 )
248 , m_track( 0 )
249 , m_bpm( Undetermined )
250 , m_bitrate( bitrate )
251 , m_length( Irrelevant )
252 , m_sampleRate( Unavailable )
253 , m_score( Undetermined )
254 , m_rating( Undetermined )
255 , m_playCount( Undetermined )
256 , m_lastPlay( abs( Undetermined ) )
257 , m_filesize( Undetermined )
258 , m_moodbar( 0 )
259 , m_type( other )
260 , m_exists( true )
261 , m_isValidMedia( false )
262 , m_isCompilation( false )
263 , m_notCompilation( false )
264 , m_safeToSave( false )
265 , m_waitingOnKIO( 0 )
266 , m_tempSavePath( QString() )
267 , m_origRenamedSavePath( QString() )
268 , m_tempSaveDigest( 0 )
269 , m_saveFileref( 0 )
270 , m_podcastBundle( 0 )
271 , m_lastFmBundle( 0 )
272 , m_isSearchDirty(true)
274 if( title.count( '-' ) )
276 m_title = title.section( '-', 1, 1 ).trimmed();
277 m_artist = title.section( '-', 0, 0 ).trimmed();
279 else
281 m_title = title;
282 m_artist = streamName; //which is sort of correct..
286 MetaBundle::MetaBundle( const MetaBundle &bundle )
287 : m_moodbar( 0 )
289 *this = bundle;
292 MetaBundle::~MetaBundle()
294 delete m_podcastBundle;
295 delete m_lastFmBundle;
297 if( m_moodbar != 0 )
298 delete m_moodbar;
301 MetaBundle&
302 MetaBundle::operator=( const MetaBundle& bundle )
304 m_url = bundle.m_url;
305 m_title = bundle.m_title;
306 m_artist = bundle.m_artist;
307 m_albumArtist = bundle.m_albumArtist;
308 m_composer = bundle.m_composer;
309 m_album = bundle.m_album;
310 m_comment = bundle.m_comment;
311 m_genre = bundle.m_genre;
312 m_streamName = bundle.m_streamName;
313 m_streamUrl = bundle.m_streamUrl;
314 m_uniqueId = bundle.m_uniqueId;
315 m_year = bundle.m_year;
316 m_discNumber = bundle.m_discNumber;
317 m_track = bundle.m_track;
318 m_bpm = bundle.m_bpm;
319 m_bitrate = bundle.m_bitrate;
320 m_length = bundle.m_length;
321 m_sampleRate = bundle.m_sampleRate;
322 m_score = bundle.m_score;
323 m_rating = bundle.m_rating;
324 m_playCount = bundle.m_playCount;
325 m_lastPlay = bundle.m_lastPlay;
326 m_filesize = bundle.m_filesize;
327 m_type = bundle.m_type;
328 m_exists = bundle.m_exists;
329 m_isValidMedia = bundle.m_isValidMedia;
330 m_isCompilation = bundle.m_isCompilation;
331 m_notCompilation = bundle.m_notCompilation;
332 m_safeToSave = bundle.m_safeToSave;
333 m_waitingOnKIO = bundle.m_waitingOnKIO;
334 m_tempSavePath = bundle.m_tempSavePath;
335 m_origRenamedSavePath = bundle.m_origRenamedSavePath;
336 m_tempSaveDigest = bundle.m_tempSaveDigest;
337 m_saveFileref = bundle.m_saveFileref;
339 if( bundle.m_moodbar != 0)
341 if( m_moodbar == 0 )
342 m_moodbar = new Moodbar( this );
343 *m_moodbar = *bundle.m_moodbar;
345 else
347 // If m_moodbar != 0, it's initialized for a reason
348 // Deleting it makes the PrettySlider code more ugly,
349 // since it'd have to reconnect the jobEvent() signal.
350 if( m_moodbar != 0 )
351 m_moodbar->reset();
355 // delete m_podcastBundle; why does this crash Amarok? apparently m_podcastBundle isn't always initialized.
356 m_podcastBundle = 0;
357 if( bundle.m_podcastBundle )
358 setPodcastBundle( *bundle.m_podcastBundle );
360 // delete m_lastFmBundle; same as above
361 m_lastFmBundle = 0;
362 if( bundle.m_lastFmBundle )
363 setLastFmBundle( *bundle.m_lastFmBundle );
365 m_isSearchDirty = true;
366 return *this;
370 bool
371 MetaBundle::checkExists()
373 debug() << "MetaBundle path is " << url().url();
374 m_exists = !isFile() || QFile::exists( url().path() );
376 return m_exists;
379 bool
380 MetaBundle::operator==( const MetaBundle& bundle ) const
382 return uniqueId() == bundle.uniqueId() && //first, since if using IDs will return faster
383 artist() == bundle.artist() &&
384 albumArtist() == bundle.albumArtist() &&
385 title() == bundle.title() &&
386 composer() == bundle.composer() &&
387 album() == bundle.album() &&
388 year() == bundle.year() &&
389 comment() == bundle.comment() &&
390 genre() == bundle.genre() &&
391 track() == bundle.track() &&
392 discNumber() == bundle.discNumber() &&
393 bpm() == bundle.bpm() &&
394 length() == bundle.length() &&
395 bitrate() == bundle.bitrate() &&
396 sampleRate() == bundle.sampleRate();
397 // FIXME: check for size equality?
400 void
401 MetaBundle::clear()
403 *this = MetaBundle();
406 void
407 MetaBundle::init( TagLib::AudioProperties *ap )
409 if ( ap )
411 m_bitrate = ap->bitrate();
412 m_length = ap->length();
413 m_sampleRate = ap->sampleRate();
415 else
416 m_bitrate = m_length = m_sampleRate = Undetermined;
419 void
420 MetaBundle::init( const KFileMetaInfo& info )
422 if( info.isValid() )
424 m_artist = info.item( "Artist" ).value().toString();
425 m_album = info.item( "Album" ).value().toString();
426 m_comment = info.item( "Comment" ).value().toString();
427 m_genre = info.item( "Genre" ).value().toString();
428 m_year = info.item( "Year" ).value().toString().toInt();
429 m_track = info.item( "Track" ).value().toString().toInt();
430 m_bitrate = info.item( "Bitrate" ).value().toInt();
431 m_length = info.item( "Length" ).value().toInt();
432 m_sampleRate = info.item( "Sample Rate" ).value().toInt();
434 // For title, check if it is valid. If not, use prettyTitle.
435 // @see bug:83650
436 const KFileMetaInfoItem itemtitle = info.item( "Title" );
437 m_title = itemtitle.isValid() ? itemtitle.value().toString() : prettyTitle( m_url.fileName() );
439 const KFileMetaInfoItem itemid = info.item( "Unique ID" );
440 m_uniqueId = itemid.isValid() ? itemid.value().toString() : QString::null;
442 // because whoever designed KMetaInfoItem is a donkey
443 #define makeSane( x ) if( x.string() == "---" ) x = null;
444 QString null;
445 makeSane( m_artist );
446 makeSane( m_album );
447 makeSane( m_comment );
448 makeSane( m_genre );
449 if ( m_title == "---" ) m_title.clear();
450 #undef makeSane
452 m_isValidMedia = true;
454 else
456 m_bitrate = m_length = m_sampleRate = m_filesize = Undetermined;
457 m_isValidMedia = false;
461 void
462 MetaBundle::embeddedImages( MetaBundle::EmbeddedImageList& images ) const
464 if ( isFile() )
466 TagLib::FileRef fileref = TagLib::FileRef( QFile::encodeName( url().path() ).data(), false );
467 if ( !fileref.isNull() ) {
468 if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) ) {
469 if ( file->ID3v2Tag() )
470 loadImagesFromTag( *file->ID3v2Tag(), images );
471 } else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) ) {
472 if ( file->ID3v2Tag() )
473 loadImagesFromTag( *file->ID3v2Tag(), images );
474 } else if ( TagLib::MP4::File *file = dynamic_cast<TagLib::MP4::File *>( fileref.file() ) ) {
475 TagLib::MP4::Tag *mp4tag = dynamic_cast<TagLib::MP4::Tag *>( file->tag() );
476 if( mp4tag && mp4tag->cover().size() ) {
477 images.push_back( EmbeddedImage( mp4tag->cover(), "" ) );
484 void
485 MetaBundle::readTags( TagLib::AudioProperties::ReadStyle readStyle, EmbeddedImageList* images )
487 if( !isFile() )
488 return;
490 const QString path = url().path();
492 TagLib::FileRef fileref;
493 TagLib::Tag *tag = 0;
494 fileref = TagLib::FileRef( QFile::encodeName( path ).data(), true, readStyle );
496 if( !fileref.isNull() )
498 setUniqueId( readUniqueId( &fileref ) );
499 m_filesize = QFile( path ).size();
501 tag = fileref.tag();
502 if ( tag )
504 #define strip( x ) TStringToQString( x ).trimmed()
505 setTitle( strip( tag->title() ) );
506 setArtist( strip( tag->artist() ) );
507 setAlbum( strip( tag->album() ) );
508 setComment( strip( tag->comment() ) );
509 setGenre( strip( tag->genre() ) );
510 setYear( tag->year() );
511 setTrack( tag->track() );
512 #undef strip
514 m_isValidMedia = true;
518 /* As mpeg implementation on TagLib uses a Tag class that's not defined on the headers,
519 we have to cast the files, not the tags! */
521 QString disc;
522 QString compilation;
523 if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) )
525 m_type = mp3;
526 if ( file->ID3v2Tag() )
528 if ( !file->ID3v2Tag()->frameListMap()["TPOS"].isEmpty() )
529 disc = TStringToQString( file->ID3v2Tag()->frameListMap()["TPOS"].front()->toString() ).trimmed();
531 if ( !file->ID3v2Tag()->frameListMap()["TBPM"].isEmpty() )
532 setBpm( TStringToQString( file->ID3v2Tag()->frameListMap()["TBPM"].front()->toString() ).trimmed().toFloat() );
534 if ( !file->ID3v2Tag()->frameListMap()["TCOM"].isEmpty() )
535 setComposer( TStringToQString( file->ID3v2Tag()->frameListMap()["TCOM"].front()->toString() ).trimmed() );
537 if ( !file->ID3v2Tag()->frameListMap()["TPE2"].isEmpty() ) // non-standard: Apple, Microsoft
538 setAlbumArtist( TStringToQString( file->ID3v2Tag()->frameListMap()["TPE2"].front()->toString() ).trimmed() );
540 if ( !file->ID3v2Tag()->frameListMap()["TCMP"].isEmpty() )
541 compilation = TStringToQString( file->ID3v2Tag()->frameListMap()["TCMP"].front()->toString() ).trimmed();
543 if(images) {
544 loadImagesFromTag( *file->ID3v2Tag(), *images );
548 else if ( TagLib::Ogg::Vorbis::File *file = dynamic_cast<TagLib::Ogg::Vorbis::File *>( fileref.file() ) )
550 m_type = ogg;
551 if ( file->tag() )
553 if ( !file->tag()->fieldListMap()[ "COMPOSER" ].isEmpty() )
554 setComposer( TStringToQString( file->tag()->fieldListMap()["COMPOSER"].front() ).trimmed() );
556 if ( !file->tag()->fieldListMap()[ "BPM" ].isEmpty() )
557 setBpm( TStringToQString( file->tag()->fieldListMap()["BPM"].front() ).trimmed().toFloat() );
559 if ( !file->tag()->fieldListMap()[ "DISCNUMBER" ].isEmpty() )
560 disc = TStringToQString( file->tag()->fieldListMap()["DISCNUMBER"].front() ).trimmed();
562 if ( !file->tag()->fieldListMap()[ "COMPILATION" ].isEmpty() )
563 compilation = TStringToQString( file->tag()->fieldListMap()["COMPILATION"].front() ).trimmed();
566 else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) )
568 m_type = flac;
569 if ( file->xiphComment() )
571 if ( !file->xiphComment()->fieldListMap()[ "COMPOSER" ].isEmpty() )
572 setComposer( TStringToQString( file->xiphComment()->fieldListMap()["COMPOSER"].front() ).trimmed() );
574 if ( !file->xiphComment()->fieldListMap()[ "BPM" ].isEmpty() )
575 setBpm( TStringToQString( file->xiphComment()->fieldListMap()["BPM"].front() ).trimmed().toFloat() );
577 if ( !file->xiphComment()->fieldListMap()[ "DISCNUMBER" ].isEmpty() )
578 disc = TStringToQString( file->xiphComment()->fieldListMap()["DISCNUMBER"].front() ).trimmed();
580 if ( !file->xiphComment()->fieldListMap()[ "COMPILATION" ].isEmpty() )
581 compilation = TStringToQString( file->xiphComment()->fieldListMap()["COMPILATION"].front() ).trimmed();
584 if ( images && file->ID3v2Tag() ) {
585 loadImagesFromTag( *file->ID3v2Tag(), *images );
588 else if ( TagLib::MP4::File *file = dynamic_cast<TagLib::MP4::File *>( fileref.file() ) )
590 m_type = mp4;
591 TagLib::MP4::Tag *mp4tag = dynamic_cast<TagLib::MP4::Tag *>( file->tag() );
592 if( mp4tag )
594 setComposer( TStringToQString( mp4tag->composer() ) );
595 setBpm( QString::number( mp4tag->bpm() ).toFloat() );
596 disc = QString::number( mp4tag->disk() );
597 compilation = QString::number( mp4tag->compilation() );
598 if ( images && mp4tag->cover().size() ) {
599 images->push_back( EmbeddedImage( mp4tag->cover(), "" ) );
604 if ( !disc.isEmpty() )
606 int i = disc.indexOf('/');
607 if ( i != -1 )
608 // disc.right( i ).toInt() is total number of discs, we don't use this at the moment
609 setDiscNumber( disc.left( i ).toInt() );
610 else
611 setDiscNumber( disc.toInt() );
614 if ( compilation.isEmpty() ) {
615 // well, it wasn't set, but if the artist is VA assume it's a compilation
616 if ( artist().string() == i18n( "Various Artists" ) )
617 setCompilation( CompilationYes );
618 } else {
619 int i = compilation.toInt();
620 if ( i == CompilationNo )
621 setCompilation( CompilationNo );
622 else if ( i == CompilationYes )
623 setCompilation( CompilationYes );
626 init( fileref.audioProperties() );
629 //FIXME disabled for beta4 as it's simpler to not got 100 bug reports
630 //else if( KMimeType::findByUrl( m_url )->is( "audio" ) )
631 // init( KFileMetaInfo( m_url, QString::null, KFileMetaInfo::Everything ) );
634 void MetaBundle::updateFilesize()
636 if( !isFile() )
638 m_filesize = Undetermined;
639 return;
642 const QString path = url().path();
643 m_filesize = QFile( path ).size();
646 float MetaBundle::score( bool ensureCached ) const
648 if( m_score == Undetermined && !ensureCached )
649 //const_cast is ugly, but other option was mutable, and then we lose const correctness checking
650 //everywhere else
651 *const_cast<float*>(&m_score) = CollectionDB::instance()->getSongPercentage( m_url.path() );
652 return m_score;
655 int MetaBundle::rating( bool ensureCached ) const
657 if( m_rating == Undetermined && !ensureCached )
658 *const_cast<int*>(&m_rating) = CollectionDB::instance()->getSongRating( m_url.path() );
659 return m_rating;
662 int MetaBundle::playCount( bool ensureCached ) const
664 if( m_playCount == Undetermined && !ensureCached )
665 *const_cast<int*>(&m_playCount) = CollectionDB::instance()->getPlayCount( m_url.path() );
666 return m_playCount;
669 uint MetaBundle::lastPlay( bool ensureCached ) const
671 if( (int)m_lastPlay == abs(Undetermined) && !ensureCached )
672 *const_cast<uint*>(&m_lastPlay) = CollectionDB::instance()->getLastPlay( m_url.path() ).toTime_t();
673 return m_lastPlay;
676 void MetaBundle::copyFrom( const MetaBundle &bundle )
678 setTitle( bundle.title() );
679 setArtist( bundle.artist() );
680 setAlbumArtist( bundle.albumArtist() );
681 setComposer( bundle.composer() );
682 setAlbum( bundle.album() );
683 setYear( bundle.year() );
684 setDiscNumber( bundle.discNumber() );
685 setBpm( bundle.bpm() );
686 setComment( bundle.comment() );
687 setGenre( bundle.genre() );
688 setTrack( bundle.track() );
689 setLength( bundle.length() );
690 setBitrate( bundle.bitrate() );
691 setSampleRate( bundle.sampleRate() );
692 setScore( bundle.score() );
693 setRating( bundle.rating() );
694 setPlayCount( bundle.playCount() );
695 setLastPlay( bundle.lastPlay() );
696 setFileType( bundle.fileType() );
697 setFilesize( bundle.filesize() );
698 if( bundle.m_podcastBundle )
699 setPodcastBundle( *bundle.m_podcastBundle );
700 else
702 delete m_podcastBundle;
703 m_podcastBundle = 0;
706 if( bundle.m_lastFmBundle )
707 setLastFmBundle( *bundle.m_lastFmBundle );
708 else
710 delete m_lastFmBundle;
711 m_lastFmBundle = 0;
715 void MetaBundle::copyFrom( const PodcastEpisodeBundle &peb )
717 setPodcastBundle( peb );
718 setTitle( peb.title() );
719 setArtist( peb.author() );
720 PodcastChannelBundle pcb;
721 if( CollectionDB::instance()->getPodcastChannelBundle( peb.parent(), &pcb ) )
723 if( !pcb.title().isEmpty() )
724 setAlbum( pcb.title() );
726 setGenre( QString ( "Podcast" ) );
729 void MetaBundle::setExactText( int column, const QString &newText )
731 switch( column )
733 case Title: setTitle( newText ); break;
734 case Artist: setArtist( newText ); break;
735 case AlbumArtist: setAlbumArtist( newText ); break;
736 case Composer: setComposer( newText ); break;
737 case Year: setYear( newText.toInt() ); break;
738 case Album: setAlbum( newText ); break;
739 case DiscNumber: setDiscNumber( newText.toInt() ); break;
740 case Track: setTrack( newText.toInt() ); break;
741 case Bpm: setBpm( newText.toFloat() ); break;
742 case Genre: setGenre( newText ); break;
743 case Comment: setComment( newText ); break;
744 case Length: setLength( newText.toInt() ); break;
745 case Bitrate: setBitrate( newText.toInt() ); break;
746 case SampleRate: setSampleRate( newText.toInt() ); break;
747 case Score: setScore( newText.toFloat() ); break;
748 case Rating: setRating( newText.toInt() ); break;
749 case PlayCount: setPlayCount( newText.toInt() ); break;
750 case LastPlayed: setLastPlay( newText.toInt() ); break;
751 case Filesize: setFilesize( newText.toInt() ); break;
752 case Type: setFileType( newText.toInt() ); break;
753 default: warning() << "Tried to set the text of an immutable or nonexistent column! [" << column;
757 QString MetaBundle::exactText( int column, bool ensureCached ) const
759 switch( column )
761 case Filename: return filename();
762 case Title: return title();
763 case Artist: return artist();
764 case AlbumArtist: return albumArtist();
765 case Composer: return composer();
766 case Year: return QString::number( year() );
767 case Album: return album();
768 case DiscNumber: return QString::number( discNumber() );
769 case Track: return QString::number( track() );
770 case Bpm: return QString::number( bpm() );
771 case Genre: return genre();
772 case Comment: return comment();
773 case Directory: return directory();
774 case Type: return QString::number( fileType() );
775 case Length: return QString::number( length() );
776 case Bitrate: return QString::number( bitrate() );
777 case SampleRate: return QString::number( sampleRate() );
778 case Score: return QString::number( score( ensureCached ) );
779 case Rating: return QString::number( rating( ensureCached ) );
780 case PlayCount: return QString::number( playCount( ensureCached ) );
781 case LastPlayed: return QString::number( lastPlay( ensureCached ) );
782 case Filesize: return QString::number( filesize() );
783 case Mood: return QString();
784 default: warning() << "Tried to get the text of a nonexistent column! [" << column;
787 return QString(); //shouldn't happen
790 QString MetaBundle::prettyText( int column ) const
792 QString text;
793 switch( column )
795 case Filename: text = isFile() ? MetaBundle::prettyTitle(filename()) : url().prettyUrl(); break;
796 case Title: text = title().isEmpty() ? MetaBundle::prettyTitle( filename() ) : title(); break;
797 case Artist: text = artist(); break;
798 case AlbumArtist: text = albumArtist(); break;
799 case Composer: text = composer(); break;
800 case Year: text = year() ? QString::number( year() ) : QString::null; break;
801 case Album: text = album(); break;
802 case DiscNumber: text = discNumber() ? QString::number( discNumber() ) : QString::null; break;
803 case Bpm: text = bpm() ? QString::number( bpm() ) : QString::null; break;
804 case Track: text = track() ? QString::number( track() ) : QString::null; break;
805 case Genre: text = genre(); break;
806 case Comment: text = comment(); break;
807 case Directory: text = url().isEmpty() ? QString() : directory(); break;
808 case Type: text = url().isEmpty() ? QString() : type(); break;
809 case Length: text = prettyLength( length(), true ); break;
810 case Bitrate: text = prettyBitrate( bitrate() ); break;
811 case SampleRate: text = prettySampleRate(); break;
812 case Score: text = QString::number( static_cast<int>( score() ) ); break;
813 case Rating: text = prettyRating(); break;
814 case PlayCount: text = QString::number( playCount() ); break;
815 case LastPlayed: text = Amarok::verboseTimeSince( lastPlay() ); break;
816 case Filesize: text = prettyFilesize(); break;
817 case Mood:
818 text = moodbar_const().state() == Moodbar::JobRunning ? i18n( "Calculating..." )
819 : moodbar_const().state() == Moodbar::JobQueued ? i18n( "Queued..." )
820 : QString::null;
821 break;
822 default: warning() << "Tried to get the text of a nonexistent column!"; break;
825 return text.trimmed();
828 bool MetaBundle::matchesSimpleExpression( const QString &expression, const Q3ValueList<int> &columns ) const
830 const QStringList terms = QString( expression.toLower() ).split( ' ' );
831 bool matches = true;
832 for( int x = 0; matches && x < terms.count(); ++x )
834 int y = 0, n = columns.count();
835 for(; y < n; ++y )
836 if ( prettyText( columns[y] ).toLower().count( terms[x] ) )
837 break;
838 matches = ( y < n );
841 return matches;
844 void MetaBundle::reactToChanges( const Q3ValueList<int>& columns)
846 // mark search dirty if we need to
847 for (int i = 0; !m_isSearchDirty && i < columns.count(); i++)
848 if ((m_searchColumns & (1 << columns[i])) > 0)
849 m_isSearchDirty = true;
852 bool MetaBundle::matchesFast(const QStringList &terms, ColumnMask columnMask) const
854 // simple search for rating, last played, etc. makes no sense and it hurts us a
855 // lot if we have to fetch it from the db. so zero them out
856 columnMask &= ~( 1<<Score | 1<<Rating | 1<<PlayCount | 1<<LastPlayed | 1<<Mood );
858 if (m_isSearchDirty || m_searchColumns != columnMask) {
859 // assert the size of ColumnMask is large enough. In the absence of
860 // a compile assert mechanism, this is pretty much as good for
861 // optimized code (ie, free)
863 if ( sizeof(ColumnMask) < (NUM_COLUMNS / 8) ) {
864 warning() << "ColumnMask is not big enough!\n";
867 // recompute search text
868 // There is potential for mishap here if matchesFast gets called from multiple
869 // threads, but it's *highly* unlikely that something bad will happen
870 m_isSearchDirty = false;
871 m_searchColumns = columnMask;
872 m_searchStr.resize(0);
874 for (int i = 0; i < NUM_COLUMNS; i++) {
875 if ((columnMask & (1 << i)) > 0) {
876 if (!m_searchStr.isEmpty()) m_searchStr += ' ';
877 m_searchStr += prettyText(i).toLower();
882 // now search
883 for (int i = 0; i < terms.count(); i++) {
884 if (!m_searchStr.count(terms[i])) return false;
887 return true;
891 bool MetaBundle::matchesExpression( const QString &expression, const Q3ValueList<int> &defaultColumns ) const
893 return matchesParsedExpression( ExpressionParser::parse( expression ), defaultColumns );
896 bool MetaBundle::matchesParsedExpression( const ParsedExpression &data, const Q3ValueList<int> &defaults ) const
898 for( uint i = 0, n = data.count(); i < n; ++i ) //check each part for matchiness
900 bool b = false; //whether at least one matches
901 for( uint ii = 0, count = data[i].count(); ii < count; ++ii )
903 expression_element e = data[i][ii];
904 int column = -1;
905 if( !e.field.isEmpty() )
907 QString field = e.field.toLower();
908 column = columnIndex( field );
909 if( column == -1 )
911 column = -2;
912 if( field == "size" )
913 field = "filesize";
914 else if( field == "filetype" )
915 field = "type";
916 else if( field == "disc" )
917 field = "discnumber";
918 else
919 column = -1;
921 if( column == -2 )
922 column = columnIndex( field );
925 if( column >= 0 ) //a field was specified and it exists
927 QString q = e.text, v = prettyText( column ).toLower(), w = q.toLower();
928 //q = query, v = contents of the field, w = match against it
929 bool condition; //whether it matches, not taking e.negateation into account
931 bool numeric;
932 switch( column )
934 case Year:
935 case DiscNumber:
936 case Track:
937 case Bpm:
938 case Bitrate:
939 case SampleRate:
940 case Score:
941 case PlayCount:
942 case LastPlayed:
943 case Filesize:
944 numeric = true;
945 break;
946 default:
947 numeric = false;
950 if( column == Filesize )
952 v = QString::number( filesize() );
953 if( w.endsWith( "m" ) )
954 w = QString::number( w.left( w.length()-1 ).toLong() * 1024 * 1024 );
955 else if( w.endsWith( "k" ) )
956 w = QString::number( w.left( w.length()-1 ).toLong() * 1024 );
959 if( e.match == expression_element::More )
961 if( numeric )
962 condition = v.toInt() > w.toInt();
963 else if( column == Rating )
964 condition = v.toFloat() > w.toFloat();
965 else if( column == Length )
967 int g = v.indexOf( ':' ), h = w.indexOf( ':' );
968 condition = v.left( g ).toInt() > w.left( h ).toInt() ||
969 ( v.left( g ).toInt() == w.left( h ).toInt() &&
970 v.mid( g + 1 ).toInt() > w.mid( h + 1 ).toInt() );
972 else
973 condition = v > w; //compare the strings
975 else if( e.match == expression_element::Less )
977 if( numeric )
978 condition = v.toInt() < w.toInt();
979 else if( column == Rating )
980 condition = v.toFloat() < w.toFloat();
981 else if( column == Length )
983 int g = v.indexOf( ':' ), h = w.indexOf( ':' );
984 condition = v.left( g ).toInt() < w.left( h ).toInt() ||
985 ( v.left( g ).toInt() == w.left( h ).toInt() &&
986 v.mid( g + 1 ).toInt() < w.mid( h + 1 ).toInt() );
988 else
989 condition = v < w;
991 else
993 if( numeric )
994 condition = v.toInt() == w.toInt();
995 else if( column == Rating )
996 condition = v.toFloat() == w.toFloat();
997 else if( column == Length )
999 int g = v.indexOf( ':' ), h = w.indexOf( ':' );
1000 condition = v.left( g ).toInt() == w.left( h ).toInt() &&
1001 v.mid( g + 1 ).toInt() == w.mid( h + 1 ).toInt();
1003 else
1004 condition = v.count( q, Qt::CaseInsensitive );
1006 if( condition == ( e.negate ? false : true ) )
1008 b = true;
1009 break;
1012 else //check just the default fields
1014 for( int it = 0, end = defaults.size(); it != end; ++it )
1016 b = prettyText( defaults[it] ).count( e.text, Qt::CaseInsensitive ) == ( e.negate ? false : true );
1017 if( ( e.negate && !b ) || ( !e.negate && b ) )
1018 break;
1020 if( b )
1021 break;
1024 if( !b )
1025 return false;
1028 return true;
1031 QString
1032 MetaBundle::prettyTitle() const
1034 QString s = artist();
1036 //NOTE this gets regressed often, please be careful!
1037 // whatever you do, handle the stream case, streams have no artist but have an excellent title
1039 //FIXME doesn't work for resume playback
1041 if( s.isEmpty() )
1042 s = title();
1043 else
1044 s = i18n("%1 - %2", artist(), title() );
1046 if( s.isEmpty() ) s = prettyTitle( filename() );
1048 return s;
1051 QString
1052 MetaBundle::veryNiceTitle() const
1054 QString s;
1055 //NOTE I'm not sure, but the notes and FIXME's in the prettyTitle function should be fixed now.
1056 // If not then they do apply to this function also!
1057 if( !title().isEmpty() )
1059 if( !artist().isEmpty() )
1060 s = i18n( "%1 by %2", title(), artist() );
1061 else
1062 s = title();
1064 else
1066 s = prettyTitle( filename() );
1068 return s;
1071 QString
1072 MetaBundle::prettyTitle( const QString &filename ) //static
1074 QString s = filename; //just so the code is more readable
1076 //remove .part extension if it exists
1077 if (s.endsWith( ".part" ))
1078 s = s.left( s.length() - 5 );
1080 //remove file extension, s/_/ /g and decode %2f-like sequences
1081 s = s.left( s.lastIndexOf( '.' ) ).replace( '_', ' ' );
1082 s = QUrl::fromPercentEncoding( s.toAscii() );
1084 return s;
1087 QString
1088 MetaBundle::prettyLength( int seconds, bool showHours ) //static
1090 if( seconds > 0 ) return prettyTime( seconds, showHours );
1091 if( seconds == Undetermined ) return "?";
1092 if( seconds == Irrelevant ) return "-";
1094 return QString(); //Unavailable = ""
1097 QString
1098 MetaBundle::prettyTime( uint seconds, bool showHours ) //static
1100 QString s = QChar( ':' );
1101 s.append( zeroPad( seconds % 60 ) ); //seconds
1102 seconds /= 60;
1104 if( showHours && seconds >= 60)
1106 s.prepend( zeroPad( seconds % 60 ) ); //minutes
1107 s.prepend( ':' );
1108 seconds /= 60;
1111 //don't zeroPad the last one, as it can be greater than 2 digits
1112 s.prepend( QString::number( seconds ) ); //hours or minutes depending on above if block
1114 return s;
1117 QString
1118 MetaBundle::veryPrettyTime( int time )
1120 if( time == Undetermined )
1121 return i18n( "?" );
1122 if( time == Irrelevant )
1123 return i18n( "-" );
1125 QStringList s;
1126 s << QString::number( time % 60 ); //seconds
1127 time /= 60;
1128 if( time )
1129 s << QString::number( time % 60 ); //minutes
1130 time /= 60;
1131 if( time )
1132 s << QString::number( time % 24 ); //hours
1133 time /= 24;
1134 if( time )
1135 s << QString::number( time ); //days
1137 switch( s.count() )
1139 // xgettext: no-c-format
1140 case 1: return i18nc( "seconds", "%1s", s[0] );
1141 // xgettext: no-c-format
1142 case 2: return i18nc( "minutes, seconds", "%2m %1s", s[0], s[1] );
1143 // xgettext: no-c-format
1144 case 3: return i18nc( "hours, minutes, seconds", "%3h %2m %1s", s[0], s[1], s[2] );
1145 // xgettext: no-c-format
1146 case 4: return i18nc( "days, hours, minutes, seconds", "%4d %3h %2m %1s", s[0], s[1], s[2], s[3] );
1147 default: return "omg bug!";
1151 QString
1152 MetaBundle::fuzzyTime( int time )
1154 QString s;
1155 int secs=0, min=0, hr=0, day=0, week=0;
1157 if( time == Undetermined )
1158 return i18n( "?" );
1159 if( time == Irrelevant )
1160 return i18n( "-" );
1162 secs = time % 60; //seconds
1163 time /= 60;
1164 min = time % 60; //minutes
1165 time /= 60;
1166 hr = time % 24 ; //hours
1167 time /= 24;
1168 day = time % 7 ; //days
1169 time /= 7;
1170 week = time; //weeks
1172 if( week && hr >= 12 )
1174 day++;
1175 if( day == 7 )
1177 week++;
1178 day = 0;
1181 else if( day && min >= 30 )
1183 hr++;
1184 if( hr == 24 )
1186 day++;
1187 hr = 0;
1190 else if( hr && secs >= 30 )
1192 min++;
1193 if( min == 60 )
1195 hr++;
1196 min = 0;
1200 QString weeks = i18np( "1 week ", "%1 weeks ", week );
1201 QString days = i18np( "1 day ", "%1 days ", day );
1202 QString hours = i18np( "1 hour", "%1 hours", hr );
1204 if( week )
1205 return ( weeks + ( day ? days.arg("") : "" ) ).simplified();
1206 else if ( day )
1207 return ( days + ( hr ? hours : "" ) ).simplified();
1208 else if ( hr )
1209 return i18n( "%1:%2 hours", hr, zeroPad( min ) );
1210 else
1211 return i18n( "%1:%2", min,zeroPad( secs ) );
1214 QString
1215 MetaBundle::prettyBitrate( int i )
1217 //the point here is to force sharing of these strings returned from prettyBitrate()
1218 static const QString bitrateStore[9] = {
1219 "?", "32", "64", "96", "128", "160", "192", "224", "256" };
1221 return (i >=0 && i <= 256 && i % 32 == 0)
1222 ? bitrateStore[ i / 32 ]
1223 : prettyGeneric( "%1", i );
1226 QString
1227 MetaBundle::prettyFilesize( int s )
1229 return KIO::convertSize( s );
1232 QString
1233 MetaBundle::prettyRating( int r, bool trailingzero ) //static
1235 if( trailingzero )
1236 return QString::number( float( r ) / 2, 'f', 1 );
1237 else
1238 return r ? QString::number( float( r ) / 2 ) : QString();
1241 QString
1242 MetaBundle::ratingDescription( int r )
1244 switch( r )
1246 case 2: return i18n( "Awful" );
1247 case 3: return i18n( "Barely tolerable" );
1248 case 4: return i18n( "Tolerable" );
1249 case 5: return i18n( "Okay" );
1250 case 6: return i18n( "Good" );
1251 case 7: return i18n( "Very good" );
1252 case 8: return i18n( "Excellent" );
1253 case 9: return i18n( "Amazing" );
1254 case 10: return i18n( "Favorite" );
1255 case 0: default: return i18n( "Not rated" ); // assume weird values as not rated
1257 return "if you can see this, then that's a bad sign.";
1260 QStringList
1261 MetaBundle::ratingList()
1263 QStringList list;
1264 list += ratingDescription( 0 );
1265 for ( int i = 2; i<=10; i++ )
1266 list += i18nc( "rating - description", "%1 - %2", prettyRating( i, true ), ratingDescription( i ) );
1267 return list;
1270 QStringList
1271 MetaBundle::genreList() //static
1273 QStringList list;
1275 TagLib::StringList genres = TagLib::ID3v1::genreList();
1276 for( TagLib::StringList::ConstIterator it = genres.begin(), end = genres.end(); it != end; ++it )
1277 list += TStringToQString( (*it) );
1279 list.sort();
1281 return list;
1284 void
1285 MetaBundle::setExtendedTag( TagLib::File *file, int tag, const QString value )
1287 char *id = 0;
1289 if ( m_type == mp3 )
1291 switch( tag )
1293 case ( composerTag ): id = "TCOM"; break;
1294 case ( discNumberTag ): id = "TPOS"; break;
1295 case ( bpmTag ): id = "TBPM"; break;
1296 case ( compilationTag ): id = "TCMP"; break;
1297 case ( albumArtistTag ): id = "TPE2"; break; // non-standard: Apple, Microsoft
1298 default: return; //don't write the tag if it is unknown
1300 fprintf(stderr, "Setting extended tag %s to %s\n", id, value.toUtf8().data());
1301 TagLib::MPEG::File *mpegFile = dynamic_cast<TagLib::MPEG::File *>( file );
1302 if ( mpegFile && mpegFile->ID3v2Tag() )
1304 if ( value.isEmpty() )
1305 mpegFile->ID3v2Tag()->removeFrames( id );
1306 else
1308 if( !mpegFile->ID3v2Tag()->frameListMap()[id].isEmpty() )
1309 mpegFile->ID3v2Tag()->frameListMap()[id].front()->setText( QStringToTString( value ) );
1310 else
1312 TagLib::ID3v2::TextIdentificationFrame *frame = new TagLib::ID3v2::TextIdentificationFrame( id, TagLib::ID3v2::FrameFactory::instance()->defaultTextEncoding() );
1313 frame->setText( QStringToTString( value ) );
1314 mpegFile->ID3v2Tag()->addFrame( frame );
1319 else if ( m_type == ogg )
1321 switch( tag )
1323 case ( composerTag ): id = "COMPOSER"; break;
1324 case ( discNumberTag ): id = "DISCNUMBER"; break;
1325 case ( bpmTag ): id = "BPM"; break;
1326 case ( compilationTag ): id = "COMPILATION"; break;
1327 case ( albumArtistTag ): id = "ALBUMARTIST"; break; // non-standard: Amarok
1328 default: return; //don't write the tag if it is unknown
1330 TagLib::Ogg::Vorbis::File *oggFile = dynamic_cast<TagLib::Ogg::Vorbis::File *>( file );
1331 if ( oggFile && oggFile->tag() )
1333 value.isEmpty() ?
1334 oggFile->tag()->removeField( id ):
1335 oggFile->tag()->addField( id, QStringToTString( value ), true );
1338 else if ( m_type == flac )
1340 switch( tag )
1342 case ( composerTag ): id = "COMPOSER"; break;
1343 case ( discNumberTag ): id = "DISCNUMBER"; break;
1344 case ( bpmTag ): id = "BPM"; break;
1345 case ( compilationTag ): id = "COMPILATION"; break;
1346 case ( albumArtistTag ): id = "ALBUMARTIST"; break; // non-standard: Amarok
1347 default: return; //don't write the tag if it is unknown
1349 TagLib::FLAC::File *flacFile = dynamic_cast<TagLib::FLAC::File *>( file );
1350 if ( flacFile && flacFile->xiphComment() )
1352 value.isEmpty() ?
1353 flacFile->xiphComment()->removeField( id ):
1354 flacFile->xiphComment()->addField( id, QStringToTString( value ), true );
1357 else if ( m_type == mp4 )
1359 TagLib::MP4::Tag *mp4tag = dynamic_cast<TagLib::MP4::Tag *>( file->tag() );
1360 if( mp4tag )
1362 switch( tag )
1364 case ( composerTag ): mp4tag->setComposer( QStringToTString( value ) ); break;
1365 case ( discNumberTag ): mp4tag->setDisk( value.toInt() );
1366 case ( bpmTag ): mp4tag->setBpm( value.toInt() ); // mp4 doesn't support float bpm
1367 case ( compilationTag ): mp4tag->setCompilation( value.toInt() == CompilationYes );
1373 void
1374 MetaBundle::setPodcastBundle( const PodcastEpisodeBundle &peb )
1376 delete m_podcastBundle;
1377 m_podcastBundle = new PodcastEpisodeBundle;
1378 *m_podcastBundle = peb;
1381 void
1382 MetaBundle::setLastFmBundle( const LastFm::Bundle &last )
1384 delete m_lastFmBundle;
1385 // m_lastFmBundle = new LastFm::Bundle(last);
1386 m_lastFmBundle = new LastFm::Bundle;
1387 *m_lastFmBundle = last;
1390 void MetaBundle::loadImagesFromTag( const TagLib::ID3v2::Tag &tag, EmbeddedImageList& images ) const
1392 TagLib::ID3v2::FrameList l = tag.frameListMap()[ "APIC" ];
1393 oldForeachType( TagLib::ID3v2::FrameList, l ) {
1394 debug() << "Found APIC frame";
1395 TagLib::ID3v2::AttachedPictureFrame *ap = static_cast<TagLib::ID3v2::AttachedPictureFrame*>( *it );
1397 const TagLib::ByteVector &imgVector = ap->picture();
1398 debug() << "Size of image: " << imgVector.size() << " byte";
1399 // ignore APIC frames without picture and those with obviously bogus size
1400 if( imgVector.size() > 0 && imgVector.size() < 10000000 /*10MB*/ ) {
1401 images.push_back( EmbeddedImage( imgVector, ap->description() ) );
1406 bool
1407 MetaBundle::safeSave()
1409 /*bool noproblem;
1410 MetaBundleSaver mbs( this );
1411 TagLib::FileRef* fileref = mbs.prepareToSave();
1412 if( !fileref )
1414 debug() << "Could not get a fileref!";
1415 mbs.cleanupSave();
1416 return false;
1419 noproblem = save( fileref );
1421 if( !noproblem )
1423 debug() << "MetaBundle::save() didn't work!";
1424 mbs.cleanupSave();
1425 return false;
1428 noproblem = mbs.doSave();
1430 if( !noproblem )
1432 debug() << "Something failed during the save, cleaning up and exiting!";
1433 mbs.cleanupSave();
1434 return false;
1437 setUniqueId( readUniqueId() );
1438 if( CollectionDB::instance()->isFileInCollection( url().path() ) )
1439 CollectionDB::instance()->doAFTStuff( this, false );
1441 noproblem = mbs.cleanupSave();
1443 return noproblem;*/
1444 return true;
1447 bool
1448 MetaBundle::save( TagLib::FileRef* fileref )
1450 DEBUG_BLOCK
1451 if( !isFile() )
1452 return false;
1454 //Set default codec to UTF-8 (see bugs 111246 and 111232)
1455 TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF8);
1457 bool passedin = fileref;
1458 bool returnval = false;
1460 TagLib::FileRef* f;
1462 if( !passedin )
1463 f = new TagLib::FileRef( QFile::encodeName( url().path() ).data(), false );
1464 else
1465 f = fileref;
1467 if ( f && !f->isNull() )
1469 TagLib::Tag * t = f->tag();
1470 if ( t ) { // f.tag() can return null if the file couldn't be opened for writing
1471 t->setTitle( QStringToTString( title().trimmed() ) );
1472 t->setArtist( QStringToTString( artist().string().trimmed() ) );
1473 t->setAlbum( QStringToTString( album().string().trimmed() ) );
1474 t->setTrack( track() );
1475 t->setYear( year() );
1476 t->setComment( QStringToTString( comment().string().trimmed() ) );
1477 t->setGenre( QStringToTString( genre().string().trimmed() ) );
1479 if ( hasExtendedMetaInformation() )
1481 setExtendedTag( f->file(), albumArtistTag, albumArtist() );
1482 setExtendedTag( f->file(), composerTag, composer().string().trimmed() );
1483 setExtendedTag( f->file(), discNumberTag, discNumber() ? QString::number( discNumber() ) : QString() );
1484 setExtendedTag( f->file(), bpmTag, bpm() ? QString::number( bpm() ) : QString() );
1485 if ( compilation() != CompilationUnknown )
1486 setExtendedTag( f->file(), compilationTag, QString::number( compilation() ) );
1488 if( !passedin )
1490 returnval = f->save();
1491 setUniqueId( readUniqueId() );
1492 if( returnval && CollectionDB::instance()->isFileInCollection( url().path() ) )
1493 CollectionDB::instance()->doAFTStuff( this, false );
1495 else
1496 returnval = true;
1499 if ( !passedin )
1500 delete f;
1502 return returnval;
1505 bool MetaBundle::save( QTextStream &stream, const QStringList &attributes ) const
1507 QDomDocument qDomSucksItNeedsADocument;
1508 QDomElement item = qDomSucksItNeedsADocument.createElement( "item" );
1509 item.setAttribute( "url", url().url() );
1510 item.setAttribute( "uniqueid", uniqueId() );
1511 if( m_isCompilation )
1512 item.setAttribute( "compilation", "true" );
1514 for( int i = 0, n = attributes.count(); i < n; i += 2 )
1515 item.setAttribute( attributes[i], attributes[i+1] );
1517 for( int i = 0; i < NUM_COLUMNS; ++i )
1519 QDomElement tag = qDomSucksItNeedsADocument.createElement( exactColumnName( i ) );
1520 //debug() << "exactColumName(i) = " << exactColumnName( i );
1521 QDomText text = qDomSucksItNeedsADocument.createTextNode( exactText( i, true ) );
1522 //debug() << "exactText(i) = " << exactText( i );
1523 tag.appendChild( text );
1525 item.appendChild( tag );
1528 item.save( stream, 1 );
1529 return true;
1532 void MetaBundle::setUrl( const KUrl &url )
1534 Q3ValueList<int> changes;
1535 for( int i = 0; i < NUM_COLUMNS; ++i ) changes << i;
1536 aboutToChange( changes ); m_url = url; reactToChanges( changes );
1538 setUniqueId();
1541 void MetaBundle::setPath( const QString &path )
1543 Q3ValueList<int> changes;
1544 for( int i = 0; i < NUM_COLUMNS; ++i ) changes << i;
1545 aboutToChange( changes ); m_url.setPath( path ); reactToChanges( changes );
1547 setUniqueId();
1550 void MetaBundle::setUniqueId()
1552 //if the file isn't already in the database, not checking for amarokcollectionscanner
1553 //will result in the UID being set to QString::null during the scan...bad!
1554 if( !isFile() )
1555 return;
1557 m_uniqueId = CollectionDB::instance()->uniqueIdFromUrl( url() );
1560 void MetaBundle::setUniqueId( const QString &id )
1562 //WARNING WARNING WARNING
1563 //Don't call this function if you don't know what you're doing!
1564 m_uniqueId = id;
1567 const TagLib::ByteVector
1568 MetaBundle::readUniqueIdHelper( TagLib::FileRef fileref ) const
1570 if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) )
1572 if( file->ID3v2Tag() )
1573 return file->ID3v2Tag()->render();
1574 else if( file->ID3v1Tag() )
1575 return file->ID3v1Tag()->render();
1576 else if( file->APETag() )
1577 return file->APETag()->render();
1579 else if ( TagLib::Ogg::Vorbis::File *file = dynamic_cast<TagLib::Ogg::Vorbis::File *>( fileref.file() ) )
1581 if( file->tag() )
1582 return file->tag()->render();
1584 else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) )
1586 if( file->ID3v2Tag() )
1587 return file->ID3v2Tag()->render();
1588 else if( file->ID3v1Tag() )
1589 return file->ID3v1Tag()->render();
1590 else if( file->xiphComment() )
1591 return file->xiphComment()->render();
1593 else if ( TagLib::Ogg::FLAC::File *file = dynamic_cast<TagLib::Ogg::FLAC::File *>( fileref.file() ) )
1595 if( file->tag() )
1596 return file->tag()->render();
1598 else if ( TagLib::MPC::File *file = dynamic_cast<TagLib::MPC::File *>( fileref.file() ) )
1600 if( file->ID3v1Tag() )
1601 return file->ID3v1Tag()->render();
1602 else if( file->APETag() )
1603 return file->APETag()->render();
1605 TagLib::ByteVector bv;
1606 return bv;
1609 QString
1610 MetaBundle::readUniqueId( TagLib::FileRef* fileref )
1612 //This is used in case we don't get given a fileref
1613 TagLib::FileRef tmpfileref;
1615 if( !fileref && isFile() )
1617 const QString path = url().path();
1618 //Make it get cleaned up at the end of the function automagically
1619 tmpfileref = TagLib::FileRef( QFile::encodeName( path ).data(), true, TagLib::AudioProperties::Fast );
1620 fileref = &tmpfileref;
1623 if( !fileref || fileref->isNull() )
1624 return QString();
1626 TagLib::ByteVector bv = readUniqueIdHelper( *fileref );
1628 //get our unique id
1629 KMD5 md5( 0, 0 );
1631 QFile qfile( url().path() );
1633 char databuf[8192];
1634 int readlen = 0;
1635 QByteArray size = 0;
1636 QString returnval;
1638 md5.update( bv.data(), bv.size() );
1640 if( qfile.open( QIODevice::Unbuffered | QIODevice::ReadOnly ) )
1642 if( ( readlen = qfile.read( databuf, 8192 ) ) > 0 )
1644 md5.update( databuf, readlen );
1645 md5.update( size.setNum( qfile.size() ) );
1646 return QString( md5.hexDigest().data() );
1648 else
1649 return QString();
1652 return QString();
1656 MetaBundle::getRand()
1658 return KRandom::random();
1661 QString
1662 MetaBundle::getRandomString( int size, bool numbersOnly )
1664 if( size != 8 )
1666 debug() << "Wrong size passed in!";
1667 return QString();
1670 QString str;
1671 //do a memory op once, much faster than doing multiple later, especially since we know how big it will be
1672 str.reserve( size );
1673 int i = getRand(); //seed it
1674 i = 0;
1675 while (size--)
1677 // check your ASCII tables
1678 // we want characters you can see...93 is the range from ! to ~
1679 int r=rand() % 94;
1680 // shift the value to the visible characters
1681 r+=33;
1682 // we don't want ", %, ', <, >, \, `, or &
1683 // so that we don't have issues with escaping/quoting in QStrings,
1684 // and so that we don't have <> in our XML files where they might cause issues
1685 // hopefully this list is final, as once users really start using this
1686 // it will be a pain to change...however, there is an ATF version in CollectionDB
1687 // which will help if this ever needs to change
1688 // In addition we can change our vendor string
1689 while ( r==34 || r==37 || r == 38 || r==39 || r==60 ||r == 62 || r==92 || r==96 )
1690 r++;
1692 if( numbersOnly && ( r < 48 || r > 57 ) )
1694 size++;
1695 continue;
1698 str[i++] = char(r);
1699 // this next comment kept in for fun, as it was from the source of KRandomString, where I got
1700 // most of this code from to start with :-)
1701 // so what if I work backwards?
1703 return str;
1706 void MetaBundle::setTitle( const QString &title )
1707 { aboutToChange( Title ); m_title = title; reactToChange( Title ); }
1709 void MetaBundle::setArtist( const AtomicString &artist )
1710 { aboutToChange( Artist ); m_artist = artist; reactToChange( Artist ); }
1712 void MetaBundle::setAlbum( const AtomicString &album )
1713 { aboutToChange( Album ); m_album = album; reactToChange( Album ); }
1715 void MetaBundle::setComment( const AtomicString &comment )
1716 { aboutToChange( Comment ); m_comment = comment; reactToChange( Comment ); }
1718 void MetaBundle::setGenre( const AtomicString &genre )
1719 { aboutToChange( Genre ); m_genre = genre; reactToChange( Genre ); }
1721 void MetaBundle::setYear( int year)
1722 { aboutToChange( Year ); m_year = year; reactToChange( Year ); }
1724 void MetaBundle::setTrack( int track )
1725 { aboutToChange( Track ); m_track = track; reactToChange( Track ); }
1727 void MetaBundle::setCompilation( int compilation )
1729 switch( compilation )
1731 case CompilationYes:
1732 m_isCompilation = true;
1733 m_notCompilation = false;
1734 break;
1735 case CompilationNo:
1736 m_isCompilation = false;
1737 m_notCompilation = true;
1738 break;
1739 case CompilationUnknown:
1740 m_isCompilation = m_notCompilation = false;
1741 break;
1745 void MetaBundle::setLength( int length )
1746 { aboutToChange( Length ); m_length = length; reactToChange( Length ); }
1748 void MetaBundle::setBitrate( int bitrate )
1749 { aboutToChange( Bitrate ); m_bitrate = bitrate; reactToChange( Bitrate ); }
1751 void MetaBundle::setSampleRate( int sampleRate )
1752 { aboutToChange( SampleRate ); m_sampleRate = sampleRate; reactToChange( SampleRate ); }
1754 void MetaBundle::setDiscNumber( int discnumber )
1755 { aboutToChange( DiscNumber ); m_discNumber = discnumber; reactToChange( DiscNumber ); }
1757 void MetaBundle::setBpm( float bpm )
1758 { aboutToChange( Bpm ); m_bpm = bpm; reactToChange( Bpm ); }
1760 void MetaBundle::setComposer( const AtomicString &composer )
1761 { aboutToChange( Composer ); m_composer = composer; reactToChange( Composer ); }
1763 void MetaBundle::setAlbumArtist( const AtomicString &albumArtist )
1764 { aboutToChange( AlbumArtist ); m_albumArtist = albumArtist; reactToChange( AlbumArtist ); }
1766 void MetaBundle::setPlayCount( int playcount )
1767 { aboutToChange( PlayCount ); m_playCount = playcount; reactToChange( PlayCount ); }
1769 void MetaBundle::setLastPlay( uint lastplay )
1770 { aboutToChange( LastPlayed ); m_lastPlay = lastplay; reactToChange( LastPlayed ); }
1772 void MetaBundle::setRating( int rating )
1773 { aboutToChange( Rating ); m_rating = rating; reactToChange( Rating ); }
1775 void MetaBundle::setScore( float score )
1776 { aboutToChange( Score ); m_score = score; reactToChange( Score ); }
1778 void MetaBundle::setFilesize( int bytes )
1779 { aboutToChange( Filesize ); m_filesize = bytes; reactToChange( Filesize ); }
1781 void MetaBundle::setFileType( int type ) { m_type = type; }