Commit b6de380b authored by Michael Pyne's avatar Michael Pyne

Add a CoverManager class to JuK (with a shockingly minimal GUI). It will

import covers stored in the original format, so starting up JuK for the first
time with this code will probably take awhile, after that the speed should be
unaffected.

The CoverManager is capable of associating a track with an arbitrary cover.
Each cover in the CoverManager can have associated with it an Artist and
Album to allow for automatically applying covers to newly added tracks.
However now the cover for a track isn't internally limited based on its
Artist and Album info.

Note that this needs some fleshing out.  The GUI will definitely need some
improvement, and some things may not work exactly like they used to, but at
least we've got a good base under us now.

svn path=/trunk/KDE/kdemultimedia/juk/; revision=414471
parent be92d8f6
......@@ -9,7 +9,10 @@ juk_SOURCES = \
cache.cpp \
categoryreaderinterface.cpp \
collectionlist.cpp \
coverdialog.cpp \
coverdialogbase.ui \
coverinfo.cpp \
covermanager.cpp \
deletedialog.cpp \
deletedialogbase.ui \
directorylist.cpp \
......
/***************************************************************************
begin : Sun May 15 2005
copyright : (C) 2005 by Michael Pyne
email : michael.pyne@kdemail.net
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include <klistview.h>
#include <kiconview.h>
#include <kapplication.h>
#include <kpopupmenu.h>
#include <klocale.h>
#include <qtimer.h>
#include "coverdialog.h"
#include "covermanager.h"
#include "collectionlist.h"
namespace CoverDialogPrivate
{
class CoverIconViewItem;
};
class CoverDialogPrivate::CoverIconViewItem : public KIconViewItem
{
public:
CoverIconViewItem(coverKey id, QIconView *parent) :
KIconViewItem(parent), m_id(id)
{
CoverDataPtr data = CoverManager::coverInfo(id);
setText(QString("%1 - %2").arg(data->artist, data->album));
setPixmap(data->thumbnail());
}
coverKey id() const { return m_id; }
private:
coverKey m_id;
};
using CoverDialogPrivate::CoverIconViewItem;
class AllArtistsListViewItem : public KListViewItem
{
public:
AllArtistsListViewItem(QListView *parent) :
KListViewItem(parent, i18n("<All Artists>"))
{
}
int compare(QListViewItem *, int, bool) const
{
return -1; // Always be at the top.
}
};
CoverDialog::CoverDialog(QWidget *parent) :
CoverDialogBase(parent, "juk_cover_dialog", WType_Dialog)
{
m_covers->setResizeMode(QIconView::Adjust);
m_covers->setGridX(140);
m_covers->setGridY(150);
}
CoverDialog::~CoverDialog()
{
}
void CoverDialog::show()
{
m_artists->clear();
m_covers->clear();
QStringList artists = CollectionList::instance()->uniqueSet(CollectionList::Artists);
m_artists->setSorting(-1);
new AllArtistsListViewItem(m_artists);
for(QStringList::ConstIterator it = artists.begin(); it != artists.end(); ++it)
new KListViewItem(m_artists, *it);
m_artists->setSorting(0);
QTimer::singleShot(0, this, SLOT(loadCovers()));
CoverDialogBase::show();
}
// Here we try to keep the GUI from freezing for too long while we load the
// covers.
void CoverDialog::loadCovers()
{
QValueList<coverKey> keys = CoverManager::keys();
QValueList<coverKey>::ConstIterator it;
int i = 0;
for(it = keys.begin(); it != keys.end(); ++it) {
new CoverIconViewItem(*it, m_covers);
if(++i == 10) {
i = 0;
kapp->processEvents();
}
}
}
// TODO: Add a way to show cover art for tracks with no artist.
void CoverDialog::slotArtistClicked(QListViewItem *item)
{
m_covers->clear();
if(dynamic_cast<AllArtistsListViewItem *>(item)) {
// All artists.
loadCovers();
}
else {
QString artist = item->text(0).lower();
QValueList<coverKey> keys = CoverManager::keys();
QValueList<coverKey>::ConstIterator it;
for(it = keys.begin(); it != keys.end(); ++it) {
CoverDataPtr data = CoverManager::coverInfo(*it);
if(data->artist == artist)
new CoverIconViewItem(*it, m_covers);
}
}
}
void CoverDialog::slotContextRequested(QIconViewItem *item, const QPoint &pt)
{
static KPopupMenu *menu = 0;
if(!item)
return;
if(!menu) {
menu = new KPopupMenu(this);
menu->insertItem(i18n("Remove cover"), this, SLOT(removeSelectedCover()));
}
menu->popup(pt);
}
void CoverDialog::removeSelectedCover()
{
CoverIconViewItem *coverItem = static_cast<CoverIconViewItem *>(m_covers->currentItem());
if(!coverItem || !coverItem->isSelected()) {
kdWarning(65432) << "No item selected for removeSelectedCover.\n";
return;
}
if(!CoverManager::removeCover(coverItem->id()))
kdError(65432) << "Unable to remove selected cover: " << coverItem->id() << endl;
else
delete coverItem;
}
#include "coverdialog.moc"
// vim: set et ts=4 sw=4:
/***************************************************************************
begin : Sun May 15 2005
copyright : (C) 2005 by Michael Pyne
email : michael.pyne@kdemail.net
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef JUK_COVERDIALOG_H
#define JUK_COVERDIALOG_H
#include "coverdialogbase.h"
class CoverDialog : public CoverDialogBase
{
Q_OBJECT
public:
CoverDialog(QWidget *parent);
~CoverDialog();
virtual void show();
public slots:
void slotArtistClicked(QListViewItem *item);
void slotContextRequested(QIconViewItem *item, const QPoint &pt);
private slots:
void loadCovers();
void removeSelectedCover();
};
#endif /* JUK_COVERDIALOG_H */
// vim: set et ts=4 sw=4:
<!DOCTYPE UI><UI version="3.3" stdsetdef="1">
<class>CoverDialogBase</class>
<widget class="QWidget">
<property name="name">
<cstring>CoverDialogBase</cstring>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>652</width>
<height>525</height>
</rect>
</property>
<property name="caption">
<string>Cover Manager</string>
</property>
<hbox>
<property name="name">
<cstring>unnamed</cstring>
</property>
<widget class="KListView">
<column>
<property name="text">
<string>Artist</string>
</property>
<property name="clickable">
<bool>false</bool>
</property>
<property name="resizable">
<bool>false</bool>
</property>
</column>
<item>
<property name="text">
<string>&lt;All&gt;</string>
</property>
<property name="pixmap">
<pixmap></pixmap>
</property>
</item>
<property name="name">
<cstring>m_artists</cstring>
</property>
<property name="sizePolicy">
<sizepolicy>
<hsizetype>5</hsizetype>
<vsizetype>7</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>164</width>
<height>0</height>
</size>
</property>
<property name="resizeMode">
<enum>LastColumn</enum>
</property>
<property name="shadeSortColumn">
<bool>false</bool>
</property>
</widget>
<widget class="KIconView">
<property name="name">
<cstring>m_covers</cstring>
</property>
<property name="gridX">
<number>140</number>
</property>
<property name="gridY">
<number>150</number>
</property>
<property name="resizeMode">
<enum>Adjust</enum>
</property>
</widget>
</hbox>
</widget>
<customwidgets>
</customwidgets>
<connections>
<connection>
<sender>m_artists</sender>
<signal>clicked(QListViewItem*)</signal>
<receiver>CoverDialogBase</receiver>
<slot>slotArtistClicked(QListViewItem*)</slot>
</connection>
<connection>
<sender>m_covers</sender>
<signal>contextMenuRequested(QIconViewItem*,const QPoint&amp;)</signal>
<receiver>CoverDialogBase</receiver>
<slot>slotContextRequested(QIconViewItem*,const QPoint&amp;)</slot>
</connection>
</connections>
<slots>
<slot>slotArtistClicked(QListViewItem *item)</slot>
<slot>slotContextRequested(QIconViewItem *, const QPoint &amp;pt)</slot>
</slots>
<layoutdefaults spacing="6" margin="11"/>
<includehints>
<includehint>klistview.h</includehint>
<includehint>kiconview.h</includehint>
</includehints>
</UI>
......@@ -53,16 +53,64 @@ struct CoverPopup : public QWidget
CoverInfo::CoverInfo(const FileHandle &file) :
m_file(file),
m_hasCover(false),
m_haveCheckedForCover(false)
m_haveCheckedForCover(false),
m_coverKey(CoverManager::NoMatch)
{
}
bool CoverInfo::hasCover()
{
if(!m_haveCheckedForCover) {
if(m_haveCheckedForCover)
return m_hasCover;
m_haveCheckedForCover = true;
// Check for new-style covers. First let's determine what our coverKey is
// if it's not already set, as that's also tracked by the CoverManager.
if(m_coverKey == CoverManager::NoMatch)
m_coverKey = CoverManager::idForTrack(m_file.absFilePath());
// We were assigned a key, let's see if we already have a cover. Notice
// that due to the way the CoverManager is structured, we should have a
// cover if we have a cover key. If we don't then either there's a logic
// error, or the user has been mucking around where they shouldn't.
if(m_coverKey != CoverManager::NoMatch)
m_hasCover = CoverManager::hasCover(m_coverKey);
// If we don't have a cover, first check if the CoverManager
// has one matching our album and artist.
if(!m_hasCover) {
QString artist = m_file.tag()->artist();
QString album = m_file.tag()->album();
coverKey match = CoverManager::idFromMetadata(artist, album);
if(match != CoverManager::NoMatch) {
m_hasCover = true;
m_coverKey = match;
// Make CoverManager remember this association
CoverManager::setIdForTrack(m_file.absFilePath(), match);
}
}
// We *still* don't have it? Check the old-style covers then.
if(!m_hasCover) {
m_hasCover = QFile(coverLocation(FullSize)).exists();
m_haveCheckedForCover = true;
if(m_hasCover) {
// Ah, old-style cover. Let's transfer it to the new system.
kdDebug() << "Found old style cover for " << m_file.absFilePath() << endl;
QString artist = m_file.tag()->artist();
QString album = m_file.tag()->album();
m_coverKey = CoverManager::addCover(coverLocation(FullSize), artist, album);
if(m_coverKey != CoverManager::NoMatch)
CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
else
kdDebug() << "We were unable to replace the old style cover.\n";
}
}
return m_hasCover;
......@@ -74,6 +122,10 @@ void CoverInfo::clearCover()
QFile::remove(coverLocation(Thumbnail));
m_hasCover = false;
m_haveCheckedForCover = false;
CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch);
CoverManager::removeCover(m_coverKey);
m_coverKey = CoverManager::NoMatch;
}
void CoverInfo::setCover(const QImage &image)
......@@ -86,23 +138,27 @@ void CoverInfo::setCover(const QImage &image)
if(m_hasCover)
clearCover();
image.save(coverLocation(FullSize), "PNG");
QPixmap cover;
cover.convertFromImage(image);
if(m_coverKey != CoverManager::NoMatch)
CoverManager::replaceCover(m_coverKey, cover);
else {
m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album());
if(m_coverKey != CoverManager::NoMatch)
CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey);
}
}
QPixmap CoverInfo::pixmap(CoverSize size) const
{
if(m_file.tag()->artist().isEmpty() || m_file.tag()->album().isEmpty())
if(m_coverKey == CoverManager::NoMatch)
return QPixmap();
if(size == Thumbnail && !QFile(coverLocation(Thumbnail)).exists()) {
QPixmap large = pixmap(FullSize);
if(!large.isNull()) {
QImage image(large.convertToImage());
image.smoothScale(80, 80).save(coverLocation(Thumbnail), "PNG");
}
}
return QPixmap(coverLocation(size));
if(size == Thumbnail)
return CoverManager::coverFromId(m_coverKey, CoverManager::Thumbnail);
else
return CoverManager::coverFromId(m_coverKey, CoverManager::FullSize);
}
void CoverInfo::popup() const
......@@ -135,6 +191,9 @@ void CoverInfo::popup() const
new CoverPopup(image, QPoint(x, y));
}
/**
* DEPRECATED
*/
QString CoverInfo::coverLocation(CoverSize size) const
{
QString fileName(QFile::encodeName(m_file.tag()->artist() + " - " + m_file.tag()->album()));
......
......@@ -18,6 +18,7 @@
#include <qimage.h>
#include "filehandle.h"
#include "covermanager.h"
class CoverInfo
{
......@@ -42,6 +43,7 @@ private:
FileHandle m_file;
bool m_hasCover;
bool m_haveCheckedForCover;
coverKey m_coverKey;
};
#endif
This diff is collapsed.
/***************************************************************************
begin : Sun May 15 2005
copyright : (C) 2005 by Michael Pyne
email : michael.pyne@kdemail.net
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include <ksharedptr.h>
#include <qmap.h>
class CoverManagerPrivate;
class QString;
class QPixmap;
class QDataStream;
/**
* This class holds the data on a cover. This includes the path to the cover
* representation on-disk, and the artist and album associated with the cover.
* Don't assume that the artist or album information is filled out, it is
* there to allow the CoverManager to try to automatically assign covers to
* new tracks.
*
* @author Michael Pyne <michael.pyne@kdemail.net>
* @see CoverManager
*/
class CoverData : public KShared
{
public:
QPixmap pixmap() const;
QPixmap thumbnail() const;
void setPixmap(const QPixmap &pixmap)
{
m_pixmap = pixmap;
m_thumbnail = QPixmap();
}
QString artist;
QString album;
QString path;
private:
mutable QPixmap m_pixmap;
mutable QPixmap m_thumbnail;
};
typedef KSharedPtr<CoverData> CoverDataPtr;
typedef unsigned long coverKey; ///< Type of the id for a cover.
typedef QMap<coverKey, CoverDataPtr> CoverDataMap;
/**
* This class holds all of the cover art, and manages looking it up by artist
* and/or album. This class is similar to a singleton class, but instead all
* of the methods are static. This way you can invoke methods like this:
* \code
* CoverManager::method()
* \endcode
* instead of using:
* \code
* CoverManager::instance()->method()
* \endcode
*
* @author Michael Pyne <michael.pyne@kdemail.net>
*/
class CoverManager
{
public:
/// The set of different sizes you can request a pixmap as.
typedef enum { Thumbnail, FullSize } Size;
/**
* Tries to match @p artist and @p album to a cover in the database.
*
* @param artist The artist to look for matching covers on.
* @param album The album to look for matching covers on.
* @return NoMatch if no match could be found, otherwise the id of the
* cover art that matches the given metadata.
*/
static coverKey idFromMetadata(const QString &artist, const QString &album);
/**
* Returns the cover art for @p id.
*
* @param id The id of the cover.
* @param size The size to return it as. Note that FullSize doesn't
* necessarily mean the pixmap is large, so you may need to
* scale it up.
* @return QPixmap::null if there is no cover art for @p id, otherwise the
* cover art.
*/
static QPixmap coverFromId(coverKey id, Size size = Thumbnail);
/**
* Returns the full suite of information known about the cover given by
* @p id.
*
* @param id the id of the cover to retrieve info on.
* @return 0 if there is no info on @p id, otherwise its information.
*/
static CoverDataPtr coverInfo(coverKey id);
/**
* Adds @p large to the cover database, associating with it @p artist and
* @p album.
*
* @param large The full size cover (the thumbnail is automatically
* generated).
* @param artist The artist of the new cover.
* @param album The album of the new cover.
*/
static coverKey addCover(const QPixmap &large, const QString &artist = "", const QString &album = "");
/**
* Adds the file pointed to by the local path @p path to the database,
* associating it with @p artist and @p album.
*
* @param path The absolute path to the fullsize cover art.
* @param artist The artist of the new cover.
* @param album The album of the new cover.
*/
static coverKey addCover(const QString &path, const QString &artist = "", const QString &album = "");
/**
* Function to determine if @p id matches any covers in the database.
*
* @param id The id of the cover to search for.
* @return true if the database has a cover identified by @p id, false
* otherwise.
*/
static bool hasCover(coverKey id);
/**
* Removes the cover identified by @p id.
*
* @param id the id of the cover to remove.
* @return true if the removal was successful, false if unsuccessful or if
* the cover didn't exist.
*/
static bool removeCover(coverKey id);
/**
* Replaces the cover art for the cover identified by @p id with @p large.
* Any other metadata such as artist and album is unchanged.
*
* @param id The id of the cover to replace.
* @param large The full size cover art for the new cover.
*/
static bool replaceCover(coverKey id, const QPixmap &large);
/**
* This is a hack, as we should be shut down automatically by
* KStaticDeleter, but JuK is crashing for me on shutdown before
* KStaticDeleter gets a chance to run, which is cramping my testing.
*/
static void shutdown();
/**
* @return Iterator pointing to the first element in the cover database.
*/
static CoverDataMap::ConstIterator begin();
/**
* @return Iterator pointing after the last element in the cover database.
*/
static CoverDataMap::ConstIterator end();
/**
* @return A list of all of the id's listed in the database.
*/
static QValueList<coverKey> keys();
/**
* Associates @p path with the cover identified by @id. No comparison of
* metadata is performed to enforce this matching.
*
* @param path The absolute file path to the track.
* @param id The identifier of the cover to use with @p path.
*/
static void setIdForTrack(const QString &path, coverKey id);
/**
* Returns the identifier of the cover for the track at @p path.
*
* @param path The absolute file path to the track.
* @return NoMatch if @p path doesn't have a cover, otherwise the id of
* its cover.
*/
static coverKey idForTrack(const QString &path);
/**
* This identifier is used to indicate that no cover was found in the
* database.
*/
static const coverKey NoMatch = 0;
private:
static CoverManagerPrivate *m_data;
static CoverManagerPrivate *data();
static QPixmap createThumbnail(const QPixmap &base);
};
// vim: set et sw=4 ts=4:
......@@ -34,6 +34,7 @@
#include "cache.h"
#include "playlistsplitter.h"
#include "collectionlist.h"
#include "covermanager.h"
#include "tagtransactionmanager.h"
using namespace ActionCollection;
......@@ -156,7 +157,6 @@ void JuK::setupActions()
new KAction(i18n("Seek Forward"), "seekForward", 0, m_player, SLOT(seekForward()), actions(), "seekForward");
new KAction(i18n("Seek Back"), "seekBack", 0, m_player, SLOT(seekBack()), actions(), "seekBack");
//////////////////////////////////////////////////
// settings menu
//////////////////////////////////////////////////
......@@ -421,6 +421,8 @@ void JuK::slotQuit()
{
kdDebug(65432) << k_funcinfo << endl;
m_shuttingDown = true;