/* This file is part of Clementine. Clementine is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Clementine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Clementine. If not, see . */ #include "librarywatcher.h" #include "librarybackend.h" #include "engines/enginebase.h" #include #include #include #include #include #include #include #include QStringList LibraryWatcher::sValidImages; QStringList LibraryWatcher::sValidPlaylists; LibraryWatcher::LibraryWatcher(QObject* parent) : QObject(parent), stop_requested_(false), rescan_timer_(new QTimer(this)), total_watches_(0) { rescan_timer_->setInterval(1000); rescan_timer_->setSingleShot(true); if (sValidImages.isEmpty()) { sValidImages << "jpg" << "png" << "gif" << "jpeg"; sValidPlaylists << "m3u" << "pls"; } connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow())); } LibraryWatcher::ScanTransaction::ScanTransaction(LibraryWatcher* watcher, int dir, bool incremental) : dir_(dir), incremental_(incremental), watcher_(watcher), cached_songs_dirty_(true), known_subdirs_dirty_(true) { emit watcher_->ScanStarted(); } LibraryWatcher::ScanTransaction::~ScanTransaction() { if (!new_songs.isEmpty()) emit watcher_->NewOrUpdatedSongs(new_songs); if (!touched_songs.isEmpty()) emit watcher_->SongsMTimeUpdated(touched_songs); if (!deleted_songs.isEmpty()) emit watcher_->SongsDeleted(deleted_songs); if (!new_subdirs.isEmpty()) emit watcher_->SubdirsDiscovered(new_subdirs); if (!touched_subdirs.isEmpty()) emit watcher_->SubdirsMTimeUpdated(touched_subdirs); emit watcher_->ScanFinished(); // Watch the new subdirectories foreach (const Subdirectory& subdir, new_subdirs) { watcher_->AddWatch(watcher_->watched_dirs_[dir_].watcher, subdir.path); } } SongList LibraryWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) { if (cached_songs_dirty_) { cached_songs_ = watcher_->backend_->FindSongsInDirectory(dir_); cached_songs_dirty_ = false; } // TODO: Make this faster SongList ret; foreach (const Song& song, cached_songs_) { if (song.filename().section('/', 0, -2) == path) ret << song; } return ret; } void LibraryWatcher::ScanTransaction::SetKnownSubdirs(const SubdirectoryList &subdirs) { known_subdirs_ = subdirs; known_subdirs_dirty_ = false; } bool LibraryWatcher::ScanTransaction::HasSeenSubdir(const QString &path) { if (known_subdirs_dirty_) SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_)); foreach (const Subdirectory& subdir, known_subdirs_) { if (subdir.path == path && subdir.mtime != 0) return true; } return false; } void LibraryWatcher::AddDirectory(const Directory& dir, const SubdirectoryList& subdirs) { DirData data; data.dir = dir; data.watcher = new QFileSystemWatcher(this); connect(data.watcher, SIGNAL(directoryChanged(QString)), SLOT(DirectoryChanged(QString))); watched_dirs_[dir.id] = data; if (subdirs.isEmpty()) { // This is a new directory that we've never seen before. // Scan it fully. ScanTransaction transaction(this, dir.id, false); transaction.SetKnownSubdirs(subdirs); ScanSubdirectory(dir.path, Subdirectory(), &transaction); } 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); transaction.SetKnownSubdirs(subdirs); foreach (const Subdirectory& subdir, subdirs) { ScanSubdirectory(subdir.path, subdir, &transaction); AddWatch(data.watcher, subdir.path); } } backend_->UpdateCompilationsAsync(); } void LibraryWatcher::ScanSubdirectory( const QString& path, const Subdirectory& subdir, ScanTransaction* t, bool force_noincremental) { QFileInfo path_info(path); if (!force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toTime_t()) { // The directory hasn't changed since last time return; } QMap album_art; QStringList files_on_disk; SubdirectoryList my_new_subdirs; // First we "quickly" get a list of the files in the directory that we // think might be music. While we're here, we also look for new subdirectories // and possible album artwork. QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); while (it.hasNext()) { QString child(it.next()); QFileInfo child_info(child); if (child_info.isDir()) { if (!t->HasSeenSubdir(child)) { // We haven't seen this subdirectory before - add it to a list and // later we'll tell the backend about it and scan it. Subdirectory new_subdir; new_subdir.directory_id = -1; new_subdir.path = child; new_subdir.mtime = child_info.lastModified().toTime_t(); my_new_subdirs << new_subdir; } } else { QString ext_part(ExtensionPart(child)); QString dir_part(DirectoryPart(child)); if (sValidImages.contains(ext_part)) album_art[dir_part] << child; else if (engine_->canDecode(QUrl::fromLocalFile(child))) files_on_disk << child; } } if (stop_requested_) return; // Ask the database for a list of files in this directory SongList songs_in_db = t->FindSongsInSubdirectory(path); // Now compare the list from the database with the list of files on disk foreach (const QString& file, files_on_disk) { if (stop_requested_) return; Song matching_song; if (FindSongByPath(songs_in_db, file, &matching_song)) { // The song is in the database and still on disk. // Check the mtime to see if it's been changed since it was added. 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); continue; } bool changed = matching_song.mtime() != file_info.lastModified().toTime_t(); // Also want to look to see whether the album art has changed QString image = ImageForSong(file, album_art); if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || (!matching_song.art_automatic().isEmpty() && !QFile::exists(matching_song.art_automatic()))) { changed = true; } if (changed) { qDebug() << file << "changed"; // It's changed - reread the metadata from the file Song song_on_disk; song_on_disk.InitFromFile(file, t->dir()); if (!song_on_disk.is_valid()) continue; song_on_disk.set_id(matching_song.id()); song_on_disk.set_art_automatic(image); if (!matching_song.IsMetadataEqual(song_on_disk)) { qDebug() << file << "metadata changed"; // Update the song in the DB t->new_songs << song_on_disk; } else { // Only the metadata changed t->touched_songs << song_on_disk; } } } else { // The song is on disk but not in the DB Song song; song.InitFromFile(file, t->dir()); if (!song.is_valid()) continue; qDebug() << file << "created"; // Choose an image for the song song.set_art_automatic(ImageForSong(file, album_art)); t->new_songs << song; } } // Look for deleted songs foreach (const Song& song, songs_in_db) { if (!files_on_disk.contains(song.filename())) { qDebug() << "Song deleted from disk:" << song.filename(); t->deleted_songs << song; } } // Add this subdir to the new or touched list Subdirectory updated_subdir; updated_subdir.directory_id = t->dir(); updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toTime_t() : 0; updated_subdir.path = path; if (subdir.directory_id == -1) t->new_subdirs << updated_subdir; else t->touched_subdirs << updated_subdir; // Recurse into the new subdirs that we found foreach (const Subdirectory& my_new_subdir, my_new_subdirs) { ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true); } } void LibraryWatcher::AddWatch(QFileSystemWatcher* w, const QString& path) { #ifdef Q_OS_DARWIN if (++total_watches_ > kMaxWatches) { qWarning() << "Trying to watch more files than we can manage"; return; } #endif if (!QFile::exists(path)) return; w->addPath(path); } void LibraryWatcher::RemoveDirectory(const Directory& dir) { if (watched_dirs_.contains(dir.id)) { delete watched_dirs_[dir.id].watcher; } rescan_queue_.remove(dir.id); watched_dirs_.remove(dir.id); } bool LibraryWatcher::FindSongByPath(const SongList& list, const QString& path, Song* out) { // TODO: Make this faster foreach (const Song& song, list) { if (song.filename() == path) { *out = song; return true; } } return false; } void LibraryWatcher::DirectoryChanged(const QString &subdir) { // Find what dir it was in QFileSystemWatcher* watcher = qobject_cast(sender()); if (!watcher) return; Directory dir; foreach (const DirData& info, watched_dirs_) { if (info.watcher == watcher) dir = info.dir; } qDebug() << "Subdir" << subdir << "changed under directory" << dir.path << "id" << dir.id; // Queue the subdir for rescanning if (!rescan_queue_[dir.id].contains(subdir)) rescan_queue_[dir.id] << subdir; rescan_timer_->start(); } void LibraryWatcher::RescanPathsNow() { foreach (int dir, rescan_queue_.keys()) { if (stop_requested_) return; ScanTransaction transaction(this, dir, false); foreach (const QString& path, rescan_queue_[dir]) { if (stop_requested_) return; Subdirectory subdir; subdir.directory_id = dir; subdir.mtime = 0; subdir.path = path; ScanSubdirectory(path, subdir, &transaction); } } rescan_queue_.clear(); backend_->UpdateCompilationsAsync(); } QString LibraryWatcher::PickBestImage(const QStringList& images) { // This is used when there is more than one image in a directory. // Just pick the biggest image. int biggest_size = 0; QString biggest_path; foreach (const QString& path, images) { QImage image(path); if (image.isNull()) continue; int size = image.width() * image.height(); if (size > biggest_size) { biggest_size = size; biggest_path = path; } } return biggest_path; } QString LibraryWatcher::ImageForSong(const QString& path, QMap& album_art) { QString dir(DirectoryPart(path)); if (album_art.contains(dir)) { if (album_art[dir].count() == 1) return album_art[dir][0]; else { QString best_image = PickBestImage(album_art[dir]); album_art[dir] = QStringList() << best_image; return best_image; } } return QString(); }