Load album cover art from files on disk
This commit is contained in:
parent
7de70bbaea
commit
1a26380e3f
@ -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
5
data/schema-2.sql
Normal 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;
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
32
src/song.cpp
32
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();
|
||||
}
|
||||
|
31
src/song.h
31
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
|
||||
|
@ -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 \
|
||||
|
Loading…
x
Reference in New Issue
Block a user