Initial commit.
This commit is contained in:
155
src/collection/collection.cpp
Normal file
155
src/collection/collection.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include "collection.h"
|
||||
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "core/application.h"
|
||||
#include "core/database.h"
|
||||
#include "core/player.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/thread.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
const char *Collection::kSongsTable = "songs";
|
||||
const char *Collection::kDirsTable = "directories";
|
||||
const char *Collection::kSubdirsTable = "subdirectories";
|
||||
const char *Collection::kFtsTable = "songs_fts";
|
||||
|
||||
Collection::Collection(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
backend_(nullptr),
|
||||
model_(nullptr),
|
||||
watcher_(nullptr),
|
||||
watcher_thread_(nullptr) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
backend_ = new CollectionBackend;
|
||||
backend()->moveToThread(app->database()->thread());
|
||||
|
||||
backend_->Init(app->database(), kSongsTable, kDirsTable, kSubdirsTable, kFtsTable);
|
||||
|
||||
model_ = new CollectionModel(backend_, app_, this);
|
||||
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
Collection::~Collection() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
watcher_->deleteLater();
|
||||
watcher_thread_->exit();
|
||||
watcher_thread_->wait(5000 /* five seconds */);
|
||||
}
|
||||
|
||||
void Collection::Init() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
watcher_ = new CollectionWatcher;
|
||||
watcher_thread_ = new Thread(this);
|
||||
watcher_thread_->SetIoPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
watcher_->moveToThread(watcher_thread_);
|
||||
watcher_thread_->start(QThread::IdlePriority);
|
||||
|
||||
watcher_->set_backend(backend_);
|
||||
watcher_->set_task_manager(app_->task_manager());
|
||||
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory, SubdirectoryList)), watcher_, SLOT(AddDirectory(Directory, SubdirectoryList)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)), watcher_, SLOT(RemoveDirectory(Directory)));
|
||||
connect(watcher_, SIGNAL(NewOrUpdatedSongs(SongList)), backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsMTimeUpdated(SongList)), backend_, SLOT(UpdateMTimesOnly(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsDeleted(SongList)), backend_, SLOT(MarkSongsUnavailable(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsReadded(SongList, bool)), backend_, SLOT(MarkSongsUnavailable(SongList, bool)));
|
||||
connect(watcher_, SIGNAL(SubdirsDiscovered(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_, SIGNAL(SubdirsMTimeUpdated(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_, SIGNAL(CompilationsNeedUpdating()), backend_, SLOT(UpdateCompilations()));
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
|
||||
connect(app_->player(), SIGNAL(Stopped()), SLOT(Stopped()));
|
||||
|
||||
// This will start the watcher checking for updates
|
||||
backend_->LoadDirectoriesAsync();
|
||||
}
|
||||
|
||||
void Collection::IncrementalScan() { watcher_->IncrementalScanAsync(); }
|
||||
|
||||
void Collection::FullScan() { watcher_->FullScanAsync(); }
|
||||
|
||||
void Collection::PauseWatcher() { watcher_->SetRescanPausedAsync(true); }
|
||||
|
||||
void Collection::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); }
|
||||
|
||||
void Collection::ReloadSettings() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
watcher_->ReloadSettingsAsync();
|
||||
|
||||
}
|
||||
|
||||
void Collection::Stopped() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
CurrentSongChanged(Song());
|
||||
}
|
||||
|
||||
void Collection::CurrentSongChanged(const Song &song) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
TagReaderReply *reply = nullptr;
|
||||
|
||||
if (reply) {
|
||||
connect(reply, SIGNAL(Finished(bool)), reply, SLOT(deleteLater()));
|
||||
}
|
||||
|
||||
if (song.filetype() == Song::Type_Asf) {
|
||||
current_wma_song_url_ = song.url();
|
||||
}
|
||||
}
|
||||
|
||||
SongList Collection::FilterCurrentWMASong(SongList songs, Song* queued) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
for (SongList::iterator it = songs.begin(); it != songs.end(); ) {
|
||||
if (it->url() == current_wma_song_url_) {
|
||||
*queued = *it;
|
||||
it = songs.erase(it);
|
||||
}
|
||||
else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
return songs;
|
||||
}
|
||||
98
src/collection/collection.h
Normal file
98
src/collection/collection.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTION_H
|
||||
#define COLLECTION_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class Application;
|
||||
class Database;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionWatcher;
|
||||
class TaskManager;
|
||||
class Thread;
|
||||
|
||||
class Collection : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Collection(Application* app, QObject* parent);
|
||||
~Collection();
|
||||
|
||||
static const char *kSongsTable;
|
||||
static const char *kDirsTable;
|
||||
static const char *kSubdirsTable;
|
||||
static const char *kFtsTable;
|
||||
|
||||
void Init();
|
||||
|
||||
CollectionBackend *backend() const { return backend_; }
|
||||
CollectionModel *model() const { return model_; }
|
||||
|
||||
QString full_rescan_reason(int schema_version) const { return full_rescan_revisions_.value(schema_version, QString()); }
|
||||
|
||||
int Total_Albums = 0;
|
||||
int total_songs_ = 0;
|
||||
int Total_Artists = 0;
|
||||
|
||||
public slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void PauseWatcher();
|
||||
void ResumeWatcher();
|
||||
|
||||
void FullScan();
|
||||
|
||||
private slots:
|
||||
void IncrementalScan();
|
||||
|
||||
void CurrentSongChanged(const Song &song);
|
||||
void Stopped();
|
||||
|
||||
private:
|
||||
SongList FilterCurrentWMASong(SongList songs, Song* queued);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
CollectionBackend *backend_;
|
||||
CollectionModel *model_;
|
||||
|
||||
CollectionWatcher *watcher_;
|
||||
Thread *watcher_thread_;
|
||||
|
||||
// Hack: Gstreamer doesn't cope well with WMA files being rewritten while
|
||||
// being played, so we delay statistics and rating changes until the current
|
||||
// song has finished playing.
|
||||
QUrl current_wma_song_url_;
|
||||
|
||||
// DB schema versions which should trigger a full collection rescan (each of
|
||||
// those with a short reason why).
|
||||
QHash<int, QString> full_rescan_revisions_;
|
||||
};
|
||||
|
||||
#endif
|
||||
1132
src/collection/collectionbackend.cpp
Normal file
1132
src/collection/collectionbackend.cpp
Normal file
File diff suppressed because it is too large
Load Diff
232
src/collection/collectionbackend.h
Normal file
232
src/collection/collectionbackend.h
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONBACKEND_H
|
||||
#define COLLECTIONBACKEND_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QUrl>
|
||||
|
||||
#include "directory.h"
|
||||
#include "collectionquery.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class Database;
|
||||
|
||||
class CollectionBackendInterface : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionBackendInterface(QObject *parent = nullptr) : QObject(parent) {}
|
||||
virtual ~CollectionBackendInterface() {}
|
||||
|
||||
struct Album {
|
||||
Album() {}
|
||||
Album(const QString &_artist, const QString &_album_artist, const QString &_album_name, const QString &_art_automatic, const QString &_art_manual, const QUrl &_first_url) :
|
||||
artist(_artist),
|
||||
album_artist(_album_artist),
|
||||
album_name(_album_name),
|
||||
art_automatic(_art_automatic),
|
||||
art_manual(_art_manual),
|
||||
first_url(_first_url) {}
|
||||
|
||||
const QString &effective_albumartist() const {
|
||||
return album_artist.isEmpty() ? artist : album_artist;
|
||||
}
|
||||
|
||||
QString artist;
|
||||
QString album_artist;
|
||||
QString album_name;
|
||||
|
||||
QString art_automatic;
|
||||
QString art_manual;
|
||||
QUrl first_url;
|
||||
};
|
||||
typedef QList<Album> AlbumList;
|
||||
|
||||
virtual QString songs_table() const = 0;
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
virtual void LoadDirectoriesAsync() = 0;
|
||||
|
||||
virtual void UpdateTotalSongCountAsync() = 0;
|
||||
virtual void UpdateTotalArtistCountAsync() = 0;
|
||||
virtual void UpdateTotalAlbumCountAsync() = 0;
|
||||
|
||||
virtual SongList FindSongsInDirectory(int id) = 0;
|
||||
virtual SubdirectoryList SubdirsInDirectory(int id) = 0;
|
||||
virtual DirectoryList GetAllDirectories() = 0;
|
||||
virtual void ChangeDirPath(int id, const QString &old_path, const QString &new_path) = 0;
|
||||
|
||||
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetSongs(const QString &artist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
|
||||
virtual SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
|
||||
virtual AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
|
||||
virtual void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art) = 0;
|
||||
virtual Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) = 0;
|
||||
|
||||
virtual Song GetSongById(int id) = 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) = 0;
|
||||
// Returns a section of a song with the given filename and beginning. If the section
|
||||
// is not present in collection, returns invalid song.
|
||||
// Using default beginning value is suitable when searching for single-section songs.
|
||||
virtual Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) = 0;
|
||||
|
||||
virtual void AddDirectory(const QString &path) = 0;
|
||||
virtual void RemoveDirectory(const Directory &dir) = 0;
|
||||
|
||||
virtual bool ExecQuery(CollectionQuery *q) = 0;
|
||||
};
|
||||
|
||||
class CollectionBackend : public CollectionBackendInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
Q_INVOKABLE CollectionBackend(QObject *parent = nullptr);
|
||||
void Init(Database *db, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table, const QString &fts_table);
|
||||
|
||||
Database *db() const { return db_; }
|
||||
|
||||
QString songs_table() const { return songs_table_; }
|
||||
QString dirs_table() const { return dirs_table_; }
|
||||
QString subdirs_table() const { return subdirs_table_; }
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
void LoadDirectoriesAsync();
|
||||
|
||||
void UpdateTotalSongCountAsync();
|
||||
void UpdateTotalArtistCountAsync();
|
||||
void UpdateTotalAlbumCountAsync();
|
||||
|
||||
SongList FindSongsInDirectory(int id);
|
||||
SubdirectoryList SubdirsInDirectory(int id);
|
||||
DirectoryList GetAllDirectories();
|
||||
void ChangeDirPath(int id, const QString &old_path, const QString &new_path);
|
||||
|
||||
QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions());
|
||||
QStringList GetAllArtists(const QueryOptions &opt = QueryOptions());
|
||||
QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions());
|
||||
SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions());
|
||||
SongList GetSongs(const QString &artist, const QString &album, const QueryOptions &opt = QueryOptions());
|
||||
|
||||
SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions());
|
||||
|
||||
AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetAlbumsByAlbumArtist(const QString &albumartist, const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions());
|
||||
|
||||
void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art);
|
||||
Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album);
|
||||
|
||||
Song GetSongById(int id);
|
||||
SongList GetSongsById(const QList<int> &ids);
|
||||
SongList GetSongsById(const QStringList &ids);
|
||||
SongList GetSongsByForeignId(const QStringList &ids, const QString &table, const QString &column);
|
||||
|
||||
SongList GetSongsByUrl(const QUrl &url);
|
||||
Song GetSongByUrl(const QUrl &url, qint64 beginning = 0);
|
||||
|
||||
void AddDirectory(const QString &path);
|
||||
void RemoveDirectory(const Directory &dir);
|
||||
|
||||
bool ExecQuery(CollectionQuery *q);
|
||||
SongList ExecCollectionQuery(CollectionQuery *query);
|
||||
|
||||
void IncrementPlayCountAsync(int id);
|
||||
void IncrementSkipCountAsync(int id, float progress);
|
||||
void ResetStatisticsAsync(int id);
|
||||
|
||||
void DeleteAll();
|
||||
|
||||
public slots:
|
||||
void LoadDirectories();
|
||||
void UpdateTotalSongCount();
|
||||
void UpdateTotalArtistCount();
|
||||
void UpdateTotalAlbumCount();
|
||||
void AddOrUpdateSongs(const SongList &songs);
|
||||
void UpdateMTimesOnly(const SongList &songs);
|
||||
void DeleteSongs(const SongList &songs);
|
||||
void MarkSongsUnavailable(const SongList &songs, bool unavailable = true);
|
||||
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs);
|
||||
void UpdateCompilations();
|
||||
void UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QString &art);
|
||||
void ForceCompilation(const QString &album, const QList<QString> &artists, bool on);
|
||||
void IncrementPlayCount(int id);
|
||||
void IncrementSkipCount(int id, float progress);
|
||||
void ResetStatistics(int id);
|
||||
|
||||
signals:
|
||||
void DirectoryDiscovered(const Directory &dir, const SubdirectoryList &subdirs);
|
||||
void DirectoryDeleted(const Directory &dir);
|
||||
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
|
||||
void DatabaseReset();
|
||||
|
||||
void TotalSongCountUpdated(int total);
|
||||
void TotalArtistCountUpdated(int total);
|
||||
void TotalAlbumCountUpdated(int total);
|
||||
|
||||
private:
|
||||
struct CompilationInfo {
|
||||
CompilationInfo() : has_compilation_detected(false), has_not_compilation_detected(false) {}
|
||||
|
||||
QSet<QString> artists;
|
||||
QSet<QString> directories;
|
||||
|
||||
bool has_compilation_detected;
|
||||
bool has_not_compilation_detected;
|
||||
};
|
||||
|
||||
void UpdateCompilations(QSqlQuery &find_songs, QSqlQuery &update, SongList &deleted_songs, SongList &added_songs, const QString &album, int compilation_detected);
|
||||
AlbumList GetAlbums(const QString &artist, const QString &album_artist, bool compilation = false, const QueryOptions &opt = QueryOptions());
|
||||
SubdirectoryList SubdirsInDirectory(int id, QSqlDatabase &db);
|
||||
|
||||
Song GetSongById(int id, QSqlDatabase &db);
|
||||
SongList GetSongsById(const QStringList &ids, QSqlDatabase &db);
|
||||
|
||||
private:
|
||||
Database *db_;
|
||||
QString songs_table_;
|
||||
QString dirs_table_;
|
||||
QString subdirs_table_;
|
||||
QString fts_table_;
|
||||
|
||||
};
|
||||
|
||||
#endif // COLLECTIONBACKEND_H
|
||||
|
||||
110
src/collection/collectiondirectorymodel.cpp
Normal file
110
src/collection/collectiondirectorymodel.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "collectiondirectorymodel.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "core/application.h"
|
||||
#include "core/filesystemmusicstorage.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
CollectionDirectoryModel::CollectionDirectoryModel(CollectionBackend* backend, QObject* parent)
|
||||
: QStandardItemModel(parent),
|
||||
dir_icon_(IconLoader::Load("document-open-folder")),
|
||||
backend_(backend)
|
||||
{
|
||||
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory, SubdirectoryList)), SLOT(DirectoryDiscovered(Directory)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)), SLOT(DirectoryDeleted(Directory)));
|
||||
|
||||
}
|
||||
|
||||
CollectionDirectoryModel::~CollectionDirectoryModel() {}
|
||||
|
||||
void CollectionDirectoryModel::DirectoryDiscovered(const Directory &dir) {
|
||||
|
||||
QStandardItem* item;
|
||||
if (Application::kIsPortable && Utilities::UrlOnSameDriveAsStrawberry(QUrl::fromLocalFile(dir.path))) {
|
||||
item = new QStandardItem(Utilities::GetRelativePathToStrawberryBin(QUrl::fromLocalFile(dir.path)).toLocalFile());
|
||||
}
|
||||
else {
|
||||
item = new QStandardItem(dir.path);
|
||||
}
|
||||
item->setData(dir.id, kIdRole);
|
||||
item->setIcon(dir_icon_);
|
||||
storage_ << std::shared_ptr<MusicStorage>(new FilesystemMusicStorage(dir.path));
|
||||
appendRow(item);
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::DirectoryDeleted(const Directory &dir) {
|
||||
|
||||
for (int i = 0; i < rowCount(); ++i) {
|
||||
if (item(i, 0)->data(kIdRole).toInt() == dir.id) {
|
||||
removeRow(i);
|
||||
storage_.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::AddDirectory(const QString &path) {
|
||||
|
||||
if (!backend_) return;
|
||||
|
||||
backend_->AddDirectory(path);
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &index) {
|
||||
|
||||
if (!backend_ || !index.isValid()) return;
|
||||
|
||||
Directory dir;
|
||||
dir.path = index.data().toString();
|
||||
dir.id = index.data(kIdRole).toInt();
|
||||
|
||||
backend_->RemoveDirectory(dir);
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionDirectoryModel::data(const QModelIndex &index, int role) const {
|
||||
|
||||
switch (role) {
|
||||
case MusicStorage::Role_Storage:
|
||||
case MusicStorage::Role_StorageForceConnect:
|
||||
return QVariant::fromValue(storage_[index.row()]);
|
||||
|
||||
case MusicStorage::Role_FreeSpace:
|
||||
return Utilities::FileSystemFreeSpace(data(index, Qt::DisplayRole).toString());
|
||||
|
||||
case MusicStorage::Role_Capacity:
|
||||
return Utilities::FileSystemCapacity(data(index, Qt::DisplayRole).toString());
|
||||
|
||||
default:
|
||||
return QStandardItemModel::data(index, role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
62
src/collection/collectiondirectorymodel.h
Normal file
62
src/collection/collectiondirectorymodel.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONDIRECTORYMODEL_H
|
||||
#define COLLECTIONDIRECTORYMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
#include "directory.h"
|
||||
|
||||
class CollectionBackend;
|
||||
class MusicStorage;
|
||||
|
||||
class CollectionDirectoryModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionDirectoryModel(CollectionBackend* backend, QObject *parent = nullptr);
|
||||
~CollectionDirectoryModel();
|
||||
|
||||
// To be called by GUIs
|
||||
void AddDirectory(const QString &path);
|
||||
void RemoveDirectory(const QModelIndex &index);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const;
|
||||
|
||||
private slots:
|
||||
// To be called by the backend
|
||||
void DirectoryDiscovered(const Directory &directories);
|
||||
void DirectoryDeleted(const Directory &directories);
|
||||
|
||||
private:
|
||||
static const int kIdRole = Qt::UserRole + 1;
|
||||
|
||||
QIcon dir_icon_;
|
||||
CollectionBackend* backend_;
|
||||
QList<std::shared_ptr<MusicStorage> > storage_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONDIRECTORYMODEL_H
|
||||
364
src/collection/collectionfilterwidget.cpp
Normal file
364
src/collection/collectionfilterwidget.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QRegExp>
|
||||
#include <QSettings>
|
||||
#include <QSignalMapper>
|
||||
#include <QTimer>
|
||||
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionquery.h"
|
||||
#include "groupbydialog.h"
|
||||
#include "ui_collectionfilterwidget.h"
|
||||
#include "core/song.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
|
||||
CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_CollectionFilterWidget),
|
||||
model_(nullptr),
|
||||
group_by_dialog_(new GroupByDialog),
|
||||
filter_delay_(new QTimer(this)),
|
||||
filter_applies_to_model_(true),
|
||||
delay_behaviour_(DelayedOnLargeLibraries) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Add the available fields to the tooltip here instead of the ui
|
||||
// file to prevent that they get translated by mistake.
|
||||
QString available_fields = Song::kFtsColumns.join(", ").replace(QRegExp("\\bfts"), "");
|
||||
ui_->filter->setToolTip(ui_->filter->toolTip().arg(available_fields));
|
||||
|
||||
connect(ui_->filter, SIGNAL(returnPressed()), SIGNAL(ReturnPressed()));
|
||||
connect(filter_delay_, SIGNAL(timeout()), SLOT(FilterDelayTimeout()));
|
||||
|
||||
filter_delay_->setInterval(kFilterDelay);
|
||||
filter_delay_->setSingleShot(true);
|
||||
|
||||
// Icons
|
||||
ui_->options->setIcon(IconLoader::Load("configure"));
|
||||
|
||||
// Filter by age
|
||||
QActionGroup *filter_age_group = new QActionGroup(this);
|
||||
filter_age_group->addAction(ui_->filter_age_all);
|
||||
filter_age_group->addAction(ui_->filter_age_today);
|
||||
filter_age_group->addAction(ui_->filter_age_week);
|
||||
filter_age_group->addAction(ui_->filter_age_month);
|
||||
filter_age_group->addAction(ui_->filter_age_three_months);
|
||||
filter_age_group->addAction(ui_->filter_age_year);
|
||||
|
||||
filter_age_menu_ = new QMenu(tr("Show"), this);
|
||||
filter_age_menu_->addActions(filter_age_group->actions());
|
||||
|
||||
filter_age_mapper_ = new QSignalMapper(this);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_all, -1);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_today, 60 * 60 * 24);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_week, 60 * 60 * 24 * 7);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_month, 60 * 60 * 24 * 30);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_three_months, 60 * 60 * 24 * 30 * 3);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_year, 60 * 60 * 24 * 365);
|
||||
|
||||
connect(ui_->filter_age_all, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_today, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_week, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_month, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_three_months, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_year, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
|
||||
// "Group by ..."
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
|
||||
group_by_menu_ = new QMenu(tr("Group by"), this);
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
|
||||
connect(ui_->save_grouping, SIGNAL(triggered()), this, SLOT(SaveGroupBy()));
|
||||
connect(ui_->manage_groupings, SIGNAL(triggered()), this, SLOT(ShowGroupingManager()));
|
||||
|
||||
// Collection config menu
|
||||
collection_menu_ = new QMenu(tr("Display options"), this);
|
||||
collection_menu_->setIcon(ui_->options->icon());
|
||||
collection_menu_->addMenu(filter_age_menu_);
|
||||
collection_menu_->addMenu(group_by_menu_);
|
||||
collection_menu_->addAction(ui_->save_grouping);
|
||||
collection_menu_->addAction(ui_->manage_groupings);
|
||||
collection_menu_->addSeparator();
|
||||
ui_->options->setMenu(collection_menu_);
|
||||
|
||||
connect(ui_->filter, SIGNAL(textChanged(QString)), SLOT(FilterTextChanged(QString)));
|
||||
|
||||
}
|
||||
|
||||
CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
|
||||
|
||||
void CollectionFilterWidget::UpdateGroupByActions() {
|
||||
|
||||
if (group_by_group_) {
|
||||
disconnect(group_by_group_, 0, 0, 0);
|
||||
delete group_by_group_;
|
||||
}
|
||||
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
group_by_menu_->clear();
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)),
|
||||
SLOT(GroupByClicked(QAction*)));
|
||||
if (model_) {
|
||||
CheckCurrentGrouping(model_->GetGroupBy());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
QActionGroup *CollectionFilterWidget::CreateGroupByActions(QObject *parent) {
|
||||
|
||||
QActionGroup *ret = new QActionGroup(parent);
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbum)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
|
||||
|
||||
QAction *sep1 = new QAction(parent);
|
||||
sep1->setSeparator(true);
|
||||
ret->addAction(sep1);
|
||||
|
||||
// read saved groupings
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
||||
}
|
||||
|
||||
QAction *sep2 = new QAction(parent);
|
||||
sep2->setSeparator(true);
|
||||
ret->addAction(sep2);
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Advanced grouping..."), parent, CollectionModel::Grouping()));
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObject *parent, const CollectionModel::Grouping &grouping) {
|
||||
|
||||
QAction *ret = new QAction(text, parent);
|
||||
ret->setCheckable(true);
|
||||
|
||||
if (grouping.first != CollectionModel::GroupBy_None) {
|
||||
ret->setProperty("group_by", QVariant::fromValue(grouping));
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SaveGroupBy() {
|
||||
|
||||
QString text = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (!text.isEmpty() && model_) {
|
||||
model_->SaveGrouping(text);
|
||||
UpdateGroupByActions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ShowGroupingManager() {
|
||||
|
||||
if (!groupings_manager_) {
|
||||
groupings_manager_.reset(new SavedGroupingManager);
|
||||
}
|
||||
groupings_manager_->SetFilter(this);
|
||||
groupings_manager_->UpdateModel();
|
||||
groupings_manager_->show();
|
||||
|
||||
}
|
||||
|
||||
|
||||
void CollectionFilterWidget::FocusOnFilter(QKeyEvent *event) {
|
||||
|
||||
ui_->filter->setFocus();
|
||||
QApplication::sendEvent(ui_->filter, event);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetCollectionModel(CollectionModel *model) {
|
||||
|
||||
if (model_) {
|
||||
disconnect(model_, 0, this, 0);
|
||||
disconnect(model_, 0, group_by_dialog_.get(), 0);
|
||||
disconnect(group_by_dialog_.get(), 0, model_, 0);
|
||||
disconnect(filter_age_mapper_, 0, model_, 0);
|
||||
}
|
||||
|
||||
model_ = model;
|
||||
|
||||
// Connect signals
|
||||
connect(model_, SIGNAL(GroupingChanged(CollectionModel::Grouping)), group_by_dialog_.get(), SLOT(CollectionGroupingChanged(CollectionModel::Grouping)));
|
||||
connect(model_, SIGNAL(GroupingChanged(CollectionModel::Grouping)), SLOT(GroupingChanged(CollectionModel::Grouping)));
|
||||
connect(group_by_dialog_.get(), SIGNAL(Accepted(CollectionModel::Grouping)), model_, SLOT(SetGroupBy(CollectionModel::Grouping)));
|
||||
connect(filter_age_mapper_, SIGNAL(mapped(int)), model_, SLOT(SetFilterAge(int)));
|
||||
|
||||
// Load settings
|
||||
if (!settings_group_.isEmpty()) {
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
model_->SetGroupBy(CollectionModel::Grouping(
|
||||
CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_Artist)).toInt()),
|
||||
CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()),
|
||||
CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::GroupByClicked(QAction *action) {
|
||||
if (action->property("group_by").isNull()) {
|
||||
group_by_dialog_->show();
|
||||
return;
|
||||
}
|
||||
|
||||
CollectionModel::Grouping g = action->property("group_by").value<CollectionModel::Grouping>();
|
||||
model_->SetGroupBy(g);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping &g) {
|
||||
|
||||
if (!settings_group_.isEmpty()) {
|
||||
// Save the settings
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
s.setValue("group_by1", int(g[0]));
|
||||
s.setValue("group_by2", int(g[1]));
|
||||
s.setValue("group_by3", int(g[2]));
|
||||
}
|
||||
|
||||
// Now make sure the correct action is checked
|
||||
CheckCurrentGrouping(g);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Grouping &g) {
|
||||
|
||||
for (QAction *action : group_by_group_->actions()) {
|
||||
if (action->property("group_by").isNull()) continue;
|
||||
|
||||
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
|
||||
action->setChecked(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the advanced action
|
||||
group_by_group_->actions().last()->setChecked(true);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetFilterHint(const QString &hint) {
|
||||
ui_->filter->setPlaceholderText(hint);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetQueryMode(QueryOptions::QueryMode query_mode) {
|
||||
|
||||
ui_->filter->clear();
|
||||
ui_->filter->setEnabled(query_mode == QueryOptions::QueryMode_All);
|
||||
|
||||
model_->SetFilterQueryMode(query_mode);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ShowInCollection(const QString &search) {
|
||||
ui_->filter->setText(search);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetAgeFilterEnabled(bool enabled) {
|
||||
filter_age_menu_->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetGroupByEnabled(bool enabled) {
|
||||
group_by_menu_->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::AddMenuAction(QAction *action) {
|
||||
collection_menu_->addAction(action);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
|
||||
|
||||
switch (e->key()) {
|
||||
case Qt::Key_Up:
|
||||
emit UpPressed();
|
||||
e->accept();
|
||||
break;
|
||||
|
||||
case Qt::Key_Down:
|
||||
emit DownPressed();
|
||||
e->accept();
|
||||
break;
|
||||
|
||||
case Qt::Key_Escape:
|
||||
ui_->filter->clear();
|
||||
e->accept();
|
||||
break;
|
||||
}
|
||||
|
||||
QWidget::keyReleaseEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
||||
|
||||
// Searching with one or two characters can be very expensive on the database
|
||||
// even with FTS, so if there are a large number of songs in the database
|
||||
// introduce a small delay before actually filtering the model, so if the
|
||||
// user is typing the first few characters of something it will be quicker.
|
||||
const bool delay = (delay_behaviour_ == AlwaysDelayed) || (delay_behaviour_ == DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
|
||||
|
||||
if (delay) {
|
||||
filter_delay_->start();
|
||||
}
|
||||
else {
|
||||
filter_delay_->stop();
|
||||
FilterDelayTimeout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::FilterDelayTimeout() {
|
||||
|
||||
emit Filter(ui_->filter->text());
|
||||
if (filter_applies_to_model_) {
|
||||
model_->SetFilterText(ui_->filter->text());
|
||||
}
|
||||
|
||||
}
|
||||
123
src/collection/collectionfilterwidget.h
Normal file
123
src/collection/collectionfilterwidget.h
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONFILTERWIDGET_H
|
||||
#define COLLECTIONFILTERWIDGET_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "collectionmodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
|
||||
class GroupByDialog;
|
||||
class SettingsDialog;
|
||||
class Ui_CollectionFilterWidget;
|
||||
|
||||
struct QueryOptions;
|
||||
|
||||
class QMenu;
|
||||
class QActionGroup;
|
||||
class QSignalMapper;
|
||||
|
||||
class CollectionFilterWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionFilterWidget(QWidget *parent = nullptr);
|
||||
~CollectionFilterWidget();
|
||||
|
||||
static const int kFilterDelay = 500; // msec
|
||||
|
||||
enum DelayBehaviour {
|
||||
AlwaysInstant,
|
||||
DelayedOnLargeLibraries,
|
||||
AlwaysDelayed,
|
||||
};
|
||||
|
||||
static QActionGroup *CreateGroupByActions(QObject *parent);
|
||||
|
||||
void UpdateGroupByActions();
|
||||
void SetFilterHint(const QString &hint);
|
||||
void SetApplyFilterToCollection(bool filter_applies_to_model) { filter_applies_to_model_ = filter_applies_to_model; }
|
||||
void SetDelayBehaviour(DelayBehaviour behaviour) { delay_behaviour_ = behaviour; }
|
||||
void SetAgeFilterEnabled(bool enabled);
|
||||
void SetGroupByEnabled(bool enabled);
|
||||
void ShowInCollection(const QString &search);
|
||||
|
||||
QMenu *menu() const { return collection_menu_; }
|
||||
void AddMenuAction(QAction *action);
|
||||
|
||||
void SetSettingsGroup(const QString &group) { settings_group_ = group; }
|
||||
void SetCollectionModel(CollectionModel *model);
|
||||
|
||||
public slots:
|
||||
void SetQueryMode(QueryOptions::QueryMode view);
|
||||
void FocusOnFilter(QKeyEvent *e);
|
||||
|
||||
signals:
|
||||
void UpPressed();
|
||||
void DownPressed();
|
||||
void ReturnPressed();
|
||||
void Filter(const QString &text);
|
||||
|
||||
protected:
|
||||
void keyReleaseEvent(QKeyEvent *e);
|
||||
|
||||
private slots:
|
||||
void GroupingChanged(const CollectionModel::Grouping &g);
|
||||
void GroupByClicked(QAction *action);
|
||||
void SaveGroupBy();
|
||||
void ShowGroupingManager();
|
||||
|
||||
void FilterTextChanged(const QString &text);
|
||||
void FilterDelayTimeout();
|
||||
|
||||
private:
|
||||
static QAction *CreateGroupByAction(const QString &text, QObject *parent, const CollectionModel::Grouping &grouping);
|
||||
void CheckCurrentGrouping(const CollectionModel::Grouping &g);
|
||||
|
||||
private:
|
||||
Ui_CollectionFilterWidget *ui_;
|
||||
CollectionModel *model_;
|
||||
|
||||
std::unique_ptr<GroupByDialog> group_by_dialog_;
|
||||
std::unique_ptr<SavedGroupingManager> groupings_manager_;
|
||||
SettingsDialog *settings_dialog_;
|
||||
|
||||
QMenu *filter_age_menu_;
|
||||
QMenu *group_by_menu_;
|
||||
QMenu *collection_menu_;
|
||||
QActionGroup *group_by_group_;
|
||||
QSignalMapper *filter_age_mapper_;
|
||||
|
||||
QTimer *filter_delay_;
|
||||
|
||||
bool filter_applies_to_model_;
|
||||
DelayBehaviour delay_behaviour_;
|
||||
|
||||
QString settings_group_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTERWIDGET_H
|
||||
|
||||
121
src/collection/collectionfilterwidget.ui
Normal file
121
src/collection/collectionfilterwidget.ui
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CollectionFilterWidget</class>
|
||||
<widget class="QWidget" name="CollectionFilterWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>30</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSearchField" name="filter" native="true">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Prefix a word with a field name to limit the search to that field, e.g. <span style=" font-weight:600;">artist:</span><span style=" font-style:italic;">Bode</span> searches the collection for all artists that contain the word Bode.</p><p><span style=" font-weight:600;">Available fields: </span><span style=" font-style:italic;">%1</span>.</p></body></html></string>
|
||||
</property>
|
||||
<property name="placeholderText" stdset="0">
|
||||
<string>Enter search terms here</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="options">
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::InstantPopup</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<action name="filter_age_all">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Entire collection</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_today">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added today</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_week">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added this week</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_three_months">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added within three months</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Added within three months</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_year">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added this year</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_month">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added this month</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="save_grouping">
|
||||
<property name="text">
|
||||
<string>Save current grouping</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="manage_groupings">
|
||||
<property name="text">
|
||||
<string>Manage saved groupings</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QSearchField</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>3rdparty/qocoa/qsearchfield.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
58
src/collection/collectionitem.h
Normal file
58
src/collection/collectionitem.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONITEM_H
|
||||
#define COLLECTIONITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
#include "core/simpletreeitem.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class CollectionItem : public SimpleTreeItem<CollectionItem> {
|
||||
public:
|
||||
enum Type {
|
||||
Type_Root,
|
||||
Type_Divider,
|
||||
Type_Container,
|
||||
Type_Song,
|
||||
Type_PlaylistContainer,
|
||||
Type_LoadingIndicator,
|
||||
};
|
||||
|
||||
CollectionItem(SimpleTreeModel<CollectionItem> *model)
|
||||
: SimpleTreeItem<CollectionItem>(Type_Root, model),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
CollectionItem(Type type, CollectionItem *parent = nullptr)
|
||||
: SimpleTreeItem<CollectionItem>(type, parent),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
int container_level;
|
||||
Song metadata;
|
||||
CollectionItem *compilation_artist_node_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONITEM_H
|
||||
1522
src/collection/collectionmodel.cpp
Normal file
1522
src/collection/collectionmodel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
284
src/collection/collectionmodel.h
Normal file
284
src/collection/collectionmodel.h
Normal file
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONMODEL_H
|
||||
#define COLLECTIONMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QIcon>
|
||||
#include <QNetworkDiskCache>
|
||||
|
||||
#include "collectionitem.h"
|
||||
#include "collectionquery.h"
|
||||
#include "collectionwatcher.h"
|
||||
#include "sqlrow.h"
|
||||
#include "core/simpletreemodel.h"
|
||||
#include "core/song.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "engine/engine_fwd.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
class Application;
|
||||
class AlbumCoverLoader;
|
||||
class CollectionDirectoryModel;
|
||||
class CollectionBackend;
|
||||
|
||||
class QSettings;
|
||||
|
||||
class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
Q_OBJECT
|
||||
Q_ENUMS(GroupBy);
|
||||
|
||||
public:
|
||||
CollectionModel(CollectionBackend *backend, Application *app, QObject *parent = nullptr);
|
||||
~CollectionModel();
|
||||
|
||||
static const char *kSavedGroupingsSettingsGroup;
|
||||
|
||||
static const int kPrettyCoverSize;
|
||||
static const qint64 kIconCacheSize;
|
||||
|
||||
enum Role {
|
||||
Role_Type = Qt::UserRole + 1,
|
||||
Role_ContainerType,
|
||||
Role_SortText,
|
||||
Role_Key,
|
||||
Role_Artist,
|
||||
Role_IsDivider,
|
||||
Role_Editable,
|
||||
LastRole
|
||||
};
|
||||
|
||||
// These values get saved in QSettings - don't change them
|
||||
enum GroupBy {
|
||||
GroupBy_None = 0,
|
||||
GroupBy_Artist = 1,
|
||||
GroupBy_Album = 2,
|
||||
GroupBy_YearAlbum = 3,
|
||||
GroupBy_Year = 4,
|
||||
GroupBy_Composer = 5,
|
||||
GroupBy_Genre = 6,
|
||||
GroupBy_AlbumArtist = 7,
|
||||
GroupBy_FileType = 8,
|
||||
GroupBy_Performer = 9,
|
||||
GroupBy_Grouping = 10,
|
||||
GroupBy_Bitrate = 11,
|
||||
GroupBy_Disc = 12,
|
||||
GroupBy_OriginalYearAlbum = 13,
|
||||
GroupBy_OriginalYear = 14,
|
||||
};
|
||||
|
||||
struct Grouping {
|
||||
Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None, GroupBy t = GroupBy_None)
|
||||
: first(f), second(s), third(t) {}
|
||||
|
||||
GroupBy first;
|
||||
GroupBy second;
|
||||
GroupBy third;
|
||||
|
||||
const GroupBy &operator[](int i) const;
|
||||
GroupBy &operator[](int i);
|
||||
bool operator==(const Grouping &other) const {
|
||||
return first == other.first && second == other.second && third == other.third;
|
||||
}
|
||||
bool operator!=(const Grouping &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
struct QueryResult {
|
||||
QueryResult() : create_va(false) {}
|
||||
|
||||
SqlRowList rows;
|
||||
bool create_va;
|
||||
};
|
||||
|
||||
CollectionBackend *backend() const { return backend_; }
|
||||
CollectionDirectoryModel *directory_model() const { return dir_model_; }
|
||||
|
||||
// Call before Init()
|
||||
void set_show_various_artists(bool show_various_artists) { show_various_artists_ = show_various_artists; }
|
||||
|
||||
// Get information about the collection
|
||||
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex &index) const;
|
||||
SongList GetChildSongs(const QModelIndexList &indexes) const;
|
||||
|
||||
// Might be accurate
|
||||
int total_song_count() const { return total_song_count_; }
|
||||
int total_artist_count() const { return total_artist_count_; }
|
||||
int total_album_count() const { return total_album_count_; }
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const;
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const;
|
||||
bool canFetchMore(const QModelIndex &parent) const;
|
||||
|
||||
// Whether or not to use album cover art, if it exists, in the collection view
|
||||
void set_pretty_covers(bool use_pretty_covers);
|
||||
bool use_pretty_covers() const { return use_pretty_covers_; }
|
||||
|
||||
// Whether or not to show letters heading in the collection view
|
||||
void set_show_dividers(bool show_dividers);
|
||||
|
||||
// Save the current grouping
|
||||
void SaveGrouping(QString name);
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(int year, const QString &album);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForNumber(int year);
|
||||
static QString SortTextForArtist(QString artist);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(int year);
|
||||
static QString SortTextForBitrate(int bitrate);
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void TotalArtistCountUpdated(int count);
|
||||
void TotalAlbumCountUpdated(int count);
|
||||
void GroupingChanged(const CollectionModel::Grouping &g);
|
||||
|
||||
public slots:
|
||||
void SetFilterAge(int age);
|
||||
void SetFilterText(const QString &text);
|
||||
void SetFilterQueryMode(QueryOptions::QueryMode query_mode);
|
||||
|
||||
void SetGroupBy(const CollectionModel::Grouping &g);
|
||||
const CollectionModel::Grouping &GetGroupBy() const { return group_by_; }
|
||||
void Init(bool async = true);
|
||||
void Reset();
|
||||
void ResetAsync();
|
||||
|
||||
protected:
|
||||
void LazyPopulate(CollectionItem *item) { LazyPopulate(item, true); }
|
||||
void LazyPopulate(CollectionItem *item, bool signal);
|
||||
|
||||
private slots:
|
||||
// From CollectionBackend
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsSlightlyChanged(const SongList &songs);
|
||||
void TotalSongCountUpdatedSlot(int count);
|
||||
void TotalArtistCountUpdatedSlot(int count);
|
||||
void TotalAlbumCountUpdatedSlot(int count);
|
||||
|
||||
// Called after ResetAsync
|
||||
void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future);
|
||||
|
||||
void AlbumArtLoaded(quint64 id, const QImage &image);
|
||||
|
||||
private:
|
||||
// Provides some optimisations for loading the list of items in the root.
|
||||
// This gets called a lot when filtering the playlist, so it's nice to be
|
||||
// able to do it in a background thread.
|
||||
QueryResult RunQuery(CollectionItem *parent);
|
||||
void PostQuery(CollectionItem *parent, const QueryResult &result, bool signal);
|
||||
|
||||
bool HasCompilations(const CollectionQuery &query);
|
||||
|
||||
void BeginReset();
|
||||
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Collection
|
||||
// constructs a database query to populate the items. Filters are added
|
||||
// for each parent item, restricting the songs returned to a particular
|
||||
// album or artist for example.
|
||||
static void InitQuery(GroupBy type, CollectionQuery *q);
|
||||
void FilterQuery(GroupBy type, CollectionItem *item, CollectionQuery *q);
|
||||
|
||||
// Items can be created either from a query that's been run to populate a
|
||||
// node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
CollectionItem *ItemFromQuery(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const SqlRow &row, int container_level);
|
||||
CollectionItem *ItemFromSong(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const Song &s, int container_level);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
CollectionItem *CreateCompilationArtistNode(bool signal, CollectionItem *parent);
|
||||
|
||||
// Smart playlists are shown in another top-level node
|
||||
|
||||
void ItemFromSmartPlaylist(const QSettings &s, bool notify) const;
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
CollectionItem *InitItem(GroupBy type, bool signal, CollectionItem *parent, int container_level);
|
||||
void FinishItem(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
|
||||
QString DividerKey(GroupBy type, CollectionItem *item) const;
|
||||
QString DividerDisplayText(GroupBy type, const QString &key) const;
|
||||
|
||||
// Helpers
|
||||
QString AlbumIconPixmapCacheKey(const QModelIndex &index) const;
|
||||
QVariant AlbumIcon(const QModelIndex &index);
|
||||
QVariant data(const CollectionItem *item, int role) const;
|
||||
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
|
||||
|
||||
private:
|
||||
CollectionBackend *backend_;
|
||||
Application *app_;
|
||||
CollectionDirectoryModel *dir_model_;
|
||||
bool show_various_artists_;
|
||||
|
||||
int total_song_count_;
|
||||
int total_artist_count_;
|
||||
int total_album_count_;
|
||||
|
||||
QueryOptions query_options_;
|
||||
Grouping group_by_;
|
||||
|
||||
// Keyed on database ID
|
||||
QMap<int, CollectionItem*> song_nodes_;
|
||||
|
||||
// Keyed on whatever the key is for that level - artist, album, year, etc.
|
||||
QMap<QString, CollectionItem*> container_nodes_[3];
|
||||
|
||||
// Keyed on a letter, a year, a century, etc.
|
||||
QMap<QString, CollectionItem*> divider_nodes_;
|
||||
|
||||
QIcon artist_icon_;
|
||||
QIcon album_icon_;
|
||||
// used as a generic icon to show when no cover art is found,
|
||||
// fixed to the same size as the artwork (32x32)
|
||||
QPixmap no_cover_icon_;
|
||||
QIcon playlists_dir_icon_;
|
||||
QIcon playlist_icon_;
|
||||
|
||||
QNetworkDiskCache *icon_cache_;
|
||||
|
||||
int init_task_id_;
|
||||
|
||||
bool use_pretty_covers_;
|
||||
bool show_dividers_;
|
||||
|
||||
AlbumCoverLoaderOptions cover_loader_options_;
|
||||
|
||||
typedef QPair<CollectionItem*, QString> ItemAndCacheKey;
|
||||
QMap<quint64, ItemAndCacheKey> pending_art_;
|
||||
QSet<QString> pending_cache_keys_;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(CollectionModel::Grouping);
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping &g);
|
||||
QDataStream &operator>>(QDataStream &s, CollectionModel::Grouping &g);
|
||||
|
||||
#endif // COLLECTIONMODEL_H
|
||||
58
src/collection/collectionplaylistitem.cpp
Normal file
58
src/collection/collectionplaylistitem.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "collectionplaylistitem.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const QString &type)
|
||||
: PlaylistItem(type) {}
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song)
|
||||
: PlaylistItem("Collection"), song_(song) {}
|
||||
|
||||
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
||||
|
||||
void CollectionPlaylistItem::Reload() {
|
||||
TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
|
||||
}
|
||||
|
||||
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||
// Rows from the songs tables come first
|
||||
song_.InitFromQuery(query, true);
|
||||
|
||||
return song_.is_valid();
|
||||
}
|
||||
|
||||
QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const {
|
||||
switch (column) {
|
||||
case Column_CollectionId: return song_.id();
|
||||
default: return PlaylistItem::DatabaseValue(column);
|
||||
}
|
||||
}
|
||||
|
||||
Song CollectionPlaylistItem::Metadata() const {
|
||||
if (HasTemporaryMetadata()) return temp_metadata_;
|
||||
return song_;
|
||||
}
|
||||
|
||||
52
src/collection/collectionplaylistitem.h
Normal file
52
src/collection/collectionplaylistitem.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONPLAYLISTITEM_H
|
||||
#define COLLECTIONPLAYLISTITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
class CollectionPlaylistItem : public PlaylistItem {
|
||||
public:
|
||||
CollectionPlaylistItem(const QString &type);
|
||||
CollectionPlaylistItem(const Song &song);
|
||||
|
||||
bool InitFromQuery(const SqlRow &query);
|
||||
void Reload();
|
||||
|
||||
Song Metadata() const;
|
||||
void SetMetadata(const Song &song) { song_ = song; }
|
||||
|
||||
QUrl Url() const;
|
||||
|
||||
bool IsLocalCollectionItem() const { return true; }
|
||||
|
||||
protected:
|
||||
QVariant DatabaseValue(DatabaseColumn column) const;
|
||||
|
||||
protected:
|
||||
Song song_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONPLAYLISTITEM_H
|
||||
|
||||
204
src/collection/collectionquery.cpp
Normal file
204
src/collection/collectionquery.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "collectionquery.h"
|
||||
#include "core/song.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QDateTime>
|
||||
#include <QSqlError>
|
||||
|
||||
QueryOptions::QueryOptions() : max_age_(-1), query_mode_(QueryMode_All) {}
|
||||
|
||||
CollectionQuery::CollectionQuery(const QueryOptions& options)
|
||||
: include_unavailable_(false), join_with_fts_(false), limit_(-1) {
|
||||
|
||||
if (!options.filter().isEmpty()) {
|
||||
// We need to munge the filter text a little bit to get it to work as
|
||||
// expected with sqlite's FTS3:
|
||||
// 1) Append * to all tokens.
|
||||
// 2) Prefix "fts" to column names.
|
||||
// 3) Remove colons which don't correspond to column names.
|
||||
|
||||
// Split on whitespace
|
||||
QStringList tokens(options.filter().split(QRegExp("\\s+"), QString::SkipEmptyParts));
|
||||
QString query;
|
||||
for (QString token : tokens) {
|
||||
token.remove('(');
|
||||
token.remove(')');
|
||||
token.remove('"');
|
||||
token.replace('-', ' ');
|
||||
|
||||
if (token.contains(':')) {
|
||||
// Only prefix fts if the token is a valid column name.
|
||||
if (Song::kFtsColumns.contains("fts" + token.section(':', 0, 0),
|
||||
Qt::CaseInsensitive)) {
|
||||
// Account for multiple colons.
|
||||
QString columntoken = token.section(':', 0, 0, QString::SectionIncludeTrailingSep);
|
||||
QString subtoken = token.section(':', 1, -1);
|
||||
subtoken.replace(":", " ");
|
||||
subtoken = subtoken.trimmed();
|
||||
query += "fts" + columntoken + subtoken + "* ";
|
||||
}
|
||||
else {
|
||||
token.replace(":", " ");
|
||||
token = token.trimmed();
|
||||
query += token + "* ";
|
||||
}
|
||||
}
|
||||
else {
|
||||
query += token + "* ";
|
||||
}
|
||||
}
|
||||
|
||||
where_clauses_ << "fts.%fts_table_noprefix MATCH ?";
|
||||
bound_values_ << query;
|
||||
join_with_fts_ = true;
|
||||
}
|
||||
|
||||
if (options.max_age() != -1) {
|
||||
int cutoff = QDateTime::currentDateTime().toTime_t() - options.max_age();
|
||||
|
||||
where_clauses_ << "ctime > ?";
|
||||
bound_values_ << cutoff;
|
||||
}
|
||||
|
||||
// TODO: currently you cannot use any QueryMode other than All and fts at the
|
||||
// same time.
|
||||
// joining songs, duplicated_songs and songs_fts all together takes a huge
|
||||
// amount of
|
||||
// time. the query takes about 20 seconds on my machine then. why?
|
||||
// untagged mode could work with additional filtering but I'm disabling it
|
||||
// just to be
|
||||
// consistent - this way filtering is available only in the All mode.
|
||||
// remember though that when you fix the Duplicates + FTS cooperation, enable
|
||||
// the filtering in both Duplicates and Untagged modes.
|
||||
duplicates_only_ = options.query_mode() == QueryOptions::QueryMode_Duplicates;
|
||||
|
||||
if (options.query_mode() == QueryOptions::QueryMode_Untagged) {
|
||||
where_clauses_ << "(artist = '' OR album = '' OR title ='')";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString CollectionQuery::GetInnerQuery() {
|
||||
return duplicates_only_
|
||||
? QString(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
: QString();
|
||||
}
|
||||
|
||||
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||
|
||||
// ignore 'literal' for IN
|
||||
if (!op.compare("IN", Qt::CaseInsensitive)) {
|
||||
QStringList final;
|
||||
for (const QString& single_value : value.toStringList()) {
|
||||
final.append("?");
|
||||
bound_values_ << single_value;
|
||||
}
|
||||
|
||||
where_clauses_ << QString("%1 IN (" + final.join(",") + ")").arg(column);
|
||||
}
|
||||
else {
|
||||
// Do integers inline - sqlite seems to get confused when you pass integers
|
||||
// to bound parameters
|
||||
if (value.type() == QVariant::Int) {
|
||||
where_clauses_ << QString("%1 %2 %3").arg(column, op, value.toString());
|
||||
}
|
||||
else {
|
||||
where_clauses_ << QString("%1 %2 ?").arg(column, op);
|
||||
bound_values_ << value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionQuery::AddCompilationRequirement(bool compilation) {
|
||||
// The unary + is added to prevent sqlite from using the index
|
||||
// idx_comp_artist. When joining with fts, sqlite 3.8 has a tendency
|
||||
// to use this index and thereby nesting the tables in an order
|
||||
// which gives very poor performance
|
||||
|
||||
where_clauses_ << QString("+compilation_effective = %1").arg(compilation ? 1 : 0);
|
||||
|
||||
}
|
||||
|
||||
QSqlQuery CollectionQuery::Exec(QSqlDatabase db, const QString &songs_table, const QString &fts_table) {
|
||||
|
||||
QString sql;
|
||||
|
||||
if (join_with_fts_) {
|
||||
sql = QString("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table, fts_table);
|
||||
}
|
||||
else {
|
||||
sql = QString("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table, GetInnerQuery());
|
||||
}
|
||||
|
||||
QStringList where_clauses(where_clauses_);
|
||||
if (!include_unavailable_) {
|
||||
where_clauses << "unavailable = 0";
|
||||
}
|
||||
|
||||
if (!where_clauses.isEmpty()) sql += " WHERE " + where_clauses.join(" AND ");
|
||||
|
||||
if (!order_by_.isEmpty()) sql += " ORDER BY " + order_by_;
|
||||
|
||||
if (limit_ != -1) sql += " LIMIT " + QString::number(limit_);
|
||||
|
||||
sql.replace("%songs_table", songs_table);
|
||||
sql.replace("%fts_table_noprefix", fts_table.section('.', -1, -1));
|
||||
sql.replace("%fts_table", fts_table);
|
||||
|
||||
query_ = QSqlQuery(db);
|
||||
query_.prepare(sql);
|
||||
|
||||
// Bind values
|
||||
for (const QVariant& value : bound_values_) {
|
||||
query_.addBindValue(value);
|
||||
}
|
||||
|
||||
query_.exec();
|
||||
return query_;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionQuery::Next() { return query_.next(); }
|
||||
|
||||
QVariant CollectionQuery::Value(int column) const { return query_.value(column); }
|
||||
|
||||
bool QueryOptions::Matches(const Song &song) const {
|
||||
|
||||
if (max_age_ != -1) {
|
||||
const uint cutoff = QDateTime::currentDateTime().toTime_t() - max_age_;
|
||||
if (song.ctime() <= cutoff) return false;
|
||||
}
|
||||
|
||||
if (!filter_.isNull()) {
|
||||
return song.artist().contains(filter_, Qt::CaseInsensitive) || song.album().contains(filter_, Qt::CaseInsensitive) || song.title().contains(filter_, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
116
src/collection/collectionquery.h
Normal file
116
src/collection/collectionquery.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONQUERY_H
|
||||
#define COLLECTIONQUERY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QSqlQuery>
|
||||
#include <QStringList>
|
||||
#include <QVariantList>
|
||||
|
||||
class Song;
|
||||
class CollectionBackend;
|
||||
|
||||
// This structure let's you customize behaviour of any CollectionQuery.
|
||||
struct QueryOptions {
|
||||
// Modes of CollectionQuery:
|
||||
// - use the all songs table
|
||||
// - use the duplicated songs view; by duplicated we mean those songs
|
||||
// for which the (artist, album, title) tuple is found more than once
|
||||
// in the songs table
|
||||
// - use the untagged songs view; by untagged we mean those for which
|
||||
// at least one of the (artist, album, title) tags is empty
|
||||
// Please note that additional filtering based on fts table (the filter
|
||||
// attribute) won't work in Duplicates and Untagged modes.
|
||||
enum QueryMode {
|
||||
QueryMode_All,
|
||||
QueryMode_Duplicates,
|
||||
QueryMode_Untagged
|
||||
};
|
||||
|
||||
QueryOptions();
|
||||
|
||||
bool Matches(const Song &song) const;
|
||||
|
||||
QString filter() const { return filter_; }
|
||||
void set_filter(const QString &filter) {
|
||||
this->filter_ = filter;
|
||||
this->query_mode_ = QueryMode_All;
|
||||
}
|
||||
|
||||
int max_age() const { return max_age_; }
|
||||
void set_max_age(int max_age) { this->max_age_ = max_age; }
|
||||
|
||||
QueryMode query_mode() const { return query_mode_; }
|
||||
void set_query_mode(QueryMode query_mode) {
|
||||
this->query_mode_ = query_mode;
|
||||
this->filter_ = QString();
|
||||
}
|
||||
|
||||
private:
|
||||
QString filter_;
|
||||
int max_age_;
|
||||
QueryMode query_mode_;
|
||||
};
|
||||
|
||||
class CollectionQuery {
|
||||
public:
|
||||
CollectionQuery(const QueryOptions &options = QueryOptions());
|
||||
|
||||
// Sets contents of SELECT clause on the query (list of columns to get).
|
||||
void SetColumnSpec(const QString &spec) { column_spec_ = spec; }
|
||||
// Sets an ORDER BY clause on the query.
|
||||
void SetOrderBy(const QString &order_by) { order_by_ = order_by; }
|
||||
|
||||
// Adds a fragment of WHERE clause. When executed, this Query will connect all
|
||||
// the fragments with AND operator.
|
||||
// Please note that IN operator expects a QStringList as value.
|
||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
||||
|
||||
void AddCompilationRequirement(bool compilation);
|
||||
void SetLimit(int limit) { limit_ = limit; }
|
||||
void SetIncludeUnavailable(bool include_unavailable) { include_unavailable_ = include_unavailable; }
|
||||
|
||||
QSqlQuery Exec(QSqlDatabase db, const QString &songs_table, const QString &fts_table);
|
||||
bool Next();
|
||||
QVariant Value(int column) const;
|
||||
|
||||
operator const QSqlQuery &() const { return query_; }
|
||||
|
||||
private:
|
||||
QString GetInnerQuery();
|
||||
|
||||
bool include_unavailable_;
|
||||
bool join_with_fts_;
|
||||
QString column_spec_;
|
||||
QString order_by_;
|
||||
QStringList where_clauses_;
|
||||
QVariantList bound_values_;
|
||||
int limit_;
|
||||
bool duplicates_only_;
|
||||
|
||||
QSqlQuery query_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONQUERY_H
|
||||
716
src/collection/collectionview.cpp
Normal file
716
src/collection/collectionview.cpp
Normal file
@@ -0,0 +1,716 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "collectionview.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QHelpEvent>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QToolTip>
|
||||
#include <QWhatsThis>
|
||||
|
||||
#include "collectiondirectorymodel.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionitem.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#ifdef HAVE_GSTREAMER
|
||||
#include "dialogs/organisedialog.h"
|
||||
#include "dialogs/organiseerrordialog.h"
|
||||
#endif
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
CollectionItemDelegate::CollectionItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
|
||||
|
||||
void CollectionItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const {
|
||||
|
||||
const bool is_divider = index.data(CollectionModel::Role_IsDivider).toBool();
|
||||
|
||||
if (is_divider) {
|
||||
QString text(index.data().toString());
|
||||
|
||||
painter->save();
|
||||
|
||||
QRect text_rect(opt.rect);
|
||||
|
||||
// Does this item have an icon?
|
||||
QPixmap pixmap;
|
||||
QVariant decoration = index.data(Qt::DecorationRole);
|
||||
if (!decoration.isNull()) {
|
||||
if (decoration.canConvert<QPixmap>()) {
|
||||
pixmap = decoration.value<QPixmap>();
|
||||
}
|
||||
else if (decoration.canConvert<QIcon>()) {
|
||||
pixmap = decoration.value<QIcon>().pixmap(opt.decorationSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the icon at the left of the text rectangle
|
||||
if (!pixmap.isNull()) {
|
||||
QRect icon_rect(text_rect.topLeft(), opt.decorationSize);
|
||||
const int padding = (text_rect.height() - icon_rect.height()) / 2;
|
||||
icon_rect.adjust(padding, padding, padding, padding);
|
||||
text_rect.moveLeft(icon_rect.right() + padding + 6);
|
||||
|
||||
if (pixmap.size() != opt.decorationSize) {
|
||||
pixmap = pixmap.scaled(opt.decorationSize, Qt::KeepAspectRatio);
|
||||
}
|
||||
|
||||
painter->drawPixmap(icon_rect, pixmap);
|
||||
}
|
||||
else {
|
||||
text_rect.setLeft(text_rect.left() + 30);
|
||||
}
|
||||
|
||||
// Draw the text
|
||||
QFont bold_font(opt.font);
|
||||
bold_font.setBold(true);
|
||||
|
||||
painter->setPen(opt.palette.color(QPalette::Text));
|
||||
painter->setFont(bold_font);
|
||||
painter->drawText(text_rect, text);
|
||||
|
||||
// Draw the line under the item
|
||||
QColor line_color = opt.palette.color(QPalette::Text);
|
||||
QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight());
|
||||
const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width();
|
||||
line_color.setAlphaF(0.0);
|
||||
grad_color.setColorAt(0, line_color);
|
||||
line_color.setAlphaF(0.5);
|
||||
grad_color.setColorAt(fade_start_end, line_color);
|
||||
grad_color.setColorAt(1.0 - fade_start_end, line_color);
|
||||
line_color.setAlphaF(0.0);
|
||||
grad_color.setColorAt(1, line_color);
|
||||
painter->setPen(QPen(grad_color, 1));
|
||||
painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
else {
|
||||
QStyledItemDelegate::paint(painter, opt, index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) {
|
||||
|
||||
Q_UNUSED(option);
|
||||
|
||||
if (!event || !view) return false;
|
||||
|
||||
QHelpEvent *he = static_cast<QHelpEvent*>(event);
|
||||
QString text = displayText(index.data(), QLocale::system());
|
||||
|
||||
if (text.isEmpty() || !he) return false;
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::ToolTip: {
|
||||
QRect displayed_text;
|
||||
QSize real_text;
|
||||
bool is_elided = false;
|
||||
|
||||
real_text = sizeHint(option, index);
|
||||
displayed_text = view->visualRect(index);
|
||||
is_elided = displayed_text.width() < real_text.width();
|
||||
|
||||
if (is_elided) {
|
||||
QToolTip::showText(he->globalPos(), text, view);
|
||||
}
|
||||
else if (index.data(Qt::ToolTipRole).isValid()) {
|
||||
// If the item has a tooltip text, display it
|
||||
QString tooltip_text = index.data(Qt::ToolTipRole).toString();
|
||||
QToolTip::showText(he->globalPos(), tooltip_text, view);
|
||||
}
|
||||
else {
|
||||
// in case that another text was previously displayed
|
||||
QToolTip::hideText();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::QueryWhatsThis:
|
||||
return true;
|
||||
|
||||
case QEvent::WhatsThis:
|
||||
QWhatsThis::showText(he->globalPos(), text, view);
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
CollectionView::CollectionView(QWidget *parent)
|
||||
: AutoExpandingTreeView(parent),
|
||||
app_(nullptr),
|
||||
filter_(nullptr),
|
||||
total_song_count_(-1),
|
||||
total_artist_count_(-1),
|
||||
total_album_count_(-1),
|
||||
nomusic_(":/pictures/nomusic.png"),
|
||||
context_menu_(nullptr),
|
||||
is_in_keyboard_search_(false)
|
||||
{
|
||||
|
||||
setItemDelegate(new CollectionItemDelegate(this));
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
setHeaderHidden(true);
|
||||
setAllColumnsShowFocus(true);
|
||||
setDragEnabled(true);
|
||||
setDragDropMode(QAbstractItemView::DragOnly);
|
||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
|
||||
setStyleSheet("QTreeView::item{padding-top:1px;}");
|
||||
|
||||
}
|
||||
|
||||
CollectionView::~CollectionView() {}
|
||||
|
||||
void CollectionView::SaveFocus() {
|
||||
|
||||
QModelIndex current = currentIndex();
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Song || type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_selected_path_.clear();
|
||||
last_selected_song_ = Song();
|
||||
last_selected_container_ = QString();
|
||||
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song: {
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (!songs.isEmpty()) {
|
||||
last_selected_song_ = songs.last();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider: {
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
last_selected_container_ = text;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
SaveContainerPath(current);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SaveContainerPath(const QModelIndex &child) {
|
||||
|
||||
QModelIndex current = model()->parent(child);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
last_selected_path_ << text;
|
||||
SaveContainerPath(current);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::RestoreFocus() {
|
||||
|
||||
if (last_selected_container_.isEmpty() && last_selected_song_.url().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
RestoreLevelFocus();
|
||||
|
||||
}
|
||||
|
||||
bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
||||
|
||||
if (model()->canFetchMore(parent)) {
|
||||
model()->fetchMore(parent);
|
||||
}
|
||||
int rows = model()->rowCount(parent);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
QModelIndex current = model()->index(i, 0, parent);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:
|
||||
if (!last_selected_song_.url().isEmpty()) {
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
for (const Song& song : songs) {
|
||||
if (song == last_selected_song_) {
|
||||
setCurrentIndex(current);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider: {
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
|
||||
emit expand(current);
|
||||
setCurrentIndex(current);
|
||||
return true;
|
||||
}
|
||||
else if (last_selected_path_.contains(text)) {
|
||||
emit expand(current);
|
||||
// If a selected container or song were not found, we've got into a wrong subtree
|
||||
// (happens with "unknown" all the time)
|
||||
if (!RestoreLevelFocus(current)) {
|
||||
emit collapse(current);
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::ReloadSettings() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QSettings settings;
|
||||
|
||||
settings.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
SetAutoOpen(settings.value("auto_open", true).toBool());
|
||||
|
||||
if (app_ != nullptr) {
|
||||
app_->collection_model()->set_pretty_covers(settings.value("pretty_covers", true).toBool());
|
||||
app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool());
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SetApplication(Application *app) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
app_ = app;
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SetFilter(CollectionFilterWidget *filter) { filter_ = filter; }
|
||||
|
||||
void CollectionView::TotalSongCountUpdated(int count) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__ << count;
|
||||
|
||||
bool old = total_song_count_;
|
||||
total_song_count_ = count;
|
||||
if (old != total_song_count_) update();
|
||||
|
||||
if (total_song_count_ == 0)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
emit TotalSongCountUpdated_();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::TotalArtistCountUpdated(int count) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__ << count;
|
||||
|
||||
bool old = total_artist_count_;
|
||||
total_artist_count_ = count;
|
||||
if (old != total_artist_count_) update();
|
||||
|
||||
if (total_artist_count_ == 0)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
emit TotalArtistCountUpdated_();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::TotalAlbumCountUpdated(int count) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__ << count;
|
||||
|
||||
bool old = total_album_count_;
|
||||
total_album_count_ = count;
|
||||
if (old != total_album_count_) update();
|
||||
|
||||
if (total_album_count_ == 0)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
emit TotalAlbumCountUpdated_();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::paintEvent(QPaintEvent *event) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__;
|
||||
|
||||
if (total_song_count_ == 0) {
|
||||
QPainter p(viewport());
|
||||
QRect rect(viewport()->rect());
|
||||
|
||||
// Draw the confused strawberry
|
||||
QRect image_rect((rect.width() - nomusic_.width()) / 2, 50, nomusic_.width(), nomusic_.height());
|
||||
p.drawPixmap(image_rect, nomusic_);
|
||||
|
||||
// Draw the title text
|
||||
QFont bold_font;
|
||||
bold_font.setBold(true);
|
||||
p.setFont(bold_font);
|
||||
|
||||
QFontMetrics metrics(bold_font);
|
||||
|
||||
QRect title_rect(0, image_rect.bottom() + 20, rect.width(), metrics.height());
|
||||
p.drawText(title_rect, Qt::AlignHCenter, tr("Your collection is empty!"));
|
||||
|
||||
// Draw the other text
|
||||
p.setFont(QFont());
|
||||
|
||||
QRect text_rect(0, title_rect.bottom() + 5, rect.width(), metrics.height());
|
||||
p.drawText(text_rect, Qt::AlignHCenter, tr("Click here to add some music"));
|
||||
}
|
||||
else {
|
||||
QTreeView::paintEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::mouseReleaseEvent(QMouseEvent *e) {
|
||||
|
||||
QTreeView::mouseReleaseEvent(e);
|
||||
|
||||
if (total_song_count_ == 0) {
|
||||
emit ShowConfigDialog();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu(this);
|
||||
add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-play"), tr("Append to current playlist"), this, SLOT(AddToPlaylist()));
|
||||
load_ = context_menu_->addAction(IconLoader::Load("media-play"), tr("Replace current playlist"), this, SLOT(Load()));
|
||||
open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue()));
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
context_menu_->addSeparator();
|
||||
organise_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organise files..."), this, SLOT(Organise()));
|
||||
copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(CopyToDevice()));
|
||||
//delete_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(Delete()));
|
||||
#endif
|
||||
|
||||
context_menu_->addSeparator();
|
||||
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks()));
|
||||
edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, SLOT(EditTracks()));
|
||||
show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious()));
|
||||
no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
|
||||
context_menu_->addMenu(filter_->menu());
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
|
||||
connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), copy_to_device_, SLOT(setDisabled(bool)));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
context_menu_index_ = indexAt(e->pos());
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
|
||||
|
||||
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
|
||||
int regular_elements = 0;
|
||||
int regular_editable = 0;
|
||||
|
||||
for (const QModelIndex& index : selected_indexes) {
|
||||
regular_elements++;
|
||||
if(app_->collection_model()->data(index, CollectionModel::Role_Editable).toBool()) {
|
||||
regular_editable++;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if custom plugin actions should be enabled / visible
|
||||
//const int songs_selected = smart_playlists + smart_playlists_header + regular_elements;
|
||||
const int songs_selected = regular_elements;
|
||||
const bool regular_elements_only = songs_selected == regular_elements && regular_elements > 0;
|
||||
|
||||
// in all modes
|
||||
load_->setEnabled(songs_selected);
|
||||
add_to_playlist_->setEnabled(songs_selected);
|
||||
open_in_new_playlist_->setEnabled(songs_selected);
|
||||
add_to_playlist_enqueue_->setEnabled(songs_selected);
|
||||
|
||||
// if neither edit_track not edit_tracks are available, we show disabled edit_track element
|
||||
//edit_track_->setVisible(!smart_playlists_only && (regular_editable <= 1));
|
||||
edit_track_->setVisible(regular_editable <= 1);
|
||||
edit_track_->setEnabled(regular_editable == 1);
|
||||
|
||||
// only when no smart playlists selected
|
||||
#ifdef HAVE_GSTREAMER
|
||||
organise_->setVisible(regular_elements_only);
|
||||
copy_to_device_->setVisible(regular_elements_only);
|
||||
//delete_->setVisible(regular_elements_only);
|
||||
#endif
|
||||
show_in_various_->setVisible(regular_elements_only);
|
||||
no_show_in_various_->setVisible(regular_elements_only);
|
||||
|
||||
// only when all selected items are editable
|
||||
#ifdef HAVE_GSTREAMER
|
||||
organise_->setEnabled(regular_elements == regular_editable);
|
||||
copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||
//delete_->setEnabled(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
context_menu_->popup(e->globalPos());
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::ShowInVarious() { ShowInVarious(true); }
|
||||
|
||||
void CollectionView::NoShowInVarious() { ShowInVarious(false); }
|
||||
|
||||
void CollectionView::ShowInVarious(bool on) {
|
||||
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
// Map is from album name -> all artists sharing that album name, built from each selected
|
||||
// song. We put through "Various Artists" changes one album at a time, to make sure the old album
|
||||
// node gets removed (due to all children removed), before the new one gets added
|
||||
QMultiMap<QString, QString> albums;
|
||||
for (const Song& song : GetSelectedSongs()) {
|
||||
if (albums.find(song.album(), song.artist()) == albums.end())
|
||||
albums.insert(song.album(), song.artist());
|
||||
}
|
||||
|
||||
// If we have only one album and we are putting it into Various Artists, check to see
|
||||
// if there are other Artists in this album and prompt the user if they'd like them moved, too
|
||||
if (on && albums.keys().count() == 1) {
|
||||
const QString album = albums.keys().first();
|
||||
QList<Song> all_of_album = app_->collection_backend()->GetSongsByAlbum(album);
|
||||
QSet<QString> other_artists;
|
||||
for (const Song &s : all_of_album) {
|
||||
if (!albums.contains(album, s.artist()) &&
|
||||
!other_artists.contains(s.artist())) {
|
||||
other_artists.insert(s.artist());
|
||||
}
|
||||
}
|
||||
if (other_artists.count() > 0) {
|
||||
if (QMessageBox::question(this,
|
||||
tr("There are other songs in this album"),
|
||||
tr("Would you like to move the other songs in this album to Various Artists as well?"),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes) == QMessageBox::Yes) {
|
||||
for (const QString &s : other_artists) {
|
||||
albums.insert(album, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &album : QSet<QString>::fromList(albums.keys())) {
|
||||
app_->collection_backend()->ForceCompilation(album, albums.values(album), on);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::Load() {
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData* mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->clear_first_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::AddToPlaylist() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
emit AddToPlaylistSignal(model()->mimeData(selectedIndexes()));
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::AddToPlaylistEnqueue() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData* mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->enqueue_now_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::OpenInNewPlaylist() {
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData* mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->open_in_new_playlist_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::keyboardSearch(const QString &search) {
|
||||
|
||||
is_in_keyboard_search_ = true;
|
||||
QTreeView::keyboardSearch(search);
|
||||
is_in_keyboard_search_ = false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::scrollTo(const QModelIndex &index, ScrollHint hint) {
|
||||
|
||||
if (is_in_keyboard_search_)
|
||||
QTreeView::scrollTo(index, QAbstractItemView::PositionAtTop);
|
||||
else
|
||||
QTreeView::scrollTo(index, hint);
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionView::GetSelectedSongs() const {
|
||||
|
||||
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
return app_->collection_model()->GetChildSongs(selected_indexes);
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void CollectionView::Organise() {
|
||||
|
||||
if (!organise_dialog_)
|
||||
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
|
||||
|
||||
organise_dialog_->SetDestinationModel(app_->collection_model()->directory_model());
|
||||
organise_dialog_->SetCopy(false);
|
||||
if (organise_dialog_->SetSongs(GetSelectedSongs()))
|
||||
organise_dialog_->show();
|
||||
else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void CollectionView::EditTracks() {
|
||||
|
||||
if (!edit_tag_dialog_) {
|
||||
edit_tag_dialog_.reset(new EditTagDialog(app_, this));
|
||||
}
|
||||
edit_tag_dialog_->SetSongs(GetSelectedSongs());
|
||||
edit_tag_dialog_->show();
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
if (!organise_dialog_)
|
||||
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
|
||||
|
||||
organise_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organise_dialog_->SetCopy(true);
|
||||
organise_dialog_->SetSongs(GetSelectedSongs());
|
||||
organise_dialog_->show();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void CollectionView::FilterReturnPressed() {
|
||||
|
||||
if (!currentIndex().isValid()) {
|
||||
// Pick the first thing that isn't a divider
|
||||
for (int row = 0; row < model()->rowCount(); ++row) {
|
||||
QModelIndex idx(model()->index(row, 0));
|
||||
if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) {
|
||||
setCurrentIndex(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentIndex().isValid()) return;
|
||||
|
||||
emit doubleClicked(currentIndex());
|
||||
}
|
||||
|
||||
void CollectionView::ShowInBrowser() {
|
||||
QList<QUrl> urls;
|
||||
for (const Song &song : GetSelectedSongs()) {
|
||||
urls << song.url();
|
||||
}
|
||||
|
||||
Utilities::OpenInFileBrowser(urls);
|
||||
}
|
||||
|
||||
int CollectionView::TotalSongs() {
|
||||
return total_song_count_;
|
||||
}
|
||||
int CollectionView::TotalArtists() {
|
||||
return total_artist_count_;
|
||||
}
|
||||
int CollectionView::TotalAlbums() {
|
||||
return total_album_count_;
|
||||
}
|
||||
163
src/collection/collectionview.h
Normal file
163
src/collection/collectionview.h
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONVIEW_H
|
||||
#define COLLECTIONVIEW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "dialogs/edittagdialog.h"
|
||||
#include "widgets/autoexpandingtreeview.h"
|
||||
|
||||
class Application;
|
||||
class CollectionFilterWidget;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
class OrganiseDialog;
|
||||
#endif
|
||||
|
||||
class QMimeData;
|
||||
|
||||
class CollectionItemDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionItemDelegate(QObject *parent);
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
|
||||
public slots:
|
||||
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index);
|
||||
};
|
||||
|
||||
class CollectionView : public AutoExpandingTreeView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionView(QWidget *parent = nullptr);
|
||||
~CollectionView();
|
||||
|
||||
//static const char *kSettingsGroup;
|
||||
|
||||
// Returns Songs currently selected in the collection view. Please note that the
|
||||
// selection is recursive meaning that if for example an album is selected
|
||||
// this will return all of it's songs.
|
||||
SongList GetSelectedSongs() const;
|
||||
|
||||
void SetApplication(Application *app);
|
||||
void SetFilter(CollectionFilterWidget *filter);
|
||||
|
||||
// QTreeView
|
||||
void keyboardSearch(const QString &search);
|
||||
void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible);
|
||||
|
||||
int TotalSongs();
|
||||
int TotalArtists();
|
||||
int TotalAlbums();
|
||||
|
||||
public slots:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void TotalArtistCountUpdated(int count);
|
||||
void TotalAlbumCountUpdated(int count);
|
||||
void ReloadSettings();
|
||||
|
||||
void FilterReturnPressed();
|
||||
|
||||
void SaveFocus();
|
||||
void RestoreFocus();
|
||||
|
||||
signals:
|
||||
void ShowConfigDialog();
|
||||
|
||||
void TotalSongCountUpdated_();
|
||||
void TotalArtistCountUpdated_();
|
||||
void TotalAlbumCountUpdated_();
|
||||
|
||||
protected:
|
||||
// QWidget
|
||||
void paintEvent(QPaintEvent *event);
|
||||
void mouseReleaseEvent(QMouseEvent *e);
|
||||
void contextMenuEvent(QContextMenuEvent *e);
|
||||
|
||||
private slots:
|
||||
void Load();
|
||||
void AddToPlaylist();
|
||||
void AddToPlaylistEnqueue();
|
||||
void OpenInNewPlaylist();
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void Organise();
|
||||
void CopyToDevice();
|
||||
#endif
|
||||
void EditTracks();
|
||||
void ShowInBrowser();
|
||||
void ShowInVarious();
|
||||
void NoShowInVarious();
|
||||
|
||||
private:
|
||||
void RecheckIsEmpty();
|
||||
void ShowInVarious(bool on);
|
||||
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
||||
void SaveContainerPath(const QModelIndex &child);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
CollectionFilterWidget *filter_;
|
||||
|
||||
int total_song_count_;
|
||||
int total_artist_count_;
|
||||
int total_album_count_;
|
||||
|
||||
QPixmap nomusic_;
|
||||
|
||||
QMenu *context_menu_;
|
||||
QModelIndex context_menu_index_;
|
||||
QAction *load_;
|
||||
QAction *add_to_playlist_;
|
||||
QAction *add_to_playlist_enqueue_;
|
||||
QAction *open_in_new_playlist_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
QAction *organise_;
|
||||
QAction *copy_to_device_;
|
||||
#endif
|
||||
QAction *delete_;
|
||||
QAction *edit_track_;
|
||||
QAction *edit_tracks_;
|
||||
QAction *show_in_browser_;
|
||||
QAction *show_in_various_;
|
||||
QAction *no_show_in_various_;
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
std::unique_ptr<OrganiseDialog> organise_dialog_;
|
||||
#endif
|
||||
std::unique_ptr<EditTagDialog> edit_tag_dialog_;
|
||||
|
||||
bool is_in_keyboard_search_;
|
||||
|
||||
// Save focus
|
||||
Song last_selected_song_;
|
||||
QString last_selected_container_;
|
||||
QSet<QString> last_selected_path_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONVIEW_H
|
||||
|
||||
48
src/collection/collectionviewcontainer.cpp
Normal file
48
src/collection/collectionviewcontainer.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "collectionviewcontainer.h"
|
||||
#include "ui_collectionviewcontainer.h"
|
||||
|
||||
CollectionViewContainer::CollectionViewContainer(QWidget *parent) : QWidget(parent), ui_(new Ui_CollectionViewContainer) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
view()->SetFilter(filter());
|
||||
|
||||
connect(filter(), SIGNAL(UpPressed()), view(), SLOT(UpAndFocus()));
|
||||
connect(filter(), SIGNAL(DownPressed()), view(), SLOT(DownAndFocus()));
|
||||
connect(filter(), SIGNAL(ReturnPressed()), view(), SLOT(FilterReturnPressed()));
|
||||
connect(view(), SIGNAL(FocusOnFilterSignal(QKeyEvent*)), filter(), SLOT(FocusOnFilter(QKeyEvent*)));
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
CollectionViewContainer::~CollectionViewContainer() { delete ui_; }
|
||||
|
||||
CollectionView* CollectionViewContainer::view() const { return ui_->view; }
|
||||
|
||||
CollectionFilterWidget *CollectionViewContainer::filter() const {
|
||||
return ui_->filter;
|
||||
}
|
||||
|
||||
void CollectionViewContainer::ReloadSettings() { view()->ReloadSettings(); }
|
||||
49
src/collection/collectionviewcontainer.h
Normal file
49
src/collection/collectionviewcontainer.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONVIEWCONTAINER_H
|
||||
#define COLLECTIONVIEWCONTAINER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class CollectionFilterWidget;
|
||||
class CollectionView;
|
||||
class Ui_CollectionViewContainer;
|
||||
|
||||
class CollectionViewContainer : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionViewContainer(QWidget *parent = nullptr);
|
||||
~CollectionViewContainer();
|
||||
|
||||
CollectionFilterWidget *filter() const;
|
||||
CollectionView *view() const;
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
private:
|
||||
Ui_CollectionViewContainer *ui_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONVIEWCONTAINER_H
|
||||
|
||||
47
src/collection/collectionviewcontainer.ui
Normal file
47
src/collection/collectionviewcontainer.ui
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CollectionViewContainer</class>
|
||||
<widget class="QWidget" name="CollectionViewContainer">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="CollectionFilterWidget" name="filter" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="CollectionView" name="view" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>CollectionFilterWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>collection/collectionfilterwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>CollectionView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>collection/collectionview.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
802
src/collection/collectionwatcher.cpp
Normal file
802
src/collection/collectionwatcher.cpp
Normal file
@@ -0,0 +1,802 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <fileref.h>
|
||||
#include <tag.h>
|
||||
|
||||
#include "collectionwatcher.h"
|
||||
|
||||
#include "collectionbackend.h"
|
||||
#include "core/filesystemwatcherinterface.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/utilities.h"
|
||||
#include "playlistparsers/cueparser.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDirIterator>
|
||||
#include <QtDebug>
|
||||
#include <QThread>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
|
||||
// This is defined by one of the windows headers that is included by taglib.
|
||||
#ifdef RemoveDirectory
|
||||
#undef RemoveDirectory
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
static const char *kNoMediaFile = ".nomedia";
|
||||
static const char *kNoMusicFile = ".nomusic";
|
||||
}
|
||||
|
||||
QStringList CollectionWatcher::sValidImages;
|
||||
|
||||
CollectionWatcher::CollectionWatcher(QObject *parent)
|
||||
: QObject(parent),
|
||||
backend_(nullptr),
|
||||
task_manager_(nullptr),
|
||||
fs_watcher_(FileSystemWatcherInterface::Create(this)),
|
||||
stop_requested_(false),
|
||||
scan_on_startup_(true),
|
||||
monitor_(true),
|
||||
rescan_timer_(new QTimer(this)),
|
||||
rescan_paused_(false),
|
||||
total_watches_(0),
|
||||
cue_parser_(new CueParser(backend_, this)) {
|
||||
Utilities::SetThreadIOPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
rescan_timer_->setInterval(1000);
|
||||
rescan_timer_->setSingleShot(true);
|
||||
|
||||
if (sValidImages.isEmpty()) {
|
||||
sValidImages << "jpg" << "png" << "gif" << "jpeg";
|
||||
}
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow()));
|
||||
}
|
||||
|
||||
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, int dir, bool incremental, bool ignores_mtime)
|
||||
: progress_(0),
|
||||
progress_max_(0),
|
||||
dir_(dir),
|
||||
incremental_(incremental),
|
||||
ignores_mtime_(ignores_mtime),
|
||||
watcher_(watcher),
|
||||
cached_songs_dirty_(true),
|
||||
known_subdirs_dirty_(true) {
|
||||
|
||||
QString description;
|
||||
|
||||
if (watcher_->device_name_.isEmpty())
|
||||
description = tr("Updating collection");
|
||||
else
|
||||
description = tr("Updating %1").arg(watcher_->device_name_);
|
||||
|
||||
task_id_ = watcher_->task_manager_->StartTask(description);
|
||||
emit watcher_->ScanStarted(task_id_);
|
||||
|
||||
}
|
||||
|
||||
CollectionWatcher::ScanTransaction::~ScanTransaction() {
|
||||
|
||||
// If we're stopping then don't commit the transaction
|
||||
if (watcher_->stop_requested_) return;
|
||||
|
||||
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 (!readded_songs.isEmpty()) emit watcher_->SongsReadded(readded_songs);
|
||||
|
||||
if (!new_subdirs.isEmpty()) emit watcher_->SubdirsDiscovered(new_subdirs);
|
||||
|
||||
if (!touched_subdirs.isEmpty())
|
||||
emit watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||
|
||||
watcher_->task_manager_->SetTaskFinished(task_id_);
|
||||
|
||||
if (watcher_->monitor_) {
|
||||
// Watch the new subdirectories
|
||||
for (const Subdirectory& subdir : new_subdirs) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::AddToProgress(int n) {
|
||||
|
||||
progress_ += n;
|
||||
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::AddToProgressMax(int n) {
|
||||
|
||||
progress_max_ += n;
|
||||
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionWatcher::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;
|
||||
for (const Song &song : cached_songs_) {
|
||||
if (song.url().toLocalFile().section('/', 0, -2) == path) ret << song;
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const SubdirectoryList &subdirs) {
|
||||
|
||||
known_subdirs_ = subdirs;
|
||||
known_subdirs_dirty_ = false;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_)
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
|
||||
for (const Subdirectory &subdir : known_subdirs_) {
|
||||
if (subdir.path == path && subdir.mtime != 0) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_)
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
|
||||
SubdirectoryList ret;
|
||||
for (const Subdirectory &subdir : known_subdirs_) {
|
||||
if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path &&
|
||||
subdir.mtime != 0) {
|
||||
ret << subdir;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
if (known_subdirs_dirty_)
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
return known_subdirs_;
|
||||
}
|
||||
|
||||
void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryList &subdirs) {
|
||||
|
||||
watched_dirs_[dir.id] = dir;
|
||||
|
||||
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);
|
||||
transaction.AddToProgressMax(1);
|
||||
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);
|
||||
transaction.AddToProgressMax(subdirs.count());
|
||||
for (const Subdirectory& subdir : subdirs) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction);
|
||||
|
||||
if (monitor_) AddWatch(dir, subdir.path);
|
||||
}
|
||||
}
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory &subdir, ScanTransaction *t, bool force_noincremental) {
|
||||
|
||||
QFileInfo path_info(path);
|
||||
QDir path_dir(path);
|
||||
|
||||
// Do not scan symlinked dirs that are already in collection
|
||||
if (path_info.isSymLink()) {
|
||||
QString real_path = path_info.symLinkTarget();
|
||||
for (const Directory& dir : watched_dirs_) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
t->AddToProgress(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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().toTime_t()) {
|
||||
// The directory hasn't changed since last time
|
||||
t->AddToProgress(1);
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, QStringList> album_art;
|
||||
QStringList files_on_disk;
|
||||
SubdirectoryList my_new_subdirs;
|
||||
|
||||
// If a directory is moved then only its parent gets a changed notification,
|
||||
// so we need to look and see if any of our children don't exist any more.
|
||||
// If one has been removed, "rescan" it to get the deleted songs
|
||||
SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||
for (const Subdirectory& subdir : previous_subdirs) {
|
||||
if (!QFile::exists(subdir.path) && subdir.path != path) {
|
||||
t->AddToProgressMax(1);
|
||||
ScanSubdirectory(subdir.path, subdir, t, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 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::Hidden | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
QString child(it.next());
|
||||
QFileInfo child_info(child);
|
||||
|
||||
if (child_info.isDir()) {
|
||||
if (!child_info.isHidden() && !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 (!child_info.isHidden())
|
||||
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);
|
||||
|
||||
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) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
// associated cue
|
||||
QString matching_cue = NoExtensionPart(file) + ".cue";
|
||||
|
||||
Song matching_song;
|
||||
if (FindSongByPath(songs_in_db, file, &matching_song)) {
|
||||
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// cue sheet's path from collection (if any)
|
||||
QString song_cue = matching_song.cue_path();
|
||||
uint song_cue_mtime = GetMtimeForCue(song_cue);
|
||||
|
||||
bool cue_deleted = song_cue_mtime == 0 && matching_song.has_cue();
|
||||
bool cue_added = matching_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().toTime_t(), song_cue_mtime)) || cue_deleted || cue_added;
|
||||
|
||||
// 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() && !matching_song.has_embedded_cover() && !QFile::exists(matching_song.art_automatic()))) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// the song's changed - reread the metadata from file
|
||||
if (t->ignores_mtime() || changed) {
|
||||
qLog(Debug) << file << "changed";
|
||||
|
||||
// if cue associated...
|
||||
if (!cue_deleted && (matching_song.has_cue() || cue_added)) {
|
||||
UpdateCueAssociatedSongs(file, path, matching_cue, image, t);
|
||||
// if no cue or it's about to lose it...
|
||||
}
|
||||
else {
|
||||
UpdateNonCueAssociatedSong(file, matching_song, image, cue_deleted, t);
|
||||
}
|
||||
}
|
||||
|
||||
// nothing has changed - mark the song available without re-scanning
|
||||
if (matching_song.is_unavailable()) t->readded_songs << matching_song;
|
||||
|
||||
} 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;
|
||||
}
|
||||
|
||||
qLog(Debug) << file << "created";
|
||||
// choose an image for the song(s)
|
||||
QString image = ImageForSong(file, album_art);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QString &image, ScanTransaction *t) {
|
||||
|
||||
QFile cue(matching_cue);
|
||||
cue.open(QIODevice::ReadOnly);
|
||||
|
||||
SongList old_sections = backend_->GetSongsByUrl(QUrl::fromLocalFile(file));
|
||||
|
||||
QHash<quint64, Song> sections_map;
|
||||
for (const Song& song : old_sections) {
|
||||
sections_map[song.beginning_nanosec()] = song;
|
||||
}
|
||||
|
||||
QSet<int> used_ids;
|
||||
|
||||
// update every song that's in the cue and collection
|
||||
for (Song cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
||||
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
|
||||
} else {
|
||||
PreserveUserSetData(file, image, matching, &cue_song, t);
|
||||
used_ids.insert(matching.id());
|
||||
}
|
||||
}
|
||||
|
||||
// sections that are now missing
|
||||
for (const Song &matching : old_sections) {
|
||||
if (!used_ids.contains(matching.id())) {
|
||||
t->deleted_songs << matching;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QString &image, 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
|
||||
if (cue_deleted) {
|
||||
for (const Song &song :
|
||||
backend_->GetSongsByUrl(QUrl::fromLocalFile(file))) {
|
||||
if (!song.IsMetadataEqual(matching_song)) {
|
||||
t->deleted_songs << song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Song song_on_disk;
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &matching_cue, QSet<QString> *cues_processed) {
|
||||
|
||||
SongList song_list;
|
||||
|
||||
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
||||
// if it's a cue - create virtual tracks
|
||||
if (matching_cue_mtime) {
|
||||
// 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);
|
||||
|
||||
// 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 (const Song& cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
||||
if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) {
|
||||
if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
|
||||
song_list << cue_song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!song_list.isEmpty()) {
|
||||
*cues_processed << matching_cue;
|
||||
}
|
||||
|
||||
// it's a normal media file
|
||||
}
|
||||
else {
|
||||
Song song;
|
||||
TagReaderClient::Instance()->ReadFileBlocking(file, &song);
|
||||
|
||||
if (song.is_valid()) {
|
||||
song_list << song;
|
||||
}
|
||||
}
|
||||
|
||||
return song_list;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::PreserveUserSetData(const QString &file, const QString &image, const Song &matching_song, Song *out, 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;
|
||||
}
|
||||
else if (!matching_song.IsMetadataEqual(*out)) {
|
||||
qLog(Debug) << file << "metadata changed";
|
||||
|
||||
// Update the song in the DB
|
||||
t->new_songs << *out;
|
||||
}
|
||||
else {
|
||||
// Only the mtime's changed
|
||||
t->touched_songs << *out;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint CollectionWatcher::GetMtimeForCue(const QString &cue_path) {
|
||||
|
||||
// slight optimisation
|
||||
if (cue_path.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QFileInfo file_info(cue_path);
|
||||
if (!file_info.exists()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QDateTime cue_last_modified = file_info.lastModified();
|
||||
|
||||
return cue_last_modified.isValid() ? cue_last_modified.toTime_t() : 0;
|
||||
}
|
||||
|
||||
void CollectionWatcher::AddWatch(const Directory &dir, const QString &path) {
|
||||
|
||||
if (!QFile::exists(path)) return;
|
||||
|
||||
connect(fs_watcher_, SIGNAL(PathChanged(const QString&)), this, SLOT(DirectoryChanged(const QString&)), Qt::UniqueConnection);
|
||||
fs_watcher_->AddPath(path);
|
||||
subdir_mapping_[path] = dir;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RemoveDirectory(const Directory& dir) {
|
||||
|
||||
rescan_queue_.remove(dir.id);
|
||||
watched_dirs_.remove(dir.id);
|
||||
|
||||
// Stop watching the directory's subdirectories
|
||||
for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
|
||||
fs_watcher_->RemovePath(subdir_path);
|
||||
subdir_mapping_.remove(subdir_path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionWatcher::FindSongByPath(const SongList &list, const QString &path, Song *out) {
|
||||
|
||||
// TODO: Make this faster
|
||||
for (const Song &song : list) {
|
||||
if (song.url().toLocalFile() == path) {
|
||||
*out = song;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
||||
|
||||
// Find what dir it was in
|
||||
QHash<QString, Directory>::const_iterator it = subdir_mapping_.constFind(subdir);
|
||||
if (it == subdir_mapping_.constEnd()) {
|
||||
return;
|
||||
}
|
||||
Directory dir = *it;
|
||||
|
||||
qLog(Debug) << "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;
|
||||
|
||||
if (!rescan_paused_) rescan_timer_->start();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
for (int dir : rescan_queue_.keys()) {
|
||||
if (stop_requested_) return;
|
||||
ScanTransaction transaction(this, dir, false);
|
||||
transaction.AddToProgressMax(rescan_queue_[dir].count());
|
||||
|
||||
for (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();
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
|
||||
QString CollectionWatcher::PickBestImage(const QStringList &images) {
|
||||
|
||||
// This is used when there is more than one image in a directory.
|
||||
// Pick the biggest image that matches the most important filter
|
||||
|
||||
QStringList filtered;
|
||||
|
||||
for (const QString &filter_text : best_image_filters_) {
|
||||
// the images in the images list are represented by a full path,
|
||||
// so we need to isolate just the filename
|
||||
for (const QString& image : images) {
|
||||
QFileInfo file_info(image);
|
||||
QString filename(file_info.fileName());
|
||||
if (filename.contains(filter_text, Qt::CaseInsensitive))
|
||||
filtered << image;
|
||||
}
|
||||
|
||||
/* We assume the filters are give in the order best to worst, so
|
||||
if we've got a result, we go with it. Otherwise we might
|
||||
start capturing more generic rules */
|
||||
if (!filtered.isEmpty()) break;
|
||||
}
|
||||
|
||||
if (filtered.isEmpty()) {
|
||||
// the filter was too restrictive, just use the original list
|
||||
filtered = images;
|
||||
}
|
||||
|
||||
int biggest_size = 0;
|
||||
QString biggest_path;
|
||||
|
||||
for (const QString& path : filtered) {
|
||||
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 CollectionWatcher::ImageForSong(const QString &path, QMap<QString, QStringList> &album_art) {
|
||||
|
||||
QString dir(DirectoryPart(path));
|
||||
|
||||
if (album_art.contains(dir)) {
|
||||
if (album_art[dir].count() == 1)
|
||||
return album_art[dir][0];
|
||||
else {
|
||||
QString best_image = PickBestImage(album_art[dir]);
|
||||
album_art[dir] = QStringList() << best_image;
|
||||
return best_image;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ReloadSettingsAsync() {
|
||||
|
||||
QMetaObject::invokeMethod(this, "ReloadSettings", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ReloadSettings() {
|
||||
|
||||
const bool was_monitoring_before = monitor_;
|
||||
QSettings s;
|
||||
|
||||
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
scan_on_startup_ = s.value("startup_scan", true).toBool();
|
||||
monitor_ = s.value("monitor", true).toBool();
|
||||
|
||||
best_image_filters_.clear();
|
||||
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
|
||||
for (const QString& filter : filters) {
|
||||
QString s = filter.trimmed();
|
||||
if (!s.isEmpty()) best_image_filters_ << s;
|
||||
}
|
||||
|
||||
if (!monitor_ && was_monitoring_before) {
|
||||
fs_watcher_->Clear();
|
||||
}
|
||||
else if (monitor_ && !was_monitoring_before) {
|
||||
// Add all directories to all QFileSystemWatchers again
|
||||
for (const Directory& dir : watched_dirs_.values()) {
|
||||
SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
|
||||
for (const Subdirectory& subdir : subdirs) {
|
||||
AddWatch(dir, subdir.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::SetRescanPausedAsync(bool pause) {
|
||||
|
||||
QMetaObject::invokeMethod(this, "SetRescanPaused", Qt::QueuedConnection, Q_ARG(bool, pause));
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::SetRescanPaused(bool pause) {
|
||||
|
||||
rescan_paused_ = pause;
|
||||
if (!rescan_paused_ && !rescan_queue_.isEmpty()) RescanPathsNow();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::IncrementalScanAsync() {
|
||||
|
||||
QMetaObject::invokeMethod(this, "IncrementalScanNow", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::FullScanAsync() {
|
||||
|
||||
QMetaObject::invokeMethod(this, "FullScanNow", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::IncrementalScanNow() { PerformScan(true, false); }
|
||||
|
||||
void CollectionWatcher::FullScanNow() { PerformScan(false, true); }
|
||||
|
||||
void CollectionWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
|
||||
|
||||
for (const Directory & dir : watched_dirs_.values()) {
|
||||
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes);
|
||||
SubdirectoryList subdirs(transaction.GetAllSubdirs());
|
||||
transaction.AddToProgressMax(subdirs.count());
|
||||
|
||||
for (const Subdirectory & subdir : subdirs) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
ScanSubdirectory(subdir.path, subdir, &transaction);
|
||||
}
|
||||
}
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
213
src/collection/collectionwatcher.h
Normal file
213
src/collection/collectionwatcher.h
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONWATCHER_H
|
||||
#define COLLECTIONWATCHER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "directory.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QMap>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class QFileSystemWatcher;
|
||||
class QTimer;
|
||||
|
||||
class CueParser;
|
||||
class FileSystemWatcherInterface;
|
||||
class CollectionBackend;
|
||||
class TaskManager;
|
||||
|
||||
class CollectionWatcher : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionWatcher(QObject *parent = nullptr);
|
||||
|
||||
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 IncrementalScanAsync();
|
||||
void FullScanAsync();
|
||||
void SetRescanPausedAsync(bool pause);
|
||||
void ReloadSettingsAsync();
|
||||
|
||||
void Stop() { stop_requested_ = true; }
|
||||
|
||||
signals:
|
||||
void NewOrUpdatedSongs(const SongList &songs);
|
||||
void SongsMTimeUpdated(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsReadded(const SongList &songs, bool unavailable = false);
|
||||
void SubdirsDiscovered(const SubdirectoryList &subdirs);
|
||||
void SubdirsMTimeUpdated(const SubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
|
||||
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);
|
||||
|
||||
private:
|
||||
// This class encapsulates a full or partial scan of a directory.
|
||||
// Each directory has one or more subdirectories, and any number of
|
||||
// subdirectories can be scanned during one transaction. ScanSubdirectory()
|
||||
// adds its results to the members of this transaction class, and they are
|
||||
// "committed" through calls to the CollectionBackend in the transaction's dtor.
|
||||
// The transaction also caches the list of songs in this directory according
|
||||
// to the collection. Multiple calls to FindSongsInSubdirectory during one
|
||||
// transaction will only result in one call to
|
||||
// CollectionBackend::FindSongsInDirectory.
|
||||
class ScanTransaction {
|
||||
public:
|
||||
ScanTransaction(CollectionWatcher *watcher, int dir, bool incremental, bool ignores_mtime = false);
|
||||
~ScanTransaction();
|
||||
|
||||
SongList FindSongsInSubdirectory(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);
|
||||
|
||||
int dir() const { return dir_; }
|
||||
bool is_incremental() const { return incremental_; }
|
||||
bool ignores_mtime() const { return ignores_mtime_; }
|
||||
|
||||
SongList deleted_songs;
|
||||
SongList readded_songs;
|
||||
SongList new_songs;
|
||||
SongList touched_songs;
|
||||
SubdirectoryList new_subdirs;
|
||||
SubdirectoryList touched_subdirs;
|
||||
|
||||
private:
|
||||
ScanTransaction(const ScanTransaction&) {}
|
||||
ScanTransaction& operator=(const ScanTransaction&) { return *this; }
|
||||
|
||||
int task_id_;
|
||||
int progress_;
|
||||
int progress_max_;
|
||||
|
||||
int dir_;
|
||||
// Incremental scan enters a directory only if it has changed since the last scan.
|
||||
bool incremental_;
|
||||
// This type of scan updates every file in a folder that's
|
||||
// being scanned. Even if it detects the file hasn't changed since
|
||||
// the last scan. Also, since it's ignoring mtimes on folders too,
|
||||
// it will go as deep in the folder hierarchy as it's possible.
|
||||
bool ignores_mtime_;
|
||||
|
||||
CollectionWatcher *watcher_;
|
||||
|
||||
SongList cached_songs_;
|
||||
bool cached_songs_dirty_;
|
||||
|
||||
SubdirectoryList known_subdirs_;
|
||||
bool known_subdirs_dirty_;
|
||||
};
|
||||
|
||||
private slots:
|
||||
void DirectoryChanged(const QString &path);
|
||||
void IncrementalScanNow();
|
||||
void FullScanNow();
|
||||
void RescanPathsNow();
|
||||
void ScanSubdirectory(const QString &path, const Subdirectory &subdir, ScanTransaction *t, bool force_noincremental = false);
|
||||
|
||||
private:
|
||||
static bool FindSongByPath(const SongList &list, const QString &path, Song *out);
|
||||
inline static QString NoExtensionPart(const QString &fileName);
|
||||
inline static QString ExtensionPart(const QString &fileName);
|
||||
inline static QString DirectoryPart(const QString &fileName);
|
||||
QString PickBestImage(const QStringList &images);
|
||||
QString ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
|
||||
void AddWatch(const Directory &dir, const QString &path);
|
||||
uint GetMtimeForCue(const QString &cue_path);
|
||||
void PerformScan(bool incremental, 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 QString &image, 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 QString &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 QString &image, const Song &matching_song, Song *out, 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);
|
||||
|
||||
private:
|
||||
CollectionBackend *backend_;
|
||||
TaskManager *task_manager_;
|
||||
QString device_name_;
|
||||
|
||||
FileSystemWatcherInterface *fs_watcher_;
|
||||
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.
|
||||
* e.g. using ["front", "cover"] would identify front.jpg and
|
||||
* exclude back.jpg.
|
||||
*/
|
||||
QStringList best_image_filters_;
|
||||
|
||||
bool stop_requested_;
|
||||
bool scan_on_startup_;
|
||||
bool monitor_;
|
||||
|
||||
QMap<int, Directory> watched_dirs_;
|
||||
QTimer *rescan_timer_;
|
||||
QMap<int, QStringList> rescan_queue_; // dir id -> list of subdirs to be scanned
|
||||
bool rescan_paused_;
|
||||
|
||||
int total_watches_;
|
||||
|
||||
CueParser *cue_parser_;
|
||||
|
||||
static QStringList sValidImages;
|
||||
};
|
||||
|
||||
inline QString CollectionWatcher::NoExtensionPart(const QString& fileName) {
|
||||
return fileName.contains('.') ? fileName.section('.', 0, -2) : "";
|
||||
}
|
||||
// Thanks Amarok
|
||||
inline QString CollectionWatcher::ExtensionPart(const QString& fileName) {
|
||||
return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : "";
|
||||
}
|
||||
inline QString CollectionWatcher::DirectoryPart(const QString& fileName) {
|
||||
return fileName.section('/', 0, -2);
|
||||
}
|
||||
|
||||
#endif // COLLECTIONWATCHER_H
|
||||
|
||||
61
src/collection/directory.h
Normal file
61
src/collection/directory.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DIRECTORY_H
|
||||
#define DIRECTORY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QMetaType>
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
struct Directory {
|
||||
Directory() : id(-1) {}
|
||||
|
||||
bool operator ==(const Directory& other) const {
|
||||
return path == other.path && id == other.id;
|
||||
}
|
||||
|
||||
QString path;
|
||||
int id;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Directory)
|
||||
|
||||
typedef QList<Directory> DirectoryList;
|
||||
Q_DECLARE_METATYPE(DirectoryList)
|
||||
|
||||
|
||||
struct Subdirectory {
|
||||
Subdirectory() : directory_id(-1), mtime(0) {}
|
||||
|
||||
int directory_id;
|
||||
QString path;
|
||||
uint mtime;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Subdirectory)
|
||||
|
||||
typedef QList<Subdirectory> SubdirectoryList;
|
||||
Q_DECLARE_METATYPE(SubdirectoryList)
|
||||
|
||||
#endif // DIRECTORY_H
|
||||
|
||||
121
src/collection/groupbydialog.cpp
Normal file
121
src/collection/groupbydialog.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
#include "groupbydialog.h"
|
||||
#include "ui_groupbydialog.h"
|
||||
|
||||
// boost::multi_index still relies on these being in the global namespace.
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
|
||||
#include <boost/multi_index_container.hpp>
|
||||
#include <boost/multi_index/member.hpp>
|
||||
#include <boost/multi_index/ordered_index.hpp>
|
||||
|
||||
using boost::multi_index_container;
|
||||
using boost::multi_index::indexed_by;
|
||||
using boost::multi_index::ordered_unique;
|
||||
using boost::multi_index::tag;
|
||||
using boost::multi_index::member;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Mapping {
|
||||
Mapping(CollectionModel::GroupBy g, int i) : group_by(g), combo_box_index(i) {}
|
||||
|
||||
CollectionModel::GroupBy group_by;
|
||||
int combo_box_index;
|
||||
};
|
||||
|
||||
struct tag_index {};
|
||||
struct tag_group_by {};
|
||||
|
||||
} // namespace
|
||||
|
||||
class GroupByDialogPrivate {
|
||||
private:
|
||||
typedef multi_index_container<
|
||||
Mapping,
|
||||
indexed_by<
|
||||
ordered_unique<tag<tag_index>,
|
||||
member<Mapping, int, &Mapping::combo_box_index> >,
|
||||
ordered_unique<tag<tag_group_by>,
|
||||
member<Mapping, CollectionModel::GroupBy,
|
||||
&Mapping::group_by> > > > MappingContainer;
|
||||
|
||||
public:
|
||||
MappingContainer mapping_;
|
||||
};
|
||||
|
||||
GroupByDialog::GroupByDialog(QWidget* parent) : QDialog(parent), ui_(new Ui_GroupByDialog), p_(new GroupByDialogPrivate) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
Reset();
|
||||
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_None, 0));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Album, 1));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Artist, 2));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_AlbumArtist, 3));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Composer, 4));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_FileType, 5));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Genre, 6));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Year, 7));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYear, 8));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_YearAlbum, 9));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYearAlbum, 10));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Bitrate, 11));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Disc, 12));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Performer, 13));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Grouping, 14));
|
||||
|
||||
connect(ui_->button_box->button(QDialogButtonBox::Reset), SIGNAL(clicked()), SLOT(Reset()));
|
||||
|
||||
resize(sizeHint());
|
||||
}
|
||||
|
||||
GroupByDialog::~GroupByDialog() {}
|
||||
|
||||
void GroupByDialog::Reset() {
|
||||
ui_->first->setCurrentIndex(2); // Artist
|
||||
ui_->second->setCurrentIndex(1); // Album
|
||||
ui_->third->setCurrentIndex(0); // None
|
||||
}
|
||||
|
||||
void GroupByDialog::accept() {
|
||||
emit Accepted(CollectionModel::Grouping(
|
||||
p_->mapping_.get<tag_index>().find(ui_->first->currentIndex())->group_by,
|
||||
p_->mapping_.get<tag_index>().find(ui_->second->currentIndex())->group_by,
|
||||
p_->mapping_.get<tag_index>().find(ui_->third->currentIndex())->group_by)
|
||||
);
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void GroupByDialog::CollectionGroupingChanged(const CollectionModel::Grouping &g) {
|
||||
ui_->first->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[0])->combo_box_index);
|
||||
ui_->second->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[1])->combo_box_index);
|
||||
ui_->third->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[2])->combo_box_index);
|
||||
}
|
||||
|
||||
57
src/collection/groupbydialog.h
Normal file
57
src/collection/groupbydialog.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GROUPBYDIALOG_H
|
||||
#define GROUPBYDIALOG_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "collectionmodel.h"
|
||||
|
||||
class GroupByDialogPrivate;
|
||||
class Ui_GroupByDialog;
|
||||
|
||||
class GroupByDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GroupByDialog(QWidget *parent = nullptr);
|
||||
~GroupByDialog();
|
||||
|
||||
public slots:
|
||||
void CollectionGroupingChanged(const CollectionModel::Grouping &g);
|
||||
void accept();
|
||||
|
||||
signals:
|
||||
void Accepted(const CollectionModel::Grouping &g);
|
||||
|
||||
private slots:
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui_GroupByDialog> ui_;
|
||||
std::unique_ptr<GroupByDialogPrivate> p_;
|
||||
};
|
||||
|
||||
#endif // GROUPBYDIALOG_H
|
||||
366
src/collection/groupbydialog.ui
Normal file
366
src/collection/groupbydialog.ui
Normal file
@@ -0,0 +1,366 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GroupByDialog</class>
|
||||
<widget class="QDialog" name="GroupByDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>354</width>
|
||||
<height>236</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Collection advanced grouping</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>You can change the way the songs in the collection are organised.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Group Collection by...</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>First level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="first">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>File type</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Genre</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bitrate</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Second level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="second">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>File type</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Genre</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bitrate</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Third level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="third">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>File type</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Genre</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bitrate</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>11</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="button_box">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>first</tabstop>
|
||||
<tabstop>second</tabstop>
|
||||
<tabstop>third</tabstop>
|
||||
<tabstop>button_box</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>button_box</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>GroupByDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>button_box</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>GroupByDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
164
src/collection/savedgroupingmanager.cpp
Normal file
164
src/collection/savedgroupingmanager.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "core/iconloader.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "ui_savedgroupingmanager.h"
|
||||
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QSettings>
|
||||
#include <QStandardItem>
|
||||
|
||||
SavedGroupingManager::SavedGroupingManager(QWidget *parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui_SavedGroupingManager),
|
||||
model_(new QStandardItemModel(0, 4, this)) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
model_->setHorizontalHeaderItem(0, new QStandardItem(tr("Name")));
|
||||
model_->setHorizontalHeaderItem(1, new QStandardItem(tr("First level")));
|
||||
model_->setHorizontalHeaderItem(2, new QStandardItem(tr("Second Level")));
|
||||
model_->setHorizontalHeaderItem(3, new QStandardItem(tr("Third Level")));
|
||||
ui_->list->setModel(model_);
|
||||
ui_->remove->setIcon(IconLoader::Load("edit-delete"));
|
||||
ui_->remove->setEnabled(false);
|
||||
|
||||
ui_->remove->setShortcut(QKeySequence::Delete);
|
||||
connect(ui_->list->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(UpdateButtonState()));
|
||||
|
||||
connect(ui_->remove, SIGNAL(clicked()), SLOT(Remove()));
|
||||
}
|
||||
|
||||
SavedGroupingManager::~SavedGroupingManager() {
|
||||
delete ui_;
|
||||
delete model_;
|
||||
}
|
||||
|
||||
QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy &g) {
|
||||
switch (g) {
|
||||
case CollectionModel::GroupBy_None: {
|
||||
return tr("None");
|
||||
}
|
||||
case CollectionModel::GroupBy_Artist: {
|
||||
return tr("Artist");
|
||||
}
|
||||
case CollectionModel::GroupBy_Album: {
|
||||
return tr("Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_YearAlbum: {
|
||||
return tr("Year - Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_Year: {
|
||||
return tr("Year");
|
||||
}
|
||||
case CollectionModel::GroupBy_Composer: {
|
||||
return tr("Composer");
|
||||
}
|
||||
case CollectionModel::GroupBy_Genre: {
|
||||
return tr("Genre");
|
||||
}
|
||||
case CollectionModel::GroupBy_AlbumArtist: {
|
||||
return tr("Album artist");
|
||||
}
|
||||
case CollectionModel::GroupBy_FileType: {
|
||||
return tr("File type");
|
||||
}
|
||||
case CollectionModel::GroupBy_Performer: {
|
||||
return tr("Performer");
|
||||
}
|
||||
case CollectionModel::GroupBy_Grouping: {
|
||||
return tr("Grouping");
|
||||
}
|
||||
case CollectionModel::GroupBy_Bitrate: {
|
||||
return tr("Bitrate");
|
||||
}
|
||||
case CollectionModel::GroupBy_Disc: {
|
||||
return tr("Disc");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYearAlbum: {
|
||||
return tr("Original year - Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYear: {
|
||||
return tr("Original year");
|
||||
}
|
||||
default: { return tr("Unknown"); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SavedGroupingManager::UpdateModel() {
|
||||
|
||||
model_->setRowCount(0); // don't use clear, it deletes headers
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
|
||||
QList<QStandardItem*> list;
|
||||
list << new QStandardItem(saved.at(i))
|
||||
<< new QStandardItem(GroupByToString(g.first))
|
||||
<< new QStandardItem(GroupByToString(g.second))
|
||||
<< new QStandardItem(GroupByToString(g.third));
|
||||
|
||||
model_->appendRow(list);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SavedGroupingManager::Remove() {
|
||||
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
for (const QModelIndex &index :
|
||||
ui_->list->selectionModel()->selectedRows()) {
|
||||
if (index.isValid()) {
|
||||
qLog(Debug) << "Remove saved grouping: " << model_->item(index.row(), 0)->text();
|
||||
s.remove(model_->item(index.row(), 0)->text());
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateModel();
|
||||
filter_->UpdateGroupByActions();
|
||||
|
||||
}
|
||||
|
||||
void SavedGroupingManager::UpdateButtonState() {
|
||||
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
const QModelIndex current = ui_->list->selectionModel()->currentIndex();
|
||||
ui_->remove->setEnabled(current.isValid());
|
||||
}
|
||||
else {
|
||||
ui_->remove->setEnabled(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
56
src/collection/savedgroupingmanager.h
Normal file
56
src/collection/savedgroupingmanager.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2015, Nick Lanham <nick@afternight.org>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SAVEDGROUPINGMANAGER_H
|
||||
#define SAVEDGROUPINGMANAGER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
#include "collectionmodel.h"
|
||||
|
||||
class Ui_SavedGroupingManager;
|
||||
class CollectionFilterWidget;
|
||||
|
||||
class SavedGroupingManager : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SavedGroupingManager(QWidget *parent = nullptr);
|
||||
~SavedGroupingManager();
|
||||
|
||||
void UpdateModel();
|
||||
void SetFilter(CollectionFilterWidget* filter) { filter_ = filter; }
|
||||
|
||||
static QString GroupByToString(const CollectionModel::GroupBy &g);
|
||||
|
||||
private slots:
|
||||
void UpdateButtonState();
|
||||
void Remove();
|
||||
|
||||
private:
|
||||
Ui_SavedGroupingManager* ui_;
|
||||
QStandardItemModel *model_;
|
||||
CollectionFilterWidget *filter_;
|
||||
};
|
||||
|
||||
#endif // SAVEDGROUPINGMANAGER_H
|
||||
144
src/collection/savedgroupingmanager.ui
Normal file
144
src/collection/savedgroupingmanager.ui
Normal file
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SavedGroupingManager</class>
|
||||
<widget class="QDialog" name="SavedGroupingManager">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>582</width>
|
||||
<height>363</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Saved Grouping Manager</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTreeView" name="list">
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Up</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SavedGroupingManager</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SavedGroupingManager</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
40
src/collection/sqlrow.cpp
Normal file
40
src/collection/sqlrow.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "collectionquery.h"
|
||||
#include "sqlrow.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
|
||||
SqlRow::SqlRow(const QSqlQuery &query) { Init(query); }
|
||||
|
||||
SqlRow::SqlRow(const CollectionQuery &query) { Init(query); }
|
||||
|
||||
void SqlRow::Init(const QSqlQuery &query) {
|
||||
|
||||
int rows = query.record().count();
|
||||
for (int i = 0; i < rows; ++i) {
|
||||
columns_ << query.value(i);
|
||||
}
|
||||
|
||||
}
|
||||
54
src/collection/sqlrow.h
Normal file
54
src/collection/sqlrow.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SQLROW_H
|
||||
#define SQLROW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
class CollectionQuery;
|
||||
|
||||
class SqlRow {
|
||||
|
||||
public:
|
||||
// WARNING: Implicit construction from QSqlQuery and CollectionQuery.
|
||||
SqlRow(const QSqlQuery &query);
|
||||
SqlRow(const CollectionQuery &query);
|
||||
|
||||
const QVariant& value(int i) const { return columns_[i]; }
|
||||
|
||||
QList<QVariant> columns_;
|
||||
|
||||
private:
|
||||
SqlRow();
|
||||
|
||||
void Init(const QSqlQuery &query);
|
||||
|
||||
};
|
||||
|
||||
typedef QList<SqlRow> SqlRowList;
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user