Compare commits
2 Commits
6ae861ff54
...
c366ca5ce0
| Author | SHA1 | Date | |
|---|---|---|---|
| c366ca5ce0 | |||
| 774cb799a2 |
@@ -17,6 +17,12 @@ ANALYSIS_NUM_WORKERS=4
|
|||||||
ESSENTIA_MODELS_PATH=/app/models
|
ESSENTIA_MODELS_PATH=/app/models
|
||||||
AUDIO_LIBRARY_PATH=/path/to/your/audio/library
|
AUDIO_LIBRARY_PATH=/path/to/your/audio/library
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
ADMIN_EMAIL=admin@example.com
|
||||||
|
ADMIN_PASSWORD=changeme
|
||||||
|
JWT_SECRET_KEY=your-super-secret-jwt-key-change-this-in-production
|
||||||
|
JWT_EXPIRATION_HOURS=24
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
# API URL accessed by the browser (use port 8001 since backend is mapped to 8001)
|
# API URL accessed by the browser (use port 8001 since backend is mapped to 8001)
|
||||||
# For production on a remote server, set this to your server's public URL
|
# For production on a remote server, set this to your server's public URL
|
||||||
|
|||||||
@@ -23,41 +23,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download Essentia models
|
|
||||||
run: |
|
|
||||||
mkdir -p backend/models
|
|
||||||
cd backend/models
|
|
||||||
|
|
||||||
# Download models from Essentia
|
|
||||||
echo "Downloading Essentia models..."
|
|
||||||
|
|
||||||
# Embedding model (18 MB)
|
|
||||||
curl -L -o discogs-effnet-bs64-1.pb \
|
|
||||||
https://essentia.upf.edu/models/feature-extractors/discogs-effnet/discogs-effnet-bs64-1.pb
|
|
||||||
|
|
||||||
# Genre classifier (2 MB)
|
|
||||||
curl -L -o genre_discogs400-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/genre_discogs400/genre_discogs400-discogs-effnet-1.pb
|
|
||||||
|
|
||||||
# Genre metadata
|
|
||||||
curl -L -o genre_discogs400-discogs-effnet-1.json \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/genre_discogs400/genre_discogs400-discogs-effnet-1.json
|
|
||||||
|
|
||||||
# Mood classifier (2.7 MB)
|
|
||||||
curl -L -o mtg_jamendo_moodtheme-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/mtg_jamendo_moodtheme/mtg_jamendo_moodtheme-discogs-effnet-1.pb
|
|
||||||
|
|
||||||
# Instrument classifier (2.6 MB)
|
|
||||||
curl -L -o mtg_jamendo_instrument-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/mtg_jamendo_instrument/mtg_jamendo_instrument-discogs-effnet-1.pb
|
|
||||||
|
|
||||||
# Genre classifier alternative (2.7 MB)
|
|
||||||
curl -L -o mtg_jamendo_genre-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/mtg_jamendo_genre/mtg_jamendo_genre-discogs-effnet-1.pb
|
|
||||||
|
|
||||||
ls -lh
|
|
||||||
echo "Models downloaded successfully!"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
@@ -112,25 +77,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download Essentia models (for context)
|
|
||||||
run: |
|
|
||||||
mkdir -p backend/models
|
|
||||||
cd backend/models
|
|
||||||
|
|
||||||
# Download models (needed because frontend build context is root)
|
|
||||||
curl -L -o discogs-effnet-bs64-1.pb \
|
|
||||||
https://essentia.upf.edu/models/feature-extractors/discogs-effnet/discogs-effnet-bs64-1.pb
|
|
||||||
curl -L -o genre_discogs400-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/genre_discogs400/genre_discogs400-discogs-effnet-1.pb
|
|
||||||
curl -L -o genre_discogs400-discogs-effnet-1.json \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/genre_discogs400/genre_discogs400-discogs-effnet-1.json
|
|
||||||
curl -L -o mtg_jamendo_moodtheme-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/mtg_jamendo_moodtheme/mtg_jamendo_moodtheme-discogs-effnet-1.pb
|
|
||||||
curl -L -o mtg_jamendo_instrument-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/mtg_jamendo_instrument/mtg_jamendo_instrument-discogs-effnet-1.pb
|
|
||||||
curl -L -o mtg_jamendo_genre-discogs-effnet-1.pb \
|
|
||||||
https://essentia.upf.edu/models/classification-heads/mtg_jamendo_genre/mtg_jamendo_genre-discogs-effnet-1.pb
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -75,10 +75,6 @@ yarn-error.log*
|
|||||||
# Docker
|
# Docker
|
||||||
postgres_data/
|
postgres_data/
|
||||||
|
|
||||||
# Essentia models (large files, download separately)
|
|
||||||
backend/models/*.pb
|
|
||||||
backend/models/*.json
|
|
||||||
|
|
||||||
# Audio analysis cache
|
# Audio analysis cache
|
||||||
*.peaks.json
|
*.peaks.json
|
||||||
.audio_cache/
|
.audio_cache/
|
||||||
|
|||||||
BIN
backend/models/discogs-effnet-bs64-1.pb
Normal file
BIN
backend/models/discogs-effnet-bs64-1.pb
Normal file
Binary file not shown.
462
backend/models/genre_discogs400-discogs-effnet-1.json
Normal file
462
backend/models/genre_discogs400-discogs-effnet-1.json
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
{
|
||||||
|
"name": "Genre Discogs400",
|
||||||
|
"type": "Music genre classification",
|
||||||
|
"link": "https://essentia.upf.edu/models/classification-heads/genre_discogs400/genre_discogs400-discogs-effnet-1.pb",
|
||||||
|
"version": "1",
|
||||||
|
"description": "Prediction of 400 music styles in the from the Discogs taxonomy",
|
||||||
|
"author": "Pablo Alonso",
|
||||||
|
"email": "pablo.alonso@upf.edu",
|
||||||
|
"release_date": "2023-05-04",
|
||||||
|
"framework": "tensorflow",
|
||||||
|
"framework_version": "2.8.0",
|
||||||
|
"classes": [
|
||||||
|
"Blues---Boogie Woogie",
|
||||||
|
"Blues---Chicago Blues",
|
||||||
|
"Blues---Country Blues",
|
||||||
|
"Blues---Delta Blues",
|
||||||
|
"Blues---Electric Blues",
|
||||||
|
"Blues---Harmonica Blues",
|
||||||
|
"Blues---Jump Blues",
|
||||||
|
"Blues---Louisiana Blues",
|
||||||
|
"Blues---Modern Electric Blues",
|
||||||
|
"Blues---Piano Blues",
|
||||||
|
"Blues---Rhythm & Blues",
|
||||||
|
"Blues---Texas Blues",
|
||||||
|
"Brass & Military---Brass Band",
|
||||||
|
"Brass & Military---Marches",
|
||||||
|
"Brass & Military---Military",
|
||||||
|
"Children's---Educational",
|
||||||
|
"Children's---Nursery Rhymes",
|
||||||
|
"Children's---Story",
|
||||||
|
"Classical---Baroque",
|
||||||
|
"Classical---Choral",
|
||||||
|
"Classical---Classical",
|
||||||
|
"Classical---Contemporary",
|
||||||
|
"Classical---Impressionist",
|
||||||
|
"Classical---Medieval",
|
||||||
|
"Classical---Modern",
|
||||||
|
"Classical---Neo-Classical",
|
||||||
|
"Classical---Neo-Romantic",
|
||||||
|
"Classical---Opera",
|
||||||
|
"Classical---Post-Modern",
|
||||||
|
"Classical---Renaissance",
|
||||||
|
"Classical---Romantic",
|
||||||
|
"Electronic---Abstract",
|
||||||
|
"Electronic---Acid",
|
||||||
|
"Electronic---Acid House",
|
||||||
|
"Electronic---Acid Jazz",
|
||||||
|
"Electronic---Ambient",
|
||||||
|
"Electronic---Bassline",
|
||||||
|
"Electronic---Beatdown",
|
||||||
|
"Electronic---Berlin-School",
|
||||||
|
"Electronic---Big Beat",
|
||||||
|
"Electronic---Bleep",
|
||||||
|
"Electronic---Breakbeat",
|
||||||
|
"Electronic---Breakcore",
|
||||||
|
"Electronic---Breaks",
|
||||||
|
"Electronic---Broken Beat",
|
||||||
|
"Electronic---Chillwave",
|
||||||
|
"Electronic---Chiptune",
|
||||||
|
"Electronic---Dance-pop",
|
||||||
|
"Electronic---Dark Ambient",
|
||||||
|
"Electronic---Darkwave",
|
||||||
|
"Electronic---Deep House",
|
||||||
|
"Electronic---Deep Techno",
|
||||||
|
"Electronic---Disco",
|
||||||
|
"Electronic---Disco Polo",
|
||||||
|
"Electronic---Donk",
|
||||||
|
"Electronic---Downtempo",
|
||||||
|
"Electronic---Drone",
|
||||||
|
"Electronic---Drum n Bass",
|
||||||
|
"Electronic---Dub",
|
||||||
|
"Electronic---Dub Techno",
|
||||||
|
"Electronic---Dubstep",
|
||||||
|
"Electronic---Dungeon Synth",
|
||||||
|
"Electronic---EBM",
|
||||||
|
"Electronic---Electro",
|
||||||
|
"Electronic---Electro House",
|
||||||
|
"Electronic---Electroclash",
|
||||||
|
"Electronic---Euro House",
|
||||||
|
"Electronic---Euro-Disco",
|
||||||
|
"Electronic---Eurobeat",
|
||||||
|
"Electronic---Eurodance",
|
||||||
|
"Electronic---Experimental",
|
||||||
|
"Electronic---Freestyle",
|
||||||
|
"Electronic---Future Jazz",
|
||||||
|
"Electronic---Gabber",
|
||||||
|
"Electronic---Garage House",
|
||||||
|
"Electronic---Ghetto",
|
||||||
|
"Electronic---Ghetto House",
|
||||||
|
"Electronic---Glitch",
|
||||||
|
"Electronic---Goa Trance",
|
||||||
|
"Electronic---Grime",
|
||||||
|
"Electronic---Halftime",
|
||||||
|
"Electronic---Hands Up",
|
||||||
|
"Electronic---Happy Hardcore",
|
||||||
|
"Electronic---Hard House",
|
||||||
|
"Electronic---Hard Techno",
|
||||||
|
"Electronic---Hard Trance",
|
||||||
|
"Electronic---Hardcore",
|
||||||
|
"Electronic---Hardstyle",
|
||||||
|
"Electronic---Hi NRG",
|
||||||
|
"Electronic---Hip Hop",
|
||||||
|
"Electronic---Hip-House",
|
||||||
|
"Electronic---House",
|
||||||
|
"Electronic---IDM",
|
||||||
|
"Electronic---Illbient",
|
||||||
|
"Electronic---Industrial",
|
||||||
|
"Electronic---Italo House",
|
||||||
|
"Electronic---Italo-Disco",
|
||||||
|
"Electronic---Italodance",
|
||||||
|
"Electronic---Jazzdance",
|
||||||
|
"Electronic---Juke",
|
||||||
|
"Electronic---Jumpstyle",
|
||||||
|
"Electronic---Jungle",
|
||||||
|
"Electronic---Latin",
|
||||||
|
"Electronic---Leftfield",
|
||||||
|
"Electronic---Makina",
|
||||||
|
"Electronic---Minimal",
|
||||||
|
"Electronic---Minimal Techno",
|
||||||
|
"Electronic---Modern Classical",
|
||||||
|
"Electronic---Musique Concr\u00e8te",
|
||||||
|
"Electronic---Neofolk",
|
||||||
|
"Electronic---New Age",
|
||||||
|
"Electronic---New Beat",
|
||||||
|
"Electronic---New Wave",
|
||||||
|
"Electronic---Noise",
|
||||||
|
"Electronic---Nu-Disco",
|
||||||
|
"Electronic---Power Electronics",
|
||||||
|
"Electronic---Progressive Breaks",
|
||||||
|
"Electronic---Progressive House",
|
||||||
|
"Electronic---Progressive Trance",
|
||||||
|
"Electronic---Psy-Trance",
|
||||||
|
"Electronic---Rhythmic Noise",
|
||||||
|
"Electronic---Schranz",
|
||||||
|
"Electronic---Sound Collage",
|
||||||
|
"Electronic---Speed Garage",
|
||||||
|
"Electronic---Speedcore",
|
||||||
|
"Electronic---Synth-pop",
|
||||||
|
"Electronic---Synthwave",
|
||||||
|
"Electronic---Tech House",
|
||||||
|
"Electronic---Tech Trance",
|
||||||
|
"Electronic---Techno",
|
||||||
|
"Electronic---Trance",
|
||||||
|
"Electronic---Tribal",
|
||||||
|
"Electronic---Tribal House",
|
||||||
|
"Electronic---Trip Hop",
|
||||||
|
"Electronic---Tropical House",
|
||||||
|
"Electronic---UK Garage",
|
||||||
|
"Electronic---Vaporwave",
|
||||||
|
"Folk, World, & Country---African",
|
||||||
|
"Folk, World, & Country---Bluegrass",
|
||||||
|
"Folk, World, & Country---Cajun",
|
||||||
|
"Folk, World, & Country---Canzone Napoletana",
|
||||||
|
"Folk, World, & Country---Catalan Music",
|
||||||
|
"Folk, World, & Country---Celtic",
|
||||||
|
"Folk, World, & Country---Country",
|
||||||
|
"Folk, World, & Country---Fado",
|
||||||
|
"Folk, World, & Country---Flamenco",
|
||||||
|
"Folk, World, & Country---Folk",
|
||||||
|
"Folk, World, & Country---Gospel",
|
||||||
|
"Folk, World, & Country---Highlife",
|
||||||
|
"Folk, World, & Country---Hillbilly",
|
||||||
|
"Folk, World, & Country---Hindustani",
|
||||||
|
"Folk, World, & Country---Honky Tonk",
|
||||||
|
"Folk, World, & Country---Indian Classical",
|
||||||
|
"Folk, World, & Country---La\u00efk\u00f3",
|
||||||
|
"Folk, World, & Country---Nordic",
|
||||||
|
"Folk, World, & Country---Pacific",
|
||||||
|
"Folk, World, & Country---Polka",
|
||||||
|
"Folk, World, & Country---Ra\u00ef",
|
||||||
|
"Folk, World, & Country---Romani",
|
||||||
|
"Folk, World, & Country---Soukous",
|
||||||
|
"Folk, World, & Country---S\u00e9ga",
|
||||||
|
"Folk, World, & Country---Volksmusik",
|
||||||
|
"Folk, World, & Country---Zouk",
|
||||||
|
"Folk, World, & Country---\u00c9ntekhno",
|
||||||
|
"Funk / Soul---Afrobeat",
|
||||||
|
"Funk / Soul---Boogie",
|
||||||
|
"Funk / Soul---Contemporary R&B",
|
||||||
|
"Funk / Soul---Disco",
|
||||||
|
"Funk / Soul---Free Funk",
|
||||||
|
"Funk / Soul---Funk",
|
||||||
|
"Funk / Soul---Gospel",
|
||||||
|
"Funk / Soul---Neo Soul",
|
||||||
|
"Funk / Soul---New Jack Swing",
|
||||||
|
"Funk / Soul---P.Funk",
|
||||||
|
"Funk / Soul---Psychedelic",
|
||||||
|
"Funk / Soul---Rhythm & Blues",
|
||||||
|
"Funk / Soul---Soul",
|
||||||
|
"Funk / Soul---Swingbeat",
|
||||||
|
"Funk / Soul---UK Street Soul",
|
||||||
|
"Hip Hop---Bass Music",
|
||||||
|
"Hip Hop---Boom Bap",
|
||||||
|
"Hip Hop---Bounce",
|
||||||
|
"Hip Hop---Britcore",
|
||||||
|
"Hip Hop---Cloud Rap",
|
||||||
|
"Hip Hop---Conscious",
|
||||||
|
"Hip Hop---Crunk",
|
||||||
|
"Hip Hop---Cut-up/DJ",
|
||||||
|
"Hip Hop---DJ Battle Tool",
|
||||||
|
"Hip Hop---Electro",
|
||||||
|
"Hip Hop---G-Funk",
|
||||||
|
"Hip Hop---Gangsta",
|
||||||
|
"Hip Hop---Grime",
|
||||||
|
"Hip Hop---Hardcore Hip-Hop",
|
||||||
|
"Hip Hop---Horrorcore",
|
||||||
|
"Hip Hop---Instrumental",
|
||||||
|
"Hip Hop---Jazzy Hip-Hop",
|
||||||
|
"Hip Hop---Miami Bass",
|
||||||
|
"Hip Hop---Pop Rap",
|
||||||
|
"Hip Hop---Ragga HipHop",
|
||||||
|
"Hip Hop---RnB/Swing",
|
||||||
|
"Hip Hop---Screw",
|
||||||
|
"Hip Hop---Thug Rap",
|
||||||
|
"Hip Hop---Trap",
|
||||||
|
"Hip Hop---Trip Hop",
|
||||||
|
"Hip Hop---Turntablism",
|
||||||
|
"Jazz---Afro-Cuban Jazz",
|
||||||
|
"Jazz---Afrobeat",
|
||||||
|
"Jazz---Avant-garde Jazz",
|
||||||
|
"Jazz---Big Band",
|
||||||
|
"Jazz---Bop",
|
||||||
|
"Jazz---Bossa Nova",
|
||||||
|
"Jazz---Contemporary Jazz",
|
||||||
|
"Jazz---Cool Jazz",
|
||||||
|
"Jazz---Dixieland",
|
||||||
|
"Jazz---Easy Listening",
|
||||||
|
"Jazz---Free Improvisation",
|
||||||
|
"Jazz---Free Jazz",
|
||||||
|
"Jazz---Fusion",
|
||||||
|
"Jazz---Gypsy Jazz",
|
||||||
|
"Jazz---Hard Bop",
|
||||||
|
"Jazz---Jazz-Funk",
|
||||||
|
"Jazz---Jazz-Rock",
|
||||||
|
"Jazz---Latin Jazz",
|
||||||
|
"Jazz---Modal",
|
||||||
|
"Jazz---Post Bop",
|
||||||
|
"Jazz---Ragtime",
|
||||||
|
"Jazz---Smooth Jazz",
|
||||||
|
"Jazz---Soul-Jazz",
|
||||||
|
"Jazz---Space-Age",
|
||||||
|
"Jazz---Swing",
|
||||||
|
"Latin---Afro-Cuban",
|
||||||
|
"Latin---Bai\u00e3o",
|
||||||
|
"Latin---Batucada",
|
||||||
|
"Latin---Beguine",
|
||||||
|
"Latin---Bolero",
|
||||||
|
"Latin---Boogaloo",
|
||||||
|
"Latin---Bossanova",
|
||||||
|
"Latin---Cha-Cha",
|
||||||
|
"Latin---Charanga",
|
||||||
|
"Latin---Compas",
|
||||||
|
"Latin---Cubano",
|
||||||
|
"Latin---Cumbia",
|
||||||
|
"Latin---Descarga",
|
||||||
|
"Latin---Forr\u00f3",
|
||||||
|
"Latin---Guaguanc\u00f3",
|
||||||
|
"Latin---Guajira",
|
||||||
|
"Latin---Guaracha",
|
||||||
|
"Latin---MPB",
|
||||||
|
"Latin---Mambo",
|
||||||
|
"Latin---Mariachi",
|
||||||
|
"Latin---Merengue",
|
||||||
|
"Latin---Norte\u00f1o",
|
||||||
|
"Latin---Nueva Cancion",
|
||||||
|
"Latin---Pachanga",
|
||||||
|
"Latin---Porro",
|
||||||
|
"Latin---Ranchera",
|
||||||
|
"Latin---Reggaeton",
|
||||||
|
"Latin---Rumba",
|
||||||
|
"Latin---Salsa",
|
||||||
|
"Latin---Samba",
|
||||||
|
"Latin---Son",
|
||||||
|
"Latin---Son Montuno",
|
||||||
|
"Latin---Tango",
|
||||||
|
"Latin---Tejano",
|
||||||
|
"Latin---Vallenato",
|
||||||
|
"Non-Music---Audiobook",
|
||||||
|
"Non-Music---Comedy",
|
||||||
|
"Non-Music---Dialogue",
|
||||||
|
"Non-Music---Education",
|
||||||
|
"Non-Music---Field Recording",
|
||||||
|
"Non-Music---Interview",
|
||||||
|
"Non-Music---Monolog",
|
||||||
|
"Non-Music---Poetry",
|
||||||
|
"Non-Music---Political",
|
||||||
|
"Non-Music---Promotional",
|
||||||
|
"Non-Music---Radioplay",
|
||||||
|
"Non-Music---Religious",
|
||||||
|
"Non-Music---Spoken Word",
|
||||||
|
"Pop---Ballad",
|
||||||
|
"Pop---Bollywood",
|
||||||
|
"Pop---Bubblegum",
|
||||||
|
"Pop---Chanson",
|
||||||
|
"Pop---City Pop",
|
||||||
|
"Pop---Europop",
|
||||||
|
"Pop---Indie Pop",
|
||||||
|
"Pop---J-pop",
|
||||||
|
"Pop---K-pop",
|
||||||
|
"Pop---Kay\u014dkyoku",
|
||||||
|
"Pop---Light Music",
|
||||||
|
"Pop---Music Hall",
|
||||||
|
"Pop---Novelty",
|
||||||
|
"Pop---Parody",
|
||||||
|
"Pop---Schlager",
|
||||||
|
"Pop---Vocal",
|
||||||
|
"Reggae---Calypso",
|
||||||
|
"Reggae---Dancehall",
|
||||||
|
"Reggae---Dub",
|
||||||
|
"Reggae---Lovers Rock",
|
||||||
|
"Reggae---Ragga",
|
||||||
|
"Reggae---Reggae",
|
||||||
|
"Reggae---Reggae-Pop",
|
||||||
|
"Reggae---Rocksteady",
|
||||||
|
"Reggae---Roots Reggae",
|
||||||
|
"Reggae---Ska",
|
||||||
|
"Reggae---Soca",
|
||||||
|
"Rock---AOR",
|
||||||
|
"Rock---Acid Rock",
|
||||||
|
"Rock---Acoustic",
|
||||||
|
"Rock---Alternative Rock",
|
||||||
|
"Rock---Arena Rock",
|
||||||
|
"Rock---Art Rock",
|
||||||
|
"Rock---Atmospheric Black Metal",
|
||||||
|
"Rock---Avantgarde",
|
||||||
|
"Rock---Beat",
|
||||||
|
"Rock---Black Metal",
|
||||||
|
"Rock---Blues Rock",
|
||||||
|
"Rock---Brit Pop",
|
||||||
|
"Rock---Classic Rock",
|
||||||
|
"Rock---Coldwave",
|
||||||
|
"Rock---Country Rock",
|
||||||
|
"Rock---Crust",
|
||||||
|
"Rock---Death Metal",
|
||||||
|
"Rock---Deathcore",
|
||||||
|
"Rock---Deathrock",
|
||||||
|
"Rock---Depressive Black Metal",
|
||||||
|
"Rock---Doo Wop",
|
||||||
|
"Rock---Doom Metal",
|
||||||
|
"Rock---Dream Pop",
|
||||||
|
"Rock---Emo",
|
||||||
|
"Rock---Ethereal",
|
||||||
|
"Rock---Experimental",
|
||||||
|
"Rock---Folk Metal",
|
||||||
|
"Rock---Folk Rock",
|
||||||
|
"Rock---Funeral Doom Metal",
|
||||||
|
"Rock---Funk Metal",
|
||||||
|
"Rock---Garage Rock",
|
||||||
|
"Rock---Glam",
|
||||||
|
"Rock---Goregrind",
|
||||||
|
"Rock---Goth Rock",
|
||||||
|
"Rock---Gothic Metal",
|
||||||
|
"Rock---Grindcore",
|
||||||
|
"Rock---Grunge",
|
||||||
|
"Rock---Hard Rock",
|
||||||
|
"Rock---Hardcore",
|
||||||
|
"Rock---Heavy Metal",
|
||||||
|
"Rock---Indie Rock",
|
||||||
|
"Rock---Industrial",
|
||||||
|
"Rock---Krautrock",
|
||||||
|
"Rock---Lo-Fi",
|
||||||
|
"Rock---Lounge",
|
||||||
|
"Rock---Math Rock",
|
||||||
|
"Rock---Melodic Death Metal",
|
||||||
|
"Rock---Melodic Hardcore",
|
||||||
|
"Rock---Metalcore",
|
||||||
|
"Rock---Mod",
|
||||||
|
"Rock---Neofolk",
|
||||||
|
"Rock---New Wave",
|
||||||
|
"Rock---No Wave",
|
||||||
|
"Rock---Noise",
|
||||||
|
"Rock---Noisecore",
|
||||||
|
"Rock---Nu Metal",
|
||||||
|
"Rock---Oi",
|
||||||
|
"Rock---Parody",
|
||||||
|
"Rock---Pop Punk",
|
||||||
|
"Rock---Pop Rock",
|
||||||
|
"Rock---Pornogrind",
|
||||||
|
"Rock---Post Rock",
|
||||||
|
"Rock---Post-Hardcore",
|
||||||
|
"Rock---Post-Metal",
|
||||||
|
"Rock---Post-Punk",
|
||||||
|
"Rock---Power Metal",
|
||||||
|
"Rock---Power Pop",
|
||||||
|
"Rock---Power Violence",
|
||||||
|
"Rock---Prog Rock",
|
||||||
|
"Rock---Progressive Metal",
|
||||||
|
"Rock---Psychedelic Rock",
|
||||||
|
"Rock---Psychobilly",
|
||||||
|
"Rock---Pub Rock",
|
||||||
|
"Rock---Punk",
|
||||||
|
"Rock---Rock & Roll",
|
||||||
|
"Rock---Rockabilly",
|
||||||
|
"Rock---Shoegaze",
|
||||||
|
"Rock---Ska",
|
||||||
|
"Rock---Sludge Metal",
|
||||||
|
"Rock---Soft Rock",
|
||||||
|
"Rock---Southern Rock",
|
||||||
|
"Rock---Space Rock",
|
||||||
|
"Rock---Speed Metal",
|
||||||
|
"Rock---Stoner Rock",
|
||||||
|
"Rock---Surf",
|
||||||
|
"Rock---Symphonic Rock",
|
||||||
|
"Rock---Technical Death Metal",
|
||||||
|
"Rock---Thrash",
|
||||||
|
"Rock---Twist",
|
||||||
|
"Rock---Viking Metal",
|
||||||
|
"Rock---Y\u00e9-Y\u00e9",
|
||||||
|
"Stage & Screen---Musical",
|
||||||
|
"Stage & Screen---Score",
|
||||||
|
"Stage & Screen---Soundtrack",
|
||||||
|
"Stage & Screen---Theme"
|
||||||
|
],
|
||||||
|
"model_types": [
|
||||||
|
"frozen_model",
|
||||||
|
"SavedModel",
|
||||||
|
"onnx"
|
||||||
|
],
|
||||||
|
"dataset": {
|
||||||
|
"name": "Discogs-4M (unreleased)",
|
||||||
|
"citation": "In-house dataset",
|
||||||
|
"size": "4M full tracks (3.3M used)",
|
||||||
|
"metrics": {
|
||||||
|
"ROC-AUC": 0.95417,
|
||||||
|
"PR-AUC": 0.20629
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "serving_default_model_Placeholder",
|
||||||
|
"type": "float",
|
||||||
|
"shape": [
|
||||||
|
"batch_size",
|
||||||
|
1280
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "PartitionedCall:0",
|
||||||
|
"type": "float",
|
||||||
|
"shape": [
|
||||||
|
"batch_size",
|
||||||
|
400
|
||||||
|
],
|
||||||
|
"op": "Sigmoid",
|
||||||
|
"output_purpose": "predictions"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"citation": "@inproceedings{alonso2022music,\n title={Music Representation Learning Based on Editorial Metadata from Discogs},\n author={Alonso-Jim{\\'e}nez, Pablo and Serra, Xavier and Bogdanov, Dmitry},\n booktitle={Conference of the International Society for Music Information Retrieval (ISMIR)},\n year={2022}\n}",
|
||||||
|
"inference": {
|
||||||
|
"sample_rate": 16000,
|
||||||
|
"algorithm": "TensorflowPredict2D",
|
||||||
|
"embedding_model": {
|
||||||
|
"algorithm": "TensorflowPredictEffnetDiscogs",
|
||||||
|
"model_name": "discogs-effnet-bs64-1",
|
||||||
|
"link": "https://essentia.upf.edu/models/music-style-classification/discogs-effnet/discogs-effnet-bs64-1.pb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
backend/models/genre_discogs400-discogs-effnet-1.pb
Normal file
BIN
backend/models/genre_discogs400-discogs-effnet-1.pb
Normal file
Binary file not shown.
BIN
backend/models/mtg_jamendo_genre-discogs-effnet-1.pb
Normal file
BIN
backend/models/mtg_jamendo_genre-discogs-effnet-1.pb
Normal file
Binary file not shown.
BIN
backend/models/mtg_jamendo_instrument-discogs-effnet-1.pb
Normal file
BIN
backend/models/mtg_jamendo_instrument-discogs-effnet-1.pb
Normal file
Binary file not shown.
BIN
backend/models/mtg_jamendo_moodtheme-discogs-effnet-1.pb
Normal file
BIN
backend/models/mtg_jamendo_moodtheme-discogs-effnet-1.pb
Normal file
Binary file not shown.
@@ -27,6 +27,10 @@ pydantic==2.5.3
|
|||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
"""FastAPI main application."""
|
"""FastAPI main application."""
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Depends
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from ..utils.config import settings
|
from ..utils.config import settings
|
||||||
from ..utils.logging import setup_logging, get_logger
|
from ..utils.logging import setup_logging, get_logger
|
||||||
from ..models.database import engine, Base
|
from ..models.database import engine, Base
|
||||||
|
from ..core.auth import require_auth
|
||||||
|
|
||||||
# Import routes
|
# Import routes
|
||||||
from .routes import tracks, search, audio, analyze, similar, stats, library
|
from .routes import tracks, search, audio, analyze, similar, stats, library, auth
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
setup_logging()
|
setup_logging()
|
||||||
@@ -62,13 +63,17 @@ async def health_check():
|
|||||||
|
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(tracks.router, prefix="/api/tracks", tags=["tracks"])
|
# Auth endpoints (public - no auth required)
|
||||||
app.include_router(search.router, prefix="/api/search", tags=["search"])
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||||
app.include_router(audio.router, prefix="/api/audio", tags=["audio"])
|
|
||||||
app.include_router(analyze.router, prefix="/api/analyze", tags=["analyze"])
|
# Protected endpoints (auth required for ALL routes)
|
||||||
app.include_router(similar.router, prefix="/api", tags=["similar"])
|
app.include_router(tracks.router, prefix="/api/tracks", tags=["tracks"], dependencies=[Depends(require_auth)])
|
||||||
app.include_router(stats.router, prefix="/api/stats", tags=["stats"])
|
app.include_router(search.router, prefix="/api/search", tags=["search"], dependencies=[Depends(require_auth)])
|
||||||
app.include_router(library.router, prefix="/api/library", tags=["library"])
|
app.include_router(audio.router, prefix="/api/audio", tags=["audio"], dependencies=[Depends(require_auth)])
|
||||||
|
app.include_router(analyze.router, prefix="/api/analyze", tags=["analyze"], dependencies=[Depends(require_auth)])
|
||||||
|
app.include_router(similar.router, prefix="/api", tags=["similar"], dependencies=[Depends(require_auth)])
|
||||||
|
app.include_router(stats.router, prefix="/api/stats", tags=["stats"], dependencies=[Depends(require_auth)])
|
||||||
|
app.include_router(library.router, prefix="/api/library", tags=["library"], dependencies=[Depends(require_auth)])
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", tags=["root"])
|
@app.get("/", tags=["root"])
|
||||||
|
|||||||
82
backend/src/api/routes/auth.py
Normal file
82
backend/src/api/routes/auth.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Authentication endpoints."""
|
||||||
|
from datetime import timedelta
|
||||||
|
from fastapi import APIRouter, HTTPException, status, Depends
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
|
||||||
|
from ...core.auth import authenticate_user, create_access_token, get_current_user
|
||||||
|
from ...utils.config import settings
|
||||||
|
from ...utils.logging import get_logger
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
"""Login request model."""
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class LoginResponse(BaseModel):
|
||||||
|
"""Login response model."""
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
user: dict
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
"""User response model."""
|
||||||
|
email: str
|
||||||
|
role: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=LoginResponse)
|
||||||
|
async def login(request: LoginRequest):
|
||||||
|
"""Authenticate user and return JWT token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Login credentials
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Access token and user info
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: 401 if credentials are invalid
|
||||||
|
"""
|
||||||
|
user = authenticate_user(request.email, request.password)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
logger.warning(f"Failed login attempt for: {request.email}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect email or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create access token
|
||||||
|
access_token_expires = timedelta(hours=settings.JWT_EXPIRATION_HOURS)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user["email"], "role": user["role"]},
|
||||||
|
expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"User logged in: {user['email']}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": user
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserResponse)
|
||||||
|
async def get_me(current_user: dict = Depends(get_current_user)):
|
||||||
|
"""Get current authenticated user info.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_user: Current user from JWT token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User information
|
||||||
|
"""
|
||||||
|
return current_user
|
||||||
151
backend/src/core/auth.py
Normal file
151
backend/src/core/auth.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""Authentication utilities."""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from fastapi import HTTPException, status, Depends
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
|
||||||
|
from ..utils.config import settings
|
||||||
|
from ..utils.logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# Password hashing
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
# HTTP Bearer for JWT
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""Verify a password against its hash.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plain_password: Plain text password
|
||||||
|
hashed_password: Hashed password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if password matches
|
||||||
|
"""
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""Hash a password.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
password: Plain text password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Hashed password
|
||||||
|
"""
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||||
|
"""Create JWT access token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Data to encode in token
|
||||||
|
expires_delta: Token expiration time
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JWT token string
|
||||||
|
"""
|
||||||
|
to_encode = data.copy()
|
||||||
|
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(hours=settings.JWT_EXPIRATION_HOURS)
|
||||||
|
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm="HS256")
|
||||||
|
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def verify_token(token: str) -> dict:
|
||||||
|
"""Verify and decode JWT token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: JWT token string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decoded token payload
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If token is invalid
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=["HS256"])
|
||||||
|
return payload
|
||||||
|
except JWTError as e:
|
||||||
|
logger.error(f"Token verification failed: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_user(email: str, password: str) -> Optional[dict]:
|
||||||
|
"""Authenticate user with email and password.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
email: User email
|
||||||
|
password: User password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User data if authenticated, None otherwise
|
||||||
|
"""
|
||||||
|
# Check against admin credentials from environment
|
||||||
|
if email == settings.ADMIN_EMAIL and password == settings.ADMIN_PASSWORD:
|
||||||
|
return {
|
||||||
|
"email": email,
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
|
||||||
|
"""Get current authenticated user from JWT token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
credentials: HTTP Bearer credentials
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User data from token
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If authentication fails
|
||||||
|
"""
|
||||||
|
token = credentials.credentials
|
||||||
|
payload = verify_token(token)
|
||||||
|
|
||||||
|
email: str = payload.get("sub")
|
||||||
|
if email is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"email": email,
|
||||||
|
"role": payload.get("role", "user")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def require_auth(current_user: dict = Depends(get_current_user)) -> dict:
|
||||||
|
"""Dependency to require authentication.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_user: Current user from get_current_user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current user data
|
||||||
|
"""
|
||||||
|
return current_user
|
||||||
@@ -21,6 +21,12 @@ class Settings(BaseSettings):
|
|||||||
ESSENTIA_MODELS_PATH: str = "./models"
|
ESSENTIA_MODELS_PATH: str = "./models"
|
||||||
AUDIO_LIBRARY_PATH: str = "/audio"
|
AUDIO_LIBRARY_PATH: str = "/audio"
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
ADMIN_EMAIL: str = "admin@example.com"
|
||||||
|
ADMIN_PASSWORD: str = "changeme"
|
||||||
|
JWT_SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||||
|
JWT_EXPIRATION_HOURS: int = 24
|
||||||
|
|
||||||
# Application
|
# Application
|
||||||
APP_NAME: str = "Audio Classifier API"
|
APP_NAME: str = "Audio Classifier API"
|
||||||
APP_VERSION: str = "1.0.0"
|
APP_VERSION: str = "1.0.0"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next"
|
|||||||
import { Inter } from "next/font/google"
|
import { Inter } from "next/font/google"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
import { QueryProvider } from "@/components/providers/QueryProvider"
|
import { QueryProvider } from "@/components/providers/QueryProvider"
|
||||||
|
import AuthGuard from "@/components/AuthGuard"
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] })
|
const inter = Inter({ subsets: ["latin"] })
|
||||||
@@ -23,7 +24,9 @@ export default function RootLayout({
|
|||||||
</head>
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
|
<AuthGuard>
|
||||||
{children}
|
{children}
|
||||||
|
</AuthGuard>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
124
frontend/app/login/page.tsx
Normal file
124
frontend/app/login/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { getApiUrl } from "@/lib/api"
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError("")
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getApiUrl()}/api/auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
throw new Error(data.detail || "Login failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Store token in localStorage
|
||||||
|
localStorage.setItem("access_token", data.access_token)
|
||||||
|
localStorage.setItem("user", JSON.stringify(data.user))
|
||||||
|
|
||||||
|
// Redirect to home
|
||||||
|
router.push("/")
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Login failed")
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||||||
|
<div className="max-w-md w-full mx-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-2xl p-8">
|
||||||
|
{/* Logo/Title */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Audio Classifier
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">Sign in to continue</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error message */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-md text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Login form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="admin@example.com"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors disabled:bg-blue-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isLoading ? "Signing in..." : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<p className="text-center text-gray-400 text-sm mt-6">
|
||||||
|
Audio Classifier v1.0.0
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useMemo } from "react"
|
import { useState, useMemo } from "react"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { getTracks, getApiUrl } from "@/lib/api"
|
import { getTracks, getApiUrl } from "@/lib/api"
|
||||||
|
import { logout, getUser } from "@/lib/auth"
|
||||||
import type { FilterParams, Track } from "@/lib/types"
|
import type { FilterParams, Track } from "@/lib/types"
|
||||||
import FilterPanel from "@/components/FilterPanel"
|
import FilterPanel from "@/components/FilterPanel"
|
||||||
import AudioPlayer from "@/components/AudioPlayer"
|
import AudioPlayer from "@/components/AudioPlayer"
|
||||||
@@ -160,6 +161,18 @@ export default function Home() {
|
|||||||
{tracksData?.total || 0} piste{(tracksData?.total || 0) > 1 ? 's' : ''}
|
{tracksData?.total || 0} piste{(tracksData?.total || 0) > 1 ? 's' : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Logout button */}
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="px-3 py-2 text-sm text-slate-600 hover:text-slate-900 hover:bg-slate-100 rounded-lg transition-colors flex items-center gap-2"
|
||||||
|
title="Déconnexion"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Rescan button */}
|
{/* Rescan button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleRescan}
|
onClick={handleRescan}
|
||||||
|
|||||||
37
frontend/components/AuthGuard.tsx
Normal file
37
frontend/components/AuthGuard.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useRouter, usePathname } from "next/navigation"
|
||||||
|
import { isAuthenticated } from "@/lib/auth"
|
||||||
|
|
||||||
|
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const [isChecking, setIsChecking] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip auth check for login page
|
||||||
|
if (pathname === "/login") {
|
||||||
|
setIsChecking(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
router.push("/login")
|
||||||
|
} else {
|
||||||
|
setIsChecking(false)
|
||||||
|
}
|
||||||
|
}, [pathname, router])
|
||||||
|
|
||||||
|
// Show loading while checking auth
|
||||||
|
if (isChecking && pathname !== "/login") {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
<div className="text-white">Loading...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
@@ -24,12 +24,38 @@ export function getApiUrl(): string {
|
|||||||
|
|
||||||
// Create axios instance dynamically to use runtime config
|
// Create axios instance dynamically to use runtime config
|
||||||
function getApiClient() {
|
function getApiClient() {
|
||||||
return axios.create({
|
const client = axios.create({
|
||||||
baseURL: getApiUrl(),
|
baseURL: getApiUrl(),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add JWT token to requests if available
|
||||||
|
client.interceptors.request.use((config) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle 401 errors (redirect to login)
|
||||||
|
client.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401 && typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracks
|
// Tracks
|
||||||
|
|||||||
34
frontend/lib/auth.ts
Normal file
34
frontend/lib/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Authentication utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getToken(): string | null {
|
||||||
|
if (typeof window === "undefined") return null
|
||||||
|
return localStorage.getItem("access_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token: string): void {
|
||||||
|
localStorage.setItem("access_token", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken(): void {
|
||||||
|
localStorage.removeItem("access_token")
|
||||||
|
localStorage.removeItem("user")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUser(): any | null {
|
||||||
|
if (typeof window === "undefined") return null
|
||||||
|
const user = localStorage.getItem("user")
|
||||||
|
return user ? JSON.parse(user) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAuthenticated(): boolean {
|
||||||
|
return getToken() !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout(): void {
|
||||||
|
removeToken()
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.location.href = "/login"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
frontend/middleware.ts
Normal file
20
frontend/middleware.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
// Middleware runs on server, can't access localStorage
|
||||||
|
// Auth check will be done client-side in layout.tsx
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
*/
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user