Compare commits
9 Commits
afb20fe75f
...
A-new-Comp
| Author | SHA1 | Date | |
|---|---|---|---|
| 9705257178 | |||
| 2f78442deb | |||
|
|
c586cc3932 | ||
|
|
11694d36dd | ||
|
|
3b05938162 | ||
|
|
f75a1481bd | ||
|
|
bb791e35d1 | ||
|
|
a5fe876e3f | ||
|
|
9001025837 |
@@ -1,5 +1,6 @@
|
||||
# Builder
|
||||
FROM node:20-bookworm AS builder
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --legacy-peer-deps
|
||||
@@ -8,9 +9,12 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Runtime
|
||||
FROM node:20-slim AS runner
|
||||
FROM node:20-alpine AS runner
|
||||
RUN apk add --no-cache python3 make g++ sqlite
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/.output ./.output
|
||||
COPY package*.json ./
|
||||
COPY ./server/database ./server/database
|
||||
EXPOSE 3000
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<div class="face back flex flex-row flex-wrap items-start p-4 overflow-hidden"
|
||||
:class="{ 'overflow-y-scroll': !isCompilation }" ref="backFace">
|
||||
<li class="list-none text-xxs w-1/2 flex flex-row"
|
||||
<!-- <li class="list-none text-xxs w-1/2 flex flex-row"
|
||||
v-for="track in dataStore.getTracksByboxId(box.id, box.activeSide)" :key="track.id" :track="track">
|
||||
<span class="text-slate-700" v-if="isNotManifesto"> {{ track.order }}. </span>
|
||||
<p class="text-left text-slate-700">
|
||||
@@ -19,7 +19,7 @@
|
||||
<br />
|
||||
{{ track.artist.name }}
|
||||
</p>
|
||||
</li>
|
||||
</li> -->
|
||||
</div>
|
||||
<div class="face right" ref="rightFace" />
|
||||
<div class="face left" ref="leftFace" />
|
||||
|
||||
@@ -60,6 +60,8 @@ onMounted(() => {
|
||||
// Gestion du drag and drop desktop
|
||||
const handleDragStart = (event: { item: HTMLElement }) => {
|
||||
drag.value = true
|
||||
// Émettre un événement personnalisé pour indiquer qu'un glisser a commencé depuis le bucket
|
||||
document.dispatchEvent(new CustomEvent('bucket-drag-start'))
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: { item: HTMLElement; newIndex: number; oldIndex: number }) => {
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div class="platine pointer-events-none" :class="{ 'drag-over': isDragOver }" @dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
|
||||
<div class="disc pointer-events-auto fixed" ref="discRef" :style="'background-image: url(/card-dock.svg)'"
|
||||
id="disc">
|
||||
<div
|
||||
class="bobine bg-slate-900 bg-opacity-50 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full"
|
||||
<div class="platine pointer-events-none" :class="{ 'loading': platineStore.isLoadingTrack, 'mounted': isMounted }"
|
||||
ref="platine">
|
||||
<img class="cover" :src="platineStore.currentTrack?.coverId" />
|
||||
<div class="disc pointer-events-auto fixed bg-transparent" ref="discRef" id="disc">
|
||||
<div class="bobine"
|
||||
:style="{ height: platineStore.progressPercentage + '%', width: platineStore.progressPercentage + '%' }"></div>
|
||||
|
||||
|
||||
<div class="disc-label rounded-full bg-cover bg-center">
|
||||
<img src="/favicon.svg" class="size-1/3">
|
||||
<img src="/favicon.svg" class="size-1/2 bg-black rounded-full p-5">
|
||||
<div v-if="platineStore.isLoadingTrack" class="loading-indicator">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-1/5 flex justify-center items-center text-8xl text-white absolute pointer-events-none">
|
||||
{{ platineStore.currentTrack?.title }}
|
||||
<br>
|
||||
{{ platineStore.currentTrack?.artist.name }}
|
||||
</div>
|
||||
<div v-if="!platineStore.isLoadingTrack" class="absolute top-1/2 right-8 size-1/12 rounded-full bg-esyellow">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="w-full h-1/5 text-base">
|
||||
{{ platineStore.currentTrack?.title }}
|
||||
<br>
|
||||
{{ platineStore.currentTrack?.artist?.name }}
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,41 +32,12 @@ import type { Track } from '~~/types/types'
|
||||
const props = defineProps<{ track?: Track }>()
|
||||
const platineStore = usePlatineStore()
|
||||
const discRef = ref<HTMLElement>()
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// Gestion du drag and drop
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const onDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
isDragOver.value = false
|
||||
const cardData = e.dataTransfer?.getData('application/json')
|
||||
|
||||
if (cardData) {
|
||||
try {
|
||||
const newTrack = JSON.parse(cardData)
|
||||
if (newTrack && newTrack.url) {
|
||||
platineStore.loadTrack(newTrack)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du traitement de la carte déposée', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
const platine = ref<HTMLElement>()
|
||||
const isMounted = ref(false)
|
||||
|
||||
// Initialisation du lecteur
|
||||
onMounted(() => {
|
||||
isMounted.value = true
|
||||
if (discRef.value) {
|
||||
platineStore.initPlatine(discRef.value)
|
||||
}
|
||||
@@ -74,6 +45,7 @@ onMounted(() => {
|
||||
|
||||
// Nettoyage
|
||||
onUnmounted(() => {
|
||||
isMounted.value = false
|
||||
platineStore.cleanup()
|
||||
})
|
||||
|
||||
@@ -92,21 +64,34 @@ watch(() => props.track, (newTrack) => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.card {
|
||||
position: absolute !important;
|
||||
z-index: 99;
|
||||
top: -20%;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: opacity 3s ease;
|
||||
|
||||
.loading & {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disc {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -114,10 +99,10 @@ watch(() => props.track, (newTrack) => {
|
||||
cursor: grab;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
|
||||
|
||||
.dragoOver & {
|
||||
background-color: #4CAF50;
|
||||
.loading & {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +175,6 @@ watch(() => props.track, (newTrack) => {
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
@@ -209,15 +193,6 @@ watch(() => props.track, (newTrack) => {
|
||||
}
|
||||
|
||||
.bobine {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@apply bg-slate-900 bg-opacity-50 backdrop-blur absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rounded-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
<SelectCardRank @change="onRankChange" />
|
||||
<SelectCardSuit @change="onSuitChange" />
|
||||
</div>
|
||||
<div ref="deck" class="deck flex flex-wrap justify-center gap-4" :class="{ 'pb-36': playerStore.currentTrack }">
|
||||
<div ref="deck" class="deck flex flex-wrap justify-center gap-4" :class="{ 'pb-36': playerStore.currentTrack }"
|
||||
@dragover.prevent @drop.prevent="handleGlobalDrop">
|
||||
<card v-for="(track, i) in filteredTracks" :key="track.id" :track="track" :tabindex="i"
|
||||
@card-click="playerStore.playPlaylistTrack(track)" :is-face-up="isCardRevealed(track.id)"
|
||||
@click-card-symbol="openCardSharer()" />
|
||||
@@ -18,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useDataStore } from '~/store/data'
|
||||
import { useCardStore } from '~/store/card'
|
||||
import { usePlayerStore } from '~/store/player'
|
||||
@@ -48,7 +49,82 @@ const uiStore = useUiStore()
|
||||
|
||||
const deck = ref()
|
||||
const tracks = computed(() => dataStore.getTracksByboxId(props.box.id))
|
||||
const filteredTracks = ref(tracks.value)
|
||||
|
||||
// Suivre si un glisser est en cours depuis le bucket
|
||||
const isDraggingFromBucket = ref(false)
|
||||
|
||||
// Gérer le dépôt d'une carte en dehors du bucket
|
||||
const handleGlobalDrop = (e: DragEvent) => {
|
||||
if (isDraggingFromBucket.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// Récupérer les données de la carte glissée
|
||||
const cardData = e.dataTransfer?.getData('application/json')
|
||||
if (cardData) {
|
||||
try {
|
||||
const track = JSON.parse(cardData)
|
||||
// Retirer la carte du panier
|
||||
cardStore.removeFromBucket(track.id)
|
||||
// La carte réapparaîtra automatiquement dans la playlist
|
||||
// grâce à la computed property filteredTracks
|
||||
} catch (e) {
|
||||
console.error('Erreur lors du traitement de la carte glissée', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
isDraggingFromBucket.value = false
|
||||
}
|
||||
|
||||
// Gérer le début du glisser depuis le bucket
|
||||
const handleBucketDragStart = () => {
|
||||
isDraggingFromBucket.value = true
|
||||
}
|
||||
|
||||
// Configurer les écouteurs d'événements
|
||||
onMounted(() => {
|
||||
document.addEventListener('drop', handleGlobalDrop)
|
||||
document.addEventListener('dragover', (e) => e.preventDefault()) // Nécessaire pour permettre le drop
|
||||
document.addEventListener('bucket-drag-start', handleBucketDragStart)
|
||||
})
|
||||
|
||||
// Nettoyer les écouteurs d'événements
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('drop', handleGlobalDrop)
|
||||
document.removeEventListener('dragover', (e) => e.preventDefault())
|
||||
document.removeEventListener('bucket-drag-start', handleBucketDragStart)
|
||||
})
|
||||
|
||||
// Utiliser une computed property pour filteredTracks qui réagit aux changements
|
||||
const filteredTracks = computed(() => {
|
||||
let result = [...tracks.value]
|
||||
|
||||
// Exclure les pistes déjà dans le panier
|
||||
result = result.filter(track => !cardStore.isInBucket(track.id))
|
||||
|
||||
// Appliquer les autres filtres
|
||||
if (selectedSuit.value) {
|
||||
result = result.filter(track => track.card?.suit === selectedSuit.value)
|
||||
}
|
||||
|
||||
if (selectedRank.value) {
|
||||
result = result.filter(track => track.card?.rank === selectedRank.value)
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(track => {
|
||||
const artistName = typeof track.artist === 'object' ? track.artist?.name : String(track.artist || '')
|
||||
return (
|
||||
track.title?.toLowerCase().includes(query) ||
|
||||
artistName.toLowerCase().includes(query) ||
|
||||
String(track.year || '').includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Variables réactives pour les filtres
|
||||
const selectedSuit = ref('')
|
||||
@@ -86,35 +162,11 @@ const onSearch = (query: string) => {
|
||||
}
|
||||
|
||||
// Applique tous les filtres (couleur, rang et recherche)
|
||||
// La computed property filteredTracks se mettra automatiquement à jour
|
||||
// car elle dépend des mêmes réactifs que cette fonction
|
||||
const applyFilters = () => {
|
||||
let result = [...tracks.value]
|
||||
|
||||
// Filtre par couleur
|
||||
if (selectedSuit.value) {
|
||||
result = result.filter(track => track.card?.suit === selectedSuit.value)
|
||||
}
|
||||
|
||||
// Filtre par rang
|
||||
if (selectedRank.value) {
|
||||
result = result.filter(track => track.card?.rank === selectedRank.value)
|
||||
}
|
||||
|
||||
// Filtre par recherche textuelle
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(track => {
|
||||
// Gestion du nom d'artiste (peut être un objet ou une chaîne)
|
||||
const artistName = typeof track.artist === 'object' ? track.artist?.name : String(track.artist || '')
|
||||
// Recherche dans le titre, l'artiste et l'année
|
||||
return (
|
||||
track.title?.toLowerCase().includes(query) ||
|
||||
artistName.toLowerCase().includes(query) ||
|
||||
String(track.year || '').includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
filteredTracks.value = result
|
||||
// Cette fonction ne fait plus que déclencher la réévaluation des dépendances
|
||||
// La computed property filteredTracks fera le reste
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<slot />
|
||||
<Bucket />
|
||||
<Platine />
|
||||
<Platine v-if="playerStore.currentTrack" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Track } from '~~/types/types'
|
||||
import { usePlayerStore } from '~/store/player'
|
||||
|
||||
const playerStore = usePlayerStore()
|
||||
const onCardDropped = (card: Track) => {
|
||||
console.log('Carte déposée dans le bucket:', card)
|
||||
}
|
||||
@@ -16,16 +18,17 @@ const onCardDropped = (card: Track) => {
|
||||
.bucket,
|
||||
.platine {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
bottom: -100%;
|
||||
right: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.bucket {
|
||||
z-index: 70;
|
||||
bottom: -260px;
|
||||
transition: bottom 0.3s ease;
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
transition: bottom .3s ease;
|
||||
|
||||
&:hover,
|
||||
.card-dragging & {
|
||||
@@ -38,9 +41,15 @@ const onCardDropped = (card: Track) => {
|
||||
}
|
||||
|
||||
.platine {
|
||||
z-index: 60;
|
||||
bottom: -70%;
|
||||
transition: bottom 0.3s ease;
|
||||
/* width: 25%; */
|
||||
bottom: -100%;
|
||||
transition: bottom 2s ease;
|
||||
|
||||
&.mounted {
|
||||
z-index: 80;
|
||||
bottom: 0;
|
||||
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
3
env.sh
3
env.sh
@@ -1,3 +0,0 @@
|
||||
export DOMAIN="evilspins.com"
|
||||
export PORT="7901"
|
||||
export PORT_EXPOSED="3000"
|
||||
18057
package-lock.json
generated
18057
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -11,13 +11,18 @@
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write ."
|
||||
"format:fix": "prettier --write .",
|
||||
"migrate": "tsx server/database/migrate.ts",
|
||||
"db:reset": "rm -f server/database/evilspins.db && npm run migrate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/eslint": "1.9.0",
|
||||
"@nuxtjs/tailwindcss": "6.14.0",
|
||||
"@pinia/nuxt": "0.11.2",
|
||||
"@types/chokidar": "^2.1.7",
|
||||
"atropos": "^2.0.2",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"nuxt": "^4.2.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
@@ -27,15 +32,16 @@
|
||||
"engines": {
|
||||
"pnpm": ">=10 <11"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.1",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@nuxt/eslint-config": "^1.10.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
@@ -43,6 +49,7 @@
|
||||
"globals": "^16.5.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"sass-embedded": "^1.93.2",
|
||||
"tsx": "^4.21.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
3464
pnpm-lock.yaml
generated
3464
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
168
server/MIGRATION_GUIDE.md
Normal file
168
server/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Migration vers SQLite - Guide d'utilisation
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
pnpm add better-sqlite3
|
||||
pnpm add -D @types/better-sqlite3
|
||||
```
|
||||
|
||||
## 🚀 Migration des données
|
||||
|
||||
### 1. Exécuter la migration
|
||||
|
||||
```bash
|
||||
pnpm tsx server/database/migrate.ts
|
||||
```
|
||||
|
||||
Cette commande va :
|
||||
|
||||
- Créer la base de données SQLite dans `server/database/evilspins.db`
|
||||
- Créer les tables (boxes, sides, artists, tracks)
|
||||
- Importer toutes vos données existantes
|
||||
|
||||
### 2. Vérifier la migration
|
||||
|
||||
Lancez votre serveur Nuxt :
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Testez les nouveaux endpoints :
|
||||
|
||||
```bash
|
||||
# Récupérer toutes les boxes
|
||||
curl http://localhost:3000/api/boxes
|
||||
|
||||
# Récupérer tous les artistes
|
||||
curl http://localhost:3000/api/artists
|
||||
|
||||
# Récupérer les tracks de compilation (première page)
|
||||
curl http://localhost:3000/api/tracks/compilation
|
||||
|
||||
# Filtrer par boxId
|
||||
curl http://localhost:3000/api/tracks/compilation?boxId=ES00
|
||||
|
||||
# Pagination
|
||||
curl http://localhost:3000/api/tracks/compilation?page=2&limit=10
|
||||
```
|
||||
|
||||
## 📁 Structure créée
|
||||
|
||||
```
|
||||
server/
|
||||
├── database/
|
||||
│ ├── evilspins.db # Base SQLite (créée automatiquement)
|
||||
│ ├── schema.sql # Schéma de la base
|
||||
│ └── migrate.ts # Script de migration
|
||||
├── utils/
|
||||
│ └── database.ts # Utilitaire de connexion
|
||||
└── api/
|
||||
├── boxes.ts # ✅ Nouveau (SQLite)
|
||||
├── artists.ts # ✅ Nouveau (SQLite)
|
||||
└── tracks/
|
||||
├── compilation.ts # ✅ Nouveau (SQLite avec pagination)
|
||||
└── playlist.ts # ⚠️ À adapter
|
||||
```
|
||||
|
||||
## 🔄 Côté client : utiliser la pagination
|
||||
|
||||
Exemple pour charger les tracks progressivement :
|
||||
|
||||
```typescript
|
||||
// Au lieu de charger tout d'un coup
|
||||
const { data } = await useFetch('/api/tracks/compilation')
|
||||
|
||||
// Maintenant avec pagination
|
||||
const { data } = await useFetch('/api/tracks/compilation', {
|
||||
query: {
|
||||
page: 1,
|
||||
limit: 50,
|
||||
boxId: 'ES00',
|
||||
side: 'A'
|
||||
}
|
||||
})
|
||||
|
||||
// data.tracks -> tableau de tracks
|
||||
// data.pagination -> { page, limit, total, totalPages }
|
||||
```
|
||||
|
||||
## 📊 Avantages obtenus
|
||||
|
||||
✅ **Performances** : Plus de chargement massif, pagination efficace
|
||||
✅ **Scalabilité** : Peut gérer des milliers de tracks sans ralentir
|
||||
✅ **Filtrage** : Recherche et filtres côté serveur (ultra rapide)
|
||||
✅ **Déploiement** : Un seul fichier `.db` à déployer
|
||||
|
||||
## 🔧 À faire ensuite
|
||||
|
||||
### 1. Adapter l'endpoint playlist
|
||||
|
||||
L'endpoint `tracks/playlist.ts` lit des fichiers sur disque. Options :
|
||||
|
||||
**Option A** : Scanner le dossier au démarrage et insérer dans SQLite
|
||||
**Option B** : Garder la lecture filesystem mais optimiser avec un cache
|
||||
|
||||
### 2. Modifier le frontend
|
||||
|
||||
Mettre à jour vos composants Vue pour utiliser la pagination :
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const page = ref(1)
|
||||
const { data, refresh } = await useFetch('/api/tracks/compilation', {
|
||||
query: { page, limit: 20 }
|
||||
})
|
||||
|
||||
function loadMore() {
|
||||
page.value++
|
||||
refresh()
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3. Ajouter des fonctionnalités
|
||||
|
||||
Exemples de requêtes possibles maintenant :
|
||||
|
||||
```typescript
|
||||
// Recherche par titre
|
||||
GET /api/tracks/compilation?search=love
|
||||
|
||||
// Tracks d'un artiste
|
||||
GET /api/tracks/compilation?artistId=5
|
||||
|
||||
// Tri personnalisé
|
||||
GET /api/tracks/compilation?sortBy=title&order=asc
|
||||
```
|
||||
|
||||
## 🐛 Dépannage
|
||||
|
||||
### Erreur "Cannot find module 'better-sqlite3'"
|
||||
|
||||
```bash
|
||||
pnpm add better-sqlite3
|
||||
```
|
||||
|
||||
### La base ne se crée pas
|
||||
|
||||
Vérifiez les permissions :
|
||||
|
||||
```bash
|
||||
chmod -R 755 server/database
|
||||
```
|
||||
|
||||
### Données manquantes après migration
|
||||
|
||||
Re-exécutez la migration :
|
||||
|
||||
```bash
|
||||
pnpm tsx scripts/migrate.ts
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- La base SQLite est créée automatiquement au premier lancement
|
||||
- Elle est incluse dans `.gitignore` par défaut (à ajuster selon vos besoins)
|
||||
- Pour un déploiement, commitez le fichier `.db` OU re-exécutez la migration en production
|
||||
@@ -1,151 +1,23 @@
|
||||
import { eventHandler } from 'h3'
|
||||
import { getDatabase } from '../utils/database'
|
||||
|
||||
export default eventHandler(() => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: "L'efondras",
|
||||
url: 'https://leffondras.bandcamp.com/music',
|
||||
coverId: '0024705317'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'The kundalini genie',
|
||||
url: 'https://the-kundalini-genie.bandcamp.com',
|
||||
coverId: '0012045550'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Fontaines D.C.',
|
||||
url: 'https://fontainesdc.bandcamp.com',
|
||||
coverId: '0027327090'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Fontanarosa',
|
||||
url: 'https://fontanarosa.bandcamp.com',
|
||||
coverId: '0035380235'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Johnny mafia',
|
||||
url: 'https://johnnymafia.bandcamp.com',
|
||||
coverId: '0035009392'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'New candys',
|
||||
url: 'https://newcandys.bandcamp.com',
|
||||
coverId: '0039963261'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Magic shoppe',
|
||||
url: 'https://magicshoppe.bandcamp.com',
|
||||
coverId: '0030748374'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Les jaguars',
|
||||
url: 'https://radiomartiko.bandcamp.com/album/surf-qu-b-cois',
|
||||
coverId: '0016551336'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'TRAAMS',
|
||||
url: 'https://traams.bandcamp.com',
|
||||
coverId: '0028348410'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Blue orchid',
|
||||
url: 'https://blue-orchid.bandcamp.com',
|
||||
coverId: '0034796193'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'I love UFO',
|
||||
url: 'https://bruitblanc.bandcamp.com',
|
||||
coverId: 'a2203158939'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Kid Congo & The Pink Monkey Birds',
|
||||
url: 'https://kidcongothepinkmonkeybirds.bandcamp.com/',
|
||||
coverId: '0017196290'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: 'Firefriend',
|
||||
url: 'https://firefriend.bandcamp.com/',
|
||||
coverId: '0031072203'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: 'Squid',
|
||||
url: 'https://squiduk.bandcamp.com/',
|
||||
coverId: '0037649385'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: 'Lysistrata',
|
||||
url: 'https://lysistrata.bandcamp.com/',
|
||||
coverId: '0033900158'
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: 'Pablo X Broadcasting Services',
|
||||
url: 'https://pabloxbroadcastingservices.bandcamp.com/',
|
||||
coverId: '0036956486'
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'Night Beats',
|
||||
url: 'https://nightbeats.bandcamp.com/',
|
||||
coverId: '0036987720'
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
name: 'Deltron 3030',
|
||||
url: 'https://delthefunkyhomosapien.bandcamp.com/',
|
||||
coverId: '0005254781'
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
name: 'The Amorphous Androgynous',
|
||||
url: 'https://theaa.bandcamp.com/',
|
||||
coverId: '0022226700'
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
name: 'Wooden Shjips',
|
||||
url: 'https://woodenshjips.bandcamp.com/',
|
||||
coverId: '0012406678'
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: 'Silas J. Dirge',
|
||||
url: 'https://silasjdirge.bandcamp.com/',
|
||||
coverId: '0035751570'
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Secret Colours',
|
||||
url: 'https://secretcolours.bandcamp.com/',
|
||||
coverId: '0010661379'
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: 'Larry McNeil And The Blue Knights',
|
||||
url: 'https://www.discogs.com/artist/6528940-Larry-McNeil-And-The-Blue-Knights',
|
||||
coverId:
|
||||
'https://i.discogs.com/Yr05_neEXwzPwKlDeV7dimmTG34atkAMgpxbMBhHBkI/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEyMTEw/ODE1LTE1Mjg1NjU1/NzQtMjcyOC5qcGVn.jpeg'
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Hugo Blanco',
|
||||
url: 'https://elpalmasmusic.bandcamp.com/album/color-de-tr-pico-compiled-by-el-dr-gon-criollo-el-palmas',
|
||||
coverId: '0016886708'
|
||||
}
|
||||
]
|
||||
const db = getDatabase()
|
||||
|
||||
const artists = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id, name, url, cover_id
|
||||
FROM artists
|
||||
ORDER BY id
|
||||
`
|
||||
)
|
||||
.all()
|
||||
|
||||
return artists.map((artist: any) => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
url: artist.url,
|
||||
coverId: `https://f4.bcbits.com/img/${artist.cover_id}_4.jpg`
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -1,68 +1,61 @@
|
||||
import { eventHandler } from 'h3'
|
||||
import type { Box } from '~~/types/types'
|
||||
import { getDatabase } from '../utils/database'
|
||||
|
||||
export default eventHandler<Box[]>(() => {
|
||||
return [
|
||||
{
|
||||
id: 'ES01',
|
||||
type: 'compilation',
|
||||
name: '...',
|
||||
description: '...',
|
||||
state: 'box-hidden',
|
||||
duration: 3487 + 3773, // Somme des durées A et B
|
||||
sides: {
|
||||
A: {
|
||||
name: '...',
|
||||
description: '...',
|
||||
duration: 3487,
|
||||
color1: '#c7b3aa',
|
||||
color2: '#000100'
|
||||
},
|
||||
B: {
|
||||
name: '... B',
|
||||
description: '...',
|
||||
duration: 3773,
|
||||
color1: '#f7dd01',
|
||||
color2: '#010103'
|
||||
export default eventHandler(() => {
|
||||
const db = getDatabase()
|
||||
|
||||
// Récupérer les boxes
|
||||
const boxes = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT id, type, name, description, state, duration, active_side, color1, color2
|
||||
FROM boxes
|
||||
ORDER BY id
|
||||
`
|
||||
)
|
||||
.all()
|
||||
|
||||
// Récupérer les sides pour chaque box
|
||||
const sides = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT box_id, side, name, description, duration, color1, color2
|
||||
FROM sides
|
||||
`
|
||||
)
|
||||
.all()
|
||||
|
||||
// Grouper les sides par box_id
|
||||
const sidesByBoxId: Record<string, any> = {}
|
||||
for (const side of sides) {
|
||||
if (!sidesByBoxId[side.box_id]) {
|
||||
sidesByBoxId[side.box_id] = {}
|
||||
}
|
||||
},
|
||||
activeSide: 'A'
|
||||
},
|
||||
{
|
||||
id: 'ES00',
|
||||
type: 'compilation',
|
||||
name: 'manifeste',
|
||||
description: 'Zero is for manifesto',
|
||||
state: 'box-hidden',
|
||||
duration: 2794 + 2470, // Somme des durées A et B
|
||||
sides: {
|
||||
A: {
|
||||
name: 'manifeste',
|
||||
description: 'Zero is for manifesto',
|
||||
duration: 2794,
|
||||
color1: '#ffffff',
|
||||
color2: '#48959d'
|
||||
},
|
||||
B: {
|
||||
name: 'manifeste B',
|
||||
description: 'Even Zero has a b-side',
|
||||
duration: 2470,
|
||||
color1: '#0d01b9',
|
||||
color2: '#3b7589'
|
||||
sidesByBoxId[side.box_id][side.side] = {
|
||||
name: side.name,
|
||||
description: side.description,
|
||||
duration: side.duration,
|
||||
color1: side.color1,
|
||||
color2: side.color2
|
||||
}
|
||||
},
|
||||
activeSide: 'A'
|
||||
},
|
||||
{
|
||||
id: 'ESPLAYLIST',
|
||||
type: 'playlist',
|
||||
name: 'playlists',
|
||||
duration: 0,
|
||||
description: '♠♦♣♥',
|
||||
state: 'box-hidden',
|
||||
activeSide: 'A',
|
||||
color1: '#fdec50ff',
|
||||
color2: '#fdec50ff'
|
||||
}
|
||||
]
|
||||
|
||||
// Formater les résultats
|
||||
return boxes.map((box: any) => ({
|
||||
id: box.id,
|
||||
type: box.type,
|
||||
name: box.name,
|
||||
description: box.description,
|
||||
state: box.state,
|
||||
duration: box.duration,
|
||||
activeSide: box.active_side,
|
||||
...(box.type === 'compilation'
|
||||
? {
|
||||
sides: sidesByBoxId[box.id] || {}
|
||||
}
|
||||
: {
|
||||
color1: box.color1,
|
||||
color2: box.color2
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { eventHandler } from 'h3'
|
||||
import { getCardFromDate } from '../../../utils/cards'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const dirPath = path.join(process.cwd(), '/mnt/media/files/music')
|
||||
const urlPrefix = `https://files.erudi.fr/music`
|
||||
|
||||
try {
|
||||
let allTracks: any[] = []
|
||||
|
||||
let files = await fs.promises.readdir(dirPath)
|
||||
files = files.filter((f) => !f.startsWith('.') && !f.endsWith('.jpg'))
|
||||
|
||||
const tracks = files.map((file, index) => {
|
||||
const EXT_RE = /\.(mp3|flac|wav|opus)$/i
|
||||
const nameWithoutExt = file.replace(EXT_RE, '')
|
||||
|
||||
// On split sur __
|
||||
const parts = nameWithoutExt.split('__')
|
||||
let stamp = parts[0] || ''
|
||||
let artist = parts[1] || ''
|
||||
let title = parts[2] || ''
|
||||
|
||||
title = title.replaceAll('_', ' ')
|
||||
artist = artist.replaceAll('_', ' ')
|
||||
|
||||
// Parser la date depuis le stamp
|
||||
let year = 2020,
|
||||
month = 1,
|
||||
day = 1,
|
||||
hour = 0
|
||||
if (stamp.length === 10) {
|
||||
year = Number(stamp.slice(0, 4))
|
||||
month = Number(stamp.slice(4, 6))
|
||||
day = Number(stamp.slice(6, 8))
|
||||
hour = Number(stamp.slice(8, 10))
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day, hour)
|
||||
const card = getCardFromDate(date)
|
||||
const url = `${urlPrefix}/${encodeURIComponent(file)}`
|
||||
const coverId = `${urlPrefix}/cover/${encodeURIComponent(file).replace(EXT_RE, '.jpg')}`
|
||||
|
||||
return {
|
||||
id: Number(`${year}${index + 1}`),
|
||||
boxId: `ESPLAYLIST`,
|
||||
year,
|
||||
date,
|
||||
title: title.trim(),
|
||||
artist: artist.trim(),
|
||||
url,
|
||||
coverId,
|
||||
card,
|
||||
order: 0,
|
||||
type: 'playlist'
|
||||
}
|
||||
})
|
||||
|
||||
tracks.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
// assign a stable order after sort (1..N)
|
||||
tracks.forEach((t, i) => (t.order = i + 1))
|
||||
allTracks.push(...tracks)
|
||||
|
||||
// Récupérer l'ID depuis les paramètres de la requête
|
||||
const trackId = event.context.params?.id
|
||||
|
||||
// Si un ID est fourni dans l'URL, filtrer pour ne retourner que ce morceau
|
||||
if (trackId) {
|
||||
const track = allTracks.find((track) => track.id === Number(trackId))
|
||||
return track || { error: 'Track not found' }
|
||||
}
|
||||
|
||||
return allTracks
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: (error as Error).message
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,455 +1,93 @@
|
||||
import { eventHandler } from 'h3'
|
||||
import { eventHandler, getQuery } from 'h3'
|
||||
import { getDatabase } from '../../utils/database'
|
||||
|
||||
export default eventHandler(() => {
|
||||
const tracks = [
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'The grinding wheel',
|
||||
artist: 0,
|
||||
start: 0,
|
||||
link: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
||||
coverId: 'a3236746052'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Bleach',
|
||||
artist: 1,
|
||||
start: 392,
|
||||
link: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2',
|
||||
coverId: 'a1714786533'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Televised mind',
|
||||
artist: 2,
|
||||
start: 896,
|
||||
link: 'https://fontainesdc.bandcamp.com/track/televised-mind',
|
||||
coverId: 'a3772806156'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'In it',
|
||||
artist: 3,
|
||||
start: 1139,
|
||||
link: 'https://howlinbananarecords.bandcamp.com/track/in-it',
|
||||
coverId: 'a1720372066'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Bad michel',
|
||||
artist: 4,
|
||||
start: 1245,
|
||||
link: 'https://johnnymafia.bandcamp.com/track/bad-michel-3',
|
||||
coverId: 'a0984622869'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Overall',
|
||||
artist: 5,
|
||||
start: 1394,
|
||||
link: 'https://newcandys.bandcamp.com/track/overall',
|
||||
coverId: 'a0559661270'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Blowup',
|
||||
artist: 6,
|
||||
start: 1674,
|
||||
link: 'https://magicshoppe.bandcamp.com/track/blowup',
|
||||
coverId: 'a1444895293'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Guitar jet',
|
||||
artist: 7,
|
||||
start: 1880,
|
||||
link: 'https://radiomartiko.bandcamp.com/track/guitare-jet',
|
||||
coverId: 'a1494681687'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Intercontinental radio waves',
|
||||
artist: 8,
|
||||
start: 2024,
|
||||
link: 'https://traams.bandcamp.com/track/intercontinental-radio-waves',
|
||||
coverId: 'a0046738552'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Here comes the sun',
|
||||
artist: 9,
|
||||
start: 2211,
|
||||
link: 'https://blue-orchid.bandcamp.com/track/here-come-the-sun',
|
||||
coverId: 'a4102567047'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Like in the movies',
|
||||
artist: 10,
|
||||
start: 2560,
|
||||
link: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2',
|
||||
coverId: 'a2203158939'
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: "Ce que révèle l'éclipse",
|
||||
artist: 0,
|
||||
start: 0,
|
||||
link: 'https://arakirecords.bandcamp.com/track/ce-que-r-v-le-l-clipse',
|
||||
coverId: 'a3236746052'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: "Bleedin' Gums Mushrool",
|
||||
artist: 1,
|
||||
start: 263,
|
||||
link: 'https://the-kundalini-genie.bandcamp.com/track/bleedin-gums-mushroom',
|
||||
coverId: 'a1714786533'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'A lucid dream',
|
||||
artist: 2,
|
||||
start: 554,
|
||||
link: 'https://fontainesdc.bandcamp.com/track/a-lucid-dream',
|
||||
coverId: 'a3772806156'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Lights off',
|
||||
artist: 3,
|
||||
start: 781,
|
||||
link: 'https://howlinbananarecords.bandcamp.com/track/lights-off',
|
||||
coverId: 'a1720372066'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: "I'm sentimental",
|
||||
artist: 4,
|
||||
start: 969,
|
||||
link: 'https://johnnymafia.bandcamp.com/track/im-sentimental-2',
|
||||
coverId: 'a2333676849'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Thrill or trip',
|
||||
artist: 5,
|
||||
start: 1128,
|
||||
link: 'https://newcandys.bandcamp.com/track/thrill-or-trip',
|
||||
coverId: 'a0559661270'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Redhead',
|
||||
artist: 6,
|
||||
start: 1303,
|
||||
link: 'https://magicshoppe.bandcamp.com/track/redhead',
|
||||
coverId: 'a0594426943'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Supersonic twist',
|
||||
artist: 7,
|
||||
start: 1584,
|
||||
link: 'https://open.spotify.com/track/66voQIZAJ3zD3Eju2qtNjF',
|
||||
coverId: 'a1494681687'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Flowers',
|
||||
artist: 8,
|
||||
start: 1749,
|
||||
link: 'https://traams.bandcamp.com/track/flowers',
|
||||
coverId: 'a3644668199'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'The shade',
|
||||
artist: 9,
|
||||
start: 1924,
|
||||
link: 'https://blue-orchid.bandcamp.com/track/the-shade',
|
||||
coverId: 'a0804204790'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Like in the movies',
|
||||
artist: 10,
|
||||
start: 2186,
|
||||
link: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies',
|
||||
coverId: 'a3647322740'
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'He Walked In',
|
||||
artist: 11,
|
||||
start: 0,
|
||||
link: 'https://kidcongothepinkmonkeybirds.bandcamp.com/track/he-walked-in',
|
||||
coverId: 'a0336300523'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'The Third Wave',
|
||||
artist: 12,
|
||||
start: 841,
|
||||
link: 'https://firefriend.bandcamp.com/track/the-third-wave',
|
||||
coverId: 'a2803689859'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Broadcaster',
|
||||
artist: 13,
|
||||
start: 1104.5,
|
||||
link: 'https://squiduk.bandcamp.com/track/broadcaster',
|
||||
coverId: 'a3391719769'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Mourn',
|
||||
artist: 14,
|
||||
start: 1441,
|
||||
link: 'https://lysistrata.bandcamp.com/track/mourn-2',
|
||||
coverId: 'a0872900041'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Let it Blow',
|
||||
artist: 15,
|
||||
start: 1844.8,
|
||||
link: 'https://pabloxbroadcastingservices.bandcamp.com/track/let-it-blow',
|
||||
coverId: 'a4000148031'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Sunday Mourning',
|
||||
artist: 16,
|
||||
start: 2091.7,
|
||||
link: 'https://nightbeats.bandcamp.com/track/sunday-mourning',
|
||||
coverId: 'a0031987121'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: '3030 Instrumental',
|
||||
artist: 17,
|
||||
start: 2339.3,
|
||||
link: 'https://delthefunkyhomosapien.bandcamp.com/track/3030',
|
||||
coverId: 'a1948146136'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Immortality Break',
|
||||
artist: 18,
|
||||
start: 2530.5,
|
||||
link: 'https://theaa.bandcamp.com/track/immortality-break',
|
||||
coverId: 'a2749250329'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Lazy Bones',
|
||||
artist: 19,
|
||||
start: 2718,
|
||||
link: 'https://woodenshjips.bandcamp.com/track/lazy-bones',
|
||||
coverId: 'a1884221104'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'On the Train of Aches',
|
||||
artist: 20,
|
||||
start: 2948,
|
||||
link: 'https://silasjdirge.bandcamp.com/track/on-the-train-of-aches',
|
||||
coverId: 'a1124177379'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Me',
|
||||
artist: 21,
|
||||
start: 3265,
|
||||
link: 'https://secretcolours.bandcamp.com/track/me',
|
||||
coverId: 'a1497022499'
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Lady Hawke Blues',
|
||||
artist: 11,
|
||||
start: 0,
|
||||
link: 'https://kidcongothepinkmonkeybirds.bandcamp.com/track/lady-hawke-blues',
|
||||
coverId: 'a2532623230'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Dreamscapes',
|
||||
artist: 12,
|
||||
start: 235,
|
||||
link: 'https://littlecloudrecords.bandcamp.com/track/dreamscapes',
|
||||
coverId: 'a3498981203'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Crispy Skin',
|
||||
artist: 13,
|
||||
start: 644.2,
|
||||
link: 'https://squiduk.bandcamp.com/track/crispy-skin-2',
|
||||
coverId: 'a2516727021'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'The Boy Who Stood Above The Earth',
|
||||
artist: 14,
|
||||
start: 1018,
|
||||
link: 'https://lysistrata.bandcamp.com/track/the-boy-who-stood-above-the-earth-2',
|
||||
coverId: 'a0350933426'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Better Off Alone',
|
||||
artist: 15,
|
||||
start: 1698,
|
||||
link: 'https://pabloxbroadcastingservices.bandcamp.com/track/better-off-alone',
|
||||
coverId: 'a4000148031'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Celebration #1',
|
||||
artist: 16,
|
||||
start: 2235,
|
||||
link: 'https://nightbeats.bandcamp.com/track/celebration-1',
|
||||
coverId: 'a0031987121'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: '3030 Instrumental',
|
||||
artist: 17,
|
||||
start: 2458.3,
|
||||
link: 'https://delthefunkyhomosapien.bandcamp.com/track/3030',
|
||||
coverId: 'a1948146136'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'The Emptiness Of Nothingness',
|
||||
artist: 18,
|
||||
start: 2864.5,
|
||||
link: 'https://theaa.bandcamp.com/track/the-emptiness-of-nothingness',
|
||||
coverId: 'a1053923875'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Rising',
|
||||
artist: 19,
|
||||
start: 3145,
|
||||
link: 'https://woodenshjips.bandcamp.com/track/rising',
|
||||
coverId: 'a1884221104'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'The Last Time',
|
||||
artist: 22,
|
||||
start: 3447,
|
||||
link: 'https://www.discogs.com/release/12110815-Larry-McNeil-And-The-Blue-Knights-Jealous-Woman',
|
||||
coverId:
|
||||
'https://i.discogs.com/Yr05_neEXwzPwKlDeV7dimmTG34atkAMgpxbMBhHBkI/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEyMTEw/ODE1LTE1Mjg1NjU1/NzQtMjcyOC5qcGVn.jpeg'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Guajira Con Arpa',
|
||||
artist: 23,
|
||||
start: 3586,
|
||||
link: 'https://elpalmasmusic.bandcamp.com/track/guajira-con-arpa',
|
||||
coverId: 'a3463036407'
|
||||
export default eventHandler((event) => {
|
||||
const db = getDatabase()
|
||||
const query = getQuery(event)
|
||||
|
||||
// Paramètres de pagination
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = parseInt(query.limit as string) || 50
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
// Filtres optionnels
|
||||
const boxId = query.boxId as string | undefined
|
||||
const side = query.side as string | undefined
|
||||
|
||||
// Construction de la requête
|
||||
let sql = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.box_id,
|
||||
t.side,
|
||||
t.track_order,
|
||||
t.title,
|
||||
t.artist_id,
|
||||
t.start,
|
||||
t.link,
|
||||
t.cover_id,
|
||||
t.url,
|
||||
t.type,
|
||||
a.name as artist_name
|
||||
FROM tracks t
|
||||
LEFT JOIN artists a ON t.artist_id = a.id
|
||||
WHERE t.type = 'compilation'
|
||||
`
|
||||
|
||||
const params: any[] = []
|
||||
|
||||
if (boxId) {
|
||||
sql += ' AND t.box_id = ?'
|
||||
params.push(boxId)
|
||||
}
|
||||
]
|
||||
|
||||
return tracks.map((track, index) => ({
|
||||
id: index + 1,
|
||||
...track,
|
||||
url: `https://files.erudi.fr/evilspins/${track.boxId}${track.side}.mp3`,
|
||||
coverId: `https://f4.bcbits.com/img/${track.coverId}_4.jpg`,
|
||||
type: 'compilation'
|
||||
}))
|
||||
if (side) {
|
||||
sql += ' AND t.side = ?'
|
||||
params.push(side)
|
||||
}
|
||||
|
||||
sql += ' ORDER BY t.box_id, t.side, t.track_order'
|
||||
sql += ' LIMIT ? OFFSET ?'
|
||||
params.push(limit, offset)
|
||||
|
||||
const tracks = db.prepare(sql).all(...params)
|
||||
|
||||
// Compter le total pour la pagination
|
||||
let countSql = "SELECT COUNT(*) as total FROM tracks WHERE type = 'compilation'"
|
||||
const countParams: any[] = []
|
||||
|
||||
if (boxId) {
|
||||
countSql += ' AND box_id = ?'
|
||||
countParams.push(boxId)
|
||||
}
|
||||
|
||||
if (side) {
|
||||
countSql += ' AND side = ?'
|
||||
countParams.push(side)
|
||||
}
|
||||
|
||||
const { total } = db.prepare(countSql).get(...countParams) as { total: number }
|
||||
|
||||
return {
|
||||
tracks: tracks.map((track: any) => ({
|
||||
id: track.id,
|
||||
boxId: track.box_id,
|
||||
side: track.side,
|
||||
order: track.track_order,
|
||||
title: track.title,
|
||||
artist: track.artist_id,
|
||||
artistName: track.artist_name,
|
||||
start: track.start,
|
||||
link: track.link,
|
||||
coverId: track.cover_id,
|
||||
url: track.url,
|
||||
type: track.type
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,8 +10,25 @@ export default eventHandler(async (event) => {
|
||||
try {
|
||||
let allTracks: any[] = []
|
||||
|
||||
let files = await fs.promises.readdir(dirPath)
|
||||
files = files.filter((f) => !f.startsWith('.') && !f.endsWith('.jpg'))
|
||||
const items = await fs.promises.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
// Process files
|
||||
const files = items
|
||||
.filter((item) => item.isFile() && !item.name.startsWith('.') && !item.name.endsWith('.jpg'))
|
||||
.map((item) => item.name)
|
||||
|
||||
// Process folders
|
||||
const folders = items
|
||||
.filter((item) => item.isDirectory() && !item.name.startsWith('.'))
|
||||
.map((folder, index) => ({
|
||||
id: `folder-${index}`,
|
||||
boxId: 'ESFOLDER',
|
||||
title: folder.name.replace(/_/g, ' ').trim(),
|
||||
type: 'folder',
|
||||
order: 0,
|
||||
date: new Date(),
|
||||
card: getCardFromDate(new Date())
|
||||
}))
|
||||
|
||||
const tracks = files.map((file, index) => {
|
||||
const EXT_RE = /\.(mp3|flac|wav|opus)$/i
|
||||
@@ -41,7 +58,7 @@ export default eventHandler(async (event) => {
|
||||
const date = new Date(year, month - 1, day, hour)
|
||||
const card = getCardFromDate(date)
|
||||
const url = `${urlPrefix}/${encodeURIComponent(file)}`
|
||||
const coverId = `${urlPrefix}/cover/${encodeURIComponent(file).replace(EXT_RE, '.jpg')}`
|
||||
const coverId = `${urlPrefix}/${encodeURIComponent(file).replace(EXT_RE, '.jpg')}`
|
||||
|
||||
return {
|
||||
id: Number(`${year}${index + 1}`),
|
||||
@@ -59,11 +76,14 @@ export default eventHandler(async (event) => {
|
||||
})
|
||||
|
||||
tracks.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
// assign a stable order after sort (1..N)
|
||||
tracks.forEach((t, i) => (t.order = i + 1))
|
||||
allTracks.push(...tracks)
|
||||
// Combine folders and tracks
|
||||
const allItems = [...folders, ...tracks]
|
||||
|
||||
return allTracks
|
||||
// Sort by date (newest first) and assign order
|
||||
allItems.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
allItems.forEach((item, i) => (item.order = i + 1))
|
||||
|
||||
return allItems
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
91
server/api/tracks/search.ts
Normal file
91
server/api/tracks/search.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { eventHandler, getQuery } from 'h3'
|
||||
import { getDatabase } from '../../utils/database'
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const db = getDatabase()
|
||||
const query = getQuery(event)
|
||||
|
||||
const search = (query.search as string) || ''
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = parseInt(query.limit as string) || 50
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
// Construction de la requête de recherche
|
||||
let sql = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.box_id,
|
||||
t.side,
|
||||
t.track_order,
|
||||
t.title,
|
||||
t.artist_id,
|
||||
t.start,
|
||||
t.link,
|
||||
t.cover_id,
|
||||
t.url,
|
||||
t.type,
|
||||
a.name as artist_name,
|
||||
a.url as artist_url
|
||||
FROM tracks t
|
||||
LEFT JOIN artists a ON t.artist_id = a.id
|
||||
WHERE 1=1
|
||||
`
|
||||
|
||||
const params: any[] = []
|
||||
|
||||
// Recherche par titre ou artiste
|
||||
if (search) {
|
||||
sql += ` AND (t.title LIKE ? OR a.name LIKE ?)`
|
||||
const searchPattern = `%${search}%`
|
||||
params.push(searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
sql += ' ORDER BY t.box_id, t.track_order'
|
||||
sql += ' LIMIT ? OFFSET ?'
|
||||
params.push(limit, offset)
|
||||
|
||||
const tracks = db.prepare(sql).all(...params)
|
||||
|
||||
// Compter le total
|
||||
let countSql = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM tracks t
|
||||
LEFT JOIN artists a ON t.artist_id = a.id
|
||||
WHERE 1=1
|
||||
`
|
||||
|
||||
const countParams: any[] = []
|
||||
|
||||
if (search) {
|
||||
countSql += ` AND (t.title LIKE ? OR a.name LIKE ?)`
|
||||
const searchPattern = `%${search}%`
|
||||
countParams.push(searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
const { total } = db.prepare(countSql).get(...countParams) as { total: number }
|
||||
|
||||
return {
|
||||
tracks: tracks.map((track: any) => ({
|
||||
id: track.id,
|
||||
boxId: track.box_id,
|
||||
side: track.side,
|
||||
order: track.track_order,
|
||||
title: track.title,
|
||||
artist: track.artist_id,
|
||||
artistName: track.artist_name,
|
||||
artistUrl: track.artist_url,
|
||||
start: track.start,
|
||||
link: track.link,
|
||||
coverId: track.cover_id,
|
||||
url: track.url,
|
||||
type: track.type
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
},
|
||||
search
|
||||
}
|
||||
})
|
||||
BIN
server/database/evilspins.db
Normal file
BIN
server/database/evilspins.db
Normal file
Binary file not shown.
678
server/database/migrate.ts
Normal file
678
server/database/migrate.ts
Normal file
@@ -0,0 +1,678 @@
|
||||
import { getDatabase } from '../utils/database'
|
||||
|
||||
// Import des données depuis vos anciens fichiers
|
||||
const boxes = [
|
||||
{
|
||||
id: 'ES01',
|
||||
type: 'compilation',
|
||||
name: '...',
|
||||
description: '...',
|
||||
state: 'box-hidden',
|
||||
duration: 3487 + 3773,
|
||||
sides: {
|
||||
A: {
|
||||
name: '...',
|
||||
description: '...',
|
||||
duration: 3487,
|
||||
color1: '#c7b3aa',
|
||||
color2: '#000100'
|
||||
},
|
||||
B: {
|
||||
name: '... B',
|
||||
description: '...',
|
||||
duration: 3773,
|
||||
color1: '#f7dd01',
|
||||
color2: '#010103'
|
||||
}
|
||||
},
|
||||
activeSide: 'A'
|
||||
},
|
||||
{
|
||||
id: 'ES00',
|
||||
type: 'compilation',
|
||||
name: 'manifeste',
|
||||
description: 'Zero is for manifesto',
|
||||
state: 'box-hidden',
|
||||
duration: 2794 + 2470,
|
||||
sides: {
|
||||
A: {
|
||||
name: 'manifeste',
|
||||
description: 'Zero is for manifesto',
|
||||
duration: 2794,
|
||||
color1: '#ffffff',
|
||||
color2: '#48959d'
|
||||
},
|
||||
B: {
|
||||
name: 'manifeste B',
|
||||
description: 'Even Zero has a b-side',
|
||||
duration: 2470,
|
||||
color1: '#0d01b9',
|
||||
color2: '#3b7589'
|
||||
}
|
||||
},
|
||||
activeSide: 'A'
|
||||
},
|
||||
{
|
||||
id: 'ESPLAYLIST',
|
||||
type: 'playlist',
|
||||
name: 'playlists',
|
||||
duration: 0,
|
||||
description: '♠♦♣♥',
|
||||
state: 'box-hidden',
|
||||
activeSide: 'A',
|
||||
color1: '#fdec50ff',
|
||||
color2: '#fdec50ff'
|
||||
}
|
||||
]
|
||||
|
||||
const artists = [
|
||||
{ id: 0, name: "L'efondras", url: 'https://leffondras.bandcamp.com/music', coverId: '0024705317' },
|
||||
{
|
||||
id: 1,
|
||||
name: 'The kundalini genie',
|
||||
url: 'https://the-kundalini-genie.bandcamp.com',
|
||||
coverId: '0012045550'
|
||||
},
|
||||
{ id: 2, name: 'Fontaines D.C.', url: 'https://fontainesdc.bandcamp.com', coverId: '0027327090' },
|
||||
{ id: 3, name: 'Fontanarosa', url: 'https://fontanarosa.bandcamp.com', coverId: '0035380235' },
|
||||
{ id: 4, name: 'Johnny mafia', url: 'https://johnnymafia.bandcamp.com', coverId: '0035009392' },
|
||||
{ id: 5, name: 'New candys', url: 'https://newcandys.bandcamp.com', coverId: '0039963261' },
|
||||
{ id: 6, name: 'Magic shoppe', url: 'https://magicshoppe.bandcamp.com', coverId: '0030748374' },
|
||||
{
|
||||
id: 7,
|
||||
name: 'Les jaguars',
|
||||
url: 'https://radiomartiko.bandcamp.com/album/surf-qu-b-cois',
|
||||
coverId: '0016551336'
|
||||
},
|
||||
{ id: 8, name: 'TRAAMS', url: 'https://traams.bandcamp.com', coverId: '0028348410' },
|
||||
{ id: 9, name: 'Blue orchid', url: 'https://blue-orchid.bandcamp.com', coverId: '0034796193' },
|
||||
{ id: 10, name: 'I love UFO', url: 'https://bruitblanc.bandcamp.com', coverId: 'a2203158939' },
|
||||
{
|
||||
id: 11,
|
||||
name: 'Kid Congo & The Pink Monkey Birds',
|
||||
url: 'https://kidcongothepinkmonkeybirds.bandcamp.com/',
|
||||
coverId: '0017196290'
|
||||
},
|
||||
{ id: 12, name: 'Firefriend', url: 'https://firefriend.bandcamp.com/', coverId: '0031072203' },
|
||||
{ id: 13, name: 'Squid', url: 'https://squiduk.bandcamp.com/', coverId: '0037649385' },
|
||||
{ id: 14, name: 'Lysistrata', url: 'https://lysistrata.bandcamp.com/', coverId: '0033900158' },
|
||||
{
|
||||
id: 15,
|
||||
name: 'Pablo X Broadcasting Services',
|
||||
url: 'https://pabloxbroadcastingservices.bandcamp.com/',
|
||||
coverId: '0036956486'
|
||||
},
|
||||
{ id: 16, name: 'Night Beats', url: 'https://nightbeats.bandcamp.com/', coverId: '0036987720' },
|
||||
{ id: 17, name: 'Deltron 3030', url: 'https://delthefunkyhomosapien.bandcamp.com/', coverId: '0005254781' },
|
||||
{
|
||||
id: 18,
|
||||
name: 'The Amorphous Androgynous',
|
||||
url: 'https://theaa.bandcamp.com/',
|
||||
coverId: '0022226700'
|
||||
},
|
||||
{ id: 19, name: 'Wooden Shjips', url: 'https://woodenshjips.bandcamp.com/', coverId: '0012406678' },
|
||||
{ id: 20, name: 'Silas J. Dirge', url: 'https://silasjdirge.bandcamp.com/', coverId: '0035751570' },
|
||||
{ id: 21, name: 'Secret Colours', url: 'https://secretcolours.bandcamp.com/', coverId: '0010661379' },
|
||||
{
|
||||
id: 22,
|
||||
name: 'Larry McNeil And The Blue Knights',
|
||||
url: 'https://www.discogs.com/artist/6528940-Larry-McNeil-And-The-Blue-Knights',
|
||||
coverId:
|
||||
'https://i.discogs.com/Yr05_neEXwzPwKlDeV7dimmTG34atkAMgpxbMBhHBkI/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEyMTEw/ODE1LTE1Mjg1NjU1/NzQtMjcyOC5qcGVn.jpeg'
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: 'Hugo Blanco',
|
||||
url: 'https://elpalmasmusic.bandcamp.com/album/color-de-tr-pico-compiled-by-el-dr-gon-criollo-el-palmas',
|
||||
coverId: '0016886708'
|
||||
}
|
||||
]
|
||||
|
||||
const tracks = [
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'The grinding wheel',
|
||||
artist: 0,
|
||||
start: 0,
|
||||
link: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
|
||||
coverId: 'a3236746052'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Bleach',
|
||||
artist: 1,
|
||||
start: 392,
|
||||
link: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2',
|
||||
coverId: 'a1714786533'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Televised mind',
|
||||
artist: 2,
|
||||
start: 896,
|
||||
link: 'https://fontainesdc.bandcamp.com/track/televised-mind',
|
||||
coverId: 'a3772806156'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'In it',
|
||||
artist: 3,
|
||||
start: 1139,
|
||||
link: 'https://howlinbananarecords.bandcamp.com/track/in-it',
|
||||
coverId: 'a1720372066'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Bad michel',
|
||||
artist: 4,
|
||||
start: 1245,
|
||||
link: 'https://johnnymafia.bandcamp.com/track/bad-michel-3',
|
||||
coverId: 'a0984622869'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Overall',
|
||||
artist: 5,
|
||||
start: 1394,
|
||||
link: 'https://newcandys.bandcamp.com/track/overall',
|
||||
coverId: 'a0559661270'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Blowup',
|
||||
artist: 6,
|
||||
start: 1674,
|
||||
link: 'https://magicshoppe.bandcamp.com/track/blowup',
|
||||
coverId: 'a1444895293'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Guitar jet',
|
||||
artist: 7,
|
||||
start: 1880,
|
||||
link: 'https://radiomartiko.bandcamp.com/track/guitare-jet',
|
||||
coverId: 'a1494681687'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Intercontinental radio waves',
|
||||
artist: 8,
|
||||
start: 2024,
|
||||
link: 'https://traams.bandcamp.com/track/intercontinental-radio-waves',
|
||||
coverId: 'a0046738552'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Here comes the sun',
|
||||
artist: 9,
|
||||
start: 2211,
|
||||
link: 'https://blue-orchid.bandcamp.com/track/here-come-the-sun',
|
||||
coverId: 'a4102567047'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES00',
|
||||
side: 'A',
|
||||
title: 'Like in the movies',
|
||||
artist: 10,
|
||||
start: 2560,
|
||||
link: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2',
|
||||
coverId: 'a2203158939'
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: "Ce que révèle l'éclipse",
|
||||
artist: 0,
|
||||
start: 0,
|
||||
link: 'https://arakirecords.bandcamp.com/track/ce-que-r-v-le-l-clipse',
|
||||
coverId: 'a3236746052'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: "Bleedin' Gums Mushrool",
|
||||
artist: 1,
|
||||
start: 263,
|
||||
link: 'https://the-kundalini-genie.bandcamp.com/track/bleedin-gums-mushroom',
|
||||
coverId: 'a1714786533'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'A lucid dream',
|
||||
artist: 2,
|
||||
start: 554,
|
||||
link: 'https://fontainesdc.bandcamp.com/track/a-lucid-dream',
|
||||
coverId: 'a3772806156'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Lights off',
|
||||
artist: 3,
|
||||
start: 781,
|
||||
link: 'https://howlinbananarecords.bandcamp.com/track/lights-off',
|
||||
coverId: 'a1720372066'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: "I'm sentimental",
|
||||
artist: 4,
|
||||
start: 969,
|
||||
link: 'https://johnnymafia.bandcamp.com/track/im-sentimental-2',
|
||||
coverId: 'a2333676849'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Thrill or trip',
|
||||
artist: 5,
|
||||
start: 1128,
|
||||
link: 'https://newcandys.bandcamp.com/track/thrill-or-trip',
|
||||
coverId: 'a0559661270'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Redhead',
|
||||
artist: 6,
|
||||
start: 1303,
|
||||
link: 'https://magicshoppe.bandcamp.com/track/redhead',
|
||||
coverId: 'a0594426943'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Supersonic twist',
|
||||
artist: 7,
|
||||
start: 1584,
|
||||
link: 'https://open.spotify.com/track/66voQIZAJ3zD3Eju2qtNjF',
|
||||
coverId: 'a1494681687'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Flowers',
|
||||
artist: 8,
|
||||
start: 1749,
|
||||
link: 'https://traams.bandcamp.com/track/flowers',
|
||||
coverId: 'a3644668199'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'The shade',
|
||||
artist: 9,
|
||||
start: 1924,
|
||||
link: 'https://blue-orchid.bandcamp.com/track/the-shade',
|
||||
coverId: 'a0804204790'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES00',
|
||||
side: 'B',
|
||||
title: 'Like in the movies',
|
||||
artist: 10,
|
||||
start: 2186,
|
||||
link: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies',
|
||||
coverId: 'a3647322740'
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'He Walked In',
|
||||
artist: 11,
|
||||
start: 0,
|
||||
link: 'https://kidcongothepinkmonkeybirds.bandcamp.com/track/he-walked-in',
|
||||
coverId: 'a0336300523'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'The Third Wave',
|
||||
artist: 12,
|
||||
start: 841,
|
||||
link: 'https://firefriend.bandcamp.com/track/the-third-wave',
|
||||
coverId: 'a2803689859'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Broadcaster',
|
||||
artist: 13,
|
||||
start: 1104.5,
|
||||
link: 'https://squiduk.bandcamp.com/track/broadcaster',
|
||||
coverId: 'a3391719769'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Mourn',
|
||||
artist: 14,
|
||||
start: 1441,
|
||||
link: 'https://lysistrata.bandcamp.com/track/mourn-2',
|
||||
coverId: 'a0872900041'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Let it Blow',
|
||||
artist: 15,
|
||||
start: 1844.8,
|
||||
link: 'https://pabloxbroadcastingservices.bandcamp.com/track/let-it-blow',
|
||||
coverId: 'a4000148031'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Sunday Mourning',
|
||||
artist: 16,
|
||||
start: 2091.7,
|
||||
link: 'https://nightbeats.bandcamp.com/track/sunday-mourning',
|
||||
coverId: 'a0031987121'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: '3030 Instrumental',
|
||||
artist: 17,
|
||||
start: 2339.3,
|
||||
link: 'https://delthefunkyhomosapien.bandcamp.com/track/3030',
|
||||
coverId: 'a1948146136'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Immortality Break',
|
||||
artist: 18,
|
||||
start: 2530.5,
|
||||
link: 'https://theaa.bandcamp.com/track/immortality-break',
|
||||
coverId: 'a2749250329'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Lazy Bones',
|
||||
artist: 19,
|
||||
start: 2718,
|
||||
link: 'https://woodenshjips.bandcamp.com/track/lazy-bones',
|
||||
coverId: 'a1884221104'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'On the Train of Aches',
|
||||
artist: 20,
|
||||
start: 2948,
|
||||
link: 'https://silasjdirge.bandcamp.com/track/on-the-train-of-aches',
|
||||
coverId: 'a1124177379'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES01',
|
||||
side: 'A',
|
||||
title: 'Me',
|
||||
artist: 21,
|
||||
start: 3265,
|
||||
link: 'https://secretcolours.bandcamp.com/track/me',
|
||||
coverId: 'a1497022499'
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Lady Hawke Blues',
|
||||
artist: 11,
|
||||
start: 0,
|
||||
link: 'https://kidcongothepinkmonkeybirds.bandcamp.com/track/lady-hawke-blues',
|
||||
coverId: 'a2532623230'
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Dreamscapes',
|
||||
artist: 12,
|
||||
start: 235,
|
||||
link: 'https://littlecloudrecords.bandcamp.com/track/dreamscapes',
|
||||
coverId: 'a3498981203'
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Crispy Skin',
|
||||
artist: 13,
|
||||
start: 644.2,
|
||||
link: 'https://squiduk.bandcamp.com/track/crispy-skin-2',
|
||||
coverId: 'a2516727021'
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'The Boy Who Stood Above The Earth',
|
||||
artist: 14,
|
||||
start: 1018,
|
||||
link: 'https://lysistrata.bandcamp.com/track/the-boy-who-stood-above-the-earth-2',
|
||||
coverId: 'a0350933426'
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Better Off Alone',
|
||||
artist: 15,
|
||||
start: 1698,
|
||||
link: 'https://pabloxbroadcastingservices.bandcamp.com/track/better-off-alone',
|
||||
coverId: 'a4000148031'
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Celebration #1',
|
||||
artist: 16,
|
||||
start: 2235,
|
||||
link: 'https://nightbeats.bandcamp.com/track/celebration-1',
|
||||
coverId: 'a0031987121'
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: '3030 Instrumental',
|
||||
artist: 17,
|
||||
start: 2458.3,
|
||||
link: 'https://delthefunkyhomosapien.bandcamp.com/track/3030',
|
||||
coverId: 'a1948146136'
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'The Emptiness Of Nothingness',
|
||||
artist: 18,
|
||||
start: 2864.5,
|
||||
link: 'https://theaa.bandcamp.com/track/the-emptiness-of-nothingness',
|
||||
coverId: 'a1053923875'
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Rising',
|
||||
artist: 19,
|
||||
start: 3145,
|
||||
link: 'https://woodenshjips.bandcamp.com/track/rising',
|
||||
coverId: 'a1884221104'
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'The Last Time',
|
||||
artist: 22,
|
||||
start: 3447,
|
||||
link: 'https://www.discogs.com/release/12110815-Larry-McNeil-And-The-Blue-Knights-Jealous-Woman',
|
||||
coverId:
|
||||
'https://i.discogs.com/Yr05_neEXwzPwKlDeV7dimmTG34atkAMgpxbMBhHBkI/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTEyMTEw/ODE1LTE1Mjg1NjU1/NzQtMjcyOC5qcGVn.jpeg'
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
boxId: 'ES01',
|
||||
side: 'B',
|
||||
title: 'Guajira Con Arpa',
|
||||
artist: 23,
|
||||
start: 3586,
|
||||
link: 'https://elpalmasmusic.bandcamp.com/track/guajira-con-arpa',
|
||||
coverId: 'a3463036407'
|
||||
}
|
||||
]
|
||||
|
||||
export async function migrate() {
|
||||
console.log('🚀 Début de la migration...')
|
||||
|
||||
const db = getDatabase()
|
||||
|
||||
// Vider les tables existantes
|
||||
db.exec('DELETE FROM tracks')
|
||||
db.exec('DELETE FROM sides')
|
||||
db.exec('DELETE FROM artists')
|
||||
db.exec('DELETE FROM boxes')
|
||||
|
||||
console.log('🗑️ Tables vidées')
|
||||
|
||||
// Insérer les boxes
|
||||
const insertBox = db.prepare(`
|
||||
INSERT INTO boxes (id, type, name, description, state, duration, active_side, color1, color2)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
const insertSide = db.prepare(`
|
||||
INSERT INTO sides (box_id, side, name, description, duration, color1, color2)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
for (const box of boxes) {
|
||||
insertBox.run(
|
||||
box.id,
|
||||
box.type,
|
||||
box.name,
|
||||
box.description,
|
||||
box.state,
|
||||
box.duration,
|
||||
box.activeSide,
|
||||
box.color1 || null,
|
||||
box.color2 || null
|
||||
)
|
||||
|
||||
// Insérer les sides si c'est une compilation
|
||||
if (box.sides) {
|
||||
for (const [sideName, sideData] of Object.entries(box.sides)) {
|
||||
insertSide.run(
|
||||
box.id,
|
||||
sideName,
|
||||
sideData.name,
|
||||
sideData.description,
|
||||
sideData.duration,
|
||||
sideData.color1,
|
||||
sideData.color2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ ${boxes.length} boxes insérées`)
|
||||
|
||||
// Insérer les artists
|
||||
const insertArtist = db.prepare(`
|
||||
INSERT INTO artists (id, name, url, cover_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
for (const artist of artists) {
|
||||
insertArtist.run(artist.id, artist.name, artist.url, artist.coverId)
|
||||
}
|
||||
|
||||
console.log(`✅ ${artists.length} artistes insérés`)
|
||||
|
||||
// Insérer les tracks
|
||||
const insertTrack = db.prepare(`
|
||||
INSERT INTO tracks (box_id, side, track_order, title, artist_id, start, link, cover_id, url, type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
for (const track of tracks) {
|
||||
const url = `https://files.erudi.fr/evilspins/${track.boxId}${track.side}.mp3`
|
||||
const coverId = `https://f4.bcbits.com/img/${track.coverId}_4.jpg`
|
||||
|
||||
insertTrack.run(
|
||||
track.boxId,
|
||||
track.side,
|
||||
track.order,
|
||||
track.title,
|
||||
track.artist,
|
||||
track.start,
|
||||
track.link,
|
||||
coverId,
|
||||
url,
|
||||
'compilation'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`✅ ${tracks.length} tracks insérées`)
|
||||
console.log('🎉 Migration terminée avec succès !')
|
||||
}
|
||||
|
||||
// Exécuter la migration si appelé directement
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
migrate()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => {
|
||||
console.error('❌ Erreur lors de la migration:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
63
server/database/schema.sql
Normal file
63
server/database/schema.sql
Normal file
@@ -0,0 +1,63 @@
|
||||
-- Boxes table
|
||||
CREATE TABLE IF NOT EXISTS boxes (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
state TEXT,
|
||||
duration INTEGER,
|
||||
active_side TEXT,
|
||||
color1 TEXT,
|
||||
color2 TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sides table (pour les compilations qui ont A et B)
|
||||
CREATE TABLE IF NOT EXISTS sides (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
box_id TEXT NOT NULL,
|
||||
side TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
duration INTEGER,
|
||||
color1 TEXT,
|
||||
color2 TEXT,
|
||||
FOREIGN KEY (box_id) REFERENCES boxes(id) ON DELETE CASCADE,
|
||||
UNIQUE(box_id, side)
|
||||
);
|
||||
|
||||
-- Artists table
|
||||
CREATE TABLE IF NOT EXISTS artists (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT,
|
||||
cover_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Tracks table
|
||||
CREATE TABLE IF NOT EXISTS tracks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
box_id TEXT NOT NULL,
|
||||
side TEXT,
|
||||
track_order INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
artist_id INTEGER,
|
||||
start INTEGER,
|
||||
link TEXT,
|
||||
cover_id TEXT,
|
||||
url TEXT,
|
||||
type TEXT,
|
||||
year INTEGER,
|
||||
date DATETIME,
|
||||
card TEXT,
|
||||
FOREIGN KEY (box_id) REFERENCES boxes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (artist_id) REFERENCES artists(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Index pour les requêtes fréquentes
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_box_id ON tracks(box_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_artist_id ON tracks(artist_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_type ON tracks(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_year ON tracks(year);
|
||||
CREATE INDEX IF NOT EXISTS idx_sides_box_id ON sides(box_id);
|
||||
163
server/services/playlist-sync.service.ts
Normal file
163
server/services/playlist-sync.service.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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)
|
||||
82
server/types/database.ts
Normal file
82
server/types/database.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// Types pour les tables de la base de données
|
||||
|
||||
export interface Box {
|
||||
id: string
|
||||
type: 'compilation' | 'playlist'
|
||||
name: string
|
||||
description?: string
|
||||
state?: string
|
||||
duration: number
|
||||
activeSide?: string
|
||||
color1?: string
|
||||
color2?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface Side {
|
||||
id: number
|
||||
boxId: string
|
||||
side: string
|
||||
name?: string
|
||||
description?: string
|
||||
duration: number
|
||||
color1?: string
|
||||
color2?: string
|
||||
}
|
||||
|
||||
export interface Artist {
|
||||
id: number
|
||||
name: string
|
||||
url?: string
|
||||
coverId?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface Track {
|
||||
id: number
|
||||
boxId: string
|
||||
side?: string
|
||||
trackOrder: number
|
||||
title: string
|
||||
artistId?: number
|
||||
start: number
|
||||
link?: string
|
||||
coverId?: string
|
||||
url?: string
|
||||
type: 'compilation' | 'playlist'
|
||||
year?: number
|
||||
date?: string
|
||||
card?: string
|
||||
}
|
||||
|
||||
// Types pour les réponses API
|
||||
|
||||
export interface BoxWithSides extends Box {
|
||||
sides?: Record<string, Omit<Side, 'id' | 'boxId'>>
|
||||
}
|
||||
|
||||
export interface TrackWithArtist extends Track {
|
||||
artistName?: string
|
||||
artistUrl?: string
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[]
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface TracksSearchResponse {
|
||||
tracks: TrackWithArtist[]
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
totalPages: number
|
||||
}
|
||||
search?: string
|
||||
}
|
||||
63
server/utils/database.ts
Normal file
63
server/utils/database.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user