From 8f5fe7534dd943f517cf4010111bbc21773ce35b Mon Sep 17 00:00:00 2001 From: glenneth Date: Sun, 21 Dec 2025 09:07:56 +0300 Subject: [PATCH] feat: Add avatar upload and fix authentication errors Avatars: - Add avatar_path column to USERS table (migration 006) - Upload API endpoint /api/asteroid/user/avatar/upload - Profile page shows avatar with hover-to-change overlay - Default SVG avatar for users without uploaded image - Avatars stored in static/avatars/ directory Fixes: - 401 errors now return proper JSON instead of 500 - SQL escaping for history recording (single quotes) - Added debug logging for history/record API - Avatar container has background color for visibility For production: run migrations/006-user-avatars.sql --- migrations/006-user-avatars.sql | 14 ++ parenscript/profile.lisp | 42 ++++- playlists/stream-queue.m3u | 282 +++++++++++++++++++++++--------- static/asteroid.css | 51 ++++++ static/asteroid.lass | 41 +++++ static/avatars/.gitkeep | 0 static/avatars/1.png | Bin 0 -> 14757 bytes static/icons/default-avatar.svg | 5 + template/profile.ctml | 41 +++-- user-management.lisp | 8 +- user-profile.lisp | 79 ++++++++- 11 files changed, 459 insertions(+), 104 deletions(-) create mode 100644 migrations/006-user-avatars.sql create mode 100644 static/avatars/.gitkeep create mode 100644 static/avatars/1.png create mode 100644 static/icons/default-avatar.svg diff --git a/migrations/006-user-avatars.sql b/migrations/006-user-avatars.sql new file mode 100644 index 0000000..82aeea0 --- /dev/null +++ b/migrations/006-user-avatars.sql @@ -0,0 +1,14 @@ +-- Migration 006: User Avatars +-- Adds avatar support to user profiles + +-- Add avatar_path column to USERS table +ALTER TABLE "USERS" ADD COLUMN IF NOT EXISTS avatar_path TEXT; + +-- Grant permissions +GRANT ALL PRIVILEGES ON "USERS" TO asteroid; + +-- Verification +DO $$ +BEGIN + RAISE NOTICE 'Migration 006: User avatars column added successfully!'; +END $$; diff --git a/parenscript/profile.lisp b/parenscript/profile.lisp index cbcc146..a6da081 100644 --- a/parenscript/profile.lisp +++ b/parenscript/profile.lisp @@ -232,6 +232,45 @@ (defun load-more-favorites () (show-message "Loading more favorites..." "info")) + (defun load-avatar () + (ps:chain + (fetch "/api/asteroid/user/avatar") + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (when (and (= (ps:@ data status) "success") + (ps:@ data avatar_path)) + (let ((img (ps:chain document (get-element-by-id "user-avatar")))) + (when img + (setf (ps:@ img src) (ps:@ data avatar_path)))))))) + (catch (lambda (error) + (ps:chain console (log "No avatar set or error loading:" error)))))) + + (defun upload-avatar (input) + (let ((file (ps:getprop (ps:@ input files) 0))) + (when file + (let ((form-data (ps:new (-form-data)))) + (ps:chain form-data (append "avatar" file)) + (ps:chain form-data (append "filename" (ps:@ file name))) + (show-message "Uploading avatar..." "info") + (ps:chain + (fetch "/api/asteroid/user/avatar/upload" + (ps:create :method "POST" + :body form-data)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (if (= (ps:@ data status) "success") + (progn + (let ((img (ps:chain document (get-element-by-id "user-avatar")))) + (when img + (setf (ps:@ img src) (+ (ps:@ data avatar_path) "?" (ps:chain -date (now)))))) + (show-message "Avatar updated!" "success")) + (show-message "Failed to upload avatar" "error"))))) + (catch (lambda (error) + (ps:chain console (error "Error uploading avatar:" error)) + (show-message "Error uploading avatar" "error")))))))) + (defun load-activity-chart () (ps:chain (fetch "/api/asteroid/user/activity?days=30") @@ -303,7 +342,8 @@ (load-recent-tracks) (load-favorites) (load-top-artists) - (load-activity-chart)) + (load-activity-chart) + (load-avatar)) ;; Action functions (defun load-more-recent-tracks () diff --git a/playlists/stream-queue.m3u b/playlists/stream-queue.m3u index b1180e1..0f84bed 100644 --- a/playlists/stream-queue.m3u +++ b/playlists/stream-queue.m3u @@ -1,83 +1,207 @@ #EXTM3U -#PLAYLIST:Midnight Ambient -#PHASE:Midnight Ambient +#PLAYLIST:Morning Drift +#PHASE:Morning Drift #DURATION:6 hours (approx) #CURATOR:Asteroid Radio -#DESCRIPTION:Deep, dark ambient for the late night hours (00:00-06:00) +#DESCRIPTION:Lighter, awakening ambient for the morning hours (06:00-12:00) -#EXTINF:-1,Biosphere - The Petrified Forest -/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/04. Biosphere - The Petrified Forest.flac -#EXTINF:-1,Labradford - S -/app/music/Labradford/1997 - Mi Media Naranja/1 S.flac -#EXTINF:-1,Tim Hecker - Seasick -/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 01 Seasick.flac -#EXTINF:-1,Pye Corner Audio - Hollow Earth -/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/01 - Hollow Earth.mp3 -#EXTINF:-1,Dead Voices On Air - Red Howls -/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/01 - Red Howls.flac -#EXTINF:-1,Tangerine Dream - The Seventh Propellor of Silence -/app/music/Tangerine Dream - Ambient Monkeys (flac)/03 Tangerine Dream - The Seventh Propellor of Silence.flac -#EXTINF:-1,Biosphere - Fall Asleep For Me -/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/17 - Fall Asleep For Me.flac -#EXTINF:-1,Locrian - Arc of Extinction -/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/01 - Arc of Extinction.flac -#EXTINF:-1,FSOL - Polarize -/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/01 - Polarize.flac -#EXTINF:-1,Marconi Union - Sleeper -/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/01. Marconi Union - Sleeper.flac -#EXTINF:-1,Labradford - G -/app/music/Labradford/1997 - Mi Media Naranja/2 G.flac -#EXTINF:-1,Biosphere - This Is The End -/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/06. Biosphere - This Is The End.flac -#EXTINF:-1,Pye Corner Audio - Descent -/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/02 - Descent.mp3 -#EXTINF:-1,Brian Eno - Reflection -/app/music/Brian Eno/2017 - Reflection/01. Reflection.mp3 -#EXTINF:-1,Dead Voices On Air - On Winters Gibbet -/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/02 - On Winters Gibbet.flac -#EXTINF:-1,Tim Hecker - Left On The Ice -/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 05 Left On The Ice.flac -#EXTINF:-1,Autechre - Further -/app/music/Autechre/1994 - Amber/08 Further.flac -#EXTINF:-1,Tangerine Dream - Moon Marble -/app/music/Tangerine Dream - Ambient Monkeys (flac)/06 Tangerine Dream - Moon Marble.flac -#EXTINF:-1,Biosphere - Sweet Dreams Form A Shade -/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/07 - Sweet Dreams Form A Shade.flac -#EXTINF:-1,Locrian - Dark Shales -/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/02 - Dark Shales.flac -#EXTINF:-1,FSOL - Forest Soundbed -/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/06 - Forest Soundbed.flac -#EXTINF:-1,Pye Corner Audio - Subterranean Lakes -/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/09 - Subterranean Lakes.mp3 -#EXTINF:-1,Labradford - WR -/app/music/Labradford/1997 - Mi Media Naranja/3 WR.flac -#EXTINF:-1,Bark Psychosis - Hex -/app/music/Bark Psychosis/1994 - Hex/01 The Loom.flac -#EXTINF:-1,Biosphere - Turned To Stone -/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/03. Biosphere - Turned To Stone.flac -#EXTINF:-1,Tim Hecker - Winter's Coming -/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 10 Winter's Coming.flac -#EXTINF:-1,Dead Voices On Air - On Wicca Way -/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/03 - On Wicca Way.flac -#EXTINF:-1,Marconi Union - Abandoned - In Silence -/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/03. Marconi Union - Abandoned - In Silence.flac -#EXTINF:-1,Pye Corner Audio - Deeper Dreaming -/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/13 - Deeper Dreaming.mp3 -#EXTINF:-1,Tangerine Dream - Myopia World -/app/music/Tangerine Dream - Ambient Monkeys (flac)/12 Tangerine Dream - Myopia World.flac -#EXTINF:-1,Biosphere - Departed Glories -/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/09 - Departed Glories.flac -#EXTINF:-1,Locrian - The Future of Death -/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/04 - The Future of Death.flac -#EXTINF:-1,Labradford - V -/app/music/Labradford/1997 - Mi Media Naranja/6 V.flac -#EXTINF:-1,FSOL - Solace -/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/23 - Solace.flac -#EXTINF:-1,Pye Corner Audio - Buried Memories -/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/12 - Buried Memories.mp3 -#EXTINF:-1,Tim Hecker - Twinkle In The Wasteland -/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 14 Twinkle In The Wasteland.flac -#EXTINF:-1,Locrian - Heavy Water -/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/08 - Heavy Water.flac -#EXTINF:-1,Tape Loop Orchestra - 1953 Culture Festival -/app/music/Tape Loop Orchestra/2009 - 1953 Culture Festival/01 - 1953 Culture Festival.flac +#EXTINF:-1,Brian Eno - Emerald And Lime +/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A1 Emerald And Lime.flac +#EXTINF:-1,Tycho - Glider +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/01 - Glider.flac +#EXTINF:-1,Biosphere - Drifter +/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/01. Biosphere - Drifter.flac +#EXTINF:-1,Four Tet - Alap +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/01 Alap.flac +#EXTINF:-1,Johann Johannsson - Cambridge, 1963 +/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/01 - Cambridge, 1963.flac +#EXTINF:-1,Ulrich Schnauss - Negative Sunrise (2019 Version) +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/09. Negative Sunrise (2019 Version).flac +#EXTINF:-1,Kiasmos - Lit +/app/music/Kiasmos/2014 - Kiasmos/01 - Lit.flac +#EXTINF:-1,FSOL - Mountain Path +/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/02 - Mountain Path.flac +#EXTINF:-1,Brian Eno - Garden of Stars +/app/music/Brian Eno/2022 - ForeverAndEverNoMore/04 Garden of Stars.flac +#EXTINF:-1,Clark - Kiri's Glee +/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/04 - Kiri's Glee.flac +#EXTINF:-1,Tycho - Source +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/07 - Source.flac +#EXTINF:-1,Biosphere - Out Of The Cradle +/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/01 - Out Of The Cradle.flac +#EXTINF:-1,Tangerine Dream - Token from Birdland +/app/music/Tangerine Dream - Ambient Monkeys (flac)/01 Tangerine Dream - Token from Birdland.flac +#EXTINF:-1,Four Tet - Scientists +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/06 Scientists.flac +#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Solitary Falling +/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/03. Solitary Falling.flac +#EXTINF:-1,Proem - Modern Rope +/app/music/Proem - 2018 Modern Rope (WEB)/05. Modern Rope.flac +#EXTINF:-1,Johann Johannsson - Rowing +/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/02 - Rowing.flac +#EXTINF:-1,Kiasmos - Held +/app/music/Kiasmos/2014 - Kiasmos/02 - Held.flac +#EXTINF:-1,Brian Eno - Complex Heaven +/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A2 Complex Heaven.flac +#EXTINF:-1,Biosphere - Skålbrekka +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/01 - Skålbrekka.flac +#EXTINF:-1,FSOL - Thought Pattern +/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/03 - Thought Pattern.flac +#EXTINF:-1,Tycho - Into The Woods +/app/music/Tycho - Simulcast (2020) [WEB FLAC]/04 - Into The Woods.flac +#EXTINF:-1,arovane - hymn +/app/music/arovane - Wirkung (2020) [WEB FLAC16]/12. arovane - hymn.flac +#EXTINF:-1,Four Tet - Green +/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/12 - Four Tet - Green.flac +#EXTINF:-1,Clark - Primary Pluck +/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/08 - Primary Pluck.flac +#EXTINF:-1,Biosphere - Strandby +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/02 - Strandby.flac +#EXTINF:-1,Johann Johannsson - The Dreams That Stuff Is Made Of +/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/11 - The Dreams That Stuff Is Made Of.flac +#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Polychrome +/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/08. Polychrome.flac +#EXTINF:-1,Kiasmos - Swayed +/app/music/Kiasmos/2014 - Kiasmos/04 - Swayed.flac +#EXTINF:-1,Brian Eno - These Small Noises +/app/music/Brian Eno/2022 - ForeverAndEverNoMore/09 These Small Noises.flac +#EXTINF:-1,Tycho - Ascension +/app/music/Thievery Corporation and Tycho - Fragments Ascension EP (flac)/3. Tycho - Ascension.flac +#EXTINF:-1,FSOL - Imagined Friends +/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/15 - Imagined Friends.flac +#EXTINF:-1,Biosphere - Lysbotn +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/11 - Lysbotn.flac +#EXTINF:-1,Boards of Canada - In a Beautiful Place Out in the Country +/app/music/Boards of Canada/In a Beautiful Place Out in the Country/01 Kid for Today.flac +#EXTINF:-1,Brian Eno - Making Gardens Out of Silence +/app/music/Brian Eno/2022 - ForeverAndEverNoMore/10 Making Gardens Out of Silence.flac +#EXTINF:-1,Tangerine Dream - Virtue Is Its Own Reward +/app/music/Tangerine Dream - Ambient Monkeys (flac)/10 Tangerine Dream - Virtue Is Its Own Reward.flac +#EXTINF:-1,Brian Eno - Small Craft On A Milk Sea +/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A3 Small Craft On A Milk Sea.flac +#EXTINF:-1,Tycho - Horizon +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/02 - Horizon.flac +#EXTINF:-1,Four Tet - Two Thousand And Seventeen +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/02 Two Thousand And Seventeen.flac +#EXTINF:-1,Boards of Canada - Dayvan Cowboy +/app/music/Boards of Canada/Trans Canada Highway/01 - Dayvan Cowboy.mp3 +#EXTINF:-1,Ulrich Schnauss - Melts into Air (2019 Version) +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/01. Melts into Air (2019 Version).flac +#EXTINF:-1,arovane - olopp_eleen +/app/music/arovane - Wirkung (2020) [WEB FLAC16]/01. arovane - olopp_eleen.flac +#EXTINF:-1,Biosphere - Bergsbotn +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/03 - Bergsbotn.flac +#EXTINF:-1,F.S.Blumm & Nils Frahm - Perff +/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/02. F.S.Blumm & Nils Frahm - Perff.flac +#EXTINF:-1,Clark - Simple Homecoming Loop +/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/02 - Simple Homecoming Loop.flac +#EXTINF:-1,Tycho - Slack +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/03 - Slack.flac +#EXTINF:-1,Johann Johannsson - A Game Of Croquet +/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/07 - A Game Of Croquet.flac +#EXTINF:-1,FSOL - Motioned +/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/04 - Motioned.flac +#EXTINF:-1,Brian Eno - Flint March +/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A4 Flint March.flac +#EXTINF:-1,Four Tet - Lush +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/05 Lush.flac +#EXTINF:-1,Boards of Canada - Turquoise Hexagon Sun +/app/music/Boards of Canada/Hi Scores/02 - Turquoise Hexagon Sun.mp3 +#EXTINF:-1,Ulrich Schnauss - Love Grows Out of Thin Air (2019 Version) +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/02. Love Grows Out of Thin Air (2019 Version).flac +#EXTINF:-1,arovane - wirkung +/app/music/arovane - Wirkung (2020) [WEB FLAC16]/02. arovane - wirkung.flac +#EXTINF:-1,Biosphere - Berg +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/05 - Berg.flac +#EXTINF:-1,God is an Astronaut - Komorebi +/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/05. Komorebi.flac +#EXTINF:-1,Tycho - Weather +/app/music/Tycho - Simulcast (2020) [WEB FLAC]/01 - Weather.flac +#EXTINF:-1,Port Blue - Sunset Cruiser +/app/music/Port Blue - The Airship (2007)/04. Sunset Cruiser.flac +#EXTINF:-1,F.S.Blumm & Nils Frahm - Exercising Levitation +/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/07. F.S.Blumm & Nils Frahm - Exercising Levitation.flac +#EXTINF:-1,Four Tet - You Are Loved +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/08 You Are Loved.flac +#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Asteroid 2467 +/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/01. Asteroid 2467.flac +#EXTINF:-1,Boards of Canada - Left Side Drive +/app/music/Boards of Canada/Trans Canada Highway/02 - Left Side Drive.mp3 +#EXTINF:-1,Brian Eno - Who Gives a Thought +/app/music/Brian Eno/2022 - ForeverAndEverNoMore/01 Who Gives a Thought.flac +#EXTINF:-1,arovane - find +/app/music/arovane - Wirkung (2020) [WEB FLAC16]/03. arovane - find.flac +#EXTINF:-1,Tycho - Receiver +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/04 - Receiver.flac +#EXTINF:-1,Johann Johannsson - The Origins Of Time +/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/08 - The Origins Of Time.flac +#EXTINF:-1,FSOL - Lichaen +/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/05 - Lichaen.flac +#EXTINF:-1,Biosphere - Kyle +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/06 - Kyle.flac +#EXTINF:-1,Port Blue - The Grand Staircase +/app/music/Port Blue - The Airship (2007)/03. The Grand Staircase.flac +#EXTINF:-1,Vector Lovers - City Lights From A Train +/app/music/Vector Lovers/2005 - Capsule For One/01 - City Lights From A Train.mp3 +#EXTINF:-1,Four Tet - Teenage Birdsong +/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/04 - Four Tet - Teenage Birdsong.flac +#EXTINF:-1,Ulrich Schnauss - The Magic in You (2019 Version) +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/03. The Magic in You (2019 Version).flac +#EXTINF:-1,Boards of Canada - Oirectine +/app/music/Boards of Canada/Twoism/02 - Oirectine.mp3 +#EXTINF:-1,Clark - Bench +/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/03 - Bench.flac +#EXTINF:-1,Tycho - Alright +/app/music/Tycho - Simulcast (2020) [WEB FLAC]/02 - Alright.flac +#EXTINF:-1,Brian Eno - Lesser Heaven +/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/C3 Lesser Heaven.flac +#EXTINF:-1,arovane - sloon +/app/music/arovane - Wirkung (2020) [WEB FLAC16]/05. arovane - sloon.flac +#EXTINF:-1,God is an Astronaut - Epitaph +/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/01. Epitaph.flac +#EXTINF:-1,F.S.Blumm & Nils Frahm - Silently Sharing +/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/10. F.S.Blumm & Nils Frahm - Silently Sharing.flac +#EXTINF:-1,Biosphere - Fjølhøgget +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/07 - Fjølhøgget.flac +#EXTINF:-1,Port Blue - Over Atlantic City +/app/music/Port Blue - The Airship (2007)/02. Over Atlantic City.flac +#EXTINF:-1,Johann Johannsson - Viva Voce +/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/09 - Viva Voce.flac +#EXTINF:-1,Tangerine Dream - Symphony in A-minor +/app/music/Tangerine Dream - Ambient Monkeys (flac)/02 Tangerine Dream - Symphony in A-minor.flac +#EXTINF:-1,Tycho - Epoch +/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/05 - Epoch.flac +#EXTINF:-1,Four Tet - Memories +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/11 Memories.flac +#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Return To Burlington +/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/02. Return To Burlington.flac +#EXTINF:-1,Boards of Canada - Skyliner +/app/music/Boards of Canada/Trans Canada Highway/04 - Skyliner.mp3 +#EXTINF:-1,Vector Lovers - Melodies And Memory +/app/music/Vector Lovers/2005 - Capsule For One/07 - Melodies And Memory.mp3 +#EXTINF:-1,Brian Eno - Icarus or Blériot +/app/music/Brian Eno/2022 - ForeverAndEverNoMore/03 Icarus or Blériot.flac +#EXTINF:-1,arovane - noondt +/app/music/arovane - Wirkung (2020) [WEB FLAC16]/07. arovane - noondt.flac +#EXTINF:-1,FSOL - Symphony for Halia +/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/14 - Symphony for Halia.flac +#EXTINF:-1,Biosphere - Straumen +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/12 - Straumen.flac +#EXTINF:-1,Port Blue - The Gentle Descent +/app/music/Port Blue - The Airship (2007)/12. The Gentle Descent.flac +#EXTINF:-1,Proem - As They Go +/app/music/Proem/2019 - As They Go/Proem - As They Go - 01 As They Go.flac +#EXTINF:-1,Tycho - Outer Sunset +/app/music/Tycho - Simulcast (2020) [WEB FLAC]/03 - Outer Sunset.flac +#EXTINF:-1,Four Tet - Planet +/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/14 Planet.flac +#EXTINF:-1,Ulrich Schnauss - New Day Starts at Dawn (2019 Version) +/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/07. New Day Starts at Dawn (2019 Version).flac +#EXTINF:-1,Clark - Goodnight Kiri +/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/14 - Goodnight Kiri.flac +#EXTINF:-1,Biosphere - Steinfjord +/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/13 - Steinfjord.flac +#EXTINF:-1,Tangerine Dream - Calyx Calamander +/app/music/Tangerine Dream - Ambient Monkeys (flac)/04 Tangerine Dream - Calyx Calamander.flac +#EXTINF:-1,Vector Lovers - To The Stars +/app/music/Vector Lovers/2005 - Capsule For One/12 - To The Stars.mp3 diff --git a/static/asteroid.css b/static/asteroid.css index fcda789..cf58547 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -1703,6 +1703,57 @@ body.popout-body .status-mini{ font-size: 0.8em; } +.profile-header{ + display: flex; + gap: 30px; + align-items: flex-start; +} + +.avatar-section{ + flex-shrink: 0; +} + +.avatar-container{ + position: relative; + width: 120px; + height: 120px; + border-radius: 50%; + overflow: hidden; + border: 3px solid #00cc00; + cursor: pointer; + background: #1a1a1a; +} + +.avatar-image{ + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-overlay{ + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); + color: #00cc00; + text-align: center; + padding: 8px; + font-size: 0.8em; + opacity: 0; + -moz-transition: opacity 0.2s ease; + -o-transition: opacity 0.2s ease; + -webkit-transition: opacity 0.2s ease; + -ms-transition: opacity 0.2s ease; + transition: opacity 0.2s ease; +} + + + +.avatar-container:hover .avatar-overlay{ + opacity: 1; +} + .activity-chart{ padding: 15px; } diff --git a/static/asteroid.lass b/static/asteroid.lass index 6460b89..5bd0449 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -1360,6 +1360,47 @@ :padding "4px 8px" :font-size "0.8em")) + ;; Avatar styling + (.profile-header + :display "flex" + :gap "30px" + :align-items "flex-start") + + (.avatar-section + :flex-shrink "0") + + (.avatar-container + :position "relative" + :width "120px" + :height "120px" + :border-radius "50%" + :overflow "hidden" + :border "3px solid #00cc00" + :cursor "pointer" + :background "#1a1a1a") + + (.avatar-image + :width "100%" + :height "100%" + :object-fit "cover") + + (.avatar-overlay + :position "absolute" + :bottom "0" + :left "0" + :right "0" + :background "rgba(0, 0, 0, 0.7)" + :color "#00cc00" + :text-align "center" + :padding "8px" + :font-size "0.8em" + :opacity "0" + :transition "opacity 0.2s ease") + + ((:and .avatar-container :hover) + (.avatar-overlay + :opacity "1")) + ;; Activity chart styling (.activity-chart :padding "15px" diff --git a/static/avatars/.gitkeep b/static/avatars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/static/avatars/1.png b/static/avatars/1.png new file mode 100644 index 0000000000000000000000000000000000000000..95e48be0d3a9690b779dde0ac69c973826007818 GIT binary patch literal 14757 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 literal 0 HcmV?d00001 diff --git a/static/icons/default-avatar.svg b/static/icons/default-avatar.svg new file mode 100644 index 0000000..c26c3c5 --- /dev/null +++ b/static/icons/default-avatar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/template/profile.ctml b/template/profile.ctml index f829f43..dc149c3 100644 --- a/template/profile.ctml +++ b/template/profile.ctml @@ -21,22 +21,33 @@

🎧 User Profile

-
-
- Username: - user +
+
+
+ User Avatar +
+ Change +
+
+
-
- Role: - listener -
-
- Member Since: - 2024-01-01 -
-
- Last Active: - Today +
+
+ Username: + user +
+
+ Role: + listener +
+
+ Member Since: + 2024-01-01 +
+
+ Last Active: + Today +
diff --git a/user-management.lisp b/user-management.lisp index 25f3566..c4fb60e 100644 --- a/user-management.lisp +++ b/user-management.lisp @@ -174,12 +174,12 @@ ;; Not authenticated - emit error and signal to stop processing (progn (if is-api-request - ;; API request - emit JSON error with 401 status + ;; API request - return JSON error with 401 status using api-output (progn (format t "Authentication failed - returning JSON 401~%") - (setf (radiance:return-code *response*) 401) - (setf (radiance:content-type *response*) "application/json") - (error 'radiance:request-denied :message "Authentication required")) + (api-output `(("status" . "error") + ("message" . "Authentication required")) + :status 401)) ;; Page request - redirect to login (progn (format t "Authentication failed - redirecting to login~%") diff --git a/user-profile.lisp b/user-profile.lisp index 1ddca88..edcc58c 100644 --- a/user-profile.lisp +++ b/user-profile.lisp @@ -73,6 +73,12 @@ ;;; Listening History - Per-user track play history ;;; ========================================================================== +(defun sql-escape-string (str) + "Escape a string for SQL by doubling single quotes" + (if str + (cl-ppcre:regex-replace-all "'" str "''") + "")) + (defun record-listen (user-id &key track-id track-title (duration 0) (completed nil)) "Record a track listen in user's history. Can use track-id or track-title." (with-db @@ -80,12 +86,12 @@ (postmodern:query (:raw (format nil "INSERT INTO listening_history (\"user-id\", \"track-id\", track_title, \"listen-duration\", completed) VALUES (~a, ~a, ~a, ~a, ~a)" user-id track-id - (if track-title (format nil "$$~a$$" track-title) "NULL") + (if track-title (format nil "'~a'" (sql-escape-string track-title)) "NULL") duration (if completed 1 0)))) (when track-title (postmodern:query - (:raw (format nil "INSERT INTO listening_history (\"user-id\", track_title, \"listen-duration\", completed) VALUES (~a, $$~a$$, ~a, ~a)" - user-id track-title duration (if completed 1 0)))))))) + (:raw (format nil "INSERT INTO listening_history (\"user-id\", track_title, \"listen-duration\", completed) VALUES (~a, '~a', ~a, ~a)" + user-id (sql-escape-string track-title) duration (if completed 1 0)))))))) (defun get-listening-history (user-id &key (limit 20) (offset 0)) "Get user's listening history - works with title-based history" @@ -230,8 +236,10 @@ (parse-integer track-id :junk-allowed t))) (duration-int (if duration (parse-integer duration :junk-allowed t) 0)) (completed-bool (and completed (string-equal completed "true")))) - (record-listen user-id :track-id track-id-int :track-title title - :duration (or duration-int 0) :completed completed-bool) + (format t "Recording listen: user-id=~a title=~a~%" user-id title) + (when (and user-id title) + (record-listen user-id :track-id track-id-int :track-title title + :duration (or duration-int 0) :completed completed-bool)) (api-output `(("status" . "success") ("message" . "Listen recorded")))))) @@ -256,3 +264,64 @@ `(("day" . ,(cdr (assoc :day a))) ("track_count" . ,(cdr (assoc :track-count a))))) activity))))))) + +;;; ========================================================================== +;;; Avatar Management +;;; ========================================================================== + +(defun get-avatars-directory () + "Get the path to the avatars directory" + (merge-pathnames "static/avatars/" (asdf:system-source-directory :asteroid))) + +(defun save-avatar (user-id temp-file-path original-filename) + "Save an avatar file from temp path and return the relative path" + (let* ((extension (pathname-type original-filename)) + (safe-ext (if (member extension '("png" "jpg" "jpeg" "gif" "webp") :test #'string-equal) + extension + "png")) + (new-filename (format nil "~a.~a" user-id safe-ext)) + (full-path (merge-pathnames new-filename (get-avatars-directory))) + (relative-path (format nil "/asteroid/static/avatars/~a" new-filename))) + ;; Copy from temp file to avatars directory + (uiop:copy-file temp-file-path full-path) + ;; Update database + (with-db + (postmodern:query + (:raw (format nil "UPDATE \"USERS\" SET avatar_path = '~a' WHERE _id = ~a" + relative-path user-id)))) + relative-path)) + +(defun get-user-avatar (user-id) + "Get the avatar path for a user" + (with-db + (postmodern:query + (:raw (format nil "SELECT avatar_path FROM \"USERS\" WHERE _id = ~a" user-id)) + :single))) + +(define-api asteroid/user/avatar/upload () () + "Upload a new avatar image" + (require-authentication) + (with-error-handling + (let* ((user-id (session:field "user-id")) + ;; Radiance wraps hunchentoot - post-var returns (path filename content-type) for files + (file-info (radiance:post-var "avatar")) + (temp-path (when (listp file-info) (first file-info))) + (original-name (when (listp file-info) (second file-info)))) + (format t "Avatar upload: file-info=~a temp-path=~a original-name=~a~%" file-info temp-path original-name) + (if (and temp-path (probe-file temp-path)) + (let ((avatar-path (save-avatar user-id temp-path (or original-name "avatar.png")))) + (api-output `(("status" . "success") + ("message" . "Avatar uploaded successfully") + ("avatar_path" . ,avatar-path)))) + (api-output `(("status" . "error") + ("message" . "No file provided")) + :status 400))))) + +(define-api asteroid/user/avatar () () + "Get current user's avatar path" + (require-authentication) + (with-error-handling + (let* ((user-id (session:field "user-id")) + (avatar-path (get-user-avatar user-id))) + (api-output `(("status" . "success") + ("avatar_path" . ,avatar-path))))))