diff options
| author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-09 23:52:48 +0000 |
|---|---|---|
| committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-09 23:52:48 +0000 |
| commit | 3ce9174229de91411a9abf5381a1f335fe0c6a98 (patch) | |
| tree | 84b2736fa1b0d3fbf9c60fc04f510d2a13916b09 /amarok/src/database_refactor | |
| download | amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.tar.gz amarok-3ce9174229de91411a9abf5381a1f335fe0c6a98.zip | |
Added abandoned KDE3 version of Amarok
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/amarok@1072335 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'amarok/src/database_refactor')
| -rw-r--r-- | amarok/src/database_refactor/README | 9 | ||||
| -rw-r--r-- | amarok/src/database_refactor/_Makefile.am | 35 | ||||
| -rw-r--r-- | amarok/src/database_refactor/collectiondb.cpp | 1975 | ||||
| -rw-r--r-- | amarok/src/database_refactor/collectiondb.h | 239 | ||||
| -rw-r--r-- | amarok/src/database_refactor/dbenginebase.cpp | 544 | ||||
| -rw-r--r-- | amarok/src/database_refactor/dbenginebase.h | 133 | ||||
| -rw-r--r-- | amarok/src/database_refactor/sqlite/_Makefile.am | 38 | ||||
| -rw-r--r-- | amarok/src/database_refactor/sqlite/amarok_sqlite_dbengine_plugin.desktop | 103 | ||||
| -rw-r--r-- | amarok/src/database_refactor/sqlite/sqlite_dbengine.cpp | 227 | ||||
| -rw-r--r-- | amarok/src/database_refactor/sqlite/sqlite_dbengine.h | 58 |
10 files changed, 3361 insertions, 0 deletions
diff --git a/amarok/src/database_refactor/README b/amarok/src/database_refactor/README new file mode 100644 index 00000000..4bd89d81 --- /dev/null +++ b/amarok/src/database_refactor/README @@ -0,0 +1,9 @@ + DATABASE +========== + +This folder contains a draft of the planned plugin-based database redesign. If you are +interested in helping, please contact our mailing list amarok-devel@lists.sf.net + + +WORK IN PROGRESS + diff --git a/amarok/src/database_refactor/_Makefile.am b/amarok/src/database_refactor/_Makefile.am new file mode 100644 index 00000000..b6c32c96 --- /dev/null +++ b/amarok/src/database_refactor/_Makefile.am @@ -0,0 +1,35 @@ +noinst_LTLIBRARIES = libdbengine.la + +INCLUDES = \ + -I$(top_srcdir)/amarok/src/plugin \ + -I$(top_srcdir)/amarok/src \ + -I$(top_srcdir)/amarok/src/engine \ + -I$(top_srcdir)/amarok/src/amarokcore \ + -I$(top_srcdir)/amarok/src/statusbar \ + $(all_includes) + +#if enable_sqlite + SQLITE_DBENGINE_SUBDIR = sqlite +#endif + +if enable_mysql + MYSQL_DBENGINE_SUBDIR = mysql +endif + +if enable_postgresql + POSTGRESQL_DBENGINE_SUBDIR = postgresql +endif + +libdbengine_la_SOURCES = \ + dbenginebase.cpp + +noinst_HEADERS = dbenginebase.h + +METASOURCES = \ + AUTO + +SUBDIRS = . \ + $(SQLITE_DBENGINE_SUBDIR) \ + $(MYSQL_DBENGINE_SUBDIR) \ + $(POSTGRESQL_DBENGINE_SUBDIR) + diff --git a/amarok/src/database_refactor/collectiondb.cpp b/amarok/src/database_refactor/collectiondb.cpp new file mode 100644 index 00000000..32a5a237 --- /dev/null +++ b/amarok/src/database_refactor/collectiondb.cpp @@ -0,0 +1,1975 @@ +// (c) 2004 Mark Kretschmann <markey@web.de> +// (c) 2004 Christian Muehlhaeuser <chris@chris.de> +// (c) 2004 Sami Nieminen <sami.nieminen@iki.fi> +// (c) 2005 Ian Monroe <ian@monroe.nu> +// See COPYING file for licensing information. + +#define DEBUG_PREFIX "CollectionDB" + +#include "app.h" +#include "amarok.h" +#include "amarokconfig.h" +#include "config.h" +#include "debug.h" +#include "collectionbrowser.h" //updateTags() +#include "collectiondb.h" +#include "collectionreader.h" +#include "coverfetcher.h" +#include "enginecontroller.h" +#include "metabundle.h" //updateTags() +#include "playlist.h" +#include "playlistbrowser.h" +#include "pluginmanager.h" +#include "scrobbler.h" +#include "statusbar.h" +#include "threadweaver.h" + +#include <qfile.h> +#include <qimage.h> +#include <qtimer.h> + +#include <kapplication.h> +#include <kconfig.h> +#include <kglobal.h> +#include <kinputdialog.h> //setupCoverFetcher() +#include <kio/job.h> +#include <klineedit.h> //setupCoverFetcher() +#include <klocale.h> +#include <kmdcodec.h> +#include <kstandarddirs.h> +#include <kurl.h> +#include <kio/netaccess.h> + +#include <cmath> //DbConnection::sqlite_power() +#include <ctime> //query() +#include <unistd.h> //usleep() + +#include <taglib/mpegfile.h> +#include <taglib/mpegfile.h> +#include <taglib/id3v2tag.h> +#include <taglib/attachedpictureframe.h> +#include <taglib/tbytevector.h> + + +////////////////////////////////////////////////////////////////////////////////////////// +// CLASS CollectionDB +////////////////////////////////////////////////////////////////////////////////////////// + +CollectionDB* CollectionDB::instance() +{ + static CollectionDB db; + return &db; +} + + +CollectionDB::CollectionDB() + : EngineObserver( EngineController::instance() ) + , m_cacheDir( amaroK::saveLocation() ) + , m_coverDir( amaroK::saveLocation() ) +{ + DEBUG_BLOCK + + // create cover dir, if it doesn't exist. + if( !m_coverDir.exists( "albumcovers", false ) ) + m_coverDir.mkdir( "albumcovers", false ); + m_coverDir.cd( "albumcovers" ); + + // create image cache dir, if it doesn't exist. + if( !m_cacheDir.exists( "albumcovers/cache", false ) ) + m_cacheDir.mkdir( "albumcovers/cache", false ); + m_cacheDir.cd( "albumcovers/cache" ); + + // Load DBEngine plugin + QString query = "[X-KDE-Amarok-plugintype] == 'dbengine' and [X-KDE-Amarok-name] != '%1'"; + KTrader::OfferList offers = PluginManager::query( query.arg( "sqlite-dbengine" ) ); + m_dbEngine = (DBEngine*) PluginManager::createFromService( offers.first() ); + + //<OPEN DATABASE> + initialize(); + //</OPEN DATABASE> + + // TODO: Should write to config in dtor, but it crashes... + KConfig* config = amaroK::config( "Collection Browser" ); + config->writeEntry( "Database Version", DATABASE_VERSION ); + config->writeEntry( "Database Stats Version", DATABASE_STATS_VERSION ); + + startTimer( MONITOR_INTERVAL * 1000 ); + + connect( Scrobbler::instance(), SIGNAL( similarArtistsFetched( const QString&, const QStringList& ) ), + this, SLOT( similarArtistsFetched( const QString&, const QStringList& ) ) ); +} + + +CollectionDB::~CollectionDB() +{ + DEBUG_FUNC_INFO + + destroy(); + +// This crashes so it's done at the end of ctor. +// KConfig* const config = amaroK::config( "Collection Browser" ); +// config->writeEntry( "Database Version", DATABASE_VERSION ); +// config->writeEntry( "Database Stats Version", DATABASE_STATS_VERSION ); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// PUBLIC +////////////////////////////////////////////////////////////////////////////////////////// + + +DbConnection +*CollectionDB::getStaticDbConnection() +{ + return m_dbConnPool->getDbConnection(); +} + + +void +CollectionDB::returnStaticDbConnection( DbConnection *conn ) +{ + m_dbConnPool->putDbConnection( conn ); +} + + +/** + * Executes a SQL query on the already opened database + * @param statement SQL program to execute. Only one SQL statement is allowed. + * @return The queried data, or QStringList() on error. + */ +QStringList +CollectionDB::query( const QString& statement, DbConnection *conn ) +{ + if ( DEBUG ) + debug() << "Query-start: " << statement << endl; + + clock_t start = clock(); + + DbConnection *dbConn; + if ( conn != NULL ) + { + dbConn = conn; + } + else + { + dbConn = m_dbConnPool->getDbConnection(); + } + + QStringList values = dbConn->query( statement ); + + if ( conn == NULL ) + { + m_dbConnPool->putDbConnection( dbConn ); + } + + if ( DEBUG ) + { + clock_t finish = clock(); + const double duration = (double) (finish - start) / CLOCKS_PER_SEC; + debug() << "SQL-query (" << duration << "s): " << statement << endl; + } + return values; +} + + +/** + * Executes a SQL insert on the already opened database + * @param statement SQL statement to execute. Only one SQL statement is allowed. + * @return The rowid of the inserted item. + */ +int +CollectionDB::insert( const QString& statement, const QString& table, DbConnection *conn ) +{ + if ( DEBUG ) + debug() << "insert-start: " << statement << endl; + + clock_t start = clock(); + + DbConnection *dbConn; + if ( conn != NULL ) + { + dbConn = conn; + } + else + { + dbConn = m_dbConnPool->getDbConnection(); + } + + int id = dbConn->insert( statement, table ); + + if ( conn == NULL ) + { + m_dbConnPool->putDbConnection( dbConn ); + } + + if ( DEBUG ) + { + clock_t finish = clock(); + const double duration = (double) (finish - start) / CLOCKS_PER_SEC; + debug() << "SQL-insert (" << duration << "s): " << statement << endl; + } + return id; +} + + +bool +CollectionDB::isEmpty() +{ + QStringList values; + + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) + { + values = query( "SELECT COUNT( url ) FROM tags OFFSET 0 LIMIT 1;" ); + } + else + { + values = query( "SELECT COUNT( url ) FROM tags LIMIT 0, 1;" ); + } + + return values.isEmpty() ? true : values.first() == "0"; +} + + +bool +CollectionDB::isValid() +{ + QStringList values1; + QStringList values2; + + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) { + values1 = query( "SELECT COUNT( url ) FROM tags OFFSET 0 LIMIT 1;" ); + values2 = query( "SELECT COUNT( url ) FROM statistics OFFSET 0 LIMIT 1;" ); + } + else + { + values1 = query( "SELECT COUNT( url ) FROM tags LIMIT 0, 1;" ); + values2 = query( "SELECT COUNT( url ) FROM statistics LIMIT 0, 1;" ); + } + + //TODO? this returns true if value1 or value2 is not empty. Shouldn't this be and (&&)??? + return !values1.isEmpty() || !values2.isEmpty(); +} + + +void +CollectionDB::createTables( DbConnection *conn ) +{ + DEBUG_FUNC_INFO + + //create tag table + query( QString( "CREATE %1 TABLE tags%2 (" + "url " + textColumnType() + "," + "dir " + textColumnType() + "," + "createdate INTEGER," + "album INTEGER," + "artist INTEGER," + "genre INTEGER," + "title " + textColumnType() + "," + "year INTEGER," + "comment " + textColumnType() + "," + "track NUMERIC(4)," + "bitrate INTEGER," + "length INTEGER," + "samplerate INTEGER," + "sampler BOOL );" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ), conn ); + + QString albumAutoIncrement = ""; + QString artistAutoIncrement = ""; + QString genreAutoIncrement = ""; + QString yearAutoIncrement = ""; + if ( m_dbConnPool->getDbConnectionType() == DbConnection::postgresql ) + { + query( QString( "CREATE SEQUENCE album_seq;" ), conn ); + query( QString( "CREATE SEQUENCE artist_seq;" ), conn ); + query( QString( "CREATE SEQUENCE genre_seq;" ), conn ); + query( QString( "CREATE SEQUENCE year_seq;" ), conn ); + + albumAutoIncrement = QString("DEFAULT nextval('album_seq')"); + artistAutoIncrement = QString("DEFAULT nextval('artist_seq')"); + genreAutoIncrement = QString("DEFAULT nextval('genre_seq')"); + yearAutoIncrement = QString("DEFAULT nextval('year_seq')"); + } + else if ( m_dbConnPool->getDbConnectionType() == DbConnection::mysql ) + { + albumAutoIncrement = "AUTO_INCREMENT"; + artistAutoIncrement = "AUTO_INCREMENT"; + genreAutoIncrement = "AUTO_INCREMENT"; + yearAutoIncrement = "AUTO_INCREMENT"; + } + //create album table + query( QString( "CREATE %1 TABLE album%2 (" + "id INTEGER PRIMARY KEY %3," + "name " + textColumnType() + ");" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ) + .arg( albumAutoIncrement ), conn ); + + //create artist table + query( QString( "CREATE %1 TABLE artist%2 (" + "id INTEGER PRIMARY KEY %3," + "name " + textColumnType() + ");" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ) + .arg( artistAutoIncrement ), conn ); + + //create genre table + query( QString( "CREATE %1 TABLE genre%2 (" + "id INTEGER PRIMARY KEY %3," + "name " + textColumnType() +");" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ) + .arg( genreAutoIncrement ), conn ); + + //create year table + query( QString( "CREATE %1 TABLE year%2 (" + "id INTEGER PRIMARY KEY %3," + "name " + textColumnType() + ");" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ) + .arg( yearAutoIncrement ), conn ); + + //create images table + query( QString( "CREATE %1 TABLE images%2 (" + "path " + textColumnType() + "," + "artist " + textColumnType() + "," + "album " + textColumnType() + ");" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ), conn ); + + // create directory statistics table + query( QString( "CREATE %1 TABLE directories%2 (" + "dir " + textColumnType() + " UNIQUE," + "changedate INTEGER );" ) + .arg( conn ? "TEMPORARY" : "" ) + .arg( conn ? "_temp" : "" ), conn ); + + + //create indexes + query( QString( "CREATE INDEX album_idx%1 ON album%2( name );" ) + .arg( conn ? "_temp" : "" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "CREATE INDEX artist_idx%1 ON artist%2( name );" ) + .arg( conn ? "_temp" : "" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "CREATE INDEX genre_idx%1 ON genre%2( name );" ) + .arg( conn ? "_temp" : "" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "CREATE INDEX year_idx%1 ON year%2( name );" ) + .arg( conn ? "_temp" : "" ).arg( conn ? "_temp" : "" ), conn ); + + if ( !conn ) + { + // create related artists cache + query( QString( "CREATE TABLE related_artists (" + "artist " + textColumnType() + "," + "suggestion " + textColumnType() + "," + "changedate INTEGER );" ) ); + + query( "CREATE INDEX url_tag ON tags( url );" ); + query( "CREATE INDEX album_tag ON tags( album );" ); + query( "CREATE INDEX artist_tag ON tags( artist );" ); + query( "CREATE INDEX genre_tag ON tags( genre );" ); + query( "CREATE INDEX year_tag ON tags( year );" ); + query( "CREATE INDEX sampler_tag ON tags( sampler );" ); + + query( "CREATE INDEX images_album ON images( album );" ); + query( "CREATE INDEX images_artist ON images( artist );" ); + + query( "CREATE INDEX directories_dir ON directories( dir );" ); + query( "CREATE INDEX related_artists_artist ON related_artists( artist );" ); + } +} + + +void +CollectionDB::dropTables( DbConnection *conn ) +{ + DEBUG_FUNC_INFO + + query( QString( "DROP TABLE tags%1;" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "DROP TABLE album%1;" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "DROP TABLE artist%1;" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "DROP TABLE genre%1;" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "DROP TABLE year%1;" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "DROP TABLE images%1;" ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "DROP TABLE directories%1;" ).arg( conn ? "_temp" : "" ), conn ); + if ( !conn ) + { + query( QString( "DROP TABLE related_artists;" ) ); + } + + if ( m_dbConnPool->getDbConnectionType() == DbConnection::postgresql ) + { + if (conn == NULL) { + query( QString( "DROP SEQUENCE album_seq;" ), conn ); + query( QString( "DROP SEQUENCE artist_seq;" ), conn ); + query( QString( "DROP SEQUENCE genre_seq;" ), conn ); + query( QString( "DROP SEQUENCE year_seq;" ), conn ); + } + } +} + + +void +CollectionDB::clearTables( DbConnection *conn ) +{ + DEBUG_FUNC_INFO + + QString clearCommand = "DELETE FROM"; + if ( m_dbConnPool->getDbConnectionType() == DbConnection::mysql ) + { + // TRUNCATE TABLE is faster than DELETE FROM TABLE, so use it when supported. + clearCommand = "TRUNCATE TABLE"; + } + + query( QString( "%1 tags%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "%1 album%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "%1 artist%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "%1 genre%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "%1 year%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "%1 images%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + query( QString( "%1 directories%2;" ).arg( clearCommand ).arg( conn ? "_temp" : "" ), conn ); + if ( !conn ) + { + query( QString( "%1 related_artists;" ).arg( clearCommand ) ); + } +} + + +void +CollectionDB::moveTempTables( DbConnection *conn ) +{ + insert( "INSERT INTO tags SELECT * FROM tags_temp;", NULL, conn ); + insert( "INSERT INTO album SELECT * FROM album_temp;", NULL, conn ); + insert( "INSERT INTO artist SELECT * FROM artist_temp;", NULL, conn ); + insert( "INSERT INTO genre SELECT * FROM genre_temp;", NULL, conn ); + insert( "INSERT INTO year SELECT * FROM year_temp;", NULL, conn ); + insert( "INSERT INTO images SELECT * FROM images_temp;", NULL, conn ); + insert( "INSERT INTO directories SELECT * FROM directories_temp;", NULL, conn ); +} + + +void +CollectionDB::createStatsTable() +{ + DEBUG_FUNC_INFO + + // create music statistics database + query( QString( "CREATE TABLE statistics (" + "url " + textColumnType() + " UNIQUE," + "createdate INTEGER," + "accessdate INTEGER," + "percentage FLOAT," + "playcounter INTEGER );" ) ); + + query( "CREATE INDEX url_stats ON statistics( url );" ); + query( "CREATE INDEX percentage_stats ON statistics( percentage );" ); + query( "CREATE INDEX playcounter_stats ON statistics( playcounter );" ); +} + + +void +CollectionDB::dropStatsTable() +{ + DEBUG_FUNC_INFO + + query( "DROP TABLE statistics;" ); +} + + +uint +CollectionDB::artistID( QString value, bool autocreate, const bool temporary, const bool updateSpelling, DbConnection *conn ) +{ + // lookup cache + if ( m_cacheArtist == value ) + return m_cacheArtistID; + + uint id = IDFromValue( "artist", value, autocreate, temporary, updateSpelling, conn ); + + // cache values + m_cacheArtist = value; + m_cacheArtistID = id; + + return id; +} + + +QString +CollectionDB::artistValue( uint id ) +{ + // lookup cache + if ( m_cacheArtistID == id ) + return m_cacheArtist; + + QString value = valueFromID( "artist", id ); + + // cache values + m_cacheArtist = value; + m_cacheArtistID = id; + + return value; +} + + + +uint +CollectionDB::albumID( QString value, bool autocreate, const bool temporary, const bool updateSpelling, DbConnection *conn ) +{ + // lookup cache + if ( m_cacheAlbum == value ) + return m_cacheAlbumID; + + uint id = IDFromValue( "album", value, autocreate, temporary, updateSpelling, conn ); + + // cache values + m_cacheAlbum = value; + m_cacheAlbumID = id; + + return id; +} + + +QString +CollectionDB::albumValue( uint id ) +{ + // lookup cache + if ( m_cacheAlbumID == id ) + return m_cacheAlbum; + + QString value = valueFromID( "album", id ); + + // cache values + m_cacheAlbum = value; + m_cacheAlbumID = id; + + return value; +} + + +uint +CollectionDB::genreID( QString value, bool autocreate, const bool temporary, const bool updateSpelling, DbConnection *conn ) +{ + return IDFromValue( "genre", value, autocreate, temporary, updateSpelling, conn ); +} + + +QString +CollectionDB::genreValue( uint id ) +{ + return valueFromID( "genre", id ); +} + + +uint +CollectionDB::yearID( QString value, bool autocreate, const bool temporary, const bool updateSpelling, DbConnection *conn ) +{ + return IDFromValue( "year", value, autocreate, temporary, updateSpelling, conn ); +} + + +QString +CollectionDB::yearValue( uint id ) +{ + return valueFromID( "year", id ); +} + + +uint +CollectionDB::IDFromValue( QString name, QString value, bool autocreate, const bool temporary, const bool updateSpelling, DbConnection *conn ) +{ + if ( temporary ) + name.append( "_temp" ); + else + conn = NULL; + + QStringList values = + query( QString( + "SELECT id, name FROM %1 WHERE name LIKE '%2';" ) + .arg( name ) + .arg( CollectionDB::instance()->escapeString( value ) ), conn ); + + if ( updateSpelling && !values.isEmpty() && ( values[1] != value ) ) + { + query( QString( "UPDATE %1 SET id = %2, name = '%3' WHERE id = %4;" ) + .arg( name ) + .arg( values.first() ) + .arg( CollectionDB::instance()->escapeString( value ) ) + .arg( values.first() ), conn ); + } + + //check if item exists. if not, should we autocreate it? + uint id; + if ( values.isEmpty() && autocreate ) + { + id = insert( QString( "INSERT INTO %1 ( name ) VALUES ( '%2' );" ) + .arg( name ) + .arg( CollectionDB::instance()->escapeString( value ) ), name, conn ); + + return id; + } + + return values.isEmpty() ? 0 : values.first().toUInt(); +} + + +QString +CollectionDB::valueFromID( QString table, uint id ) +{ + QStringList values = + query( QString( + "SELECT name FROM %1 WHERE id=%2;" ) + .arg( table ) + .arg( id ) ); + + + return values.isEmpty() ? 0 : values.first(); +} + + +QString +CollectionDB::albumSongCount( const QString &artist_id, const QString &album_id ) +{ + QStringList values = + query( QString( + "SELECT COUNT( url ) FROM tags WHERE album = %1 AND artist = %2;" ) + .arg( album_id ) + .arg( artist_id ) ); + return values.first(); +} + +bool +CollectionDB::albumIsCompilation( const QString &album_id ) +{ + QStringList values = + query( QString( + "SELECT sampler FROM tags WHERE sampler=%1 AND album=%2" ) + .arg( CollectionDB::instance()->boolT() ) + .arg( album_id ) ); + + return (values.count() != 0); +} + +QStringList +CollectionDB::albumTracks( const QString &artist_id, const QString &album_id ) +{ + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) { + return query( QString( "SELECT tags.url, tags.track AS __discard FROM tags, year WHERE tags.album = %1 AND " + "( tags.sampler = %2 OR tags.artist = %3 ) AND year.id = tags.year " + "ORDER BY tags.track;" ) + .arg( album_id ) + .arg( boolT() ) + .arg( artist_id ) ); + } + else + { + return query( QString( "SELECT tags.url FROM tags, year WHERE tags.album = %1 AND " + "( tags.sampler = 1 OR tags.artist = %2 ) AND year.id = tags.year " + "ORDER BY tags.track;" ) + .arg( album_id ) + .arg( artist_id ) ); + } +} + + +void +CollectionDB::addImageToAlbum( const QString& image, QValueList< QPair<QString, QString> > info, DbConnection *conn ) +{ + for ( QValueList< QPair<QString, QString> >::ConstIterator it = info.begin(); it != info.end(); ++it ) + { + if ( (*it).first.isEmpty() || (*it).second.isEmpty() ) + continue; + + debug() << "Added image for album: " << (*it).first << " - " << (*it).second << ": " << image << endl; + insert( QString( "INSERT INTO images%1 ( path, artist, album ) VALUES ( '%1', '%2', '%3' );" ) + .arg( conn ? "_temp" : "" ) + .arg( escapeString( image ) ) + .arg( escapeString( (*it).first ) ) + .arg( escapeString( (*it).second ) ), NULL, conn ); + } +} + +QImage +CollectionDB::fetchImage(const KURL& url, QString &/*tmpFile*/) +{ + if(url.protocol() != "file") + { + QString tmpFile; + KIO::NetAccess::download( url, tmpFile, 0); //TODO set 0 to the window, though it probably doesn't really matter + return QImage(tmpFile); + } + else + { + return QImage( url.path() ); + } + +} +bool +CollectionDB::setAlbumImage( const QString& artist, const QString& album, const KURL& url ) +{ + QString tmpFile; + bool success = setAlbumImage( artist, album, fetchImage(url, tmpFile) ); + KIO::NetAccess::removeTempFile( tmpFile ); //only removes file if it was created with NetAccess + return success; +} + + +bool +CollectionDB::setAlbumImage( const QString& artist, const QString& album, QImage img, const QString& amazonUrl ) +{ + debug() << "Saving cover for: " << artist << " - " << album << endl; + + //show a wait cursor for the duration + amaroK::OverrideCursor keep; + + // remove existing album covers + removeAlbumImage( artist, album ); + + QDir largeCoverDir( amaroK::saveLocation( "albumcovers/large/" ) ); + QCString key = md5sum( artist, album ); + + // Save Amazon product page URL as embedded string, for later retreival + if ( !amazonUrl.isEmpty() ) + img.setText( "amazon-url", 0, amazonUrl ); + + return img.save( largeCoverDir.filePath( key ), "PNG"); +} + + +QString +CollectionDB::findImageByMetabundle( MetaBundle trackInformation, uint width ) +{ + if( width == 1 ) width = AmarokConfig::coverPreviewSize(); + + QCString widthKey = makeWidthKey( width ); + QCString tagKey = md5sum( trackInformation.artist(), trackInformation.album() ); + QDir tagCoverDir( amaroK::saveLocation( "albumcovers/tagcover/" ) ); + + //FIXME: the cached versions will never be refreshed + if ( tagCoverDir.exists( widthKey + tagKey ) ) + { + // cached version + return tagCoverDir.filePath( widthKey + tagKey ); + } else + { + // look into the tag + TagLib::MPEG::File f( QFile::encodeName( trackInformation.url().path() ) ); + TagLib::ID3v2::Tag *tag = f.ID3v2Tag(); + + if ( tag ) + { + TagLib::ID3v2::FrameList l = f.ID3v2Tag()->frameListMap()[ "APIC" ]; + if ( !l.isEmpty() ) + { + debug() << "Found APIC frame(s)" << endl; + TagLib::ID3v2::Frame *f = l.front(); + TagLib::ID3v2::AttachedPictureFrame *ap = (TagLib::ID3v2::AttachedPictureFrame*)f; + + const TagLib::ByteVector &imgVector = ap->picture(); + debug() << "Size of image: " << imgVector.size() << " byte" << endl; + + // ignore APIC frames without picture and those with obviously bogus size + if( imgVector.size() == 0 || imgVector.size() > 10000000 /*10MB*/ ) + return QString(); + + QImage image; + if( image.loadFromData((const uchar*)imgVector.data(), imgVector.size()) ) + { + if ( width > 1 ) + { + image.smoothScale( width, width, QImage::ScaleMin ).save( m_cacheDir.filePath( widthKey + tagKey ), "PNG" ); + return m_cacheDir.filePath( widthKey + tagKey ); + } else + { + image.save( tagCoverDir.filePath( tagKey ), "PNG" ); + return tagCoverDir.filePath( tagKey ); + } + } // image.isNull + } // apic list is empty + } // tag is empty + } // caching + + return QString(); +} + + +QString +CollectionDB::findImageByArtistAlbum( const QString &artist, const QString &album, uint width ) +{ + QCString widthKey = makeWidthKey( width ); + + if ( artist.isEmpty() && album.isEmpty() ) + return notAvailCover( width ); + else + { + QCString key = md5sum( artist, album ); + + // check cache for existing cover + if ( m_cacheDir.exists( widthKey + key ) ) + return m_cacheDir.filePath( widthKey + key ); + else + { + // we need to create a scaled version of this cover + QDir largeCoverDir( amaroK::saveLocation( "albumcovers/large/" ) ); + if ( largeCoverDir.exists( key ) ) + if ( width > 1 ) + { + QImage img( largeCoverDir.filePath( key ) ); + img.smoothScale( width, width, QImage::ScaleMin ).save( m_cacheDir.filePath( widthKey + key ), "PNG" ); + + return m_cacheDir.filePath( widthKey + key ); + } + else + return largeCoverDir.filePath( key ); + } + + // no amazon cover found, let's try to find a cover in the song's directory + return getImageForAlbum( artist, album, width ); + } +} + + +QString +CollectionDB::albumImage( const QString &artist, const QString &album, uint width ) +{ + QString s; + // we aren't going to need a 1x1 size image. this is just a quick hack to be able to show full size images. + if ( width == 1 ) width = AmarokConfig::coverPreviewSize(); + + s = findImageByArtistAlbum( artist, album, width ); + if ( s == notAvailCover( width ) ) + return findImageByArtistAlbum( "", album, width ); + + return s; +} + + +QString +CollectionDB::albumImage( const uint artist_id, const uint album_id, const uint width ) +{ + return albumImage( artistValue( artist_id ), albumValue( album_id ), width ); +} + + +QString +CollectionDB::albumImage( MetaBundle trackInformation, uint width ) +{ + QString path = findImageByMetabundle( trackInformation, width ); + if( path.isEmpty() ) + path =albumImage( trackInformation.artist(), trackInformation.album(), width ); + + return path; +} + + +QCString +CollectionDB::makeWidthKey( uint width ) +{ + return QString::number( width ).local8Bit() + "@"; +} + +// get image from path +QString +CollectionDB::getImageForAlbum( const QString& artist, const QString& album, uint width ) +{ + if ( width == 1 ) width = AmarokConfig::coverPreviewSize(); + QCString widthKey = QString::number( width ).local8Bit() + "@"; + + if ( album.isEmpty() ) + return notAvailCover( width ); + + QStringList values = + query( QString( + "SELECT path FROM images WHERE artist LIKE '%1' AND album LIKE '%2' ORDER BY path;" ) + .arg( escapeString( artist ) ) + .arg( escapeString( album ) ) ); + + if ( !values.isEmpty() ) + { + QString image( values.first() ); + uint matches = 0; + uint maxmatches = 0; + for ( uint i = 0; i < values.count(); i++ ) + { + matches = values[i].contains( "front", false ) + values[i].contains( "cover", false ) + values[i].contains( "folder", false ); + if ( matches > maxmatches ) + { + image = values[i]; + maxmatches = matches; + } + } + + QCString key = md5sum( artist, album, image ); + + if ( width > 1 ) + { + if ( !m_cacheDir.exists( widthKey + key ) ) + { + QImage img = QImage( image ); + img.smoothScale( width, width, QImage::ScaleMin ).save( m_cacheDir.filePath( widthKey + key ), "PNG" ); + } + + return m_cacheDir.filePath( widthKey + key ); + } + else //large image + { + return image; + } + } + + return notAvailCover( width ); +} + + +bool +CollectionDB::removeAlbumImage( const QString &artist, const QString &album ) +{ + QCString widthKey = "*@"; + QCString key = md5sum( artist, album ); + + // remove scaled versions of images + QStringList scaledList = m_cacheDir.entryList( widthKey + key ); + if ( scaledList.count() > 0 ) + for ( uint i = 0; i < scaledList.count(); i++ ) + QFile::remove( m_cacheDir.filePath( scaledList[ i ] ) ); + + // remove large, original image + QDir largeCoverDir( amaroK::saveLocation( "albumcovers/large/" ) ); + + if ( largeCoverDir.exists( key ) && QFile::remove( largeCoverDir.filePath( key ) ) ) { + emit coverRemoved( artist, album ); + return true; + } + + return false; +} + +bool +CollectionDB::removeAlbumImage( const uint artist_id, const uint album_id ) +{ + return removeAlbumImage( artistValue( artist_id ), albumValue( album_id ) ); +} + + +QString +CollectionDB::notAvailCover( int width ) +{ + if ( !width ) width = AmarokConfig::coverPreviewSize(); + QString widthKey = QString::number( width ) + "@"; + + if( m_cacheDir.exists( widthKey + "nocover.png" ) ) + return m_cacheDir.filePath( widthKey + "nocover.png" ); + else + { + QImage nocover( locate( "data", "amarok/images/nocover.png" ) ); + nocover.smoothScale( width, width, QImage::ScaleMin ).save( m_cacheDir.filePath( widthKey + "nocover.png" ), "PNG" ); + return m_cacheDir.filePath( widthKey + "nocover.png" ); + } +} + + +QStringList +CollectionDB::artistList( bool withUnknowns, bool withCompilations ) +{ + QueryBuilder qb; + qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName ); + + if ( !withUnknowns ) + qb.excludeMatch( QueryBuilder::tabArtist, i18n( "Unknown" ) ); + if ( !withCompilations ) + qb.setOptions( QueryBuilder::optNoCompilations ); + + qb.setOptions( QueryBuilder::optRemoveDuplicates ); + qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName ); + return qb.run(); +} + + +QStringList +CollectionDB::albumList( bool withUnknowns, bool withCompilations ) +{ + QueryBuilder qb; + qb.addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valName ); + + if ( !withUnknowns ) + qb.excludeMatch( QueryBuilder::tabAlbum, i18n( "Unknown" ) ); + if ( !withCompilations ) + qb.setOptions( QueryBuilder::optNoCompilations ); + + qb.setOptions( QueryBuilder::optRemoveDuplicates ); + qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName ); + return qb.run(); +} + + +QStringList +CollectionDB::genreList( bool withUnknowns, bool withCompilations ) +{ + QueryBuilder qb; + qb.addReturnValue( QueryBuilder::tabGenre, QueryBuilder::valName ); + + if ( !withUnknowns ) + qb.excludeMatch( QueryBuilder::tabGenre, i18n( "Unknown" ) ); + if ( !withCompilations ) + qb.setOptions( QueryBuilder::optNoCompilations ); + + qb.setOptions( QueryBuilder::optRemoveDuplicates ); + qb.sortBy( QueryBuilder::tabGenre, QueryBuilder::valName ); + return qb.run(); +} + + +QStringList +CollectionDB::yearList( bool withUnknowns, bool withCompilations ) +{ + QueryBuilder qb; + qb.addReturnValue( QueryBuilder::tabYear, QueryBuilder::valName ); + + if ( !withUnknowns ) + qb.excludeMatch( QueryBuilder::tabYear, i18n( "Unknown" ) ); + if ( !withCompilations ) + qb.setOptions( QueryBuilder::optNoCompilations ); + + qb.setOptions( QueryBuilder::optRemoveDuplicates ); + qb.sortBy( QueryBuilder::tabYear, QueryBuilder::valName ); + return qb.run(); +} + + +QStringList +CollectionDB::albumListOfArtist( const QString &artist, bool withUnknown, bool withCompilations ) +{ + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) + { + return query( "SELECT DISTINCT album.name, lower( album.name ) AS __discard FROM tags, album, artist WHERE " + "tags.album = album.id AND tags.artist = artist.id " + "AND artist.name = '" + escapeString( artist ) + "' " + + ( withUnknown ? QString::null : "AND album.name <> '' " ) + + ( withCompilations ? QString::null : "AND tags.sampler = " + boolF() ) + + " ORDER BY lower( album.name );" ); + } + else + { + return query( "SELECT DISTINCT album.name FROM tags, album, artist WHERE " + "tags.album = album.id AND tags.artist = artist.id " + "AND artist.name = '" + escapeString( artist ) + "' " + + ( withUnknown ? QString::null : "AND album.name <> '' " ) + + ( withCompilations ? QString::null : "AND tags.sampler = " + boolF() ) + + " ORDER BY lower( album.name );" ); + } +} + + +QStringList +CollectionDB::artistAlbumList( bool withUnknown, bool withCompilations ) +{ + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) + { + return query( "SELECT DISTINCT artist.name, album.name, lower( album.name ) AS __discard FROM tags, album, artist WHERE " + "tags.album = album.id AND tags.artist = artist.id " + + ( withUnknown ? QString::null : "AND album.name <> '' AND artist.name <> '' " ) + + ( withCompilations ? QString::null : "AND tags.sampler = " + boolF() ) + + " ORDER BY lower( album.name );" ); + } + else + { + return query( "SELECT DISTINCT artist.name, album.name FROM tags, album, artist WHERE " + "tags.album = album.id AND tags.artist = artist.id " + + ( withUnknown ? QString::null : "AND album.name <> '' AND artist.name <> '' " ) + + ( withCompilations ? QString::null : "AND tags.sampler = " + boolF() ) + + " ORDER BY lower( album.name );" ); + } +} + + +bool +CollectionDB::addSong( MetaBundle* bundle, const bool incremental, DbConnection *conn ) +{ + if ( !QFileInfo( bundle->url().path() ).isReadable() ) return false; + + QString command = "INSERT INTO tags_temp " + "( url, dir, createdate, album, artist, genre, year, title, comment, track, sampler, length, bitrate, samplerate ) " + "VALUES ('"; + + QString artist = bundle->artist(); + QString title = bundle->title(); + if ( title.isEmpty() ) + { + title = bundle->url().fileName(); + if ( bundle->url().fileName().find( '-' ) > 0 ) + { + if ( artist.isEmpty() ) artist = bundle->url().fileName().section( '-', 0, 0 ).stripWhiteSpace(); + title = bundle->url().fileName().section( '-', 1 ).stripWhiteSpace(); + title = title.left( title.findRev( '.' ) ).stripWhiteSpace(); + if ( title.isEmpty() ) title = bundle->url().fileName(); + } + } + bundle->setArtist( artist ); + bundle->setTitle( title ); + + command += escapeString( bundle->url().path() ) + "','"; + command += escapeString( bundle->url().directory() ) + "',"; + command += QString::number( QFileInfo( bundle->url().path() ).lastModified().toTime_t() ) + ","; + + command += escapeString( QString::number( albumID( bundle->album(), true, !incremental, false, conn ) ) ) + ","; + command += escapeString( QString::number( artistID( bundle->artist(), true, !incremental, false, conn ) ) ) + ","; + command += escapeString( QString::number( genreID( bundle->genre(), true, !incremental, false, conn ) ) ) + ",'"; + command += escapeString( QString::number( yearID( bundle->year(), true, !incremental, false, conn ) ) ) + "','"; + + command += escapeString( bundle->title() ) + "','"; + command += escapeString( bundle->comment() ) + "', "; + command += ( bundle->track().isEmpty() ? "NULL" : escapeString( bundle->track() ) ) + " , "; + command += artist == i18n( "Various Artists" ) ? boolT() + "," : boolF() + ","; + + // NOTE any of these may be -1 or -2, this is what we want + // see MetaBundle::Undetermined + command += QString::number( bundle->length() ) + ","; + command += QString::number( bundle->bitrate() ) + ","; + command += QString::number( bundle->sampleRate() ) + ")"; + + //FIXME: currently there's no way to check if an INSERT query failed or not - always return true atm. + // Now it might be possible as insert returns the rowid. + insert( command, NULL, conn ); + return true; +} + + +static void +fillInBundle( QStringList values, MetaBundle &bundle ) +{ + //TODO use this whenever possible + + // crash prevention + while( values.count() != 10 ) + values += "IF YOU CAN SEE THIS THERE IS A BUG!"; + + QStringList::ConstIterator it = values.begin(); + + bundle.setAlbum ( *it ); ++it; + bundle.setArtist ( *it ); ++it; + bundle.setGenre ( *it ); ++it; + bundle.setTitle ( *it ); ++it; + bundle.setYear ( *it ); ++it; + bundle.setComment ( *it ); ++it; + bundle.setTrack ( *it ); ++it; + bundle.setBitrate ( (*it).toInt() ); ++it; + bundle.setLength ( (*it).toInt() ); ++it; + bundle.setSampleRate( (*it).toInt() ); +} + +bool +CollectionDB::bundleForUrl( MetaBundle* bundle ) +{ + QStringList values = query( QString( + "SELECT album.name, artist.name, genre.name, tags.title, " + "year.name, tags.comment, tags.track, tags.bitrate, tags.length, " + "tags.samplerate " + "FROM tags, album, artist, genre, year " + "WHERE album.id = tags.album AND artist.id = tags.artist AND " + "genre.id = tags.genre AND year.id = tags.year AND tags.url = '%1';" ) + .arg( escapeString( bundle->url().path() ) ) ); + + if ( !values.empty() ) + fillInBundle( values, *bundle ); + + return !values.isEmpty(); +} + + +QValueList<MetaBundle> +CollectionDB::bundlesByUrls( const KURL::List& urls ) +{ + typedef QValueList<MetaBundle> BundleList; + BundleList bundles; + QStringList paths; + QueryBuilder qb; + + for( KURL::List::ConstIterator it = urls.begin(), end = urls.end(), last = urls.fromLast(); it != end; ++it ) + { + // non file stuff won't exist in the db, but we still need to + // re-insert it into the list we return, just with no tags assigned + paths += (*it).protocol() == "file" ? (*it).path() : (*it).url(); + + if( paths.count() == 50 || it == last ) + { + qb.clear(); + + qb.addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valName ); + qb.addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName ); + qb.addReturnValue( QueryBuilder::tabGenre, QueryBuilder::valName ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valTitle ); + qb.addReturnValue( QueryBuilder::tabYear, QueryBuilder::valName ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valComment ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valTrack ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valBitrate ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valLength ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valSamplerate ); + qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL ); + + qb.addURLFilters( paths ); + qb.setOptions( QueryBuilder::optRemoveDuplicates ); + + const QStringList values = qb.run(); + + BundleList buns50; + MetaBundle b; + foreach( values ) + { + b.setAlbum ( *it ); + b.setArtist ( *++it ); + b.setGenre ( *++it ); + b.setTitle ( *++it ); + b.setYear ( *++it ); + b.setComment ( *++it ); + b.setTrack ( *++it ); + b.setBitrate ( (*++it).toInt() ); + b.setLength ( (*++it).toInt() ); + b.setSampleRate( (*++it).toInt() ); + b.setPath ( *++it ); + + buns50.append( b ); + } + + // we get no guarantee about the order that the database + // will return our values, and sqlite indeed doesn't return + // them in the desired order :( (MySQL does though) + foreach( paths ) { + for( BundleList::Iterator jt = buns50.begin(), end = buns50.end(); jt != end; ++jt ) + if ( (*jt).url().path() == (*it) ) { + bundles += *jt; + buns50.remove( jt ); + goto success; + } + + // if we get here, we didn't find an entry + debug() << "No bundle recovered for: " << *it << endl; + b = MetaBundle(); + b.setUrl( KURL::fromPathOrURL(*it) ); + bundles += b; + + success: ; + } + + paths.clear(); + } + } + + return bundles; +} + + +void +CollectionDB::addAudioproperties( const MetaBundle& bundle ) +{ + query( QString( "UPDATE tags SET bitrate='%1', length='%2', samplerate='%3' WHERE url='%4';" ) + .arg( bundle.bitrate() ) + .arg( bundle.length() ) + .arg( bundle.sampleRate() ) + .arg( escapeString( bundle.url().path() ) ) ); +} + + +int +CollectionDB::addSongPercentage( const QString &url, int percentage ) +{ + float score; + QStringList values = + query( QString( + "SELECT playcounter, createdate, percentage FROM statistics " + "WHERE url = '%1';" ) + .arg( escapeString( url ) ) ); + + // check boundaries + if ( percentage > 100 ) percentage = 100; + if ( percentage < 1 ) percentage = 1; + + if ( !values.isEmpty() ) + { + // entry exists, increment playcounter and update accesstime + score = ( ( values[2].toDouble() * values.first().toInt() ) + percentage ) / ( values.first().toInt() + 1 ); + + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) { + query( QString( "UPDATE statistics SET percentage=%1, playcounter=%2+1 WHERE url='%3';" ) + .arg( score ) + .arg( values[0] + " + 1" ) + .arg( escapeString( url ) ) ); + } + else + { + query( QString( "REPLACE INTO statistics ( url, createdate, accessdate, percentage, playcounter ) " + "VALUES ( '%1', %2, %3, %4, %5 );" ) + .arg( escapeString( url ) ) + .arg( values[1] ) + .arg( QDateTime::currentDateTime().toTime_t() ) + .arg( score ) + .arg( values[0] + " + 1" ) ); + } + } + else + { + // entry didn't exist yet, create a new one + score = ( ( 50 + percentage ) / 2 ); + + insert( QString( "INSERT INTO statistics ( url, createdate, accessdate, percentage, playcounter ) " + "VALUES ( '%1', %2, %3, %4, 1 );" ) + .arg( escapeString( url ) ) + .arg( QDateTime::currentDateTime().toTime_t() ) + .arg( QDateTime::currentDateTime().toTime_t() ) + .arg( score ), NULL ); + } + + int iscore = getSongPercentage( url ); + emit scoreChanged( url, iscore ); + return iscore; +} + + +int +CollectionDB::getSongPercentage( const QString &url ) +{ + QStringList values = query( QString( "SELECT round( percentage + 0.4 ) FROM statistics WHERE url = '%1';" ) + .arg( escapeString( url ) ) ); + + if( values.count() ) + return values.first().toInt(); + + return 0; +} + + +void +CollectionDB::setSongPercentage( const QString &url , int percentage ) +{ + QStringList values = + query( QString( + "SELECT playcounter, createdate, accessdate FROM statistics WHERE url = '%1';" ) + .arg( escapeString( url ) ) ); + + // check boundaries + if ( percentage > 100 ) percentage = 100; + if ( percentage < 1 ) percentage = 1; + + if ( !values.isEmpty() ) + { + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) { + query( QString( "UPDATE statistics SET percentage=%1 WHERE url='%2';" ) + .arg( percentage ) + .arg( escapeString( url ) ) ); + } + else + { + // entry exists + query( QString( "REPLACE INTO statistics ( url, createdate, accessdate, percentage, playcounter ) " + "VALUES ( '%1', '%2', '%3', %4, %5 );" ) + .arg( escapeString( url ) ) + .arg( values[1] ) + .arg( values[2] ) + .arg( percentage ) + .arg( values[0] ) ); + } + } + else + { + insert( QString( "INSERT INTO statistics ( url, createdate, accessdate, percentage, playcounter ) " + "VALUES ( '%1', %2, %3, %4, 0 );" ) + .arg( escapeString( url ) ) + .arg( QDateTime::currentDateTime().toTime_t() ) + .arg( QDateTime::currentDateTime().toTime_t() ) + .arg( percentage ), NULL ); + } + + emit scoreChanged( url, percentage ); +} + + +void +CollectionDB::updateDirStats( QString path, const long datetime, DbConnection *conn ) +{ + if ( path.endsWith( "/" ) ) + path = path.left( path.length() - 1 ); + + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) { + query( QString( "UPDATE directories%1 SET changedate=%2 WHERE dir='%3';") + .arg( conn ? "_temp" : "" ) + .arg( datetime ) + .arg( escapeString( path ) ), conn ); + } + else + { + query( QString( "REPLACE INTO directories%1 ( dir, changedate ) VALUES ( '%3', %2 );" ) + .arg( conn ? "_temp" : "" ) + .arg( datetime ) + .arg( escapeString( path ) ), + conn ); + } +} + + +void +CollectionDB::removeSongsInDir( QString path ) +{ + if ( path.endsWith( "/" ) ) + path = path.left( path.length() - 1 ); + + query( QString( "DELETE FROM tags WHERE dir = '%1';" ) + .arg( escapeString( path ) ) ); +} + + +bool +CollectionDB::isDirInCollection( QString path ) +{ + if ( path.endsWith( "/" ) ) + path = path.left( path.length() - 1 ); + + QStringList values = + query( QString( "SELECT changedate FROM directories WHERE dir = '%1';" ) + .arg( escapeString( path ) ) ); + + return !values.isEmpty(); +} + + +bool +CollectionDB::isFileInCollection( const QString &url ) +{ + QStringList values = + query( QString( "SELECT url FROM tags WHERE url = '%1';" ) + .arg( escapeString( url ) ) ); + + return !values.isEmpty(); +} + + +void +CollectionDB::removeSongs( const KURL::List& urls ) +{ + for( KURL::List::ConstIterator it = urls.begin(), end = urls.end(); it != end; ++it ) + { + query( QString( "DELETE FROM tags WHERE url = '%1';" ) + .arg( escapeString( (*it).path() ) ) ); + } +} + + +QStringList +CollectionDB::similarArtists( const QString &artist, uint count ) +{ + QStringList values; + + if (m_dbConnPool->getDbConnectionType() == DbConnection::postgresql) { + values = query( QString( "SELECT suggestion FROM related_artists WHERE artist = '%1' OFFSET 0 LIMIT %2;" ) + .arg( escapeString( artist ) ).arg( count ) ); + } + else + { + values = query( QString( "SELECT suggestion FROM related_artists WHERE artist = '%1' LIMIT 0, %2;" ) + .arg( escapeString( artist ) ).arg( count ) ); + } + + if ( values.isEmpty() ) + Scrobbler::instance()->similarArtists( artist ); + + return values; +} + + +void +CollectionDB::checkCompilations( const QString &path, const bool temporary, DbConnection *conn ) +{ + QStringList albums; + QStringList artists; + QStringList dirs; + + albums = query( QString( "SELECT DISTINCT album.name FROM tags_temp, album%1 AS album WHERE tags_temp.dir = '%2' AND album.id = tags_temp.album;" ) + .arg( temporary ? "_temp" : "" ) + .arg( escapeString( path ) ), conn ); + + for ( uint i = 0; i < albums.count(); i++ ) + { + if ( albums[ i ].isEmpty() ) continue; + + const uint album_id = albumID( albums[ i ], false, temporary, false, conn ); + artists = query( QString( "SELECT DISTINCT artist.name FROM tags_temp, artist%1 AS artist WHERE tags_temp.album = '%2' AND tags_temp.artist = artist.id;" ) + .arg( temporary ? "_temp" : "" ) + .arg( album_id ), conn ); + dirs = query( QString( "SELECT DISTINCT dir FROM tags_temp WHERE album = '%1';" ) + .arg( album_id ), conn ); + + if ( artists.count() > dirs.count() ) + { + debug() << "Detected compilation: " << albums[ i ] << " - " << artists.count() << ":" << dirs.count() << endl; + query( QString( "UPDATE tags_temp SET sampler = %1 WHERE album = '%2';" ) + .arg(boolT()).arg( album_id ), conn ); + } + } +} + + +void +CollectionDB::setCompilation( const QString &album, const bool enabled, const bool updateView ) +{ + query( QString( "UPDATE tags, album SET tags.sampler = %1 WHERE tags.album = album.id AND album.name = '%2';" ) + .arg( enabled ? "1" : "0" ) + .arg( escapeString( album ) ) ); + + // Update the Collection-Browser view, + // using QTimer to make sure we don't manipulate the GUI from a thread + if ( updateView ) + QTimer::singleShot( 0, CollectionView::instance(), SLOT( renderView() ) ); +} + + +void +CollectionDB::removeDirFromCollection( QString path ) +{ + if ( path.endsWith( "/" ) ) + path = path.left( path.length() - 1 ); + + query( QString( "DELETE FROM directories WHERE dir = '%1';" ) + .arg( escapeString( path ) ) ); +} + + +void +CollectionDB::updateTags( const QString &url, const MetaBundle &bundle, const bool updateView ) +{ + QString command = "UPDATE tags SET "; + command += "title = '" + escapeString( bundle.title() ) + "', "; + command += "artist = " + QString::number( artistID( bundle.artist(), true, false, true ) ) + ", "; + command += "album = " + QString::number( albumID( bundle.album(), true, false, true ) ) + ", "; + command += "genre = " + QString::number( genreID( bundle.genre(), true, false, true ) ) + ", "; + command += "year = " + QString::number( yearID( bundle.year(), true, false, true ) ) + ", "; + if ( !bundle.track().isEmpty() ) + command += "track = " + bundle.track() + ", "; + command += "comment = '" + escapeString( bundle.comment() ) + "' "; + command += "WHERE url = '" + escapeString( url ) + "';"; + + query( command ); + + if ( EngineController::instance()->bundle().url() == bundle.url() ) + { + debug() << "Current song edited, updating widgets: " << bundle.title() << endl; + EngineController::instance()->currentTrackMetaDataChanged( bundle ); + } + + // Update the Collection-Browser view, + // using QTimer to make sure we don't manipulate the GUI from a thread + if ( updateView ) + QTimer::singleShot( 0, CollectionView::instance(), SLOT( renderView() ) ); +} + + +void +CollectionDB::updateURL( const QString &url, const bool updateView ) +{ + // don't use the KURL ctor as it checks the db first + MetaBundle bundle; + bundle.setPath( url ); + bundle.readTags( TagLib::AudioProperties::Fast ); + + updateTags( url, bundle, updateView ); +} + + +void +CollectionDB::applySettings() +{ + bool recreateConnections = false; + if ( AmarokConfig::databaseEngine().toInt() != m_dbConnPool->getDbConnectionType() ) + { + recreateConnections = true; + } + else if ( AmarokConfig::databaseEngine().toInt() == DbConnection::mysql ) + { + // Using MySQL, so check if MySQL settings were changed + const MySqlConfig *config = + static_cast<const MySqlConfig*> ( m_dbConnPool->getDbConfig() ); + if ( AmarokConfig::mySqlHost() != config->host() ) + { + recreateConnections = true; + } + else if ( AmarokConfig::mySqlPort() != config->port() ) + { + recreateConnections = true; + } + else if ( AmarokConfig::mySqlDbName() != config->database() ) + { + recreateConnections = true; + } + else if ( AmarokConfig::mySqlUser() != config->username() ) + { + recreateConnections = true; + } + else if ( AmarokConfig::mySqlPassword() != config->password() ) + { + recreateConnections = true; + } + } + else if ( AmarokConfig::databaseEngine().toInt() == DbConnection::postgresql ) + { + const PostgresqlConfig *config = + static_cast<const PostgresqlConfig*> ( m_dbConnPool->getDbConfig() ); + if ( AmarokConfig::postgresqlConninfo() != config->conninfo() ) + { + recreateConnections = true; + } + } + if ( recreateConnections ) + { + debug() + << "Database engine settings changed: " + << "recreating DbConnections" << endl; + // If Database engine was changed, recreate DbConnections. + destroy(); + initialize(); + CollectionView::instance()->renderView(); + emit databaseEngineChanged(); + } +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// PROTECTED +////////////////////////////////////////////////////////////////////////////////////////// + +QCString +CollectionDB::md5sum( const QString& artist, const QString& album, const QString& file ) +{ + KMD5 context( artist.lower().local8Bit() + album.lower().local8Bit() + file.local8Bit() ); +// debug() << "MD5 SUM for " << artist << ", " << album << ": " << context.hexDigest() << endl; + return context.hexDigest(); +} + + +void CollectionDB::engineTrackEnded( int finalPosition, int trackLength ) +{ + //This is where percentages are calculated + //TODO statistics are not calculated when currentTrack doesn't exist + + // Don't update statistics if song has been played for less than 15 seconds + // if ( finalPosition < 15000 ) return; + + const KURL url = EngineController::instance()->bundle().url(); + if ( url.path().isEmpty() ) return; + + // sanity check + if ( finalPosition > trackLength || finalPosition <= 0 ) + finalPosition = trackLength; + + int pct = (int) ( ( (double) finalPosition / (double) trackLength ) * 100 ); + + // increase song counter & calculate new statistics + addSongPercentage( url.path(), pct ); +} + + +void +CollectionDB::timerEvent( QTimerEvent* ) +{ + if ( AmarokConfig::monitorChanges() ) + scanMonitor(); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// PUBLIC SLOTS +////////////////////////////////////////////////////////////////////////////////////////// + +void +CollectionDB::fetchCover( QWidget* parent, const QString& artist, const QString& album, bool noedit ) //SLOT +{ + #ifdef AMAZON_SUPPORT + debug() << "Fetching cover for " << artist << " - " << album << endl; + + CoverFetcher* fetcher = new CoverFetcher( parent, artist, album ); + connect( fetcher, SIGNAL(result( CoverFetcher* )), SLOT(coverFetcherResult( CoverFetcher* )) ); + fetcher->setUserCanEditQuery( !noedit ); + fetcher->startFetch(); + #endif +} + + +void +CollectionDB::scanMonitor() //SLOT +{ + scanModifiedDirs(); +} + + +void +CollectionDB::startScan() //SLOT +{ + QStringList folders = MountPointManager::instance()->collectionFolders(); + + if ( folders.isEmpty() ) { + dropTables(); + createTables(); + } + else if( PlaylistBrowser::instance() ) + { + emit scanStarted(); + + ThreadWeaver::instance()->queueJob( new CollectionReader( this, folders ) ); + } +} + + +void +CollectionDB::stopScan() //SLOT +{ + ThreadWeaver::instance()->abortAllJobsNamed( "CollectionReader" ); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// PRIVATE SLOTS +////////////////////////////////////////////////////////////////////////////////////////// + +void +CollectionDB::dirDirty( const QString& path ) +{ + debug() << k_funcinfo << "Dirty: " << path << endl; + + ThreadWeaver::instance()->queueJob( new CollectionReader( this, path ) ); +} + + +void +CollectionDB::coverFetcherResult( CoverFetcher *fetcher ) +{ + if( fetcher->wasError() ) { + error() << fetcher->errors() << endl; + emit coverFetcherError( fetcher->errors().front() ); + } + + else { + setAlbumImage( fetcher->artist(), fetcher->album(), fetcher->image(), fetcher->amazonURL() ); + emit coverFetched( fetcher->artist(), fetcher->album() ); + } +} + +/** + * This query is fairly slow with sqlite, and often happens just + * after the OSD is shown. Threading it restores responsivity. + */ +class SimilarArtistsInsertionJob : public ThreadWeaver::DependentJob +{ + virtual bool doJob() + { + CollectionDB::instance()->query( QString( "DELETE FROM related_artists WHERE artist = '%1';" ).arg( escapedArtist ) ); + + const QString sql = "INSERT INTO related_artists ( artist, suggestion, changedate ) VALUES ( '%1', '%2', 0 );"; + foreach( suggestions ) + CollectionDB::instance()->insert( sql + .arg( escapedArtist ) + .arg( CollectionDB::instance()->escapeString( *it ) ), NULL ); + + return true; + } + + virtual void completeJob() { emit CollectionDB::instance()->similarArtistsFetched( artist ); } + + const QString artist; + const QString escapedArtist; + const QStringList suggestions; + +public: + SimilarArtistsInsertionJob( CollectionDB *parent, const QString &s, const QStringList &list ) + : ThreadWeaver::DependentJob( parent, "SimilarArtistsInsertionJob" ) + , artist( s ) + , escapedArtist( parent->escapeString( s ) ) + , suggestions( list ) + {} +}; + +void +CollectionDB::similarArtistsFetched( const QString& artist, const QStringList& suggestions ) +{ + debug() << "Received similar artists\n"; + + ThreadWeaver::instance()->queueJob( new SimilarArtistsInsertionJob( this, artist, suggestions ) ); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// PRIVATE +////////////////////////////////////////////////////////////////////////////////////////// + + +void +CollectionDB::initialize() +{ + m_dbConnPool = new DbConnectionPool(); + DbConnection *dbConn = m_dbConnPool->getDbConnection(); + m_dbConnPool->putDbConnection( dbConn ); + + KConfig* config = amaroK::config( "Collection Browser" ); + if(!dbConn->isConnected()) + amaroK::MessageQueue::instance()->addMessage(dbConn->lastError()); + if ( !dbConn->isInitialized() || !isValid() ) + { + createTables(); + createStatsTable(); + } + else + { + //remove database file if version is incompatible + if ( config->readNumEntry( "Database Version", 0 ) != DATABASE_VERSION ) + { + debug() << "Rebuilding database!" << endl; + dropTables(); + createTables(); + } + if ( config->readNumEntry( "Database Stats Version", 0 ) != DATABASE_STATS_VERSION ) + { + debug() << "Rebuilding stats-database!" << endl; + dropStatsTable(); + createStatsTable(); + } + } + + m_dbConnPool->createDbConnections(); +} + + +void +CollectionDB::destroy() +{ + delete m_dbConnPool; +} + + +void +CollectionDB::scanModifiedDirs() +{ + //we check if a job is pending because we don't want to abort incremental collection readings + if ( !ThreadWeaver::instance()->isJobPending( "CollectionReader" ) && PlaylistBrowser::instance() ) { + emit scanStarted(); + ThreadWeaver::instance()->onlyOneJob( new IncrementalCollectionReader( this ) ); + } +} + + +void +CollectionDB::customEvent( QCustomEvent *e ) +{ + if ( e->type() == (int)CollectionReader::JobFinishedEvent ) + emit scanDone( static_cast<ThreadWeaver::Job*>(e)->wasSuccessful() ); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// CLASS DbConnectionPool +////////////////////////////////////////////////////////////////////////////////////////// + +DbConnectionPool::DbConnectionPool() : m_semaphore( POOL_SIZE ) +{ +#ifdef USE_MYSQL + if ( AmarokConfig::databaseEngine().toInt() == DbConnection::mysql ) + m_dbConnType = DbConnection::mysql; + else +#endif +#ifdef USE_POSTGRESQL + if ( AmarokConfig::databaseEngine().toInt() == DbConnection::postgresql ) + m_dbConnType = DbConnection::postgresql; + else +#endif + m_dbConnType = DbConnection::sqlite; + + m_semaphore += POOL_SIZE; + DbConnection *dbConn; + +#ifdef USE_MYSQL + if ( m_dbConnType == DbConnection::mysql ) + { + m_dbConfig = + new MySqlConfig( + AmarokConfig::mySqlHost(), + AmarokConfig::mySqlPort(), + AmarokConfig::mySqlDbName(), + AmarokConfig::mySqlUser(), + AmarokConfig::mySqlPassword() ); + dbConn = new MySqlConnection( static_cast<MySqlConfig*> ( m_dbConfig ) ); + } + else +#endif +#ifdef USE_POSTGRESQL + if ( m_dbConnType == DbConnection::postgresql ) + { + m_dbConfig = + new PostgresqlConfig( + AmarokConfig::postgresqlConninfo() ); + dbConn = new PostgresqlConnection( static_cast<PostgresqlConfig*> ( m_dbConfig ) ); + } + else +#endif + { + m_dbConfig = new SqliteConfig( "collection.db" ); + dbConn = new SqliteConnection( static_cast<SqliteConfig*> ( m_dbConfig ) ); + } + enqueue( dbConn ); + m_semaphore--; + debug() << "Available db connections: " << m_semaphore.available() << endl; +} + + +DbConnectionPool::~DbConnectionPool() +{ + m_semaphore += POOL_SIZE; + DbConnection *conn; + bool vacuum = true; + + while ( ( conn = dequeue() ) != 0 ) + { + if ( m_dbConnType == DbConnection::sqlite && vacuum ) + { + vacuum = false; + debug() << "Running VACUUM" << endl; + conn->query( "VACUUM; "); + } + + delete conn; + } + + delete m_dbConfig; +} + + +void DbConnectionPool::createDbConnections() +{ + for ( int i = 0; i < POOL_SIZE - 1; i++ ) + { + DbConnection *dbConn; + +#ifdef USE_MYSQL + if ( m_dbConnType == DbConnection::mysql ) + dbConn = new MySqlConnection( static_cast<MySqlConfig*> ( m_dbConfig ) ); + else +#endif +#ifdef USE_POSTGRESQL + if ( m_dbConnType == DbConnection::postgresql ) + dbConn = new PostgresqlConnection( static_cast<PostgresqlConfig*> ( m_dbConfig ) ); + else +#endif + dbConn = new SqliteConnection( static_cast<SqliteConfig*> ( m_dbConfig ) ); + enqueue( dbConn ); + m_semaphore--; + } + debug() << "Available db connections: " << m_semaphore.available() << endl; +} + + +DbConnection *DbConnectionPool::getDbConnection() +{ + m_semaphore++; + return dequeue(); +} + + +void DbConnectionPool::putDbConnection( const DbConnection *conn ) +{ + enqueue( conn ); + m_semaphore--; +} + + +#include "collectiondb.moc" diff --git a/amarok/src/database_refactor/collectiondb.h b/amarok/src/database_refactor/collectiondb.h new file mode 100644 index 00000000..7d0f6d12 --- /dev/null +++ b/amarok/src/database_refactor/collectiondb.h @@ -0,0 +1,239 @@ +// (c) 2004 Mark Kretschmann <markey@web.de> +// (c) 2004 Christian Muehlhaeuser <chris@chris.de> +// (c) 2004 Sami Nieminen <sami.nieminen@iki.fi> +// See COPYING file for licensing information. + +#ifndef AMAROK_COLLECTIONDB_H +#define AMAROK_COLLECTIONDB_H + +#include "engineobserver.h" +#include "dbenginebase.h" +#include <kurl.h> +#include <qdir.h> //stack allocated +#include <qobject.h> //baseclass +#include <qptrqueue.h> //baseclass +#include <qsemaphore.h> //stack allocated +#include <qstringlist.h> //stack allocated + +class CoverFetcher; +class MetaBundle; +class Scrobbler; + + +class DbConnectionPool : QPtrQueue<DbConnection> +{ + public: + DbConnectionPool(); + ~DbConnectionPool(); + + const DbConnection::DbConnectionType getDbConnectionType() const { return m_dbConnType; } + const DbConfig *getDbConfig() const { return m_dbConfig; } + void createDbConnections(); + + DbConnection *getDbConnection(); + void putDbConnection( const DbConnection* /* conn */ ); + + private: + static const int POOL_SIZE = 5; + QSemaphore m_semaphore; + DbConnection::DbConnectionType m_dbConnType; + DbConfig *m_dbConfig; +}; + + +class CollectionDB : public QObject, public EngineObserver +{ + Q_OBJECT + + friend class SimilarArtistsInsertionJob; + + signals: + void scanStarted(); + void scanDone( bool changed ); + void databaseEngineChanged(); + + void scoreChanged( const QString &url, int score ); + + void coverFetched( const QString &artist, const QString &album ); + void coverRemoved( const QString &artist, const QString &album ); + void coverFetcherError( const QString &error ); + + void similarArtistsFetched( const QString &artist ); + + public: + static CollectionDB *instance(); + + QString escapeString( QString string ) { return m_dbConnPool->escapeString(string); } + int getType() { return m_dbConnPool->getDbConnectionType(); } + + + /** + * This method returns a static DbConnection for components that want to use + * the same connection for the whole time. Should not be used anywhere else + * but in CollectionReader. + * + * @return static DbConnection + */ + DbConnection *getStaticDbConnection(); + + /** + * Returns the DbConnection back to connection pool. + * + * @param conn DbConnection to be returned + */ + void returnStaticDbConnection( DbConnection *conn ); + + //sql helper methods + QStringList query( const QString& statement, DbConnection *conn = NULL ); + int insert( const QString& statement, const QString& table, DbConnection *conn = NULL ); + + //table management methods + bool isEmpty(); + bool isValid(); + void createTables( DbConnection *conn = NULL ); + void dropTables( DbConnection *conn = NULL ); + void clearTables( DbConnection *conn = NULL ); + void moveTempTables( DbConnection *conn ); + + uint artistID( QString value, bool autocreate = true, const bool temporary = false, const bool updateSpelling = false, DbConnection *conn = NULL ); + uint albumID( QString value, bool autocreate = true, const bool temporary = false, const bool updateSpelling = false, DbConnection *conn = NULL ); + uint genreID( QString value, bool autocreate = true, const bool temporary = false, const bool updateSpelling = false, DbConnection *conn = NULL ); + uint yearID( QString value, bool autocreate = true, const bool temporary = false, const bool updateSpelling = false, DbConnection *conn = NULL ); + + bool isDirInCollection( QString path ); + bool isFileInCollection( const QString &url ); + void removeDirFromCollection( QString path ); + void removeSongsInDir( QString path ); + void removeSongs( const KURL::List& urls ); + void updateDirStats( QString path, const long datetime, DbConnection *conn = NULL ); + + //song methods + bool addSong( MetaBundle* bundle, const bool incremental = false, DbConnection *conn = NULL ); + + /** + * The @p bundle parameter's url() will be looked up in the Collection + * @param bundle this will be filled in with tags for you + * @return true if in the collection + */ + bool bundleForUrl( MetaBundle* bundle ); + QValueList<MetaBundle> bundlesByUrls( const KURL::List& urls ); + void addAudioproperties( const MetaBundle& bundle ); + + void updateTags( const QString &url, const MetaBundle &bundle, const bool updateView = true ); + void updateURL( const QString &url, const bool updateView = true ); + + //statistics methods + int addSongPercentage( const QString &url, int percentage ); + int getSongPercentage( const QString &url ); + void setSongPercentage( const QString &url , int percentage ); + + //artist methods + QStringList similarArtists( const QString &artist, uint count ); + + //album methods + void checkCompilations( const QString &path, const bool temporary = false, DbConnection *conn = NULL ); + void setCompilation( const QString &album, const bool enabled, const bool updateView = true ); + QString albumSongCount( const QString &artist_id, const QString &album_id ); + bool albumIsCompilation( const QString &album_id ); + + //list methods + QStringList artistList( bool withUnknowns = true, bool withCompilations = true ); + QStringList albumList( bool withUnknowns = true, bool withCompilations = true ); + QStringList genreList( bool withUnknowns = true, bool withCompilations = true ); + QStringList yearList( bool withUnknowns = true, bool withCompilations = true ); + + QStringList albumListOfArtist( const QString &artist, bool withUnknown = true, bool withCompilations = true ); + QStringList artistAlbumList( bool withUnknown = true, bool withCompilations = true ); + + QStringList albumTracks( const QString &artist_id, const QString &album_id ); + + //cover management methods + /** Returns the image from a given URL, network-transparently. + * You must run KIO::NetAccess::removeTempFile( tmpFile ) when you are finished using the image; + **/ + static QImage fetchImage(const KURL& url, QString &tmpFile); + /** Saves images located on the user's filesystem */ + bool setAlbumImage( const QString& artist, const QString& album, const KURL& url ); + /** Saves images obtained from CoverFetcher */ + bool setAlbumImage( const QString& artist, const QString& album, QImage img, const QString& amazonUrl = QString::null ); + + QString findImageByMetabundle( MetaBundle trackInformation, const uint = 1 ); + QString findImageByArtistAlbum( const QString &artist, const QString &album, const uint width = 1 ); + QString albumImage( MetaBundle trackInformation, const uint width = 1 ); + QString albumImage( const uint artist_id, const uint album_id, const uint width = 1 ); + QString albumImage( const QString &artist, const QString &album, const uint width = 1 ); + + bool removeAlbumImage( const uint artist_id, const uint album_id ); + bool removeAlbumImage( const QString &artist, const QString &album ); + + //local cover methods + void addImageToAlbum( const QString& image, QValueList< QPair<QString, QString> > info, DbConnection *conn = NULL ); + QString getImageForAlbum( const QString& artist, const QString& album, uint width = 0 ); + QString notAvailCover( int width = 0 ); + + void applySettings(); + + protected: + CollectionDB(); + ~CollectionDB(); + + QCString md5sum( const QString& artist, const QString& album, const QString& file = QString::null ); + void engineTrackEnded( int finalPosition, int trackLength ); + /** Manages regular folder monitoring scan */ + void timerEvent( QTimerEvent* e ); + + public slots: + void fetchCover( QWidget* parent, const QString& artist, const QString& album, bool noedit ); + void scanMonitor(); + void startScan(); + void stopScan(); + + private slots: + void dirDirty( const QString& path ); + void coverFetcherResult( CoverFetcher* ); + void similarArtistsFetched( const QString& artist, const QStringList& suggestions ); + + private: + //bump DATABASE_VERSION whenever changes to the table structure are made. will remove old db file. + static const int DATABASE_VERSION = 18; + static const int DATABASE_STATS_VERSION = 3; + static const int MONITOR_INTERVAL = 60; //sec + static const bool DEBUG = false; + + void initialize(); + void destroy(); + void customEvent( QCustomEvent* ); + + //general management methods + void createStatsTable(); + void dropStatsTable(); + void scanModifiedDirs(); + + QCString makeWidthKey( uint width ); + QString artistValue( uint id ); + QString albumValue( uint id ); + QString genreValue( uint id ); + QString yearValue( uint id ); + + uint IDFromValue( QString name, QString value, bool autocreate = true, const bool temporary = false, + const bool updateSpelling = false, DbConnection *conn = NULL ); + + QString valueFromID( QString table, uint id ); + + //member variables + QString m_amazonLicense; + QString m_cacheArtist; + uint m_cacheArtistID; + QString m_cacheAlbum; + uint m_cacheAlbumID; + + DBEngine* m_dbEngine; + DbConnectionPool *m_dbConnPool; + + bool m_monitor; + QDir m_cacheDir; + QDir m_coverDir; +}; + + +#endif /* AMAROK_COLLECTIONDB_H */ diff --git a/amarok/src/database_refactor/dbenginebase.cpp b/amarok/src/database_refactor/dbenginebase.cpp new file mode 100644 index 00000000..42f40366 --- /dev/null +++ b/amarok/src/database_refactor/dbenginebase.cpp @@ -0,0 +1,544 @@ +/*************************************************************************** + * Copyright (C) 2004-2005 Mark Kretschmann <markey@web.de> * + * 2004 Christian Muehlhaeuser <chris@chris.de> * + * 2004 Sami Nieminen <sami.nieminen@iki.fi> * + * 2005 Ian Monroe <ian@monroe.nu> * + * * + * 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 Steet, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <dbenginebase.h> +#include <qstringlist.h> + +#include <klocale.h> + + +////////////////////////////////////////////////////////////////////////////////////////// +// CLASS DBConnection +////////////////////////////////////////////////////////////////////////////////////////// + +DbConnection::DbConnection( DbConfig* config ) + : m_config( config ) +{} + + +DbConnection::~DbConnection() +{} + + +////////////////////////////////////////////////////////////////////////////////////////// +// CLASS QueryBuilder +////////////////////////////////////////////////////////////////////////////////////////// + +QueryBuilder::QueryBuilder() +{ + clear(); +} + + +void +QueryBuilder::linkTables( int tables ) +{ + + m_tables = tableName( tabSong ); + + if ( !(tables & tabSong ) ) + { + // check if only one table is selected (does somebody know a better way to check that?) + if (tables == tabAlbum || tables==tabArtist || tables==tabGenre || tables == tabYear || tables == tabStats) + m_tables = tableName(tables); + else + tables |= tabSong; + } + + + if ( tables & tabSong ) + { + if ( tables & tabAlbum ) + m_tables += " INNER JOIN " + tableName( tabAlbum) + " ON album.id=tags.album"; + if ( tables & tabArtist ) + m_tables += " INNER JOIN " + tableName( tabArtist) + " ON artist.id=tags.artist"; + if ( tables & tabGenre ) + m_tables += " INNER JOIN " + tableName( tabGenre) + " ON genre.id=tags.genre"; + if ( tables & tabYear ) + m_tables += " INNER JOIN " + tableName( tabYear) + " ON year.id=tags.year"; + if ( tables & tabStats ) + m_tables += " INNER JOIN " + tableName( tabStats) + " ON statistics.url=tags.url"; + } +} + + +void +QueryBuilder::addReturnValue( int table, int value ) +{ + if ( !m_values.isEmpty() && m_values != "DISTINCT " ) m_values += ","; + if ( table & tabStats && value & valScore ) m_values += "round("; + + if ( value == valDummy ) + m_values += "''"; + else + { + m_values += tableName( table ) + "."; + m_values += valueName( value ); + } + + if ( table & tabStats && value & valScore ) m_values += " + 0.4 )"; + + m_linkTables |= table; + m_returnValues++; +} + +void +QueryBuilder::addReturnFunctionValue( int function, int table, int value) +{ + if ( !m_values.isEmpty() && m_values != "DISTINCT " ) m_values += ","; + m_values += functionName( function ) + "("; + m_values += tableName( table ) + "."; + m_values += valueName( value )+ ")"; + m_values += " AS "; + m_values += functionName( function )+tableName( table )+valueName( value ); + + m_linkTables |= table; + m_returnValues++; +} + +uint +QueryBuilder::countReturnValues() +{ + return m_returnValues; +} + + +void +QueryBuilder::addURLFilters( const QStringList& filter ) +{ + if ( !filter.isEmpty() ) + { + m_where += "AND ( true "; + + for ( uint i = 0; i < filter.count(); i++ ) + { + m_where += "OR tags.url = '" + escapeString( filter[i] ) + "' "; + } + + m_where += " ) "; + } + + m_linkTables |= tabSong; +} + + +void +QueryBuilder::addFilter( int tables, const QString& filter, int /*mode*/ ) +{ + if ( !filter.isEmpty() ) + { + m_where += "AND ( true "; + if ( tables & tabAlbum ) m_where += "OR album.name LIKE '%" + escapeString( filter ) + "%' "; + if ( tables & tabArtist ) m_where += "OR artist.name LIKE '%" + escapeString( filter ) + "%' "; + if ( tables & tabGenre ) m_where += "OR genre.name LIKE '%" + escapeString( filter ) + "%' "; + if ( tables & tabYear ) m_where += "OR year.name LIKE '%" + escapeString( filter ) + "%' "; + if ( tables & tabSong ) m_where += "OR tags.title LIKE '%" + escapeString( filter ) + "%' "; + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::addFilters( int tables, const QStringList& filter ) +{ + if ( !filter.isEmpty() ) + { + m_where += "AND ( true "; + + for ( uint i = 0; i < filter.count(); i++ ) + { + m_where += " AND ( true "; + if ( tables & tabAlbum ) m_where += "OR album.name LIKE '%" + escapeString( filter[i] ) + "%' "; + if ( tables & tabArtist ) m_where += "OR artist.name LIKE '%" + escapeString( filter[i] ) + "%' "; + if ( tables & tabGenre ) m_where += "OR genre.name LIKE '%" + escapeString( filter[i] ) + "%' "; + if ( tables & tabYear ) m_where += "OR year.name LIKE '%" + escapeString( filter[i] ) + "%' "; + if ( tables & tabSong ) m_where += "OR tags.title LIKE '%" + escapeString( filter[i] ) + "%' "; + m_where += " ) "; + } + + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::addMatch( int tables, const QString& match ) +{ + if ( !match.isEmpty() ) + { + m_where += "AND ( true "; + if ( tables & tabAlbum ) m_where += "OR album.name LIKE '" + escapeString( match ) + "' "; + if ( tables & tabArtist ) m_where += "OR artist.name LIKE '" + escapeString( match ) + "' "; + if ( tables & tabGenre ) m_where += "OR genre.name LIKE '" + escapeString( match ) + "' "; + if ( tables & tabYear ) m_where += "OR year.name LIKE '" + escapeString( match ) + "' "; + if ( tables & tabSong ) m_where += "OR tags.title LIKE '" + escapeString( match ) + "' "; + + if ( match == i18n( "Unknown" ) ) + { + if ( tables & tabAlbum ) m_where += "OR album.name = '' "; + if ( tables & tabArtist ) m_where += "OR artist.name = '' "; + if ( tables & tabGenre ) m_where += "OR genre.name = '' "; + if ( tables & tabYear ) m_where += "OR year.name = '' "; + } + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::addMatch( int tables, int value, const QString& match ) +{ + if ( !match.isEmpty() ) + { + m_where += "AND ( true "; + m_where += QString( "OR %1.%2 LIKE '" ).arg( tableName( tables ) ).arg( valueName( value ) ) + escapeString( match ) + "' "; + + if ( ( value & valName ) && match == i18n( "Unknown" ) ) + m_where += QString( "OR %1.%2 = '' " ).arg( tableName( tables ) ).arg( valueName( value ) ); + + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::addMatches( int tables, const QStringList& match ) +{ + if ( !match.isEmpty() ) + { + m_where += "AND ( true "; + + for ( uint i = 0; i < match.count(); i++ ) + { + if ( tables & tabAlbum ) m_where += "OR album.name LIKE '" + escapeString( match[i] ) + "' "; + if ( tables & tabArtist ) m_where += "OR artist.name LIKE '" + escapeString( match[i] ) + "' "; + if ( tables & tabGenre ) m_where += "OR genre.name LIKE '" + escapeString( match[i] ) + "' "; + if ( tables & tabYear ) m_where += "OR year.name LIKE '" + escapeString( match[i] ) + "' "; + if ( tables & tabSong ) m_where += "OR tags.title LIKE '" + escapeString( match[i] ) + "' "; + if ( tables & tabStats ) m_where += "OR statistics.url LIKE '" + escapeString( match[i] ) + "' "; + + if ( match[i] == i18n( "Unknown" ) ) + { + if ( tables & tabAlbum ) m_where += "OR album.name = '' "; + if ( tables & tabArtist ) m_where += "OR artist.name = '' "; + if ( tables & tabGenre ) m_where += "OR genre.name = '' "; + if ( tables & tabYear ) m_where += "OR year.name = '' "; + } + } + + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::excludeFilter( int tables, const QString& filter ) +{ + if ( !filter.isEmpty() ) + { + m_where += "AND ( true "; + if ( tables & tabAlbum ) m_where += "AND album.name <> '%" + escapeString( filter ) + "%' "; + if ( tables & tabArtist ) m_where += "AND artist.name <> '%" + escapeString( filter ) + "%' "; + if ( tables & tabGenre ) m_where += "AND genre.name <> '%" + escapeString( filter ) + "%' "; + if ( tables & tabYear ) m_where += "AND year.name <> '%" + escapeString( filter ) + "%' "; + if ( tables & tabSong ) m_where += "AND tags.title <> '%" + escapeString( filter ) + "%' "; + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::excludeMatch( int tables, const QString& match ) +{ + if ( !match.isEmpty() ) + { + m_where += "AND ( true "; + if ( tables & tabAlbum ) m_where += "AND album.name <> '" + escapeString( match ) + "' "; + if ( tables & tabArtist ) m_where += "AND artist.name <> '" + escapeString( match ) + "' "; + if ( tables & tabGenre ) m_where += "AND genre.name <> '" + escapeString( match ) + "' "; + if ( tables & tabYear ) m_where += "AND year.name <> '" + escapeString( match ) + "' "; + if ( tables & tabSong ) m_where += "AND tags.title <> '" + escapeString( match ) + "' "; + + if ( match == i18n( "Unknown" ) ) + { + if ( tables & tabAlbum ) m_where += "AND album.name <> '' "; + if ( tables & tabArtist ) m_where += "AND artist.name <> '' "; + if ( tables & tabGenre ) m_where += "AND genre.name <> '' "; + if ( tables & tabYear ) m_where += "AND year.name <> '' "; + } + m_where += " ) "; + } + + m_linkTables |= tables; +} + + +void +QueryBuilder::exclusiveFilter( int tableMatching, int tableNotMatching, int value ) +{ + m_join += " LEFT JOIN "; + m_join += tableName( tableNotMatching ); + m_join += " ON "; + + m_join += tableName( tableMatching ) + "."; + m_join += valueName( value ); + m_join+= " = "; + m_join += tableName( tableNotMatching ) + "."; + m_join += valueName( value ); + + m_where += " AND "; + m_where += tableName( tableNotMatching ) + "."; + m_where += valueName( value ); + m_where += " IS null "; +} + + +void +QueryBuilder::setOptions( int options ) +{ + if ( options & optNoCompilations || options & optOnlyCompilations ) + m_linkTables |= tabSong; + + if ( options & optNoCompilations ) m_where += "AND tags.sampler = 0 "; + if ( options & optOnlyCompilations ) m_where += "AND tags.sampler = 1 "; + + if ( options & optRemoveDuplicates ) m_values = "DISTINCT " + m_values; + if ( options & optRandomize ) + { + if ( !m_sort.isEmpty() ) m_sort += ","; + m_sort += "RAND() "; + } +} + + +void +QueryBuilder::sortBy( int table, int value, bool descending ) +{ + //shall we sort case-sensitively? (not for integer columns!) + bool b = true; + if ( value & valID || value & valTrack || value & valScore || value & valLength || value & valBitrate || + value & valSamplerate || value & valPlayCounter || value & valAccessDate || value & valCreateDate || value & valPercentage || + table & tabYear ) + b = false; + + if ( !m_sort.isEmpty() ) m_sort += ","; + if ( b ) m_sort += "LOWER( "; + if ( table & tabYear ) m_sort += "("; + + m_sort += tableName( table ) + "."; + m_sort += valueName( value ); + + if ( table & tabYear ) m_sort += "+0)"; + + if ( b ) m_sort += " ) "; + if ( descending ) m_sort += " DESC "; + + m_linkTables |= table; +} + +void +QueryBuilder::sortByFunction( int function, int table, int value, bool descending ) +{ + // This function should be used with the equivalent addReturnFunctionValue (with the same function on same values) + // since it uses the "func(table.value) AS functablevalue" definition. + + //shall we sort case-sensitively? (not for integer columns!) + bool b = true; + if ( value & valID || value & valTrack || value & valScore || value & valLength || value & valBitrate || + value & valSamplerate || value & valPlayCounter || value & valAccessDate || value & valCreateDate || value & valPercentage || + table & tabYear ) + b = false; + + if ( !m_sort.isEmpty() ) m_sort += ","; + //m_sort += functionName( function ) + "("; + if ( b ) m_sort += "LOWER( "; + if ( table & tabYear ) m_sort += "("; + + QString columnName = functionName( function )+tableName( table )+valueName( value ); + m_sort += columnName; + + if ( table & tabYear ) m_sort += "+0)"; + if ( b ) m_sort += " ) "; + //m_sort += " ) "; + if ( descending ) m_sort += " DESC "; + + m_linkTables |= table; +} + +void +QueryBuilder::groupBy( int table, int value ) +{ + if ( !m_group.isEmpty() ) m_group += ","; + m_group += tableName( table ) + "."; + m_group += valueName( value ); + + m_linkTables |= table; +} + + +void +QueryBuilder::setLimit( int startPos, int length ) +{ + m_limit = QString( " LIMIT %1, %2 " ).arg( startPos ).arg( length ); +} + + +void +QueryBuilder::initSQLDrag() +{ + clear(); + addReturnValue( QueryBuilder::tabAlbum, QueryBuilder::valName ); + addReturnValue( QueryBuilder::tabArtist, QueryBuilder::valName ); + addReturnValue( QueryBuilder::tabGenre, QueryBuilder::valName ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valTitle ); + addReturnValue( QueryBuilder::tabYear, QueryBuilder::valName ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valComment ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valTrack ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valBitrate ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valLength ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valSamplerate ); + addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL ); +} + + +void +QueryBuilder::buildQuery() +{ + if ( m_query.isEmpty() ) + { + linkTables( m_linkTables ); + + m_query = "SELECT " + m_values + " FROM " + m_tables + " " + m_join + " WHERE true " + m_where; + // GROUP BY must be before ORDER BY for sqlite + if ( !m_group.isEmpty() ) m_query += " GROUP BY " + m_group; + if ( !m_sort.isEmpty() ) m_query += " ORDER BY " + m_sort; + m_query += m_limit; + } +} + +// get the builded SQL-Query (used in smartplaylisteditor soon) +QString +QueryBuilder::getQuery() +{ + if ( m_query.isEmpty()) + { + buildQuery(); + } + return m_query; +} + +QStringList +QueryBuilder::run() +{ + buildQuery(); + //debug() << m_query << endl; +// return query( m_query ); +} + + +void +QueryBuilder::clear() +{ + m_query = ""; + m_values = ""; + m_tables = ""; + m_join = ""; + m_where = ""; + m_sort = ""; + m_group = ""; + m_limit = ""; + + m_linkTables = 0; + m_returnValues = 0; +} + + +QString +QueryBuilder::tableName( int table ) +{ + QString tables; + + if ( table & tabSong ) tables += ",tags"; + if ( table & tabArtist ) tables += ",artist"; + if ( table & tabAlbum ) tables += ",album"; + if ( table & tabGenre ) tables += ",genre"; + if ( table & tabYear ) tables += ",year"; + if ( table & tabStats ) tables += ",statistics"; + + // when there are multiple tables involved, we always need table tags for linking them + return tables.mid( 1 ); +} + + +QString +QueryBuilder::valueName( int value ) +{ + QString values; + + if ( value & valID ) values += "id"; + if ( value & valName ) values += "name"; + if ( value & valURL ) values += "url"; + if ( value & valTitle ) values += "title"; + if ( value & valTrack ) values += "track"; + if ( value & valScore ) values += "percentage"; + if ( value & valComment ) values += "comment"; + if ( value & valBitrate ) values += "bitrate"; + if ( value & valLength ) values += "length"; + if ( value & valSamplerate ) values += "samplerate"; + if ( value & valPlayCounter ) values += "playcounter"; + if ( value & valAccessDate ) values += "accessdate"; + if ( value & valCreateDate ) values += "createdate"; + if ( value & valPercentage ) values += "percentage"; + if ( value & valArtistID ) values += "artist"; + if ( value & valAlbumID ) values += "album"; + if ( value & valGenreID ) values += "genre"; + if ( value & valYearID ) values += "year"; + + return values; +} + +QString +QueryBuilder::functionName( int value ) +{ + QString function; + + if ( value & funcCount ) function += "Count"; + if ( value & funcMax ) function += "Max"; + if ( value & funcMin ) function += "Min"; + if ( value & funcAvg ) function += "Avg"; + if ( value & funcSum ) function += "Sum"; + + return function; +} + diff --git a/amarok/src/database_refactor/dbenginebase.h b/amarok/src/database_refactor/dbenginebase.h new file mode 100644 index 00000000..880a34bd --- /dev/null +++ b/amarok/src/database_refactor/dbenginebase.h @@ -0,0 +1,133 @@ +/*************************************************************************** + * Copyright (C) 2004-2005 Mark Kretschmann <markey@web.de> * + * 2004 Christian Muehlhaeuser <chris@chris.de> * + * 2004 Sami Nieminen <sami.nieminen@iki.fi> * + * 2005 Ian Monroe <ian@monroe.nu> * + * * + * 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 Steet, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef AMAROK_DBENGINEBASE_H +#define AMAROK_DBENGINEBASE_H + +#include "plugin/plugin.h" //baseclass +#include <qobject.h> //baseclass + + +class DbConfig +{}; + +class DbConnection : public QObject, public amaroK::Plugin +{ + public: + enum DbConnectionType { sqlite = 0, mysql = 1, postgresql = 2 }; + + DbConnection( DbConfig* /* config */ ); + virtual ~DbConnection() = 0; + + virtual QStringList query( const QString& /* statement */ ) = 0; + virtual int insert( const QString& /* statement */, const QString& /* table */ ) = 0; + const bool isInitialized() const { return m_initialized; } + virtual bool isConnected() const = 0; + virtual const QString lastError() const { return "None"; } + + protected: + bool m_initialized; + DbConfig *m_config; +}; + + +class QueryBuilder +{ + public: + //attributes: + enum qBuilderTables { tabAlbum = 1, tabArtist = 2, tabGenre = 4, tabYear = 8, tabSong = 32, tabStats = 64, tabDummy = 0 }; + enum qBuilderOptions { optNoCompilations = 1, optOnlyCompilations = 2, optRemoveDuplicates = 4, optRandomize = 8 }; + enum qBuilderValues { valID = 1, valName = 2, valURL = 4, valTitle = 8, valTrack = 16, valScore = 32, valComment = 64, + valBitrate = 128, valLength = 256, valSamplerate = 512, valPlayCounter = 1024, + valCreateDate = 2048, valAccessDate = 4096, valPercentage = 8192, valArtistID = 16384, valAlbumID = 32768, + valYearID = 65536, valGenreID = 131072, valDummy = 0 }; + enum qBuilderFunctions { funcCount = 1, funcMax = 2, funcMin = 4, funcAvg = 8, funcSum = 16 }; + + enum qBuilderFilter { modeNormal = 0, modeFuzzy = 1 }; + + QueryBuilder(); + + QString escapeString( QString string ) + { + return + #ifdef USE_MYSQL + // We have to escape "\" for mysql, but can't do so for sqlite + (m_dbConnType == DbConnection::mysql) + ? string.replace("\\", "\\\\").replace( '\'', "''" ) + : + #endif + string.replace( '\'', "''" ); + } + + void addReturnValue( int table, int value ); + void addReturnFunctionValue( int function, int table, int value); + uint countReturnValues(); + + void addURLFilters( const QStringList& filter ); + + void addFilter( int tables, const QString& filter, int mode = modeNormal ); + void addFilters( int tables, const QStringList& filter ); + void excludeFilter( int tables, const QString& filter ); + + void addMatch( int tables, const QString& match ); + void addMatch( int tables, int value, const QString& match ); + void addMatches( int tables, const QStringList& match ); + void excludeMatch( int tables, const QString& match ); + + void exclusiveFilter( int tableMatching, int tableNotMatching, int value ); + + void setOptions( int options ); + void sortBy( int table, int value, bool descending = false ); + void sortByFunction( int function, int table, int value, bool descending = false ); + void groupBy( int table, int value ); + void setLimit( int startPos, int length ); + + void initSQLDrag(); + void buildQuery(); + QString getQuery(); + QString query() { buildQuery(); return m_query; }; + void clear(); + + QStringList run(); + + private: + QString tableName( int table ); + QString valueName( int value ); + QString functionName( int value ); + + void linkTables( int tables ); + + QString m_query; + QString m_values; + QString m_tables; + QString m_join; + QString m_where; + QString m_sort; + QString m_group; + QString m_limit; + + int m_linkTables; + uint m_returnValues; +}; + + +#endif /*AMAROK_DBENGINEBASE_H*/ diff --git a/amarok/src/database_refactor/sqlite/_Makefile.am b/amarok/src/database_refactor/sqlite/_Makefile.am new file mode 100644 index 00000000..bc58791a --- /dev/null +++ b/amarok/src/database_refactor/sqlite/_Makefile.am @@ -0,0 +1,38 @@ +kde_module_LTLIBRARIES = \ + libamarok_sqlite_dbengine_plugin.la + +SUBDIRS = \ + sqlite + +INCLUDES = \ + -I$(top_srcdir)/amarok/src/database/sqlite/sqlite \ + -I$(top_srcdir)/amarok/src/database \ + -I$(top_srcdir)/amarok/src/plugin \ + -I$(top_srcdir)/amarok/src/engine \ + -I$(top_srcdir)/amarok/src/amarokcore \ + -I$(top_srcdir)/amarok/src/statusbar \ + -I$(top_srcdir)/amarok/src \ + $(all_includes) $(taglib_includes) + +libamarok_sqlite_dbengine_plugin_la_LIBADD = \ + $(top_builddir)/amarok/src/database/sqlite/sqlite/libsqlite.la \ + $(top_builddir)/amarok/src/database/libdbengine.la \ + $(top_builddir)/amarok/src/plugin/libplugin.la \ + $(LIB_KDECORE) + +libamarok_sqlite_dbengine_plugin_la_SOURCES = \ + sqlite_dbengine.cpp + +libamarok_sqlite_dbengine_plugin_la_LDFLAGS = \ + -module \ + -no-undefined \ + $(KDE_PLUGIN) \ + $(all_libraries) + +METASOURCES = \ + AUTO + +kde_services_DATA = \ + amarok_sqlite_dbengine_plugin.desktop + + diff --git a/amarok/src/database_refactor/sqlite/amarok_sqlite_dbengine_plugin.desktop b/amarok/src/database_refactor/sqlite/amarok_sqlite_dbengine_plugin.desktop new file mode 100644 index 00000000..89fce118 --- /dev/null +++ b/amarok/src/database_refactor/sqlite/amarok_sqlite_dbengine_plugin.desktop @@ -0,0 +1,103 @@ +[Desktop Entry] +Encoding=UTF-8 +Type=Service +Name=SQLite DBEngine +Name[af]=SQLite DBEnjin +Name[ar]= محرك SQLite DBEngine +Name[bn]=এসকিউ-লাইট ডিবি-ইঞ্জিন +Name[br]=Keflusker DB SQLite +Name[da]=SQLite DB-motor +Name[de]=SQLite +Name[eo]=SQLite DBIlo +Name[es]=Motor de base de datos SQLite +Name[et]=SQLite'i andmebaasimootor +Name[fi]=SQLite-tietokantajärjestelmä +Name[fr]=Moteur de base de données SQLite +Name[ga]=Inneall SQLite +Name[gl]=Motor de BBDD SQLite +Name[he]=מנוע מסד נתונים SQLite +Name[hu]=SQLite adatbázis-alrendszer +Name[is]=SQLite gagnagrunnur +Name[it]=Motore DB SQLite +Name[ja]=SQLite DB エンジン +Name[lt]=SQLite duomenų bazės variklis +Name[nds]=SQLite +Name[ne]=एसक्यु लाइट डीबी इन्जिन +Name[nn]=SQLite-databasemotor +Name[pl]=Baza danych SQLite +Name[pt]=Motor de BD SQLite +Name[pt_BR]=Mecanismo do Banco de Dados SQLite +Name[ru]=SQLite +Name[sq]=Motor SQLite DB +Name[sr]=Мотор SQLite DB +Name[sr@Latn]=Motor SQLite DB +Name[ss]=Motor SQLite DB +Name[sv]=SQLite-databasgränssnitt +Name[tg]=Муҳаррики-DB барои SQLite +Name[tr]=SQLite DBMotoru +Name[uk]=Рушій бази даних SQLite +Name[uz]=SQLite maʼlumot bazasi +Name[uz@cyrillic]=SQLite маълумот базаси +Name[wa]=Éndjin d' BD SQLite +Name[zh_CN]=SQLite 数据引擎 +Name[zh_TW]=SQLite 資料庫引擎 +X-KDE-Library=libamarok_sqlite_dbengine_plugin +Comment=Plugin for Amarok +Comment[af]=Inprop module vir Amarok +Comment[ar]= قابس ( برنامج مضاف الى) AmaroK +Comment[bg]=Приставка за Amarok +Comment[bn]=আমারক-এর জন্য প্লাগিন +Comment[br]=Lugent evit Amarok +Comment[ca]=Connector per l'Amarok +Comment[cs]=Modul pro AmaroK +Comment[de]=Modul für Amarok +Comment[el]=Πρόσθετο για το AmaroK +Comment[eo]=Kromaĵo por Amarok +Comment[es]=Extensión para Amarok +Comment[et]=Amaroki plugin +Comment[fa]=وصله برای amaroK +Comment[fi]=Amarok-liitännäinen +Comment[fr]=Module pour Amarok +Comment[ga]=Breiseán AmaroK +Comment[gl]=Extensión para Amarok +Comment[hu]=Bővítőmodul az Amarokhoz +Comment[is]=Íforrit fyrir Amarok +Comment[it]=Plugin per Amarok +Comment[ja]=Amarok のためのプラグイン +Comment[ka]=მოდული Amarok-ისთვის +Comment[km]=កម្មវិធីជំនួយសម្រាប់ Amarok +Comment[lt]=Amarok įskiepis +Comment[mk]=Приклучок за Амарок +Comment[nb]=Programtillegg for Amarok +Comment[nds]=Moduul för Amarok +Comment[ne]=अमारोकका लागि प्लगइन +Comment[nl]=Plugin voor Amarok +Comment[nn]=Programtillegg for Amarok +Comment[pa]=ਅਮਰੋਕ ਲਈ ਪਲੱਗਇਨ +Comment[pl]=Wtyczka Amaroka +Comment[pt]='Plugin' para o Amarok +Comment[pt_BR]=Plugin para o Amarok +Comment[ru]=Модуль amaroK +Comment[se]=Lassemoduvla Amarok:ii +Comment[sk]=Amarok modul +Comment[sr]=Прикључак за Amarok +Comment[sr@Latn]=Priključak za Amarok +Comment[sv]=Insticksprogram för Amarok +Comment[th]=โปรแกรมเสริมสำหรับ Amarok +Comment[tr]=Amarok için Eklenti +Comment[uk]=Втулок для Amarok +Comment[uz]=Amarok uchun plagin +Comment[uz@cyrillic]=Amarok учун плагин +Comment[wa]=Tchôke-divins po Amarok +Comment[zh_CN]=Amarok 插件 +Comment[zh_TW]=amaroK 插件 +ServiceTypes=amaroK/Plugin + +X-KDE-amaroK-plugintype=dbengine +X-KDE-amaroK-name=sqlite-dbengine +X-KDE-amaroK-authors=Mark Kretschmann, Christian Muehlhaeuser +X-KDE-amaroK-email=markey@web.de +X-KDE-amaroK-rank=255 +X-KDE-amaroK-version=1 +X-KDE-amaroK-framework-version=5 + diff --git a/amarok/src/database_refactor/sqlite/sqlite_dbengine.cpp b/amarok/src/database_refactor/sqlite/sqlite_dbengine.cpp new file mode 100644 index 00000000..3d93a84c --- /dev/null +++ b/amarok/src/database_refactor/sqlite/sqlite_dbengine.cpp @@ -0,0 +1,227 @@ +// (c) 2004 Mark Kretschmann <markey@web.de> +// (c) 2004 Christian Muehlhaeuser <chris@chris.de> +// (c) 2004 Sami Nieminen <sami.nieminen@iki.fi> +// (c) 2005 Ian Monroe <ian@monroe.nu> +// See COPYING file for licensing information. + +#define DEBUG_PREFIX "SQLite-DBEngine" + +#include "app.h" +#include "amarok.h" +#include "amarokconfig.h" +#include "debug.h" +#include "sqlite_dbengine.h" + +#include <kapplication.h> + +#include <qfile.h> +#include <qimage.h> +#include <qtimer.h> + +#include <cmath> //DbConnection::sqlite_power() +#include <ctime> //query() +#include <unistd.h> //usleep() + +#include "sqlite/sqlite3.h" + +AMAROK_EXPORT_PLUGIN( SqliteDbEngine ) + + +////////////////////////////////////////////////////////////////////////////////////////// +// CLASS SqliteConnection +////////////////////////////////////////////////////////////////////////////////////////// + +SqliteDbEngine::SqliteDbEngine() + : DbConnection( new SqliteConfig( "collection.db" ) ) +{ + const QCString path = QString(/*amaroK::saveLocation()+*/"collection.db").local8Bit(); + + // Open database file and check for correctness + m_initialized = false; + QFile file( path ); + if ( file.open( IO_ReadOnly ) ) + { + QString format; + file.readLine( format, 50 ); + if ( !format.startsWith( "SQLite format 3" ) ) + { + warning() << "Database versions incompatible. Removing and rebuilding database.\n"; + } + else if ( sqlite3_open( path, &m_db ) != SQLITE_OK ) + { + warning() << "Database file corrupt. Removing and rebuilding database.\n"; + sqlite3_close( m_db ); + } + else + m_initialized = true; + } + + if ( !m_initialized ) + { + // Remove old db file; create new + QFile::remove( path ); + if ( sqlite3_open( path, &m_db ) == SQLITE_OK ) + { + m_initialized = true; + } + } + if ( m_initialized ) + { + if( sqlite3_create_function(m_db, "rand", 0, SQLITE_UTF8, NULL, sqlite_rand, NULL, NULL) != SQLITE_OK ) + m_initialized = false; + if( sqlite3_create_function(m_db, "power", 2, SQLITE_UTF8, NULL, sqlite_power, NULL, NULL) != SQLITE_OK ) + m_initialized = false; + } + + //optimization for speeding up SQLite + query( "PRAGMA default_synchronous = OFF;" ); +} + + +SqliteDbEngine::~SqliteDbEngine() +{ + if ( m_db ) sqlite3_close( m_db ); +} + + +QStringList SqliteDbEngine::query( const QString& statement ) +{ + QStringList values; + int error; + const char* tail; + sqlite3_stmt* stmt; + + //compile SQL program to virtual machine + error = sqlite3_prepare( m_db, statement.utf8(), statement.length(), &stmt, &tail ); + + if ( error != SQLITE_OK ) + { + Debug::error() << k_funcinfo << " sqlite3_compile error:" << endl; + Debug::error() << sqlite3_errmsg( m_db ) << endl; + Debug::error() << "on query: " << statement << endl; + values = QStringList(); + } + else + { + int busyCnt = 0; + int number = sqlite3_column_count( stmt ); + //execute virtual machine by iterating over rows + while ( true ) + { + error = sqlite3_step( stmt ); + + if ( error == SQLITE_BUSY ) + { + if ( busyCnt++ > 20 ) { + Debug::error() << "Busy-counter has reached maximum. Aborting this sql statement!\n"; + break; + } + ::usleep( 100000 ); // Sleep 100 msec + debug() << "sqlite3_step: BUSY counter: " << busyCnt << endl; + } + if ( error == SQLITE_MISUSE ) + debug() << "sqlite3_step: MISUSE" << endl; + if ( error == SQLITE_DONE || error == SQLITE_ERROR ) + break; + + //iterate over columns + for ( int i = 0; i < number; i++ ) + { + values << QString::fromUtf8( (const char*) sqlite3_column_text( stmt, i ) ); + } + } + //deallocate vm resources + sqlite3_finalize( stmt ); + + if ( error != SQLITE_DONE ) + { + Debug::error() << k_funcinfo << "sqlite_step error.\n"; + Debug::error() << sqlite3_errmsg( m_db ) << endl; + Debug::error() << "on query: " << statement << endl; + values = QStringList(); + } + } + + return values; +} + + +int SqliteDbEngine::insert( const QString& statement, const QString& /* table */ ) +{ + int error; + const char* tail; + sqlite3_stmt* stmt; + + //compile SQL program to virtual machine + error = sqlite3_prepare( m_db, statement.utf8(), statement.length(), &stmt, &tail ); + + if ( error != SQLITE_OK ) + { + Debug::error() << k_funcinfo << " sqlite3_compile error:" << endl; + Debug::error() << sqlite3_errmsg( m_db ) << endl; + Debug::error() << "on insert: " << statement << endl; + } + else + { + int busyCnt = 0; + //execute virtual machine by iterating over rows + while ( true ) + { + error = sqlite3_step( stmt ); + + if ( error == SQLITE_BUSY ) + { + if ( busyCnt++ > 20 ) { + Debug::error() << "Busy-counter has reached maximum. Aborting this sql statement!\n"; + break; + } + ::usleep( 100000 ); // Sleep 100 msec + debug() << "sqlite3_step: BUSY counter: " << busyCnt << endl; + } + if ( error == SQLITE_MISUSE ) + debug() << "sqlite3_step: MISUSE" << endl; + if ( error == SQLITE_DONE || error == SQLITE_ERROR ) + break; + } + //deallocate vm resources + sqlite3_finalize( stmt ); + + if ( error != SQLITE_DONE ) + { + Debug::error() << k_funcinfo << "sqlite_step error.\n"; + Debug::error() << sqlite3_errmsg( m_db ) << endl; + Debug::error() << "on insert: " << statement << endl; + } + } + return sqlite3_last_insert_rowid( m_db ); +} + + +// this implements a RAND() function compatible with the MySQL RAND() (0-param-form without seed) +void SqliteDbEngine::sqlite_rand(sqlite3_context *context, int /*argc*/, sqlite3_value ** /*argv*/) +{ + //sqlite3_result_double( context, static_cast<double>(KApplication::random()) / (RAND_MAX+1.0) ); +} + +// this implements a POWER() function compatible with the MySQL POWER() +void SqliteDbEngine::sqlite_power(sqlite3_context *context, int argc, sqlite3_value **argv) +{ + Q_ASSERT( argc==2 ); + if( sqlite3_value_type(argv[0])==SQLITE_NULL || sqlite3_value_type(argv[1])==SQLITE_NULL ) { + sqlite3_result_null(context); + return; + } + double a = sqlite3_value_double(argv[0]); + double b = sqlite3_value_double(argv[1]); + sqlite3_result_double( context, pow(a,b) ); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +// CLASS SqliteConfig +////////////////////////////////////////////////////////////////////////////////////////// + +SqliteConfig::SqliteConfig( const QString& dbfile ) + : m_dbfile( dbfile ) +{} + diff --git a/amarok/src/database_refactor/sqlite/sqlite_dbengine.h b/amarok/src/database_refactor/sqlite/sqlite_dbengine.h new file mode 100644 index 00000000..d19263e0 --- /dev/null +++ b/amarok/src/database_refactor/sqlite/sqlite_dbengine.h @@ -0,0 +1,58 @@ +// (c) 2004 Mark Kretschmann <markey@web.de> +// (c) 2004 Christian Muehlhaeuser <chris@chris.de> +// (c) 2004 Sami Nieminen <sami.nieminen@iki.fi> +// See COPYING file for licensing information. + +#ifndef AMAROK_SQLITE_DBENGINE_H +#define AMAROK_SQLITE_DBENGINE_H + +#include "dbenginebase.h" +#include <kurl.h> +#include <qdir.h> //stack allocated +#include <qobject.h> //baseclass +#include <qptrqueue.h> //baseclass +#include <qsemaphore.h> //stack allocated +#include <qstringlist.h> //stack allocated + +class DbConfig; +class DbConnection; +class DbConnectionPool; +class CoverFetcher; +class MetaBundle; +class Scrobbler; + + +class SqliteConfig : public DbConfig +{ + public: + SqliteConfig( const QString& /* dbfile */ ); + + const QString dbFile() const { return m_dbfile; } + + private: + QString m_dbfile; +}; + + +typedef struct sqlite3 sqlite3; +typedef struct sqlite3_context sqlite3_context; +typedef struct Mem sqlite3_value; + +class SqliteDbEngine : public DbConnection +{ + public: + SqliteDbEngine(); + ~SqliteDbEngine(); + + QStringList query( const QString& /* statement */ ); + int insert( const QString& /* statement */, const QString& /* table */ ); + bool isConnected()const { return true; } + private: + static void sqlite_rand(sqlite3_context *context, int /*argc*/, sqlite3_value ** /*argv*/); + static void sqlite_power(sqlite3_context *context, int argc, sqlite3_value **argv); + + sqlite3* m_db; +}; + + +#endif /*SQLITE_DBENGINE_H*/ |
