const PRIORITY_STEP = 10; function numericPriority(source) { return Number.isInteger(source.epgPriority) && source.epgPriority >= 0 ? source.epgPriority : Number.MAX_SAFE_INTEGER; } function sortEpgSources(list, { enabledOnly = false } = {}) { return list .filter(source => source.type === 'epg' && (!enabledOnly || source.enabled)) .slice() .sort((left, right) => numericPriority(left) - numericPriority(right) || left.id - right.id); } function migrateEpgPriorities(database) { const epgSources = database.sources.filter(source => source.type === 'epg'); let next = epgSources.reduce((maximum, source) => Number.isInteger(source.epgPriority) ? Math.max(maximum, source.epgPriority + PRIORITY_STEP) : maximum, 0); for (const source of epgSources) { if (!Number.isInteger(source.epgPriority) || source.epgPriority < 0) { source.epgPriority = next; next += PRIORITY_STEP; } } return database; } function nextEpgPriority(sources) { return sortEpgSources(sources).reduce((maximum, source) => Math.max(maximum, numericPriority(source) + PRIORITY_STEP), 0); } function validateCompleteOrder(allSources, orderedIds) { if (!Array.isArray(orderedIds)) return { code: 'INVALID_ORDER', message: 'orderedIds must be an array' }; if (orderedIds.some(id => !Number.isInteger(id) || id <= 0)) return { code: 'INVALID_ORDER_ID', message: 'orderedIds must contain positive integer IDs' }; if (new Set(orderedIds).size !== orderedIds.length) return { code: 'DUPLICATE_SOURCE_ID', message: 'orderedIds contains duplicate IDs' }; const byId = new Map(allSources.map(source => [source.id, source])); const unknown = orderedIds.find(id => !byId.has(id)); if (unknown !== undefined) return { code: 'UNKNOWN_SOURCE_ID', message: `Unknown source ID: ${unknown}` }; const nonEpg = orderedIds.find(id => byId.get(id).type !== 'epg'); if (nonEpg !== undefined) return { code: 'SOURCE_NOT_EPG', message: `Source ${nonEpg} is not an EPG source` }; const expected = allSources.filter(source => source.type === 'epg').map(source => source.id); if (orderedIds.length !== expected.length || expected.some(id => !orderedIds.includes(id))) { return { code: 'INCOMPLETE_EPG_ORDER', message: 'orderedIds must contain every EPG source' }; } return null; } function applyEpgOrder(allSources, orderedIds, updatedAt = new Date().toISOString()) { const error = validateCompleteOrder(allSources, orderedIds); if (error) throw Object.assign(new Error(error.message), error); orderedIds.forEach((id, index) => { const source = allSources.find(item => item.id === id); source.epgPriority = index * PRIORITY_STEP; source.updated_at = updatedAt; }); return sortEpgSources(allSources); } module.exports = { applyEpgOrder, migrateEpgPriorities, nextEpgPriority, numericPriority, PRIORITY_STEP, sortEpgSources, validateCompleteOrder };