yeah
All checks were successful
Deploy App / build (push) Successful in 10s
Deploy App / deploy (push) Successful in 11s

This commit is contained in:
valere
2026-02-02 21:00:28 +01:00
parent e257e076c4
commit b9a3d0184f
98 changed files with 5068 additions and 3713 deletions

View File

@@ -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
View 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
}
}

View 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
View 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(/-+$/, '')
}