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
/***************************************************************************
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 <qpixmap.h>
#include <qmap.h>
#include <qstring.h>
#include <qfile.h>
#include <qimage.h>
#include <qdir.h>
#include <qdatastream.h>
#include <qdict.h>
#include <kdebug.h>
#include <kstaticdeleter.h>
#include <kstandarddirs.h>
#include <kglobal.h>
#include "covermanager.h"
// This is a dictionary to map the track path to their ID. Otherwise we'd have
// to store this info with each CollectionListItem, which would break the cache
// of users who upgrade, and would just generally be a big mess.
typedef QDict<coverKey> TrackLookupMap;
// This is responsible for making sure that the CoverManagerPrivate class
// gets properly destructed on shutdown.
static KStaticDeleter<CoverManagerPrivate> sd;
CoverManagerPrivate *CoverManager::m_data = 0;
// Used to save and load CoverData from a QDataStream
QDataStream &operator<<(QDataStream &out, const CoverData &data);
QDataStream &operator>>(QDataStream &in, CoverData &data);
//
// Implementation of CoverData struct
//
QPixmap CoverData::pixmap() const
{
if(m_pixmap.isNull())
m_pixmap = QPixmap(path);
return m_pixmap;
}
QPixmap CoverData::thumbnail() const
{
if(!m_thumbnail.isNull())
return m_thumbnail;
QPixmap base = pixmap();
if(base.isNull())
return QPixmap();
// Convert to image for smoothScale()
QImage image = base.convertToImage();
m_thumbnail.convertFromImage(image.smoothScale(80, 80));
return m_thumbnail;
}
/**
* This class is responsible for actually keeping track of the storage for the
* different covers and such. It holds the covers, and the map of path names
* to cover ids, and has a few utility methods to load and save the data.
*
* @author Michael Pyne <michael.pyne@kdemail.net>
* @see CoverManager
*/
class CoverManagerPrivate
{
public:
CoverManagerPrivate() : m_trackMapping(1301)
{
loadCovers();
}
~CoverManagerPrivate()
{
saveCovers();
}
CoverDataMap covers() const { return m_covers; }
CoverDataMap covers() { return m_covers; }
TrackLookupMap tracks() const { return m_trackMapping; }
TrackLookupMap tracks() { return m_trackMapping; }
/**
* Creates the data directory for the covers if it doesn't already exist.
* Must be in this class for loadCovers() and saveCovers().
*/
void createDataDir() const;
/**
* Returns the next available unused coverKey that can be used for
* inserting new items.
*
* @return unused id that can be used for new CoverData
*/
coverKey nextId() const;
private:
void loadCovers();
void saveCovers() const;
/**
* @return the full path and filename of the file storing the cover
* lookup map and the translations between pathnames and ids.
*/
QString coverLocation() const;
CoverDataMap m_covers;
TrackLookupMap m_trackMapping;
};
//
// Implementation of CoverManagerPrivate methods.
//
void CoverManagerPrivate::createDataDir() const
{
QDir dir;
QString dirPath(QDir::cleanDirPath(coverLocation() + "/.."));
if(!dir.exists(dirPath))
KStandardDirs::makeDir(dirPath);
}
void CoverManagerPrivate::saveCovers() const
{
kdDebug() << k_funcinfo << endl;
// Make sure the directory exists first.
createDataDir();
QFile file(coverLocation());
kdDebug() << "Opening covers db: " << coverLocation() << endl;
if(!file.open(IO_WriteOnly)) {
kdError() << "Unable to save covers to disk!\n";
return;
}
QDataStream out(&file);
// Write out the version and count
out << Q_UINT32(0) << Q_UINT32(m_covers.count());