const express = require('express'); const path = require('path'); const fs = require('fs'); const router = express.Router(); const database = require('../lib/database'); const storage = require('../lib/storage'); const imageFetcher = require('../lib/image-fetcher'); const CONFIG_PATH = path.join(__dirname, '..', 'image-sources.json'); function readSourcesConfig() { const defaultConfig = { sources: [], fetchInterval: 2.5 }; try { if (!fs.existsSync(CONFIG_PATH)) { return defaultConfig; } const content = fs.readFileSync(CONFIG_PATH, 'utf8'); const parsed = JSON.parse(content); return { sources: Array.isArray(parsed.sources) ? parsed.sources : [], fetchInterval: typeof parsed.fetchInterval === 'number' ? parsed.fetchInterval : 2.5, comments: parsed.comments || undefined }; } catch (err) { console.warn('Failed to read image-sources.json:', err.message); return defaultConfig; } } function writeSourcesConfig(config) { const payload = { ...config, comments: config.comments || { sources: 'Array of image sources to fetch from', name: 'Human-readable name for the source', url: 'Full URL to the image', tags: 'Array of tags to apply to fetched images (useful for ML training)', enabled: 'Set to true to include this source in fetching', fetchInterval: 'Minutes between fetch cycles. Examples: 0.033 = 2 seconds, 0.05 = 3 seconds, 0.083 = 5 seconds, 1 = 1 minute, 2.5 = 2.5 minutes (default)' } }; fs.writeFileSync(CONFIG_PATH, JSON.stringify(payload, null, 2)); } function restartFetcherFromConfig(config) { imageFetcher.stopFetcher(); const enabledSources = (config.sources || []).filter((source) => source.enabled); const interval = config.fetchInterval || 2.5; if (enabledSources.length > 0) { imageFetcher.startFetcher(enabledSources, interval); } } /** * GET /api/images - Get all images with optional filtering */ router.get('/images', async (req, res) => { try { const { tag, sourceUrl, page = 1, pageSize = 50, sort = 'fetched_at', order = 'DESC' } = req.query; const limit = parseInt(pageSize); const offset = (parseInt(page) - 1) * limit; const images = await database.getImages({ tag, sourceUrl, limit, offset, sortBy: sort, order }); // Get tags for each image for (let i = 0; i < images.length; i++) { images[i].tags = await database.getImageTags(images[i].id); } const total = await database.getImageCount(tag); res.json({ success: true, images, pagination: { page: parseInt(page), pageSize: limit, total, pages: Math.ceil(total / limit) } }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * GET /api/images/:id - Get a specific image */ router.get('/images/:id', async (req, res) => { try { const image = await database.getImage(req.params.id); if (!image) { return res.status(404).json({ success: false, error: 'Image not found' }); } image.tags = await database.getImageTags(image.id); res.json({ success: true, image }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * GET /api/images/:id/download - Download image file */ router.get('/images/:id/download', async (req, res) => { try { const image = await database.getImage(req.params.id); if (!image) { return res.status(404).json({ success: false, error: 'Image not found' }); } const buffer = await storage.getImageBuffer(image.file_path); res.contentType(image.mime_type); res.send(buffer); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * POST /api/images - Fetch and save a new image */ router.post('/images', async (req, res) => { try { const { source_url, tags = [] } = req.body; if (!source_url) { return res.status(400).json({ success: false, error: 'source_url is required' }); } const result = await imageFetcher.fetchImage(source_url, tags); if (!result.success) { return res.status(400).json({ success: false, error: result.error }); } const image = await database.getImage(result.imageId); image.tags = await database.getImageTags(image.id); res.status(201).json({ success: true, image }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * POST /api/images/:id/tags - Add tags to an image */ router.post('/images/:id/tags', async (req, res) => { try { const { tags = [] } = req.body; if (!Array.isArray(tags) || tags.length === 0) { return res.status(400).json({ success: false, error: 'tags must be a non-empty array' }); } const image = await database.getImage(req.params.id); if (!image) { return res.status(404).json({ success: false, error: 'Image not found' }); } await database.addTags(image.id, tags); image.tags = await database.getImageTags(image.id); res.json({ success: true, image }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * DELETE /api/images/:id - Delete an image */ router.delete('/images/:id', async (req, res) => { try { const image = await database.getImage(req.params.id); if (!image) { return res.status(404).json({ success: false, error: 'Image not found' }); } // Delete file await storage.deleteImageFile(image.file_path); // Delete database record await database.deleteImage(image.id); res.json({ success: true, message: 'Image deleted' }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * GET /api/tags - Get all available tags */ router.get('/tags', async (req, res) => { try { const tags = await database.getAllTags(); res.json({ success: true, tags }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * GET /api/stats - Get storage and image statistics */ router.get('/stats', async (req, res) => { try { const storageStats = await storage.getStorageStats(); const imageCount = await database.getImageCount(); res.json({ success: true, stats: { imageCount, ...storageStats } }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * POST /api/verify - Verify all images for corruption */ router.post('/verify', async (req, res) => { try { const result = await imageFetcher.verifyAllImages(); res.json({ success: true, result }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * POST /api/cleanup - Clean up old images */ router.post('/cleanup', async (req, res) => { try { const { daysOld = 30 } = req.body; const deleted = await database.cleanupOldImages(daysOld); const orphaned = await storage.cleanupOrphanedFiles(database.db); res.json({ success: true, cleanup: { deletedFromDb: deleted, orphanedFilesRemoved: orphaned.cleaned } }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * POST /api/cleanup/before - Delete images before a specific date */ router.post('/cleanup/before', async (req, res) => { try { const { beforeDate } = req.body; if (!beforeDate || typeof beforeDate !== 'string') { return res.status(400).json({ success: false, error: 'beforeDate is required (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)' }); } const normalizedInput = beforeDate.replace('T', ' '); const normalized = normalizedInput.length <= 10 ? `${normalizedInput} 00:00:00` : (normalizedInput.length === 16 ? `${normalizedInput}:00` : normalizedInput); const imagesToDelete = await database.getImagesBeforeDate(normalized); let filesDeleted = 0; for (const image of imagesToDelete) { const deletedFile = await storage.deleteImageFile(image.file_path); if (deletedFile) filesDeleted++; } const deletedFromDb = await database.deleteImagesBeforeDate(normalized); res.json({ success: true, cleanup: { deletedFromDb, filesDeleted, beforeDate: normalized } }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * GET /api/fetcher/status - Get fetcher status */ router.get('/fetcher/status', (req, res) => { const status = imageFetcher.getStatus(); res.json({ success: true, status }); }); /** * GET /api/sources - Get image sources configuration */ router.get('/sources', (req, res) => { const config = readSourcesConfig(); res.json({ success: true, config }); }); /** * POST /api/sources - Add a new image source */ router.post('/sources', (req, res) => { try { const { name, url, tags = [], enabled = true } = req.body; if (!name || !url) { return res.status(400).json({ success: false, error: 'name and url are required' }); } const config = readSourcesConfig(); const newSource = { name: String(name).trim(), url: String(url).trim(), tags: Array.isArray(tags) ? tags.map(tag => String(tag).trim()).filter(Boolean) : [], enabled: Boolean(enabled) }; config.sources = [...(config.sources || []), newSource]; writeSourcesConfig(config); restartFetcherFromConfig(config); res.status(201).json({ success: true, config }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * PUT /api/sources/config - Update fetch interval */ router.put('/sources/config', (req, res) => { try { const { fetchInterval } = req.body; const intervalValue = Number(fetchInterval); if (!Number.isFinite(intervalValue) || intervalValue <= 0) { return res.status(400).json({ success: false, error: 'fetchInterval must be a positive number' }); } const config = readSourcesConfig(); config.fetchInterval = intervalValue; writeSourcesConfig(config); restartFetcherFromConfig(config); res.json({ success: true, config }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * PUT /api/sources/:index - Update an existing source */ router.put('/sources/:index', (req, res) => { try { const index = Number(req.params.index); if (!Number.isInteger(index)) { return res.status(400).json({ success: false, error: 'Invalid source index' }); } const config = readSourcesConfig(); if (!config.sources || !config.sources[index]) { return res.status(404).json({ success: false, error: 'Source not found' }); } const current = config.sources[index]; const updated = { ...current, name: req.body.name !== undefined ? String(req.body.name).trim() : current.name, url: req.body.url !== undefined ? String(req.body.url).trim() : current.url, tags: Array.isArray(req.body.tags) ? req.body.tags.map(tag => String(tag).trim()).filter(Boolean) : current.tags, enabled: req.body.enabled !== undefined ? Boolean(req.body.enabled) : current.enabled }; config.sources[index] = updated; writeSourcesConfig(config); restartFetcherFromConfig(config); res.json({ success: true, config }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); /** * DELETE /api/sources/:index - Remove a source */ router.delete('/sources/:index', (req, res) => { try { const index = Number(req.params.index); const config = readSourcesConfig(); if (!Number.isInteger(index) || !config.sources || !config.sources[index]) { return res.status(404).json({ success: false, error: 'Source not found' }); } config.sources.splice(index, 1); writeSourcesConfig(config); restartFetcherFromConfig(config); res.json({ success: true, config }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } }); module.exports = router;