From 1a26380e3f5c85ca51ded9f93b524a6af3650634 Mon Sep 17 00:00:00 2001 From: David Sansome Date: Sun, 28 Feb 2010 00:35:20 +0000 Subject: [PATCH] Load album cover art from files on disk --- data/data.qrc | 1 + data/schema-2.sql | 5 +++ src/librarybackend.cpp | 30 +++++++++--------- src/librarywatcher.cpp | 69 ++++++++++++++++++++++++++++++++++++++---- src/librarywatcher.h | 12 ++++++++ src/osd.cpp | 2 +- src/song.cpp | 32 +++++++++++++++++--- src/song.h | 31 ++++++++++++++----- src/src.pro | 3 +- 9 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 data/schema-2.sql diff --git a/data/data.qrc b/data/data.qrc index b661d19c7..78ffd3bf1 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -60,5 +60,6 @@ open_media.png open_stream.png schema-1.sql + schema-2.sql diff --git a/data/schema-2.sql b/data/schema-2.sql new file mode 100644 index 000000000..b342970d9 --- /dev/null +++ b/data/schema-2.sql @@ -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; diff --git a/src/librarybackend.cpp b/src/librarybackend.cpp index e8757d35e..0d88b3a2b 100644 --- a/src/librarybackend.cpp +++ b/src/librarybackend.cpp @@ -12,7 +12,7 @@ #include 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) { diff --git a/src/librarywatcher.cpp b/src/librarywatcher.cpp index 3d87e99ad..d27d297fc 100644 --- a/src/librarywatcher.cpp +++ b/src/librarywatcher.cpp @@ -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 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& 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(); +} diff --git a/src/librarywatcher.h b/src/librarywatcher.h index ca5e8dfb2..f90bcd854 100644 --- a/src/librarywatcher.h +++ b/src/librarywatcher.h @@ -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& 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 diff --git a/src/osd.cpp b/src/osd.cpp index 3787258eb..7e23c0c39 100644 --- a/src/osd.cpp +++ b/src/osd.cpp @@ -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() { diff --git a/src/song.cpp b/src/song.cpp index 0a6230f78..18bef06b1 100644 --- a/src/song.cpp +++ b/src/song.cpp @@ -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(); +} diff --git a/src/song.h b/src/song.h index d8a3571d2..2e6f00786 100644 --- a/src/song.h +++ b/src/song.h @@ -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 diff --git a/src/src.pro b/src/src.pro index e1bf58ec3..010447c02 100644 --- a/src/src.pro +++ b/src/src.pro @@ -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 \