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.
8075 lines
289 KiB
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)
|