2016-03-03 22:21:15 +01:00
|
|
|
#include "./dbquery.h"
|
|
|
|
|
|
|
|
#include "../misc/networkaccessmanager.h"
|
2018-03-07 01:18:01 +01:00
|
|
|
#include "../misc/utility.h"
|
2016-03-03 22:21:15 +01:00
|
|
|
|
2016-05-14 23:23:16 +02:00
|
|
|
#include <tagparser/signature.h>
|
2018-03-07 01:18:01 +01:00
|
|
|
#include <tagparser/tag.h>
|
|
|
|
#include <tagparser/tagvalue.h>
|
2016-03-03 22:21:15 +01:00
|
|
|
|
2016-03-06 17:52:33 +01:00
|
|
|
#include <QMessageBox>
|
2016-03-03 22:21:15 +01:00
|
|
|
|
2016-03-06 17:52:33 +01:00
|
|
|
using namespace std;
|
2016-03-03 22:21:15 +01:00
|
|
|
using namespace Utility;
|
2018-03-06 23:10:13 +01:00
|
|
|
using namespace TagParser;
|
2016-03-03 22:21:15 +01:00
|
|
|
|
|
|
|
namespace QtGui {
|
|
|
|
|
2018-03-07 01:18:01 +01:00
|
|
|
SongDescription::SongDescription(const QString &songId)
|
|
|
|
: songId(songId)
|
|
|
|
, track(0)
|
|
|
|
, totalTracks(0)
|
|
|
|
, disk(0)
|
|
|
|
, cover(nullptr)
|
|
|
|
{
|
|
|
|
}
|
2016-03-03 22:21:15 +01:00
|
|
|
|
2019-06-14 18:08:05 +02:00
|
|
|
std::list<QString> QueryResultsModel::s_coverNames = std::list<QString>();
|
|
|
|
map<QString, QByteArray> QueryResultsModel::s_coverData = map<QString, QByteArray>();
|
2017-07-30 20:30:50 +02:00
|
|
|
|
2018-03-07 01:18:01 +01:00
|
|
|
QueryResultsModel::QueryResultsModel(QObject *parent)
|
|
|
|
: QAbstractTableModel(parent)
|
|
|
|
, m_resultsAvailable(false)
|
|
|
|
, m_fetchingCover(false)
|
|
|
|
{
|
|
|
|
}
|
2016-03-03 22:21:15 +01:00
|
|
|
|
|
|
|
void QueryResultsModel::setResultsAvailable(bool resultsAvailable)
|
|
|
|
{
|
2018-03-07 01:18:01 +01:00
|
|
|
if ((m_resultsAvailable = resultsAvailable)) {
|
2016-03-06 17:52:33 +01:00
|
|
|
emit this->resultsAvailable();
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-06 17:52:33 +01:00
|
|
|
void QueryResultsModel::setFetchingCover(bool fetchingCover)
|
|
|
|
{
|
|
|
|
m_fetchingCover = fetchingCover;
|
|
|
|
}
|
|
|
|
|
|
|
|
void QueryResultsModel::abort()
|
2018-03-07 01:18:01 +01:00
|
|
|
{
|
|
|
|
}
|
2016-03-06 17:52:33 +01:00
|
|
|
|
2017-08-08 20:22:37 +02:00
|
|
|
QUrl QueryResultsModel::webUrl(const QModelIndex &index)
|
|
|
|
{
|
|
|
|
Q_UNUSED(index)
|
|
|
|
return QUrl();
|
|
|
|
}
|
|
|
|
|
2016-11-23 21:46:33 +01:00
|
|
|
#define returnValue(field) return qstringToTagValue(res.field, TagTextEncoding::Utf16LittleEndian)
|
|
|
|
|
2016-03-03 22:21:15 +01:00
|
|
|
TagValue QueryResultsModel::fieldValue(int row, KnownField knownField) const
|
|
|
|
{
|
2018-08-19 15:11:46 +02:00
|
|
|
if (row >= m_results.size()) {
|
|
|
|
return TagValue();
|
|
|
|
}
|
|
|
|
const SongDescription &res = m_results.at(row);
|
|
|
|
switch (knownField) {
|
|
|
|
case KnownField::Title:
|
|
|
|
returnValue(title);
|
|
|
|
case KnownField::Album:
|
|
|
|
returnValue(album);
|
|
|
|
case KnownField::Artist:
|
|
|
|
returnValue(artist);
|
|
|
|
case KnownField::Genre:
|
|
|
|
returnValue(genre);
|
2021-02-01 17:11:48 +01:00
|
|
|
case KnownField::RecordDate:
|
2018-08-19 15:11:46 +02:00
|
|
|
returnValue(year);
|
|
|
|
case KnownField::TrackPosition:
|
|
|
|
return TagValue(PositionInSet(res.track, res.totalTracks));
|
|
|
|
case KnownField::PartNumber:
|
|
|
|
return TagValue(res.track);
|
|
|
|
case KnownField::TotalParts:
|
|
|
|
return TagValue(res.totalTracks);
|
2019-06-01 12:50:09 +02:00
|
|
|
case KnownField::DiskPosition:
|
|
|
|
return TagValue(PositionInSet(res.disk));
|
2018-08-19 15:11:46 +02:00
|
|
|
case KnownField::Cover:
|
|
|
|
if (!res.cover.isEmpty()) {
|
|
|
|
TagValue tagValue(res.cover.data(), static_cast<size_t>(res.cover.size()), TagDataType::Picture);
|
2021-03-20 21:59:49 +01:00
|
|
|
tagValue.setMimeType(containerMimeType(parseSignature(res.cover.data(), static_cast<std::size_t>(res.cover.size()))));
|
2018-08-19 15:11:46 +02:00
|
|
|
return tagValue;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
2018-08-19 15:11:46 +02:00
|
|
|
break;
|
|
|
|
case KnownField::Lyrics:
|
|
|
|
returnValue(lyrics);
|
|
|
|
default:;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
return TagValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
#undef returnValue
|
|
|
|
|
|
|
|
QVariant QueryResultsModel::data(const QModelIndex &index, int role) const
|
|
|
|
{
|
2018-08-19 15:11:46 +02:00
|
|
|
if (!index.isValid() || index.row() >= m_results.size()) {
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
const SongDescription &res = m_results.at(index.row());
|
|
|
|
switch (role) {
|
|
|
|
case Qt::DisplayRole:
|
|
|
|
switch (index.column()) {
|
|
|
|
case TitleCol:
|
|
|
|
return res.title;
|
|
|
|
case AlbumCol:
|
|
|
|
return res.album;
|
|
|
|
case ArtistCol:
|
|
|
|
return res.artist;
|
|
|
|
case GenreCol:
|
|
|
|
return res.genre;
|
|
|
|
case YearCol:
|
|
|
|
return res.year;
|
|
|
|
case TrackCol:
|
|
|
|
if (res.track) {
|
|
|
|
return res.track;
|
|
|
|
} else {
|
2019-06-01 12:50:09 +02:00
|
|
|
return QVariant();
|
2018-08-19 15:11:46 +02:00
|
|
|
}
|
|
|
|
case TotalTracksCol:
|
|
|
|
if (res.totalTracks) {
|
|
|
|
return res.totalTracks;
|
|
|
|
} else {
|
2019-06-01 12:50:09 +02:00
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
case DiskCol:
|
|
|
|
if (res.disk) {
|
|
|
|
return res.disk;
|
|
|
|
} else {
|
|
|
|
return QVariant();
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
2018-03-07 01:18:01 +01:00
|
|
|
default:;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
2018-08-19 15:11:46 +02:00
|
|
|
break;
|
|
|
|
default:;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
Qt::ItemFlags QueryResultsModel::flags(const QModelIndex &index) const
|
|
|
|
{
|
|
|
|
Qt::ItemFlags flags = Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
|
2018-03-07 01:18:01 +01:00
|
|
|
if (index.isValid()) {
|
2016-03-03 22:21:15 +01:00
|
|
|
flags |= Qt::ItemIsUserCheckable;
|
|
|
|
}
|
|
|
|
return flags;
|
|
|
|
}
|
|
|
|
|
|
|
|
QVariant QueryResultsModel::headerData(int section, Qt::Orientation orientation, int role) const
|
|
|
|
{
|
2018-03-07 01:18:01 +01:00
|
|
|
switch (orientation) {
|
2016-03-03 22:21:15 +01:00
|
|
|
case Qt::Horizontal:
|
2018-03-07 01:18:01 +01:00
|
|
|
switch (role) {
|
2016-03-03 22:21:15 +01:00
|
|
|
case Qt::DisplayRole:
|
2018-03-07 01:18:01 +01:00
|
|
|
switch (section) {
|
2016-03-03 22:21:15 +01:00
|
|
|
case TitleCol:
|
|
|
|
return tr("Song title");
|
|
|
|
case AlbumCol:
|
|
|
|
return tr("Album");
|
|
|
|
case ArtistCol:
|
|
|
|
return tr("Artist");
|
2019-06-01 12:50:09 +02:00
|
|
|
case GenreCol:
|
|
|
|
return tr("Genre");
|
2016-03-03 22:21:15 +01:00
|
|
|
case YearCol:
|
|
|
|
return tr("Year");
|
|
|
|
case TrackCol:
|
|
|
|
return tr("Track");
|
|
|
|
case TotalTracksCol:
|
|
|
|
return tr("Total tracks");
|
2019-06-01 12:50:09 +02:00
|
|
|
case DiskCol:
|
|
|
|
return tr("Disk");
|
2018-03-07 01:18:01 +01:00
|
|
|
default:;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
break;
|
2018-03-07 01:18:01 +01:00
|
|
|
default:;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
break;
|
2018-03-07 01:18:01 +01:00
|
|
|
default:;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
return QVariant();
|
|
|
|
}
|
|
|
|
|
|
|
|
int QueryResultsModel::rowCount(const QModelIndex &parent) const
|
|
|
|
{
|
2023-07-23 22:17:47 +02:00
|
|
|
return parent.isValid() ? 0 : Utility::containerSizeToInt(m_results.size());
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
int QueryResultsModel::columnCount(const QModelIndex &parent) const
|
|
|
|
{
|
2019-06-01 12:50:09 +02:00
|
|
|
return parent.isValid() ? 0 : EndCol;
|
2016-03-03 22:21:15 +01:00
|
|
|
}
|
|
|
|
|
2016-03-06 17:52:33 +01:00
|
|
|
const QByteArray *QueryResultsModel::cover(const QModelIndex &index) const
|
|
|
|
{
|
2018-08-19 15:11:46 +02:00
|
|
|
if (!index.isValid() || index.row() >= m_results.size()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
const auto &cover = m_results.at(index.row()).cover;
|
|
|
|
if (!cover.isEmpty()) {
|
|
|
|
return &cover;
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Fetches the cover the specified \a index.
|
2016-10-09 22:41:34 +02:00
|
|
|
* \returns
|
2021-07-03 19:38:27 +02:00
|
|
|
* - true if the cover is immediately available or an error occurs immediately
|
2016-10-09 22:41:34 +02:00
|
|
|
* - and false if the cover will be fetched asynchronously.
|
2016-03-06 17:52:33 +01:00
|
|
|
*
|
|
|
|
* If the cover is fetched asynchronously the coverAvailable() signal is emitted, when the cover
|
2016-10-09 22:41:34 +02:00
|
|
|
* becomes available.
|
2016-03-06 17:52:33 +01:00
|
|
|
*
|
|
|
|
* The resultsAvailable() signal is emitted if errors have been added to errorList().
|
|
|
|
*/
|
2016-10-09 22:41:34 +02:00
|
|
|
bool QueryResultsModel::fetchCover(const QModelIndex &index)
|
2016-03-06 17:52:33 +01:00
|
|
|
{
|
2016-10-09 22:41:34 +02:00
|
|
|
Q_UNUSED(index)
|
|
|
|
m_errorList << tr("Fetching cover is not implemented for this provider");
|
2016-03-06 17:52:33 +01:00
|
|
|
emit resultsAvailable();
|
2016-10-09 22:41:34 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const QString *QueryResultsModel::lyrics(const QModelIndex &index) const
|
|
|
|
{
|
2018-08-19 15:11:46 +02:00
|
|
|
if (!index.isValid() || index.row() >= m_results.size()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
const auto &lyrics = m_results.at(index.row()).lyrics;
|
|
|
|
if (!lyrics.isEmpty()) {
|
|
|
|
return &lyrics;
|
2016-10-09 22:41:34 +02:00
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Fetches the lyrics the specified \a index.
|
|
|
|
* \returns
|
2021-07-03 19:38:27 +02:00
|
|
|
* - true if the lyrics are immediately available or an error occurs immediately
|
2016-10-09 22:41:34 +02:00
|
|
|
* - and false if the lyrics will be fetched asynchronously.
|
|
|
|
*
|
|
|
|
* If the lyrics are fetched asynchronously the lyricsAvailable() signal is emitted, when the lyrics
|
|
|
|
* become available.
|
|
|
|
*
|
|
|
|
* The resultsAvailable() signal is emitted if errors have been added to errorList().
|
|
|
|
*/
|
|
|
|
bool QueryResultsModel::fetchLyrics(const QModelIndex &index)
|
|
|
|
{
|
|
|
|
Q_UNUSED(index)
|
|
|
|
m_errorList << tr("Fetching lyrics is not implemented for this provider");
|
|
|
|
emit resultsAvailable();
|
|
|
|
return true;
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
|
|
|
|
2016-10-09 19:44:06 +02:00
|
|
|
/*!
|
|
|
|
* \brief Constructs a new HttpResultsModel for the specified \a reply.
|
|
|
|
* \remarks The model takes ownership over the specified \a reply.
|
|
|
|
*/
|
2018-03-07 01:18:01 +01:00
|
|
|
HttpResultsModel::HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply)
|
|
|
|
: m_initialDescription(initialSongDescription)
|
2016-03-06 17:52:33 +01:00
|
|
|
{
|
2016-10-09 22:41:34 +02:00
|
|
|
addReply(reply, this, &HttpResultsModel::handleInitialReplyFinished);
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
|
|
|
|
2016-10-09 19:44:06 +02:00
|
|
|
/*!
|
|
|
|
* \brief Deletes all associated replies.
|
|
|
|
*/
|
2016-03-06 17:52:33 +01:00
|
|
|
HttpResultsModel::~HttpResultsModel()
|
|
|
|
{
|
|
|
|
qDeleteAll(m_replies);
|
|
|
|
}
|
|
|
|
|
2016-10-09 19:44:06 +02:00
|
|
|
/*!
|
|
|
|
* \brief Evaluates request results.
|
|
|
|
* \remarks Calls parseResults() if the requested data is available. Handles errors/redirections otherwise.
|
|
|
|
*/
|
2016-10-09 22:41:34 +02:00
|
|
|
void HttpResultsModel::handleInitialReplyFinished()
|
2016-03-06 17:52:33 +01:00
|
|
|
{
|
2018-08-19 15:11:46 +02:00
|
|
|
auto *const reply = static_cast<QNetworkReply *>(sender());
|
2016-10-09 22:41:34 +02:00
|
|
|
QByteArray data;
|
2018-08-19 15:11:46 +02:00
|
|
|
if (auto *const newReply = evaluateReplyResults(reply, data, false)) {
|
2016-10-09 22:41:34 +02:00
|
|
|
addReply(newReply, this, &HttpResultsModel::handleInitialReplyFinished);
|
2018-08-19 15:11:46 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!data.isEmpty()) {
|
|
|
|
parseInitialResults(data);
|
2016-10-09 22:41:34 +02:00
|
|
|
}
|
2018-08-19 15:11:46 +02:00
|
|
|
setResultsAvailable(true); // update status, emit resultsAvailable()
|
2016-10-09 22:41:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection)
|
|
|
|
{
|
|
|
|
// delete reply (later)
|
|
|
|
reply->deleteLater();
|
|
|
|
m_replies.removeAll(reply);
|
|
|
|
|
2018-08-19 15:11:46 +02:00
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
|
|
m_errorList << reply->errorString();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
const auto redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
|
|
|
|
if (redirectionTarget.isNull()) {
|
|
|
|
// read all data if it is not redirection
|
|
|
|
if ((data = reply->readAll()).isEmpty()) {
|
|
|
|
m_errorList << tr("Server replied no data.");
|
|
|
|
}
|
2019-06-12 20:47:44 +02:00
|
|
|
#ifdef CPP_UTILITIES_DEBUG_BUILD
|
2018-08-19 15:11:46 +02:00
|
|
|
cerr << "Results from HTTP query:" << endl;
|
|
|
|
cerr << data.data() << endl;
|
2016-03-06 17:52:33 +01:00
|
|
|
#endif
|
2018-08-19 15:11:46 +02:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
// there's a redirection available
|
|
|
|
// -> resolve new URL
|
|
|
|
const auto newUrl = reply->url().resolved(redirectionTarget.toUrl());
|
|
|
|
// -> ask user whether to follow redirection unless alwaysFollowRedirection is true
|
|
|
|
if (!alwaysFollowRedirection) {
|
|
|
|
const auto message = tr("<p>Do you want to redirect form <i>%1</i> to <i>%2</i>?</p>").arg(reply->url().toString(), newUrl.toString());
|
|
|
|
alwaysFollowRedirection = QMessageBox::question(nullptr, tr("Search"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
|
|
|
|
}
|
|
|
|
if (alwaysFollowRedirection) {
|
|
|
|
return networkAccessManager().get(QNetworkRequest(newUrl));
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
2018-08-19 15:11:46 +02:00
|
|
|
m_errorList << tr("Redirection to: ") + newUrl.toString();
|
2016-10-09 22:41:34 +02:00
|
|
|
return nullptr;
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
|
|
|
|
2016-10-09 19:44:06 +02:00
|
|
|
/*!
|
|
|
|
* \brief Aborts all ongoing requests and causes error "Aborted by user" if requests where ongoing.
|
|
|
|
*/
|
2016-03-06 17:52:33 +01:00
|
|
|
void HttpResultsModel::abort()
|
|
|
|
{
|
2018-08-19 15:11:46 +02:00
|
|
|
if (m_replies.isEmpty()) {
|
|
|
|
return;
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
2018-08-19 15:11:46 +02:00
|
|
|
qDeleteAll(m_replies);
|
|
|
|
m_replies.clear();
|
|
|
|
// must update status manually because handleReplyFinished() won't be called anymore
|
|
|
|
m_errorList << tr("Aborted by user.");
|
|
|
|
setResultsAvailable(true);
|
2016-03-06 17:52:33 +01:00
|
|
|
}
|
|
|
|
|
2017-07-30 20:30:50 +02:00
|
|
|
void HttpResultsModel::handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row)
|
|
|
|
{
|
|
|
|
QByteArray data;
|
2018-08-19 15:11:46 +02:00
|
|
|
if (auto *const newReply = evaluateReplyResults(reply, data, true)) {
|
2017-07-30 20:30:50 +02:00
|
|
|
addReply(newReply, bind(&HttpResultsModel::handleCoverReplyFinished, this, newReply, albumId, row));
|
2018-08-19 15:11:46 +02:00
|
|
|
return;
|
2017-07-30 20:30:50 +02:00
|
|
|
}
|
2018-08-19 15:11:46 +02:00
|
|
|
if (!data.isEmpty()) {
|
|
|
|
parseCoverResults(albumId, row, data);
|
|
|
|
}
|
2019-09-25 18:18:06 +02:00
|
|
|
if (!m_resultsAvailable) {
|
|
|
|
setResultsAvailable(true);
|
|
|
|
}
|
2017-07-30 20:30:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void HttpResultsModel::parseCoverResults(const QString &albumId, int row, const QByteArray &data)
|
|
|
|
{
|
2019-09-25 18:18:06 +02:00
|
|
|
setFetchingCover(false);
|
|
|
|
|
2017-07-30 20:30:50 +02:00
|
|
|
// add cover -> determine album ID and row
|
2018-08-19 15:11:46 +02:00
|
|
|
if (albumId.isEmpty() || row >= m_results.size()) {
|
2017-07-30 20:30:50 +02:00
|
|
|
m_errorList << tr("Internal error: context for cover reply invalid");
|
|
|
|
setResultsAvailable(true);
|
2018-08-19 15:11:46 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-06-14 18:08:05 +02:00
|
|
|
|
2019-09-25 18:18:06 +02:00
|
|
|
if (data.isEmpty()) {
|
|
|
|
return;
|
2017-07-30 20:30:50 +02:00
|
|
|
}
|
2019-09-25 18:18:06 +02:00
|
|
|
|
|
|
|
// cache the fetched cover
|
|
|
|
const auto currentCachedCoverCount = s_coverData.size();
|
|
|
|
s_coverData[albumId] = data;
|
|
|
|
if (s_coverData.size() > currentCachedCoverCount) {
|
|
|
|
s_coverNames.emplace_back(albumId);
|
|
|
|
|
|
|
|
// keep only the last 20 cover images around
|
|
|
|
while (s_coverNames.size() > 20) {
|
|
|
|
s_coverData.erase(s_coverNames.front());
|
|
|
|
s_coverNames.pop_front();
|
|
|
|
}
|
|
|
|
} else if (s_coverNames.back() != albumId) {
|
|
|
|
s_coverNames.remove(albumId);
|
|
|
|
s_coverNames.emplace_back(albumId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// add the cover to the results
|
|
|
|
m_results[row].cover = data;
|
|
|
|
setResultsAvailable(true);
|
|
|
|
emit coverAvailable(index(row, 0));
|
2017-07-30 20:30:50 +02:00
|
|
|
}
|
|
|
|
|
2018-03-07 01:18:01 +01:00
|
|
|
} // namespace QtGui
|
2016-03-03 22:21:15 +01:00
|
|
|
|
|
|
|
#include "dbquery.moc"
|