add mp3 and ogg torrent url info to JamendoAlbum
[amarok.git] / src / metabundle.cpp
blob5b2e881023d3bd74875f0386d81e55e2fff9b07a
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 <qdom.h>
31 #include <QFile> //decodePath()
32 //Added by qt3to4:
33 #include <Q3ValueList>
34 #include <QByteArray>
35 #include <attachedpictureframe.h>
36 #include <fileref.h>
37 #include <id3v1genres.h> //used to load genre list
38 #include <mpegfile.h>
39 #include <tag.h>
40 #include <tstring.h>
41 #include <tlist.h>
42 #include <apetag.h>
43 #include <id3v2tag.h>
44 #include <id3v1tag.h>
45 #include <mpcfile.h>
46 #include <oggfile.h>
47 #include <oggflacfile.h>
48 #include <vorbisfile.h>
49 #include <flacfile.h>
50 #include <textidentificationframe.h>
51 #include <uniquefileidentifierframe.h>
52 #include <xiphcomment.h>
54 #include "config-amarok.h"
55 #ifdef HAVE_MP4V2
56 #include "metadata/mp4/mp4file.h"
57 #include "metadata/mp4/mp4tag.h"
58 #else
59 #include "metadata/m4a/mp4file.h"
60 #include "metadata/m4a/mp4itunestag.h"
61 #endif
63 #include "lastfm.h"
64 #include "metabundle.h"
65 #include "podcastbundle.h"
68 MetaBundle::EmbeddedImage::EmbeddedImage( const TagLib::ByteVector& data, const TagLib::String& description )
69 : m_description( TStringToQString( description ) )
71 m_data = QByteArray( data.data(), data.size() );
74 const QByteArray &MetaBundle::EmbeddedImage::hash() const
76 if( m_hash.isEmpty() ) {
77 m_hash = KMD5( m_data ).hexDigest();
79 return m_hash;
82 bool MetaBundle::EmbeddedImage::save( const QDir& dir ) const
84 QFile file( dir.filePath( hash() ) );
86 if( file.open( QIODevice::WriteOnly | QIODevice::Unbuffered ) ) {
87 const Q_LONG s = file.write( m_data.data(), m_data.size() );
88 if( s >= 0 && Q_LONG( s ) == m_data.size() ) {
89 debug() << "EmbeddedImage::save " << file.fileName();
90 return true;
92 file.remove();
94 debug() << "EmbeddedImage::save failed! " << file.fileName();
95 return false;
98 /// These are untranslated and used for storing/retrieving XML playlist
99 const QString &MetaBundle::exactColumnName( int c ) //static
101 // construct static qstrings to avoid constructing them all the time
102 static QString columns[] = {
103 "Filename", "Title", "Artist", "AlbumArtist", "Composer", "Year", "Album", "DiscNumber", "Track", "BPM", "Genre", "Comment",
104 "Directory", "Type", "Length", "Bitrate", "SampleRate", "Score", "Rating", "PlayCount", "LastPlayed",
105 "Mood", "Filesize" };
106 static QString error( "ERROR" );
108 if ( c >= 0 && c < NUM_COLUMNS )
109 return columns[c];
110 else
111 return error;
114 const QString MetaBundle::prettyColumnName( int index ) //static
116 switch( index )
118 case Filename: return i18n( "Filename" );
119 case Title: return i18n( "Title" );
120 case Artist: return i18n( "Artist" );
121 case AlbumArtist:return i18n( "Album Artist");
122 case Composer: return i18n( "Composer" );
123 case Year: return i18n( "Year" );
124 case Album: return i18n( "Album" );
125 case DiscNumber: return i18n( "Disc Number" );
126 case Track: return i18n( "Track" );
127 case Bpm: return i18n( "BPM" );
128 case Genre: return i18n( "Genre" );
129 case Comment: return i18n( "Comment" );
130 case Directory: return i18n( "Directory" );
131 case Type: return i18n( "Type" );
132 case Length: return i18n( "Length" );
133 case Bitrate: return i18n( "Bitrate" );
134 case SampleRate: return i18n( "Sample Rate" );
135 case Score: return i18n( "Score" );
136 case Rating: return i18n( "Rating" );
137 case PlayCount: return i18n( "Play Count" );
138 case LastPlayed: return i18nc( "Column name", "Last Played" );
139 case Mood: return i18n( "Mood" );
140 case Filesize: return i18n( "File Size" );
142 return "This is a bug.";
145 int MetaBundle::columnIndex( const QString &name )
147 for( int i = 0; i < NUM_COLUMNS; ++i )
148 if( exactColumnName( i ).toLower() == name.toLower() )
149 return i;
150 return -1;
153 MetaBundle::MetaBundle()
154 : m_uniqueId( QString() )
155 , m_year( Undetermined )
156 , m_discNumber( Undetermined )
157 , m_track( Undetermined )
158 , m_bpm( Undetermined )
159 , m_bitrate( Undetermined )
160 , m_length( Undetermined )
161 , m_sampleRate( Undetermined )
162 , m_score( Undetermined )
163 , m_rating( Undetermined )
164 , m_playCount( Undetermined )
165 , m_lastPlay( abs( Undetermined ) )
166 , m_filesize( Undetermined )
167 , m_moodbar( 0 )
168 , m_type( other )
169 , m_exists( true )
170 , m_isValidMedia( true )
171 , m_isCompilation( false )
172 , m_notCompilation( false )
173 , m_safeToSave( false )
174 , m_waitingOnKIO( 0 )
175 , m_tempSavePath( QString() )
176 , m_origRenamedSavePath( QString() )
177 , m_tempSaveDigest( 0 )
178 , m_saveFileref( 0 )
179 , m_podcastBundle( 0 )
180 , m_lastFmBundle( 0 )
181 , m_isSearchDirty(true)
183 init();
186 MetaBundle::MetaBundle( const KUrl &url, bool noCache, TagLib::AudioProperties::ReadStyle readStyle, EmbeddedImageList* images )
187 : m_url( url )
188 , m_uniqueId( QString() )
189 , m_year( Undetermined )
190 , m_discNumber( Undetermined )
191 , m_track( Undetermined )
192 , m_bpm( Undetermined )
193 , m_bitrate( Undetermined )
194 , m_length( Undetermined )
195 , m_sampleRate( Undetermined )
196 , m_score( Undetermined )
197 , m_rating( Undetermined )
198 , m_playCount( Undetermined )
199 , m_lastPlay( abs( Undetermined ) )
200 , m_filesize( Undetermined )
201 , m_moodbar( 0 )
202 , m_type( other )
203 , m_exists( isFile() && QFile::exists( url.path() ) )
204 , m_isValidMedia( false )
205 , m_isCompilation( false )
206 , m_notCompilation( false )
207 , m_safeToSave( false )
208 , m_waitingOnKIO( 0 )
209 , m_tempSavePath( QString() )
210 , m_origRenamedSavePath( QString() )
211 , m_tempSaveDigest( 0 )
212 , m_saveFileref( 0 )
213 , m_podcastBundle( 0 )
214 , m_lastFmBundle( 0 )
215 , m_isSearchDirty(true)
217 if ( exists() )
219 if ( !noCache )
220 m_isValidMedia = CollectionDB::instance()->bundleForUrl( this );
222 if ( !isValidMedia() || ( !m_podcastBundle && m_length <= 0 ) )
223 readTags( readStyle, images );
225 else
227 // if it's a podcast we might get some info this way
228 CollectionDB::instance()->bundleForUrl( this );
229 m_bitrate = m_length = m_sampleRate = Unavailable;
233 //StreamProvider ctor
234 MetaBundle::MetaBundle( const QString& title,
235 const QString& streamUrl,
236 const int bitrate,
237 const QString& genre,
238 const QString& streamName,
239 const KUrl& url )
240 : m_url ( url )
241 , m_genre ( genre )
242 , m_streamName( streamName )
243 , m_streamUrl ( streamUrl )
244 , m_uniqueId( QString() )
245 , m_year( 0 )
246 , m_discNumber( 0 )
247 , m_track( 0 )
248 , m_bpm( Undetermined )
249 , m_bitrate( bitrate )
250 , m_length( Irrelevant )
251 , m_sampleRate( Unavailable )
252 , m_score( Undetermined )
253 , m_rating( Undetermined )
254 , m_playCount( Undetermined )
255 , m_lastPlay( abs( Undetermined ) )
256 , m_filesize( Undetermined )
257 , m_moodbar( 0 )
258 , m_type( other )
259 , m_exists( true )
260 , m_isValidMedia( false )
261 , m_isCompilation( false )
262 , m_notCompilation( false )
263 , m_safeToSave( false )
264 , m_waitingOnKIO( 0 )
265 , m_tempSavePath( QString() )
266 , m_origRenamedSavePath( QString() )
267 , m_tempSaveDigest( 0 )
268 , m_saveFileref( 0 )
269 , m_podcastBundle( 0 )
270 , m_lastFmBundle( 0 )
271 , m_isSearchDirty(true)
273 if( title.count( '-' ) )
275 m_title = title.section( '-', 1, 1 ).trimmed();
276 m_artist = title.section( '-', 0, 0 ).trimmed();
278 else
280 m_title = title;
281 m_artist = streamName; //which is sort of correct..
285 MetaBundle::MetaBundle( const MetaBundle &bundle )
286 : m_moodbar( 0 )
288 *this = bundle;
291 MetaBundle::~MetaBundle()
293 delete m_podcastBundle;
294 delete m_lastFmBundle;
296 if( m_moodbar != 0 )
297 delete m_moodbar;
300 MetaBundle&
301 MetaBundle::operator=( const MetaBundle& bundle )
303 m_url = bundle.m_url;
304 m_title = bundle.m_title;
305 m_artist = bundle.m_artist;
306 m_albumArtist = bundle.m_albumArtist;
307 m_composer = bundle.m_composer;
308 m_album = bundle.m_album;
309 m_comment = bundle.m_comment;
310 m_genre = bundle.m_genre;
311 m_streamName = bundle.m_streamName;
312 m_streamUrl = bundle.m_streamUrl;
313 m_uniqueId = bundle.m_uniqueId;
314 m_year = bundle.m_year;
315 m_discNumber = bundle.m_discNumber;
316 m_track = bundle.m_track;
317 m_bpm = bundle.m_bpm;
318 m_bitrate = bundle.m_bitrate;
319 m_length = bundle.m_length;
320 m_sampleRate = bundle.m_sampleRate;
321 m_score = bundle.m_score;
322 m_rating = bundle.m_rating;
323 m_playCount = bundle.m_playCount;
324 m_lastPlay = bundle.m_lastPlay;
325 m_filesize = bundle.m_filesize;
326 m_type = bundle.m_type;
327 m_exists = bundle.m_exists;
328 m_isValidMedia = bundle.m_isValidMedia;
329 m_isCompilation = bundle.m_isCompilation;
330 m_notCompilation = bundle.m_notCompilation;
331 m_safeToSave = bundle.m_safeToSave;
332 m_waitingOnKIO = bundle.m_waitingOnKIO;
333 m_tempSavePath = bundle.m_tempSavePath;
334 m_origRenamedSavePath = bundle.m_origRenamedSavePath;
335 m_tempSaveDigest = bundle.m_tempSaveDigest;
336 m_saveFileref = bundle.m_saveFileref;
338 if( bundle.m_moodbar != 0)
340 if( m_moodbar == 0 )
341 m_moodbar = new Moodbar( this );
342 *m_moodbar = *bundle.m_moodbar;
344 else
346 // If m_moodbar != 0, it's initialized for a reason
347 // Deleting it makes the PrettySlider code more ugly,
348 // since it'd have to reconnect the jobEvent() signal.
349 if( m_moodbar != 0 )
350 m_moodbar->reset();
354 // delete m_podcastBundle; why does this crash Amarok? apparently m_podcastBundle isn't always initialized.
355 m_podcastBundle = 0;
356 if( bundle.m_podcastBundle )
357 setPodcastBundle( *bundle.m_podcastBundle );
359 // delete m_lastFmBundle; same as above
360 m_lastFmBundle = 0;
361 if( bundle.m_lastFmBundle )
362 setLastFmBundle( *bundle.m_lastFmBundle );
364 m_isSearchDirty = true;
365 return *this;
369 bool
370 MetaBundle::checkExists()
372 debug() << "MetaBundle path is " << url().url();
373 m_exists = !isFile() || QFile::exists( url().path() );
375 return m_exists;
378 bool
379 MetaBundle::operator==( const MetaBundle& bundle ) const
381 return uniqueId() == bundle.uniqueId() && //first, since if using IDs will return faster
382 artist() == bundle.artist() &&
383 albumArtist() == bundle.albumArtist() &&
384 title() == bundle.title() &&
385 composer() == bundle.composer() &&
386 album() == bundle.album() &&
387 year() == bundle.year() &&
388 comment() == bundle.comment() &&
389 genre() == bundle.genre() &&
390 track() == bundle.track() &&
391 discNumber() == bundle.discNumber() &&
392 bpm() == bundle.bpm() &&
393 length() == bundle.length() &&
394 bitrate() == bundle.bitrate() &&
395 sampleRate() == bundle.sampleRate();
396 // FIXME: check for size equality?
399 void
400 MetaBundle::clear()
402 *this = MetaBundle();
405 void
406 MetaBundle::init( TagLib::AudioProperties *ap )
408 if ( ap )
410 m_bitrate = ap->bitrate();
411 m_length = ap->length();
412 m_sampleRate = ap->sampleRate();
414 else
415 m_bitrate = m_length = m_sampleRate = Undetermined;
418 void
419 MetaBundle::init( const KFileMetaInfo& info )
421 if( info.isValid() )
423 m_artist = info.item( "Artist" ).value().toString();
424 m_album = info.item( "Album" ).value().toString();
425 m_comment = info.item( "Comment" ).value().toString();
426 m_genre = info.item( "Genre" ).value().toString();
427 m_year = info.item( "Year" ).value().toString().toInt();
428 m_track = info.item( "Track" ).value().toString().toInt();
429 m_bitrate = info.item( "Bitrate" ).value().toInt();
430 m_length = info.item( "Length" ).value().toInt();
431 m_sampleRate = info.item( "Sample Rate" ).value().toInt();
433 // For title, check if it is valid. If not, use prettyTitle.
434 // @see bug:83650
435 const KFileMetaInfoItem itemtitle = info.item( "Title" );
436 m_title = itemtitle.isValid() ? itemtitle.value().toString() : prettyTitle( m_url.fileName() );
438 const KFileMetaInfoItem itemid = info.item( "Unique ID" );
439 m_uniqueId = itemid.isValid() ? itemid.value().toString() : QString::null;
441 // because whoever designed KMetaInfoItem is a donkey
442 #define makeSane( x ) if( x.string() == "---" ) x = null;
443 QString null;
444 makeSane( m_artist );
445 makeSane( m_album );
446 makeSane( m_comment );
447 makeSane( m_genre );
448 if ( m_title == "---" ) m_title.clear();
449 #undef makeSane
451 m_isValidMedia = true;
453 else
455 m_bitrate = m_length = m_sampleRate = m_filesize = Undetermined;
456 m_isValidMedia = false;
460 void
461 MetaBundle::embeddedImages( MetaBundle::EmbeddedImageList& images ) const
463 if ( isFile() )
465 TagLib::FileRef fileref = TagLib::FileRef( QFile::encodeName( url().path() ), false );
466 if ( !fileref.isNull() ) {
467 if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) ) {
468 if ( file->ID3v2Tag() )
469 loadImagesFromTag( *file->ID3v2Tag(), images );
470 } else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) ) {
471 if ( file->ID3v2Tag() )
472 loadImagesFromTag( *file->ID3v2Tag(), images );
473 } else if ( TagLib::MP4::File *file = dynamic_cast<TagLib::MP4::File *>( fileref.file() ) ) {
474 TagLib::MP4::Tag *mp4tag = dynamic_cast<TagLib::MP4::Tag *>( file->tag() );
475 if( mp4tag && mp4tag->cover().size() ) {
476 images.push_back( EmbeddedImage( mp4tag->cover(), "" ) );
483 void
484 MetaBundle::readTags( TagLib::AudioProperties::ReadStyle readStyle, EmbeddedImageList* images )
486 if( !isFile() )
487 return;
489 const QString path = url().path();
491 TagLib::FileRef fileref;
492 TagLib::Tag *tag = 0;
493 fileref = TagLib::FileRef( QFile::encodeName( path ), true, readStyle );
495 if( !fileref.isNull() )
497 setUniqueId( readUniqueId( &fileref ) );
498 m_filesize = QFile( path ).size();
500 tag = fileref.tag();
501 if ( tag )
503 #define strip( x ) TStringToQString( x ).trimmed()
504 setTitle( strip( tag->title() ) );
505 setArtist( strip( tag->artist() ) );
506 setAlbum( strip( tag->album() ) );
507 setComment( strip( tag->comment() ) );
508 setGenre( strip( tag->genre() ) );
509 setYear( tag->year() );
510 setTrack( tag->track() );
511 #undef strip
513 m_isValidMedia = true;
517 /* As mpeg implementation on TagLib uses a Tag class that's not defined on the headers,
518 we have to cast the files, not the tags! */
520 QString disc;
521 QString compilation;
522 if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) )
524 m_type = mp3;
525 if ( file->ID3v2Tag() )
527 if ( !file->ID3v2Tag()->frameListMap()["TPOS"].isEmpty() )
528 disc = TStringToQString( file->ID3v2Tag()->frameListMap()["TPOS"].front()->toString() ).trimmed();
530 if ( !file->ID3v2Tag()->frameListMap()["TBPM"].isEmpty() )
531 setBpm( TStringToQString( file->ID3v2Tag()->frameListMap()["TBPM"].front()->toString() ).trimmed().toFloat() );
533 if ( !file->ID3v2Tag()->frameListMap()["TCOM"].isEmpty() )
534 setComposer( TStringToQString( file->ID3v2Tag()->frameListMap()["TCOM"].front()->toString() ).trimmed() );
536 if ( !file->ID3v2Tag()->frameListMap()["TPE2"].isEmpty() ) // non-standard: Apple, Microsoft
537 setAlbumArtist( TStringToQString( file->ID3v2Tag()->frameListMap()["TPE2"].front()->toString() ).trimmed() );
539 if ( !file->ID3v2Tag()->frameListMap()["TCMP"].isEmpty() )
540 compilation = TStringToQString( file->ID3v2Tag()->frameListMap()["TCMP"].front()->toString() ).trimmed();
542 if(images) {
543 loadImagesFromTag( *file->ID3v2Tag(), *images );
547 else if ( TagLib::Ogg::Vorbis::File *file = dynamic_cast<TagLib::Ogg::Vorbis::File *>( fileref.file() ) )
549 m_type = ogg;
550 if ( file->tag() )
552 if ( !file->tag()->fieldListMap()[ "COMPOSER" ].isEmpty() )
553 setComposer( TStringToQString( file->tag()->fieldListMap()["COMPOSER"].front() ).trimmed() );
555 if ( !file->tag()->fieldListMap()[ "BPM" ].isEmpty() )
556 setBpm( TStringToQString( file->tag()->fieldListMap()["BPM"].front() ).trimmed().toFloat() );
558 if ( !file->tag()->fieldListMap()[ "DISCNUMBER" ].isEmpty() )
559 disc = TStringToQString( file->tag()->fieldListMap()["DISCNUMBER"].front() ).trimmed();
561 if ( !file->tag()->fieldListMap()[ "COMPILATION" ].isEmpty() )
562 compilation = TStringToQString( file->tag()->fieldListMap()["COMPILATION"].front() ).trimmed();
565 else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) )
567 m_type = flac;
568 if ( file->xiphComment() )
570 if ( !file->xiphComment()->fieldListMap()[ "COMPOSER" ].isEmpty() )
571 setComposer( TStringToQString( file->xiphComment()->fieldListMap()["COMPOSER"].front() ).trimmed() );
573 if ( !file->xiphComment()->fieldListMap()[ "BPM" ].isEmpty() )
574 setBpm( TStringToQString( file->xiphComment()->fieldListMap()["BPM"].front() ).trimmed().toFloat() );
576 if ( !file->xiphComment()->fieldListMap()[ "DISCNUMBER" ].isEmpty() )
577 disc = TStringToQString( file->xiphComment()->fieldListMap()["DISCNUMBER"].front() ).trimmed();
579 if ( !file->xiphComment()->fieldListMap()[ "COMPILATION" ].isEmpty() )
580 compilation = TStringToQString( file->xiphComment()->fieldListMap()["COMPILATION"].front() ).trimmed();
583 if ( images && file->ID3v2Tag() ) {
584 loadImagesFromTag( *file->ID3v2Tag(), *images );
587 else if ( TagLib::MP4::File *file = dynamic_cast<TagLib::MP4::File *>( fileref.file() ) )
589 m_type = mp4;
590 TagLib::MP4::Tag *mp4tag = dynamic_cast<TagLib::MP4::Tag *>( file->tag() );
591 if( mp4tag )
593 setComposer( TStringToQString( mp4tag->composer() ) );
594 setBpm( QString::number( mp4tag->bpm() ).toFloat() );
595 disc = QString::number( mp4tag->disk() );
596 compilation = QString::number( mp4tag->compilation() );
597 if ( images && mp4tag->cover().size() ) {
598 images->push_back( EmbeddedImage( mp4tag->cover(), "" ) );
603 if ( !disc.isEmpty() )
605 int i = disc.indexOf('/');
606 if ( i != -1 )
607 // disc.right( i ).toInt() is total number of discs, we don't use this at the moment
608 setDiscNumber( disc.left( i ).toInt() );
609 else
610 setDiscNumber( disc.toInt() );
613 if ( compilation.isEmpty() ) {
614 // well, it wasn't set, but if the artist is VA assume it's a compilation
615 if ( artist().string() == i18n( "Various Artists" ) )
616 setCompilation( CompilationYes );
617 } else {
618 int i = compilation.toInt();
619 if ( i == CompilationNo )
620 setCompilation( CompilationNo );
621 else if ( i == CompilationYes )
622 setCompilation( CompilationYes );
625 init( fileref.audioProperties() );
628 //FIXME disabled for beta4 as it's simpler to not got 100 bug reports
629 //else if( KMimeType::findByUrl( m_url )->is( "audio" ) )
630 // init( KFileMetaInfo( m_url, QString::null, KFileMetaInfo::Everything ) );
633 void MetaBundle::updateFilesize()
635 if( !isFile() )
637 m_filesize = Undetermined;
638 return;
641 const QString path = url().path();
642 m_filesize = QFile( path ).size();
645 float MetaBundle::score( bool ensureCached ) const
647 if( m_score == Undetermined && !ensureCached )
648 //const_cast is ugly, but other option was mutable, and then we lose const correctness checking
649 //everywhere else
650 *const_cast<float*>(&m_score) = CollectionDB::instance()->getSongPercentage( m_url.path() );
651 return m_score;
654 int MetaBundle::rating( bool ensureCached ) const
656 if( m_rating == Undetermined && !ensureCached )
657 *const_cast<int*>(&m_rating) = CollectionDB::instance()->getSongRating( m_url.path() );
658 return m_rating;
661 int MetaBundle::playCount( bool ensureCached ) const
663 if( m_playCount == Undetermined && !ensureCached )
664 *const_cast<int*>(&m_playCount) = CollectionDB::instance()->getPlayCount( m_url.path() );
665 return m_playCount;
668 uint MetaBundle::lastPlay( bool ensureCached ) const
670 if( (int)m_lastPlay == abs(Undetermined) && !ensureCached )
671 *const_cast<uint*>(&m_lastPlay) = CollectionDB::instance()->getLastPlay( m_url.path() ).toTime_t();
672 return m_lastPlay;
675 void MetaBundle::copyFrom( const MetaBundle &bundle )
677 setTitle( bundle.title() );
678 setArtist( bundle.artist() );
679 setAlbumArtist( bundle.albumArtist() );
680 setComposer( bundle.composer() );
681 setAlbum( bundle.album() );
682 setYear( bundle.year() );
683 setDiscNumber( bundle.discNumber() );
684 setBpm( bundle.bpm() );
685 setComment( bundle.comment() );
686 setGenre( bundle.genre() );
687 setTrack( bundle.track() );
688 setLength( bundle.length() );
689 setBitrate( bundle.bitrate() );
690 setSampleRate( bundle.sampleRate() );
691 setScore( bundle.score() );
692 setRating( bundle.rating() );
693 setPlayCount( bundle.playCount() );
694 setLastPlay( bundle.lastPlay() );
695 setFileType( bundle.fileType() );
696 setFilesize( bundle.filesize() );
697 if( bundle.m_podcastBundle )
698 setPodcastBundle( *bundle.m_podcastBundle );
699 else
701 delete m_podcastBundle;
702 m_podcastBundle = 0;
705 if( bundle.m_lastFmBundle )
706 setLastFmBundle( *bundle.m_lastFmBundle );
707 else
709 delete m_lastFmBundle;
710 m_lastFmBundle = 0;
714 void MetaBundle::copyFrom( const PodcastEpisodeBundle &peb )
716 setPodcastBundle( peb );
717 setTitle( peb.title() );
718 setArtist( peb.author() );
719 PodcastChannelBundle pcb;
720 if( CollectionDB::instance()->getPodcastChannelBundle( peb.parent(), &pcb ) )
722 if( !pcb.title().isEmpty() )
723 setAlbum( pcb.title() );
725 setGenre( QString ( "Podcast" ) );
728 void MetaBundle::setExactText( int column, const QString &newText )
730 switch( column )
732 case Title: setTitle( newText ); break;
733 case Artist: setArtist( newText ); break;
734 case AlbumArtist: setAlbumArtist( newText ); break;
735 case Composer: setComposer( newText ); break;
736 case Year: setYear( newText.toInt() ); break;
737 case Album: setAlbum( newText ); break;
738 case DiscNumber: setDiscNumber( newText.toInt() ); break;
739 case Track: setTrack( newText.toInt() ); break;
740 case Bpm: setBpm( newText.toFloat() ); break;
741 case Genre: setGenre( newText ); break;
742 case Comment: setComment( newText ); break;
743 case Length: setLength( newText.toInt() ); break;
744 case Bitrate: setBitrate( newText.toInt() ); break;
745 case SampleRate: setSampleRate( newText.toInt() ); break;
746 case Score: setScore( newText.toFloat() ); break;
747 case Rating: setRating( newText.toInt() ); break;
748 case PlayCount: setPlayCount( newText.toInt() ); break;
749 case LastPlayed: setLastPlay( newText.toInt() ); break;
750 case Filesize: setFilesize( newText.toInt() ); break;
751 case Type: setFileType( newText.toInt() ); break;
752 default: warning() << "Tried to set the text of an immutable or nonexistent column! [" << column;
756 QString MetaBundle::exactText( int column, bool ensureCached ) const
758 switch( column )
760 case Filename: return filename();
761 case Title: return title();
762 case Artist: return artist();
763 case AlbumArtist: return albumArtist();
764 case Composer: return composer();
765 case Year: return QString::number( year() );
766 case Album: return album();
767 case DiscNumber: return QString::number( discNumber() );
768 case Track: return QString::number( track() );
769 case Bpm: return QString::number( bpm() );
770 case Genre: return genre();
771 case Comment: return comment();
772 case Directory: return directory();
773 case Type: return QString::number( fileType() );
774 case Length: return QString::number( length() );
775 case Bitrate: return QString::number( bitrate() );
776 case SampleRate: return QString::number( sampleRate() );
777 case Score: return QString::number( score( ensureCached ) );
778 case Rating: return QString::number( rating( ensureCached ) );
779 case PlayCount: return QString::number( playCount( ensureCached ) );
780 case LastPlayed: return QString::number( lastPlay( ensureCached ) );
781 case Filesize: return QString::number( filesize() );
782 case Mood: return QString();
783 default: warning() << "Tried to get the text of a nonexistent column! [" << column;
786 return QString(); //shouldn't happen
789 QString MetaBundle::prettyText( int column ) const
791 QString text;
792 switch( column )
794 case Filename: text = isFile() ? MetaBundle::prettyTitle(filename()) : url().prettyUrl(); break;
795 case Title: text = title().isEmpty() ? MetaBundle::prettyTitle( filename() ) : title(); break;
796 case Artist: text = artist(); break;
797 case AlbumArtist: text = albumArtist(); break;
798 case Composer: text = composer(); break;
799 case Year: text = year() ? QString::number( year() ) : QString::null; break;
800 case Album: text = album(); break;
801 case DiscNumber: text = discNumber() ? QString::number( discNumber() ) : QString::null; break;
802 case Bpm: text = bpm() ? QString::number( bpm() ) : QString::null; break;
803 case Track: text = track() ? QString::number( track() ) : QString::null; break;
804 case Genre: text = genre(); break;
805 case Comment: text = comment(); break;
806 case Directory: text = url().isEmpty() ? QString() : directory(); break;
807 case Type: text = url().isEmpty() ? QString() : type(); break;
808 case Length: text = prettyLength( length(), true ); break;
809 case Bitrate: text = prettyBitrate( bitrate() ); break;
810 case SampleRate: text = prettySampleRate(); break;
811 case Score: text = QString::number( static_cast<int>( score() ) ); break;
812 case Rating: text = prettyRating(); break;
813 case PlayCount: text = QString::number( playCount() ); break;
814 case LastPlayed: text = Amarok::verboseTimeSince( lastPlay() ); break;
815 case Filesize: text = prettyFilesize(); break;
816 case Mood:
817 text = moodbar_const().state() == Moodbar::JobRunning ? i18n( "Calculating..." )
818 : moodbar_const().state() == Moodbar::JobQueued ? i18n( "Queued..." )
819 : QString::null;
820 break;
821 default: warning() << "Tried to get the text of a nonexistent column!"; break;
824 return text.trimmed();
827 bool MetaBundle::matchesSimpleExpression( const QString &expression, const Q3ValueList<int> &columns ) const
829 const QStringList terms = QString( expression.toLower() ).split( ' ' );
830 bool matches = true;
831 for( int x = 0; matches && x < terms.count(); ++x )
833 int y = 0, n = columns.count();
834 for(; y < n; ++y )
835 if ( prettyText( columns[y] ).toLower().count( terms[x] ) )
836 break;
837 matches = ( y < n );
840 return matches;
843 void MetaBundle::reactToChanges( const Q3ValueList<int>& columns)
845 // mark search dirty if we need to
846 for (int i = 0; !m_isSearchDirty && i < columns.count(); i++)
847 if ((m_searchColumns & (1 << columns[i])) > 0)
848 m_isSearchDirty = true;
851 bool MetaBundle::matchesFast(const QStringList &terms, ColumnMask columnMask) const
853 // simple search for rating, last played, etc. makes no sense and it hurts us a
854 // lot if we have to fetch it from the db. so zero them out
855 columnMask &= ~( 1<<Score | 1<<Rating | 1<<PlayCount | 1<<LastPlayed | 1<<Mood );
857 if (m_isSearchDirty || m_searchColumns != columnMask) {
858 // assert the size of ColumnMask is large enough. In the absence of
859 // a compile assert mechanism, this is pretty much as good for
860 // optimized code (ie, free)
862 if ( sizeof(ColumnMask) < (NUM_COLUMNS / 8) ) {
863 warning() << "ColumnMask is not big enough!\n";
866 // recompute search text
867 // There is potential for mishap here if matchesFast gets called from multiple
868 // threads, but it's *highly* unlikely that something bad will happen
869 m_isSearchDirty = false;
870 m_searchColumns = columnMask;
871 m_searchStr.resize(0);
873 for (int i = 0; i < NUM_COLUMNS; i++) {
874 if ((columnMask & (1 << i)) > 0) {
875 if (!m_searchStr.isEmpty()) m_searchStr += ' ';
876 m_searchStr += prettyText(i).toLower();
881 // now search
882 for (int i = 0; i < terms.count(); i++) {
883 if (!m_searchStr.count(terms[i])) return false;
886 return true;
890 bool MetaBundle::matchesExpression( const QString &expression, const Q3ValueList<int> &defaultColumns ) const
892 return matchesParsedExpression( ExpressionParser::parse( expression ), defaultColumns );
895 bool MetaBundle::matchesParsedExpression( const ParsedExpression &data, const Q3ValueList<int> &defaults ) const
897 for( uint i = 0, n = data.count(); i < n; ++i ) //check each part for matchiness
899 bool b = false; //whether at least one matches
900 for( uint ii = 0, count = data[i].count(); ii < count; ++ii )
902 expression_element e = data[i][ii];
903 int column = -1;
904 if( !e.field.isEmpty() )
906 QString field = e.field.toLower();
907 column = columnIndex( field );
908 if( column == -1 )
910 column = -2;
911 if( field == "size" )
912 field = "filesize";
913 else if( field == "filetype" )
914 field = "type";
915 else if( field == "disc" )
916 field = "discnumber";
917 else
918 column = -1;
920 if( column == -2 )
921 column = columnIndex( field );
924 if( column >= 0 ) //a field was specified and it exists
926 QString q = e.text, v = prettyText( column ).toLower(), w = q.toLower();
927 //q = query, v = contents of the field, w = match against it
928 bool condition; //whether it matches, not taking e.negateation into account
930 bool numeric;
931 switch( column )
933 case Year:
934 case DiscNumber:
935 case Track:
936 case Bpm:
937 case Bitrate:
938 case SampleRate:
939 case Score:
940 case PlayCount:
941 case LastPlayed:
942 case Filesize:
943 numeric = true;
944 break;
945 default:
946 numeric = false;
949 if( column == Filesize )
951 v = QString::number( filesize() );
952 if( w.endsWith( "m" ) )
953 w = QString::number( w.left( w.length()-1 ).toLong() * 1024 * 1024 );
954 else if( w.endsWith( "k" ) )
955 w = QString::number( w.left( w.length()-1 ).toLong() * 1024 );
958 if( e.match == expression_element::More )
960 if( numeric )
961 condition = v.toInt() > w.toInt();
962 else if( column == Rating )
963 condition = v.toFloat() > w.toFloat();
964 else if( column == Length )
966 int g = v.indexOf( ':' ), h = w.indexOf( ':' );
967 condition = v.left( g ).toInt() > w.left( h ).toInt() ||
968 ( v.left( g ).toInt() == w.left( h ).toInt() &&
969 v.mid( g + 1 ).toInt() > w.mid( h + 1 ).toInt() );
971 else
972 condition = v > w; //compare the strings
974 else if( e.match == expression_element::Less )
976 if( numeric )
977 condition = v.toInt() < w.toInt();
978 else if( column == Rating )
979 condition = v.toFloat() < w.toFloat();
980 else if( column == Length )
982 int g = v.indexOf( ':' ), h = w.indexOf( ':' );
983 condition = v.left( g ).toInt() < w.left( h ).toInt() ||
984 ( v.left( g ).toInt() == w.left( h ).toInt() &&
985 v.mid( g + 1 ).toInt() < w.mid( h + 1 ).toInt() );
987 else
988 condition = v < w;
990 else
992 if( numeric )
993 condition = v.toInt() == w.toInt();
994 else if( column == Rating )
995 condition = v.toFloat() == w.toFloat();
996 else if( column == Length )
998 int g = v.indexOf( ':' ), h = w.indexOf( ':' );
999 condition = v.left( g ).toInt() == w.left( h ).toInt() &&
1000 v.mid( g + 1 ).toInt() == w.mid( h + 1 ).toInt();
1002 else
1003 condition = v.count( q, Qt::CaseInsensitive );
1005 if( condition == ( e.negate ? false : true ) )
1007 b = true;
1008 break;
1011 else //check just the default fields
1013 for( int it = 0, end = defaults.size(); it != end; ++it )
1015 b = prettyText( defaults[it] ).count( e.text, Qt::CaseInsensitive ) == ( e.negate ? false : true );
1016 if( ( e.negate && !b ) || ( !e.negate && b ) )
1017 break;
1019 if( b )
1020 break;
1023 if( !b )
1024 return false;
1027 return true;
1030 QString
1031 MetaBundle::prettyTitle() const
1033 QString s = artist();
1035 //NOTE this gets regressed often, please be careful!
1036 // whatever you do, handle the stream case, streams have no artist but have an excellent title
1038 //FIXME doesn't work for resume playback
1040 if( s.isEmpty() )
1041 s = title();
1042 else
1043 s = i18n("%1 - %2", artist(), title() );
1045 if( s.isEmpty() ) s = prettyTitle( filename() );
1047 return s;
1050 QString
1051 MetaBundle::veryNiceTitle() const
1053 QString s;
1054 //NOTE I'm not sure, but the notes and FIXME's in the prettyTitle function should be fixed now.
1055 // If not then they do apply to this function also!
1056 if( !title().isEmpty() )
1058 if( !artist().isEmpty() )
1059 s = i18n( "%1 by %2", title(), artist() );
1060 else
1061 s = title();
1063 else
1065 s = prettyTitle( filename() );
1067 return s;
1070 QString
1071 MetaBundle::prettyTitle( const QString &filename ) //static
1073 QString s = filename; //just so the code is more readable
1075 //remove .part extension if it exists
1076 if (s.endsWith( ".part" ))
1077 s = s.left( s.length() - 5 );
1079 //remove file extension, s/_/ /g and decode %2f-like sequences
1080 s = s.left( s.lastIndexOf( '.' ) ).replace( '_', ' ' );
1081 s = QUrl::fromPercentEncoding( s.toAscii() );
1083 return s;
1086 QString
1087 MetaBundle::prettyLength( int seconds, bool showHours ) //static
1089 if( seconds > 0 ) return prettyTime( seconds, showHours );
1090 if( seconds == Undetermined ) return "?";
1091 if( seconds == Irrelevant ) return "-";
1093 return QString(); //Unavailable = ""
1096 QString
1097 MetaBundle::prettyTime( uint seconds, bool showHours ) //static
1099 QString s = QChar( ':' );
1100 s.append( zeroPad( seconds % 60 ) ); //seconds
1101 seconds /= 60;
1103 if( showHours && seconds >= 60)
1105 s.prepend( zeroPad( seconds % 60 ) ); //minutes
1106 s.prepend( ':' );
1107 seconds /= 60;
1110 //don't zeroPad the last one, as it can be greater than 2 digits
1111 s.prepend( QString::number( seconds ) ); //hours or minutes depending on above if block
1113 return s;
1116 QString
1117 MetaBundle::veryPrettyTime( int time )
1119 if( time == Undetermined )
1120 return i18n( "?" );
1121 if( time == Irrelevant )
1122 return i18n( "-" );
1124 QStringList s;
1125 s << QString::number( time % 60 ); //seconds
1126 time /= 60;
1127 if( time )
1128 s << QString::number( time % 60 ); //minutes
1129 time /= 60;
1130 if( time )
1131 s << QString::number( time % 24 ); //hours
1132 time /= 24;
1133 if( time )
1134 s << QString::number( time ); //days
1136 switch( s.count() )
1138 // xgettext: no-c-format
1139 case 1: return i18nc( "seconds", "%1s", s[0] );
1140 // xgettext: no-c-format
1141 case 2: return i18nc( "minutes, seconds", "%2m %1s", s[0], s[1] );
1142 // xgettext: no-c-format
1143 case 3: return i18nc( "hours, minutes, seconds", "%3h %2m %1s", s[0], s[1], s[2] );
1144 // xgettext: no-c-format
1145 case 4: return i18nc( "days, hours, minutes, seconds", "%4d %3h %2m %1s", s[0], s[1], s[2], s[3] );
1146 default: return "omg bug!";
1150 QString
1151 MetaBundle::fuzzyTime( int time )
1153 QString s;
1154 int secs=0, min=0, hr=0, day=0, week=0;
1156 if( time == Undetermined )
1157 return i18n( "?" );
1158 if( time == Irrelevant )
1159 return i18n( "-" );
1161 secs = time % 60; //seconds
1162 time /= 60;
1163 min = time % 60; //minutes
1164 time /= 60;
1165 hr = time % 24 ; //hours
1166 time /= 24;
1167 day = time % 7 ; //days
1168 time /= 7;
1169 week = time; //weeks
1171 if( week && hr >= 12 )
1173 day++;
1174 if( day == 7 )
1176 week++;
1177 day = 0;
1180 else if( day && min >= 30 )
1182 hr++;
1183 if( hr == 24 )
1185 day++;
1186 hr = 0;
1189 else if( hr && secs >= 30 )
1191 min++;
1192 if( min == 60 )
1194 hr++;
1195 min = 0;
1199 QString weeks = i18np( "1 week ", "%1 weeks ", week );
1200 QString days = i18np( "1 day ", "%1 days ", day );
1201 QString hours = i18np( "1 hour", "%1 hours", hr );
1203 if( week )
1204 return ( weeks + ( day ? days.arg("") : "" ) ).simplified();
1205 else if ( day )
1206 return ( days + ( hr ? hours : "" ) ).simplified();
1207 else if ( hr )
1208 return i18n( "%1:%2 hours", hr, zeroPad( min ) );
1209 else
1210 return i18n( "%1:%2", min,zeroPad( secs ) );
1213 QString
1214 MetaBundle::prettyBitrate( int i )
1216 //the point here is to force sharing of these strings returned from prettyBitrate()
1217 static const QString bitrateStore[9] = {
1218 "?", "32", "64", "96", "128", "160", "192", "224", "256" };
1220 return (i >=0 && i <= 256 && i % 32 == 0)
1221 ? bitrateStore[ i / 32 ]
1222 : prettyGeneric( "%1", i );
1225 QString
1226 MetaBundle::prettyFilesize( int s )
1228 return KIO::convertSize( s );
1231 QString
1232 MetaBundle::prettyRating( int r, bool trailingzero ) //static
1234 if( trailingzero )
1235 return QString::number( float( r ) / 2, 'f', 1 );
1236 else
1237 return r ? QString::number( float( r ) / 2 ) : QString();
1240 QString
1241 MetaBundle::ratingDescription( int r )
1243 switch( r )
1245 case 2: return i18n( "Awful" );
1246 case 3: return i18n( "Barely tolerable" );
1247 case 4: return i18n( "Tolerable" );
1248 case 5: return i18n( "Okay" );
1249 case 6: return i18n( "Good" );
1250 case 7: return i18n( "Very good" );
1251 case 8: return i18n( "Excellent" );
1252 case 9: return i18n( "Amazing" );
1253 case 10: return i18n( "Favorite" );
1254 case 0: default: return i18n( "Not rated" ); // assume weird values as not rated
1256 return "if you can see this, then that's a bad sign.";
1259 QStringList
1260 MetaBundle::ratingList()
1262 QStringList list;
1263 list += ratingDescription( 0 );
1264 for ( int i = 2; i<=10; i++ )
1265 list += i18nc( "rating - description", "%1 - %2", prettyRating( i, true ), ratingDescription( i ) );
1266 return list;
1269 QStringList
1270 MetaBundle::genreList() //static
1272 QStringList list;
1274 TagLib::StringList genres = TagLib::ID3v1::genreList();
1275 for( TagLib::StringList::ConstIterator it = genres.begin(), end = genres.end(); it != end; ++it )
1276 list += TStringToQString( (*it) );
1278 list.sort();
1280 return list;
1283 void
1284 MetaBundle::setExtendedTag( TagLib::File *file, int tag, const QString value )
1286 char *id = 0;
1288 if ( m_type == mp3 )
1290 switch( tag )
1292 case ( composerTag ): id = "TCOM"; break;
1293 case ( discNumberTag ): id = "TPOS"; break;
1294 case ( bpmTag ): id = "TBPM"; break;
1295 case ( compilationTag ): id = "TCMP"; break;
1296 case ( albumArtistTag ): id = "TPE2"; break; // non-standard: Apple, Microsoft
1297 default: return; //don't write the tag if it is unknown
1299 fprintf(stderr, "Setting extended tag %s to %s\n", id, value.toUtf8().data());
1300 TagLib::MPEG::File *mpegFile = dynamic_cast<TagLib::MPEG::File *>( file );
1301 if ( mpegFile && mpegFile->ID3v2Tag() )
1303 if ( value.isEmpty() )
1304 mpegFile->ID3v2Tag()->removeFrames( id );
1305 else
1307 if( !mpegFile->ID3v2Tag()->frameListMap()[id].isEmpty() )
1308 mpegFile->ID3v2Tag()->frameListMap()[id].front()->setText( QStringToTString( value ) );
1309 else
1311 TagLib::ID3v2::TextIdentificationFrame *frame = new TagLib::ID3v2::TextIdentificationFrame( id, TagLib::ID3v2::FrameFactory::instance()->defaultTextEncoding() );
1312 frame->setText( QStringToTString( value ) );
1313 mpegFile->ID3v2Tag()->addFrame( frame );
1318 else if ( m_type == ogg )
1320 switch( tag )
1322 case ( composerTag ): id = "COMPOSER"; break;
1323 case ( discNumberTag ): id = "DISCNUMBER"; break;
1324 case ( bpmTag ): id = "BPM"; break;
1325 case ( compilationTag ): id = "COMPILATION"; break;
1326 case ( albumArtistTag ): id = "ALBUMARTIST"; break; // non-standard: Amarok
1327 default: return; //don't write the tag if it is unknown
1329 TagLib::Ogg::Vorbis::File *oggFile = dynamic_cast<TagLib::Ogg::Vorbis::File *>( file );
1330 if ( oggFile && oggFile->tag() )
1332 value.isEmpty() ?
1333 oggFile->tag()->removeField( id ):
1334 oggFile->tag()->addField( id, QStringToTString( value ), true );
1337 else if ( m_type == flac )
1339 switch( tag )
1341 case ( composerTag ): id = "COMPOSER"; break;
1342 case ( discNumberTag ): id = "DISCNUMBER"; break;
1343 case ( bpmTag ): id = "BPM"; break;
1344 case ( compilationTag ): id = "COMPILATION"; break;
1345 case ( albumArtistTag ): id = "ALBUMARTIST"; break; // non-standard: Amarok
1346 default: return; //don't write the tag if it is unknown
1348 TagLib::FLAC::File *flacFile = dynamic_cast<TagLib::FLAC::File *>( file );
1349 if ( flacFile && flacFile->xiphComment() )
1351 value.isEmpty() ?
1352 flacFile->xiphComment()->removeField( id ):
1353 flacFile->xiphComment()->addField( id, QStringToTString( value ), true );
1356 else if ( m_type == mp4 )
1358 TagLib::MP4::Tag *mp4tag = dynamic_cast<TagLib::MP4::Tag *>( file->tag() );
1359 if( mp4tag )
1361 switch( tag )
1363 case ( composerTag ): mp4tag->setComposer( QStringToTString( value ) ); break;
1364 case ( discNumberTag ): mp4tag->setDisk( value.toInt() );
1365 case ( bpmTag ): mp4tag->setBpm( value.toInt() ); // mp4 doesn't support float bpm
1366 case ( compilationTag ): mp4tag->setCompilation( value.toInt() == CompilationYes );
1372 void
1373 MetaBundle::setPodcastBundle( const PodcastEpisodeBundle &peb )
1375 delete m_podcastBundle;
1376 m_podcastBundle = new PodcastEpisodeBundle;
1377 *m_podcastBundle = peb;
1380 void
1381 MetaBundle::setLastFmBundle( const LastFm::Bundle &last )
1383 delete m_lastFmBundle;
1384 // m_lastFmBundle = new LastFm::Bundle(last);
1385 m_lastFmBundle = new LastFm::Bundle;
1386 *m_lastFmBundle = last;
1389 void MetaBundle::loadImagesFromTag( const TagLib::ID3v2::Tag &tag, EmbeddedImageList& images ) const
1391 TagLib::ID3v2::FrameList l = tag.frameListMap()[ "APIC" ];
1392 oldForeachType( TagLib::ID3v2::FrameList, l ) {
1393 debug() << "Found APIC frame";
1394 TagLib::ID3v2::AttachedPictureFrame *ap = static_cast<TagLib::ID3v2::AttachedPictureFrame*>( *it );
1396 const TagLib::ByteVector &imgVector = ap->picture();
1397 debug() << "Size of image: " << imgVector.size() << " byte";
1398 // ignore APIC frames without picture and those with obviously bogus size
1399 if( imgVector.size() > 0 && imgVector.size() < 10000000 /*10MB*/ ) {
1400 images.push_back( EmbeddedImage( imgVector, ap->description() ) );
1405 bool
1406 MetaBundle::safeSave()
1408 /*bool noproblem;
1409 MetaBundleSaver mbs( this );
1410 TagLib::FileRef* fileref = mbs.prepareToSave();
1411 if( !fileref )
1413 debug() << "Could not get a fileref!";
1414 mbs.cleanupSave();
1415 return false;
1418 noproblem = save( fileref );
1420 if( !noproblem )
1422 debug() << "MetaBundle::save() didn't work!";
1423 mbs.cleanupSave();
1424 return false;
1427 noproblem = mbs.doSave();
1429 if( !noproblem )
1431 debug() << "Something failed during the save, cleaning up and exiting!";
1432 mbs.cleanupSave();
1433 return false;
1436 setUniqueId( readUniqueId() );
1437 if( CollectionDB::instance()->isFileInCollection( url().path() ) )
1438 CollectionDB::instance()->doAFTStuff( this, false );
1440 noproblem = mbs.cleanupSave();
1442 return noproblem;*/
1443 return true;
1446 bool
1447 MetaBundle::save( TagLib::FileRef* fileref )
1449 DEBUG_BLOCK
1450 if( !isFile() )
1451 return false;
1453 //Set default codec to UTF-8 (see bugs 111246 and 111232)
1454 TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF8);
1456 bool passedin = fileref;
1457 bool returnval = false;
1459 TagLib::FileRef* f;
1461 if( !passedin )
1462 f = new TagLib::FileRef( QFile::encodeName( url().path() ), false );
1463 else
1464 f = fileref;
1466 if ( f && !f->isNull() )
1468 TagLib::Tag * t = f->tag();
1469 if ( t ) { // f.tag() can return null if the file couldn't be opened for writing
1470 t->setTitle( QStringToTString( title().trimmed() ) );
1471 t->setArtist( QStringToTString( artist().string().trimmed() ) );
1472 t->setAlbum( QStringToTString( album().string().trimmed() ) );
1473 t->setTrack( track() );
1474 t->setYear( year() );
1475 t->setComment( QStringToTString( comment().string().trimmed() ) );
1476 t->setGenre( QStringToTString( genre().string().trimmed() ) );
1478 if ( hasExtendedMetaInformation() )
1480 setExtendedTag( f->file(), albumArtistTag, albumArtist() );
1481 setExtendedTag( f->file(), composerTag, composer().string().trimmed() );
1482 setExtendedTag( f->file(), discNumberTag, discNumber() ? QString::number( discNumber() ) : QString() );
1483 setExtendedTag( f->file(), bpmTag, bpm() ? QString::number( bpm() ) : QString() );
1484 if ( compilation() != CompilationUnknown )
1485 setExtendedTag( f->file(), compilationTag, QString::number( compilation() ) );
1487 if( !passedin )
1489 returnval = f->save();
1490 setUniqueId( readUniqueId() );
1491 if( returnval && CollectionDB::instance()->isFileInCollection( url().path() ) )
1492 CollectionDB::instance()->doAFTStuff( this, false );
1494 else
1495 returnval = true;
1498 if ( !passedin )
1499 delete f;
1501 return returnval;
1504 bool MetaBundle::save( QTextStream &stream, const QStringList &attributes ) const
1506 QDomDocument qDomSucksItNeedsADocument;
1507 QDomElement item = qDomSucksItNeedsADocument.createElement( "item" );
1508 item.setAttribute( "url", url().url() );
1509 item.setAttribute( "uniqueid", uniqueId() );
1510 if( m_isCompilation )
1511 item.setAttribute( "compilation", "true" );
1513 for( int i = 0, n = attributes.count(); i < n; i += 2 )
1514 item.setAttribute( attributes[i], attributes[i+1] );
1516 for( int i = 0; i < NUM_COLUMNS; ++i )
1518 QDomElement tag = qDomSucksItNeedsADocument.createElement( exactColumnName( i ) );
1519 //debug() << "exactColumName(i) = " << exactColumnName( i );
1520 QDomText text = qDomSucksItNeedsADocument.createTextNode( exactText( i, true ) );
1521 //debug() << "exactText(i) = " << exactText( i );
1522 tag.appendChild( text );
1524 item.appendChild( tag );
1527 item.save( stream, 1 );
1528 return true;
1531 void MetaBundle::setUrl( const KUrl &url )
1533 Q3ValueList<int> changes;
1534 for( int i = 0; i < NUM_COLUMNS; ++i ) changes << i;
1535 aboutToChange( changes ); m_url = url; reactToChanges( changes );
1537 setUniqueId();
1540 void MetaBundle::setPath( const QString &path )
1542 Q3ValueList<int> changes;
1543 for( int i = 0; i < NUM_COLUMNS; ++i ) changes << i;
1544 aboutToChange( changes ); m_url.setPath( path ); reactToChanges( changes );
1546 setUniqueId();
1549 void MetaBundle::setUniqueId()
1551 //if the file isn't already in the database, not checking for amarokcollectionscanner
1552 //will result in the UID being set to QString::null during the scan...bad!
1553 if( !isFile() )
1554 return;
1556 m_uniqueId = CollectionDB::instance()->uniqueIdFromUrl( url() );
1559 void MetaBundle::setUniqueId( const QString &id )
1561 //WARNING WARNING WARNING
1562 //Don't call this function if you don't know what you're doing!
1563 m_uniqueId = id;
1566 const TagLib::ByteVector
1567 MetaBundle::readUniqueIdHelper( TagLib::FileRef fileref ) const
1569 if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) )
1571 if( file->ID3v2Tag() )
1572 return file->ID3v2Tag()->render();
1573 else if( file->ID3v1Tag() )
1574 return file->ID3v1Tag()->render();
1575 else if( file->APETag() )
1576 return file->APETag()->render();
1578 else if ( TagLib::Ogg::Vorbis::File *file = dynamic_cast<TagLib::Ogg::Vorbis::File *>( fileref.file() ) )
1580 if( file->tag() )
1581 return file->tag()->render();
1583 else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) )
1585 if( file->ID3v2Tag() )
1586 return file->ID3v2Tag()->render();
1587 else if( file->ID3v1Tag() )
1588 return file->ID3v1Tag()->render();
1589 else if( file->xiphComment() )
1590 return file->xiphComment()->render();
1592 else if ( TagLib::Ogg::FLAC::File *file = dynamic_cast<TagLib::Ogg::FLAC::File *>( fileref.file() ) )
1594 if( file->tag() )
1595 return file->tag()->render();
1597 else if ( TagLib::MPC::File *file = dynamic_cast<TagLib::MPC::File *>( fileref.file() ) )
1599 if( file->ID3v1Tag() )
1600 return file->ID3v1Tag()->render();
1601 else if( file->APETag() )
1602 return file->APETag()->render();
1604 TagLib::ByteVector bv;
1605 return bv;
1608 QString
1609 MetaBundle::readUniqueId( TagLib::FileRef* fileref )
1611 //This is used in case we don't get given a fileref
1612 TagLib::FileRef tmpfileref;
1614 if( !fileref && isFile() )
1616 const QString path = url().path();
1617 //Make it get cleaned up at the end of the function automagically
1618 tmpfileref = TagLib::FileRef( QFile::encodeName( path ), true, TagLib::AudioProperties::Fast );
1619 fileref = &tmpfileref;
1622 if( !fileref || fileref->isNull() )
1623 return QString();
1625 TagLib::ByteVector bv = readUniqueIdHelper( *fileref );
1627 //get our unique id
1628 KMD5 md5( 0, 0 );
1630 QFile qfile( url().path() );
1632 char databuf[8192];
1633 int readlen = 0;
1634 QByteArray size = 0;
1635 QString returnval;
1637 md5.update( bv.data(), bv.size() );
1639 if( qfile.open( QIODevice::Unbuffered | QIODevice::ReadOnly ) )
1641 if( ( readlen = qfile.read( databuf, 8192 ) ) > 0 )
1643 md5.update( databuf, readlen );
1644 md5.update( size.setNum( qfile.size() ) );
1645 return QString( md5.hexDigest().data() );
1647 else
1648 return QString();
1651 return QString();
1655 MetaBundle::getRand()
1657 //KRandom supposedly exists in SVN, although it's not checked out on my machine, and it's certainly not in 3.3, so I'm just going to steal its code
1659 unsigned int seed;
1660 int fd = open("/dev/urandom", O_RDONLY);
1661 if (fd < 0 || ::read(fd, &seed, sizeof(seed)) != sizeof(seed))
1663 // No /dev/urandom... try something else.
1664 srand(getpid());
1665 seed = rand()+time(0);
1667 if (fd >= 0) close(fd);
1668 srand(seed);
1669 return rand();
1672 QString
1673 MetaBundle::getRandomString( int size, bool numbersOnly )
1675 if( size != 8 )
1677 debug() << "Wrong size passed in!";
1678 return QString();
1681 QString str;
1682 //do a memory op once, much faster than doing multiple later, especially since we know how big it will be
1683 str.reserve( size );
1684 int i = getRand(); //seed it
1685 i = 0;
1686 while (size--)
1688 // check your ASCII tables
1689 // we want characters you can see...93 is the range from ! to ~
1690 int r=rand() % 94;
1691 // shift the value to the visible characters
1692 r+=33;
1693 // we don't want ", %, ', <, >, \, `, or &
1694 // so that we don't have issues with escaping/quoting in QStrings,
1695 // and so that we don't have <> in our XML files where they might cause issues
1696 // hopefully this list is final, as once users really start using this
1697 // it will be a pain to change...however, there is an ATF version in CollectionDB
1698 // which will help if this ever needs to change
1699 // In addition we can change our vendor string
1700 while ( r==34 || r==37 || r == 38 || r==39 || r==60 ||r == 62 || r==92 || r==96 )
1701 r++;
1703 if( numbersOnly && ( r < 48 || r > 57 ) )
1705 size++;
1706 continue;
1709 str[i++] = char(r);
1710 // this next comment kept in for fun, as it was from the source of KRandomString, where I got
1711 // most of this code from to start with :-)
1712 // so what if I work backwards?
1714 return str;
1717 void MetaBundle::setTitle( const QString &title )
1718 { aboutToChange( Title ); m_title = title; reactToChange( Title ); }
1720 void MetaBundle::setArtist( const AtomicString &artist )
1721 { aboutToChange( Artist ); m_artist = artist; reactToChange( Artist ); }
1723 void MetaBundle::setAlbum( const AtomicString &album )
1724 { aboutToChange( Album ); m_album = album; reactToChange( Album ); }
1726 void MetaBundle::setComment( const AtomicString &comment )
1727 { aboutToChange( Comment ); m_comment = comment; reactToChange( Comment ); }
1729 void MetaBundle::setGenre( const AtomicString &genre )
1730 { aboutToChange( Genre ); m_genre = genre; reactToChange( Genre ); }
1732 void MetaBundle::setYear( int year)
1733 { aboutToChange( Year ); m_year = year; reactToChange( Year ); }
1735 void MetaBundle::setTrack( int track )
1736 { aboutToChange( Track ); m_track = track; reactToChange( Track ); }
1738 void MetaBundle::setCompilation( int compilation )
1740 switch( compilation )
1742 case CompilationYes:
1743 m_isCompilation = true;
1744 m_notCompilation = false;
1745 break;
1746 case CompilationNo:
1747 m_isCompilation = false;
1748 m_notCompilation = true;
1749 break;
1750 case CompilationUnknown:
1751 m_isCompilation = m_notCompilation = false;
1752 break;
1756 void MetaBundle::setLength( int length )
1757 { aboutToChange( Length ); m_length = length; reactToChange( Length ); }
1759 void MetaBundle::setBitrate( int bitrate )
1760 { aboutToChange( Bitrate ); m_bitrate = bitrate; reactToChange( Bitrate ); }
1762 void MetaBundle::setSampleRate( int sampleRate )
1763 { aboutToChange( SampleRate ); m_sampleRate = sampleRate; reactToChange( SampleRate ); }
1765 void MetaBundle::setDiscNumber( int discnumber )
1766 { aboutToChange( DiscNumber ); m_discNumber = discnumber; reactToChange( DiscNumber ); }
1768 void MetaBundle::setBpm( float bpm )
1769 { aboutToChange( Bpm ); m_bpm = bpm; reactToChange( Bpm ); }
1771 void MetaBundle::setComposer( const AtomicString &composer )
1772 { aboutToChange( Composer ); m_composer = composer; reactToChange( Composer ); }
1774 void MetaBundle::setAlbumArtist( const AtomicString &albumArtist )
1775 { aboutToChange( AlbumArtist ); m_albumArtist = albumArtist; reactToChange( AlbumArtist ); }
1777 void MetaBundle::setPlayCount( int playcount )
1778 { aboutToChange( PlayCount ); m_playCount = playcount; reactToChange( PlayCount ); }
1780 void MetaBundle::setLastPlay( uint lastplay )
1781 { aboutToChange( LastPlayed ); m_lastPlay = lastplay; reactToChange( LastPlayed ); }
1783 void MetaBundle::setRating( int rating )
1784 { aboutToChange( Rating ); m_rating = rating; reactToChange( Rating ); }
1786 void MetaBundle::setScore( float score )
1787 { aboutToChange( Score ); m_score = score; reactToChange( Score ); }
1789 void MetaBundle::setFilesize( int bytes )
1790 { aboutToChange( Filesize ); m_filesize = bytes; reactToChange( Filesize ); }
1792 void MetaBundle::setFileType( int type ) { m_type = type; }