feat(library): page Artists from local catalog
Materialize source-scoped artist aggregates and FTS, then expose exact keyset pages and bounded A-Z metadata for the existing Artists rail. Query the selected artist's albums through the indexed credit relationship with an independent bounded model, generation cancellation, artwork updates, and per-surface legacy fallback. cx
This commit is contained in:
18 files changed
+3128
-36
No files matched your search
@@ -13,6 +13,7 @@
|
||||
|
||||
namespace {
|
||||
QPointer<LocalAlbumsModel> g_model;
|
||||
QPointer<LocalAlbumsModel> g_artist_model;
|
||||
|
||||
void registerLocalAlbumsModel()
|
||||
{
|
||||
@@ -21,6 +22,10 @@ void registerLocalAlbumsModel()
|
||||
g_model = model;
|
||||
qmlRegisterSingletonInstance<LocalAlbumsModel>(
|
||||
"com.blitzfc.qbz", 1, 0, "QbzLocalAlbums", model);
|
||||
auto *artistModel = new LocalAlbumsModel(application);
|
||||
g_artist_model = artistModel;
|
||||
qmlRegisterSingletonInstance<LocalAlbumsModel>(
|
||||
"com.blitzfc.qbz", 1, 0, "QbzLocalArtistAlbums", artistModel);
|
||||
}
|
||||
|
||||
Q_COREAPP_STARTUP_FUNCTION(registerLocalAlbumsModel)
|
||||
@@ -303,3 +308,17 @@ extern "C" void qbz_local_albums_set_selection(
|
||||
g_model->setSelection(
|
||||
generation, QByteArray(json ? json : "{}"), selectedCount);
|
||||
}
|
||||
|
||||
extern "C" void qbz_local_artist_albums_reset(
|
||||
int generation, qint64 totalCount, qint64 albumTotal)
|
||||
{
|
||||
if (g_artist_model)
|
||||
g_artist_model->resetQuery(generation, totalCount, albumTotal);
|
||||
}
|
||||
|
||||
extern "C" bool qbz_local_artist_albums_apply_page(
|
||||
int generation, int page, const char *json)
|
||||
{
|
||||
return g_artist_model && g_artist_model->applyPage(
|
||||
generation, page, QByteArray(json ? json : "[]"));
|
||||
}
|
||||
@@ -83,6 +83,8 @@ void qbz_local_albums_register_qml_type();
|
||||
void qbz_local_albums_reset(int generation, qint64 totalCount, qint64 albumTotal);
|
||||
bool qbz_local_albums_apply_page(int generation, int page, const char *json);
|
||||
void qbz_local_albums_set_selection(int generation, const char *json, qint64 selectedCount);
|
||||
void qbz_local_artist_albums_reset(int generation, qint64 totalCount, qint64 albumTotal);
|
||||
bool qbz_local_artist_albums_apply_page(int generation, int page, const char *json);
|
||||
}
|
||||
|
||||
#endif // QBZ_LOCAL_ALBUMS_MODEL_H
|
||||
@@ -0,0 +1,235 @@
|
||||
#include "local_artists_model.h"
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QMetaMethod>
|
||||
#include <QtCore/QMetaObject>
|
||||
#include <QtCore/QPointer>
|
||||
#include <QtQml/qqml.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
QPointer<LocalArtistsModel> g_model;
|
||||
|
||||
void registerLocalArtistsModel()
|
||||
{
|
||||
auto *model = new LocalArtistsModel(QCoreApplication::instance());
|
||||
g_model = model;
|
||||
qmlRegisterSingletonInstance<LocalArtistsModel>(
|
||||
"com.blitzfc.qbz", 1, 0, "QbzLocalArtists", model);
|
||||
}
|
||||
|
||||
Q_COREAPP_STARTUP_FUNCTION(registerLocalArtistsModel)
|
||||
}
|
||||
|
||||
LocalArtistsModel::LocalArtistsModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int LocalArtistsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return int(qMin<qint64>(m_totalCount, std::numeric_limits<int>::max()));
|
||||
}
|
||||
|
||||
QVariant LocalArtistsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || role != ModelDataRole)
|
||||
return {};
|
||||
return modelData(index.row());
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> LocalArtistsModel::roleNames() const
|
||||
{
|
||||
return {{ ModelDataRole, QByteArrayLiteral("modelData") }};
|
||||
}
|
||||
|
||||
QVariantMap LocalArtistsModel::rowAt(qint64 row) const
|
||||
{
|
||||
if (row < 0 || row >= m_totalCount)
|
||||
return {};
|
||||
return modelData(row);
|
||||
}
|
||||
|
||||
QVariantMap LocalArtistsModel::modelData(qint64 row) const
|
||||
{
|
||||
const int page = int(row / PageEntries);
|
||||
const int offset = int(row % PageEntries);
|
||||
const auto found = m_pages.constFind(page);
|
||||
if (found == m_pages.cend() || offset >= found->size()) {
|
||||
requestPage(page, false);
|
||||
return {
|
||||
{ QStringLiteral("t"), 1 },
|
||||
{ QStringLiteral("loading"), true },
|
||||
{ QStringLiteral("base"), 0 },
|
||||
{ QStringLiteral("item"), QVariantMap() },
|
||||
};
|
||||
}
|
||||
touch(page);
|
||||
QVariantMap values = found->at(offset).values;
|
||||
values.insert(QStringLiteral("loading"), false);
|
||||
return values;
|
||||
}
|
||||
|
||||
void LocalArtistsModel::requestPage(int page, bool prefetch) const
|
||||
{
|
||||
if (page < 0)
|
||||
return;
|
||||
const QMetaMethod missSignal = QMetaMethod::fromSignal(&LocalArtistsModel::pageMiss);
|
||||
if (!isSignalConnected(missSignal))
|
||||
return;
|
||||
if (m_pending.contains(page)) {
|
||||
if (!prefetch)
|
||||
m_pending.insert(page, false);
|
||||
return;
|
||||
}
|
||||
m_pending.insert(page, prefetch);
|
||||
const int generation = m_generation;
|
||||
auto *self = const_cast<LocalArtistsModel *>(this);
|
||||
QMetaObject::invokeMethod(
|
||||
self,
|
||||
[self, page, generation]() {
|
||||
if (generation != self->m_generation)
|
||||
return;
|
||||
if (self->isSignalConnected(
|
||||
QMetaMethod::fromSignal(&LocalArtistsModel::pageMiss))) {
|
||||
emit self->pageMiss(page, generation);
|
||||
} else {
|
||||
self->m_pending.remove(page);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void LocalArtistsModel::touch(int page) const
|
||||
{
|
||||
m_lru.removeAll(page);
|
||||
m_lru.append(page);
|
||||
}
|
||||
|
||||
void LocalArtistsModel::resetQuery(int generation, qint64 totalCount, qint64 artistTotal)
|
||||
{
|
||||
beginResetModel();
|
||||
m_generation = generation;
|
||||
m_totalCount = qMax<qint64>(0, totalCount);
|
||||
m_artistTotal = qMax<qint64>(0, artistTotal);
|
||||
m_pages.clear();
|
||||
m_lru.clear();
|
||||
m_pending.clear();
|
||||
m_residentArtists = 0;
|
||||
m_evictions = 0;
|
||||
endResetModel();
|
||||
emit generationChanged();
|
||||
emit totalCountChanged();
|
||||
emit artistTotalChanged();
|
||||
emit residentArtistsChanged();
|
||||
emit evictionsChanged();
|
||||
}
|
||||
|
||||
bool LocalArtistsModel::applyPage(int generation, int page, const QByteArray &json)
|
||||
{
|
||||
if (generation != m_generation || page < 0)
|
||||
return false;
|
||||
QJsonParseError error;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(json, &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isArray())
|
||||
return false;
|
||||
|
||||
Page entries;
|
||||
const QJsonArray array = document.array();
|
||||
entries.reserve(array.size());
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject())
|
||||
return false;
|
||||
Entry entry;
|
||||
entry.values = value.toObject().toVariantMap();
|
||||
entry.isArtist = entry.values.value(QStringLiteral("t")).toInt() == 1;
|
||||
entry.artKey = entry.values.value(QStringLiteral("item"))
|
||||
.toMap()
|
||||
.value(QStringLiteral("artKey"))
|
||||
.toString();
|
||||
entries.append(std::move(entry));
|
||||
}
|
||||
|
||||
const auto previous = m_pages.constFind(page);
|
||||
if (previous != m_pages.cend()) {
|
||||
for (const Entry &entry : *previous)
|
||||
m_residentArtists -= int(entry.isArtist);
|
||||
}
|
||||
m_pages.insert(page, std::move(entries));
|
||||
for (const Entry &entry : m_pages.value(page))
|
||||
m_residentArtists += int(entry.isArtist);
|
||||
const bool wasPrefetch = m_pending.take(page);
|
||||
touch(page);
|
||||
evictIfNeeded();
|
||||
|
||||
const int first = page * PageEntries;
|
||||
const int last = qMin(rowCount() - 1, first + PageEntries - 1);
|
||||
if (first <= last)
|
||||
emit dataChanged(index(first, 0), index(last, 0), { ModelDataRole });
|
||||
emit residentArtistsChanged();
|
||||
const int next = page + 1;
|
||||
if (!wasPrefetch && qint64(next) * PageEntries < m_totalCount && !m_pages.contains(next))
|
||||
requestPage(next, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void LocalArtistsModel::evictIfNeeded()
|
||||
{
|
||||
while (m_residentArtists > MaxResidentArtists && m_pages.size() > 1 && !m_lru.isEmpty()) {
|
||||
const int page = m_lru.takeFirst();
|
||||
const auto found = m_pages.find(page);
|
||||
if (found == m_pages.end())
|
||||
continue;
|
||||
for (const Entry &entry : *found)
|
||||
m_residentArtists -= int(entry.isArtist);
|
||||
m_pages.erase(found);
|
||||
++m_evictions;
|
||||
const int first = page * PageEntries;
|
||||
const int last = qMin(rowCount() - 1, first + PageEntries - 1);
|
||||
if (first <= last)
|
||||
emit dataChanged(index(first, 0), index(last, 0), { ModelDataRole });
|
||||
}
|
||||
emit evictionsChanged();
|
||||
}
|
||||
|
||||
void LocalArtistsModel::setArtwork(const QString &artKey, const QString &path)
|
||||
{
|
||||
if (artKey.isEmpty() || path.isEmpty())
|
||||
return;
|
||||
for (auto page = m_pages.begin(); page != m_pages.end(); ++page) {
|
||||
for (int offset = 0; offset < page->size(); ++offset) {
|
||||
Entry &entry = (*page)[offset];
|
||||
if (entry.artKey != artKey)
|
||||
continue;
|
||||
QVariantMap item = entry.values.value(QStringLiteral("item")).toMap();
|
||||
item.insert(QStringLiteral("artPath"), path);
|
||||
entry.values.insert(QStringLiteral("item"), item);
|
||||
const int row = page.key() * PageEntries + offset;
|
||||
emit dataChanged(index(row, 0), index(row, 0), { ModelDataRole });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void qbz_local_artists_register_qml_type()
|
||||
{
|
||||
// Link anchor. Registration is performed by Q_COREAPP_STARTUP_FUNCTION.
|
||||
}
|
||||
|
||||
extern "C" void qbz_local_artists_reset(
|
||||
int generation, qint64 totalCount, qint64 artistTotal)
|
||||
{
|
||||
if (g_model)
|
||||
g_model->resetQuery(generation, totalCount, artistTotal);
|
||||
}
|
||||
|
||||
extern "C" bool qbz_local_artists_apply_page(int generation, int page, const char *json)
|
||||
{
|
||||
return g_model && g_model->applyPage(
|
||||
generation, page, QByteArray(json ? json : "[]"));
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef QBZ_LOCAL_ARTISTS_MODEL_H
|
||||
#define QBZ_LOCAL_ARTISTS_MODEL_H
|
||||
|
||||
#include <QtCore/QAbstractListModel>
|
||||
#include <QtCore/QHash>
|
||||
#include <QtCore/QList>
|
||||
#include <QtCore/QVariantMap>
|
||||
|
||||
class LocalArtistsModel final : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int generation READ generation NOTIFY generationChanged)
|
||||
Q_PROPERTY(qint64 totalCount READ totalCount NOTIFY totalCountChanged)
|
||||
Q_PROPERTY(qint64 artistTotal READ artistTotal NOTIFY artistTotalChanged)
|
||||
Q_PROPERTY(int residentArtists READ residentArtists NOTIFY residentArtistsChanged)
|
||||
Q_PROPERTY(qint64 evictions READ evictions NOTIFY evictionsChanged)
|
||||
|
||||
public:
|
||||
enum Role { ModelDataRole = Qt::UserRole + 1 };
|
||||
|
||||
explicit LocalArtistsModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
int generation() const { return m_generation; }
|
||||
qint64 totalCount() const { return m_totalCount; }
|
||||
qint64 artistTotal() const { return m_artistTotal; }
|
||||
int residentArtists() const { return m_residentArtists; }
|
||||
qint64 evictions() const { return m_evictions; }
|
||||
|
||||
Q_INVOKABLE QVariantMap rowAt(qint64 row) const;
|
||||
Q_INVOKABLE void setArtwork(const QString &artKey, const QString &path);
|
||||
|
||||
void resetQuery(int generation, qint64 totalCount, qint64 artistTotal);
|
||||
bool applyPage(int generation, int page, const QByteArray &json);
|
||||
|
||||
signals:
|
||||
void generationChanged();
|
||||
void totalCountChanged();
|
||||
void artistTotalChanged();
|
||||
void residentArtistsChanged();
|
||||
void evictionsChanged();
|
||||
void pageMiss(int page, int generation);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QVariantMap values;
|
||||
QString artKey;
|
||||
bool isArtist = false;
|
||||
};
|
||||
using Page = QList<Entry>;
|
||||
|
||||
static constexpr int PageEntries = 100;
|
||||
static constexpr int MaxResidentArtists = 2000;
|
||||
|
||||
QVariantMap modelData(qint64 row) const;
|
||||
void requestPage(int page, bool prefetch = false) const;
|
||||
void touch(int page) const;
|
||||
void evictIfNeeded();
|
||||
|
||||
int m_generation = 0;
|
||||
qint64 m_totalCount = 0;
|
||||
qint64 m_artistTotal = 0;
|
||||
QHash<int, Page> m_pages;
|
||||
mutable QList<int> m_lru;
|
||||
mutable QHash<int, bool> m_pending;
|
||||
int m_residentArtists = 0;
|
||||
qint64 m_evictions = 0;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
void qbz_local_artists_register_qml_type();
|
||||
void qbz_local_artists_reset(int generation, qint64 totalCount, qint64 artistTotal);
|
||||
bool qbz_local_artists_apply_page(int generation, int page, const char *json);
|
||||
}
|
||||
|
||||
#endif // QBZ_LOCAL_ARTISTS_MODEL_H
|
||||
Reference in new issue
Block a user