From 374a87b2fd515c853cc2edb11c8bb78d30b5e2f7 Mon Sep 17 00:00:00 2001 From: Trevor Mears Date: Sat, 3 Jan 2026 21:46:07 -0800 Subject: [PATCH] Implemented cascading update save logic --- public/js/components/SourceManager.js | 37 ++++++++++++++-------- server/routes/channels.js | 44 +++++++++++++++++++++------ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/public/js/components/SourceManager.js b/public/js/components/SourceManager.js index d761fcb..98113d2 100644 --- a/public/js/components/SourceManager.js +++ b/public/js/components/SourceManager.js @@ -967,23 +967,22 @@ class SourceManager { const itemsToHide = []; // Only collect items that have CHANGED from their original state - this.treeData.groups.forEach(group => { - // First, handle the GROUP itself (category visibility) - // Determine the correct item type for the group based on content type - let groupItemType = 'group'; // default for live channels - if (this.treeData.type === 'movies') { - groupItemType = 'vod_category'; - } else if (this.treeData.type === 'series') { - groupItemType = 'series_category'; - } + // Track group changes for redundancy check + const changedGroups = new Map(); // categoryId -> isHidden + + // First pass: Identify all changed groups + this.treeData.groups.forEach(group => { + let groupItemType = 'group'; + if (this.treeData.type === 'movies') groupItemType = 'vod_category'; + else if (this.treeData.type === 'series') groupItemType = 'series_category'; - // Only process groups that have a real categoryId (not wrapper groups like 'all_categories') if (group.categoryId) { const groupKey = `${groupItemType}:${group.categoryId}`; const isGroupNowHidden = this.hiddenSet.has(groupKey); const wasGroupHidden = this.originalHiddenSet.has(groupKey); if (isGroupNowHidden !== wasGroupHidden) { + changedGroups.set(group.categoryId, isGroupNowHidden); if (isGroupNowHidden) { itemsToHide.push({ sourceId, itemType: groupItemType, itemId: String(group.categoryId) }); } else { @@ -991,8 +990,13 @@ class SourceManager { } } } + }); + + // Second pass: Process items, skipping if redundant with group change + this.treeData.groups.forEach(group => { + const groupIsChanging = changedGroups.has(group.categoryId); + const groupNewState = changedGroups.get(group.categoryId); // true = hiding, false = showing - // Then handle the ITEMS within the group group.items.forEach(item => { const key = `${item.type}:${item.id}`; const isNowHidden = this.hiddenSet.has(key); @@ -1000,6 +1004,13 @@ class SourceManager { // Only send if state changed if (isNowHidden !== wasHidden) { + // Check for redundancy: + // If group is changing to the SAME state as the item, skip the item + // The backend cascade will handle it. + if (groupIsChanging && groupNewState === isNowHidden) { + return; + } + if (isNowHidden) { itemsToHide.push({ sourceId, itemType: item.type, itemId: String(item.id) }); } else { @@ -1032,8 +1043,8 @@ class SourceManager { } } - // Batch large operations to avoid timeouts (1000 items per batch) - const BATCH_SIZE = 1000; + // Batch large operations to avoid timeouts (5000 items per batch) + const BATCH_SIZE = 5000; const processBatches = async (items, apiFn, label) => { for (let i = 0; i < items.length; i += BATCH_SIZE) { diff --git a/server/routes/channels.js b/server/routes/channels.js index f6548ec..36e33c8 100644 --- a/server/routes/channels.js +++ b/server/routes/channels.js @@ -149,15 +149,27 @@ router.post('/hide/bulk', async (req, res) => { if (!Array.isArray(items)) return res.status(400).json({ error: 'items array required' }); const db = getDb(); + + // Prepare statements once + const hideCat = db.prepare('UPDATE categories SET is_hidden = 1 WHERE source_id = ? AND type = ? AND category_id = ?'); + const hideItem = db.prepare('UPDATE playlist_items SET is_hidden = 1 WHERE source_id = ? AND type = ? AND item_id = ?'); + + // Cascading statements (hide all children of a category) + const hideCatChildren = db.prepare('UPDATE playlist_items SET is_hidden = 1 WHERE source_id = ? AND type = ? AND category_id = ?'); + const runBulk = db.transaction((list) => { for (const item of list) { const mapping = mapItemType(item.itemType); if (mapping) { - const idCol = mapping.table === 'categories' ? 'category_id' : 'item_id'; - db.prepare(` - UPDATE ${mapping.table} SET is_hidden = 1 - WHERE source_id = ? AND type = ? AND ${idCol} = ? - `).run(item.sourceId, mapping.type, item.itemId); + if (mapping.table === 'categories') { + // Hide the category + hideCat.run(item.sourceId, mapping.type, item.itemId); + // Cascade to children + hideCatChildren.run(item.sourceId, mapping.type, item.itemId); + } else { + // Hide individual item + hideItem.run(item.sourceId, mapping.type, item.itemId); + } } } }); @@ -180,15 +192,27 @@ router.post('/show/bulk', async (req, res) => { if (!Array.isArray(items)) return res.status(400).json({ error: 'items array required' }); const db = getDb(); + + // Prepare statements once + const showCat = db.prepare('UPDATE categories SET is_hidden = 0 WHERE source_id = ? AND type = ? AND category_id = ?'); + const showItem = db.prepare('UPDATE playlist_items SET is_hidden = 0 WHERE source_id = ? AND type = ? AND item_id = ?'); + + // Cascading statements (show all children of a category) + const showCatChildren = db.prepare('UPDATE playlist_items SET is_hidden = 0 WHERE source_id = ? AND type = ? AND category_id = ?'); + const runBulk = db.transaction((list) => { for (const item of list) { const mapping = mapItemType(item.itemType); if (mapping) { - const idCol = mapping.table === 'categories' ? 'category_id' : 'item_id'; - db.prepare(` - UPDATE ${mapping.table} SET is_hidden = 0 - WHERE source_id = ? AND type = ? AND ${idCol} = ? - `).run(item.sourceId, mapping.type, item.itemId); + if (mapping.table === 'categories') { + // Show the category + showCat.run(item.sourceId, mapping.type, item.itemId); + // Cascade to children + showCatChildren.run(item.sourceId, mapping.type, item.itemId); + } else { + // Show individual item + showItem.run(item.sourceId, mapping.type, item.itemId); + } } } });