Files
evilspins/server/services/playlist-sync.service.ts
2025-12-31 16:31:53 +01:00

164 lines
5.4 KiB
TypeScript

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)