import fs from 'fs' import path from 'path' import { Database } from 'sqlite' import { fileURLToPath } from 'url' import chokidar from 'chokidar' import { db } from '../database' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const PLAYLISTS_DIR = path.join(process.cwd(), 'public/ESPLAYLISTS') const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a', '.flac'] export class PlaylistSyncService { private watcher: chokidar.FSWatcher | null = null constructor(private db: Database) {} async initialize() { await this.scanPlaylists() this.setupWatcher() } private async scanPlaylists() { try { if (!fs.existsSync(PLAYLISTS_DIR)) { console.warn(`Playlists directory not found: ${PLAYLISTS_DIR}`) return } const playlists = fs .readdirSync(PLAYLISTS_DIR, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name) for (const playlistName of playlists) { await this.syncPlaylist(playlistName) } } catch (error) { console.error('Error scanning playlists:', error) } } private async syncPlaylist(playlistName: string) { const playlistPath = path.join(PLAYLISTS_DIR, playlistName) try { const stats = fs.statSync(playlistPath) // Vérifier ou créer la playlist dans la base de données let playlist = await this.db.get('SELECT * FROM playlists WHERE name = ?', [playlistName]) if (!playlist) { const result = await this.db.run( 'INSERT INTO playlists (name, path, last_modified) VALUES (?, ?, ?)', [playlistName, playlistPath, new Date(stats.mtime).toISOString()] ) playlist = await this.db.get('SELECT * FROM playlists WHERE id = ?', [result.lastID]) } // Récupérer les pistes actuelles const currentTracks = fs .readdirSync(playlistPath) .filter((file) => { const ext = path.extname(file).toLowerCase() return AUDIO_EXTENSIONS.includes(ext) }) .map((file, index) => ({ path: path.join(playlistName, file), order: index })) // Mettre à jour les pistes dans la base de données await this.db.run('BEGIN TRANSACTION') try { // Supprimer les anciennes entrées await this.db.run('DELETE FROM playlist_tracks WHERE playlist_id = ?', [playlist.id]) // Ajouter les nouvelles pistes for (const track of currentTracks) { await this.db.run( 'INSERT INTO playlist_tracks (playlist_id, track_path, track_order) VALUES (?, ?, ?)', [playlist.id, track.path, track.order] ) } // Mettre à jour la date de modification await this.db.run('UPDATE playlists SET last_modified = ? WHERE id = ?', [ new Date().toISOString(), playlist.id ]) await this.db.run('COMMIT') console.log(`Playlist "${playlistName}" synchronized with ${currentTracks.length} tracks`) } catch (error) { await this.db.run('ROLLBACK') console.error(`Error syncing playlist ${playlistName}:`, error) } } catch (error) { console.error(`Error accessing playlist directory ${playlistPath}:`, error) } } private setupWatcher() { if (!fs.existsSync(PLAYLISTS_DIR)) { console.warn(`Playlists directory not found, watcher not started: ${PLAYLISTS_DIR}`) return } this.watcher = chokidar.watch(PLAYLISTS_DIR, { ignored: /(^|[\/\\])\../, // ignore les fichiers cachés persistent: true, ignoreInitial: true, depth: 2 // surveille un seul niveau de sous-dossiers }) this.watcher .on('add', (filePath) => this.handleFileChange('add', filePath)) .on('change', (filePath) => this.handleFileChange('change', filePath)) .on('unlink', (filePath) => this.handleFileChange('unlink', filePath)) .on('addDir', (dirPath) => this.handleDirChange('addDir', dirPath)) .on('unlinkDir', (dirPath) => this.handleDirChange('unlinkDir', dirPath)) .on('error', (error) => console.error('Watcher error:', error)) } private async handleFileChange(event: string, filePath: string) { const relativePath = path.relative(PLAYLISTS_DIR, filePath) const playlistName = path.dirname(relativePath) const ext = path.extname(filePath).toLowerCase() // Ignorer les fichiers qui ne sont pas des fichiers audio if (playlistName === '.' || !AUDIO_EXTENSIONS.includes(ext)) { return } console.log(`File ${event}: ${relativePath}`) await this.syncPlaylist(playlistName) } private async handleDirChange(event: string, dirPath: string) { const relativePath = path.relative(PLAYLISTS_DIR, dirPath) if (relativePath === '') return // Ignorer le dossier racine console.log(`Directory ${event}: ${relativePath}`) if (event === 'addDir') { await this.syncPlaylist(relativePath) } else if (event === 'unlinkDir') { // Supprimer la playlist de la base de données await this.db.run('DELETE FROM playlists WHERE name = ?', [relativePath]) console.log(`Removed playlist from database: ${relativePath}`) } } close() { if (this.watcher) { return this.watcher.close() } return Promise.resolve() } } // Singleton instance export const playlistSyncService = new PlaylistSyncService(db)