diff --git a/shared/collectionscanner/Directory.cpp b/shared/collectionscanner/Directory.cpp index 6077733e8c..3cc6d19ac5 100644 --- a/shared/collectionscanner/Directory.cpp +++ b/shared/collectionscanner/Directory.cpp @@ -1,247 +1,247 @@ /*************************************************************************** * Copyright (C) 2003-2005 Max Howell * * (C) 2003-2010 Mark Kretschmann * * (C) 2005-2007 Alexandre Oliveira * * (C) 2008 Dan Meltzer * * (C) 2008-2009 Jeff Mitchell * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "Directory.h" #include "collectionscanner/ScanningState.h" #include "collectionscanner/Track.h" #include "collectionscanner/utils.h" #include #include #include #include #include #include #include #include #include #include CollectionScanner::Directory::Directory( const QString &path, CollectionScanner::ScanningState *state, bool skip ) : m_ignored( false ) { m_path = path; m_rpath = QDir::current().relativeFilePath( path ); - m_mtime = QFileInfo( path ).lastModified().toTime_t(); + m_mtime = QFileInfo( path ).lastModified().toSecsSinceEpoch(); m_skipped = skip; if( m_skipped ) return; QDir dir( path ); if( dir.exists( QStringLiteral("fmps_ignore") ) ) { m_ignored = true; return; } QStringList validImages; validImages << QStringLiteral("jpg") << QStringLiteral("png") << QStringLiteral("gif") << QStringLiteral("jpeg") << QStringLiteral("bmp") << QStringLiteral("svg") << QStringLiteral("xpm"); QStringList validPlaylists; validPlaylists << QStringLiteral("m3u") << QStringLiteral("pls") << QStringLiteral("xspf"); // --- check if we were restarted and failed at a file QStringList badFiles; if( state->lastDirectory() == path ) { badFiles << state->badFiles(); QString lastFile = state->lastFile(); if( !lastFile.isEmpty() ) { badFiles << state->lastFile(); state->setBadFiles( badFiles ); } } else state->setLastDirectory( path ); state->setLastFile( QString() ); // reset so we don't add a leftover file dir.setFilter( QDir::NoDotAndDotDot | QDir::Files ); QFileInfoList fileInfos = dir.entryInfoList(); foreach( const QFileInfo &fi, fileInfos ) { if( !fi.exists() ) continue; const QFileInfo &f = fi.isSymLink() ? QFileInfo( fi.symLinkTarget() ) : fi; if( badFiles.contains( f.absoluteFilePath() ) ) continue; const QString suffix = fi.suffix().toLower(); const QString filePath = f.absoluteFilePath(); // -- cover image ? if( validImages.contains( suffix ) ) m_covers.append( filePath ); // -- playlist ? else if( validPlaylists.contains( suffix ) ) m_playlists.append( CollectionScanner::Playlist( filePath ) ); // -- audio track ? else { // remember the last file before it get's dangerous. Before starting taglib state->setLastFile( f.absoluteFilePath() ); CollectionScanner::Track *newTrack = new CollectionScanner::Track( filePath, this ); if( newTrack->isValid() ) m_tracks.append( newTrack ); else delete newTrack; } } } CollectionScanner::Directory::Directory( QXmlStreamReader *reader ) : m_mtime( 0 ) , m_skipped( false ) , m_ignored( false ) { // improve scanner with skipCurrentElement as soon as Amarok requires Qt 4.6 while (!reader->atEnd()) { reader->readNext(); if( reader->isStartElement() ) { QStringRef name = reader->name(); if( name == QLatin1String("path") ) m_path = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("rpath") ) m_rpath = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("mtime") ) m_mtime = reader->readElementText(QXmlStreamReader::SkipChildElements).toUInt(); else if( name == QLatin1String("cover") ) m_covers.append(reader->readElementText(QXmlStreamReader::SkipChildElements)); else if( name == QLatin1String("skipped") ) { m_skipped = true; reader->skipCurrentElement(); } else if( name == QLatin1String("ignored") ) { m_ignored = true; reader->skipCurrentElement(); } else if( name == QLatin1String("track") ) m_tracks.append( new CollectionScanner::Track( reader, this ) ); else if( name == QLatin1String("playlist") ) m_playlists.append( CollectionScanner::Playlist( reader ) ); else { qDebug() << "Unexpected xml start element"<skipCurrentElement(); } } else if( reader->isEndElement() ) { break; } } } CollectionScanner::Directory::~Directory() { foreach( CollectionScanner::Track *track, m_tracks ) delete track; } QString CollectionScanner::Directory::path() const { return m_path; } QString CollectionScanner::Directory::rpath() const { return m_rpath; } uint CollectionScanner::Directory::mtime() const { return m_mtime; } bool CollectionScanner::Directory::isSkipped() const { return m_skipped; } const QStringList& CollectionScanner::Directory::covers() const { return m_covers; } const QList& CollectionScanner::Directory::tracks() const { return m_tracks; } const QList& CollectionScanner::Directory::playlists() const { return m_playlists; } void CollectionScanner::Directory::toXml( QXmlStreamWriter *writer ) const { writer->writeTextElement( QStringLiteral("path"), escapeXml10(m_path) ); writer->writeTextElement( QStringLiteral("rpath"), escapeXml10(m_rpath) ); writer->writeTextElement( QStringLiteral("mtime"), QString::number( m_mtime ) ); if( m_skipped ) writer->writeEmptyElement( QStringLiteral("skipped") ); if( m_ignored ) writer->writeEmptyElement( QStringLiteral("ignored") ); foreach( const QString &cover, m_covers ) { writer->writeTextElement( QStringLiteral("cover"), escapeXml10(cover) ); } foreach( CollectionScanner::Track *track, m_tracks ) { writer->writeStartElement( QStringLiteral("track") ); track->toXml( writer ); writer->writeEndElement(); } foreach( const CollectionScanner::Playlist &playlist, m_playlists ) { writer->writeStartElement( QStringLiteral("playlist") ); playlist.toXml( writer ); writer->writeEndElement(); } } diff --git a/shared/collectionscanner/Track.cpp b/shared/collectionscanner/Track.cpp index 77e2d03a5f..44ec460cb0 100644 --- a/shared/collectionscanner/Track.cpp +++ b/shared/collectionscanner/Track.cpp @@ -1,530 +1,530 @@ /*************************************************************************** * Copyright (C) 2003-2005 Max Howell * * (C) 2003-2010 Mark Kretschmann * * (C) 2005-2007 Alexandre Oliveira * * (C) 2008 Dan Meltzer * * (C) 2008-2009 Jeff Mitchell * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "Track.h" #include "utils.h" #include "MetaTagLib.h" #include "MetaReplayGain.h" #include #include #include #include #include #include bool CollectionScanner::Track::s_useCharsetDetector = false; CollectionScanner::Track::Track( const QString &path, CollectionScanner::Directory* directory ) : m_valid( true ) , m_directory( directory ) , m_filetype( Amarok::Unknown ) , m_compilation( false ) , m_noCompilation( false ) , m_hasCover( false ) , m_year( -1 ) , m_disc( -1 ) , m_track( -1 ) , m_bpm( -1.0 ) , m_bitrate( -1 ) , m_length( -1.0 ) , m_samplerate( -1 ) , m_filesize( -1 ) , m_trackGain( -1.0 ) , m_trackPeakGain( -1.0 ) , m_albumGain( -1.0 ) , m_albumPeakGain( -1.0 ) , m_rating( -1.0 ) , m_score( -1.0 ) , m_playcount( -1.0 ) { static const int MAX_SENSIBLE_LENGTH = 1023; // the maximum length for normal strings. // in corner cases a longer string might cause problems see BUG:276894 // for the unit test. // in a debug build a file called "crash_amarok_here.ogg" will crash the collection // scanner if( path.contains(QLatin1String("crash_amarok_here.ogg")) ) { qDebug() << "Crashing at"<atEnd()) { reader->readNext(); if( reader->isStartElement() ) { QStringRef name = reader->name(); if( name == QLatin1String("uniqueid") ) m_uniqueid = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("path") ) m_path = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("rpath") ) m_rpath = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("filetype") ) m_filetype = (Amarok::FileType)reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("title") ) m_title = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("artist") ) m_artist = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("albumArtist") ) m_albumArtist = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("album") ) m_album = reader->readElementText(); else if( name == QLatin1String("compilation") ) { m_compilation = true; reader->skipCurrentElement(); } else if( name == QLatin1String("noCompilation") ) { m_noCompilation = true; reader->skipCurrentElement(); } else if( name == QLatin1String("hasCover") ) { m_hasCover = true; reader->skipCurrentElement(); } else if( name == QLatin1String("comment") ) m_comment = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("genre") ) m_genre = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("year") ) m_year = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("disc") ) m_disc = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("track") ) m_track = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("bpm") ) m_bpm = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("bitrate") ) m_bitrate = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("length") ) m_length = reader->readElementText(QXmlStreamReader::SkipChildElements).toLong(); else if( name == QLatin1String("samplerate") ) m_samplerate = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else if( name == QLatin1String("filesize") ) m_filesize = reader->readElementText(QXmlStreamReader::SkipChildElements).toLong(); else if( name == QLatin1String("mtime") ) - m_modified = QDateTime::fromTime_t(reader->readElementText(QXmlStreamReader::SkipChildElements).toLong()); + m_modified = QDateTime::fromSecsSinceEpoch(reader->readElementText(QXmlStreamReader::SkipChildElements).toLong()); else if( name == QLatin1String("trackGain") ) m_trackGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("trackPeakGain") ) m_trackPeakGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("albumGain") ) m_albumGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("albumPeakGain") ) m_albumPeakGain = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("composer") ) m_composer = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == QLatin1String("rating") ) m_rating = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("score") ) m_score = reader->readElementText(QXmlStreamReader::SkipChildElements).toFloat(); else if( name == QLatin1String("playcount") ) m_playcount = reader->readElementText(QXmlStreamReader::SkipChildElements).toInt(); else { qDebug() << "Unexpected xml start element"<skipCurrentElement(); } } else if( reader->isEndElement() ) { break; } } } void CollectionScanner::Track::write( QXmlStreamWriter *writer, const QString &tag, const QString &str ) const { if( !str.isEmpty() ) writer->writeTextElement( tag, escapeXml10(str) ); } void CollectionScanner::Track::toXml( QXmlStreamWriter *writer ) const { if( !m_valid ) return; write( writer, QStringLiteral("uniqueid"), m_uniqueid ); write( writer, QStringLiteral("path"), m_path ); write( writer, QStringLiteral("rpath"), m_rpath ); write(writer, QStringLiteral("filetype"), QString::number( (int)m_filetype ) ); write( writer, QStringLiteral("title"), m_title); write( writer, QStringLiteral("artist"), m_artist); write( writer, QStringLiteral("albumArtist"), m_albumArtist); write( writer, QStringLiteral("album"), m_album); if( m_compilation ) writer->writeEmptyElement( QStringLiteral("compilation") ); if( m_noCompilation ) writer->writeEmptyElement( QStringLiteral("noCompilation") ); if( m_hasCover ) writer->writeEmptyElement( QStringLiteral("hasCover") ); write( writer, QStringLiteral("comment"), m_comment); write( writer, QStringLiteral("genre"), m_genre); if( m_year != -1 ) write(writer, QStringLiteral("year"), QString::number( m_year ) ); if( m_disc != -1 ) write(writer, QStringLiteral("disc"), QString::number( m_disc ) ); if( m_track != -1 ) write(writer, QStringLiteral("track"), QString::number( m_track ) ); if( m_bpm != -1 ) write(writer, QStringLiteral("bpm"), QString::number( m_bpm ) ); if( m_bitrate != -1 ) write(writer, QStringLiteral("bitrate"), QString::number( m_bitrate ) ); if( m_length != -1 ) write(writer, QStringLiteral("length"), QString::number( m_length ) ); if( m_samplerate != -1 ) write(writer, QStringLiteral("samplerate"), QString::number( m_samplerate ) ); if( m_filesize != -1 ) write(writer, QStringLiteral("filesize"), QString::number( m_filesize ) ); if( m_modified.isValid() ) - write(writer, QStringLiteral("mtime"), QString::number( m_modified.toTime_t() ) ); + write(writer, QStringLiteral("mtime"), QString::number( m_modified.toSecsSinceEpoch() ) ); if( m_trackGain != 0 ) write(writer, QStringLiteral("trackGain"), QString::number( m_trackGain ) ); if( m_trackPeakGain != 0 ) write(writer, QStringLiteral("trackPeakGain"), QString::number( m_trackPeakGain ) ); if( m_albumGain != 0 ) write(writer, QStringLiteral("albumGain"), QString::number( m_albumGain ) ); if( m_albumPeakGain != 0 ) write(writer, QStringLiteral("albumPeakGain"), QString::number( m_albumPeakGain ) ); write( writer, QStringLiteral("composer"), m_composer); if( m_rating != -1 ) write(writer, QStringLiteral("rating"), QString::number( m_rating ) ); if( m_score != -1 ) write(writer, QStringLiteral("score"), QString::number( m_score ) ); if( m_playcount != -1 ) write(writer, QStringLiteral("playcount"), QString::number( m_playcount ) ); } bool CollectionScanner::Track::isValid() const { return m_valid; } CollectionScanner::Directory* CollectionScanner::Track::directory() const { return m_directory; } QString CollectionScanner::Track::uniqueid() const { return m_uniqueid; } QString CollectionScanner::Track::path() const { return m_path; } QString CollectionScanner::Track::rpath() const { return m_rpath; } Amarok::FileType CollectionScanner::Track::filetype() const { return m_filetype; } QString CollectionScanner::Track::title() const { return m_title; } QString CollectionScanner::Track::artist() const { return m_artist; } QString CollectionScanner::Track::albumArtist() const { return m_albumArtist; } QString CollectionScanner::Track::album() const { return m_album; } bool CollectionScanner::Track::isCompilation() const { return m_compilation; } bool CollectionScanner::Track::isNoCompilation() const { return m_noCompilation; } bool CollectionScanner::Track::hasCover() const { return m_hasCover; } QString CollectionScanner::Track::comment() const { return m_comment; } QString CollectionScanner::Track::genre() const { return m_genre; } int CollectionScanner::Track::year() const { return m_year; } int CollectionScanner::Track::disc() const { return m_disc; } int CollectionScanner::Track::track() const { return m_track; } int CollectionScanner::Track::bpm() const { return m_bpm; } int CollectionScanner::Track::bitrate() const { return m_bitrate; } qint64 CollectionScanner::Track::length() const { return m_length; } int CollectionScanner::Track::samplerate() const { return m_samplerate; } qint64 CollectionScanner::Track::filesize() const { return m_filesize; } QDateTime CollectionScanner::Track::modified() const { return m_modified; } QString CollectionScanner::Track::composer() const { return m_composer; } qreal CollectionScanner::Track::replayGain( Meta::ReplayGainTag mode ) const { switch( mode ) { case Meta::ReplayGain_Track_Gain: return m_trackGain; case Meta::ReplayGain_Track_Peak: return m_trackPeakGain; case Meta::ReplayGain_Album_Gain: return m_albumGain; case Meta::ReplayGain_Album_Peak: return m_albumPeakGain; } return 0.0; } qreal CollectionScanner::Track::rating() const { return m_rating; } qreal CollectionScanner::Track::score() const { return m_score; } int CollectionScanner::Track::playcount() const { return m_playcount; } void CollectionScanner::Track::setUseCharsetDetector( bool value ) { s_useCharsetDetector = value; } diff --git a/src/core-impl/collections/daap/daapreader/Reader.cpp b/src/core-impl/collections/daap/daapreader/Reader.cpp index 8d83d9e99f..460407e7a8 100644 --- a/src/core-impl/collections/daap/daapreader/Reader.cpp +++ b/src/core-impl/collections/daap/daapreader/Reader.cpp @@ -1,588 +1,588 @@ /**************************************************************************************** * Copyright (c) 2006 Ian Monroe * * Copyright (c) 2007 Maximilian Kossick * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "DaapReader" #include "Reader.h" #include "authentication/contentfetcher.h" #include "../DaapCollection.h" #include "../DaapMeta.h" #include "core/support/Debug.h" #include #include #include #include #include #include using namespace Daap; using namespace Meta; //#define DEBUGTAG( VAR ) debug() << tag << " has value " << VAR; #define DEBUGTAG( VAR ) Reader::Reader( Collections::DaapCollection* mc, const QString& host, quint16 port, const QString& password, QObject* parent, const char* name) : QObject( parent ) , m_memColl( mc ) , m_host( host ) , m_port( port ) , m_sessionId( -1 ) , m_password( password ) { setObjectName( name ); debug() << "Host: " << host << " port: " << port; // these content codes are needed to learn all others m_codes["mccr"] = Code( "dmap.contentcodesresponse", CONTAINER ); m_codes["mstt"] = Code( "dmap.status", LONG ); m_codes["mdcl"] = Code( "dmap.dictionary", CONTAINER ); // mcnm is actually an int, but string makes parsing easier m_codes["mcnm"] = Code( "dmap.contentcodesnumber", STRING ); m_codes["mcna"] = Code( "dmap.contentcodesname", STRING ); m_codes["mcty"] = Code( "dmap.contentcodestype", SHORT ); // stupid, stupid. The reflection just isn't good enough // to connect to an iPhoto server. m_codes["ppro"] = Code( "dpap.protocolversion", LONG ); m_codes["avdb"] = Code( "daap.serverdatabases", CONTAINER ); m_codes["adbs"] = Code( "daap.databasesongs", CONTAINER ); m_codes["pret"] = Code( "dpap.unknown", CONTAINER ); } Reader::~Reader() { } void Reader::logoutRequest() { DEBUG_BLOCK ContentFetcher* http = new ContentFetcher( m_host, m_port, m_password, this, "readerLogoutHttp" ); connect( http, &ContentFetcher::httpError, this, &Reader::fetchingError ); connect( http, &ContentFetcher::finished, this, &Reader::logoutRequestFinished ); http->getDaap( "/logout?" + m_loginString ); } void Reader::logoutRequestFinished() { DEBUG_BLOCK sender()->deleteLater(); deleteLater(); } void Reader::loginRequest() { DEBUG_BLOCK ContentFetcher* http = new ContentFetcher( m_host, m_port, m_password, this, "readerHttp"); connect( http, &ContentFetcher::httpError, this, &Reader::fetchingError ); connect( http, &ContentFetcher::finished, this, &Reader::contentCodesReceived ); http->getDaap( "/content-codes" ); } void Reader::contentCodesReceived() { DEBUG_BLOCK ContentFetcher* http = (ContentFetcher*) sender(); disconnect( http, &ContentFetcher::finished, this, &Reader::contentCodesReceived ); QDataStream raw( http->results() ); Map contentCodes = parse( raw ); QList root = contentCodes["mccr"].toList(); if( root.isEmpty() ) return; //error root = root[0].toMap().value( "mdcl" ).toList(); foreach( const QVariant &v, root ) { Map entry = v.toMap(); QString code = entry.value( "mcnm" ).toList().value( 0 ).toString(); QString name = entry.value( "mcna" ).toList().value( 0 ).toString(); ContentTypes type = ContentTypes( entry.value( "mcty" ).toList().value( 0 ).toInt() ); if( !m_codes.contains( code ) && !code.isEmpty() && type > 0 ) { m_codes[code] = Code( name, type ); debug() << "Added DAAP code" << code << ":" << name << "with type" << type; } } connect( http, &ContentFetcher::loginRequired, this, &Reader::loginHeaderReceived ); http->getDaap( "/login" ); } void Reader::loginHeaderReceived() { DEBUG_BLOCK ContentFetcher* http = (ContentFetcher*) sender(); disconnect( http, &ContentFetcher::loginRequired, this, &Reader::loginHeaderReceived ); Q_EMIT passwordRequired(); http->deleteLater(); // connect( http, &ContentFetcher::finished, this, &Reader::loginFinished ); } void Reader::loginFinished() { DEBUG_BLOCK ContentFetcher* http = (ContentFetcher*) sender(); disconnect( http, &ContentFetcher::finished, this, &Reader::loginFinished ); QDataStream raw( http->results() ); Map loginResults = parse( raw ); QVariantList list = loginResults.value( "mlog" ).toList(); debug() << "list size is " << list.size(); QVariantList innerList = list.value( 0 ).toMap().value( "mlid" ).toList(); debug() << "innerList size is " << innerList.size(); if( innerList.isEmpty() ) { http->deleteLater(); return; } m_sessionId = innerList.value( 0 ).toInt(); m_loginString = "session-id=" + QString::number( m_sessionId ); connect( http, &ContentFetcher::finished, this, &Reader::updateFinished ); http->getDaap( "/update?" + m_loginString ); } void Reader::updateFinished() { DEBUG_BLOCK ContentFetcher* http = (ContentFetcher*) sender(); disconnect( http, &ContentFetcher::finished, this, &Reader::updateFinished ); QDataStream raw( http->results() ); Map updateResults = parse( raw ); if( updateResults["mupd"].toList().isEmpty() ) return; //error if( updateResults["mupd"].toList()[0].toMap()["musr"].toList().isEmpty() ) return; //error m_loginString = m_loginString + "&revision-number=" + QString::number( updateResults["mupd"].toList()[0].toMap()["musr"].toList()[0].toInt() ); connect( http, &ContentFetcher::finished, this, &Reader::databaseIdFinished ); http->getDaap( "/databases?" + m_loginString ); } void Reader::databaseIdFinished() { ContentFetcher* http = (ContentFetcher*) sender(); disconnect( http, &ContentFetcher::finished, this, &Reader::databaseIdFinished ); QDataStream raw( http->results() ); Map dbIdResults = parse( raw ); m_databaseId = QString::number( dbIdResults["avdb"].toList()[0].toMap()["mlcl"].toList()[0].toMap()["mlit"].toList()[0].toMap()["miid"].toList()[0].toInt() ); connect( http, &ContentFetcher::finished, this, &Reader::songListFinished ); http->getDaap( QStringLiteral("/databases/%1/items?type=music&meta=dmap.itemid,dmap.itemname,daap.songformat,daap.songartist,daap.songalbum,daap.songtime,daap.songtracknumber,daap.songcomment,daap.songyear,daap.songgenre&%2") .arg( m_databaseId, m_loginString ) ); } void Reader::songListFinished() { DEBUG_BLOCK ContentFetcher* http = (ContentFetcher*) sender(); disconnect( http, &ContentFetcher::finished, this, &Reader::songListFinished ); QByteArray result = http->results(); http->deleteLater(); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(new WorkerThread( result, this, m_memColl )) ); } bool Reader::parseSongList( const QByteArray &data, bool set_collection ) { // The original implementation used parse(), which uses addElement() and // makes heavy usage of QMaps and QList which hurts performance very badly. // Therefore this function parses the daap responses directly into the // DaapCollection which is 27 times faster here and saves a slight bit of // heap space. // parse() and addElement() create a more qt like structure though and might be // kept for other daap tasks. DEBUG_BLOCK QDataStream raw( data ); // Cache for music data QString itemId; QString format; QString title; QString artist; QString composer; QString comment; QString album; QString genre; int year = 0; qint32 trackNumber=0; qint32 songTime=0; while( !raw.atEnd() ) { char rawTag[5]; quint32 tagLength = getTagAndLength( raw, rawTag ); if( tagLength == 0 ) continue; QVariant tagData = readTagData( raw, rawTag, tagLength ); if( !tagData.isValid() ) continue; QString tag = QString( rawTag ); if( m_codes[tag].type == CONTAINER ) { parseSongList( tagData.toByteArray() ); continue; } if( tag == "astn" ) trackNumber = tagData.toInt(); else if( tag == "asyr" ) year = tagData.toInt(); else if( tag == "miid" ) itemId = tagData.toString(); else if(tag == "astm" ) songTime = tagData.toInt(); else if( tag== "asfm" ) format = tagData.toString(); else if( tag == "minm" ) title = tagData.toString(); else if( tag == "asal" ) album = tagData.toString(); else if( tag == "asar" ) artist = tagData.toString(); else if( tag == "ascp" ) composer = tagData.toString(); else if( tag == "ascm" ) comment = tagData.toString(); else if( tag == "asgn" ) genre = tagData.toString(); } if( !itemId.isEmpty() ) addTrack( itemId, title, artist, composer, comment, album, genre, year, format, trackNumber, songTime ); if( set_collection ) { m_memColl->memoryCollection()->acquireWriteLock(); m_memColl->memoryCollection()->setTrackMap( m_trackMap ); m_memColl->memoryCollection()->setArtistMap( m_artistMap ); m_memColl->memoryCollection()->setAlbumMap( m_albumMap ); m_memColl->memoryCollection()->setGenreMap( m_genreMap ); m_memColl->memoryCollection()->setComposerMap( m_composerMap ); m_memColl->memoryCollection()->setYearMap( m_yearMap ); m_memColl->memoryCollection()->releaseLock(); m_trackMap.clear(); m_artistMap.clear(); m_albumMap.clear(); m_genreMap.clear(); m_composerMap.clear(); m_yearMap.clear(); } return true; } void Reader::addTrack( const QString& itemId, const QString& title, const QString& artist, const QString& composer, const QString& comment, const QString& album, const QString& genre, int year, const QString& format, qint32 trackNumber, qint32 songTime ) { DaapTrackPtr track( new DaapTrack( m_memColl, m_host, m_port, m_databaseId, itemId, format ) ); track->setTitle( title ); track->setLength( songTime ); track->setTrackNumber( trackNumber ); track->setComment( comment ); track->setComposer( composer ); DaapArtistPtr artistPtr; if ( m_artistMap.contains( artist ) ) artistPtr = DaapArtistPtr::staticCast( m_artistMap.value( artist ) ); else { artistPtr = DaapArtistPtr( new DaapArtist( artist ) ); m_artistMap.insert( artist, ArtistPtr::staticCast( artistPtr ) ); } artistPtr->addTrack( track ); track->setArtist( artistPtr ); DaapAlbumPtr albumPtr; if ( m_albumMap.contains( album, artist ) ) albumPtr = DaapAlbumPtr::staticCast( m_albumMap.value( album, artist ) ); else { albumPtr = DaapAlbumPtr( new DaapAlbum( album ) ); albumPtr->setAlbumArtist( artistPtr ); m_albumMap.insert( AlbumPtr::staticCast( albumPtr ) ); } albumPtr->addTrack( track ); track->setAlbum( albumPtr ); DaapComposerPtr composerPtr; if ( m_composerMap.contains( composer ) ) composerPtr = DaapComposerPtr::staticCast( m_composerMap.value( composer ) ); else { composerPtr = DaapComposerPtr( new DaapComposer ( composer ) ); m_composerMap.insert( composer, ComposerPtr::staticCast( composerPtr ) ); } composerPtr->addTrack( track ); track->setComposer ( composerPtr ); DaapYearPtr yearPtr; if ( m_yearMap.contains( year ) ) yearPtr = DaapYearPtr::staticCast( m_yearMap.value( year ) ); else { yearPtr = DaapYearPtr( new DaapYear( QString::number(year) ) ); m_yearMap.insert( year, YearPtr::staticCast( yearPtr ) ); } yearPtr->addTrack( track ); track->setYear( yearPtr ); DaapGenrePtr genrePtr; if ( m_genreMap.contains( genre ) ) genrePtr = DaapGenrePtr::staticCast( m_genreMap.value( genre ) ); else { genrePtr = DaapGenrePtr( new DaapGenre( genre ) ); m_genreMap.insert( genre, GenrePtr::staticCast( genrePtr ) ); } genrePtr->addTrack( track ); track->setGenre( genrePtr ); m_trackMap.insert( track->uidUrl(), TrackPtr::staticCast( track ) ); } quint32 Reader::getTagAndLength( QDataStream &raw, char tag[5] ) { tag[4] = 0; raw.readRawData(tag, 4); quint32 tagLength = 0; raw >> tagLength; return tagLength; } QVariant Reader::readTagData( QDataStream &raw, char *tag, quint32 tagLength) { /** * Consume tagLength bytes of data from the stream and convert it to the * proper type, while making sure that datalength/datatype mismatches are handled properly */ QVariant ret = QVariant(); if ( tagLength == 0 ) return ret; #define READ_DATA(var) \ DEBUGTAG( var ) \ if( sizeof(var) != tagLength ) { \ warning() << "Bad tag data length:" << tag << ":" << tagLength; \ raw.skipRawData(tagLength); \ break; \ } else { \ raw >> var ; \ ret = QVariant(var); \ } switch( m_codes[tag].type ) { case CHAR: { qint8 charData; READ_DATA( charData ) break; } case SHORT: { qint16 shortData; READ_DATA( shortData ) break; } case LONG: { qint32 longData; READ_DATA( longData ); break; } case LONGLONG: { qint64 longlongData; READ_DATA( longlongData ); break; } case STRING: { QByteArray stringData( tagLength, ' ' ); raw.readRawData( stringData.data(), tagLength ); ret = QVariant(QString::fromUtf8( stringData, tagLength )); DEBUGTAG( QString::fromUtf8( stringData, tagLength ) ) break; } case DATE: { qint64 dateData; READ_DATA( dateData ) QDateTime date; - date.setTime_t( dateData ); + date.setSecsSinceEpoch( dateData ); ret = QVariant( date ); break; } case DVERSION: { qint32 verData; READ_DATA( verData ) QString version( "%1.%2.%3" ); version = version.arg( verData >> 16, (verData >> 8) & 0xFF, verData & 0xFF); ret = QVariant( version ); break; } case CONTAINER: { QByteArray containerData( tagLength, ' ' ); raw.readRawData( containerData.data(), tagLength ); ret = QVariant( containerData ); break; } default: warning() << "Tag" << tag << "has unhandled type."; raw.skipRawData(tagLength); break; } #undef READ_DATA return ret; } Map Reader::parse( QDataStream &raw ) { DEBUG_BLOCK /** * http://daap.sourceforge.net/docs/index.html * 0-3 Content code OSType (unsigned long), description of the contents of this chunk * 4-7 Length Length of the contents of this chunk (not the whole chunk) * 8- Data The data contained within the chunk **/ Map childMap; while( !raw.atEnd() ) { char tag[5]; quint32 tagLength = getTagAndLength( raw, tag ); if( tagLength == 0 ) continue; QVariant tagData = readTagData(raw, tag, tagLength); if( !tagData.isValid() ) continue; if( m_codes[tag].type == CONTAINER ) { QDataStream substream( tagData.toByteArray() ); addElement( childMap, tag, QVariant( parse( substream ) ) ); } else addElement( childMap, tag, tagData ); } return childMap; } void Reader::addElement( Map &parentMap, char* tag, const QVariant &element ) { QList list; Map::Iterator it = parentMap.find( tag ); if ( it == parentMap.end() ) { list.append( element ); parentMap.insert( tag, QVariant( list ) ); } else { list = it.value().toList(); list.append( element ); it.value() = QVariant( list ); } } void Reader::fetchingError( const QString& error ) { DEBUG_BLOCK sender()->deleteLater(); Q_EMIT httpError( error ); } WorkerThread::WorkerThread( const QByteArray &data, Reader *reader, Collections::DaapCollection *coll ) : QObject() , ThreadWeaver::Job() , m_success( false ) , m_data( data ) , m_reader( reader ) { connect( this, &WorkerThread::done, coll, &Collections::DaapCollection::loadedDataFromServer ); connect( this, &WorkerThread::failed, coll, &Collections::DaapCollection::parsingFailed ); connect( this, &WorkerThread::done, this, &Reader::deleteLater ); } WorkerThread::~WorkerThread() { //nothing to do } bool WorkerThread::success() const { return m_success; } void WorkerThread::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) { Q_UNUSED(self); Q_UNUSED(thread); m_success = m_reader->parseSongList( m_data, true ); } void WorkerThread::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) { Q_EMIT started(self); ThreadWeaver::Job::defaultBegin(self, thread); } void WorkerThread::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) { ThreadWeaver::Job::defaultEnd(self, thread); if (!self->success()) { Q_EMIT failed(self); } Q_EMIT done(self); } diff --git a/src/core-impl/collections/db/sql/SqlMeta.cpp b/src/core-impl/collections/db/sql/SqlMeta.cpp index e6cafd802d..9d598da8ca 100644 --- a/src/core-impl/collections/db/sql/SqlMeta.cpp +++ b/src/core-impl/collections/db/sql/SqlMeta.cpp @@ -1,2270 +1,2270 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Daniel Winter * * Copyright (c) 2010 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "SqlMeta" #include "SqlMeta.h" #include "amarokconfig.h" #include "SqlCapabilities.h" #include "SqlCollection.h" #include "SqlQueryMaker.h" #include "SqlRegistry.h" #include "SqlReadLabelCapability.h" #include "SqlWriteLabelCapability.h" #include "MetaTagLib.h" // for getting an embedded cover #include "amarokurls/BookmarkMetaActions.h" #include #include "core/meta/support/MetaUtility.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "core/capabilities/BookmarkThisCapability.h" #include "core-impl/capabilities/AlbumActionsCapability.h" #include "core-impl/collections/db/MountPointManager.h" #include "core-impl/collections/support/ArtistHelper.h" #include "core-impl/collections/support/jobs/WriteTagsJob.h" #include "covermanager/CoverCache.h" #include "covermanager/CoverFetcher.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // additional constants namespace Meta { static const qint64 valAlbumId = valCustom + 1; } using namespace Meta; QString SqlTrack::getTrackReturnValues() { //do not use any weird column names that contains commas: this will break getTrackReturnValuesCount() // NOTE: when changing this, always check that SqlTrack::TrackReturnIndex enum remains valid return "urls.id, urls.deviceid, urls.rpath, urls.directory, urls.uniqueid, " "tracks.id, tracks.title, tracks.comment, " "tracks.tracknumber, tracks.discnumber, " "statistics.score, statistics.rating, " "tracks.bitrate, tracks.length, " "tracks.filesize, tracks.samplerate, " "statistics.id, " "statistics.createdate, statistics.accessdate, " "statistics.playcount, tracks.filetype, tracks.bpm, " "tracks.createdate, tracks.modifydate, tracks.albumgain, tracks.albumpeakgain, " "tracks.trackgain, tracks.trackpeakgain, " "artists.name, artists.id, " // TODO: just reading the id should be sufficient "albums.name, albums.id, albums.artist, " // TODO: again here "genres.name, genres.id, " // TODO: again here "composers.name, composers.id, " // TODO: again here "years.name, years.id"; // TODO: again here } QString SqlTrack::getTrackJoinConditions() { return "LEFT JOIN tracks ON urls.id = tracks.url " "LEFT JOIN statistics ON urls.id = statistics.url " "LEFT JOIN artists ON tracks.artist = artists.id " "LEFT JOIN albums ON tracks.album = albums.id " "LEFT JOIN genres ON tracks.genre = genres.id " "LEFT JOIN composers ON tracks.composer = composers.id " "LEFT JOIN years ON tracks.year = years.id"; } int SqlTrack::getTrackReturnValueCount() { static int count = getTrackReturnValues().split( QLatin1Char(',') ).count(); return count; } SqlTrack::SqlTrack( Collections::SqlCollection *collection, int deviceId, const QString &rpath, int directoryId, const QString &uidUrl ) : Track() , m_collection( collection ) , m_batchUpdate( 0 ) , m_writeFile( true ) , m_labelsInCache( false ) { m_batchUpdate = 1; // I don't want commits yet m_urlId = -1; // this will be set with the first database write m_trackId = -1; // this will be set with the first database write m_statisticsId = -1; setUrl( deviceId, rpath, directoryId ); m_url = QUrl::fromUserInput(m_cache.value( Meta::valUrl ).toString()); // SqlRegistry already has this url setUidUrl( uidUrl ); m_uid = m_cache.value( Meta::valUniqueId ).toString(); // SqlRegistry already has this uid // ensure that these values get a correct database id m_cache.insert( Meta::valAlbum, QVariant() ); m_cache.insert( Meta::valArtist, QVariant() ); m_cache.insert( Meta::valComposer, QVariant() ); m_cache.insert( Meta::valYear, QVariant() ); m_cache.insert( Meta::valGenre, QVariant() ); m_trackNumber = 0; m_discNumber = 0; m_score = 0; m_rating = 0; m_bitrate = 0; m_length = 0; m_filesize = 0; m_sampleRate = 0; m_playCount = 0; m_bpm = 0.0; m_createDate = QDateTime::currentDateTime(); m_cache.insert( Meta::valCreateDate, m_createDate ); // ensure that the created date is written the next time m_trackGain = 0.0; m_trackPeakGain = 0.0; m_albumGain = 0.0; m_albumPeakGain = 0.0; m_batchUpdate = 0; // reset in-batch-update without committing m_filetype = Amarok::Unknown; } SqlTrack::SqlTrack( Collections::SqlCollection *collection, const QStringList &result ) : Track() , m_collection( collection ) , m_batchUpdate( 0 ) , m_writeFile( true ) , m_labelsInCache( false ) { QStringList::ConstIterator iter = result.constBegin(); m_urlId = (*(iter++)).toInt(); Q_ASSERT( m_urlId > 0 && "refusing to create SqlTrack with non-positive urlId, please file a bug" ); m_deviceId = (*(iter++)).toInt(); Q_ASSERT( m_deviceId != 0 && "refusing to create SqlTrack with zero deviceId, please file a bug" ); m_rpath = *(iter++); m_directoryId = (*(iter++)).toInt(); Q_ASSERT( m_directoryId > 0 && "refusing to create SqlTrack with non-positive directoryId, please file a bug" ); m_url = QUrl::fromLocalFile( m_collection->mountPointManager()->getAbsolutePath( m_deviceId, m_rpath ) ); m_uid = *(iter++); m_trackId = (*(iter++)).toInt(); m_title = *(iter++); m_comment = *(iter++); m_trackNumber = (*(iter++)).toInt(); m_discNumber = (*(iter++)).toInt(); m_score = (*(iter++)).toDouble(); m_rating = (*(iter++)).toInt(); m_bitrate = (*(iter++)).toInt(); m_length = (*(iter++)).toInt(); m_filesize = (*(iter++)).toInt(); m_sampleRate = (*(iter++)).toInt(); m_statisticsId = (*(iter++)).toInt(); uint time = (*(iter++)).toUInt(); if( time > 0 ) - m_firstPlayed = QDateTime::fromTime_t(time); + m_firstPlayed = QDateTime::fromSecsSinceEpoch(time); time = (*(iter++)).toUInt(); if( time > 0 ) - m_lastPlayed = QDateTime::fromTime_t(time); + m_lastPlayed = QDateTime::fromSecsSinceEpoch(time); m_playCount = (*(iter++)).toInt(); m_filetype = Amarok::FileType( (*(iter++)).toInt() ); m_bpm = (*(iter++)).toFloat(); - m_createDate = QDateTime::fromTime_t((*(iter++)).toUInt()); - m_modifyDate = QDateTime::fromTime_t((*(iter++)).toUInt()); + m_createDate = QDateTime::fromSecsSinceEpoch((*(iter++)).toUInt()); + m_modifyDate = QDateTime::fromSecsSinceEpoch((*(iter++)).toUInt()); // if there is no track gain, we assume a gain of 0 // if there is no album gain, we use the track gain QString albumGain = *(iter++); QString albumPeakGain = *(iter++); m_trackGain = (*(iter++)).toDouble(); m_trackPeakGain = (*(iter++)).toDouble(); if ( albumGain.isEmpty() ) { m_albumGain = m_trackGain; m_albumPeakGain = m_trackPeakGain; } else { m_albumGain = albumGain.toDouble(); m_albumPeakGain = albumPeakGain.toDouble(); } SqlRegistry* registry = m_collection->registry(); QString artist = *(iter++); int artistId = (*(iter++)).toInt(); if( artistId > 0 ) m_artist = registry->getArtist( artistId, artist ); QString album = *(iter++); int albumId =(*(iter++)).toInt(); int albumArtistId = (*(iter++)).toInt(); if( albumId > 0 ) // sanity check m_album = registry->getAlbum( albumId, album, albumArtistId ); QString genre = *(iter++); int genreId = (*(iter++)).toInt(); if( genreId > 0 ) // sanity check m_genre = registry->getGenre( genreId, genre ); QString composer = *(iter++); int composerId = (*(iter++)).toInt(); if( composerId > 0 ) // sanity check m_composer = registry->getComposer( composerId, composer ); QString year = *(iter++); int yearId = (*(iter++)).toInt(); if( yearId > 0 ) // sanity check m_year = registry->getYear( year.toInt(), yearId ); //Q_ASSERT_X( iter == result.constEnd(), "SqlTrack( Collections::SqlCollection*, QStringList )", "number of expected fields did not match number of actual fields: expected " + result.size() ); } SqlTrack::~SqlTrack() { QWriteLocker locker( &m_lock ); if( !m_cache.isEmpty() ) warning() << "Destroying track with unwritten meta information." << m_title << "cache:" << m_cache; if( m_batchUpdate ) warning() << "Destroying track with unclosed batch update." << m_title; } QString SqlTrack::name() const { QReadLocker locker( &m_lock ); return m_title; } QString SqlTrack::prettyName() const { if ( !name().isEmpty() ) return name(); return prettyTitle( m_url.fileName() ); } void SqlTrack::setTitle( const QString &newTitle ) { QWriteLocker locker( &m_lock ); if ( m_title != newTitle ) commitIfInNonBatchUpdate( Meta::valTitle, newTitle ); } QUrl SqlTrack::playableUrl() const { QReadLocker locker( &m_lock ); return m_url; } QString SqlTrack::prettyUrl() const { QReadLocker locker( &m_lock ); return m_url.path(); } void SqlTrack::setUrl( int deviceId, const QString &rpath, int directoryId ) { QWriteLocker locker( &m_lock ); if( m_deviceId == deviceId && m_rpath == rpath && m_directoryId == directoryId ) return; m_deviceId = deviceId; m_rpath = rpath; m_directoryId = directoryId; commitIfInNonBatchUpdate( Meta::valUrl, m_collection->mountPointManager()->getAbsolutePath( m_deviceId, m_rpath ) ); } QString SqlTrack::uidUrl() const { QReadLocker locker( &m_lock ); return m_uid; } void SqlTrack::setUidUrl( const QString &uid ) { QWriteLocker locker( &m_lock ); // -- ensure that the uid starts with the collections protocol (amarok-sqltrackuid) QString newid = uid; QString protocol; if( m_collection ) protocol = m_collection->uidUrlProtocol()+"://"; if( !newid.startsWith( protocol ) ) newid.prepend( protocol ); m_cache.insert( Meta::valUniqueId, newid ); if( m_batchUpdate == 0 ) { debug() << "setting uidUrl manually...did you really mean to do this?"; commitIfInNonBatchUpdate(); } } QString SqlTrack::notPlayableReason() const { return localFileNotPlayableReason( playableUrl().toLocalFile() ); } bool SqlTrack::isEditable() const { QReadLocker locker( &m_lock ); QFile::Permissions p = QFile::permissions( m_url.path() ); const bool editable = ( p & QFile::WriteUser ) || ( p & QFile::WriteGroup ) || ( p & QFile::WriteOther ); return m_collection && QFile::exists( m_url.path() ) && editable; } Meta::AlbumPtr SqlTrack::album() const { QReadLocker locker( &m_lock ); return m_album; } void SqlTrack::setAlbum( const QString &newAlbum ) { QWriteLocker locker( &m_lock ); if( !m_album || m_album->name() != newAlbum ) commitIfInNonBatchUpdate( Meta::valAlbum, newAlbum ); } void SqlTrack::setAlbum( int albumId ) { QWriteLocker locker( &m_lock ); commitIfInNonBatchUpdate( Meta::valAlbumId, albumId ); } Meta::ArtistPtr SqlTrack::artist() const { QReadLocker locker( &m_lock ); return m_artist; } void SqlTrack::setArtist( const QString &newArtist ) { QWriteLocker locker( &m_lock ); if( !m_artist || m_artist->name() != newArtist ) commitIfInNonBatchUpdate( Meta::valArtist, newArtist ); } void SqlTrack::setAlbumArtist( const QString &newAlbumArtist ) { if( m_album.isNull() ) return; if( !newAlbumArtist.compare( "Various Artists", Qt::CaseInsensitive ) || !newAlbumArtist.compare( i18n( "Various Artists" ), Qt::CaseInsensitive ) ) { commitIfInNonBatchUpdate( Meta::valCompilation, true ); } else { m_cache.insert( Meta::valAlbumArtist, ArtistHelper::realTrackArtist( newAlbumArtist ) ); m_cache.insert( Meta::valCompilation, false ); commitIfInNonBatchUpdate(); } } Meta::ComposerPtr SqlTrack::composer() const { QReadLocker locker( &m_lock ); return m_composer; } void SqlTrack::setComposer( const QString &newComposer ) { QWriteLocker locker( &m_lock ); if( !m_composer || m_composer->name() != newComposer ) commitIfInNonBatchUpdate( Meta::valComposer, newComposer ); } Meta::YearPtr SqlTrack::year() const { QReadLocker locker( &m_lock ); return m_year; } void SqlTrack::setYear( int newYear ) { QWriteLocker locker( &m_lock ); if( !m_year || m_year->year() != newYear ) commitIfInNonBatchUpdate( Meta::valYear, newYear ); } Meta::GenrePtr SqlTrack::genre() const { QReadLocker locker( &m_lock ); return m_genre; } void SqlTrack::setGenre( const QString &newGenre ) { QWriteLocker locker( &m_lock ); if( !m_genre || m_genre->name() != newGenre ) commitIfInNonBatchUpdate( Meta::valGenre, newGenre ); } QString SqlTrack::type() const { QReadLocker locker( &m_lock ); return m_url.isLocalFile() ? Amarok::FileTypeSupport::toString( m_filetype ) // don't localize. This is used in different files to identify streams, see EngineController quirks : "stream"; } void SqlTrack::setType( Amarok::FileType newType ) { QWriteLocker locker( &m_lock ); if ( m_filetype != newType ) commitIfInNonBatchUpdate( Meta::valFormat, int(newType) ); } qreal SqlTrack::bpm() const { QReadLocker locker( &m_lock ); return m_bpm; } void SqlTrack::setBpm( const qreal newBpm ) { QWriteLocker locker( &m_lock ); if ( m_bpm != newBpm ) commitIfInNonBatchUpdate( Meta::valBpm, newBpm ); } QString SqlTrack::comment() const { QReadLocker locker( &m_lock ); return m_comment; } void SqlTrack::setComment( const QString &newComment ) { QWriteLocker locker( &m_lock ); if( newComment != m_comment ) commitIfInNonBatchUpdate( Meta::valComment, newComment ); } double SqlTrack::score() const { QReadLocker locker( &m_lock ); return m_score; } void SqlTrack::setScore( double newScore ) { QWriteLocker locker( &m_lock ); newScore = qBound( double(0), newScore, double(100) ); if( qAbs( newScore - m_score ) > 0.001 ) // we don't commit for minimal changes commitIfInNonBatchUpdate( Meta::valScore, newScore ); } int SqlTrack::rating() const { QReadLocker locker( &m_lock ); return m_rating; } void SqlTrack::setRating( int newRating ) { QWriteLocker locker( &m_lock ); newRating = qBound( 0, newRating, 10 ); if( newRating != m_rating ) commitIfInNonBatchUpdate( Meta::valRating, newRating ); } qint64 SqlTrack::length() const { QReadLocker locker( &m_lock ); return m_length; } void SqlTrack::setLength( qint64 newLength ) { QWriteLocker locker( &m_lock ); if( newLength != m_length ) commitIfInNonBatchUpdate( Meta::valLength, newLength ); } int SqlTrack::filesize() const { QReadLocker locker( &m_lock ); return m_filesize; } int SqlTrack::sampleRate() const { QReadLocker locker( &m_lock ); return m_sampleRate; } void SqlTrack::setSampleRate( int newSampleRate ) { QWriteLocker locker( &m_lock ); if( newSampleRate != m_sampleRate ) commitIfInNonBatchUpdate( Meta::valSamplerate, newSampleRate ); } int SqlTrack::bitrate() const { QReadLocker locker( &m_lock ); return m_bitrate; } void SqlTrack::setBitrate( int newBitrate ) { QWriteLocker locker( &m_lock ); if( newBitrate != m_bitrate ) commitIfInNonBatchUpdate( Meta::valBitrate, newBitrate ); } QDateTime SqlTrack::createDate() const { QReadLocker locker( &m_lock ); return m_createDate; } QDateTime SqlTrack::modifyDate() const { QReadLocker locker( &m_lock ); return m_modifyDate; } void SqlTrack::setModifyDate( const QDateTime &newTime ) { QWriteLocker locker( &m_lock ); if( newTime != m_modifyDate ) commitIfInNonBatchUpdate( Meta::valModified, newTime ); } int SqlTrack::trackNumber() const { QReadLocker locker( &m_lock ); return m_trackNumber; } void SqlTrack::setTrackNumber( int newTrackNumber ) { QWriteLocker locker( &m_lock ); if( newTrackNumber != m_trackNumber ) commitIfInNonBatchUpdate( Meta::valTrackNr, newTrackNumber ); } int SqlTrack::discNumber() const { QReadLocker locker( &m_lock ); return m_discNumber; } void SqlTrack::setDiscNumber( int newDiscNumber ) { QWriteLocker locker( &m_lock ); if( newDiscNumber != m_discNumber ) commitIfInNonBatchUpdate( Meta::valDiscNr, newDiscNumber ); } QDateTime SqlTrack::lastPlayed() const { QReadLocker locker( &m_lock ); return m_lastPlayed; } void SqlTrack::setLastPlayed( const QDateTime &newTime ) { QWriteLocker locker( &m_lock ); if( newTime != m_lastPlayed ) commitIfInNonBatchUpdate( Meta::valLastPlayed, newTime ); } QDateTime SqlTrack::firstPlayed() const { QReadLocker locker( &m_lock ); return m_firstPlayed; } void SqlTrack::setFirstPlayed( const QDateTime &newTime ) { QWriteLocker locker( &m_lock ); if( newTime != m_firstPlayed ) commitIfInNonBatchUpdate( Meta::valFirstPlayed, newTime ); } int SqlTrack::playCount() const { QReadLocker locker( &m_lock ); return m_playCount; } void SqlTrack::setPlayCount( const int newCount ) { QWriteLocker locker( &m_lock ); if( newCount != m_playCount ) commitIfInNonBatchUpdate( Meta::valPlaycount, newCount ); } qreal SqlTrack::replayGain( ReplayGainTag mode ) const { QReadLocker locker(&(const_cast(this)->m_lock)); switch( mode ) { case Meta::ReplayGain_Track_Gain: return m_trackGain; case Meta::ReplayGain_Track_Peak: return m_trackPeakGain; case Meta::ReplayGain_Album_Gain: return m_albumGain; case Meta::ReplayGain_Album_Peak: return m_albumPeakGain; } return 0.0; } void SqlTrack::setReplayGain( Meta::ReplayGainTag mode, qreal value ) { if( qAbs( value - replayGain( mode ) ) < 0.01 ) return; { QWriteLocker locker( &m_lock ); switch( mode ) { case Meta::ReplayGain_Track_Gain: m_cache.insert( Meta::valTrackGain, value ); break; case Meta::ReplayGain_Track_Peak: m_cache.insert( Meta::valTrackGainPeak, value ); break; case Meta::ReplayGain_Album_Gain: m_cache.insert( Meta::valAlbumGain, value ); break; case Meta::ReplayGain_Album_Peak: m_cache.insert( Meta::valAlbumGainPeak, value ); break; } commitIfInNonBatchUpdate(); } } void SqlTrack::beginUpdate() { QWriteLocker locker( &m_lock ); m_batchUpdate++; } void SqlTrack::endUpdate() { QWriteLocker locker( &m_lock ); Q_ASSERT( m_batchUpdate > 0 ); m_batchUpdate--; commitIfInNonBatchUpdate(); } void SqlTrack::commitIfInNonBatchUpdate( qint64 field, const QVariant &value ) { m_cache.insert( field, value ); commitIfInNonBatchUpdate(); } void SqlTrack::commitIfInNonBatchUpdate() { if( m_batchUpdate > 0 || m_cache.isEmpty() ) return; // nothing to do // debug() << "SqlTrack::commitMetaDataChanges " << m_cache; QString oldUid = m_uid; // for all the following objects we need to invalidate the cache and // notify the observers after the update AmarokSharedPointer oldArtist; AmarokSharedPointer newArtist; AmarokSharedPointer oldAlbum; AmarokSharedPointer newAlbum; AmarokSharedPointer oldComposer; AmarokSharedPointer newComposer; AmarokSharedPointer oldGenre; AmarokSharedPointer newGenre; AmarokSharedPointer oldYear; AmarokSharedPointer newYear; if( m_cache.contains( Meta::valFormat ) ) m_filetype = Amarok::FileType(m_cache.value( Meta::valFormat ).toInt()); if( m_cache.contains( Meta::valTitle ) ) m_title = m_cache.value( Meta::valTitle ).toString(); if( m_cache.contains( Meta::valComment ) ) m_comment = m_cache.value( Meta::valComment ).toString(); if( m_cache.contains( Meta::valScore ) ) m_score = m_cache.value( Meta::valScore ).toDouble(); if( m_cache.contains( Meta::valRating ) ) m_rating = m_cache.value( Meta::valRating ).toInt(); if( m_cache.contains( Meta::valLength ) ) m_length = m_cache.value( Meta::valLength ).toLongLong(); if( m_cache.contains( Meta::valSamplerate ) ) m_sampleRate = m_cache.value( Meta::valSamplerate ).toInt(); if( m_cache.contains( Meta::valBitrate ) ) m_bitrate = m_cache.value( Meta::valBitrate ).toInt(); if( m_cache.contains( Meta::valFirstPlayed ) ) m_firstPlayed = m_cache.value( Meta::valFirstPlayed ).toDateTime(); if( m_cache.contains( Meta::valLastPlayed ) ) m_lastPlayed = m_cache.value( Meta::valLastPlayed ).toDateTime(); if( m_cache.contains( Meta::valTrackNr ) ) m_trackNumber = m_cache.value( Meta::valTrackNr ).toInt(); if( m_cache.contains( Meta::valDiscNr ) ) m_discNumber = m_cache.value( Meta::valDiscNr ).toInt(); if( m_cache.contains( Meta::valPlaycount ) ) m_playCount = m_cache.value( Meta::valPlaycount ).toInt(); if( m_cache.contains( Meta::valCreateDate ) ) m_createDate = m_cache.value( Meta::valCreateDate ).toDateTime(); if( m_cache.contains( Meta::valModified ) ) m_modifyDate = m_cache.value( Meta::valModified ).toDateTime(); if( m_cache.contains( Meta::valTrackGain ) ) m_trackGain = m_cache.value( Meta::valTrackGain ).toDouble(); if( m_cache.contains( Meta::valTrackGainPeak ) ) m_trackPeakGain = m_cache.value( Meta::valTrackGainPeak ).toDouble(); if( m_cache.contains( Meta::valAlbumGain ) ) m_albumGain = m_cache.value( Meta::valAlbumGain ).toDouble(); if( m_cache.contains( Meta::valAlbumGainPeak ) ) m_albumPeakGain = m_cache.value( Meta::valAlbumGainPeak ).toDouble(); if( m_cache.contains( Meta::valUrl ) ) { // slight problem here: it is possible to set the url to the one of an already // existing track, which is forbidden by the database // At least the ScanResultProcessor handles this problem QUrl oldUrl = m_url; QUrl newUrl = QUrl::fromUserInput(m_cache.value( Meta::valUrl ).toString()); if( oldUrl != newUrl ) m_collection->registry()->updateCachedUrl( oldUrl.path(), newUrl.path() ); m_url = newUrl; // debug() << "m_cache contains a new URL, setting m_url to " << m_url << " from " << oldUrl; } if( m_cache.contains( Meta::valArtist ) ) { //invalidate cache of the old artist... oldArtist = static_cast(m_artist.data()); m_artist = m_collection->registry()->getArtist( m_cache.value( Meta::valArtist ).toString() ); //and the new one newArtist = static_cast(m_artist.data()); // if the current album is no compilation and we aren't changing // the album anyway, then we need to create a new album with the // new artist. if( m_album ) { bool supp = m_album->suppressImageAutoFetch(); m_album->setSuppressImageAutoFetch( true ); if( m_album->hasAlbumArtist() && m_album->albumArtist() == oldArtist && !m_cache.contains( Meta::valAlbum ) && !m_cache.contains( Meta::valAlbumId ) ) { m_cache.insert( Meta::valAlbum, m_album->name() ); } m_album->setSuppressImageAutoFetch( supp ); } } if( m_cache.contains( Meta::valAlbum ) || m_cache.contains( Meta::valAlbumId ) || m_cache.contains( Meta::valAlbumArtist ) ) { oldAlbum = static_cast(m_album.data()); if( m_cache.contains( Meta::valAlbumId ) ) m_album = m_collection->registry()->getAlbum( m_cache.value( Meta::valAlbumId ).toInt() ); else { // the album should remain a compilation after renaming it // TODO: we would need to use the artist helper QString newArtistName; if( m_cache.contains( Meta::valAlbumArtist ) ) newArtistName = m_cache.value( Meta::valAlbumArtist ).toString(); else if( oldAlbum && oldAlbum->isCompilation() && !oldAlbum->name().isEmpty() ) newArtistName.clear(); else if( oldAlbum && oldAlbum->hasAlbumArtist() ) newArtistName = oldAlbum->albumArtist()->name(); m_album = m_collection->registry()->getAlbum( m_cache.contains( Meta::valAlbum) ? m_cache.value( Meta::valAlbum ).toString() : oldAlbum->name(), newArtistName ); } newAlbum = static_cast(m_album.data()); // due to the complex logic with artist and albumId it can happen that // in the end we have the same album as before. if( newAlbum == oldAlbum ) { m_cache.remove( Meta::valAlbum ); m_cache.remove( Meta::valAlbumId ); m_cache.remove( Meta::valAlbumArtist ); oldAlbum.clear(); newAlbum.clear(); } } if( m_cache.contains( Meta::valComposer ) ) { oldComposer = static_cast(m_composer.data()); m_composer = m_collection->registry()->getComposer( m_cache.value( Meta::valComposer ).toString() ); newComposer = static_cast(m_composer.data()); } if( m_cache.contains( Meta::valGenre ) ) { oldGenre = static_cast(m_genre.data()); m_genre = m_collection->registry()->getGenre( m_cache.value( Meta::valGenre ).toString() ); newGenre = static_cast(m_genre.data()); } if( m_cache.contains( Meta::valYear ) ) { oldYear = static_cast(m_year.data()); m_year = m_collection->registry()->getYear( m_cache.value( Meta::valYear ).toInt() ); newYear = static_cast(m_year.data()); } if( m_cache.contains( Meta::valBpm ) ) m_bpm = m_cache.value( Meta::valBpm ).toDouble(); // --- write the file if( m_writeFile && AmarokConfig::writeBack() ) { Meta::Tag::writeTags( m_url.path(), m_cache, AmarokConfig::writeBackStatistics() ); // unique id may have changed QString uid = Meta::Tag::readTags( m_url.path() ).value( Meta::valUniqueId ).toString(); if( !uid.isEmpty() ) m_cache[ Meta::valUniqueId ] = m_collection->generateUidUrl( uid ); } // needs to be after writing to file; that may have changed generated uid if( m_cache.contains( Meta::valUniqueId ) ) { QString newUid = m_cache.value( Meta::valUniqueId ).toString(); if( oldUid != newUid && m_collection->registry()->updateCachedUid( oldUid, newUid ) ) m_uid = newUid; } //updating the fields might have changed the filesize //read the current filesize so that we can update the db QFile file( m_url.path() ); if( file.exists() ) { if( m_filesize != file.size() ) { m_cache.insert( Meta::valFilesize, file.size() ); m_filesize = file.size(); } } // --- add to the registry dirty list SqlRegistry *registry = 0; // prevent writing to the db when we don't know the directory, bug 322474. Note that // m_urlId is created by registry->commitDirtyTracks() if there is none. if( m_deviceId != 0 && m_directoryId > 0 ) { registry = m_collection->registry(); QMutexLocker locker2( ®istry->m_blockMutex ); registry->m_dirtyTracks.insert( Meta::SqlTrackPtr( this ) ); if( oldArtist ) registry->m_dirtyArtists.insert( oldArtist ); if( newArtist ) registry->m_dirtyArtists.insert( newArtist ); if( oldAlbum ) registry->m_dirtyAlbums.insert( oldAlbum ); if( newAlbum ) registry->m_dirtyAlbums.insert( newAlbum ); if( oldComposer ) registry->m_dirtyComposers.insert( oldComposer ); if( newComposer ) registry->m_dirtyComposers.insert( newComposer ); if( oldGenre ) registry->m_dirtyGenres.insert( oldGenre ); if( newGenre ) registry->m_dirtyGenres.insert( newGenre ); if( oldYear ) registry->m_dirtyYears.insert( oldYear ); if( newYear ) registry->m_dirtyYears.insert( newYear ); } else error() << Q_FUNC_INFO << "non-positive urlId, zero deviceId or non-positive" << "directoryId encountered in track" << m_url << "urlId:" << m_urlId << "deviceId:" << m_deviceId << "directoryId:" << m_directoryId << "- not writing back metadata" << "changes to the database."; m_lock.unlock(); // or else we provoke a deadlock // copy the image BUG: 203211 (we need to do it here or provoke a dead lock) if( oldAlbum && newAlbum ) { bool oldSupp = oldAlbum->suppressImageAutoFetch(); bool newSupp = newAlbum->suppressImageAutoFetch(); oldAlbum->setSuppressImageAutoFetch( true ); newAlbum->setSuppressImageAutoFetch( true ); if( oldAlbum->hasImage() && !newAlbum->hasImage() ) newAlbum->setImage( oldAlbum->imageLocation().path() ); oldAlbum->setSuppressImageAutoFetch( oldSupp ); newAlbum->setSuppressImageAutoFetch( newSupp ); } if( registry ) registry->commitDirtyTracks(); // calls notifyObservers() as appropriate else notifyObservers(); m_lock.lockForWrite(); // reset back to state it was during call if( m_uid != oldUid ) { updatePlaylistsToDb( m_cache, oldUid ); updateEmbeddedCoversToDb( m_cache, oldUid ); } // --- clean up m_cache.clear(); } void SqlTrack::updatePlaylistsToDb( const FieldHash &fields, const QString &oldUid ) { if( fields.isEmpty() ) return; // nothing to do auto storage = m_collection->sqlStorage(); QStringList tags; // keep this in sync with SqlPlaylist::saveTracks()! if( fields.contains( Meta::valUrl ) ) tags << QString( "url='%1'" ).arg( storage->escape( m_url.path() ) ); if( fields.contains( Meta::valTitle ) ) tags << QString( "title='%1'" ).arg( storage->escape( m_title ) ); if( fields.contains( Meta::valAlbum ) ) tags << QString( "album='%1'" ).arg( m_album ? storage->escape( m_album->prettyName() ) : "" ); if( fields.contains( Meta::valArtist ) ) tags << QString( "artist='%1'" ).arg( m_artist ? storage->escape( m_artist->prettyName() ) : "" ); if( fields.contains( Meta::valLength ) ) tags << QString( "length=%1").arg( QString::number( m_length ) ); if( fields.contains( Meta::valUniqueId ) ) { // SqlPlaylist mirrors uniqueid to url, update it too, bug 312128 tags << QString( "url='%1'" ).arg( storage->escape( m_uid ) ); tags << QString( "uniqueid='%1'" ).arg( storage->escape( m_uid ) ); } if( !tags.isEmpty() ) { QString update = "UPDATE playlist_tracks SET %1 WHERE uniqueid = '%2';"; update = update.arg( tags.join( ", " ), storage->escape( oldUid ) ); storage->query( update ); } } void SqlTrack::updateEmbeddedCoversToDb( const FieldHash &fields, const QString &oldUid ) { if( fields.isEmpty() ) return; // nothing to do auto storage = m_collection->sqlStorage(); QString tags; if( fields.contains( Meta::valUniqueId ) ) tags += QString( ",path='%1'" ).arg( storage->escape( m_uid ) ); if( !tags.isEmpty() ) { tags = tags.remove(0, 1); // the first character is always a ',' QString update = "UPDATE images SET %1 WHERE path = '%2';"; update = update.arg( tags, storage->escape( oldUid ) ); storage->query( update ); } } QString SqlTrack::prettyTitle( const QString &filename ) //static { QString s = filename; //just so the code is more readable //remove .part extension if it exists if (s.endsWith( ".part" )) s = s.left( s.length() - 5 ); //remove file extension, s/_/ /g and decode %2f-like sequences s = s.left( s.lastIndexOf( QLatin1Char('.') ) ).replace( '_', ' ' ); s = QUrl::fromPercentEncoding( s.toLatin1() ); return s; } bool SqlTrack::inCollection() const { QReadLocker locker( &m_lock ); return m_trackId > 0; } Collections::Collection* SqlTrack::collection() const { return m_collection; } QString SqlTrack::cachedLyrics() const { /* We don't cache the string as it may be potentially very long */ QString query = QStringLiteral( "SELECT lyrics FROM lyrics WHERE url = %1" ).arg( m_urlId ); QStringList result = m_collection->sqlStorage()->query( query ); if( result.isEmpty() ) return QString(); return result.first(); } void SqlTrack::setCachedLyrics( const QString &lyrics ) { QString query = QString( "SELECT count(*) FROM lyrics WHERE url = %1").arg( m_urlId ); const QStringList queryResult = m_collection->sqlStorage()->query( query ); if( queryResult.isEmpty() ) return; // error in the query? if( queryResult.first().toInt() == 0 ) { QString insert = QString( "INSERT INTO lyrics( url, lyrics ) VALUES ( %1, '%2' )" ) .arg( QString::number( m_urlId ), m_collection->sqlStorage()->escape( lyrics ) ); m_collection->sqlStorage()->insert( insert, "lyrics" ); } else { QString update = QString( "UPDATE lyrics SET lyrics = '%1' WHERE url = %2" ) .arg( m_collection->sqlStorage()->escape( lyrics ), QString::number( m_urlId ) ); m_collection->sqlStorage()->query( update ); } notifyObservers(); } bool SqlTrack::hasCapabilityInterface( Capabilities::Capability::Type type ) const { switch( type ) { case Capabilities::Capability::Actions: case Capabilities::Capability::Organisable: case Capabilities::Capability::BookmarkThis: case Capabilities::Capability::WriteTimecode: case Capabilities::Capability::LoadTimecode: case Capabilities::Capability::ReadLabel: case Capabilities::Capability::WriteLabel: case Capabilities::Capability::FindInSource: return true; default: return Track::hasCapabilityInterface( type ); } } Capabilities::Capability* SqlTrack::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::Actions: { QList actions; //TODO These actions will hang around until m_collection is destructed. // Find a better parent to avoid this memory leak. //actions.append( new CopyToDeviceAction( m_collection, this ) ); return new Capabilities::ActionsCapability( actions ); } case Capabilities::Capability::Organisable: return new Capabilities::OrganiseCapabilityImpl( this ); case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkCurrentTrackPositionAction( 0 ) ); case Capabilities::Capability::WriteTimecode: return new Capabilities::TimecodeWriteCapabilityImpl( this ); case Capabilities::Capability::LoadTimecode: return new Capabilities::TimecodeLoadCapabilityImpl( this ); case Capabilities::Capability::ReadLabel: return new Capabilities::SqlReadLabelCapability( this, sqlCollection()->sqlStorage() ); case Capabilities::Capability::WriteLabel: return new Capabilities::SqlWriteLabelCapability( this, sqlCollection()->sqlStorage() ); case Capabilities::Capability::FindInSource: return new Capabilities::FindInSourceCapabilityImpl( this ); default: return Track::createCapabilityInterface( type ); } } void SqlTrack::addLabel( const QString &label ) { Meta::LabelPtr realLabel = m_collection->registry()->getLabel( label ); addLabel( realLabel ); } void SqlTrack::addLabel( const Meta::LabelPtr &label ) { AmarokSharedPointer sqlLabel = AmarokSharedPointer::dynamicCast( label ); if( !sqlLabel ) { Meta::LabelPtr tmp = m_collection->registry()->getLabel( label->name() ); sqlLabel = AmarokSharedPointer::dynamicCast( tmp ); } if( sqlLabel ) { QWriteLocker locker( &m_lock ); commitIfInNonBatchUpdate(); // we need to have a up-to-date m_urlId if( m_urlId <= 0 ) { warning() << "Track does not have an urlId."; return; } QString countQuery = "SELECT COUNT(*) FROM urls_labels WHERE url = %1 AND label = %2;"; QStringList countRs = m_collection->sqlStorage()->query( countQuery.arg( QString::number( m_urlId ), QString::number( sqlLabel->id() ) ) ); if( !countRs.isEmpty() && countRs.first().toInt() == 0 ) { QString insert = "INSERT INTO urls_labels(url,label) VALUES (%1,%2);"; m_collection->sqlStorage()->insert( insert.arg( QString::number( m_urlId ), QString::number( sqlLabel->id() ) ), "urls_labels" ); if( m_labelsInCache ) { m_labelsCache.append( Meta::LabelPtr::staticCast( sqlLabel ) ); } locker.unlock(); notifyObservers(); sqlLabel->invalidateCache(); } } } int SqlTrack::id() const { QReadLocker locker( &m_lock ); return m_trackId; } int SqlTrack::urlId() const { QReadLocker locker( &m_lock ); return m_urlId; } void SqlTrack::removeLabel( const Meta::LabelPtr &label ) { AmarokSharedPointer sqlLabel = AmarokSharedPointer::dynamicCast( label ); if( !sqlLabel ) { Meta::LabelPtr tmp = m_collection->registry()->getLabel( label->name() ); sqlLabel = AmarokSharedPointer::dynamicCast( tmp ); } if( sqlLabel ) { QString query = "DELETE FROM urls_labels WHERE label = %2 and url = (SELECT url FROM tracks WHERE id = %1);"; m_collection->sqlStorage()->query( query.arg( QString::number( m_trackId ), QString::number( sqlLabel->id() ) ) ); if( m_labelsInCache ) { m_labelsCache.removeAll( Meta::LabelPtr::staticCast( sqlLabel ) ); } notifyObservers(); sqlLabel->invalidateCache(); } } Meta::LabelList SqlTrack::labels() const { { QReadLocker locker( &m_lock ); if( m_labelsInCache ) return m_labelsCache; } if( !m_collection ) return Meta::LabelList(); // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Label ); qm->addMatch( Meta::TrackPtr( const_cast(this) ) ); qm->setBlocking( true ); qm->run(); { QWriteLocker locker( &m_lock ); m_labelsInCache = true; m_labelsCache = qm->labels(); delete qm; return m_labelsCache; } } TrackEditorPtr SqlTrack::editor() { return TrackEditorPtr( isEditable() ? this : 0 ); } StatisticsPtr SqlTrack::statistics() { return StatisticsPtr( this ); } void SqlTrack::remove() { QWriteLocker locker( &m_lock ); m_cache.clear(); locker.unlock(); m_collection->registry()->removeTrack( m_urlId, m_uid ); // -- inform all albums, artist, years #undef foreachInvalidateCache #define INVALIDATE_AND_UPDATE(X) if( X ) \ { \ X->invalidateCache(); \ X->notifyObservers(); \ } INVALIDATE_AND_UPDATE(static_cast(m_artist.data())); INVALIDATE_AND_UPDATE(static_cast(m_album.data())); INVALIDATE_AND_UPDATE(static_cast(m_composer.data())); INVALIDATE_AND_UPDATE(static_cast(m_genre.data())); INVALIDATE_AND_UPDATE(static_cast(m_year.data())); #undef INVALIDATE_AND_UPDATE m_artist = 0; m_album = 0; m_composer = 0; m_genre = 0; m_year = 0; m_urlId = 0; m_trackId = 0; m_statisticsId = 0; m_collection->collectionUpdated(); } //---------------------- class Artist -------------------------- SqlArtist::SqlArtist( Collections::SqlCollection *collection, int id, const QString &name ) : Artist() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } Meta::SqlArtist::~SqlArtist() { } void SqlArtist::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlArtist::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::ArtistPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } bool SqlArtist::hasCapabilityInterface( Capabilities::Capability::Type type ) const { switch( type ) { case Capabilities::Capability::BookmarkThis: return true; default: return Artist::hasCapabilityInterface( type ); } } Capabilities::Capability* SqlArtist::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkArtistAction( 0, Meta::ArtistPtr( this ) ) ); default: return Artist::createCapabilityInterface( type ); } } //--------------- class Album --------------------------------- const QString SqlAlbum::AMAROK_UNSET_MAGIC = QString( "AMAROK_UNSET_MAGIC" ); SqlAlbum::SqlAlbum( Collections::SqlCollection *collection, int id, const QString &name, int artist ) : Album() , m_collection( collection ) , m_name( name ) , m_id( id ) , m_artistId( artist ) , m_imageId( -1 ) , m_hasImage( false ) , m_hasImageChecked( false ) , m_unsetImageId( -1 ) , m_tracksLoaded( NotLoaded ) , m_suppressAutoFetch( false ) , m_mutex( QMutex::Recursive ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } Meta::SqlAlbum::~SqlAlbum() { CoverCache::invalidateAlbum( this ); } void SqlAlbum::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = NotLoaded; m_hasImage = false; m_hasImageChecked = false; m_tracks.clear(); } TrackList SqlAlbum::tracks() { bool startQuery = false; { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded == Loaded ) return m_tracks; else if( m_tracksLoaded == NotLoaded ) { startQuery = true; m_tracksLoaded = Loading; } } if( startQuery ) { // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::AlbumPtr( this ) ); qm->orderBy( Meta::valDiscNr ); qm->orderBy( Meta::valTrackNr ); qm->orderBy( Meta::valTitle ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = Loaded; delete qm; return m_tracks; } } else { // Wait for tracks to be loaded forever { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded == Loaded ) return m_tracks; else QThread::yieldCurrentThread(); } } } // note for internal implementation: // if hasImage returns true then m_imagePath is set bool SqlAlbum::hasImage( int size ) const { Q_UNUSED(size); // we have every size if we have an image at all QMutexLocker locker( &m_mutex ); if( m_name.isEmpty() ) return false; if( !m_hasImageChecked ) { m_hasImageChecked = true; const_cast( this )->largeImagePath(); // The user has explicitly set no cover if( m_imagePath == AMAROK_UNSET_MAGIC ) m_hasImage = false; // if we don't have an image but it was not explicitly blocked else if( m_imagePath.isEmpty() ) { // Cover fetching runs in another thread. If there is a retrieved cover // then updateImage() gets called which updates the cache and alerts the // subscribers. We use queueAlbum() because this runs the fetch as a // background job and doesn't give an intruding popup asking for confirmation if( !m_suppressAutoFetch && !m_name.isEmpty() && AmarokConfig::autoGetCoverArt() ) CoverFetcher::instance()->queueAlbum( AlbumPtr(const_cast(this)) ); m_hasImage = false; } else m_hasImage = true; } return m_hasImage; } QImage SqlAlbum::image( int size ) const { QMutexLocker locker( &m_mutex ); if( !hasImage() ) return Meta::Album::image( size ); // findCachedImage looks for a scaled version of the fullsize image // which may have been saved on a previous lookup QString cachedImagePath; if( size <= 1 ) cachedImagePath = m_imagePath; else cachedImagePath = scaledDiskCachePath( size ); //FIXME this cache doesn't differentiate between shadowed/unshadowed // a image exists. just load it. if( !cachedImagePath.isEmpty() && QFile( cachedImagePath ).exists() ) { QImage image( cachedImagePath ); if( image.isNull() ) return Meta::Album::image( size ); return image; } // no cached scaled image exists. Have to create it QImage image; // --- embedded cover if( m_collection && m_imagePath.startsWith( m_collection->uidUrlProtocol() ) ) { // -- check if we have a track with the given path as uid Meta::TrackPtr track = m_collection->getTrackFromUid( m_imagePath ); if( track ) image = Meta::Tag::embeddedCover( track->playableUrl().path() ); } // --- a normal path if( image.isNull() ) image = QImage( m_imagePath ); if( image.isNull() ) return Meta::Album::image( size ); if( size > 1 && size < 1000 ) { image = image.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation ); image.save( cachedImagePath, "PNG", -1 ); } return image; } QUrl SqlAlbum::imageLocation( int size ) { if( !hasImage() ) return QUrl(); // findCachedImage looks for a scaled version of the fullsize image // which may have been saved on a previous lookup if( size <= 1 ) return QUrl::fromLocalFile( m_imagePath ); QString cachedImagePath = scaledDiskCachePath( size ); if( cachedImagePath.isEmpty() ) return QUrl(); if( !QFile( cachedImagePath ).exists() ) { // If we don't have the location, it's possible that we haven't tried to find the image yet // So, let's look for it and just ignore the result QImage i = image( size ); Q_UNUSED( i ) } if( !QFile( cachedImagePath ).exists() ) return QUrl(); return QUrl::fromLocalFile(cachedImagePath); } void SqlAlbum::setImage( const QImage &image ) { // the unnamed album is special. it will never have an image if( m_name.isEmpty() ) return; if( image.isNull() ) return; QMutexLocker locker( &m_mutex ); // removeImage() will destroy all scaled cached versions of the artwork // and remove references from the database if required. removeImage(); QString path = largeDiskCachePath(); // make sure not to overwrite existing images while( QFile(path).exists() ) path += '_'; // not that nice but it shouldn't happen that often. image.save( path, "JPG", -1 ); setImage( path ); locker.unlock(); notifyObservers(); // -- write back the album cover if allowed if( AmarokConfig::writeBackCover() ) { // - scale to cover to a sensible size QImage scaledImage( image ); if( scaledImage.width() > AmarokConfig::writeBackCoverDimensions() || scaledImage.height() > AmarokConfig::writeBackCoverDimensions() ) scaledImage = scaledImage.scaled( AmarokConfig::writeBackCoverDimensions(), AmarokConfig::writeBackCoverDimensions(), Qt::KeepAspectRatio, Qt::SmoothTransformation ); // - set the image for each track Meta::TrackList myTracks = tracks(); foreach( Meta::TrackPtr metaTrack, myTracks ) { // the song needs to be at least one mb big or we won't set an image // that means that the new image will increase the file size by less than 2% if( metaTrack->filesize() > 1024l * 1024l ) { Meta::FieldHash fields; fields.insert( Meta::valImage, scaledImage ); WriteTagsJob *job = new WriteTagsJob( metaTrack->playableUrl().path(), fields ); QObject::connect( job, &WriteTagsJob::done, job, &WriteTagsJob::deleteLater ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(job) ); } // note: we might want to update the track file size after writing the image } } } void SqlAlbum::removeImage() { QMutexLocker locker( &m_mutex ); if( !hasImage() ) return; // Update the database image path // Set the album image to a magic value which will tell Amarok not to fetch it automatically const int unsetId = unsetImageId(); QString query = "UPDATE albums SET image = %1 WHERE id = %2"; m_collection->sqlStorage()->query( query.arg( QString::number( unsetId ), QString::number( m_id ) ) ); // From here on we check if there are any remaining references to that particular image in the database // If there aren't, then we should remove the image path from the database ( and possibly delete the file? ) // If there are, we need to leave it since other albums will reference this particular image path. // query = "SELECT count( albums.id ) FROM albums " "WHERE albums.image = %1"; QStringList res = m_collection->sqlStorage()->query( query.arg( QString::number( m_imageId ) ) ); if( !res.isEmpty() ) { int references = res.first().toInt(); // If there are no more references to this particular image, then we should clean up if( references <= 0 ) { query = "DELETE FROM images WHERE id = %1"; m_collection->sqlStorage()->query( query.arg( QString::number( m_imageId ) ) ); // remove the large cover only if it was cached. QDir largeCoverDir( Amarok::saveLocation( "albumcovers/large/" ) ); if( QFileInfo(m_imagePath).absoluteDir() == largeCoverDir ) QFile::remove( m_imagePath ); // remove all cache images QString key = md5sum( QString(), QString(), m_imagePath ); QDir cacheDir( Amarok::saveLocation( "albumcovers/cache/" ) ); QStringList cacheFilter; cacheFilter << QString( "*@" ) + key; QStringList cachedImages = cacheDir.entryList( cacheFilter ); foreach( const QString &image, cachedImages ) { bool r = QFile::remove( cacheDir.filePath( image ) ); debug() << "deleting cached image: " << image << " : " + ( r ? QStringLiteral("ok") : QStringLiteral("fail") ); } CoverCache::invalidateAlbum( this ); } } m_imageId = -1; m_imagePath.clear(); m_hasImage = false; m_hasImageChecked = true; locker.unlock(); notifyObservers(); } int SqlAlbum::unsetImageId() const { // Return the cached value if we have already done the lookup before if( m_unsetImageId >= 0 ) return m_unsetImageId; QString query = "SELECT id FROM images WHERE path = '%1'"; QStringList res = m_collection->sqlStorage()->query( query.arg( AMAROK_UNSET_MAGIC ) ); // We already have the AMAROK_UNSET_MAGIC variable in the database if( !res.isEmpty() ) { m_unsetImageId = res.first().toInt(); } else { // We need to create this value query = QString( "INSERT INTO images( path ) VALUES ( '%1' )" ) .arg( m_collection->sqlStorage()->escape( AMAROK_UNSET_MAGIC ) ); m_unsetImageId = m_collection->sqlStorage()->insert( query, "images" ); } return m_unsetImageId; } bool SqlAlbum::isCompilation() const { return !hasAlbumArtist(); } bool SqlAlbum::hasAlbumArtist() const { return !albumArtist().isNull(); } Meta::ArtistPtr SqlAlbum::albumArtist() const { if( m_artistId > 0 && !m_artist ) { const_cast( this )->m_artist = m_collection->registry()->getArtist( m_artistId ); } return m_artist; } QByteArray SqlAlbum::md5sum( const QString& artist, const QString& album, const QString& file ) const { // FIXME: All existing image stores have been invalidated. return QCryptographicHash::hash( artist.toLower().toUtf8() + QByteArray( "#" ) + album.toLower().toUtf8() + QByteArray( "?" ) + file.toUtf8(), QCryptographicHash::Md5 ).toHex(); } QString SqlAlbum::largeDiskCachePath() const { // IMPROVEMENT: the large disk cache path could be human readable const QString artist = hasAlbumArtist() ? albumArtist()->name() : QString(); if( artist.isEmpty() && m_name.isEmpty() ) return QString(); QDir largeCoverDir( Amarok::saveLocation( "albumcovers/large/" ) ); const QString key = md5sum( artist, m_name, QString() ); if( !key.isEmpty() ) return largeCoverDir.filePath( key ); return QString(); } QString SqlAlbum::scaledDiskCachePath( int size ) const { const QByteArray widthKey = QByteArray::number( size ) + '@'; QDir cacheCoverDir( Amarok::saveLocation( "albumcovers/cache/" ) ); QString key = md5sum( QString(), QString(), m_imagePath ); if( !cacheCoverDir.exists( widthKey + key ) ) { // the correct location is empty // check deprecated locations for the image cache and delete them // (deleting the scaled image cache is fine) const QString artist = hasAlbumArtist() ? albumArtist()->name() : QString(); if( artist.isEmpty() && m_name.isEmpty() ) ; // do nothing special else { QString oldKey; oldKey = md5sum( artist, m_name, m_imagePath ); if( cacheCoverDir.exists( widthKey + oldKey ) ) cacheCoverDir.remove( widthKey + oldKey ); oldKey = md5sum( artist, m_name, QString() ); if( cacheCoverDir.exists( widthKey + oldKey ) ) cacheCoverDir.remove( widthKey + oldKey ); } } return cacheCoverDir.filePath( widthKey + key ); } QString SqlAlbum::largeImagePath() { if( !m_collection ) return m_imagePath; // Look up in the database QString query = "SELECT images.id, images.path FROM images, albums WHERE albums.image = images.id AND albums.id = %1;"; // TODO: shouldn't we do a JOIN here? QStringList res = m_collection->sqlStorage()->query( query.arg( m_id ) ); if( !res.isEmpty() ) { m_imageId = res.at(0).toInt(); m_imagePath = res.at(1); // explicitly deleted image if( m_imagePath == AMAROK_UNSET_MAGIC ) return AMAROK_UNSET_MAGIC; // embedded image (e.g. id3v2 APIC // We store embedded images as unique ids in the database // we will get the real image later on from the track. if( m_imagePath.startsWith( m_collection->uidUrlProtocol()+"://" ) ) return m_imagePath; // normal file if( !m_imagePath.isEmpty() && QFile::exists( m_imagePath ) ) return m_imagePath; } // After a rescan we currently lose all image information, so we need // to check that we haven't already downloaded this image before. m_imagePath = largeDiskCachePath(); if( !m_imagePath.isEmpty() && QFile::exists( m_imagePath ) ) { setImage(m_imagePath); return m_imagePath; } m_imageId = -1; m_imagePath.clear(); return m_imagePath; } // note: we won't notify the observers. we are a private function. the caller must do that. void SqlAlbum::setImage( const QString &path ) { if( m_name.isEmpty() ) // the empty album never has an image return; QMutexLocker locker( &m_mutex ); if( m_imagePath == path ) return; QString query = "SELECT id FROM images WHERE path = '%1'"; query = query.arg( m_collection->sqlStorage()->escape( path ) ); QStringList res = m_collection->sqlStorage()->query( query ); if( res.isEmpty() ) { QString insert = QString( "INSERT INTO images( path ) VALUES ( '%1' )" ) .arg( m_collection->sqlStorage()->escape( path ) ); m_imageId = m_collection->sqlStorage()->insert( insert, "images" ); } else m_imageId = res.first().toInt(); if( m_imageId >= 0 ) { query = QStringLiteral("UPDATE albums SET image = %1 WHERE albums.id = %2" ) .arg( QString::number( m_imageId ), QString::number( m_id ) ); m_collection->sqlStorage()->query( query ); m_imagePath = path; m_hasImage = true; m_hasImageChecked = true; CoverCache::invalidateAlbum( this ); } } /** Set the compilation flag. * Actually it does not change this album but instead moves * the tracks to other albums (e.g. one with the same name which is a * compilation) * If the compilation flag is set to "false" then all songs * with different artists will be moved to other albums, possibly even * creating them. */ void SqlAlbum::setCompilation( bool compilation ) { if( m_name.isEmpty() ) return; if( isCompilation() == compilation ) { return; } else { m_collection->blockUpdatedSignal(); if( compilation ) { // get the new compilation album Meta::AlbumPtr metaAlbum = m_collection->registry()->getAlbum( name(), QString() ); AmarokSharedPointer sqlAlbum = AmarokSharedPointer::dynamicCast( metaAlbum ); Meta::FieldHash changes; changes.insert( Meta::valCompilation, 1); Meta::TrackList myTracks = tracks(); foreach( Meta::TrackPtr metaTrack, myTracks ) { SqlTrack* sqlTrack = static_cast(metaTrack.data()); // copy over the cover image if( sqlTrack->album()->hasImage() && !sqlAlbum->hasImage() ) sqlAlbum->setImage( sqlTrack->album()->imageLocation().path() ); // move the track sqlTrack->setAlbum( sqlAlbum->id() ); if( AmarokConfig::writeBack() ) Meta::Tag::writeTags( sqlTrack->playableUrl().path(), changes, AmarokConfig::writeBackStatistics() ); } /* TODO: delete all old tracks albums */ } else { Meta::FieldHash changes; changes.insert( Meta::valCompilation, 0); Meta::TrackList myTracks = tracks(); foreach( Meta::TrackPtr metaTrack, myTracks ) { SqlTrack* sqlTrack = static_cast(metaTrack.data()); Meta::ArtistPtr trackArtist = sqlTrack->artist(); // get the new album Meta::AlbumPtr metaAlbum = m_collection->registry()->getAlbum( sqlTrack->album()->name(), trackArtist ? ArtistHelper::realTrackArtist( trackArtist->name() ) : QString() ); AmarokSharedPointer sqlAlbum = AmarokSharedPointer::dynamicCast( metaAlbum ); // copy over the cover image if( sqlTrack->album()->hasImage() && !sqlAlbum->hasImage() ) sqlAlbum->setImage( sqlTrack->album()->imageLocation().path() ); // move the track sqlTrack->setAlbum( sqlAlbum->id() ); if( AmarokConfig::writeBack() ) Meta::Tag::writeTags( sqlTrack->playableUrl().path(), changes, AmarokConfig::writeBackStatistics() ); } /* TODO //step 5: delete the original album, if necessary */ } m_collection->unblockUpdatedSignal(); } } bool SqlAlbum::hasCapabilityInterface( Capabilities::Capability::Type type ) const { if( m_name.isEmpty() ) return false; switch( type ) { case Capabilities::Capability::Actions: case Capabilities::Capability::BookmarkThis: return true; default: return Album::hasCapabilityInterface( type ); } } Capabilities::Capability* SqlAlbum::createCapabilityInterface( Capabilities::Capability::Type type ) { if( m_name.isEmpty() ) return 0; switch( type ) { case Capabilities::Capability::Actions: return new Capabilities::AlbumActionsCapability( Meta::AlbumPtr( this ) ); case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkAlbumAction( 0, Meta::AlbumPtr( this ) ) ); default: return Album::createCapabilityInterface( type ); } } //---------------SqlComposer--------------------------------- SqlComposer::SqlComposer( Collections::SqlCollection *collection, int id, const QString &name ) : Composer() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlComposer::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlComposer::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::ComposerPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } //---------------SqlGenre--------------------------------- SqlGenre::SqlGenre( Collections::SqlCollection *collection, int id, const QString &name ) : Genre() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlGenre::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlGenre::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::GenrePtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } //---------------SqlYear--------------------------------- SqlYear::SqlYear( Collections::SqlCollection *collection, int id, int year) : Year() , m_collection( collection ) , m_id( id ) , m_year( year ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlYear::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlYear::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::YearPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } //---------------SqlLabel--------------------------------- SqlLabel::SqlLabel( Collections::SqlCollection *collection, int id, const QString &name ) : Label() , m_collection( collection ) , m_id( id ) , m_name( name ) , m_tracksLoaded( false ) { Q_ASSERT( m_collection ); Q_ASSERT( m_id > 0 ); } void SqlLabel::invalidateCache() { QMutexLocker locker( &m_mutex ); m_tracksLoaded = false; m_tracks.clear(); } TrackList SqlLabel::tracks() { { QMutexLocker locker( &m_mutex ); if( m_tracksLoaded ) return m_tracks; } // when running the query maker don't lock. might lead to deadlock via registry Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->addMatch( Meta::LabelPtr( this ) ); qm->setBlocking( true ); qm->run(); { QMutexLocker locker( &m_mutex ); m_tracks = qm->tracks(); m_tracksLoaded = true; delete qm; return m_tracks; } } diff --git a/src/core-impl/collections/db/sql/SqlRegistry_p.cpp b/src/core-impl/collections/db/sql/SqlRegistry_p.cpp index 6542dd4858..0eee72b87b 100644 --- a/src/core-impl/collections/db/sql/SqlRegistry_p.cpp +++ b/src/core-impl/collections/db/sql/SqlRegistry_p.cpp @@ -1,347 +1,347 @@ /**************************************************************************************** * Copyright (c) 2010 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "SqlRegistryP" #include "core/support/Debug.h" #include "SqlRegistry_p.h" #include "SqlMeta.h" #include "SqlCollection.h" void AbstractTrackTableCommitter::commit( const QList &tracks ) { // Note: The code is greatly inspired by the old ScanResultProcessor // by jeffrai // Note2: The code is optimized for batch update. // Reason: a single update is completely harmless and not frequent. // The real difficulty is the collection scanner and it's runtime // Especially with collections larger than 30000 tracks. if( tracks.isEmpty() ) return; m_storage = tracks.first()->sqlCollection()->sqlStorage(); // -- get the maximum size for our commit static int maxSize = 0; if( maxSize == 0 ) { QStringList res = m_storage->query( "SHOW VARIABLES LIKE 'max_allowed_packet';" ); if( res.size() < 2 || res[1].toInt() == 0 ) { warning() << "Uh oh! For some reason MySQL thinks there isn't a max allowed size!"; return; } debug() << "obtained max_allowed_packet is " << res[1]; maxSize = res[1].toInt() / 3; //for safety, due to multibyte encoding } QStringList fields = getFields(); const QString updateQueryStart = "UPDATE LOW_PRIORITY "+tableName()+" SET "; const QString insertQueryStart = "INSERT INTO "+tableName()+ " ("+fields.join(",")+") VALUES "; QList< Meta::SqlTrackPtr > insertedTracks; QString insertQuery; insertQuery.reserve( 1024 ); // a sensible initial size foreach( Meta::SqlTrackPtr track, tracks ) { QStringList values = getValues( track.data() ); // -- update if( getId( track.data() ) > 0 ) { // we just commit all values to save code complexity. // we would need to track the real changed fields otherwise QString updateQuery; updateQuery.reserve( 256 ); // a sensible initial size for( int i = 0; i < fields.count() && i < values.count(); i++ ) { if( !updateQuery.isEmpty() ) updateQuery += ", "; updateQuery += fields.at( i ); updateQuery += '='; updateQuery += values.at( i ); } updateQuery = updateQueryStart + updateQuery + " WHERE id=" + QString::number( getId( track.data() ) ) + ';'; m_storage->query( updateQuery ); } else // -- insert { QString newValues = '(' + values.join(",") + ')'; // - if the insertQuery is long enough, commit it. if( insertQueryStart.length() + insertQuery.length() + newValues.length() + 1 >= maxSize - 3 ) // ";" { // commit insertQuery = insertQueryStart + insertQuery + ';'; int firstId = m_storage->insert( insertQuery, tableName() ); // set the resulting ids if( firstId <= 0 ) warning() << "Insert failed."; for( int i = 0; i < insertedTracks.count(); i++ ) setId( const_cast(insertedTracks.at( i ).data()), firstId + i ); insertQuery.clear(); insertedTracks.clear(); } if( !insertQuery.isEmpty() ) insertQuery += ','; insertQuery += newValues; insertedTracks.append( track ); } } // - insert the rest if( !insertQuery.isEmpty() ) { // commit insertQuery = insertQueryStart + insertQuery + ';'; int firstId = m_storage->insert( insertQuery, tableName() ); // set the resulting ids if( firstId <= 0 ) warning() << "Insert failed."; for( int i = 0; i < insertedTracks.count(); i++ ) setId( const_cast(insertedTracks.at( i ).data()), firstId + i ); insertQuery.clear(); insertedTracks.clear(); } } // --- some help functions for the query QString AbstractTrackTableCommitter::nullString( const QString &str ) const { if( str.isEmpty() ) return "NULL"; else return str; } QString AbstractTrackTableCommitter::nullNumber( const qint64 number ) const { if( number <= 0 ) return "NULL"; else return QString::number( number ); } QString AbstractTrackTableCommitter::nullNumber( const int number ) const { if( number <= 0 ) return "NULL"; else return QString::number( number ); } QString AbstractTrackTableCommitter::nullNumber( const double number ) const { if( number <= 0 ) return "NULL"; else return QString::number( number ); } QString AbstractTrackTableCommitter::nullDate( const QDateTime &date ) const { if( date.isValid() ) - return QString::number( date.toTime_t() ); + return QString::number( date.toSecsSinceEpoch() ); else return "NULL"; } QString AbstractTrackTableCommitter::escape( const QString &str ) const { return '\'' + m_storage->escape( str ) + '\''; } // ------------ urls --------------- QString TrackUrlsTableCommitter::tableName() { return "urls"; } int TrackUrlsTableCommitter::getId( Meta::SqlTrack *track ) { return track->m_urlId; } void TrackUrlsTableCommitter::setId( Meta::SqlTrack *track, int id ) { track->m_urlId = id; } QStringList TrackUrlsTableCommitter::getFields() { QStringList result; result << "deviceid" << "rpath" << "directory" << "uniqueid"; return result; } QStringList TrackUrlsTableCommitter::getValues( Meta::SqlTrack *track ) { QStringList result; Q_ASSERT( track->m_deviceId != 0 && "refusing to write zero deviceId to urls table, please file a bug" ); result << QString::number( track->m_deviceId ); result << escape( track->m_rpath ); Q_ASSERT( track->m_directoryId > 0 && "refusing to write non-positive directoryId to urls table, please file a bug" ); result << nullNumber( track->m_directoryId ); result << escape( track->m_uid ); return result; } // ------------ tracks --------------- QString TrackTracksTableCommitter::tableName() { return "tracks"; } int TrackTracksTableCommitter::getId( Meta::SqlTrack *track ) { return track->m_trackId; } void TrackTracksTableCommitter::setId( Meta::SqlTrack *track, int id ) { track->m_trackId = id; } QStringList TrackTracksTableCommitter::getFields() { QStringList result; result << "url" << "artist" << "album" << "genre" << "composer" << "year" << "title" << "comment" << "tracknumber" << "discnumber" << "bitrate" << "length" << "samplerate" << "filesize" << "filetype" << "bpm" << "createdate" << "modifydate" << "albumgain" << "albumpeakgain" << "trackgain" << "trackpeakgain"; return result; } QStringList TrackTracksTableCommitter::getValues( Meta::SqlTrack *track ) { QStringList result; Q_ASSERT( track->m_urlId > 0 && "refusing to write non-positive urlId to tracks table, please file a bug" ); result << QString::number( track->m_urlId ); result << QString::number( track->m_artist ? AmarokSharedPointer::staticCast( track->m_artist )->id() : -1 ); result << QString::number( track->m_album ? AmarokSharedPointer::staticCast( track->m_album )->id() : -1 ); result << QString::number( track->m_genre ? AmarokSharedPointer::staticCast( track->m_genre )->id() : -1 ); result << QString::number( track->m_composer ? AmarokSharedPointer::staticCast( track->m_composer )->id() : -1 ); result << QString::number( track->m_year ? AmarokSharedPointer::staticCast( track->m_year )->id() : -1 ); result << escape( track->m_title ); result << escape( track->m_comment ); result << nullNumber( track->m_trackNumber ); result << nullNumber( track->m_discNumber ); result << nullNumber( track->m_bitrate ); result << nullNumber( track->m_length ); result << nullNumber( track->m_sampleRate ); result << nullNumber( track->m_filesize ); result << nullNumber( int(track->m_filetype) ); result << nullNumber( track->m_bpm ); result << nullDate( track->m_createDate ); result << nullDate( track->m_modifyDate ); result << QString::number( track->m_albumGain ); result << QString::number( track->m_albumPeakGain ); result << QString::number( track->m_trackGain ); result << QString::number( track->m_trackPeakGain ); return result; } // ------------ statistics --------------- QString TrackStatisticsTableCommitter::tableName() { return "statistics"; } int TrackStatisticsTableCommitter::getId( Meta::SqlTrack *track ) { return track->m_statisticsId; } void TrackStatisticsTableCommitter::setId( Meta::SqlTrack *track, int id ) { track->m_statisticsId = id; } QStringList TrackStatisticsTableCommitter::getFields() { QStringList result; result << "url" << "createdate" << "accessdate" << "score" << "rating" << "playcount" << "deleted"; return result; } QStringList TrackStatisticsTableCommitter::getValues( Meta::SqlTrack *track ) { QStringList result; Q_ASSERT( track->m_urlId > 0 && "refusing to write non-positive urlId to statistics table, please file a bug" ); result << QString::number( track->m_urlId ); result << nullDate( track->m_firstPlayed ); result << nullDate( track->m_lastPlayed ); result << nullNumber( track->m_score ); result << QString::number( track->m_rating ); // NOT NULL result << QString::number( track->m_playCount ); // NOT NULL result << "0"; // not deleted return result; } diff --git a/src/core-impl/collections/support/MemoryFilter.cpp b/src/core-impl/collections/support/MemoryFilter.cpp index 0bfd9cb2eb..9cd1b3b709 100644 --- a/src/core-impl/collections/support/MemoryFilter.cpp +++ b/src/core-impl/collections/support/MemoryFilter.cpp @@ -1,277 +1,277 @@ /**************************************************************************************** * Copyright (c) 2008 Maximilian Kossick * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "MemoryFilter.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaConstants.h" #include class UrlMemoryFilter : public StringMemoryFilter { protected: QString value( const Meta::TrackPtr &track ) const override { return track->playableUrl().url(); } }; class GenericStringMemoryFilter : public StringMemoryFilter { public: GenericStringMemoryFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd ) : m_value( value ) { setFilter( filter, matchBegin, matchEnd ); } protected: QString value( const Meta::TrackPtr &track ) const override { return Meta::valueForField( m_value, track ).toString(); } private: qint64 m_value; }; class GenericNumberMemoryFilter : public NumberMemoryFilter { public: GenericNumberMemoryFilter( qint64 value, qint64 filter, Collections::QueryMaker::NumberComparison compare ) : m_value( value ) { setFilter( filter, compare ); } protected: qint64 value( const Meta::TrackPtr &track ) const override { QVariant v = Meta::valueForField( m_value, track ); if( v.type() == QVariant::DateTime ) - return v.toDateTime().toTime_t(); + return v.toDateTime().toSecsSinceEpoch(); else return v.toLongLong(); } private: qint64 m_value; }; namespace FilterFactory { MemoryFilter* filter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd ) { MemoryFilter *result = new GenericStringMemoryFilter( value, filter, matchBegin, matchEnd ); return result; } MemoryFilter* numberFilter( qint64 value, qint64 filter, Collections::QueryMaker::NumberComparison compare ) { NumberMemoryFilter *result = new GenericNumberMemoryFilter( value, filter, compare ); return result; } } MemoryFilter::MemoryFilter() { } MemoryFilter::~MemoryFilter() { } ContainerMemoryFilter::ContainerMemoryFilter() : MemoryFilter() { } ContainerMemoryFilter::~ContainerMemoryFilter() { qDeleteAll( m_filters ); } void ContainerMemoryFilter::addFilter( MemoryFilter *filter ) { if( filter ) m_filters.append( filter ); } AndContainerMemoryFilter::AndContainerMemoryFilter() : ContainerMemoryFilter() { } AndContainerMemoryFilter::~AndContainerMemoryFilter() { } bool AndContainerMemoryFilter::filterMatches( const Meta::TrackPtr &track ) const { if( m_filters.isEmpty() ) return false; foreach( MemoryFilter *filter, m_filters ) { if( filter && !filter->filterMatches( track ) ) return false; } return true; } OrContainerMemoryFilter::OrContainerMemoryFilter() : ContainerMemoryFilter() { } OrContainerMemoryFilter::~OrContainerMemoryFilter() { } bool OrContainerMemoryFilter::filterMatches( const Meta::TrackPtr &track ) const { if( m_filters.isEmpty() ) return false; foreach( MemoryFilter *filter, m_filters ) { if( filter && filter->filterMatches( track ) ) return true; } return false; } NegateMemoryFilter::NegateMemoryFilter( MemoryFilter *filter ) :MemoryFilter() , m_filter( filter ) { } NegateMemoryFilter::~NegateMemoryFilter() { delete m_filter; } bool NegateMemoryFilter::filterMatches( const Meta::TrackPtr &track ) const { return !m_filter->filterMatches( track ); } StringMemoryFilter::StringMemoryFilter() : MemoryFilter() , m_matchBegin( false ) , m_matchEnd( false ) { } StringMemoryFilter::~StringMemoryFilter() { } void StringMemoryFilter::setFilter( const QString &filter, bool matchBegin, bool matchEnd ) { m_filter = filter; m_matchBegin = matchBegin; m_matchEnd = matchEnd; } bool StringMemoryFilter::filterMatches( const Meta::TrackPtr &track ) const { const QString &str = value( track ); if( m_matchBegin && m_matchEnd ) { return QString::compare( str, m_filter, Qt::CaseInsensitive ) == 0; } else if( m_matchBegin ) { return str.startsWith( m_filter, Qt::CaseInsensitive ); } else if( m_matchEnd ) { return str.endsWith( m_filter, Qt::CaseInsensitive ); } else { return str.contains( m_filter, Qt::CaseInsensitive ); } } NumberMemoryFilter::NumberMemoryFilter() : MemoryFilter() , m_filter( 0 ) , m_compare( Collections::QueryMaker::Equals ) { } NumberMemoryFilter::~NumberMemoryFilter() { } void NumberMemoryFilter::setFilter( qint64 filter, Collections::QueryMaker::NumberComparison compare ) { m_filter = filter; m_compare = compare; } bool NumberMemoryFilter::filterMatches( const Meta::TrackPtr &track ) const { qint64 currentValue = value( track ); switch( m_compare ) { case Collections::QueryMaker::Equals: return currentValue == m_filter; case Collections::QueryMaker::GreaterThan: return currentValue > m_filter; case Collections::QueryMaker::LessThan: return currentValue < m_filter; } return false; } LabelFilter::LabelFilter( const QString &filter, bool matchBegin, bool matchEnd ) : MemoryFilter() { QString pattern; if( matchBegin ) pattern += '^'; pattern += filter; if( matchEnd ) pattern += '$'; m_expression = QRegExp( pattern, Qt::CaseInsensitive ); } LabelFilter::~LabelFilter() { //nothing to do } bool LabelFilter::filterMatches(const Meta::TrackPtr &track ) const { foreach( const Meta::LabelPtr &label, track->labels() ) { if( m_expression.indexIn( label->name() ) != -1 ) return true; } return false; } diff --git a/src/core-impl/collections/support/TextualQueryFilter.cpp b/src/core-impl/collections/support/TextualQueryFilter.cpp index e3a61d2b49..0e126099fa 100644 --- a/src/core-impl/collections/support/TextualQueryFilter.cpp +++ b/src/core-impl/collections/support/TextualQueryFilter.cpp @@ -1,357 +1,357 @@ /**************************************************************************************** * Copyright (c) 2007 Alexandre Pereira de Oliveira * * Copyright (c) 2007-2009 Maximilian Kossick * * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2011 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "TextualQueryFilter" #include "TextualQueryFilter.h" #include "Expression.h" #include "FileType.h" #include "core/support/Debug.h" #include using namespace Meta; #define ADD_OR_EXCLUDE_FILTER( VALUE, FILTER, MATCHBEGIN, MATCHEND ) \ { if( elem.negate ) \ qm->excludeFilter( VALUE, FILTER, MATCHBEGIN, MATCHEND ); \ else \ qm->addFilter( VALUE, FILTER, MATCHBEGIN, MATCHEND ); } #define ADD_OR_EXCLUDE_NUMBER_FILTER( VALUE, FILTER, COMPARE ) \ { if( elem.negate ) \ qm->excludeNumberFilter( VALUE, FILTER, COMPARE ); \ else \ qm->addNumberFilter( VALUE, FILTER, COMPARE ); } void Collections::addTextualFilter( Collections::QueryMaker *qm, const QString &filter ) { const int validFilters = qm->validFilterMask(); ParsedExpression parsed = ExpressionParser::parse( filter ); foreach( const or_list &orList, parsed ) { qm->beginOr(); foreach( const expression_element &elem, orList ) { if( elem.negate ) qm->beginAnd(); else qm->beginOr(); if ( elem.field.isEmpty() ) { qm->beginOr(); if( ( validFilters & Collections::QueryMaker::TitleFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valTitle, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::UrlFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valUrl, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::AlbumFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valAlbum, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::ArtistFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valArtist, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::AlbumArtistFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valAlbumArtist, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::ComposerFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valComposer, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::GenreFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valGenre, elem.text, false, false ); if( ( validFilters & Collections::QueryMaker::YearFilter ) ) ADD_OR_EXCLUDE_FILTER( Meta::valYear, elem.text, false, false ); ADD_OR_EXCLUDE_FILTER( Meta::valLabel, elem.text, false, false ); qm->endAndOr(); } else { //get field values based on name const qint64 field = Meta::fieldForName( elem.field ); Collections::QueryMaker::NumberComparison compare = Collections::QueryMaker::Equals; switch( elem.match ) { case expression_element::More: compare = Collections::QueryMaker::GreaterThan; break; case expression_element::Less: compare = Collections::QueryMaker::LessThan; break; case expression_element::Equals: case expression_element::Contains: compare = Collections::QueryMaker::Equals; break; } const bool matchEqual = ( elem.match == expression_element::Equals ); switch( field ) { case Meta::valAlbum: if( ( validFilters & Collections::QueryMaker::AlbumFilter ) == 0 ) break; ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valArtist: if( ( validFilters & Collections::QueryMaker::ArtistFilter ) == 0 ) break; ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valAlbumArtist: if( ( validFilters & Collections::QueryMaker::AlbumArtistFilter ) == 0 ) break; ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valGenre: if( ( validFilters & Collections::QueryMaker::GenreFilter ) == 0 ) break; ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valTitle: if( ( validFilters & Collections::QueryMaker::TitleFilter ) == 0 ) break; ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valComposer: if( ( validFilters & Collections::QueryMaker::ComposerFilter ) == 0 ) break; ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valYear: if( ( validFilters & Collections::QueryMaker::YearFilter ) == 0 ) break; ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toInt(), compare ); break; case Meta::valLabel: case Meta::valComment: ADD_OR_EXCLUDE_FILTER( field, elem.text, matchEqual, matchEqual ); break; case Meta::valUrl: ADD_OR_EXCLUDE_FILTER( field, elem.text, false, false ); break; case Meta::valBpm: case Meta::valBitrate: case Meta::valScore: case Meta::valPlaycount: case Meta::valSamplerate: case Meta::valDiscNr: case Meta::valTrackNr: ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toInt(), compare ); break; case Meta::valRating: ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toFloat() * 2, compare ); break; case Meta::valLength: ADD_OR_EXCLUDE_NUMBER_FILTER( field, elem.text.toInt() * 1000, compare ); break; case Meta::valLastPlayed: case Meta::valFirstPlayed: case Meta::valCreateDate: case Meta::valModified: addDateFilter( field, compare, elem.negate, elem.text, qm ); break; case Meta::valFilesize: { bool doubleOk( false ); const double mbytes = elem.text.toDouble( &doubleOk ); // input in MBs if( !doubleOk ) { qm->endAndOr(); return; } /* * A special case is made for Equals (e.g. filesize:100), which actually filters * for anything between 100 and 101MBs. Megabytes are used because for audio files * they are the most reasonable units for the user to deal with. */ const qreal bytes = mbytes * 1024.0 * 1024.0; const qint64 mbFloor = qint64( qAbs(mbytes) ); switch( compare ) { case Collections::QueryMaker::Equals: qm->endAndOr(); qm->beginAnd(); ADD_OR_EXCLUDE_NUMBER_FILTER( field, mbFloor * 1024 * 1024, Collections::QueryMaker::GreaterThan ); ADD_OR_EXCLUDE_NUMBER_FILTER( field, (mbFloor + 1) * 1024 * 1024, Collections::QueryMaker::LessThan ); break; case Collections::QueryMaker::GreaterThan: case Collections::QueryMaker::LessThan: ADD_OR_EXCLUDE_NUMBER_FILTER( field, bytes, compare ); break; } break; } case Meta::valFormat: { const QString &ftStr = elem.text; Amarok::FileType ft = Amarok::FileTypeSupport::fileType(ftStr); ADD_OR_EXCLUDE_NUMBER_FILTER( field, int(ft), compare ); break; } } } qm->endAndOr(); } qm->endAndOr(); } } void Collections::addDateFilter( qint64 field, Collections::QueryMaker::NumberComparison compare, bool negate, const QString &text, Collections::QueryMaker *qm ) { bool absolute = false; - const uint date = semanticDateTimeParser( text, &absolute ).toTime_t(); + const uint date = semanticDateTimeParser( text, &absolute ).toSecsSinceEpoch(); if( date == 0 ) return; if( compare == Collections::QueryMaker::Equals ) { // equal means, on the same day uint day = 24 * 60 * 60; qm->endAndOr(); qm->beginAnd(); if( negate ) { qm->excludeNumberFilter( field, date - day, Collections::QueryMaker::GreaterThan ); qm->excludeNumberFilter( field, date + day, Collections::QueryMaker::LessThan ); } else { qm->addNumberFilter( field, date - day, Collections::QueryMaker::GreaterThan ); qm->addNumberFilter( field, date + day, Collections::QueryMaker::LessThan ); } } // note: if the date is a relative time difference, invert the condition else if( ( compare == Collections::QueryMaker::LessThan && !absolute ) || ( compare == Collections::QueryMaker::GreaterThan && absolute ) ) { if( negate ) qm->excludeNumberFilter( field, date, Collections::QueryMaker::GreaterThan ); else qm->addNumberFilter( field, date, Collections::QueryMaker::GreaterThan ); } else if( ( compare == Collections::QueryMaker::GreaterThan && !absolute ) || ( compare == Collections::QueryMaker::LessThan && absolute ) ) { if( negate ) qm->excludeNumberFilter( field, date, Collections::QueryMaker::LessThan ); else qm->addNumberFilter( field, date, Collections::QueryMaker::LessThan ); } } QDateTime Collections::semanticDateTimeParser( const QString &text, bool *absolute ) { /* TODO: semanticDateTimeParser: has potential to extend and form a class of its own */ // some code duplication, see EditFilterDialog::parseTextFilter const QString lowerText = text.toLower(); const QDateTime curTime = QDateTime::currentDateTime(); if( absolute ) *absolute = false; // parse date using local settings QDateTime result = QLocale().toDateTime( text, QLocale::ShortFormat ); // parse date using a backup standard independent from local settings QRegExp shortDateReg("(\\d{1,2})[-.](\\d{1,2})"); QRegExp longDateReg("(\\d{1,2})[-.](\\d{1,2})[-.](\\d{4})"); if( text.at(0).isLetter() ) { if( ( lowerText.compare( QLatin1String("today") ) == 0 ) || ( lowerText.compare( i18n( "today" ) ) == 0 ) ) result = curTime.addDays( -1 ); else if( ( lowerText.compare( QLatin1String("last week") ) == 0 ) || ( lowerText.compare( i18n( "last week" ) ) == 0 ) ) result = curTime.addDays( -7 ); else if( ( lowerText.compare( QLatin1String("last month") ) == 0 ) || ( lowerText.compare( i18n( "last month" ) ) == 0 ) ) result = curTime.addMonths( -1 ); else if( ( lowerText.compare( QLatin1String("two months ago") ) == 0 ) || ( lowerText.compare( i18n( "two months ago" ) ) == 0 ) ) result = curTime.addMonths( -2 ); else if( ( lowerText.compare( QLatin1String("three months ago") ) == 0 ) || ( lowerText.compare( i18n( "three months ago" ) ) == 0 ) ) result = curTime.addMonths( -3 ); } else if( result.isValid() ) { if( absolute ) *absolute = true; } else if( text.contains(shortDateReg) ) { result = QDateTime( QDate( QDate::currentDate().year(), shortDateReg.cap(2).toInt(), shortDateReg.cap(1).toInt() ) ); if( absolute ) *absolute = true; } else if( text.contains(longDateReg) ) { result = QDateTime( QDate( longDateReg.cap(3).toInt(), longDateReg.cap(2).toInt(), longDateReg.cap(1).toInt() ) ); if( absolute ) *absolute = true; } else // first character is a number { // parse a "#m#d" (discoverability == 0, but without a GUI, how to do it?) int years = 0, months = 0, days = 0, secs = 0; QString tmp; for( int i = 0; i < text.length(); i++ ) { QChar c = text.at( i ); if( c.isNumber() ) { tmp += c; } else if( c == 'y' ) { years += -tmp.toInt(); tmp.clear(); } else if( c == 'm' ) { months += -tmp.toInt(); tmp.clear(); } else if( c == 'w' ) { days += -tmp.toInt() * 7; tmp.clear(); } else if( c == 'd' ) { days += -tmp.toInt(); tmp.clear(); } else if( c == 'h' ) { secs += -tmp.toInt() * 60 * 60; tmp.clear(); } else if( c == 'M' ) { secs += -tmp.toInt() * 60; tmp.clear(); } else if( c == 's' ) { secs += -tmp.toInt(); tmp.clear(); } } result = QDateTime::currentDateTime().addYears( years ).addMonths( months ).addDays( days ).addSecs( secs ); } return result; } diff --git a/src/core-impl/collections/upnpcollection/UpnpQueryMaker.cpp b/src/core-impl/collections/upnpcollection/UpnpQueryMaker.cpp index d9f57d66ec..b6f02bfde4 100644 --- a/src/core-impl/collections/upnpcollection/UpnpQueryMaker.cpp +++ b/src/core-impl/collections/upnpcollection/UpnpQueryMaker.cpp @@ -1,553 +1,553 @@ /**************************************************************************************** * Copyright (c) 2010 Nikhil Marathe * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "UpnpQueryMaker" #include "UpnpQueryMaker.h" #include "upnptypes.h" #include #include #include #include "core/support/Debug.h" #include "UpnpSearchCollection.h" #include "UpnpQueryMakerInternal.h" #include "UpnpMeta.h" #include "UpnpCache.h" namespace Collections { UpnpQueryMaker::UpnpQueryMaker( UpnpSearchCollection *collection ) : QueryMaker() , m_collection( collection ) , m_internalQM( new UpnpQueryMakerInternal( collection ) ) { reset(); connect( m_internalQM, &UpnpQueryMakerInternal::done, this, &UpnpQueryMaker::slotDone ); connect( m_internalQM, &UpnpQueryMakerInternal::newTracksReady, this, &UpnpQueryMaker::handleTracks ); connect( m_internalQM, &UpnpQueryMakerInternal::newArtistsReady, this, &UpnpQueryMaker::handleArtists ); connect( m_internalQM, &UpnpQueryMakerInternal::newAlbumsReady, this, &UpnpQueryMaker::handleAlbums ); // connect( m_internalQM, &UpnpQueryMakerInternal::newResultReady, // this, &UpnpQueryMaker::handleCustom ); } UpnpQueryMaker::~UpnpQueryMaker() { m_internalQM->deleteLater(); } QueryMaker* UpnpQueryMaker::reset() { // TODO kill all jobs here too m_queryType = None; m_albumMode = AllAlbums; m_query.reset(); m_jobCount = 0; m_numericFilters.clear(); m_internalQM->reset(); // the Amarok Collection Model expects at least one entry // otherwise it will harass us continuously for more entries. // of course due to the poor quality of UPnP servers I've // had experience with :P, some may not have sub-results // for something ( they may have a track with an artist, but // not be able to give any album for it ) m_noResults = true; return this; } void UpnpQueryMaker::run() { DEBUG_BLOCK QUrl baseUrl( m_collection->collectionId() ); QUrlQuery query( baseUrl ); query.addQueryItem( "search", "1" ); baseUrl.setQuery( query ); if( m_queryType == Custom ) { switch( m_returnFunction ) { case Count: { m_query.reset(); m_query.setType( "( upnp:class derivedfrom \"object.item.audioItem\" )" ); QUrlQuery query( baseUrl ); query.addQueryItem( "getCount", "1" ); baseUrl.setQuery( query ); break; } case Sum: case Max: case Min: break; } } // we don't deal with compilations else if( m_queryType == Album && m_albumMode == OnlyCompilations ) { // we don't support any other attribute Q_EMIT newTracksReady( Meta::TrackList() ); Q_EMIT newArtistsReady( Meta::ArtistList() ); Q_EMIT newAlbumsReady( Meta::AlbumList() ); Q_EMIT newGenresReady( Meta::GenreList() ); Q_EMIT newComposersReady( Meta::ComposerList() ); Q_EMIT newYearsReady( Meta::YearList() ); Q_EMIT newResultReady( QStringList() ); Q_EMIT newLabelsReady( Meta::LabelList() ); Q_EMIT queryDone(); return; } QStringList queryList; if( m_query.hasMatchFilter() || !m_numericFilters.empty() ) { queryList = m_query.queries(); } else { switch( m_queryType ) { case Artist: debug() << this << "Query type Artist"; queryList << "( upnp:class derivedfrom \"object.container.person.musicArtist\" )"; break; case Album: debug() << this << "Query type Album"; queryList << "( upnp:class derivedfrom \"object.container.album.musicAlbum\" )"; break; case Track: debug() << this << "Query type Track"; queryList << "( upnp:class derivedfrom \"object.item.audioItem\" )"; break; case Genre: debug() << this << "Query type Genre"; queryList << "( upnp:class derivedfrom \"object.container.genre.musicGenre\" )"; break; case Custom: debug() << this << "Query type Custom"; queryList << "( upnp:class derivedfrom \"object.item.audioItem\" )"; break; default: debug() << this << "Default case: Query type"; // we don't support any other attribute Q_EMIT newTracksReady( Meta::TrackList() ); Q_EMIT newArtistsReady( Meta::ArtistList() ); Q_EMIT newAlbumsReady( Meta::AlbumList() ); Q_EMIT newGenresReady( Meta::GenreList() ); Q_EMIT newComposersReady( Meta::ComposerList() ); Q_EMIT newYearsReady( Meta::YearList() ); Q_EMIT newResultReady( QStringList() ); Q_EMIT newLabelsReady( Meta::LabelList() ); Q_EMIT queryDone(); return; } } // and experiment in using the filter only for the query // and checking the returned upnp:class // based on your query types. for( int i = 0; i < queryList.length() ; i++ ) { if( queryList[i].isEmpty() ) continue; QUrl url( baseUrl ); QUrlQuery query( url ); query.addQueryItem( "query", queryList[i] ); url.setQuery( query ); debug() << this << "Running query" << url; m_internalQM->runQuery( url ); } } void UpnpQueryMaker::abortQuery() { DEBUG_BLOCK Q_ASSERT( false ); // TODO implement this to kill job } QueryMaker* UpnpQueryMaker::setQueryType( QueryType type ) { DEBUG_BLOCK // TODO allow all, based on search capabilities // which should be passed on by the factory m_queryType = type; m_query.setType( "( upnp:class derivedfrom \"object.item.audioItem\" )" ); m_internalQM->setQueryType( type ); return this; } QueryMaker* UpnpQueryMaker::addReturnValue( qint64 value ) { DEBUG_BLOCK debug() << this << "Add return value" << value; m_returnValue = value; return this; } QueryMaker* UpnpQueryMaker::addReturnFunction( ReturnFunction function, qint64 value ) { DEBUG_BLOCK Q_UNUSED( function ) debug() << this << "Return function with value" << value; m_returnFunction = function; m_returnValue = value; return this; } QueryMaker* UpnpQueryMaker::orderBy( qint64 value, bool descending ) { DEBUG_BLOCK debug() << this << "Order by " << value << "Descending?" << descending; return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::TrackPtr &track ) { DEBUG_BLOCK debug() << this << "Adding track match" << track->name(); // TODO: CHECK query type before searching by dc:title? m_query.addMatch( "( dc:title = \"" + track->name() + "\" )" ); return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::ArtistPtr &artist, QueryMaker::ArtistMatchBehaviour behaviour ) { DEBUG_BLOCK Q_UNUSED( behaviour ); // TODO: does UPnP tell between track and album artists? debug() << this << "Adding artist match" << artist->name(); m_query.addMatch( "( upnp:artist = \"" + artist->name() + "\" )" ); return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::AlbumPtr &album ) { DEBUG_BLOCK debug() << this << "Adding album match" << album->name(); m_query.addMatch( "( upnp:album = \"" + album->name() + "\" )" ); return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::ComposerPtr &composer ) { DEBUG_BLOCK debug() << this << "Adding composer match" << composer->name(); // NOTE unsupported return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::GenrePtr &genre ) { DEBUG_BLOCK debug() << this << "Adding genre match" << genre->name(); m_query.addMatch( "( upnp:genre = \"" + genre->name() + "\" )" ); return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::YearPtr &year ) { DEBUG_BLOCK debug() << this << "Adding year match" << year->name(); // TODO return this; } QueryMaker* UpnpQueryMaker::addMatch( const Meta::LabelPtr &label ) { DEBUG_BLOCK debug() << this << "Adding label match" << label->name(); // NOTE how? return this; } QueryMaker* UpnpQueryMaker::addFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd ) { DEBUG_BLOCK debug() << this << "Adding filter" << value << filter << matchBegin << matchEnd; // theoretically this should be '=' I think and set to contains below if required QString cmpOp = "contains"; //TODO should we add filters ourselves // eg. we always query for audioItems, but how do we decide // whether to add a dc:title filter or others. // for example, for the artist list // our query should be like ( pseudocode ) // ( upnp:class = audioItem ) and ( dc:title contains "filter" ) // OR // ( upnp:class = audioItem ) and ( upnp:artist contains "filter" ); // ... // so who adds the second query? QString property = propertyForValue( value ); if( property.isNull() ) return this; if( matchBegin || matchEnd ) cmpOp = "contains"; QString filterString = "( " + property + " " + cmpOp + " \"" + filter + "\" ) "; m_query.addFilter( filterString ); return this; } QueryMaker* UpnpQueryMaker::excludeFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd ) { DEBUG_BLOCK debug() << this << "Excluding filter" << value << filter << matchBegin << matchEnd; QString cmpOp = "!="; QString property = propertyForValue( value ); if( property.isNull() ) return this; if( matchBegin || matchEnd ) cmpOp = "doesNotContain"; QString filterString = "( " + property + " " + cmpOp + " \"" + filter + "\" ) "; m_query.addFilter( filterString ); return this; } QueryMaker* UpnpQueryMaker::addNumberFilter( qint64 value, qint64 filter, NumberComparison compare ) { DEBUG_BLOCK debug() << this << "Adding number filter" << value << filter << compare; NumericFilter f = { value, filter, compare }; m_numericFilters << f; return this; } QueryMaker* UpnpQueryMaker::excludeNumberFilter( qint64 value, qint64 filter, NumberComparison compare ) { DEBUG_BLOCK debug() << this << "Excluding number filter" << value << filter << compare; return this; } QueryMaker* UpnpQueryMaker::limitMaxResultSize( int size ) { DEBUG_BLOCK debug() << this << "Limit max results to" << size; return this; } QueryMaker* UpnpQueryMaker::setAlbumQueryMode( AlbumQueryMode mode ) { DEBUG_BLOCK debug() << this << "Set album query mode" << mode; m_albumMode = mode; return this; } QueryMaker* UpnpQueryMaker::setLabelQueryMode( LabelQueryMode mode ) { DEBUG_BLOCK debug() << this << "Set label query mode" << mode; return this; } QueryMaker* UpnpQueryMaker::beginAnd() { DEBUG_BLOCK m_query.beginAnd(); return this; } QueryMaker* UpnpQueryMaker::beginOr() { DEBUG_BLOCK m_query.beginOr(); return this; } QueryMaker* UpnpQueryMaker::endAndOr() { DEBUG_BLOCK debug() << this << "End AND/OR"; m_query.endAndOr(); return this; } QueryMaker* UpnpQueryMaker::setAutoDelete( bool autoDelete ) { DEBUG_BLOCK debug() << this << "Auto delete" << autoDelete; return this; } int UpnpQueryMaker::validFilterMask() { int mask = 0; QStringList caps = m_collection->searchCapabilities(); if( caps.contains( "dc:title" ) ) mask |= TitleFilter; if( caps.contains( "upnp:album" ) ) mask |= AlbumFilter; if( caps.contains( "upnp:artist" ) ) mask |= ArtistFilter; if( caps.contains( "upnp:genre" ) ) mask |= GenreFilter; return mask; } void UpnpQueryMaker::handleArtists( const Meta::ArtistList &list ) { // TODO Post filtering Q_EMIT newArtistsReady( list ); } void UpnpQueryMaker::handleAlbums( const Meta::AlbumList &list ) { // TODO Post filtering Q_EMIT newAlbumsReady( list ); } void UpnpQueryMaker::handleTracks( const Meta::TrackList &list ) { // TODO Post filtering Q_EMIT newTracksReady( list ); } /* void UpnpQueryMaker::handleCustom( const KIO::UDSEntryList& list ) { if( m_returnFunction == Count ) { { Q_ASSERT( !list.empty() ); QString count = list.first().stringValue( KIO::UDSEntry::UDS_NAME ); m_collection->setProperty( "numberOfTracks", count.toUInt() ); Q_EMIT newResultReady( QStringList( count ) ); } default: debug() << "Custom result functions other than \"Count\" are not supported by UpnpQueryMaker"; } } */ void UpnpQueryMaker::slotDone() { DEBUG_BLOCK if( m_noResults ) { debug() << "++++++++++++++++++++++++++++++++++++ NO RESULTS ++++++++++++++++++++++++"; // TODO proper data types not just DataPtr Meta::DataList ret; Meta::UpnpTrack *fake = new Meta::UpnpTrack( m_collection ); fake->setTitle( "No results" ); fake->setYear( Meta::UpnpYearPtr( new Meta::UpnpYear( 2010 ) ) ); Meta::DataPtr ptr( fake ); ret << ptr; //Q_EMIT newResultReady( ret ); } switch( m_queryType ) { case Artist: { Meta::ArtistList list; foreach( Meta::DataPtr ptr, m_cacheEntries ) list << Meta::ArtistPtr::staticCast( ptr ); Q_EMIT newArtistsReady( list ); break; } case Album: { Meta::AlbumList list; foreach( Meta::DataPtr ptr, m_cacheEntries ) list << Meta::AlbumPtr::staticCast( ptr ); Q_EMIT newAlbumsReady( list ); break; } case Track: { Meta::TrackList list; foreach( Meta::DataPtr ptr, m_cacheEntries ) list << Meta::TrackPtr::staticCast( ptr ); Q_EMIT newTracksReady( list ); break; } default: { debug() << "Query type not supported by UpnpQueryMaker"; } } debug() << "ALL JOBS DONE< TERMINATING THIS QM" << this; Q_EMIT queryDone(); } QString UpnpQueryMaker::propertyForValue( qint64 value ) { switch( value ) { case Meta::valTitle: return "dc:title"; case Meta::valArtist: { //if( m_queryType != Artist ) return "upnp:artist"; } case Meta::valAlbum: { //if( m_queryType != Album ) return "upnp:album"; } case Meta::valGenre: return "upnp:genre"; break; default: debug() << "UNSUPPORTED QUERY TYPE" << value; return QString(); } } bool UpnpQueryMaker::postFilter( const KIO::UDSEntry &entry ) { //numeric filters foreach( const NumericFilter &filter, m_numericFilters ) { // should be set by the filter based on filter.type qint64 aValue = 0; switch( filter.type ) { case Meta::valCreateDate: { // TODO might use UDSEntry::UDS_CREATION_TIME instead later QString dateString = entry.stringValue( KIO::UPNP_DATE ); QDateTime time = QDateTime::fromString( dateString, Qt::ISODate ); if( !time.isValid() ) return false; - aValue = time.toTime_t(); + aValue = time.toSecsSinceEpoch(); debug() << "FILTER BY creation timestamp entry:" << aValue << "query:" << filter.value << "OP:" << filter.compare; break; } } if( ( filter.compare == Equals ) && ( filter.value != aValue ) ) return false; else if( ( filter.compare == GreaterThan ) && ( filter.value >= aValue ) ) return false; // since only allow entries with aValue > filter.value else if( ( filter.compare == LessThan ) && ( filter.value <= aValue ) ) return false; } return true; } } //namespace Collections diff --git a/src/core-impl/meta/file/File.cpp b/src/core-impl/meta/file/File.cpp index 43506a0140..f9ff3f705c 100644 --- a/src/core-impl/meta/file/File.cpp +++ b/src/core-impl/meta/file/File.cpp @@ -1,594 +1,594 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Seb Ruiz * * Copyright (c) 2012 Matěj Laitl * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "File.h" #include "File_p.h" #include #ifdef HAVE_LIBLASTFM #include "LastfmReadLabelCapability.h" #endif #include "MainWindow.h" #include "amarokurls/BookmarkMetaActions.h" #include "amarokurls/PlayUrlRunner.h" #include "browsers/BrowserDock.h" #include "browsers/filebrowser/FileBrowser.h" #include "core/capabilities/BookmarkThisCapability.h" #include "core/capabilities/FindInSourceCapability.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaUtility.h" #include "core/playlists/PlaylistFormat.h" #include "core/support/Amarok.h" #include "core-impl/capabilities/timecode/TimecodeWriteCapability.h" #include "core-impl/capabilities/timecode/TimecodeLoadCapability.h" #include "core-impl/support/UrlStatisticsStore.h" #include #include #include #include #include #include #include using namespace MetaFile; class TimecodeWriteCapabilityImpl : public Capabilities::TimecodeWriteCapability { public: TimecodeWriteCapabilityImpl( MetaFile::Track *track ) : Capabilities::TimecodeWriteCapability() , m_track( track ) {} bool writeTimecode ( qint64 miliseconds ) override { DEBUG_BLOCK return Capabilities::TimecodeWriteCapability::writeTimecode( miliseconds, Meta::TrackPtr( m_track.data() ) ); } bool writeAutoTimecode ( qint64 miliseconds ) override { DEBUG_BLOCK return Capabilities::TimecodeWriteCapability::writeAutoTimecode( miliseconds, Meta::TrackPtr( m_track.data() ) ); } private: AmarokSharedPointer m_track; }; class TimecodeLoadCapabilityImpl : public Capabilities::TimecodeLoadCapability { public: TimecodeLoadCapabilityImpl( MetaFile::Track *track ) : Capabilities::TimecodeLoadCapability() , m_track( track ) {} bool hasTimecodes() override { if ( loadTimecodes().size() > 0 ) return true; return false; } BookmarkList loadTimecodes() override { BookmarkList list = PlayUrlRunner::bookmarksFromUrl( m_track->playableUrl() ); return list; } private: AmarokSharedPointer m_track; }; class FindInSourceCapabilityImpl : public Capabilities::FindInSourceCapability { public: FindInSourceCapabilityImpl( MetaFile::Track *track ) : Capabilities::FindInSourceCapability() , m_track( track ) {} void findInSource( QFlags tag ) override { Q_UNUSED( tag ) //first show the filebrowser AmarokUrl url; url.setCommand( QStringLiteral("navigate") ); url.setPath( QStringLiteral("files") ); url.run(); //then navigate to the correct directory BrowserCategory * fileCategory = The::mainWindow()->browserDock()->list()->activeCategoryRecursive(); if( fileCategory ) { FileBrowser * fileBrowser = dynamic_cast( fileCategory ); if( fileBrowser ) { //get the path of the parent directory of the file QUrl playableUrl = m_track->playableUrl(); fileBrowser->setDir( playableUrl.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash) ); } } } private: AmarokSharedPointer m_track; }; Track::Track( const QUrl &url ) : Meta::Track() , d( new Track::Private( this ) ) { d->url = url; d->readMetaData(); d->album = Meta::AlbumPtr( new MetaFile::FileAlbum( d ) ); d->artist = Meta::ArtistPtr( new MetaFile::FileArtist( d ) ); d->albumArtist = Meta::ArtistPtr( new MetaFile::FileArtist( d, true ) ); d->genre = Meta::GenrePtr( new MetaFile::FileGenre( d ) ); d->composer = Meta::ComposerPtr( new MetaFile::FileComposer( d ) ); d->year = Meta::YearPtr( new MetaFile::FileYear( d ) ); } Track::~Track() { delete d; } QString Track::name() const { if( d ) { const QString trackName = d->m_data.title; return trackName; } return QStringLiteral("This is a bug!"); } QUrl Track::playableUrl() const { return d->url; } QString Track::prettyUrl() const { if(d->url.isLocalFile()) { return d->url.toLocalFile(); } else { return d->url.path(); } } QString Track::uidUrl() const { return d->url.url(); } QString Track::notPlayableReason() const { return localFileNotPlayableReason( playableUrl().toLocalFile() ); } bool Track::isEditable() const { QFileInfo info = QFileInfo( playableUrl().toLocalFile() ); return info.isFile() && info.isWritable(); } Meta::AlbumPtr Track::album() const { return d->album; } Meta::ArtistPtr Track::artist() const { return d->artist; } Meta::GenrePtr Track::genre() const { return d->genre; } Meta::ComposerPtr Track::composer() const { return d->composer; } Meta::YearPtr Track::year() const { return d->year; } void Track::setAlbum( const QString &newAlbum ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valAlbum, newAlbum ); } void Track::setAlbumArtist( const QString &newAlbumArtist ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valAlbumArtist, newAlbumArtist ); } void Track::setArtist( const QString &newArtist ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valArtist, newArtist ); } void Track::setGenre( const QString &newGenre ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valGenre, newGenre ); } void Track::setComposer( const QString &newComposer ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valComposer, newComposer ); } void Track::setYear( int newYear ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valYear, newYear ); } void Track::setTitle( const QString &newTitle ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valTitle, newTitle ); } void Track::setBpm( const qreal newBpm ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valBpm, newBpm ); } qreal Track::bpm() const { const qreal bpm = d->m_data.bpm; return bpm; } QString Track::comment() const { const QString commentName = d->m_data.comment; if( !commentName.isEmpty() ) return commentName; return QString(); } void Track::setComment( const QString& newComment ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valComment, newComment ); } int Track::trackNumber() const { return d->m_data.trackNumber; } void Track::setTrackNumber( int newTrackNumber ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valTrackNr, newTrackNumber ); } int Track::discNumber() const { return d->m_data.discNumber; } void Track::setDiscNumber( int newDiscNumber ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valDiscNr, newDiscNumber ); } qint64 Track::length() const { qint64 length = d->m_data.length; if( length == -2 /*Undetermined*/ ) length = 0; return length; } int Track::filesize() const { return d->m_data.fileSize; } int Track::sampleRate() const { int sampleRate = d->m_data.sampleRate; if( sampleRate == -2 /*Undetermined*/ ) sampleRate = 0; return sampleRate; } int Track::bitrate() const { int bitrate = d->m_data.bitRate; if( bitrate == -2 /*Undetermined*/ ) bitrate = 0; return bitrate; } QDateTime Track::createDate() const { if( d->m_data.created > 0 ) - return QDateTime::fromTime_t(d->m_data.created); + return QDateTime::fromSecsSinceEpoch(d->m_data.created); else return QDateTime(); } qreal Track::replayGain( Meta::ReplayGainTag mode ) const { switch( mode ) { case Meta::ReplayGain_Track_Gain: return d->m_data.trackGain; case Meta::ReplayGain_Track_Peak: return d->m_data.trackPeak; case Meta::ReplayGain_Album_Gain: return d->m_data.albumGain; case Meta::ReplayGain_Album_Peak: return d->m_data.albumPeak; } return 0.0; } QString Track::type() const { return Amarok::extension( d->url.fileName() ); } bool Track::isTrack( const QUrl &url ) { // some playlists lay under audio/ mime category, filter them if( Playlists::isPlaylist( url ) ) return false; // accept remote files, it's too slow to check them at this point if( !url.isLocalFile() ) return true; QFileInfo fileInfo( url.toLocalFile() ); if( fileInfo.size() <= 0 ) return false; // We can't play directories if( fileInfo.isDir() ) return false; QMimeDatabase db; const QMimeType mimeType = db.mimeTypeForFile( url.toLocalFile() ); const QString name = mimeType.name(); return name.startsWith( QLatin1String("audio/") ) || name.startsWith( QLatin1String("video/") ); } void Track::beginUpdate() { QWriteLocker locker( &d->lock ); d->batchUpdate++; } void Track::endUpdate() { QWriteLocker locker( &d->lock ); Q_ASSERT( d->batchUpdate > 0 ); d->batchUpdate--; commitIfInNonBatchUpdate(); } bool Track::inCollection() const { return d->collection; // calls QWeakPointer's (bool) operator } Collections::Collection* Track::collection() const { return d->collection.data(); } void Track::setCollection( Collections::Collection *newCollection ) { d->collection = newCollection; } bool Track::hasCapabilityInterface( Capabilities::Capability::Type type ) const { bool readlabel = false; #ifdef HAVE_LIBLASTFM readlabel = true; #endif return type == Capabilities::Capability::BookmarkThis || type == Capabilities::Capability::WriteTimecode || type == Capabilities::Capability::LoadTimecode || ( type == Capabilities::Capability::ReadLabel && readlabel ) || type == Capabilities::Capability::FindInSource; } Capabilities::Capability* Track::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkCurrentTrackPositionAction( 0 ) ); case Capabilities::Capability::WriteTimecode: return new TimecodeWriteCapabilityImpl( this ); case Capabilities::Capability::LoadTimecode: return new TimecodeLoadCapabilityImpl( this ); case Capabilities::Capability::FindInSource: return new FindInSourceCapabilityImpl( this ); #ifdef HAVE_LIBLASTFM case Capabilities::Capability::ReadLabel: if( !d->readLabelCapability ) d->readLabelCapability = new Capabilities::LastfmReadLabelCapability( this ); return 0; #endif default: return 0; } } Meta::TrackEditorPtr Track::editor() { return Meta::TrackEditorPtr( isEditable() ? this : 0 ); } Meta::StatisticsPtr Track::statistics() { return Meta::StatisticsPtr( this ); } double Track::score() const { return d->m_data.score; } void Track::setScore( double newScore ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valScore, newScore ); } int Track::rating() const { return d->m_data.rating; } void Track::setRating( int newRating ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valRating, newRating ); } int Track::playCount() const { return d->m_data.playCount; } void Track::setPlayCount( int newPlayCount ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valPlaycount, newPlayCount ); } QImage Track::getEmbeddedCover() const { if( d->m_data.embeddedImage ) return Meta::Tag::embeddedCover( d->url.path() ); return QImage(); } void Track::commitIfInNonBatchUpdate( qint64 field, const QVariant &value ) { d->changes.insert( field, value ); commitIfInNonBatchUpdate(); } void Track::commitIfInNonBatchUpdate() { static const QSet statFields = ( QSet() << Meta::valFirstPlayed << Meta::valLastPlayed << Meta::valPlaycount << Meta::valScore << Meta::valRating ); if( d->batchUpdate > 0 || d->changes.isEmpty() ) return; // special case (shortcut) when writing statistics is disabled if( !AmarokConfig::writeBackStatistics() && (QSet::fromList( d->changes.keys() ) - statFields).isEmpty() ) { d->changes.clear(); return; } d->writeMetaData(); // clears d->changes d->lock.unlock(); // rather call notifyObservers() without a lock notifyObservers(); d->lock.lockForWrite(); // return to original state } diff --git a/src/core-impl/meta/file/File_p.h b/src/core-impl/meta/file/File_p.h index ae866a9a33..042f33a2d8 100644 --- a/src/core-impl/meta/file/File_p.h +++ b/src/core-impl/meta/file/File_p.h @@ -1,460 +1,460 @@ /**************************************************************************************** * Copyright (c) 2007-2009 Maximilian Kossick * * Copyright (c) 2008 Peter ZHOU * * Copyright (c) 2008 Seb Ruiz * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #ifndef AMAROK_META_FILE_P_H #define AMAROK_META_FILE_P_H #include "amarokconfig.h" #include "core/collections/Collection.h" #include "core/support/Debug.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaUtility.h" #include "MetaReplayGain.h" #include "MetaTagLib.h" #include "core-impl/collections/support/jobs/WriteTagsJob.h" #include "core-impl/collections/support/ArtistHelper.h" #include "core-impl/capabilities/AlbumActionsCapability.h" #include "covermanager/CoverCache.h" #include "File.h" #include #include #include #include #include #include #include #include #include namespace Capabilities { class LastfmReadLabelCapability; } namespace MetaFile { //d-pointer implementation struct MetaData { MetaData() : created( 0 ) , discNumber( 0 ) , trackNumber( 0 ) , length( 0 ) , fileSize( 0 ) , sampleRate( 0 ) , bitRate( 0 ) , year( 0 ) , bpm( -1.0 ) , trackGain( 0.0 ) , trackPeak( 0.0 ) , albumGain( 0.0 ) , albumPeak( 0.0 ) , embeddedImage( false ) , rating( 0 ) , score( 0.0 ) , playCount( 0 ) { } QString title; QString artist; QString album; QString albumArtist; QString comment; QString composer; QString genre; uint created; int discNumber; int trackNumber; qint64 length; int fileSize; int sampleRate; int bitRate; int year; qreal bpm; qreal trackGain; qreal trackPeak; qreal albumGain; qreal albumPeak; bool embeddedImage; int rating; double score; int playCount; }; class Track::Private : public QObject { Q_OBJECT public: Private( Track *t ) : QObject() , url() , album() , artist() , albumArtist() , batchUpdate( 0 ) , track( t ) {} QUrl url; Meta::AlbumPtr album; Meta::ArtistPtr artist; Meta::ArtistPtr albumArtist; Meta::GenrePtr genre; Meta::ComposerPtr composer; Meta::YearPtr year; QPointer readLabelCapability; QPointer collection; /** * Number of current batch operations started by @see beginUpdate() and not * yet ended by @see endUpdate(). Must only be accessed with lock held. */ int batchUpdate; Meta::FieldHash changes; QReadWriteLock lock; void writeMetaData() { DEBUG_BLOCK debug() << "changes:" << changes; if( AmarokConfig::writeBack() ) Meta::Tag::writeTags( url.isLocalFile() ? url.toLocalFile() : url.path(), changes, AmarokConfig::writeBackStatistics() ); changes.clear(); readMetaData(); } void notifyObservers() { track->notifyObservers(); } MetaData m_data; private: TagLib::FileRef getFileRef(); Track *track; public Q_SLOTS: void readMetaData() { QFileInfo fi( url.isLocalFile() ? url.toLocalFile() : url.path() ); - m_data.created = fi.created().toTime_t(); + m_data.created = fi.created().toSecsSinceEpoch(); Meta::FieldHash values = Meta::Tag::readTags( fi.absoluteFilePath() ); // (re)set all fields to behave the same as the constructor. E.g. catch even complete // removal of tags etc. MetaData def; // default m_data.title = values.value( Meta::valTitle, def.title ).toString(); m_data.artist = values.value( Meta::valArtist, def.artist ).toString(); m_data.album = values.value( Meta::valAlbum, def.album ).toString(); m_data.albumArtist = values.value( Meta::valAlbumArtist, def.albumArtist ).toString(); m_data.embeddedImage = values.value( Meta::valHasCover, def.embeddedImage ).toBool(); m_data.comment = values.value( Meta::valComment, def.comment ).toString(); m_data.genre = values.value( Meta::valGenre, def.genre ).toString(); m_data.composer = values.value( Meta::valComposer, def.composer ).toString(); m_data.year = values.value( Meta::valYear, def.year ).toInt(); m_data.discNumber = values.value( Meta::valDiscNr, def.discNumber ).toInt(); m_data.trackNumber = values.value( Meta::valTrackNr, def.trackNumber ).toInt(); m_data.bpm = values.value( Meta::valBpm, def.bpm ).toReal(); m_data.bitRate = values.value( Meta::valBitrate, def.bitRate ).toInt(); m_data.length = values.value( Meta::valLength, def.length ).toLongLong(); m_data.sampleRate = values.value( Meta::valSamplerate, def.sampleRate ).toInt(); m_data.fileSize = values.value( Meta::valFilesize, def.fileSize ).toLongLong(); m_data.trackGain = values.value( Meta::valTrackGain, def.trackGain ).toReal(); m_data.trackPeak= values.value( Meta::valTrackGainPeak, def.trackPeak ).toReal(); m_data.albumGain = values.value( Meta::valAlbumGain, def.albumGain ).toReal(); m_data.albumPeak= values.value( Meta::valAlbumGainPeak, def.albumPeak ).toReal(); // only read the stats if we can write them later. Would be annoying to have // read-only rating that you don't like if( AmarokConfig::writeBackStatistics() ) { m_data.rating = values.value( Meta::valRating, def.rating ).toInt(); m_data.score = values.value( Meta::valScore, def.score ).toDouble(); m_data.playCount = values.value( Meta::valPlaycount, def.playCount ).toInt(); } if(url.isLocalFile()) { m_data.fileSize = QFile( url.toLocalFile() ).size(); } else { m_data.fileSize = QFile( url.path() ).size(); } //as a last ditch effort, use the filename as the title if nothing else has been found if ( m_data.title.isEmpty() ) { m_data.title = url.fileName(); } // try to guess best album artist (even if non-empty, part of compilation detection) m_data.albumArtist = ArtistHelper::bestGuessAlbumArtist( m_data.albumArtist, m_data.artist, m_data.genre, m_data.composer ); } //Definition of slot readMetaData ends }; //Definition of class Track::Private ends // internal helper classes class FileArtist : public Meta::Artist { public: explicit FileArtist( MetaFile::Track::Private *dptr, bool isAlbumArtist = false ) : Meta::Artist() , d( dptr ) , m_isAlbumArtist( isAlbumArtist ) {} Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { const QString artist = m_isAlbumArtist ? d.data()->m_data.albumArtist : d.data()->m_data.artist; return artist; } bool operator==( const Meta::Artist &other ) const override { return name() == other.name(); } QPointer const d; const bool m_isAlbumArtist; }; class FileAlbum : public Meta::Album { public: explicit FileAlbum( MetaFile::Track::Private *dptr ) : Meta::Album() , d( dptr ) {} bool hasCapabilityInterface( Capabilities::Capability::Type type ) const override { switch( type ) { case Capabilities::Capability::Actions: return true; default: return false; } } Capabilities::Capability* createCapabilityInterface( Capabilities::Capability::Type type ) override { switch( type ) { case Capabilities::Capability::Actions: return new Capabilities::AlbumActionsCapability( Meta::AlbumPtr( this ) ); default: return 0; } } bool isCompilation() const override { /* non-compilation albums with no album artists may be hidden in collection * browser if certain modes are used, so force compilation in this case */ return !hasAlbumArtist(); } bool hasAlbumArtist() const override { return !d.data()->albumArtist->name().isEmpty(); } Meta::ArtistPtr albumArtist() const override { /* only return album artist if it would be non-empty, some Amarok parts do not * call hasAlbumArtist() prior to calling albumArtist() and it is better to be * consistent with other Meta::Track implementations */ if( hasAlbumArtist() ) return d.data()->albumArtist; return Meta::ArtistPtr(); } Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { if( d ) { const QString albumName = d.data()->m_data.album; return albumName; } else return QString(); } bool hasImage( int /* size */ = 0 ) const override { if( d && d.data()->m_data.embeddedImage ) return true; return false; } QImage image( int size = 0 ) const override { QImage image; if( d && d.data()->m_data.embeddedImage ) { image = Meta::Tag::embeddedCover( d.data()->url.toLocalFile() ); } if( image.isNull() || size <= 0 /* do not scale */ ) return image; return image.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation ); } bool canUpdateImage() const override { return d; // true if underlying track is not null } void setImage( const QImage &image ) override { if( !d ) return; Meta::FieldHash fields; fields.insert( Meta::valImage, image ); WriteTagsJob *job = new WriteTagsJob( d.data()->url.toLocalFile(), fields ); QObject::connect( job, &WriteTagsJob::done, job, &QObject::deleteLater ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(job) ); if( d.data()->m_data.embeddedImage == image.isNull() ) // we need to toggle the embeddedImage switch in this case QObject::connect( job, &WriteTagsJob::done, d.data(), &Track::Private::readMetaData ); CoverCache::invalidateAlbum( this ); notifyObservers(); // following call calls Track's notifyObservers. This is needed because for example // UmsCollection justifiably listens only to Track's metadataChanged() to update // its MemoryCollection maps d.data()->notifyObservers(); } void removeImage() override { setImage( QImage() ); } bool operator==( const Meta::Album &other ) const override { return name() == other.name(); } QPointer const d; }; class FileGenre : public Meta::Genre { public: explicit FileGenre( MetaFile::Track::Private *dptr ) : Meta::Genre() , d( dptr ) {} Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { const QString genreName = d.data()->m_data.genre; return genreName; } bool operator==( const Meta::Genre &other ) const override { return name() == other.name(); } QPointer const d; }; class FileComposer : public Meta::Composer { public: explicit FileComposer( MetaFile::Track::Private *dptr ) : Meta::Composer() , d( dptr ) {} Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { const QString composer = d.data()->m_data.composer; return composer; } bool operator==( const Meta::Composer &other ) const override { return name() == other.name(); } QPointer const d; }; class FileYear : public Meta::Year { public: explicit FileYear( MetaFile::Track::Private *dptr ) : Meta::Year() , d( dptr ) {} Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { const QString year = QString::number( d.data()->m_data.year ); return year; } bool operator==( const Meta::Year &other ) const override { return name() == other.name(); } QPointer const d; }; } #endif diff --git a/src/core/support/Amarok.cpp b/src/core/support/Amarok.cpp index 924f715288..25b233b7cd 100644 --- a/src/core/support/Amarok.cpp +++ b/src/core/support/Amarok.cpp @@ -1,454 +1,454 @@ /**************************************************************************************** * Copyright (c) 2002 Mark Kretschmann * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "core/support/Amarok.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaUtility.h" #include "core/capabilities/SourceInfoCapability.h" #include "core/playlists/PlaylistFormat.h" #include #include #include #include #include #include #include #include #include QPointer Amarok::actionCollectionObject; QMutex Amarok::globalDirsMutex; namespace Amarok { // TODO: sometimes we have a playcount but no valid datetime. // in such a case we should maybe display "Unknown" and not "Never" QString verboseTimeSince( const QDateTime &datetime ) { - if( datetime.isNull() || !datetime.toTime_t() ) + if( datetime.isNull() || !datetime.toSecsSinceEpoch() ) return i18nc( "The amount of time since last played", "Never" ); const QDateTime now = QDateTime::currentDateTime(); const int datediff = datetime.daysTo( now ); // HACK: Fix 203522. Arithmetic overflow? // Getting weird values from Plasma::DataEngine (LAST_PLAYED field). if( datediff < 0 ) return i18nc( "When this track was last played", "Unknown" ); if( datediff >= 6*7 /*six weeks*/ ) { // return absolute month/year QString month_year = datetime.date().toString(QStringLiteral("MM yyyy")); return i18nc( "monthname year", "%1", month_year ); } //TODO "last week" = maybe within 7 days, but prolly before last Sunday if( datediff >= 7 ) // return difference in weeks return i18np( "One week ago", "%1 weeks ago", (datediff+3)/7 ); const int timediff = datetime.secsTo( now ); if( timediff >= 24*60*60 /*24 hours*/ ) // return difference in days return datediff == 1 ? i18n( "Yesterday" ) : i18np( "One day ago", "%1 days ago", (timediff+12*60*60)/(24*60*60) ); if( timediff >= 90*60 /*90 minutes*/ ) // return difference in hours return i18np( "One hour ago", "%1 hours ago", (timediff+30*60)/(60*60) ); //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently" if( timediff >= 0 ) // return difference in minutes return timediff/60 ? i18np( "One minute ago", "%1 minutes ago", (timediff+30)/60 ) : i18n( "Within the last minute" ); return i18n( "The future" ); } QString verboseTimeSince( uint time_t ) { if( !time_t ) return i18nc( "The amount of time since last played", "Never" ); QDateTime dt; - dt.setTime_t( time_t ); + dt.setSecsSinceEpoch( time_t ); return verboseTimeSince( dt ); } QString conciseTimeSince( uint time_t ) { if( !time_t ) return i18nc( "The amount of time since last played", "0" ); QDateTime datetime; - datetime.setTime_t( time_t ); + datetime.setSecsSinceEpoch( time_t ); const QDateTime now = QDateTime::currentDateTime(); const int datediff = datetime.daysTo( now ); if( datediff >= 6*7 /*six weeks*/ ) { // return difference in months return i18nc( "number of months ago", "%1M", datediff/7/4 ); } if( datediff >= 7 ) // return difference in weeks return i18nc( "w for weeks", "%1w", (datediff+3)/7 ); if( datediff == -1 ) return i18nc( "When this track was last played", "Tomorrow" ); const int timediff = datetime.secsTo( now ); if( timediff >= 24*60*60 /*24 hours*/ ) // return difference in days // xgettext: no-c-format return i18nc( "d for days", "%1d", (timediff+12*60*60)/(24*60*60) ); if( timediff >= 90*60 /*90 minutes*/ ) // return difference in hours return i18nc( "h for hours", "%1h", (timediff+30*60)/(60*60) ); //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently" if( timediff >= 60 ) // return difference in minutes return QStringLiteral("%1'").arg( ( timediff + 30 )/60 ); if( timediff >= 0 ) // return difference in seconds return QStringLiteral("%1\"").arg( ( timediff + 1 )/60 ); return i18n( "0" ); } void manipulateThe( QString &str, bool reverse ) { if( reverse ) { if( !str.startsWith( QLatin1String("the "), Qt::CaseInsensitive ) ) return; QString begin = str.left( 3 ); str = str.append( ", %1" ).arg( begin ); str = str.mid( 4 ); return; } if( !str.endsWith( QLatin1String(", the"), Qt::CaseInsensitive ) ) return; QString end = str.right( 3 ); str = str.prepend( "%1 " ).arg( end ); uint newLen = str.length() - end.length() - 2; str.truncate( newLen ); } QString generatePlaylistName( const Meta::TrackList& tracks ) { QString datePart = QLocale::system().toString( QDateTime::currentDateTime(), QLocale::ShortFormat ); if( tracks.isEmpty() ) { return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \ the parentheses", "Empty Playlist (%1)", datePart ); } bool singleArtist = true; bool singleAlbum = true; Meta::ArtistPtr artist = tracks.first()->artist(); Meta::AlbumPtr album = tracks.first()->album(); QString artistPart; QString albumPart; foreach( const Meta::TrackPtr track, tracks ) { if( artist != track->artist() ) singleArtist = false; if( album != track->album() ) singleAlbum = false; if ( !singleArtist && !singleAlbum ) break; } if( ( !singleArtist && !singleAlbum ) || ( !artist && !album ) ) return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \ the parentheses", "Various Tracks (%1)", datePart ); if( singleArtist ) { if( artist ) artistPart = artist->prettyName(); else artistPart = i18n( "Unknown Artist(s)" ); } else if( album && album->hasAlbumArtist() && singleAlbum ) { artistPart = album->albumArtist()->prettyName(); } else { artistPart = i18n( "Various Artists" ); } if( singleAlbum ) { if( album ) albumPart = album->prettyName(); else albumPart = i18n( "Unknown Album(s)" ); } else { albumPart = i18n( "Various Albums" ); } return i18nc( "A saved playlist titled - ", "%1 - %2", artistPart, albumPart ); } KActionCollection* actionCollection() // TODO: constify? { if( !actionCollectionObject ) { actionCollectionObject = new KActionCollection( qApp ); actionCollectionObject->setObjectName( QStringLiteral("Amarok-KActionCollection") ); } return actionCollectionObject.data(); } KConfigGroup config( const QString &group ) { //Slightly more useful config() that allows setting the group simultaneously return KSharedConfig::openConfig()->group( group ); } namespace ColorScheme { QColor Base; QColor Text; QColor Background; QColor Foreground; QColor AltBase; } OverrideCursor::OverrideCursor( Qt::CursorShape cursor ) { QApplication::setOverrideCursor( cursor == Qt::WaitCursor ? Qt::WaitCursor : Qt::BusyCursor ); } OverrideCursor::~OverrideCursor() { QApplication::restoreOverrideCursor(); } QString saveLocation( const QString &directory ) { globalDirsMutex.lock(); QString result = QStandardPaths::writableLocation( QStandardPaths::AppDataLocation ) + QDir::separator() + directory; if( !result.endsWith( QDir::separator() ) ) result.append( QDir::separator() ); QDir dir( result ); if( !dir.exists() ) dir.mkpath( QStringLiteral( "." ) ); globalDirsMutex.unlock(); return result; } QString defaultPlaylistPath() { return Amarok::saveLocation() + QLatin1String("current.xspf"); } QString cleanPath( const QString &path ) { /* Unicode uses combining characters to form accented versions of other characters. * (Exception: Latin-1 table for compatibility with ASCII.) * Those can be found in the Unicode tables listed at: * http://en.wikipedia.org/w/index.php?title=Combining_character&oldid=255990982 * Removing those characters removes accents. :) */ QString result = path; // German umlauts result.replace( QChar(0x00e4), QLatin1String("ae") ).replace( QChar(0x00c4), QLatin1String("Ae") ); result.replace( QChar(0x00f6), QLatin1String("oe") ).replace( QChar(0x00d6), QLatin1String("Oe") ); result.replace( QChar(0x00fc), QLatin1String("ue") ).replace( QChar(0x00dc), QLatin1String("Ue") ); result.replace( QChar(0x00df), QLatin1String("ss") ); // other special cases result.replace( QChar(0x00C6), QLatin1String("AE") ); result.replace( QChar(0x00E6), QLatin1String("ae") ); result.replace( QChar(0x00D8), QLatin1String("OE") ); result.replace( QChar(0x00F8), QLatin1String("oe") ); // normalize in a form where accents are separate characters result = result.normalized( QString::NormalizationForm_D ); // remove accents from table "Combining Diacritical Marks" for( int i = 0x0300; i <= 0x036F; i++ ) { result.remove( QChar( i ) ); } return result; } QString asciiPath( const QString &path ) { QString result = path; for( int i = 0; i < result.length(); i++ ) { QChar c = result[ i ]; if( c > QChar(0x7f) || c == QChar(0) ) { c = '_'; } result[ i ] = c; } return result; } QString vfatPath( const QString &path, PathSeparatorBehaviour behaviour ) { if( path.isEmpty() ) return QString(); QString s = path; QChar separator = ( behaviour == AutoBehaviour ) ? QDir::separator() : ( behaviour == UnixBehaviour ) ? '/' : '\\'; if( behaviour == UnixBehaviour ) // we are on *nix, \ is a valid character in file or directory names, NOT the dir separator s.replace( '\\', '_' ); else s.replace( QLatin1Char('/'), '_' ); // on windows we have to replace / instead int start = 0; #ifdef Q_OS_WIN // exclude the leading "C:/" from special character replacement in the loop below // bug 279560, bug 302251 if( QDir::isAbsolutePath( s ) ) start = 3; #endif for( int i = start; i < s.length(); i++ ) { QChar c = s[ i ]; if( c < QChar(0x20) || c == QChar(0x7F) // 0x7F = 127 = DEL control character || c=='*' || c=='?' || c=='<' || c=='>' || c=='|' || c=='"' || c==':' ) c = '_'; else if( c == '[' ) c = '('; else if ( c == ']' ) c = ')'; s[ i ] = c; } /* beware of reserved device names */ uint len = s.length(); if( len == 3 || (len > 3 && s[3] == '.') ) { QString l = s.left(3).toLower(); if( l==QLatin1String("aux") || l==QLatin1String("con") || l==QLatin1String("nul") || l==QLatin1String("prn") ) s = '_' + s; } else if( len == 4 || (len > 4 && s[4] == '.') ) { QString l = s.left(3).toLower(); QString d = s.mid(3,1); if( (l==QLatin1String("com") || l==QLatin1String("lpt")) && (d==QLatin1String("0") || d==QLatin1String("1") || d==QLatin1String("2") || d==QLatin1String("3") || d==QLatin1String("4") || d==QLatin1String("5") || d==QLatin1String("6") || d==QLatin1String("7") || d==QLatin1String("8") || d==QLatin1String("9")) ) s = '_' + s; } // "clock$" is only allowed WITH extension, according to: // http://en.wikipedia.org/w/index.php?title=Filename&oldid=303934888#Comparison_of_file_name_limitations if( QString::compare( s, QStringLiteral("clock$"), Qt::CaseInsensitive ) == 0 ) s = '_' + s; /* max path length of Windows API */ s = s.left(255); /* whitespace or dot at the end of folder/file names or extensions are bad */ len = s.length(); if( s.at(len - 1) == ' ' || s.at(len - 1) == '.' ) s[len - 1] = '_'; for( int i = 1; i < s.length(); i++ ) // correct trailing whitespace in folder names { if( s.at(i) == separator && s.at(i - 1) == ' ' ) s[i - 1] = '_'; } for( int i = 1; i < s.length(); i++ ) // correct trailing dot in folder names, excluding . and .. { if( s.at(i) == separator && s.at(i - 1) == '.' && !( i == 1 // ./any || ( i == 2 && s.at(i - 2) == '.' ) // ../any || ( i >= 2 && s.at(i - 2) == separator ) // any/./any || ( i >= 3 && s.at(i - 3) == separator && s.at(i - 2) == '.' ) // any/../any ) ) s[i - 1] = '_'; } /* correct trailing spaces in file name itself, not needed for dots */ int extensionIndex = s.lastIndexOf( QLatin1Char('.') ); if( ( s.length() > 1 ) && ( extensionIndex > 0 ) ) if( s.at(extensionIndex - 1) == ' ' ) s[extensionIndex - 1] = '_'; return s; } QPixmap semiTransparentLogo( int dim ) { QPixmap logo; #define AMAROK_LOGO_CACHE_KEY QLatin1String("AmarokSemiTransparentLogo")+QString::number(dim) if( !QPixmapCache::find( AMAROK_LOGO_CACHE_KEY, &logo ) ) { QImage amarokIcon = QIcon::fromTheme( QStringLiteral("amarok") ).pixmap( dim, dim ).toImage(); amarokIcon = amarokIcon.convertToFormat( QImage::Format_ARGB32 ); QRgb *data = reinterpret_cast( amarokIcon.bits() ); QRgb *end = data + amarokIcon.byteCount() / 4; while(data != end) { unsigned char gray = qGray(*data); *data = qRgba(gray, gray, gray, 127); ++data; } logo = QPixmap::fromImage( amarokIcon ); QPixmapCache::insert( AMAROK_LOGO_CACHE_KEY, logo ); } #undef AMAROK_LOGO_CACHE_KEY return logo; } } // End namespace Amarok diff --git a/src/dialogs/EditFilterDialog.cpp b/src/dialogs/EditFilterDialog.cpp index 59ffcdf644..f667884b8a 100644 --- a/src/dialogs/EditFilterDialog.cpp +++ b/src/dialogs/EditFilterDialog.cpp @@ -1,501 +1,501 @@ /**************************************************************************************** * Copyright (c) 2006 Giovanni Venturi * * Copyright (c) 2010 Sergey Ivanov <123kash@gmail.com> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "EditFilterDialog" #include "amarokconfig.h" #include "ui_EditFilterDialog.h" #include "core/support/Debug.h" #include "core-impl/collections/support/CollectionManager.h" #include "core-impl/collections/support/Expression.h" #include "dialogs/EditFilterDialog.h" #include "widgets/TokenDropTarget.h" #include #include #include #include #define OR_TOKEN Meta::valCustom + 1 #define AND_TOKEN Meta::valCustom + 2 #define AND_TOKEN_CONSTRUCT new Token( i18n( "AND" ), "filename-and-amarok", AND_TOKEN ) #define OR_TOKEN_CONSTRUCT new Token( i18n( "OR" ), "filename-divider", OR_TOKEN ) #define SIMPLE_TEXT_CONSTRUCT new Token( i18n( "Simple text" ), "media-track-edit-amarok", 0 ) EditFilterDialog::EditFilterDialog( QWidget* parent, const QString &text ) : QDialog( parent ) , m_ui( new Ui::EditFilterDialog ) , m_curToken( 0 ) , m_separator( " AND " ) , m_isUpdating() { setWindowTitle( i18n( "Edit Filter" ) ); setLayout( new QVBoxLayout ); auto mainWidget = new QWidget( this ); m_ui->setupUi( mainWidget ); layout()->addWidget( mainWidget ); auto buttonBox = new QDialogButtonBox( QDialogButtonBox::Reset | QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this ); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); auto resetButton = buttonBox->button( QDialogButtonBox::Reset ); connect( resetButton, &QPushButton::clicked, this, &EditFilterDialog::slotReset ); layout()->addWidget( buttonBox ); m_ui->dropTarget->setRowLimit( 1 ); initTokenPool(); m_ui->searchEdit->setText( text ); updateDropTarget( text ); updateAttributeEditor(); connect( m_ui->mqwAttributeEditor, &MetaQueryWidget::changed, this, &EditFilterDialog::slotAttributeChanged ); connect( m_ui->cbInvert, &QCheckBox::toggled, this, &EditFilterDialog::slotInvert ); connect( m_ui->rbAnd, &QCheckBox::toggled, this, &EditFilterDialog::slotSeparatorChange ); connect( m_ui->rbOr, &QCheckBox::toggled, this, &EditFilterDialog::slotSeparatorChange ); connect( m_ui->tpTokenPool, &TokenPool::onDoubleClick, m_ui->dropTarget, &TokenDropTarget::appendToken ); connect( m_ui->dropTarget, &TokenDropTarget::tokenSelected, this, &EditFilterDialog::slotTokenSelected ); connect( m_ui->dropTarget, &TokenDropTarget::changed, this, &EditFilterDialog::updateSearchEdit ); // in case someone dragged a token around. connect( m_ui->searchEdit, &QLineEdit::textEdited, this, &EditFilterDialog::slotSearchEditChanged ); } EditFilterDialog::~EditFilterDialog() { delete m_ui; } void EditFilterDialog::initTokenPool() { m_ui->tpTokenPool->addToken( SIMPLE_TEXT_CONSTRUCT ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valTitle ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valArtist ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valAlbumArtist ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valAlbum ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valGenre ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valComposer ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valComment ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valUrl ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valYear ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valTrackNr ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valDiscNr ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valBpm ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valLength ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valBitrate ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valSamplerate ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valFilesize ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valFormat ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valCreateDate ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valScore ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valRating ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valFirstPlayed ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valLastPlayed ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valPlaycount ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valLabel ) ); m_ui->tpTokenPool->addToken( tokenForField( Meta::valModified ) ); m_ui->tpTokenPool->addToken( OR_TOKEN_CONSTRUCT ); m_ui->tpTokenPool->addToken( AND_TOKEN_CONSTRUCT ); } Token * EditFilterDialog::tokenForField( const qint64 field ) { QString icon = Meta::iconForField( field ); QString text = Meta::i18nForField( field ); return new Token( text, icon, field ); } EditFilterDialog::Filter & EditFilterDialog::filterForToken( Token *token ) { // a new token! if( !m_filters.contains( token ) ) { Filter newFilter; newFilter.filter.setField( token->value() ); newFilter.inverted = false; m_filters.insert( token, newFilter ); connect( token, &Token::destroyed, this, &EditFilterDialog::slotTokenDestroyed ); } return m_filters[token]; } void EditFilterDialog::slotTokenSelected( Token *token ) { DEBUG_BLOCK; if( m_curToken == token ) return; // nothing to do m_curToken = token; if( m_curToken && m_curToken->value() > Meta::valCustom ) // OR / AND tokens case m_curToken = 0; updateAttributeEditor(); } void EditFilterDialog::slotTokenDestroyed( QObject *token ) { DEBUG_BLOCK m_filters.take( qobject_cast(token) ); if( m_curToken == token ) { m_curToken = 0; updateAttributeEditor(); } updateSearchEdit(); } void EditFilterDialog::slotAttributeChanged( const MetaQueryWidget::Filter &newFilter ) { DEBUG_BLOCK; if( m_curToken ) m_filters[m_curToken].filter = newFilter; updateSearchEdit(); } void EditFilterDialog::slotInvert( bool checked ) { if( m_curToken ) m_filters[m_curToken].inverted = checked; updateSearchEdit(); } void EditFilterDialog::slotSeparatorChange() { if( m_ui->rbAnd->isChecked() ) m_separator = QString( " AND " ); else m_separator = QString( " OR " ); updateSearchEdit(); } void EditFilterDialog::slotSearchEditChanged( const QString &filterText ) { updateDropTarget( filterText ); updateAttributeEditor(); } void EditFilterDialog::slotReset() { m_ui->dropTarget->clear(); m_ui->rbAnd->setChecked( true ); updateAttributeEditor(); updateSearchEdit(); } void EditFilterDialog::accept() { Q_EMIT filterChanged( filter() ); QDialog::accept(); } void EditFilterDialog::updateAttributeEditor() { DEBUG_BLOCK; if( m_isUpdating ) return; m_isUpdating = true; if( m_curToken ) { Filter &filter = filterForToken( m_curToken ); m_ui->mqwAttributeEditor->setFilter( filter.filter ); m_ui->cbInvert->setChecked( filter.inverted ); } m_ui->mqwAttributeEditor->setEnabled( ( bool )m_curToken ); m_ui->cbInvert->setEnabled( ( bool )m_curToken ); m_isUpdating = false; } void EditFilterDialog::updateSearchEdit() { DEBUG_BLOCK; if( m_isUpdating ) return; m_isUpdating = true; m_ui->searchEdit->setText( filter() ); m_isUpdating = false; } void EditFilterDialog::updateDropTarget( const QString &text ) { DEBUG_BLOCK; if( m_isUpdating ) return; m_isUpdating = true; m_ui->dropTarget->clear(); // some code duplication, see Collections::semanticDateTimeParser ParsedExpression parsed = ExpressionParser::parse( text ); bool AND = false; // need an AND token bool OR = false; // need an OR token bool isDateAbsolute = false; foreach( const or_list &orList, parsed ) { foreach( const expression_element &elem, orList ) { if( AND ) m_ui->dropTarget->appendToken( AND_TOKEN_CONSTRUCT ); else if( OR ) m_ui->dropTarget->appendToken( OR_TOKEN_CONSTRUCT ); Filter filter; filter.filter.setField( !elem.field.isEmpty() ? Meta::fieldForName( elem.field ) : 0 ); if( filter.filter.field() == Meta::valRating ) { filter.filter.numValue = 2 * elem.text.toFloat(); } else if( filter.filter.isDate() ) { QString strTime = elem.text; // parse date using local settings auto date = QLocale().toDate( strTime, QLocale::ShortFormat ); // parse date using a backup standard independent from local settings QRegExp shortDateReg("(\\d{1,2})[-.](\\d{1,2})"); QRegExp longDateReg("(\\d{1,2})[-.](\\d{1,2})[-.](\\d{4})"); // NOTE for absolute time specifications numValue is a unix timestamp, // for relative time specifications numValue is a time difference in seconds 'pointing to the past' if( date.isValid() ) { - filter.filter.numValue = QDateTime( date ).toTime_t(); + filter.filter.numValue = QDateTime( date ).toSecsSinceEpoch(); isDateAbsolute = true; } else if( strTime.contains(shortDateReg) ) { - filter.filter.numValue = QDateTime( QDate( QDate::currentDate().year(), shortDateReg.cap(2).toInt(), shortDateReg.cap(1).toInt() ) ).toTime_t(); + filter.filter.numValue = QDateTime( QDate( QDate::currentDate().year(), shortDateReg.cap(2).toInt(), shortDateReg.cap(1).toInt() ) ).toSecsSinceEpoch(); isDateAbsolute = true; } else if( strTime.contains(longDateReg) ) { - filter.filter.numValue = QDateTime( QDate( longDateReg.cap(3).toInt(), longDateReg.cap(2).toInt(), longDateReg.cap(1).toInt() ) ).toTime_t(); + filter.filter.numValue = QDateTime( QDate( longDateReg.cap(3).toInt(), longDateReg.cap(2).toInt(), longDateReg.cap(1).toInt() ) ).toSecsSinceEpoch(); isDateAbsolute = true; } else { // parse a "#m#d" (discoverability == 0, but without a GUI, how to do it?) int years = 0, months = 0, days = 0, secs = 0; QString tmp; for( int i = 0; i < strTime.length(); i++ ) { QChar c = strTime.at( i ); if( c.isNumber() ) { tmp += c; } else if( c == 'y' ) { years += tmp.toInt(); tmp.clear(); } else if( c == 'm' ) { months += tmp.toInt(); tmp.clear(); } else if( c == 'w' ) { days += tmp.toInt() * 7; tmp.clear(); } else if( c == 'd' ) { days += tmp.toInt(); tmp.clear(); } else if( c == 'h' ) { secs += tmp.toInt() * 60 * 60; tmp.clear(); } else if( c == 'M' ) { secs += tmp.toInt() * 60; tmp.clear(); } else if( c == 's' ) { secs += tmp.toInt(); tmp.clear(); } } filter.filter.numValue = years*365*24*60*60 + months*30*24*60*60 + days*24*60*60 + secs; isDateAbsolute = false; } } else if( filter.filter.isNumeric() ) { filter.filter.numValue = elem.text.toInt(); } if( filter.filter.isDate() ) { switch( elem.match ) { case expression_element::Less: if( isDateAbsolute ) filter.filter.condition = MetaQueryWidget::LessThan; else filter.filter.condition = MetaQueryWidget::NewerThan; break; case expression_element::More: if( isDateAbsolute ) filter.filter.condition = MetaQueryWidget::GreaterThan; else filter.filter.condition = MetaQueryWidget::OlderThan; break; default: filter.filter.condition = MetaQueryWidget::Equals; } } else if( filter.filter.isNumeric() ) { switch( elem.match ) { case expression_element::Equals: filter.filter.condition = MetaQueryWidget::Equals; break; case expression_element::Less: filter.filter.condition = MetaQueryWidget::LessThan; break; case expression_element::More: filter.filter.condition = MetaQueryWidget::GreaterThan; break; case expression_element::Contains: break; } } else { switch( elem.match ) { case expression_element::Contains: filter.filter.condition = MetaQueryWidget::Contains; break; case expression_element::Equals: filter.filter.condition = MetaQueryWidget::Equals; break; case expression_element::Less: case expression_element::More: break; } filter.filter.value = elem.text; } filter.inverted = elem.negate; Token *nToken = filter.filter.field() ? tokenForField( filter.filter.field() ) : SIMPLE_TEXT_CONSTRUCT; m_filters.insert( nToken, filter ); connect( nToken, &Token::destroyed, this, &EditFilterDialog::slotTokenDestroyed ); m_ui->dropTarget->appendToken( nToken ); OR = true; } OR = false; AND = true; } m_isUpdating = false; } QString EditFilterDialog::filter() { QString filterString; QList < Token *> tokens = m_ui->dropTarget->tokensAtRow(); bool join = false; foreach( Token *token, tokens ) { if( token->value() == OR_TOKEN ) { filterString.append( " OR " ); join = false; } else if( token->value() == AND_TOKEN ) { filterString.append( " AND " ); join = false; } else { if( join ) filterString.append( m_separator ); Filter &filter = filterForToken( token ); filterString.append( filter.filter.toString( filter.inverted ) ); join = true; } } return filterString; } diff --git a/src/dynamic/biases/TagMatchBias.cpp b/src/dynamic/biases/TagMatchBias.cpp index 9998a37e22..f000612b07 100644 --- a/src/dynamic/biases/TagMatchBias.cpp +++ b/src/dynamic/biases/TagMatchBias.cpp @@ -1,522 +1,522 @@ /**************************************************************************************** * Copyright (c) 2008 Daniel Jones * * Copyright (c) 2009 Leo Franchi * * Copyright (c) 2010,2011 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) version 3 or * * any later version accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "TagMatchBias" #include "TagMatchBias.h" #include "core/collections/Collection.h" #include "core/collections/QueryMaker.h" #include "core/meta/Meta.h" #include "core/support/Debug.h" #include "core-impl/collections/support/CollectionManager.h" #include "dynamic/TrackSet.h" #include #include #include #include #include #include #include QString Dynamic::TagMatchBiasFactory::i18nName() const { return i18nc("Name of the \"TagMatch\" bias", "Match meta tag"); } QString Dynamic::TagMatchBiasFactory::name() const { return Dynamic::TagMatchBias::sName(); } QString Dynamic::TagMatchBiasFactory::i18nDescription() const { return i18nc("Description of the \"TagMatch\" bias", "The \"TagMatch\" bias adds tracks that\n" "fulfill a specific condition."); } Dynamic::BiasPtr Dynamic::TagMatchBiasFactory::createBias() { return Dynamic::BiasPtr( new Dynamic::TagMatchBias() ); } // ----- SimpleMatchBias -------- Dynamic::SimpleMatchBias::SimpleMatchBias() : m_invert( false ) { } void Dynamic::SimpleMatchBias::fromXml( QXmlStreamReader *reader ) { m_invert = reader->attributes().value( QStringLiteral("invert") ).toString().toInt(); } void Dynamic::SimpleMatchBias::toXml( QXmlStreamWriter *writer ) const { if( m_invert ) writer->writeAttribute(QStringLiteral("invert"), QStringLiteral("1")); } bool Dynamic::SimpleMatchBias::isInvert() const { return m_invert; } void Dynamic::SimpleMatchBias::setInvert( bool value ) { DEBUG_BLOCK; if( value == m_invert ) return; m_invert = value; // setting "invert" does not invalidate the search results Q_EMIT changed( BiasPtr(this) ); } Dynamic::TrackSet Dynamic::SimpleMatchBias::matchingTracks( const Meta::TrackList& playlist, int contextCount, int finalCount, const Dynamic::TrackCollectionPtr &universe ) const { Q_UNUSED( playlist ); Q_UNUSED( contextCount ); Q_UNUSED( finalCount ); if( tracksValid() ) return m_tracks; m_tracks = Dynamic::TrackSet( universe, m_invert ); QTimer::singleShot(0, const_cast(this), &SimpleMatchBias::newQuery); // create the new query from my parent thread return Dynamic::TrackSet(); } void Dynamic::SimpleMatchBias::updateReady( const QStringList &uids ) { if( m_invert ) m_tracks.subtract( uids ); else m_tracks.unite( uids ); } void Dynamic::SimpleMatchBias::updateFinished() { m_tracksTime = QDateTime::currentDateTime(); m_qm.reset(); debug() << "SimpleMatchBias::"<setAlignment( Qt::AlignLeft | Qt::AlignVCenter ); label->setBuddy( m_invertBox ); invertLayout->addWidget( m_invertBox, 0 ); invertLayout->addWidget( label, 1 ); layout->addLayout(invertLayout); m_queryWidget = new MetaQueryWidget(); layout->addWidget( m_queryWidget ); syncControlsToBias(); connect( m_invertBox, &QCheckBox::toggled, this, &TagMatchBiasWidget::syncBiasToControls ); connect( m_queryWidget, &MetaQueryWidget::changed, this, &TagMatchBiasWidget::syncBiasToControls ); } void Dynamic::TagMatchBiasWidget::syncControlsToBias() { m_queryWidget->setFilter( m_bias->filter() ); m_invertBox->setChecked( m_bias->isInvert() ); } void Dynamic::TagMatchBiasWidget::syncBiasToControls() { m_bias->setFilter( m_queryWidget->filter() ); m_bias->setInvert( m_invertBox->isChecked() ); } // ----- TagMatchBias -------- Dynamic::TagMatchBias::TagMatchBias() : SimpleMatchBias() { } void Dynamic::TagMatchBias::fromXml( QXmlStreamReader *reader ) { SimpleMatchBias::fromXml( reader ); while (!reader->atEnd()) { reader->readNext(); if( reader->isStartElement() ) { QStringRef name = reader->name(); if( name == "field" ) m_filter.setField( Meta::fieldForPlaylistName( reader->readElementText(QXmlStreamReader::SkipChildElements) ) ); else if( name == "numValue" ) m_filter.numValue = reader->readElementText(QXmlStreamReader::SkipChildElements).toUInt(); else if( name == "numValue2" ) m_filter.numValue2 = reader->readElementText(QXmlStreamReader::SkipChildElements).toUInt(); else if( name == "value" ) m_filter.value = reader->readElementText(QXmlStreamReader::SkipChildElements); else if( name == "condition" ) m_filter.condition = conditionForName( reader->readElementText(QXmlStreamReader::SkipChildElements) ); else { debug()<<"Unexpected xml start element"<name()<<"in input"; reader->skipCurrentElement(); } } else if( reader->isEndElement() ) { break; } } } void Dynamic::TagMatchBias::toXml( QXmlStreamWriter *writer ) const { SimpleMatchBias::toXml( writer ); writer->writeTextElement( QStringLiteral("field"), Meta::playlistNameForField( m_filter.field() ) ); if( m_filter.isNumeric() ) { writer->writeTextElement( QStringLiteral("numValue"), QString::number( m_filter.numValue ) ); writer->writeTextElement( QStringLiteral("numValue2"), QString::number( m_filter.numValue2 ) ); } else { writer->writeTextElement( QStringLiteral("value"), m_filter.value ); } writer->writeTextElement( QStringLiteral("condition"), nameForCondition( m_filter.condition ) ); } QString Dynamic::TagMatchBias::sName() { return QStringLiteral( "tagMatchBias" ); } QString Dynamic::TagMatchBias::name() const { return Dynamic::TagMatchBias::sName(); } QString Dynamic::TagMatchBias::toString() const { if( isInvert() ) return i18nc("Inverted condition in tag match bias", "Not %1", m_filter.toString() ); else return m_filter.toString(); } QWidget* Dynamic::TagMatchBias::widget( QWidget* parent ) { return new Dynamic::TagMatchBiasWidget( this, parent ); } bool Dynamic::TagMatchBias::trackMatches( int position, const Meta::TrackList& playlist, int contextCount ) const { Q_UNUSED( contextCount ); if( tracksValid() ) return m_tracks.contains( playlist.at(position) ); else return matches( playlist.at(position) ); } MetaQueryWidget::Filter Dynamic::TagMatchBias::filter() const { return m_filter; } void Dynamic::TagMatchBias::setFilter( const MetaQueryWidget::Filter &filter) { DEBUG_BLOCK; m_filter = filter; invalidate(); Q_EMIT changed( BiasPtr(this) ); } void Dynamic::TagMatchBias::newQuery() { DEBUG_BLOCK; // ok, I need a new query maker m_qm.reset( CollectionManager::instance()->queryMaker() ); // -- set the querymaker if( m_filter.isDate() ) { switch( m_filter.condition ) { case MetaQueryWidget::LessThan: case MetaQueryWidget::Equals: case MetaQueryWidget::GreaterThan: m_qm->addNumberFilter( m_filter.field(), m_filter.numValue, (Collections::QueryMaker::NumberComparison)m_filter.condition ); break; case MetaQueryWidget::Between: m_qm->beginAnd(); m_qm->addNumberFilter( m_filter.field(), qMin(m_filter.numValue, m_filter.numValue2)-1, Collections::QueryMaker::GreaterThan ); m_qm->addNumberFilter( m_filter.field(), qMax(m_filter.numValue, m_filter.numValue2)+1, Collections::QueryMaker::LessThan ); m_qm->endAndOr(); break; case MetaQueryWidget::OlderThan: - m_qm->addNumberFilter( m_filter.field(), QDateTime::currentDateTimeUtc().toTime_t() - m_filter.numValue, + m_qm->addNumberFilter( m_filter.field(), QDateTime::currentDateTimeUtc().toSecsSinceEpoch() - m_filter.numValue, Collections::QueryMaker::LessThan ); break; default: ; } } else if( m_filter.isNumeric() ) { switch( m_filter.condition ) { case MetaQueryWidget::LessThan: case MetaQueryWidget::Equals: case MetaQueryWidget::GreaterThan: m_qm->addNumberFilter( m_filter.field(), m_filter.numValue, (Collections::QueryMaker::NumberComparison)m_filter.condition ); break; case MetaQueryWidget::Between: m_qm->beginAnd(); m_qm->addNumberFilter( m_filter.field(), qMin(m_filter.numValue, m_filter.numValue2)-1, Collections::QueryMaker::GreaterThan ); m_qm->addNumberFilter( m_filter.field(), qMax(m_filter.numValue, m_filter.numValue2)+1, Collections::QueryMaker::LessThan ); m_qm->endAndOr(); break; default: ; } } else { switch( m_filter.condition ) { case MetaQueryWidget::Equals: m_qm->addFilter( m_filter.field(), m_filter.value, true, true ); break; case MetaQueryWidget::Contains: if( m_filter.field() == 0 ) { // simple search // TODO: split different words and make separate searches m_qm->beginOr(); m_qm->addFilter( Meta::valArtist, m_filter.value ); m_qm->addFilter( Meta::valTitle, m_filter.value ); m_qm->addFilter( Meta::valAlbum, m_filter.value ); m_qm->addFilter( Meta::valGenre, m_filter.value ); m_qm->addFilter( Meta::valUrl, m_filter.value ); m_qm->addFilter( Meta::valComment, m_filter.value ); m_qm->addFilter( Meta::valLabel, m_filter.value ); m_qm->endAndOr(); } else { m_qm->addFilter( m_filter.field(), m_filter.value ); } break; default: ; } } m_qm->setQueryType( Collections::QueryMaker::Custom ); m_qm->addReturnValue( Meta::valUniqueId ); connect( m_qm.data(), &Collections::QueryMaker::newResultReady, this, &TagMatchBias::updateReady, Qt::QueuedConnection ); connect( m_qm.data(), &Collections::QueryMaker::queryDone, this, &TagMatchBias::updateFinished, Qt::QueuedConnection ); m_qm.data()->run(); } QString Dynamic::TagMatchBias::nameForCondition( MetaQueryWidget::FilterCondition cond ) { switch( cond ) { case MetaQueryWidget::Equals: return QStringLiteral("equals"); case MetaQueryWidget::GreaterThan: return QStringLiteral("greater"); case MetaQueryWidget::LessThan: return QStringLiteral("less"); case MetaQueryWidget::Between: return QStringLiteral("between"); case MetaQueryWidget::OlderThan: return QStringLiteral("older"); case MetaQueryWidget::Contains: return QStringLiteral("contains"); default: ;// the other conditions are only for the advanced playlist generator } return QString(); } MetaQueryWidget::FilterCondition Dynamic::TagMatchBias::conditionForName( const QString &name ) { if( name == QLatin1String("equals") ) return MetaQueryWidget::Equals; else if( name == QLatin1String("greater") ) return MetaQueryWidget::GreaterThan; else if( name == QLatin1String("less") ) return MetaQueryWidget::LessThan; else if( name == QLatin1String("between") ) return MetaQueryWidget::Between; else if( name == QLatin1String("older") ) return MetaQueryWidget::OlderThan; else if( name == QLatin1String("contains") ) return MetaQueryWidget::Contains; else return MetaQueryWidget::Equals; } bool Dynamic::TagMatchBias::matches( const Meta::TrackPtr &track ) const { QVariant value = Meta::valueForField( m_filter.field(), track ); bool result = false; if( m_filter.isDate() ) { switch( m_filter.condition ) { case MetaQueryWidget::LessThan: result = value.toLongLong() < m_filter.numValue; break; case MetaQueryWidget::Equals: result = value.toLongLong() == m_filter.numValue; break; case MetaQueryWidget::GreaterThan: result = value.toLongLong() > m_filter.numValue; break; case MetaQueryWidget::Between: result = value.toLongLong() > m_filter.numValue && value.toLongLong() < m_filter.numValue2; break; case MetaQueryWidget::OlderThan: - result = value.toLongLong() < m_filter.numValue + QDateTime::currentDateTimeUtc().toTime_t(); + result = value.toLongLong() < m_filter.numValue + QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); break; default: ; } } else if( m_filter.isNumeric() ) { switch( m_filter.condition ) { case MetaQueryWidget::LessThan: result = value.toLongLong() < m_filter.numValue; break; case MetaQueryWidget::Equals: result = value.toLongLong() == m_filter.numValue; break; case MetaQueryWidget::GreaterThan: result = value.toLongLong() > m_filter.numValue; break; case MetaQueryWidget::Between: result = value.toLongLong() > m_filter.numValue && value.toLongLong() < m_filter.numValue2; break; default: ; } } else { switch( m_filter.condition ) { case MetaQueryWidget::Equals: result = value.toString() == m_filter.value; break; case MetaQueryWidget::Contains: result = value.toString().contains( m_filter.value, Qt::CaseInsensitive ); break; default: ; } } if( m_invert ) return !result; else return result; } diff --git a/src/importers/clementine/ClementineTrack.cpp b/src/importers/clementine/ClementineTrack.cpp index e1ddcc41ee..c78e0f5448 100644 --- a/src/importers/clementine/ClementineTrack.cpp +++ b/src/importers/clementine/ClementineTrack.cpp @@ -1,137 +1,137 @@ /**************************************************************************************** * Copyright (c) 2013 Konrad Zemek * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "ClementineTrack.h" #include "importers/ImporterSqlConnection.h" #include #include #include using namespace StatSyncing; ClementineTrack::ClementineTrack( const QVariant &filename, const ImporterSqlConnectionPtr &connection, const Meta::FieldHash &metadata ) : SimpleWritableTrack( metadata ) , m_connection( connection ) , m_filename( filename ) { } ClementineTrack::~ClementineTrack() { } int ClementineTrack::year() const { const int yr = SimpleWritableTrack::year(); return yr == -1 ? 0 : yr; } int ClementineTrack::trackNumber() const { const int tn = SimpleWritableTrack::trackNumber(); return tn == -1 ? 0 : tn; } int ClementineTrack::discNumber() const { const int dn = SimpleWritableTrack::discNumber(); return dn == -1 ? 0 : dn; } QDateTime ClementineTrack::lastPlayed() const { QReadLocker lock( &m_lock ); const int lp = m_statistics.value( Meta::valLastPlayed ).toInt(); return lp == -1 ? QDateTime() : getDateTime( lp ); } void ClementineTrack::setLastPlayed( const QDateTime &lastPlayed ) { QWriteLocker lock( &m_lock ); m_statistics.insert( Meta::valLastPlayed, lastPlayed.isValid() - ? lastPlayed.toTime_t() : -1 ); + ? lastPlayed.toSecsSinceEpoch() : -1 ); m_changes |= Meta::valLastPlayed; } int ClementineTrack::playCount() const { const int pc = SimpleWritableTrack::playCount(); return pc == -1 ? 0 : pc; } void ClementineTrack::setPlayCount( int playCount ) { SimpleWritableTrack::setPlayCount( playCount == 0 ? -1 : playCount ); } int ClementineTrack::rating() const { QReadLocker lock( &m_lock ); const qreal rt = m_statistics.value( Meta::valRating ).toReal(); return rt < 0 ? 0 : qRound( rt * 10 ); } void ClementineTrack::setRating( int rating ) { QWriteLocker lock( &m_lock ); m_statistics.insert( Meta::valRating, rating == 0 ? -1.0 : 0.1 * rating ); m_changes |= Meta::valRating; } void ClementineTrack::doCommit( const qint64 fields ) { QStringList updates; QVariantMap bindValues; if( fields & Meta::valLastPlayed ) { updates << "lastplayed = :lastplayed"; bindValues.insert( ":lastplayed", m_statistics.value( Meta::valLastPlayed ) ); } if( fields & Meta::valRating ) { updates << "rating = :rating"; bindValues.insert( ":rating", m_statistics.value( Meta::valRating ) ); } if( fields & Meta::valPlaycount ) { updates << "playcount = :playcount"; bindValues.insert( ":playcount", m_statistics.value( Meta::valPlaycount ) ); } if( !updates.empty() ) { const QString query = "UPDATE songs SET " + updates.join(", ") + " WHERE filename = :name"; bindValues.insert( ":name", m_filename ); m_connection->query( query, bindValues ); } } diff --git a/src/playlist/navigators/RandomTrackNavigator.cpp b/src/playlist/navigators/RandomTrackNavigator.cpp index 385e2576e8..f5febadcca 100644 --- a/src/playlist/navigators/RandomTrackNavigator.cpp +++ b/src/playlist/navigators/RandomTrackNavigator.cpp @@ -1,97 +1,97 @@ /**************************************************************************************** * Copyright (c) 2008 Seb Ruiz * * Copyright (c) 2008 Soren Harward * * Copyright (c) 2008 Nikolaj Hald Nielsen * * Copyright (c) 2009 Téo Mrnjavac * * Copyright (c) 2010 Nanno Langstraat * * Copyright (c) 2013 Daniel Schmitz * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) version 3 or * * any later version accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "Playlist::RandomTrackNavigator" #include "RandomTrackNavigator.h" #include "core/support/Debug.h" #include #include // For 'qrand()' #include // For 'round()' Playlist::RandomTrackNavigator::RandomTrackNavigator() { loadFromSourceModel(); - qsrand( QDateTime::currentDateTimeUtc().toTime_t() ); + qsrand( QDateTime::currentDateTimeUtc().toSecsSinceEpoch() ); } void Playlist::RandomTrackNavigator::planOne() { DEBUG_BLOCK if ( m_plannedItems.isEmpty() ) { if ( !allItemsList().isEmpty() ) { quint64 chosenItem; int avoidRecentlyPlayedSize = AVOID_RECENTLY_PLAYED_MAX; // Start with being very picky. // Don't over-constrain ourself: // - Keep enough headroom to be unpredictable. // - Make sure that 'chooseRandomItem()' doesn't need to find a needle in a haystack. avoidRecentlyPlayedSize = qMin( avoidRecentlyPlayedSize, allItemsList().size() / 2 ); QSet avoidSet = getRecentHistory( avoidRecentlyPlayedSize ); chosenItem = chooseRandomItem( avoidSet ); m_plannedItems.append( chosenItem ); } } } QSet Playlist::RandomTrackNavigator::getRecentHistory( int size ) { QList allHistory = historyItems(); QSet recentHistory; if ( size > 0 ) { // If '== 0', we even need to consider playing the same item again. recentHistory.insert( currentItem() ); // Might be '0' size--; } for ( int i = allHistory.size() - 1; ( i >= 0 ) && ( i >= allHistory.size() - size ); i-- ) recentHistory.insert( allHistory.at( i ) ); return recentHistory; } quint64 Playlist::RandomTrackNavigator::chooseRandomItem( const QSet &avoidSet ) { quint64 chosenItem; do { int maxPosition = allItemsList().size() - 1; int randomPosition = round( ( qrand() / (float)RAND_MAX ) * maxPosition ); chosenItem = allItemsList().at( randomPosition ); } while ( avoidSet.contains( chosenItem ) ); return chosenItem; } diff --git a/src/playlistgenerator/constraints/TagMatch.cpp b/src/playlistgenerator/constraints/TagMatch.cpp index 20ee395e9c..74b43b7244 100644 --- a/src/playlistgenerator/constraints/TagMatch.cpp +++ b/src/playlistgenerator/constraints/TagMatch.cpp @@ -1,755 +1,755 @@ /**************************************************************************************** * Copyright (c) 2008-2012 Soren Harward * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "Constraint::TagMatch" #include "TagMatch.h" #include "playlistgenerator/Constraint.h" #include "playlistgenerator/ConstraintFactory.h" #include "core/collections/QueryMaker.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/support/Debug.h" #include #include Constraint* ConstraintTypes::TagMatch::createFromXml( QDomElement& xmlelem, ConstraintNode* p ) { if ( p ) return new TagMatch( xmlelem, p ); else return nullptr; } Constraint* ConstraintTypes::TagMatch::createNew( ConstraintNode* p ) { if ( p ) return new TagMatch( p ); else return nullptr; } ConstraintFactoryEntry* ConstraintTypes::TagMatch::registerMe() { return new ConstraintFactoryEntry( QStringLiteral("TagMatch"), i18n("Match Tags"), i18n("Make all tracks in the playlist match the specified characteristic"), &TagMatch::createFromXml, &TagMatch::createNew ); } ConstraintTypes::TagMatch::TagMatch( QDomElement& xmlelem, ConstraintNode* p ) : MatchingConstraint( p ) , m_comparer( new Comparer() ) , m_fieldsModel( new TagMatchFieldsModel() ) { QDomAttr a; a = xmlelem.attributeNode( QStringLiteral("field") ); if ( !a.isNull() ) { if ( m_fieldsModel->contains( a.value() ) ) m_field = a.value(); } a = xmlelem.attributeNode( QStringLiteral("comparison") ); if ( !a.isNull() ) { m_comparison = a.value().toInt(); } a = xmlelem.attributeNode( QStringLiteral("value") ); if ( !a.isNull() ) { if ( m_fieldsModel->type_of( m_field ) == FieldTypeInt ) { m_value = a.value().toInt(); } else if ( m_fieldsModel->type_of( m_field ) == FieldTypeDate ) { if ( m_comparison == CompareDateWithin ) { QStringList parts = a.value().split(' '); if ( parts.size() == 2 ) { int u = parts.at( 0 ).toInt(); int v = 0; if ( parts.at( 1 ) == QLatin1String("months") ) v = 1; else if ( parts.at( 1 ) == QLatin1String("years") ) v = 2; m_value = QVariant::fromValue( DateRange( u, v ) ); } else m_value = QVariant::fromValue( DateRange( 0, 0 ) ); } else m_value = QDate::fromString( a.value(), Qt::ISODate ); } else { // String type m_value = a.value(); } } a = xmlelem.attributeNode( QStringLiteral("invert") ); if ( !a.isNull() && a.value() == QLatin1String("true") ) m_invert = true; else m_invert = false; a = xmlelem.attributeNode( QStringLiteral("strictness") ); if ( !a.isNull() ) m_strictness = a.value().toDouble(); } ConstraintTypes::TagMatch::TagMatch( ConstraintNode* p ) : MatchingConstraint( p ) , m_comparison( CompareStrEquals ) , m_field( QStringLiteral("title") ) , m_invert( false ) , m_strictness( 1.0 ) , m_value() , m_comparer( new Comparer() ) , m_fieldsModel( new TagMatchFieldsModel() ) { } ConstraintTypes::TagMatch::~TagMatch() { delete m_comparer; delete m_fieldsModel; } QWidget* ConstraintTypes::TagMatch::editWidget() const { TagMatchEditWidget* e = new TagMatchEditWidget( m_comparison, m_field, m_invert, static_cast( m_strictness * 10 ), m_value ); connect( e, &TagMatchEditWidget::comparisonChanged, this, &TagMatch::setComparison ); connect( e, &TagMatchEditWidget::fieldChanged, this, &TagMatch::setField ); connect( e, &TagMatchEditWidget::invertChanged, this, &TagMatch::setInvert ); connect( e, &TagMatchEditWidget::strictnessChanged, this, &TagMatch::setStrictness ); connect( e, &TagMatchEditWidget::valueChanged, this, &TagMatch::setValue ); return e; } void ConstraintTypes::TagMatch::toXml( QDomDocument& doc, QDomElement& elem ) const { QDomElement c = doc.createElement( QStringLiteral("constraint") ); c.setAttribute( QStringLiteral("type"), QStringLiteral("TagMatch") ); c.setAttribute( QStringLiteral("field"), m_field ); c.setAttribute( QStringLiteral("comparison"), m_comparison ); c.setAttribute( QStringLiteral("value"), valueToString() ); if ( m_invert ) c.setAttribute( QStringLiteral("invert"), QStringLiteral("true") ); else c.setAttribute( QStringLiteral("invert"), QStringLiteral("false") ); c.setAttribute( QStringLiteral("strictness"), QString::number( m_strictness ) ); elem.appendChild( c ); } QString ConstraintTypes::TagMatch::getName() const { QString v( i18nc( "%1 = empty string or \"not\"; " "%2 = a metadata field, like \"title\" or \"artist name\"; " "%3 = a predicate, can be equals, starts with, ends with or contains; " "%4 = a string to match; " "Example: Match tag: not title contains \"foo\"", "Match tag:%1 %2 %3 %4") ); v = v.arg( ( m_invert ? i18n(" not") : QLatin1String("") ), m_fieldsModel->pretty_name_of( m_field ), comparisonToString() ); if ( m_field == QLatin1String("rating") ) { double r = m_value.toDouble() / 2.0; return v.arg( i18ncp("number of stars in the rating of a track", "%1 star", "%1 stars", r) ); } else if ( m_field == QLatin1String("length") ) { return v.arg( QTime(0, 0, 0).addMSecs( m_value.toInt() ).toString( QStringLiteral("H:mm:ss") ) ); } else { if ( m_fieldsModel->type_of( m_field ) == FieldTypeString ) { // put quotes around any strings (eg, track title or artist name) ... QString s = i18nc("an arbitrary string surrounded by quotes", "\"%1\"", valueToString() ); return v.arg( s ); } else { // ... but don't quote put quotes around anything else return v.arg( valueToString() ); } } } Collections::QueryMaker* ConstraintTypes::TagMatch::initQueryMaker( Collections::QueryMaker* qm ) const { if ( ( m_fieldsModel->type_of( m_field ) == FieldTypeInt ) ) { int v = m_value.toInt(); int range = static_cast( m_comparer->rangeNum( m_strictness, m_fieldsModel->meta_value_of( m_field ) ) ); if ( m_comparison == CompareNumEquals ) { if ( !m_invert ) { if ( m_strictness < 0.99 ) { // fuzzy approximation of "1.0" qm->beginAnd(); qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), v - range, Collections::QueryMaker::GreaterThan ); qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), v + range, Collections::QueryMaker::LessThan ); qm->endAndOr(); } else { qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), v, Collections::QueryMaker::Equals ); } } else { if ( m_strictness > 0.99 ) { qm->excludeNumberFilter( m_fieldsModel->meta_value_of( m_field ), v, Collections::QueryMaker::Equals ); } } } else if ( m_comparison == CompareNumGreaterThan ) { if ( m_invert ) qm->excludeNumberFilter( m_fieldsModel->meta_value_of( m_field ), v + range, Collections::QueryMaker::GreaterThan ); else qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), v - range, Collections::QueryMaker::GreaterThan ); } else if ( m_comparison == CompareNumLessThan ) { if ( m_invert ) qm->excludeNumberFilter( m_fieldsModel->meta_value_of( m_field ), v - range, Collections::QueryMaker::LessThan ); else qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), v + range, Collections::QueryMaker::LessThan ); } } else if ( m_fieldsModel->type_of( m_field ) == FieldTypeDate ) { uint referenceDate = 0; int range = m_comparer->rangeDate( m_strictness ); if ( m_comparison == CompareDateBefore ) { - referenceDate = m_value.toDateTime().toTime_t(); + referenceDate = m_value.toDateTime().toSecsSinceEpoch(); if ( m_invert ) qm->excludeNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate - range, Collections::QueryMaker::LessThan ); else qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate + range, Collections::QueryMaker::LessThan ); } else if ( m_comparison == CompareDateOn ) { - referenceDate = m_value.toDateTime().toTime_t(); + referenceDate = m_value.toDateTime().toSecsSinceEpoch(); if ( !m_invert ) { qm->beginAnd(); qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate - range, Collections::QueryMaker::GreaterThan ); qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate + range, Collections::QueryMaker::LessThan ); qm->endAndOr(); } } else if ( m_comparison == CompareDateAfter ) { - referenceDate = m_value.toDateTime().toTime_t(); + referenceDate = m_value.toDateTime().toSecsSinceEpoch(); if ( m_invert ) qm->excludeNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate + range, Collections::QueryMaker::GreaterThan ); else qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate - range, Collections::QueryMaker::GreaterThan ); } else if ( m_comparison == CompareDateWithin ) { QDateTime now = QDateTime::currentDateTime(); DateRange r = m_value.value(); switch ( r.second ) { case 0: - referenceDate = now.addDays( -1 * r.first ).toTime_t(); + referenceDate = now.addDays( -1 * r.first ).toSecsSinceEpoch(); break; case 1: - referenceDate = now.addMonths( -1 * r.first ).toTime_t(); + referenceDate = now.addMonths( -1 * r.first ).toSecsSinceEpoch(); break; case 2: - referenceDate = now.addYears( -1 * r.first ).toTime_t(); + referenceDate = now.addYears( -1 * r.first ).toSecsSinceEpoch(); break; default: break; } if ( m_invert ) qm->excludeNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate + range, Collections::QueryMaker::GreaterThan ); else qm->addNumberFilter( m_fieldsModel->meta_value_of( m_field ), referenceDate - range, Collections::QueryMaker::GreaterThan ); } } else if ( m_fieldsModel->type_of( m_field ) == FieldTypeString ) { if ( m_comparison == CompareStrEquals ) { if ( m_invert ) qm->excludeFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), true, true ); else qm->addFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), true, true ); } else if ( m_comparison == CompareStrStartsWith ) { if ( m_invert ) qm->excludeFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), true, false ); else qm->addFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), true, false ); } else if ( m_comparison == CompareStrEndsWith ) { if ( m_invert ) qm->excludeFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), false, true ); else qm->addFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), false, true ); } else if ( m_comparison == CompareStrContains ) { if ( m_invert ) qm->excludeFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), false, false ); else qm->addFilter( m_fieldsModel->meta_value_of( m_field ), m_value.toString(), false, false ); } // TODO: regexp } else { error() << "TagMatch cannot initialize QM for unknown type"; } return qm; } double ConstraintTypes::TagMatch::satisfaction( const Meta::TrackList& tl ) const { double satisfaction = 0.0; foreach( Meta::TrackPtr t, tl ) { if ( matches( t ) ) { satisfaction += 1.0; } } satisfaction /= ( double )tl.size(); return satisfaction; } const QBitArray ConstraintTypes::TagMatch::whatTracksMatch( const Meta::TrackList& tl ) { QBitArray match = QBitArray( tl.size() ); for ( int i = 0; i < tl.size(); i++ ) { if ( matches( tl.at( i ) ) ) match.setBit( i, true ); } return match; } int ConstraintTypes::TagMatch::constraintMatchType() const { return ( 0 << 28 ) + m_fieldsModel->index_of( m_field ); } QString ConstraintTypes::TagMatch::comparisonToString() const { if ( m_fieldsModel->type_of( m_field ) == FieldTypeInt ) { if ( m_comparison == CompareNumEquals ) { return i18nc("a numerical tag (like year or track number) equals a value","equals"); } else if ( m_comparison == CompareNumGreaterThan ) { return i18n("greater than"); } else if ( m_comparison == CompareNumLessThan ) { return i18n("less than"); } } else if ( m_fieldsModel->type_of( m_field ) == FieldTypeDate ) { if ( m_comparison == CompareDateBefore ) { return i18n("before"); } else if ( m_comparison == CompareDateOn ) { return i18n("on"); } else if ( m_comparison == CompareDateAfter ) { return i18n("after"); } else if ( m_comparison == CompareDateWithin ) { return i18n("within"); } } else { if ( m_comparison == CompareStrEquals ) { return i18nc("an alphabetical tag (like title or artist name) equals some string","equals"); } else if ( m_comparison == CompareStrStartsWith ) { return i18nc("an alphabetical tag (like title or artist name) starts with some string","starts with"); } else if ( m_comparison == CompareStrEndsWith ) { return i18nc("an alphabetical tag (like title or artist name) ends with some string","ends with"); } else if ( m_comparison == CompareStrContains ) { return i18nc("an alphabetical tag (like title or artist name) contains some string","contains"); } else if ( m_comparison == CompareStrRegExp ) { return i18n("regexp"); } } return i18n("unknown comparison"); } QString ConstraintTypes::TagMatch::valueToString() const { if ( m_fieldsModel->type_of( m_field ) == FieldTypeDate ) { if ( m_comparison != CompareDateWithin ) { return m_value.toDate().toString( Qt::ISODate ); } else { KLocalizedString unit; switch ( m_value.value().second ) { case 0: unit = ki18np("%1 day", "%1 days"); break; case 1: unit = ki18np("%1 month", "%1 months"); break; case 2: unit = ki18np("%1 year", "%1 years"); break; default: break; } return unit.subs( m_value.value().first ).toString(); } } else { return m_value.toString(); } } bool ConstraintTypes::TagMatch::matches( Meta::TrackPtr track ) const { if ( !m_matchCache.contains( track ) ) { double v = 0.0; qint64 fmv = m_fieldsModel->meta_value_of( m_field ); switch ( fmv ) { case Meta::valUrl: v = m_comparer->compareStr( track->prettyUrl(), m_comparison, m_value.toString() ); break; case Meta::valTitle: v = m_comparer->compareStr( track->prettyName(), m_comparison, m_value.toString() ); break; case Meta::valArtist: v = m_comparer->compareStr( track->artist()->prettyName(), m_comparison, m_value.toString() ); break; case Meta::valAlbum: v = m_comparer->compareStr( track->album()->prettyName(), m_comparison, m_value.toString() ); break; case Meta::valGenre: v = m_comparer->compareStr( track->genre()->prettyName(), m_comparison, m_value.toString() ); break; case Meta::valComposer: v = m_comparer->compareStr( track->composer()->prettyName(), m_comparison, m_value.toString() ); break; case Meta::valYear: v = m_comparer->compareNum( track->year()->prettyName().toInt(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valComment: v = m_comparer->compareStr( track->comment(), m_comparison, m_value.toString() ); break; case Meta::valTrackNr: v = m_comparer->compareNum( track->trackNumber(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valDiscNr: v = m_comparer->compareNum( track->discNumber(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valLength: v = m_comparer->compareNum( track->length(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valBitrate: v = m_comparer->compareNum( track->bitrate(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valFilesize: v = m_comparer->compareNum( track->filesize(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valCreateDate: - v = m_comparer->compareDate( track->createDate().toTime_t(), m_comparison, m_value, m_strictness ); + v = m_comparer->compareDate( track->createDate().toSecsSinceEpoch(), m_comparison, m_value, m_strictness ); break; case Meta::valScore: v = m_comparer->compareNum( track->statistics()->score(), m_comparison, m_value.toDouble(), m_strictness, fmv ); break; case Meta::valRating: v = m_comparer->compareNum( track->statistics()->rating(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valFirstPlayed: - v = m_comparer->compareDate( track->statistics()->firstPlayed().toTime_t(), m_comparison, m_value, m_strictness ); + v = m_comparer->compareDate( track->statistics()->firstPlayed().toSecsSinceEpoch(), m_comparison, m_value, m_strictness ); break; case Meta::valLastPlayed: - v = m_comparer->compareDate( track->statistics()->lastPlayed().toTime_t(), m_comparison, m_value, m_strictness ); + v = m_comparer->compareDate( track->statistics()->lastPlayed().toSecsSinceEpoch(), m_comparison, m_value, m_strictness ); break; case Meta::valPlaycount: v = m_comparer->compareNum( track->statistics()->playCount(), m_comparison, m_value.toInt(), m_strictness, fmv ); break; case Meta::valLabel: v = m_comparer->compareLabels( track, m_comparison, m_value.toString() ); break; default: v = 0.0; break; } if ( m_invert ) v = 1.0 - v; m_matchCache.insert( track, ( v > ( (double)qrand() / (double)RAND_MAX ) ) ); } return m_matchCache.value( track ); } void ConstraintTypes::TagMatch::setComparison( int c ) { m_comparison = c; m_matchCache.clear(); Q_EMIT dataChanged(); } void ConstraintTypes::TagMatch::setField( const QString& s ) { m_field = s; m_matchCache.clear(); Q_EMIT dataChanged(); } void ConstraintTypes::TagMatch::setInvert( bool v ) { if ( m_invert != v ) { foreach( const Meta::TrackPtr t, m_matchCache.keys() ) { m_matchCache.insert( t, !m_matchCache.value( t ) ); } } m_invert = v; Q_EMIT dataChanged(); } void ConstraintTypes::TagMatch::setStrictness( int v ) { m_strictness = static_cast( v ) / 10.0; m_matchCache.clear(); } void ConstraintTypes::TagMatch::setValue( const QVariant& v ) { m_value = v; m_matchCache.clear(); Q_EMIT dataChanged(); } /****************************** * Edit Widget * ******************************/ ConstraintTypes::TagMatchEditWidget::TagMatchEditWidget( const int comparison, const QString& field, const bool invert, const int strictness, const QVariant& value ) : QWidget( nullptr ) , m_fieldsModel( new TagMatchFieldsModel() ) { ui.setupUi( this ); // plural support in combobox labels connect( ui.spinBox_ValueDateValue, QOverload::of(&QSpinBox::valueChanged), this, &TagMatchEditWidget::slotUpdateComboBoxLabels ); ui.comboBox_ValueDateUnit->insertItem(0, i18ncp("within the last %1 days", "day", "days", 0)); ui.comboBox_ValueDateUnit->insertItem(1, i18ncp("within the last %1 months", "month", "months", 0)); ui.comboBox_ValueDateUnit->insertItem(2, i18ncp("within the last %1 years", "year", "years", 0)); // fill in appropriate defaults for some attributes ui.qcalendarwidget_DateSpecific->setSelectedDate( QDate::currentDate() ); // fill in user-specified values before the slots have been connected to we don't have to call back to the constraint a dozen times ui.comboBox_Field->setModel( m_fieldsModel ); ui.checkBox_Invert->setChecked( invert ); if ( field == QLatin1String("rating") ) { ui.comboBox_ComparisonRating->setCurrentIndex( comparison ); ui.slider_StrictnessRating->setValue( strictness ); ui.rating_RatingValue->setRating( value.toInt() ); } else if ( field == QLatin1String("length") ) { ui.comboBox_ComparisonTime->setCurrentIndex( comparison ); ui.slider_StrictnessTime->setValue( strictness ); ui.timeEdit_TimeValue->setTime( QTime(0, 0, 0).addMSecs( value.toInt() ) ); } else if ( m_fieldsModel->type_of( field ) == TagMatch::FieldTypeInt ) { ui.comboBox_ComparisonInt->setCurrentIndex( comparison ); ui.slider_StrictnessInt->setValue( strictness ); ui.spinBox_ValueInt->setValue( value.toInt() ); } else if ( m_fieldsModel->type_of( field ) == TagMatch::FieldTypeDate ) { ui.comboBox_ComparisonDate->setCurrentIndex( comparison ); ui.slider_StrictnessDate->setValue( strictness ); if ( comparison == TagMatch::CompareDateWithin ) { ui.stackedWidget_Date->setCurrentIndex( 1 ); ui.spinBox_ValueDateValue->setValue( value.value().first ); ui.comboBox_ValueDateUnit->setCurrentIndex( value.value().second ); } else { ui.stackedWidget_Date->setCurrentIndex( 0 ); ui.qcalendarwidget_DateSpecific->setSelectedDate( value.toDate() ); } } else if ( m_fieldsModel->type_of( field ) == TagMatch::FieldTypeString ) { ui.comboBox_ComparisonString->setCurrentIndex( comparison ); ui.lineEdit_StringValue->setText( value.toString() ); } // set this after the slot has been connected so that it also sets the field page correctly ui.comboBox_Field->setCurrentIndex( m_fieldsModel->index_of( field ) ); } ConstraintTypes::TagMatchEditWidget::~TagMatchEditWidget() { delete m_fieldsModel; } // ComboBox slots for comparisons void ConstraintTypes::TagMatchEditWidget::on_comboBox_ComparisonDate_currentIndexChanged( int c ) { if ( c == TagMatch::CompareDateWithin ) ui.stackedWidget_Date->setCurrentIndex( 1 ); else ui.stackedWidget_Date->setCurrentIndex( 0 ); Q_EMIT comparisonChanged( c ); } void ConstraintTypes::TagMatchEditWidget::on_comboBox_ComparisonInt_currentIndexChanged( int c ) { Q_EMIT comparisonChanged( c ); } void ConstraintTypes::TagMatchEditWidget::on_comboBox_ComparisonRating_currentIndexChanged( int c ) { Q_EMIT comparisonChanged( c ); } void ConstraintTypes::TagMatchEditWidget::on_comboBox_ComparisonString_currentIndexChanged( int c ) { Q_EMIT comparisonChanged( c ); } void ConstraintTypes::TagMatchEditWidget::on_comboBox_ComparisonTime_currentIndexChanged( int c ) { Q_EMIT comparisonChanged( c ); } // ComboBox slots for field void ConstraintTypes::TagMatchEditWidget::on_comboBox_Field_currentIndexChanged( int idx ) { QString field = m_fieldsModel->field_at( idx ); int c = 0; int s = 0; QVariant v; if ( field == QLatin1String("length") ) { ui.stackedWidget_Field->setCurrentIndex( 3 ); c = ui.comboBox_ComparisonTime->currentIndex(); s = ui.slider_StrictnessTime->value(); v = QTime(0, 0, 0).msecsTo( ui.timeEdit_TimeValue->time() ); } else if ( field == QLatin1String("rating") ) { ui.stackedWidget_Field->setCurrentIndex( 4 ); c = ui.comboBox_ComparisonRating->currentIndex(); s = ui.slider_StrictnessRating->value(); v = ui.rating_RatingValue->rating(); } else { if ( m_fieldsModel->type_of( field ) == TagMatch::FieldTypeInt ) { ui.stackedWidget_Field->setCurrentIndex( 0 ); c = ui.comboBox_ComparisonInt->currentIndex(); s = ui.slider_StrictnessInt->value(); v = ui.spinBox_ValueInt->value(); } else if ( m_fieldsModel->type_of( field ) == TagMatch::FieldTypeDate ) { ui.stackedWidget_Field->setCurrentIndex( 1 ); c = ui.comboBox_ComparisonDate->currentIndex(); s = ui.slider_StrictnessDate->value(); if ( c == TagMatch::CompareDateWithin ) { ui.stackedWidget_Date->setCurrentIndex( 1 ); int a = ui.spinBox_ValueDateValue->value(); int b = ui.comboBox_ValueDateUnit->currentIndex(); v = QVariant::fromValue( DateRange( a, b ) ); } else { ui.stackedWidget_Date->setCurrentIndex( 0 ); v = ui.qcalendarwidget_DateSpecific->selectedDate(); } } else if ( m_fieldsModel->type_of( field ) == TagMatch::FieldTypeString ) { ui.stackedWidget_Field->setCurrentIndex( 2 ); c = ui.comboBox_ComparisonString->currentIndex(); s = 1.0; v = ui.lineEdit_StringValue->text(); } } // TODO: set range limitations and default values depending on field Q_EMIT fieldChanged( field ); Q_EMIT valueChanged( v ); Q_EMIT comparisonChanged( c ); Q_EMIT strictnessChanged( s ); } // Invert checkbox slot void ConstraintTypes::TagMatchEditWidget::on_checkBox_Invert_clicked( bool v ) { Q_EMIT invertChanged( v ); } // Strictness Slider slots void ConstraintTypes::TagMatchEditWidget::on_slider_StrictnessDate_valueChanged( int v ) { Q_EMIT strictnessChanged( v ); } void ConstraintTypes::TagMatchEditWidget::on_slider_StrictnessInt_valueChanged( int v ) { Q_EMIT strictnessChanged( v ); } void ConstraintTypes::TagMatchEditWidget::on_slider_StrictnessRating_valueChanged( int v ) { Q_EMIT strictnessChanged( v ); } void ConstraintTypes::TagMatchEditWidget::on_slider_StrictnessTime_valueChanged( int v ) { Q_EMIT strictnessChanged( v ); } // various value slots void ConstraintTypes::TagMatchEditWidget::on_kdatewidget_DateSpecific_changed( const QDate& v ) { Q_EMIT valueChanged( QVariant( v ) ); } void ConstraintTypes::TagMatchEditWidget::on_comboBox_ValueDateUnit_currentIndexChanged( int u ) { int v = ui.spinBox_ValueDateValue->value(); Q_EMIT valueChanged( QVariant::fromValue( DateRange( v, u ) ) ); } void ConstraintTypes::TagMatchEditWidget::on_spinBox_ValueDateValue_valueChanged( int v ) { int u = ui.comboBox_ValueDateUnit->currentIndex(); Q_EMIT valueChanged( QVariant::fromValue( DateRange( v, u ) ) ); } void ConstraintTypes::TagMatchEditWidget::on_spinBox_ValueInt_valueChanged( int v ) { Q_EMIT valueChanged( QVariant( v ) ); } void ConstraintTypes::TagMatchEditWidget::on_lineEdit_StringValue_textChanged( const QString& v ) { Q_EMIT valueChanged( QVariant( v ) ); } void ConstraintTypes::TagMatchEditWidget::on_rating_RatingValue_ratingChanged( int v ) { Q_EMIT valueChanged( QVariant( v ) ); } void ConstraintTypes::TagMatchEditWidget::on_timeEdit_TimeValue_timeChanged( const QTime& t ) { int v = QTime(0, 0, 0).msecsTo( t ); Q_EMIT valueChanged( QVariant( v ) ); } void ConstraintTypes::TagMatchEditWidget::slotUpdateComboBoxLabels( int value ) { ui.comboBox_ValueDateUnit->setItemText(0, i18ncp("within the last %1 days", "day", "days", value)); ui.comboBox_ValueDateUnit->setItemText(1, i18ncp("within the last %1 months", "month", "months", value)); ui.comboBox_ValueDateUnit->setItemText(2, i18ncp("within the last %1 years", "year", "years", value)); } diff --git a/src/playlistgenerator/constraints/TagMatchSupport.cpp b/src/playlistgenerator/constraints/TagMatchSupport.cpp index dbb96989bd..8383e90564 100644 --- a/src/playlistgenerator/constraints/TagMatchSupport.cpp +++ b/src/playlistgenerator/constraints/TagMatchSupport.cpp @@ -1,337 +1,337 @@ /**************************************************************************************** * Copyright (c) 2010-2012 Soren Harward * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "Constraint::TagMatchSupport" #include "TagMatch.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaConstants.h" #include ConstraintTypes::TagMatchFieldsModel::TagMatchFieldsModel() { m_fieldNames << QStringLiteral("url") << QStringLiteral("title") << QStringLiteral("artist name") << QStringLiteral("album name") << QStringLiteral("genre") << QStringLiteral("composer") << QStringLiteral("year") << QStringLiteral("comment") << QStringLiteral("track number") << QStringLiteral("disc number") << QStringLiteral("length") << QStringLiteral("score") << QStringLiteral("rating") << QStringLiteral("create date") << QStringLiteral("first played") << QStringLiteral("last played") << QStringLiteral("play count") << QStringLiteral("label"); m_fieldTypes.insert( QStringLiteral("url"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("title"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("artist name"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("album name"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("genre"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("composer"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("year"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("comment"), TagMatch::FieldTypeString ); m_fieldTypes.insert( QStringLiteral("track number"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("disc number"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("length"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("create date"), TagMatch::FieldTypeDate); m_fieldTypes.insert( QStringLiteral("score"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("rating"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("first played"), TagMatch::FieldTypeDate ); m_fieldTypes.insert( QStringLiteral("last played"), TagMatch::FieldTypeDate ); m_fieldTypes.insert( QStringLiteral("play count"), TagMatch::FieldTypeInt ); m_fieldTypes.insert( QStringLiteral("label"), TagMatch::FieldTypeString ); m_fieldMetaValues.insert( QStringLiteral("url"), Meta::valUrl ); m_fieldMetaValues.insert( QStringLiteral("title"), Meta::valTitle ); m_fieldMetaValues.insert( QStringLiteral("artist name"), Meta::valArtist ); m_fieldMetaValues.insert( QStringLiteral("album name"), Meta::valAlbum ); m_fieldMetaValues.insert( QStringLiteral("genre"), Meta::valGenre ); m_fieldMetaValues.insert( QStringLiteral("composer"), Meta::valComposer ); m_fieldMetaValues.insert( QStringLiteral("year"), Meta::valYear ); m_fieldMetaValues.insert( QStringLiteral("comment"), Meta::valComment ); m_fieldMetaValues.insert( QStringLiteral("track number"), Meta::valTrackNr ); m_fieldMetaValues.insert( QStringLiteral("disc number"), Meta::valDiscNr ); m_fieldMetaValues.insert( QStringLiteral("length"), Meta::valLength ); m_fieldMetaValues.insert( QStringLiteral("create date"), Meta::valCreateDate); m_fieldMetaValues.insert( QStringLiteral("score"), Meta::valScore ); m_fieldMetaValues.insert( QStringLiteral("rating"), Meta::valRating ); m_fieldMetaValues.insert( QStringLiteral("first played"), Meta::valFirstPlayed ); m_fieldMetaValues.insert( QStringLiteral("last played"), Meta::valLastPlayed ); m_fieldMetaValues.insert( QStringLiteral("play count"), Meta::valPlaycount ); m_fieldMetaValues.insert( QStringLiteral("label"), Meta::valLabel ); m_fieldPrettyNames.insert( QStringLiteral("url"), i18n("url") ); m_fieldPrettyNames.insert( QStringLiteral("title"), i18n("title") ); m_fieldPrettyNames.insert( QStringLiteral("artist name"), i18n("artist name") ); m_fieldPrettyNames.insert( QStringLiteral("album name"), i18n("album name") ); m_fieldPrettyNames.insert( QStringLiteral("genre"), i18n("genre") ); m_fieldPrettyNames.insert( QStringLiteral("composer"), i18n("composer") ); m_fieldPrettyNames.insert( QStringLiteral("year"), i18n("year") ); m_fieldPrettyNames.insert( QStringLiteral("comment"), i18n("comment") ); m_fieldPrettyNames.insert( QStringLiteral("track number"), i18n("track number") ); m_fieldPrettyNames.insert( QStringLiteral("disc number"), i18n("disc number") ); m_fieldPrettyNames.insert( QStringLiteral("length"), i18n("length") ); m_fieldPrettyNames.insert( QStringLiteral("create date"), i18n("added to collection") ); m_fieldPrettyNames.insert( QStringLiteral("score"), i18n("score") ); m_fieldPrettyNames.insert( QStringLiteral("rating"), i18n("rating") ); m_fieldPrettyNames.insert( QStringLiteral("first played"), i18n("first played") ); m_fieldPrettyNames.insert( QStringLiteral("last played"), i18n("last played") ); m_fieldPrettyNames.insert( QStringLiteral("play count"), i18n("play count") ); m_fieldPrettyNames.insert( QStringLiteral("label"), i18n("label") ); } ConstraintTypes::TagMatchFieldsModel::~TagMatchFieldsModel() { } int ConstraintTypes::TagMatchFieldsModel::rowCount( const QModelIndex& parent ) const { Q_UNUSED( parent ) return m_fieldNames.length(); } QVariant ConstraintTypes::TagMatchFieldsModel::data( const QModelIndex& idx, int role ) const { QString s = m_fieldNames.at( idx.row() ); switch ( role ) { case Qt::DisplayRole: case Qt::EditRole: return QVariant( m_fieldPrettyNames.value( s ) ); break; default: return QVariant(); } return QVariant(); } bool ConstraintTypes::TagMatchFieldsModel::contains( const QString& s ) const { return m_fieldNames.contains( s ); } int ConstraintTypes::TagMatchFieldsModel::index_of( const QString& s ) const { return m_fieldNames.indexOf( s ); } QString ConstraintTypes::TagMatchFieldsModel::field_at( int idx ) const { if ( ( idx >= 0 ) && ( idx < m_fieldNames.length() ) ) return m_fieldNames.at( idx ); else return QString(); } qint64 ConstraintTypes::TagMatchFieldsModel::meta_value_of( const QString& f ) const { return m_fieldMetaValues.value( f ); } QString ConstraintTypes::TagMatchFieldsModel::pretty_name_of( const QString& f ) const { return m_fieldPrettyNames.value( f ); } ConstraintTypes::TagMatch::FieldTypes ConstraintTypes::TagMatchFieldsModel::type_of( const QString& f ) const { return m_fieldTypes.value( f ); } /************************************* **************************************/ ConstraintTypes::TagMatch::Comparer::Comparer() : m_dateWeight( 1209600.0 ) { m_numFieldWeight.insert( Meta::valYear, 8.0 ); m_numFieldWeight.insert( Meta::valTrackNr, 5.0 ); m_numFieldWeight.insert( Meta::valDiscNr, 0.75 ); m_numFieldWeight.insert( Meta::valLength, 100000.0 ); m_numFieldWeight.insert( Meta::valScore, 20.0 ); m_numFieldWeight.insert( Meta::valRating, 3.0 ); m_numFieldWeight.insert( Meta::valPlaycount, 4.0 ); } ConstraintTypes::TagMatch::Comparer::~Comparer() { } double ConstraintTypes::TagMatch::Comparer::compareNum( const double test, const int comparison, const double target, const double strictness, const qint64 field ) const { const double weight = m_numFieldWeight.value( field ); if ( comparison == CompareNumEquals ) { // fuzzy equals -- within 1%, or within 0.001 if ( ( abs( test - target ) < ( abs( test + target ) / 200.0 ) ) || ( abs( test - target ) < 0.001 ) ) { return 1.0; } else { return fuzzyProb( test, target, strictness, weight ); } } else if ( comparison == CompareNumGreaterThan ) { return ( test > target ) ? 1.0 : fuzzyProb( test, target, strictness, weight ); } else if ( comparison == CompareNumLessThan ) { return ( test < target ) ? 1.0 : fuzzyProb( test, target, strictness, weight ); } else { return 0.0; } return 0.0; } double ConstraintTypes::TagMatch::Comparer::compareStr( const QString& test, const int comparison, const QString& target ) const { if ( comparison == CompareStrEquals ) { if ( test.compare( target, Qt::CaseInsensitive ) == 0 ) return 1.0; } else if ( comparison == CompareStrStartsWith ) { if ( test.startsWith( target, Qt::CaseInsensitive ) ) return 1.0; } else if ( comparison == CompareStrEndsWith ) { if ( test.endsWith( target, Qt::CaseInsensitive ) ) return 1.0; } else if ( comparison == CompareStrContains ) { if ( test.contains( target, Qt::CaseInsensitive ) ) return 1.0; } else if ( comparison == CompareStrRegExp ) { QRegExp rx( target ); if ( rx.indexIn( test ) >= 0 ) return 1.0; } else { return 0.0; } return 0.0; } double ConstraintTypes::TagMatch::Comparer::compareDate( const uint test, const int comparison, const QVariant& targetVar, const double strictness ) const { const double weight = m_dateWeight; int comp = comparison; uint target = 0; if ( comparison == CompareDateWithin ) { comp = CompareDateAfter; QDateTime now = QDateTime::currentDateTime(); DateRange r = targetVar.value(); switch ( r.second ) { case 0: - target = now.addDays( -1 * r.first ).toTime_t(); + target = now.addDays( -1 * r.first ).toSecsSinceEpoch(); break; case 1: - target = now.addMonths( -1 * r.first ).toTime_t(); + target = now.addMonths( -1 * r.first ).toSecsSinceEpoch(); break; case 2: - target = now.addYears( -1 * r.first ).toTime_t(); + target = now.addYears( -1 * r.first ).toSecsSinceEpoch(); break; default: break; } } else { target = targetVar.value(); } const double dte = static_cast( test ); const double dta = static_cast( target ); if ( comp == CompareDateOn ) { // fuzzy equals -- within 1%, or within 10.0 if ( ( abs( dte - dta ) < ( abs( dte + dta ) / 200.0 ) ) || ( abs( dte - dta ) < 10.0 ) ) { return 1.0; } else { return fuzzyProb( dte, dta, strictness, weight ); } } else if ( comp == CompareDateAfter ) { return ( test > target ) ? 1.0 : fuzzyProb( dte, dta, strictness, weight ); } else if ( comp == CompareDateBefore ) { return ( test < target ) ? 1.0 : fuzzyProb( dte, dta, strictness, weight ); } else { return 0.0; } return 0.0; } double ConstraintTypes::TagMatch::Comparer::compareLabels( const Meta::TrackPtr &t, const int comparison, const QString& target ) const { Meta::LabelList labelList = t->labels(); double v = 0.0; foreach ( Meta::LabelPtr label, labelList ) { // this is technically more correct ... // v = qMax( compare( label->prettyName(), comparison, target ), v ); // ... but as long as compareStr() returns only 0.0 or 1.0, the following is faster: v = compareStr( label->prettyName(), comparison, target ); if ( v > 0.99 ) { return 1.0; } } return v; } uint ConstraintTypes::TagMatch::Comparer::rangeDate( const double strictness ) const { if ( strictness > 0.99 ) return 0; const double s = strictness * strictness; return static_cast( ceil( 0.460517 * m_dateWeight / ( 0.1 + s ) ) ); } int ConstraintTypes::TagMatch::Comparer::rangeNum( const double strictness, const qint64 field ) const { if ( strictness > 0.99 ) return 0; const double s = strictness * strictness; const double w = m_numFieldWeight.value( field ); return static_cast( ceil( 0.460517 * w / ( 0.1 + s ) ) ); } double ConstraintTypes::TagMatch::Comparer::fuzzyProb( const double a, const double b, const double strictness, const double w ) const { const double s = strictness * strictness; return exp( -10.0 * ( 0.1 + s ) / w * ( 1 + abs( a - b ) ) ); } diff --git a/src/scripting/scriptmanager/ScriptManager.cpp b/src/scripting/scriptmanager/ScriptManager.cpp index e7d1b829d4..f612364864 100644 --- a/src/scripting/scriptmanager/ScriptManager.cpp +++ b/src/scripting/scriptmanager/ScriptManager.cpp @@ -1,415 +1,415 @@ /**************************************************************************************** * Copyright (c) 2004-2010 Mark Kretschmann * * Copyright (c) 2005-2007 Seb Ruiz * * Copyright (c) 2006 Alexandre Pereira de Oliveira * * Copyright (c) 2006 Martin Ellis * * Copyright (c) 2007 Leo Franchi * * Copyright (c) 2008 Peter ZHOU * * Copyright (c) 2009 Jakob Kummerow * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "ScriptManager" #include "ScriptManager.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "core/support/Components.h" #include "core/logger/Logger.h" #include "MainWindow.h" #include "amarokconfig.h" #include // for the compile flags #include "services/scriptable/ScriptableServiceManager.h" #include "ScriptItem.h" #include "ScriptUpdater.h" #include #include #include #include #include #include #include #include ScriptManager* ScriptManager::s_instance = nullptr; ScriptManager::ScriptManager( QObject* parent ) : QObject( parent ) { DEBUG_BLOCK setObjectName( "ScriptManager" ); s_instance = this; if( AmarokConfig::enableScripts() == false ) { AmarokConfig::setEnableScripts( true ); } // Delay this call via eventloop, because it's a bit slow and would block QTimer::singleShot( 0, this, &ScriptManager::updateAllScripts ); } ScriptManager::~ScriptManager() {} void ScriptManager::destroy() { if (s_instance) { delete s_instance; s_instance = nullptr; } } ScriptManager* ScriptManager::instance() { return s_instance ? s_instance : new ScriptManager( The::mainWindow() ); } //////////////////////////////////////////////////////////////////////////////// // public //////////////////////////////////////////////////////////////////////////////// bool ScriptManager::runScript( const QString& name, bool silent ) { if( !m_scripts.contains( name ) ) return false; return slotRunScript( name, silent ); } bool ScriptManager::stopScript( const QString& name ) { if( name.isEmpty() ) return false; if( !m_scripts.contains( name ) ) return false; m_scripts[name]->stop(); return true; } QStringList ScriptManager::listRunningScripts() const { QStringList runningScripts; foreach( const ScriptItem *item, m_scripts ) { if( item->running() ) runningScripts << item->info().pluginName(); } return runningScripts; } QString ScriptManager::specForScript( const QString& name ) const { if( !m_scripts.contains( name ) ) return QString(); return m_scripts[name]->specPath(); } bool ScriptManager::lyricsScriptRunning() const { return !m_lyricsScript.isEmpty(); } void ScriptManager::notifyFetchLyrics( const QString& artist, const QString& title, const QString& url, const Meta::TrackPtr &track ) { DEBUG_BLOCK Q_EMIT fetchLyrics( artist, title, url, track ); } //////////////////////////////////////////////////////////////////////////////// // private slots (script updater stuff) //////////////////////////////////////////////////////////////////////////////// void ScriptManager::updateAllScripts() // SLOT { DEBUG_BLOCK // find all scripts (both in $KDEHOME and /usr) QStringList foundScripts; QStringList locations = QStandardPaths::standardLocations( QStandardPaths::GenericDataLocation ); for( const auto &location : locations ) { QDir dir( location + "/amarok/scripts" ); if( !dir.exists() ) continue; for( const auto &scriptLocation : dir.entryList( QDir::NoDotAndDotDot | QDir::Dirs ) ) { QDir scriptDir( dir.absoluteFilePath( scriptLocation ) ); if( scriptDir.exists( QStringLiteral( "main.js" ) ) ) foundScripts << scriptDir.absoluteFilePath( QStringLiteral( "main.js" ) ); } } // remove deleted scripts foreach( ScriptItem *item, m_scripts ) { const QString specPath = QString( "%1/script.spec" ).arg( QFileInfo( item->url().path() ).path() ); if( !QFile::exists( specPath ) ) { debug() << "Removing script " << item->info().pluginName(); item->uninstall(); m_scripts.remove( item->info().pluginName() ); } } m_nScripts = foundScripts.count(); // get timestamp of the last update check KConfigGroup config = Amarok::config( "ScriptManager" ); const uint lastCheck = config.readEntry( "LastUpdateCheck", QVariant( 0 ) ).toUInt(); - const uint now = QDateTime::currentDateTimeUtc().toTime_t(); + const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); bool autoUpdateScripts = AmarokConfig::autoUpdateScripts(); // note: we can't update scripts without the QtCryptoArchitecture, so don't even try #ifndef QCA2_FOUND autoUpdateScripts = false; #endif // last update was at least 7 days ago -> check now if auto update is enabled if( autoUpdateScripts && (now - lastCheck > 7*24*60*60) ) { debug() << "ScriptUpdater: Performing script update check now!"; for( int i = 0; i < m_nScripts; ++i ) { ScriptUpdater *updater = new ScriptUpdater( this ); // all the ScriptUpdaters are now started in parallel. // tell them which script to work on updater->setScriptPath( foundScripts.at( i ) ); // tell them whom to signal when they're finished connect( updater, &ScriptUpdater::finished, this, &ScriptManager::updaterFinished ); // and finally tell them to get to work QTimer::singleShot( 0, updater, &ScriptUpdater::updateScript ); } // store current timestamp config.writeEntry( "LastUpdateCheck", QVariant( now ) ); config.sync(); } // last update was pretty recent, don't check again else { debug() << "ScriptUpdater: Skipping update check"; for ( int i = 0; i < m_nScripts; i++ ) { loadScript( foundScripts.at( i ) ); } configChanged( true ); } } void ScriptManager::updaterFinished( const QString &scriptPath ) // SLOT { DEBUG_BLOCK // count this event m_updateSemaphore.release(); loadScript( scriptPath ); if ( m_updateSemaphore.tryAcquire(m_nScripts) ) { configChanged( true ); } sender()->deleteLater(); } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// bool ScriptManager::slotRunScript( const QString &name, bool silent ) { ScriptItem *item = m_scripts.value( name ); connect( item, &ScriptItem::signalHandlerException, this, &ScriptManager::handleException ); if( item->info().category() == "Lyrics" ) { m_lyricsScript = name; debug() << "lyrics script started:" << name; Q_EMIT lyricsScriptStarted(); } return item->start( silent ); } void ScriptManager::handleException(const QScriptValue& value) { DEBUG_BLOCK QScriptEngine *engine = value.engine(); if (!engine) return; Amarok::Logger::longMessage( i18n( "Script error reported by: %1\n%2", scriptNameForEngine( engine ), value.toString() ), Amarok::Logger::Error ); } void ScriptManager::ServiceScriptPopulate( const QString &name, int level, int parent_id, const QString &path, const QString &filter ) { if( m_scripts.value( name )->service() ) m_scripts.value( name )->service()->slotPopulate( name, level, parent_id, path, filter ); } void ScriptManager::ServiceScriptCustomize( const QString &name ) { if( m_scripts.value( name )->service() ) m_scripts.value( name )->service()->slotCustomize( name ); } void ScriptManager::ServiceScriptRequestInfo( const QString &name, int level, const QString &callbackString ) { if( m_scripts.value( name )->service() ) m_scripts.value( name )->service()->slotRequestInfo( name, level, callbackString ); } void ScriptManager::configChanged( bool changed ) { Q_EMIT scriptsChanged(); if( !changed ) return; //evil scripts may prevent the config dialog from dismissing, delay execution QTimer::singleShot( 0, this, &ScriptManager::slotConfigChanged ); } //////////////////////////////////////////////////////////////////////////////// // private //////////////////////////////////////////////////////////////////////////////// void ScriptManager::slotConfigChanged() { foreach( ScriptItem *item, m_scripts ) { const QString name = item->info().pluginName(); bool enabledByDefault = item->info().isPluginEnabledByDefault(); bool enabled = Amarok::config( "Plugins" ).readEntry( name + "Enabled", enabledByDefault ); if( !item->running() && enabled ) { slotRunScript( name ); } else if( item->running() && !enabled ) { item->stop(); } } } bool ScriptManager::loadScript( const QString& path ) { if( path.isEmpty() ) return false; QStringList SupportAPIVersion; SupportAPIVersion << QLatin1String("API V1.0.0") << QLatin1String("API V1.0.1"); QString ScriptVersion; QFileInfo info( path ); const QString jsonPath = QString( "%1/script.json" ).arg( info.path() ); if( !QFile::exists( jsonPath ) ) { error() << "script.json for "<< path << " is missing!"; return false; } KPluginMetaData pluginMetadata( jsonPath ); if( !pluginMetadata.isValid() ) { error() << "PluginMetaData invalid for" << jsonPath; return false; } const QString pluginName = pluginMetadata.pluginId(); const QString category = pluginMetadata.category(); const QString version = pluginMetadata.version(); if( pluginName.isEmpty() || category.isEmpty() || version.isEmpty() ) { error() << "PluginMetaData has empty values for" << jsonPath; return false; } KPluginInfo pluginInfo( pluginMetadata ); ScriptItem *item; if( !m_scripts.contains( pluginName ) ) { item = new ScriptItem( this, pluginName, path, pluginInfo ); m_scripts[ pluginName ] = item; } else if( m_scripts[pluginName]->info().version() < pluginInfo.version() ) { m_scripts[ pluginName ]->deleteLater(); item = new ScriptItem( this, pluginName, path, pluginInfo ); m_scripts[ pluginName ] = item; } else item = m_scripts.value( pluginName ); //assume it is API V1.0.0 if there is no "API V" prefix found if( !item->info().dependencies().at(0).startsWith("API V") ) ScriptVersion = QLatin1String("API V1.0.0"); else ScriptVersion = item->info().dependencies().at(0); if( !SupportAPIVersion.contains( ScriptVersion ) ) { warning() << "script API version not compatible with Amarok."; return false; } debug() << "found script:" << category << pluginName << version << item->info().dependencies(); return true; } KPluginInfo::List ScriptManager::scripts( const QString &category ) const { KPluginInfo::List scripts; foreach( const ScriptItem *script, m_scripts ) { if( script->info().category() == category ) scripts << script->info(); } return scripts; } QString ScriptManager::scriptNameForEngine( const QScriptEngine *engine ) const { foreach( const QString &name, m_scripts.keys() ) { ScriptItem *script = m_scripts[name]; if( script->engine() == engine ) return name; } return QString(); } diff --git a/src/services/ampache/AmpacheServiceQueryMaker.cpp b/src/services/ampache/AmpacheServiceQueryMaker.cpp index 943dd2af6e..44efaa296a 100644 --- a/src/services/ampache/AmpacheServiceQueryMaker.cpp +++ b/src/services/ampache/AmpacheServiceQueryMaker.cpp @@ -1,751 +1,751 @@ /**************************************************************************************** * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2007 Adam Pigg * * Copyright (c) 2007 Casey Link * * (c) 2013 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "AmpacheServiceQueryMaker" #include "AmpacheServiceQueryMaker.h" #include "AmpacheMeta.h" #include "core/meta/Statistics.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "core/meta/support/MetaConstants.h" #include "core-impl/collections/support/MemoryMatcher.h" #include #include #include #include using namespace Collections; struct AmpacheServiceQueryMaker::Private { AmpacheServiceCollection* collection; QueryMaker::QueryType type; int maxsize; QAtomicInt expectedReplies; QUrl server; QString sessionId; QList parentTrackIds; QList parentAlbumIds; QList parentArtistIds; uint dateFilter; QString artistFilter; QString albumFilter; /** We are collecting the results of the queries and submit them in one block to ensure that we don't report albums twice and because the CollectionTreeItemModelBase does not handle multiple results correctly (which it should). */ Meta::AlbumList albumResults; Meta::ArtistList artistResults; Meta::TrackList trackResults; }; AmpacheServiceQueryMaker::AmpacheServiceQueryMaker( AmpacheServiceCollection * collection, const QUrl &server, const QString &sessionId ) : DynamicServiceQueryMaker() , d( new Private ) { d->collection = collection; d->type = QueryMaker::None; d->maxsize = 0; d->server = server; d->sessionId = sessionId; d->dateFilter = 0; } AmpacheServiceQueryMaker::~AmpacheServiceQueryMaker() { delete d; } void AmpacheServiceQueryMaker::run() { DEBUG_BLOCK if( d->expectedReplies ) // still running an old query return; //naive implementation, fix this //note: we are not handling filtering yet d->collection->acquireReadLock(); if ( d->type == QueryMaker::Artist ) fetchArtists(); else if( d->type == QueryMaker::Album ) fetchAlbums(); else if( d->type == QueryMaker::Track ) fetchTracks(); else warning() << "Requested unhandled query type"; //TODO error handling d->collection->releaseLock(); } void AmpacheServiceQueryMaker::abortQuery() { } QueryMaker * AmpacheServiceQueryMaker::setQueryType( QueryType type ) { d->type = type; return this; } QueryMaker* AmpacheServiceQueryMaker::addMatch( const Meta::TrackPtr &track ) { DEBUG_BLOCK const Meta::AmpacheTrack* serviceTrack = dynamic_cast< const Meta::AmpacheTrack * >( track.data() ); if( serviceTrack ) { d->parentTrackIds << serviceTrack->id(); debug() << "parent id set to: " << d->parentTrackIds; } else { // searching for something from another collection //hmm, not sure what to do now } return this; } QueryMaker* AmpacheServiceQueryMaker::addMatch( const Meta::ArtistPtr &artist, ArtistMatchBehaviour behaviour ) { Q_UNUSED( behaviour ) // TODO DEBUG_BLOCK if( d->parentAlbumIds.isEmpty() ) { const Meta::AmpacheArtist* serviceArtist = dynamic_cast< const Meta::AmpacheArtist * >( artist.data() ); if( serviceArtist ) { d->parentArtistIds << serviceArtist->id(); } else { // searching for something from another collection if( d->collection->artistMap().contains( artist->name() ) ) { serviceArtist = static_cast< const Meta::AmpacheArtist* >( d->collection->artistMap().value( artist->name() ).data() ); d->parentArtistIds << serviceArtist->id(); } else { //hmm, not sure what to do now } } } return this; } QueryMaker * AmpacheServiceQueryMaker::addMatch( const Meta::AlbumPtr & album ) { DEBUG_BLOCK const Meta::AmpacheAlbum* serviceAlbum = dynamic_cast< const Meta::AmpacheAlbum * >( album.data() ); if( serviceAlbum ) { d->parentAlbumIds << serviceAlbum->ids(); debug() << "parent id set to: " << d->parentAlbumIds; d->parentArtistIds.clear(); } else { // searching for something from another collection if( d->collection->albumMap().contains( album ) ) // compares albums by value { serviceAlbum = static_cast< const Meta::AmpacheAlbum* >( d->collection->albumMap().value( album ).data() ); d->parentAlbumIds << serviceAlbum->ids(); d->parentArtistIds.clear(); } else { //hmm, not sure what to do now } } return this; } void AmpacheServiceQueryMaker::fetchArtists() { DEBUG_BLOCK Meta::ArtistList artists; // first try the cache if( !d->parentArtistIds.isEmpty() ) { for( int artistId : qAsConst(d->parentArtistIds) ) artists << d->collection->artistById( artistId ); } if( !artists.isEmpty() ) { debug() << "got" << artists.count() << "artists from the memory collection"; Q_EMIT newArtistsReady( artists ); Q_EMIT queryDone(); return; } QUrl request = getRequestUrl( "artists" ); QUrlQuery query( request ); if ( !d->artistFilter.isEmpty() ) { query.addQueryItem( "filter", d->artistFilter ); request.setQuery( query ); } d->expectedReplies.ref(); The::networkAccessManager()->getData( request, this, &AmpacheServiceQueryMaker::artistDownloadComplete ); } void AmpacheServiceQueryMaker::fetchAlbums() { DEBUG_BLOCK Meta::AlbumList albums; // first try the cache if( !d->parentArtistIds.isEmpty() ) { foreach( int artistId, d->parentArtistIds ) albums << matchAlbums( d->collection, d->collection->artistById( artistId ) ); } if( !albums.isEmpty() ) { debug() << "got" << albums.count() << "albums from the memory collection"; Q_EMIT newAlbumsReady( albums ); Q_EMIT queryDone(); return; } if( !d->parentArtistIds.isEmpty() ) { foreach( int id, d->parentArtistIds ) { QUrl request = getRequestUrl( "artist_albums" ); QUrlQuery query( request ); query.addQueryItem( "filter", QString::number( id ) ); request.setQuery( query ); d->expectedReplies.ref(); The::networkAccessManager()->getData( request, this, &AmpacheServiceQueryMaker::albumDownloadComplete ); } } else { QUrl request = getRequestUrl( "albums" ); QUrlQuery query( request ); if ( !d->albumFilter.isEmpty() ) { query.addQueryItem( "filter", d->albumFilter ); request.setQuery( query ); } d->expectedReplies.ref(); The::networkAccessManager()->getData( request, this, &AmpacheServiceQueryMaker::albumDownloadComplete ); } } void AmpacheServiceQueryMaker::fetchTracks() { DEBUG_BLOCK Meta::TrackList tracks; //debug() << "parent album id: " << d->parentAlbumId; // first try the cache // TODO: this is fishy as we cannot be sure that the cache contains // everything // we should cache database query results instead if( !d->parentTrackIds.isEmpty() ) { foreach( int trackId, d->parentTrackIds ) { tracks << d->collection->trackById( trackId ); } } else if( !d->parentAlbumIds.isEmpty() ) { foreach( int albumId, d->parentAlbumIds ) { AlbumMatcher albumMatcher( d->collection->albumById( albumId ) ); tracks << albumMatcher.match( d->collection->trackMap().values() ); } } else if( d->parentArtistIds.isEmpty() ) { foreach( int artistId, d->parentArtistIds ) { ArtistMatcher artistMatcher( d->collection->artistById( artistId ) ); tracks << artistMatcher.match( d->collection->trackMap().values() ); } } if( !tracks.isEmpty() ) { debug() << "got" << tracks.count() << "tracks from the memory collection"; Q_EMIT newTracksReady( tracks ); Q_EMIT queryDone(); return; } QUrl request = getRequestUrl(); if( !d->parentAlbumIds.isEmpty() ) { foreach( int id, d->parentAlbumIds ) { QUrl request = getRequestUrl( "album_songs" ); QUrlQuery query( request ); query.addQueryItem( "filter", QString::number( id ) ); request.setQuery( query ); d->expectedReplies.ref(); The::networkAccessManager()->getData( request, this, &AmpacheServiceQueryMaker::trackDownloadComplete ); } } else if( !d->parentArtistIds.isEmpty() ) { foreach( int id, d->parentArtistIds ) { QUrl request = getRequestUrl( "artist_songs" ); QUrlQuery query( request ); query.addQueryItem( "filter", QString::number( id ) ); request.setQuery( query ); d->expectedReplies.ref(); The::networkAccessManager()->getData( request, this, &AmpacheServiceQueryMaker::trackDownloadComplete ); } } else { QUrl request = getRequestUrl( "songs" ); d->expectedReplies.ref(); The::networkAccessManager()->getData( request, this, &AmpacheServiceQueryMaker::trackDownloadComplete ); } } void AmpacheServiceQueryMaker::artistDownloadComplete( const QUrl &url, const QByteArray &data, const NetworkAccessManagerProxy::Error &e ) { Q_UNUSED( url ); if( e.code != QNetworkReply::NoError ) { warning() << "Artist download error:" << e.description; if( !d->expectedReplies.deref() ) Q_EMIT queryDone(); return; } // DEBUG_BLOCK // so lets figure out what we got here: QDomDocument doc( "reply" ); doc.setContent( data ); QDomElement root = doc.firstChildElement( "root" ); // Is this an error, if so we need to 'un-ready' the service and re-authenticate before continuing QDomElement domError = root.firstChildElement( "error" ); if ( !domError.isNull() ) { warning() << "Error getting Artist List" << domError.text() << "Code:" << domError.attribute("code"); AmpacheService *parentService = dynamic_cast< AmpacheService * >( d->collection->service() ); if( !parentService ) return; else parentService->reauthenticate(); } for( QDomNode n = root.firstChild(); !n.isNull(); n = n.nextSibling() ) { QDomElement e = n.toElement(); // try to convert the node to an element. QDomElement element = n.firstChildElement( "name" ); int artistId = e.attribute( "id", "0").toInt(); // check if we have the artist already Meta::ArtistPtr artistPtr = d->collection->artistById( artistId ); if( !artistPtr ) { // new artist Meta::ServiceArtist* artist = new Meta::AmpacheArtist( element.text(), d->collection->service() ); artist->setId( artistId ); // debug() << "Adding artist: " << element.text() << " with id: " << artistId; artistPtr = artist; d->collection->acquireWriteLock(); d->collection->addArtist( artistPtr ); d->collection->releaseLock(); } if( !d->artistResults.contains( artistPtr ) ) d->artistResults.push_back( artistPtr ); } if( !d->expectedReplies.deref() ) { Q_EMIT newArtistsReady( d->artistResults ); Q_EMIT queryDone(); d->artistResults.clear(); } } void AmpacheServiceQueryMaker::albumDownloadComplete( const QUrl &url, const QByteArray &data, const NetworkAccessManagerProxy::Error &e ) { Q_UNUSED( url ); if( e.code != QNetworkReply::NoError ) { warning() << "Album download error:" << e.description; if( !d->expectedReplies.deref() ) Q_EMIT queryDone(); return; } // DEBUG_BLOCK //so lets figure out what we got here: QDomDocument doc( "reply" ); doc.setContent( data ); QDomElement root = doc.firstChildElement( "root" ); // Is this an error, if so we need to 'un-ready' the service and re-authenticate before continuing QDomElement domError = root.firstChildElement( "error" ); if( !domError.isNull() ) { warning() << "Error getting Album List" << domError.text() << "Code:" << domError.attribute("code"); AmpacheService *parentService = dynamic_cast< AmpacheService * >(d->collection->service()); if( parentService == nullptr ) return; else parentService->reauthenticate(); } for( QDomNode n = root.firstChild(); !n.isNull(); n = n.nextSibling() ) { QDomElement e = n.toElement(); // try to convert the node to an element. // --- the album artist Meta::ArtistPtr artistPtr; QDomElement artistElement = n.firstChildElement( "artist" ); if( !artistElement.isNull() ) { int artistId = artistElement.attribute( "id", "0").toInt(); // check if we already know the artist artistPtr = d->collection->artistById( artistId ); if( !artistPtr.data() ) { // new artist. Meta::ServiceArtist* artist = new Meta::AmpacheArtist( artistElement.text(), d->collection->service() ); artistPtr = artist; artist->setId( artistId ); // debug() << "Adding artist: " << artistElement.text() << " with id: " << artistId; d->collection->acquireWriteLock(); d->collection->addArtist( artistPtr ); d->collection->releaseLock(); } } QDomElement element = n.firstChildElement( "name" ); QString title = element.text(); Meta::AmpacheAlbum::AmpacheAlbumInfo info; info.id = e.attribute( "id", "0" ).toInt(); element = n.firstChildElement( "disk" ); info.discNumber = element.text().toInt(); element = n.firstChildElement( "year" ); info.year = element.text().toInt(); // check if we have the album already Meta::AlbumPtr albumPtr = d->collection->albumById( info.id ); if( !albumPtr ) { // check if we at least have an album with the same title and artist Meta::AmpacheAlbum* album = static_cast( const_cast( d->collection->albumMap().value( title, artistPtr ? artistPtr->name() : QString() ).data() ) ); if( !album ) { // new album album = new Meta::AmpacheAlbum( title ); album->setAlbumArtist( artistPtr ); // -- cover element = n.firstChildElement( "art" ); QString coverUrl = element.text(); album->setCoverUrl( coverUrl ); } album->addInfo( info ); // debug() << "Adding album" << title << "with id:" << info.id; albumPtr = album; // register a new id with the ServiceCollection album->setId( info.id ); d->collection->acquireWriteLock(); d->collection->addAlbum( albumPtr ); d->collection->releaseLock(); } if( !d->albumResults.contains( albumPtr ) ) d->albumResults.push_back( albumPtr ); } if( !d->expectedReplies.deref() ) { Q_EMIT newAlbumsReady( d->albumResults ); Q_EMIT queryDone(); d->albumResults.clear(); } } void AmpacheServiceQueryMaker::trackDownloadComplete( const QUrl &url, const QByteArray &data, const NetworkAccessManagerProxy::Error &e ) { Q_UNUSED( url ); if( e.code != QNetworkReply::NoError ) { warning() << "Track download error:" << e.description; if( !d->expectedReplies.deref() ) Q_EMIT queryDone(); return; } // DEBUG_BLOCK //so lets figure out what we got here: QDomDocument doc( "reply" ); doc.setContent( data ); QDomElement root = doc.firstChildElement( "root" ); // Is this an error, if so we need to 'un-ready' the service and re-authenticate before continuing QDomElement domError = root.firstChildElement( "error" ); if( !domError.isNull() ) { warning() << "Error getting Track Download " << domError.text() << "Code:" << domError.attribute("code"); AmpacheService *parentService = dynamic_cast< AmpacheService * >( d->collection->service() ); if( parentService == nullptr ) return; else parentService->reauthenticate(); } for( QDomNode n = root.firstChild(); !n.isNull(); n = n.nextSibling() ) { QDomElement e = n.toElement(); // try to convert the node to an element. int trackId = e.attribute( "id", "0" ).toInt(); Meta::TrackPtr trackPtr = d->collection->trackById( trackId ); if( !trackPtr ) { // new track QDomElement element = n.firstChildElement( "title" ); QString title = element.text(); Meta::AmpacheTrack * track = new Meta::AmpacheTrack( title, d->collection->service() ); trackPtr = track; track->setId( trackId ); element = n.firstChildElement( "url" ); track->setUidUrl( element.text() ); element = n.firstChildElement( "time" ); track->setLength( element.text().toInt() * 1000 ); element = n.firstChildElement( "track" ); track->setTrackNumber( element.text().toInt() ); element = n.firstChildElement( "rating" ); track->statistics()->setRating( element.text().toDouble() * 2.0 ); QDomElement albumElement = n.firstChildElement( "album" ); int albumId = albumElement.attribute( "id", "0").toInt(); QDomElement artistElement = n.firstChildElement( "artist" ); int artistId = artistElement.attribute( "id", "0").toInt(); Meta::ArtistPtr artistPtr = d->collection->artistById( artistId ); // TODO: this assumes that we query all artist before tracks if( artistPtr ) { // debug() << "Found parent artist " << artistPtr->name(); Meta::ServiceArtist *artist = dynamic_cast< Meta::ServiceArtist * > ( artistPtr.data() ); track->setArtist( artistPtr ); artist->addTrack( trackPtr ); } Meta::AlbumPtr albumPtr = d->collection->albumById( albumId ); // TODO: this assumes that we query all albums before tracks if( albumPtr ) { // debug() << "Found parent album " << albumPtr->name() << albumId; Meta::AmpacheAlbum *album = dynamic_cast< Meta::AmpacheAlbum * > ( albumPtr.data() ); track->setDiscNumber( album->getInfo( albumId ).discNumber ); track->setYear( album->getInfo( albumId ).year ); track->setAlbumPtr( albumPtr ); // debug() << " parent album with"<discNumber()<year(); album->addTrack( trackPtr ); } // debug() << "Adding track: " << title << " with id: " << trackId; d->collection->acquireWriteLock(); d->collection->addTrack( trackPtr ); d->collection->releaseLock(); } if( !d->trackResults.contains( trackPtr ) ) d->trackResults.push_back( trackPtr ); } if( !d->expectedReplies.deref() ) { Q_EMIT newTracksReady( d->trackResults ); Q_EMIT queryDone(); d->trackResults.clear(); } } QueryMaker * AmpacheServiceQueryMaker::addFilter( qint64 value, const QString & filter, bool matchBegin, bool matchEnd ) { Q_UNUSED( matchBegin ) Q_UNUSED( matchEnd ) //for now, only accept artist filters // TODO: What about albumArtist? if( value == Meta::valArtist ) { d->artistFilter = filter; } else if( value == Meta::valAlbum ) { d->albumFilter = filter; } else { warning() << "unsupported filter" << Meta::nameForField( value ); } return this; } QueryMaker* AmpacheServiceQueryMaker::addNumberFilter( qint64 value, qint64 filter, QueryMaker::NumberComparison compare ) { if( value == Meta::valCreateDate && compare == QueryMaker::GreaterThan ) { debug() << "asking to filter based on added date"; d->dateFilter = filter; debug() << "setting dateFilter to:" << d->dateFilter; } else { warning() << "unsupported filter" << Meta::nameForField( value ); } return this; } int AmpacheServiceQueryMaker::validFilterMask() { //we only support artist and album filters for now... return ArtistFilter | AlbumFilter; } QueryMaker * AmpacheServiceQueryMaker::limitMaxResultSize( int size ) { d->maxsize = size; return this; } QUrl AmpacheServiceQueryMaker::getRequestUrl( const QString &action ) const { QUrl url = d->server; QString scheme = url.scheme(); if( scheme != "http" && scheme != "https" ) url.setScheme( "http" ); QUrlQuery query( url ); url = url.adjusted( QUrl::StripTrailingSlash ); url.setPath( url.path() + "/server/xml.server.php" ); query.addQueryItem( "auth", d->sessionId ); if( !action.isEmpty() ) query.addQueryItem( "action", action ); if( d->dateFilter > 0 ) { QDateTime from; - from.setTime_t( d->dateFilter ); + from.setSecsSinceEpoch( d->dateFilter ); query.addQueryItem( "add", from.toString( Qt::ISODate ) ); } query.addQueryItem( "limit", QString::number( d->maxsize ) ); url.setQuery( query ); return url; } diff --git a/src/services/lastfm/biases/WeeklyTopBias.cpp b/src/services/lastfm/biases/WeeklyTopBias.cpp index da377e3eb9..c89f69130b 100644 --- a/src/services/lastfm/biases/WeeklyTopBias.cpp +++ b/src/services/lastfm/biases/WeeklyTopBias.cpp @@ -1,489 +1,489 @@ /**************************************************************************************** * Copyright (c) 2009 Leo Franchi * * Copyright (c) 2011 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "WeeklyTopBias.h" #include "core/meta/Meta.h" #include "core/support/Amarok.h" #include "core/support/Debug.h" #include "core-impl/collections/support/CollectionManager.h" #include #include #include #include #include #include #include #include #include #include QString Dynamic::WeeklyTopBiasFactory::i18nName() const { return i18nc("Name of the \"WeeklyTop\" bias", "Last.fm weekly top artist"); } QString Dynamic::WeeklyTopBiasFactory::name() const { return Dynamic::WeeklyTopBias::sName(); } QString Dynamic::WeeklyTopBiasFactory::i18nDescription() const { return i18nc("Description of the \"WeeklyTop\" bias", "The \"WeeklyTop\" bias adds tracks that are in the weekly top chart of Last.fm."); } Dynamic::BiasPtr Dynamic::WeeklyTopBiasFactory::createBias() { return Dynamic::BiasPtr( new Dynamic::WeeklyTopBias() ); } // ----- WeeklyTopBias -------- Dynamic::WeeklyTopBias::WeeklyTopBias() : SimpleMatchBias() , m_weeklyTimesJob( ) { m_range.from = QDateTime::currentDateTime(); m_range.to = QDateTime::currentDateTime(); loadFromFile(); } Dynamic::WeeklyTopBias::~WeeklyTopBias() { } void Dynamic::WeeklyTopBias::fromXml( QXmlStreamReader *reader ) { loadFromFile(); while (!reader->atEnd()) { reader->readNext(); if( reader->isStartElement() ) { QStringRef name = reader->name(); if( name == "from" ) - m_range.from = QDateTime::fromTime_t( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() ); + m_range.from = QDateTime::fromSecsSinceEpoch( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() ); else if( name == "to" ) - m_range.to = QDateTime::fromTime_t( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() ); + m_range.to = QDateTime::fromSecsSinceEpoch( reader->readElementText(QXmlStreamReader::SkipChildElements).toLong() ); else { debug()<<"Unexpected xml start element"<skipCurrentElement(); } } else if( reader->isEndElement() ) { break; } } } void Dynamic::WeeklyTopBias::toXml( QXmlStreamWriter *writer ) const { - writer->writeTextElement( "from", QString::number( m_range.from.toTime_t() ) ); - writer->writeTextElement( "to", QString::number( m_range.to.toTime_t() ) ); + writer->writeTextElement( "from", QString::number( m_range.from.toSecsSinceEpoch() ) ); + writer->writeTextElement( "to", QString::number( m_range.to.toSecsSinceEpoch() ) ); } QString Dynamic::WeeklyTopBias::sName() { return "lastfm_weeklytop"; } QString Dynamic::WeeklyTopBias::name() const { return Dynamic::WeeklyTopBias::sName(); } QString Dynamic::WeeklyTopBias::toString() const { return i18nc("WeeklyTopBias bias representation", "Tracks from the Last.fm top lists from %1 to %2", m_range.from.toString(), m_range.to.toString() ); } QWidget* Dynamic::WeeklyTopBias::widget( QWidget* parent ) { QWidget *widget = new QWidget( parent ); QVBoxLayout *layout = new QVBoxLayout( widget ); QLabel *label = new QLabel( i18nc( "in WeeklyTopBias. Label for the date widget", "from:" ) ); QDateTimeEdit *fromEdit = new QDateTimeEdit( QDate::currentDate().addDays( -7 ) ); - fromEdit->setMinimumDate( QDateTime::fromTime_t( 1111320001 ).date() ); // That's the first week in last fm + fromEdit->setMinimumDate( QDateTime::fromSecsSinceEpoch( 1111320001 ).date() ); // That's the first week in last fm fromEdit->setMaximumDate( QDate::currentDate() ); fromEdit->setCalendarPopup( true ); if( m_range.from.isValid() ) fromEdit->setDateTime( m_range.from ); connect( fromEdit, &QDateTimeEdit::dateTimeChanged, this, &WeeklyTopBias::fromDateChanged ); label->setBuddy( fromEdit ); layout->addWidget( label ); layout->addWidget( fromEdit ); label = new QLabel( i18nc( "in WeeklyTopBias. Label for the date widget", "to:" ) ); QDateTimeEdit *toEdit = new QDateTimeEdit( QDate::currentDate().addDays( -7 ) ); - toEdit->setMinimumDate( QDateTime::fromTime_t( 1111320001 ).date() ); // That's the first week in last fm + toEdit->setMinimumDate( QDateTime::fromSecsSinceEpoch( 1111320001 ).date() ); // That's the first week in last fm toEdit->setMaximumDate( QDate::currentDate() ); toEdit->setCalendarPopup( true ); if( m_range.to.isValid() ) toEdit->setDateTime( m_range.to ); connect( toEdit, &QDateTimeEdit::dateTimeChanged, this, &WeeklyTopBias::toDateChanged ); label->setBuddy( toEdit ); layout->addWidget( label ); layout->addWidget( toEdit ); return widget; } bool Dynamic::WeeklyTopBias::trackMatches( int position, const Meta::TrackList& playlist, int contextCount ) const { Q_UNUSED( contextCount ); if( position < 0 || position >= playlist.count()) return false; // - determine the current artist Meta::TrackPtr currentTrack = playlist[position-1]; Meta::ArtistPtr currentArtist = currentTrack->artist(); QString currentArtistName = currentArtist ? currentArtist->name() : QString(); // - collect all the artists QStringList artists; bool weeksMissing = false; - uint fromTime = m_range.from.toTime_t(); - uint toTime = m_range.to.toTime_t(); + uint fromTime = m_range.from.toSecsSinceEpoch(); + uint toTime = m_range.to.toSecsSinceEpoch(); uint lastWeekTime = 0; foreach( uint weekTime, m_weeklyFromTimes ) { if( weekTime > fromTime && weekTime < toTime && lastWeekTime ) { if( m_weeklyArtistMap.contains( lastWeekTime ) ) { artists.append( m_weeklyArtistMap.value( lastWeekTime ) ); // debug() << "found already-saved data for week:" << lastWeekTime << m_weeklyArtistMap.value( lastWeekTime ); } else { weeksMissing = true; } } lastWeekTime = weekTime; } if( weeksMissing ) warning() << "didn't have a cached suggestions for weeks:" << m_range.from << "to" << m_range.to; return artists.contains( currentArtistName ); } void Dynamic::WeeklyTopBias::newQuery() { DEBUG_BLOCK; // - check if we have week times if( m_weeklyFromTimes.isEmpty() ) { newWeeklyTimesQuery(); return; // not yet ready to do construct a query maker } // - collect all the artists QStringList artists; bool weeksMissing = false; - uint fromTime = m_range.from.toTime_t(); - uint toTime = m_range.to.toTime_t(); + uint fromTime = m_range.from.toSecsSinceEpoch(); + uint toTime = m_range.to.toSecsSinceEpoch(); uint lastWeekTime = 0; foreach( uint weekTime, m_weeklyFromTimes ) { if( weekTime > fromTime && weekTime < toTime && lastWeekTime ) { if( m_weeklyArtistMap.contains( lastWeekTime ) ) { artists.append( m_weeklyArtistMap.value( lastWeekTime ) ); // debug() << "found already-saved data for week:" << lastWeekTime << m_weeklyArtistMap.value( lastWeekTime ); } else { weeksMissing = true; } } lastWeekTime = weekTime; } if( weeksMissing ) { newWeeklyArtistQuery(); return; // not yet ready to construct a query maker } // ok, I need a new query maker m_qm.reset( CollectionManager::instance()->queryMaker() ); // - construct the query m_qm->beginOr(); foreach( const QString &artist, artists ) { // debug() << "adding artist to query:" << artist; m_qm->addFilter( Meta::valArtist, artist, true, true ); } m_qm->endAndOr(); m_qm->setQueryType( Collections::QueryMaker::Custom ); m_qm->addReturnValue( Meta::valUniqueId ); connect( m_qm.data(), &Collections::QueryMaker::newResultReady, this, &WeeklyTopBias::updateReady ); connect( m_qm.data(), &Collections::QueryMaker::queryDone, this, &WeeklyTopBias::updateFinished ); // - run the query m_qm->run(); } void Dynamic::WeeklyTopBias::newWeeklyTimesQuery() { DEBUG_BLOCK QMap< QString, QString > params; params[ "method" ] = "user.getWeeklyChartList" ; params[ "user" ] = lastfm::ws::Username; m_weeklyTimesJob = lastfm::ws::get( params ); connect( m_weeklyTimesJob, &QNetworkReply::finished, this, &WeeklyTopBias::weeklyTimesQueryFinished ); } void Dynamic::WeeklyTopBias::newWeeklyArtistQuery() { DEBUG_BLOCK debug() << "getting top artist info from" << m_range.from << "to" << m_range.to; // - check if we have week times if( m_weeklyFromTimes.isEmpty() ) { newWeeklyTimesQuery(); return; // not yet ready to do construct a query maker } // fetch 5 at a time, so as to conform to lastfm api requirements uint jobCount = m_weeklyArtistJobs.count(); if( jobCount >= 5 ) return; - uint fromTime = m_range.from.toTime_t(); - uint toTime = m_range.to.toTime_t(); + uint fromTime = m_range.from.toSecsSinceEpoch(); + uint toTime = m_range.to.toSecsSinceEpoch(); uint lastWeekTime = 0; foreach( uint weekTime, m_weeklyFromTimes ) { if( weekTime > fromTime && weekTime < toTime && lastWeekTime ) { if( m_weeklyArtistMap.contains( lastWeekTime ) ) { // we already have the data } else if( m_weeklyArtistJobs.contains( lastWeekTime ) ) { // we already fetch the data } else { QMap< QString, QString > params; params[ "method" ] = "user.getWeeklyArtistChart"; params[ "user" ] = lastfm::ws::Username; params[ "from" ] = QString::number( lastWeekTime ); params[ "to" ] = QString::number( m_weeklyToTimes[m_weeklyFromTimes.indexOf(lastWeekTime)] ); QNetworkReply* reply = lastfm::ws::get( params ); connect( reply, &QNetworkReply::finished, this, &WeeklyTopBias::weeklyArtistQueryFinished ); m_weeklyArtistJobs.insert( lastWeekTime, reply ); jobCount++; if( jobCount >= 5 ) return; } } lastWeekTime = weekTime; } } void Dynamic::WeeklyTopBias::weeklyArtistQueryFinished() { DEBUG_BLOCK QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { warning() << "Failed to get qnetwork reply in finished slot."; return; } lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { // debug() << "got response:" << lfm; QStringList artists; for( int i = 0; i < lfm[ "weeklyartistchart" ].children( "artist" ).size(); i++ ) { if( i == 12 ) // only up to 12 artist. break; lastfm::XmlQuery artist = lfm[ "weeklyartistchart" ].children( "artist" ).at( i ); artists.append( artist[ "name" ].text() ); } uint week = QDomElement( lfm[ "weeklyartistchart" ] ).attribute( "from" ).toUInt(); m_weeklyArtistMap.insert( week, artists ); debug() << "got artists:" << artists << week; if( m_weeklyArtistJobs.contains( week) ) { m_weeklyArtistJobs.remove( week ); } else { warning() << "Got a reply for a week"<deleteLater(); saveDataToFile(); newQuery(); // try again to get the tracks } void Dynamic::WeeklyTopBias::weeklyTimesQueryFinished() // SLOT { DEBUG_BLOCK if( !m_weeklyTimesJob ) return; // argh. where does this come from QDomDocument doc; if( !doc.setContent( m_weeklyTimesJob->readAll() ) ) { debug() << "couldn't parse XML from rangeJob!"; return; } QDomNodeList nodes = doc.elementsByTagName( "chart" ); if( nodes.count() == 0 ) { debug() << "USER has no history! can't do this!"; return; } for( int i = 0; i < nodes.size(); i++ ) { QDomNode n = nodes.at( i ); m_weeklyFromTimes.append( n.attributes().namedItem( "from" ).nodeValue().toUInt() ); m_weeklyToTimes.append( n.attributes().namedItem( "to" ).nodeValue().toUInt() ); // debug() << "weeklyTimesResult"<deleteLater(); newQuery(); // try again to get the tracks } void Dynamic::WeeklyTopBias::fromDateChanged( const QDateTime& d ) // SLOT { if( d > m_range.to ) return; m_range.from = d; invalidate(); emit changed( BiasPtr( this ) ); } void Dynamic::WeeklyTopBias::toDateChanged( const QDateTime& d ) // SLOT { if( d < m_range.from ) return; m_range.to = d; invalidate(); emit changed( BiasPtr( this ) ); } void Dynamic::WeeklyTopBias::loadFromFile() { QFile file( Amarok::saveLocation() + "dynamic_lastfm_topweeklyartists.xml" ); file.open( QIODevice::ReadOnly | QIODevice::Text ); QTextStream in( &file ); while( !in.atEnd() ) { QString line = in.readLine(); m_weeklyArtistMap.insert( line.split( '#' )[ 0 ].toUInt(), line.split( '#' )[ 1 ].split( '^' ) ); } file.close(); } void Dynamic::WeeklyTopBias::saveDataToFile() const { QFile file( Amarok::saveLocation() + "dynamic_lastfm_topweeklyartists.xml" ); file.open( QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text ); QTextStream out( &file ); foreach( uint key, m_weeklyArtistMap.keys() ) { out << key << "#" << m_weeklyArtistMap[ key ].join( "^" ) << endl; } file.close(); } diff --git a/src/services/lastfm/meta/LastFmMeta_p.h b/src/services/lastfm/meta/LastFmMeta_p.h index de66b61bc3..f30da286e4 100644 --- a/src/services/lastfm/meta/LastFmMeta_p.h +++ b/src/services/lastfm/meta/LastFmMeta_p.h @@ -1,376 +1,376 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Leo Franchi * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #ifndef AMAROK_LASTFMMETA_P_H #define AMAROK_LASTFMMETA_P_H #include "core/support/Debug.h" #include "amarokconfig.h" #include "core/meta/Meta.h" #include "core/support/Amarok.h" #include "core-impl/support/TagStatisticsStore.h" #include #include #include #include #include #include #include #include #include #include #include namespace LastFm { class Track::Private : public QObject { Q_OBJECT public: Track *t; lastfm::Track lastFmTrack; // this is how we love, ban, etc QUrl trackPath; QUrl lastFmUri; QImage albumArt; QString artist; QString album; QString track; qint64 length; //not sure what these are for but they exist in the LastFmBundle QString albumUrl; QString artistUrl; QString trackUrl; QString imageUrl; Meta::ArtistPtr artistPtr; Meta::AlbumPtr albumPtr; Meta::GenrePtr genrePtr; Meta::ComposerPtr composerPtr; Meta::YearPtr yearPtr; QNetworkReply* trackFetch; QNetworkReply* wsReply; Meta::StatisticsPtr statsStore; uint currentTrackStartTime; public: Private() : lastFmUri( QUrl() ) , currentTrackStartTime( 0 ) { artist = QString ( "Last.fm" ); } ~Private() { } void notifyObservers(); void setTrackInfo( const lastfm::Track &trackInfo ) { DEBUG_BLOCK bool newTrackInfo = artist != trackInfo.artist() || album != trackInfo.album() || track != trackInfo.title(); lastFmTrack = trackInfo; artist = trackInfo.artist(); album = trackInfo.album(); track = trackInfo.title(); length = trackInfo.duration() * 1000; trackPath = trackInfo.url(); // need to reset other items albumUrl = ""; trackUrl = ""; albumArt = QImage(); if( newTrackInfo ) { statsStore = new TagStatisticsStore( t ); - currentTrackStartTime = QDateTime::currentDateTimeUtc().toTime_t(); + currentTrackStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); } notifyObservers(); if( !trackInfo.isNull() ) { QMap< QString, QString > params; params[ "method" ] = "track.getInfo"; params[ "artist" ] = artist; params[ "track" ] = track; m_userFetch = lastfm::ws::post( params ); connect( m_userFetch, SIGNAL( finished() ), SLOT( requestResult() ) ); } } public Q_SLOTS: void requestResult( ) { if( !m_userFetch ) return; if( m_userFetch->error() == QNetworkReply::NoError ) { lastfm::XmlQuery lfm; if( lfm.parse( m_userFetch->readAll() ) ) { albumUrl = lfm[ "track" ][ "album" ][ "url" ].text(); trackUrl = lfm[ "track" ][ "url" ].text(); artistUrl = lfm[ "track" ][ "artist" ][ "url" ].text(); notifyObservers(); imageUrl = lfm[ "track" ][ "album" ][ "image size=large" ].text(); if( !imageUrl.isEmpty() ) { KIO::Job* job = KIO::storedGet( QUrl( imageUrl ), KIO::Reload, KIO::HideProgressInfo ); connect( job, SIGNAL( result( KJob* ) ), this, SLOT( fetchImageFinished( KJob* ) ) ); } } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } } } void fetchImageFinished( KJob* job ) { if( job->error() == 0 ) { const int size = 100; QImage img = QImage::fromData( static_cast( job )->data() ); if( !img.isNull() ) { img.scaled( size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); albumArt = img; } else albumArt = QImage(); } else { //use default image albumArt = QImage(); } notifyObservers(); } private: QNetworkReply* m_userFetch; }; // internal helper classes class LastFmArtist : public Meta::Artist { public: explicit LastFmArtist( Track::Private *dptr ) : Meta::Artist() , d( dptr ) {} Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { if( d ) return d->artist; return QStringLiteral( "Last.fm" ); } Track::Private * const d; friend class Track::Private; }; class LastFmAlbum : public Meta::Album { public: explicit LastFmAlbum( Track::Private *dptr ) : Meta::Album() , d( dptr ) {} bool isCompilation() const override { return false; } bool hasAlbumArtist() const override { return false; } Meta::ArtistPtr albumArtist() const override { return Meta::ArtistPtr(); } Meta::TrackList tracks() override { return Meta::TrackList(); } QString name() const override { if( d ) return d->album; return QString(); } QImage image( int size ) const override { if( !d || d->albumArt.isNull() ) { //return Meta::Album::image( size, withShadow ); //TODO implement shadow //TODO improve this if ( size <= 1 ) size = 100; QString sizeKey = QString::number( size ) + '@'; QImage image; QDir cacheCoverDir = QDir( Amarok::saveLocation( "albumcovers/cache/" ) ); if( cacheCoverDir.exists( sizeKey + "lastfm-default-cover.png" ) ) image = QImage( cacheCoverDir.filePath( sizeKey + "lastfm-default-cover.png" ) ); else { QImage orgImage = QImage( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/lastfm-default-cover.png" ) ); //optimize this! //scaled() does not change the original image but returns a scaled copy image = orgImage.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation ); image.save( cacheCoverDir.filePath( sizeKey + "lastfm-default-cover.png" ), "PNG" ); } return image; } if( d->albumArt.width() != size && size > 0 ) return d->albumArt.scaled( size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); return d->albumArt; } QUrl imageLocation( int size ) override { Q_UNUSED( size ); if( d && !d->imageUrl.isEmpty() ) return QUrl( d->imageUrl ); return QUrl(); } // return true since we handle our own fetching bool hasImage( int size = 1 ) const override { Q_UNUSED( size ); return true; } Track::Private * const d; friend class Track::Private; }; class LastFmGenre : public Meta::Genre { public: explicit LastFmGenre( Track::Private *dptr ) : Meta::Genre() , d( dptr ) {} QString name() const override { return QString(); } Meta::TrackList tracks() override { return Meta::TrackList(); } Track::Private * const d; friend class Track::Private; }; class LastFmComposer : public Meta::Composer { public: explicit LastFmComposer( Track::Private *dptr ) : Meta::Composer() , d( dptr ) {} QString name() const override { return QString(); } Meta::TrackList tracks() override { return Meta::TrackList(); } Track::Private * const d; friend class Track::Private; }; class LastFmYear : public Meta::Year { public: explicit LastFmYear( Track::Private *dptr ) : Meta::Year() , d( dptr ) {} QString name() const override { return QString(); } Meta::TrackList tracks() override { return Meta::TrackList(); } Track::Private * const d; friend class Track::Private; }; void Track::Private::notifyObservers() { // TODO: only notify what actually has changed t->notifyObservers(); static_cast( t->album().data() )->notifyObservers(); static_cast( t->artist().data() )->notifyObservers(); } } #endif diff --git a/src/services/magnatune/MagnatuneStore.cpp b/src/services/magnatune/MagnatuneStore.cpp index 397caefcce..bfddaa8694 100644 --- a/src/services/magnatune/MagnatuneStore.cpp +++ b/src/services/magnatune/MagnatuneStore.cpp @@ -1,742 +1,742 @@ /**************************************************************************************** * Copyright (c) 2006,2007 Nikolaj Hald Nielsen * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "MagnatuneStore.h" #include "core/support/Amarok.h" #include "core/support/Components.h" #include "core/logger/Logger.h" #include "amarokurls/AmarokUrlHandler.h" #include "browsers/CollectionTreeItem.h" #include "browsers/CollectionTreeView.h" #include "browsers/SingleCollectionTreeItemModel.h" #include "EngineController.h" #include "MagnatuneConfig.h" #include "MagnatuneDatabaseWorker.h" #include "MagnatuneInfoParser.h" #include "MagnatuneNeedUpdateWidget.h" #include "browsers/InfoProxy.h" #include "MagnatuneUrlRunner.h" #include "ui_MagnatuneSignupDialogBase.h" #include "../ServiceSqlRegistry.h" #include "core-impl/collections/support/CollectionManager.h" #include "core/support/Debug.h" #include "playlist/PlaylistModelStack.h" #include "widgets/SearchWidget.h" #include #include #include #include #include #include #include #include #include #include #include //////////////////////////////////////////////////////////////////////////////////////////////////////////// // class MagnatuneServiceFactory //////////////////////////////////////////////////////////////////////////////////////////////////////////// MagnatuneServiceFactory::MagnatuneServiceFactory() : ServiceFactory() { } void MagnatuneServiceFactory::init() { DEBUG_BLOCK MagnatuneStore* service = new MagnatuneStore( this, "Magnatune.com" ); m_initialized = true; Q_EMIT newService( service ); } QString MagnatuneServiceFactory::name() { return "Magnatune.com"; } KConfigGroup MagnatuneServiceFactory::config() { return Amarok::config( "Service_Magnatune" ); } //////////////////////////////////////////////////////////////////////////////////////////////////////////// // class MagnatuneStore //////////////////////////////////////////////////////////////////////////////////////////////////////////// MagnatuneStore::MagnatuneStore( MagnatuneServiceFactory* parent, const char *name ) : ServiceBase( name, parent ) , m_downloadHandler( 0 ) , m_redownloadHandler( 0 ) , m_needUpdateWidget( 0 ) , m_downloadInProgress( 0 ) , m_currentAlbum( 0 ) , m_streamType( MagnatuneMetaFactory::OGG ) , m_magnatuneTimestamp( 0 ) , m_registry( 0 ) , m_signupInfoWidget( 0 ) { DEBUG_BLOCK setObjectName(name); //initTopPanel( ); setShortDescription( i18n( "\"Fair trade\" online music store" ) ); setIcon( QIcon::fromTheme( "view-services-magnatune-amarok" ) ); // xgettext: no-c-format setLongDescription( i18n( "Magnatune.com is a different kind of record company with the motto \"We are not evil!\" 50% of every purchase goes directly to the artist and if you purchase an album through Amarok, the Amarok project receives a 10% commission. Magnatune.com also offers \"all you can eat\" memberships that lets you download as much of their music as you like." ) ); setImagePath( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/hover_info_magnatune.png" ) ); //initBottomPanel(); // m_currentlySelectedItem = 0; m_polished = false; //polish( ); //FIXME not happening when shown for some reason //do this stuff now to make us function properly as a track provider on startup. The expensive stuff will //not happen until the model is added to the view anyway. MagnatuneMetaFactory * metaFactory = new MagnatuneMetaFactory( "magnatune", this ); MagnatuneConfig config; if ( config.isMember() ) { setMembership( config.membershipType(), config.username(), config.password() ); metaFactory->setMembershipInfo( config.membershipPrefix(), m_username, m_password ); } setStreamType( config.streamType() ); metaFactory->setStreamType( m_streamType ); m_registry = new ServiceSqlRegistry( metaFactory ); m_collection = new Collections::MagnatuneSqlCollection( "magnatune", "Magnatune.com", metaFactory, m_registry ); CollectionManager::instance()->addTrackProvider( m_collection ); setServiceReady( true ); } MagnatuneStore::~MagnatuneStore() { CollectionManager::instance()->removeTrackProvider( m_collection ); delete m_registry; delete m_collection; } void MagnatuneStore::download( ) { DEBUG_BLOCK if ( m_downloadInProgress ) return; if ( !m_polished ) polish(); debug() << "here"; //check if we need to start a download or show the signup dialog if( !m_isMember || m_membershipType != MagnatuneConfig::DOWNLOAD ) { showSignupDialog(); return; } m_downloadInProgress = true; m_downloadAlbumButton->setEnabled( false ); if ( !m_downloadHandler ) { m_downloadHandler = new MagnatuneDownloadHandler(); m_downloadHandler->setParent( this ); connect( m_downloadHandler, &MagnatuneDownloadHandler::downloadCompleted, this, &MagnatuneStore::downloadCompleted ); } if ( m_currentAlbum != 0 ) m_downloadHandler->downloadAlbum( m_currentAlbum ); } void MagnatuneStore::downloadTrack( Meta::MagnatuneTrack * track ) { Meta::MagnatuneAlbum * album = dynamic_cast( track->album().data() ); if ( album ) downloadAlbum( album ); } void MagnatuneStore::downloadAlbum( Meta::MagnatuneAlbum * album ) { DEBUG_BLOCK if ( m_downloadInProgress ) return; if ( !m_polished ) polish(); m_downloadInProgress = true; m_downloadAlbumButton->setEnabled( false ); if ( !m_downloadHandler ) { m_downloadHandler = new MagnatuneDownloadHandler(); m_downloadHandler->setParent( this ); connect( m_downloadHandler, &MagnatuneDownloadHandler::downloadCompleted, this, &MagnatuneStore::downloadCompleted ); } m_downloadHandler->downloadAlbum( album ); } void MagnatuneStore::initTopPanel( ) { QMenu *filterMenu = new QMenu( nullptr ); QAction *action = filterMenu->addAction( i18n("Artist") ); connect( action, &QAction::triggered, this, &MagnatuneStore::sortByArtist ); action = filterMenu->addAction( i18n( "Artist / Album" ) ); connect( action, &QAction::triggered, this, &MagnatuneStore::sortByArtistAlbum ); action = filterMenu->addAction( i18n( "Album" ) ) ; connect( action, &QAction::triggered, this, &MagnatuneStore::sortByAlbum ); action = filterMenu->addAction( i18n( "Genre / Artist" ) ); connect( action, &QAction::triggered, this, &MagnatuneStore::sortByGenreArtist ); action = filterMenu->addAction( i18n( "Genre / Artist / Album" ) ); connect( action, &QAction::triggered, this, &MagnatuneStore::sortByGenreArtistAlbum ); QAction *filterMenuAction = new QAction( QIcon::fromTheme( "preferences-other" ), i18n( "Sort Options" ), this ); filterMenuAction->setMenu( filterMenu ); m_searchWidget->toolBar()->addSeparator(); m_searchWidget->toolBar()->addAction( filterMenuAction ); QToolButton *tbutton = qobject_cast( m_searchWidget->toolBar()->widgetForAction( filterMenuAction ) ); if( tbutton ) tbutton->setPopupMode( QToolButton::InstantPopup ); QMenu * actionsMenu = new QMenu( nullptr ); action = actionsMenu->addAction( i18n( "Re-download" ) ); connect( action, &QAction::triggered, this, &MagnatuneStore::processRedownload ); m_updateAction = actionsMenu->addAction( i18n( "Update Database" ) ); connect( m_updateAction, &QAction::triggered, this, &MagnatuneStore::updateButtonClicked ); QAction *actionsMenuAction = new QAction( QIcon::fromTheme( "list-add" ), i18n( "Tools" ), this ); actionsMenuAction->setMenu( actionsMenu ); m_searchWidget->toolBar()->addAction( actionsMenuAction ); tbutton = qobject_cast( m_searchWidget->toolBar()->widgetForAction( actionsMenuAction ) ); if( tbutton ) tbutton->setPopupMode( QToolButton::InstantPopup ); } void MagnatuneStore::initBottomPanel() { //m_bottomPanel->setMaximumHeight( 24 ); m_downloadAlbumButton = new QPushButton; m_downloadAlbumButton->setParent( m_bottomPanel ); MagnatuneConfig config; if ( config.isMember() && config.membershipType() == MagnatuneConfig::DOWNLOAD ) { m_downloadAlbumButton->setText( i18n( "Download Album" ) ); m_downloadAlbumButton->setEnabled( false ); } else if ( config.isMember() ) m_downloadAlbumButton->hide(); else { m_downloadAlbumButton->setText( i18n( "Signup" ) ); m_downloadAlbumButton->setEnabled( true ); } m_downloadAlbumButton->setObjectName( "downloadButton" ); m_downloadAlbumButton->setIcon( QIcon::fromTheme( "download-amarok" ) ); connect( m_downloadAlbumButton, &QPushButton::clicked, this, &MagnatuneStore::download ); if ( !config.lastUpdateTimestamp() ) { m_needUpdateWidget = new MagnatuneNeedUpdateWidget(m_bottomPanel); connect( m_needUpdateWidget, &MagnatuneNeedUpdateWidget::wantUpdate, this, &MagnatuneStore::updateButtonClicked ); m_downloadAlbumButton->setParent(0); } } void MagnatuneStore::updateButtonClicked() { DEBUG_BLOCK m_updateAction->setEnabled( false ); if ( m_needUpdateWidget ) m_needUpdateWidget->disable(); updateMagnatuneList(); } bool MagnatuneStore::updateMagnatuneList() { DEBUG_BLOCK //download new list from magnatune debug() << "MagnatuneStore: start downloading xml file"; QTemporaryFile tempFile; // tempFile.setSuffix( ".bz2" ); tempFile.setAutoRemove( false ); // file will be removed in MagnatuneXmlParser if( !tempFile.open() ) { return false; //error } m_tempFileName = tempFile.fileName(); m_listDownloadJob = KIO::file_copy( QUrl("http://magnatune.com/info/album_info_xml.bz2"), QUrl::fromLocalFile( m_tempFileName ), 0700 , KIO::HideProgressInfo | KIO::Overwrite ); Amarok::Logger::newProgressOperation( m_listDownloadJob, i18n( "Downloading Magnatune.com database..." ), this, &MagnatuneStore::listDownloadCancelled ); connect( m_listDownloadJob, &KJob::result, this, &MagnatuneStore::listDownloadComplete ); return true; } void MagnatuneStore::listDownloadComplete( KJob * downLoadJob ) { DEBUG_BLOCK debug() << "MagnatuneStore: xml file download complete"; if ( downLoadJob != m_listDownloadJob ) { debug() << "wrong job, ignoring...."; return ; //not the right job, so let's ignore it } m_updateAction->setEnabled( true ); if ( downLoadJob->error() != 0 ) { debug() << "Got an error, bailing out: " << downLoadJob->errorString(); //TODO: error handling here return ; } Amarok::Logger::shortMessage( i18n( "Updating the local Magnatune database." ) ); MagnatuneXmlParser * parser = new MagnatuneXmlParser( m_tempFileName ); parser->setDbHandler( new MagnatuneDatabaseHandler() ); connect( parser, &MagnatuneXmlParser::doneParsing, this, &MagnatuneStore::doneParsing ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(parser) ); } void MagnatuneStore::listDownloadCancelled( ) { DEBUG_BLOCK m_listDownloadJob->kill(); m_listDownloadJob = 0; debug() << "Aborted xml download"; m_updateAction->setEnabled( true ); if ( m_needUpdateWidget ) m_needUpdateWidget->enable(); } void MagnatuneStore::doneParsing() { debug() << "MagnatuneStore: done parsing"; m_collection->emitUpdated(); //update the last update timestamp MagnatuneConfig config; if ( m_magnatuneTimestamp == 0 ) - config.setLastUpdateTimestamp( QDateTime::currentDateTimeUtc().toTime_t() ); + config.setLastUpdateTimestamp( QDateTime::currentDateTimeUtc().toSecsSinceEpoch() ); else config.setLastUpdateTimestamp( m_magnatuneTimestamp ); config.save(); if ( m_needUpdateWidget ) { m_needUpdateWidget->setParent(0); m_needUpdateWidget->deleteLater(); m_needUpdateWidget = 0; m_downloadAlbumButton->setParent(m_bottomPanel); } } void MagnatuneStore::processRedownload( ) { debug() << "Process redownload"; if ( m_redownloadHandler == 0 ) { m_redownloadHandler = new MagnatuneRedownloadHandler( this ); } m_redownloadHandler->showRedownloadDialog(); } void MagnatuneStore::downloadCompleted( bool ) { delete m_downloadHandler; m_downloadHandler = 0; m_downloadAlbumButton->setEnabled( true ); m_downloadInProgress = false; debug() << "Purchase operation complete"; //TODO: display some kind of success dialog here? } void MagnatuneStore::itemSelected( CollectionTreeItem * selectedItem ) { DEBUG_BLOCK //only care if the user has a download membership if( !m_isMember || m_membershipType != MagnatuneConfig::DOWNLOAD ) return; //we only enable the purchase button if there is only one item selected and it happens to //be an album or a track Meta::DataPtr dataPtr = selectedItem->data(); if ( auto track = AmarokSharedPointer::dynamicCast( dataPtr ) ) { debug() << "is right type (track)"; m_currentAlbum = static_cast ( track->album().data() ); m_downloadAlbumButton->setEnabled( true ); } else if ( auto album = AmarokSharedPointer::dynamicCast( dataPtr ) ) { m_currentAlbum = album.data(); debug() << "is right type (album) named " << m_currentAlbum->name(); m_downloadAlbumButton->setEnabled( true ); } else { debug() << "is wrong type"; m_downloadAlbumButton->setEnabled( false ); } } void MagnatuneStore::addMoodyTracksToPlaylist( const QString &mood, int count ) { MagnatuneDatabaseWorker *databaseWorker = new MagnatuneDatabaseWorker(); databaseWorker->fetchTrackswithMood( mood, count, m_registry ); connect( databaseWorker, &MagnatuneDatabaseWorker::gotMoodyTracks, this, &MagnatuneStore::moodyTracksReady ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(databaseWorker) ); } void MagnatuneStore::polish() { DEBUG_BLOCK; if (!m_polished) { m_polished = true; initTopPanel( ); initBottomPanel(); QList levels; levels << CategoryId::Genre << CategoryId::Artist << CategoryId::Album; m_magnatuneInfoParser = new MagnatuneInfoParser(); setInfoParser( m_magnatuneInfoParser ); setModel( new SingleCollectionTreeItemModel( m_collection, levels ) ); connect( qobject_cast(m_contentView), &CollectionTreeView::itemSelected, this, &MagnatuneStore::itemSelected ); //add a custom url runner MagnatuneUrlRunner *runner = new MagnatuneUrlRunner(); connect( runner, &MagnatuneUrlRunner::showFavorites, this, &MagnatuneStore::showFavoritesPage ); connect( runner, &MagnatuneUrlRunner::showHome, this, &MagnatuneStore::showHomePage ); connect( runner, &MagnatuneUrlRunner::showRecommendations, this, &MagnatuneStore::showRecommendationsPage ); connect( runner, &MagnatuneUrlRunner::buyOrDownload, this, &MagnatuneStore::downloadSku ); connect( runner, &MagnatuneUrlRunner::removeFromFavorites, this, &MagnatuneStore::removeFromFavorites ); The::amarokUrlHandler()->registerRunner( runner, runner->command() ); } MagnatuneInfoParser * parser = dynamic_cast ( infoParser() ); if ( parser ) parser->getFrontPage(); //get a mood map we can show to the cloud view MagnatuneDatabaseWorker * databaseWorker = new MagnatuneDatabaseWorker(); databaseWorker->fetchMoodMap(); connect( databaseWorker, &MagnatuneDatabaseWorker::gotMoodMap, this, &MagnatuneStore::moodMapReady ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(databaseWorker) ); if ( MagnatuneConfig().autoUpdateDatabase() ) checkForUpdates(); } void MagnatuneStore::setMembership( int type, const QString & username, const QString & password) { m_isMember = true; m_membershipType = type; m_username = username; m_password = password; } void MagnatuneStore::moodMapReady(const QMap< QString, int > &map) { QVariantMap variantMap; QList strings; QList weights; QVariantMap dbusActions; foreach( const QString &key, map.keys() ) { strings << key; weights << map.value( key ); QString escapedKey = key; escapedKey.replace( ' ', "%20" ); QVariantMap action; action["component"] = "/ServicePluginManager"; action["function"] = "sendMessage"; action["arg1"] = QStringLiteral( "Magnatune.com"); action["arg2"] = QStringLiteral( "addMoodyTracks %1 10").arg( escapedKey ); dbusActions[key] = action; } variantMap["cloud_name"] = QVariant( "Magnatune Moods" ); variantMap["cloud_strings"] = QVariant( strings ); variantMap["cloud_weights"] = QVariant( weights ); variantMap["cloud_actions"] = QVariant( dbusActions ); The::infoProxy()->setCloud( variantMap ); } void MagnatuneStore::setStreamType( int type ) { m_streamType = type; } void MagnatuneStore::checkForUpdates() { m_updateTimestampDownloadJob = KIO::storedGet( QUrl("http://magnatune.com/info/last_update_timestamp"), KIO::Reload, KIO::HideProgressInfo ); connect( m_updateTimestampDownloadJob, &KJob::result, this, &MagnatuneStore::timestampDownloadComplete ); } void MagnatuneStore::timestampDownloadComplete( KJob * job ) { DEBUG_BLOCK if ( job->error() != 0 ) { //TODO: error handling here return ; } if ( job != m_updateTimestampDownloadJob ) return ; //not the right job, so let's ignore it QString timestampString = ( ( KIO::StoredTransferJob* ) job )->data(); debug() << "Magnatune timestamp: " << timestampString; bool ok; qulonglong magnatuneTimestamp = timestampString.toULongLong( &ok ); MagnatuneConfig config; qulonglong localTimestamp = config.lastUpdateTimestamp(); debug() << "Last update timestamp: " << QString::number( localTimestamp ); if ( ok && magnatuneTimestamp > localTimestamp ) { m_magnatuneTimestamp = magnatuneTimestamp; updateButtonClicked(); } } void MagnatuneStore::moodyTracksReady( const Meta::TrackList &tracks ) { DEBUG_BLOCK The::playlistController()->insertOptioned( tracks, Playlist::Replace ); } QString MagnatuneStore::messages() { QString text = i18n( "The Magnatune.com service accepts the following messages: \n\n\taddMoodyTracks mood count: Adds a number of random tracks with the specified mood to the playlist. The mood argument must have spaces escaped with %%20" ); return text; } QString MagnatuneStore::sendMessage( const QString & message ) { QStringList args = message.split( ' ', QString::SkipEmptyParts ); if ( args.size() < 1 ) { return i18n( "ERROR: No arguments supplied" ); } if ( args[0] == "addMoodyTracks" ) { if ( args.size() != 3 ) { return i18n( "ERROR: Wrong number of arguments for addMoodyTracks" ); } QString mood = args[1]; mood = mood.replace( "%20", " " ); bool ok; int count = args[2].toInt( &ok ); if ( !ok ) return i18n( "ERROR: Parse error for argument 2 ( count )" ); addMoodyTracksToPlaylist( mood, count ); return i18n( "ok" ); } return i18n( "ERROR: Unknown argument." ); } void MagnatuneStore::showFavoritesPage() { DEBUG_BLOCK m_magnatuneInfoParser->getFavoritesPage(); } void MagnatuneStore::showHomePage() { DEBUG_BLOCK m_magnatuneInfoParser->getFrontPage(); } void MagnatuneStore::showRecommendationsPage() { DEBUG_BLOCK m_magnatuneInfoParser->getRecommendationsPage(); } void MagnatuneStore::downloadSku( const QString &sku ) { DEBUG_BLOCK debug() << "sku: " << sku; MagnatuneDatabaseWorker * databaseWorker = new MagnatuneDatabaseWorker(); databaseWorker->fetchAlbumBySku( sku, m_registry ); connect( databaseWorker, &MagnatuneDatabaseWorker::gotAlbumBySku, this, &MagnatuneStore::downloadAlbum ); ThreadWeaver::Queue::instance()->enqueue( QSharedPointer(databaseWorker) ); } void MagnatuneStore::addToFavorites( const QString &sku ) { DEBUG_BLOCK MagnatuneConfig config; if( !config.isMember() ) return; QString url = "http://%1:%2@%3.magnatune.com/member/favorites?action=add_api&sku=%4"; url = url.arg( config.username(), config.password(), config.membershipPrefix(), sku ); debug() << "favorites url: " << url; m_favoritesJob = KIO::storedGet( QUrl( url ), KIO::Reload, KIO::HideProgressInfo ); connect( m_favoritesJob, &KJob::result, this, &MagnatuneStore::favoritesResult ); } void MagnatuneStore::removeFromFavorites( const QString &sku ) { DEBUG_BLOCK MagnatuneConfig config; if( !config.isMember() ) return; QString url = "http://%1:%2@%3.magnatune.com/member/favorites?action=remove_api&sku=%4"; url = url.arg( config.username(), config.password(), config.membershipPrefix(), sku ); debug() << "favorites url: " << url; m_favoritesJob = KIO::storedGet( QUrl( url ), KIO::Reload, KIO::HideProgressInfo ); connect( m_favoritesJob, &KJob::result, this, &MagnatuneStore::favoritesResult ); } void MagnatuneStore::favoritesResult( KJob* addToFavoritesJob ) { if( addToFavoritesJob != m_favoritesJob ) return; QString result = m_favoritesJob->data(); Amarok::Logger::longMessage( result ); //show the favorites page showFavoritesPage(); } void MagnatuneStore::showSignupDialog() { if ( m_signupInfoWidget== 0 ) { m_signupInfoWidget = new QDialog; Ui::SignupDialog ui; ui.setupUi( m_signupInfoWidget ); } m_signupInfoWidget->show(); } diff --git a/src/statsyncing/SimpleTrack.cpp b/src/statsyncing/SimpleTrack.cpp index 47ed237077..306eb5d4e2 100644 --- a/src/statsyncing/SimpleTrack.cpp +++ b/src/statsyncing/SimpleTrack.cpp @@ -1,112 +1,112 @@ /**************************************************************************************** * Copyright (c) 2013 Konrad Zemek * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "SimpleTrack.h" using namespace StatSyncing; SimpleTrack::SimpleTrack( const Meta::FieldHash &metadata, const QSet &labels ) : m_labels( labels ) , m_metadata( metadata ) { } SimpleTrack::~SimpleTrack() { } QString SimpleTrack::name() const { return m_metadata.value( Meta::valTitle ).toString(); } QString SimpleTrack::album() const { return m_metadata.value( Meta::valAlbum ).toString(); } QString SimpleTrack::artist() const { return m_metadata.value( Meta::valArtist ).toString(); } QString SimpleTrack::composer() const { return m_metadata.value( Meta::valComposer ).toString(); } int SimpleTrack::year() const { return m_metadata.value( Meta::valYear ).toInt(); } int SimpleTrack::trackNumber() const { return m_metadata.value( Meta::valTrackNr ).toInt(); } int SimpleTrack::discNumber() const { return m_metadata.value( Meta::valDiscNr ).toInt(); } QDateTime SimpleTrack::firstPlayed() const { return getDateTime( m_metadata.value( Meta::valFirstPlayed ) ); } QDateTime SimpleTrack::lastPlayed() const { return getDateTime( m_metadata.value( Meta::valLastPlayed ) ); } int SimpleTrack::rating() const { return m_metadata.value( Meta::valRating ).toInt(); } int SimpleTrack::playCount() const { return m_metadata.value( Meta::valPlaycount ).toInt(); } QSet SimpleTrack::labels() const { return m_labels; } QDateTime SimpleTrack::getDateTime( const QVariant &v ) const { if( v.toDateTime().isValid() ) return v.toDateTime(); else if( v.toUInt() != 0 ) - return QDateTime::fromTime_t( v.toUInt() ); + return QDateTime::fromSecsSinceEpoch( v.toUInt() ); else return QDateTime(); } diff --git a/src/statsyncing/SimpleWritableTrack.cpp b/src/statsyncing/SimpleWritableTrack.cpp index ec90e60c34..5e30dba33a 100644 --- a/src/statsyncing/SimpleWritableTrack.cpp +++ b/src/statsyncing/SimpleWritableTrack.cpp @@ -1,134 +1,134 @@ /**************************************************************************************** * Copyright (c) 2013 Konrad Zemek * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "SimpleWritableTrack.h" #include #include using namespace StatSyncing; SimpleWritableTrack::SimpleWritableTrack( const Meta::FieldHash &metadata, const QSet &labels ) : SimpleTrack( metadata, labels ) { // Move statistics to separate container, so we don't have to lock for other metadata foreach( const qint64 metaValue, metadata.keys() ) { switch( metaValue ) { case Meta::valFirstPlayed: case Meta::valLastPlayed: case Meta::valRating: case Meta::valPlaycount: m_metadata.remove( metaValue ); m_statistics.insert( metaValue, metadata[metaValue] ); break; default: break; } } } SimpleWritableTrack::~SimpleWritableTrack() { } QDateTime SimpleWritableTrack::firstPlayed() const { QReadLocker lock( &m_lock ); return getDateTime( m_statistics.value( Meta::valFirstPlayed ) ); } void SimpleWritableTrack::setFirstPlayed( const QDateTime &firstPlayed ) { QWriteLocker lock( &m_lock ); m_statistics.insert( Meta::valFirstPlayed, firstPlayed.isValid() - ? firstPlayed.toTime_t() : 0u ); + ? firstPlayed.toSecsSinceEpoch() : 0u ); m_changes |= Meta::valFirstPlayed; } QDateTime SimpleWritableTrack::lastPlayed() const { QReadLocker lock( &m_lock ); return getDateTime( m_statistics.value( Meta::valLastPlayed ) ); } void SimpleWritableTrack::setLastPlayed( const QDateTime &lastPlayed ) { QWriteLocker lock( &m_lock ); m_statistics.insert( Meta::valLastPlayed, lastPlayed.isValid() - ? lastPlayed.toTime_t() : 0u ); + ? lastPlayed.toSecsSinceEpoch() : 0u ); m_changes |= Meta::valLastPlayed; } int SimpleWritableTrack::rating() const { QReadLocker lock( &m_lock ); return m_statistics.value( Meta::valRating ).toInt(); } void SimpleWritableTrack::setRating( int rating ) { QWriteLocker lock( &m_lock ); m_statistics.insert( Meta::valRating, rating ); m_changes |= Meta::valRating; } int SimpleWritableTrack::playCount() const { QReadLocker lock( &m_lock ); return m_statistics.value( Meta::valPlaycount ).toInt(); } void SimpleWritableTrack::setPlayCount( int playCount ) { QWriteLocker lock( &m_lock ); m_statistics.insert( Meta::valPlaycount, playCount ); m_changes |= Meta::valPlaycount; } QSet SimpleWritableTrack::labels() const { QReadLocker lock( &m_lock ); return m_labels; } void SimpleWritableTrack::setLabels( const QSet &labels ) { QWriteLocker lock( &m_lock ); m_labels = labels; m_changes |= Meta::valLabel; } void SimpleWritableTrack::commit() { QWriteLocker lock( &m_lock ); doCommit( m_changes ); m_changes = 0; } diff --git a/src/widgets/MetaQueryWidget.cpp b/src/widgets/MetaQueryWidget.cpp index add618548a..f6415e4fff 100644 --- a/src/widgets/MetaQueryWidget.cpp +++ b/src/widgets/MetaQueryWidget.cpp @@ -1,1207 +1,1207 @@ /**************************************************************************************** * Copyright (c) 2008 Daniel Caleb Jones * * Copyright (c) 2009 Mark Kretschmann * * Copyright (c) 2010 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) version 3 or * * any later version accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "core-impl/collections/support/CollectionManager.h" #include "core/collections/MetaQueryMaker.h" #include "core/collections/QueryMaker.h" #include "widgets/MetaQueryWidget.h" #include "widgets/kdatecombo.h" #include "FileType.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Amarok; static const int maxHours = 24; TimeDistanceWidget::TimeDistanceWidget( QWidget *parent ) : QWidget( parent ) { m_timeEdit = new QSpinBox(this); m_timeEdit->setMinimum( 0 ); m_timeEdit->setMaximum( 600 ); m_unitSelection = new QComboBox(this); connect( m_timeEdit, QOverload::of(&QSpinBox::valueChanged), this, &TimeDistanceWidget::slotUpdateComboBoxLabels ); for (int i = 0; i < 7; ++i) { m_unitSelection->addItem( QString() ); } slotUpdateComboBoxLabels( 0 ); QHBoxLayout *hLayout = new QHBoxLayout(this); hLayout->setContentsMargins(0, 0, 0, 0); hLayout->addWidget( m_timeEdit ); hLayout->addWidget( m_unitSelection ); } qint64 TimeDistanceWidget::timeDistance() const { qint64 time = m_timeEdit->value(); switch( m_unitSelection->currentIndex() ) { case 6: time *= 365*24*60*60; // years break; case 5: time *= 30*24*60*60; // months break; case 4: time *= 7*24*60*60; // weeks break; case 3: time *= 24*60*60; // days break; case 2: time *= 60*60; // hours break; case 1: time *= 60; // minutes break; } return time; } void TimeDistanceWidget::setTimeDistance( qint64 value ) { // as we don't store the time unit we try to reconstruct it int unit = 0; if( value > 600 || !(value % 60) ) { unit = 1; value /= 60; if( value > 600 || !(value % 60) ) { unit = 2; value /= 60; if( value > 72 || !(value % 24) ) { unit = 3; value /= 24; if( !(value % 365) ) { unit = 6; value /= 365; } else if( !(value % 30) ) { unit = 5; value /= 30; } else if( !(value % 7) ) { unit = 4; value /= 7; } } } } m_unitSelection->setCurrentIndex( unit ); m_timeEdit->setValue( value ); } void TimeDistanceWidget::slotUpdateComboBoxLabels( int value ) { m_unitSelection->setItemText(0, i18np("second", "seconds", value)); m_unitSelection->setItemText(1, i18np("minute", "minutes", value)); m_unitSelection->setItemText(2, i18np("hour", "hours", value)); m_unitSelection->setItemText(3, i18np("day", "days", value)); m_unitSelection->setItemText(4, i18np("week", "weeks", value)); m_unitSelection->setItemText(5, i18np("month", "months", value)); m_unitSelection->setItemText(6, i18np("year", "years", value)); } void MetaQueryWidget::Filter::setField( qint64 newField ) { if( m_field == newField ) return; // -- reset the value and the condition if the new filter has another type if( MetaQueryWidget::isNumeric( m_field ) != MetaQueryWidget::isNumeric( newField ) ) { value.clear(); if( MetaQueryWidget::isNumeric( newField ) ) condition = Equals; else condition = Contains; } if( !MetaQueryWidget::isDate( m_field ) && MetaQueryWidget::isDate( newField ) ) { - numValue = QDateTime::currentDateTimeUtc().toTime_t(); - numValue2 = QDateTime::currentDateTimeUtc().toTime_t(); + numValue = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); + numValue2 = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); } else { numValue = 0; numValue2 = 0; } if (numValue < minimumValue( newField ) || numValue > maximumValue( newField ) ) numValue = defaultValue( newField ); if (numValue2 < minimumValue( newField ) || numValue2 > maximumValue( newField ) ) numValue2 = defaultValue( newField ); m_field = newField; } qint64 MetaQueryWidget::Filter::minimumValue( quint64 field ) { switch( field ) { case Meta::valYear: return 1900; case Meta::valTrackNr: return 0; case Meta::valDiscNr: return 0; case Meta::valBpm: return 60; case Meta::valBitrate: return 60; case Meta::valSamplerate: return 8000; case Meta::valFilesize: return 0; case Meta::valScore: return 0; case Meta::valPlaycount: return 0; case Meta::valRating: return 0; case Meta::valLength: return 0; default: return 0; } } qint64 MetaQueryWidget::Filter::maximumValue( quint64 field ) { switch( field ) { case Meta::valYear: return 2300; case Meta::valTrackNr: return 100; case Meta::valDiscNr: return 10; case Meta::valBpm: return 200; case Meta::valBitrate: return 2000; case Meta::valSamplerate: return 48000; case Meta::valFilesize: return 1000; case Meta::valScore: return 100; case Meta::valPlaycount: return 1000; case Meta::valRating: return 10; case Meta::valLength: return maxHours * 60 * 60 - 1; default: return 0; } } qint64 MetaQueryWidget::Filter::defaultValue( quint64 field ) { switch( field ) { case Meta::valYear: return 1976; case Meta::valTrackNr: return 0; case Meta::valDiscNr: return 0; case Meta::valBpm: return 80; case Meta::valBitrate: return 160; case Meta::valSamplerate: return 44100; case Meta::valFilesize: return 10; case Meta::valScore: return 0; case Meta::valPlaycount: return 00; case Meta::valRating: return 0; case Meta::valLength: return 3 * 60 + 59; default: return 0; } } MetaQueryWidget::MetaQueryWidget( QWidget* parent, bool onlyNumeric, bool noCondition ) : QWidget( parent ) , m_onlyNumeric( onlyNumeric ) , m_noCondition( noCondition ) , m_settingFilter( false ) , m_andLabel(0) , m_compareSelection(0) , m_valueSelection1(0) , m_valueSelection2(0) { // note: we are using the strange layout structure because the KRatingWidget size depends on the height. m_layoutMain = new QVBoxLayout( this ); m_layoutMain->setContentsMargins(0, 0, 0, 0); makeFieldSelection(); m_layoutMain->addWidget( m_fieldSelection ); m_layoutValue = new QHBoxLayout(); m_layoutMain->addLayout(m_layoutValue); m_layoutValueLabels = new QVBoxLayout(); m_layoutValue->addLayout(m_layoutValueLabels, 0); m_layoutValueValues = new QVBoxLayout(); m_layoutValue->addLayout(m_layoutValueValues, 1); if( m_onlyNumeric ) m_filter.setField( Meta::valYear ); else m_filter.setField( 0 ); setFilter(m_filter); } MetaQueryWidget::~MetaQueryWidget() { } MetaQueryWidget::Filter MetaQueryWidget::filter() const { // special handling for between if( m_filter.condition == Contains ) { Filter f = m_filter; f.numValue = qMin(m_filter.numValue, m_filter.numValue2) - 1; f.numValue2 = qMax(m_filter.numValue, m_filter.numValue2) + 1; } return m_filter; } void MetaQueryWidget::setFilter( const MetaQueryWidget::Filter &value ) { m_settingFilter = true; m_filter = value; int index = m_fieldSelection->findData( int(m_filter.field()) ); m_fieldSelection->setCurrentIndex( index == -1 ? 0 : index ); if( !m_noCondition ) makeCompareSelection(); makeValueSelection(); setValueSelection(); m_settingFilter = false; Q_EMIT changed(m_filter); } static void addIconItem( QComboBox *box, qint64 field ) { QString icon = Meta::iconForField( field ); QString text = Meta::i18nForField( field ); if( icon.isEmpty() ) box->addItem( text, field ); else box->addItem( QIcon::fromTheme( icon ), text, field ); } void MetaQueryWidget::makeFieldSelection() { m_fieldSelection = new QComboBox( this ); if (!m_onlyNumeric) { m_fieldSelection->addItem( i18n( "Simple Search" ), 0 ); addIconItem( m_fieldSelection, Meta::valUrl ); // note: what about directory? addIconItem( m_fieldSelection, Meta::valTitle ); addIconItem( m_fieldSelection, Meta::valArtist ); addIconItem( m_fieldSelection, Meta::valAlbumArtist ); addIconItem( m_fieldSelection, Meta::valAlbum ); addIconItem( m_fieldSelection, Meta::valGenre ); addIconItem( m_fieldSelection, Meta::valComposer ); } addIconItem( m_fieldSelection, Meta::valYear ); if (!m_onlyNumeric) addIconItem( m_fieldSelection, Meta::valComment ); addIconItem( m_fieldSelection, Meta::valTrackNr ); addIconItem( m_fieldSelection, Meta::valDiscNr ); addIconItem( m_fieldSelection, Meta::valBpm ); addIconItem( m_fieldSelection, Meta::valLength ); addIconItem( m_fieldSelection, Meta::valBitrate ); addIconItem( m_fieldSelection, Meta::valSamplerate ); addIconItem( m_fieldSelection, Meta::valFilesize ); if (!m_onlyNumeric) addIconItem( m_fieldSelection, Meta::valFormat ); addIconItem( m_fieldSelection, Meta::valCreateDate ); addIconItem( m_fieldSelection, Meta::valScore ); addIconItem( m_fieldSelection, Meta::valRating ); addIconItem( m_fieldSelection, Meta::valFirstPlayed ); addIconItem( m_fieldSelection, Meta::valLastPlayed ); addIconItem( m_fieldSelection, Meta::valPlaycount ); if (!m_onlyNumeric) addIconItem( m_fieldSelection, Meta::valLabel ); addIconItem( m_fieldSelection, Meta::valModified ); connect( m_fieldSelection, QOverload::of(&QComboBox::currentIndexChanged), this, &MetaQueryWidget::fieldChanged ); } void MetaQueryWidget::fieldChanged( int i ) { if( m_settingFilter ) return; qint64 field = 0; if( i<0 || i>=m_fieldSelection->count() ) field = m_fieldSelection->itemData( 0 ).toInt(); else field = m_fieldSelection->itemData( i ).toInt(); m_filter.setField( field ); // in the fieldChanged slot we assume that the field was really changed, // so we don't have a problem with throwing away all the old widgets if( !m_noCondition ) makeCompareSelection(); makeValueSelection(); setValueSelection(); Q_EMIT changed(m_filter); } void MetaQueryWidget::compareChanged( int index ) { FilterCondition condition = FilterCondition( m_compareSelection->itemData( index ).toInt() ); if( m_filter.condition == condition ) return; // nothing to do if( m_filter.isDate() ) { if( ( condition == OlderThan || condition == NewerThan ) && m_filter.condition != OlderThan && m_filter.condition != NewerThan ) { // fix some inaccuracies caused by the conversion absolute/relative time specifications // this is actually just for visual consistency int unit = 0; - qint64 value = QDateTime::currentDateTimeUtc().toTime_t() - m_filter.numValue; + qint64 value = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() - m_filter.numValue; if( value > 600 || !(value % 60) ) { unit = 1; value /= 60; if( value > 600 || !(value % 60) ) { unit = 2; value /= 60; if( value > 72 || !(value % 24) ) { unit = 3; value /= 24; if( !(value % 365) ) { unit = 6; value /= 365; } else if( !(value % 30) ) { unit = 5; value /= 30; } else if( !(value % 7) ) { unit = 4; value /= 7; } } } } switch( unit ) { case 6: value *= 365*24*60*60; // years break; case 5: value *= 30*24*60*60; // months break; case 4: value *= 7*24*60*60; // weeks break; case 3: value *= 24*60*60; // days break; case 2: value *= 60*60; // hours break; case 1: value *= 60; // minutes break; } m_filter.numValue = value; } else if( condition != OlderThan && condition != NewerThan && ( m_filter.condition == OlderThan || m_filter.condition == NewerThan ) ) { - m_filter.numValue = QDateTime::currentDateTimeUtc().toTime_t() - m_filter.numValue; + m_filter.numValue = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() - m_filter.numValue; } } m_filter.condition = condition; // need to re-generate the value selection fields makeValueSelection(); setValueSelection(); Q_EMIT changed(m_filter); } void MetaQueryWidget::valueChanged( const QString& value ) { m_filter.value = value; Q_EMIT changed(m_filter); } void MetaQueryWidget::numValueChanged( int value ) { m_filter.numValue = value; Q_EMIT changed(m_filter); } void MetaQueryWidget::numValue2Changed( int value ) { m_filter.numValue2 = value; Q_EMIT changed(m_filter); } void MetaQueryWidget::numValueChanged( qint64 value ) { m_filter.numValue = value; Q_EMIT changed(m_filter); } void MetaQueryWidget::numValue2Changed( qint64 value ) { m_filter.numValue2 = value; Q_EMIT changed(m_filter); } void MetaQueryWidget::numValueChanged( const QTime& value ) { m_filter.numValue = qAbs( value.secsTo( QTime(0,0,0) ) ); Q_EMIT changed(m_filter); } void MetaQueryWidget::numValue2Changed( const QTime& value ) { m_filter.numValue2 = qAbs( value.secsTo( QTime(0,0,0) ) ); Q_EMIT changed(m_filter); } void MetaQueryWidget::numValueDateChanged() { KDateCombo* dateSelection = qobject_cast( sender() ); if( dateSelection ) { QDate date; dateSelection->getDate( &date ); - m_filter.numValue = QDateTime( date ).toTime_t(); + m_filter.numValue = QDateTime( date ).toSecsSinceEpoch(); Q_EMIT changed(m_filter); } } void MetaQueryWidget::numValue2DateChanged() { KDateCombo* dateSelection = qobject_cast( sender() ); if( dateSelection ) { QDate date; dateSelection->getDate( &date ); - m_filter.numValue2 = QDateTime( date ).toTime_t(); + m_filter.numValue2 = QDateTime( date ).toSecsSinceEpoch(); Q_EMIT changed(m_filter); } } void MetaQueryWidget::numValueTimeDistanceChanged() { if( !sender() ) return; // static_cast. Remember: the TimeDistanceWidget does not have a Q_OBJECT macro TimeDistanceWidget* distanceSelection = static_cast( sender()->parent() ); if( distanceSelection ) { m_filter.numValue = distanceSelection->timeDistance(); Q_EMIT changed(m_filter); } } void MetaQueryWidget::numValueFormatChanged(int index) { QComboBox* combo = static_cast(sender()); if( combo ) { m_filter.numValue = combo->itemData( index ).toInt(); Q_EMIT changed(m_filter); } } void MetaQueryWidget::setValueSelection() { if( m_compareSelection ) m_layoutValueLabels->addWidget( m_compareSelection ); if( m_filter.condition == Between ) { delete m_andLabel; // delete the old label m_andLabel = new QLabel( i18n( "and" ), this ); m_layoutValueLabels->addWidget( m_andLabel ); } else { delete m_andLabel; m_andLabel = 0; } if( m_valueSelection1 ) m_layoutValueValues->addWidget( m_valueSelection1 ); if( m_valueSelection2 ) m_layoutValueValues->addWidget( m_valueSelection2 ); } void MetaQueryWidget::makeCompareSelection() { delete m_compareSelection; m_compareSelection = 0; qint64 field = m_filter.field(); if( field == Meta::valFormat ) return; // the field is fixed else if( isDate(field) ) { m_compareSelection = new QComboBox(); m_compareSelection->addItem( conditionToString( Equals, field ), (int)Equals ); m_compareSelection->addItem( conditionToString( LessThan, field ), (int)LessThan ); m_compareSelection->addItem( conditionToString( GreaterThan, field ), (int)GreaterThan ); m_compareSelection->addItem( conditionToString( Between, field ), (int)Between ); m_compareSelection->addItem( conditionToString( OlderThan, field ), (int)OlderThan ); m_compareSelection->addItem( conditionToString( NewerThan, field ), (int)NewerThan ); } else if( isNumeric(field) ) { m_compareSelection = new QComboBox(); m_compareSelection->addItem( conditionToString( Equals, field ), (int)Equals ); m_compareSelection->addItem( conditionToString( LessThan, field ), (int)LessThan ); m_compareSelection->addItem( conditionToString( GreaterThan, field ), (int)GreaterThan ); m_compareSelection->addItem( conditionToString( Between, field ), (int)Between ); } else { m_compareSelection = new QComboBox(); m_compareSelection->addItem( conditionToString( Contains, field ), (int)Contains ); m_compareSelection->addItem( conditionToString( Equals, field ), (int)Equals ); } // -- select the correct entry (even if the condition is not one of the selection) int index = m_compareSelection->findData( int(m_filter.condition) ); if( index == -1 ) { index = 0; m_filter.condition = FilterCondition(m_compareSelection->itemData( index ).toInt()); compareChanged(index); } m_compareSelection->setCurrentIndex( index == -1 ? 0 : index ); connect( m_compareSelection, QOverload::of(&QComboBox::currentIndexChanged), this, &MetaQueryWidget::compareChanged ); } void MetaQueryWidget::makeValueSelection() { delete m_valueSelection1; m_valueSelection1 = 0; delete m_valueSelection2; m_valueSelection2 = 0; qint64 field = m_filter.field(); if( field == Meta::valUrl ) makeFilenameSelection(); else if( field == Meta::valTitle ) // We,re not going to populate this. There tends to be too many titles. makeGenericComboSelection( true, 0 ); else if( field == Meta::valArtist || field == Meta::valAlbumArtist || field == Meta::valAlbum || field == Meta::valGenre || field == Meta::valComposer ) makeMetaComboSelection( field ); else if( field == Meta::valYear ) makeGenericNumberSelection( field ); else if( field == Meta::valComment ) makeGenericComboSelection( true, 0 ); else if( field == Meta::valTrackNr ) makeGenericNumberSelection( field ); else if( field == Meta::valDiscNr ) makeGenericNumberSelection( field ); else if( field == Meta::valBpm ) makeGenericNumberSelection( field ); else if( field == Meta::valLength ) makeLengthSelection(); else if( field == Meta::valBitrate ) makeGenericNumberSelection( field, i18nc("Unit for data rate kilo bit per seconds", "kbps") ); else if( field == Meta::valSamplerate ) makeGenericNumberSelection( field, i18nc("Unit for sample rate", "Hz") ); else if( field == Meta::valFilesize ) makeGenericNumberSelection( field, i18nc("Unit for file size in mega byte", "MiB") ); else if( field == Meta::valFormat ) makeFormatComboSelection(); else if( field == Meta::valCreateDate ) makeDateTimeSelection(); else if( field == Meta::valScore ) makeGenericNumberSelection( field ); else if( field == Meta::valRating ) makeRatingSelection(); else if( field == Meta::valFirstPlayed ) makeDateTimeSelection(); else if( field == Meta::valLastPlayed ) makeDateTimeSelection(); else if( field == Meta::valPlaycount ) makeGenericNumberSelection( field ); else if( field == Meta::valLabel ) makeGenericComboSelection( true, 0 ); else if( field == Meta::valModified ) makeDateTimeSelection(); else // e.g. the simple search makeGenericComboSelection( true, 0 ); } void MetaQueryWidget::makeGenericComboSelection( bool editable, Collections::QueryMaker* populateQuery ) { KComboBox* combo = new KComboBox( this ); combo->setEditable( editable ); if( populateQuery != 0 ) { m_runningQueries.insert(populateQuery, QPointer(combo)); connect( populateQuery, &Collections::QueryMaker::newResultReady, this, &MetaQueryWidget::populateComboBox ); connect( populateQuery, &Collections::QueryMaker::queryDone, this, &MetaQueryWidget::comboBoxPopulated ); populateQuery->run(); } combo->setEditText( m_filter.value ); connect( combo, &KComboBox::editTextChanged, this, &MetaQueryWidget::valueChanged ); combo->completionObject()->setIgnoreCase( true ); combo->setCompletionMode( KCompletion::CompletionPopup ); combo->setInsertPolicy( KComboBox::InsertAtTop ); m_valueSelection1 = combo; } void MetaQueryWidget::makeMetaComboSelection( qint64 field ) { Collections::QueryMaker* qm = CollectionManager::instance()->queryMaker(); qm->setQueryType( Collections::QueryMaker::Custom ); qm->addReturnValue( field ); qm->setAutoDelete( true ); makeGenericComboSelection( true, qm ); } void MetaQueryWidget::populateComboBox( const QStringList &results ) { QObject* query = sender(); if( !query ) return; QPointer combo = m_runningQueries.value(query); if( combo.isNull() ) return; // note: adding items seems to reset the edit text, so we have // to take care of that. disconnect( combo.data(), 0, this, 0 ); // want the results unique and sorted const QSet dataSet = results.toSet(); QStringList dataList = dataSet.toList(); dataList.sort(); combo->addItems( dataList ); KCompletion* comp = combo->completionObject(); comp->setItems( dataList ); // reset the text and re-enable the signal combo.data()->setEditText( m_filter.value ); connect( combo.data(), &QComboBox::editTextChanged, this, &MetaQueryWidget::valueChanged ); } void MetaQueryWidget::makeFormatComboSelection() { QComboBox* combo = new QComboBox( this ); combo->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Preferred ); QStringList filetypes = Amarok::FileTypeSupport::possibleFileTypes(); for (int listpos=0;listposaddItem(filetypes.at(listpos),listpos); } int index = m_fieldSelection->findData( (int)m_filter.numValue ); combo->setCurrentIndex( index == -1 ? 0 : index ); connect( combo, QOverload::of(&QComboBox::currentIndexChanged), this, &MetaQueryWidget::numValueFormatChanged ); m_valueSelection1 = combo; } void MetaQueryWidget::comboBoxPopulated() { QObject* query = sender(); if( !query ) return; m_runningQueries.remove( query ); } void MetaQueryWidget::makeFilenameSelection() { // Don't populate the combobox. Too many urls. makeGenericComboSelection( true, 0 ); } void MetaQueryWidget::makeRatingSelection() { KRatingWidget* ratingWidget = new KRatingWidget(); ratingWidget->setRating( (int)m_filter.numValue ); connect( ratingWidget, QOverload::of(&KRatingWidget::ratingChanged), this, QOverload::of(&MetaQueryWidget::numValueChanged) ); m_valueSelection1 = ratingWidget; if( m_filter.condition != Between ) return; // second KRatingWidget for the between selection KRatingWidget* ratingWidget2 = new KRatingWidget(); ratingWidget2->setRating( (int)m_filter.numValue2 ); connect( ratingWidget2, QOverload::of(&KRatingWidget::ratingChanged), this, QOverload::of(&MetaQueryWidget::numValue2Changed) ); m_valueSelection2 = ratingWidget2; } void MetaQueryWidget::makeLengthSelection() { QString displayFormat = i18nc( "time format for specifying track length - hours, minutes, seconds", "h:m:ss" ); QTimeEdit* timeSpin = new QTimeEdit(); timeSpin->setDisplayFormat( displayFormat ); timeSpin->setMinimumTime( QTime( 0, 0, 0 ) ); timeSpin->setMaximumTime( QTime( maxHours - 1, 59, 59 ) ); timeSpin->setTime( QTime(0, 0, 0).addSecs( m_filter.numValue ) ); connect( timeSpin, &QTimeEdit::timeChanged, this, QOverload::of(&MetaQueryWidget::numValueChanged) ); m_valueSelection1 = timeSpin; if( m_filter.condition != Between ) return; QTimeEdit* timeSpin2 = new QTimeEdit(); timeSpin2->setDisplayFormat( displayFormat ); timeSpin2->setMinimumTime( QTime( 0, 0, 0 ) ); timeSpin2->setMaximumTime( QTime( maxHours - 1, 59, 59 ) ); timeSpin2->setTime( QTime(0, 0, 0).addSecs( m_filter.numValue2 ) ); connect( timeSpin2, &QTimeEdit::timeChanged, this, QOverload::of(&MetaQueryWidget::numValue2Changed) ); m_valueSelection2 = timeSpin2; } void MetaQueryWidget::makeGenericNumberSelection( qint64 field, const QString& unit ) { QSpinBox* spin = new QSpinBox(); spin->setMinimum( Filter::minimumValue( field ) ); spin->setMaximum( Filter::maximumValue( field ) ); if( !unit.isEmpty() ) spin->setSuffix( ' ' + unit ); spin->setValue( m_filter.numValue ); connect( spin, QOverload::of(&QSpinBox::valueChanged), this, QOverload::of(&MetaQueryWidget::numValueChanged) ); m_valueSelection1 = spin; if( m_filter.condition != Between ) return; // second spin box for the between selection QSpinBox* spin2 = new QSpinBox(); spin2->setMinimum( Filter::minimumValue( field ) ); spin2->setMaximum( Filter::maximumValue( field ) ); if( !unit.isEmpty() ) spin2->setSuffix( ' ' + unit ); spin2->setValue( m_filter.numValue2 ); connect( spin2, QOverload::of(&QSpinBox::valueChanged), this, QOverload::of(&MetaQueryWidget::numValue2Changed) ); m_valueSelection2 = spin2; } void MetaQueryWidget::makeDateTimeSelection() { if( m_filter.condition == OlderThan || m_filter.condition == NewerThan ) { TimeDistanceWidget* distanceSelection = new TimeDistanceWidget(); distanceSelection->setTimeDistance( m_filter.numValue ); distanceSelection->connectChanged( this, &MetaQueryWidget::numValueTimeDistanceChanged); m_valueSelection1 = distanceSelection; } else { KDateCombo* dateSelection = new KDateCombo(); QDateTime dt; // if( m_filter.condition == Contains || m_filter.condition == Equals ) // dt = QDateTime::currentDateTime(); // else -// dt.setTime_t( m_filter.numValue ); - dt.setTime_t( m_filter.numValue ); +// dt.setSecsSinceEpoch( m_filter.numValue ); + dt.setSecsSinceEpoch( m_filter.numValue ); dateSelection->setDate( dt.date() ); connect( dateSelection, QOverload::of(&KDateCombo::currentIndexChanged), this, &MetaQueryWidget::numValueDateChanged ); m_valueSelection1 = dateSelection; if( m_filter.condition != Between ) return; // second KDateCombo for the between selection KDateCombo* dateSelection2 = new KDateCombo(); - dt.setTime_t( m_filter.numValue2 ); + dt.setSecsSinceEpoch( m_filter.numValue2 ); dateSelection2->setDate( dt.date() ); connect( dateSelection2, QOverload::of(&KDateCombo::currentIndexChanged), this, &MetaQueryWidget::numValue2DateChanged ); m_valueSelection2 = dateSelection2; } } bool MetaQueryWidget::isNumeric( qint64 field ) { switch( field ) { case Meta::valYear: case Meta::valTrackNr: case Meta::valDiscNr: case Meta::valBpm: case Meta::valLength: case Meta::valBitrate: case Meta::valSamplerate: case Meta::valFilesize: case Meta::valFormat: case Meta::valCreateDate: case Meta::valScore: case Meta::valRating: case Meta::valFirstPlayed: case Meta::valLastPlayed: case Meta::valPlaycount: case Meta::valModified: return true; default: return false; } } bool MetaQueryWidget::isDate( qint64 field ) { switch( field ) { case Meta::valCreateDate: case Meta::valFirstPlayed: case Meta::valLastPlayed: case Meta::valModified: return true; default: return false; } } QString MetaQueryWidget::conditionToString( FilterCondition condition, qint64 field ) { if( isDate(field) ) { switch( condition ) { case LessThan: return i18nc( "The date lies before the given fixed date", "before" ); case Equals: return i18nc( "The date is the same as the given fixed date", "on" ); case GreaterThan: return i18nc( "The date is after the given fixed date", "after" ); case Between: return i18nc( "The date is between the given fixed dates", "between" ); case OlderThan: return i18nc( "The date lies before the given time interval", "older than" ); case NewerThan: return i18nc( "The date lies after the given time interval", "newer than" ); default: ; // fall through } } else if( isNumeric(field) ) { switch( condition ) { case LessThan: return i18n("less than"); case Equals: return i18nc("a numerical tag (like year or track number) equals a value","equals"); case GreaterThan: return i18n("greater than"); case Between: return i18nc( "a numerical tag (like year or track number) is between two values", "between" ); default: ; // fall through } } else { switch( condition ) { case Equals: return i18nc("an alphabetical tag (like title or artist name) equals some string","equals"); case Contains: return i18nc("an alphabetical tag (like title or artist name) contains some string", "contains"); default: ; // fall through } } return i18n("unknown comparison"); } QString MetaQueryWidget::Filter::fieldToString() const { return Meta::shortI18nForField( m_field ); } QString MetaQueryWidget::Filter::toString( bool invert ) const { // this member is called when there is a keyword that needs numeric attributes QString strValue1 = value; QString strValue2 = value; if( m_field == Meta::valFormat ) { strValue1 = Amarok::FileTypeSupport::toString( Amarok::FileType( numValue )); } else if( m_field == Meta::valRating ) { strValue1 = QString::number( (float)numValue / 2 ); strValue2 = QString::number( (float)numValue2 / 2 ); } else if( isDate() ) { if( condition == OlderThan || condition == NewerThan ) { strValue1 = QString::number( numValue ); strValue2 = QString::number( numValue2 ); } else { - strValue1 = QLocale().toString( QDateTime::fromTime_t(numValue).date(), QLocale::ShortFormat ); - strValue2 = QLocale().toString( QDateTime::fromTime_t(numValue2).date(), QLocale::ShortFormat ); + strValue1 = QLocale().toString( QDateTime::fromSecsSinceEpoch(numValue).date(), QLocale::ShortFormat ); + strValue2 = QLocale().toString( QDateTime::fromSecsSinceEpoch(numValue2).date(), QLocale::ShortFormat ); } } else if( isNumeric() ) { if ( condition != Between ) { strValue1 = QString::number( numValue ); } else if (numValue < numValue2) // two values are only used for "between". We want to order them by size { strValue1 = QString::number( numValue ); strValue2 = QString::number( numValue2 ); } else { strValue1 = QString::number( numValue2 ); strValue2 = QString::number( numValue ); } } QString result; if( m_field ) result = fieldToString() + ':'; switch( condition ) { case Equals: { if( isNumeric() ) result += strValue1; else result += '=' + QString( "\"%1\"" ).arg( value ); if( invert ) result.prepend( QChar('-') ); break; } case GreaterThan: { result += '>' + strValue1; if( invert ) result.prepend( QChar('-') ); break; } case LessThan: { result +='<' + strValue1; if( invert ) result.prepend( QChar('-') ); break; } case Between: { if( invert ) result = QString( "%1<%2 OR %1>%3" ).arg( result, strValue1, strValue2 ); else result = QString( "%1>%2 AND %1<%3" ).arg( result, strValue1, strValue2 ); break; } case OlderThan: case NewerThan: { // a human readable time.. QChar strUnit = 's'; qint64 value = numValue; if( !(value % 60) ) { strUnit = 'M'; value /= 60; if( !(value % 60) ) { strUnit = 'h'; value /= 60; if( !(value % 24) ) { strUnit = 'd'; value /= 24; if( !(value % 365) ) { strUnit = 'y'; value /= 365; } else if( !(value % 30) ) { strUnit = 'm'; value /= 30; } else if( !(value % 7) ) { strUnit = 'w'; value /= 7; } } } } if( condition == OlderThan ) result += '>' + QString::number(value) + strUnit; else result += '<' + QString::number(value) + strUnit; if( invert ) result.prepend( QChar('-') ); break; } case Contains: { result += QString( "\"%1\"" ).arg( value ); if( invert ) result.prepend( QChar('-') ); break; } } return result; } bool MetaQueryWidget::isFieldSelectorHidden() const { return m_fieldSelection->isHidden(); } void MetaQueryWidget::setFieldSelectorHidden( const bool hidden ) { m_fieldSelection->setVisible( !hidden ); } void MetaQueryWidget::setField( const qint64 field ) { int index = m_fieldSelection->findData( field ); m_fieldSelection->setCurrentIndex( index == -1 ? 0 : index ); } diff --git a/tests/TestAmarok.cpp b/tests/TestAmarok.cpp index 5ac7ba07ed..368add495e 100644 --- a/tests/TestAmarok.cpp +++ b/tests/TestAmarok.cpp @@ -1,371 +1,371 @@ /*************************************************************************** * Copyright (c) 2009 Sven Krohlas * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "TestAmarok.h" #include "core/support/Amarok.h" #include "config-amarok-test.h" #include #include #include QTEST_MAIN( TestAmarok ) TestAmarok::TestAmarok() {} QString TestAmarok::dataPath( const QString &relPath ) { return QDir::toNativeSeparators( QString( AMAROK_TEST_DIR ) + '/' + relPath ); } void TestAmarok::testAsciiPath() { QCOMPARE( Amarok::asciiPath( "" ), QString( "" ) ); QCOMPARE( Amarok::asciiPath( "/home/sven" ), QString( "/home/sven" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/äöü" ) ), QString( "/___" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/äöütest" ) ), QString( "/___test" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/äöü/test" ) ), QString( "/___/test" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/123/" ) ), QString( "/123/" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/.hidden" ) ), QString( "/.hidden" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/here be dragons" ) ), QString( "/here be dragons" ) ); QCOMPARE( Amarok::asciiPath( QString::fromUtf8( "/!important/some%20stuff/what's this?" ) ), QString( "/!important/some%20stuff/what's this?" ) ); /* 0x7F = 127 = DEL control character, explicitly ok on *nix file systems */ QCOMPARE( Amarok::asciiPath( QString( "/abc" ) + QChar( 0x7F ) + ".1" ), QString( QString( "/abc" ) + QChar( 0x7F ) + ".1" ) ); /* random control character: ok */ QCOMPARE( Amarok::asciiPath( QString( "/abc" ) + QChar( 0x07 ) + ".1" ), QString( QString( "/abc" ) + QChar( 0x07 ) + ".1" ) ); /* null byte is not ok */ QCOMPARE( Amarok::asciiPath( QString( "/abc" ) + QChar( 0x00 ) + ".1" ), QString( "/abc_.1" ) ); } void TestAmarok::testCleanPath() { /* no changes expected here */ QCOMPARE( Amarok::cleanPath( QString( "" ) ), QString( "" ) ); QCOMPARE( Amarok::cleanPath( QString( "abcdefghijklmnopqrstuvwxyz" ) ), QString( "abcdefghijklmnopqrstuvwxyz" ) ); QCOMPARE( Amarok::cleanPath( QString( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) ), QString( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) ); QCOMPARE( Amarok::cleanPath( QString( "/\\.,-+" ) ), QString( "/\\.,-+" ) ); /* German */ QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "äöüß" ) ), QString( "aeoeuess" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ÄÖÜß" ) ), QString( "AeOeUess" ) ); // capital ß only exists in theory in the German language, but had been defined some time ago, iirc /* French */ QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "áàéèêô" ) ), QString( "aaeeeo" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ÁÀÉÈÊÔ" ) ), QString( "AAEEEO" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "æ" ) ), QString( "ae" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "Æ" ) ), QString( "AE" ) ); /* Czech and other east European languages */ QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "çńǹýỳź" ) ), QString( "cnnyyz" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ÇŃǸÝỲŹ" ) ), QString( "CNNYYZ" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ěĺľôŕřůž" ) ), QString( "ellorruz" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ" ) ), QString( "ACDEEINORSTUUYZ" ) ); /* Skandinavian languages */ QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "åø" ) ), QString( "aoe" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ÅØ" ) ), QString( "AOE" ) ); /* Spanish */ QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ñóÿ" ) ), QString( "noy" ) ); QCOMPARE( Amarok::cleanPath( QString::fromUtf8( "ÑÓŸ" ) ), QString( "NOY" ) ); /* add missing ones here */ } void TestAmarok::testComputeScore() { QVERIFY( 50 < Amarok::computeScore( 50, 1, 1 ) ); // greater score if played completely QVERIFY( 0 < Amarok::computeScore( 0, 1, 1 ) ); // handle 0 score QVERIFY( 50 > Amarok::computeScore( 50, 1, 0.1 ) ); // lower score if aborted early QVERIFY( 50 > Amarok::computeScore( 50, 1, 0 ) ); // handle 0% played fraction QVERIFY( 50 > Amarok::computeScore( 50, 0, 0 ) ); // handle 0 playcount } void TestAmarok::testConciseTimeSince() { QCOMPARE( Amarok::conciseTimeSince( 0 ).isEmpty(), false ); QCOMPARE( Amarok::conciseTimeSince( 10 ).isEmpty(), false ); QCOMPARE( Amarok::conciseTimeSince( 100 ).isEmpty(), false ); QCOMPARE( Amarok::conciseTimeSince( 1000 ).isEmpty(), false ); /* any other good ideas what to test here? */ } void TestAmarok::testExtension() { QCOMPARE( Amarok::extension( "" ), QString( "" ) ); QCOMPARE( Amarok::extension( "..." ), QString( "" ) ); QCOMPARE( Amarok::extension( "test" ), QString( "" ) ); QCOMPARE( Amarok::extension( "test." ), QString( "" ) ); QCOMPARE( Amarok::extension( "test.mp3" ), QString( "mp3" ) ); QCOMPARE( Amarok::extension( "test.mP3" ), QString( "mp3" ) ); QCOMPARE( Amarok::extension( "test.MP3" ), QString( "mp3" ) ); QCOMPARE( Amarok::extension( "test.longextension" ), QString( "longextension" ) ); QCOMPARE( Amarok::extension( "test.long.extension" ), QString( "extension" ) ); QCOMPARE( Amarok::extension( "test.m" ), QString( "m" ) ); QCOMPARE( Amarok::extension( QString::fromUtf8( "test.äöü" ) ), QString::fromUtf8( "äöü" ) ); QCOMPARE( Amarok::extension( QString::fromUtf8( "test.ÄÖÜ" ) ), QString::fromUtf8( "äöü" ) ); QCOMPARE( Amarok::extension( "..test.mp3" ), QString( "mp3" ) ); QCOMPARE( Amarok::extension( "..te st.mp3" ), QString( "mp3" ) ); QCOMPARE( Amarok::extension( "..te st.m p3" ), QString( "m p3" ) ); } void TestAmarok::testManipulateThe() { QString teststring; Amarok::manipulateThe( teststring = "", true ); QCOMPARE( teststring, QString( "" ) ); Amarok::manipulateThe( teststring = "", false ); QCOMPARE( teststring, QString( "" ) ); Amarok::manipulateThe( teststring = 'A', true ); QCOMPARE( teststring, QString( "A" ) ); Amarok::manipulateThe( teststring = 'A', false ); QCOMPARE( teststring, QString( "A" ) ); Amarok::manipulateThe( teststring = "ABC", true ); QCOMPARE( teststring, QString( "ABC" ) ); Amarok::manipulateThe( teststring = "ABC", false ); QCOMPARE( teststring, QString( "ABC" ) ); Amarok::manipulateThe( teststring = "The Eagles", true ); QCOMPARE( teststring, QString( "Eagles, The" ) ); Amarok::manipulateThe( teststring = "Eagles, The", false ); QCOMPARE( teststring, QString( "The Eagles" ) ); Amarok::manipulateThe( teststring = "The The", true ); QCOMPARE( teststring, QString( "The, The" ) ); Amarok::manipulateThe( teststring = "The, The", false ); QCOMPARE( teststring, QString( "The The" ) ); Amarok::manipulateThe( teststring = "Something else", true ); QCOMPARE( teststring, QString( "Something else" ) ); Amarok::manipulateThe( teststring = "The Äöü", true ); QCOMPARE( teststring, QString( "Äöü, The" ) ); Amarok::manipulateThe( teststring = QString::fromUtf8( "Äöü, The" ), false ); QCOMPARE( teststring, QString::fromUtf8( "The Äöü" ) ); } void TestAmarok::testSaveLocation() { QString saveLocation = Amarok::saveLocation(); QDir saveLocationDir( saveLocation ); QCOMPARE( saveLocationDir.exists(), true ); QCOMPARE( QDir::isAbsolutePath( saveLocation ), true ); QCOMPARE( saveLocationDir.isReadable(), true ); /* any other good ideas what to test here? */ } void TestAmarok::testVerboseTimeSince() { /* There are two overloaded variants of this function */ QCOMPARE( Amarok::verboseTimeSince( 0 ).isEmpty(), false ); - QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromTime_t( 0 ) ).isEmpty(), false ); + QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromSecsSinceEpoch( 0 ) ).isEmpty(), false ); QCOMPARE( Amarok::verboseTimeSince( 10 ).isEmpty(), false ); - QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromTime_t( 10 ) ).isEmpty(), false ); + QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromSecsSinceEpoch( 10 ) ).isEmpty(), false ); QCOMPARE( Amarok::verboseTimeSince( 100 ).isEmpty(), false ); - QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromTime_t( 100 ) ).isEmpty(), false ); + QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromSecsSinceEpoch( 100 ) ).isEmpty(), false ); QCOMPARE( Amarok::verboseTimeSince( 1000 ).isEmpty(), false ); - QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromTime_t( 1000 ) ).isEmpty(), false ); + QCOMPARE( Amarok::verboseTimeSince( QDateTime::fromSecsSinceEpoch( 1000 ) ).isEmpty(), false ); /* any other good ideas what to test here? */ } void TestAmarok::testVfatPath() { QCOMPARE( Amarok::vfatPath( "" ), QString( "" ) ); /* allowed characters */ QCOMPARE( Amarok::vfatPath( "abcdefghijklmnopqrstuvwxyz" ), QString( "abcdefghijklmnopqrstuvwxyz" ) ); QCOMPARE( Amarok::vfatPath( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ), QString( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) ); QCOMPARE( Amarok::vfatPath( "0123456789" ), QString( "0123456789" ) ); QCOMPARE( Amarok::vfatPath( "! # $ % & ' ( ) - @ ^ _ ` { } ~" ), QString( "! # $ % & ' ( ) - @ ^ _ ` { } ~" ) ); /* only allowed in long file names */ QCOMPARE( Amarok::vfatPath( "+,.;=[]" ), QString( "+,.;=()" ) ); /* illegal characters, without / and \ (see later tests) */ QCOMPARE( Amarok::vfatPath( "\"_*_:_<_>_?_|" ), QString( "_____________" ) ); /* illegal control characters: 0-31, 127 */ QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x00 ) + QChar( 0x01 ) + QChar( 0x02 ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x03 ) + QChar( 0x04 ) + QChar( 0x05 ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x06 ) + QChar( 0x07 ) + QChar( 0x08 ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x09 ) + QChar( 0x0A ) + QChar( 0x0B ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x0C ) + QChar( 0x0D ) + QChar( 0x0E ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x0F ) + QChar( 0x10 ) + QChar( 0x11 ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x12 ) + QChar( 0x13 ) + QChar( 0x14 ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x15 ) + QChar( 0x16 ) + QChar( 0x17 ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x18 ) + QChar( 0x19 ) + QChar( 0x1A ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x1B ) + QChar( 0x1C ) + QChar( 0x1D ) + ".1" ), QString( "abc___.1" ) ); QCOMPARE( Amarok::vfatPath( QString( "abc" ) + QChar( 0x1E ) + QChar( 0x7F ) + ".1" ), QString( "abc__.1" ) ); // 0x7F = 127 = DEL control character /* trailing spaces in extension, directory and file names are being ignored (!) */ QCOMPARE( Amarok::vfatPath( "test " ), QString( "test _" ) ); QCOMPARE( Amarok::vfatPath( " test " ), QString( " test _" ) ); QCOMPARE( Amarok::vfatPath( "test.ext " ), QString( "test.ext _" ) ); QCOMPARE( Amarok::vfatPath( "test .ext " ), QString( "test _.ext _" ) ); QCOMPARE( Amarok::vfatPath( " test .ext " ), QString( " test _.ext _" ) ); // yes, really! /* trailing dot in directory and file names are unsupported are being ignored (!) */ QCOMPARE( Amarok::vfatPath( "test..." ), QString( "test.._" ) ); QCOMPARE( Amarok::vfatPath( "...test..." ), QString( "...test.._" ) ); QCOMPARE( Amarok::vfatPath( "test.ext..." ), QString( "test.ext.._" ) ); QCOMPARE( Amarok::vfatPath( "test....ext..." ), QString( "test....ext.._" ) ); QCOMPARE( Amarok::vfatPath( "...test....ext..." ), QString( "...test....ext.._" ) ); /* more tests of trailing spaces and dot in directory names for Windows */ QCOMPARE( Amarok::vfatPath( "\\some\\folder \\", Amarok::WindowsBehaviour ), QString( "\\some\\folder _\\" ) ); QCOMPARE( Amarok::vfatPath( "\\some \\folder \\", Amarok::WindowsBehaviour ), QString( "\\some _\\folder _\\" ) ); QCOMPARE( Amarok::vfatPath( "\\...some \\ev il \\folders...\\", Amarok::WindowsBehaviour ), QString( "\\...some _\\ev il _\\folders.._\\" ) ); QCOMPARE( Amarok::vfatPath( "\\some\\fol/der \\", Amarok::WindowsBehaviour ), QString( "\\some\\fol_der _\\" ) ); QCOMPARE( Amarok::vfatPath( "\\some...\\folder...\\", Amarok::WindowsBehaviour ), QString( "\\some.._\\folder.._\\" ) ); QCOMPARE( Amarok::vfatPath( "\\some\\fol/der...\\", Amarok::WindowsBehaviour ), QString( "\\some\\fol_der.._\\" ) ); QCOMPARE( Amarok::vfatPath( "\\so..me.\\folder .\\", Amarok::WindowsBehaviour ), QString( "\\so..me_\\folder _\\" ) ); QCOMPARE( Amarok::vfatPath( ".\\any", Amarok::WindowsBehaviour ), QString( ".\\any" ) ); QCOMPARE( Amarok::vfatPath( "..\\any", Amarok::WindowsBehaviour ), QString( "..\\any" ) ); QCOMPARE( Amarok::vfatPath( "...\\any", Amarok::WindowsBehaviour ), QString( ".._\\any" ) ); QCOMPARE( Amarok::vfatPath( "a..\\any", Amarok::WindowsBehaviour ), QString( "a._\\any" ) ); QCOMPARE( Amarok::vfatPath( "any\\.\\any.", Amarok::WindowsBehaviour ), QString( "any\\.\\any_" ) ); QCOMPARE( Amarok::vfatPath( "any\\..\\any ", Amarok::WindowsBehaviour ), QString( "any\\..\\any_" ) ); QCOMPARE( Amarok::vfatPath( "any.\\...\\any", Amarok::WindowsBehaviour ), QString( "any_\\.._\\any" ) ); QCOMPARE( Amarok::vfatPath( "any \\a..\\any", Amarok::WindowsBehaviour ), QString( "any_\\a._\\any" ) ); QCOMPARE( Amarok::vfatPath( "Music\\R.E.M.\\Automatic for the people", Amarok::WindowsBehaviour ), QString( "Music\\R.E.M_\\Automatic for the people" ) ); /* more tests of trailing spaces and dot in directory names for Unix */ QCOMPARE( Amarok::vfatPath( "/some/folder /", Amarok::UnixBehaviour ), QString( "/some/folder _/" ) ); QCOMPARE( Amarok::vfatPath( "/some /folder /", Amarok::UnixBehaviour ), QString( "/some _/folder _/" ) ); QCOMPARE( Amarok::vfatPath( "/...some /ev il /folders.../", Amarok::UnixBehaviour ), QString( "/...some _/ev il _/folders.._/" ) ); QCOMPARE( Amarok::vfatPath( "/some/fol\\der /", Amarok::UnixBehaviour ), QString( "/some/fol_der _/" ) ); QCOMPARE( Amarok::vfatPath( "/some.../folder.../", Amarok::UnixBehaviour ), QString( "/some.._/folder.._/" ) ); QCOMPARE( Amarok::vfatPath( "/some/fol\\der.../", Amarok::UnixBehaviour ), QString( "/some/fol_der.._/" ) ); QCOMPARE( Amarok::vfatPath( "/so..me./folder ./", Amarok::UnixBehaviour ), QString( "/so..me_/folder _/" ) ); QCOMPARE( Amarok::vfatPath( "./any", Amarok::UnixBehaviour ), QString( "./any" ) ); QCOMPARE( Amarok::vfatPath( "../any", Amarok::UnixBehaviour ), QString( "../any" ) ); QCOMPARE( Amarok::vfatPath( ".../any", Amarok::UnixBehaviour ), QString( ".._/any" ) ); QCOMPARE( Amarok::vfatPath( "a../any", Amarok::UnixBehaviour ), QString( "a._/any" ) ); QCOMPARE( Amarok::vfatPath( "any/./any.", Amarok::UnixBehaviour ), QString( "any/./any_" ) ); QCOMPARE( Amarok::vfatPath( "any/../any ", Amarok::UnixBehaviour ), QString( "any/../any_" ) ); QCOMPARE( Amarok::vfatPath( "any./.../any", Amarok::UnixBehaviour ), QString( "any_/.._/any" ) ); QCOMPARE( Amarok::vfatPath( "any /a../any", Amarok::UnixBehaviour ), QString( "any_/a._/any" ) ); QCOMPARE( Amarok::vfatPath( "Music/R.E.M./Automatic for the people", Amarok::UnixBehaviour ), QString( "Music/R.E.M_/Automatic for the people" ) ); /* Stepping deeper into M$ hell: reserved device names * See http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx */ QCOMPARE( Amarok::vfatPath( "CLOCK$" ), QString( "_CLOCK$" ) ); /* this one IS allowed according to * http://en.wikipedia.org/w/index.php?title=Filename&oldid=303934888#Comparison_of_file_name_limitations */ QCOMPARE( Amarok::vfatPath( "CLOCK$.ext" ), QString( "CLOCK$.ext" ) ); QCOMPARE( Amarok::vfatPath( "CON" ), QString( "_CON" ) ); QCOMPARE( Amarok::vfatPath( "CON.ext" ), QString( "_CON.ext" ) ); QCOMPARE( Amarok::vfatPath( "PRN" ), QString( "_PRN" ) ); QCOMPARE( Amarok::vfatPath( "PRN.ext" ), QString( "_PRN.ext" ) ); QCOMPARE( Amarok::vfatPath( "AUX" ), QString( "_AUX" ) ); QCOMPARE( Amarok::vfatPath( "AUX.ext" ), QString( "_AUX.ext" ) ); QCOMPARE( Amarok::vfatPath( "NUL" ), QString( "_NUL" ) ); QCOMPARE( Amarok::vfatPath( "NUL.ext" ), QString( "_NUL.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM1" ), QString( "_COM1" ) ); QCOMPARE( Amarok::vfatPath( "COM1.ext" ), QString( "_COM1.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM2" ), QString( "_COM2" ) ); QCOMPARE( Amarok::vfatPath( "COM2.ext" ), QString( "_COM2.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM3" ), QString( "_COM3" ) ); QCOMPARE( Amarok::vfatPath( "COM3.ext" ), QString( "_COM3.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM4" ), QString( "_COM4" ) ); QCOMPARE( Amarok::vfatPath( "COM4.ext" ), QString( "_COM4.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM5" ), QString( "_COM5" ) ); QCOMPARE( Amarok::vfatPath( "COM5.ext" ), QString( "_COM5.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM6" ), QString( "_COM6" ) ); QCOMPARE( Amarok::vfatPath( "COM6.ext" ), QString( "_COM6.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM7" ), QString( "_COM7" ) ); QCOMPARE( Amarok::vfatPath( "COM7.ext" ), QString( "_COM7.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM8" ), QString( "_COM8" ) ); QCOMPARE( Amarok::vfatPath( "COM8.ext" ), QString( "_COM8.ext" ) ); QCOMPARE( Amarok::vfatPath( "COM9" ), QString( "_COM9" ) ); QCOMPARE( Amarok::vfatPath( "COM9.ext" ), QString( "_COM9.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT1" ), QString( "_LPT1" ) ); QCOMPARE( Amarok::vfatPath( "LPT1.ext" ), QString( "_LPT1.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT2" ), QString( "_LPT2" ) ); QCOMPARE( Amarok::vfatPath( "LPT2.ext" ), QString( "_LPT2.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT3" ), QString( "_LPT3" ) ); QCOMPARE( Amarok::vfatPath( "LPT3.ext" ), QString( "_LPT3.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT4" ), QString( "_LPT4" ) ); QCOMPARE( Amarok::vfatPath( "LPT4.ext" ), QString( "_LPT4.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT5" ), QString( "_LPT5" ) ); QCOMPARE( Amarok::vfatPath( "LPT5.ext" ), QString( "_LPT5.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT6" ), QString( "_LPT6" ) ); QCOMPARE( Amarok::vfatPath( "LPT6.ext" ), QString( "_LPT6.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT7" ), QString( "_LPT7" ) ); QCOMPARE( Amarok::vfatPath( "LPT7.ext" ), QString( "_LPT7.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT8" ), QString( "_LPT8" ) ); QCOMPARE( Amarok::vfatPath( "LPT8.ext" ), QString( "_LPT8.ext" ) ); QCOMPARE( Amarok::vfatPath( "LPT9" ), QString( "_LPT9" ) ); QCOMPARE( Amarok::vfatPath( "LPT9.ext" ), QString( "_LPT9.ext" ) ); /* Device names in different cases: */ QCOMPARE( Amarok::vfatPath( "con" ), QString( "_con" ) ); QCOMPARE( Amarok::vfatPath( "con.ext" ), QString( "_con.ext" ) ); QCOMPARE( Amarok::vfatPath( "cON" ), QString( "_cON" ) ); QCOMPARE( Amarok::vfatPath( "cON.ext" ), QString( "_cON.ext" ) ); /* This one is ok :) */ QCOMPARE( Amarok::vfatPath( "CONCERT" ), QString( "CONCERT" ) ); QCOMPARE( Amarok::vfatPath( "CONCERT.ext" ), QString( "CONCERT.ext" ) ); } diff --git a/tests/core-impl/collections/db/sql/TestSqlScanManager.cpp b/tests/core-impl/collections/db/sql/TestSqlScanManager.cpp index d7f8639703..e05f579cf9 100644 --- a/tests/core-impl/collections/db/sql/TestSqlScanManager.cpp +++ b/tests/core-impl/collections/db/sql/TestSqlScanManager.cpp @@ -1,1635 +1,1635 @@ /**************************************************************************************** * Copyright (c) 2009 Maximilian Kossick * * Copyright (c) 2010 Ralf Engels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "TestSqlScanManager.h" #include "amarokconfig.h" #include "MetaTagLib.h" #include "scanner/GenericScanManager.h" #include "core-impl/collections/db/sql/SqlCollection.h" #include "core-impl/collections/db/sql/SqlQueryMaker.h" #include "core-impl/collections/db/sql/SqlRegistry.h" #include "core-impl/storage/sql/mysqlestorage/MySqlEmbeddedStorage.h" #include "config-amarok-test.h" #include "SqlMountPointManagerMock.h" #include #include #include #include #include #include QTEST_GUILESS_MAIN( TestSqlScanManager ) TestSqlScanManager::TestSqlScanManager() : QObject() { QString help = i18n("Amarok"); // prevent a bug when the scanner is the first thread creating a translator } void TestSqlScanManager::initTestCase() { AmarokConfig::instance("amarokrc"); m_autoGetCoverArt = AmarokConfig::autoGetCoverArt(); AmarokConfig::setAutoGetCoverArt( false ); // setenv( "LC_ALL", "", 1 ); // this breaks the test // Amarok does not force LC_ALL=C but obviously the test does it which // will prevent scanning of files with umlauts. //Tell GenericScanManager that we want to use the recently built scanner, not an installed version. const QString overridePath = QString( AMAROK_OVERRIDE_UTILITIES_PATH ); qApp->setProperty( "overrideUtilitiesPath", overridePath ); // that is the original mp3 file that we use to generate the "real" tracks m_sourcePath = QDir::toNativeSeparators( QString( AMAROK_TEST_DIR ) + "/data/audio/Platz 01.mp3" ); QVERIFY( QFile::exists( m_sourcePath ) ); m_tmpDatabaseDir = new QTemporaryDir(); QVERIFY( m_tmpDatabaseDir->isValid() ); m_storage = QSharedPointer( new MySqlEmbeddedStorage() ); QVERIFY( m_storage->init( m_tmpDatabaseDir->path() ) ); m_collection = new Collections::SqlCollection( m_storage ); connect( m_collection, &Collections::SqlCollection::updated, this, &TestSqlScanManager::slotCollectionUpdated ); // TODO: change the mock mount point manager so that it doesn't pull // in all the devices. Not much of a mock like this. SqlMountPointManagerMock *mock = new SqlMountPointManagerMock( this, m_storage ); m_collection->setMountPointManager( mock ); m_scanManager = m_collection->scanManager(); AmarokConfig::setScanRecursively( true ); AmarokConfig::setMonitorChanges( false ); // switch on writing back so that we can create the test files with all the information AmarokConfig::setWriteBack( true ); AmarokConfig::setWriteBackStatistics( true ); AmarokConfig::setWriteBackCover( true ); // I just need the table and not the whole playlist manager /* m_storage->query( QString( "CREATE TABLE playlist_tracks (" " id " + m_storage->idType() + ", playlist_id INTEGER " ", track_num INTEGER " ", url " + m_storage->exactTextColumnType() + ", title " + m_storage->textColumnType() + ", album " + m_storage->textColumnType() + ", artist " + m_storage->textColumnType() + ", length INTEGER " ", uniqueid " + m_storage->textColumnType(128) + ") ENGINE = MyISAM;" ) ); */ } void TestSqlScanManager::cleanupTestCase() { // aborts a ThreadWeaver job that would otherwise cause next statement to stall delete m_collection; // we cannot simply call WeaverInterface::finish(), it stops event loop // QSignalSpy spy( ThreadWeaver::Queue::instance(), &ThreadWeaver::Queue::finished ); // if( !ThreadWeaver::Queue::instance()->isIdle() ) // QVERIFY2( spy.wait( 5000 ), "threads did not finish in timeout" ); delete m_tmpDatabaseDir; AmarokConfig::setAutoGetCoverArt( m_autoGetCoverArt ); } void TestSqlScanManager::init() { m_tmpCollectionDir = new QTemporaryDir(); QVERIFY( m_tmpCollectionDir->isValid() ); QStringList collectionFolders; collectionFolders << m_tmpCollectionDir->path(); m_collection->mountPointManager()->setCollectionFolders( collectionFolders ); } void TestSqlScanManager::cleanup() { m_scanManager->abort(); m_storage->query( "BEGIN" ); m_storage->query( "TRUNCATE TABLE tracks;" ); m_storage->query( "TRUNCATE TABLE albums;" ); m_storage->query( "TRUNCATE TABLE artists;" ); m_storage->query( "TRUNCATE TABLE composers;" ); m_storage->query( "TRUNCATE TABLE genres;" ); m_storage->query( "TRUNCATE TABLE years;" ); m_storage->query( "TRUNCATE TABLE urls;" ); m_storage->query( "TRUNCATE TABLE statistics;" ); m_storage->query( "TRUNCATE TABLE directories;" ); m_storage->query( "COMMIT" ); m_collection->registry()->emptyCache(); delete m_tmpCollectionDir; } void TestSqlScanManager::testScanSingle() { m_collectionUpdatedCount = 0; createSingleTrack(); fullScanAndWait(); QVERIFY( m_collectionUpdatedCount > 0 ); // -- check the commit Meta::TrackPtr track = m_collection->registry()->getTrack( 1 ); QVERIFY( track ); QCOMPARE( track->name(), QString("Theme From Armageddon") ); QVERIFY( track->artist() ); QCOMPARE( track->artist()->name(), QString("Soundtrack & Theme Orchestra") ); QVERIFY( track->album() ); QCOMPARE( track->album()->name(), QString("Big Screen Adventures") ); QVERIFY( track->album()->albumArtist() ); QCOMPARE( track->album()->albumArtist()->name(), QString("Theme Orchestra") ); QVERIFY( !track->album()->isCompilation() ); // One single track is not compilation QCOMPARE( track->composer()->name(), QString("Unknown Composer") ); QCOMPARE( track->comment(), QString("Amazon.com Song ID: 210541237") ); QCOMPARE( track->year()->year(), 2009 ); QCOMPARE( track->type(), QString("mp3") ); QCOMPARE( track->trackNumber(), 28 ); QCOMPARE( track->bitrate(), 257 ); // TagLib reports 257 kbit/s QCOMPARE( track->length(), qint64(12000) ); QCOMPARE( track->sampleRate(), 44100 ); QCOMPARE( track->filesize(), 389679 ); QDateTime aDate = QDateTime::currentDateTime(); QVERIFY( track->createDate().secsTo( aDate ) < 5 ); // I just imported the file QVERIFY( track->createDate().secsTo( aDate ) >= 0 ); QVERIFY( track->modifyDate().secsTo( aDate ) < 5 ); // I just wrote the file QVERIFY( track->modifyDate().secsTo( aDate ) >= 0 ); Meta::StatisticsPtr statistics = track->statistics(); QVERIFY( qFuzzyCompare( statistics->score(), 0.875 ) ); QCOMPARE( statistics->playCount(), 5 ); QVERIFY( !statistics->firstPlayed().isValid() ); QVERIFY( !statistics->lastPlayed().isValid() ); QVERIFY( track->createDate().isValid() ); // -- check that a further scan doesn't change anything m_collectionUpdatedCount = 0; fullScanAndWait(); QCOMPARE( m_collectionUpdatedCount, 0 ); } void TestSqlScanManager::testScanDirectory() { createAlbum(); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->name(), QString("Thriller") ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); QVERIFY( !album->hasImage() ); } void TestSqlScanManager::testDuplicateUid() { Meta::FieldHash values; // create two tracks with same uid values.clear(); values.insert( Meta::valUniqueId, QVariant("c6c29f50279ab9523a0f44928bc1e96b") ); values.insert( Meta::valUrl, QVariant("track1.mp3") ); values.insert( Meta::valTitle, QVariant("Track 1") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("c6c29f50279ab9523a0f44928bc1e96b") ); values.insert( Meta::valUrl, QVariant("track2.mp3") ); values.insert( Meta::valTitle, QVariant("Track 2") ); createTrack( values ); fullScanAndWait(); // -- check the commit (the database needs to have been updated correctly) m_collection->registry()->emptyCache(); // -- both tracks should be present Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( 1 ); QVERIFY( album ); QVERIFY( album->tracks().count() >= 1 ); } void TestSqlScanManager::testLongUid() { Meta::FieldHash values; // create two tracks with different very long values.clear(); values.insert( Meta::valUniqueId, QVariant("c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96bbbbbbbbbbbbbbc6c29f50279ab9523a0f44928bc1e96b1") ); values.insert( Meta::valUrl, QVariant("track1.mp3") ); values.insert( Meta::valTitle, QVariant("Track 1") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96c6c29f50279ab9523a0f44928bc1e96bbbbbbbbbbbbbbc6c29f50279ab9523a0f44928bc1e96b2") ); values.insert( Meta::valUrl, QVariant("track2.mp3") ); values.insert( Meta::valTitle, QVariant("Track 2") ); createTrack( values ); fullScanAndWait(); // -- check the commit (the database needs to have been updated correctly) m_collection->registry()->emptyCache(); // both tracks should be present Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( 1 ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 2 ); } void TestSqlScanManager::testCompilation() { createAlbum(); createCompilation(); createCompilationLookAlikeAlbum(); Meta::FieldHash values; // create one compilation track values.clear(); values.insert( Meta::valUniqueId, QVariant("c6c29f50279ab9523a0f44928bc1e96b") ); values.insert( Meta::valUrl, QVariant("Amazon MP3/The Sum Of All Fears (O.S.T.)/The Sum of All Fears/01 - If We Could Remember (O.S.T. LP Version).mp3") ); values.insert( Meta::valTitle, QVariant("If We Could Remember (O.S.T. LP Version)") ); values.insert( Meta::valArtist, QVariant("The Sum Of All Fears (O.S.T.)/Yolanda Adams") ); values.insert( Meta::valAlbum, QVariant("The Sum of All Fears") ); values.insert( Meta::valCompilation, QVariant(true) ); createTrack( values ); // create one various artists track values.clear(); values.insert( Meta::valUniqueId, QVariant("6ae759476c34256ff1d06f0b5c964d75") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/06 - The Dream Of The Dolphin.mp3") ); values.insert( Meta::valTitle, QVariant("The Dream Of The Dolphin") ); values.insert( Meta::valArtist, QVariant("Various Artists") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valCompilation, QVariant(false) ); createTrack( values ); // create two tracks in the same directory with different albums values.clear(); values.insert( Meta::valUniqueId, QVariant("7957bc25521c1dc91351d497321c27a6") ); values.insert( Meta::valUrl, QVariant("01 - Solid.mp3") ); values.insert( Meta::valTitle, QVariant("Solid") ); values.insert( Meta::valArtist, QVariant("Ashford & Simpson") ); values.insert( Meta::valAlbum, QVariant("Solid") ); createTrack( values ); // create one none compilation track values.clear(); values.insert( Meta::valUniqueId, QVariant("b88c3405cfee64c50768b75eb6e3feea") ); values.insert( Meta::valUrl, QVariant("In-Mood feat. Juliette - The Last Unicorn (Elemental Radio Mix).mp3") ); values.insert( Meta::valTitle, QVariant("The Last Unicorn (Elemental Radio Mix)") ); values.insert( Meta::valArtist, QVariant("In-Mood") ); values.insert( Meta::valAlbum, QVariant("The Last Unicorn") ); createTrack( values ); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QCOMPARE( album->name(), QString("Top Gun") ); QCOMPARE( album->tracks().count(), 10 ); QVERIFY( album->isCompilation() ); album = m_collection->registry()->getAlbum( "The Sum of All Fears", QString() ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( album->isCompilation() ); album = m_collection->registry()->getAlbum( "The Cross Of Changes", QString() ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( album->isCompilation() ); // the album is by various artists album = m_collection->registry()->getAlbum( "Solid", "Ashford & Simpson" ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( !album->isCompilation() ); album = m_collection->registry()->getAlbum( "The Last Unicorn", "In-Mood" ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( !album->isCompilation() ); // this album is a little tricky because it has some nasty special characters in it. Meta::TrackPtr track = m_collection->registry()->getTrackFromUid( m_collection->uidUrlProtocol() + "://" + "0969ea6128444e128cfcac95207bd525" ); QVERIFY( track ); album = track->album(); QCOMPARE( album->tracks().count(), 13 ); QVERIFY( !album->isCompilation() ); } void TestSqlScanManager::testBlock() { /** TODO: do we need blocking at all? createSingleTrack(); Meta::TrackPtr track; m_scanManager->blockScan(); // block the incremental scanning m_scanManager->requestFullScan(); QTest::qWait( 100 ); track = m_collection->registry()->getTrack( 1 ); QVERIFY( !track ); QVERIFY( !m_scanManager->isRunning() ); m_scanManager->unblockScan(); // block the incremental scanning // now the actual behaviour is not defined. // it might or might not continue with the old scan waitScannerFinished(); // in case it does continue after all */ } void TestSqlScanManager::testAddDirectory() { createAlbum(); fullScanAndWait(); createCompilation(); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 10 ); QVERIFY( album->isCompilation() ); } void TestSqlScanManager::testRemoveDir() { Meta::AlbumPtr album; createAlbum(); createCompilation(); fullScanAndWait(); // -- check the commit album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 10 ); QVERIFY( album->isCompilation() ); // -- remove one album album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); foreach( Meta::TrackPtr t, album->tracks() ) QVERIFY( QFile::remove( t->playableUrl().path() ) ); QVERIFY( QDir( m_tmpCollectionDir->path() ).rmdir( QFileInfo( album->tracks().first()->playableUrl().path() ).path() ) ); fullScanAndWait(); // this one is still here album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); // this one is gone album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 0 ); // -- remove the second album // this time it's a directory inside a directory album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); foreach( Meta::TrackPtr t, album->tracks() ) QVERIFY( QFile::remove( t->playableUrl().path() ) ); QVERIFY( QDir( m_tmpCollectionDir->path() ).rmdir( QFileInfo( album->tracks().first()->playableUrl().path() ).path() ) ); incrementalScanAndWait(); // this time both are gone album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 0 ); album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 0 ); } void TestSqlScanManager::testUidChangeMoveDirectoryIncrementalScan() { createAlbum(); fullScanAndWait(); Meta::AlbumPtr album; Meta::TrackList tracks; // -- check the commit album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); tracks = album->tracks(); QCOMPARE( tracks.count(), 9 ); QCOMPARE( tracks.first()->uidUrl(), QString("amarok-sqltrackuid://1dc7022c52a3e4c51b46577da9b3c8ff") ); QVERIFY( !album->isCompilation() ); // change all the track uids in a silly way QHash uidChanges; // uid hashed with track number foreach( const Meta::TrackPtr &track, tracks ) { Meta::FieldHash uidChange; QString uid = track->uidUrl().remove( QString("amarok-sqltrackuid://") ); QStringRef left = uid.leftRef( 10 ); QStringRef right = uid.rightRef( uid.size() - left.size() ); QString newUid = QString("%1%2").arg( right.toString(), left.toString() ); uidChange.insert( Meta::valUniqueId, newUid ); uidChanges.insert( track->trackNumber(), newUid ); QUrl url = track->playableUrl(); QVERIFY( url.isLocalFile() ); Meta::Tag::writeTags( url.path(), uidChange, true ); } // move album directory const QUrl oldUrl = tracks.first()->playableUrl(); const QString base = m_tmpCollectionDir->path() + "/Pop"; QVERIFY( QFile::rename( base, base + "Albums" ) ); // do an incremental scan incrementalScanAndWait(); // recheck album album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); tracks = album->tracks(); QCOMPARE( tracks.count(), 9 ); // check changed uids foreach( const Meta::TrackPtr &track, tracks ) { QString uid = track->uidUrl().remove( QString("amarok-sqltrackuid://") ); QCOMPARE( uid, uidChanges.value( track->trackNumber() ) ); } } void TestSqlScanManager::testRemoveTrack() { Meta::AlbumPtr album; Meta::TrackPtr track; QDateTime aDate = QDateTime::currentDateTime(); createAlbum(); fullScanAndWait(); // -- check the commit album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); track = album->tracks().first(); // the tracks are sorted, so this is always the same track QCOMPARE( track->trackNumber(), 1 ); QVERIFY( !track->statistics()->firstPlayed().isValid() ); static_cast(track.data())->setFirstPlayed( aDate ); // -- remove one track QVERIFY( QFile::remove( track->playableUrl().path() ) ); fullScanAndWait(); // -- check that the track is really gone QCOMPARE( album->tracks().count(), 8 ); } void TestSqlScanManager::testMove() { createAlbum(); createCompilation(); // we use the created and first played attributes for identifying the moved tracks. // currently those are not written back to the track Meta::AlbumPtr album; Meta::TrackPtr track; QDateTime aDate = QDateTime::currentDateTime(); fullScanAndWait(); if( qgetenv("AMAROK_RUN_LONG_TESTS").isNull() ) QSKIP( "takes too long to be run by default;\nDefine AMAROK_RUN_LONG_TESTS " "environment variable to run all tests.", SkipAll ); // -- check the commit album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); track = album->tracks().first(); QCOMPARE( track->trackNumber(), 1 ); QDateTime createDate = track->createDate(); QDateTime modifyDate = track->modifyDate(); // --- move one track static_cast(track.data())->setFirstPlayed( aDate ); const QString targetPath = m_tmpCollectionDir->path() + "/moved.mp3"; QVERIFY( QFile::rename( track->playableUrl().path(), targetPath ) ); fullScanAndWait(); // -- check that the track is moved QVERIFY( createDate == track->createDate() ); // create date should not have changed QVERIFY( modifyDate == track->modifyDate() ); // we just changed the track. it should have changed QCOMPARE( track->statistics()->firstPlayed(), aDate ); QCOMPARE( track->playableUrl().path(), targetPath ); // --- move a directory album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 10 ); track = album->tracks().first(); QUrl oldUrl = track->playableUrl(); QVERIFY( QFile::rename( m_tmpCollectionDir->path() + "/Top Gun", m_tmpCollectionDir->path() + "/Top Gun - Soundtrack" ) ); // do an incremental scan incrementalScanAndWait(); // check that the track is now moved (but still the old object) QCOMPARE( album->tracks().count(), 10 ); // no doublicate tracks QVERIFY( oldUrl != track->playableUrl() ); } void TestSqlScanManager::testFeat() { Meta::FieldHash values; // create one compilation track values.clear(); values.insert( Meta::valUniqueId, QVariant("b88c3405cfee64c50768b75eb6e3feea") ); values.insert( Meta::valUrl, QVariant("In-Mood feat. Juliette - The Last Unicorn (Elemental Radio Mix).mp3") ); values.insert( Meta::valTitle, QVariant("The Last Unicorn (Elemental Radio Mix)") ); values.insert( Meta::valArtist, QVariant("In-Mood feat. Juliette") ); values.insert( Meta::valAlbum, QVariant("The Last Unicorn") ); createTrack( values ); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( "The Last Unicorn", "In-Mood" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 1 ); } void TestSqlScanManager::testAlbumImage() { createSingleTrack(); createAlbum(); createCompilation(); // put an image into the album directory QString imageSourcePath = QDir::toNativeSeparators( QString( AMAROK_TEST_DIR ) + "/data/playlists/no-playlist.png" ); QVERIFY( QFile::exists( imageSourcePath ) ); QString targetPath; targetPath = m_tmpCollectionDir->path() + "/Pop/Thriller/cover.png"; QVERIFY( QFile::copy( m_sourcePath, targetPath ) ); // put an image into the compilation directory targetPath = m_tmpCollectionDir->path() + "/Top Gun/front.png"; QVERIFY( QFile::copy( m_sourcePath, targetPath ) ); // set an embedded image targetPath = m_tmpCollectionDir->path() + "/Various Artists/Big Screen Adventures/28 - Theme From Armageddon.mp3"; Meta::Tag::setEmbeddedCover( targetPath, QImage( 200, 200, QImage::Format_RGB32 ) ); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QVERIFY( album->hasImage() ); album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QVERIFY( album->hasImage() ); album = m_collection->registry()->getAlbum( "Big Screen Adventures", "Theme Orchestra" ); QVERIFY( album ); QVERIFY( album->hasImage() ); } void TestSqlScanManager::testMerges() { // songs from same album but different directory // check that images are merged // check that old image is not overwritten Meta::FieldHash values; values.clear(); values.insert( Meta::valUniqueId, QVariant("123456d040d5dd9b5b45c1494d84cc82") ); values.insert( Meta::valUrl, QVariant("Various Artists/Big Screen Adventures/28 - Theme From Armageddon.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("Unnamed track") ); values.insert( Meta::valArtist, QVariant("Unknown artist") ); createTrack( values ); // -- check the commit fullScanAndWait(); Meta::TrackPtr track = m_collection->registry()->getTrack( 1 ); QVERIFY( track ); QCOMPARE( track->name(), QString("Unnamed track") ); // -- now overwrite the track with changed information and a new uid // - remove one track QVERIFY( QFile::remove( track->playableUrl().path() ) ); values.clear(); values.insert( Meta::valUniqueId, QVariant("794b1bd040d5dd9b5b45c1494d84cc82") ); values.insert( Meta::valUrl, QVariant("Various Artists/Big Screen Adventures/28 - Theme From Armageddon.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("Theme From Armageddon") ); values.insert( Meta::valArtist, QVariant("Soundtrack & Theme Orchestra") ); values.insert( Meta::valAlbum, QVariant("Big Screen Adventures") ); values.insert( Meta::valComposer, QVariant("Unknown Composer") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 210541237") ); values.insert( Meta::valGenre, QVariant("Broadway & Vocalists") ); values.insert( Meta::valYear, QVariant(2009) ); values.insert( Meta::valTrackNr, QVariant(28) ); values.insert( Meta::valScore, QVariant(0.875) ); values.insert( Meta::valPlaycount, QVariant(5) ); createTrack( values ); fullScanAndWait(); // -- check the commit QCOMPARE( track->name(), QString("Theme From Armageddon") ); QVERIFY( track->artist() ); QCOMPARE( track->artist()->name(), QString("Soundtrack & Theme Orchestra") ); QVERIFY( track->album() ); QCOMPARE( track->album()->name(), QString("Big Screen Adventures") );if( qgetenv("AMAROK_RUN_LONG_TESTS").isNull() ) QSKIP( "takes too long to be run by default;\nDefine AMAROK_RUN_LONG_TESTS " "environment variable to run all tests.", SkipAll ); QCOMPARE( track->composer()->name(), QString("Unknown Composer") ); QCOMPARE( track->comment(), QString("Amazon.com Song ID: 210541237") ); QCOMPARE( track->year()->year(), 2009 ); QCOMPARE( track->type(), QString("mp3") ); QCOMPARE( track->trackNumber(), 28 ); QCOMPARE( track->bitrate(), 257 ); QCOMPARE( track->length(), qint64(12000) ); QCOMPARE( track->sampleRate(), 44100 ); QCOMPARE( track->filesize(), 389679 ); Meta::StatisticsPtr statistics = track->statistics(); QVERIFY( qFuzzyCompare( statistics->score(), 0.875 ) ); QCOMPARE( statistics->playCount(), 5 ); QVERIFY( !statistics->firstPlayed().isValid() ); QVERIFY( !statistics->lastPlayed().isValid() ); QVERIFY( track->createDate().isValid() ); // -- now do an incremental scan createAlbum(); // add a new album incrementalScanAndWait(); // -- check the commit Meta::AlbumPtr album; // the old track is still there album = m_collection->registry()->getAlbum( "Big Screen Adventures", "Soundtrack & Theme Orchestra" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 1 ); // the new album is now here album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); } void TestSqlScanManager::testLargeInsert() { if( qgetenv("AMAROK_RUN_LONG_TESTS").isNull() ) QSKIP( "takes too long to be run by default;\nDefine AMAROK_RUN_LONG_TESTS " "environment variable to run all tests.", SkipAll ); // the old large insert test was misleading as the problems with // the insertion started upwards of 20000 tracks. // // For now here are the "ok" numbers on a sensible fast computer: // Scanning 10000 files <3 min // Committing 10000 files <30 sec // Scanning 50000 files <13 min // Committing 50000 files <1 min QDateTime aDate = QDateTime::currentDateTime(); // -- create the input data QByteArray byteArray; QBuffer *buffer = new QBuffer(&byteArray); buffer->open(QIODevice::ReadWrite); QXmlStreamWriter writer( buffer ); writer.writeStartElement( "scanner" ); int trackCount = 0; // some simulated normal albums for( int dirId = 0; dirId < 2000; dirId++ ) { writer.writeStartElement( "directory" ); writer.writeTextElement( "path", QString::number(dirId) ); writer.writeTextElement( "rpath", '/' + QString::number(dirId) ); - writer.writeTextElement( "mtime", QString::number(aDate.toTime_t()) ); + writer.writeTextElement( "mtime", QString::number(aDate.toSecsSinceEpoch()) ); for( int trackId = 0; trackId < 20; trackId++ ) { writer.writeStartElement( "track" ); writer.writeTextElement( "uniqueid", "uid" + QString::number(trackCount) ); writer.writeTextElement( "path", "/path" + QString::number(trackCount) ); writer.writeTextElement( "rpath", "path" + QString::number(trackCount) ); trackCount++; writer.writeTextElement( "title", "track" + QString::number(trackCount) ); writer.writeTextElement( "artist", "artist" + QString::number(dirId) ); writer.writeTextElement( "album", QString::number(dirId) ); writer.writeEndElement(); } writer.writeEndElement(); } // a simulated genre folders for( int dirId = 0; dirId < 7; dirId++ ) { writer.writeStartElement( "directory" ); writer.writeTextElement( "path", "genre" + QString::number(dirId) ); writer.writeTextElement( "rpath", "/genre" + QString::number(dirId) ); - writer.writeTextElement( "mtime", QString::number(aDate.toTime_t()) ); + writer.writeTextElement( "mtime", QString::number(aDate.toSecsSinceEpoch()) ); for( int albumId = 0; albumId < 1000; albumId++ ) { writer.writeStartElement( "track" ); writer.writeTextElement( "uniqueid", "uid" + QString::number(trackCount) ); writer.writeTextElement( "path", "/path" + QString::number(trackCount) ); writer.writeTextElement( "rpath", "path" + QString::number(trackCount) ); trackCount++; writer.writeTextElement( "title", "track" + QString::number(trackCount) ); writer.writeTextElement( "artist", "artist" + QString::number(dirId) + "xx" + QString::number(albumId) ); writer.writeTextElement( "album", "genre album" + QString::number(dirId) + "xx" + QString::number(albumId) ); writer.writeEndElement(); } writer.writeEndElement(); } // A simulated amarok 1.4 collection folder for( int dirId = 0; dirId < 3000; dirId++ ) { writer.writeStartElement( "directory" ); writer.writeTextElement( "path", "collection" + QString::number(dirId) ); writer.writeTextElement( "rpath", "/collection" + QString::number(dirId) ); - writer.writeTextElement( "mtime", QString::number(aDate.toTime_t()) ); + writer.writeTextElement( "mtime", QString::number(aDate.toSecsSinceEpoch()) ); writer.writeStartElement( "track" ); writer.writeTextElement( "uniqueid", "uid" + QString::number(trackCount) ); writer.writeTextElement( "path", "/path" + QString::number(trackCount) ); writer.writeTextElement( "rpath", "path" + QString::number(trackCount) ); trackCount++; writer.writeTextElement( "title", "track" + QString::number(trackCount) ); writer.writeTextElement( "artist", "album artist" + QString::number(dirId % 200) ); writer.writeTextElement( "album", "album" + QString::number(dirId % 300) ); writer.writeEndElement(); writer.writeEndElement(); } writer.writeEndElement(); aDate = QDateTime::currentDateTime(); // -- feed the scanner in batch mode buffer->seek( 0 ); importAndWait( buffer ); qDebug() << "performance test secs:"<< aDate.secsTo( QDateTime::currentDateTime() ); QVERIFY( aDate.secsTo( QDateTime::currentDateTime() ) < 120 ); // -- get all tracks Collections::SqlQueryMaker *qm = static_cast< Collections::SqlQueryMaker* >( m_collection->queryMaker() ); qm->setQueryType( Collections::QueryMaker::Track ); qm->setBlocking( true ); qm->run(); Meta::TrackList tracks = qm->tracks(); delete qm; for( int i = 0; i < trackCount; i++ ) { Meta::TrackPtr track = m_collection->registry()->getTrackFromUid( m_collection->uidUrlProtocol() + "://uid" + QString::number(i) ); QVERIFY( track ); } qDebug() << "performance test secs:"<< aDate.secsTo( QDateTime::currentDateTime() ) << "tracks:" << trackCount; QCOMPARE( tracks.count(), trackCount ); // -- scan the input a second time. that should be a lot faster (but currently isn't) aDate = QDateTime::currentDateTime(); // -- feed the scanner in batch mode buffer = new QBuffer(&byteArray); // the old scanner deleted the old buffer. buffer->open(QIODevice::ReadWrite); importAndWait( buffer ); qDebug() << "performance test secs:"<< aDate.secsTo( QDateTime::currentDateTime() ); QVERIFY( aDate.secsTo( QDateTime::currentDateTime() ) < 80 ); } void TestSqlScanManager::testIdentifyCompilationInMultipleDirectories() { // Compilations where each is track is from a different artist // are often stored as one track per directory, e.g. // /artistA/compilation/track1 // /artistB/compilation/track2 // // this is how Amarok 1 (after using Organize Collection) and iTunes are storing // these albums on disc // the bad thing is that Amarok 1 (as far as I know) didn't set the id3 tags Meta::FieldHash values; values.insert( Meta::valUniqueId, QVariant("5ef9fede5b3f98deb088b33428b0398e") ); values.insert( Meta::valUrl, QVariant("Kenny Loggins/Top Gun/Top Gun - 01 - Kenny Loggins - Danger Zone.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("Danger Zone") ); values.insert( Meta::valArtist, QVariant("Kenny Loggins") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); values.insert( Meta::valTrackNr, QVariant("1") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("3e3970f52b0eda3f2a8c1b3a8c8d39ea") ); values.insert( Meta::valUrl, QVariant("Cheap Trick/Top Gun/Top Gun - 02 - Cheap Trick - Mighty Wings.mp3") ); values.insert( Meta::valTitle, QVariant("Mighty Wings") ); values.insert( Meta::valArtist, QVariant("Cheap Trick") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("6ea0bbd97ad8068df58ad75a81f271f7") ); values.insert( Meta::valUrl, QVariant("Kenny Loggins/Top Gun/Top Gun - 03 - Kenny Loggins - Playing With The Boys.mp3") ); values.insert( Meta::valTitle, QVariant("Playing With The Boys") ); values.insert( Meta::valArtist, QVariant("Kenny Loggins") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("f3ac2e15288361d779a0ae813a2018ba") ); values.insert( Meta::valUrl, QVariant("Teena Marie/Top Gun/Top Gun - 04 - Teena Marie - Lead Me On.mp3") ); values.insert( Meta::valTitle, QVariant("Lead Me On") ); values.insert( Meta::valArtist, QVariant("Teena Marie") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album = m_collection->registry()->getAlbum( "Top Gun", QString() ); QVERIFY( album ); QCOMPARE( album->name(), QString("Top Gun") ); QCOMPARE( album->tracks().count(), 4 ); QVERIFY( album->isCompilation() ); } void TestSqlScanManager::testAlbumArtistMerges() { // three tracks with the same artist but different album artist. // (one is unset) // Those should end up in different albums. Meta::FieldHash values; values.insert( Meta::valUniqueId, QVariant("1ef9fede5b3f98deb088b33428b0398e") ); values.insert( Meta::valUrl, QVariant("test1/song1.mp3") ); values.insert( Meta::valTitle, QVariant("title1") ); values.insert( Meta::valArtist, QVariant("artist") ); values.insert( Meta::valAlbumArtist, QVariant("albumArtist1") ); values.insert( Meta::valAlbum, QVariant("test1") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("2ef9fede5b3f98deb088b33428b0398b") ); values.insert( Meta::valUrl, QVariant("test1/song2.mp3") ); values.insert( Meta::valTitle, QVariant("title2") ); values.insert( Meta::valArtist, QVariant("artist") ); values.insert( Meta::valAlbumArtist, QVariant("albumArtist2") ); values.insert( Meta::valAlbum, QVariant("test1") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("3ef9fede5b3f98deb088b33428b0398c") ); values.insert( Meta::valUrl, QVariant("test1/song3.mp3") ); values.insert( Meta::valTitle, QVariant("title3") ); values.insert( Meta::valArtist, QVariant("artist") ); values.insert( Meta::valAlbum, QVariant("test1") ); createTrack( values ); fullScanAndWait(); // -- check the commit Meta::AlbumPtr album; album = m_collection->registry()->getAlbum( "test1", QString() ); QVERIFY( album ); QCOMPARE( album->name(), QString("test1") ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( album->isCompilation() ); album = m_collection->registry()->getAlbum( "test1", QString("albumArtist1") ); QVERIFY( album ); QCOMPARE( album->name(), QString("test1") ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( !album->isCompilation() ); album = m_collection->registry()->getAlbum( "test1", QString("albumArtist2") ); QVERIFY( album ); QCOMPARE( album->name(), QString("test1") ); QCOMPARE( album->tracks().count(), 1 ); QVERIFY( !album->isCompilation() ); } void TestSqlScanManager::testCrossRenaming() { createAlbum(); // we use the created and first played attributes for identifying the moved tracks. // currently those are not written back to the track Meta::AlbumPtr album; Meta::TrackPtr track; fullScanAndWait(); // -- check the commit album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); QVERIFY( !album->isCompilation() ); // --- cross-rename two track track = album->tracks().at( 0 ); static_cast(track.data())->setRating( 1 ); QString path1 = track->playableUrl().path(); track = album->tracks().at( 1 ); static_cast(track.data())->setRating( 2 ); QString path2 = track->playableUrl().path(); QString targetPath = m_tmpCollectionDir->path() + "moved.mp3"; QVERIFY( QFile::rename( path2, targetPath ) ); QVERIFY( QFile::rename( path1, path2 ) ); QVERIFY( QFile::rename( targetPath, path1 ) ); fullScanAndWait(); // -- check that the tracks are moved correctly album = m_collection->registry()->getAlbum( "Thriller", "Michael Jackson" ); QVERIFY( album ); QCOMPARE( album->tracks().count(), 9 ); track = album->tracks().at( 0 ); QCOMPARE( track->statistics()->rating(), 1 ); QCOMPARE( track->playableUrl().path(), path2 ); track = album->tracks().at( 1 ); QCOMPARE( track->statistics()->rating(), 2 ); QCOMPARE( track->playableUrl().path(), path1 ); } void TestSqlScanManager::slotCollectionUpdated() { m_collectionUpdatedCount++; } void TestSqlScanManager::fullScanAndWait() { QScopedPointer csc( m_collection->create()); if( csc ) { csc->startFullScan(); waitScannerFinished(); } } void TestSqlScanManager::incrementalScanAndWait() { // incremental scans use the modification time of the file system. // this time is only in seconds, so to be sure that the incremental scan // works we need to wait at least one second. QTest::qWait( 1000 ); QScopedPointer csc( m_collection->create()); if( csc ) csc->startIncrementalScan(); waitScannerFinished(); } void TestSqlScanManager::importAndWait( QIODevice* input ) { QScopedPointer csc( m_collection->create()); if( csc ) csc->import( input, 0 ); waitScannerFinished(); } void TestSqlScanManager::waitScannerFinished() { QVERIFY( m_scanManager->isRunning() ); QSignalSpy succeedSpy( m_scanManager, &GenericScanManager::succeeded ); QSignalSpy failSpy( m_scanManager, &GenericScanManager::failed ); QSignalSpy resultSpy( this, &TestSqlScanManager::scanManagerResult ); // connect the result signal *after* the spies to ensure they are updated first connect( m_scanManager, &GenericScanManager::succeeded, this, &TestSqlScanManager::scanManagerResult ); connect( m_scanManager, &GenericScanManager::failed, this, &TestSqlScanManager::scanManagerResult); const bool ok = resultSpy.wait( 5000 ); disconnect( m_scanManager, &GenericScanManager::succeeded, this, &TestSqlScanManager::scanManagerResult ); disconnect( m_scanManager, &GenericScanManager::failed, this, &TestSqlScanManager::scanManagerResult ); QVERIFY2( ok, "Scan Manager timed out without a result" ); if( failSpy.count() > 0 ) { QStringList errors; foreach( const QList &arguments, static_cast > >( failSpy ) ) errors << arguments.value( 0 ).toString(); // this will fire each time: qWarning() << "ScanManager failed with an error:" << errors.join( ", " ); } QCOMPARE( qMakePair( succeedSpy.count(), failSpy.count() ), qMakePair( 1, 0 ) ); QVERIFY( !m_scanManager->isRunning() ); } void TestSqlScanManager::createTrack( const Meta::FieldHash &values ) { // -- copy the file from our original QVERIFY( values.contains( Meta::valUrl ) ); const QString targetPath = m_tmpCollectionDir->path() + '/' + values.value( Meta::valUrl ).toString(); QVERIFY( QDir( m_tmpCollectionDir->path() ).mkpath( QFileInfo( values.value( Meta::valUrl ).toString() ).path() ) ); QVERIFY( QFile::copy( m_sourcePath, targetPath ) ); // -- set all the values that we need Meta::Tag::writeTags( targetPath, values, true ); } void TestSqlScanManager::createSingleTrack() { Meta::FieldHash values; values.insert( Meta::valUniqueId, QVariant("794b1bd040d5dd9b5b45c1494d84cc82") ); values.insert( Meta::valUrl, QVariant("Various Artists/Big Screen Adventures/28 - Theme From Armageddon.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("Theme From Armageddon") ); values.insert( Meta::valArtist, QVariant("Soundtrack & Theme Orchestra") ); values.insert( Meta::valAlbumArtist, QVariant("Theme Orchestra") ); values.insert( Meta::valAlbum, QVariant("Big Screen Adventures") ); values.insert( Meta::valComposer, QVariant("Unknown Composer") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 210541237") ); values.insert( Meta::valGenre, QVariant("Broadway & Vocalists") ); values.insert( Meta::valYear, QVariant(2009) ); values.insert( Meta::valTrackNr, QVariant(28) ); // values.insert( Meta::valBitrate, QVariant(216) ); // the bitrate can not be set. it's computed // values.insert( Meta::valLength, QVariant(184000) ); // also can't be set // values.insert( Meta::valSamplerate, QVariant(44100) ); // again // values.insert( Meta::valFilesize, QVariant(5094892) ); // again values.insert( Meta::valScore, QVariant(0.875) ); values.insert( Meta::valPlaycount, QVariant(5) ); // TODO: set an embedded cover createTrack( values ); } void TestSqlScanManager::createAlbum() { Meta::FieldHash values; values.insert( Meta::valUniqueId, QVariant("1dc7022c52a3e4c51b46577da9b3c8ff") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 01 - Michael Jackson - Track01.mp3") ); values.insert( Meta::valTitle, QVariant("Wanna Be Startin' Somethin'") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(1) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("1dc708934a3e4c51b46577da9b3ab11") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 02 - Michael Jackson - Track02.mp3") ); values.insert( Meta::valTitle, QVariant("Baby Be Mine") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(2) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("15a6b1bf79747fdc8e9c6b6f06203017") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 03 - Michael Jackson - Track03.mp3") ); values.insert( Meta::valTitle, QVariant("The Girl Is Mine") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(3) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("4aba4c8b1d1893c03c112cc3c01221e9") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 04 - Michael Jackson - Track04.mp3") ); values.insert( Meta::valTitle, QVariant("Thriller") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(4) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("cb44d2a3d8053829b04672723bf0bd6e") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 05 - Michael Jackson - Track05.mp3") ); values.insert( Meta::valTitle, QVariant("Beat It") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(5) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("eba1858eeeb3c6d97fe3385200114d86") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 06 - Michael Jackson - Track06.mp3") ); values.insert( Meta::valTitle, QVariant("Billy Jean") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(6) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("4623850290998486b0f7b39a2719904e") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 07 - Michael Jackson - Track07.mp3") ); values.insert( Meta::valTitle, QVariant("Human Nature") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(7) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("6d9a7de13af1e16bb13a6208e44b046d") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 08 - Michael Jackson - Track08.mp3") ); values.insert( Meta::valTitle, QVariant("P.Y.T. (Pretty Young Thing)") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(8) ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("91cf9a7c0d255399f9f6babfacae432b") ); values.insert( Meta::valUrl, QVariant("Pop/Thriller/Thriller - 09 - Michael Jackson - Track09.mp3") ); values.insert( Meta::valTitle, QVariant("The Lady In My Life") ); values.insert( Meta::valArtist, QVariant("Michael Jackson") ); values.insert( Meta::valAlbum, QVariant("Thriller") ); values.insert( Meta::valYear, QVariant(1982) ); values.insert( Meta::valTrackNr, QVariant(9) ); createTrack( values ); } void TestSqlScanManager::createCompilation() { // a compilation without the compilation flags values.insert( Meta::valCompilation, QVariant(true) ); Meta::FieldHash values; values.insert( Meta::valUniqueId, QVariant("5ef9fede5b3f98deb088b33428b0398e") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 01 - Kenny Loggins - Danger Zone.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("Danger Zone") ); values.insert( Meta::valArtist, QVariant("Kenny Loggins") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("3e3970f52b0eda3f2a8c1b3a8c8d39ea") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 02 - Cheap Trick - Mighty Wings.mp3") ); values.insert( Meta::valTitle, QVariant("Mighty Wings") ); values.insert( Meta::valArtist, QVariant("Cheap Trick") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("6ea0bbd97ad8068df58ad75a81f271f7") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 03 - Kenny Loggins - Playing With The Boys.mp3") ); values.insert( Meta::valTitle, QVariant("Playing With The Boys") ); values.insert( Meta::valArtist, QVariant("Kenny Loggins") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("f3ac2e15288361d779a0ae813a2018ba") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 04 - Teena Marie - Lead Me On.mp3") ); values.insert( Meta::valTitle, QVariant("Lead Me On") ); values.insert( Meta::valArtist, QVariant("Teena Marie") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("ffe2bb3e6e2f698983c95e40937545ff") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 05 - Berlin - Take My Breath Away (Love Theme From _Top Gun_).mp3") ); values.insert( Meta::valTitle, QVariant("Take My Breath Away (Love Theme From "Top Gun")") ); values.insert( Meta::valArtist, QVariant("Berlin") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("c871dba16f92483898bcd6a1ed1bc14f") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 06 - Miami Sound Machine - Hot Summer Nights.mp3") ); values.insert( Meta::valTitle, QVariant("Hot Summer Nights") ); values.insert( Meta::valArtist, QVariant("Miami Sound Machine") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("80d157c36ed334192ed8df4c01bf0d4e") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 07 - Loverboy - Heaven In Your Eyes.mp3") ); values.insert( Meta::valTitle, QVariant("Heaven In Your Eyes") ); values.insert( Meta::valArtist, QVariant("Loverboy") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("1fe5897cdea860348c3a5eb40d47c382") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 08 - Larry Greene - Through The Fire.mp3") ); values.insert( Meta::valTitle, QVariant("Through The Fire") ); values.insert( Meta::valArtist, QVariant("Larry Greene") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("e0eacff604bfe38b5c275b45aa4f5323") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 09 - Marietta - Destination Unknown.mp3") ); values.insert( Meta::valTitle, QVariant("Destination Unknown") ); values.insert( Meta::valArtist, QVariant("Marietta") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("9f1b00dab2df7537b6c5b2be9f08b220") ); values.insert( Meta::valUrl, QVariant("Top Gun/Top Gun - 10 - Harold Faltermeyer & Steve Stevens - Top Gun Anthem.mp3") ); values.insert( Meta::valTitle, QVariant("Top Gun Anthem") ); values.insert( Meta::valArtist, QVariant("Harold Faltermeyer & Steve Stevens") ); values.insert( Meta::valAlbum, QVariant("Top Gun") ); createTrack( values ); } void TestSqlScanManager::createCompilationLookAlikeAlbum() { Meta::FieldHash values; // Some systems have problems with the umlauts in the file names. // That is the case where the system encoding when compiling does not // match the one of the file system. // the following is the original filename // values.insert( Meta::valUrl, QVariant( "Glen Hansard & Markéta Irglová/Once/01 Glen Hansard & Markéta Irglová - Falling Slowly.mp3" ) ); values.insert( Meta::valUniqueId, QVariant( "8375aa24e0e0434ca0c36e382b6f188c" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/01 Glen Hansard & Marketa Irglova - Falling Slowly.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Falling Slowly" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "1" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "ff3f82b1c2e1434d9d1a7b6aec67ac9c" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/02 Glen Hansard & Marketa Irglova - If You Want Me.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "If You Want Me" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "2" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "8fb2396f8d974f6196d2b2ef93ba2551" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/03 Glen Hansard - Broken Hearted Hoover Fixer Sucker Guy.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Broken Hearted Hoover Fixer Sucker Guy" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "3" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "3a211546b91c4bf7a4ec9d41325e5a01" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/04 Glen Hansard & Marketa Irglova - When Your Mind's Made Up.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "When Your Mind's Made Up" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "4" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "e7a1ed52777c437582a217cd29cc35f7" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/05 Glen Hansard - Lies.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Lies" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "5" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "e0c88a85884d40c899522cd733718d9e" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/06 Interference - Gold.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Gold" ) ); values.insert( Meta::valArtist, QVariant( "Interference" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "6" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "0969ea6128444e128cfcac95207bd525" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/07 Marketa Irglova - The Hill.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "The Hill" ) ); values.insert( Meta::valArtist, QVariant( "Markéta Irglová" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "7" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "c1d6eff3cb6c42eaa0d63e186ef1b749" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/08 Glen Hansard - Fallen From the Sky.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Fallen From the Sky" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "8" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "b6611dbccd0e49bca8db5dc598b7bf4f" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/09 Glen Hansard - Leave.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Leave" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "9" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "46873076087f48dda553fc5ebd3c0fb6" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/10 Glen Hansard - Trying to Pull Myself Away.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Trying to Pull Myself Away" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "10" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "ea29de7b131c4cf28df177a8cda990ee" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/11 Glen Hansard - All the Way Down.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "All the Way Down" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "11" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "66259801d8ba4d50a2dfdf0129bc8792" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/12 Glen Hansard & Marketa Irglova - Once.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Once" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "12" ) ); createTrack( values ); values.insert( Meta::valUniqueId, QVariant( "a654e8c5afb14de7b55b6548ac02f724" ) ); values.insert( Meta::valUrl, QVariant( "Glen Hansard & Marketa Irglova/Once/13 Glen Hansard - Say It to Me Now.mp3" ) ); values.insert( Meta::valFormat, QVariant( "1" ) ); values.insert( Meta::valTitle, QVariant( "Say It to Me Now" ) ); values.insert( Meta::valArtist, QVariant( "Glen Hansard" ) ); values.insert( Meta::valAlbum, QVariant( "Once" ) ); values.insert( Meta::valAlbumArtist, QVariant( "Glen Hansard & Markéta Irglová" ) ); values.insert( Meta::valTrackNr, QVariant( "13" ) ); createTrack( values ); } void TestSqlScanManager::createCompilationTrack() { Meta::FieldHash values; values.insert( Meta::valUniqueId, QVariant("c6c29f50279ab9523a0f44928bc1e96b") ); values.insert( Meta::valUrl, QVariant("Amazon MP3/The Sum Of All Fears (O.S.T.)/The Sum of All Fears/01 - If We Could Remember (O.S.T. LP Version).mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("If We Could Remember (O.S.T. LP Version)") ); values.insert( Meta::valArtist, QVariant("The Sum Of All Fears (O.S.T.)/Yolanda Adams") ); values.insert( Meta::valAlbumArtist, QVariant("The Sum Of All Fears (O.S.T.)") ); values.insert( Meta::valAlbum, QVariant("The Sum of All Fears") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 203452096") ); values.insert( Meta::valGenre, QVariant("Soundtracks") ); values.insert( Meta::valYear, QVariant("2002") ); values.insert( Meta::valTrackNr, QVariant("1") ); values.insert( Meta::valComposer, QVariant("Jerry Goldsmith") ); values.insert( Meta::valScore, QVariant("0.875") ); values.insert( Meta::valPlaycount, QVariant("6") ); createTrack( values ); values.clear(); values.insert( Meta::valUniqueId, QVariant("2188afd457cd75a363905f411966b9a0") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/01 - Second Chapter.mp3") ); values.insert( Meta::valFormat, QVariant(1) ); values.insert( Meta::valTitle, QVariant("Second Chapter") ); values.insert( Meta::valArtist, QVariant("Enigma") ); values.insert( Meta::valAlbumArtist, QVariant("Enigma") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 201985325") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant(2004) ); values.insert( Meta::valTrackNr, QVariant(1) ); values.insert( Meta::valComposer, QVariant("Curly M.C.") ); values.insert( Meta::valScore, QVariant("0.54") ); values.insert( Meta::valPlaycount, QVariant("2") ); values.insert( Meta::valUniqueId, QVariant("637bee4fd456d2ff9eafe65c71ba192e") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/02 - The Eyes Of Truth.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("The Eyes Of Truth") ); values.insert( Meta::valArtist, QVariant("Enigma") ); values.insert( Meta::valAlbumArtist, QVariant("Enigma") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 201985326") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant("2004") ); values.insert( Meta::valTrackNr, QVariant("2") ); values.insert( Meta::valComposer, QVariant("Curly M.C.") ); values.insert( Meta::valScore, QVariant("0.928572") ); values.insert( Meta::valPlaycount, QVariant("1286469632") ); values.insert( Meta::valUniqueId, QVariant("b4206da4bc0335d76c2bbc5d4c1b164c") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/03 - Return To Innocence.mp3") ); values.insert( Meta::valFormat, QVariant("1") ); values.insert( Meta::valTitle, QVariant("Return To Innocence") ); values.insert( Meta::valArtist, QVariant("Enigma") ); values.insert( Meta::valAlbumArtist, QVariant("Enigma") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 201985327") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant("2004") ); values.insert( Meta::valTrackNr, QVariant("3") ); values.insert( Meta::valComposer, QVariant("Curly M.C.") ); values.insert( Meta::valScore, QVariant("0.75") ); values.insert( Meta::valPlaycount, QVariant("1286469888") ); values.insert( Meta::valUniqueId, QVariant("eb0061602f52d67140fd465dc275fbf2") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/04 - I Love You...I'Ll Kill You.mp3") ); values.insert( Meta::valFormat, 1 ); values.insert( Meta::valTitle, QVariant("I Love You...I'Ll Kill You") ); values.insert( Meta::valArtist, QVariant("Enigma") ); values.insert( Meta::valAlbumArtist, QVariant("Enigma") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 201985328") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant(2004) ); values.insert( Meta::valTrackNr, QVariant(4) ); values.insert( Meta::valComposer, QVariant("Curly M.C.") ); values.insert( Meta::valScore, QVariant(0.5) ); values.insert( Meta::valPlaycount, QVariant(1286470656) ); values.insert( Meta::valUniqueId, QVariant("94dabc09509379646458f62bee7e41ed") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/05 - Silent Warrior.mp3") ); values.insert( Meta::valFormat, 1 ); values.insert( Meta::valTitle, QVariant("Silent Warrior") ); values.insert( Meta::valArtist, QVariant("Enigma") ); values.insert( Meta::valAlbumArtist, QVariant("Enigma") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 201985329") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant(2004) ); values.insert( Meta::valTrackNr, QVariant(5) ); values.insert( Meta::valComposer, QVariant("Curly M.C.") ); values.insert( Meta::valScore, QVariant(0.96875) ); values.insert( Meta::valPlaycount, QVariant(6) ); values.insert( Meta::valUniqueId, QVariant("6ae759476c34256ff1d06f0b5c964d75") ); values.insert( Meta::valUrl, QVariant("The Cross Of Changes/06 - The Dream Of The Dolphin.mp3") ); values.insert( Meta::valTitle, QVariant("The Dream Of The Dolphin") ); values.insert( Meta::valArtist, QVariant("Enigma") ); values.insert( Meta::valAlbumArtist, QVariant("Enigma") ); values.insert( Meta::valAlbum, QVariant("The Cross Of Changes") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 201985330") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant("2004") ); values.insert( Meta::valTrackNr, QVariant(6) ); values.insert( Meta::valComposer, QVariant("Curly M.C.") ); values.insert( Meta::valScore, QVariant(0.5) ); values.insert( Meta::valPlaycount, QVariant(2) ); values.insert( Meta::valUniqueId, QVariant("7957bc25521c1dc91351d497321c27a6") ); values.insert( Meta::valUrl, QVariant("Amazon MP3/Ashford & Simpson/Solid/01 - Solid.mp3") ); values.insert( Meta::valTitle, QVariant("Solid") ); values.insert( Meta::valArtist, QVariant("Ashford & Simpson") ); values.insert( Meta::valAlbumArtist, QVariant("Ashford & Simpson") ); values.insert( Meta::valAlbum, QVariant("Solid") ); values.insert( Meta::valComment, QVariant("Amazon.com Song ID: 202265871") ); values.insert( Meta::valGenre, QVariant("Pop") ); values.insert( Meta::valYear, QVariant(2007) ); values.insert( Meta::valTrackNr, QVariant(1) ); values.insert( Meta::valComposer, QVariant("Valerie Simpson") ); values.insert( Meta::valRating, QVariant(0.898438) ); values.insert( Meta::valScore, QVariant(0.875) ); values.insert( Meta::valPlaycount, QVariant(12) ); } diff --git a/tests/core-impl/collections/db/sql/TestSqlTrack.cpp b/tests/core-impl/collections/db/sql/TestSqlTrack.cpp index 752171930f..8412a7d76c 100644 --- a/tests/core-impl/collections/db/sql/TestSqlTrack.cpp +++ b/tests/core-impl/collections/db/sql/TestSqlTrack.cpp @@ -1,561 +1,561 @@ /**************************************************************************************** * Copyright (c) 2010 Maximilian Kossick * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "TestSqlTrack.h" #include "amarokconfig.h" #include "DefaultSqlQueryMakerFactory.h" #include "core/meta/Meta.h" #include "core-impl/storage/sql/mysqlestorage/MySqlEmbeddedStorage.h" #include "SqlCollection.h" #include "SqlMeta.h" #include "SqlRegistry.h" #include "SqlMountPointManagerMock.h" #include "MetaNotificationSpy.h" #include #include QTEST_GUILESS_MAIN( TestSqlTrack ) TestSqlTrack::TestSqlTrack() : QObject() , m_collection( 0 ) , m_storage( 0 ) , m_tmpDir( 0 ) { } void TestSqlTrack::initTestCase() { AmarokConfig::instance("amarokrc"); m_tmpDir = new QTemporaryDir(); m_storage = QSharedPointer( new MySqlEmbeddedStorage() ); QVERIFY( m_storage->init( m_tmpDir->path() ) ); m_collection = new Collections::SqlCollection( m_storage ); m_collection->setMountPointManager( new SqlMountPointManagerMock( this, m_storage ) ); // I just need the table and not the whole playlist manager m_storage->query( QString( "CREATE TABLE playlist_tracks (" " id " + m_storage->idType() + ", playlist_id INTEGER " ", track_num INTEGER " ", url " + m_storage->exactTextColumnType() + ", title " + m_storage->textColumnType() + ", album " + m_storage->textColumnType() + ", artist " + m_storage->textColumnType() + ", length INTEGER " ", uniqueid " + m_storage->textColumnType(128) + ") ENGINE = MyISAM;" ) ); } void TestSqlTrack::cleanupTestCase() { delete m_collection; //m_storage is deleted by SqlCollection delete m_tmpDir; } void TestSqlTrack::init() { //setup base data m_storage->query( "INSERT INTO artists(id, name) VALUES (1, 'artist1');" ); m_storage->query( "INSERT INTO artists(id, name) VALUES (2, 'artist2');" ); m_storage->query( "INSERT INTO artists(id, name) VALUES (3, 'artist3');" ); m_storage->query( "INSERT INTO albums(id,name,artist) VALUES(1,'album1',1);" ); m_storage->query( "INSERT INTO albums(id,name,artist) VALUES(2,'album2',1);" ); m_storage->query( "INSERT INTO albums(id,name,artist) VALUES(3,'album3',2);" ); m_storage->query( "INSERT INTO albums(id,name,artist) VALUES(4,'album-compilation',0);" ); m_storage->query( "INSERT INTO composers(id, name) VALUES (1, 'composer1');" ); m_storage->query( "INSERT INTO composers(id, name) VALUES (2, 'composer2');" ); m_storage->query( "INSERT INTO composers(id, name) VALUES (3, 'composer3');" ); m_storage->query( "INSERT INTO genres(id, name) VALUES (1, 'genre1');" ); m_storage->query( "INSERT INTO genres(id, name) VALUES (2, 'genre2');" ); m_storage->query( "INSERT INTO genres(id, name) VALUES (3, 'genre3');" ); m_storage->query( "INSERT INTO years(id, name) VALUES (1, '1');" ); m_storage->query( "INSERT INTO years(id, name) VALUES (2, '2');" ); m_storage->query( "INSERT INTO years(id, name) VALUES (3, '3');" ); m_storage->query( "INSERT INTO directories(id, deviceid, dir) VALUES (1, -1, './');" ); m_storage->query( "INSERT INTO urls(id, deviceid, rpath, directory, uniqueid) VALUES (1, -1, './IDoNotExist.mp3', 1, '1');" ); m_storage->query( "INSERT INTO urls(id, deviceid, rpath, directory, uniqueid) VALUES (2, -1, './IDoNotExistAsWell.mp3', 1, '2');" ); m_storage->query( "INSERT INTO urls(id, deviceid, rpath, directory, uniqueid) VALUES (3, -1, './MeNeither.mp3', 1, '3');" ); m_storage->query( "INSERT INTO urls(id, deviceid, rpath, directory, uniqueid) VALUES (4, -1, './NothingHere.mp3', 1, '4');" ); m_storage->query( "INSERT INTO tracks(id,url,title,comment,artist,album,genre,year,composer) " "VALUES(1,1,'track1','comment1',1,1,1,1,1);" ); m_storage->query( "INSERT INTO tracks(id,url,title,comment,artist,album,genre,year,composer) " "VALUES(2,2,'track2','comment2',1,2,1,1,1);" ); m_collection->registry()->emptyCache(); } void TestSqlTrack::cleanup() { m_storage->query( "TRUNCATE TABLE years;" ); m_storage->query( "TRUNCATE TABLE genres;" ); m_storage->query( "TRUNCATE TABLE composers;" ); m_storage->query( "TRUNCATE TABLE albums;" ); m_storage->query( "TRUNCATE TABLE artists;" ); m_storage->query( "TRUNCATE TABLE tracks;" ); m_storage->query( "TRUNCATE TABLE urls;" ); m_storage->query( "TRUNCATE TABLE directories;" ); m_storage->query( "TRUNCATE TABLE statistics;" ); m_storage->query( "TRUNCATE TABLE labels;" ); m_storage->query( "TRUNCATE TABLE urls_labels;" ); } void TestSqlTrack::setAllValues( Meta::SqlTrack *track ) { track->setTitle( "New Title" ); track->setAlbum( "New Album" ); track->setArtist( "New Artist" ); track->setComposer( "New Composer" ); track->setYear( 1999 ); track->setGenre( "New Genre" ); track->setUrl( -1, "./new_url", 2 ); track->setBpm( 32.0 ); track->setComment( "New Comment" ); track->setScore( 64.0 ); track->setRating( 5 ); track->setLength( 5000 ); track->setSampleRate( 4400 ); track->setBitrate( 128 ); track->setTrackNumber( 4 ); track->setDiscNumber( 1 ); - track->setFirstPlayed( QDateTime::fromTime_t(100) ); - track->setLastPlayed( QDateTime::fromTime_t(200) ); + track->setFirstPlayed( QDateTime::fromSecsSinceEpoch(100) ); + track->setLastPlayed( QDateTime::fromSecsSinceEpoch(200) ); track->setPlayCount( 20 ); Meta::ReplayGainTag modes[] = { Meta::ReplayGain_Track_Gain, Meta::ReplayGain_Track_Peak, Meta::ReplayGain_Album_Gain, Meta::ReplayGain_Album_Peak }; for( int i=0; i<4; i++ ) track->setReplayGain( modes[i], qreal(i) ); track->addLabel( "New Label" ); } void TestSqlTrack::getAllValues( Meta::SqlTrack *track ) { QCOMPARE( track->name(), QString( "New Title" ) ); QCOMPARE( track->album()->name(), QString( "New Album" ) ); QCOMPARE( track->artist()->name(), QString( "New Artist" ) ); QCOMPARE( track->composer()->name(), QString( "New Composer" ) ); QCOMPARE( track->year()->name(), QString( "1999" ) ); QCOMPARE( track->genre()->name(), QString( "New Genre" ) ); QCOMPARE( track->playableUrl().path(), QString( "/new_url" ) ); QCOMPARE( track->bpm(), 32.0 ); QCOMPARE( track->comment(), QString( "New Comment" ) ); QCOMPARE( track->score(), 64.0 ); QCOMPARE( track->rating(), 5 ); QCOMPARE( track->length(), qint64(5000) ); QCOMPARE( track->sampleRate(), 4400 ); QCOMPARE( track->bitrate(), 128 ); QCOMPARE( track->trackNumber(), 4 ); QCOMPARE( track->discNumber(), 1 ); - QCOMPARE( track->firstPlayed(), QDateTime::fromTime_t(100) ); - QCOMPARE( track->lastPlayed(), QDateTime::fromTime_t(200) ); + QCOMPARE( track->firstPlayed(), QDateTime::fromSecsSinceEpoch(100) ); + QCOMPARE( track->lastPlayed(), QDateTime::fromSecsSinceEpoch(200) ); QCOMPARE( track->playCount(), 20 ); Meta::ReplayGainTag modes[] = { Meta::ReplayGain_Track_Gain, Meta::ReplayGain_Track_Peak, Meta::ReplayGain_Album_Gain, Meta::ReplayGain_Album_Peak }; for( int i=0; i<4; i++ ) QCOMPARE( track->replayGain( modes[i] ), qreal(i) ); QVERIFY( track->labels().count() > 0 ); QVERIFY( track->labels().contains( m_collection->registry()->getLabel("New Label") ) ); } /** Check that the registry always returns the same track pointer */ void TestSqlTrack::testGetTrack() { { Meta::TrackPtr track1 = m_collection->registry()->getTrack( 1 ); Meta::TrackPtr track2 = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::TrackPtr track3 = m_collection->registry()->getTrackFromUid( "1" ); QVERIFY( track1 ); QVERIFY( track1 == track2 ); QVERIFY( track1 == track3 ); } // and also after empty cache m_collection->registry()->emptyCache(); // changed order... { Meta::TrackPtr track2 = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::TrackPtr track3 = m_collection->registry()->getTrackFromUid( "1" ); Meta::TrackPtr track1 = m_collection->registry()->getTrack( 1 ); QVERIFY( track1 ); QVERIFY( track1 == track2 ); QVERIFY( track1 == track3 ); } // do again creating a new track cleanup(); m_collection->registry()->emptyCache(); // changed order... { Meta::TrackPtr track1 = m_collection->registry()->getTrack( -1, "./newTrack.mp3", 2, "amarok-sqltrackuid://newuid" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); sqlTrack1->setBpm( 100 ); // have to commit the new track QVERIFY( track1 ); QCOMPARE( track1->playableUrl().path(), QString("/newTrack.mp3" )); QCOMPARE( track1->uidUrl(), QString("amarok-sqltrackuid://newuid" )); } m_collection->registry()->emptyCache(); // changed order... { Meta::TrackPtr track1 = m_collection->registry()->getTrackFromUid("amarok-sqltrackuid://newuid"); QVERIFY( track1 ); QCOMPARE( track1->playableUrl().path(), QString("/newTrack.mp3" )); QCOMPARE( track1->uidUrl(), QString("amarok-sqltrackuid://newuid" )); QCOMPARE( track1->bpm(), 100.0 ); } } void TestSqlTrack::testSetAllValuesSingleNotExisting() { { // get a new track Meta::TrackPtr track1 = m_collection->registry()->getTrack( -1, "./IamANewTrack.mp3", 1, "1e34fb213489" ); QSignalSpy spy( m_collection, &Collections::SqlCollection::updated); MetaNotificationSpy metaSpy; metaSpy.subscribeTo( track1 ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); setAllValues( sqlTrack1 ); getAllValues( sqlTrack1 ); // new track should have an up-to-date create time (not more than 3 seconds old) QVERIFY( track1->createDate().secsTo(QDateTime::currentDateTime()) < 3 ); QVERIFY( metaSpy.notificationsFromTracks().count() > 1 ); // we should be notified about the changes } // and also after empty cache m_collection->registry()->emptyCache(); { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/new_url" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); QVERIFY( track1 ); getAllValues( sqlTrack1 ); } } /** Set all track values but before that create them in the registry. */ void TestSqlTrack::testSetAllValuesSingleExisting() { { Meta::GenrePtr genre = m_collection->registry()->getGenre( "New Genre" ); Meta::ComposerPtr composer = m_collection->registry()->getComposer( "New Composer" ); Meta::YearPtr year = m_collection->registry()->getYear( 1999 ); Meta::AlbumPtr album = m_collection->registry()->getAlbum( "New Album", "New Artist" ); m_collection->registry()->getLabel( "New Label" ); Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); setAllValues( sqlTrack1 ); getAllValues( sqlTrack1 ); // check that the existing object are really updated with the new tracklist QCOMPARE( genre->tracks().count(), 1 ); QCOMPARE( genre->tracks().first().data(), track1.data() ); QCOMPARE( composer->tracks().count(), 1 ); QCOMPARE( composer->tracks().first().data(), track1.data() ); QCOMPARE( year->tracks().count(), 1 ); QCOMPARE( year->tracks().first().data(), track1.data() ); // the logic, how renaming the track artist influences its album is still // unfinished. For sure the track must be in an album with the defined // name QCOMPARE( sqlTrack1->album()->name(), QString("New Album") ); QCOMPARE( sqlTrack1->album()->tracks().count(), 1 ); QCOMPARE( sqlTrack1->album()->tracks().first().data(), track1.data() ); } // and also after empty cache m_collection->registry()->emptyCache(); { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/new_url" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); QVERIFY( track1 ); getAllValues( sqlTrack1 ); Meta::GenrePtr genre = m_collection->registry()->getGenre( "New Genre" ); Meta::ComposerPtr composer = m_collection->registry()->getComposer( "New Composer" ); Meta::YearPtr year = m_collection->registry()->getYear( 1999 ); Meta::AlbumPtr album = m_collection->registry()->getAlbum( "New Album", "New Artist" ); // check that the existing object are really updated with the new tracklist QCOMPARE( genre->tracks().count(), 1 ); QCOMPARE( genre->tracks().first().data(), track1.data() ); QCOMPARE( composer->tracks().count(), 1 ); QCOMPARE( composer->tracks().first().data(), track1.data() ); QCOMPARE( year->tracks().count(), 1 ); QCOMPARE( year->tracks().first().data(), track1.data() ); // the logic, how renaming the track artist influences its album is still // unfinished. For sure the track must be in an album with the defined // name QCOMPARE( sqlTrack1->album()->name(), QString("New Album") ); QCOMPARE( sqlTrack1->album()->tracks().count(), 1 ); QCOMPARE( sqlTrack1->album()->tracks().first().data(), track1.data() ); } } void TestSqlTrack::testSetAllValuesBatch() { { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); QSignalSpy spy( m_collection, &Collections::SqlCollection::updated); MetaNotificationSpy metaSpy; metaSpy.subscribeTo( track1 ); sqlTrack1->beginUpdate(); setAllValues( sqlTrack1 ); QCOMPARE( metaSpy.notificationsFromTracks().count(), 1 ); // add label does one notify sqlTrack1->endUpdate(); QCOMPARE( metaSpy.notificationsFromTracks().count(), 2 ); // only one notificate for all the changes getAllValues( sqlTrack1 ); } // and also after empty cache m_collection->registry()->emptyCache(); { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/new_url" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); QVERIFY( track1 ); getAllValues( sqlTrack1 ); } } void TestSqlTrack::testUnsetValues() { { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); setAllValues( sqlTrack1 ); // now unset the values again sqlTrack1->setAlbum( "" ); sqlTrack1->setArtist( "" ); sqlTrack1->setComposer( "" ); sqlTrack1->setYear( 0 ); // it is not clear what an empty year exactly is sqlTrack1->setGenre( "" ); // note: Amarok is still not clear if an empty artist means track->artist() == 0 QVERIFY( !track1->album() || track1->album()->name().isEmpty() ); QVERIFY( !track1->artist() || track1->artist()->name().isEmpty() ); QVERIFY( !track1->composer() || track1->composer()->name().isEmpty() ); QVERIFY( !track1->year() || track1->year()->year() == 0 ); QVERIFY( !track1->genre() || track1->genre()->name().isEmpty() ); } // and also after empty cache m_collection->registry()->emptyCache(); { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/new_url" ); QVERIFY( track1 ); QVERIFY( !track1->album() || track1->album()->name().isEmpty() ); QVERIFY( !track1->artist() || track1->artist()->name().isEmpty() ); QVERIFY( !track1->composer() || track1->composer()->name().isEmpty() ); QVERIFY( !track1->year() || track1->year()->year() == 0 ); QVERIFY( !track1->genre() || track1->genre()->name().isEmpty() ); } } void TestSqlTrack::testFinishedPlaying() { Meta::TrackPtr track1 = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); sqlTrack1->setLength( 5000 ); QCOMPARE( sqlTrack1->score(), 0.0 ); QCOMPARE( sqlTrack1->playCount(), 0 ); QVERIFY( !sqlTrack1->firstPlayed().isValid() ); QVERIFY( !sqlTrack1->lastPlayed().isValid() ); // now play the track not really sqlTrack1->finishedPlaying( 0.1 ); // can't do a statement about the score here QCOMPARE( sqlTrack1->playCount(), 0 ); QVERIFY( !sqlTrack1->firstPlayed().isValid() ); QVERIFY( !sqlTrack1->lastPlayed().isValid() ); // and now really play it sqlTrack1->finishedPlaying( 1.0 ); QVERIFY( sqlTrack1->score() > 0.0 ); QCOMPARE( sqlTrack1->playCount(), 1 ); QVERIFY( sqlTrack1->firstPlayed().secsTo( QDateTime::currentDateTime() ) < 2 ); QVERIFY( sqlTrack1->lastPlayed().secsTo( QDateTime::currentDateTime() ) < 2 ); } void TestSqlTrack::testAlbumRemaingsNonCompilationAfterChangingAlbumName() { m_storage->query( "INSERT INTO tracks(id,url,title,artist,album,genre,year,composer) " "VALUES (3,3,'track1',1,1,1,1,1 );" ); m_storage->query( "INSERT INTO tracks(id,url,title,artist,album,genre,year,composer) " "VALUES (4,4,'track2',1,1,1,1,1 );" ); Meta::TrackPtr track1 = m_collection->registry()->getTrack( 3 ); Meta::TrackPtr track2 = m_collection->registry()->getTrack( 4 ); QCOMPARE( track1->album()->name(), QString( "album1" ) ); QVERIFY( track1->album()->hasAlbumArtist() ); QCOMPARE( track1->album().data(), track2->album().data() ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); sqlTrack1->setAlbum( "album2" ); Meta::SqlTrack *sqlTrack2 = static_cast( track2.data() ); sqlTrack2->beginUpdate(); sqlTrack2->setAlbum( "album2" ); sqlTrack2->endUpdate(); QCOMPARE( track1->album()->name(), QString( "album2" ) ); QVERIFY( track1->album()->hasAlbumArtist() ); QVERIFY( track1->album() == track2->album() ); } void TestSqlTrack::testAlbumRemainsCompilationAfterChangingAlbumName() { m_storage->query( "INSERT INTO tracks(id,url,title,artist,album,genre,year,composer) " "VALUES (3,3,'track1',1,4,1,1,1 );" ); m_storage->query( "INSERT INTO tracks(id,url,title,artist,album,genre,year,composer) " "VALUES (4,4,'track2',1,4,1,1,1 );" ); Meta::TrackPtr track1 = m_collection->registry()->getTrack( 3 ); Meta::TrackPtr track2 = m_collection->registry()->getTrack( 4 ); QVERIFY( track1 ); QVERIFY( track1->album() ); QVERIFY( track2 ); QVERIFY( track2->album() ); QCOMPARE( track1->album()->name(), QString( "album-compilation" ) ); QVERIFY( track1->album()->isCompilation() ); QVERIFY( track1->album().data() == track2->album().data() ); Meta::SqlTrack *sqlTrack1 = static_cast( track1.data() ); Meta::SqlTrack *sqlTrack2 = static_cast( track2.data() ); sqlTrack1->setAlbum( "album2" ); sqlTrack2->beginUpdate(); sqlTrack2->setAlbum( "album2" ); sqlTrack2->endUpdate(); QCOMPARE( track1->album()->name(), QString( "album2" ) ); QVERIFY( track1->album()->isCompilation() ); QVERIFY( track1->album() == track2->album() ); } void TestSqlTrack::testRemoveLabelFromTrack() { Meta::TrackPtr track = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::LabelPtr label = m_collection->registry()->getLabel( "A" ); track->addLabel( label ); QCOMPARE( track->labels().count(), 1 ); track->removeLabel( label ); QCOMPARE( track->labels().count(), 0 ); QStringList urlsLabelsCount = m_storage->query( "SELECT COUNT(*) FROM urls_labels;" ); QCOMPARE( urlsLabelsCount.first().toInt(), 0 ); } void TestSqlTrack::testRemoveLabelFromTrackWhenNotInCache() { m_storage->query( "INSERT INTO labels(id,label) VALUES (1,'A');" ); m_storage->query( "INSERT INTO urls_labels(url,label) VALUES (1,1);" ); Meta::TrackPtr track = m_collection->registry()->getTrack( "/IDoNotExist.mp3" ); Meta::LabelPtr label = m_collection->registry()->getLabel( "A" ); track->removeLabel( label ); QCOMPARE( track->labels().count(), 0 ); QStringList urlsLabelsCount = m_storage->query( "SELECT COUNT(*) FROM urls_labels;" ); QCOMPARE( urlsLabelsCount.first().toInt(), 0 ); } diff --git a/tests/core/meta/TestMetaTrack.cpp b/tests/core/meta/TestMetaTrack.cpp index a0aae1b023..b197535649 100644 --- a/tests/core/meta/TestMetaTrack.cpp +++ b/tests/core/meta/TestMetaTrack.cpp @@ -1,266 +1,266 @@ /*************************************************************************** * Copyright (c) 2009 Sven Krohlas * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "TestMetaTrack.h" #include "amarokconfig.h" #include "config-amarok-test.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core-impl/collections/support/CollectionManager.h" #include QTEST_GUILESS_MAIN( TestMetaTrack ) TestMetaTrack::TestMetaTrack() : m_trackPath( dataPath( "/data/audio/Platz 01.mp3" ) ) {} TestMetaTrack::~TestMetaTrack() { } QString TestMetaTrack::dataPath( const QString &relPath ) { return QDir::toNativeSeparators( QString( AMAROK_TEST_DIR ) + '/' + relPath ); } void TestMetaTrack::initTestCase() { AmarokConfig::instance("amarokrc"); QString oldPath = m_trackPath; m_trackPath = m_tempDir.path() + "TestMetaTrack-testTrack.mp3"; QVERIFY( QFile::copy( oldPath, m_trackPath ) ); m_testTrack1 = CollectionManager::instance()->trackForUrl( QUrl::fromLocalFile(m_trackPath) ); // If the pointer is 0, it makes no sense to continue. We would crash with a qFatal(). QVERIFY2( m_testTrack1, "The pointer to the test track is 0." ); // we need to enable this, otherwise testSetAndGetScore, testSetAndGetRating fails AmarokConfig::setWriteBackStatistics( true ); } void TestMetaTrack::testPrettyName() { QCOMPARE( m_testTrack1->prettyName(), QString( "Platz 01" ) ); } void TestMetaTrack::testPlayableUrl() { QCOMPARE( m_testTrack1->playableUrl().path(), m_trackPath ); } void TestMetaTrack::testPrettyUrl() { QCOMPARE( m_testTrack1->prettyUrl(), m_trackPath ); } void TestMetaTrack::testUidUrl() { QCOMPARE( m_testTrack1->uidUrl(), QUrl::fromLocalFile(m_trackPath ).url() ); } void TestMetaTrack::testIsPlayable() { QCOMPARE( m_testTrack1->isPlayable(), true ); } void TestMetaTrack::testAlbum() { QCOMPARE( m_testTrack1->album()->name() , QString( "" ) ); } void TestMetaTrack::testArtist() { QCOMPARE( m_testTrack1->artist()->name(), QString( "Free Music Charts" ) ); } void TestMetaTrack::testComposer() { QCOMPARE( m_testTrack1->composer()->name(), QString( "" ) ); } void TestMetaTrack::testGenre() { QCOMPARE( m_testTrack1->genre()->name(), QString( "Vocal" ) ); } void TestMetaTrack::testYear() { QCOMPARE( m_testTrack1->year()->name(), QString( "2010" ) ); } void TestMetaTrack::testComment() { QCOMPARE( m_testTrack1->comment(), QString( "" ) ); } void TestMetaTrack::testSetAndGetScore() { Meta::StatisticsPtr statistics = m_testTrack1->statistics(); QCOMPARE( statistics->score(), 0.0 ); /* now the code actually stores the score in track and then it reads it back. * the precision it uses is pretty low and it was failing the qFuzzyCompare * Just make it use qFuzzyCompare() */ statistics->setScore( 3 ); QCOMPARE( float( statistics->score() ), float( 3.0 ) ); statistics->setScore( 12.55 ); QCOMPARE( float( statistics->score() ), float( 12.55 ) ); statistics->setScore( 100 ); QCOMPARE( float( statistics->score() ), float( 100.0 ) ); statistics->setScore( 0 ); QCOMPARE( float( statistics->score() ), float( 0.0 ) ); } void TestMetaTrack::testSetAndGetRating() { Meta::StatisticsPtr statistics = m_testTrack1->statistics(); QCOMPARE( statistics->rating(), 0 ); statistics->setRating( 3 ); QCOMPARE( statistics->rating(), 3 ); statistics->setRating( 10 ); QCOMPARE( statistics->rating(), 10 ); statistics->setRating( 0 ); QCOMPARE( statistics->rating(), 0 ); } void TestMetaTrack::testLength() { QCOMPARE( m_testTrack1->length(), 12000LL ); } void TestMetaTrack::testFilesize() { QCOMPARE( m_testTrack1->filesize(), 389454 ); } void TestMetaTrack::testSampleRate() { QCOMPARE( m_testTrack1->sampleRate(), 44100 ); } void TestMetaTrack::testBitrate() { QCOMPARE( m_testTrack1->bitrate(), 257 ); } void TestMetaTrack::testTrackNumber() { QCOMPARE( m_testTrack1->trackNumber(), 0 ); } void TestMetaTrack::testDiscNumber() { QCOMPARE( m_testTrack1->discNumber(), 0 ); } void TestMetaTrack::testLastPlayed() { - QCOMPARE( m_testTrack1->statistics()->lastPlayed().toTime_t(), 4294967295U ); // portability? + QCOMPARE( m_testTrack1->statistics()->lastPlayed().toSecsSinceEpoch(), 4294967295U ); // portability? } void TestMetaTrack::testFirstPlayed() { - QCOMPARE( m_testTrack1->statistics()->firstPlayed().toTime_t(), 4294967295U ); // portability? + QCOMPARE( m_testTrack1->statistics()->firstPlayed().toSecsSinceEpoch(), 4294967295U ); // portability? } void TestMetaTrack::testPlayCount() { QCOMPARE( m_testTrack1->statistics()->playCount(), 0 ); } void TestMetaTrack::testReplayGain() { QCOMPARE( int(m_testTrack1->replayGain( Meta::ReplayGain_Track_Gain ) * 1000), -6655 ); QCOMPARE( int(m_testTrack1->replayGain( Meta::ReplayGain_Album_Gain ) * 1000), -6655 ); QCOMPARE( int(m_testTrack1->replayGain( Meta::ReplayGain_Track_Peak ) * 10000), 41263 ); QCOMPARE( int(m_testTrack1->replayGain( Meta::ReplayGain_Album_Peak ) * 10000), 41263 ); } void TestMetaTrack::testType() { QCOMPARE( m_testTrack1->type(), QString( "mp3" ) ); } void TestMetaTrack::testInCollection() { QVERIFY( !m_testTrack1->inCollection() ); } void TestMetaTrack::testCollection() { QVERIFY( !m_testTrack1->collection() ); } void TestMetaTrack::testSetAndGetCachedLyrics() { /* TODO: setCachedLyrics is not yet implemented QCOMPARE( m_testTrack1->cachedLyrics(), QString( "" ) ); m_testTrack1->setCachedLyrics( "test" ); QCOMPARE( m_testTrack1->cachedLyrics(), QString( "test" ) ); m_testTrack1->setCachedLyrics( "aäaüoöß" ); QCOMPARE( m_testTrack1->cachedLyrics(), QString( "aäaüoöß" ) ); m_testTrack1->setCachedLyrics( "" ); QCOMPARE( m_testTrack1->cachedLyrics(), QString( "" ) ); */ } void TestMetaTrack::testOperatorEquals() { QVERIFY( m_testTrack1 == m_testTrack1 ); QVERIFY( m_testTrack1 != m_testTrack2 ); } void TestMetaTrack::testLessThan() { Meta::TrackPtr albumTrack1, albumTrack2, albumTrack3; albumTrack1 = CollectionManager::instance()->trackForUrl( QUrl::fromLocalFile(dataPath( "data/audio/album/Track01.ogg" )) ); albumTrack2 = CollectionManager::instance()->trackForUrl( QUrl::fromLocalFile(dataPath( "data/audio/album/Track02.ogg" )) ); albumTrack3 = CollectionManager::instance()->trackForUrl( QUrl::fromLocalFile(dataPath( "data/audio/album/Track03.ogg" )) ); QVERIFY( albumTrack1 ); QVERIFY( albumTrack2 ); QVERIFY( albumTrack3 ); QVERIFY( !Meta::Track::lessThan( m_testTrack1, m_testTrack1 ) ); QVERIFY( Meta::Track::lessThan( albumTrack1, albumTrack2 ) ); QVERIFY( Meta::Track::lessThan( albumTrack2, albumTrack3 ) ); QVERIFY( Meta::Track::lessThan( albumTrack1, albumTrack3 ) ); QVERIFY( !Meta::Track::lessThan( albumTrack3, albumTrack2 ) ); QVERIFY( !Meta::Track::lessThan( albumTrack3, albumTrack1 ) ); QVERIFY( !Meta::Track::lessThan( albumTrack3, albumTrack3 ) ); } diff --git a/tests/importers/TestImporterBase.cpp b/tests/importers/TestImporterBase.cpp index d5035b200d..ef526776fd 100644 --- a/tests/importers/TestImporterBase.cpp +++ b/tests/importers/TestImporterBase.cpp @@ -1,968 +1,968 @@ /**************************************************************************************** * Copyright (c) 2013 Konrad Zemek * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include #include "MetaValues.h" #include "core/meta/support/MetaConstants.h" #include "statsyncing/Provider.h" #include "statsyncing/Track.h" #include using namespace StatSyncing; TestImporterBase::TestImporterBase() { } ProviderPtr TestImporterBase::getWritableProvider() { return getProvider(); } bool TestImporterBase::hasOddRatings() const { return true; } #define skipIfNoSupport( fieldmask, metavalue ) \ { \ if( !( fieldmask & metavalue ) ) \ { \ const QString msg = QString( "Tested provider does not support %1 metadata" ) \ .arg( Meta::nameForField( metavalue ) ); \ QSKIP( msg.toLocal8Bit().constData(), SkipAll ); \ } \ } do {} while(false) #define amarokProviderSkipIfNoMysqld( provider ) \ if( QString( provider->prettyName() ) == "Amarok2Test" ) \ if( !QFileInfo( "/usr/bin/mysqld" ).isExecutable() ) \ QSKIP( "/usr/bin/mysqld not executable, skipping Amarok provider tests", \ SkipAll ) void TestImporterBase::titleShouldBeCaseSensitive() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "caseSensitiveTitle"; QVERIFY( provider->artists().contains( artist ) ); QSet trackNames; foreach( const TrackPtr &track, provider->artistTracks( artist ) ) trackNames.insert( track->name() ); QCOMPARE( trackNames.size(), 3 ); QVERIFY( trackNames.contains( "title" ) ); QVERIFY( trackNames.contains( "Title" ) ); QVERIFY( trackNames.contains( "tiTle" ) ); } void TestImporterBase::artistShouldBeCaseSensitive() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QVector artists = QVector() << "caseSensitiveArtist" << "casesensitiveartist" << "caseSensitiveartist"; foreach( const QString &artist, artists ) { const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->artist(), artist ); } } void TestImporterBase::albumShouldBeCaseSensitive() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "caseSensitiveAlbum"; QVERIFY( provider->artists().contains( artist ) ); QSet trackAlbums; foreach( const TrackPtr &track, provider->artistTracks( artist ) ) trackAlbums.insert( track->album() ); QCOMPARE( trackAlbums.size(), 3 ); QVERIFY( trackAlbums.contains( "album" ) ); QVERIFY( trackAlbums.contains( "Album" ) ); QVERIFY( trackAlbums.contains( "alBum" ) ); } void TestImporterBase::composerShouldBeCaseSensitive() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer ); const QString artist = "caseSensitiveComposer"; QVERIFY( provider->artists().contains( artist ) ); QSet trackComposers; foreach( const TrackPtr &track, provider->artistTracks( artist ) ) trackComposers.insert( track->composer() ); QCOMPARE( trackComposers.size(), 3 ); QVERIFY( trackComposers.contains( "composer" ) ); QVERIFY( trackComposers.contains( "Composer" ) ); QVERIFY( trackComposers.contains( "comPoser" ) ); } void TestImporterBase::titleShouldSupportUTF() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "utfTitle"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->name(), QString::fromWCharArray( L"\xF906\xF907\xF908" ) ); } void TestImporterBase::artistShouldSupportUTF() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = QString::fromWCharArray( L"utf\xF909\xF90A\xF90B" ); QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->artist(), artist ); } void TestImporterBase::albumShouldSupportUTF() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "utfAlbum"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->album(), QString::fromWCharArray( L"\xF903\xF904\xF905" ) ); } void TestImporterBase::composerShouldSupportUTF() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer ); const QString artist = "utfComposer"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->composer(), QString::fromWCharArray( L"\xF900\xF901\xF902" ) ); } void TestImporterBase::titleShouldSupportMultipleWords() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "multiWordTitle"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->name(), QString( "ti tl e" ) ); } void TestImporterBase::artistShouldSupportMultipleWords() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "multi Word Artist"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->artist(), artist ); } void TestImporterBase::albumShouldSupportMultipleWords() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "multiWordAlbum"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->album(), QString( "al b um" ) ); } void TestImporterBase::composerShouldSupportMultipleWords() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer ); const QString artist = "multiWordComposer"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->composer(), QString( "com po ser" ) ); } void TestImporterBase::titleShouldBeWhitespaceTrimmed() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "trimTitle"; QVERIFY( provider->artists().contains( artist ) ); QSet trackNames; foreach( const TrackPtr &track, provider->artistTracks( artist ) ) trackNames.insert( track->name() ); QCOMPARE( trackNames.size(), 3 ); QVERIFY( trackNames.contains( "title1" ) ); QVERIFY( trackNames.contains( "title2" ) ); QVERIFY( trackNames.contains( "title3" ) ); } void TestImporterBase::artistShouldBeWhitespaceTrimmed() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "trimArtist"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 3 ); foreach( const TrackPtr &track, tracks ) QCOMPARE( track->artist(), artist ); } void TestImporterBase::albumShouldBeWhitespaceTrimmed() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "trimAlbum"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 3 ); foreach( const TrackPtr &track, tracks ) QCOMPARE( track->album(), QString( "album" ) ); } void TestImporterBase::composerShouldBeWhitespaceTrimmed() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer ); const QString artist = "trimComposer"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 3 ); foreach( const TrackPtr &track, tracks ) QCOMPARE( track->composer(), QString( "composer" ) ); } void TestImporterBase::albumShouldBeUnsetIfTagIsUnset() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "albumUnset"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->album(), QString() ); } void TestImporterBase::composerShouldBeUnsetIfTagIsUnset() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valComposer ); const QString artist = "composerUnset"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->composer(), QString() ); } void TestImporterBase::yearShouldBeUnsetIfTagIsUnset() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valYear ); const QString artist = "yearUnset"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->year(), 0 ); } void TestImporterBase::trackShouldBeUnsetIfTagIsUnset() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valTrackNr ); const QString artist = "trackUnset"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->trackNumber(), 0 ); } void TestImporterBase::discShouldBeUnsetIfTagIsUnset() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->reliableTrackMetaData(), Meta::valDiscNr ); const QString artist = "discUnset"; QVERIFY( provider->artists().contains( artist ) ); const TrackList tracks = provider->artistTracks( artist ); QCOMPARE( tracks.size(), 1 ); QCOMPARE( tracks.front()->discNumber(), 0 ); } void TestImporterBase::checkStatistics( const QString &artist ) { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); QVERIFY( provider->artists().contains( artist ) ); QMap trackForName; foreach( const TrackPtr &track, provider->artistTracks( artist ) ) trackForName.insert( track->name(), track ); const QString testName( QTest::currentDataTag() ); QCOMPARE( trackForName.size(), 10 ); QVERIFY( trackForName.contains( testName ) ); const TrackPtr &track = trackForName.value( testName ); if( reliableStatistics() & Meta::valFirstPlayed ) QTEST( track->firstPlayed(), "firstPlayed" ); if( reliableStatistics() & Meta::valLastPlayed ) QTEST( track->lastPlayed(), "lastPlayed" ); if( reliableStatistics() & Meta::valPlaycount ) QTEST( track->playCount(), "playCount" ); if( reliableStatistics() & Meta::valRating ) { QFETCH( int, rating ); if( !hasOddRatings() && (rating & 1) ) ++rating; QCOMPARE( track->rating(), rating ); } } void TestImporterBase::tracksShouldHaveStatistics_data() { QTest::addColumn ( "firstPlayed" ); QTest::addColumn ( "lastPlayed" ); QTest::addColumn ( "rating" ); QTest::addColumn ( "playCount" ); QVector d; for( uint t = 0; t < 20; ++t ) - d.push_back( QDateTime::fromTime_t( 1378125780u + t ) ); + d.push_back( QDateTime::fromSecsSinceEpoch( 1378125780u + t ) ); QTest::newRow( "title0" ) << d[ 0] << d[ 1] << 1 << 20; QTest::newRow( "title1" ) << d[ 2] << d[ 3] << 2 << 15; QTest::newRow( "title2" ) << d[ 4] << d[ 5] << 3 << 14; QTest::newRow( "title3" ) << d[ 6] << d[ 7] << 4 << 13; QTest::newRow( "title4" ) << d[ 8] << d[ 9] << 5 << 11; QTest::newRow( "title5" ) << d[10] << d[11] << 6 << 10; QTest::newRow( "title6" ) << d[12] << d[13] << 7 << 7; QTest::newRow( "title7" ) << d[14] << d[15] << 8 << 5; QTest::newRow( "title8" ) << d[16] << d[17] << 9 << 3; QTest::newRow( "title9" ) << d[18] << d[19] << 10 << 2; } void TestImporterBase::tracksShouldHaveStatistics() { checkStatistics( "testStatistics" ); } void TestImporterBase::tracksShouldBehaveNicelyWithNoStatistics_data() { QTest::addColumn ( "firstPlayed" ); QTest::addColumn ( "lastPlayed" ); QTest::addColumn ( "rating" ); QTest::addColumn ( "playCount" ); QVector d; for( uint t = 0; t < 20; ++t ) - d.push_back( QDateTime::fromTime_t( 1378125780u + t ) ); + d.push_back( QDateTime::fromSecsSinceEpoch( 1378125780u + t ) ); QTest::newRow( "title0" ) << QDateTime() << QDateTime() << 0 << 0; QTest::newRow( "title1" ) << QDateTime() << d[ 3] << 2 << 15; QTest::newRow( "title2" ) << QDateTime() << QDateTime() << 3 << 14; QTest::newRow( "title3" ) << QDateTime() << d[ 7] << 0 << 13; QTest::newRow( "title4" ) << QDateTime() << QDateTime() << 5 << 0; QTest::newRow( "title5" ) << d[10] << d[11] << 6 << 10; QTest::newRow( "title6" ) << d[12] << QDateTime() << 0 << 7; QTest::newRow( "title7" ) << d[14] << d[15] << 8 << 5; QTest::newRow( "title8" ) << d[16] << QDateTime() << 9 << 0; QTest::newRow( "title9" ) << d[18] << d[19] << 0 << 2; } void TestImporterBase::tracksShouldBehaveNicelyWithNoStatistics() { checkStatistics( "testStatisticsNotSet" ); } void TestImporterBase::labels( const ProviderPtr &provider, const QString &trackName ) { m_lbl.clear(); const QString artist = "testStatistics"; QVERIFY( provider->artists().contains( artist ) ); QMap trackForName; foreach( const TrackPtr &track, provider->artistTracks( artist ) ) { QVERIFY( !trackForName.contains( track->name() ) ); trackForName.insert( track->name(), track ); } QVERIFY( trackForName.contains( trackName ) ); m_lbl = trackForName.value( trackName )->labels(); } void TestImporterBase::tracksShouldWorkWithSingleLabel() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( reliableStatistics(), Meta::valLabel ); labels( provider, "title0" ); QCOMPARE( m_lbl.size(), 1 ); QVERIFY( m_lbl.contains( "singleTag" ) ); } void TestImporterBase::tracksShouldWorkWithMultipleLabels() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( reliableStatistics(), Meta::valLabel ); labels( provider, "title1" ); QCOMPARE( m_lbl.size(), 2 ); QVERIFY( m_lbl.contains( "multiple" ) ); QVERIFY( m_lbl.contains( "tags" ) ); } void TestImporterBase::tracksShouldWorkWithCaseSensitiveLabels() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( reliableStatistics(), Meta::valLabel ); labels( provider, "title2" ); QCOMPARE( m_lbl.size(), 2 ); QVERIFY( m_lbl.contains( "caseSensitive" ) ); QVERIFY( m_lbl.contains( "casesensitive" ) ); } void TestImporterBase::tracksShouldWorkWithUTFLabels() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( reliableStatistics(), Meta::valLabel ); labels( provider, "title3" ); QCOMPARE( m_lbl.size(), 1 ); QVERIFY( m_lbl.contains( QString::fromWCharArray( L"\x2622" ) ) ); } void TestImporterBase::providerShouldReturnNoTracksForNonexistentArtist() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "I'mNotHere"; QVERIFY( !provider->artists().contains( artist ) ); QVERIFY( provider->artistTracks( artist ).isEmpty() ); } void TestImporterBase::providerShouldNotBreakOnLittleBobbyTables() { ProviderPtr provider( getProvider() ); amarokProviderSkipIfNoMysqld( provider ); const QString artist = "Robert'); DROP TABLE students;--"; QVERIFY( !provider->artists().contains( artist ) ); QVERIFY( provider->artistTracks( artist ).isEmpty() ); } static TrackPtr trackForName( ProviderPtr &provider, const QString &name, const QString &artist ) { foreach( const TrackPtr &track, provider->artistTracks( artist ) ) if( track->name() == name ) return track; return TrackPtr( 0 ); } static Meta::FieldHash saveData( const TrackPtr &track ) { Meta::FieldHash data; data.insert( Meta::valTitle, track->name() ); data.insert( Meta::valArtist, track->artist() ); data.insert( Meta::valAlbum, track->album() ); data.insert( Meta::valComposer, track->composer() ); data.insert( Meta::valTrackNr, track->trackNumber() ); data.insert( Meta::valDiscNr, track->discNumber() ); data.insert( Meta::valFirstPlayed, track->firstPlayed() ); data.insert( Meta::valLastPlayed, track->lastPlayed() ); data.insert( Meta::valRating, track->rating() ); data.insert( Meta::valPlaycount, track->playCount() ); data.insert( Meta::valLabel, QStringList( track->labels().toList() ) ); return data; } static void verifyEqualExcept( const Meta::FieldHash &lhs, const TrackPtr &track, const qint64 except ) { const QList fields = QList() << Meta::valTitle << Meta::valArtist << Meta::valAlbum << Meta::valComposer << Meta::valTrackNr << Meta::valDiscNr << Meta::valFirstPlayed << Meta::valLastPlayed << Meta::valRating << Meta::valPlaycount << Meta::valLabel; const Meta::FieldHash rhs = saveData( track ); foreach( const qint64 field, fields ) if( !( except & field ) ) QCOMPARE( lhs.value( field ), rhs.value( field ) ); } void TestImporterBase::commitAfterSettingAllStatisticsShouldSaveThem_data() { QTest::addColumn( "title" ); QTest::addColumn( "artist" ); QTest::addColumn( "newFirstPlayed" ); QTest::addColumn( "newLastPlayed" ); QTest::addColumn( "newRating" ); QTest::addColumn( "newPlayCount" ); QTest::addColumn( "newLabels" ); - const uint now = QDateTime::currentDateTimeUtc().toTime_t(); + const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); QTest::newRow( "Replace all" ) << "title0" << "testStatistics" - << QDateTime::fromTime_t( now - 100 ) - << QDateTime::fromTime_t( now + 100 ) + << QDateTime::fromSecsSinceEpoch( now - 100 ) + << QDateTime::fromSecsSinceEpoch( now + 100 ) << 9 << 25 << ( QStringList() << "teh" << "lab'ls" ); QTest::newRow( "Add all" ) << "title0" << "testStatisticsNotSet" - << QDateTime::fromTime_t( now - 100 ) - << QDateTime::fromTime_t( now + 100 ) + << QDateTime::fromSecsSinceEpoch( now - 100 ) + << QDateTime::fromSecsSinceEpoch(now + 100 ) << 9 << 25 << ( QStringList() << "teh" << "lab'ls" ); QTest::newRow( "Add some 1" ) << "title2" << "testStatisticsNotSet" - << QDateTime::fromTime_t( now - 100 ) - << QDateTime::fromTime_t( now + 100 ) + << QDateTime::fromSecsSinceEpoch( now - 100 ) + << QDateTime::fromSecsSinceEpoch( now + 100 ) << 9 << 25 << ( QStringList() << "teh" << "lab'ls" ); QTest::newRow( "Add some 1" ) << "title4" << "testStatisticsNotSet" - << QDateTime::fromTime_t( now - 100 ) - << QDateTime::fromTime_t( now + 100 ) + << QDateTime::fromSecsSinceEpoch( now - 100 ) + << QDateTime::fromSecsSinceEpoch( now + 100 ) << 9 << 25 << ( QStringList() << "teh" << "lab'ls" ); QTest::newRow( "Add some 1" ) << "title6" << "testStatisticsNotSet" - << QDateTime::fromTime_t( now - 100 ) - << QDateTime::fromTime_t( now + 100 ) + << QDateTime::fromSecsSinceEpoch( now - 100 ) + << QDateTime::fromSecsSinceEpoch( now + 100 ) << 9 << 25 << ( QStringList() << "teh" << "lab'ls" ); } void TestImporterBase::commitAfterSettingAllStatisticsShouldSaveThem() { ProviderPtr provider( getWritableProvider() ); amarokProviderSkipIfNoMysqld( provider ); QFETCH( QString, title ); QFETCH( QString, artist ); TrackPtr track = trackForName( provider, title, artist ); QVERIFY( track ); const Meta::FieldHash data = saveData( track ); if( provider->writableTrackStatsData() & Meta::valFirstPlayed ) { QFETCH( QDateTime, newFirstPlayed ); track->setFirstPlayed( newFirstPlayed ); } if( provider->writableTrackStatsData() & Meta::valLastPlayed ) { QFETCH( QDateTime, newLastPlayed ); track->setLastPlayed( newLastPlayed ); } if( provider->writableTrackStatsData() & Meta::valRating ) { QFETCH( int, newRating ); track->setRating( newRating ); } if( provider->writableTrackStatsData() & Meta::valPlaycount ) { QFETCH( int, newPlayCount ); track->setPlayCount( newPlayCount ); } if( provider->writableTrackStatsData() & Meta::valLabel ) { QFETCH( QStringList, newLabels ); track->setLabels( newLabels.toSet() ); } track->commit(); provider->commitTracks(); track = trackForName( provider, title, artist ); QVERIFY( track ); if( provider->writableTrackStatsData() & Meta::valFirstPlayed ) { QFETCH( QDateTime, newFirstPlayed ); QCOMPARE( track->firstPlayed(), newFirstPlayed ); } if( provider->writableTrackStatsData() & Meta::valLastPlayed ) { QFETCH( QDateTime, newLastPlayed ); QCOMPARE( track->lastPlayed(), newLastPlayed ); } if( provider->writableTrackStatsData() & Meta::valRating ) { QFETCH( int, newRating ); if( !hasOddRatings() && (newRating & 1) ) ++newRating; QCOMPARE( track->rating(), newRating ); } if( provider->writableTrackStatsData() & Meta::valPlaycount ) { QFETCH( int, newPlayCount ); QCOMPARE( track->playCount(), newPlayCount ); } if( provider->writableTrackStatsData() & Meta::valLabel ) { QFETCH( QStringList, newLabels ); QCOMPARE( track->labels(), newLabels.toSet() ); } verifyEqualExcept( data, track, Meta::valFirstPlayed | Meta::valLastPlayed | Meta::valRating | Meta::valPlaycount | Meta::valLabel ); } void TestImporterBase::commitAfterSettingFirstPlayedShouldSaveIt_data() { QTest::addColumn( "title" ); QTest::addColumn( "newFirstPlayed" ); - const uint now = QDateTime::currentDateTimeUtc().toTime_t(); + const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); - QTest::newRow( "Add stat 1" ) << "title0" << QDateTime::fromTime_t( now ); - QTest::newRow( "Add stat 2" ) << "title1" << QDateTime::fromTime_t( now + 2 ); - QTest::newRow( "Add stat 3" ) << "title2" << QDateTime::fromTime_t( now + 3 ); + QTest::newRow( "Add stat 1" ) << "title0" << QDateTime::fromSecsSinceEpoch( now ); + QTest::newRow( "Add stat 2" ) << "title1" << QDateTime::fromSecsSinceEpoch( now + 2 ); + QTest::newRow( "Add stat 3" ) << "title2" << QDateTime::fromSecsSinceEpoch( now + 3 ); - QTest::newRow( "Replace stat 1" ) << "title5" << QDateTime::fromTime_t( now + 11 ); - QTest::newRow( "Replace stat 2" ) << "title6" << QDateTime::fromTime_t( now + 13 ); - QTest::newRow( "Replace stat 3" ) << "title7" << QDateTime::fromTime_t( now + 17 ); + QTest::newRow( "Replace stat 1" ) << "title5" << QDateTime::fromSecsSinceEpoch( now + 11 ); + QTest::newRow( "Replace stat 2" ) << "title6" << QDateTime::fromSecsSinceEpoch( now + 13 ); + QTest::newRow( "Replace stat 3" ) << "title7" << QDateTime::fromSecsSinceEpoch( now + 17 ); QTest::newRow( "Remove stat 1" ) << "title5" << QDateTime(); QTest::newRow( "Remove stat 2" ) << "title6" << QDateTime(); QTest::newRow( "Remove stat 3" ) << "title7" << QDateTime(); } void TestImporterBase::commitAfterSettingFirstPlayedShouldSaveIt() { ProviderPtr provider( getWritableProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->writableTrackStatsData(), Meta::valFirstPlayed ); QFETCH( QString, title ); QFETCH( QDateTime, newFirstPlayed ); TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); const Meta::FieldHash data = saveData( track ); track->setFirstPlayed( newFirstPlayed ); track->commit(); provider->commitTracks(); track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); QCOMPARE( track->firstPlayed(), newFirstPlayed ); verifyEqualExcept( data, track, Meta::valFirstPlayed ); } void TestImporterBase::commitAfterSettingLastPlayedShouldSaveIt_data() { QTest::addColumn( "title" ); QTest::addColumn( "newLastPlayed" ); - const uint now = QDateTime::currentDateTimeUtc().toTime_t(); + const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); - QTest::newRow( "Add stat 1" ) << "title0" << QDateTime::fromTime_t( now ); - QTest::newRow( "Add stat 2" ) << "title2" << QDateTime::fromTime_t( now + 2 ); - QTest::newRow( "Add stat 3" ) << "title4" << QDateTime::fromTime_t( now + 3 ); + QTest::newRow( "Add stat 1" ) << "title0" << QDateTime::fromSecsSinceEpoch( now ); + QTest::newRow( "Add stat 2" ) << "title2" << QDateTime::fromSecsSinceEpoch( now + 2 ); + QTest::newRow( "Add stat 3" ) << "title4" << QDateTime::fromSecsSinceEpoch( now + 3 ); - QTest::newRow( "Replace stat 1" ) << "title1" << QDateTime::fromTime_t( now + 11 ); - QTest::newRow( "Replace stat 2" ) << "title3" << QDateTime::fromTime_t( now + 13 ); - QTest::newRow( "Replace stat 3" ) << "title5" << QDateTime::fromTime_t( now + 17 ); + QTest::newRow( "Replace stat 1" ) << "title1" << QDateTime::fromSecsSinceEpoch( now + 11 ); + QTest::newRow( "Replace stat 2" ) << "title3" << QDateTime::fromSecsSinceEpoch( now + 13 ); + QTest::newRow( "Replace stat 3" ) << "title5" << QDateTime::fromSecsSinceEpoch( now + 17 ); QTest::newRow( "Remove stat 1" ) << "title1" << QDateTime(); QTest::newRow( "Remove stat 2" ) << "title3" << QDateTime(); QTest::newRow( "Remove stat 3" ) << "title5" << QDateTime(); } void TestImporterBase::commitAfterSettingLastPlayedShouldSaveIt() { ProviderPtr provider( getWritableProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->writableTrackStatsData(), Meta::valLastPlayed ); QFETCH( QString, title ); QFETCH( QDateTime, newLastPlayed ); TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); const Meta::FieldHash data = saveData( track ); track->setLastPlayed( newLastPlayed ); track->commit(); provider->commitTracks(); track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); QCOMPARE( track->lastPlayed(), newLastPlayed ); verifyEqualExcept( data, track, Meta::valLastPlayed ); } void TestImporterBase::commitAfterSettingRatingShouldSaveIt_data() { QTest::addColumn( "title" ); QTest::addColumn( "newRating" ); QTest::newRow( "Add stat 1" ) << "title0" << 2; QTest::newRow( "Add stat 2" ) << "title3" << 3; QTest::newRow( "Add stat 3" ) << "title6" << 5; QTest::newRow( "Replace stat 1" ) << "title1" << 1; QTest::newRow( "Replace stat 2" ) << "title2" << 3; QTest::newRow( "Replace stat 3" ) << "title4" << 6; QTest::newRow( "Remove stat 1" ) << "title1" << 0; QTest::newRow( "Remove stat 2" ) << "title2" << 0; QTest::newRow( "Remove stat 3" ) << "title4" << 0; } void TestImporterBase::commitAfterSettingRatingShouldSaveIt() { ProviderPtr provider( getWritableProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->writableTrackStatsData(), Meta::valRating ); QFETCH( QString, title ); QFETCH( int, newRating ); TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); const Meta::FieldHash data = saveData( track ); track->setRating( newRating ); track->commit(); provider->commitTracks(); if( !hasOddRatings() && (newRating & 1) ) ++newRating; track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); QCOMPARE( track->rating(), newRating ); verifyEqualExcept( data, track, Meta::valRating ); } void TestImporterBase::commitAfterSettingPlaycountShouldSaveIt_data() { QTest::addColumn( "title" ); QTest::addColumn( "newPlayCount" ); QTest::newRow( "Add stat 1" ) << "title0" << 13; QTest::newRow( "Add stat 2" ) << "title4" << 17; QTest::newRow( "Add stat 3" ) << "title8" << 23; QTest::newRow( "Replace stat 1" ) << "title1" << 1; QTest::newRow( "Replace stat 2" ) << "title2" << 3; QTest::newRow( "Replace stat 3" ) << "title3" << 6; QTest::newRow( "Remove stat 1" ) << "title1" << 0; QTest::newRow( "Remove stat 2" ) << "title2" << 0; QTest::newRow( "Remove stat 3" ) << "title3" << 0; } void TestImporterBase::commitAfterSettingPlaycountShouldSaveIt() { ProviderPtr provider( getWritableProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->writableTrackStatsData(), Meta::valPlaycount ); QFETCH( QString, title ); QFETCH( int, newPlayCount ); TrackPtr track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); const Meta::FieldHash data = saveData( track ); track->setPlayCount( newPlayCount ); track->commit(); provider->commitTracks(); track = trackForName( provider, title, "testStatisticsNotSet" ); QVERIFY( track ); QCOMPARE( track->playCount(), newPlayCount ); verifyEqualExcept( data, track, Meta::valPlaycount ); } void TestImporterBase::commitAfterSettingLabelsShouldSaveThem_data() { QTest::addColumn( "title" ); QTest::addColumn( "newLabels" ); QTest::newRow( "Add new label" ) << "title4" << ( QStringList() << "singleTag2" ); QTest::newRow( "Add existing label" ) << "title5" << ( QStringList() << "singleTag" ); QTest::newRow( "Add labels" ) << "title6" << ( QStringList() << "multi" << "labels" ); QTest::newRow( "Add existing labels" ) << "title7" << ( QStringList() << "multiple" << "labels" ); QTest::newRow( "Add case-sensitive labels" ) << "title8" << ( QStringList() << "cs" << "Cs" ); QTest::newRow( "Replace all labels" ) << "title1" << ( QStringList() << "a" << "l" ); QTest::newRow( "Replace some labels" ) << "title1" << ( QStringList() << "a" << "tags" ); QTest::newRow( "Add additional labels" ) << "title1" << ( QStringList() << "multiple" << "tags" << "2" ); QTest::newRow( "Remove labels 1" ) << "title0" << QStringList(); QTest::newRow( "Remove labels 2" ) << "title1" << QStringList(); QTest::newRow( "Remove labels 3" ) << "title2" << QStringList(); } void TestImporterBase::commitAfterSettingLabelsShouldSaveThem() { ProviderPtr provider( getWritableProvider() ); amarokProviderSkipIfNoMysqld( provider ); skipIfNoSupport( provider->writableTrackStatsData(), Meta::valLabel ); QFETCH( QString, title ); QFETCH( QStringList, newLabels ); TrackPtr track = trackForName( provider, title, "testStatistics" ); QVERIFY( track ); const Meta::FieldHash data = saveData( track ); track->setLabels( newLabels.toSet() ); track->commit(); provider->commitTracks(); track = trackForName( provider, title, "testStatistics" ); QVERIFY( track ); QCOMPARE( track->labels(), newLabels.toSet() ); verifyEqualExcept( data, track, Meta::valLabel ); } diff --git a/utilities/afttagger/AFTTagger.cpp b/utilities/afttagger/AFTTagger.cpp index 342f724a3b..82ce7bacc0 100644 --- a/utilities/afttagger/AFTTagger.cpp +++ b/utilities/afttagger/AFTTagger.cpp @@ -1,812 +1,812 @@ /* * Copyright (c) 2008-2009 Jeff Mitchell * QStringToTString and TStringToQString macros Copyright 2002-2008 by Scott Wheeler, wheeler@kde.org, licensed under LGPL 2.1 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "AFTTagger.h" #include "SafeFileSaver.h" //Taglib #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma GCC diagnostic pop #include #include #include #include #include #include #include //QT5-happy versions #define Qt5QStringToTString(s) TagLib::String(s.toUtf8().data(), TagLib::String::UTF8) static int s_currentVersion = 1; int main( int argc, char *argv[] ) { AFTTagger tagger( argc, argv ); return tagger.exec(); } AFTTagger::AFTTagger( int &argc, char **argv ) :QCoreApplication( argc, argv ) , m_delete( false ) , m_newid( false ) , m_quiet( false ) , m_recurse( false ) , m_verbose( false ) , m_fileFolderList() , m_time() , m_textStream( stderr ) { setObjectName( QStringLiteral("amarok_afttagger") ); readArgs(); QString terms; if( !m_quiet ) { m_textStream << qPrintable( tr( "TERMS OF USE:\n\n" "This program has been extensively tested and errs on the side of safety wherever possible.\n\n" "With that being said, since this program can modify thousands or hundreds of thousands of files\n" "at a time, here is the obligatory warning text:\n\n" "This program makes use of multiple libraries not written by the author, and as such neither\n" "the author nor the Amarok project can or do take any responsibility for any damage that may\n" "occur to your files through the use of this program.\n\n" "If you want more information, please see http://community.kde.org/Amarok/Development/AFT\n\n" "If you agree to be bound by these terms of use, enter 'y' or 'Y', or anything else to exit:\n" ) ); m_textStream.flush(); std::string response; std::cin >> response; std::cin.get(); if( response != "y" && response != "Y") { m_textStream << tr( "INFO: Terms not accepted; exiting..." ) << endl; ::exit( 1 ); } } - qsrand(QDateTime::currentDateTimeUtc().toTime_t()); + qsrand(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()); m_time.start(); foreach( const QString &path, m_fileFolderList ) processPath( path ); m_textStream << tr( "INFO: All done, exiting..." ) << endl; ::exit( 0 ); } void AFTTagger::processPath( const QString &path ) { QFileInfo info( path ); if( !info.isDir() && !info.isFile() ) { if( m_verbose ) m_textStream << tr( "INFO: Skipping %1 because it is neither a directory nor file." ).arg( path ) << endl; return; } if( info.isDir() ) { if( !m_recurse ) { if( m_verbose ) m_textStream << tr( "INFO: Skipping %1 because it is a directory and recursion is not specified." ).arg( path ) << endl; return; } else { if( m_verbose ) m_textStream << tr( "INFO: Processing directory %1" ).arg( path ) << endl; foreach( const QString &pathEntry, QDir( path ).entryList() ) { if( pathEntry != QLatin1String(".") && pathEntry != QLatin1String("..") ) processPath( QDir( path ).canonicalPath() + QLatin1Char('/') + pathEntry ); } } } else //isFile() { QString filePath = info.absoluteFilePath(); #ifdef COMPLEX_TAGLIB_FILENAME const wchar_t *encodedName = reinterpret_cast< const wchar_t *>(filePath.utf16()); #else QByteArray fileName = QFile::encodeName( filePath ); const char *encodedName = fileName.constData(); #endif TagLib::FileRef fileRef = TagLib::FileRef( encodedName, true, TagLib::AudioProperties::Fast ); if( fileRef.isNull() ) { if( m_verbose ) m_textStream << tr( "INFO: file %1 not able to be opened by TagLib" ).arg( filePath ) << endl; return; } m_textStream << tr( "INFO: Processing file %1" ).arg( filePath ) << endl; SafeFileSaver sfs( filePath ); sfs.setVerbose( false ); sfs.setPrefix( QStringLiteral("amarok-afttagger") ); QString tempFilePath = sfs.prepareToSave(); if( tempFilePath.isEmpty() ) { m_textStream << tr( "Error: could not create temporary file when processing %1" ).arg( filePath ) << endl; return; } if( m_verbose ) m_textStream << tr( "INFO: Temporary file is at %1").arg( tempFilePath ) << endl; #ifdef COMPLEX_TAGLIB_FILENAME const wchar_t *encodedName = reinterpret_cast< const wchar_t * >(tempFilePath.utf16()); #else QByteArray tempFileName = QFile::encodeName( tempFilePath ); const char *tempEncodedName = tempFileName.constData(); #endif bool saveNecessary = false; TagLib::FileRef tempFileRef = TagLib::FileRef( tempEncodedName, true, TagLib::AudioProperties::Fast ); if( TagLib::MPEG::File *file = dynamic_cast( tempFileRef.file() ) ) saveNecessary = handleMPEG( file ); else if( TagLib::Ogg::File *file = dynamic_cast( tempFileRef.file() ) ) saveNecessary = handleOgg( file ); else if( TagLib::FLAC::File *file = dynamic_cast( tempFileRef.file() ) ) saveNecessary = handleFLAC( file ); else if( TagLib::MPC::File *file = dynamic_cast( tempFileRef.file() ) ) saveNecessary = handleMPC( file ); else if( TagLib::MP4::File *file = dynamic_cast( tempFileRef.file() ) ) saveNecessary = handleMP4( file ); else { if( m_verbose ) m_textStream << tr( "INFO: File not able to be parsed by TagLib or wrong kind (currently this program only supports MPEG, Ogg MP4, MPC, and FLAC files), cleaning up temp file" ) << endl; if( !sfs.cleanupSave() ) m_textStream << tr( "WARNING: file at %1 could not be cleaned up; check for strays" ).arg( filePath ) << endl; return; } if( saveNecessary ) { if( m_verbose ) m_textStream << tr( "INFO: Safe-saving file" ) << endl; if( !sfs.doSave() ) m_textStream << tr( "WARNING: file at %1 could not be saved" ).arg( filePath ) << endl; } if( m_verbose ) m_textStream << tr( "INFO: Cleaning up..." ) << endl; if( !sfs.cleanupSave() ) m_textStream << tr( "WARNING: file at %1 could not be cleaned up; check for strays" ).arg( filePath ) << endl; return; } } bool AFTTagger::handleMPEG( TagLib::MPEG::File *file ) { if( file->readOnly() ) { m_textStream << tr( "ERROR: File is read-only or could not be opened" ) << endl; return false; } QString uid; bool newUid = false; bool nothingfound = true; if( m_verbose ) m_textStream << tr( "INFO: File is a MPEG file, opening..." ) << endl; if ( file->ID3v2Tag( true ) ) { if( file->ID3v2Tag()->frameListMap()["UFID"].isEmpty() ) { if( m_verbose ) m_textStream << tr( "INFO: No UFID frames found" ) << endl; if( m_delete ) return false; newUid = true; } else { if( m_verbose ) m_textStream << tr( "INFO: Found existing UFID frames, parsing" ) << endl; TagLib::ID3v2::FrameList frameList = file->ID3v2Tag()->frameListMap()["UFID"]; TagLib::ID3v2::FrameList::Iterator iter; if( m_verbose ) m_textStream << tr( "INFO: Frame list size is %1" ).arg( frameList.size() ) << endl; for( iter = frameList.begin(); iter != frameList.end(); ++iter ) { TagLib::ID3v2::UniqueFileIdentifierFrame* currFrame = dynamic_cast(*iter); if( currFrame ) { QString owner = TStringToQString( currFrame->owner() ).toUpper(); if( owner.startsWith( QLatin1String("AMAROK - REDISCOVER YOUR MUSIC") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Removing old-style ATF identifier" ) << endl; iter = frameList.erase( iter ); file->ID3v2Tag()->removeFrame( currFrame ); file->save(); if( !m_delete ) newUid = true; else return true; } if( owner.startsWith( QLatin1String("AMAROK 2 AFT") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Found an existing AFT identifier: %1" ).arg( TStringToQString( TagLib::String( currFrame->identifier() ) ) ) << endl; if( m_delete ) { iter = frameList.erase( iter ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; file->ID3v2Tag()->removeFrame( currFrame ); file->save(); return true; } int version = owner.at( 13 ).digitValue(); if( version < s_currentVersion ) { if( m_verbose ) m_textStream << tr( "INFO: Upgrading AFT identifier from version %1 to version %2" ).arg( version, s_currentVersion ) << endl; uid = upgradeUID( version, TStringToQString( TagLib::String( currFrame->identifier() ) ) ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; iter = frameList.erase( iter ); file->ID3v2Tag()->removeFrame( currFrame ); newUid = true; } else if( version == s_currentVersion && m_newid ) { if( m_verbose ) m_textStream << tr( "INFO: New IDs specified to be generated, doing so" ) << endl; iter = frameList.erase( iter ); file->ID3v2Tag()->removeFrame( currFrame ); newUid = true; } else { if( m_verbose ) m_textStream << tr( "INFO: ID is current" ) << endl; } } } } } if( newUid || ( nothingfound && !m_delete ) ) { QString ourId = QString( "Amarok 2 AFTv" + QString::number( s_currentVersion ) + " - amarok.kde.org" ); if( uid.isEmpty() ) uid = createCurrentUID( file ); if( m_verbose ) m_textStream << tr( "INFO: Adding new frame and saving file with UID: %1" ).arg( uid ) << endl; file->ID3v2Tag()->addFrame( new TagLib::ID3v2::UniqueFileIdentifierFrame( Qt5QStringToTString( ourId ), Qt5QStringToTString( uid ).data( TagLib::String::Latin1 ) ) ); file->save(); return true; } } return false; } bool AFTTagger::handleOgg( TagLib::Ogg::File *file ) { if( file->readOnly() ) { m_textStream << tr( "ERROR: File is read-only or could not be opened" ) << endl; return false; } TagLib::Ogg::XiphComment *comment = dynamic_cast( file->tag() ); if( !comment ) return false; if( handleXiphComment( comment, file ) ) { file->save(); return true; } return false; } bool AFTTagger::handleFLAC( TagLib::FLAC::File *file ) { if( file->readOnly() ) { m_textStream << tr( "ERROR: File is read-only or could not be opened" ) << endl; return false; } TagLib::Ogg::XiphComment *comment = file->xiphComment( true ); if( !comment ) return false; if( handleXiphComment( comment, file ) ) { file->save(); return true; } return false; } bool AFTTagger::handleXiphComment( TagLib::Ogg::XiphComment *comment, TagLib::File *file ) { QString uid; bool newUid = false; bool nothingfound = true; TagLib::StringList toRemove; if( m_verbose ) m_textStream << tr( "INFO: File has a XiphComment, opening..." ) << endl; if( comment->fieldListMap().isEmpty() ) { if( m_verbose ) m_textStream << tr( "INFO: No fields found in XiphComment" ) << endl; if( m_delete ) return false; } else { if( m_verbose ) m_textStream << tr( "INFO: Found existing XiphComment frames, parsing" ) << endl; TagLib::Ogg::FieldListMap fieldListMap = comment->fieldListMap(); if( m_verbose ) m_textStream << tr( "INFO: fieldListMap size is %1" ).arg( fieldListMap.size() ) << endl; TagLib::Ogg::FieldListMap::Iterator iter; for( iter = fieldListMap.begin(); iter != fieldListMap.end(); ++iter ) { TagLib::String key = iter->first; QString qkey = TStringToQString( key ).toUpper(); if( qkey.startsWith( QLatin1String("AMAROK - REDISCOVER YOUR MUSIC") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Removing old-style ATF identifier %1" ).arg( qkey ) << endl; toRemove.append( key ); if( !m_delete ) newUid = true; } else if( qkey.startsWith( QLatin1String("AMAROK 2 AFT") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Found an existing AFT identifier: %1" ).arg( qkey ) << endl; if( m_delete ) { toRemove.append( key ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; } else { int version = qkey.at( 13 ).digitValue(); if( m_verbose ) m_textStream << tr( "INFO: AFT identifier is version %1" ).arg( version ) << endl; if( version < s_currentVersion ) { if( m_verbose ) m_textStream << tr( "INFO: Upgrading AFT identifier from version %1 to version %2" ).arg( version, s_currentVersion ) << endl; uid = upgradeUID( version, TStringToQString( fieldListMap[key].front() ) ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; toRemove.append( key ); newUid = true; } else if( version == s_currentVersion && m_newid ) { if( m_verbose ) m_textStream << tr( "INFO: New IDs specified to be generated, doing so" ) << endl; toRemove.append( key ); newUid = true; } else { if( m_verbose ) m_textStream << tr( "INFO: ID is current" ) << endl; return false; } } } } for( TagLib::StringList::ConstIterator iter = toRemove.begin(); iter != toRemove.end(); ++iter ) comment->removeField( *iter ); } if( newUid || ( nothingfound && !m_delete ) ) { QString ourId = QString( "Amarok 2 AFTv" + QString::number( s_currentVersion ) + " - amarok.kde.org" ); if( uid.isEmpty() ) uid = createCurrentUID( file ); if( m_verbose ) m_textStream << tr( "INFO: Adding new field and saving file with UID: %1" ).arg( uid ) << endl; comment->addField( Qt5QStringToTString( ourId ), Qt5QStringToTString( uid ) ); return true; } else if( toRemove.size() ) return true; return false; } bool AFTTagger::handleMPC( TagLib::MPC::File *file ) { if( file->readOnly() ) { m_textStream << tr( "ERROR: File is read-only or could not be opened" ) << endl; return false; } QString uid; bool newUid = false; bool nothingfound = true; TagLib::StringList toRemove; if( m_verbose ) m_textStream << tr( "INFO: File is a MPC file, opening..." ) << endl; if( file->APETag() ) { const TagLib::APE::ItemListMap &itemsMap = file->APETag()->itemListMap(); if( itemsMap.isEmpty() ) { m_textStream << tr( "INFO: No fields found in APE tags." ) << endl; if( m_delete ) return false; } for( TagLib::APE::ItemListMap::ConstIterator it = itemsMap.begin(); it != itemsMap.end(); ++it ) { TagLib::String key = it->first; QString qkey = TStringToQString( key ).toUpper(); if( qkey.startsWith( QLatin1String("AMAROK - REDISCOVER YOUR MUSIC") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Removing old-style ATF identifier %1" ).arg( qkey ) << endl; toRemove.append( key ); if( !m_delete ) newUid = true; } else if( qkey.startsWith( QLatin1String("AMAROK 2 AFT") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Found an existing AFT identifier: %1" ).arg( qkey ) << endl; if( m_delete ) { toRemove.append( key ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; } else { int version = qkey.at( 13 ).digitValue(); if( m_verbose ) m_textStream << tr( "INFO: AFT identifier is version %1" ).arg( version ) << endl; if( version < s_currentVersion ) { if( m_verbose ) m_textStream << tr( "INFO: Upgrading AFT identifier from version %1 to version %2" ).arg( version, s_currentVersion ) << endl; uid = upgradeUID( version, TStringToQString( itemsMap[ key ].toString() ) ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; toRemove.append( key ); newUid = true; } else if( version == s_currentVersion && m_newid ) { if( m_verbose ) m_textStream << tr( "INFO: New IDs specified to be generated, doing so" ) << endl; toRemove.append( key ); newUid = true; } else { if( m_verbose ) m_textStream << tr( "INFO: ID is current" ) << endl; return false; } } } } for( TagLib::StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it ) file->APETag()->removeItem( *it ); } if( newUid || ( nothingfound && !m_delete ) ) { QString ourId = QString( "Amarok 2 AFTv" + QString::number( s_currentVersion ) + " - amarok.kde.org" ); if( uid.isEmpty() ) uid = createCurrentUID( file ); if( m_verbose ) m_textStream << tr( "INFO: Adding new field and saving file with UID: %1" ).arg( uid ) << endl; file->APETag()->addValue( Qt5QStringToTString( ourId.toUpper() ), Qt5QStringToTString( uid ) ); file->save(); return true; } else if( toRemove.size() ) { file->save(); return true; } return false; } bool AFTTagger::handleMP4( TagLib::MP4::File *file ) { if( file->readOnly() ) { m_textStream << tr( "ERROR: File is read-only or could not be opened" ) << endl; return false; } QString uid; bool newUid = false; bool nothingfound = true; TagLib::StringList toRemove; if( m_verbose ) m_textStream << tr( "INFO: File is a MP4 file, opening..." ) << endl; TagLib::MP4::ItemListMap &itemsMap = file->tag()->itemListMap(); if( !itemsMap.isEmpty() ) { for( TagLib::MP4::ItemListMap::Iterator it = itemsMap.begin(); it != itemsMap.end(); ++it ) { TagLib::String key = it->first; const QString qkey = TStringToQString( key ).toUpper(); if( qkey.contains( QLatin1String("AMAROK - REDISCOVER YOUR MUSIC") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Removing old-style ATF identifier %1" ).arg( key.toCString() ) << endl; toRemove.append( key ); if( !m_delete ) newUid = true; } else if( qkey.contains( QLatin1String("AMAROK 2 AFT") ) ) { nothingfound = false; if( m_verbose ) m_textStream << tr( "INFO: Found an existing AFT identifier: %1" ).arg( key.toCString() ) << endl; if( m_delete ) { toRemove.append( key ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; } else { int version = qkey.at( qkey.indexOf( QLatin1String("AMAROK 2 AFT") ) + 13 ).digitValue(); if( m_verbose ) m_textStream << tr( "INFO: AFT identifier is version %1" ).arg( version ) << endl; if( version < s_currentVersion ) { if( m_verbose ) m_textStream << tr( "INFO: Upgrading AFT identifier from version %1 to version %2" ) .arg( QString::number( version ), QString::number( s_currentVersion ) ) << endl; uid = upgradeUID( version, TStringToQString( itemsMap[ key ].toStringList().toString() ) ); if( m_verbose ) m_textStream << tr( "INFO: Removing current AFT frame" ) << endl; toRemove.append( key ); newUid = true; } else if( version == s_currentVersion && m_newid ) { if( m_verbose ) m_textStream << tr( "INFO: New IDs specified to be generated, doing so" ) << endl; toRemove.append( key ); newUid = true; } else { if( m_verbose ) m_textStream << tr( "INFO: ID is current" ) << endl; return false; } } } } for( TagLib::StringList::ConstIterator it = toRemove.begin(); it != toRemove.end(); ++it ) itemsMap.erase( *it ); } if( newUid || ( nothingfound && !m_delete ) ) { QString ourId = QString( "Amarok 2 AFTv" + QString::number( s_currentVersion ) + " - amarok.kde.org" ); if( uid.isEmpty() ) uid = createCurrentUID( file ); if( m_verbose ) m_textStream << tr( "INFO: Adding new field and saving file with UID: %1" ).arg( uid ) << endl; itemsMap.insert( Qt5QStringToTString( QString( "----:com.apple.iTunes:" + ourId ) ), TagLib::StringList( Qt5QStringToTString( uid ) ) ); file->save(); return true; } else if( toRemove.size() ) { file->save(); return true; } return false; } QString AFTTagger::createCurrentUID( TagLib::File *file ) { return createV1UID( file ); } QString AFTTagger::createV1UID( TagLib::File *file ) { QCryptographicHash md5( QCryptographicHash::Md5 ); QByteArray size; md5.addData( size.setNum( (qulonglong)(file->length()) ) ); md5.addData( QString::number( m_time.elapsed() ).toUtf8() ); md5.addData( QString::number( qrand() ).toUtf8() ); md5.addData( QString::number( qrand() ).toUtf8() ); md5.addData( QString::number( qrand() ).toUtf8() ); md5.addData( QString::number( qrand() ).toUtf8() ); md5.addData( QString::number( qrand() ).toUtf8() ); md5.addData( QString::number( m_time.elapsed() ).toUtf8() ); return QString( md5.result().toHex() ); } QString AFTTagger::upgradeUID( int version, const QString &currValue ) { Q_UNUSED(version) return currValue + "abcd"; } void AFTTagger::readArgs() { QStringList argslist = arguments(); if( argslist.size() < 2 ) displayHelp(); bool nomore = false; int argnum = 0; foreach( const QString &arg, argslist ) { ++argnum; if( arg.isEmpty() || argnum == 1 ) continue; if( nomore ) { m_fileFolderList.append( arg ); } else if( arg.startsWith( QLatin1String("--") ) ) { QString myarg = QString( arg ).remove( 0, 2 ); if ( myarg == QLatin1String("recurse") || myarg == QLatin1String("recursively") ) m_recurse = true; else if( myarg == QLatin1String("verbose") ) m_verbose = true; else if( myarg == QLatin1String("quiet") ) m_quiet = true; else if( myarg == QLatin1String("newid") ) m_newid = true; else if( myarg == QLatin1String("delete") ) m_delete = true; else displayHelp(); } else if( arg.startsWith( '-' ) ) { QString myarg = QString( arg ).remove( 0, 1 ); int pos = 0; while( pos < myarg.length() ) { if( myarg[pos] == 'd' ) m_delete = true; else if( myarg[pos] == 'n' ) m_newid = true; else if( myarg[pos] == 'q' ) m_quiet = true; else if( myarg[pos] == 'r' ) m_recurse = true; else if( myarg[pos] == 'v' ) m_verbose = true; else displayHelp(); ++pos; } } else { nomore = true; m_fileFolderList.append( arg ); } } } void AFTTagger::displayHelp() { m_textStream << tr( "Amarok AFT Tagger" ) << endl << endl; m_textStream << tr( "IRC:\nserver: irc.freenode.net / channels: #amarok, #amarok.de, #amarok.es, #amarok.fr\n\nFeedback:\namarok@kde.org" ) << endl << endl; m_textStream << tr( "Usage: amarok_afttagger [options] +File/Folder(s)" ) << endl << endl; m_textStream << tr( "User-modifiable Options:" ) << endl; m_textStream << tr( "+File/Folder(s) : Files or folders to tag" ) << endl; m_textStream << tr( "-h, --help : This help text" ) << endl; m_textStream << tr( "-r, --recursive : Process files and folders recursively" ) << endl; m_textStream << tr( "-d, --delete : Remove AFT tag" ) << endl; m_textStream << tr( "-n --newid : Replace any existing ID with a new one" ) << endl; m_textStream << tr( "-v, --verbose : Verbose output" ) << endl; m_textStream << tr( "-q, --quiet : Quiet output; Implies that you accept the terms of use" ) << endl; m_textStream.flush(); ::exit( 0 ); } diff --git a/utilities/collectionscanner/CollectionScanner.cpp b/utilities/collectionscanner/CollectionScanner.cpp index a418092841..924ce0c3b7 100644 --- a/utilities/collectionscanner/CollectionScanner.cpp +++ b/utilities/collectionscanner/CollectionScanner.cpp @@ -1,438 +1,438 @@ /*************************************************************************** * Copyright (C) 2003-2005 Max Howell * * (C) 2003-2010 Mark Kretschmann * * (C) 2005-2007 Alexandre Oliveira * * (C) 2008 Dan Meltzer * * (C) 2008-2009 Jeff Mitchell * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "CollectionScanner.h" #include "Version.h" // for AMAROK_VERSION #include "collectionscanner/BatchFile.h" #include "collectionscanner/Directory.h" #include "collectionscanner/Track.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX // for ioprio #include #include enum { IOPRIO_CLASS_NONE, IOPRIO_CLASS_RT, IOPRIO_CLASS_BE, IOPRIO_CLASS_IDLE }; enum { IOPRIO_WHO_PROCESS = 1, IOPRIO_WHO_PGRP, IOPRIO_WHO_USER }; #define IOPRIO_CLASS_SHIFT 13 #endif int main( int argc, char *argv[] ) { CollectionScanner::Scanner scanner( argc, argv ); return scanner.exec(); } CollectionScanner::Scanner::Scanner( int &argc, char **argv ) : QCoreApplication( argc, argv ) , m_charset( false ) , m_newerTime(0) , m_incremental( false ) , m_recursively( false ) , m_restart( false ) , m_idlePriority( false ) { setObjectName( QStringLiteral("amarokcollectionscanner") ); readArgs(); if( m_idlePriority ) { bool ioPriorityWorked = false; #if defined(Q_OS_LINUX) && defined(SYS_ioprio_set) // try setting the idle priority class ioPriorityWorked = ( syscall( SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, IOPRIO_CLASS_IDLE << IOPRIO_CLASS_SHIFT ) >= 0 ); // try setting the lowest priority in the best-effort priority class (the default class) if( !ioPriorityWorked ) ioPriorityWorked = ( syscall( SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, 7 | ( IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT ) ) >= 0 ); #endif if( !ioPriorityWorked && QThread::currentThread() ) QThread::currentThread()->setPriority( QThread::IdlePriority ); } } CollectionScanner::Scanner::~Scanner() { } void CollectionScanner::Scanner::readBatchFile( const QString &path ) { QFile batchFile( path ); if( !batchFile.exists() ) error( tr( "File \"%1\" not found." ).arg( path ) ); if( !batchFile.open( QIODevice::ReadOnly ) ) error( tr( "Could not open file \"%1\"." ).arg( path ) ); BatchFile batch( path ); foreach( const QString &str, batch.directories() ) { m_folders.append( str ); } foreach( const CollectionScanner::BatchFile::TimeDefinition &def, batch.timeDefinitions() ) { m_mTimes.insert( def.first, def.second ); } } void CollectionScanner::Scanner::readNewerTime( const QString &path ) { QFileInfo file( path ); if( !file.exists() ) error( tr( "File \"%1\" not found." ).arg( path ) ); - m_newerTime = qMax( m_newerTime, file.lastModified().toTime_t() ); + m_newerTime = qMax( m_newerTime, file.lastModified().toSecsSinceEpoch() ); } void CollectionScanner::Scanner::doJob() //SLOT { QFile xmlFile; xmlFile.open( stdout, QIODevice::WriteOnly ); QXmlStreamWriter xmlWriter( &xmlFile ); xmlWriter.setAutoFormatting( true ); // get a list of folders to scan. We do it even if resuming because we don't want // to save the (perhaps very big) list of directories into shared memory, bug 327812 QStringList entries; { QSet entriesSet; foreach( QString dir, m_folders ) // krazy:exclude=foreach { if( dir.isEmpty() ) //apparently somewhere empty strings get into the mix //which results in a full-system scan! Which we can't allow continue; // Make sure that all paths are absolute, not relative if( QDir::isRelativePath( dir ) ) dir = QDir::cleanPath( QDir::currentPath() + QLatin1Char('/') + dir ); if( !dir.endsWith( QLatin1Char('/') ) ) dir += '/'; addDir( dir, &entriesSet ); // checks m_recursively } entries = entriesSet.toList(); qSort( entries ); // the sort is crucial because of restarts and lastDirectory handling } if( m_restart ) { m_scanningState.readFull(); QString lastEntry = m_scanningState.lastDirectory(); int index = entries.indexOf( lastEntry ); if( index >= 0 ) // strip already processed entries, but *keep* the lastEntry entries = entries.mid( index ); else qWarning() << Q_FUNC_INFO << "restarting scan after a crash, but lastDirectory" << lastEntry << "not found in folders to scan (size" << entries.size() << "). Starting scanning from the beginning."; } else // first attempt { m_scanningState.writeFull(); // just trigger write to initialise memory xmlWriter.writeStartDocument(); xmlWriter.writeStartElement(QStringLiteral("scanner")); xmlWriter.writeAttribute(QStringLiteral("count"), QString::number( entries.count() ) ); if( m_incremental ) xmlWriter.writeAttribute(QStringLiteral("incremental"), QString()); // write some information into the file and close previous tag xmlWriter.writeComment("Created by amarokcollectionscanner " AMAROK_VERSION " on "+QDateTime::currentDateTime().toString()); xmlFile.flush(); } // --- now do the scanning foreach( const QString &path, entries ) { CollectionScanner::Directory dir( path, &m_scanningState, m_incremental && !isModified( path ) ); xmlWriter.writeStartElement( QStringLiteral("directory") ); dir.toXml( &xmlWriter ); xmlWriter.writeEndElement(); xmlFile.flush(); } // --- write the end element (must be done by hand as we might not have written the start element when restarting) xmlFile.write("\n\n"); quit(); } void CollectionScanner::Scanner::addDir( const QString& dir, QSet* entries ) { // Linux specific, but this fits the 90% rule if( dir.startsWith( QLatin1String("/dev") ) || dir.startsWith( QLatin1String("/sys") ) || dir.startsWith( QLatin1String("/proc") ) ) return; if( entries->contains( dir ) ) return; QDir d( dir ); if( !d.exists() ) { QTextStream stream( stderr ); stream << "Directory \""<insert( dir ); if( !m_recursively ) return; // finished d.setFilter( QDir::NoDotAndDotDot | QDir::Dirs ); const QFileInfoList fileInfos = d.entryInfoList(); for ( const QFileInfo &fi : fileInfos ) { if( !fi.exists() ) continue; const QFileInfo &f = fi.isSymLink() ? QFileInfo( fi.symLinkTarget() ) : fi; if( !f.exists() ) continue; if( f.isDir() ) { addDir( QString( f.absoluteFilePath() + '/' ), entries ); } } } bool CollectionScanner::Scanner::isModified( const QString& dir ) { QFileInfo info( dir ); if( !info.exists() ) return false; - uint lastModified = info.lastModified().toTime_t(); + uint lastModified = info.lastModified().toSecsSinceEpoch(); if( m_mTimes.contains( dir ) ) return m_mTimes.value( dir ) != lastModified; else return m_newerTime < lastModified; } void CollectionScanner::Scanner::readArgs() { QStringList argslist = arguments(); if( argslist.size() < 2 ) displayHelp(); bool missingArg = false; for( int argnum = 1; argnum < argslist.count(); argnum++ ) { QString arg = argslist.at( argnum ); if( arg.startsWith( QLatin1String("--") ) ) { QString myarg = QString( arg ).remove( 0, 2 ); if( myarg == QLatin1String("newer") ) { if( argslist.count() > argnum + 1 ) readNewerTime( argslist.at( argnum + 1 ) ); else missingArg = true; argnum++; } else if( myarg == QLatin1String("batch") ) { if( argslist.count() > argnum + 1 ) readBatchFile( argslist.at( argnum + 1 ) ); else missingArg = true; argnum++; } else if( myarg == QLatin1String("sharedmemory") ) { if( argslist.count() > argnum + 1 ) m_scanningState.setKey( argslist.at( argnum + 1 ) ); else missingArg = true; argnum++; } else if( myarg == QLatin1String("version") ) displayVersion(); else if( myarg == QLatin1String("incremental") ) m_incremental = true; else if( myarg == QLatin1String("recursive") ) m_recursively = true; else if( myarg == QLatin1String("restart") ) m_restart = true; else if( myarg == QLatin1String("idlepriority") ) m_idlePriority = true; else if( myarg == QLatin1String("charset") ) m_charset = true; else displayHelp(); } else if( arg.startsWith( '-' ) ) { QString myarg = QString( arg ).remove( 0, 1 ); int pos = 0; while( pos < myarg.length() ) { if( myarg[pos] == 'r' ) m_recursively = true; else if( myarg[pos] == 'v' ) displayVersion(); else if( myarg[pos] == 's' ) m_restart = true; else if( myarg[pos] == 'c' ) m_charset = true; else if( myarg[pos] == 'i' ) m_incremental = true; else displayHelp(); ++pos; } } else { if( !arg.isEmpty() ) m_folders.append( arg ); } } if( missingArg ) displayHelp( tr( "Missing argument for option %1" ).arg( argslist.last() ) ); CollectionScanner::Track::setUseCharsetDetector( m_charset ); // Start the actual scanning job QTimer::singleShot( 0, this, &Scanner::doJob ); } void CollectionScanner::Scanner::error( const QString &str ) { QTextStream stream( stderr ); stream << str << endl; stream.flush(); // Nothing else to do, so we exit directly ::exit( 0 ); } /** This function is called by Amarok to verify that Amarok an Scanner versions match */ void CollectionScanner::Scanner::displayVersion() { QTextStream stream( stdout ); stream << AMAROK_VERSION << endl; stream.flush(); // Nothing else to do, so we exit directly ::exit( 0 ); } void CollectionScanner::Scanner::displayHelp( const QString &error ) { QTextStream stream( error.isEmpty() ? stdout : stderr ); stream << error << tr( "Amarok Collection Scanner\n" "Scans directories and outputs a xml file with the results.\n" "For more information see http://community.kde.org/Amarok/Development/BatchMode\n\n" "Usage: amarokcollectionscanner [options] \n" "User-modifiable Options:\n" " : list of folders to scan\n" "-h, --help : This help text\n" "-v, --version : Print the version of this tool\n" "-r, --recursive : Scan folders recursively\n" "-i, --incremental : Incremental scan (modified folders only)\n" "-s, --restart : After a crash, restart the scanner in its last position\n" " --idlepriority : Run at idle priority\n" " --sharedmemory : A shared memory segment to be used for restarting a scan\n" " --newer : Only scan directories if modification time is newer than \n" " Only useful in incremental scan mode\n" " --batch : Add the directories from the batch xml file\n" " batch file format should look like this:\n" " \n" " \n" " /absolute/path/of/directory\n" " 1234 (this is optional)\n" " \n" " \n" " You can also use a previous scan result for that.\n" ) << endl; stream.flush(); ::exit(0); }