const express = require('express'); const router = express.Router(); const { getDb } = require('../db/sqlite'); const { sources } = require('../db'); const { requireAuth } = require('../auth'); const { getAccessibleSourceIds, assertCanUseSourceById } = require('../sourceAccess'); router.use(requireAuth); // Helper to map API item types to DB types and tables function mapItemType(apiType) { switch (apiType) { case 'channel': return { table: 'playlist_items', type: 'live' }; case 'group': return { table: 'categories', type: 'live' }; case 'vod_category': return { table: 'categories', type: 'movie' }; case 'series_category': return { table: 'categories', type: 'series' }; case 'movie': return { table: 'playlist_items', type: 'movie' }; case 'series': return { table: 'playlist_items', type: 'series' }; default: return null; } } // Get all hidden items (formatted like db.json for frontend compatibility) router.get('/hidden', async (req, res) => { try { const { sourceId } = req.query; const db = getDb(); let allowedIds = null; if (req.user.role !== 'admin') { allowedIds = await getAccessibleSourceIds(req, sources); if (allowedIds.length === 0) { return res.json([]); } } const resultFormat = (row, itemType) => ({ source_id: row.source_id, item_type: itemType, item_id: itemType.includes('category') || itemType === 'group' ? row.category_id : row.item_id }); let catQuery = `SELECT source_id, category_id, type FROM categories WHERE is_hidden = 1`; let itemQuery = `SELECT source_id, item_id, type FROM playlist_items WHERE is_hidden = 1`; let catParams = []; let itemParams = []; if (sourceId) { if (!(await assertCanUseSourceById(req, res, sources, sourceId))) return; const sid = parseInt(sourceId, 10); catQuery += ` AND source_id = ?`; itemQuery += ` AND source_id = ?`; catParams.push(sid); itemParams.push(sid); } else if (allowedIds) { const ph = allowedIds.map(() => '?').join(','); catQuery += ` AND source_id IN (${ph})`; itemQuery += ` AND source_id IN (${ph})`; catParams = [...allowedIds]; itemParams = [...allowedIds]; } const hiddenCats = db.prepare(catQuery).all(...catParams); const hiddenItems = db.prepare(itemQuery).all(...itemParams); const hidden = []; hiddenCats.forEach(row => { let apiType; if (row.type === 'live') apiType = 'group'; else if (row.type === 'movie') apiType = 'vod_category'; else if (row.type === 'series') apiType = 'series_category'; if (apiType) hidden.push(resultFormat(row, apiType)); }); hiddenItems.forEach(row => { let apiType; if (row.type === 'live') apiType = 'channel'; else if (row.type === 'movie') apiType = 'movie'; else if (row.type === 'series') apiType = 'series'; if (apiType) hidden.push(resultFormat(row, apiType)); }); res.json(hidden); } catch (err) { console.error('Error getting hidden items:', err); res.status(500).json({ error: 'Failed to get hidden items' }); } }); // Hide item router.post('/hide', async (req, res) => { try { const { sourceId, itemType, itemId } = req.body; const mapping = mapItemType(itemType); if (!mapping) return res.status(400).json({ error: 'Invalid item type' }); if (!(await assertCanUseSourceById(req, res, sources, sourceId))) return; const db = getDb(); const idCol = mapping.table === 'categories' ? 'category_id' : 'item_id'; const stmt = db.prepare(` UPDATE ${mapping.table} SET is_hidden = 1 WHERE source_id = ? AND type = ? AND ${idCol} = ? `); stmt.run(sourceId, mapping.type, itemId); res.json({ success: true }); } catch (err) { console.error('Error hiding item:', err); res.status(500).json({ error: 'Failed to hide item' }); } }); // Show item router.post('/show', async (req, res) => { try { const { sourceId, itemType, itemId } = req.body; const mapping = mapItemType(itemType); if (!mapping) return res.status(400).json({ error: 'Invalid item type' }); if (!(await assertCanUseSourceById(req, res, sources, sourceId))) return; const db = getDb(); const idCol = mapping.table === 'categories' ? 'category_id' : 'item_id'; const stmt = db.prepare(` UPDATE ${mapping.table} SET is_hidden = 0 WHERE source_id = ? AND type = ? AND ${idCol} = ? `); stmt.run(sourceId, mapping.type, itemId); res.json({ success: true }); } catch (err) { console.error('Error showing item:', err); res.status(500).json({ error: 'Failed to show item' }); } }); // Check hidden status router.get('/hidden/check', async (req, res) => { try { const { sourceId, itemType, itemId } = req.query; const mapping = mapItemType(itemType); if (!mapping) return res.json({ hidden: false }); if (!(await assertCanUseSourceById(req, res, sources, sourceId))) return; const db = getDb(); const idCol = mapping.table === 'categories' ? 'category_id' : 'item_id'; const row = db.prepare(` SELECT is_hidden FROM ${mapping.table} WHERE source_id = ? AND type = ? AND ${idCol} = ? `).get(sourceId, mapping.type, itemId); res.json({ hidden: !!(row && row.is_hidden) }); } catch (err) { console.error('Error checking hidden:', err); res.status(500).json({ error: 'Failed to check status' }); } }); // Bulk Hide router.post('/hide/bulk', async (req, res) => { try { const { items } = req.body; if (!Array.isArray(items)) return res.status(400).json({ error: 'items array required' }); const uniqueIds = [...new Set(items.map(i => i.sourceId))]; for (const sid of uniqueIds) { if (!(await assertCanUseSourceById(req, res, sources, sid))) return; } const db = getDb(); 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 = ?'); 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) { if (mapping.table === 'categories') { hideCat.run(item.sourceId, mapping.type, item.itemId); hideCatChildren.run(item.sourceId, mapping.type, item.itemId); } else { hideItem.run(item.sourceId, mapping.type, item.itemId); } } } }); runBulk(items); res.json({ success: true, count: items.length }); } catch (err) { if (err.code === 'SQLITE_BUSY') { return res.status(503).json({ error: 'Database is busy, please try again' }); } console.error('Error bulk hide:', err); res.status(500).json({ error: 'Failed' }); } }); // Bulk Show router.post('/show/bulk', async (req, res) => { try { const { items } = req.body; if (!Array.isArray(items)) return res.status(400).json({ error: 'items array required' }); const uniqueIds = [...new Set(items.map(i => i.sourceId))]; for (const sid of uniqueIds) { if (!(await assertCanUseSourceById(req, res, sources, sid))) return; } const db = getDb(); 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 = ?'); 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) { if (mapping.table === 'categories') { showCat.run(item.sourceId, mapping.type, item.itemId); showCatChildren.run(item.sourceId, mapping.type, item.itemId); } else { showItem.run(item.sourceId, mapping.type, item.itemId); } } } }); runBulk(items); res.json({ success: true, count: items.length }); } catch (err) { if (err.code === 'SQLITE_BUSY') { return res.status(503).json({ error: 'Database is busy, please try again' }); } console.error('Error bulk show:', err); res.status(500).json({ error: 'Failed' }); } }); // Show ALL items for a source (single SQL statement - much faster than bulk) router.post('/show/all', async (req, res) => { try { const { sourceId, contentType } = req.body; if (!sourceId) return res.status(400).json({ error: 'sourceId required' }); if (!(await assertCanUseSourceById(req, res, sources, sourceId))) return; const db = getDb(); let catCount = 0; let itemCount = 0; const types = contentType === 'movies' ? ['movie'] : contentType === 'series' ? ['series'] : ['live']; for (const type of types) { const catResult = db.prepare(`UPDATE categories SET is_hidden = 0 WHERE source_id = ? AND type = ?`).run(sourceId, type); const itemResult = db.prepare(`UPDATE playlist_items SET is_hidden = 0 WHERE source_id = ? AND type = ?`).run(sourceId, type); catCount += catResult.changes; itemCount += itemResult.changes; } console.log(`[Channels] Show all for source ${sourceId} (${contentType}): ${catCount} categories, ${itemCount} items`); res.json({ success: true, categoriesUpdated: catCount, itemsUpdated: itemCount }); } catch (err) { console.error('Error show all:', err); res.status(500).json({ error: 'Failed to show all' }); } }); // Hide ALL items for a source (single SQL statement - much faster than bulk) router.post('/hide/all', async (req, res) => { try { const { sourceId, contentType } = req.body; if (!sourceId) return res.status(400).json({ error: 'sourceId required' }); if (!(await assertCanUseSourceById(req, res, sources, sourceId))) return; const db = getDb(); let catCount = 0; let itemCount = 0; const types = contentType === 'movies' ? ['movie'] : contentType === 'series' ? ['series'] : ['live']; for (const type of types) { const catResult = db.prepare(`UPDATE categories SET is_hidden = 1 WHERE source_id = ? AND type = ?`).run(sourceId, type); const itemResult = db.prepare(`UPDATE playlist_items SET is_hidden = 1 WHERE source_id = ? AND type = ?`).run(sourceId, type); catCount += catResult.changes; itemCount += itemResult.changes; } console.log(`[Channels] Hide all for source ${sourceId} (${contentType}): ${catCount} categories, ${itemCount} items`); res.json({ success: true, categoriesUpdated: catCount, itemsUpdated: itemCount }); } catch (err) { console.error('Error hide all:', err); res.status(500).json({ error: 'Failed to hide all' }); } }); // Get recent movies or series router.get('/recent', async (req, res) => { try { const { type, limit = 12 } = req.query; if (!type || (type !== 'movie' && type !== 'series')) { return res.status(400).json({ error: 'Valid type (movie or series) is required' }); } let accessibleFilter = ''; const params = [type]; if (req.user.role !== 'admin') { const ids = await getAccessibleSourceIds(req, sources); if (ids.length === 0) { return res.json([]); } accessibleFilter = ` AND p.source_id IN (${ids.map(() => '?').join(',')})`; params.push(...ids); } params.push(parseInt(limit, 10)); const db = getDb(); const recentItems = db.prepare(` SELECT * FROM playlist_items p WHERE p.type = ? AND p.is_hidden = 0 AND NOT EXISTS ( SELECT 1 FROM categories c WHERE c.source_id = p.source_id AND c.category_id = p.category_id AND c.type = p.type AND c.is_hidden = 1 ) ${accessibleFilter} ORDER BY p.added_at DESC LIMIT ? `).all(...params); const formatted = recentItems.map(item => ({ ...item, data: JSON.parse(item.data) })); res.json(formatted); } catch (err) { console.error(`Error getting recent ${req.query.type}:`, err); res.status(500).json({ error: 'Failed to get recent items' }); } }); module.exports = router;