feat(live): remove pagination from Live catalog (#51)

Keep server-side search and cursor pagination for Home and Guide while allowing Live to load the complete filtered catalogue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 23:51:02 +02:00
parent ccdd3cb027
commit d64a902155
4 changed files with 197 additions and 176 deletions

View File

@ -16,7 +16,7 @@ function badRequest(message) {
}
function parseQuery(query, userId) {
for (const key of ['sourceId', 'categoryId', 'q', 'includeHidden', 'favoriteOnly', 'limit', 'cursor']) {
for (const key of ['sourceId', 'categoryId', 'q', 'includeHidden', 'favoriteOnly', 'limit', 'cursor', 'full']) {
if (query[key] !== undefined && typeof query[key] !== 'string') badRequest(`Invalid ${key}`);
}
const sourceId = query.sourceId === undefined ? null : Number(query.sourceId);
@ -28,6 +28,10 @@ function parseQuery(query, userId) {
};
const requestedLimit = query.limit === undefined ? 100 : Number(query.limit);
if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) badRequest('Invalid limit');
const full = boolean('full');
if (full && (query.limit !== undefined || query.cursor !== undefined)) {
badRequest('full=true cannot be combined with limit or cursor');
}
const filters = { sourceId, categoryId: query.categoryId ?? null, q: query.q || '',
includeHidden: boolean('includeHidden'), favoriteOnly: boolean('favoriteOnly'), userId };
const fingerprint = createHash('sha256').update(JSON.stringify({ ...filters, order: ORDER_VERSION })).digest('hex');
@ -42,7 +46,7 @@ function parseQuery(query, userId) {
after = cursor.k;
} catch { badRequest('Invalid cursor'); }
}
return { ...filters, limit: Math.min(requestedLimit, 200), fingerprint, after };
return { ...filters, full, limit: Math.min(requestedLimit, 200), fingerprint, after };
}
// Optional acceleration: LIKE remains the source of truth and also handles short
@ -102,7 +106,7 @@ function getLiveCatalog(db, allSources, options) {
where.push(`(${CATALOG_ORDER}) > (@groupKey, @groupTitle, @channelKey, @name, @source, @item)`);
[params.groupKey, params.groupTitle, params.channelKey, params.name, params.source, params.item] = options.after;
}
params.limit = options.limit + 1;
if (!options.full) params.limit = options.limit + 1;
const rows = db.prepare(`WITH live_sources AS (
SELECT json_extract(value, '$.id') AS id, json_extract(value, '$.type') AS type FROM json_each(@sources)
) SELECT p.*, s.type AS source_type, ${GROUP_TITLE} AS group_title,
@ -114,8 +118,8 @@ function getLiveCatalog(db, allSources, options) {
LEFT JOIN favorites f ON f.user_id = @user AND f.source_id = p.source_id
AND f.item_id = p.item_id AND f.item_type = 'channel'
WHERE ${where.join(' AND ')}
ORDER BY ${CATALOG_ORDER} LIMIT @limit`).all(params);
const hasMore = rows.length > options.limit;
ORDER BY ${CATALOG_ORDER}${options.full ? '' : ' LIMIT @limit'}`).all(params);
const hasMore = !options.full && rows.length > options.limit;
if (hasMore) rows.pop();
const items = rows.map(row => {
const data = JSON.parse(row.data || '{}');