164 lines
5.4 KiB
TypeScript
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)
|