You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
amarok/amarok/src/collectiondb.cpp

8075 lines
289 KiB

// (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>
// (c) 2005 Jeff Mitchell <kde-dev@emailgoeshere.com>
// (c) 2005 Isaiah Damron <xepo@trifault.net>
// (c) 2005-2006 Alexandre Pereira de Oliveira <aleprj@gmail.com>
// (c) 2006 Jonas Hurrelmann <j@outpo.st>
// (c) 2006 Shane King <kde@dontletsstart.com>
// (c) 2006 Peter C. Ndikuwera <pndiku@gmail.com>
// (c) 2006 Stanislav Nikolov <valsinats@gmail.com>
// 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 "coverfetcher.h"
#include "enginecontroller.h"
#include "expression.h"
#include "mediabrowser.h"
#include "metabundle.h" //updateTags()
#include "mountpointmanager.h" //buildQuery()
#include "organizecollectiondialog.h"
#include "playlist.h"
#include "playlistloader.h"
#include "playlistbrowser.h"
#include "podcastbundle.h" //addPodcast
#include "qstringx.h"
#include "scancontroller.h"
#include "scriptmanager.h"
#include "scrobbler.h"
#include "statusbar.h"
#include "threadmanager.h"
#include <tqbuffer.h>
#include <tqcheckbox.h>
#include <tqdeepcopy.h>
#include <tqfile.h>
#include <tqmap.h>
#include <tqmutex.h>
#include <tqregexp.h> //setHTMLLyrics()
#include <tqtimer.h>
#include <tqpainter.h> //createDragPixmap()
#include <tqpalette.h>
#include <pthread.h> //debugging, can be removed later
#include <kcharsets.h> //setHTMLLyrics()
#include <kcombobox.h>
#include <kconfig.h>
#include <kdialogbase.h> //checkDatabase()
#include <kglobal.h>
#include <kinputdialog.h> //setupCoverFetcher()
#include <klineedit.h> //setupCoverFetcher()
#include <klocale.h>
#include <kmdcodec.h>
#include <kmessagebox.h>
#include <ksimpleconfig.h>
#include <kstandarddirs.h>
#include <kio/job.h>
#include <kio/netaccess.h>
#include <cmath> //DbConnection::sqlite_power()
#include <ctime> //query()
#include <cstdlib> //exit()
#include <unistd.h> //usleep()
#include <taglib/audioproperties.h>
#include "sqlite/sqlite3.h"
#ifdef USE_MYSQL
#include <mysql/mysql.h>
#include <mysql/mysql_version.h>
#endif
#ifdef USE_POSTGRESQL
#include <libpq-fe.h>
#endif
#undef HAVE_INOTIFY // NOTE Disabled for now, due to stability issues
#ifdef HAVE_INOTIFY
#include <linux/inotify.h>
#include "inotify/inotify-syscalls.h"
#endif
using Amarok::QStringx;
#define DEBUG 0
//////////////////////////////////////////////////////////////////////////////////////////
// CLASS INotify
//////////////////////////////////////////////////////////////////////////////////////////
INotify* INotify::s_instance = 0;
INotify::INotify( CollectionDB *parent, int fd )
: DependentJob( parent, "INotify" )
, m_parent( parent )
, m_fd( fd )
{
s_instance = this;
}
INotify::~INotify()
{}
bool
INotify::watchDir( const TQString directory )
{
#ifdef HAVE_INOTIFY
int wd = inotify_add_watch( m_fd, directory.local8Bit(), IN_CLOSE_WRITE | IN_DELETE | IN_MOVE |
IN_MODIFY | IN_ATTRIB );
if ( wd < 0 )
debug() << "Could not add INotify watch for: " << directory << endl;
return ( wd >= 0 );
#else
Q_UNUSED(directory);
#endif
return false;
}
bool
INotify::doJob()
{
#ifdef HAVE_INOTIFY
DEBUG_BLOCK
IdList list = MountPointManager::instance()->getMountedDeviceIds();
TQString deviceIds;
foreachType( IdList, list )
{
if ( !deviceIds.isEmpty() ) deviceIds += ',';
deviceIds += TQString::number(*it);
}
const TQStringList values = m_parent->query( TQString( "SELECT dir, deviceid FROM directories WHERE deviceid IN (%1);" )
.arg( deviceIds ) );
foreach( values )
{
TQString rpath = *it;
int deviceid = (*(++it)).toInt();
TQString abspath = MountPointManager::instance()->getAbsolutePath( deviceid, rpath );
watchDir( abspath );
}
/* size of the event structure, not counting name */
const int EVENT_SIZE = ( sizeof( struct inotify_event ) );
/* reasonable guess as to size of 1024 events */
const int BUF_LEN = 1024 * ( EVENT_SIZE + 16 );
while ( 1 )
{
char buf[BUF_LEN];
int len, i = 0;
len = read( m_fd, buf, BUF_LEN );
if ( len < 0 )
{
debug() << "Read from INotify failed" << endl;
return false;
}
else
{
if ( !len )
{
/* BUF_LEN too small? */
}
else
{
while ( i < len )
{
struct inotify_event *event;
event = (struct inotify_event *) &buf[i];
i += EVENT_SIZE + event->len;
}
TQTimer::singleShot( 0, m_parent, TQT_SLOT( scanMonitor() ) );
}
}
}
#endif
// this shouldn't happen
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////
// CLASS CollectionDB
//////////////////////////////////////////////////////////////////////////////////////////
TQMutex* CollectionDB::connectionMutex = new TQMutex();
TQMutex* CollectionDB::itemCoverMapMutex = new TQMutex();
//we don't have to worry about this map leaking memory since ThreadManager limits the total
//number of TQThreads ever created
TQMap<TQThread *, DbConnection *> *CollectionDB::threadConnections = new TQMap<TQThread *, DbConnection *>();
TQMap<TQListViewItem*, CoverFetcher*> *CollectionDB::itemCoverMap = new TQMap<TQListViewItem*, CoverFetcher*>();
CollectionDB* CollectionDB::instance()
{
static CollectionDB db;
return &db;
}
CollectionDB::CollectionDB()
: EngineObserver( EngineController::instance() )
, m_autoScoring( true )
, m_noCover( locate( "data", "amarok/images/nocover.png" ) )
, m_shadowImage( locate( "data", "amarok/images/shadow_albumcover.png" ) )
, m_scanInProgress( false )
, m_rescanRequired( false )
, m_aftEnabledPersistentTables()
, m_moveFileJobCancelled( false )
{
DEBUG_BLOCK
#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;
//perform all necessary operations to allow MountPointManager to access the devices table here
//there is a recursive dependency between CollectionDB and MountPointManager and this is the workaround
//checkDatabase has to be able to access MountPointManager
//<OPEN DATABASE>
initialize();
//</OPEN DATABASE>
// Remove cached "nocover" images, so that a new version actually gets shown
// The asterisk is for also deleting the shadow-caches.
const TQStringList entryList = cacheCoverDir().entryList( "*nocover.png*", TQDir::Files );
foreach( entryList )
cacheCoverDir().remove( *it );
connect( this, TQT_SIGNAL(fileMoved(const TQString&, const TQString&, const TQString&)),
this, TQT_SLOT(aftMigratePermanentTablesUrl(const TQString&, const TQString&, const TQString&)) );
connect( this, TQT_SIGNAL(uniqueIdChanged(const TQString&, const TQString&, const TQString&)),
this, TQT_SLOT(aftMigratePermanentTablesUniqueId(const TQString&, const TQString&, const TQString&)) );
connect( tqApp, TQT_SIGNAL( aboutToQuit() ), this, TQT_SLOT( disableAutoScoring() ) );
connect( this, TQT_SIGNAL( coverRemoved( const TQString&, const TQString& ) ),
TQT_SIGNAL( coverChanged( const TQString&, const TQString& ) ) );
connect( Scrobbler::instance(), TQT_SIGNAL( similarArtistsFetched( const TQString&, const TQStringList& ) ),
this, TQT_SLOT( similarArtistsFetched( const TQString&, const TQStringList& ) ) );
// If we're supposed to monitor dirs for changes, make sure we run it once
// on startup, since inotify can't inform us about old events
// TQTimer::singleShot( 0, this, TQT_SLOT( scanMonitor() ) )
initDirOperations();
m_aftEnabledPersistentTables << "lyrics" << "statistics" << "tags_labels";
}
CollectionDB::~CollectionDB()
{
DEBUG_BLOCK
#ifdef HAVE_INOTIFY
if ( INotify::instance()->fd() >= 0 )
close( INotify::instance()->fd() );
#endif
destroy();
}
inline TQString
CollectionDB::exactCondition( const TQString &right )
{
if ( DbConnection::mysql == instance()->getDbConnectionType() )
return TQString( "= BINARY '" + instance()->escapeString( right ) + '\'' );
else
return TQString( "= '" + instance()->escapeString( right ) + '\'' );
}
TQString
CollectionDB::likeCondition( const TQString &right, bool anyBegin, bool anyEnd )
{
TQString escaped = right;
escaped.replace( '/', "//" ).replace( '%', "/%" ).replace( '_', "/_" );
escaped = instance()->escapeString( escaped );
TQString ret;
if ( DbConnection::postgresql == instance()->getDbConnectionType() )
ret = " ILIKE "; //case-insensitive according to locale
else
ret = " LIKE ";
ret += '\'';
if ( anyBegin )
ret += '%';
ret += escaped;
if ( anyEnd )
ret += '%';
ret += '\'';
//Use / as the escape character
ret += " ESCAPE '/' ";
return ret;
}
//////////////////////////////////////////////////////////////////////////////////////////
// PUBLIC
//////////////////////////////////////////////////////////////////////////////////////////
void
CollectionDB::initDirOperations()
{
//this code was originally part of the ctor. It has to call MountPointManager to
//generate absolute paths from deviceids and relative paths. MountPointManager's ctor
//absolutely has to access the database, which resulted in a recursive ctor call. To
//solve this problem, the directory access code was moved into its own method, which can
//only be called when the CollectionDB object already exists.
//FIXME max: make sure we check additional directories if we connect a new device
#ifdef HAVE_INOTIFY
// Try to initialize inotify, if not available use the old timer approach.
int inotify_fd = inotify_init();
if ( inotify_fd < 0 )
#endif
{
// debug() << "INotify not available, using TQTimer!" << endl;
startTimer( MONITOR_INTERVAL * 1000 );
}
#ifdef HAVE_INOTIFY
else
{
debug() << "INotify enabled!" << endl;
ThreadManager::instance()->onlyOneJob( new INotify( this, inotify_fd ) );
}
#endif
}
/**
* 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 TQStringList() on error.
*/
TQStringList
CollectionDB::query( const TQString& statement, bool suppressDebug )
{
m_mutex.lock();
clock_t start;
if ( DEBUG )
{
debug() << "Query-start: " << statement << endl;
start = clock();
}
if ( statement.stripWhiteSpace().isEmpty() )
{
m_mutex.unlock();
return TQStringList();
}
DbConnection *dbConn;
dbConn = getMyConnection();
TQStringList values = dbConn->query( statement, suppressDebug );
if ( DEBUG )
{
clock_t finish = clock();
const double duration = (double) (finish - start) / CLOCKS_PER_SEC;
debug() << "SQL-query (" << duration << "s): " << statement << endl;
}
m_mutex.unlock();
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 TQString& statement, const TQString& table )
{
m_mutex.lock();
clock_t start;
if ( DEBUG )
{
debug() << "insert-start: " << statement << endl;
start = clock();
}
DbConnection *dbConn;
dbConn = getMyConnection();
int id = dbConn->insert( statement, table );
if ( DEBUG )
{
clock_t finish = clock();
const double duration = (double) (finish - start) / CLOCKS_PER_SEC;
debug() << "SQL-insert (" << duration << "s): " << statement << endl;
}
m_mutex.unlock();
return id;
}
TQString
CollectionDB::deviceidSelection( const bool showAll )
{
if ( !showAll )
{
IdList list = MountPointManager::instance()->getMountedDeviceIds();
TQString deviceIds = "";
foreachType( IdList, list )
{
if ( it != list.begin() ) deviceIds += ',';
deviceIds += TQString::number(*it);
}
return " AND tags.deviceid IN (" + deviceIds + ')';
}
else return "";
}
TQStringList
CollectionDB::URLsFromQuery( const TQStringList &result ) const
{
TQStringList values;
foreach( result )
{
const int id = (*it).toInt();
values << MountPointManager::instance()->getAbsolutePath( id, *(++it) );
}
return values;
}
KURL::List
CollectionDB::URLsFromSqlDrag( const TQStringList &values ) const
{
KURL::List urls;
for( TQStringList::const_iterator it = values.begin();
it != values.end();
it++ )
{
const TQString &rel = *it;
it++;
int id = (*it).toInt();
urls += KURL::fromPathOrURL( MountPointManager::instance()->getAbsolutePath( id, rel ) );
for( int i = 0;
i < QueryBuilder::dragFieldCount-1 && it != values.end();
i++ )
it++;
}
return urls;
}
bool
CollectionDB::isEmpty( )
{
TQStringList values;
values = query( "SELECT COUNT( url ) FROM tags LIMIT 1 OFFSET 0;" );
return values.isEmpty() ? true : values.first() == "0";
}
bool
CollectionDB::isValid( )
{
TQStringList values1;
TQStringList values2;
TQStringList values3;
TQStringList values4;
TQStringList values5;
values1 = query( "SELECT COUNT( url ) FROM tags LIMIT 1 OFFSET 0;" );
values2 = query( "SELECT COUNT( url ) FROM statistics LIMIT 1 OFFSET 0;" );
values3 = query( "SELECT COUNT( url ) FROM podcastchannels LIMIT 1 OFFSET 0;" );
values4 = query( "SELECT COUNT( url ) FROM podcastepisodes LIMIT 1 OFFSET 0;" );
values5 = query( "SELECT COUNT( id ) FROM devices LIMIT 1 OFFSET 0;" );
//It's valid as long as we've got _some_ tables that have something in.
return !( values1.isEmpty() && values2.isEmpty() && values3.isEmpty() && values4.isEmpty() && values5.isEmpty() );
}
TQString
CollectionDB::adminValue( TQString noption ) {
TQStringList values;
values = query (
TQString( "SELECT value FROM admin WHERE noption = '%1';").arg(noption)
);
return values.isEmpty() ? "" : values.first();
}
void
CollectionDB::setAdminValue( TQString noption, TQString value ) {
TQStringList values = query( TQString( "SELECT value FROM admin WHERE noption = '%1';").arg( noption ));
if(values.count() > 0)
{
query( TQString( "UPDATE admin SET value = '%1' WHERE noption = '%2';" ).arg( value, noption ) );
}
else
{
insert( TQString( "INSERT INTO admin (value, noption) values ( '%1', '%2' );" ).arg( value, noption ),
NULL );
}
}
void
CollectionDB::createTables( const bool temporary )
{
DEBUG_BLOCK
//create tag table
query( TQString( "CREATE %1 TABLE tags%2 ("
"url " + exactTextColumnType() + ","
"dir " + exactTextColumnType() + ","
"createdate INTEGER,"
"modifydate INTEGER,"
"album INTEGER,"
"artist INTEGER,"
"composer INTEGER,"
"genre INTEGER,"
"title " + textColumnType() + ","
"year INTEGER,"
"comment " + longTextColumnType() + ","
"track NUMERIC(4),"
"discnumber INTEGER,"
"bitrate INTEGER,"
"length INTEGER,"
"samplerate INTEGER,"
"filesize INTEGER,"
"filetype INTEGER,"
"sampler BOOL,"
"bpm FLOAT,"
"deviceid INTEGER);" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" ) );
TQString albumAutoIncrement = "";
TQString artistAutoIncrement = "";
TQString composerAutoIncrement = "";
TQString genreAutoIncrement = "";
TQString yearAutoIncrement = "";
if ( getDbConnectionType() == DbConnection::postgresql )
{
if(!temporary)
{
query( TQString( "CREATE SEQUENCE album_seq;" ) );
query( TQString( "CREATE SEQUENCE artist_seq;" ) );
query( TQString( "CREATE SEQUENCE composer_seq;" ) );
query( TQString( "CREATE SEQUENCE genre_seq;" ) );
query( TQString( "CREATE SEQUENCE year_seq;" ) );
}
albumAutoIncrement = TQString("DEFAULT nextval('album_seq')");
artistAutoIncrement = TQString("DEFAULT nextval('artist_seq')");
composerAutoIncrement = TQString("DEFAULT nextval('composer_seq')");
genreAutoIncrement = TQString("DEFAULT nextval('genre_seq')");
yearAutoIncrement = TQString("DEFAULT nextval('year_seq')");
}
else if ( getDbConnectionType() == DbConnection::mysql )
{
albumAutoIncrement = "AUTO_INCREMENT";
artistAutoIncrement = "AUTO_INCREMENT";
composerAutoIncrement = "AUTO_INCREMENT";
genreAutoIncrement = "AUTO_INCREMENT";
yearAutoIncrement = "AUTO_INCREMENT";
}
//create album table
query( TQString( "CREATE %1 TABLE album%2 ("
"id INTEGER PRIMARY KEY %3,"
"name " + textColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" )
.arg( albumAutoIncrement ) );
//create artist table
query( TQString( "CREATE %1 TABLE artist%2 ("
"id INTEGER PRIMARY KEY %3,"
"name " + textColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" )
.arg( artistAutoIncrement ) );
//create composer table
query( TQString( "CREATE %1 TABLE composer%2 ("
"id INTEGER PRIMARY KEY %3,"
"name " + textColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" )
.arg( composerAutoIncrement ) );
//create genre table
query( TQString( "CREATE %1 TABLE genre%2 ("
"id INTEGER PRIMARY KEY %3,"
"name " + textColumnType() +");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" )
.arg( genreAutoIncrement ) );
//create year table
query( TQString( "CREATE %1 TABLE year%2 ("
"id INTEGER PRIMARY KEY %3,"
"name " + textColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" )
.arg( yearAutoIncrement ) );
//create images table
query( TQString( "CREATE %1 TABLE images%2 ("
"path " + exactTextColumnType() + ","
"deviceid INTEGER,"
"artist " + textColumnType() + ","
"album " + textColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" ) );
//create embed table
query( TQString( "CREATE %1 TABLE embed%2 ("
"url " + exactTextColumnType() + ","
"deviceid INTEGER,"
"hash " + exactTextColumnType() + ","
"description " + textColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" ) );
// create directory statistics table
query( TQString( "CREATE %1 TABLE directories%2 ("
"dir " + exactTextColumnType() + ","
"deviceid INTEGER,"
"changedate INTEGER);" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" ) );
//create uniqueid table
query( TQString( "CREATE %1 TABLE uniqueid%2 ("
"url " + exactTextColumnType() + ","
"deviceid INTEGER,"
"uniqueid " + exactTextColumnType(32) + " UNIQUE,"
"dir " + exactTextColumnType() + ");" )
.arg( temporary ? "TEMPORARY" : "" )
.arg( temporary ? "_temp" : "" ) );
//create indexes
query( TQString( "CREATE INDEX album_idx%1 ON album%2( name );" )
.arg( temporary ? "_temp" : "" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "CREATE INDEX artist_idx%1 ON artist%2( name );" )
.arg( temporary ? "_temp" : "" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "CREATE INDEX composer_idx%1 ON composer%2( name );" )
.arg( temporary ? "_temp" : "" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "CREATE INDEX genre_idx%1 ON genre%2( name );" )
.arg( temporary ? "_temp" : "" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "CREATE INDEX year_idx%1 ON year%2( name );" )
.arg( temporary ? "_temp" : "" ).arg( temporary ? "_temp" : "" ) );
if ( !temporary )
{
//create admin table -- holds the db version, put here other stuff if necessary
query( TQString( "CREATE TABLE admin ("
"noption " + textColumnType() + ", "
"value " + textColumnType() + ");" ) );
// create related artists cache
query( TQString( "CREATE TABLE related_artists ("
"artist " + textColumnType() + ","
"suggestion " + textColumnType() + ","
"changedate INTEGER );" ) );
createIndices();
}
else
{
query( "CREATE UNIQUE INDEX url_tagtemp ON tags_temp( url, deviceid );" );
query( "CREATE UNIQUE INDEX embed_urltemp ON embed_temp( url, deviceid );" );
query( "CREATE UNIQUE INDEX dir_temp_dir ON directories_temp( dir, deviceid );" );
query( "CREATE INDEX album_tagtemp ON tags_temp( album );" );
query( "CREATE INDEX artist_tagtemp ON tags_temp( artist );" );
query( "CREATE INDEX sampler_tagtemp ON tags_temp( sampler );" );
query( "CREATE INDEX uniqueidtemp_uniqueid ON uniqueid_temp( uniqueid );");
query( "CREATE INDEX uniqueidtemp_url ON uniqueid_temp( url, deviceid );");
}
}
void
CollectionDB::createIndices()
{
//This creates the indices for tables created in createTables. It should not refer to
//tables which are not created in that function.
debug() << "Creating indices, ignore errors about already existing indices" << endl;
query( "CREATE UNIQUE INDEX url_tag ON tags( url, deviceid );" );
query( "CREATE INDEX album_tag ON tags( album );" );
query( "CREATE INDEX artist_tag ON tags( artist );" );
query( "CREATE INDEX composer_tag ON tags( composer );" );
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 images_url ON images( path, deviceid );" );
query( "CREATE UNIQUE INDEX embed_url ON embed( url, deviceid );" );
query( "CREATE INDEX embed_hash ON embed( hash );" );
query( "CREATE UNIQUE INDEX directories_dir ON directories( dir, deviceid );" );
query( "CREATE INDEX uniqueid_uniqueid ON uniqueid( uniqueid );");
query( "CREATE INDEX uniqueid_url ON uniqueid( url, deviceid );");
query( "CREATE INDEX album_idx ON album( name );" );
query( "CREATE INDEX artist_idx ON artist( name );" );
query( "CREATE INDEX composer_idx ON composer( name );" );
query( "CREATE INDEX genre_idx ON genre( name );" );
query( "CREATE INDEX year_idx ON year( name );" );
query( "CREATE INDEX tags_artist_index ON tags( artist );" );
query( "CREATE INDEX tags_album_index ON tags( album );" );
query( "CREATE INDEX tags_deviceid_index ON tags( deviceid ); ");
query( "CREATE INDEX tags_url_index ON tags( url ); ");
query( "CREATE INDEX embed_deviceid_index ON embed( deviceid ); ");
query( "CREATE INDEX embed_url_index ON embed( url ); ");
query( "CREATE INDEX related_artists_artist ON related_artists( artist );" );
debug() << "Finished creating indices, stop ignoring errors" << endl;
}
void
CollectionDB::createPermanentIndices()
{
//this method creates all indices which are not referred to in createTables
//this method is called on each startup of amarok
//until we figure out a way to handle this better it produces SQL errors if the indices
//already exist, but these can be ignored
debug() << "Creating permanent indices, ignore errors about already existing indices" << endl;
query( "CREATE UNIQUE INDEX lyrics_url ON lyrics( url, deviceid );" );
query( "CREATE INDEX lyrics_uniqueid ON lyrics( uniqueid );" );
query( "CREATE INDEX playlist_playlists ON playlists( playlist );" );
query( "CREATE INDEX url_playlists ON playlists( url );" );
query( "CREATE UNIQUE INDEX labels_name ON labels( name, type );" );
query( "CREATE INDEX tags_labels_uniqueid ON tags_labels( uniqueid );" ); //m:n relationship, DO NOT MAKE UNIQUE!
query( "CREATE INDEX tags_labels_url ON tags_labels( url, deviceid );" ); //m:n relationship, DO NOT MAKE UNIQUE!
query( "CREATE INDEX tags_labels_labelid ON tags_labels( labelid );" ); //m:n relationship, DO NOT MAKE UNIQUE!
query( "CREATE UNIQUE INDEX url_stats ON statistics( deviceid, url );" );
query( "CREATE INDEX percentage_stats ON statistics( percentage );" );
query( "CREATE INDEX rating_stats ON statistics( rating );" );
query( "CREATE INDEX playcounter_stats ON statistics( playcounter );" );
query( "CREATE INDEX uniqueid_stats ON statistics( uniqueid );" );
query( "CREATE INDEX url_podchannel ON podcastchannels( url );" );
query( "CREATE INDEX url_podepisode ON podcastepisodes( url );" );
query( "CREATE INDEX localurl_podepisode ON podcastepisodes( localurl );" );
query( "CREATE INDEX url_podfolder ON podcastfolders( id );" );
debug() << "Finished creating permanent indices, stop ignoring errors" << endl;
}
void
CollectionDB::dropTables( const bool temporary )
{
query( TQString( "DROP TABLE tags%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE album%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE artist%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE composer%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE genre%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE year%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE images%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE embed%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE directories%1;" ).arg( temporary ? "_temp" : "" ) );
query( TQString( "DROP TABLE uniqueid%1;" ).arg( temporary ? "_temp" : "" ) );
if ( !temporary )
{
query( TQString( "DROP TABLE related_artists;" ) );
debug() << "Dropping media table" << endl;
}
if ( getDbConnectionType() == DbConnection::postgresql )
{
if (temporary == false) {
query( TQString( "DROP SEQUENCE album_seq;" ) );
query( TQString( "DROP SEQUENCE artist_seq;" ) );
query( TQString( "DROP SEQUENCE composer_seq;" ) );
query( TQString( "DROP SEQUENCE genre_seq;" ) );
query( TQString( "DROP SEQUENCE year_seq;" ) );
}
}
}
void
CollectionDB::clearTables( const bool temporary )
{
TQString clearCommand = "DELETE FROM";
if ( getDbConnectionType() == DbConnection::mysql || getDbConnectionType() == DbConnection::postgresql)
{
// TRUNCATE TABLE is faster than DELETE FROM TABLE, so use it when supported.
clearCommand = "TRUNCATE TABLE";
}
query( TQString( "%1 tags%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 album%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 artist%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 composer%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 genre%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 year%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 images%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 embed%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 directories%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
query( TQString( "%1 uniqueid%2;" ).arg( clearCommand ).arg( temporary ? "_temp" : "" ) );
if ( !temporary )
{
query( TQString( "%1 related_artists;" ).arg( clearCommand ) );
//debug() << "Clearing media table" << endl;
//query( TQString( "%1 media;" ).arg( clearCommand ) );
}
}
void
CollectionDB::copyTempTables( )
{
DEBUG_BLOCK
insert( "INSERT INTO tags SELECT * FROM tags_temp;", NULL );
//mysql 5 supports subqueries with IN, mysql 4 doesn't. this way will work for all SQL servers
TQStringList albumIdList = query( "SELECT album.id FROM album;" );
//in an empty database, albumIdList is empty. This would result in a SQL query like NOT IN ( ) without
//the -1 below which is invalid SQL. The auto generated values start at 1 so this is fine
TQString albumIds = "-1";
foreach( albumIdList )
{
albumIds += ',';
albumIds += *it;
}
insert( TQString ( "INSERT INTO album SELECT * FROM album_temp WHERE album_temp.id NOT IN ( %1 );" ).arg( albumIds ), NULL );
TQStringList artistIdList = query( "SELECT artist.id FROM artist;" );
TQString artistIds = "-1";
foreach( artistIdList )
{
artistIds += ',';
artistIds += *it;
}
insert( TQString ( "INSERT INTO artist SELECT * FROM artist_temp WHERE artist_temp.id NOT IN ( %1 );" ).arg( artistIds ), NULL );
TQStringList composerIdList = query( "SELECT composer.id FROM composer;" );
TQString composerIds = "-1";
foreach( composerIdList )
{
composerIds += ',';
composerIds += *it;
}
insert( TQString ( "INSERT INTO composer SELECT * FROM composer_temp WHERE composer_temp.id NOT IN ( %1 );" ).arg( composerIds ), NULL );
TQStringList genreIdList = query( "SELECT genre.id FROM genre;" );
TQString genreIds = "-1";
foreach( genreIdList )
{
genreIds += ',';
genreIds += *it;
}
insert( TQString ( "INSERT INTO genre SELECT * FROM genre_temp WHERE genre_temp.id NOT IN ( %1 );" ).arg( genreIds ), NULL );
TQStringList yearIdList = query( "SELECT year.id FROM year;" );
TQString yearIds = "-1";
foreach( yearIdList )
{
yearIds += ',';
yearIds += *it;
}
insert( TQString ( "INSERT INTO year SELECT * FROM year_temp WHERE year_temp.id NOT IN ( %1 );" ).arg( yearIds ), NULL );
insert( "INSERT INTO images SELECT * FROM images_temp;", NULL );
insert( "INSERT INTO embed SELECT * FROM embed_temp;", NULL );
insert( "INSERT INTO directories SELECT * FROM directories_temp;", NULL );
insert( "INSERT INTO uniqueid SELECT * FROM uniqueid_temp;", NULL );
}
void
CollectionDB::prepareTempTables()
{
DEBUG_BLOCK
insert( "INSERT INTO album_temp SELECT * from album;", 0 );
insert( "INSERT INTO artist_temp SELECT * from artist;", 0 );
insert( "INSERT INTO composer_temp SELECT * from composer;", 0 );
insert( "INSERT INTO genre_temp SELECT * from genre;", 0 );
insert( "INSERT INTO year_temp SELECT * from year;", 0 );
}
void
CollectionDB::createDevicesTable()
{
debug() << "Creating DEVICES table" << endl;
TQString deviceAutoIncrement = "";
if ( getDbConnectionType() == DbConnection::postgresql )
{
query( TQString( "CREATE SEQUENCE devices_seq;" ) );
deviceAutoIncrement = TQString("DEFAULT nextval('devices_seq')");
}
else if ( getDbConnectionType() == DbConnection::mysql )
{
deviceAutoIncrement = "AUTO_INCREMENT";
}
query( TQString( "CREATE TABLE devices ("
"id INTEGER PRIMARY KEY %1,"
"type " + textColumnType() + ","
"label " + textColumnType() + ","
"lastmountpoint " + textColumnType() + ","
"uuid " + textColumnType() + ","
"servername " + textColumnType() + ","
"sharename " + textColumnType() + ");" )
.arg( deviceAutoIncrement ) );
query( "CREATE INDEX devices_type ON devices( type );" );
query( "CREATE INDEX devices_uuid ON devices( uuid );" );
query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" );
}
void
CollectionDB::createStatsTable()
{
// create music statistics database
query( TQString( "CREATE TABLE statistics ("
"url " + exactTextColumnType() + ","
"deviceid INTEGER,"
"createdate INTEGER,"
"accessdate INTEGER,"
"percentage FLOAT,"
"rating INTEGER DEFAULT 0,"
"playcounter INTEGER,"
"uniqueid " + exactTextColumnType(32) + " UNIQUE,"
"deleted BOOL DEFAULT " + boolF() + ","
"PRIMARY KEY(url, deviceid) );" ) );
}
//Old version, used in upgrade code. This should never be changed.
void
CollectionDB::createStatsTableV8()
{
// create music statistics database - old form, for upgrade code.
query( TQString( "CREATE TABLE statistics ("
"url " + textColumnType() + " UNIQUE,"
"createdate INTEGER,"
"accessdate INTEGER,"
"percentage FLOAT,"
"rating INTEGER DEFAULT 0,"
"playcounter INTEGER,"
"uniqueid " + textColumnType(8) + " UNIQUE,"
"deleted BOOL DEFAULT " + boolF() + ");" ) );
query( "CREATE INDEX url_stats ON statistics( url );" );
query( "CREATE INDEX percentage_stats ON statistics( percentage );" );
query( "CREATE INDEX rating_stats ON statistics( rating );" );
query( "CREATE INDEX playcounter_stats ON statistics( playcounter );" );
query( "CREATE INDEX uniqueid_stats ON statistics( uniqueid );" );
}
//Old version, used in upgrade code
void
CollectionDB::createStatsTableV10( bool temp )
{
// create music statistics database
query( TQString( "CREATE %1 TABLE statistics%2 ("
"url " + exactTextColumnType() + ","
"deviceid INTEGER,"
"createdate INTEGER,"
"accessdate INTEGER,"
"percentage FLOAT,"
"rating INTEGER DEFAULT 0,"
"playcounter INTEGER,"
"uniqueid " + exactTextColumnType(32) + " UNIQUE,"
"deleted BOOL DEFAULT " + boolF() + ","
"PRIMARY KEY(url, deviceid) );"
).arg( temp ? "TEMPORARY" : "" )
.arg( temp ? "_fix_ten" : "" ) );
if ( !temp )
{
query( "CREATE UNIQUE INDEX url_stats ON statistics( deviceid, url );" );
query( "CREATE INDEX percentage_stats ON statistics( percentage );" );
query( "CREATE INDEX rating_stats ON statistics( rating );" );
query( "CREATE INDEX playcounter_stats ON statistics( playcounter );" );
query( "CREATE INDEX uniqueid_stats ON statistics( uniqueid );" );
}
}
void
CollectionDB::dropStatsTable()
{
query( "DROP TABLE statistics;" );
}
void
CollectionDB::dropStatsTableV1()
{
query( "DROP TABLE statistics;" );
}
void
CollectionDB::createPersistentTables()
{
// create amazon table
query( "CREATE TABLE amazon ( "
"asin " + textColumnType(20) + ", "
"locale " + textColumnType(2) + ", "
"filename " + exactTextColumnType(33) + ", "
"refetchdate INTEGER );" );
// create lyrics table
query( TQString( "CREATE TABLE lyrics ("
"url " + exactTextColumnType() + ", "
"deviceid INTEGER,"
"lyrics " + longTextColumnType() + ", "
"uniqueid " + exactTextColumnType(32) + ");" ) );
query( TQString( "CREATE TABLE playlists ("
"playlist " + textColumnType() + ", "
"url " + exactTextColumnType() + ", "
"tracknum INTEGER );" ) );
TQString labelsAutoIncrement = "";
if ( getDbConnectionType() == DbConnection::postgresql )
{
query( TQString( "CREATE SEQUENCE labels_seq;" ) );
labelsAutoIncrement = TQString("DEFAULT nextval('labels_seq')");
}
else if ( getDbConnectionType() == DbConnection::mysql )
{
labelsAutoIncrement = "AUTO_INCREMENT";
}
//create labels tables
query( TQString( "CREATE TABLE labels ("
"id INTEGER PRIMARY KEY " + labelsAutoIncrement + ", "
"name " + textColumnType() + ", "
"type INTEGER);" ) );
query( TQString( "CREATE TABLE tags_labels ("
"deviceid INTEGER,"
"url " + exactTextColumnType() + ", "
"uniqueid " + exactTextColumnType(32) + ", " //m:n relationship, DO NOT MAKE UNIQUE!
"labelid INTEGER REFERENCES labels( id ) ON DELETE CASCADE );" ) );
}
void
CollectionDB::createPersistentTablesV12()
{
// create amazon table
query( "CREATE TABLE amazon ( "
"asin " + textColumnType(20) + ", "
"locale " + textColumnType(2) + ", "
"filename " + textColumnType(33) + ", "
"refetchdate INTEGER );" );
// create lyrics table
query( TQString( "CREATE TABLE lyrics ("
"url " + textColumnType() + ", "
"lyrics " + longTextColumnType() + ");" ) );
// create labels table
query( TQString( "CREATE TABLE label ("
"url " + textColumnType() + ","
"label " + textColumnType() + ");" ) );
query( TQString( "CREATE TABLE playlists ("
"playlist " + textColumnType() + ", "
"url " + textColumnType() + ", "
"tracknum INTEGER );" ) );
query( "CREATE INDEX url_label ON label( url );" );
query( "CREATE INDEX label_label ON label( label );" );
query( "CREATE INDEX playlist_playlists ON playlists( playlist );" );
query( "CREATE INDEX url_playlists ON playlists( url );" );
}
void
CollectionDB::createPersistentTablesV14( bool temp )
{
const TQString a( temp ? "TEMPORARY" : "" );
const TQString b( temp ? "_fix" : "" );
// create amazon table
query( TQString( "CREATE %1 TABLE amazon%2 ( "
"asin " + textColumnType(20) + ", "
"locale " + textColumnType(2) + ", "
"filename " + exactTextColumnType(33) + ", "
"refetchdate INTEGER );" ).arg( a,b ) );
// create lyrics table
query( TQString( "CREATE %1 TABLE lyrics%2 ("
"url " + exactTextColumnType() + ", "
"deviceid INTEGER,"
"lyrics " + longTextColumnType() + ");" ).arg( a,b ) );
query( TQString( "CREATE %1 TABLE playlists%2 ("
"playlist " + textColumnType() + ", "
"url " + exactTextColumnType() + ", "
"tracknum INTEGER );" ).arg( a,b ) );
if ( !temp )
{
query( "CREATE UNIQUE INDEX lyrics_url ON lyrics( url, deviceid );" );
query( "CREATE INDEX playlist_playlists ON playlists( playlist );" );
query( "CREATE INDEX url_playlists ON playlists( url );" );
}
}
void
CollectionDB::createPodcastTables()
{
TQString podcastAutoIncrement = "";
TQString podcastFolderAutoInc = "";
if ( getDbConnectionType() == DbConnection::postgresql )
{
query( TQString( "CREATE SEQUENCE podcastepisode_seq;" ) );
query( TQString( "CREATE SEQUENCE podcastfolder_seq;" ) );
podcastAutoIncrement = TQString("DEFAULT nextval('podcastepisode_seq')");
podcastFolderAutoInc = TQString("DEFAULT nextval('podcastfolder_seq')");
}
else if ( getDbConnectionType() == DbConnection::mysql )
{
podcastAutoIncrement = "AUTO_INCREMENT";
podcastFolderAutoInc = "AUTO_INCREMENT";
}
// create podcast channels table
query( TQString( "CREATE TABLE podcastchannels ("
"url " + exactTextColumnType() + " UNIQUE,"
"title " + textColumnType() + ","
"weblink " + exactTextColumnType() + ","
"image " + exactTextColumnType() + ","
"comment " + longTextColumnType() + ","
"copyright " + textColumnType() + ","
"parent INTEGER,"
"directory " + textColumnType() + ","
"autoscan BOOL, fetchtype INTEGER, "
"autotransfer BOOL, haspurge BOOL, purgecount INTEGER );" ) );
// create podcast episodes table
query( TQString( "CREATE TABLE podcastepisodes ("
"id INTEGER PRIMARY KEY %1, "
"url " + exactTextColumnType() + " UNIQUE,"
"localurl " + exactTextColumnType() + ","
"parent " + exactTextColumnType() + ","
"guid " + exactTextColumnType() + ","
"title " + textColumnType() + ","
"subtitle " + textColumnType() + ","
"composer " + textColumnType() + ","
"comment " + longTextColumnType() + ","
"filetype " + textColumnType() + ","
"createdate " + textColumnType() + ","
"length INTEGER,"
"size INTEGER,"
"isNew BOOL );" )
.arg( podcastAutoIncrement ) );
// create podcast folders table
query( TQString( "CREATE TABLE podcastfolders ("
"id INTEGER PRIMARY KEY %1, "
"name " + textColumnType() + ","
"parent INTEGER, isOpen BOOL );" )
.arg( podcastFolderAutoInc ) );
query( "CREATE INDEX url_podchannel ON podcastchannels( url );" );
query( "CREATE INDEX url_podepisode ON podcastepisodes( url );" );
query( "CREATE INDEX localurl_podepisode ON podcastepisodes( localurl );" );
query( "CREATE INDEX url_podfolder ON podcastfolders( id );" );
}
void
CollectionDB::createPodcastTablesV2( bool temp )
{
const TQString a( temp ? "TEMPORARY" : "" );
const TQString b( temp ? "_fix" : "" );
TQString podcastAutoIncrement = "";
TQString podcastFolderAutoInc = "";
if ( getDbConnectionType() == DbConnection::postgresql )
{
query( TQString( "CREATE SEQUENCE podcastepisode_seq;" ) );
query( TQString( "CREATE SEQUENCE podcastfolder_seq;" ) );
podcastAutoIncrement = TQString("DEFAULT nextval('podcastepisode_seq')");
podcastFolderAutoInc = TQString("DEFAULT nextval('podcastfolder_seq')");
}
else if ( getDbConnectionType() == DbConnection::mysql )
{
podcastAutoIncrement = "AUTO_INCREMENT";
podcastFolderAutoInc = "AUTO_INCREMENT";
}
// create podcast channels table
query( TQString( "CREATE %1 TABLE podcastchannels%2 ("
"url " + exactTextColumnType() + " UNIQUE,"
"title " + textColumnType() + ","
"weblink " + exactTextColumnType() + ","
"image " + exactTextColumnType() + ","
"comment " + longTextColumnType() + ","
"copyright " + textColumnType() + ","
"parent INTEGER,"
"directory " + textColumnType() + ","
"autoscan BOOL, fetchtype INTEGER, "
"autotransfer BOOL, haspurge BOOL, purgecount INTEGER );" ).arg( a,b ) );
// create podcast episodes table
query( TQString( "CREATE %2 TABLE podcastepisodes%3 ("
"id INTEGER PRIMARY KEY %1, "
"url " + exactTextColumnType() + " UNIQUE,"
"localurl " + exactTextColumnType() + ","
"parent " + exactTextColumnType() + ","
"guid " + exactTextColumnType() + ","
"title " + textColumnType() + ","
"subtitle " + textColumnType() + ","
"composer " + textColumnType() + ","
"comment " + longTextColumnType() + ","
"filetype " + textColumnType() + ","
"createdate " + textColumnType() + ","
"length INTEGER,"
"size INTEGER,"
"isNew BOOL );" )
.arg( podcastAutoIncrement, a, b ) );
// create podcast folders table
query( TQString( "CREATE %2 TABLE podcastfolders%3 ("
"id INTEGER PRIMARY KEY %1, "
"name " + textColumnType() + ","
"parent INTEGER, isOpen BOOL );" )
.arg( podcastFolderAutoInc, a, b ) );
if ( !temp )
{
query( "CREATE INDEX url_podchannel ON podcastchannels( url );" );
query( "CREATE INDEX url_podepisode ON podcastepisodes( url );" );
query( "CREATE INDEX localurl_podepisode ON podcastepisodes( localurl );" );
query( "CREATE INDEX url_podfolder ON podcastfolders( id );" );
}
}
void
CollectionDB::dropPersistentTables()
{
query( "DROP TABLE amazon;" );
query( "DROP TABLE lyrics;" );
query( "DROP TABLE playlists;" );
query( "DROP TABLE tags_labels;" );
query( "DROP TABLE labels;" );
}
void
CollectionDB::dropPersistentTablesV14()
{
query( "DROP TABLE amazon;" );
query( "DROP TABLE lyrics;" );
query( "DROP TABLE label;" );
query( "DROP TABLE playlists;" );
}
void
CollectionDB::dropPodcastTables()
{
query( "DROP TABLE podcastchannels;" );
query( "DROP TABLE podcastepisodes;" );
query( "DROP TABLE podcastfolders;" );
}
void
CollectionDB::dropPodcastTablesV2()
{
query( "DROP TABLE podcastchannels;" );
query( "DROP TABLE podcastepisodes;" );
query( "DROP TABLE podcastfolders;" );
}
void
CollectionDB::dropDevicesTable()
{
query( "DROP TABLE devices;" );
}
uint
CollectionDB::artistID( TQString value, bool autocreate, const bool temporary, bool exact /* = true */ )
{
// lookup cache
if ( m_validArtistCache && m_cacheArtist[(int)temporary] == value )
return m_cacheArtistID[(int)temporary];
uint id;
if ( exact )
id = IDFromExactValue( "artist", value, autocreate, temporary ).toUInt();
else
id = IDFromValue( "artist", value, autocreate, temporary );
// cache values
m_cacheArtist[(int)temporary] = value;
m_cacheArtistID[(int)temporary] = id;
m_validArtistCache = 1;
return id;
}
TQString
CollectionDB::artistValue( uint id )
{
// lookup cache
if ( m_cacheArtistID[0] == id )
return m_cacheArtist[0];
TQString value = valueFromID( "artist", id );
// cache values
m_cacheArtist[0] = value;
m_cacheArtistID[0] = id;
return value;
}
uint
CollectionDB::composerID( TQString value, bool autocreate, const bool temporary, bool exact /* = true */ )
{
// lookup cache
if ( m_validComposerCache && m_cacheComposer[(int)temporary] == value )
return m_cacheComposerID[(int)temporary];
uint id;
if ( exact )
id = IDFromExactValue( "composer", value, autocreate, temporary ).toUInt();
else
id = IDFromValue( "composer", value, autocreate, temporary );
// cache values
m_cacheComposer[(int)temporary] = value;
m_cacheComposerID[(int)temporary] = id;
m_validComposerCache = 1;
return id;
}
TQString
CollectionDB::composerValue( uint id )
{
// lookup cache
if ( m_cacheComposerID[0] == id )
return m_cacheComposer[0];
TQString value = valueFromID( "composer", id );
// cache values
m_cacheComposer[0] = value;
m_cacheComposerID[0] = id;
return value;
}
uint
CollectionDB::albumID( TQString value, bool autocreate, const bool temporary, bool exact /* = true */ )
{
// lookup cache
if ( m_validAlbumCache && m_cacheAlbum[(int)temporary] == value )
return m_cacheAlbumID[(int)temporary];
uint id;
if ( exact )
id = IDFromExactValue( "album", value, autocreate, temporary ).toUInt();
else
id = IDFromValue( "album", value, autocreate, temporary );
// cache values
m_cacheAlbum[(int)temporary] = value;
m_cacheAlbumID[(int)temporary] = id;
m_validAlbumCache = 1;
return id;
}
TQString
CollectionDB::albumValue( uint id )
{
// lookup cache
if ( m_cacheAlbumID[0] == id )
return m_cacheAlbum[0];
TQString value = valueFromID( "album", id );
// cache values
m_cacheAlbum[0] = value;
m_cacheAlbumID[0] = id;
return value;
}
uint
CollectionDB::genreID( TQString value, bool autocreate, const bool temporary, bool exact /* = true */ )
{
return exact ?
IDFromExactValue( "genre", value, autocreate, temporary ).toUInt() :
IDFromValue( "genre", value, autocreate, temporary );
}
TQString
CollectionDB::genreValue( uint id )
{
return valueFromID( "genre", id );
}
uint
CollectionDB::yearID( TQString value, bool autocreate, const bool temporary, bool exact /* = true */ )
{
return exact ?
IDFromExactValue( "year", value, autocreate, temporary ).toUInt() :
IDFromValue( "year", value, autocreate, temporary );
}
TQString
CollectionDB::yearValue( uint id )
{
return valueFromID( "year", id );
}
uint
CollectionDB::IDFromValue( TQString name, TQString value, bool autocreate, const bool temporary )
{
if ( temporary )
name.append( "_temp" );
// what the hell is the reason for this?
// else
// conn = NULL;
TQStringList values =
query( TQString(
"SELECT id, name FROM %1 WHERE name %2;" )
.arg( name )
.arg( CollectionDB::likeCondition( value ) ) );
//check if item exists. if not, should we autocreate it?
uint id;
if ( values.isEmpty() && autocreate )
{
id = insert( TQString( "INSERT INTO %1 ( name ) VALUES ( '%2' );" )
.arg( name )
.arg( CollectionDB::instance()->escapeString( value ) ), name );
return id;
}
return values.isEmpty() ? 0 : values.first().toUInt();
}
TQString
CollectionDB::valueFromID( TQString table, uint id )
{
TQStringList values =
query( TQString(
"SELECT name FROM %1 WHERE id=%2;" )
.arg( table )
.arg( id ) );
return values.isEmpty() ? 0 : values.first();
}
TQString
CollectionDB::albumSongCount( const TQString &artist_id, const TQString &album_id )
{
TQStringList values =
query( TQString(
"SELECT COUNT( url ) FROM tags WHERE album = %1 AND artist = %2;" )
.arg( album_id )
.arg( artist_id ) );
return values.first();
}
bool
CollectionDB::albumIsCompilation( const TQString &album_id )
{
TQStringList values =
query( TQString(
"SELECT sampler FROM tags WHERE sampler=%1 AND album=%2" )
.arg( CollectionDB::instance()->boolT() )
.arg( album_id ) );
return (values.count() != 0);
}
TQStringList
CollectionDB::albumTracks( const TQString &artist_id, const TQString &album_id )
{
QueryBuilder qb;
qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL );
qb.addMatch( QueryBuilder::tabAlbum, QueryBuilder::valID, album_id );
const bool isCompilation = albumIsCompilation( album_id );
if( !isCompilation )
qb.addMatch( QueryBuilder::tabArtist, QueryBuilder::valID, artist_id );
qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valDiscNumber );
qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
TQStringList ret = qb.run();
uint returnValues = qb.countReturnValues();
if ( returnValues > 1 )
{
TQStringList ret2;
for ( TQStringList::size_type i = 0; i < ret.size(); i += returnValues )
ret2 << ret[ i ];
return ret2;
}
else
return ret;
}
TQStringList
CollectionDB::albumDiscTracks( const TQString &artist_id, const TQString &album_id, const TQString &discNumber)
{
TQStringList rs;
rs = query( TQString( "SELECT tags.deviceid, tags.url FROM tags, year WHERE tags.album = %1 AND "
"tags.artist = %2 AND year.id = tags.year AND tags.discnumber = %3 "
+ deviceidSelection() + " ORDER BY tags.track;" )
.arg( album_id )
.arg( artist_id )
.arg( discNumber ) );
TQStringList result;
foreach( rs )
{
const int id = (*it).toInt();
result << MountPointManager::instance()->getAbsolutePath( id, *(++it) );
}
return result;
}
TQStringList
CollectionDB::artistTracks( const TQString &artist_id )
{
TQStringList rs = query( TQString( "SELECT tags.deviceid, tags.url FROM tags, album "
"WHERE tags.artist = '%1' AND album.id = tags.album " + deviceidSelection() +
"ORDER BY album.name, tags.discnumber, tags.track;" )
.arg( artist_id ) );
TQStringList result = TQStringList();
foreach( rs )
{
const int id = (*it).toInt();
result << MountPointManager::instance()->getAbsolutePath( id, *(++it) );
}
return result;
}
void
CollectionDB::addImageToAlbum( const TQString& image, TQValueList< TQPair<TQString, TQString> > info, const bool temporary )
{
int deviceid = MountPointManager::instance()->getIdForUrl( image );
TQString rpath = MountPointManager::instance()->getRelativePath( deviceid, image );
for ( TQValueList< TQPair<TQString, TQString> >::ConstIterator it = info.begin(); it != info.end(); ++it )
{
if ( (*it).first.isEmpty() || (*it).second.isEmpty() )
continue;
TQString sql = TQString( "INSERT INTO images%1 ( path, deviceid, artist, album ) VALUES ( '%3', %2" )
.arg( temporary ? "_temp" : "" )
.arg( deviceid )
.arg( escapeString( rpath ) );
sql += TQString( ", '%1'" ).arg( escapeString( (*it).first ) );
sql += TQString( ", '%1' );" ).arg( escapeString( (*it).second ) );
// debug() << "Added image for album: " << (*it).first << " - " << (*it).second << ": " << image << endl;
insert( sql, NULL );
}
}
void
CollectionDB::addEmbeddedImage( const TQString& path, const TQString& hash, const TQString& description )
{
// debug() << "Added embedded image hash " << hash << " for file " << path << endl;
//TODO: figure out what this embedded table does and then add the necessary code
//what are embedded images anyway?
int deviceid = MountPointManager::instance()->getIdForUrl( path );
TQString rpath = MountPointManager::instance()->getRelativePath(deviceid, path );
insert( TQString( "INSERT INTO embed_temp ( url, deviceid, hash, description ) VALUES ( '%2', %1, '%3', '%4' );" )
.arg( deviceid )
.arg( escapeString( rpath ), escapeString( hash ), escapeString( description ) ), NULL );
}
void
CollectionDB::removeOrphanedEmbeddedImages()
{
//TODO refactor
// do it the hard way, since a delete subquery wont work on MySQL
TQStringList orphaned = query( "SELECT embed.deviceid, embed.url FROM embed LEFT JOIN tags ON embed.url = tags.url AND embed.deviceid = tags.deviceid WHERE tags.url IS NULL;" );
foreach( orphaned ) {
TQString deviceid = *it;
TQString rpath = *(++it);
query( TQString( "DELETE FROM embed WHERE embed.deviceid = %1 AND embed.url = '%2';" )
.arg( deviceid, escapeString( rpath ) ) );
}
}
TQPixmap
CollectionDB::createDragPixmapFromSQL( const TQString &sql, TQString textOverRide )
{
// it is too slow to check if the url is actually in the colleciton.
//TODO mountpointmanager: figure out what has to be done here
TQStringList values = instance()->query( sql );
KURL::List list;
foreach( values )
{
KURL u = KURL::fromPathOrURL( *it );
if( u.isValid() )
list += u;
}
return createDragPixmap( list, textOverRide );
}
TQPixmap
CollectionDB::createDragPixmap( const KURL::List &urls, TQString textOverRide )
{
// settings
const int maxCovers = 4; // maximum number of cover images to show
const int coverSpacing = 20; // spacing between stacked covers
const int fontSpacing = 5; // spacing between covers and info text
const int coverW = AmarokConfig::coverPreviewSize() > 100 ? 100 : AmarokConfig::coverPreviewSize();
const int coverH = coverW;
const int margin = 2; //px margin
int covers = 0;
int songs = 0;
int pixmapW = 0;
int pixmapH = 0;
int remoteUrls = 0;
int playlists = 0;
TQMap<TQString, int> albumMap;
TQPixmap coverPm[maxCovers];
TQString song, album;
// iterate urls, get covers and count artist/albums
bool correctAlbumCount = true;
KURL::List::ConstIterator it = urls.begin();
for ( ; it != urls.end(); ++it )
{
if( PlaylistFile::isPlaylistFile( *it )
|| (*it).protocol() == "playlist" || (*it).protocol() == "smartplaylist"
|| (*it).protocol() == "dynamic" )
{
playlists++;
}
else if( (*it).isLocalFile() )
{
songs++;
if( covers >= maxCovers )
{
correctAlbumCount = false;
continue;
}
MetaBundle mb( *it );
song = mb.title();
album = mb.album();
TQString artist = mb.artist();
if( mb.compilation() == MetaBundle::CompilationYes )
artist = TQString( "Various_AMAROK_Artists" ); // magic key for the albumMap!
if( !albumMap.contains( artist + album ) )
{
albumMap[ artist + album ] = 1;
TQString coverName = CollectionDB::instance()->albumImage( mb.artist(), album, false, coverW );
if ( !coverName.endsWith( "@nocover.png" ) )
coverPm[covers++].load( coverName );
}
}
else
{
MetaBundle mb( *it );
if( !albumMap.contains( mb.artist() + mb.album() ) )
{
albumMap[ mb.artist() + mb.album() ] = 1;
TQString coverName = CollectionDB::instance()->podcastImage( mb, false, coverW );
if ( covers < maxCovers && !coverName.endsWith( "@nocover.png" ) )
coverPm[covers++].load( coverName );
}
remoteUrls++;
}
}
// make better text...
int albums = albumMap.count();
TQString text;
if( !textOverRide.isEmpty() )
{
text = textOverRide;
}
else if( ( songs && remoteUrls ) ||
( songs && playlists ) ||
( playlists && remoteUrls ) )
{
text = i18n( "One item", "%n items", songs + remoteUrls + playlists );
}
else if( songs > 0 )
{
if( correctAlbumCount ) {
text = i18n( "X songs from X albums", "%2 from %1" );
text = text.arg( albums == 1 && !album.isEmpty() ? album : i18n( "one album", "%n albums",albums ) );
}
else
text = "%1";
text = text.arg( songs == 1 && !song.isEmpty() ? song : i18n( "One song", "%n songs", songs ) );
}
else if( playlists > 0 )
text = i18n( "One playlist", "%n playlists", playlists );
else if ( remoteUrls > 0 )
text = i18n( "One remote file", "%n remote files", remoteUrls );
else
text = i18n( "Unknown item" );
TQFont font;
TQFontMetrics fm( font );
int fontH = fm.height() + margin;
int minWidth = fm.width( text ) + margin*2; //margin either side
if ( covers > 0 )
{
// insert "..." cover as first image if appropriate
if ( covers < albums )
{
if ( covers < maxCovers ) covers++;
for ( int i = maxCovers-1; i > 0; i-- )
coverPm[i] = coverPm[i-1];
TQImage im( locate( "data","amarok/images/more_albums.png" ) );
coverPm[0].convertFromImage( im.smoothScale( coverW, coverH, TQ_ScaleMin ) );
}
pixmapH = coverPm[0].height();
pixmapW = coverPm[0].width();
// caluclate pixmap height
int dW, dH;
for ( int i = 1; i < covers; i++ )
{
dW = coverPm[i].width() - coverPm[i-1].width() + coverSpacing;
dH = coverPm[i].height() - coverPm[i-1].height() + coverSpacing;
if ( dW > 0 ) pixmapW += dW;
if ( dH > 0 ) pixmapH += dH;
}
pixmapH += fontSpacing + fontH;
if ( pixmapW < minWidth )
pixmapW = minWidth;
}
else
{
pixmapW = minWidth;
pixmapH = fontH;
}
TQPixmap pmdrag( pixmapW, pixmapH );
TQPixmap pmtext( pixmapW, fontH );
TQPalette palette = TQToolTip::palette();
TQPainter p;
p.begin( &pmtext );
p.fillRect( 0, 0, pixmapW, fontH, TQBrush( TQt::black ) ); // border
p.fillRect( 1, 1, pixmapW-margin, fontH-margin, palette.brush( TQPalette::Normal, TQColorGroup::Background ) );
p.setBrush( palette.color( TQPalette::Normal, TQColorGroup::Text ) );
p.setFont( font );
p.drawText( margin, fm.ascent() + 1, text );
p.end();
TQBitmap pmtextMask(pixmapW, fontH);
pmtextMask.fill( TQt::color1 );
// when we have found no covers, just display the text message
if( !covers )
{
pmtext.setMask(pmtextMask);
return pmtext;
}
// compose image
p.begin( &pmdrag );
p.setBackgroundMode( Qt::TransparentMode );
for ( int i = 0; i < covers; i++ )
bitBlt( &pmdrag, i * coverSpacing, i * coverSpacing, &coverPm[i], 0, TQt::CopyROP );
bitBlt( &pmdrag, 0, pixmapH - fontH, &pmtext, 0, TQt::CopyROP );
p.end();
TQBitmap pmdragMask( pmdrag.size(), true );
for ( int i = 0; i < covers; i++ )
{
TQBitmap coverMask( coverPm[i].width(), coverPm[i].height() );
coverMask.fill( TQt::color1 );
bitBlt( &pmdragMask, i * coverSpacing, i * coverSpacing, &coverMask, 0, TQt::CopyROP );
}
bitBlt( &pmdragMask, 0, pixmapH - fontH, &pmtextMask, 0, TQt::CopyROP );
pmdrag.setMask( pmdragMask );
return pmdrag;
}
TQImage
CollectionDB::fetchImage( const KURL& url, TQString &/*tmpFile*/ )
{
if ( url.protocol() != "file" )
{
TQString tmpFile;
KIO::NetAccess::download( url, tmpFile, 0 ); //TODO set 0 to the window, though it probably doesn't really matter
return TQImage( tmpFile );
}
else
{
return TQImage( url.path() );
}
}
bool
CollectionDB::setAlbumImage( const TQString& artist, const TQString& album, const KURL& url )
{
TQString 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 TQString& artist, const TQString& album, TQImage img, const TQString& amazonUrl, const TQString& asin )
{
//show a wait cursor for the duration
Amarok::OverrideCursor keep;
const bool isCompilation = albumIsCompilation( TQString::number( albumID( album, false, false, true ) ) );
const TQString artist_ = isCompilation ? "" : artist;
// remove existing album covers
removeAlbumImage( artist_, album );
TQCString key = md5sum( artist_, album );
newAmazonReloadDate(asin, AmarokConfig::amazonLocale(), key);
// Save Amazon product page URL as embedded string, for later retreival
if ( !amazonUrl.isEmpty() )
img.setText( "amazon-url", 0, amazonUrl );
const bool b = img.save( largeCoverDir().filePath( key ), "PNG");
emit coverChanged( artist_, album );
return b;
}
TQString
CollectionDB::podcastImage( const MetaBundle &bundle, const bool withShadow, uint width )
{
PodcastEpisodeBundle peb;
PodcastChannelBundle pcb;
KURL url = bundle.url().url();
if( getPodcastEpisodeBundle( url, &peb ) )
{
url = peb.parent().url();
}
if( getPodcastChannelBundle( url, &pcb ) )
{
if( pcb.imageURL().isValid() )
return podcastImage( pcb.imageURL().url(), withShadow, width );
}
return notAvailCover( withShadow, width );
}
TQString
CollectionDB::podcastImage( const TQString &remoteURL, const bool withShadow, uint width )
{
// we aren't going to need a 1x1 size image. this is just a quick hack to be able to show full size images.
// width of 0 == full size
if( width == 1 )
width = AmarokConfig::coverPreviewSize();
TQString s = findAmazonImage( "Podcast", remoteURL, width );
if( s.isEmpty() )
{
s = notAvailCover( withShadow, width );
const KURL url = KURL::fromPathOrURL( remoteURL );
if( url.isValid() ) //KIO crashes with invalid URLs
{
KIO::Job *job = KIO::storedGet( url, false, false );
m_podcastImageJobs[job] = remoteURL;
connect( job, TQT_SIGNAL( result( KIO::Job* ) ), TQT_SLOT( podcastImageResult( KIO::Job* ) ) );
}
}
if ( withShadow )
s = makeShadowedImage( s );
return s;
}
void
CollectionDB::podcastImageResult( KIO::Job *gjob )
{
TQString url = m_podcastImageJobs[gjob];
m_podcastImageJobs.remove( gjob );
KIO::StoredTransferJob *job = dynamic_cast<KIO::StoredTransferJob *>( gjob );
if( !job )
{
debug() << "connected to wrong job type" << endl;
return;
}
if( job->error() )
{
debug() << "job finished with error" << endl;
return;
}
if( job->isErrorPage() )
{
debug() << "error page" << endl;
return;
}
TQImage image( job->data() );
if( !image.isNull() )
{
if( url.isEmpty() )
url = job->url().url();
TQCString key = md5sum( "Podcast", url );
if( image.save( largeCoverDir().filePath( key ), "PNG") )
emit imageFetched( url );
}
}
TQString
CollectionDB::albumImage( const TQString &artist, const TQString &album, bool withShadow, uint width, bool* embedded )
{
TQString 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.
// width of 0 == full size
if( width == 1 )
width = AmarokConfig::coverPreviewSize();
if( embedded )
*embedded = false;
s = findAmazonImage( artist, album, width );
if( s.isEmpty() )
s = findAmazonImage( "", album, width ); // handle compilations
if( s.isEmpty() )
s = findDirectoryImage( artist, album, width );
if( s.isEmpty() )
{
s = findEmbeddedImage( artist, album, width );
if( embedded && !s.isEmpty() )
*embedded = true;
}
if( s.isEmpty() )
s = notAvailCover( withShadow, width );
if ( withShadow )
s = makeShadowedImage( s );
return s;
}
TQString
CollectionDB::albumImage( const uint artist_id, const uint album_id, bool withShadow, uint width, bool* embedded )
{
return albumImage( artistValue( artist_id ), albumValue( album_id ), withShadow, width, embedded );
}
TQString
CollectionDB::albumImage( const MetaBundle &trackInformation, bool withShadow, uint width, bool* embedded )
{
TQString s;
if( width == 1 )
width = AmarokConfig::coverPreviewSize();
TQString album = trackInformation.album();
TQString artist = trackInformation.artist();
// this art is per track, so should check for it first
s = findMetaBundleImage( trackInformation, width );
if( embedded )
*embedded = !s.isEmpty();
if( s.isEmpty() )
s = findAmazonImage( artist, album, width );
if( s.isEmpty() )
s = findAmazonImage( "", album, width ); // handle compilations
if( s.isEmpty() )
s = findDirectoryImage( artist, album, width );
if( s.isEmpty() )
s = notAvailCover( withShadow, width );
if ( withShadow )
s = makeShadowedImage( s );
return s;
}
TQString
CollectionDB::makeShadowedImage( const TQString& albumImage, bool cache )
{
tqApp->lock();
const TQImage original( albumImage, "PNG" );
tqApp->unlock();
if( original.hasAlphaBuffer() )
return albumImage;
const TQFileInfo fileInfo( albumImage );
const uint shadowSize = static_cast<uint>( original.width() / 100.0 * 6.0 );
const TQString cacheFile = fileInfo.fileName() + "@shadow";
if ( !cache && cacheCoverDir().exists( cacheFile ) )
return cacheCoverDir().filePath( cacheFile );
TQImage shadow;
const TQString folder = Amarok::saveLocation( "covershadow-cache/" );
const TQString file = TQString( "shadow_albumcover%1x%2.png" ).arg( original.width() + shadowSize ).arg( original.height() + shadowSize );
if ( TQFile::exists( folder + file ) ) {
tqApp->lock();
shadow.load( folder + file, "PNG" );
tqApp->unlock();
}
else {
shadow = TQDeepCopy<TQImage>(instance()->m_shadowImage);
shadow = shadow.smoothScale( original.width() + shadowSize, original.height() + shadowSize );
shadow.save( folder + file, "PNG" );
}
TQImage target(shadow);
bitBlt( &target, 0, 0, &original );
if ( cache ) {
target.save( cacheCoverDir().filePath( cacheFile ), "PNG" );
return cacheCoverDir().filePath( cacheFile );
}
target.save( albumImage, "PNG" );
return albumImage;
}
// Amazon Image
TQString
CollectionDB::findAmazonImage( const TQString &artist, const TQString &album, uint width )
{
TQCString widthKey = makeWidthKey( width );
if ( artist.isEmpty() && album.isEmpty() )
return TQString();
TQCString key = md5sum( artist, album );
// check cache for existing cover
if ( cacheCoverDir().exists( widthKey + key ) )
return cacheCoverDir().filePath( widthKey + key );
// we need to create a scaled version of this cover
TQDir imageDir = largeCoverDir();
if ( imageDir.exists( key ) )
{
if ( width > 1 )
{
TQImage img( imageDir.filePath( key ) );
img.smoothScale( width, width, TQ_ScaleMin ).save( cacheCoverDir().filePath( widthKey + key ), "PNG" );
return cacheCoverDir().filePath( widthKey + key );
}
else
return imageDir.filePath( key );
}
return TQString();
}
TQString
CollectionDB::findDirectoryImage( const TQString& artist, const TQString& album, uint width )
{
if ( width == 1 )
width = AmarokConfig::coverPreviewSize();
TQCString widthKey = makeWidthKey( width );
if ( album.isEmpty() )
return TQString();
IdList list = MountPointManager::instance()->getMountedDeviceIds();
TQString deviceIds;
foreachType( IdList, list )
{
if ( !deviceIds.isEmpty() ) deviceIds += ',';
deviceIds += TQString::number(*it);
}
TQStringList rs;
if ( artist == i18n( "Various Artists" ) || artist.isEmpty() )
{
rs = query( TQString(
"SELECT distinct images.deviceid,images.path FROM images, artist, tags "
"WHERE images.artist = artist.name "
"AND artist.id = tags.artist "
"AND tags.sampler = %1 "
"AND images.album %2 "
"AND images.deviceid IN (%3) " )
.arg( boolT() )
.arg( CollectionDB::likeCondition( album ) )
.arg( deviceIds ) );
}
else
{
rs = query( TQString(
"SELECT distinct images.deviceid,images.path FROM images WHERE artist %1 AND album %2 AND deviceid IN (%3) ORDER BY path;" )
.arg( CollectionDB::likeCondition( artist ) )
.arg( CollectionDB::likeCondition( album ) )
.arg( deviceIds ) );
}
TQStringList values = URLsFromQuery( rs );
if ( !values.isEmpty() )
{
TQString image( values.first() );
uint matches = 0;
uint maxmatches = 0;
TQRegExp iTunesArt( "^AlbumArt_.*Large" );
for ( uint i = 0; i < values.count(); i++ )
{
matches = values[i].contains( "front", false ) + values[i].contains( "cover", false ) + values[i].contains( "folder", false ) + values[i].contains( iTunesArt );
if ( matches > maxmatches )
{
image = values[i];
maxmatches = matches;
}
}
TQCString key = md5sum( artist, album, image );
if ( width > 1 )
{
TQString path = cacheCoverDir().filePath( widthKey + key );
if ( !TQFile::exists( path ) )
{
TQImage img( image );
img.smoothScale( width, width, TQ_ScaleMin ).save( path, "PNG" );
}
return path;
}
else //large image
return image;
}
return TQString();
}
TQString
CollectionDB::findEmbeddedImage( const TQString& artist, const TQString& album, uint width )
{
// In the case of multiple embedded images, we arbitrarily choose one from the newest file
// could potentially select multiple images within a file based on description, although a
// lot of tagging software doesn't fill in that field, so we just get whatever the DB
// happens to return for us
TQStringList rs;
if ( artist == i18n("Various Artists") || artist.isEmpty() ) {
// VAs need special handling to not match on artist name but instead check for sampler flag
rs = query( TQString(
"SELECT embed.hash, embed.deviceid, embed.url FROM "
"tags INNER JOIN embed ON tags.url = embed.url "
"INNER JOIN album ON tags.album = album.id "
"WHERE "
"album.name = '%1' "
"AND tags.sampler = %2 "
"ORDER BY modifydate DESC LIMIT 1;" )
.arg( escapeString( album ) )
.arg( boolT() ) );
} else {
rs = query( TQString(
"SELECT embed.hash, embed.deviceid, embed.url FROM "
"tags INNER JOIN embed ON tags.url = embed.url "
"INNER JOIN artist ON tags.artist = artist.id "
"INNER JOIN album ON tags.album = album.id "
"WHERE "
"artist.name = '%1' "
"AND album.name = '%2' "
"ORDER BY modifydate DESC LIMIT 1;" )
.arg( escapeString( artist ) )
.arg( escapeString( album ) ) );
}
TQStringList values = TQStringList();
if ( rs.count() == 3 ) {
values += rs.first();
values += MountPointManager::instance()->getAbsolutePath( rs[1].toInt(), rs[2] );
}
if ( values.count() == 2 ) {
TQCString hash = values.first().utf8();
TQString result = loadHashFile( hash, width );
if ( result.isEmpty() ) {
// need to get original from file first
MetaBundle mb( KURL::fromPathOrURL( values.last() ) );
if ( extractEmbeddedImage( mb, hash ) ) {
// try again, as should be possible now
result = loadHashFile( hash, width );
}
}
return result;
}
return TQString();
}
TQString
CollectionDB::findMetaBundleImage( const MetaBundle& trackInformation, uint width )
{
int deviceid = MountPointManager::instance()->getIdForUrl( trackInformation.url() );
TQString rpath = MountPointManager::instance()->getRelativePath( deviceid, trackInformation.url().path() );
TQStringList values =
query( TQString(
"SELECT embed.hash FROM tags LEFT JOIN embed ON tags.url = embed.url "
" AND tags.deviceid = embed.deviceid WHERE tags.url = '%2' AND tags.deviceid = %1 ORDER BY hash DESC LIMIT 1;" )
.arg( deviceid ).arg( escapeString( rpath ) ) );
if ( values.empty() || !values.first().isEmpty() ) {
TQCString hash;
TQString result;
if( !values.empty() ) { // file in collection, so we know the hash
hash = values.first().utf8();
result = loadHashFile( hash, width );
}
if ( result.isEmpty() ) {
// need to get original from file first
if ( extractEmbeddedImage( trackInformation, hash ) ) {
// try again, as should be possible now
result = loadHashFile( hash, width );
}
}
return result;
}
return TQString();
}
TQCString
CollectionDB::makeWidthKey( uint width )
{
return TQString::number( width ).local8Bit() + '@';
}
bool
CollectionDB::removeAlbumImage( const TQString &artist, const TQString &album )
{
DEBUG_BLOCK
TQCString widthKey = "*@";
TQCString key = md5sum( artist, album );
query( "DELETE FROM amazon WHERE filename='" + key + '\'' );
// remove scaled versions of images (and add the asterisk for the shadow-caches)
TQStringList scaledList = cacheCoverDir().entryList( TQString(widthKey + key + '*') );
if ( scaledList.count() > 0 )
for ( uint i = 0; i < scaledList.count(); i++ )
TQFile::remove( cacheCoverDir().filePath( scaledList[ i ] ) );
bool deleted = false;
// remove large, original image
if ( largeCoverDir().exists( key ) && TQFile::remove( largeCoverDir().filePath( key ) ) )
deleted = true;
TQString hardImage = findDirectoryImage( artist, album );
debug() << "hardImage: " << hardImage << endl;
if( !hardImage.isEmpty() )
{
int id = MountPointManager::instance()->getIdForUrl( hardImage );
TQString rpath = MountPointManager::instance()->getRelativePath( id, hardImage );
query( "DELETE FROM images WHERE path='" + escapeString( hardImage ) + "' AND deviceid = " + TQString::number( id ) + ';' );
deleted = true;
}
if ( deleted )
{
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 ) );
}
TQString
CollectionDB::notAvailCover( const bool withShadow, int width )
{
if ( width <= 1 )
width = AmarokConfig::coverPreviewSize();
TQString widthKey = TQString::number( width ) + '@';
TQString s;
if( cacheCoverDir().exists( widthKey + "nocover.png" ) )
s = cacheCoverDir().filePath( widthKey + "nocover.png" );
else
{
m_noCover.smoothScale( width, width, TQ_ScaleMin ).save( cacheCoverDir().filePath( widthKey + "nocover.png" ), "PNG" );
s = cacheCoverDir().filePath( widthKey + "nocover.png" );
}
if ( withShadow )
s = makeShadowedImage( s );
return s;
}
TQStringList
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.groupBy( QueryBuilder::tabArtist, QueryBuilder::valName );
qb.setOptions( QueryBuilder::optShowAll );
qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName );
return qb.run();
}
TQStringList
CollectionDB::composerList( bool withUnknowns, bool withCompilations )
{
DEBUG_BLOCK
QueryBuilder qb;
qb.addReturnValue( QueryBuilder::tabComposer, QueryBuilder::valName );
if ( !withUnknowns )
qb.excludeMatch( QueryBuilder::tabComposer, i18n( "Unknown" ) );
if ( !withCompilations )
qb.setOptions( QueryBuilder::optNoCompilations );
qb.groupBy( QueryBuilder::tabComposer, QueryBuilder::valName );
qb.setOptions( QueryBuilder::optShowAll );
qb.sortBy( QueryBuilder::tabComposer, QueryBuilder::valName );
return qb.run();
}
TQStringList
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.groupBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
qb.setOptions( QueryBuilder::optShowAll );
qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
return qb.run();
}
TQStringList
CollectionDB::genreList( bool withUnknowns, bool withCompilations )
{
QueryBuilder qb;
qb.addReturnValue( QueryBuilder::tabGenre, QueryBuilder::valName );
//Only report genres that currently have at least one song
qb.addFilter( QueryBuilder::tabSong, "" );
if ( !withUnknowns )
qb.excludeMatch( QueryBuilder::tabGenre, i18n( "Unknown" ) );
if ( !withCompilations )
qb.setOptions( QueryBuilder::optNoCompilations );
qb.groupBy( QueryBuilder::tabGenre, QueryBuilder::valName );
qb.setOptions( QueryBuilder::optShowAll );
qb.sortBy( QueryBuilder::tabGenre, QueryBuilder::valName );
return qb.run();
}
TQStringList
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.groupBy( QueryBuilder::tabYear, QueryBuilder::valName );
qb.setOptions( QueryBuilder::optShowAll );
qb.sortBy( QueryBuilder::tabYear, QueryBuilder::valName );
return qb.run();
}
TQStringList
CollectionDB::labelList()
{
QueryBuilder qb;
qb.addReturnValue( QueryBuilder::tabLabels, QueryBuilder::valName );
qb.groupBy( QueryBuilder::tabLabels, QueryBuilder::valName );
qb.setOptions( QueryBuilder::optShowAll );
qb.sortBy( QueryBuilder::tabLabels, QueryBuilder::valName );
return qb.run();
}
TQStringList
CollectionDB::albumListOfArtist( const TQString &artist, bool withUnknown, bool withCompilations )
{
if (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 lower(artist.name) = lower('" + escapeString( artist ) + "') " +
( withUnknown ? TQString() : "AND album.name <> '' " ) +
( withCompilations ? TQString() : "AND tags.sampler = " + boolF() ) + deviceidSelection() +
" ORDER BY lower( album.name );" );
}
// mysql is case insensitive and lower() is very slow
else if (getDbConnectionType() == DbConnection::mysql)
{
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 ? TQString() : "AND album.name <> '' " ) +
( withCompilations ? TQString() : "AND tags.sampler = " + boolF() ) + deviceidSelection() +
" ORDER BY album.name;" );
}
else // sqlite
{
return query( "SELECT DISTINCT album.name FROM tags, album, artist WHERE "
"tags.album = album.id AND tags.artist = artist.id "
"AND lower(artist.name) = lower('" + escapeString( artist ) + "') " +
( withUnknown ? TQString() : "AND album.name <> '' " ) +
( withCompilations ? TQString() : "AND tags.sampler = " + boolF() ) + deviceidSelection() +
" ORDER BY lower( album.name );" );
}
}
TQStringList
CollectionDB::artistAlbumList( bool withUnknown, bool withCompilations )
{
if (getDbConnectionType() == DbConnection::postgresql)