Add song fingerprinting and tracking

Fixes #296
This commit is contained in:
Jonas Kvinge 2021-04-25 21:16:44 +02:00
parent a883508eca
commit f8ed2afef1
40 changed files with 826 additions and 266 deletions

View File

@ -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

View File

@ -14,6 +14,7 @@
<file>schema/schema-11.sql</file>
<file>schema/schema-12.sql</file>
<file>schema/schema-13.sql</file>
<file>schema/schema-14.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>style/smartplaylistsearchterm.css</file>

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
if (fileref->isNull()) {

View File

@ -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;
}

View File

@ -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})

View File

@ -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);

View File

@ -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<int>& ids, const double rating) {
metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList<int>, 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);
}

View File

@ -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<int> &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);

View File

@ -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<QString, quint64> 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<QString> 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<quint64, Song> 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<int> 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<QString> *cues_processed) {
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *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<QString, quint64> 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<QString, quint64> 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<QString, quint64> &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;
}

View File

@ -28,6 +28,7 @@
#include <QObject>
#include <QHash>
#include <QMap>
#include <QMultiMap>
#include <QSet>
#include <QString>
#include <QStringList>
@ -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<QString, Song> cached_songs_;
bool cached_songs_dirty_;
QMultiMap<QString, Song> 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<QString> *cues_processed);
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *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<QString, quint64> &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<QString, Directory> 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<int, Directory> watched_dirs_;
QTimer *rescan_timer_;
QTimer *periodic_scan_timer_;
QMap<int, QStringList> 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_;
};

View File

@ -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

View File

@ -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;

View File

@ -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_;
}

View File

@ -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;

View File

@ -160,20 +160,13 @@ QString Chromaprinter::CreateFingerprint() {
chromaprint_feed(chromaprint, reinterpret_cast<int16_t*>(data.data()), static_cast<int>(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);

View File

@ -29,8 +29,8 @@
#include <QUrl>
#include "core/timeconstants.h"
#include "engine/chromaprinter.h"
#include "acoustidclient.h"
#include "chromaprinter.h"
#include "musicbrainzclient.h"
#include "tagfetcher.h"

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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) {

View File

@ -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

View File

@ -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()) {

View File

@ -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:

View File

@ -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);
}

View File

@ -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:

View File

@ -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;
}

View File

@ -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.

View File

@ -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());

View File

@ -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;
};

View File

@ -41,7 +41,7 @@ bool WplParser::TryMagic(const QByteArray &data) const {
return data.contains("<?wpl") || data.contains("<smil>");
}
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);
}

View File

@ -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;
};

View File

@ -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) {

View File

@ -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

View File

@ -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<int>::of(&QComboBox::currentIndexChanged), this, &CollectionSettingsPage::CacheSizeUnitChanged);
QObject::connect(ui_->combobox_disk_cache_size, QOverload<int>::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();

View File

@ -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();

View File

@ -92,6 +92,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="song_tracking">
<property name="text">
<string>Song fingerprinting and tracking</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="mark_songs_unavailable">
<property name="text">
@ -99,6 +106,73 @@
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_expire_unavailable_songs_after">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Expire unavailable songs after</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="expire_unavailable_songs_days">
<property name="maximum">
<number>365</number>
</property>
<property name="value">
<number>60</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_expire_unavailable_songs_days">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>days</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_expire_unavailable_songs">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label_preferred_cover_filenames">
<property name="text">