diff --git a/build/depends.py b/build/depends.py index 717e8679184..7150b7b8246 100644 --- a/build/depends.py +++ b/build/depends.py @@ -809,6 +809,7 @@ def sources(self, build): "library/bpmdelegate.cpp", "library/bpmeditor.cpp", "library/previewbuttondelegate.cpp", + "library/coverartdelegate.cpp", "audiotagger.cpp", "library/treeitemmodel.cpp", diff --git a/src/library/basesqltablemodel.cpp b/src/library/basesqltablemodel.cpp index 83df44dc86f..67bee1100b9 100644 --- a/src/library/basesqltablemodel.cpp +++ b/src/library/basesqltablemodel.cpp @@ -7,6 +7,7 @@ #include "library/basesqltablemodel.h" +#include "library/coverartdelegate.h" #include "library/stardelegate.h" #include "library/starrating.h" #include "library/bpmdelegate.h" @@ -91,6 +92,8 @@ void BaseSqlTableModel::initHeaderData() { Qt::Horizontal, tr("BPM Lock")); setHeaderData(fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW), Qt::Horizontal, tr("Preview")); + setHeaderData(fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART), + Qt::Horizontal, tr("Cover Art")); } QSqlDatabase BaseSqlTableModel::database() const { @@ -637,7 +640,8 @@ Qt::ItemFlags BaseSqlTableModel::readWriteFlags( column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_LOCATION) || column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION) || column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED)) { + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART)) { return defaultFlags; } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED)) { return defaultFlags | Qt::ItemIsUserCheckable; @@ -870,6 +874,8 @@ QAbstractItemDelegate* BaseSqlTableModel::delegateForColumn(const int i, QObject return new BPMDelegate(pParent, i, fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)); } else if (PlayerManager::numPreviewDecks() > 0 && i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW)) { return new PreviewButtonDelegate(pParent, i); + } else if (i == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART)) { + return new CoverArtDelegate(pParent); } return NULL; } diff --git a/src/library/basetrackcache.cpp b/src/library/basetrackcache.cpp index a4bd2537e29..bbab972ad4d 100644 --- a/src/library/basetrackcache.cpp +++ b/src/library/basetrackcache.cpp @@ -118,6 +118,13 @@ void BaseTrackCache::slotUpdateTrack(int trackId) { updateTrackInIndex(trackId); } +void BaseTrackCache::slotUpdateTracks(QSet trackIds) { + if (sDebug) { + qDebug() << this << "slotUpdateTracks" << trackIds.size(); + } + updateTracksInIndex(trackIds); +} + bool BaseTrackCache::isCached(int trackId) const { return m_trackInfo.contains(trackId); } diff --git a/src/library/basetrackcache.h b/src/library/basetrackcache.h index 0a0c1fcacf2..6917105a76d 100644 --- a/src/library/basetrackcache.h +++ b/src/library/basetrackcache.h @@ -69,6 +69,7 @@ class BaseTrackCache : public QObject { void slotTrackChanged(int trackId); void slotDbTrackAdded(TrackPointer pTrack); void slotUpdateTrack(int trackId); + void slotUpdateTracks(QSet trackId); private: TrackPointer lookupCachedTrack(int trackId) const; diff --git a/src/library/columncache.cpp b/src/library/columncache.cpp index 5d7a5815e6d..933dbe348c0 100644 --- a/src/library/columncache.cpp +++ b/src/library/columncache.cpp @@ -46,6 +46,7 @@ void ColumnCache::setColumns(const QStringList& columns) { m_columnIndexByEnum[COLUMN_LIBRARYTABLE_KEY_ID] = fieldIndex(LIBRARYTABLE_KEY_ID); m_columnIndexByEnum[COLUMN_LIBRARYTABLE_BPM_LOCK] = fieldIndex(LIBRARYTABLE_BPM_LOCK); m_columnIndexByEnum[COLUMN_LIBRARYTABLE_PREVIEW] = fieldIndex(LIBRARYTABLE_PREVIEW); + m_columnIndexByEnum[COLUMN_LIBRARYTABLE_COVERART] = fieldIndex(LIBRARYTABLE_COVERART); m_columnIndexByEnum[COLUMN_LIBRARYTABLE_COVERART_LOCATION] = fieldIndex(LIBRARYTABLE_COVERART_LOCATION); m_columnIndexByEnum[COLUMN_LIBRARYTABLE_COVERART_MD5] = fieldIndex(LIBRARYTABLE_COVERART_MD5); diff --git a/src/library/columncache.h b/src/library/columncache.h index cb4d8e801e1..ccffa10baa6 100644 --- a/src/library/columncache.h +++ b/src/library/columncache.h @@ -41,6 +41,7 @@ class ColumnCache { COLUMN_LIBRARYTABLE_KEY_ID, COLUMN_LIBRARYTABLE_BPM_LOCK, COLUMN_LIBRARYTABLE_PREVIEW, + COLUMN_LIBRARYTABLE_COVERART, COLUMN_LIBRARYTABLE_COVERART_LOCATION, COLUMN_LIBRARYTABLE_COVERART_MD5, diff --git a/src/library/coverartcache.cpp b/src/library/coverartcache.cpp index 8cf086e18c2..e046441202a 100644 --- a/src/library/coverartcache.cpp +++ b/src/library/coverartcache.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "coverartcache.h" #include "soundsourceproxy.h" @@ -10,8 +11,11 @@ CoverArtCache::CoverArtCache() : m_pCoverArtDAO(NULL), m_pTrackDAO(NULL), - m_defaultCover(":/images/library/default_cover.png"), - m_sDefaultCoverLocation(":/images/library/default_cover.png") { + m_sDefaultCoverLocation(":/images/library/default_cover.png"), + m_defaultCover(m_sDefaultCoverLocation), + m_timer(new QTimer(this)) { + m_timer->setSingleShot(true); + connect(m_timer, SIGNAL(timeout()), SLOT(updateDB())); } CoverArtCache::~CoverArtCache() { @@ -54,23 +58,45 @@ bool CoverArtCache::changeCoverArt(int trackId, return true; } -void CoverArtCache::requestPixmap(int trackId, - const QString& coverLocation, - const QString& md5Hash) { +QPixmap CoverArtCache::requestPixmap(int trackId, + const QString& coverLocation, + const QString& md5Hash, + const bool tryLoadAndSearch, + const bool croppedPixmap, + const bool emitSignals) { if (trackId < 1) { - return; + return QPixmap(); } // keep a list of trackIds for which a future is currently running // to avoid loading the same picture again while we are loading it if (m_runningIds.contains(trackId)) { - return; + return QPixmap(); } + // check if we have already found a cover for this track + // and if it is just waiting to be inserted/updated in the DB. + if (m_queueOfUpdates.contains(trackId)) { + return QPixmap(); + } + + // If this request comes from CoverDelegate (table view), + // it'll want to get a cropped cover which is ready to be drawn + // in the table view (cover art column). + // It's very important to keep the cropped covers in cache because it avoids + // having to rescale+crop it ALWAYS (which brings a lot of performance issues). + QString cacheKey = croppedPixmap ? md5Hash % "_cropped" : md5Hash; + QPixmap pixmap; - if (QPixmapCache::find(md5Hash, &pixmap)) { - emit(pixmapFound(trackId, pixmap)); - return; + if (QPixmapCache::find(cacheKey, &pixmap)) { + if (emitSignals) { + emit(pixmapFound(trackId, pixmap)); + } + return pixmap; + } + + if (!tryLoadAndSearch) { + return QPixmap(); } QFuture future; @@ -78,27 +104,42 @@ void CoverArtCache::requestPixmap(int trackId, if (coverLocation.isEmpty() || !QFile::exists(coverLocation)) { CoverArtDAO::CoverArtInfo coverInfo; coverInfo = m_pCoverArtDAO->getCoverArtInfo(trackId); - future = QtConcurrent::run(this, &CoverArtCache::searchImage, coverInfo); + future = QtConcurrent::run(this, &CoverArtCache::searchImage, + coverInfo, croppedPixmap, emitSignals); connect(watcher, SIGNAL(finished()), this, SLOT(imageFound())); } else { future = QtConcurrent::run(this, &CoverArtCache::loadImage, - trackId, coverLocation, md5Hash); + trackId, coverLocation, md5Hash, + croppedPixmap, emitSignals); connect(watcher, SIGNAL(finished()), this, SLOT(imageLoaded())); } m_runningIds.insert(trackId); watcher->setFuture(future); + + return QPixmap(); } // Load cover from path stored in DB. // It is executed in a separate thread via QtConcurrent::run -CoverArtCache::FutureResult CoverArtCache::loadImage( - int trackId, const QString& coverLocation, const QString& md5Hash) { +CoverArtCache::FutureResult CoverArtCache::loadImage(int trackId, + const QString& coverLocation, + const QString& md5Hash, + const bool croppedPixmap, + const bool emitSignals) { FutureResult res; res.trackId = trackId; res.coverLocation = coverLocation; res.img = QImage(coverLocation); - res.img = rescaleBigImage(res.img); res.md5Hash = md5Hash; + res.croppedImg = croppedPixmap; + res.emitSignals = emitSignals; + + if (res.croppedImg) { + res.img = cropImage(res.img); + } else { + res.img = rescaleBigImage(res.img); + } + return res; } @@ -109,12 +150,15 @@ void CoverArtCache::imageLoaded() { FutureResult res = watcher->result(); QPixmap pixmap; - if (QPixmapCache::find(res.md5Hash, &pixmap)) { + QString cacheKey = res.croppedImg ? res.md5Hash % "_cropped" : res.md5Hash; + if (QPixmapCache::find(cacheKey, &pixmap) && res.emitSignals) { emit(pixmapFound(res.trackId, pixmap)); } else if (!res.img.isNull()) { pixmap.convertFromImage(res.img); - if (QPixmapCache::insert(res.md5Hash, pixmap)) { - emit(pixmapFound(res.trackId, pixmap)); + if (QPixmapCache::insert(cacheKey, pixmap)) { + if (res.emitSignals) { + emit(pixmapFound(res.trackId, pixmap)); + } } } m_runningIds.remove(res.trackId); @@ -124,9 +168,15 @@ void CoverArtCache::imageLoaded() { // that could block the main thread. Therefore, this method // is executed in a separate thread via QtConcurrent::run CoverArtCache::FutureResult CoverArtCache::searchImage( - CoverArtDAO::CoverArtInfo coverInfo) { + CoverArtDAO::CoverArtInfo coverInfo, + const bool croppedPixmap, + const bool emitSignals) { FutureResult res; res.trackId = coverInfo.trackId; + res.md5Hash = coverInfo.md5Hash; + res.croppedImg = croppedPixmap; + res.emitSignals = emitSignals; + res.newImgFound = false; // Looking for embedded cover art. // @@ -138,17 +188,32 @@ CoverArtCache::FutureResult CoverArtCache::searchImage( // so we need to recalculate the md5 hash. res.md5Hash = calculateMD5(res.img); } - return res; + res.newImgFound = true; } // Looking for cover stored in track diretory. // - res.coverLocation = searchInTrackDirectory(coverInfo.trackDirectory, - coverInfo.trackBaseName, - coverInfo.album); - res.img = QImage(res.coverLocation); - res.img = rescaleBigImage(res.img); - res.md5Hash = calculateMD5(res.img); + if (!res.newImgFound) { + res.coverLocation = searchInTrackDirectory(coverInfo.trackDirectory, + coverInfo.trackBaseName, + coverInfo.album); + res.img = rescaleBigImage(QImage(res.coverLocation)); + res.md5Hash = calculateMD5(res.img); + res.newImgFound = true; + } + + // adjusting the cover size according to the final purpose + if (res.newImgFound && res.croppedImg) { + res.img = cropImage(res.img); + } + + // check if this image is really a new one + // (different from the one that we have in db) + if (coverInfo.md5Hash == res.md5Hash) + { + res.newImgFound = false; + } + return res; } @@ -232,25 +297,69 @@ void CoverArtCache::imageFound() { FutureResult res = watcher->result(); QPixmap pixmap; - if (QPixmapCache::find(res.md5Hash, &pixmap)) { + QString cacheKey = res.croppedImg ? res.md5Hash % "_cropped" : res.md5Hash; + if (QPixmapCache::find(cacheKey, &pixmap) && res.emitSignals) { emit(pixmapFound(res.trackId, pixmap)); } else if (!res.img.isNull()) { pixmap.convertFromImage(res.img); - if (QPixmapCache::insert(res.md5Hash, pixmap)) { - emit(pixmapFound(res.trackId, pixmap)); + if (QPixmapCache::insert(cacheKey, pixmap)) { + if (res.emitSignals) { + emit(pixmapFound(res.trackId, pixmap)); + } } } + // update DB - int coverId = m_pCoverArtDAO->saveCoverArt(res.coverLocation, res.md5Hash); - m_pTrackDAO->updateCoverArt(res.trackId, coverId); + if (res.newImgFound && !m_queueOfUpdates.contains(res.trackId)) { + m_queueOfUpdates.insert(res.trackId, + qMakePair(res.coverLocation, res.md5Hash)); + } + + if (m_queueOfUpdates.size() == 1 && !m_timer->isActive()) { + m_timer->start(500); // after 0.5s, it will call `updateDB()` + } m_runningIds.remove(res.trackId); } +// sqlite can't do a huge number of updates in a very short time, +// so it is important to collect all new covers and write them at once. +void CoverArtCache::updateDB() { + if (m_queueOfUpdates.isEmpty()) { + return; + } + QSet > covers; + covers = m_pCoverArtDAO->saveCoverArt(m_queueOfUpdates); + m_pTrackDAO->updateCoverArt(covers); + m_queueOfUpdates.clear(); +} + +// It will return a cropped cover that is ready to be +// used by the tableview-cover_column (CoverDelegate). +// As QImage is optimized to manipulate images, we will do it here +// instead of rescale it directly on the CoverDelegate::paint() +// because it would be much slower and could easily freeze the UI... +// Also, this method will run in separate thread +// (via Qtconcurrent - called by searchImage() or loadImage()) +QImage CoverArtCache::cropImage(QImage img) { + if (img.isNull()) { + return QImage(); + } + + // it defines the maximum width of the covers displayed + // in the cover art column (tableviews). + // (if you want to increase it - you have to change JUST it.) + const int WIDTH = 100; + const int CELL_HEIGHT = 20; + + img = img.scaledToWidth(WIDTH, Qt::SmoothTransformation); + return img.copy(0, 0, img.width(), CELL_HEIGHT); +} + // if it's too big, we have to scale it. // big images would be quickly removed from cover cache. QImage CoverArtCache::rescaleBigImage(QImage img) { - const int MAXSIZE = 400; + const int MAXSIZE = 300; QSize size = img.size(); if (size.height() > MAXSIZE || size.width() > MAXSIZE) { return img.scaled(MAXSIZE, MAXSIZE, diff --git a/src/library/coverartcache.h b/src/library/coverartcache.h index c521b21284a..479b1fcdf5e 100644 --- a/src/library/coverartcache.h +++ b/src/library/coverartcache.h @@ -15,9 +15,12 @@ class CoverArtCache : public QObject, public Singleton Q_OBJECT public: bool changeCoverArt(int trackId, const QString& newCoverLocation); - void requestPixmap(int trackId, - const QString& coverLocation = QString(), - const QString& md5Hash = QString()); + QPixmap requestPixmap(int trackId, + const QString& coverLocation = QString(), + const QString& md5Hash = QString(), + const bool tryLoadAndSearch = true, + const bool croppedPixmap = false, + const bool emitSignals = true); void setCoverArtDAO(CoverArtDAO* coverdao); void setTrackDAO(TrackDAO* trackdao); QString getDefaultCoverLocation() { return m_sDefaultCoverLocation; } @@ -27,6 +30,9 @@ class CoverArtCache : public QObject, public Singleton void imageFound(); void imageLoaded(); + private slots: + void updateDB(); + signals: void pixmapFound(int trackId, QPixmap pixmap); @@ -40,12 +46,19 @@ class CoverArtCache : public QObject, public Singleton QString coverLocation; QString md5Hash; QImage img; + bool croppedImg; + bool emitSignals; + bool newImgFound; }; - FutureResult searchImage(CoverArtDAO::CoverArtInfo coverInfo); + FutureResult searchImage(CoverArtDAO::CoverArtInfo coverInfo, + const bool croppedPixmap = false, + const bool emitSignals = true); FutureResult loadImage(int trackId, const QString& coverLocation, - const QString& md5Hash); + const QString& md5Hash, + const bool croppedPixmap = false, + const bool emitSignals = true); private: static CoverArtCache* m_instance; @@ -53,9 +66,12 @@ class CoverArtCache : public QObject, public Singleton TrackDAO* m_pTrackDAO; const QString m_sDefaultCoverLocation; const QPixmap m_defaultCover; + QTimer* m_timer; QSet m_runningIds; + QHash > m_queueOfUpdates; QString calculateMD5(QImage img); + QImage cropImage(QImage img); QImage rescaleBigImage(QImage img); QImage extractEmbeddedCover(QString trackLocation); QString searchInTrackDirectory(QString directory, diff --git a/src/library/coverartdelegate.cpp b/src/library/coverartdelegate.cpp new file mode 100644 index 00000000000..d1b37d72c60 --- /dev/null +++ b/src/library/coverartdelegate.cpp @@ -0,0 +1,89 @@ +#include + +#include "library/coverartcache.h" +#include "library/coverartdelegate.h" +#include "library/dao/trackdao.h" + +CoverArtDelegate::CoverArtDelegate(QObject *parent) + : QStyledItemDelegate(parent), + m_pTableView(NULL), + m_pTrackModel(NULL), + m_bIsLocked(false), + m_sDefaultCover(CoverArtCache::instance()->getDefaultCoverLocation()), + m_iCoverLocationColumn(-1), + m_iMd5Column(-1) { + // This assumes that the parent is wtracktableview + connect(parent, SIGNAL(lockCoverArtDelegate(bool)), + this, SLOT(slotLock(bool))); + + if (QTableView *tableView = qobject_cast(parent)) { + m_pTableView = tableView; + m_pTrackModel = dynamic_cast(m_pTableView->model()); + m_iMd5Column = m_pTrackModel->fieldIndex(LIBRARYTABLE_COVERART_MD5); + m_iCoverLocationColumn = m_pTrackModel->fieldIndex( + LIBRARYTABLE_COVERART_LOCATION); + + int coverColumn = m_pTrackModel->fieldIndex(LIBRARYTABLE_COVERART); + m_pTableView->setColumnWidth(coverColumn, 100); + } +} + +CoverArtDelegate::~CoverArtDelegate() { +} + +void CoverArtDelegate::slotLock(bool lock) { + m_bIsLocked = lock; +} + +void CoverArtDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const { + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight()); + } + + if (!m_pTrackModel || m_iCoverLocationColumn == -1 || m_iMd5Column == -1) { + return; + } + + int trackId = m_pTrackModel->getTrackId(index); + if (trackId < 1) { + return; + } + + QString coverLocation = index.sibling(index.row(), + m_iCoverLocationColumn + ).data().toString(); + + if (coverLocation != m_sDefaultCover) { // draw cover_art + + QString md5Hash = index.sibling(index.row(), + m_iMd5Column + ).data().toString(); + + // If the CoverDelegate is locked, it must not try + // to load (from coverLocation) and search covers. + // It means that in this cases it will just draw + // covers which are already in the pixmapcache. + QPixmap pixmap = CoverArtCache::instance()->requestPixmap( + trackId, coverLocation, md5Hash, + !m_bIsLocked, true, false); + + if (!pixmap.isNull()) { + // It already got a cropped pixmap (from covercache) + // that fit to the cell. + + // If you want to change the cropped_cover size, + // you MUST do it in CoverArtCache::cropCover() + int width = pixmap.width(); + if (option.rect.width() < width) { + width = option.rect.width(); + } + + QRect target(option.rect.x(), option.rect.y(), + width, option.rect.height()); + QRect source(0, 0, width, pixmap.height()); + painter->drawPixmap(target, pixmap, source); + } + } +} diff --git a/src/library/coverartdelegate.h b/src/library/coverartdelegate.h new file mode 100644 index 00000000000..5a71ba45ba2 --- /dev/null +++ b/src/library/coverartdelegate.h @@ -0,0 +1,41 @@ +#ifndef COVERARTDELEGATE_H +#define COVERARTDELEGATE_H + +#include +#include + +#include "library/trackmodel.h" + +class CoverArtDelegate : public QStyledItemDelegate { + Q_OBJECT + + public: + explicit CoverArtDelegate(QObject* parent = NULL); + virtual ~CoverArtDelegate(); + + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + + private slots: + // If the CoverDelegate is locked, it must not try + // to load and search covers. + // It means that in this cases it will just draw + // covers which are already in the pixmapcache. + // It is very important when the user scoll down + // very fast or when they hold an arrow key, because + // in these cases paint() would be called MANY times + // and it would be doing tons of "requestPixmaps", + // which could easily freeze the whole UI. + void slotLock(bool lock); + + private: + QTableView* m_pTableView; + TrackModel* m_pTrackModel; + bool m_bIsLocked; + QString m_sDefaultCover; + int m_iCoverLocationColumn; + int m_iMd5Column; +}; + +#endif // COVERARTDELEGATE_H diff --git a/src/library/cratetablemodel.cpp b/src/library/cratetablemodel.cpp index acf58cb053e..e2a60ee9a28 100644 --- a/src/library/cratetablemodel.cpp +++ b/src/library/cratetablemodel.cpp @@ -33,8 +33,9 @@ void CrateTableModel::setTableModel(int crateId) { FieldEscaper escaper(m_database); QString filter = "library.mixxx_deleted = 0"; QStringList columns; - columns << "crate_tracks." + CRATETRACKSTABLE_TRACKID + " as " + LIBRARYTABLE_ID - << "'' as preview"; + columns << "crate_tracks." + CRATETRACKSTABLE_TRACKID + " AS " + LIBRARYTABLE_ID + << "'' AS " + LIBRARYTABLE_PREVIEW + << "'' AS " + LIBRARYTABLE_COVERART; // We drop files that have been explicitly deleted from mixxx // (mixxx_deleted=0) from the view. There was a bug in <= 1.9.0 where @@ -58,6 +59,7 @@ void CrateTableModel::setTableModel(int crateId) { columns[0] = LIBRARYTABLE_ID; columns[1] = LIBRARYTABLE_PREVIEW; + columns[2] = LIBRARYTABLE_COVERART; setTable(tableName, columns[0], columns, m_pTrackCollection->getTrackSource()); setSearch(""); diff --git a/src/library/dao/coverartdao.cpp b/src/library/dao/coverartdao.cpp index 7126a9c91fe..fd1d80a9b41 100644 --- a/src/library/dao/coverartdao.cpp +++ b/src/library/dao/coverartdao.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -45,6 +46,59 @@ int CoverArtDAO::saveCoverArt(QString coverLocation, QString md5Hash) { return coverId; } +QSet > CoverArtDAO::saveCoverArt( + QHash > covers) { + if (covers.isEmpty()) { + return QSet >(); + } + + // it'll be used to avoid writing a new ID for + // rows which have the same md5 (not changed). + QString selectCoverId = QString("SELECT id FROM cover_art WHERE md5='%1'"); + + // + QSet > res; + + // preparing query to insert multi rows + QString sQuery; + QHashIterator > i(covers); + i.next(); + res.insert(qMakePair(i.key(), -1)); + sQuery = QString("INSERT OR REPLACE INTO cover_art ('id', 'location', 'md5') " + "SELECT (%1) AS 'id', '%2' AS 'location', '%3' AS 'md5' ") + .arg(selectCoverId.arg(i.value().second)) + .arg(i.value().first) + .arg(i.value().second); + + while (i.hasNext()) { + i.next(); + res.insert(qMakePair(i.key(), -1)); + sQuery = sQuery % QString("UNION SELECT (%1), '%2', '%3'") + .arg(selectCoverId.arg(i.value().second)) + .arg(i.value().first) + .arg(i.value().second); + } + + QSqlQuery query(m_database); + if (!query.exec(sQuery)) { + LOG_FAILED_QUERY(query) << "Failed to save multiple covers!"; + return QSet >(); + } + + QSetIterator > set(res); + while (set.hasNext()) { + QPair p = set.next(); + int trackId = p.first; + int coverId = getCoverArtId(covers.value(trackId).second); + if (coverId > 0) { + res.remove(p); + res.insert(qMakePair(trackId, coverId)); + } + } + + return res; +} + void CoverArtDAO::deleteUnusedCoverArts() { QSqlQuery query(m_database); @@ -109,42 +163,39 @@ CoverArtDAO::CoverArtInfo CoverArtDAO::getCoverArtInfo(int trackId) { return CoverArtInfo(); } - QSqlQuery query(m_database); - query.prepare( - "SELECT album, cover_art.location AS cover, cover_art.md5 AS md5, " - "track_locations.directory AS directory, " - "track_locations.filename AS filename, " - "track_locations.location AS location " - "FROM Library INNER JOIN track_locations " - "ON library.location = track_locations.id " - "LEFT JOIN cover_art ON cover_art.id = library.cover_art " - "WHERE library.id=:id " - ); - query.bindValue(":id", trackId); + // This method can be called a lot of times (by CoverCache), + // if we use functions like "indexOf()" to find the column numbers + // it will do at least one new loop for each column and it can bring + // performance issues... + QString columns = "album," //0 + "cover_art.location AS cover," //1 + "cover_art.md5," //2 + "track_locations.directory," //3 + "track_locations.filename," //4 + "track_locations.location"; //5 + + QString sQuery = QString( + "SELECT " % columns % " FROM Library " + "INNER JOIN track_locations ON library.location = track_locations.id " + "LEFT JOIN cover_art ON cover_art.id = library.cover_art " + "WHERE library.id = %1").arg(trackId); - if (!query.exec()) { + QSqlQuery query(m_database); + if (!query.exec(sQuery)) { LOG_FAILED_QUERY(query); return CoverArtInfo(); } - QSqlRecord queryRecord = query.record(); - const int albumColumn = queryRecord.indexOf("album"); - const int coverColumn = queryRecord.indexOf("cover"); - const int md5Column = queryRecord.indexOf("md5"); - const int directoryColumn = queryRecord.indexOf("directory"); - const int filenameColumn = queryRecord.indexOf("filename"); - const int locationColumn = queryRecord.indexOf("location"); - if (query.next()) { CoverArtInfo coverInfo; coverInfo.trackId = trackId; - coverInfo.album = query.value(albumColumn).toString(); - coverInfo.coverLocation = query.value(coverColumn).toString(); - coverInfo.md5Hash = query.value(md5Column).toString(); - coverInfo.trackDirectory = query.value(directoryColumn).toString(); - coverInfo.trackLocation = query.value(locationColumn).toString(); - coverInfo.trackBaseName = QFileInfo(query.value(filenameColumn) - .toString()).baseName(); + coverInfo.album = query.value(0).toString(); + coverInfo.coverLocation = query.value(1).toString(); + coverInfo.md5Hash = query.value(2).toString(); + coverInfo.trackDirectory = query.value(3).toString(); + QString filename = query.value(4).toString(); + coverInfo.trackLocation = query.value(5).toString(); + coverInfo.trackBaseName = QFileInfo(filename).baseName(); return coverInfo; } diff --git a/src/library/dao/coverartdao.h b/src/library/dao/coverartdao.h index aaf2f1dd5f7..5c9db453ed6 100644 --- a/src/library/dao/coverartdao.h +++ b/src/library/dao/coverartdao.h @@ -23,6 +23,10 @@ class CoverArtDAO : public DAO { int getCoverArtId(QString md5Hash); int saveCoverArt(QString coverLocation, QString md5Hash); + // @param covers: > + // @return + QSet > saveCoverArt(QHash > covers); + struct CoverArtInfo { int trackId; QString coverLocation; diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index e763a29481c..70cf964d13f 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -842,7 +842,7 @@ TrackPointer TrackDAO::getTrackFromDB(const int id) const { QSqlQuery query(m_database); query.prepare( - "SELECT library.id, artist, title, album, album_artist, cover_art, year, genre, composer, " + "SELECT library.id, artist, title, album, album_artist, year, genre, composer, " "grouping, tracknumber, filetype, rating, key, track_locations.location as location, " "track_locations.filesize as filesize, comment, url, duration, bitrate, " "samplerate, cuepoint, bpm, replaygain, channels, " @@ -861,7 +861,6 @@ TrackPointer TrackDAO::getTrackFromDB(const int id) const { const int titleColumn = queryRecord.indexOf("title"); const int albumColumn = queryRecord.indexOf("album"); const int albumArtistColumn = queryRecord.indexOf("album_artist"); - const int coverArtColumn = queryRecord.indexOf("cover_art"); const int yearColumn = queryRecord.indexOf("year"); const int genreColumn = queryRecord.indexOf("genre"); const int composerColumn = queryRecord.indexOf("composer"); @@ -897,7 +896,6 @@ TrackPointer TrackDAO::getTrackFromDB(const int id) const { QString title = query.value(titleColumn).toString(); QString album = query.value(albumColumn).toString(); QString albumArtist = query.value(albumArtistColumn).toString(); - int coverArtId = query.value(coverArtColumn).toInt(); QString year = query.value(yearColumn).toString(); QString genre = query.value(genreColumn).toString(); QString composer = query.value(composerColumn).toString(); @@ -1246,6 +1244,38 @@ bool TrackDAO::updateCoverArt(int trackId, int coverId) { return true; } +// @param +bool TrackDAO::updateCoverArt(QSet > covers) { + if (covers.isEmpty()) { + return false; + } + + QSet trackIds; + QStringList trackIdsStringList; + QString sQuery = "UPDATE library SET cover_art = CASE id "; + + QSetIterator > set(covers); + while (set.hasNext()) { + QPair p = set.next(); + sQuery = sQuery % QString("WHEN '%1' THEN '%2' ").arg(p.first) + .arg(p.second); + trackIds.insert(p.first); + trackIdsStringList.append(QString::number(p.first)); + } + sQuery = sQuery % QString("END WHERE id IN (%1)") + .arg(trackIdsStringList.join(",")); + + QSqlQuery query(m_database); + if (!query.exec(sQuery)) { + LOG_FAILED_QUERY(query) << "couldn't update library.cover_art"; + return false; + } + + // we also need to update the cover_art column in the tablemodel. + emit(updateTracksInBTC(trackIds)); + return true; +} + // Mark all the tracks in the library as invalid. // That means we'll need to later check that those tracks actually // (still) exist as part of the library scanning procedure. diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 022c294295d..746bab891f7 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -50,6 +50,7 @@ const QString LIBRARYTABLE_KEY = "key"; const QString LIBRARYTABLE_KEY_ID = "key_id"; const QString LIBRARYTABLE_BPM_LOCK = "bpm_lock"; const QString LIBRARYTABLE_PREVIEW = "preview"; +const QString LIBRARYTABLE_COVERART = "cover"; const QString LIBRARYTABLE_COVERART_LOCATION = "cover_art"; const QString LIBRARYTABLE_COVERART_MD5 = "md5"; @@ -115,6 +116,7 @@ class TrackDAO : public QObject, public virtual DAO { // it will update the Library.cover_art column in DB bool updateCoverArt(int trackId, int coverId); + bool updateCoverArt(QSet > covers); signals: void trackDirty(int trackId); @@ -125,6 +127,7 @@ class TrackDAO : public QObject, public virtual DAO { void dbTrackAdded(TrackPointer pTrack); void progressVerifyTracksOutside(QString path); void updateTrackInBTC(int trackId); + void updateTracksInBTC(QSet trackIds); public slots: // The public interface to the TrackDAO requires a TrackPointer so that we diff --git a/src/library/librarytablemodel.cpp b/src/library/librarytablemodel.cpp index 8c9ceaae610..7b20dac063e 100644 --- a/src/library/librarytablemodel.cpp +++ b/src/library/librarytablemodel.cpp @@ -19,7 +19,9 @@ LibraryTableModel::~LibraryTableModel() { void LibraryTableModel::setTableModel(int id) { Q_UNUSED(id); QStringList columns; - columns << "library." + LIBRARYTABLE_ID << "'' as preview"; + columns << "library." + LIBRARYTABLE_ID + << "'' AS " + LIBRARYTABLE_PREVIEW + << "'' AS " + LIBRARYTABLE_COVERART; const QString tableName = "library_view"; @@ -37,6 +39,7 @@ void LibraryTableModel::setTableModel(int id) { QStringList tableColumns; tableColumns << LIBRARYTABLE_ID; tableColumns << LIBRARYTABLE_PREVIEW; + tableColumns << LIBRARYTABLE_COVERART; setTable(tableName, LIBRARYTABLE_ID, tableColumns, m_pTrackCollection->getTrackSource()); setSearch(""); diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index 3a4360f3ac3..f48130ceb79 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -102,6 +102,8 @@ MixxxLibraryFeature::MixxxLibraryFeature(QObject* parent, pBaseTrackCache, SLOT(slotDbTrackAdded(TrackPointer))); connect(&m_trackDao, SIGNAL(updateTrackInBTC(int)), pBaseTrackCache, SLOT(slotUpdateTrack(int))); + connect(&m_trackDao, SIGNAL(updateTracksInBTC(QSet)), + pBaseTrackCache, SLOT(slotUpdateTracks(QSet))); m_pBaseTrackCache = QSharedPointer(pBaseTrackCache); pTrackCollection->setTrackSource(m_pBaseTrackCache); diff --git a/src/library/playlisttablemodel.cpp b/src/library/playlisttablemodel.cpp index 1a71ae9a3e6..83874e32f15 100644 --- a/src/library/playlisttablemodel.cpp +++ b/src/library/playlisttablemodel.cpp @@ -28,10 +28,11 @@ void PlaylistTableModel::setTableModel(int playlistId) { FieldEscaper escaper(m_database); QStringList columns; - columns << PLAYLISTTRACKSTABLE_TRACKID + " as " + LIBRARYTABLE_ID + columns << PLAYLISTTRACKSTABLE_TRACKID + " AS " + LIBRARYTABLE_ID << PLAYLISTTRACKSTABLE_POSITION << PLAYLISTTRACKSTABLE_DATETIMEADDED - << "'' as preview"; + << "'' AS " + LIBRARYTABLE_PREVIEW + << "'' AS " + LIBRARYTABLE_COVERART; // We drop files that have been explicitly deleted from mixxx // (mixxx_deleted=0) from the view. There was a bug in <= 1.9.0 where @@ -54,6 +55,7 @@ void PlaylistTableModel::setTableModel(int playlistId) { columns[0] = LIBRARYTABLE_ID; columns[3] = LIBRARYTABLE_PREVIEW; + columns[4] = LIBRARYTABLE_COVERART; setTable(playlistTableName, columns[0], columns, m_pTrackCollection->getTrackSource()); setSearch(""); diff --git a/src/widget/wlibrarytableview.cpp b/src/widget/wlibrarytableview.cpp index 8a6fc0ce182..4cb9a2b3150 100644 --- a/src/widget/wlibrarytableview.cpp +++ b/src/widget/wlibrarytableview.cpp @@ -43,6 +43,9 @@ WLibraryTableView::WLibraryTableView(QWidget* parent, loadVScrollBarPosState(); + connect(verticalScrollBar(), SIGNAL(valueChanged(int)), + this, SIGNAL(scrollValueChanged(int))); + setTabKeyNavigation(false); } diff --git a/src/widget/wlibrarytableview.h b/src/widget/wlibrarytableview.h index c412f7d509e..6ad6981c5a4 100644 --- a/src/widget/wlibrarytableview.h +++ b/src/widget/wlibrarytableview.h @@ -26,6 +26,8 @@ class WLibraryTableView : public QTableView, public virtual LibraryView { void loadTrack(TrackPointer pTrack); void loadTrackToPlayer(TrackPointer pTrack, QString group, bool play = false); void loadCoverArt(const QString& coverLocation, const QString&, int trackId); + void lockCoverArtDelegate(bool); + void scrollValueChanged(int); public slots: void saveVScrollBarPos(); diff --git a/src/widget/wtracktableview.cpp b/src/widget/wtracktableview.cpp index d6ad8d4d06e..0e4b6b7dc79 100644 --- a/src/widget/wtracktableview.cpp +++ b/src/widget/wtracktableview.cpp @@ -28,7 +28,9 @@ WTrackTableView::WTrackTableView(QWidget * parent, m_pConfig(pConfig), m_pTrackCollection(pTrackCollection), m_DlgTagFetcher(NULL), - m_sorting(sorting) { + m_sorting(sorting), + m_iCoverLocationColumn(-1), + m_iMd5Column(-1) { // Give a NULL parent because otherwise it inherits our style which can make // it unreadable. Bug #673411 m_pTrackInfo = new DlgTrackInfo(NULL,m_DlgTagFetcher); @@ -92,6 +94,8 @@ WTrackTableView::WTrackTableView(QWidget * parent, m_pCOTGuiTickTime = new ControlObjectThread("[Master]", "guiTick50ms"); connect(m_pCOTGuiTickTime, SIGNAL(valueChanged(double)), this, SLOT(slotGuiTickTime(double))); + connect(this, SIGNAL(scrollValueChanged(int)), + this, SLOT(slotScrollValueChanged(int))); } WTrackTableView::~WTrackTableView() { @@ -133,6 +137,15 @@ WTrackTableView::~WTrackTableView() { delete m_pCOTGuiTickTime; } +void WTrackTableView::slotScrollValueChanged(int) { + if (m_bLastCoverLoaded) { + // not draw covers in the tableview (cover_art column) + emit(lockCoverArtDelegate(true)); + } + m_bLastCoverLoaded = false; + m_lastSelection = m_pCOTGuiTickTime->get(); +} + void WTrackTableView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(selected); @@ -141,6 +154,8 @@ void WTrackTableView::selectionChanged(const QItemSelection &selected, if (m_bLastCoverLoaded) { // load default cover art emit(loadCoverArt("", "", 0)); + // not draw covers in the tableview (cover_art column) + emit(lockCoverArtDelegate(true)); } m_bLastCoverLoaded = false; m_lastSelection = m_pCOTGuiTickTime->get(); @@ -159,6 +174,10 @@ void WTrackTableView::slotGuiTickTime(double cpuTime) { } void WTrackTableView::slotLoadCoverArt() { + if (m_iCoverLocationColumn < 0 || m_iMd5Column < 0) { + return; + } + QString coverLocation; QString md5Hash; int trackId = 0; @@ -167,14 +186,14 @@ void WTrackTableView::slotLoadCoverArt() { QModelIndex idx = indices[0]; TrackModel* trackModel = getTrackModel(); if (trackModel) { - coverLocation = idx.sibling(idx.row(), trackModel->fieldIndex( - LIBRARYTABLE_COVERART_LOCATION)).data().toString(); - md5Hash = idx.sibling(idx.row(), trackModel->fieldIndex( - LIBRARYTABLE_COVERART_MD5)).data().toString(); + md5Hash = idx.sibling(idx.row(), m_iMd5Column).data().toString(); trackId = trackModel->getTrackId(idx); + coverLocation = idx.sibling(idx.row(), + m_iCoverLocationColumn).data().toString(); } } emit(loadCoverArt(coverLocation, md5Hash, trackId)); + emit(lockCoverArtDelegate(false)); update(); } @@ -198,6 +217,13 @@ void WTrackTableView::loadTrackModel(QAbstractItemModel *model) { return; } + // The "coverLocation" and "md5" column numbers are very often required + // by slotLoadCoverArt(). As this value will not change when the model + // still the same, we must avoid doing hundreds of "fieldIndex" calls + // when it is completely unnecessary... + m_iCoverLocationColumn = track_model->fieldIndex(LIBRARYTABLE_COVERART_LOCATION); + m_iMd5Column = track_model->fieldIndex(LIBRARYTABLE_COVERART_MD5); + setVisible(false); // Save the previous track model's header state diff --git a/src/widget/wtracktableview.h b/src/widget/wtracktableview.h index d3a8e29e12e..db667568d86 100644 --- a/src/widget/wtracktableview.h +++ b/src/widget/wtracktableview.h @@ -70,6 +70,7 @@ class WTrackTableView : public WLibraryTableView { void slotScaleBpm(int); void slotClearBeats(); void slotGuiTickTime(double); + void slotScrollValueChanged(int); private: void sendToAutoDJ(bool bTop); @@ -149,6 +150,10 @@ class WTrackTableView : public WLibraryTableView { bool m_sorting; + // Column numbers + int m_iCoverLocationColumn; // cover art location + int m_iMd5Column; // cover art md5 hash + // Control the delay to load a cover art. double m_lastSelection; bool m_bLastCoverLoaded;