Initial commit.

This commit is contained in:
Jonas Kvinge
2018-02-27 18:06:05 +01:00
parent 85d9664df7
commit b2b1ba7abe
1393 changed files with 177311 additions and 1 deletions

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

View 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

File diff suppressed because it is too large Load Diff

View 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

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

View 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

View 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());
}
}

View 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

View 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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Prefix a word with a field name to limit the search to that field, e.g. &lt;span style=&quot; font-weight:600;&quot;&gt;artist:&lt;/span&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;Bode&lt;/span&gt; searches the collection for all artists that contain the word Bode.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Available fields: &lt;/span&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%1&lt;/span&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

View 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

File diff suppressed because it is too large Load Diff

View 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

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

View 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

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

View 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

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

View 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

View 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(); }

View 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

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

View 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();
}

View 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

View 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

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

View 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

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

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

View 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

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