2018-02-27 18:06:05 +01:00
|
|
|
/*
|
|
|
|
* Strawberry Music Player
|
|
|
|
* This file was part of Clementine.
|
|
|
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
2021-03-20 21:14:47 +01:00
|
|
|
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
2018-02-27 18:06:05 +01:00
|
|
|
*
|
|
|
|
* 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/>.
|
2018-08-09 18:39:44 +02:00
|
|
|
*
|
2018-02-27 18:06:05 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QtGlobal>
|
2020-10-24 03:32:40 +02:00
|
|
|
#include <QMetaType>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QDateTime>
|
|
|
|
#include <QVariant>
|
|
|
|
#include <QString>
|
|
|
|
#include <QStringList>
|
|
|
|
#include <QStringBuilder>
|
2020-07-18 04:05:07 +02:00
|
|
|
#include <QRegularExpression>
|
2018-05-01 00:41:33 +02:00
|
|
|
#include <QSqlDatabase>
|
|
|
|
#include <QSqlQuery>
|
2021-04-10 03:20:25 +02:00
|
|
|
#include <QSqlError>
|
2018-05-01 00:41:33 +02:00
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
#include "core/logging.h"
|
2018-02-27 18:06:05 +01:00
|
|
|
#include "core/song.h"
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
#include "collectionquery.h"
|
|
|
|
|
2018-02-27 18:06:05 +01:00
|
|
|
QueryOptions::QueryOptions() : max_age_(-1), query_mode_(QueryMode_All) {}
|
|
|
|
|
2021-07-11 07:40:57 +02:00
|
|
|
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const QueryOptions &options)
|
|
|
|
: QSqlQuery(db),
|
|
|
|
songs_table_(songs_table),
|
|
|
|
fts_table_(fts_table),
|
|
|
|
include_unavailable_(false),
|
|
|
|
join_with_fts_(false),
|
|
|
|
duplicates_only_(false),
|
|
|
|
limit_(-1) {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
if (!options.filter().isEmpty()) {
|
2019-07-30 22:45:22 +02:00
|
|
|
// We need to munge the filter text a little bit to get it to work as expected with sqlite's FTS5:
|
2018-02-27 18:06:05 +01:00
|
|
|
// 1) Append * to all tokens.
|
|
|
|
// 2) Prefix "fts" to column names.
|
|
|
|
// 3) Remove colons which don't correspond to column names.
|
|
|
|
|
|
|
|
// Split on whitespace
|
2020-05-29 17:40:11 +02:00
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
2020-07-18 04:05:07 +02:00
|
|
|
QStringList tokens(options.filter().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts));
|
2020-05-29 17:40:11 +02:00
|
|
|
#else
|
2020-07-18 04:05:07 +02:00
|
|
|
QStringList tokens(options.filter().split(QRegularExpression("\\s+"), QString::SkipEmptyParts));
|
2020-05-29 17:40:11 +02:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
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.
|
2018-05-01 00:41:33 +02:00
|
|
|
if (Song::kFtsColumns.contains("fts" + token.section(':', 0, 0), Qt::CaseInsensitive)) {
|
2018-02-27 18:06:05 +01:00
|
|
|
// Account for multiple colons.
|
|
|
|
QString columntoken = token.section(':', 0, 0, QString::SectionIncludeTrailingSep);
|
|
|
|
QString subtoken = token.section(':', 1, -1);
|
|
|
|
subtoken.replace(":", " ");
|
|
|
|
subtoken = subtoken.trimmed();
|
2020-07-29 21:41:35 +02:00
|
|
|
if (!subtoken.isEmpty()) {
|
|
|
|
if (!query.isEmpty()) query.append(" ");
|
|
|
|
query += "fts" + columntoken + "\"" + subtoken + "\"*";
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
token.replace(":", " ");
|
|
|
|
token = token.trimmed();
|
2020-07-29 21:41:35 +02:00
|
|
|
if (!query.isEmpty()) query.append(" ");
|
|
|
|
query += "\"" + token + "\"*";
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2020-07-29 21:41:35 +02:00
|
|
|
if (!query.isEmpty()) query.append(" ");
|
|
|
|
query += "\"" + token + "\"*";
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
}
|
2019-07-26 19:14:15 +02:00
|
|
|
if (!query.isEmpty()) {
|
|
|
|
where_clauses_ << "fts.%fts_table_noprefix MATCH ?";
|
|
|
|
bound_values_ << query;
|
|
|
|
join_with_fts_ = true;
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (options.max_age() != -1) {
|
2021-03-21 18:53:02 +01:00
|
|
|
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - options.max_age();
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
where_clauses_ << "ctime > ?";
|
|
|
|
bound_values_ << cutoff;
|
|
|
|
}
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
// TODO: Currently you cannot use any QueryMode other than All and FTS at the same time.
|
2018-05-01 00:41:33 +02:00
|
|
|
// 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.
|
2018-02-27 18:06:05 +01:00
|
|
|
duplicates_only_ = options.query_mode() == QueryOptions::QueryMode_Duplicates;
|
|
|
|
|
|
|
|
if (options.query_mode() == QueryOptions::QueryMode_Untagged) {
|
|
|
|
where_clauses_ << "(artist = '' OR album = '' OR title ='')";
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-22 13:45:29 +02:00
|
|
|
QString CollectionQuery::GetInnerQuery() const {
|
2018-02-27 18:06:05 +01:00
|
|
|
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) {
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
// Ignore 'literal' for IN
|
2021-06-22 13:54:58 +02:00
|
|
|
if (op.compare("IN", Qt::CaseInsensitive) == 0) {
|
2021-06-20 19:04:08 +02:00
|
|
|
QStringList values = value.toStringList();
|
2018-02-27 18:06:05 +01:00
|
|
|
QStringList final;
|
2021-06-20 19:04:08 +02:00
|
|
|
final.reserve(values.count());
|
|
|
|
for (const QString &single_value : values) {
|
2018-02-27 18:06:05 +01:00
|
|
|
final.append("?");
|
|
|
|
bound_values_ << single_value;
|
|
|
|
}
|
|
|
|
|
|
|
|
where_clauses_ << QString("%1 IN (" + final.join(",") + ")").arg(column);
|
|
|
|
}
|
|
|
|
else {
|
2018-05-01 00:41:33 +02:00
|
|
|
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
|
2020-10-24 03:32:40 +02:00
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
|
|
if (value.metaType().id() == QMetaType::Int) {
|
|
|
|
#else
|
2018-02-27 18:06:05 +01:00
|
|
|
if (value.type() == QVariant::Int) {
|
2020-10-24 03:32:40 +02:00
|
|
|
#endif
|
2018-02-27 18:06:05 +01:00
|
|
|
where_clauses_ << QString("%1 %2 %3").arg(column, op, value.toString());
|
|
|
|
}
|
2020-10-24 03:32:40 +02:00
|
|
|
else if (
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
|
|
value.metaType().id() == QMetaType::QString
|
|
|
|
#else
|
|
|
|
value.type() == QVariant::String
|
|
|
|
#endif
|
|
|
|
&& value.toString().isNull()) {
|
2020-10-20 17:14:38 +02:00
|
|
|
where_clauses_ << QString("%1 %2 ?").arg(column, op);
|
|
|
|
bound_values_ << QString("");
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
else {
|
|
|
|
where_clauses_ << QString("%1 %2 ?").arg(column, op);
|
|
|
|
bound_values_ << value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-04-18 00:45:32 +02:00
|
|
|
void CollectionQuery::AddWhereArtist(const QVariant &value) {
|
|
|
|
|
|
|
|
where_clauses_ << QString("((artist = ? AND albumartist = '') OR albumartist = ?)");
|
|
|
|
bound_values_ << value;
|
|
|
|
bound_values_ << value;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
2018-05-01 00:41:33 +02:00
|
|
|
// 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
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
where_clauses_ << QString("+compilation_effective = %1").arg(compilation ? 1 : 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
bool CollectionQuery::Exec() {
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
QString sql;
|
|
|
|
|
|
|
|
if (join_with_fts_) {
|
2021-04-10 03:20:25 +02:00
|
|
|
sql = QString("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table_, fts_table_);
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
else {
|
2021-04-10 03:20:25 +02:00
|
|
|
sql = QString("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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_);
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
sql.replace("%songs_table", songs_table_);
|
|
|
|
sql.replace("%fts_table_noprefix", fts_table_.section('.', -1, -1));
|
|
|
|
sql.replace("%fts_table", fts_table_);
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
prepare(sql);
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
// Bind values
|
2018-05-01 00:41:33 +02:00
|
|
|
for (const QVariant &value : bound_values_) {
|
2021-04-10 03:20:25 +02:00
|
|
|
addBindValue(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
const bool result = exec();
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
QSqlError last_error = lastError();
|
|
|
|
if (last_error.isValid()) {
|
|
|
|
qLog(Error) << "DB error: " << last_error;
|
|
|
|
qLog(Error) << "Faulty query: " << lastQuery();
|
|
|
|
qLog(Error) << "Bound values: " << boundValues();
|
|
|
|
}
|
2018-02-27 18:06:05 +01:00
|
|
|
}
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
return result;
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
bool CollectionQuery::Next() { return next(); }
|
2018-02-27 18:06:05 +01:00
|
|
|
|
2021-04-10 03:20:25 +02:00
|
|
|
QVariant CollectionQuery::Value(const int column) const { return value(column); }
|
2018-02-27 18:06:05 +01:00
|
|
|
|
|
|
|
bool QueryOptions::Matches(const Song &song) const {
|
|
|
|
|
|
|
|
if (max_age_ != -1) {
|
2021-04-10 03:20:25 +02:00
|
|
|
const qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - max_age_;
|
2018-02-27 18:06:05 +01:00
|
|
|
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;
|
|
|
|
|
|
|
|
}
|