Fix #57: Implement frameset-aware login/logout flow with AJAX navigation
- Add frameset-aware content pages: login-content, admin-content, profile-content - Implement AJAX navigation to prevent audio interruption during page transitions - Add handleLogout() function to logout without breaking frameset - Update login form to submit via AJAX and follow redirects - Add audio keep-alive protection with 500ms interval check - Update logout handler to detect frameset mode and redirect appropriately - Add .gitignore entry for generated asteroid.css The persistent audio player now continues playing through: - Login page loading - Login form submission - Redirect to admin/profile pages - Navigation within admin/profile - Logout process All navigation within frameset mode now uses fetch() to load content without actual page navigation, keeping the audio player frame untouched.
This commit is contained in:
parent
efe993e0c1
commit
3a8827f442
|
|
@ -56,3 +56,6 @@ logs/
|
|||
performance-logs/
|
||||
|
||||
# Temporary files
|
||||
|
||||
# Generated CSS (auto-generated from LASS)
|
||||
static/asteroid.css
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
# Convert JavaScript to Parenscript with Enhanced Spectrum Analyzer
|
||||
|
||||
## Overview
|
||||
|
||||
This PR converts all frontend JavaScript files to Parenscript (Common Lisp that compiles to JavaScript), improving maintainability and code consistency across the Asteroid Radio codebase. Additionally, it includes significant enhancements to the spectrum analyzer with theming, multiple visualization styles, and improved stream reliability.
|
||||
|
||||
## Major Changes
|
||||
|
||||
### 1. Parenscript Conversion
|
||||
|
||||
- **Converted 7 JavaScript files to Parenscript:**
|
||||
- `admin.js` → `parenscript/admin.lisp`
|
||||
- `auth-ui.js` → `parenscript/auth-ui.lisp`
|
||||
- `front-page.js` → `parenscript/front-page.lisp`
|
||||
- `player.js` → `parenscript/player.lisp`
|
||||
- `profile.js` → `parenscript/profile.lisp`
|
||||
- `users.js` → `parenscript/users.lisp`
|
||||
- `recently-played.js` → `parenscript/recently-played.lisp`
|
||||
|
||||
- **Original JavaScript files** renamed to `.js.original` for reference
|
||||
- **Added** `parenscript-utils.lisp` for shared compilation utilities
|
||||
- **Parenscript files are now the source of truth**; JavaScript is generated dynamically on request
|
||||
|
||||
### 2. Stream Reliability Improvements
|
||||
|
||||
- **Automatic reconnection logic** for live stream after long pauses (>10 seconds)
|
||||
- **Improved spectrum analyzer** audio context handling with proper reset functionality
|
||||
- **Fixed stream pause detection** to prevent unnecessary "now playing" updates during paused playback
|
||||
- **Better error handling** for stream stalls and connection issues
|
||||
|
||||
### 3. Spectrum Analyzer Enhancements
|
||||
|
||||
#### Color Themes (6 options)
|
||||
- **Monotone** - Deep blue to cobalt gradient matching site aesthetic
|
||||
- **Green** - Classic bright green gradient (default)
|
||||
- **Blue** - Cyan to deep blue
|
||||
- **Purple** - Magenta to deep purple
|
||||
- **Red** - Bright red to dark red
|
||||
- **Amber** - Orange to brown
|
||||
|
||||
#### Visualization Styles (3 options)
|
||||
- **Bars** - Traditional bar graph (default)
|
||||
- **Wave** - Continuous line/waveform
|
||||
- **Dots** - Particle/dot visualization
|
||||
|
||||
#### UI Features
|
||||
- **Dropdown controls** for real-time theme and style selection
|
||||
- **Persistent preferences** saved to localStorage
|
||||
- **Dynamic border color** that changes to match selected theme
|
||||
- **Dynamic dropdown styling** - dropdown boxes update their colors to match the selected theme
|
||||
- **Mute detection** with visual "MUTED" indicator overlay
|
||||
|
||||
### 4. UX Improvements
|
||||
|
||||
- **Live indicator animation** changed from blinking to smooth pulse (inspired by old MacBook sleep indicator)
|
||||
- **Removed verbose debug logging** from console output
|
||||
- **Cleaner, more professional** user experience throughout
|
||||
|
||||
### 5. Documentation & Scripts
|
||||
|
||||
- **Added** `docs/PARENSCRIPT-EXPERIMENT.org` documenting the conversion process and rationale
|
||||
- **Added helper scripts** for music library management:
|
||||
- `scripts/music-library-tree.py` - Generate library tree structure
|
||||
- `scripts/fix-m3u-paths.py` - Fix playlist paths
|
||||
- `scripts/scan.py` - Library scanning utilities
|
||||
- **Added sample playlists** and playlist documentation
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Parenscript Integration
|
||||
|
||||
- All Parenscript files compile to JavaScript **on-the-fly** via custom route handlers in `asteroid.lisp`
|
||||
- Compilation happens at request time, allowing for rapid development iteration
|
||||
- No build step required for JavaScript changes
|
||||
- Full access to Common Lisp macros and language features for frontend code
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- ✅ Maintains full backward compatibility with existing functionality
|
||||
- ✅ No breaking changes to API or user-facing features
|
||||
- ✅ All existing routes and endpoints remain unchanged
|
||||
- ✅ Original JavaScript preserved as `.original` files for reference
|
||||
|
||||
### Code Quality
|
||||
|
||||
- Removed generated `asteroid.css` from repository (auto-generated on build)
|
||||
- Consistent code style across frontend and backend (all Lisp)
|
||||
- Better type safety and error handling through Lisp's condition system
|
||||
- Easier refactoring and maintenance with unified language
|
||||
|
||||
## Testing
|
||||
|
||||
- ✅ All existing functionality verified working
|
||||
- ✅ Stream reconnection tested with long pauses (>10 seconds)
|
||||
- ✅ Spectrum analyzer themes and styles tested across all pages (front page, player, admin)
|
||||
- ✅ Preferences persistence verified across browser sessions
|
||||
- ✅ Cross-browser compatibility maintained
|
||||
- ✅ Audio context handling tested with multiple audio elements
|
||||
- ✅ Mute detection working correctly
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### For Developers
|
||||
|
||||
- Frontend changes should now be made in `parenscript/*.lisp` files
|
||||
- JavaScript is generated automatically - no need to edit `.js` files
|
||||
- Use `make clean && make` to rebuild after Parenscript changes
|
||||
- Original JavaScript files kept as `.js.original` for reference during transition
|
||||
|
||||
### For Deployment
|
||||
|
||||
- No changes to deployment process
|
||||
- Server generates JavaScript on-the-fly from Parenscript
|
||||
- No additional dependencies beyond existing Common Lisp setup
|
||||
|
||||
## Files Changed
|
||||
|
||||
### Added
|
||||
- `parenscript-utils.lisp` - Parenscript compilation utilities
|
||||
- `parenscript/admin.lisp` - Admin interface logic
|
||||
- `parenscript/auth-ui.lisp` - Authentication UI
|
||||
- `parenscript/front-page.lisp` - Front page and live stream
|
||||
- `parenscript/player.lisp` - Audio player controls
|
||||
- `parenscript/profile.lisp` - User profile management
|
||||
- `parenscript/recently-played.lisp` - Recently played tracks
|
||||
- `parenscript/users.lisp` - User management
|
||||
- `docs/PARENSCRIPT-EXPERIMENT.org` - Documentation
|
||||
- `scripts/*` - Helper scripts for library management
|
||||
|
||||
### Modified
|
||||
- `asteroid.lisp` - Added Parenscript compilation routes, removed debug logging
|
||||
- `frontend-partials.lisp` - Removed debug logging from Icecast stats
|
||||
- `static/asteroid.lass` - Updated live indicator animation
|
||||
- `template/*.ctml` - Added spectrum analyzer controls
|
||||
|
||||
### Renamed
|
||||
- `static/js/admin.js` → `static/js/admin.js.original`
|
||||
- `static/js/auth-ui.js` → `static/js/auth-ui.js.original`
|
||||
- `static/js/front-page.js` → `static/js/front-page.js.original`
|
||||
- `static/js/player.js` → `static/js/player.js.original`
|
||||
- `static/js/profile.js` → `static/js/profile.js.original`
|
||||
- `static/js/users.js` → `static/js/users.js.original`
|
||||
- `static/js/recently-played.js` → `static/js/recently-played.js.original`
|
||||
|
||||
### Deleted
|
||||
- `static/asteroid.css` - Now auto-generated, removed from repository
|
||||
|
||||
## Screenshots
|
||||
|
||||
The spectrum analyzer now features:
|
||||
- Multiple color themes with dynamic border matching
|
||||
- Three distinct visualization styles (bars, wave, dots)
|
||||
- Clean dropdown UI for easy customization
|
||||
- Smooth animations and transitions
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements building on this foundation:
|
||||
- Additional visualization styles (spectrogram, circular, etc.)
|
||||
- More color themes
|
||||
- Customizable color picker for user-defined themes
|
||||
- Visualization presets tied to music genres
|
||||
- WebGL-accelerated rendering for complex visualizations
|
||||
|
||||
## Conclusion
|
||||
|
||||
This PR represents a significant modernization of the Asteroid Radio frontend while maintaining full backward compatibility. The move to Parenscript unifies the codebase under Common Lisp, making it easier to maintain and extend. The enhanced spectrum analyzer provides users with a more engaging and customizable listening experience.
|
||||
|
||||
**Ready to merge** ✅
|
||||
|
|
@ -552,6 +552,61 @@
|
|||
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||
:default-stream-encoding "audio/aac"))
|
||||
|
||||
;; Admin content frame (for frameset mode)
|
||||
(define-page admin-content #@"/admin-content" ()
|
||||
"Admin dashboard content (displayed in content frame)"
|
||||
(require-authentication)
|
||||
(let ((track-count (handler-case
|
||||
(length (dm:get "tracks" (db:query :all)))
|
||||
(error () 0))))
|
||||
(clip:process-to-string
|
||||
(load-template "admin-content")
|
||||
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
||||
:server-status "🟢 Running"
|
||||
:database-status (handler-case
|
||||
(if (db:connected-p) "🟢 Connected" "🔴 Disconnected")
|
||||
(error () "🔴 No Database Backend"))
|
||||
:liquidsoap-status (check-liquidsoap-status)
|
||||
:icecast-status (check-icecast-status)
|
||||
:track-count (format nil "~d" track-count)
|
||||
:library-path "/home/glenn/Projects/Code/asteroid/music/library/"
|
||||
:stream-base-url *stream-base-url*
|
||||
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*))))
|
||||
|
||||
;; Profile content frame (for frameset mode)
|
||||
(define-page profile-content #@"/profile-content" ()
|
||||
"User profile content (displayed in content frame)"
|
||||
(require-authentication)
|
||||
(clip:process-to-string
|
||||
(load-template "profile-content")
|
||||
:title "🎧 admin - Profile | Asteroid Radio"
|
||||
:username "admin"
|
||||
:user-role "admin"
|
||||
:join-date "Unknown"
|
||||
:last-active "Unknown"
|
||||
:total-listen-time "0h 0m"
|
||||
:tracks-played "0"
|
||||
:session-count "0"
|
||||
:favorite-genre "Unknown"
|
||||
:recent-track-1-title ""
|
||||
:recent-track-1-artist ""
|
||||
:recent-track-1-duration ""
|
||||
:recent-track-1-played-at ""
|
||||
:recent-track-2-title ""
|
||||
:recent-track-2-artist ""
|
||||
:recent-track-2-duration ""
|
||||
:recent-track-2-played-at ""
|
||||
:recent-track-3-title ""
|
||||
:recent-track-3-artist ""
|
||||
:recent-track-3-duration ""
|
||||
:recent-track-3-played-at ""
|
||||
:top-artist-1 ""
|
||||
:top-artist-1-plays ""
|
||||
:top-artist-2 ""
|
||||
:top-artist-2-plays ""
|
||||
:top-artist-3 ""
|
||||
:top-artist-3-plays ""))
|
||||
|
||||
;; Configure static file serving for other files
|
||||
;; BUT exclude ParenScript-compiled JS files
|
||||
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
||||
|
|
|
|||
|
|
@ -47,11 +47,61 @@
|
|||
:error-message ""
|
||||
:display-error "display: none;"))))
|
||||
|
||||
;; Login content page for frameset mode
|
||||
(define-page login-content #@"/login-content" ()
|
||||
"User login page for frameset mode"
|
||||
(let ((username (radiance:post-var "username"))
|
||||
(password (radiance:post-var "password")))
|
||||
(if (and username password)
|
||||
;; Handle login form submission
|
||||
(let ((user (authenticate-user username password)))
|
||||
(if user
|
||||
(progn
|
||||
;; Login successful - store user ID in session
|
||||
(format t "Login successful for user: ~a~%" (dm:field user "username"))
|
||||
(handler-case
|
||||
(progn
|
||||
(let* ((user-id (dm:id user))
|
||||
(user-role (dm:field user "role"))
|
||||
(redirect-path (cond
|
||||
;; Admin users
|
||||
((string-equal user-role "admin") "/admin-content")
|
||||
;; Regular users
|
||||
(t "/profile-content"))))
|
||||
(format t "User ID from DB: ~a~%" user-id)
|
||||
(format t "User role: ~a, redirecting to: ~a~%" user-role redirect-path)
|
||||
(setf (session:field "user-id") user-id)
|
||||
(format t "User ID #~a persisted in session.~%" (session:field "user-id"))
|
||||
(radiance:redirect redirect-path)))
|
||||
(error (e)
|
||||
(format t "Session error: ~a~%" e)
|
||||
"Login successful but session error occurred")))
|
||||
;; Login failed - show form with error
|
||||
(progn
|
||||
(format t "Login unsuccessful for user: ~a~%" username)
|
||||
(clip:process-to-string
|
||||
(load-template "login-content")
|
||||
:title "Asteroid Radio - Login"
|
||||
:error-message "Invalid username or password"
|
||||
:display-error "display: block;"))))
|
||||
;; Show login form (no POST data)
|
||||
(clip:process-to-string
|
||||
(load-template "login-content")
|
||||
:title "Asteroid Radio - Login"
|
||||
:error-message ""
|
||||
:display-error "display: none;"))))
|
||||
|
||||
;; Simple logout handler
|
||||
(define-page logout #@"/logout" ()
|
||||
"Handle user logout"
|
||||
(setf (session:field "user-id") nil)
|
||||
(radiance:redirect "/"))
|
||||
;; Check if we're in a frameset by looking at the Referer header
|
||||
(let* ((referer (radiance:header "Referer"))
|
||||
(in-frameset (and referer
|
||||
(or (search "/frameset" referer)
|
||||
(search "/content" referer)
|
||||
(search "-content" referer)))))
|
||||
(radiance:redirect (if in-frameset "/content" "/"))))
|
||||
|
||||
;; API: Get all users (admin only)
|
||||
(define-api asteroid/users () ()
|
||||
|
|
@ -213,3 +263,4 @@
|
|||
("message" . ,(format nil "Could not set user '~a' as ~a."
|
||||
(dm:field user "username")
|
||||
role)))))))))
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
#EXTM3U
|
||||
#PLAYLIST:Escape Velocity - A Christmas Journey Through Space
|
||||
#PHASE:Escape Velocity
|
||||
#DURATION:12 hours (approx)
|
||||
#CURATOR:Asteroid Radio
|
||||
#DESCRIPTION:A festive 12-hour voyage blending Christmas classics with ambient, IDM, and space music for the holiday season
|
||||
|
||||
# === PHASE 1: WINTER AWAKENING (Ambient Beginnings) ===
|
||||
#EXTINF:-1,Brian Eno - Snow
|
||||
/app/music/Brian Eno/2020 - Roger Eno and Brian Eno - Mixing Colours/09 Snow.flac
|
||||
#EXTINF:-1,Brian Eno - Wintergreen
|
||||
/app/music/Brian Eno/2020 - Roger Eno and Brian Eno - Mixing Colours/04 Wintergreen.flac
|
||||
#EXTINF:-1,Proem - Winter Wolves
|
||||
/app/music/Proem - 2018 Modern Rope (WEB)/01. Winter Wolves.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,Biosphere - Drifter
|
||||
/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/01. Biosphere - Drifter.flac
|
||||
#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,Color Therapy - Wintering
|
||||
/app/music/Color Therapy - Mr. Wolf Is Dead (2015) WEB FLAC/12 - Wintering.flac
|
||||
|
||||
# === PHASE 2: CHRISTMAS ARRIVAL (TSO Introduction) ===
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - The Ghosts Of Christmas Eve
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/01 The Ghosts Of Christmas Eve.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas In The Air
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/14 Christmas In The Air.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Canon
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/08 Christmas Canon.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Appalachian Snowfall
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/11 Appalachian Snowfall.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - The Snow Came Down
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/13 The Snow Came Down.flac
|
||||
|
||||
# === PHASE 3: AMBIENT INTERLUDE (Space & Atmosphere) ===
|
||||
#EXTINF:-1,Biosphere - 10 Snurp 1937
|
||||
/app/music/Biosphere - Sound Installations -2000-2009 [FLAC]/Biosphere - Sound Installations -2000-2009- - 10 Snurp 1937.flac
|
||||
#EXTINF:-1,Biosphere - 05 Fluvialmorphologie
|
||||
/app/music/Biosphere - Sound Installations -2000-2009 [FLAC]/Biosphere - Sound Installations -2000-2009- - 05 Fluvialmorphologie.flac
|
||||
#EXTINF:-1,God is an Astronaut - Winter Dusk-Awakening
|
||||
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/03. Winter Dusk-Awakening.flac
|
||||
#EXTINF:-1,Proem - Snow Drifts
|
||||
/app/music/Proem - Twelve Tails-(2021) @FLAC [16-48]/11 - Snow Drifts.flac
|
||||
#EXTINF:-1,Proem - Stick to Music Snowflake
|
||||
/app/music/Proem - Until Here for Years (n5md, 2019) flac/04 - Stick to Music Snowflake.flac
|
||||
#EXTINF:-1,Four Tet - 04 Tremper
|
||||
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/04 Tremper.flac
|
||||
|
||||
# === PHASE 4: CHRISTMAS EVE STORIES ===
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - First Snow (Instrumental)
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/Christmas Eve and Other Stories/04 First Snow (Instrumental).flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - The Silent Nutcracker (Instrumental)
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/Christmas Eve and Other Stories/05 The Silent Nutcracker (Instrumental).flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - A Mad Russian's Christmas (Instrumental)
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/Christmas Eve and Other Stories/06 A Mad Russian's Christmas (Instrumental).flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Eve,Sarajevo 12,24 (Instrumental)
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/Christmas Eve and Other Stories/08 Christmas Eve,Sarajevo 12,24 (Instrumental).flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - This Christmas Day
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/Christmas Eve and Other Stories/14 This Christmas Day.flac
|
||||
|
||||
# === PHASE 5: ELECTRONIC DREAMS (IDM & Ambient) ===
|
||||
#EXTINF:-1,Autechre - NTS Session 1-005-Autechre-carefree counter dronal
|
||||
/app/music/Autechre - 2018 - NTS Session 1/NTS Session 1-005-Autechre-carefree counter dronal.flac
|
||||
#EXTINF:-1,Clark - Living Fantasy
|
||||
/app/music/Clark - Death Peak (2017) [FLAC]/08 - Living Fantasy.flac
|
||||
#EXTINF:-1,Clark - My Machines (Clark Remix)
|
||||
/app/music/Clark - Feast Beast (2013) [24 Bit WEB FLAC] [16-44]/1.17. Battles - My Machines (Clark Remix).flac
|
||||
#EXTINF:-1,Plaid - Dancers
|
||||
/app/music/Plaid - Polymer (2019) [WEB FLAC]/07 - Dancers.flac
|
||||
#EXTINF:-1,Faux Tales - Avalon
|
||||
/app/music/Faux Tales - 2015 - Kairos [FLAC] {Kensai Records KNS006 WEB}/3 - Avalon.flac
|
||||
#EXTINF:-1,Color Therapy - Expect Delays (feat. Ulrich Schnauss)
|
||||
/app/music/Color Therapy - Mr. Wolf Is Dead (2015) WEB FLAC/11 - Expect Delays (feat. Ulrich Schnauss).flac
|
||||
|
||||
# === PHASE 6: THE LOST CHRISTMAS EVE ===
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - The Lost Christmas Eve
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/02 The Lost Christmas Eve.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Dreams
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/03 Christmas Dreams.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Wizards in Winter
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/04 Wizards in Winter.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Concerto
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/07 Christmas Concerto.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Queen Of The Winter Night
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/08 Queen Of The Winter Night.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Nights In Blue
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/09 Christmas Nights In Blue.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Jazz
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/10 Christmas Jazz.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Jam
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/11 Christmas Jam.flac
|
||||
|
||||
# === PHASE 7: CLASSICAL WINTER (Nutcracker & More) ===
|
||||
#EXTINF:-1,Various Artists - Dance of the Sugar-Plum Fairy
|
||||
/app/music/Various Artists - The 50 Darkest Pieces of Classical Music (2011) - FLAC/CD 1/02 - Tchaikovsky - The Nutcracker - Dance of the Sugar-Plum Fairy.flac
|
||||
#EXTINF:-1,Quaeschning and Ulrich Schnauss - Thirst
|
||||
/app/music/Quaeschning and Ulrich Schnauss - Synthwaves (2017) {vista003, GER, CD} [FLAC]/06 - Thirst.flac
|
||||
#EXTINF:-1,Proem - 04. Drawing Room Anguish
|
||||
/app/music/Proem - 2018 Modern Rope (WEB)/04. Drawing Room Anguish.flac
|
||||
#EXTINF:-1,Dead Voices On Air - 07. Dogger Doorlopende Split
|
||||
/app/music/Dead Voices On Air - Frankie Pett En De Onderzeer Boten (2017) web/07. Dogger Doorlopende Split.flac
|
||||
|
||||
# === PHASE 8: WISDOM & REFLECTION ===
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - What Is Christmas
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/13 What Is Christmas.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - The Wisdom Of Snow
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/15 The Wisdom Of Snow.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Bells, Carousels & Time
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/18 Christmas Bells, Carousels & Time.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas Canon Rock
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Lost Christmas Eve/21 Christmas Canon Rock.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Midnight Christmas Eve
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/05 Midnight Christmas Eve.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Dream Child (A Christmas Dream)
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/15 Dream Child (A Christmas Dream).flac
|
||||
|
||||
# === PHASE 9: DEEP SPACE JOURNEY (Extended Ambient) ===
|
||||
#EXTINF:-1,Dead Voices On Air - Red Howls
|
||||
/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/01 - Red Howls.flac
|
||||
#EXTINF:-1,Cut Copy - Airborne
|
||||
/app/music/Cut Copy - Haiku From Zero (2017) [FLAC] {2557864014}/05 - Airborne.flac
|
||||
#EXTINF:-1,Owl City - 01 Hot Air Balloon
|
||||
/app/music/Owl City - Ocean Eyes (Deluxe Edition) [Flac,Cue,Logs]/Disc 2/01 Hot Air Balloon.flac
|
||||
#EXTINF:-1,VA - What Is Loneliness (feat. Danny Claire) [Skylex Radio Edit]
|
||||
/app/music/VA - Melodic Vocal Trance 2017/24. Airborn, Bogdan Vix & KeyPlayer - What Is Loneliness (feat. Danny Claire) [Skylex Radio Edit].flac
|
||||
#EXTINF:-1,VA - Winter Took Over (Radio Edit)
|
||||
/app/music/VA - Melodic Vocal Trance 2017/22. Bluskay, KeyPlayer & Esmee Bor Stotijn - Winter Took Over (Radio Edit).flac
|
||||
#EXTINF:-1,Alison Krauss and Union Station - My Opening Farewell
|
||||
/app/music/Alison Krauss and Union Station - Paper Airplane (flac)/11 - Alison Krauss & Union Station - My Opening Farewell.flac
|
||||
#EXTINF:-1,Bedouin Soundclash - Money Worries (E-Clair Refix)
|
||||
/app/music/Bedouin Soundclash - Sounding a Mosaic (2004) [FLAC] {SD1267}/14 - Money Worries (E-Clair Refix).flac
|
||||
|
||||
# === PHASE 10: RETURN TO WINTER (Closing Circle) ===
|
||||
#EXTINF:-1,Brian Eno - Snow
|
||||
/app/music/Brian Eno/2020 - Roger Eno and Brian Eno - Mixing Colours/09 Snow.flac
|
||||
#EXTINF:-1,Proem - Winter Wolves
|
||||
/app/music/Proem - 2018 Modern Rope (WEB)/01. Winter Wolves.flac
|
||||
#EXTINF:-1,God is an Astronaut - Winter Dusk-Awakening
|
||||
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/03. Winter Dusk-Awakening.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - The Snow Came Down
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/13 The Snow Came Down.flac
|
||||
#EXTINF:-1,Trans-Siberian Orchestra - Christmas In The Air
|
||||
/app/music/Trans-Siberian Orchestra - The Christmas Trilogy (2004) [FLAC]/The Christmas Attic/14 Christmas In The Air.flac
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title data-text="title">Asteroid Radio - Admin Dashboard</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||
<script src="/asteroid/static/js/admin.js"></script>
|
||||
<script>
|
||||
// Handle logout without navigation
|
||||
function handleLogout() {
|
||||
fetch('/asteroid/logout', {
|
||||
method: 'GET',
|
||||
redirect: 'manual'
|
||||
})
|
||||
.then(() => {
|
||||
// Reload the current page content to show logged-out state
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load content via AJAX to prevent audio interruption
|
||||
function loadInFrame(link) {
|
||||
const url = link.href;
|
||||
console.log('Loading via AJAX:', url);
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
|
||||
if (window.history && window.history.pushState) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load content:', error);
|
||||
return true;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎛️ ADMIN DASHBOARD</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/content" target="content-frame" onclick="return loadInFrame(this)">Home</a>
|
||||
<a href="/asteroid/profile-content" target="content-frame" onclick="return loadInFrame(this)">Profile</a>
|
||||
<a href="/asteroid/admin-content" target="content-frame" onclick="return loadInFrame(this)">Admin</a>
|
||||
<a href="/asteroid/admin/users" target="content-frame" onclick="return loadInFrame(this)">👥 Users</a>
|
||||
<a href="/asteroid/logout" class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="admin-section">
|
||||
<h2>System Status</h2>
|
||||
<div class="admin-grid">
|
||||
<div class="status-card">
|
||||
<h3>Server Status</h3>
|
||||
<p class="status-good" data-text="server-status">🟢 Running</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Database Status</h3>
|
||||
<p class="status-good" data-text="database-status">🟢 Connected</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Liquidsoap Status</h3>
|
||||
<p class="status-error" data-text="liquidsoap-status">🔴 Not Running</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Icecast Status</h3>
|
||||
<p class="status-error" data-text="icecast-status">🔴 Not Running</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Music Library Management -->
|
||||
<div class="admin-section">
|
||||
<h2>Music Library Management</h2>
|
||||
|
||||
<!-- File Upload -->
|
||||
<div class="upload-section">
|
||||
<h3>Add Music Files</h3>
|
||||
<div class="upload-info">
|
||||
<p><strong>To add your own MP3 files:</strong></p>
|
||||
<ol>
|
||||
<li>Copy your MP3/FLAC/OGG/WAV files to: <code>/home/glenn/Projects/Code/asteroid/music/incoming/</code></li>
|
||||
<li>Click "Copy Files to Library" below</li>
|
||||
<li>Files will be moved to the library and added to the database</li>
|
||||
</ol>
|
||||
<p><em>Supported formats: MP3, FLAC, OGG, WAV</em></p>
|
||||
</div>
|
||||
<div class="upload-controls">
|
||||
<button id="copy-files" class="btn btn-success">📁 Copy Files to Library</button>
|
||||
<button id="open-incoming" class="btn btn-info">📂 Open Incoming Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-controls">
|
||||
<button id="scan-library" class="btn btn-primary">🔍 Scan Library</button>
|
||||
<button id="refresh-tracks" class="btn btn-secondary">🔄 Refresh Track List</button>
|
||||
<span id="scan-status" style="margin-left: 15px; font-weight: bold;"></span>
|
||||
</div>
|
||||
|
||||
<div class="track-stats">
|
||||
<p>Total Tracks: <span id="track-count" data-text="track-count">0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Track Management -->
|
||||
<div class="admin-section">
|
||||
<h2>Track Management</h2>
|
||||
<div class="track-controls">
|
||||
<input type="text" id="track-search" placeholder="Search tracks..." class="search-input">
|
||||
<select id="sort-tracks" class="sort-select">
|
||||
<option value="title">Sort by Title</option>
|
||||
<option value="artist">Sort by Artist</option>
|
||||
<option value="album">Sort by Album</option>
|
||||
</select>
|
||||
<select id="tracks-per-page" class="sort-select" onchange="changeTracksPerPage()">
|
||||
<option value="10">10 per page</option>
|
||||
<option value="20" selected>20 per page</option>
|
||||
<option value="50">50 per page</option>
|
||||
<option value="100">100 per page</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="tracks-container" class="tracks-list">
|
||||
<div class="loading">Loading tracks...</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div id="pagination-controls" style="display: none; margin-top: 20px; text-align: center;">
|
||||
<button onclick="goToPage(1)" class="btn btn-secondary">« First</button>
|
||||
<button onclick="previousPage()" class="btn btn-secondary">‹ Prev</button>
|
||||
<span id="page-info" style="margin: 0 15px; font-weight: bold;">Page 1 of 1</span>
|
||||
<button onclick="nextPage()" class="btn btn-secondary">Next ›</button>
|
||||
<button onclick="goToLastPage()" class="btn btn-secondary">Last »</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Stream Monitor -->
|
||||
<div class="admin-section">
|
||||
<h2>📻 Live Stream Monitor</h2>
|
||||
<div class="live-stream-monitor">
|
||||
<input type="hidden" id="stream-base-url" lquery="(val stream-base-url)">
|
||||
<p><strong>Now Playing:</strong> <span id="live-now-playing">Loading...</span></p>
|
||||
<audio id="live-stream-audio" controls style="width: 100%; max-width: 600px;">
|
||||
<source id="live-stream-source" lquery="(attr :src default-stream-url)" type="audio/aac">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stream Queue Management -->
|
||||
<div class="admin-section">
|
||||
<h2>🎵 Stream Queue Management</h2>
|
||||
<p>Manage the live stream playback queue. Changes take effect within 5-10 seconds.</p>
|
||||
|
||||
<div class="queue-controls">
|
||||
<button id="refresh-queue" class="btn btn-secondary">🔄 Refresh Queue</button>
|
||||
<button id="load-from-m3u" class="btn btn-success">📂 Load Queue from M3U</button>
|
||||
<button id="clear-queue-btn" class="btn btn-warning">🗑️ Clear Queue</button>
|
||||
<button id="add-random-tracks" class="btn btn-info">🎲 Add 10 Random Tracks</button>
|
||||
</div>
|
||||
|
||||
<div id="stream-queue-container" class="queue-list">
|
||||
<div class="loading">Loading queue...</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-actions">
|
||||
<h3>Add Tracks to Queue</h3>
|
||||
<input type="text" id="queue-track-search" placeholder="Search tracks to add..." class="search-input">
|
||||
<div id="queue-track-results" class="track-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Control -->
|
||||
<div class="admin-section">
|
||||
<h2>Player Control</h2>
|
||||
<div class="card">
|
||||
<h3>🎵 Player Control</h3>
|
||||
<div class="player-controls">
|
||||
<button id="player-play" class="btn btn-primary" onclick="playTrack()">▶️ Play</button>
|
||||
<button id="player-pause" class="btn btn-secondary" onclick="pausePlayer()">⏸️ Pause</button>
|
||||
<button id="player-stop" class="btn btn-secondary" onclick="stopPlayer()">⏹️ Stop</button>
|
||||
<button id="player-resume" class="btn btn-secondary" onclick="resumePlayer()">▶️ Resume</button>
|
||||
</div>
|
||||
<div id="player-status" class="status-info">
|
||||
Status: <span id="player-state">Unknown</span><br>
|
||||
Current Track: <span id="current-track">None</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>👥 User Management</h3>
|
||||
<p>Manage user accounts, roles, and permissions.</p>
|
||||
<div class="controls">
|
||||
<a href="/asteroid/admin/users" class="btn btn-primary">👥 Manage Users</a>
|
||||
</div>
|
||||
|
||||
<!-- Admin Password Reset Form -->
|
||||
<div class="form-section" style="margin-top: 20px;">
|
||||
<h4>🔒 Reset User Password</h4>
|
||||
<form id="admin-reset-password-form" onsubmit="return resetUserPassword(event)">
|
||||
<div class="form-group">
|
||||
<label for="reset-username">Username:</label>
|
||||
<input type="text" id="reset-username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reset-new-password">New Password:</label>
|
||||
<input type="password" id="reset-new-password" name="new-password" required minlength="8">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reset-confirm-password">Confirm Password:</label>
|
||||
<input type="password" id="reset-confirm-password" name="confirm-password" required minlength="8">
|
||||
</div>
|
||||
<div id="reset-password-message" class="message"></div>
|
||||
<button type="submit" class="btn btn-primary">Reset Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Admin password reset handler
|
||||
function resetUserPassword(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('reset-username').value;
|
||||
const newPassword = document.getElementById('reset-new-password').value;
|
||||
const confirmPassword = document.getElementById('reset-confirm-password').value;
|
||||
const messageDiv = document.getElementById('reset-password-message');
|
||||
|
||||
// Client-side validation
|
||||
if (newPassword.length < 8) {
|
||||
messageDiv.textContent = 'New password must be at least 8 characters';
|
||||
messageDiv.className = 'message error';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
messageDiv.textContent = 'Passwords do not match';
|
||||
messageDiv.className = 'message error';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send request to API
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
formData.append('new-password', newPassword);
|
||||
|
||||
fetch('/api/asteroid/admin/reset-password', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' || (data.data && data.data.status === 'success')) {
|
||||
messageDiv.textContent = 'Password reset successfully for user: ' + username;
|
||||
messageDiv.className = 'message success';
|
||||
document.getElementById('admin-reset-password-form').reset();
|
||||
} else {
|
||||
messageDiv.textContent = data.message || data.data?.message || 'Failed to reset password';
|
||||
messageDiv.className = 'message error';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error resetting password:', error);
|
||||
messageDiv.textContent = 'Error resetting password';
|
||||
messageDiv.className = 'message error';
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -34,6 +34,10 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
// Track if audio should be playing
|
||||
let shouldBePlaying = false;
|
||||
let keepAliveInterval = null;
|
||||
|
||||
// Configure audio element for better streaming
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const audioElement = document.getElementById('persistent-audio');
|
||||
|
|
@ -47,19 +51,49 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Track when user intentionally plays/pauses
|
||||
audioElement.addEventListener('play', function() {
|
||||
console.log('Audio play event');
|
||||
shouldBePlaying = true;
|
||||
startKeepAlive();
|
||||
});
|
||||
|
||||
audioElement.addEventListener('playing', function() {
|
||||
console.log('Audio playing');
|
||||
shouldBePlaying = true;
|
||||
});
|
||||
|
||||
// Only set shouldBePlaying to false if user clicked pause button
|
||||
const originalPause = audioElement.pause.bind(audioElement);
|
||||
audioElement.pause = function() {
|
||||
console.log('User paused audio');
|
||||
shouldBePlaying = false;
|
||||
stopKeepAlive();
|
||||
originalPause();
|
||||
};
|
||||
|
||||
// Add event listeners for debugging
|
||||
audioElement.addEventListener('waiting', function() {
|
||||
console.log('Audio buffering...');
|
||||
});
|
||||
|
||||
audioElement.addEventListener('playing', function() {
|
||||
console.log('Audio playing');
|
||||
});
|
||||
|
||||
audioElement.addEventListener('error', function(e) {
|
||||
console.error('Audio error:', e);
|
||||
});
|
||||
|
||||
// Monitor for unexpected pauses
|
||||
audioElement.addEventListener('pause', function(e) {
|
||||
console.log('Audio paused, shouldBePlaying:', shouldBePlaying);
|
||||
if (shouldBePlaying && !audioElement.ended) {
|
||||
console.log('Unexpected pause detected, resuming...');
|
||||
setTimeout(function() {
|
||||
if (audioElement.paused && shouldBePlaying) {
|
||||
audioElement.play().catch(err => console.log('Resume failed:', err));
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
const selector = document.getElementById('stream-quality');
|
||||
const streamQuality = localStorage.getItem('stream-quality') || 'aac';
|
||||
if (selector && selector.value !== streamQuality) {
|
||||
|
|
@ -127,6 +161,27 @@
|
|||
setTimeout(updateMiniNowPlaying, 1000);
|
||||
setInterval(updateMiniNowPlaying, 10000);
|
||||
|
||||
// Keep-alive functions to maintain playback
|
||||
function startKeepAlive() {
|
||||
if (keepAliveInterval) return;
|
||||
console.log('Starting audio keep-alive');
|
||||
keepAliveInterval = setInterval(function() {
|
||||
const audioElement = document.getElementById('persistent-audio');
|
||||
if (shouldBePlaying && audioElement.paused && !audioElement.ended) {
|
||||
console.log('Keep-alive: resuming paused audio');
|
||||
audioElement.play().catch(err => console.log('Keep-alive resume failed:', err));
|
||||
}
|
||||
}, 500); // Check every 500ms
|
||||
}
|
||||
|
||||
function stopKeepAlive() {
|
||||
if (keepAliveInterval) {
|
||||
console.log('Stopping audio keep-alive');
|
||||
clearInterval(keepAliveInterval);
|
||||
keepAliveInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable frameset mode function
|
||||
function disableFramesetMode() {
|
||||
// Clear preference
|
||||
|
|
|
|||
|
|
@ -12,6 +12,56 @@
|
|||
<script src="/asteroid/static/js/front-page.js"></script>
|
||||
<script src="/asteroid/static/js/recently-played.js"></script>
|
||||
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
||||
<script>
|
||||
// Handle logout without navigation
|
||||
function handleLogout() {
|
||||
fetch('/asteroid/logout', {
|
||||
method: 'GET',
|
||||
redirect: 'manual'
|
||||
})
|
||||
.then(() => {
|
||||
// Reload the current page content to show logged-out state
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load content via AJAX to prevent audio interruption
|
||||
function loadInFrame(link) {
|
||||
const url = link.href;
|
||||
console.log('Loading via AJAX:', url);
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// Replace entire document content
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
|
||||
// Update browser history
|
||||
if (window.history && window.history.pushState) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load content:', error);
|
||||
// Fallback to normal navigation
|
||||
return true;
|
||||
});
|
||||
|
||||
// Prevent default navigation
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
@ -56,9 +106,9 @@
|
|||
<a href="/asteroid/status" target="content-frame">Status</a>
|
||||
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
|
||||
<a href="/asteroid/login-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Login</a>
|
||||
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title data-text="title">Asteroid Radio - Login</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="/asteroid/static/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/asteroid.css">
|
||||
<script>
|
||||
// Load content via AJAX to prevent audio interruption
|
||||
function loadInFrame(link) {
|
||||
const url = link.href;
|
||||
console.log('Loading via AJAX:', url);
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
|
||||
if (window.history && window.history.pushState) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load content:', error);
|
||||
return true;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle form submission via AJAX
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
console.log('Submitting login via AJAX');
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
redirect: 'follow' // Follow redirects automatically
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Login response URL:', response.url);
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
// Replace document content with response
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Login submission failed:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1 style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
||||
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||
<span>ASTEROID RADIO - LOGIN</span>
|
||||
</h1>
|
||||
<nav class="nav">
|
||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/status" target="content-frame">Status</a>
|
||||
<a href="/asteroid/register-content" target="content-frame">Register</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="auth-container">
|
||||
<div class="auth-form">
|
||||
<h2>System Access</h2>
|
||||
<div class="message error" lquery="(attr :style display-error)" style="display: none;">
|
||||
<span data-text="error-message" lquery="(text error-message)">Invalid username or password</span>
|
||||
</div>
|
||||
<form method="post" action="/asteroid/login-content">
|
||||
<div class="form-group">
|
||||
<label>Username:</label>
|
||||
<input type="text" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">LOGIN</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -8,6 +8,56 @@
|
|||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||
<script src="/asteroid/static/js/player.js"></script>
|
||||
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
||||
<script>
|
||||
// Handle logout without navigation
|
||||
function handleLogout() {
|
||||
fetch('/asteroid/logout', {
|
||||
method: 'GET',
|
||||
redirect: 'manual'
|
||||
})
|
||||
.then(() => {
|
||||
// Reload the current page content to show logged-out state
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load content via AJAX to prevent audio interruption
|
||||
function loadInFrame(link) {
|
||||
const url = link.href;
|
||||
console.log('Loading via AJAX:', url);
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// Replace entire document content
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
|
||||
// Update browser history
|
||||
if (window.history && window.history.pushState) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load content:', error);
|
||||
// Fallback to normal navigation
|
||||
return true;
|
||||
});
|
||||
|
||||
// Prevent default navigation
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
@ -48,9 +98,9 @@
|
|||
<a href="/asteroid/content" target="content-frame">Home</a>
|
||||
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
||||
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
|
||||
<a href="/asteroid/login-content" target="content-frame" data-show-if-logged-out onclick="return loadInFrame(this)">Login</a>
|
||||
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
|
||||
</div>
|
||||
|
||||
<!-- Live Stream Section - Note about persistent player -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title data-text="title">Asteroid Radio - User Profile</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||
<script src="/asteroid/static/js/profile.js"></script>
|
||||
<script>
|
||||
// Handle logout without navigation
|
||||
function handleLogout() {
|
||||
fetch('/asteroid/logout', {
|
||||
method: 'GET',
|
||||
redirect: 'manual'
|
||||
})
|
||||
.then(() => {
|
||||
// Reload the current page content to show logged-out state
|
||||
fetch(window.location.href)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Load content via AJAX to prevent audio interruption
|
||||
function loadInFrame(link) {
|
||||
const url = link.href;
|
||||
console.log('Loading via AJAX:', url);
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
|
||||
if (window.history && window.history.pushState) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load content:', error);
|
||||
return true;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>👤 USER PROFILE</h1>
|
||||
<div class="nav">
|
||||
<a href="/asteroid/content" target="content-frame" onclick="return loadInFrame(this)">Home</a>
|
||||
<a href="/asteroid/profile-content" target="content-frame" onclick="return loadInFrame(this)">Profile</a>
|
||||
<a href="/asteroid/admin-content" target="content-frame" data-show-if-admin onclick="return loadInFrame(this)">Admin</a>
|
||||
<a href="/asteroid/logout" class="btn-logout" onclick="handleLogout(); return false;">Logout</a>
|
||||
</div>
|
||||
|
||||
<!-- User Profile Header -->
|
||||
<div class="admin-section">
|
||||
<h2>🎧 User Profile</h2>
|
||||
<div class="profile-info">
|
||||
<div class="info-group">
|
||||
<span class="info-label">Username:</span>
|
||||
<span class="info-value" data-text="username">user</span>
|
||||
</div>
|
||||
<div class="info-group">
|
||||
<span class="info-label">Role:</span>
|
||||
<span class="info-value" data-text="user-role">listener</span>
|
||||
</div>
|
||||
<div class="info-group">
|
||||
<span class="info-label">Member Since:</span>
|
||||
<span class="info-value" data-text="join-date">2024-01-01</span>
|
||||
</div>
|
||||
<div class="info-group">
|
||||
<span class="info-label">Last Active:</span>
|
||||
<span class="info-value" data-text="last-active">Today</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Listening Statistics -->
|
||||
<div class="admin-section">
|
||||
<h2>📊 Listening Statistics</h2>
|
||||
<div class="admin-grid">
|
||||
<div class="status-card">
|
||||
<h3>Total Listen Time</h3>
|
||||
<p class="stat-number" data-text="total-listen-time">0h 0m</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Tracks Played</h3>
|
||||
<p class="stat-number" data-text="tracks-played">0</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Sessions</h3>
|
||||
<p class="stat-number" data-text="session-count">0</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Favorite Genre</h3>
|
||||
<p class="stat-text" data-text="favorite-genre">Unknown</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recently Played Tracks -->
|
||||
<div class="admin-section">
|
||||
<h2>🎵 Recently Played</h2>
|
||||
<div class="tracks-list" id="recent-tracks">
|
||||
<div class="track-item">
|
||||
<div class="track-info">
|
||||
<span class="track-title" data-text="recent-track-1-title">No recent tracks</span>
|
||||
<span class="track-artist" data-text="recent-track-1-artist"></span>
|
||||
</div>
|
||||
<div class="track-meta">
|
||||
<span class="track-duration" data-text="recent-track-1-duration"></span>
|
||||
<span class="track-played-at" data-text="recent-track-1-played-at"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="track-item">
|
||||
<div class="track-info">
|
||||
<span class="track-title" data-text="recent-track-2-title"></span>
|
||||
<span class="track-artist" data-text="recent-track-2-artist"></span>
|
||||
</div>
|
||||
<div class="track-meta">
|
||||
<span class="track-duration" data-text="recent-track-2-duration"></span>
|
||||
<span class="track-played-at" data-text="recent-track-2-played-at"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="track-item">
|
||||
<div class="track-info">
|
||||
<span class="track-title" data-text="recent-track-3-title"></span>
|
||||
<span class="track-artist" data-text="recent-track-3-artist"></span>
|
||||
</div>
|
||||
<div class="track-meta">
|
||||
<span class="track-duration" data-text="recent-track-3-duration"></span>
|
||||
<span class="track-played-at" data-text="recent-track-3-played-at"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-actions">
|
||||
<button class="btn btn-secondary" onclick="loadMoreRecentTracks()">Load More</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Artists -->
|
||||
<div class="admin-section">
|
||||
<h2>🎤 Top Artists</h2>
|
||||
<div class="artist-stats">
|
||||
<div class="artist-item">
|
||||
<span class="artist-name" data-text="top-artist-1">Unknown Artist</span>
|
||||
<span class="artist-plays" data-text="top-artist-1-plays">0 plays</span>
|
||||
</div>
|
||||
<div class="artist-item">
|
||||
<span class="artist-name" data-text="top-artist-2"></span>
|
||||
<span class="artist-plays" data-text="top-artist-2-plays"></span>
|
||||
</div>
|
||||
<div class="artist-item">
|
||||
<span class="artist-name" data-text="top-artist-3"></span>
|
||||
<span class="artist-plays" data-text="top-artist-3-plays"></span>
|
||||
</div>
|
||||
<div class="artist-item">
|
||||
<span class="artist-name" data-text="top-artist-4"></span>
|
||||
<span class="artist-plays" data-text="top-artist-4-plays"></span>
|
||||
</div>
|
||||
<div class="artist-item">
|
||||
<span class="artist-name" data-text="top-artist-5"></span>
|
||||
<span class="artist-plays" data-text="top-artist-5-plays"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Listening Activity Chart -->
|
||||
<div class="admin-section">
|
||||
<h2>📈 Listening Activity</h2>
|
||||
<div class="activity-chart">
|
||||
<p>Activity over the last 30 days</p>
|
||||
<div class="chart-placeholder">
|
||||
<div class="chart-bar" style="height: 20%" data-day="1"></div>
|
||||
<div class="chart-bar" style="height: 45%" data-day="2"></div>
|
||||
<div class="chart-bar" style="height: 30%" data-day="3"></div>
|
||||
<div class="chart-bar" style="height: 60%" data-day="4"></div>
|
||||
<div class="chart-bar" style="height: 80%" data-day="5"></div>
|
||||
<div class="chart-bar" style="height: 25%" data-day="6"></div>
|
||||
<div class="chart-bar" style="height: 40%" data-day="7"></div>
|
||||
<!-- More bars would be generated dynamically -->
|
||||
</div>
|
||||
<p class="chart-note">Listening hours per day</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Actions -->
|
||||
<div class="admin-section">
|
||||
<h2>⚙️ Profile Settings</h2>
|
||||
|
||||
<!-- Change Password Form -->
|
||||
<div class="form-section">
|
||||
<h3>🔒 Change Password</h3>
|
||||
<form id="change-password-form" onsubmit="return changePassword(event)">
|
||||
<div class="form-group">
|
||||
<label for="current-password">Current Password:</label>
|
||||
<input type="password" id="current-password" name="current-password" required minlength="8">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password:</label>
|
||||
<input type="password" id="new-password" name="new-password" required minlength="8">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm-password" name="confirm-password" required minlength="8">
|
||||
</div>
|
||||
<div id="password-message" class="message"></div>
|
||||
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<button class="btn btn-primary" onclick="editProfile()">✏️ Edit Profile</button>
|
||||
<button class="btn btn-secondary" onclick="exportListeningData()">📊 Export Data</button>
|
||||
<button class="btn btn-secondary" onclick="clearListeningHistory()">🗑️ Clear History</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize profile page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadProfileData();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue