Load album cover art from files on disk

This commit is contained in:
David Sansome 2010-02-28 00:35:20 +00:00
parent 7de70bbaea
commit 1a26380e3f
9 changed files with 149 additions and 36 deletions

View File

@ -60,5 +60,6 @@
<file>open_media.png</file>
<file>open_stream.png</file>
<file>schema-1.sql</file>
<file>schema-2.sql</file>
</qresource>
</RCC>

5
data/schema-2.sql Normal file
View File

@ -0,0 +1,5 @@
ALTER TABLE songs ADD COLUMN art_automatic TEXT;
ALTER TABLE songs ADD COLUMN art_manual TEXT;
UPDATE schema_version SET version=2;

View File

@ -12,7 +12,7 @@
#include <QThread>
const char* LibraryBackend::kDatabaseName = "clementine.db";
const int LibraryBackend::kSchemaVersion = 1;
const int LibraryBackend::kSchemaVersion = 2;
LibraryBackend::LibraryBackend(QObject* parent)
: QObject(parent)
@ -57,18 +57,7 @@ QSqlDatabase LibraryBackend::Connect() {
if (db.tables().count() == 0) {
// Set up initial schema
QFile schema_file(":/schema.sql");
schema_file.open(QIODevice::ReadOnly);
QString schema(QString::fromUtf8(schema_file.readAll()));
QStringList commands(schema.split(";\n\n"));
db.transaction();
foreach (const QString& command, commands) {
QSqlQuery query(db.exec(command));
if (CheckErrors(query.lastError()))
qFatal("Unable to create music library database");
}
db.commit();
UpdateDatabaseSchema(0, db);
}
// Get the database's schema version
@ -93,12 +82,21 @@ QSqlDatabase LibraryBackend::Connect() {
}
void LibraryBackend::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
QFile schema_file(QString(":/schema-%1.sql").arg(version));
schema_file.open(QIODevice::ReadOnly);
QString filename;
if (version == 0)
filename = ":/schema.sql";
else
filename = QString(":/schema-%1.sql").arg(version);
// Open and read the database schema
QFile schema_file(filename);
if (!schema_file.open(QIODevice::ReadOnly))
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
QString schema(QString::fromUtf8(schema_file.readAll()));
qDebug() << "Updating database schema to version" << version;
qDebug() << "Applying database schema version" << version;
// Run each command
QStringList commands(schema.split(";\n\n"));
db.transaction();
foreach (const QString& command, commands) {

View File

@ -82,18 +82,26 @@ void LibraryWatcher::ScanDirectory(const QString& path) {
qDebug() << "Scanning" << path;
emit ScanStarted();
QStringList valid_images = QStringList() << "jpg" << "png" << "gif" << "jpeg";
QStringList valid_playlists = QStringList() << "m3u" << "pls";
// Map from canonical directory name to list of possible filenames for cover
// art
QMap<QString, QStringList> album_art;
QStringList files_on_disk;
QDirIterator it(dir.path,
QDir::Files | QDir::NoDotAndDotDot | QDir::Readable,
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while (it.hasNext()) {
QString path(it.next());
QString ext(ExtensionPart(path));
QString dir(DirectoryPart(path));
// Don't bother if the engine can't decode it
if (!engine_->canDecode(QUrl::fromLocalFile(path)))
continue;
files_on_disk << path;
if (valid_images.contains(ext))
album_art[dir] << path;
else if (engine_->canDecode(QUrl::fromLocalFile(path)))
files_on_disk << path;
}
// Ask the database for a list of files in this directory
@ -107,14 +115,23 @@ void LibraryWatcher::ScanDirectory(const QString& path) {
if (FindSongByPath(songs_in_db, file, &matching_song)) {
// The song is in the database and still on disk.
// Check the mtime to see if it's been changed since it was added.
bool changed = matching_song.mtime() != QFileInfo(file).lastModified().toTime_t();
if (matching_song.mtime() != QFileInfo(file).lastModified().toTime_t()) {
// Also want to look to see whether the album art has changed
QString image = ImageForSong(file, album_art);
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) ||
(!matching_song.art_automatic().isEmpty() && !QFile::exists(matching_song.art_automatic()))) {
changed = true;
}
if (changed) {
qDebug() << file << "changed";
// It's changed - reread the metadata from the file
Song song_on_disk;
song_on_disk.InitFromFile(file, dir.id);
song_on_disk.set_id(matching_song.id());
song_on_disk.set_art_automatic(image);
if (!matching_song.IsMetadataEqual(song_on_disk)) {
qDebug() << file << "metadata changed";
@ -134,6 +151,9 @@ void LibraryWatcher::ScanDirectory(const QString& path) {
continue;
qDebug() << file << "created";
// Choose an image for the song
song.set_art_automatic(ImageForSong(file, album_art));
new_songs << song;
}
}
@ -186,3 +206,40 @@ void LibraryWatcher::RescanPathsNow() {
qDebug() << "Updating compilations...";
backend_.get()->UpdateCompilationsAsync();
}
QString LibraryWatcher::PickBestImage(const QStringList& images) {
// This is used when there is more than one image in a directory.
// Just pick the biggest image.
int biggest_size = 0;
QString biggest_path;
foreach (const QString& path, images) {
QImage image(path);
if (image.isNull())
continue;
int size = image.width() * image.height();
if (size > biggest_size) {
biggest_size = size;
biggest_path = path;
}
}
return biggest_path;
}
QString LibraryWatcher::ImageForSong(const QString& path, QMap<QString, QStringList>& album_art) {
QString dir(DirectoryPart(path));
if (album_art.contains(dir)) {
if (album_art[dir].count() == 1)
return album_art[dir][0];
else {
QString best_image = PickBestImage(album_art[dir]);
album_art[dir] = QStringList() << best_image;
return best_image;
}
}
return QString();
}

View File

@ -44,6 +44,10 @@ class LibraryWatcher : public QObject {
private:
static bool FindSongByPath(const SongList& list, const QString& path, Song* out);
inline static QString ExtensionPart( const QString &fileName );
inline static QString DirectoryPart( const QString &fileName );
static QString PickBestImage(const QStringList& images);
static QString ImageForSong(const QString& path, QMap<QString, QStringList>& album_art);
private:
EngineBase* engine_;
@ -62,4 +66,12 @@ class LibraryWatcher : public QObject {
#endif
};
// Thanks Amarok
inline QString LibraryWatcher::ExtensionPart( const QString &fileName ) {
return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : "";
}
inline QString LibraryWatcher::DirectoryPart( const QString &fileName ) {
return fileName.section( '/', 0, -2 );
}
#endif // LIBRARYWATCHER_H

View File

@ -44,7 +44,7 @@ void OSD::SongChanged(const Song &song) {
if (song.track() > 0)
message_parts << QString("track %1").arg(song.track());
ShowMessage(summary, message_parts.join(", "), "notification-audio-play", song.image());
ShowMessage(summary, message_parts.join(", "), "notification-audio-play", song.GetBestImage());
}
void OSD::Paused() {

View File

@ -26,13 +26,13 @@ const char* Song::kColumnSpec =
"title, album, artist, albumartist, composer, "
"track, disc, bpm, year, genre, comment, compilation, "
"length, bitrate, samplerate, directory, filename, "
"mtime, ctime, filesize, sampler";
"mtime, ctime, filesize, sampler, art_automatic, art_manual";
const char* Song::kBindSpec =
":title, :album, :artist, :albumartist, :composer, "
":track, :disc, :bpm, :year, :genre, :comment, :compilation, "
":length, :bitrate, :samplerate, :directory_id, :filename, "
":mtime, :ctime, :filesize, :sampler";
":mtime, :ctime, :filesize, :sampler, :art_automatic, :art_manual";
const char* Song::kUpdateSpec =
"title = :title, album = :album, artist = :artist, "
@ -41,7 +41,8 @@ const char* Song::kUpdateSpec =
"comment = :comment, compilation = :compilation, length = :length, "
"bitrate = :bitrate, samplerate = :samplerate, "
"directory = :directory_id, filename = :filename, mtime = :mtime, "
"ctime = :ctime, filesize = :filesize, sampler = :sampler";
"ctime = :ctime, filesize = :filesize, sampler = :sampler, "
"art_automatic = :art_automatic, art_manual = :art_manual";
SongData::SongData()
: valid_(false),
@ -210,6 +211,9 @@ void Song::InitFromQuery(const QSqlQuery& q) {
d->sampler_ = q.value(21).toBool();
d->art_automatic_ = q.value(22).toString();
d->art_manual_ = q.value(23).toString();
#undef tostr
#undef toint
#undef tofloat
@ -243,6 +247,8 @@ void Song::InitFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle) {
void Song::BindToQuery(QSqlQuery *query) const {
#define intval(x) (x == -1 ? QVariant() : x)
// Remember to bind these in the same order as kBindSpec
query->bindValue(":title", d->title_);
query->bindValue(":album", d->album_);
query->bindValue(":artist", d->artist_);
@ -267,6 +273,8 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":filesize", intval(d->filesize_));
query->bindValue(":sampler", d->sampler_ ? 1 : 0);
query->bindValue(":art_automatic", d->art_automatic_);
query->bindValue(":art_manual", d->art_manual_);
#undef intval
}
@ -325,7 +333,10 @@ bool Song::IsMetadataEqual(const Song& other) const {
d->compilation_ == other.d->compilation_ &&
d->length_ == other.d->length_ &&
d->bitrate_ == other.d->bitrate_ &&
d->samplerate_ == other.d->samplerate_;
d->samplerate_ == other.d->samplerate_ &&
d->sampler_ == other.d->sampler_ &&
d->art_automatic_ == other.d->art_automatic_ &&
d->art_manual_ == other.d->art_manual_;
}
bool Song::Save() const {
@ -356,3 +367,16 @@ bool Song::Save() const {
return ret;
}
QImage Song::GetBestImage() const {
if (!d->image_.isNull())
return d->image_;
if (!d->art_manual_.isEmpty())
return QImage(d->art_manual_);
if (!d->art_automatic_.isEmpty())
return QImage(d->art_automatic_);
return QImage();
}

View File

@ -44,6 +44,10 @@ struct SongData : public QSharedData {
int ctime_;
int filesize_;
// Filenames to album art for this song.
QString art_automatic_; // Guessed by LibraryWatcher
QString art_manual_; // Set by the user - should take priority
QImage image_;
};
@ -70,17 +74,17 @@ class Song {
bool is_valid() const { return d->valid_; }
int id() const { return d->id_; }
QString title() const { return d->title_; }
QString album() const { return d->album_; }
QString artist() const { return d->artist_; }
QString albumartist() const { return d->albumartist_; }
QString composer() const { return d->composer_; }
const QString& title() const { return d->title_; }
const QString& album() const { return d->album_; }
const QString& artist() const { return d->artist_; }
const QString& albumartist() const { return d->albumartist_; }
const QString& composer() const { return d->composer_; }
int track() const { return d->track_; }
int disc() const { return d->disc_; }
float bpm() const { return d->bpm_; }
int year() const { return d->year_; }
QString genre() const { return d->genre_; }
QString comment() const { return d->comment_; }
const QString& genre() const { return d->genre_; }
const QString& comment() const { return d->comment_; }
bool is_compilation() const { return d->compilation_ || d->sampler_; }
int length() const { return d->length_; }
@ -88,11 +92,14 @@ class Song {
int samplerate() const { return d->samplerate_; }
int directory_id() const { return d->directory_id_; }
QString filename() const { return d->filename_; }
const QString& filename() const { return d->filename_; }
uint mtime() const { return d->mtime_; }
uint ctime() const { return d->ctime_; }
int filesize() const { return d->filesize_; }
const QString& art_automatic() const { return d->art_automatic_; }
const QString& art_manual() const { return d->art_manual_; }
const QImage& image() const { return d->image_; }
// Pretty accessors
@ -100,6 +107,12 @@ class Song {
QString PrettyTitleWithArtist() const;
QString PrettyLength() const;
// Loads and returns some album art for the song. Tries, in this order:
// 1) An image set explicitly with set_image (eg. last.fm radio)
// 2) An image set by the user with set_art_manual
// 3) An image found by the library scanner
QImage GetBestImage() const;
// Setters
bool IsEditable() const { return d->valid_ && !d->filename_.isNull(); }
bool Save() const;
@ -125,6 +138,8 @@ class Song {
void set_mtime(int v) { d->mtime_ = v; }
void set_ctime(int v) { d->ctime_ = v; }
void set_filesize(int v) { d->filesize_ = v; }
void set_art_automatic(const QString& v) { d->art_automatic_ = v; }
void set_art_manual(const QString& v) { d->art_manual_ = v; }
void set_image(const QImage& i) { d->image_ = i; }
// Comparison functions

View File

@ -132,7 +132,8 @@ RESOURCES += ../data/data.qrc \
translations.qrc
OTHER_FILES += ../data/schema.sql \
../data/mainwindow.css \
../data/schema-1.sql
../data/schema-1.sql \
../data/schema-2.sql
RC_FILE += ../dist/windres.rc
TRANSLATIONS = clementine_ru.ts \
clementine_es.ts \