mirror of
https://github.com/clementine-player/Clementine
synced 2024-12-17 12:02:48 +01:00
1246 lines
37 KiB
C++
1246 lines
37 KiB
C++
/* This file is part of Clementine.
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
|
|
|
Clementine is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Clementine is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "librarymodel.h"
|
|
#include "librarybackend.h"
|
|
#include "libraryitem.h"
|
|
#include "librarydirectorymodel.h"
|
|
#include "libraryview.h"
|
|
#include "sqlrow.h"
|
|
#include "core/database.h"
|
|
#include "core/logging.h"
|
|
#include "core/taskmanager.h"
|
|
#include "covers/albumcoverloader.h"
|
|
#include "playlist/songmimedata.h"
|
|
#include "smartplaylists/generator.h"
|
|
#include "smartplaylists/generatormimedata.h"
|
|
#include "smartplaylists/querygenerator.h"
|
|
#include "ui/iconloader.h"
|
|
|
|
#include <QFuture>
|
|
#include <QFutureWatcher>
|
|
#include <QMetaEnum>
|
|
#include <QSettings>
|
|
#include <QStringList>
|
|
#include <QUrl>
|
|
#include <QtConcurrentRun>
|
|
|
|
#include <boost/bind.hpp>
|
|
|
|
using smart_playlists::Generator;
|
|
using smart_playlists::GeneratorMimeData;
|
|
using smart_playlists::GeneratorPtr;
|
|
using smart_playlists::QueryGenerator;
|
|
|
|
const char* LibraryModel::kSmartPlaylistsMimeType = "application/x-clementine-smart-playlist-generator";
|
|
const char* LibraryModel::kSmartPlaylistsSettingsGroup = "SerialisedSmartPlaylists";
|
|
const int LibraryModel::kSmartPlaylistsVersion = 4;
|
|
|
|
typedef QFuture<SqlRowList> RootQueryFuture;
|
|
typedef QFutureWatcher<SqlRowList> RootQueryWatcher;
|
|
|
|
LibraryModel::LibraryModel(LibraryBackend* backend, TaskManager* task_manager,
|
|
QObject* parent)
|
|
: SimpleTreeModel<LibraryItem>(new LibraryItem(this), parent),
|
|
backend_(backend),
|
|
task_manager_(task_manager),
|
|
dir_model_(new LibraryDirectoryModel(backend, this)),
|
|
show_smart_playlists_(false),
|
|
show_various_artists_(true),
|
|
total_song_count_(0),
|
|
artist_icon_(":/icons/22x22/x-clementine-artist.png"),
|
|
album_icon_(":/icons/22x22/x-clementine-album.png"),
|
|
no_cover_icon_(":nocover.png"),
|
|
playlists_dir_icon_(IconLoader::Load("folder-sound")),
|
|
playlist_icon_(":/icons/22x22/x-clementine-albums.png"),
|
|
init_task_id_(-1),
|
|
pretty_cover_size_(32, 32),
|
|
use_pretty_covers_(false),
|
|
show_dividers_(true)
|
|
{
|
|
root_->lazy_loaded = true;
|
|
|
|
group_by_[0] = GroupBy_Artist;
|
|
group_by_[1] = GroupBy_Album;
|
|
group_by_[2] = GroupBy_None;
|
|
|
|
no_cover_icon_pretty_ = QImage(":nocover.png").scaled(pretty_cover_size_,
|
|
Qt::KeepAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
|
|
}
|
|
|
|
LibraryModel::~LibraryModel() {
|
|
delete root_;
|
|
}
|
|
|
|
void LibraryModel::set_pretty_covers(bool use_pretty_covers) {
|
|
if (use_pretty_covers != use_pretty_covers_) {
|
|
use_pretty_covers_ = use_pretty_covers;
|
|
Reset();
|
|
}
|
|
}
|
|
|
|
void LibraryModel::set_show_dividers(bool show_dividers) {
|
|
if (show_dividers != show_dividers_) {
|
|
show_dividers_ = show_dividers;
|
|
Reset();
|
|
}
|
|
}
|
|
|
|
void LibraryModel::Init(bool async) {
|
|
connect(backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
|
|
connect(backend_, SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList)));
|
|
connect(backend_, SIGNAL(SongsStatisticsChanged(SongList)), SLOT(SongsStatisticsChanged(SongList)));
|
|
connect(backend_, SIGNAL(DatabaseReset()), SLOT(Reset()));
|
|
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(TotalSongCountUpdatedSlot(int)));
|
|
|
|
backend_->UpdateTotalSongCountAsync();
|
|
|
|
if (async) {
|
|
// Show a loading indicator in the model.
|
|
LibraryItem* loading = new LibraryItem(LibraryItem::Type_LoadingIndicator, root_);
|
|
loading->display_text = tr("Loading...");
|
|
loading->lazy_loaded = true;
|
|
reset();
|
|
|
|
// Show a loading indicator in the status bar too.
|
|
init_task_id_ = task_manager_->StartTask(tr("Loading songs"));
|
|
|
|
ResetAsync();
|
|
} else {
|
|
Reset();
|
|
}
|
|
}
|
|
|
|
void LibraryModel::SongsDiscovered(const SongList& songs) {
|
|
foreach (const Song& song, songs) {
|
|
// Sanity check to make sure we don't add songs that are outside the user's
|
|
// filter
|
|
if (!query_options_.Matches(song))
|
|
continue;
|
|
|
|
// Hey, we've already got that one!
|
|
if (song_nodes_.contains(song.id()))
|
|
continue;
|
|
|
|
// Before we can add each song we need to make sure the required container
|
|
// items already exist in the tree. These depend on which "group by"
|
|
// settings the user has on the library. Eg. if the user grouped by
|
|
// artist and album, we would need to make sure nodes for the song's artist
|
|
// and album were already in the tree.
|
|
|
|
// Find parent containers in the tree
|
|
LibraryItem* container = root_;
|
|
for (int i=0 ; i<3 ; ++i) {
|
|
GroupBy type = group_by_[i];
|
|
if (type == GroupBy_None) break;
|
|
|
|
// Special case: if we're at the top level and the song is a compilation
|
|
// and the top level is Artists, then we want the Various Artists node :(
|
|
if (i == 0 && type == GroupBy_Artist && song.is_compilation()) {
|
|
if (compilation_artist_node_ == NULL)
|
|
CreateCompilationArtistNode(true, root_);
|
|
container = compilation_artist_node_;
|
|
} else {
|
|
// Otherwise find the proper container at this level based on the
|
|
// item's key
|
|
QString key;
|
|
switch (type) {
|
|
case GroupBy_Album: key = song.album(); break;
|
|
case GroupBy_Artist: key = song.artist(); break;
|
|
case GroupBy_Composer: key = song.composer(); break;
|
|
case GroupBy_Genre: key = song.genre(); break;
|
|
case GroupBy_AlbumArtist: key = song.albumartist(); break;
|
|
case GroupBy_Year:
|
|
key = QString::number(qMax(0, song.year())); break;
|
|
case GroupBy_YearAlbum:
|
|
key = PrettyYearAlbum(qMax(0, song.year()), song.album()); break;
|
|
case GroupBy_FileType: key = song.filetype(); break;
|
|
case GroupBy_None:
|
|
qLog(Error) << "GroupBy_None";
|
|
break;
|
|
}
|
|
|
|
// Does it exist already?
|
|
if (!container_nodes_[i].contains(key)) {
|
|
// Create the container
|
|
container_nodes_[i][key] =
|
|
ItemFromSong(type, true, i == 0, container, song, i);
|
|
}
|
|
container = container_nodes_[i][key];
|
|
}
|
|
|
|
// If we just created the damn thing then we don't need to continue into
|
|
// it any further because it'll get lazy-loaded properly later.
|
|
if (!container->lazy_loaded)
|
|
break;
|
|
}
|
|
|
|
if (!container->lazy_loaded)
|
|
continue;
|
|
|
|
// We've gone all the way down to the deepest level and everything was
|
|
// already lazy loaded, so now we have to create the song in the container.
|
|
song_nodes_[song.id()] =
|
|
ItemFromSong(GroupBy_None, true, false, container, song, -1);
|
|
}
|
|
}
|
|
|
|
void LibraryModel::SongsStatisticsChanged(const SongList& songs) {
|
|
// This is called if there was a minor change to the songs that will not
|
|
// normally require the library to be restructured. We can just update our
|
|
// internal cache of Song objects without worrying about resetting the model.
|
|
foreach (const Song& song, songs) {
|
|
if (song_nodes_.contains(song.id())) {
|
|
song_nodes_[song.id()]->metadata = song;
|
|
}
|
|
}
|
|
}
|
|
|
|
LibraryItem* LibraryModel::CreateCompilationArtistNode(bool signal, LibraryItem* parent) {
|
|
if (signal)
|
|
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
|
|
|
|
compilation_artist_node_ =
|
|
new LibraryItem(LibraryItem::Type_Container, parent);
|
|
compilation_artist_node_->key = tr("Various artists");
|
|
compilation_artist_node_->sort_text = " various";
|
|
compilation_artist_node_->container_level = parent->container_level + 1;
|
|
|
|
if (signal)
|
|
endInsertRows();
|
|
|
|
return compilation_artist_node_;
|
|
}
|
|
|
|
QString LibraryModel::DividerKey(GroupBy type, LibraryItem* item) const {
|
|
// Items which are to be grouped under the same divider must produce the
|
|
// same divider key. This will only get called for top-level items.
|
|
|
|
if (item->sort_text.isEmpty())
|
|
return QString();
|
|
|
|
switch (type) {
|
|
case GroupBy_Album:
|
|
case GroupBy_Artist:
|
|
case GroupBy_Composer:
|
|
case GroupBy_Genre:
|
|
case GroupBy_AlbumArtist:
|
|
case GroupBy_FileType: {
|
|
QChar c = item->sort_text[0];
|
|
if (c.isDigit())
|
|
return "0";
|
|
if (c == ' ')
|
|
return QString();
|
|
if (c.decompositionTag() != QChar::NoDecomposition)
|
|
return QChar(c.decomposition()[0]);
|
|
return c;
|
|
}
|
|
|
|
case GroupBy_Year:
|
|
return SortTextForYear(item->sort_text.toInt() / 10 * 10);
|
|
|
|
case GroupBy_YearAlbum:
|
|
return SortTextForYear(item->metadata.year());
|
|
|
|
case GroupBy_None:
|
|
return QString();
|
|
}
|
|
qLog(Error) << "Unknown GroupBy type" << type << "for item" << item->display_text;
|
|
return QString();
|
|
}
|
|
|
|
QString LibraryModel::DividerDisplayText(GroupBy type, const QString& key) const {
|
|
// Pretty display text for the dividers.
|
|
|
|
switch (type) {
|
|
case GroupBy_Album:
|
|
case GroupBy_Artist:
|
|
case GroupBy_Composer:
|
|
case GroupBy_Genre:
|
|
case GroupBy_AlbumArtist:
|
|
case GroupBy_FileType:
|
|
if (key == "0")
|
|
return "0-9";
|
|
return key.toUpper();
|
|
|
|
case GroupBy_YearAlbum:
|
|
if (key == "0000")
|
|
return tr("Unknown");
|
|
return key.toUpper();
|
|
|
|
case GroupBy_Year:
|
|
if (key == "0000")
|
|
return tr("Unknown");
|
|
return QString::number(key.toInt()); // To remove leading 0s
|
|
|
|
case GroupBy_None:
|
|
// fallthrough
|
|
;
|
|
}
|
|
qLog(Error) << "Unknown GroupBy type" << type << "for divider key" << key;
|
|
return QString();
|
|
}
|
|
|
|
void LibraryModel::SongsDeleted(const SongList& songs) {
|
|
// Delete the actual song nodes first, keeping track of each parent so we
|
|
// might check to see if they're empty later.
|
|
QSet<LibraryItem*> parents;
|
|
foreach (const Song& song, songs) {
|
|
if (song_nodes_.contains(song.id())) {
|
|
LibraryItem* node = song_nodes_[song.id()];
|
|
|
|
if (node->parent != root_)
|
|
parents << node->parent;
|
|
|
|
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
|
|
node->parent->Delete(node->row);
|
|
song_nodes_.remove(song.id());
|
|
endRemoveRows();
|
|
} else {
|
|
// If we get here it means some of the songs we want to delete haven't
|
|
// been lazy-loaded yet. This is bad, because it would mean that to
|
|
// clean up empty parents we would need to lazy-load them all
|
|
// individually to see if they're empty. This can take a very long time,
|
|
// so better to just reset the model and be done with it.
|
|
Reset();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Now delete empty parents
|
|
QSet<QString> divider_keys;
|
|
while (!parents.isEmpty()) {
|
|
foreach (LibraryItem* node, parents) {
|
|
parents.remove(node);
|
|
if (node->children.count() != 0)
|
|
continue;
|
|
|
|
// Consider its parent for the next round
|
|
if (node->parent != root_)
|
|
parents << node->parent;
|
|
|
|
// Maybe consider its divider node
|
|
if (node->container_level == 0)
|
|
divider_keys << DividerKey(group_by_[0], node);
|
|
|
|
// Special case the Various Artists node
|
|
if (node == compilation_artist_node_)
|
|
compilation_artist_node_ = NULL;
|
|
else
|
|
container_nodes_[node->container_level].remove(node->key);
|
|
|
|
// It was empty - delete it
|
|
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
|
|
node->parent->Delete(node->row);
|
|
endRemoveRows();
|
|
}
|
|
}
|
|
|
|
// Delete empty dividers
|
|
foreach (const QString& divider_key, divider_keys) {
|
|
if (!divider_nodes_.contains(divider_key))
|
|
continue;
|
|
|
|
// Look to see if there are any other items still under this divider
|
|
bool found = false;
|
|
foreach (LibraryItem* node, container_nodes_[0].values()) {
|
|
if (DividerKey(group_by_[0], node) == divider_key) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found)
|
|
continue;
|
|
|
|
// Remove the divider
|
|
int row = divider_nodes_[divider_key]->row;
|
|
beginRemoveRows(ItemToIndex(root_), row, row);
|
|
root_->Delete(row);
|
|
endRemoveRows();
|
|
divider_nodes_.remove(divider_key);
|
|
}
|
|
}
|
|
|
|
QVariant LibraryModel::AlbumIcon(const QModelIndex& index, int role) const {
|
|
|
|
// the easiest way to get from *here* to working out what album art an index
|
|
// represents seems to be to get the node's child songs and look at their metadata.
|
|
// if none is found, return the generic CD icon
|
|
|
|
// Cache the art in the item's metadata field
|
|
LibraryItem* item = IndexToItem(index);
|
|
if (!item)
|
|
return no_cover_icon_pretty_;
|
|
if (!item->metadata.image().isNull())
|
|
return item->metadata.image();
|
|
|
|
SongList songs = GetChildSongs(index);
|
|
if (!songs.isEmpty()) {
|
|
const Song& s = songs.first();
|
|
QPixmap pixmap = AlbumCoverLoader::TryLoadPixmap(
|
|
s.art_automatic(), s.art_manual(), s.url().toLocalFile());
|
|
|
|
if (!pixmap.isNull()) {
|
|
QImage image = pixmap.toImage().scaled(
|
|
pretty_cover_size_, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
item->metadata.set_image(image);
|
|
return image;
|
|
}
|
|
}
|
|
return no_cover_icon_pretty_;
|
|
}
|
|
|
|
QVariant LibraryModel::data(const QModelIndex& index, int role) const {
|
|
const LibraryItem* item = IndexToItem(index);
|
|
|
|
// Handle a special case for returning album artwork instead of a generic CD icon.
|
|
// this is here instead of in the other data() function to let us use the
|
|
// QModelIndex& version of GetChildSongs, which satisfies const-ness, instead
|
|
// of the LibraryItem* version, which doesn't.
|
|
if (use_pretty_covers_) {
|
|
bool album_node = false;
|
|
if (role == Qt::DecorationRole && item->type == LibraryItem::Type_Container) {
|
|
GroupBy container_type = group_by_[item->container_level];
|
|
album_node = container_type == GroupBy_Album
|
|
|| container_type == GroupBy_YearAlbum
|
|
|| container_type == GroupBy_AlbumArtist;
|
|
}
|
|
if (album_node)
|
|
return AlbumIcon(index, role);
|
|
}
|
|
|
|
return data(item, role);
|
|
}
|
|
|
|
QVariant LibraryModel::data(const LibraryItem* item, int role) const {
|
|
GroupBy container_type =
|
|
item->type == LibraryItem::Type_Container ?
|
|
group_by_[item->container_level] : GroupBy_None;
|
|
|
|
switch (role) {
|
|
case Qt::DisplayRole:
|
|
case Qt::ToolTipRole:
|
|
return item->DisplayText();
|
|
|
|
case Qt::DecorationRole:
|
|
switch (item->type) {
|
|
case LibraryItem::Type_PlaylistContainer:
|
|
return playlists_dir_icon_;
|
|
case LibraryItem::Type_Container:
|
|
switch (container_type) {
|
|
case GroupBy_Album:
|
|
case GroupBy_YearAlbum:
|
|
return album_icon_;
|
|
case GroupBy_Artist:
|
|
return artist_icon_;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case LibraryItem::Type_SmartPlaylist:
|
|
return playlist_icon_;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case Role_Type:
|
|
return item->type;
|
|
|
|
case Role_IsDivider:
|
|
return item->type == LibraryItem::Type_Divider;
|
|
|
|
case Role_ContainerType:
|
|
return container_type;
|
|
|
|
case Role_Key:
|
|
return item->key;
|
|
|
|
case Role_Artist:
|
|
return item->metadata.artist();
|
|
|
|
case Role_Editable:
|
|
if (!item->lazy_loaded) {
|
|
const_cast<LibraryModel*>(this)->LazyPopulate(
|
|
const_cast<LibraryItem*>(item), true);
|
|
}
|
|
|
|
if(item->type == LibraryItem::Type_Container) {
|
|
// if we have even one non editable item as a child, we ourselves
|
|
// are not available for edit
|
|
if(!item->children.isEmpty()) {
|
|
foreach(LibraryItem* child, item->children) {
|
|
if(!data(child, role).toBool()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else if(item->type == LibraryItem::Type_Song) {
|
|
return item->metadata.IsEditable();
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
case Role_SortText:
|
|
return item->SortText();
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
SqlRowList LibraryModel::RunRootQuery(const QueryOptions& query_options,
|
|
const Grouping& group_by) {
|
|
// Warning: Some copy-paste with LazyPopulate here
|
|
|
|
// Information about what we want the children to be
|
|
GroupBy child_type = group_by[0];
|
|
|
|
// Initialise the query. child_type says what type of thing we want (artists,
|
|
// songs, etc.)
|
|
LibraryQuery q(query_options);
|
|
InitQuery(child_type, &q);
|
|
|
|
// Top-level artists is special - we don't want compilation albums appearing
|
|
if (child_type == GroupBy_Artist) {
|
|
q.AddCompilationRequirement(false);
|
|
}
|
|
|
|
// Execute the query
|
|
QMutexLocker l(backend_->db()->Mutex());
|
|
if (!backend_->ExecQuery(&q))
|
|
return SqlRowList();
|
|
|
|
SqlRowList rows;
|
|
while (q.Next()) {
|
|
rows << SqlRow(q);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
void LibraryModel::LazyPopulate(LibraryItem* parent, bool signal) {
|
|
if (parent->lazy_loaded)
|
|
return;
|
|
parent->lazy_loaded = true;
|
|
|
|
// Warning: Some copy-paste with RunRootQuery here
|
|
|
|
// Information about what we want the children to be
|
|
int child_level = parent->container_level + 1;
|
|
GroupBy child_type = child_level >= 3 ? GroupBy_None : group_by_[child_level];
|
|
|
|
// Initialise the query. child_type says what type of thing we want (artists,
|
|
// songs, etc.)
|
|
LibraryQuery q(query_options_);
|
|
InitQuery(child_type, &q);
|
|
|
|
// Top-level artists is special - we don't want compilation albums appearing
|
|
if (child_level == 0 && child_type == GroupBy_Artist) {
|
|
q.AddCompilationRequirement(false);
|
|
}
|
|
|
|
// Walk up through the item's parents adding filters as necessary
|
|
LibraryItem* p = parent;
|
|
while (p && p->type == LibraryItem::Type_Container) {
|
|
FilterQuery(group_by_[p->container_level], p, &q);
|
|
p = p->parent;
|
|
}
|
|
|
|
// Execute the query
|
|
QMutexLocker l(backend_->db()->Mutex());
|
|
if (!backend_->ExecQuery(&q))
|
|
return;
|
|
|
|
// Step through the results
|
|
while (q.Next()) {
|
|
// Warning: Some copy-paste with ResetAsyncQueryFinished here
|
|
|
|
// Create the item - it will get inserted into the model here
|
|
LibraryItem* item =
|
|
ItemFromQuery(child_type, signal, child_level == 0, parent, SqlRow(q),
|
|
child_level);
|
|
|
|
// Save a pointer to it for later
|
|
if (child_type == GroupBy_None)
|
|
song_nodes_[item->metadata.id()] = item;
|
|
else
|
|
container_nodes_[child_level][item->key] = item;
|
|
}
|
|
}
|
|
|
|
void LibraryModel::ResetAsync() {
|
|
RootQueryFuture future = QtConcurrent::run(
|
|
this, &LibraryModel::RunRootQuery, query_options_, group_by_);
|
|
RootQueryWatcher* watcher = new RootQueryWatcher(this);
|
|
watcher->setFuture(future);
|
|
|
|
connect(watcher, SIGNAL(finished()), SLOT(ResetAsyncQueryFinished()));
|
|
}
|
|
|
|
void LibraryModel::ResetAsyncQueryFinished() {
|
|
RootQueryWatcher* watcher = static_cast<RootQueryWatcher*>(sender());
|
|
const SqlRowList rows = watcher->result();
|
|
watcher->deleteLater();
|
|
|
|
BeginReset();
|
|
root_->lazy_loaded = true;
|
|
|
|
foreach (const SqlRow& row, rows) {
|
|
// Warning: Some copy-paste with LazyPopulate here
|
|
|
|
const GroupBy child_type = group_by_[0];
|
|
|
|
// Create the item - it will get inserted into the model here
|
|
LibraryItem* item =
|
|
ItemFromQuery(child_type, false, true, root_, row, 0);
|
|
|
|
// Save a pointer to it for later
|
|
if (child_type == GroupBy_None)
|
|
song_nodes_[item->metadata.id()] = item;
|
|
else
|
|
container_nodes_[0][item->key] = item;
|
|
}
|
|
|
|
if (init_task_id_ != -1) {
|
|
task_manager_->SetTaskFinished(init_task_id_);
|
|
init_task_id_ = -1;
|
|
}
|
|
|
|
reset();
|
|
}
|
|
|
|
void LibraryModel::BeginReset() {
|
|
delete root_;
|
|
song_nodes_.clear();
|
|
container_nodes_[0].clear();
|
|
container_nodes_[1].clear();
|
|
container_nodes_[2].clear();
|
|
divider_nodes_.clear();
|
|
compilation_artist_node_ = NULL;
|
|
smart_playlist_node_ = NULL;
|
|
|
|
root_ = new LibraryItem(this);
|
|
root_->lazy_loaded = false;
|
|
|
|
if (show_various_artists_) {
|
|
// Various artists?
|
|
if (group_by_[0] == GroupBy_Artist &&
|
|
backend_->HasCompilations(query_options_))
|
|
CreateCompilationArtistNode(false, root_);
|
|
}
|
|
|
|
// Smart playlists?
|
|
if (show_smart_playlists_ && query_options_.filter().isEmpty())
|
|
CreateSmartPlaylists();
|
|
}
|
|
|
|
void LibraryModel::Reset() {
|
|
BeginReset();
|
|
|
|
// Populate top level
|
|
LazyPopulate(root_, false);
|
|
|
|
reset();
|
|
}
|
|
|
|
void LibraryModel::InitQuery(GroupBy type, LibraryQuery* q) {
|
|
// Say what type of thing we want to get back from the database.
|
|
switch (type) {
|
|
case GroupBy_Artist:
|
|
q->SetColumnSpec("DISTINCT artist");
|
|
break;
|
|
case GroupBy_Album:
|
|
q->SetColumnSpec("DISTINCT album");
|
|
break;
|
|
case GroupBy_Composer:
|
|
q->SetColumnSpec("DISTINCT composer");
|
|
break;
|
|
case GroupBy_YearAlbum:
|
|
q->SetColumnSpec("DISTINCT year, album");
|
|
break;
|
|
case GroupBy_Year:
|
|
q->SetColumnSpec("DISTINCT year");
|
|
break;
|
|
case GroupBy_Genre:
|
|
q->SetColumnSpec("DISTINCT genre");
|
|
break;
|
|
case GroupBy_AlbumArtist:
|
|
q->SetColumnSpec("DISTINCT albumartist");
|
|
break;
|
|
case GroupBy_None:
|
|
q->SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
|
|
break;
|
|
case GroupBy_FileType:
|
|
q->SetColumnSpec("DISTINCT filetype");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void LibraryModel::FilterQuery(GroupBy type, LibraryItem* item, LibraryQuery* q) {
|
|
// Say how we want the query to be filtered. This is done once for each
|
|
// parent going up the tree.
|
|
|
|
switch (type) {
|
|
case GroupBy_Artist:
|
|
if (item == compilation_artist_node_)
|
|
q->AddCompilationRequirement(true);
|
|
else {
|
|
if (item->container_level == 0) // Stupid hack
|
|
q->AddCompilationRequirement(false);
|
|
q->AddWhere("artist", item->key);
|
|
}
|
|
break;
|
|
case GroupBy_Album:
|
|
q->AddWhere("album", item->key);
|
|
break;
|
|
case GroupBy_YearAlbum:
|
|
q->AddWhere("year", item->metadata.year());
|
|
q->AddWhere("album", item->metadata.album());
|
|
break;
|
|
case GroupBy_Year:
|
|
q->AddWhere("year", item->key);
|
|
break;
|
|
case GroupBy_Composer:
|
|
q->AddWhere("composer", item->key);
|
|
break;
|
|
case GroupBy_Genre:
|
|
q->AddWhere("genre", item->key);
|
|
break;
|
|
case GroupBy_AlbumArtist:
|
|
q->AddWhere("albumartist", item->key);
|
|
break;
|
|
case GroupBy_FileType:
|
|
q->AddWhere("filetype", item->metadata.filetype());
|
|
break;
|
|
case GroupBy_None:
|
|
qLog(Error) << "Unknown GroupBy type" << type << "used in filter";
|
|
break;
|
|
}
|
|
}
|
|
|
|
LibraryItem* LibraryModel::InitItem(GroupBy type, bool signal, LibraryItem *parent,
|
|
int container_level) {
|
|
LibraryItem::Type item_type =
|
|
type == GroupBy_None ? LibraryItem::Type_Song :
|
|
LibraryItem::Type_Container;
|
|
|
|
if (signal)
|
|
beginInsertRows(ItemToIndex(parent),
|
|
parent->children.count(),parent->children.count());
|
|
|
|
// Initialise the item depending on what type it's meant to be
|
|
LibraryItem* item = new LibraryItem(item_type, parent);
|
|
item->container_level = container_level;
|
|
return item;
|
|
}
|
|
|
|
LibraryItem* LibraryModel::ItemFromQuery(GroupBy type,
|
|
bool signal, bool create_divider,
|
|
LibraryItem* parent, const SqlRow& row,
|
|
int container_level) {
|
|
LibraryItem* item = InitItem(type, signal, parent, container_level);
|
|
int year = 0;
|
|
|
|
switch (type) {
|
|
case GroupBy_Artist:
|
|
item->key = row.value(0).toString();
|
|
item->display_text = TextOrUnknown(item->key);
|
|
item->sort_text = SortTextForArtist(item->key);
|
|
break;
|
|
|
|
case GroupBy_YearAlbum:
|
|
year = qMax(0, row.value(0).toInt());
|
|
item->metadata.set_year(row.value(0).toInt());
|
|
item->metadata.set_album(row.value(1).toString());
|
|
item->key = PrettyYearAlbum(year, item->metadata.album());
|
|
item->sort_text = SortTextForYear(year) + item->metadata.album();
|
|
break;
|
|
|
|
case GroupBy_Year:
|
|
year = qMax(0, row.value(0).toInt());
|
|
item->key = QString::number(year);
|
|
item->sort_text = SortTextForYear(year) + " ";
|
|
break;
|
|
|
|
case GroupBy_Composer:
|
|
case GroupBy_Genre:
|
|
case GroupBy_Album:
|
|
case GroupBy_AlbumArtist:
|
|
item->key = row.value(0).toString();
|
|
item->display_text = TextOrUnknown(item->key);
|
|
item->sort_text = SortTextForArtist(item->key);
|
|
break;
|
|
|
|
case GroupBy_FileType:
|
|
item->metadata.set_filetype(Song::FileType(row.value(0).toInt()));
|
|
item->key = item->metadata.TextForFiletype();
|
|
break;
|
|
|
|
case GroupBy_None:
|
|
item->metadata.InitFromQuery(row, true);
|
|
item->key = item->metadata.title();
|
|
item->display_text = item->metadata.TitleWithCompilationArtist();
|
|
item->sort_text = SortTextForSong(item->metadata);
|
|
break;
|
|
}
|
|
|
|
FinishItem(type, signal, create_divider, parent, item);
|
|
return item;
|
|
}
|
|
|
|
LibraryItem* LibraryModel::ItemFromSong(GroupBy type,
|
|
bool signal, bool create_divider,
|
|
LibraryItem* parent, const Song& s,
|
|
int container_level) {
|
|
LibraryItem* item = InitItem(type, signal, parent, container_level);
|
|
int year = 0;
|
|
|
|
switch (type) {
|
|
case GroupBy_Artist:
|
|
item->key = s.artist();
|
|
item->display_text = TextOrUnknown(item->key);
|
|
item->sort_text = SortTextForArtist(item->key);
|
|
break;
|
|
|
|
case GroupBy_YearAlbum:
|
|
year = qMax(0, s.year());
|
|
item->metadata.set_year(year);
|
|
item->metadata.set_album(s.album());
|
|
item->key = PrettyYearAlbum(year, s.album());
|
|
item->sort_text = SortTextForYear(year) + s.album();
|
|
break;
|
|
|
|
case GroupBy_Year:
|
|
year = qMax(0, s.year());
|
|
item->key = QString::number(year);
|
|
item->sort_text = SortTextForYear(year) + " ";
|
|
break;
|
|
|
|
case GroupBy_Composer: item->key = s.composer();
|
|
case GroupBy_Genre: if (item->key.isNull()) item->key = s.genre();
|
|
case GroupBy_Album: if (item->key.isNull()) item->key = s.album();
|
|
case GroupBy_AlbumArtist: if (item->key.isNull()) item->key = s.albumartist();
|
|
item->display_text = TextOrUnknown(item->key);
|
|
item->sort_text = SortTextForArtist(item->key);
|
|
break;
|
|
|
|
case GroupBy_FileType:
|
|
item->metadata.set_filetype(s.filetype());
|
|
item->key = s.TextForFiletype();
|
|
break;
|
|
|
|
case GroupBy_None:
|
|
item->metadata = s;
|
|
item->key = s.title();
|
|
item->display_text = s.TitleWithCompilationArtist();
|
|
item->sort_text = SortTextForSong(s);
|
|
break;
|
|
}
|
|
|
|
FinishItem(type, signal, create_divider, parent, item);
|
|
if (s.url().scheme() == "cdda")
|
|
item->lazy_loaded = true;
|
|
return item;
|
|
}
|
|
|
|
void LibraryModel::FinishItem(GroupBy type,
|
|
bool signal, bool create_divider,
|
|
LibraryItem *parent, LibraryItem *item) {
|
|
if (type == GroupBy_None)
|
|
item->lazy_loaded = true;
|
|
|
|
if (signal)
|
|
endInsertRows();
|
|
|
|
// Create the divider entry if we're supposed to
|
|
if (create_divider && show_dividers_) {
|
|
QString divider_key = DividerKey(type, item);
|
|
item->sort_text.prepend(divider_key);
|
|
|
|
if (!divider_key.isEmpty() && !divider_nodes_.contains(divider_key)) {
|
|
if (signal)
|
|
beginInsertRows(ItemToIndex(parent), parent->children.count(),
|
|
parent->children.count());
|
|
|
|
LibraryItem* divider =
|
|
new LibraryItem(LibraryItem::Type_Divider, root_);
|
|
divider->key = divider_key;
|
|
divider->display_text = DividerDisplayText(type, divider_key);
|
|
divider->lazy_loaded = true;
|
|
|
|
divider_nodes_[divider_key] = divider;
|
|
|
|
if (signal)
|
|
endInsertRows();
|
|
}
|
|
}
|
|
}
|
|
|
|
QString LibraryModel::TextOrUnknown(const QString& text) const {
|
|
if (text.isEmpty()) {
|
|
return tr("Unknown");
|
|
}
|
|
return text;
|
|
}
|
|
|
|
QString LibraryModel::PrettyYearAlbum(int year, const QString& album) const {
|
|
if (year <= 0)
|
|
return TextOrUnknown(album);
|
|
return QString::number(year) + " - " + TextOrUnknown(album);
|
|
}
|
|
|
|
QString LibraryModel::SortText(QString text) const {
|
|
if (text.isEmpty()) {
|
|
text = " unknown";
|
|
} else {
|
|
text = text.toLower();
|
|
}
|
|
text = text.remove(QRegExp("[^\\w ]"));
|
|
|
|
return text;
|
|
}
|
|
|
|
QString LibraryModel::SortTextForArtist(QString artist) const {
|
|
artist = SortText(artist);
|
|
|
|
if (artist.startsWith("the ")) {
|
|
artist = artist.right(artist.length() - 4) + ", the";
|
|
}
|
|
|
|
return artist;
|
|
}
|
|
|
|
QString LibraryModel::SortTextForYear(int year) const {
|
|
QString str = QString::number(year);
|
|
return QString("0").repeated(qMax(0, 4 - str.length())) + str;
|
|
}
|
|
|
|
QString LibraryModel::SortTextForSong(const Song& song) const {
|
|
QString ret = QString::number(qMax(0, song.disc()) * 1000 + qMax(0, song.track()));
|
|
ret.prepend(QString("0").repeated(6 - ret.length()));
|
|
ret.append(song.url().toString());
|
|
return ret;
|
|
}
|
|
|
|
Qt::ItemFlags LibraryModel::flags(const QModelIndex& index) const {
|
|
switch (IndexToItem(index)->type) {
|
|
case LibraryItem::Type_Song:
|
|
case LibraryItem::Type_Container:
|
|
case LibraryItem::Type_SmartPlaylist:
|
|
return Qt::ItemIsSelectable |
|
|
Qt::ItemIsEnabled |
|
|
Qt::ItemIsDragEnabled;
|
|
case LibraryItem::Type_Divider:
|
|
case LibraryItem::Type_Root:
|
|
case LibraryItem::Type_LoadingIndicator:
|
|
default:
|
|
return Qt::ItemIsEnabled;
|
|
}
|
|
}
|
|
|
|
QStringList LibraryModel::mimeTypes() const {
|
|
return QStringList() << "text/uri-list";
|
|
}
|
|
|
|
QMimeData* LibraryModel::mimeData(const QModelIndexList& indexes) const {
|
|
if (indexes.isEmpty())
|
|
return NULL;
|
|
|
|
// Special case: a smart playlist was dragged
|
|
if (IndexToItem(indexes.first())->type == LibraryItem::Type_SmartPlaylist) {
|
|
GeneratorPtr generator = CreateGenerator(indexes.first());
|
|
if (!generator)
|
|
return NULL;
|
|
|
|
GeneratorMimeData* data = new GeneratorMimeData(generator);
|
|
data->setData(kSmartPlaylistsMimeType, QByteArray());
|
|
data->name_for_new_playlist_ = this->data(indexes.first()).toString();
|
|
return data;
|
|
}
|
|
|
|
SongMimeData* data = new SongMimeData;
|
|
QList<QUrl> urls;
|
|
QSet<int> song_ids;
|
|
|
|
data->backend = backend_;
|
|
|
|
foreach (const QModelIndex& index, indexes) {
|
|
GetChildSongs(IndexToItem(index), &urls, &data->songs, &song_ids);
|
|
}
|
|
|
|
data->setUrls(urls);
|
|
data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
|
|
|
|
return data;
|
|
}
|
|
|
|
bool LibraryModel::CompareItems(const LibraryItem* a, const LibraryItem* b) const {
|
|
QVariant left(data(a, LibraryModel::Role_SortText));
|
|
QVariant right(data(b, LibraryModel::Role_SortText));
|
|
|
|
if (left.type() == QVariant::Int)
|
|
return left.toInt() < right.toInt();
|
|
return left.toString() < right.toString();
|
|
}
|
|
|
|
void LibraryModel::GetChildSongs(LibraryItem* item, QList<QUrl>* urls,
|
|
SongList* songs, QSet<int>* song_ids) const {
|
|
switch (item->type) {
|
|
case LibraryItem::Type_Container: {
|
|
const_cast<LibraryModel*>(this)->LazyPopulate(item);
|
|
|
|
QList<LibraryItem*> children = item->children;
|
|
qSort(children.begin(), children.end(), boost::bind(
|
|
&LibraryModel::CompareItems, this, _1, _2));
|
|
|
|
foreach (LibraryItem* child, children)
|
|
GetChildSongs(child, urls, songs, song_ids);
|
|
break;
|
|
}
|
|
|
|
case LibraryItem::Type_Song:
|
|
urls->append(item->metadata.url());
|
|
if (!song_ids->contains(item->metadata.id())) {
|
|
songs->append(item->metadata);
|
|
song_ids->insert(item->metadata.id());
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
SongList LibraryModel::GetChildSongs(const QModelIndexList& indexes) const {
|
|
QList<QUrl> dontcare;
|
|
SongList ret;
|
|
QSet<int> song_ids;
|
|
|
|
foreach (const QModelIndex& index, indexes) {
|
|
GetChildSongs(IndexToItem(index), &dontcare, &ret, &song_ids);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
SongList LibraryModel::GetChildSongs(const QModelIndex &index) const {
|
|
return GetChildSongs(QModelIndexList() << index);
|
|
}
|
|
|
|
void LibraryModel::SetFilterAge(int age) {
|
|
query_options_.set_max_age(age);
|
|
ResetAsync();
|
|
}
|
|
|
|
void LibraryModel::SetFilterText(const QString& text) {
|
|
query_options_.set_filter(text);
|
|
ResetAsync();
|
|
}
|
|
|
|
void LibraryModel::SetFilterQueryMode(QueryOptions::QueryMode query_mode) {
|
|
query_options_.set_query_mode(query_mode);
|
|
ResetAsync();
|
|
}
|
|
|
|
bool LibraryModel::canFetchMore(const QModelIndex &parent) const {
|
|
if (!parent.isValid())
|
|
return false;
|
|
|
|
LibraryItem* item = IndexToItem(parent);
|
|
return !item->lazy_loaded;
|
|
}
|
|
|
|
void LibraryModel::SetGroupBy(const Grouping& g) {
|
|
group_by_ = g;
|
|
|
|
ResetAsync();
|
|
emit GroupingChanged(g);
|
|
}
|
|
|
|
const LibraryModel::GroupBy& LibraryModel::Grouping::operator [](int i) const {
|
|
switch (i) {
|
|
case 0: return first;
|
|
case 1: return second;
|
|
case 2: return third;
|
|
}
|
|
qLog(Error) << "LibraryModel::Grouping[] index out of range" << i;
|
|
return first;
|
|
}
|
|
|
|
LibraryModel::GroupBy& LibraryModel::Grouping::operator [](int i) {
|
|
switch (i) {
|
|
case 0: return first;
|
|
case 1: return second;
|
|
case 2: return third;
|
|
}
|
|
qLog(Error) << "LibraryModel::Grouping[] index out of range" << i;
|
|
return first;
|
|
}
|
|
|
|
void LibraryModel::CreateSmartPlaylists() {
|
|
smart_playlist_node_ = new LibraryItem(LibraryItem::Type_PlaylistContainer, root_);
|
|
smart_playlist_node_->container_level = 0;
|
|
smart_playlist_node_->sort_text = "\0";
|
|
smart_playlist_node_->key = tr("Smart playlists");
|
|
smart_playlist_node_->lazy_loaded = true;
|
|
|
|
QSettings s;
|
|
s.beginGroup(kSmartPlaylistsSettingsGroup);
|
|
int version = s.value(backend_->songs_table() + "_version", 0).toInt();
|
|
|
|
// How many defaults do we have to write?
|
|
int unwritten_defaults = 0;
|
|
for (int i=version; i < default_smart_playlists_.count() ; ++i) {
|
|
unwritten_defaults += default_smart_playlists_[i].count();
|
|
}
|
|
|
|
// Save the defaults if there are any unwritten ones
|
|
if (unwritten_defaults) {
|
|
// How many items are stored already?
|
|
int playlist_index = s.beginReadArray(backend_->songs_table());
|
|
s.endArray();
|
|
|
|
// Append the new ones
|
|
s.beginWriteArray(backend_->songs_table(), playlist_index + unwritten_defaults);
|
|
for (; version < default_smart_playlists_.count() ; ++version) {
|
|
foreach (smart_playlists::GeneratorPtr gen, default_smart_playlists_[version]) {
|
|
SaveGenerator(&s, playlist_index++, gen);
|
|
}
|
|
}
|
|
s.endArray();
|
|
}
|
|
|
|
s.setValue(backend_->songs_table() + "_version", version);
|
|
|
|
const int count = s.beginReadArray(backend_->songs_table());
|
|
for (int i=0 ; i<count ; ++i) {
|
|
s.setArrayIndex(i);
|
|
ItemFromSmartPlaylist(s, false);
|
|
}
|
|
}
|
|
|
|
void LibraryModel::ItemFromSmartPlaylist(const QSettings& s, bool notify) const {
|
|
LibraryItem* item = new LibraryItem(LibraryItem::Type_SmartPlaylist,
|
|
notify ? NULL : smart_playlist_node_);
|
|
item->display_text = tr(qPrintable(s.value("name").toString()));
|
|
item->sort_text = item->display_text;
|
|
item->key = s.value("type").toString();
|
|
item->smart_playlist_data = s.value("data").toByteArray();
|
|
item->lazy_loaded = true;
|
|
|
|
if (notify)
|
|
item->InsertNotify(smart_playlist_node_);
|
|
}
|
|
|
|
void LibraryModel::AddGenerator(GeneratorPtr gen) {
|
|
QSettings s;
|
|
s.beginGroup(kSmartPlaylistsSettingsGroup);
|
|
|
|
// Count the existing items
|
|
const int count = s.beginReadArray(backend_->songs_table());
|
|
s.endArray();
|
|
|
|
// Add this one to the end
|
|
s.beginWriteArray(backend_->songs_table(), count + 1);
|
|
SaveGenerator(&s, count, gen);
|
|
|
|
// Add it to the model
|
|
ItemFromSmartPlaylist(s, true);
|
|
|
|
s.endArray();
|
|
}
|
|
|
|
void LibraryModel::UpdateGenerator(const QModelIndex& index, GeneratorPtr gen) {
|
|
if (index.parent() != ItemToIndex(smart_playlist_node_))
|
|
return;
|
|
LibraryItem* item = IndexToItem(index);
|
|
if (!item)
|
|
return;
|
|
|
|
// Update the config
|
|
QSettings s;
|
|
s.beginGroup(kSmartPlaylistsSettingsGroup);
|
|
|
|
// Count the existing items
|
|
const int count = s.beginReadArray(backend_->songs_table());
|
|
s.endArray();
|
|
|
|
s.beginWriteArray(backend_->songs_table(), count);
|
|
SaveGenerator(&s, index.row(), gen);
|
|
|
|
// Update the text of the item
|
|
item->display_text = gen->name();
|
|
item->sort_text = item->display_text;
|
|
item->key = gen->type();
|
|
item->smart_playlist_data = gen->Save();
|
|
item->ChangedNotify();
|
|
}
|
|
|
|
void LibraryModel::DeleteGenerator(const QModelIndex& index) {
|
|
if (index.parent() != ItemToIndex(smart_playlist_node_))
|
|
return;
|
|
|
|
// Remove the item from the tree
|
|
smart_playlist_node_->DeleteNotify(index.row());
|
|
|
|
QSettings s;
|
|
s.beginGroup(kSmartPlaylistsSettingsGroup);
|
|
|
|
// Rewrite all the items to the settings
|
|
s.beginWriteArray(backend_->songs_table(), smart_playlist_node_->children.count());
|
|
int i = 0;
|
|
foreach (LibraryItem* item, smart_playlist_node_->children) {
|
|
s.setArrayIndex(i++);
|
|
s.setValue("name", item->display_text);
|
|
s.setValue("type", item->key);
|
|
s.setValue("data", item->smart_playlist_data);
|
|
}
|
|
s.endArray();
|
|
}
|
|
|
|
void LibraryModel::SaveGenerator(QSettings* s, int i, GeneratorPtr generator) const {
|
|
s->setArrayIndex(i);
|
|
s->setValue("name", generator->name());
|
|
s->setValue("type", generator->type());
|
|
s->setValue("data", generator->Save());
|
|
}
|
|
|
|
GeneratorPtr LibraryModel::CreateGenerator(const QModelIndex& index) const {
|
|
GeneratorPtr ret;
|
|
|
|
const LibraryItem* item = IndexToItem(index);
|
|
if (!item || item->type != LibraryItem::Type_SmartPlaylist)
|
|
return ret;
|
|
|
|
ret = Generator::Create(item->key);
|
|
if (!ret)
|
|
return ret;
|
|
|
|
ret->set_name(item->display_text);
|
|
ret->set_library(backend());
|
|
ret->Load(item->smart_playlist_data);
|
|
return ret;
|
|
}
|
|
|
|
void LibraryModel::TotalSongCountUpdatedSlot(int count) {
|
|
total_song_count_ = count;
|
|
emit TotalSongCountUpdated(count);
|
|
}
|