From f8ed2afef18c23968c0473124eee4509aa4b432c Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 25 Apr 2021 21:16:44 +0200 Subject: [PATCH] Add song fingerprinting and tracking Fixes #296 --- CMakeLists.txt | 7 +- data/data.qrc | 1 + data/schema/device-schema.sql | 3 + data/schema/schema-14.sql | 5 + data/schema/schema.sql | 31 +- ext/libstrawberry-tagreader/tagreader.cpp | 1 + .../tagreadermessages.proto | 7 +- src/CMakeLists.txt | 24 +- src/collection/collection.cpp | 1 + src/collection/collectionbackend.cpp | 86 +++ src/collection/collectionbackend.h | 9 + src/collection/collectionwatcher.cpp | 600 +++++++++++++----- src/collection/collectionwatcher.h | 55 +- src/config.h.in | 1 + src/core/database.cpp | 2 +- src/core/song.cpp | 32 +- src/core/song.h | 8 +- src/{musicbrainz => engine}/chromaprinter.cpp | 11 +- src/{musicbrainz => engine}/chromaprinter.h | 0 src/musicbrainz/tagfetcher.cpp | 2 +- src/playlist/playlistbackend.cpp | 15 +- src/playlistparsers/asxiniparser.cpp | 4 +- src/playlistparsers/asxiniparser.h | 2 +- src/playlistparsers/asxparser.cpp | 8 +- src/playlistparsers/asxparser.h | 4 +- src/playlistparsers/cueparser.cpp | 4 +- src/playlistparsers/cueparser.h | 2 +- src/playlistparsers/m3uparser.cpp | 4 +- src/playlistparsers/m3uparser.h | 2 +- src/playlistparsers/parserbase.cpp | 25 +- src/playlistparsers/parserbase.h | 6 +- src/playlistparsers/plsparser.cpp | 4 +- src/playlistparsers/plsparser.h | 2 +- src/playlistparsers/wplparser.cpp | 8 +- src/playlistparsers/wplparser.h | 4 +- src/playlistparsers/xspfparser.cpp | 8 +- src/playlistparsers/xspfparser.h | 4 +- src/settings/collectionsettingspage.cpp | 25 +- src/settings/collectionsettingspage.h | 1 + src/settings/collectionsettingspage.ui | 74 +++ 40 files changed, 826 insertions(+), 266 deletions(-) create mode 100644 data/schema/schema-14.sql rename src/{musicbrainz => engine}/chromaprinter.cpp (97%) rename src/{musicbrainz => engine}/chromaprinter.h (100%) 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 + + + + + + +