Compare commits

...

10 Commits

Author SHA1 Message Date
valere
7a1cdf2178 install atropos (demo on /wait)
All checks were successful
Deploy App / deploy (push) Successful in 1m49s
2025-08-30 10:26:32 +02:00
valere
d013e62fcf button play + black index 2025-03-22 19:14:15 +01:00
valere
9c1204b46b change bkg (need update home !) 2025-03-16 09:07:59 +01:00
valere
8541050011 style: playlsits 2024-12-30 12:30:39 +01:00
valere
480e8f008c TEST-CI: wording test 2024-11-15 11:04:29 +01:00
valere
7f8eb7bbb9 FIX: umami script 2024-11-09 12:50:46 +01:00
valere
7e9c0d3caf FEAT: add umami tracking script 2024-11-06 12:52:26 +01:00
valere
f972137389 FIX: scroll only screen 2024-11-03 17:27:54 +01:00
valere
7f8ed0e8a0 FEAT: playlists player v1 2024-11-01 13:15:23 +01:00
valere
0ca4cc3bfe FEAT: api for playlists v1 2024-10-28 13:59:38 +01:00
55 changed files with 10760 additions and 9477 deletions

View File

@@ -1,16 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: deploy
image: docker:dind
commands:
- apk add --upgrade npm bash findutils rsync sed
- WORKDIR="/var/docker-web/apps/$DRONE_REPO_NAME"
- rm -rf $WORKDIR
- mkdir $WORKDIR
- rsync -av --exclude ./node_modules /drone/src/ $WORKDIR
- cd $WORKDIR
- npm ci
- bash /var/docker-web/src/cli.sh up $DRONE_REPO_NAME

3
.env
View File

@@ -1,3 +0,0 @@
DOMAIN=evilspins.com
PORT=7783
DASHBOARD_HIDDEN=false

21
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Deploy App
on: [push]
jobs:
deploy:
runs-on: ubuntu-22.04
container:
volumes:
- /var/docker-web:/var/docker-web
steps:
- uses: actions/checkout@v4
- name: install
run: |
APP_DIR=/var/docker-web/apps/${GITHUB_REPOSITORY##*/}
mkdir -p $APP_DIR
cp -a $(find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'node_modules') "$APP_DIR/"
- name: up
run: |
export COMPOSE_BAKE=false
bash /var/docker-web/src/cli.sh up ${GITHUB_REPOSITORY##*/}

5
.gitignore vendored
View File

@@ -17,3 +17,8 @@ logs
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

2
.nvmrc
View File

@@ -1 +1 @@
18.20.2
20

View File

@@ -1,19 +1,30 @@
# INSTALL
FROM node:18-alpine as builder
# Stage de build
FROM node:20-alpine AS build
WORKDIR /app
# Installer pnpm
RUN npm install -g pnpm
# Copier package.json et lockfile pour cache pnpm
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile
# Copier tout le projet
COPY . .
RUN npm ci && npm cache clean --force
ADD . .
# BUILD
RUN npm run build
# Build Nuxt
RUN pnpm build
# Stage production
FROM node:20-alpine
# PROD
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.output /app/.output
COPY --from=builder /app/.nuxt /app/.nuxt
COPY --from=builder /app/.env /app/.env
ENV HOST 0.0.0.0
COPY --from=build /app/.output .output
COPY --from=build /app/package.json ./
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD source .env && node .output/server/index.mjs
CMD ["node", ".output/server/index.mjs"]

View File

@@ -1 +1,75 @@
# evilSpins
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

11
app.vue
View File

@@ -1,11 +0,0 @@
<template>
<NuxtPage />
</template>
<script setup>
// @todo : laod datas as plugin/middleware (cant load pinia in plugin/middleware) ?
onMounted(async ()=>{
const dataStore = await useDataStore()
await dataStore.loadData()
})
</script>

6
app/app.vue Normal file
View File

@@ -0,0 +1,6 @@
<template>
<NuxtLayout>
<NuxtPage />
<NuxtRouteAnnouncer />
</NuxtLayout>
</template>

View File

@@ -0,0 +1,8 @@
<template>
<div class="flex items-center justify-center min-h-screen">
<atropos-component class="h-72 w-60" active-offset="80" shadow-scale="1.05">
<img src="/logo.svg">
<img src="/ES01A/object.png">
</atropos-component>
</div>
</template>

96
app/pages/index.vue Normal file
View File

@@ -0,0 +1,96 @@
<template>
<div>
<div class="background fixed w-full h-full">
<video class="animation screen" loop autoplay muted ref="animation">
<source src="https://files.erudi.fr/evilspins/sloughi-run-loop-big.webm" type="video/webm">
<source src="https://files.erudi.fr/evilspins/sloughi-run-loop-small.webm" type="video/webm"
media="all and (max-width: 640px)">
</video>
</div>
<section class="splash-screen flex items-center flex-col">
<figure class="ui">
<img class="logo" src="/logo.svg">
</figure>
</section>
</div>
</template>
<style scoped>
body {
margin: 0;
overflow-x: hidden;
}
.logo,
.button,
.shadow,
.animation,
.mix {
transition: .7s opacity;
}
.screen {
position: absolute;
height: 100vh;
min-width: 100%;
max-width: 100%;
}
.splash-screen {
position: relative;
height: 100vh;
box-shadow: inset black 0px 1px 800px 200px;
}
.animation {
z-index: 1;
object-fit: cover;
opacity: .8;
/* opacity: 0; */
}
.mix {
z-index: 4;
position: fixed;
}
.shadow {
z-index: 3;
box-shadow: rgb(0, 0, 0) 0px 0px 170px 70px inset;
opacity: .9;
}
.ui {
z-index: 4;
position: absolute;
top: 50%;
left: 50%;
max-width: 80%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
}
.logo {
filter: drop-shadow(8px 8px 0 rgb(0 0 0 / 0.8));
}
.mixPlayer {
background: black;
max-height: 70vh;
}
.hide {
opacity: 0;
z-index: 0;
}
.show {
opacity: 1;
}
.text-shadow {
text-shadow: 3px 2px 8px black;
}
</style>

3
app/pages/wait.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<atropos />
</template>

View File

@@ -1,45 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-black;
}
.button {
text-decoration: none;
box-shadow: 0 8px 0 0 black;
transition: all .3s;
border: 8px black solid;
line-height: 100%;
border-width: 2px;
border-radius: 100px;
cursor: pointer;
color: black;
font-size: 26px;
background-color: #ffffff59;
height: 40px;
width: 40px;
@media (min-width: 780px) {
height: 70px;
width: 70px;
}
}
.button:hover {
background-color: #fdec50ff;
}
.button:active {
box-shadow: 0 0 0 0 black;
}
.button--screened {
top: 74px;
}
.compilation {
cursor: pointer;
max-width: 420px;
}

View File

@@ -1,22 +0,0 @@
<template>
<NuxtLink :to="'/compilations/' + props.data.id" class="compilation mx-auto p-4 inline-flex">
<atropos-component class="my-atropos" active-offset="80" shadow-scale="1.05">
<img :src="props.data.id + '/bkg.jpg'" data-atropos-offset="-8" />
<img :src="props.data.id + '/object.png'" data-atropos-offset="-3" class="absolute inset-0 object-cover" />
<img :src="props.data.id + '/name.png'" data-atropos-offset="0" class="absolute inset-0 object-cover" />
<img src="/logo.svg" data-atropos-offset="0" width="70%" class="logo absolute inset-0" />
</atropos-component>
</NuxtLink>
</template>
<script setup>
const props = defineProps(['data', 'template'])
</script>
<style scoped>
.logo {
filter: drop-shadow(4px 4px 0 rgb(0 0 0 / 0.5));
left: 14%;
top: 10%;
}
</style>

View File

@@ -1,11 +0,0 @@
<template>
<section>
<div v-for="compilation in store.getAllCompilations" class="text-white">
<compilationObject :data="compilation" template="full" />
</div>
</section>
</template>
<script setup lang="ts">
const store = useDataStore()
</script>

View File

@@ -1,31 +0,0 @@
<script setup lang="ts">
const isLoaded = ref(false)
const isPlaying = ref(false)
const video = ref()
async function play() {
await video.value.player.playVideo()
}
function stateChange(event) {
isPlaying.value = event.data === 1
}
</script>
<template>
<div>
<div class="flex items-center justify-center p-5">
<ScriptYouTubePlayer ref="video" video-id="iyPiiZly864" class="group" @ready="isLoaded = true" @state-change="stateChange">
<template #awaitingLoad>
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 h-[48px] w-[68px]">
<svg height="100%" version="1.1" viewBox="0 0 68 48" width="100%"><path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00" /><path d="M 45,24 27,14 27,34" fill="#fff" /></svg>
</div>
</template>
</ScriptYouTubePlayer>
</div>
<div class="text-center">
<div v-if="!isLoaded" class="mb-5" size="sm" color="blue" variant="soft" title="Click to load" description="Clicking the video will load the Vimeo iframe and start the video." />
<button v-if="isLoaded && !isPlaying" @click="play">
Play Video
</button>
</div>
</div>
</template>

View File

@@ -1,28 +0,0 @@
<template>
<div class="compilation mx-auto p-4 inline-flex">
<atropos-component ref="atropos" class="my-atropos" active-offset="80" shadow-scale="1.05">
<img src="/zero/sky-b.jpg" data-atropos-offset="-8" />
<img src="/zero/propeller-b.png" data-atropos-offset="-3" class="absolute inset-0 object-cover" />
<img src="/zero/zero-b.png" data-atropos-offset="0" class="absolute inset-0 object-cover" />
<img src="/logo.svg" data-atropos-offset="0" width="70%" class="logo absolute inset-0" />
</atropos-component>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const atropos = ref(null)
</script>
<style scoped>
/* .my-atropos {
width: 320px;
height: 160px;
} */
.logo {
filter: drop-shadow(4px 4px 0 rgb(0 0 0 / 0.5));
left: 14%;
top: 10%;
}
</style>

5
config.sh Executable file
View File

@@ -0,0 +1,5 @@
export REPO_NAME="evilspins"
export DOMAIN="evilspins.$MAIN_DOMAIN"
export PORT="7901"
export PORT_EXPOSED="3000"
export REDIRECTIONS="" # example.$MAIN_DOMAIN->/route $MAIN_DOMAIN->url /route->/another-route /route->url

View File

@@ -1,20 +1,23 @@
services:
evilspins:
build: .
image: local/evilspins
build:
context: .
dockerfile: Dockerfile
container_name: evilspins
restart: unless-stopped
working_dir: /app
ports:
- $PORT:3000
- "${PORT}:${PORT_EXPOSED}"
volumes:
- "${MEDIA_DIR}:/mnt/media"
environment:
VIRTUAL_HOST: "${DOMAIN}"
LETSENCRYPT_HOST: "${DOMAIN}"
PUID: "${PUID}"
PGID: "${PGID}"
volumes:
- "${MEDIA_DIR}:/app/media"
networks:
default:
name: dockerweb
external: true
external: true

6
eslint.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

125
logo.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,24 +1,11 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
ssr: true,
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
css: ['~/assets/css/main.css'],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
modules: ['@nuxt/eslint', '@nuxtjs/tailwindcss', '@pinia/nuxt'],
vue: {
compilerOptions: {
isCustomElement: (tag) => ['atropos-component'].includes(tag)
}
},
compatibilityDate: '2024-07-10',
modules: ['@pinia/nuxt']
}
})

8186
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,28 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev --host",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@pinia/nuxt": "^0.5.5",
"@nuxt/eslint": "1.9.0",
"@nuxtjs/tailwindcss": "6.14.0",
"atropos": "^2.0.2",
"nuxt": "^3.12.3",
"pinia": "^2.2.4",
"unhead": "^1.9.15",
"vue": "^3.4.31",
"vue-router": "^4.4.0"
"eslint": "^9.33.0",
"nuxt": "^4.0.3",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},
"engines": {
"pnpm": ">=10 <11"
},
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"sass": "^1.77.6",
"tailwindcss": "^3.4.4"
"@pinia/nuxt": "^0.11.2"
}
}

View File

@@ -1,137 +0,0 @@
<template>
<div class="text-white p-12 text-lg flex flex-col items-center">
<a href="/" class="mb-12">
<img class="logo" src="/logo.svg">
</a>
<div class="max-w-4xl">
<h1 class="text-esyellow text-6xl mb-8">About evilSpins ...</h1>
<pre>
Rather than explaining the "artistic" approach of evilspins in too formal a manner, I prefer to freely share the notes and sketches as a testament to the project's long genesis (2019 -> 2024) :
</pre>
<pre>
"The idea of the name comes from Anton Newcombe, who defines music as something that must remain independent,
hence his quote "keep music evil", from which 'evil' and 'spins' refer to music as an object that turns (cassette,
vinyl, CD...), music is a living object in perpetual motion and it's 'evil' / independent / unmanageable because
it makes you dance, it takes the listener without even realizing it.
</pre>
<img src="https://files.erudi.fr/evilspins/ABOUT-1.jpg">
<pre>
Each compilation is the original soundtrack of a non-existent film. It must leave room for imagination and
reflection.
A compilation tries to produce the effect of a film, to provoke imagination.
It's an opportunity to decontextualize forgotten, outdated, or unconsciously caricatured musical genres
(bluegrass,
punk, folk, hip-hop...) or simply to discover new genres.
Approach music without thinking about the stylistic references of the piece, like in a film.
</pre>
<img src="https://files.erudi.fr/evilspins/ABOUT-2.jpg">
<pre>
A compilation can therefore offer several readings,
- a panel of characters,
- several versions (short, long like a director's cut, side A/B (side B could be a parallel version with the same
artists in the same order but with different titles very close))
- several possible reading modes (mode 1: only the mixed sound and artworks (only play/pause is available),
mode 2: portraits and names of the protagonists, anecdotes, quotes, and web links (possibility to go to the
next/previous track and to 'seek'))
- an artwork (square format (vinyl) or 16/9 + black band (cinema)) - vocal samples to make characters speak?
(samples of artists in interviews, for example)
- a mix must follow this order:
List of quintessence sound :
1. accessible but instrumental
2. more researched and instrumental
3. more rhythmic instrumental
4->7. rhythmic sung
8. rhythmic instrumental
9. Instrumental
10. bluegrass final Evils spins from the idea that music needs the imagination of its listener.
11. hidden track
</pre>
<img src="https://files.erudi.fr/evilspins/ABOUT-3.jpg">
<pre>
Design: timeline of the player like the itinerary of a journey on a map OR like a display board at a train
station/airport "A problem created cannot be solved by thinking in the same way it was created." "God only depends
on the imagination of men" Drawing situations where you meet someone you think you know well but who doesn't have
the same vision of things as you. Example: discussion about music and completely divergent points of view, (Nick
Waterhouse = completely inaccessible jazz) Going to see local labels inventing a religion and therefore a god, is
only a way to personify one's conscience. The projects converge on the concept of a "cyber residence of artists"
drawing a face to one's morality. Dreams are nightmares, nightmares are dreams. Starting from reflections on the
use
of new technologies; questioning one's use of the internet on one's computer, phone, etc., allows one to wonder
what? when? why do I need the internet, to watch a movie, to listen to music... In my case, the answers to these
questions propose a use that mixes tools and methods from different eras. TOOLS & METHODS FROM DIFFERENT
ERAS/CONTEXTS Just like in the film 'La fille du 14 juillet', it can be very interesting to play on the contrasts
between different eras by mixing them in the same visual and sound realization. Idea of realization graphic novel
or
comic book: Observation: with the revolution of the internet and new technologies, cinema has become a major art
that everyone masters more or less. The public's gaze is sharpened and everyone knows how to apprehend
cinematographic realization. The plans, camera movements, sound effects are a language that everyone knows how to
read (cinema is carried by all through downloading & streaming) ... and even write (I can make a film with my
phone,
reflex cameras have become affordable). How to draw cinema? Try to take key scenes from cinema, draw/paint them
and
animate them. fixed plan? traveling? zoom? panoramic? plunge? counter-plunge? wide-angle? long focal? work on the
Wikipedia of 'film noir' re-watch film noir link with the polar noir? film noir has a pretentious approach at
first
glance. so we need to find a technique (visual? sound?) to disarm this bad image. to do this we need to start by
isolating and understanding the possible pretentious aspect of film noir.
</pre>
<img src="https://files.erudi.fr/evilspins/ABOUT-4.jpg">
<pre>
The solitude of the character? The
voice-overs that narrate the thoughts? The appearance of the protagonist? The cliché (visual & script) of the
polar
noir too worn out? (the detective alone in his thoughts, in his apartment smoking a cigarette on the balcony) the
cigarette can still be a more powerful visual element than a cliché transition with animation of the cigarette?
cigarette = huge filmographic symbol == we find it in most films especially in the era of film noir == commercial
influence tool == influence of American culture (cowboy), == symbol of the ephemeral, like life, like the
characters
of a film, like a film. the 2 mega clichés: Nicholson/Polansky in Chinatown & Ridley Scott in Blade Runner How to
integrate the cigarette? = 3D animation? (often ugly, technically very difficult, aesthetically difficult) =
hand-drawn animation? (long... very long to do but good pretext to train) = vector-drawn animation? (technically
moderately difficult but aesthetically difficult) = film excerpts? (technically easy & aesthetically very easy BUT
not really creation (montage), and illegal... to check) = animation of the spin (film zoom in Cassette, CD, Vinyl,
the plates of a hard drive (joke -> why not make a montage effect), find an equivalent for digital (server bays,
server fan? (joke))) why not call the box to meet at Slift to turn it properly and start a collaboration?
otherwise,
a lot of material to Bellecours photo + ... Fnac? no, much more interesting to go through a collaboration. Take
out
the rushes of films.
</pre>
<img src="https://files.erudi.fr/evilspins/ABOUT-5.jpg">
<pre>
The thinking of Jack Nicholson in Chinatown No visual drawing during the reading of the piece,
only why not the log / cover / artwork of the album the reading should not look like a PowerPoint, the
counter-example is everywhere on YouTube. On the other hand, a drawn and animated artwork in parallax why not. See
3D js cover -> at the mouse movement: preview with animation -> At the click fade of the box zoom on the cover the
animation is no longer manual but automatic (linear movement? random?) after the cigarette... the smoke... too
cliché? possible but the smoke can make a transition between 2 listens / animations. the listening ends the smoke
arrives brings us to the cigarette which brings us to a protagonist who brings us to a film excerpt. why
evilspins?
The spin obviously refers to the movement of music both by its format (cassette, vinyl, CD) and by its effect
(membrane, air movement, dance) possible sound effect: end of the piece Purple Mercy - Purple -> the end of the
piece ends with an abrupt snare with a subtle reverb -> effect felt: conclusion on an impression of grand space ->
effect analyzed / symbolic: grandeur of the proposal but above all empty room -> no public -> therefore call to
the
public -> idea expressible: maybe too pretentious again. film noir has a pretentious image at first glance. we
need
to find a technique (visual? sound?) to disarm this bad image. to do this we need to start by isolating and
understanding the possible pretentious aspect of film noir. The solitude of the character? The voice-overs that
narrate the thoughts? The appearance of the protagonist? The cliché (visual & script) of the polar noir too worn
out? (the detective alone in his thoughts, in his apartment smoking a cigarette on the balcony)"
</pre>
<img src="https://files.erudi.fr/evilspins/ABOUT-6.jpg">
</div>
</div>
</template>
<style scoped>
img {
margin: 32px 0;
}
pre {
white-space: pre-line;
font-family: sans-serif;
}
</style>

View File

@@ -1,154 +0,0 @@
<template @keydown.esc="closePlayer()">
<div class="text-white w-full flex items-center flex-col">
<button class="text-sm md:text-5xl leading-none button button--close m-3 flex justify-center items-center z-50"
@click="closePlayer()">
</button>
<video class="mixPlayer w-full" controls ref="mixPlayer">
<source :src="videoSD" type="video/mp4">
</video>
<nav class="text-esyellow w-full flex" v-if="currentTrack">
<button v-for="(track, index) in tracks" @click="listenTo(track.start)" :index="track.id"
class="border-l-wihte-400 border-l-2 p-2 flex-grow hover:bg-esyellow hover:text-black"
:class="{ 'border-l-0': index === 0, 'bg-esyellow text-black': track.id === currentTrack.id }">
<span class="block">
{{ index + 1 }}
</span>
<span class="hidden 2xl:block">
{{ track.title }}
</span>
<span class="hidden lg:block">
{{ getArtistName(track.artist) }}
</span>
</button>
</nav>
<article class="text-white p-8 max-w-5xl" v-if="currentTrack">
<div class="flex flex-col sm:flex-row items-center ">
<a :href="currentTrack.url" target="_blank" class="mr-4">
<atropos-component>
<img class="flex-grow-0" :src="'https://f4.bcbits.com/img/' + currentTrack.cover + '_8.jpg'" />
</atropos-component>
</a>
<div>
<a :href="currentTrack.url" target="_blank" rel="noopener noreferrer">
<h3 class="text-5xl">
{{ currentTrack.title }}
</h3>
</a>
<a v-if="currentArtist" :href="currentArtist.url" target="_blank" rel="noopener noreferrer">
<h2 class="font-bold text-6xl text-esyellow">
{{ currentArtist.name }}
</h2>
</a>
<h4 class="text-xl text-slate-200">
{{ compilation.name }}
</h4>
</div>
</div>
<p class="block mt-10">
see artist page:<br>
<a v-if="currentArtist" target="_blank" class="underline text-orange-500 hover:text-orange-400"
:href="currentArtist.url">
{{ currentArtist.name }}
</a><br>
purchase the track:<br>
<a target="_blank" class="underline text-orange-500 hover:text-orange-400" :href="currentTrack.url">
{{ currentTrack.title }}
</a><br>
<br>
</p>
</article>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
const store = useDataStore()
const compilation = ref()
const tracks = ref()
const mixPlayer = ref()
const videoSD = ref()
const currentTrack = ref()
const { isLoaded } = storeToRefs(store)
const currentArtist = computed(() => {
return store.getArtistById(currentTrack.value.artist)
})
const getArtistName = (id: number) => {
return store.getArtistById(id)?.name
}
// LOAD DATAs
onMounted(() => {
loadCompilation() // if user arrive directly on compilation page
})
watch(isLoaded, () => {
loadCompilation() // if the user came from another page
})
const watchPlayingTrack = () => {
setInterval(() => {
if (mixPlayer.value && compilation.value.id) {
currentTrack.value = tracks.value.find((track: { start: number; }, index: number) => {
const nextTrackStart = tracks.value[index + 1]?.start ?? Infinity
return track.start <= mixPlayer.value.currentTime && mixPlayer.value.currentTime < nextTrackStart
})
}
}, 1000)
}
const loadCompilation = () => {
if (isLoaded.value) {
compilation.value = store.getCompilationById(route.params.id as string)
tracks.value = store.getTracksByCompilationId(route.params.id as string)
videoSD.value = 'https://files.erudi.fr/evilspins/' + compilation.value.id + '-SD.mp4'
mixPlayer.value.load()
mixPlayer.value.play()
mixPlayer.value.focus()
watchPlayingTrack()
}
}
const listenTo = (start: number) => {
mixPlayer.value.currentTime = start
mixPlayer.value.play()
}
const closePlayer = async () => {
await navigateTo('/')
}
</script>
<style lang="scss" scoped>
body {
margin: 0;
}
.logo {
filter: drop-shadow(8px 8px 0 rgb(0 0 0 / 0.8));
}
a:hover {
text-decoration: underline;
}
.mixPlayer {
background: black;
max-height: 70vh;
}
nav>button:first-child {
border-left: none;
}
.button--close {
position: fixed;
right: 2vw;
&:after {
content: "\00d7";
}
}
</style>

View File

@@ -1,127 +0,0 @@
<template>
<div>
<div class="w-full flex justify-center">
<nav class="[&>*]:p-2 text-white bottom-0 right-0 fixed flex justify-center z-50">
<a href="https://www.youtube.com/channel/UCATtFHnOLDCv8qroi2KW3ZA" target="about:blank" class="mt-1">
<img src="/youtube.svg" alt="youtube channel" />
</a>
<a href="mailto:contact@evilspins.com">📬</a>
<a href="/about"></a>
</nav>
</div>
<section class="splash-screen flex items-center flex-col">
<figure class="ui">
<img class="logo" src="/logo.svg">
<h1 class="text-white pt-6 text-sm md:text-md lg:text-lg text-center font-bold tracking-widest">Compilations
Indépendantes
</h1>
<button class="button button--screened relative top-16 flex justify-center items-center" @click="scrollDown()">
</button>
</figure>
<div class="shadow screen" />
<video class="animation screen" loop autoplay muted ref="animation">
<source src="https://files.erudi.fr/evilspins/sloughi-run-loop-big.webm" type="video/webm">
<source src="https://files.erudi.fr/evilspins/sloughi-run-loop-small.webm" type="video/webm"
media="all and (max-width: 640px)">
</video>
</section>
<section class="flex justify-center">
<div class="flex max-w-2xl">
<compilationsList />
</div>
</section>
</div>
</template>
<script setup lang="ts">
// SEO
useSeoMeta({
title: 'evilSpins - compilations indépendantes',
ogTitle: 'evilSpins - compilations indépendantes',
description: 'evilSpins - compilations indépendantes, la bande originale d\'un film qui n\'existe pas',
ogDescription: 'evilSpins - compilations indépendantes, la bande originale d\'un film qui n\'existe pas',
ogImage: 'https://evilspins.com/logo.svg'
})
const dataStore = useDataStore()
const scrollDown = function () {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
}
</script>
<style lang="scss" scoped>
body {
margin: 0;
overflow-x: hidden;
}
.logo,
.button,
.shadow,
.animation,
.mix {
transition: .7s opacity;
}
.screen {
position: absolute;
height: 100vh;
min-width: 100%;
max-width: 100%;
}
.splash-screen {
position: relative;
height: 100vh;
background-color: black;
}
.animation {
z-index: 1;
object-fit: cover;
opacity: .8;
/* opacity: 0; */
}
.mix {
z-index: 4;
position: fixed;
}
.shadow {
z-index: 3;
box-shadow: rgb(0, 0, 0) 0px 0px 170px 70px inset;
opacity: .9;
}
.ui {
z-index: 4;
position: absolute;
top: 50%;
left: 50%;
max-width: 80%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
}
.logo {
filter: drop-shadow(8px 8px 0 rgb(0 0 0 / 0.8));
}
.mixPlayer {
background: black;
max-height: 70vh;
}
.hide {
opacity: 0;
z-index: 0;
}
.show {
opacity: 1;
}
</style>

View File

@@ -1,5 +0,0 @@
<template>
<div class="h-screen w-full flex justify-center p-16">
<img src="/logo.svg">
</div>
</template>

10234
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/ES01A/bkg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
public/ES01A/name.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

50
public/ES01A/number1.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/ES01A/object.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

BIN
public/ES01B/B.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
public/ES01B/bkg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
public/ES01B/name.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

22
public/ES01B/name.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/ES01B/object.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

BIN
public/ESPLAYLISTS/bkg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/ESPLAYLISTS/name.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,38 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
height="800px"
width="800px"
version="1.1"
id="svg1"
id="Capa_1"
viewBox="0 0 60 60"
xml:space="preserve"
sodipodi:docname="play.svg"
width="25.177818"
height="31.875"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="1.9448516"
inkscape:cx="202.07197"
inkscape:cy="136.00009"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
style="font-size:64px;line-height:0.6;font-family:'Noto Sans Rejang';-inkscape-font-specification:'Noto Sans Rejang';letter-spacing:0.03px;word-spacing:0.16px;stroke-width:5.38174;stroke-miterlimit:2.3;stroke-dasharray:1.07635, 5.91989"
d="M 0,31.875 V 0 l 25.177818,15.9375 z"
id="text1"
aria-label="▸" />
</svg>
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs2">
</defs><sodipodi:namedview
id="namedview2"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="0.94997202"
inkscape:cx="340.01001"
inkscape:cy="397.38012"
inkscape:window-width="1920"
inkscape:window-height="1132"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="path4" />
<g
id="g4"
transform="translate(9.7969913,-22.06049)"><g
id="path4"
inkscape:transform-center-x="-3.1076416"
inkscape:transform-center-y="0.031482236"
style="opacity:1"
transform="matrix(0.16696126,-0.60372499,0.52316491,0.19267096,25.039308,35.922386)"><path
id="path1"
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2.3;paint-order:fill markers stroke;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m -9.5847112,-19.414681 a 2.4868138,2.4868138 0 0 0 -3.1047968,-1.516874 l -18.788944,6.087811 -18.791084,6.0858648 a 2.4868138,2.4868138 0 0 0 -0.900333,4.2129952 L -36.501957,8.6836742 -21.835903,21.91414 a 2.4868138,2.4868138 0 0 0 4.097569,-1.326305 l 4.124976,-19.3182854 4.1231169,-19.3163776 a 2.4868138,2.4868138 0 0 0 -0.09447,-1.367853 z m -0.7440438,70.809953 a 54.628808,47.339229 72.300195 0 1 -61.707096,-37.65028 54.628808,47.339229 72.300195 0 1 28.489522,-66.435356 54.628808,47.339229 72.300195 0 1 61.707096,37.650281 54.628808,47.339229 72.300195 0 1 -28.489522,66.435355 z" /></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View File

@@ -1,81 +0,0 @@
export default eventHandler(() => {
return [
{
id: 0,
name: "L'efondras",
url: "https://leffondras.bandcamp.com/music",
style: [0, 1, 2],
cover: "0024705317"
},
{
id: 1,
name: "The kundalini genie",
url: "https://the-kundalini-genie.bandcamp.com",
style: [0, 1, 2],
cover: "0012045550"
},
{
id: 2,
name: "Fontaines D.C.",
url: "https://fontainesdc.bandcamp.com",
style: [0, 1, 2],
cover: "0027327090"
},
{
id: 3,
name: "Fontanarosa",
url: "https://fontanarosa.bandcamp.com",
style: [0, 1, 2],
cover: "0035380235",
},
{
id: 4,
name: "Johnny mafia",
url: "https://johnnymafia.bandcamp.com",
style: [0, 1, 2],
cover: "0035009392",
},
{
id: 5,
name: "New candys",
url: "https://newcandys.bandcamp.com",
style: [0, 1, 2],
cover: "0033518637",
},
{
id: 6,
name: "Magic shoppe",
url: "https://magicshoppe.bandcamp.com",
style: [0, 1, 2],
cover: "0030748374"
},
{
id: 7,
name: "Les jaguars",
url: "https://radiomartiko.bandcamp.com/album/surf-qu-b-cois",
style: [0, 1, 2],
cover: "0016551336",
},
{
id: 8,
name: "TRAAMS",
url: "https://traams.bandcamp.com",
style: [0, 1, 2],
cover: "0028348410",
},
{
id: 9,
name: "Blue orchid",
url: "https://blue-orchid.bandcamp.com",
style: [0, 1, 2],
cover: "0034796193",
},
{
id: 10,
name: "I love UFO",
url: "https://bruitblanc.bandcamp.com",
style: [0, 1, 2],
cover: "a2203158939",
}
]
})

View File

@@ -1,16 +0,0 @@
export default eventHandler(() => {
return [
{
id: 'ES00A',
name: 'zero',
duration: 2794,
description: '...',
},
{
id: 'ES00B',
name: 'zero b-sides',
duration: 2470,
description: '...',
}
]
})

View File

@@ -1,21 +0,0 @@
import fs from 'fs'
import path from 'path'
export default eventHandler(async (event) => {
const directoryPath = path.join(process.cwd(), 'media/files/music') // replace 'your-folder' with the folder you want to list
try {
// Read the directory contents
const files = await fs.promises.readdir(directoryPath)
return {
success: true,
files: files.filter(file => !file.startsWith('.')) // optional: exclude unwanted files
}
} catch (error) {
return {
success: false,
error: error.message
}
}
})

View File

@@ -1,16 +0,0 @@
export default eventHandler(() => {
return [
{
"id": 0,
"name": "post-rock"
},
{
"id": 1,
"name": "math-rock"
},
{
"id": 2,
"name": "indie-pop"
}
]
})

View File

@@ -1,224 +0,0 @@
export default eventHandler(() => {
return [
{
id: 1,
compilation: 'ES00A',
title: 'The grinding wheel',
artist: 0,
start: 0,
bpm: 0,
url: 'https://arakirecords.bandcamp.com/track/the-grinding-wheel',
cover: 'a3236746052',
},
{
id: 2,
compilation: 'ES00A',
title: 'Bleach',
artist: 1,
start: 393,
bpm: 0,
url: 'https://the-kundalini-genie.bandcamp.com/track/bleach-2',
cover: 'a1714786533',
},
{
id: 3,
compilation: 'ES00A',
title: 'Televised mind',
artist: 2,
start: 892,
bpm: 0,
url: 'https://fontainesdc.bandcamp.com/track/televised-mind',
cover: 'a3772806156'
},
{
id: 4,
compilation: 'ES00A',
title: 'In it',
artist: 3,
start: 1138,
bpm: 0,
url: 'https://howlinbananarecords.bandcamp.com/track/in-it',
cover: 'a1720372066',
},
{
id: 5,
compilation: 'ES00A',
title: 'Bad michel',
artist: 4,
start: 1245,
bpm: 0,
url: 'https://johnnymafia.bandcamp.com/track/bad-michel-3',
cover: 'a0984622869',
},
{
id: 6,
compilation: 'ES00A',
title: 'Overall',
artist: 5,
start: 1394,
bpm: 0,
url: 'https://newcandys.bandcamp.com/track/overall',
cover: 'a0559661270',
},
{
id: 7,
compilation: 'ES00A',
title: 'Blowup',
artist: 6,
start: 1674,
bpm: 0,
url: 'https://magicshoppe.bandcamp.com/track/blowup',
cover: 'a1444895293',
},
{
id: 8,
compilation: 'ES00A',
title: 'Guitar jet',
artist: 7,
start: 1880,
bpm: 0,
url: 'https://radiomartiko.bandcamp.com/track/guitare-jet',
cover: 'a1494681687',
},
{
id: 9,
compilation: 'ES00A',
title: 'Intercontinental radio waves',
artist: 8,
start: 2024,
bpm: 0,
url: 'https://traams.bandcamp.com/track/intercontinental-radio-waves',
cover: 'a0046738552',
},
{
id: 10,
compilation: 'ES00A',
title: 'Here comes the sun',
artist: 9,
start: 2211,
bpm: 0,
url: 'https://blue-orchid.bandcamp.com/track/here-come-the-sun',
cover: 'a4102567047',
},
{
id: 11,
compilation: 'ES00A',
title: 'Like in the movies',
artist: 10,
start: 2559,
bpm: 0,
url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies-2',
cover: 'a2203158939',
},
{
id: 21,
compilation: 'ES00B',
title: 'Ce que révèle l\'éclipse',
artist: 0,
start: 0,
bpm: 0,
url: 'https://arakirecords.bandcamp.com/track/ce-que-r-v-le-l-clipse',
cover: 'a3236746052',
},
{
id: 22,
compilation: 'ES00B',
title: 'Bleedin\' Gums Mushrool',
artist: 1,
start: 263,
bpm: 0,
url: 'https://the-kundalini-genie.bandcamp.com/track/bleedin-gums-mushroom',
cover: 'a1714786533',
},
{
id: 23,
compilation: 'ES00B',
title: 'A lucid dream',
artist: 2,
start: 554,
bpm: 0,
url: 'https://fontainesdc.bandcamp.com/track/a-lucid-dream',
cover: 'a3772806156',
},
{
id: 24,
compilation: 'ES00B',
title: 'Lights off',
artist: 3,
start: 781,
bpm: 0,
url: 'https://howlinbananarecords.bandcamp.com/track/lights-off',
cover: 'a1720372066',
},
{
id: 25,
compilation: 'ES00B',
title: 'I\'m sentimental',
artist: 4,
start: 969,
bpm: 0,
url: 'https://johnnymafia.bandcamp.com/track/im-sentimental-2',
cover: 'a2333676849',
},
{
id: 26,
compilation: 'ES00B',
title: 'Thrill or trip',
artist: 5,
start: 1128,
bpm: 0,
url: 'https://newcandys.bandcamp.com/track/thrill-or-trip',
cover: 'a0559661270',
},
{
id: 27,
compilation: 'ES00B',
title: 'Redhead',
artist: 6,
start: 1303,
bpm: 0,
url: 'https://magicshoppe.bandcamp.com/track/redhead',
cover: 'a0594426943',
},
{
id: 28,
compilation: 'ES00B',
title: 'Supersonic twist',
artist: 7,
start: 1584,
bpm: 0,
url: 'https://open.spotify.com/track/66voQIZAJ3zD3Eju2qtNjF',
cover: 'a1494681687',
},
{
id: 29,
compilation: 'ES00B',
title: 'Flowers',
artist: 8,
start: 1749,
bpm: 0,
url: 'https://traams.bandcamp.com/track/flowers',
cover: 'a3644668199',
},
{
id: 30,
compilation: 'ES00B',
title: 'The shade',
artist: 9,
start: 1924,
bpm: 0,
url: 'https://blue-orchid.bandcamp.com/track/the-shade',
cover: 'a0804204790',
},
{
id: 31,
compilation: 'ES00B',
title: 'Like in the movies',
artist: 10,
start: 2185,
bpm: 0,
url: 'https://bruitblanc.bandcamp.com/track/like-in-the-movies',
cover: 'a3647322740',
},
]
})

View File

@@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@@ -1,51 +0,0 @@
import type { Compilation, Artist, Track } from '~/types/types'
// stores/data.ts
import { defineStore } from 'pinia'
export const useDataStore = defineStore('data', {
state: () => ({
compilations: [] as Compilation[], // Store your compilation data here
artists: [] as Artist[], // Store artist data here
tracks: [] as Track[], // Store track data here
isLoaded: false, // To track if data is already loaded
}),
actions: {
async loadData() {
if (this.isLoaded) return // Avoid re-fetching if already loaded
// Fetch your data once (e.g., from an API or local JSON)
const { data: compilations } = await useFetch('/api/compilations')
const { data: artists } = await useFetch('/api/artists')
const { data: tracks } = await useFetch('/api/tracks')
// Set the data in the store
this.compilations = compilations.value
this.artists = artists.value
this.tracks = tracks.value
this.isLoaded = true
}
},
getters: {
// Obtenir tous les compilations
getAllCompilations: (state) => state.compilations,
getCompilationById: (state) => {
return (id: string) => {
return state.compilations.find(compilation => compilation.id === id)
}
},
// Obtenir toutes les pistes d'une compilation donnée
getTracksByCompilationId: (state) => (compilationId: string) => {
return state.tracks.filter(track => track.compilation === compilationId)
},
// Filtrer les artistes selon certains critères
getArtistById: (state) => (id: number) => state.artists.find(artist => artist.id === id),
// Obtenir toutes les pistes d'un artiste donné
getTracksByArtistId: (state) => (artistId: string) => {
return state.tracks.filter(track => track.artistId === artistId)
},
},
})

View File

@@ -11,12 +11,12 @@ module.exports = {
theme: {
extend: {
colors: {
esyellow: '#fdec50ff',
esyellow: "#fdec50ff",
},
screens: {
'2sm': '320px',
"2sm": "320px",
},
},
},
plugins: [],
}
};

View File

@@ -1,4 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}

View File

@@ -1,27 +0,0 @@
// types.ts
export interface Compilation {
id: string
name: string
duration: number
tracks?: Track[]
description: string
}
export interface Artist {
id: number
name: string
url: string
style: Array<number>
cover: string
}
export interface Track {
id: string
compilationId: string
title: string
artistId: number
artist?: Artist
start: number
link: string
cover: string
}