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_media.png</file>
|
||||||
<file>open_stream.png</file>
|
<file>open_stream.png</file>
|
||||||
<file>schema-1.sql</file>
|
<file>schema-1.sql</file>
|
||||||
|
<file>schema-2.sql</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</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>
|
#include <QThread>
|
||||||
|
|
||||||
const char* LibraryBackend::kDatabaseName = "clementine.db";
|
const char* LibraryBackend::kDatabaseName = "clementine.db";
|
||||||
const int LibraryBackend::kSchemaVersion = 1;
|
const int LibraryBackend::kSchemaVersion = 2;
|
||||||
|
|
||||||
LibraryBackend::LibraryBackend(QObject* parent)
|
LibraryBackend::LibraryBackend(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@ -57,18 +57,7 @@ QSqlDatabase LibraryBackend::Connect() {
|
|||||||
|
|
||||||
if (db.tables().count() == 0) {
|
if (db.tables().count() == 0) {
|
||||||
// Set up initial schema
|
// Set up initial schema
|
||||||
QFile schema_file(":/schema.sql");
|
UpdateDatabaseSchema(0, db);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the database's schema version
|
// Get the database's schema version
|
||||||
@ -93,12 +82,21 @@ QSqlDatabase LibraryBackend::Connect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LibraryBackend::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
|
void LibraryBackend::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
|
||||||
QFile schema_file(QString(":/schema-%1.sql").arg(version));
|
QString filename;
|
||||||
schema_file.open(QIODevice::ReadOnly);
|
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()));
|
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"));
|
QStringList commands(schema.split(";\n\n"));
|
||||||
db.transaction();
|
db.transaction();
|
||||||
foreach (const QString& command, commands) {
|
foreach (const QString& command, commands) {
|
||||||
|
@ -82,17 +82,25 @@ void LibraryWatcher::ScanDirectory(const QString& path) {
|
|||||||
qDebug() << "Scanning" << path;
|
qDebug() << "Scanning" << path;
|
||||||
emit ScanStarted();
|
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;
|
QStringList files_on_disk;
|
||||||
QDirIterator it(dir.path,
|
QDirIterator it(dir.path,
|
||||||
QDir::Files | QDir::NoDotAndDotDot | QDir::Readable,
|
QDir::Files | QDir::NoDotAndDotDot | QDir::Readable,
|
||||||
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
QString path(it.next());
|
QString path(it.next());
|
||||||
|
QString ext(ExtensionPart(path));
|
||||||
|
QString dir(DirectoryPart(path));
|
||||||
|
|
||||||
// Don't bother if the engine can't decode it
|
if (valid_images.contains(ext))
|
||||||
if (!engine_->canDecode(QUrl::fromLocalFile(path)))
|
album_art[dir] << path;
|
||||||
continue;
|
else if (engine_->canDecode(QUrl::fromLocalFile(path)))
|
||||||
|
|
||||||
files_on_disk << path;
|
files_on_disk << path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,14 +115,23 @@ void LibraryWatcher::ScanDirectory(const QString& path) {
|
|||||||
if (FindSongByPath(songs_in_db, file, &matching_song)) {
|
if (FindSongByPath(songs_in_db, file, &matching_song)) {
|
||||||
// The song is in the database and still on disk.
|
// The song is in the database and still on disk.
|
||||||
// Check the mtime to see if it's been changed since it was added.
|
// 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";
|
qDebug() << file << "changed";
|
||||||
|
|
||||||
// It's changed - reread the metadata from the file
|
// It's changed - reread the metadata from the file
|
||||||
Song song_on_disk;
|
Song song_on_disk;
|
||||||
song_on_disk.InitFromFile(file, dir.id);
|
song_on_disk.InitFromFile(file, dir.id);
|
||||||
song_on_disk.set_id(matching_song.id());
|
song_on_disk.set_id(matching_song.id());
|
||||||
|
song_on_disk.set_art_automatic(image);
|
||||||
|
|
||||||
if (!matching_song.IsMetadataEqual(song_on_disk)) {
|
if (!matching_song.IsMetadataEqual(song_on_disk)) {
|
||||||
qDebug() << file << "metadata changed";
|
qDebug() << file << "metadata changed";
|
||||||
@ -134,6 +151,9 @@ void LibraryWatcher::ScanDirectory(const QString& path) {
|
|||||||
continue;
|
continue;
|
||||||
qDebug() << file << "created";
|
qDebug() << file << "created";
|
||||||
|
|
||||||
|
// Choose an image for the song
|
||||||
|
song.set_art_automatic(ImageForSong(file, album_art));
|
||||||
|
|
||||||
new_songs << song;
|
new_songs << song;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,3 +206,40 @@ void LibraryWatcher::RescanPathsNow() {
|
|||||||
qDebug() << "Updating compilations...";
|
qDebug() << "Updating compilations...";
|
||||||
backend_.get()->UpdateCompilationsAsync();
|
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:
|
private:
|
||||||
static bool FindSongByPath(const SongList& list, const QString& path, Song* out);
|
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:
|
private:
|
||||||
EngineBase* engine_;
|
EngineBase* engine_;
|
||||||
@ -62,4 +66,12 @@ class LibraryWatcher : public QObject {
|
|||||||
#endif
|
#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
|
#endif // LIBRARYWATCHER_H
|
||||||
|
@ -44,7 +44,7 @@ void OSD::SongChanged(const Song &song) {
|
|||||||
if (song.track() > 0)
|
if (song.track() > 0)
|
||||||
message_parts << QString("track %1").arg(song.track());
|
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() {
|
void OSD::Paused() {
|
||||||
|
32
src/song.cpp
32
src/song.cpp
@ -26,13 +26,13 @@ const char* Song::kColumnSpec =
|
|||||||
"title, album, artist, albumartist, composer, "
|
"title, album, artist, albumartist, composer, "
|
||||||
"track, disc, bpm, year, genre, comment, compilation, "
|
"track, disc, bpm, year, genre, comment, compilation, "
|
||||||
"length, bitrate, samplerate, directory, filename, "
|
"length, bitrate, samplerate, directory, filename, "
|
||||||
"mtime, ctime, filesize, sampler";
|
"mtime, ctime, filesize, sampler, art_automatic, art_manual";
|
||||||
|
|
||||||
const char* Song::kBindSpec =
|
const char* Song::kBindSpec =
|
||||||
":title, :album, :artist, :albumartist, :composer, "
|
":title, :album, :artist, :albumartist, :composer, "
|
||||||
":track, :disc, :bpm, :year, :genre, :comment, :compilation, "
|
":track, :disc, :bpm, :year, :genre, :comment, :compilation, "
|
||||||
":length, :bitrate, :samplerate, :directory_id, :filename, "
|
":length, :bitrate, :samplerate, :directory_id, :filename, "
|
||||||
":mtime, :ctime, :filesize, :sampler";
|
":mtime, :ctime, :filesize, :sampler, :art_automatic, :art_manual";
|
||||||
|
|
||||||
const char* Song::kUpdateSpec =
|
const char* Song::kUpdateSpec =
|
||||||
"title = :title, album = :album, artist = :artist, "
|
"title = :title, album = :album, artist = :artist, "
|
||||||
@ -41,7 +41,8 @@ const char* Song::kUpdateSpec =
|
|||||||
"comment = :comment, compilation = :compilation, length = :length, "
|
"comment = :comment, compilation = :compilation, length = :length, "
|
||||||
"bitrate = :bitrate, samplerate = :samplerate, "
|
"bitrate = :bitrate, samplerate = :samplerate, "
|
||||||
"directory = :directory_id, filename = :filename, mtime = :mtime, "
|
"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()
|
SongData::SongData()
|
||||||
: valid_(false),
|
: valid_(false),
|
||||||
@ -210,6 +211,9 @@ void Song::InitFromQuery(const QSqlQuery& q) {
|
|||||||
|
|
||||||
d->sampler_ = q.value(21).toBool();
|
d->sampler_ = q.value(21).toBool();
|
||||||
|
|
||||||
|
d->art_automatic_ = q.value(22).toString();
|
||||||
|
d->art_manual_ = q.value(23).toString();
|
||||||
|
|
||||||
#undef tostr
|
#undef tostr
|
||||||
#undef toint
|
#undef toint
|
||||||
#undef tofloat
|
#undef tofloat
|
||||||
@ -243,6 +247,8 @@ void Song::InitFromSimpleMetaBundle(const Engine::SimpleMetaBundle &bundle) {
|
|||||||
void Song::BindToQuery(QSqlQuery *query) const {
|
void Song::BindToQuery(QSqlQuery *query) const {
|
||||||
#define intval(x) (x == -1 ? QVariant() : x)
|
#define intval(x) (x == -1 ? QVariant() : x)
|
||||||
|
|
||||||
|
// Remember to bind these in the same order as kBindSpec
|
||||||
|
|
||||||
query->bindValue(":title", d->title_);
|
query->bindValue(":title", d->title_);
|
||||||
query->bindValue(":album", d->album_);
|
query->bindValue(":album", d->album_);
|
||||||
query->bindValue(":artist", d->artist_);
|
query->bindValue(":artist", d->artist_);
|
||||||
@ -267,6 +273,8 @@ void Song::BindToQuery(QSqlQuery *query) const {
|
|||||||
query->bindValue(":filesize", intval(d->filesize_));
|
query->bindValue(":filesize", intval(d->filesize_));
|
||||||
|
|
||||||
query->bindValue(":sampler", d->sampler_ ? 1 : 0);
|
query->bindValue(":sampler", d->sampler_ ? 1 : 0);
|
||||||
|
query->bindValue(":art_automatic", d->art_automatic_);
|
||||||
|
query->bindValue(":art_manual", d->art_manual_);
|
||||||
|
|
||||||
#undef intval
|
#undef intval
|
||||||
}
|
}
|
||||||
@ -325,7 +333,10 @@ bool Song::IsMetadataEqual(const Song& other) const {
|
|||||||
d->compilation_ == other.d->compilation_ &&
|
d->compilation_ == other.d->compilation_ &&
|
||||||
d->length_ == other.d->length_ &&
|
d->length_ == other.d->length_ &&
|
||||||
d->bitrate_ == other.d->bitrate_ &&
|
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 {
|
bool Song::Save() const {
|
||||||
@ -356,3 +367,16 @@ bool Song::Save() const {
|
|||||||
|
|
||||||
return ret;
|
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 ctime_;
|
||||||
int filesize_;
|
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_;
|
QImage image_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -70,17 +74,17 @@ class Song {
|
|||||||
bool is_valid() const { return d->valid_; }
|
bool is_valid() const { return d->valid_; }
|
||||||
int id() const { return d->id_; }
|
int id() const { return d->id_; }
|
||||||
|
|
||||||
QString title() const { return d->title_; }
|
const QString& title() const { return d->title_; }
|
||||||
QString album() const { return d->album_; }
|
const QString& album() const { return d->album_; }
|
||||||
QString artist() const { return d->artist_; }
|
const QString& artist() const { return d->artist_; }
|
||||||
QString albumartist() const { return d->albumartist_; }
|
const QString& albumartist() const { return d->albumartist_; }
|
||||||
QString composer() const { return d->composer_; }
|
const QString& composer() const { return d->composer_; }
|
||||||
int track() const { return d->track_; }
|
int track() const { return d->track_; }
|
||||||
int disc() const { return d->disc_; }
|
int disc() const { return d->disc_; }
|
||||||
float bpm() const { return d->bpm_; }
|
float bpm() const { return d->bpm_; }
|
||||||
int year() const { return d->year_; }
|
int year() const { return d->year_; }
|
||||||
QString genre() const { return d->genre_; }
|
const QString& genre() const { return d->genre_; }
|
||||||
QString comment() const { return d->comment_; }
|
const QString& comment() const { return d->comment_; }
|
||||||
bool is_compilation() const { return d->compilation_ || d->sampler_; }
|
bool is_compilation() const { return d->compilation_ || d->sampler_; }
|
||||||
|
|
||||||
int length() const { return d->length_; }
|
int length() const { return d->length_; }
|
||||||
@ -88,11 +92,14 @@ class Song {
|
|||||||
int samplerate() const { return d->samplerate_; }
|
int samplerate() const { return d->samplerate_; }
|
||||||
|
|
||||||
int directory_id() const { return d->directory_id_; }
|
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 mtime() const { return d->mtime_; }
|
||||||
uint ctime() const { return d->ctime_; }
|
uint ctime() const { return d->ctime_; }
|
||||||
int filesize() const { return d->filesize_; }
|
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_; }
|
const QImage& image() const { return d->image_; }
|
||||||
|
|
||||||
// Pretty accessors
|
// Pretty accessors
|
||||||
@ -100,6 +107,12 @@ class Song {
|
|||||||
QString PrettyTitleWithArtist() const;
|
QString PrettyTitleWithArtist() const;
|
||||||
QString PrettyLength() 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
|
// Setters
|
||||||
bool IsEditable() const { return d->valid_ && !d->filename_.isNull(); }
|
bool IsEditable() const { return d->valid_ && !d->filename_.isNull(); }
|
||||||
bool Save() const;
|
bool Save() const;
|
||||||
@ -125,6 +138,8 @@ class Song {
|
|||||||
void set_mtime(int v) { d->mtime_ = v; }
|
void set_mtime(int v) { d->mtime_ = v; }
|
||||||
void set_ctime(int v) { d->ctime_ = v; }
|
void set_ctime(int v) { d->ctime_ = v; }
|
||||||
void set_filesize(int v) { d->filesize_ = 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; }
|
void set_image(const QImage& i) { d->image_ = i; }
|
||||||
|
|
||||||
// Comparison functions
|
// Comparison functions
|
||||||
|
@ -132,7 +132,8 @@ RESOURCES += ../data/data.qrc \
|
|||||||
translations.qrc
|
translations.qrc
|
||||||
OTHER_FILES += ../data/schema.sql \
|
OTHER_FILES += ../data/schema.sql \
|
||||||
../data/mainwindow.css \
|
../data/mainwindow.css \
|
||||||
../data/schema-1.sql
|
../data/schema-1.sql \
|
||||||
|
../data/schema-2.sql
|
||||||
RC_FILE += ../dist/windres.rc
|
RC_FILE += ../dist/windres.rc
|
||||||
TRANSLATIONS = clementine_ru.ts \
|
TRANSLATIONS = clementine_ru.ts \
|
||||||
clementine_es.ts \
|
clementine_es.ts \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user