diff --git a/CMakeLists.txt b/CMakeLists.txt
index e38001e4..787034e5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -123,7 +123,7 @@ pkg_check_modules(GSTREAMER_PBUTILS gstreamer-pbutils-1.0)
pkg_check_modules(LIBVLC libvlc)
pkg_check_modules(SQLITE REQUIRED sqlite3>=3.9)
pkg_check_modules(LIBPULSE libpulse)
-pkg_check_modules(CHROMAPRINT libchromaprint)
+pkg_check_modules(CHROMAPRINT libchromaprint>=1.4)
pkg_check_modules(LIBGPOD libgpod-1.0>=0.7.92)
pkg_check_modules(LIBMTP libmtp>=1.0)
pkg_check_modules(GDK_PIXBUF gdk-pixbuf-2.0)
@@ -296,6 +296,11 @@ optional_component(VLC ON "Engine: VLC backend"
DEPENDS "libvlc" LIBVLC_FOUND
)
+optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
+ DEPENDS "chromaprint" CHROMAPRINT_FOUND
+ DEPENDS "gstreamer" GSTREAMER_FOUND
+)
+
optional_component(MUSICBRAINZ ON "MusicBrainz integration"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
diff --git a/data/data.qrc b/data/data.qrc
index e1ba7a53..3027a9f3 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -14,6 +14,7 @@
schema/schema-11.sql
schema/schema-12.sql
schema/schema-13.sql
+ schema/schema-14.sql
schema/device-schema.sql
style/strawberry.css
style/smartplaylistsearchterm.css
diff --git a/data/schema/device-schema.sql b/data/schema/device-schema.sql
index fbcd438a..69edd863 100644
--- a/data/schema/device-schema.sql
+++ b/data/schema/device-schema.sql
@@ -47,9 +47,12 @@ CREATE TABLE device_%deviceid_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
diff --git a/data/schema/schema-14.sql b/data/schema/schema-14.sql
new file mode 100644
index 00000000..a9afd055
--- /dev/null
+++ b/data/schema/schema-14.sql
@@ -0,0 +1,5 @@
+ALTER TABLE %allsongstables ADD COLUMN fingerprint TEXT;
+
+ALTER TABLE %allsongstables ADD COLUMN lastseen INTEGER NOT NULL DEFAULT -1;
+
+UPDATE schema_version SET version=14;
diff --git a/data/schema/schema.sql b/data/schema/schema.sql
index 92dc3a4a..eaa88349 100644
--- a/data/schema/schema.sql
+++ b/data/schema/schema.sql
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
-INSERT INTO schema_version (version) VALUES (13);
+INSERT INTO schema_version (version) VALUES (14);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -55,9 +55,12 @@ CREATE TABLE IF NOT EXISTS songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -114,9 +117,12 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -173,9 +179,12 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -232,9 +241,12 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -291,9 +303,12 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -350,9 +365,12 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -409,9 +427,12 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -468,9 +489,12 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
+ lastseen INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
@@ -547,9 +571,12 @@ CREATE TABLE IF NOT EXISTS playlist_items (
ctime INTEGER,
unavailable INTEGER DEFAULT 0,
+ fingerprint TEXT,
+
playcount INTEGER DEFAULT 0,
skipcount INTEGER DEFAULT 0,
- lastplayed INTEGER DEFAULT 0,
+ lastplayed INTEGER DEFAULT -1,
+ lastseen INTEGER DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER DEFAULT 0,
diff --git a/ext/libstrawberry-tagreader/tagreader.cpp b/ext/libstrawberry-tagreader/tagreader.cpp
index 80d2741e..5ddfcaeb 100644
--- a/ext/libstrawberry-tagreader/tagreader.cpp
+++ b/ext/libstrawberry-tagreader/tagreader.cpp
@@ -189,6 +189,7 @@ void TagReader::ReadFile(const QString &filename, spb::tagreader::SongMetadata *
#else
song->set_ctime(info.created().toSecsSinceEpoch());
#endif
+ song->set_lastseen(QDateTime::currentDateTime().toSecsSinceEpoch());
std::unique_ptr fileref(factory_->GetFileRef(filename));
if (fileref->isNull()) {
diff --git a/ext/libstrawberry-tagreader/tagreadermessages.proto b/ext/libstrawberry-tagreader/tagreadermessages.proto
index be098f77..2eca21a3 100644
--- a/ext/libstrawberry-tagreader/tagreadermessages.proto
+++ b/ext/libstrawberry-tagreader/tagreadermessages.proto
@@ -60,10 +60,11 @@ message SongMetadata {
optional int32 playcount = 27;
optional int32 skipcount = 28;
- optional int32 lastplayed = 29;
+ optional int64 lastplayed = 29;
+ optional int64 lastseen = 30;
- optional bool suspicious_tags = 30;
- optional string art_automatic = 31;
+ optional bool suspicious_tags = 31;
+ optional string art_automatic = 32;
}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 302d40ac..391885ee 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -214,9 +214,6 @@ set(SOURCES
osd/osdbase.cpp
osd/osdpretty.cpp
- musicbrainz/acoustidclient.cpp
- musicbrainz/musicbrainzclient.cpp
-
internet/internetservices.cpp
internet/internetservice.cpp
internet/internetplaylistitem.cpp
@@ -429,9 +426,6 @@ set(HEADERS
osd/osdbase.h
osd/osdpretty.h
- musicbrainz/acoustidclient.h
- musicbrainz/musicbrainzclient.h
-
internet/internetservices.h
internet/internetservice.h
internet/internetsongmimedata.h
@@ -831,7 +825,7 @@ optional_source(HAVE_LIBPULSE
engine/pulsedevicefinder.cpp
)
-# MusicBrainz and transcoder require GStreamer
+# Transcoder require GStreamer
optional_source(HAVE_GSTREAMER
SOURCES
transcoder/transcoder.cpp
@@ -867,12 +861,18 @@ UI
settings/transcodersettingspage.ui
)
+# CHROMAPRINT
+optional_source(CHROMAPRINT_FOUND SOURCES engine/chromaprinter.cpp)
+
# MusicBrainz
optional_source(HAVE_MUSICBRAINZ
SOURCES
- musicbrainz/chromaprinter.cpp
+ musicbrainz/acoustidclient.cpp
+ musicbrainz/musicbrainzclient.cpp
musicbrainz/tagfetcher.cpp
HEADERS
+ musicbrainz/acoustidclient.h
+ musicbrainz/musicbrainzclient.h
musicbrainz/tagfetcher.h
)
@@ -1093,9 +1093,9 @@ if(HAVE_VLC)
link_directories(${LIBVLC_LIBRARY_DIRS})
endif()
-if(HAVE_MUSICBRAINZ)
+if(CHROMAPRINT_FOUND)
link_directories(${CHROMAPRINT_LIBRARY_DIRS})
-endif(HAVE_MUSICBRAINZ)
+endif(CHROMAPRINT_FOUND)
if(X11_FOUND)
link_directories(${X11_LIBRARY_DIRS})
@@ -1211,10 +1211,10 @@ if(HAVE_VLC)
target_link_libraries(strawberry_lib PRIVATE ${LIBVLC_LIBRARIES})
endif()
-if(HAVE_MUSICBRAINZ)
+if(CHROMAPRINT_FOUND)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${CHROMAPRINT_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${CHROMAPRINT_LIBRARIES})
-endif(HAVE_MUSICBRAINZ)
+endif(CHROMAPRINT_FOUND)
if(X11_FOUND)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${X11_INCLUDE_DIR})
diff --git a/src/collection/collection.cpp b/src/collection/collection.cpp
index 300a9e9f..f0fd9c29 100644
--- a/src/collection/collection.cpp
+++ b/src/collection/collection.cpp
@@ -107,6 +107,7 @@ void SCollection::Init() {
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, backend_, &CollectionBackend::AddOrUpdateSubdirs);
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, backend_, &CollectionBackend::CompilationsNeedUpdating);
+ QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, backend_, &CollectionBackend::UpdateLastSeen);
QObject::connect(app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, backend_, &CollectionBackend::UpdateLastPlayed);
QObject::connect(app_->lastfm_import(), &LastFMImport::UpdatePlayCount, backend_, &CollectionBackend::UpdatePlayCount);
diff --git a/src/collection/collectionbackend.cpp b/src/collection/collectionbackend.cpp
index e7da72b2..eb82fadf 100644
--- a/src/collection/collectionbackend.cpp
+++ b/src/collection/collectionbackend.cpp
@@ -348,6 +348,27 @@ SongList CollectionBackend::FindSongsInDirectory(const int id) {
}
+SongList CollectionBackend::SongsWithMissingFingerprint(const int id) {
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QSqlQuery q(db);
+ q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE directory_id = :directory_id AND unavailable = 0 AND fingerprint = ''").arg(songs_table_));
+ q.bindValue(":directory_id", id);
+ q.exec();
+ if (db_->CheckErrors(q)) return SongList();
+
+ SongList ret;
+ while (q.next()) {
+ Song song(source_);
+ song.InitFromQuery(q, true);
+ ret << song;
+ }
+ return ret;
+
+}
+
void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_file) {
// Take a song and update its path
@@ -914,6 +935,30 @@ SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids, QSqlDa
}
+SongList CollectionBackend::GetSongsByFingerprint(const QString &fingerprint) {
+
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QSqlQuery q(db);
+ q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE fingerprint = :fingerprint").arg(songs_table_));
+ q.bindValue(":fingerprint", fingerprint);
+ q.exec();
+
+ if (db_->CheckErrors(q)) return SongList();
+
+ SongList songs;
+ while (q.next()) {
+ Song song(source_);
+ song.InitFromQuery(q, true);
+ songs << song;
+ }
+
+ return songs;
+
+}
+
+
CollectionBackend::AlbumList CollectionBackend::GetCompilationAlbums(const QueryOptions &opt) {
return GetAlbums(QString(), true, opt);
}
@@ -1546,3 +1591,44 @@ void CollectionBackend::UpdateSongRatingAsync(const int id, const double rating)
void CollectionBackend::UpdateSongsRatingAsync(const QList& ids, const double rating) {
metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList, ids), Q_ARG(double, rating));
}
+
+void CollectionBackend::UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days) {
+
+ {
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+
+ QSqlQuery q(db);
+ q.prepare(QString("UPDATE %1 SET lastseen = :lastseen WHERE directory_id = :directory_id AND unavailable = 0").arg(songs_table_));
+ q.bindValue(":lastseen", QDateTime::currentDateTime().toSecsSinceEpoch());
+ q.bindValue(":directory_id", directory_id);
+ q.exec();
+ db_->CheckErrors(q);
+ }
+
+ if (expire_unavailable_songs_days > 0) ExpireSongs(directory_id, expire_unavailable_songs_days);
+
+}
+
+void CollectionBackend::ExpireSongs(const int directory_id, const int expire_unavailable_songs_days) {
+
+ SongList songs;
+ {
+ QMutexLocker l(db_->Mutex());
+ QSqlDatabase db(db_->Connect());
+ QSqlQuery q(db);
+ q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE directory_id = :directory_id AND unavailable = 1 AND lastseen > 0 AND lastseen < :time").arg(songs_table_));
+ q.bindValue(":directory_id", directory_id);
+ q.bindValue(":time", QDateTime::currentDateTime().toSecsSinceEpoch() - (expire_unavailable_songs_days * 86400));
+ q.exec();
+ if (db_->CheckErrors(q)) return;
+ while (q.next()) {
+ Song song(source_);
+ song.InitFromQuery(q, true);
+ songs << song;
+ }
+ }
+
+ if (!songs.isEmpty()) DeleteSongs(songs);
+
+}
diff --git a/src/collection/collectionbackend.h b/src/collection/collectionbackend.h
index 8316ef79..5a923caa 100644
--- a/src/collection/collectionbackend.h
+++ b/src/collection/collectionbackend.h
@@ -83,6 +83,7 @@ class CollectionBackendInterface : public QObject {
virtual void UpdateTotalAlbumCountAsync() = 0;
virtual SongList FindSongsInDirectory(const int id) = 0;
+ virtual SongList SongsWithMissingFingerprint(const int id) = 0;
virtual SubdirectoryList SubdirsInDirectory(const int id) = 0;
virtual DirectoryList GetAllDirectories() = 0;
virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0;
@@ -106,6 +107,8 @@ class CollectionBackendInterface : public QObject {
virtual Song GetSongById(const int id) = 0;
+ virtual SongList GetSongsByFingerprint(const QString &fingerprint) = 0;
+
// Returns all sections of a song with the given filename. If there's just one section the resulting list will have it's size equal to 1.
virtual SongList GetSongsByUrl(const QUrl &url, const bool unavailable = false) = 0;
// Returns a section of a song with the given filename and beginning. If the section is not present in collection, returns invalid song.
@@ -143,6 +146,7 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateTotalAlbumCountAsync() override;
SongList FindSongsInDirectory(const int id) override;
+ SongList SongsWithMissingFingerprint(const int id) override;
SubdirectoryList SubdirsInDirectory(const int id) override;
DirectoryList GetAllDirectories() override;
void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) override;
@@ -187,6 +191,8 @@ class CollectionBackend : public CollectionBackendInterface {
Song GetSongBySongId(const QString &song_id);
SongList GetSongsBySongId(const QStringList &song_ids);
+ SongList GetSongsByFingerprint(const QString &fingerprint) override;
+
SongList GetAllSongs();
SongList FindSongs(const SmartPlaylistSearch &search);
@@ -224,6 +230,9 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateSongRating(const int id, const double rating);
void UpdateSongsRating(const QList &id_list, const double rating);
+ void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
+ void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
+
signals:
void DirectoryDiscovered(Directory, SubdirectoryList);
void DirectoryDeleted(Directory);
diff --git a/src/collection/collectionwatcher.cpp b/src/collection/collectionwatcher.cpp
index ab3526f9..044156c2 100644
--- a/src/collection/collectionwatcher.cpp
+++ b/src/collection/collectionwatcher.cpp
@@ -47,13 +47,18 @@
#include "core/filesystemwatcherinterface.h"
#include "core/logging.h"
+#include "core/timeconstants.h"
#include "core/tagreaderclient.h"
#include "core/taskmanager.h"
+#include "core/imageutils.h"
#include "directory.h"
#include "collectionbackend.h"
#include "collectionwatcher.h"
#include "playlistparsers/cueparser.h"
#include "settings/collectionsettingspage.h"
+#ifdef HAVE_SONGFINGERPRINTING
+# include "engine/chromaprinter.h"
+#endif
// This is defined by one of the windows headers that is included by taglib.
#ifdef RemoveDirectory
@@ -73,25 +78,40 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
backend_(nullptr),
task_manager_(nullptr),
fs_watcher_(FileSystemWatcherInterface::Create(this)),
+ original_thread_(nullptr),
scan_on_startup_(true),
monitor_(true),
- mark_songs_unavailable_(false),
+ song_tracking_(true),
+ mark_songs_unavailable_(true),
+ expire_unavailable_songs_days_(60),
stop_requested_(false),
rescan_in_progress_(false),
rescan_timer_(new QTimer(this)),
+ periodic_scan_timer_(new QTimer(this)),
rescan_paused_(false),
total_watches_(0),
cue_parser_(new CueParser(backend_, this)),
- original_thread_(nullptr) {
+ last_scan_time_(0) {
original_thread_ = thread();
- rescan_timer_->setInterval(1000);
+ rescan_timer_->setInterval(2000);
rescan_timer_->setSingleShot(true);
+ periodic_scan_timer_->setInterval(86400 * kMsecPerSec);
+ periodic_scan_timer_->setSingleShot(false);
+
+ QStringList image_formats = ImageUtils::SupportedImageFormats();
+ for (const QString &format : image_formats) {
+ if (!sValidImages.contains(format)) {
+ sValidImages.append(format);
+ }
+ }
+
ReloadSettings();
QObject::connect(rescan_timer_, &QTimer::timeout, this, &CollectionWatcher::RescanPathsNow);
+ QObject::connect(periodic_scan_timer_, &QTimer::timeout, this, &CollectionWatcher::IncrementalScanCheck);
}
@@ -123,8 +143,10 @@ void CollectionWatcher::ReloadSettings() {
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
scan_on_startup_ = s.value("startup_scan", true).toBool();
monitor_ = s.value("monitor", true).toBool();
- mark_songs_unavailable_ = s.value("mark_songs_unavailable", false).toBool();
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
+ song_tracking_ = s.value("song_tracking", false).toBool();
+ mark_songs_unavailable_ = song_tracking_ ? true : s.value("mark_songs_unavailable", true).toBool();
+ expire_unavailable_songs_days_ = s.value("expire_unavailable_songs", 60).toInt();
s.endGroup();
best_image_filters_.clear();
@@ -147,6 +169,13 @@ void CollectionWatcher::ReloadSettings() {
}
}
+ if (mark_songs_unavailable_ && !periodic_scan_timer_->isActive()) {
+ periodic_scan_timer_->start();
+ }
+ else if (!mark_songs_unavailable_ && periodic_scan_timer_->isActive()) {
+ periodic_scan_timer_->stop();
+ }
+
}
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
@@ -156,8 +185,10 @@ CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher,
incremental_(incremental),
ignores_mtime_(ignores_mtime),
mark_songs_unavailable_(mark_songs_unavailable),
+ expire_unavailable_songs_days_(60),
watcher_(watcher),
cached_songs_dirty_(true),
+ cached_songs_missing_fingerprint_dirty_(true),
known_subdirs_dirty_(true) {
QString description;
@@ -185,14 +216,14 @@ CollectionWatcher::ScanTransaction::~ScanTransaction() {
}
-void CollectionWatcher::ScanTransaction::AddToProgress(int n) {
+void CollectionWatcher::ScanTransaction::AddToProgress(const quint64 n) {
progress_ += n;
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
}
-void CollectionWatcher::ScanTransaction::AddToProgressMax(int n) {
+void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
progress_max_ += n;
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
@@ -201,16 +232,6 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(int n) {
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
- if (!new_songs.isEmpty()) {
- emit watcher_->NewOrUpdatedSongs(new_songs);
- new_songs.clear();
- }
-
- if (!touched_songs.isEmpty()) {
- emit watcher_->SongsMTimeUpdated(touched_songs);
- touched_songs.clear();
- }
-
if (!deleted_songs.isEmpty()) {
if (mark_songs_unavailable_) {
emit watcher_->SongsUnavailable(deleted_songs);
@@ -221,6 +242,16 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
deleted_songs.clear();
}
+ if (!new_songs.isEmpty()) {
+ emit watcher_->NewOrUpdatedSongs(new_songs);
+ new_songs.clear();
+ }
+
+ if (!touched_songs.isEmpty()) {
+ emit watcher_->SongsMTimeUpdated(touched_songs);
+ touched_songs.clear();
+ }
+
if (!readded_songs.isEmpty()) {
emit watcher_->SongsReadded(readded_songs);
readded_songs.clear();
@@ -252,22 +283,43 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
}
new_subdirs.clear();
+ if (incremental_ || ignores_mtime_) {
+ emit watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
+ }
+
}
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
if (cached_songs_dirty_) {
- cached_songs_ = watcher_->backend_->FindSongsInDirectory(dir_);
+ const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
+ for (const Song &song : songs) {
+ const QString p = song.url().toLocalFile().section('/', 0, -2);
+ cached_songs_.insert(p, song);
+ }
cached_songs_dirty_ = false;
}
- // TODO: Make this faster
- SongList ret;
- for (const Song &song : cached_songs_) {
- if (song.url().toLocalFile().section('/', 0, -2) == path) ret << song;
+ if (cached_songs_.contains(path)) {
+ return cached_songs_.values(path);
}
- return ret;
+ else return SongList();
+
+}
+
+bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
+
+ if (cached_songs_missing_fingerprint_dirty_) {
+ const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
+ for (const Song &song : songs) {
+ const QString p = song.url().toLocalFile().section('/', 0, -2);
+ cached_songs_missing_fingerprint_.insert(p, song);
+ }
+ cached_songs_missing_fingerprint_dirty_ = false;
+ }
+
+ return cached_songs_missing_fingerprint_.contains(path);
}
@@ -292,8 +344,9 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
- if (known_subdirs_dirty_)
+ if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
+ }
SubdirectoryList ret;
for (const Subdirectory &subdir : known_subdirs_) {
@@ -308,9 +361,12 @@ SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const Q
SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
- if (known_subdirs_dirty_)
+ if (known_subdirs_dirty_) {
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
+ }
+
return known_subdirs_;
+
}
void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryList &subdirs) {
@@ -320,29 +376,36 @@ void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryLis
if (subdirs.isEmpty()) {
// This is a new directory that we've never seen before. Scan it fully.
ScanTransaction transaction(this, dir.id, false, false, mark_songs_unavailable_);
+ const quint64 files_count = FilesCountForPath(&transaction, dir.path);
transaction.SetKnownSubdirs(subdirs);
- transaction.AddToProgressMax(1);
- ScanSubdirectory(dir.path, Subdirectory(), &transaction);
+ transaction.AddToProgressMax(files_count);
+ ScanSubdirectory(dir.path, Subdirectory(), files_count, &transaction);
+ last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
}
else {
// We can do an incremental scan - looking at the mtimes of each subdirectory and only rescan if the directory has changed.
ScanTransaction transaction(this, dir.id, true, false, mark_songs_unavailable_);
+ QMap subdir_files_count;
+ const quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
transaction.SetKnownSubdirs(subdirs);
- transaction.AddToProgressMax(subdirs.count());
+ transaction.AddToProgressMax(files_count);
for (const Subdirectory &subdir : subdirs) {
if (stop_requested_) break;
- if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction);
+ if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
if (monitor_) AddWatch(dir, subdir.path);
}
+
+ last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
+
}
emit CompilationsNeedUpdating();
}
-void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory &subdir, ScanTransaction *t, bool force_noincremental) {
+void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
QFileInfo path_info(path);
QDir path_dir(path);
@@ -352,7 +415,6 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
QString real_path = path_info.symLinkTarget();
for (const Directory &dir : qAsConst(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
- t->AddToProgress(1);
return;
}
}
@@ -360,13 +422,19 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
// Do not scan directories containing a .nomedia or .nomusic file
if (path_dir.exists(kNoMediaFile) || path_dir.exists(kNoMusicFile)) {
- t->AddToProgress(1);
return;
}
- if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toSecsSinceEpoch()) {
+ bool songs_missing_fingerprint = false;
+#ifdef HAVE_SONGFINGERPRINTING
+ if (song_tracking_) {
+ songs_missing_fingerprint = t->HasSongsWithMissingFingerprint(path);
+ }
+#endif
+
+ if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toSecsSinceEpoch() && !songs_missing_fingerprint) {
// The directory hasn't changed since last time
- t->AddToProgress(1);
+ t->AddToProgress(files_count);
return;
}
@@ -379,8 +447,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
for (const Subdirectory &prev_subdir : previous_subdirs) {
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
- t->AddToProgressMax(1);
- ScanSubdirectory(prev_subdir.path, prev_subdir, t, true);
+ ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
}
}
@@ -402,15 +469,21 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
new_subdir.mtime = child_info.lastModified().toSecsSinceEpoch();
my_new_subdirs << new_subdir;
}
+ t->AddToProgress(1);
}
else {
QString ext_part(ExtensionPart(child));
QString dir_part(DirectoryPart(child));
-
- if (sValidImages.contains(ext_part))
+ if (sValidImages.contains(ext_part)) {
album_art[dir_part] << child;
- else if (!child_info.isHidden())
+ t->AddToProgress(1);
+ }
+ else if (TagReaderClient::Instance()->IsMediaFileBlocking(child)) {
files_on_disk << child;
+ }
+ else {
+ t->AddToProgress(1);
+ }
}
}
@@ -422,15 +495,18 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
QSet cues_processed;
// Now compare the list from the database with the list of files on disk
- for (const QString &file : files_on_disk) {
+ QStringList files_on_disk_copy = files_on_disk;
+ for (const QString &file : files_on_disk_copy) {
+
if (stop_requested_) return;
- // Associated cue
- QString matching_cue = NoExtensionPart(file) + ".cue";
+ // Associated CUE
+ QString new_cue = NoExtensionPart(file) + ".cue";
- Song matching_song(source_);
- if (FindSongByPath(songs_in_db, file, &matching_song)) {
- qint64 matching_cue_mtime = GetMtimeForCue(matching_cue);
+ SongList matching_songs;
+ if (FindSongsByPath(songs_in_db, file, &matching_songs)) { // Found matching song in DB by path.
+
+ Song matching_song = matching_songs.first();
// The song is in the database and still on disk.
// Check the mtime to see if it's been changed since it was added.
@@ -439,18 +515,21 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
if (!file_info.exists()) {
// Partially fixes race condition - if file was removed between being added to the list and now.
files_on_disk.removeAll(file);
+ t->AddToProgress(1);
continue;
}
- // CUE sheet's path from collection (if any)
- QString song_cue = matching_song.cue_path();
- qint64 song_cue_mtime = GetMtimeForCue(song_cue);
+ // CUE sheet's path from collection (if any).
+ qint64 matching_song_cue_mtime = GetMtimeForCue(matching_song.cue_path());
- bool cue_deleted = song_cue_mtime == 0 && matching_song.has_cue();
- bool cue_added = matching_cue_mtime != 0 && !matching_song.has_cue();
+ // CUE sheet's path from this file (if any).
+ qint64 new_cue_mtime = GetMtimeForCue(new_cue);
+
+ bool cue_added = new_cue_mtime != 0 && !matching_song.has_cue();
+ bool cue_deleted = matching_song_cue_mtime == 0 && matching_song.has_cue();
// Watch out for CUE songs which have their mtime equal to qMax(media_file_mtime, cue_sheet_mtime)
- bool changed = (matching_song.mtime() != qMax(file_info.lastModified().toSecsSinceEpoch(), song_cue_mtime)) || cue_deleted || cue_added;
+ bool changed = (matching_song.mtime() != qMax(file_info.lastModified().toSecsSinceEpoch(), matching_song_cue_mtime)) || cue_deleted || cue_added;
// Also want to look to see whether the album art has changed
QUrl image = ImageForSong(file, album_art);
@@ -458,53 +537,132 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
changed = true;
}
- // The song's changed - reread the metadata from file
- if (t->ignores_mtime() || changed) {
- qLog(Debug) << file << "changed";
+ bool missing_fingerprint = false;
+#ifdef HAVE_SONGFINGERPRINTING
+ if (song_tracking_ && matching_song.fingerprint().isEmpty()) {
+ missing_fingerprint = true;
+ }
+#endif
+
+ if (changed) {
+ qLog(Debug) << file << "has changed.";
+ }
+ else if (missing_fingerprint) {
+ qLog(Debug) << file << "is missing fingerprint.";
+ }
+
+ // The song's changed or missing fingerprint - create fingerprint and reread the metadata from file.
+ if (t->ignores_mtime() || changed || missing_fingerprint) {
+
+ QString fingerprint;
+#ifdef HAVE_SONGFINGERPRINTING
+ if (song_tracking_) {
+ Chromaprinter chromaprinter(file);
+ fingerprint = chromaprinter.CreateFingerprint();
+ if (fingerprint.isEmpty()) {
+ fingerprint = "NONE";
+ }
+ }
+#endif
if (!cue_deleted && (matching_song.has_cue() || cue_added)) { // If CUE associated.
- UpdateCueAssociatedSongs(file, path, matching_cue, image, t);
+ UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, image, matching_songs, t);
}
else { // If no CUE or it's about to lose it.
- UpdateNonCueAssociatedSong(file, matching_song, image, cue_deleted, t);
+ UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, image, cue_deleted, t);
}
}
// Nothing has changed - mark the song available without re-scanning
- if (matching_song.is_unavailable()) {
- if (matching_song.has_cue()) {
- t->readded_songs << backend_->GetSongsByUrl(QUrl::fromLocalFile(file), true);
- }
- else {
- t->readded_songs << matching_song;
- }
+ else if (matching_song.is_unavailable()) {
+ t->readded_songs << matching_songs;
}
}
- else {
- // The song is on disk but not in the DB
- SongList song_list = ScanNewFile(file, path, matching_cue, &cues_processed);
-
- if (song_list.isEmpty()) {
- continue;
+ else { // Search the DB by fingerprint.
+ QString fingerprint;
+#ifdef HAVE_SONGFINGERPRINTING
+ if (song_tracking_) {
+ Chromaprinter chromaprinter(file);
+ fingerprint = chromaprinter.CreateFingerprint();
+ if (fingerprint.isEmpty()) {
+ fingerprint = "NONE";
+ }
}
+#endif
+ if (song_tracking_ && !fingerprint.isEmpty() && fingerprint != "NONE" && FindSongsByFingerprint(file, fingerprint, &matching_songs)) {
- qLog(Debug) << file << "created";
- // Choose an image for the song(s)
- QUrl image = ImageForSong(file, album_art);
+ // The song is in the database and still on disk.
+ // Check the mtime to see if it's been changed since it was added.
+ QFileInfo file_info(file);
+ if (!file_info.exists()) {
+ // Partially fixes race condition - if file was removed between being added to the list and now.
+ files_on_disk.removeAll(file);
+ t->AddToProgress(1);
+ continue;
+ }
- for (Song song : song_list) {
- song.set_directory_id(t->dir());
- if (song.art_automatic().isEmpty()) song.set_art_automatic(image);
- t->new_songs << song;
+ // Make sure the songs aren't deleted, as they still exist elsewhere with a different fingerprint.
+ bool matching_songs_has_cue = false;
+ for (const Song &matching_song : matching_songs) {
+ QString matching_filename = matching_song.url().toLocalFile();
+ if (!t->files_changed_path_.contains(matching_filename)) {
+ t->files_changed_path_ << matching_filename;
+ qLog(Debug) << matching_filename << "has changed path to" << file;
+ }
+ if (t->deleted_songs.contains(matching_song)) {
+ t->deleted_songs.removeAll(matching_song);
+ }
+ if (matching_song.has_cue()) {
+ matching_songs_has_cue = true;
+ }
+ }
+
+ // CUE sheet's path from this file (if any).
+ const qint64 new_cue_mtime = GetMtimeForCue(new_cue);
+
+ const bool cue_deleted = new_cue_mtime == 0 && matching_songs_has_cue;
+ const bool cue_added = new_cue_mtime != 0 && !matching_songs_has_cue;
+
+ // Get new album art
+ QUrl image = ImageForSong(file, album_art);
+
+ if (!cue_deleted && (matching_songs_has_cue || cue_added)) { // CUE associated.
+ UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, image, matching_songs, t);
+ }
+ else { // If no CUE or it's about to lose it.
+ UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, image, cue_deleted, t);
+ }
+
+ }
+ else { // The song is on disk but not in the DB
+
+ SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
+ if (songs.isEmpty()) {
+ t->AddToProgress(1);
+ continue;
+ }
+
+ qLog(Debug) << file << "is new.";
+
+ // Choose an image for the song(s)
+ QUrl image = ImageForSong(file, album_art);
+
+ for (Song song : songs) {
+ song.set_directory_id(t->dir());
+ if (song.art_automatic().isEmpty()) song.set_art_automatic(image);
+ t->new_songs << song;
+ }
}
}
+ t->AddToProgress(1);
}
// Look for deleted songs
for (const Song &song : songs_in_db) {
- if (!song.is_unavailable() && !files_on_disk.contains(song.url().toLocalFile())) {
- qLog(Debug) << "Song deleted from disk:" << song.url().toLocalFile();
+ QString file = song.url().toLocalFile();
+ if (!song.is_unavailable() && !files_on_disk.contains(file) && !t->files_changed_path_.contains(file)) {
+ qLog(Debug) << "Song deleted from disk:" << file;
t->deleted_songs << song;
}
}
@@ -515,162 +673,178 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
updated_subdir.path = path;
- if (subdir.directory_id == -1)
+ if (subdir.directory_id == -1) {
t->new_subdirs << updated_subdir;
- else
+ }
+ else {
t->touched_subdirs << updated_subdir;
+ }
if (updated_subdir.mtime == 0) { // Subdirectory deleted, mark it for removal from the watcher.
t->deleted_subdirs << updated_subdir;
}
- t->AddToProgress(1);
-
// Recurse into the new subdirs that we found
- t->AddToProgressMax(my_new_subdirs.count());
for (const Subdirectory &my_new_subdir : my_new_subdirs) {
if (stop_requested_) return;
- ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true);
+ ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
}
}
-void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QUrl &image, ScanTransaction *t) {
-
- QFile cue(matching_cue);
- cue.open(QIODevice::ReadOnly);
-
- SongList old_sections = backend_->GetSongsByUrl(QUrl::fromLocalFile(file));
+void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
+ const QString &path,
+ const QString &fingerprint,
+ const QString &matching_cue,
+ const QUrl &image,
+ const SongList &old_cue_songs,
+ ScanTransaction *t) {
QHash sections_map;
- for (const Song &song : old_sections) {
- sections_map[song.beginning_nanosec()] = song;
+ for (const Song &song : old_cue_songs) {
+ sections_map.insert(song.beginning_nanosec(), song);
}
+ // Load new CUE songs
+ QFile cue_file(matching_cue);
+ if (!cue_file.exists() || !cue_file.open(QIODevice::ReadOnly)) return;
+ const SongList songs = cue_parser_->Load(&cue_file, matching_cue, path, false);
+ cue_file.close();
+
+ // Update every song that's in the CUE and collection
QSet used_ids;
+ for (Song new_cue_song : songs) {
+ new_cue_song.set_source(source_);
+ new_cue_song.set_directory_id(t->dir());
+ new_cue_song.set_fingerprint(fingerprint);
- // Update every song that's in the cue and collection
- for (Song cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
- cue_song.set_source(source_);
- cue_song.set_directory_id(t->dir());
-
- Song matching = sections_map[cue_song.beginning_nanosec()];
- // A new section
- if (!matching.is_valid()) {
- t->new_songs << cue_song;
- // changed section
+ if (sections_map.contains(new_cue_song.beginning_nanosec())) { // Changed section
+ const Song matching_cue_song = sections_map[new_cue_song.beginning_nanosec()];
+ new_cue_song.set_id(matching_cue_song.id());
+ if (!new_cue_song.has_embedded_cover()) new_cue_song.set_art_automatic(image);
+ new_cue_song.MergeUserSetData(matching_cue_song);
+ AddChangedSong(file, matching_cue_song, new_cue_song, t);
+ used_ids.insert(matching_cue_song.id());
}
- else {
- PreserveUserSetData(file, image, matching, &cue_song, t);
- used_ids.insert(matching.id());
+ else { // A new section
+ t->new_songs << new_cue_song;
}
}
// Sections that are now missing
- for (const Song &matching : old_sections) {
- if (!used_ids.contains(matching.id())) {
- t->deleted_songs << matching;
+ for (const Song &old_cue : old_cue_songs) {
+ if (!used_ids.contains(old_cue.id())) {
+ t->deleted_songs << old_cue;
}
}
}
-void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QUrl &image, bool cue_deleted, ScanTransaction *t) {
+void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
+ const QString &fingerprint,
+ const SongList &matching_songs,
+ const QUrl &image,
+ const bool cue_deleted,
+ ScanTransaction *t) {
// If a CUE got deleted, we turn it's first section into the new 'raw' (cueless) song and we just remove the rest of the sections from the collection
+ const Song &matching_song = matching_songs.first();
if (cue_deleted) {
- for (const Song &song : backend_->GetSongsByUrl(QUrl::fromLocalFile(file))) {
- if (!song.IsMetadataAndArtEqual(matching_song)) {
+ for (const Song &song : matching_songs) {
+ if (!song.IsMetadataAndMoreEqual(matching_song)) {
t->deleted_songs << song;
}
}
}
Song song_on_disk(source_);
- song_on_disk.set_directory_id(t->dir());
TagReaderClient::Instance()->ReadFileBlocking(file, &song_on_disk);
-
if (song_on_disk.is_valid()) {
- PreserveUserSetData(file, image, matching_song, &song_on_disk, t);
+ song_on_disk.set_source(source_);
+ song_on_disk.set_directory_id(t->dir());
+ song_on_disk.set_id(matching_song.id());
+ song_on_disk.set_fingerprint(fingerprint);
+ if (!song_on_disk.has_embedded_cover()) song_on_disk.set_art_automatic(image);
+ song_on_disk.MergeUserSetData(matching_song);
+ AddChangedSong(file, matching_song, song_on_disk, t);
}
}
-SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &matching_cue, QSet *cues_processed) {
+SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet *cues_processed) {
- SongList song_list;
+ SongList songs;
quint64 matching_cue_mtime = GetMtimeForCue(matching_cue);
if (matching_cue_mtime) { // If it's a CUE - create virtual tracks
- // Don't process the same cue many times
- if (cues_processed->contains(matching_cue)) return song_list;
- QFile cue(matching_cue);
- cue.open(QIODevice::ReadOnly);
+ // Don't process the same CUE many times
+ if (cues_processed->contains(matching_cue)) return songs;
+
+ QFile cue_file(matching_cue);
+ if (!cue_file.exists() || !cue_file.open(QIODevice::ReadOnly)) return songs;
// Ignore FILEs pointing to other media files.
// Also, watch out for incorrect media files.
// Playlist parser for CUEs considers every entry in sheet valid and we don't want invalid media getting into collection!
QString file_nfd = file.normalized(QString::NormalizationForm_D);
- for (Song &cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
+ SongList cue_congs = cue_parser_->Load(&cue_file, matching_cue, path, false);
+ for (Song &cue_song : cue_congs) {
cue_song.set_source(source_);
+ cue_song.set_fingerprint(fingerprint);
if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) {
- if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
- song_list << cue_song;
- }
+ songs << cue_song;
}
}
-
- if (!song_list.isEmpty()) {
+ if (!songs.isEmpty()) {
*cues_processed << matching_cue;
}
-
}
else { // It's a normal media file
Song song(source_);
TagReaderClient::Instance()->ReadFileBlocking(file, &song);
if (song.is_valid()) {
song.set_source(source_);
- song_list << song;
+ song.set_fingerprint(fingerprint);
+ songs << song;
}
}
- return song_list;
+ return songs;
}
-void CollectionWatcher::PreserveUserSetData(const QString &file, const QUrl &image, const Song &matching_song, Song *out, ScanTransaction *t) {
+void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching_song, const Song &new_song, ScanTransaction *t) {
- out->set_id(matching_song.id());
-
- // Previous versions of Clementine incorrectly overwrote this and stored it in the DB,
- // so we can't rely on matching_song to know if it has embedded artwork or not, but we can check here.
- if (!out->has_embedded_cover()) out->set_art_automatic(image);
-
- out->MergeUserSetData(matching_song);
-
- // The song was deleted from the database (e.g. due to an unmounted filesystem), but has been restored.
if (matching_song.is_unavailable()) {
- qLog(Debug) << file << " unavailable song restored";
-
- t->new_songs << *out;
+ qLog(Debug) << file << "unavailable song restored.";
+ t->new_songs << new_song;
}
- else if (!matching_song.IsMetadataAndArtEqual(*out)) {
- qLog(Debug) << file << "metadata changed";
-
- // Update the song in the DB
- t->new_songs << *out;
+ else if (!matching_song.IsMetadataEqual(new_song)) {
+ qLog(Debug) << file << "metadata changed.";
+ t->new_songs << new_song;
+ }
+ else if (matching_song.fingerprint() != new_song.fingerprint()) {
+ qLog(Debug) << file << "fingerprint changed.";
+ t->new_songs << new_song;
+ }
+ else if (matching_song.art_automatic() != new_song.art_automatic() || matching_song.art_manual() != new_song.art_manual()) {
+ qLog(Debug) << file << "art changed.";
+ t->new_songs << new_song;
+ }
+ else if (matching_song.mtime() != new_song.mtime()) {
+ qLog(Debug) << file << "mtime changed.";
+ t->touched_songs << new_song;
}
else {
- // Only the mtime's changed
- t->touched_songs << *out;
+ qLog(Debug) << file << "unchanged.";
+ t->touched_songs << new_song;
}
}
quint64 CollectionWatcher::GetMtimeForCue(const QString &cue_path) {
- // Slight optimisation
if (cue_path.isEmpty()) {
return 0;
}
@@ -721,16 +895,46 @@ void CollectionWatcher::RemoveDirectory(const Directory &dir) {
}
-bool CollectionWatcher::FindSongByPath(const SongList &list, const QString &path, Song *out) {
+bool CollectionWatcher::FindSongsByPath(const SongList &songs, const QString &path, SongList *out) {
- // TODO: Make this faster
- for (const Song &song : list) {
+ for (const Song &song : songs) {
if (song.url().toLocalFile() == path) {
- *out = song;
+ *out << song;
+ }
+ }
+
+ return !out->isEmpty();
+
+}
+
+bool CollectionWatcher::FindSongsByFingerprint(const QString &file, const QString &fingerprint, SongList *out) {
+
+ SongList songs = backend_->GetSongsByFingerprint(fingerprint);
+ for (const Song &song : songs) {
+ QString filename = song.url().toLocalFile();
+ QFileInfo info(filename);
+ // Allow mulitiple songs in different directories with the same fingerprint.
+ // Only use the matching song by fingerprint if it doesn't already exist in a different path.
+ if (file == filename || !info.exists()) {
+ *out << song;
+ }
+ }
+
+ return !out->isEmpty();
+
+}
+
+bool CollectionWatcher::FindSongsByFingerprint(const QString &file, const SongList &songs, const QString &fingerprint, SongList *out) {
+
+ for (const Song &song : songs) {
+ QString filename = song.url().toLocalFile();
+ if (song.fingerprint() == fingerprint && (file == filename || !QFileInfo(filename).exists())) {
+ *out << song;
return true;
}
}
- return false;
+
+ return !out->isEmpty();
}
@@ -758,7 +962,13 @@ void CollectionWatcher::RescanPathsNow() {
for (const int dir : dirs) {
if (stop_requested_) break;
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
- transaction.AddToProgressMax(rescan_queue_[dir].count());
+
+ QMap subdir_files_count;
+ for (const QString &path : rescan_queue_[dir]) {
+ quint64 files_count = FilesCountForPath(&transaction, path);
+ subdir_files_count[path] = files_count;
+ transaction.AddToProgressMax(files_count);
+ }
for (const QString &path : rescan_queue_[dir]) {
if (stop_requested_) break;
@@ -766,7 +976,7 @@ void CollectionWatcher::RescanPathsNow() {
subdir.directory_id = dir;
subdir.mtime = 0;
subdir.path = path;
- ScanSubdirectory(path, subdir, &transaction);
+ ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
}
}
@@ -877,6 +1087,16 @@ void CollectionWatcher::RescanTracksAsync(const SongList &songs) {
}
+void CollectionWatcher::IncrementalScanCheck() {
+
+ qint64 duration = QDateTime::currentDateTime().toSecsSinceEpoch() - last_scan_time_;
+ if (duration >= 86400) {
+ qLog(Debug) << "Performing periodic incremental scan.";
+ IncrementalScanNow();
+ }
+
+}
+
void CollectionWatcher::IncrementalScanNow() { PerformScan(true, false); }
void CollectionWatcher::FullScanNow() { PerformScan(false, true); }
@@ -895,7 +1115,8 @@ void CollectionWatcher::RescanTracksNow() {
if (!scanned_dirs.contains(songdir)) {
qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir;
ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_);
- ScanSubdirectory(songdir, Subdirectory(), &transaction);
+ quint64 files_count = FilesCountForPath(&transaction, songdir);
+ ScanSubdirectory(songdir, Subdirectory(), files_count, &transaction);
scanned_dirs << songdir;
emit CompilationsNeedUpdating();
}
@@ -908,7 +1129,7 @@ void CollectionWatcher::RescanTracksNow() {
}
-void CollectionWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
+void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mtimes) {
stop_requested_ = false;
@@ -928,15 +1149,72 @@ void CollectionWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
subdirs << subdir;
}
- transaction.AddToProgressMax(subdirs.count());
+ QMap subdir_files_count;
+ quint64 files_count = FilesCountForSubdirs(&transaction, subdirs, subdir_files_count);
+ transaction.AddToProgressMax(files_count);
for (const Subdirectory &subdir : subdirs) {
if (stop_requested_) break;
-
- ScanSubdirectory(subdir.path, subdir, &transaction);
+ ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
}
+
}
+ last_scan_time_ = QDateTime::currentDateTime().toSecsSinceEpoch();
+
emit CompilationsNeedUpdating();
}
+
+quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &path) {
+
+ quint64 i = 0;
+ QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot);
+ while (it.hasNext()) {
+
+ if (stop_requested_) break;
+
+ QString child = it.next();
+ QFileInfo path_info(child);
+
+ if (path_info.isDir()) {
+ if (path_info.exists(kNoMediaFile) || path_info.exists(kNoMusicFile)) {
+ continue;
+ }
+ if (path_info.isSymLink()) {
+ QString real_path = path_info.symLinkTarget();
+ for (const Directory &dir : qAsConst(watched_dirs_)) {
+ if (real_path.startsWith(dir.path)) {
+ continue;
+ }
+ }
+ }
+
+ if (!t->HasSeenSubdir(child) && !path_info.isHidden()) {
+ // We haven't seen this subdirectory before, so we need to include the file count for this directory too.
+ i += FilesCountForPath(t, child);
+ }
+
+ }
+
+ ++i;
+
+ }
+
+ return i;
+
+}
+
+quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const SubdirectoryList &subdirs, QMap &subdir_files_count) {
+
+ quint64 i = 0;
+ for (const Subdirectory &subdir : subdirs) {
+ if (stop_requested_) break;
+ const quint64 files_count = FilesCountForPath(t, subdir.path);
+ subdir_files_count[subdir.path] = files_count;
+ i += files_count;
+ }
+
+ return i;
+
+}
diff --git a/src/collection/collectionwatcher.h b/src/collection/collectionwatcher.h
index fec43b35..2b56657f 100644
--- a/src/collection/collectionwatcher.h
+++ b/src/collection/collectionwatcher.h
@@ -28,6 +28,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -52,12 +53,12 @@ class CollectionWatcher : public QObject {
void set_backend(CollectionBackend *backend) { backend_ = backend; }
void set_task_manager(TaskManager *task_manager) { task_manager_ = task_manager; }
- void set_device_name(const QString& device_name) { device_name_ = device_name; }
+ void set_device_name(const QString &device_name) { device_name_ = device_name; }
void IncrementalScanAsync();
void FullScanAsync();
void RescanTracksAsync(const SongList &songs);
- void SetRescanPausedAsync(bool pause);
+ void SetRescanPausedAsync(const bool pause);
void ReloadSettingsAsync();
void Stop() { stop_requested_ = true; }
@@ -73,12 +74,12 @@ class CollectionWatcher : public QObject {
void SubdirsDiscovered(SubdirectoryList subdirs);
void SubdirsMTimeUpdated(SubdirectoryList subdirs);
void CompilationsNeedUpdating();
+ void UpdateLastSeen(int directory_id, int expire_unavailable_songs_days);
void ExitFinished();
void ScanStarted(int task_id);
public slots:
- void ReloadSettings();
void AddDirectory(const Directory &dir, const SubdirectoryList &subdirs);
void RemoveDirectory(const Directory &dir);
void SetRescanPaused(bool pause);
@@ -96,13 +97,14 @@ class CollectionWatcher : public QObject {
~ScanTransaction();
SongList FindSongsInSubdirectory(const QString &path);
+ bool HasSongsWithMissingFingerprint(const QString &path);
bool HasSeenSubdir(const QString &path);
void SetKnownSubdirs(const SubdirectoryList &subdirs);
SubdirectoryList GetImmediateSubdirs(const QString &path);
SubdirectoryList GetAllSubdirs();
- void AddToProgress(int n = 1);
- void AddToProgressMax(int n);
+ void AddToProgress(const quint64 n = 1);
+ void AddToProgressMax(const quint64 n);
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
void CommitNewOrUpdatedSongs();
@@ -119,13 +121,15 @@ class CollectionWatcher : public QObject {
SubdirectoryList touched_subdirs;
SubdirectoryList deleted_subdirs;
+ QStringList files_changed_path_;
+
private:
ScanTransaction(const ScanTransaction&) {}
- ScanTransaction& operator=(const ScanTransaction&) { return *this; }
+ ScanTransaction &operator=(const ScanTransaction&) { return *this; }
int task_id_;
- int progress_;
- int progress_max_;
+ quint64 progress_;
+ quint64 progress_max_;
int dir_;
// Incremental scan enters a directory only if it has changed since the last scan.
@@ -138,27 +142,35 @@ class CollectionWatcher : public QObject {
// Set this to true to prevent deleting missing files from database.
// Useful for unstable network connections.
bool mark_songs_unavailable_;
+ int expire_unavailable_songs_days_;
CollectionWatcher *watcher_;
- SongList cached_songs_;
+ QMultiMap cached_songs_;
bool cached_songs_dirty_;
+ QMultiMap cached_songs_missing_fingerprint_;
+ bool cached_songs_missing_fingerprint_dirty_;
+
SubdirectoryList known_subdirs_;
bool known_subdirs_dirty_;
};
private slots:
+ void ReloadSettings();
void Exit();
void DirectoryChanged(const QString &subdir);
+ void IncrementalScanCheck();
void IncrementalScanNow();
void FullScanNow();
void RescanTracksNow();
void RescanPathsNow();
- void ScanSubdirectory(const QString &path, const Subdirectory &subdir, CollectionWatcher::ScanTransaction *t, bool force_noincremental = false);
+ void ScanSubdirectory(const QString &path, const Subdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
private:
- static bool FindSongByPath(const SongList &list, const QString &path, Song *out);
+ static bool FindSongsByPath(const SongList &list, const QString &path, SongList *out);
+ bool FindSongsByFingerprint(const QString &file, const QString &fingerprint, SongList *out);
+ static bool FindSongsByFingerprint(const QString &file, const SongList &songs, const QString &fingerprint, SongList *out);
inline static QString NoExtensionPart(const QString &fileName);
inline static QString ExtensionPart(const QString &fileName);
inline static QString DirectoryPart(const QString &fileName);
@@ -167,17 +179,20 @@ class CollectionWatcher : public QObject {
void AddWatch(const Directory &dir, const QString &path);
void RemoveWatch(const Directory &dir, const Subdirectory &subdir);
quint64 GetMtimeForCue(const QString &cue_path);
- void PerformScan(bool incremental, bool ignore_mtimes);
+ void PerformScan(const bool incremental, const bool ignore_mtimes);
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
- void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QUrl &image, ScanTransaction *t);
+ void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &image, const SongList &old_cue_songs, ScanTransaction *t);
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
- void UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QUrl &image, bool cue_deleted, ScanTransaction *t);
- // Updates a new song with some metadata taken from it's equivalent old song (for example rating and score).
- void PreserveUserSetData(const QString &file, const QUrl &image, const Song &matching_song, Song *out, ScanTransaction *t);
+ void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &image, const bool cue_deleted, ScanTransaction *t);
// Scans a single media file that's present on the disk but not yet in the collection.
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
- SongList ScanNewFile(const QString &file, const QString &path, const QString &matching_cue, QSet *cues_processed);
+ SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet *cues_processed);
+
+ void AddChangedSong(const QString &file, const Song &matching_song, const Song &new_song, ScanTransaction *t);
+
+ quint64 FilesCountForPath(ScanTransaction *t, const QString &path);
+ quint64 FilesCountForSubdirs(ScanTransaction *t, const SubdirectoryList &subdirs, QMap &subdir_files_count);
private:
Song::Source source_;
@@ -186,6 +201,7 @@ class CollectionWatcher : public QObject {
QString device_name_;
FileSystemWatcherInterface *fs_watcher_;
+ QThread *original_thread_;
QHash subdir_mapping_;
// A list of words use to try to identify the (likely) best image found in an directory to use as cover artwork.
@@ -194,13 +210,16 @@ class CollectionWatcher : public QObject {
bool scan_on_startup_;
bool monitor_;
+ bool song_tracking_;
bool mark_songs_unavailable_;
+ int expire_unavailable_songs_days_;
bool stop_requested_;
bool rescan_in_progress_; // True if RescanTracksNow() has been called and is working.
QMap watched_dirs_;
QTimer *rescan_timer_;
+ QTimer *periodic_scan_timer_;
QMap rescan_queue_; // dir id -> list of subdirs to be scanned
bool rescan_paused_;
@@ -212,7 +231,7 @@ class CollectionWatcher : public QObject {
SongList song_rescan_queue_; // Set by ui thread
- QThread *original_thread_;
+ qint64 last_scan_time_;
};
diff --git a/src/config.h.in b/src/config.h.in
index ee919203..1978026a 100644
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -17,6 +17,7 @@
#cmakedefine HAVE_LIBPULSE
#cmakedefine HAVE_SPARKLE
#cmakedefine HAVE_QTSPARKLE
+#cmakedefine HAVE_SONGFINGERPRINTING
#cmakedefine HAVE_MUSICBRAINZ
#cmakedefine HAVE_GLOBALSHORTCUTS
#cmakedefine HAVE_X11_GLOBALSHORTCUTS
diff --git a/src/core/database.cpp b/src/core/database.cpp
index 9c32f2ec..3db8bebe 100644
--- a/src/core/database.cpp
+++ b/src/core/database.cpp
@@ -54,7 +54,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
-const int Database::kSchemaVersion = 13;
+const int Database::kSchemaVersion = 14;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
diff --git a/src/core/song.cpp b/src/core/song.cpp
index 5645b21d..c5ebdf6b 100644
--- a/src/core/song.cpp
+++ b/src/core/song.cpp
@@ -103,9 +103,12 @@ const QStringList Song::kColumns = QStringList() << "title"
<< "ctime"
<< "unavailable"
+ << "fingerprint"
+
<< "playcount"
<< "skipcount"
<< "lastplayed"
+ << "lastseen"
<< "compilation_detected"
<< "compilation_on"
@@ -202,9 +205,12 @@ struct Song::Private : public QSharedData {
qint64 ctime_;
bool unavailable_;
+ QString fingerprint_;
+
int playcount_;
int skipcount_;
qint64 lastplayed_;
+ qint64 lastseen_;
bool compilation_detected_; // From the collection scanner
bool compilation_on_; // Set by the user
@@ -255,6 +261,7 @@ Song::Private::Private(Song::Source source)
playcount_(0),
skipcount_(0),
lastplayed_(-1),
+ lastseen_(-1),
compilation_detected_(false),
compilation_on_(false),
@@ -328,9 +335,12 @@ int Song::filesize() const { return d->filesize_; }
qint64 Song::mtime() const { return d->mtime_; }
qint64 Song::ctime() const { return d->ctime_; }
+QString Song::fingerprint() const { return d->fingerprint_; }
+
int Song::playcount() const { return d->playcount_; }
int Song::skipcount() const { return d->skipcount_; }
qint64 Song::lastplayed() const { return d->lastplayed_; }
+qint64 Song::lastseen() const { return d->lastseen_; }
bool Song::compilation_detected() const { return d->compilation_detected_; }
bool Song::compilation_off() const { return d->compilation_off_; }
@@ -451,9 +461,12 @@ void Song::set_mtime(qint64 v) { d->mtime_ = v; }
void Song::set_ctime(qint64 v) { d->ctime_ = v; }
void Song::set_unavailable(bool v) { d->unavailable_ = v; }
+void Song::set_fingerprint(const QString &v) { d->fingerprint_ = v; }
+
void Song::set_playcount(int v) { d->playcount_ = v; }
void Song::set_skipcount(int v) { d->skipcount_ = v; }
void Song::set_lastplayed(qint64 v) { d->lastplayed_ = v; }
+void Song::set_lastseen(qint64 v) { d->lastseen_ = v; }
void Song::set_compilation_detected(bool v) { d->compilation_detected_ = v; }
void Song::set_compilation_on(bool v) { d->compilation_on_ = v; }
@@ -789,6 +802,7 @@ void Song::InitFromProtobuf(const spb::tagreader::SongMetadata &pb) {
d->ctime_ = pb.ctime();
d->skipcount_ = pb.skipcount();
d->lastplayed_ = pb.lastplayed();
+ d->lastseen_ = pb.lastseen();
d->suspicious_tags_ = pb.suspicious_tags();
if (pb.has_playcount()) {
@@ -828,6 +842,7 @@ void Song::ToProtobuf(spb::tagreader::SongMetadata *pb) const {
pb->set_playcount(d->playcount_);
pb->set_skipcount(d->skipcount_);
pb->set_lastplayed(d->lastplayed_);
+ pb->set_lastseen(d->lastseen_);
pb->set_length_nanosec(length_nanosec());
pb->set_bitrate(d->bitrate_);
pb->set_samplerate(d->samplerate_);
@@ -964,6 +979,10 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
d->unavailable_ = q.value(x).toBool();
}
+ else if (Song::kColumns.value(i) == "fingerprint") {
+ d->fingerprint_ = tostr(x);
+ }
+
else if (Song::kColumns.value(i) == "playcount") {
d->playcount_ = q.value(x).isNull() ? 0 : q.value(x).toInt();
}
@@ -973,6 +992,9 @@ void Song::InitFromQuery(const SqlRow &q, bool reliable_metadata, int col) {
else if (Song::kColumns.value(i) == "lastplayed") {
d->lastplayed_ = tolonglong(x);
}
+ else if (Song::kColumns.value(i) == "lastseen") {
+ d->lastseen_ = tolonglong(x);
+ }
else if (Song::kColumns.value(i) == "compilation_detected") {
d->compilation_detected_ = q.value(x).toBool();
@@ -1376,9 +1398,12 @@ void Song::BindToQuery(QSqlQuery *query) const {
query->bindValue(":ctime", notnullintval(d->ctime_));
query->bindValue(":unavailable", d->unavailable_ ? 1 : 0);
+ query->bindValue(":fingerprint", strval(d->fingerprint_));
+
query->bindValue(":playcount", d->playcount_);
query->bindValue(":skipcount", d->skipcount_);
query->bindValue(":lastplayed", intval(d->lastplayed_));
+ query->bindValue(":lastseen", intval(d->lastseen_));
query->bindValue(":compilation_detected", d->compilation_detected_ ? 1 : 0);
query->bindValue(":compilation_on", d->compilation_on_ ? 1 : 0);
@@ -1519,9 +1544,12 @@ bool Song::IsMetadataEqual(const Song &other) const {
d->cue_path_ == other.d->cue_path_;
}
-bool Song::IsMetadataAndArtEqual(const Song &other) const {
+bool Song::IsMetadataAndMoreEqual(const Song &other) const {
- return IsMetadataEqual(other) && d->art_automatic_ == other.d->art_automatic_ && d->art_manual_ == other.d->art_manual_;
+ return IsMetadataEqual(other) &&
+ d->fingerprint_ == other.d->fingerprint_ &&
+ d->art_automatic_ == other.d->art_automatic_ &&
+ d->art_manual_ == other.d->art_manual_;
}
diff --git a/src/core/song.h b/src/core/song.h
index db2b40a4..d433864a 100644
--- a/src/core/song.h
+++ b/src/core/song.h
@@ -231,9 +231,12 @@ class Song {
qint64 mtime() const;
qint64 ctime() const;
+ QString fingerprint() const;
+
int playcount() const;
int skipcount() const;
qint64 lastplayed() const;
+ qint64 lastseen() const;
bool compilation_detected() const;
bool compilation_off() const;
@@ -345,9 +348,12 @@ class Song {
void set_ctime(qint64 v);
void set_unavailable(bool v);
+ void set_fingerprint(const QString &v);
+
void set_playcount(int v);
void set_skipcount(int v);
void set_lastplayed(qint64 v);
+ void set_lastseen(qint64 v);
void set_compilation_detected(bool v);
void set_compilation_on(bool v);
@@ -365,7 +371,7 @@ class Song {
// Comparison functions
bool IsMetadataEqual(const Song &other) const;
- bool IsMetadataAndArtEqual(const Song &other) const;
+ bool IsMetadataAndMoreEqual(const Song &other) const;
bool IsOnSameAlbum(const Song &other) const;
bool IsSimilar(const Song &other) const;
diff --git a/src/musicbrainz/chromaprinter.cpp b/src/engine/chromaprinter.cpp
similarity index 97%
rename from src/musicbrainz/chromaprinter.cpp
rename to src/engine/chromaprinter.cpp
index 5b10ca1d..eca56bb6 100644
--- a/src/musicbrainz/chromaprinter.cpp
+++ b/src/engine/chromaprinter.cpp
@@ -160,20 +160,13 @@ QString Chromaprinter::CreateFingerprint() {
chromaprint_feed(chromaprint, reinterpret_cast(data.data()), static_cast(data.size() / 2));
chromaprint_finish(chromaprint);
- int size = 0;
-
-#if CHROMAPRINT_VERSION_MAJOR >= 1 && CHROMAPRINT_VERSION_MINOR >= 4
u_int32_t *fprint = nullptr;
- char *encoded = nullptr;
-#else
- void *fprint = nullptr;
- void *encoded = nullptr;
-#endif
-
+ int size = 0;
int ret = chromaprint_get_raw_fingerprint(chromaprint, &fprint, &size);
QByteArray fingerprint;
if (ret == 1) {
+ char *encoded = nullptr;
int encoded_size = 0;
chromaprint_encode_fingerprint(fprint, size, CHROMAPRINT_ALGORITHM_DEFAULT, &encoded, &encoded_size, 1);
diff --git a/src/musicbrainz/chromaprinter.h b/src/engine/chromaprinter.h
similarity index 100%
rename from src/musicbrainz/chromaprinter.h
rename to src/engine/chromaprinter.h
diff --git a/src/musicbrainz/tagfetcher.cpp b/src/musicbrainz/tagfetcher.cpp
index 1921e0af..f3eb626f 100644
--- a/src/musicbrainz/tagfetcher.cpp
+++ b/src/musicbrainz/tagfetcher.cpp
@@ -29,8 +29,8 @@
#include
#include "core/timeconstants.h"
+#include "engine/chromaprinter.h"
#include "acoustidclient.h"
-#include "chromaprinter.h"
#include "musicbrainzclient.h"
#include "tagfetcher.h"
diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp
index dc82c928..b8c162d6 100644
--- a/src/playlist/playlistbackend.cpp
+++ b/src/playlist/playlistbackend.cpp
@@ -272,11 +272,11 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, std::share
CueParser cue_parser(app_->collection_backend());
Song song = item->Metadata();
- // we're only interested in .cue songs here
+ // We're only interested in .cue songs here
if (!song.has_cue()) return item;
QString cue_path = song.cue_path();
- // if .cue was deleted - reload the song
+ // If .cue was deleted - reload the song
if (!QFile::exists(cue_path)) {
item->Reload();
return item;
@@ -287,10 +287,10 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, std::share
QMutexLocker locker(&state->mutex_);
if (!state->cached_cues_.contains(cue_path)) {
- QFile cue(cue_path);
- cue.open(QIODevice::ReadOnly);
+ QFile cue_file(cue_path);
+ if (!cue_file.open(QIODevice::ReadOnly)) return item;
- song_list = cue_parser.Load(&cue, cue_path, QDir(cue_path.section('/', 0, -2)));
+ song_list = cue_parser.Load(&cue_file, cue_path, QDir(cue_path.section('/', 0, -2)));
state->cached_cues_[cue_path] = song_list;
}
else {
@@ -300,13 +300,14 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, std::share
for (const Song &from_list : song_list) {
if (from_list.url().toEncoded() == song.url().toEncoded() && from_list.beginning_nanosec() == song.beginning_nanosec()) {
- // we found a matching section; replace the input item with a new one containing CUE metadata
+ // We found a matching section; replace the input item with a new one containing CUE metadata
return PlaylistItemPtr(new SongPlaylistItem(from_list));
}
}
- // there's no such section in the related .cue -> reload the song
+ // There's no such section in the related .cue -> reload the song
item->Reload();
+
return item;
}
diff --git a/src/playlistparsers/asxiniparser.cpp b/src/playlistparsers/asxiniparser.cpp
index 65045568..dcb31482 100644
--- a/src/playlistparsers/asxiniparser.cpp
+++ b/src/playlistparsers/asxiniparser.cpp
@@ -44,7 +44,7 @@ bool AsxIniParser::TryMagic(const QByteArray &data) const {
return data.toLower().contains("[reference]");
}
-SongList AsxIniParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList AsxIniParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
Q_UNUSED(playlist_path);
@@ -57,7 +57,7 @@ SongList AsxIniParser::Load(QIODevice *device, const QString &playlist_path, con
QString value = line.mid(equals + 1);
if (key.startsWith("ref")) {
- Song song = LoadSong(value, 0, dir);
+ Song song = LoadSong(value, 0, dir, collection_search);
if (song.is_valid()) {
ret << song;
}
diff --git a/src/playlistparsers/asxiniparser.h b/src/playlistparsers/asxiniparser.h
index edfb82c8..c14b7440 100644
--- a/src/playlistparsers/asxiniparser.h
+++ b/src/playlistparsers/asxiniparser.h
@@ -46,7 +46,7 @@ class AsxIniParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const override;
};
diff --git a/src/playlistparsers/asxparser.cpp b/src/playlistparsers/asxparser.cpp
index f092217c..f376f3dd 100644
--- a/src/playlistparsers/asxparser.cpp
+++ b/src/playlistparsers/asxparser.cpp
@@ -41,7 +41,7 @@ class CollectionBackendInterface;
ASXParser::ASXParser(CollectionBackendInterface *collection, QObject *parent)
: XMLParser(collection, parent) {}
-SongList ASXParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList ASXParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
Q_UNUSED(playlist_path);
@@ -73,7 +73,7 @@ SongList ASXParser::Load(QIODevice *device, const QString &playlist_path, const
}
while (!reader.atEnd() && Utilities::ParseUntilElementCI(&reader, "entry")) {
- Song song = ParseTrack(&reader, dir);
+ Song song = ParseTrack(&reader, dir, collection_search);
if (song.is_valid()) {
ret << song;
}
@@ -83,7 +83,7 @@ SongList ASXParser::Load(QIODevice *device, const QString &playlist_path, const
}
-Song ASXParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir) const {
+Song ASXParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir, const bool collection_search) const {
QString title, artist, album, ref;
@@ -117,7 +117,7 @@ Song ASXParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir) const {
}
return_song:
- Song song = LoadSong(ref, 0, dir);
+ Song song = LoadSong(ref, 0, dir, collection_search);
// Override metadata with what was in the playlist
if (song.source() != Song::Source_Collection) {
diff --git a/src/playlistparsers/asxparser.h b/src/playlistparsers/asxparser.h
index 14b6a76e..f0270fdc 100644
--- a/src/playlistparsers/asxparser.h
+++ b/src/playlistparsers/asxparser.h
@@ -48,11 +48,11 @@ class ASXParser : public XMLParser {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const override;
private:
- Song ParseTrack(QXmlStreamReader *reader, const QDir &dir) const;
+ Song ParseTrack(QXmlStreamReader *reader, const QDir &dir, const bool collection_search) const;
};
#endif
diff --git a/src/playlistparsers/cueparser.cpp b/src/playlistparsers/cueparser.cpp
index 424b3f38..539f6194 100644
--- a/src/playlistparsers/cueparser.cpp
+++ b/src/playlistparsers/cueparser.cpp
@@ -58,7 +58,7 @@ const char *CueParser::kDisc = "discnumber";
CueParser::CueParser(CollectionBackendInterface *collection, QObject *parent)
: ParserBase(collection, parent) {}
-SongList CueParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList CueParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
SongList ret;
@@ -230,7 +230,7 @@ SongList CueParser::Load(QIODevice *device, const QString &playlist_path, const
for (int i = 0; i < entries.length(); i++) {
CueEntry entry = entries.at(i);
- Song song = LoadSong(entry.file, IndexToMarker(entry.index), dir);
+ Song song = LoadSong(entry.file, IndexToMarker(entry.index), dir, collection_search);
// Cue song has mtime equal to qMax(media_file_mtime, cue_sheet_mtime)
if (cue_mtime.isValid()) {
diff --git a/src/playlistparsers/cueparser.h b/src/playlistparsers/cueparser.h
index 47e51506..d017bb79 100644
--- a/src/playlistparsers/cueparser.h
+++ b/src/playlistparsers/cueparser.h
@@ -67,7 +67,7 @@ class CueParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const override;
private:
diff --git a/src/playlistparsers/m3uparser.cpp b/src/playlistparsers/m3uparser.cpp
index 9039271e..6ba3605b 100644
--- a/src/playlistparsers/m3uparser.cpp
+++ b/src/playlistparsers/m3uparser.cpp
@@ -42,7 +42,7 @@ class CollectionBackendInterface;
M3UParser::M3UParser(CollectionBackendInterface *collection, QObject *parent)
: ParserBase(collection, parent) {}
-SongList M3UParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList M3UParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
Q_UNUSED(playlist_path);
@@ -75,7 +75,7 @@ SongList M3UParser::Load(QIODevice *device, const QString &playlist_path, const
}
}
else if (!line.isEmpty()) {
- Song song = LoadSong(line, 0, dir);
+ Song song = LoadSong(line, 0, dir, collection_search);
if (!current_metadata.title.isEmpty()) {
song.set_title(current_metadata.title);
}
diff --git a/src/playlistparsers/m3uparser.h b/src/playlistparsers/m3uparser.h
index f0bfa34e..68504607 100644
--- a/src/playlistparsers/m3uparser.h
+++ b/src/playlistparsers/m3uparser.h
@@ -49,7 +49,7 @@ class M3UParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const override;
private:
diff --git a/src/playlistparsers/parserbase.cpp b/src/playlistparsers/parserbase.cpp
index 53bb6bb2..d56c4505 100644
--- a/src/playlistparsers/parserbase.cpp
+++ b/src/playlistparsers/parserbase.cpp
@@ -36,7 +36,7 @@
ParserBase::ParserBase(CollectionBackendInterface *collection, QObject *parent)
: QObject(parent), collection_(collection) {}
-void ParserBase::LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir, Song *song) const {
+void ParserBase::LoadSong(const QString &filename_or_url, const qint64 beginning, const QDir &dir, Song *song, const bool collection_search) const {
if (filename_or_url.isEmpty()) {
return;
@@ -78,25 +78,24 @@ void ParserBase::LoadSong(const QString &filename_or_url, qint64 beginning, cons
const QUrl url = QUrl::fromLocalFile(filename);
// Search in the collection
- Song collection_song(Song::Source_Collection);
- if (collection_) {
- collection_song = collection_->GetSongByUrl(url, beginning);
+ if (collection_ && collection_search) {
+ Song collection_song = collection_->GetSongByUrl(url, beginning);
+ // If it was found in the collection then use it, otherwise load metadata from disk.
+ if (collection_song.is_valid()) {
+ *song = collection_song;
+ return;
+ }
}
- // If it was found in the collection then use it, otherwise load metadata from disk.
- if (collection_song.is_valid()) {
- *song = collection_song;
- }
- else {
- TagReaderClient::Instance()->ReadFileBlocking(filename, song);
- }
+ TagReaderClient::Instance()->ReadFileBlocking(filename, song);
}
-Song ParserBase::LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir) const {
+Song ParserBase::LoadSong(const QString &filename_or_url, const qint64 beginning, const QDir &dir, const bool collection_search) const {
Song song(Song::Source_LocalFile);
- LoadSong(filename_or_url, beginning, dir, &song);
+ LoadSong(filename_or_url, beginning, dir, &song, collection_search);
+
return song;
}
diff --git a/src/playlistparsers/parserbase.h b/src/playlistparsers/parserbase.h
index 01652971..409be9e0 100644
--- a/src/playlistparsers/parserbase.h
+++ b/src/playlistparsers/parserbase.h
@@ -54,7 +54,7 @@ class ParserBase : public QObject {
// This method might not return all of the songs found in the playlist.
// Any playlist parser may decide to leave out some entries if it finds them incomplete or invalid.
// This means that the final resulting SongList should be considered valid (at least from the parser's point of view).
- virtual SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const = 0;
+ virtual SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_lookup = true) const = 0;
virtual void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const = 0;
protected:
@@ -62,8 +62,8 @@ class ParserBase : public QObject {
// If it is a filename or a file:// URL then it is made absolute and canonical and set as a file:// url on the song.
// Also sets the song's metadata by searching in the Collection, or loading from the file as a fallback.
// This function should always be used when loading a playlist.
- Song LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir) const;
- void LoadSong(const QString &filename_or_url, qint64 beginning, const QDir &dir, Song *song) const;
+ Song LoadSong(const QString &filename_or_url, const qint64 beginning, const QDir &dir, const bool collection_search) const;
+ void LoadSong(const QString &filename_or_url, const qint64 beginning, const QDir &dir, Song *song, const bool collection_search) const;
// If the URL is a file:// URL then returns its path, absolute or relative to the directory depending on the path_type option.
// Otherwise returns the URL as is. This function should always be used when saving a playlist.
diff --git a/src/playlistparsers/plsparser.cpp b/src/playlistparsers/plsparser.cpp
index daa581c3..f5ef4622 100644
--- a/src/playlistparsers/plsparser.cpp
+++ b/src/playlistparsers/plsparser.cpp
@@ -44,7 +44,7 @@ class CollectionBackendInterface;
PLSParser::PLSParser(CollectionBackendInterface *collection, QObject *parent)
: ParserBase(collection, parent) {}
-SongList PLSParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList PLSParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
Q_UNUSED(playlist_path);
@@ -61,7 +61,7 @@ SongList PLSParser::Load(QIODevice *device, const QString &playlist_path, const
int n = re_match.captured(0).toInt();
if (key.startsWith("file")) {
- Song song = LoadSong(value, 0, dir);
+ Song song = LoadSong(value, 0, dir, collection_search);
// Use the title and length we've already loaded if any
if (!songs[n].title().isEmpty()) song.set_title(songs[n].title());
diff --git a/src/playlistparsers/plsparser.h b/src/playlistparsers/plsparser.h
index ef601bd6..21241af4 100644
--- a/src/playlistparsers/plsparser.h
+++ b/src/playlistparsers/plsparser.h
@@ -47,7 +47,7 @@ class PLSParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const override;
};
diff --git a/src/playlistparsers/wplparser.cpp b/src/playlistparsers/wplparser.cpp
index 887fa5ab..a0364e1b 100644
--- a/src/playlistparsers/wplparser.cpp
+++ b/src/playlistparsers/wplparser.cpp
@@ -41,7 +41,7 @@ bool WplParser::TryMagic(const QByteArray &data) const {
return data.contains("");
}
-SongList WplParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList WplParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
Q_UNUSED(playlist_path);
@@ -53,13 +53,13 @@ SongList WplParser::Load(QIODevice *device, const QString &playlist_path, const
}
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, "seq")) {
- ParseSeq(dir, &reader, &ret);
+ ParseSeq(dir, &reader, &ret, collection_search);
}
return ret;
}
-void WplParser::ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs) const {
+void WplParser::ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs, const bool collection_search) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
@@ -69,7 +69,7 @@ void WplParser::ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *so
if (name == "media") {
QString src = reader->attributes().value("src").toString();
if (!src.isEmpty()) {
- Song song = LoadSong(src, 0, dir);
+ Song song = LoadSong(src, 0, dir, collection_search);
if (song.is_valid()) {
songs->append(song);
}
diff --git a/src/playlistparsers/wplparser.h b/src/playlistparsers/wplparser.h
index 5157f563..18c90e14 100644
--- a/src/playlistparsers/wplparser.h
+++ b/src/playlistparsers/wplparser.h
@@ -49,11 +49,11 @@ class WplParser : public XMLParser {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir, Playlist::Path path_type = Playlist::Path_Automatic) const override;
private:
- void ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs) const;
+ void ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs, const bool collection_search = true) const;
void WriteMeta(const QString &name, const QString &content, QXmlStreamWriter *writer) const;
};
diff --git a/src/playlistparsers/xspfparser.cpp b/src/playlistparsers/xspfparser.cpp
index 61410d6a..0529581f 100644
--- a/src/playlistparsers/xspfparser.cpp
+++ b/src/playlistparsers/xspfparser.cpp
@@ -42,7 +42,7 @@ class CollectionBackendInterface;
XSPFParser::XSPFParser(CollectionBackendInterface *collection, QObject *parent)
: XMLParser(collection, parent) {}
-SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir) const {
+SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_search) const {
Q_UNUSED(playlist_path);
@@ -54,7 +54,7 @@ SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const
}
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, "track")) {
- Song song = ParseTrack(&reader, dir);
+ Song song = ParseTrack(&reader, dir, collection_search);
if (song.is_valid()) {
ret << song;
}
@@ -63,7 +63,7 @@ SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const
}
-Song XSPFParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir) const {
+Song XSPFParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir, const bool collection_search) const {
QString title, artist, album, location;
qint64 nanosec = -1;
@@ -121,7 +121,7 @@ Song XSPFParser::ParseTrack(QXmlStreamReader *reader, const QDir &dir) const {
}
return_song:
- Song song = LoadSong(location, 0, dir);
+ Song song = LoadSong(location, 0, dir, collection_search);
// Override metadata with what was in the playlist
if (song.source() != Song::Source_Collection) {
diff --git a/src/playlistparsers/xspfparser.h b/src/playlistparsers/xspfparser.h
index d48a664d..6ed6bb72 100644
--- a/src/playlistparsers/xspfparser.h
+++ b/src/playlistparsers/xspfparser.h
@@ -48,11 +48,11 @@ class XSPFParser : public XMLParser {
bool TryMagic(const QByteArray &data) const override;
- SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir()) const override;
+ SongList Load(QIODevice *device, const QString &playlist_path = "", const QDir &dir = QDir(), const bool collection_search = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), Playlist::Path path_type = Playlist::Path_Automatic) const override;
private:
- Song ParseTrack(QXmlStreamReader *reader, const QDir &dir) const;
+ Song ParseTrack(QXmlStreamReader *reader, const QDir &dir, const bool collection_search) const;
};
#endif
diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp
index dbed2aad..3b46f89f 100644
--- a/src/settings/collectionsettingspage.cpp
+++ b/src/settings/collectionsettingspage.cpp
@@ -80,6 +80,10 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog)
QObject::connect(ui_->add, &QPushButton::clicked, this, &CollectionSettingsPage::Add);
QObject::connect(ui_->remove, &QPushButton::clicked, this, &CollectionSettingsPage::Remove);
+#ifdef HAVE_SONGFINGERPRINTING
+ QObject::connect(ui_->song_tracking, &QCheckBox::toggled, this, &CollectionSettingsPage::SongTrackingToggled);
+#endif
+
QObject::connect(ui_->radiobutton_save_albumcover_albumdir, &QRadioButton::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
QObject::connect(ui_->radiobutton_cover_hash, &QRadioButton::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
QObject::connect(ui_->radiobutton_cover_pattern, &QRadioButton::toggled, this, &CollectionSettingsPage::CoverSaveInAlbumDirChanged);
@@ -91,6 +95,10 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog)
QObject::connect(ui_->combobox_cache_size, QOverload::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::CacheSizeUnitChanged);
QObject::connect(ui_->combobox_disk_cache_size, QOverload::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::DiskCacheSizeUnitChanged);
+#ifndef HAVE_SONGFINGERPRINTING
+ ui_->song_tracking->hide();
+#endif
+
}
CollectionSettingsPage::~CollectionSettingsPage() { delete ui_; }
@@ -124,6 +132,15 @@ void CollectionSettingsPage::CurrentRowChanged(const QModelIndex &idx) {
ui_->remove->setEnabled(idx.isValid());
}
+void CollectionSettingsPage::SongTrackingToggled() {
+
+ ui_->mark_songs_unavailable->setEnabled(!ui_->song_tracking->isChecked());
+ if (ui_->song_tracking->isChecked()) {
+ ui_->mark_songs_unavailable->setChecked(true);
+ }
+
+}
+
void CollectionSettingsPage::DiskCacheEnable(const int state) {
bool checked = state == Qt::Checked;
@@ -157,7 +174,9 @@ void CollectionSettingsPage::Load() {
ui_->show_dividers->setChecked(s.value("show_dividers", true).toBool());
ui_->startup_scan->setChecked(s.value("startup_scan", true).toBool());
ui_->monitor->setChecked(s.value("monitor", true).toBool());
- ui_->mark_songs_unavailable->setChecked(s.value("mark_songs_unavailable", false).toBool());
+ ui_->song_tracking->setChecked(s.value("song_tracking", false).toBool());
+ ui_->mark_songs_unavailable->setChecked(ui_->song_tracking->isChecked() ? true : s.value("mark_songs_unavailable", true).toBool());
+ ui_->expire_unavailable_songs_days->setValue(s.value("expire_unavailable_songs", 60).toInt());
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
ui_->cover_art_patterns->setText(filters.join(","));
@@ -216,7 +235,9 @@ void CollectionSettingsPage::Save() {
s.setValue("show_dividers", ui_->show_dividers->isChecked());
s.setValue("startup_scan", ui_->startup_scan->isChecked());
s.setValue("monitor", ui_->monitor->isChecked());
- s.setValue("mark_songs_unavailable", ui_->mark_songs_unavailable->isChecked());
+ s.setValue("song_tracking", ui_->song_tracking->isChecked());
+ s.setValue("mark_songs_unavailable", ui_->song_tracking->isChecked() ? true : ui_->mark_songs_unavailable->isChecked());
+ s.setValue("expire_unavailable_songs", ui_->expire_unavailable_songs_days->value());
QString filter_text = ui_->cover_art_patterns->text();
diff --git a/src/settings/collectionsettingspage.h b/src/settings/collectionsettingspage.h
index 1dd7eeeb..c455128e 100644
--- a/src/settings/collectionsettingspage.h
+++ b/src/settings/collectionsettingspage.h
@@ -76,6 +76,7 @@ class CollectionSettingsPage : public SettingsPage {
void Remove();
void CurrentRowChanged(const QModelIndex &idx);
+ void SongTrackingToggled();
void DiskCacheEnable(const int state);
void CoverSaveInAlbumDirChanged();
void ClearPixmapDiskCache();
diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui
index 0bb81767..e4c4b9be 100644
--- a/src/settings/collectionsettingspage.ui
+++ b/src/settings/collectionsettingspage.ui
@@ -92,6 +92,13 @@
+ -
+
+
+ Song fingerprinting and tracking
+
+
+
-
@@ -99,6 +106,73 @@
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Expire unavailable songs after
+
+
+
+ -
+
+
+ 365
+
+
+ 60
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ days
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
-