yeah
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
let db: Database.Database | null = null
|
||||
|
||||
export function getDatabase(): Database.Database {
|
||||
if (db) return db
|
||||
|
||||
const dbDir = path.join(process.cwd(), 'server/database')
|
||||
const dbPath = path.join(dbDir, 'evilspins.db')
|
||||
|
||||
// Créer le dossier si nécessaire
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Connexion à la base
|
||||
db = new Database(dbPath, {
|
||||
verbose: process.env.NODE_ENV === 'development' ? console.log : undefined
|
||||
})
|
||||
|
||||
// Activer les clés étrangères
|
||||
db.pragma('foreign_keys = ON')
|
||||
|
||||
// Initialiser le schéma si la DB est vide
|
||||
initializeSchema(db)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
function initializeSchema(database: Database.Database) {
|
||||
const schemaPath = path.join(process.cwd(), 'server/database/schema.sql')
|
||||
|
||||
// Vérifier si les tables existent déjà
|
||||
const tableCheck = database
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='tracks'")
|
||||
.get()
|
||||
|
||||
if (!tableCheck) {
|
||||
console.log('🔧 Initialisation du schéma de la base de données...')
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8')
|
||||
|
||||
// Exécuter chaque statement SQL
|
||||
const statements = schema
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
|
||||
for (const statement of statements) {
|
||||
database.exec(statement)
|
||||
}
|
||||
|
||||
console.log('✅ Schéma initialisé avec succès')
|
||||
}
|
||||
}
|
||||
|
||||
export function closeDatabase() {
|
||||
if (db) {
|
||||
db.close()
|
||||
db = null
|
||||
}
|
||||
}
|
||||
119
server/utils/fileScanner.ts
Normal file
119
server/utils/fileScanner.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import { join, extname, basename } from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { slugify } from './slugify'
|
||||
import { getCardFromDate } from './getCardFromDate'
|
||||
import type { Card } from '@/types/types'
|
||||
|
||||
const listAudioExts = ['.mp3', '.opus', 'flac']
|
||||
const listImageExts = ['.jpg', '.jpeg', '.webp']
|
||||
|
||||
export async function scanMusicFolder(folderPath: string): Promise<Card[]> {
|
||||
try {
|
||||
const files = await readdir(folderPath)
|
||||
const cardMap = new Map<string, Card>()
|
||||
|
||||
// D'abord, on traite tous les fichiers audio
|
||||
for (const file of files) {
|
||||
const ext = extname(file).toLowerCase()
|
||||
|
||||
// On ne traite que les fichiers audio
|
||||
if (!listAudioExts.includes(ext)) continue
|
||||
|
||||
const parsed = await parseFilename(join(folderPath, file))
|
||||
if (parsed) {
|
||||
// On vérifie s'il existe une image avec le même nom de base
|
||||
const baseName = basename(file, ext)
|
||||
let imageUrl = ''
|
||||
|
||||
// On cherche une image correspondante
|
||||
for (const imgExt of listImageExts) {
|
||||
const potentialImage = baseName + imgExt
|
||||
if (files.includes(potentialImage)) {
|
||||
imageUrl = process.env.URL_PREFIX + baseName + imgExt
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cardMap.set(parsed.esid, {
|
||||
...parsed,
|
||||
url_audio: process.env.URL_PREFIX + baseName + ext,
|
||||
url_image: imageUrl,
|
||||
suit: parsed.suit,
|
||||
rank: parsed.rank
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(cardMap.values())
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du scan du dossier:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function parseFilename(
|
||||
filename: string
|
||||
): Promise<Omit<Card, 'url_audio' | 'url_image'> | null> {
|
||||
// Format: yyyymmddhh__artist__title.ext
|
||||
const nameWithoutExt = basename(filename, extname(filename))
|
||||
const parts = nameWithoutExt.split('__')
|
||||
|
||||
if (parts.length !== 3) {
|
||||
console.warn(`Nom de fichier invalide: ${filename}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const [datetime, artist, title] = parts
|
||||
|
||||
if (!datetime || !artist || !title) {
|
||||
console.warn(`Format de fichier invalide: ${filename} - manque des parties`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (datetime.length !== 10) {
|
||||
console.warn(`Format de date invalide: ${filename}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Utilisation d'un hash basé sur le contenu du fichier pour un ESID stable
|
||||
let fileHash = ''
|
||||
try {
|
||||
const fileContent = await readFile(filename)
|
||||
fileHash = createHash('md5').update(fileContent).digest('hex').substring(0, 8)
|
||||
} catch (error) {
|
||||
console.warn(`Impossible de lire le fichier pour générer le hash: ${filename}`)
|
||||
fileHash = createHash('md5').update(filename).digest('hex').substring(0, 8)
|
||||
}
|
||||
|
||||
const year = datetime.substring(0, 4)
|
||||
const month = datetime.substring(4, 6)
|
||||
const day = datetime.substring(6, 8)
|
||||
const hour = datetime.substring(8, 10)
|
||||
// Créer l'ID unique pour la card
|
||||
const esid = createHash('md5')
|
||||
.update(`${year}${month}${day}${hour}${artist}${title}`)
|
||||
.digest('hex')
|
||||
|
||||
const date = new Date(
|
||||
parseInt(year, 10),
|
||||
parseInt(month, 10) - 1,
|
||||
parseInt(day, 10),
|
||||
parseInt(hour, 10)
|
||||
)
|
||||
const card = getCardFromDate(date)
|
||||
|
||||
return {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
artist: artist.replace(/_/g, ' '), // Remplacer les _ par des espaces
|
||||
title: title.replace(/_/g, ' '),
|
||||
esid,
|
||||
slug: slugify(`${artist} ${title}`),
|
||||
createdAt: date,
|
||||
suit: card.suit,
|
||||
rank: card.rank
|
||||
}
|
||||
}
|
||||
21
server/utils/getCardFromDate.ts
Normal file
21
server/utils/getCardFromDate.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Suit, Rank } from '../../types/types'
|
||||
|
||||
export function getCardFromDate(date: Date): { suit: Suit; rank: Rank } {
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
|
||||
const suit: Suit =
|
||||
month >= 12 || month <= 2
|
||||
? '♠'
|
||||
: month >= 3 && month <= 5
|
||||
? '♥'
|
||||
: month >= 6 && month <= 8
|
||||
? '♦'
|
||||
: '♣'
|
||||
|
||||
const ranks: Rank[] = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
|
||||
const rank = ranks[(day + hour) % ranks.length]
|
||||
|
||||
return { suit, rank }
|
||||
}
|
||||
9
server/utils/slugify.ts
Normal file
9
server/utils/slugify.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function slugify(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '')
|
||||
}
|
||||
Reference in New Issue
Block a user