fix: ParenScript compilation working - intercept static route
The issue was route ordering. Since Radiance matches routes in load order, we couldn't override the static file route. Solution: intercept the static route and check if path is 'js/auth-ui.js', then serve ParenScript-compiled JavaScript instead. Changes: - Compile ParenScript to string at load time (stored in *auth-ui-js*) - Intercept static route to serve ParenScript for auth-ui.js - JavaScript successfully generated (1290 chars) - Ready for browser testing
This commit is contained in:
parent
b12e366d2c
commit
3c2ddf84c0
|
|
@ -471,18 +471,26 @@
|
||||||
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
;;; ParenScript JavaScript Routes
|
|
||||||
;;; These routes serve dynamically compiled ParenScript as JavaScript
|
|
||||||
;;; MUST come BEFORE the static file route to override specific JS files
|
|
||||||
|
|
||||||
(define-page js-auth-ui #@"/static/js/auth-ui.js" ()
|
|
||||||
(:content-type "application/javascript")
|
|
||||||
(generate-auth-ui-js))
|
|
||||||
|
|
||||||
;; Configure static file serving for other files
|
;; Configure static file serving for other files
|
||||||
|
;; BUT exclude auth-ui.js which is served by ParenScript
|
||||||
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
||||||
|
(if (string= path "js/auth-ui.js")
|
||||||
|
;; Serve ParenScript-compiled JavaScript
|
||||||
|
(progn
|
||||||
|
(format t "~%=== SERVING PARENSCRIPT auth-ui.js ===~%")
|
||||||
|
(setf (content-type *response*) "application/javascript")
|
||||||
|
(handler-case
|
||||||
|
(let ((js (generate-auth-ui-js)))
|
||||||
|
(format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL"))
|
||||||
|
(if js
|
||||||
|
js
|
||||||
|
"// Error: No JavaScript generated"))
|
||||||
|
(error (e)
|
||||||
|
(format t "ERROR generating auth-ui.js: ~a~%" e)
|
||||||
|
(format nil "// Error generating JavaScript: ~a~%" e))))
|
||||||
|
;; Serve regular static file
|
||||||
(serve-file (merge-pathnames (format nil "static/~a" path)
|
(serve-file (merge-pathnames (format nil "static/~a" path)
|
||||||
(asdf:system-source-directory :asteroid))))
|
(asdf:system-source-directory :asteroid)))))
|
||||||
|
|
||||||
;; Status check functions
|
;; Status check functions
|
||||||
(defun check-icecast-status ()
|
(defun check-icecast-status ()
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
|
|
||||||
(in-package #:asteroid)
|
(in-package #:asteroid)
|
||||||
|
|
||||||
(defun generate-auth-ui-js ()
|
(defparameter *auth-ui-js*
|
||||||
"Generate JavaScript for authentication UI handling"
|
|
||||||
(ps:ps*
|
(ps:ps*
|
||||||
'(progn
|
'(progn
|
||||||
|
|
||||||
|
|
@ -60,3 +59,7 @@
|
||||||
(ps:chain console (log "Auth status:" auth-status))
|
(ps:chain console (log "Auth status:" auth-status))
|
||||||
(update-auth-ui auth-status)
|
(update-auth-ui auth-status)
|
||||||
(ps:chain console (log "Auth UI updated")))))))))
|
(ps:chain console (log "Auth UI updated")))))))))
|
||||||
|
|
||||||
|
(defun generate-auth-ui-js ()
|
||||||
|
"Return the pre-compiled JavaScript for authentication UI"
|
||||||
|
*auth-ui-js*)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:370,Vector Lovers - City Lights From a Train
|
||||||
|
/app/music/Vector Lovers/City Lights From a Train.flac
|
||||||
|
#EXTINF:400,The Black Dog - Psil-Cosyin
|
||||||
|
/app/music/The Black Dog/Psil-Cosyin.flac
|
||||||
|
#EXTINF:320,Plaid - Eyen
|
||||||
|
/app/music/Plaid/Eyen.flac
|
||||||
|
#EXTINF:330,ISAN - Birds Over Barges
|
||||||
|
/app/music/ISAN/Birds Over Barges.flac
|
||||||
|
#EXTINF:360,Ochre - Bluebottle Farm
|
||||||
|
/app/music/Ochre/Bluebottle Farm.flac
|
||||||
|
#EXTINF:390,Arovane - Theme
|
||||||
|
/app/music/Arovane/Theme.flac
|
||||||
|
#EXTINF:380,Proem - Deep Like Airline Failure
|
||||||
|
/app/music/Proem/Deep Like Airline Failure.flac
|
||||||
|
#EXTINF:310,Solvent - My Radio (Remix)
|
||||||
|
/app/music/Solvent/My Radio (Remix).flac
|
||||||
|
#EXTINF:350,Bochum Welt - Marylebone (7th)
|
||||||
|
/app/music/Bochum Welt/Marylebone (7th).flac
|
||||||
|
#EXTINF:290,Mrs Jynx - Shibuya Lullaby
|
||||||
|
/app/music/Mrs Jynx/Shibuya Lullaby.flac
|
||||||
|
#EXTINF:340,Kettel - Whisper Me Wishes
|
||||||
|
/app/music/Kettel/Whisper Me Wishes.flac
|
||||||
|
#EXTINF:360,Christ. - Perlandine Friday
|
||||||
|
/app/music/Christ./Perlandine Friday.flac
|
||||||
|
#EXTINF:330,Cepia - Ithaca
|
||||||
|
/app/music/Cepia/Ithaca.flac
|
||||||
|
#EXTINF:340,Datassette - Vacuform
|
||||||
|
/app/music/Datassette/Vacuform.flac
|
||||||
|
#EXTINF:390,Plant43 - Dreams of the Sentient City
|
||||||
|
/app/music/Plant43/Dreams of the Sentient City.flac
|
||||||
|
#EXTINF:410,Claro Intelecto - Peace of Mind (Electrosoul)
|
||||||
|
/app/music/Claro Intelecto/Peace of Mind (Electrosoul).flac
|
||||||
|
#EXTINF:430,E.R.P. - Evoked
|
||||||
|
/app/music/E.R.P./Evoked.flac
|
||||||
|
#EXTINF:310,Der Zyklus - Formenverwandler
|
||||||
|
/app/music/Der Zyklus/Formenverwandler.flac
|
||||||
|
#EXTINF:330,Dopplereffekt - Infophysix
|
||||||
|
/app/music/Dopplereffekt/Infophysix.flac
|
||||||
|
#EXTINF:350,Drexciya - Wavejumper
|
||||||
|
/app/music/Drexciya/Wavejumper.flac
|
||||||
|
#EXTINF:375,The Other People Place - Sorrow & A Cup of Joe
|
||||||
|
/app/music/The Other People Place/Sorrow & A Cup of Joe.flac
|
||||||
|
#EXTINF:340,Arpanet - Wireless Internet
|
||||||
|
/app/music/Arpanet/Wireless Internet.flac
|
||||||
|
#EXTINF:380,Legowelt - Sturmvogel
|
||||||
|
/app/music/Legowelt/Sturmvogel.flac
|
||||||
|
#EXTINF:310,DMX Krew - Space Paranoia
|
||||||
|
/app/music/DMX Krew/Space Paranoia.flac
|
||||||
|
#EXTINF:360,Skywave Theory - Nova Drift
|
||||||
|
/app/music/Skywave Theory/Nova Drift.flac
|
||||||
|
#EXTINF:460,Pye Corner Audio - Transmission Four
|
||||||
|
/app/music/Pye Corner Audio/Transmission Four.flac
|
||||||
|
#EXTINF:390,B12 - Heaven Sent
|
||||||
|
/app/music/B12/Heaven Sent.flac
|
||||||
|
#EXTINF:450,Higher Intelligence Agency - Tortoise
|
||||||
|
/app/music/Higher Intelligence Agency/Tortoise.flac
|
||||||
|
#EXTINF:420,Biosphere - Kobresia
|
||||||
|
/app/music/Biosphere/Kobresia.flac
|
||||||
|
#EXTINF:870,Global Communication - 14:31
|
||||||
|
/app/music/Global Communication/14:31.flac
|
||||||
|
#EXTINF:500,Monolake - Cyan
|
||||||
|
/app/music/Monolake/Cyan.flac
|
||||||
|
#EXTINF:660,Deepchord - Electromagnetic
|
||||||
|
/app/music/Deepchord/Electromagnetic.flac
|
||||||
|
#EXTINF:1020,GAS - Pop 4
|
||||||
|
/app/music/GAS/Pop 4.flac
|
||||||
|
#EXTINF:600,Yagya - Rigning Nýju
|
||||||
|
/app/music/Yagya/Rigning Nýju.flac
|
||||||
|
#EXTINF:990,Voices From The Lake - Velo di Maya
|
||||||
|
/app/music/Voices From The Lake/Velo di Maya.flac
|
||||||
|
#EXTINF:3720,ASC - Time Heals All
|
||||||
|
/app/music/ASC/Time Heals All.flac
|
||||||
|
#EXTINF:540,36 - Room 237
|
||||||
|
/app/music/36/Room 237.flac
|
||||||
|
#EXTINF:900,Loscil - Endless Falls
|
||||||
|
/app/music/Loscil/Endless Falls.flac
|
||||||
|
#EXTINF:450,Kiasmos - Looped
|
||||||
|
/app/music/Kiasmos/Looped.flac
|
||||||
|
#EXTINF:590,Underworld - Rez
|
||||||
|
/app/music/Underworld/Rez.flac
|
||||||
|
#EXTINF:570,Orbital - Halcyon + On + On
|
||||||
|
/app/music/Orbital/Halcyon + On + On.flac
|
||||||
|
#EXTINF:1080,The Orb - A Huge Ever Growing Pulsating Brain
|
||||||
|
/app/music/The Orb/A Huge Ever Growing Pulsating Brain.flac
|
||||||
|
#EXTINF:360,Autechre - Slip
|
||||||
|
/app/music/Autechre/Slip.flac
|
||||||
|
#EXTINF:400,Labradford - S (Mi Media Naranja)
|
||||||
|
/app/music/Labradford/S (Mi Media Naranja).flac
|
||||||
|
#EXTINF:350,Vector Lovers - Rusting Cars and Wildflowers
|
||||||
|
/app/music/Vector Lovers/Rusting Cars and Wildflowers.flac
|
||||||
|
#EXTINF:390,The Black Dog - Raxmus
|
||||||
|
/app/music/The Black Dog/Raxmus.flac
|
||||||
|
#EXTINF:315,Plaid - Hawkmoth
|
||||||
|
/app/music/Plaid/Hawkmoth.flac
|
||||||
|
#EXTINF:320,ISAN - What This Button Did
|
||||||
|
/app/music/ISAN/What This Button Did.flac
|
||||||
|
#EXTINF:370,Ochre - Circadies
|
||||||
|
/app/music/Ochre/Circadies.flac
|
||||||
|
#EXTINF:420,Arovane - Tides
|
||||||
|
/app/music/Arovane/Tides.flac
|
||||||
|
#EXTINF:370,Proem - Nothing is as It Seems
|
||||||
|
/app/music/Proem/Nothing is as It Seems.flac
|
||||||
|
#EXTINF:300,Solvent - Loss For Words
|
||||||
|
/app/music/Solvent/Loss For Words.flac
|
||||||
|
#EXTINF:340,Bochum Welt - Saint (77sunset)
|
||||||
|
/app/music/Bochum Welt/Saint (77sunset).flac
|
||||||
|
#EXTINF:280,Mrs Jynx - Stay Home
|
||||||
|
/app/music/Mrs Jynx/Stay Home.flac
|
||||||
|
#EXTINF:330,Kettel - Church
|
||||||
|
/app/music/Kettel/Church.flac
|
||||||
|
#EXTINF:370,Christ. - Cordate
|
||||||
|
/app/music/Christ./Cordate.flac
|
||||||
|
#EXTINF:350,Datassette - Computers Elevate
|
||||||
|
/app/music/Datassette/Computers Elevate.flac
|
||||||
|
#EXTINF:420,Plant43 - The Cold Surveyor
|
||||||
|
/app/music/Plant43/The Cold Surveyor.flac
|
||||||
|
#EXTINF:380,Claro Intelecto - Section
|
||||||
|
/app/music/Claro Intelecto/Section.flac
|
||||||
|
#EXTINF:440,E.R.P. - Vox Automaton
|
||||||
|
/app/music/E.R.P./Vox Automaton.flac
|
||||||
|
#EXTINF:300,Dopplereffekt - Z-Boson
|
||||||
|
/app/music/Dopplereffekt/Z-Boson.flac
|
||||||
|
#EXTINF:380,Drexciya - Digital Tsunami
|
||||||
|
/app/music/Drexciya/Digital Tsunami.flac
|
||||||
|
#EXTINF:350,The Other People Place - You Said You Want Me
|
||||||
|
/app/music/The Other People Place/You Said You Want Me.flac
|
||||||
|
#EXTINF:370,Legowelt - Star Gazing
|
||||||
|
/app/music/Legowelt/Star Gazing.flac
|
||||||
|
#EXTINF:440,Pye Corner Audio - Electronic Rhythm Number 3
|
||||||
|
/app/music/Pye Corner Audio/Electronic Rhythm Number 3.flac
|
||||||
|
#EXTINF:460,B12 - Infinite Lites (Classic Mix)
|
||||||
|
/app/music/B12/Infinite Lites (Classic Mix).flac
|
||||||
|
#EXTINF:390,Biosphere - The Things I Tell You
|
||||||
|
/app/music/Biosphere/The Things I Tell You.flac
|
||||||
|
#EXTINF:580,Global Communication - 9:39
|
||||||
|
/app/music/Global Communication/9:39.flac
|
||||||
|
#EXTINF:460,Monolake - T-Channel
|
||||||
|
/app/music/Monolake/T-Channel.flac
|
||||||
|
#EXTINF:690,Deepchord - Vantage Isle (Variant)
|
||||||
|
/app/music/Deepchord/Vantage Isle (Variant).flac
|
||||||
|
#EXTINF:840,GAS - Königsforst 5
|
||||||
|
/app/music/GAS/Königsforst 5.flac
|
||||||
|
#EXTINF:520,Yagya - The Salt on Her Cheeks
|
||||||
|
/app/music/Yagya/The Salt on Her Cheeks.flac
|
||||||
|
#EXTINF:720,Voices From The Lake - Dream State
|
||||||
|
/app/music/Voices From The Lake/Dream State.flac
|
||||||
|
#EXTINF:510,36 - Night Rain
|
||||||
|
/app/music/36/Night Rain.flac
|
||||||
|
#EXTINF:470,Loscil - First Narrows
|
||||||
|
/app/music/Loscil/First Narrows.flac
|
||||||
|
#EXTINF:400,Kiasmos - Burnt
|
||||||
|
/app/music/Kiasmos/Burnt.flac
|
||||||
|
#EXTINF:570,Underworld - Jumbo (Extended)
|
||||||
|
/app/music/Underworld/Jumbo (Extended).flac
|
||||||
|
#EXTINF:480,Orbital - Belfast
|
||||||
|
/app/music/Orbital/Belfast.flac
|
||||||
|
#EXTINF:540,The Orb - Little Fluffy Clouds (Ambient Mix)
|
||||||
|
/app/music/The Orb/Little Fluffy Clouds (Ambient Mix).flac
|
||||||
|
#EXTINF:390,Autechre - Nine
|
||||||
|
/app/music/Autechre/Nine.flac
|
||||||
|
#EXTINF:380,Labradford - G (Mi Media Naranja)
|
||||||
|
/app/music/Labradford/G (Mi Media Naranja).flac
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXTINF:370,Vector Lovers - City Lights From a Train
|
||||||
|
Vector Lovers/City Lights From a Train.flac
|
||||||
|
#EXTINF:400,The Black Dog - Psil-Cosyin
|
||||||
|
The Black Dog/Psil-Cosyin.flac
|
||||||
|
#EXTINF:320,Plaid - Eyen
|
||||||
|
Plaid/Eyen.flac
|
||||||
|
#EXTINF:330,ISAN - Birds Over Barges
|
||||||
|
ISAN/Birds Over Barges.flac
|
||||||
|
#EXTINF:360,Ochre - Bluebottle Farm
|
||||||
|
Ochre/Bluebottle Farm.flac
|
||||||
|
#EXTINF:390,Arovane - Theme
|
||||||
|
Arovane/Theme.flac
|
||||||
|
#EXTINF:380,Proem - Deep Like Airline Failure
|
||||||
|
Proem/Deep Like Airline Failure.flac
|
||||||
|
#EXTINF:310,Solvent - My Radio (Remix)
|
||||||
|
Solvent/My Radio (Remix).flac
|
||||||
|
#EXTINF:350,Bochum Welt - Marylebone (7th)
|
||||||
|
Bochum Welt/Marylebone (7th).flac
|
||||||
|
#EXTINF:290,Mrs Jynx - Shibuya Lullaby
|
||||||
|
Mrs Jynx/Shibuya Lullaby.flac
|
||||||
|
#EXTINF:340,Kettel - Whisper Me Wishes
|
||||||
|
Kettel/Whisper Me Wishes.flac
|
||||||
|
#EXTINF:360,Christ. - Perlandine Friday
|
||||||
|
Christ./Perlandine Friday.flac
|
||||||
|
#EXTINF:330,Cepia - Ithaca
|
||||||
|
Cepia/Ithaca.flac
|
||||||
|
#EXTINF:340,Datassette - Vacuform
|
||||||
|
Datassette/Vacuform.flac
|
||||||
|
#EXTINF:390,Plant43 - Dreams of the Sentient City
|
||||||
|
Plant43/Dreams of the Sentient City.flac
|
||||||
|
#EXTINF:410,Claro Intelecto - Peace of Mind (Electrosoul)
|
||||||
|
Claro Intelecto/Peace of Mind (Electrosoul).flac
|
||||||
|
#EXTINF:430,E.R.P. - Evoked
|
||||||
|
E.R.P./Evoked.flac
|
||||||
|
#EXTINF:310,Der Zyklus - Formenverwandler
|
||||||
|
Der Zyklus/Formenverwandler.flac
|
||||||
|
#EXTINF:330,Dopplereffekt - Infophysix
|
||||||
|
Dopplereffekt/Infophysix.flac
|
||||||
|
#EXTINF:350,Drexciya - Wavejumper
|
||||||
|
Drexciya/Wavejumper.flac
|
||||||
|
#EXTINF:375,The Other People Place - Sorrow & A Cup of Joe
|
||||||
|
The Other People Place/Sorrow & A Cup of Joe.flac
|
||||||
|
#EXTINF:340,Arpanet - Wireless Internet
|
||||||
|
Arpanet/Wireless Internet.flac
|
||||||
|
#EXTINF:380,Legowelt - Sturmvogel
|
||||||
|
Legowelt/Sturmvogel.flac
|
||||||
|
#EXTINF:310,DMX Krew - Space Paranoia
|
||||||
|
DMX Krew/Space Paranoia.flac
|
||||||
|
#EXTINF:360,Skywave Theory - Nova Drift
|
||||||
|
Skywave Theory/Nova Drift.flac
|
||||||
|
#EXTINF:460,Pye Corner Audio - Transmission Four
|
||||||
|
Pye Corner Audio/Transmission Four.flac
|
||||||
|
#EXTINF:390,B12 - Heaven Sent
|
||||||
|
B12/Heaven Sent.flac
|
||||||
|
#EXTINF:450,Higher Intelligence Agency - Tortoise
|
||||||
|
Higher Intelligence Agency/Tortoise.flac
|
||||||
|
#EXTINF:420,Biosphere - Kobresia
|
||||||
|
Biosphere/Kobresia.flac
|
||||||
|
#EXTINF:870,Global Communication - 14:31
|
||||||
|
Global Communication/14:31.flac
|
||||||
|
#EXTINF:500,Monolake - Cyan
|
||||||
|
Monolake/Cyan.flac
|
||||||
|
#EXTINF:660,Deepchord - Electromagnetic
|
||||||
|
Deepchord/Electromagnetic.flac
|
||||||
|
#EXTINF:1020,GAS - Pop 4
|
||||||
|
GAS/Pop 4.flac
|
||||||
|
#EXTINF:600,Yagya - Rigning Nýju
|
||||||
|
Yagya/Rigning Nýju.flac
|
||||||
|
#EXTINF:990,Voices From The Lake - Velo di Maya
|
||||||
|
Voices From The Lake/Velo di Maya.flac
|
||||||
|
#EXTINF:3720,ASC - Time Heals All
|
||||||
|
ASC/Time Heals All.flac
|
||||||
|
#EXTINF:540,36 - Room 237
|
||||||
|
36/Room 237.flac
|
||||||
|
#EXTINF:900,Loscil - Endless Falls
|
||||||
|
Loscil/Endless Falls.flac
|
||||||
|
#EXTINF:450,Kiasmos - Looped
|
||||||
|
Kiasmos/Looped.flac
|
||||||
|
#EXTINF:590,Underworld - Rez
|
||||||
|
Underworld/Rez.flac
|
||||||
|
#EXTINF:570,Orbital - Halcyon + On + On
|
||||||
|
Orbital/Halcyon + On + On.flac
|
||||||
|
#EXTINF:1080,The Orb - A Huge Ever Growing Pulsating Brain
|
||||||
|
The Orb/A Huge Ever Growing Pulsating Brain.flac
|
||||||
|
#EXTINF:360,Autechre - Slip
|
||||||
|
Autechre/Slip.flac
|
||||||
|
#EXTINF:400,Labradford - S (Mi Media Naranja)
|
||||||
|
Labradford/S (Mi Media Naranja).flac
|
||||||
|
#EXTINF:350,Vector Lovers - Rusting Cars and Wildflowers
|
||||||
|
Vector Lovers/Rusting Cars and Wildflowers.flac
|
||||||
|
#EXTINF:390,The Black Dog - Raxmus
|
||||||
|
The Black Dog/Raxmus.flac
|
||||||
|
#EXTINF:315,Plaid - Hawkmoth
|
||||||
|
Plaid/Hawkmoth.flac
|
||||||
|
#EXTINF:320,ISAN - What This Button Did
|
||||||
|
ISAN/What This Button Did.flac
|
||||||
|
#EXTINF:370,Ochre - Circadies
|
||||||
|
Ochre/Circadies.flac
|
||||||
|
#EXTINF:420,Arovane - Tides
|
||||||
|
Arovane/Tides.flac
|
||||||
|
#EXTINF:370,Proem - Nothing is as It Seems
|
||||||
|
Proem/Nothing is as It Seems.flac
|
||||||
|
#EXTINF:300,Solvent - Loss For Words
|
||||||
|
Solvent/Loss For Words.flac
|
||||||
|
#EXTINF:340,Bochum Welt - Saint (77sunset)
|
||||||
|
Bochum Welt/Saint (77sunset).flac
|
||||||
|
#EXTINF:280,Mrs Jynx - Stay Home
|
||||||
|
Mrs Jynx/Stay Home.flac
|
||||||
|
#EXTINF:330,Kettel - Church
|
||||||
|
Kettel/Church.flac
|
||||||
|
#EXTINF:370,Christ. - Cordate
|
||||||
|
Christ./Cordate.flac
|
||||||
|
#EXTINF:350,Datassette - Computers Elevate
|
||||||
|
Datassette/Computers Elevate.flac
|
||||||
|
#EXTINF:420,Plant43 - The Cold Surveyor
|
||||||
|
Plant43/The Cold Surveyor.flac
|
||||||
|
#EXTINF:380,Claro Intelecto - Section
|
||||||
|
Claro Intelecto/Section.flac
|
||||||
|
#EXTINF:440,E.R.P. - Vox Automaton
|
||||||
|
E.R.P./Vox Automaton.flac
|
||||||
|
#EXTINF:300,Dopplereffekt - Z-Boson
|
||||||
|
Dopplereffekt/Z-Boson.flac
|
||||||
|
#EXTINF:380,Drexciya - Digital Tsunami
|
||||||
|
Drexciya/Digital Tsunami.flac
|
||||||
|
#EXTINF:350,The Other People Place - You Said You Want Me
|
||||||
|
The Other People Place/You Said You Want Me.flac
|
||||||
|
#EXTINF:370,Legowelt - Star Gazing
|
||||||
|
Legowelt/Star Gazing.flac
|
||||||
|
#EXTINF:440,Pye Corner Audio - Electronic Rhythm Number 3
|
||||||
|
Pye Corner Audio/Electronic Rhythm Number 3.flac
|
||||||
|
#EXTINF:460,B12 - Infinite Lites (Classic Mix)
|
||||||
|
B12/Infinite Lites (Classic Mix).flac
|
||||||
|
#EXTINF:390,Biosphere - The Things I Tell You
|
||||||
|
Biosphere/The Things I Tell You.flac
|
||||||
|
#EXTINF:580,Global Communication - 9:39
|
||||||
|
Global Communication/9:39.flac
|
||||||
|
#EXTINF:460,Monolake - T-Channel
|
||||||
|
Monolake/T-Channel.flac
|
||||||
|
#EXTINF:690,Deepchord - Vantage Isle (Variant)
|
||||||
|
Deepchord/Vantage Isle (Variant).flac
|
||||||
|
#EXTINF:840,GAS - Königsforst 5
|
||||||
|
GAS/Königsforst 5.flac
|
||||||
|
#EXTINF:520,Yagya - The Salt on Her Cheeks
|
||||||
|
Yagya/The Salt on Her Cheeks.flac
|
||||||
|
#EXTINF:720,Voices From The Lake - Dream State
|
||||||
|
Voices From The Lake/Dream State.flac
|
||||||
|
#EXTINF:510,36 - Night Rain
|
||||||
|
36/Night Rain.flac
|
||||||
|
#EXTINF:470,Loscil - First Narrows
|
||||||
|
Loscil/First Narrows.flac
|
||||||
|
#EXTINF:400,Kiasmos - Burnt
|
||||||
|
Kiasmos/Burnt.flac
|
||||||
|
#EXTINF:570,Underworld - Jumbo (Extended)
|
||||||
|
Underworld/Jumbo (Extended).flac
|
||||||
|
#EXTINF:480,Orbital - Belfast
|
||||||
|
Orbital/Belfast.flac
|
||||||
|
#EXTINF:540,The Orb - Little Fluffy Clouds (Ambient Mix)
|
||||||
|
The Orb/Little Fluffy Clouds (Ambient Mix).flac
|
||||||
|
#EXTINF:390,Autechre - Nine
|
||||||
|
Autechre/Nine.flac
|
||||||
|
#EXTINF:380,Labradford - G (Mi Media Naranja)
|
||||||
|
Labradford/G (Mi Media Naranja).flac
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,90 @@
|
||||||
|
#+TITLE: Asteroid Low Orbit Playlist
|
||||||
|
#+AUTHOR: Glenn
|
||||||
|
#+DATE: 2025-11-06
|
||||||
|
|
||||||
|
* Files
|
||||||
|
|
||||||
|
- *Asteroid-Low-Orbit.m3u* - Original playlist with relative paths
|
||||||
|
- *Asteroid-Low-Orbit-DOCKER.m3u* - Ready for VPS deployment (Docker container paths)
|
||||||
|
|
||||||
|
* For VPS Deployment
|
||||||
|
|
||||||
|
The =Asteroid-Low-Orbit-DOCKER.m3u= file is ready to use on the VPS (b612).
|
||||||
|
|
||||||
|
** Installation Steps
|
||||||
|
|
||||||
|
1. *Copy the file to the VPS:*
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
scp scripts/Asteroid-Low-Orbit-DOCKER.m3u glenneth@b612:~/asteroid/stream-queue.m3u
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
2. *Ensure music files exist on VPS:*
|
||||||
|
- Music should be in =/home/glenneth/Music/=
|
||||||
|
- The directory structure should match the paths in the playlist
|
||||||
|
- Example: =/home/glenneth/Music/Vector Lovers/City Lights From a Train.flac=
|
||||||
|
|
||||||
|
3. *Restart Liquidsoap container:*
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
cd ~/asteroid/docker
|
||||||
|
docker-compose restart liquidsoap
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** How It Works
|
||||||
|
|
||||||
|
- *Host path*: =/home/glenneth/Music/= (on VPS)
|
||||||
|
- *Container path*: =/app/music/= (inside Liquidsoap Docker container)
|
||||||
|
- *Playlist paths*: Use =/app/music/...= because Liquidsoap reads from inside the container
|
||||||
|
|
||||||
|
The docker-compose.yml mounts the music directory:
|
||||||
|
|
||||||
|
#+begin_src yaml
|
||||||
|
volumes:
|
||||||
|
- ${MUSIC_LIBRARY:-../music/library}:/app/music:ro
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
* Playlist Contents
|
||||||
|
|
||||||
|
This playlist contains ~50 tracks of ambient/IDM music curated for Asteroid Radio's "Low Orbit" programming block.
|
||||||
|
|
||||||
|
** Artists Featured
|
||||||
|
|
||||||
|
- Vector Lovers
|
||||||
|
- The Black Dog
|
||||||
|
- Plaid
|
||||||
|
- ISAN
|
||||||
|
- Ochre
|
||||||
|
- Arovane
|
||||||
|
- Proem
|
||||||
|
- Solvent
|
||||||
|
- Bochum Welt
|
||||||
|
- Mrs Jynx
|
||||||
|
- Kettel
|
||||||
|
- Christ.
|
||||||
|
- Cepia
|
||||||
|
- Datassette
|
||||||
|
- Plant43
|
||||||
|
- Claro Intelecto
|
||||||
|
- E.R.P.
|
||||||
|
- Der Zyklus
|
||||||
|
- Dopplereffekt
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
* Notes for Fade & easilok
|
||||||
|
|
||||||
|
- This playlist is ready to deploy to b612
|
||||||
|
- All paths are formatted for the Docker container setup
|
||||||
|
- Music files need to be present in =/home/glenneth/Music/= on the VPS
|
||||||
|
- The playlist can be manually copied to replace =stream-queue.m3u= when ready
|
||||||
|
- No changes to the main project repository required
|
||||||
|
|
||||||
|
* Generating New Playlists
|
||||||
|
|
||||||
|
To create additional playlists with correct paths:
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
# Create a playlist with relative paths first
|
||||||
|
# Then convert it:
|
||||||
|
python3 scripts/fix-m3u-paths.py your-playlist.m3u output-playlist.m3u --docker
|
||||||
|
#+end_src
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Fix M3U file paths for VPS or Docker deployment
|
||||||
|
Usage: python3 fix-m3u-paths.py input.m3u output.m3u [--docker|--vps]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def fix_m3u_paths(input_file, output_file, mode='vps'):
|
||||||
|
"""Convert relative paths to absolute paths for VPS or Docker"""
|
||||||
|
|
||||||
|
if mode == 'docker':
|
||||||
|
base_path = '/app/music'
|
||||||
|
else: # vps
|
||||||
|
base_path = '/home/glenneth/Music'
|
||||||
|
|
||||||
|
with open(input_file, 'r', encoding='utf-8') as f_in:
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f_out:
|
||||||
|
for line in f_in:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
|
||||||
|
# Keep #EXTM3U and #EXTINF lines as-is
|
||||||
|
if line.startswith('#'):
|
||||||
|
f_out.write(line + '\n')
|
||||||
|
# Convert file paths
|
||||||
|
elif line.strip():
|
||||||
|
# Remove leading/trailing whitespace
|
||||||
|
path = line.strip()
|
||||||
|
# If it's already an absolute path, keep it
|
||||||
|
if path.startswith('/'):
|
||||||
|
f_out.write(path + '\n')
|
||||||
|
else:
|
||||||
|
# Make it absolute
|
||||||
|
full_path = f"{base_path}/{path}"
|
||||||
|
f_out.write(full_path + '\n')
|
||||||
|
else:
|
||||||
|
f_out.write('\n')
|
||||||
|
|
||||||
|
print(f"Converted {input_file} -> {output_file}")
|
||||||
|
print(f"Mode: {mode}")
|
||||||
|
print(f"Base path: {base_path}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python3 fix-m3u-paths.py input.m3u output.m3u [--docker|--vps]")
|
||||||
|
print(" --docker: Use /app/music/ prefix (for Docker container)")
|
||||||
|
print(" --vps: Use /home/glenneth/Music/ prefix (default)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
input_file = sys.argv[1]
|
||||||
|
output_file = sys.argv[2]
|
||||||
|
mode = 'vps'
|
||||||
|
|
||||||
|
if len(sys.argv) > 3:
|
||||||
|
if sys.argv[3] == '--docker':
|
||||||
|
mode = 'docker'
|
||||||
|
elif sys.argv[3] == '--vps':
|
||||||
|
mode = 'vps'
|
||||||
|
|
||||||
|
if not Path(input_file).exists():
|
||||||
|
print(f"Error: Input file '{input_file}' not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
fix_m3u_paths(input_file, output_file, mode)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Basic music library tree generator (no external tools required)
|
||||||
|
# Shows file structure with sizes only
|
||||||
|
# Usage: ./music-library-tree-basic.sh [music-directory] [output-file]
|
||||||
|
|
||||||
|
MUSIC_DIR="${1:-/home/glenneth/Music}"
|
||||||
|
OUTPUT_FILE="${2:-music-library-tree.txt}"
|
||||||
|
|
||||||
|
# Check if music directory exists
|
||||||
|
if [ ! -d "$MUSIC_DIR" ]; then
|
||||||
|
echo "Error: Music directory '$MUSIC_DIR' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to format file size
|
||||||
|
format_size() {
|
||||||
|
local size=$1
|
||||||
|
if [ $size -ge 1073741824 ]; then
|
||||||
|
awk "BEGIN {printf \"%.1fG\", $size/1073741824}"
|
||||||
|
elif [ $size -ge 1048576 ]; then
|
||||||
|
awk "BEGIN {printf \"%.1fM\", $size/1048576}"
|
||||||
|
elif [ $size -ge 1024 ]; then
|
||||||
|
awk "BEGIN {printf \"%.0fK\", $size/1024}"
|
||||||
|
else
|
||||||
|
printf "%dB" $size
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to recursively build tree
|
||||||
|
build_tree() {
|
||||||
|
local dir="$1"
|
||||||
|
local prefix="$2"
|
||||||
|
|
||||||
|
# Get all entries sorted
|
||||||
|
local entries=()
|
||||||
|
while IFS= read -r -d $'\0' entry; do
|
||||||
|
entries+=("$entry")
|
||||||
|
done < <(find "$dir" -maxdepth 1 -mindepth 1 -print0 2>/dev/null | sort -z)
|
||||||
|
|
||||||
|
# Separate directories and files
|
||||||
|
local dirs=()
|
||||||
|
local files=()
|
||||||
|
|
||||||
|
for entry in "${entries[@]}"; do
|
||||||
|
if [ -d "$entry" ]; then
|
||||||
|
dirs+=("$entry")
|
||||||
|
else
|
||||||
|
files+=("$entry")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Combine: directories first, then files
|
||||||
|
local all_entries=("${dirs[@]}" "${files[@]}")
|
||||||
|
local count=${#all_entries[@]}
|
||||||
|
local index=0
|
||||||
|
|
||||||
|
for entry in "${all_entries[@]}"; do
|
||||||
|
index=$((index + 1))
|
||||||
|
local basename=$(basename "$entry")
|
||||||
|
local is_last=false
|
||||||
|
[ $index -eq $count ] && is_last=true
|
||||||
|
|
||||||
|
if [ -d "$entry" ]; then
|
||||||
|
# Directory - count files inside
|
||||||
|
local file_count=$(find "$entry" -type f 2>/dev/null | wc -l)
|
||||||
|
if $is_last; then
|
||||||
|
echo "${prefix}└── $basename/ ($file_count files)" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$entry" "${prefix} "
|
||||||
|
else
|
||||||
|
echo "${prefix}├── $basename/ ($file_count files)" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$entry" "${prefix}│ "
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# File
|
||||||
|
local ext="${basename##*.}"
|
||||||
|
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||||
|
local size=$(stat -c%s "$entry" 2>/dev/null || stat -f%z "$entry" 2>/dev/null || echo "0")
|
||||||
|
local size_fmt=$(format_size $size)
|
||||||
|
|
||||||
|
if [[ "$ext" =~ ^(mp3|flac|ogg|m4a|wav|aac|opus|wma)$ ]]; then
|
||||||
|
if $is_last; then
|
||||||
|
echo "${prefix}└── ♪ $basename ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
echo "${prefix}├── ♪ $basename ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if $is_last; then
|
||||||
|
echo "${prefix}└── $basename ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
echo "${prefix}├── $basename ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Generating music library tree (basic mode - no duration info)..."
|
||||||
|
|
||||||
|
# Start generating the tree
|
||||||
|
{
|
||||||
|
echo "Music Library Tree"
|
||||||
|
echo "=================="
|
||||||
|
echo "Generated: $(date)"
|
||||||
|
echo "Directory: $MUSIC_DIR"
|
||||||
|
echo "Note: Duration info not available (requires mediainfo/ffprobe)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Count total files
|
||||||
|
total_audio=$(find "$MUSIC_DIR" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.ogg" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.aac" -o -iname "*.opus" -o -iname "*.wma" \) 2>/dev/null | wc -l)
|
||||||
|
total_dirs=$(find "$MUSIC_DIR" -type d 2>/dev/null | wc -l)
|
||||||
|
total_size=$(du -sh "$MUSIC_DIR" 2>/dev/null | cut -f1)
|
||||||
|
|
||||||
|
echo "Total audio files: $total_audio"
|
||||||
|
echo "Total directories: $total_dirs"
|
||||||
|
echo "Total size: $total_size"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build the tree
|
||||||
|
echo "$(basename "$MUSIC_DIR")/"
|
||||||
|
} > "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
build_tree "$MUSIC_DIR" ""
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Tree generated successfully!"
|
||||||
|
echo "Output saved to: $OUTPUT_FILE"
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Simple music library tree generator using 'tree' command
|
||||||
|
# Usage: ./music-library-tree-simple.sh [music-directory] [output-file]
|
||||||
|
|
||||||
|
MUSIC_DIR="${1:-/home/glenn/Projects/Code/asteroid/music}"
|
||||||
|
OUTPUT_FILE="${2:-music-library-tree.txt}"
|
||||||
|
|
||||||
|
# Check if music directory exists
|
||||||
|
if [ ! -d "$MUSIC_DIR" ]; then
|
||||||
|
echo "Error: Music directory '$MUSIC_DIR' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if tree command is available
|
||||||
|
if ! command -v tree &> /dev/null; then
|
||||||
|
echo "Error: 'tree' command not found. Please install it:"
|
||||||
|
echo " Ubuntu/Debian: sudo apt-get install tree"
|
||||||
|
echo " CentOS/RHEL: sudo yum install tree"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating music library tree..."
|
||||||
|
|
||||||
|
# Generate header
|
||||||
|
{
|
||||||
|
echo "Music Library Tree"
|
||||||
|
echo "=================="
|
||||||
|
echo "Generated: $(date)"
|
||||||
|
echo "Directory: $MUSIC_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Count audio files
|
||||||
|
total_audio=$(find "$MUSIC_DIR" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.ogg" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.aac" -o -iname "*.opus" -o -iname "*.wma" \) 2>/dev/null | wc -l)
|
||||||
|
echo "Total audio files: $total_audio"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Generate tree with file sizes
|
||||||
|
tree -h -F --dirsfirst "$MUSIC_DIR"
|
||||||
|
|
||||||
|
} > "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Tree generated successfully!"
|
||||||
|
echo "Output saved to: $OUTPUT_FILE"
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Music library tree generator for VPS (no ffprobe required)
|
||||||
|
# Usage: ./music-library-tree-vps.sh [music-directory] [output-file]
|
||||||
|
|
||||||
|
MUSIC_DIR="${1:-/home/glenneth/Music}"
|
||||||
|
OUTPUT_FILE="${2:-music-library-tree.txt}"
|
||||||
|
|
||||||
|
# Check if music directory exists
|
||||||
|
if [ ! -d "$MUSIC_DIR" ]; then
|
||||||
|
echo "Error: Music directory '$MUSIC_DIR' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to get duration using available tools
|
||||||
|
get_duration() {
|
||||||
|
local file="$1"
|
||||||
|
|
||||||
|
# Try mediainfo first
|
||||||
|
if command -v mediainfo &> /dev/null; then
|
||||||
|
duration=$(mediainfo --Inform="General;%Duration%" "$file" 2>/dev/null)
|
||||||
|
if [ -n "$duration" ] && [ "$duration" != "" ]; then
|
||||||
|
# Convert milliseconds to minutes:seconds
|
||||||
|
duration_sec=$((duration / 1000))
|
||||||
|
printf "%02d:%02d" $((duration_sec/60)) $((duration_sec%60))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try mp3info for MP3 files
|
||||||
|
if [[ "$file" == *.mp3 ]] && command -v mp3info &> /dev/null; then
|
||||||
|
duration=$(mp3info -p "%m:%02s" "$file" 2>/dev/null)
|
||||||
|
if [ -n "$duration" ]; then
|
||||||
|
echo "$duration"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try soxi (from sox package)
|
||||||
|
if command -v soxi &> /dev/null; then
|
||||||
|
duration=$(soxi -D "$file" 2>/dev/null)
|
||||||
|
if [ -n "$duration" ]; then
|
||||||
|
duration_sec=${duration%.*}
|
||||||
|
printf "%02d:%02d" $((duration_sec/60)) $((duration_sec%60))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No duration available
|
||||||
|
echo "--:--"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to format file size
|
||||||
|
format_size() {
|
||||||
|
local size=$1
|
||||||
|
if [ $size -ge 1073741824 ]; then
|
||||||
|
printf "%.1fG" $(awk "BEGIN {printf \"%.1f\", $size/1073741824}")
|
||||||
|
elif [ $size -ge 1048576 ]; then
|
||||||
|
printf "%.1fM" $(awk "BEGIN {printf \"%.1f\", $size/1048576}")
|
||||||
|
elif [ $size -ge 1024 ]; then
|
||||||
|
printf "%.0fK" $(awk "BEGIN {printf \"%.0f\", $size/1024}")
|
||||||
|
else
|
||||||
|
printf "%dB" $size
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to recursively build tree
|
||||||
|
build_tree() {
|
||||||
|
local dir="$1"
|
||||||
|
local prefix="$2"
|
||||||
|
|
||||||
|
# Get all entries sorted (directories first, then files)
|
||||||
|
local entries=($(find "$dir" -maxdepth 1 -mindepth 1 | sort))
|
||||||
|
local dirs=()
|
||||||
|
local files=()
|
||||||
|
|
||||||
|
# Separate directories and files
|
||||||
|
for entry in "${entries[@]}"; do
|
||||||
|
if [ -d "$entry" ]; then
|
||||||
|
dirs+=("$entry")
|
||||||
|
else
|
||||||
|
files+=("$entry")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Combine: directories first, then files
|
||||||
|
local all_entries=("${dirs[@]}" "${files[@]}")
|
||||||
|
local count=${#all_entries[@]}
|
||||||
|
local index=0
|
||||||
|
|
||||||
|
for entry in "${all_entries[@]}"; do
|
||||||
|
index=$((index + 1))
|
||||||
|
local basename=$(basename "$entry")
|
||||||
|
local is_last=false
|
||||||
|
[ $index -eq $count ] && is_last=true
|
||||||
|
|
||||||
|
if [ -d "$entry" ]; then
|
||||||
|
# Directory
|
||||||
|
if $is_last; then
|
||||||
|
echo "${prefix}└── $basename/" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$entry" "${prefix} "
|
||||||
|
else
|
||||||
|
echo "${prefix}├── $basename/" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$entry" "${prefix}│ "
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# File - check if it's an audio file
|
||||||
|
local ext="${basename##*.}"
|
||||||
|
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
if [[ "$ext" =~ ^(mp3|flac|ogg|m4a|wav|aac|opus|wma)$ ]]; then
|
||||||
|
local duration=$(get_duration "$entry")
|
||||||
|
local size=$(stat -c%s "$entry" 2>/dev/null || stat -f%z "$entry" 2>/dev/null)
|
||||||
|
local size_fmt=$(format_size $size)
|
||||||
|
|
||||||
|
if $is_last; then
|
||||||
|
echo "${prefix}└── $basename [$duration] ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
echo "${prefix}├── $basename [$duration] ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Non-audio file
|
||||||
|
if $is_last; then
|
||||||
|
echo "${prefix}└── $basename" >> "$OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
echo "${prefix}├── $basename" >> "$OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect available tools
|
||||||
|
echo "Checking for available metadata tools..."
|
||||||
|
TOOLS_AVAILABLE=""
|
||||||
|
command -v mediainfo &> /dev/null && TOOLS_AVAILABLE="$TOOLS_AVAILABLE mediainfo"
|
||||||
|
command -v mp3info &> /dev/null && TOOLS_AVAILABLE="$TOOLS_AVAILABLE mp3info"
|
||||||
|
command -v soxi &> /dev/null && TOOLS_AVAILABLE="$TOOLS_AVAILABLE soxi"
|
||||||
|
|
||||||
|
if [ -z "$TOOLS_AVAILABLE" ]; then
|
||||||
|
echo "Warning: No metadata tools found (mediainfo, mp3info, soxi)"
|
||||||
|
echo "Duration information will not be available"
|
||||||
|
else
|
||||||
|
echo "Found tools:$TOOLS_AVAILABLE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating music library tree..."
|
||||||
|
|
||||||
|
# Start generating the tree
|
||||||
|
{
|
||||||
|
echo "Music Library Tree"
|
||||||
|
echo "=================="
|
||||||
|
echo "Generated: $(date)"
|
||||||
|
echo "Directory: $MUSIC_DIR"
|
||||||
|
echo "Tools available:$TOOLS_AVAILABLE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Count total files
|
||||||
|
total_audio=$(find "$MUSIC_DIR" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.ogg" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.aac" -o -iname "*.opus" -o -iname "*.wma" \) 2>/dev/null | wc -l)
|
||||||
|
echo "Total audio files: $total_audio"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build the tree
|
||||||
|
echo "$(basename "$MUSIC_DIR")/"
|
||||||
|
} > "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
build_tree "$MUSIC_DIR" ""
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Tree generated successfully!"
|
||||||
|
echo "Output saved to: $OUTPUT_FILE"
|
||||||
|
echo "Total audio files: $(find "$MUSIC_DIR" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.ogg" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.aac" -o -iname "*.opus" -o -iname "*.wma" \) 2>/dev/null | wc -l)"
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate a tree view of the music library with track durations
|
||||||
|
Usage: python3 music-library-tree.py [music-directory] [output-file]
|
||||||
|
|
||||||
|
Requires: mutagen (install with: pip3 install mutagen)
|
||||||
|
If mutagen not available, falls back to showing file info without duration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Try to import mutagen for audio metadata
|
||||||
|
try:
|
||||||
|
from mutagen import File as MutagenFile
|
||||||
|
MUTAGEN_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
MUTAGEN_AVAILABLE = False
|
||||||
|
print("Warning: mutagen not installed. Duration info will not be available.")
|
||||||
|
print("Install with: pip3 install mutagen")
|
||||||
|
|
||||||
|
AUDIO_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.m4a', '.wav', '.aac', '.opus', '.wma'}
|
||||||
|
|
||||||
|
def get_duration(file_path):
|
||||||
|
"""Get duration of audio file using mutagen"""
|
||||||
|
if not MUTAGEN_AVAILABLE:
|
||||||
|
return "--:--"
|
||||||
|
|
||||||
|
try:
|
||||||
|
audio = MutagenFile(str(file_path))
|
||||||
|
if audio is not None and hasattr(audio.info, 'length'):
|
||||||
|
duration_sec = int(audio.info.length)
|
||||||
|
minutes = duration_sec // 60
|
||||||
|
seconds = duration_sec % 60
|
||||||
|
return f"{minutes:02d}:{seconds:02d}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "--:--"
|
||||||
|
|
||||||
|
def format_size(size):
|
||||||
|
"""Format file size in human-readable format"""
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||||
|
if size < 1024.0:
|
||||||
|
return f"{size:.2f} {unit}"
|
||||||
|
size /= 1024.0
|
||||||
|
return f"{size:.2f} TB"
|
||||||
|
|
||||||
|
def build_tree(directory, output_file, prefix="", is_last=True):
|
||||||
|
"""Recursively build tree structure"""
|
||||||
|
try:
|
||||||
|
entries = sorted(Path(directory).iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
||||||
|
except PermissionError:
|
||||||
|
return
|
||||||
|
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
is_last_entry = (i == len(entries) - 1)
|
||||||
|
connector = "└── " if is_last_entry else "├── "
|
||||||
|
|
||||||
|
if entry.is_dir():
|
||||||
|
output_file.write(f"{prefix}{connector}📁 {entry.name}/\n")
|
||||||
|
extension = " " if is_last_entry else "│ "
|
||||||
|
build_tree(entry, output_file, prefix + extension, is_last_entry)
|
||||||
|
else:
|
||||||
|
ext = entry.suffix.lower()
|
||||||
|
if ext in AUDIO_EXTENSIONS:
|
||||||
|
duration = get_duration(entry)
|
||||||
|
size = entry.stat().st_size
|
||||||
|
size_fmt = format_size(size)
|
||||||
|
output_file.write(f"{prefix}{connector}🎵 {entry.name} [{duration}] ({size_fmt})\n")
|
||||||
|
else:
|
||||||
|
output_file.write(f"{prefix}{connector}📄 {entry.name}\n")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
music_dir = sys.argv[1] if len(sys.argv) > 1 else "/home/glenneth/Music"
|
||||||
|
output_path = sys.argv[2] if len(sys.argv) > 2 else "music-library-tree.txt"
|
||||||
|
|
||||||
|
music_path = Path(music_dir)
|
||||||
|
if not music_path.exists():
|
||||||
|
print(f"Error: Music directory '{music_dir}' does not exist")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Generating music library tree...")
|
||||||
|
|
||||||
|
# Count audio files
|
||||||
|
audio_files = []
|
||||||
|
for ext in AUDIO_EXTENSIONS:
|
||||||
|
audio_files.extend(music_path.rglob(f"*{ext}"))
|
||||||
|
total_audio = len(audio_files)
|
||||||
|
|
||||||
|
# Generate tree
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write("Music Library Tree\n")
|
||||||
|
f.write("==================\n")
|
||||||
|
f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
|
f.write(f"Directory: {music_dir}\n")
|
||||||
|
f.write(f"Mutagen available: {'Yes' if MUTAGEN_AVAILABLE else 'No (install with: pip3 install mutagen)'}\n")
|
||||||
|
f.write(f"\nTotal audio files: {total_audio}\n\n")
|
||||||
|
f.write(f"📁 {music_path.name}/\n")
|
||||||
|
build_tree(music_path, f, "", True)
|
||||||
|
|
||||||
|
print(f"\nTree generated successfully!")
|
||||||
|
print(f"Output saved to: {output_path}")
|
||||||
|
print(f"Total audio files: {total_audio}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Generate a tree view of the music library with track durations
|
||||||
|
# Usage: ./music-library-tree.sh [music-directory] [output-file]
|
||||||
|
|
||||||
|
MUSIC_DIR="${1:-/home/glenn/Projects/Code/asteroid/music}"
|
||||||
|
OUTPUT_FILE="${2:-music-library-tree.txt}"
|
||||||
|
|
||||||
|
# Check if music directory exists
|
||||||
|
if [ ! -d "$MUSIC_DIR" ]; then
|
||||||
|
echo "Error: Music directory '$MUSIC_DIR' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to get duration using ffprobe
|
||||||
|
get_duration() {
|
||||||
|
local file="$1"
|
||||||
|
if command -v ffprobe &> /dev/null; then
|
||||||
|
duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null)
|
||||||
|
if [ -n "$duration" ]; then
|
||||||
|
# Convert to minutes:seconds
|
||||||
|
printf "%02d:%02d" $((${duration%.*}/60)) $((${duration%.*}%60))
|
||||||
|
else
|
||||||
|
echo "??:??"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "??:??"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to format file size
|
||||||
|
format_size() {
|
||||||
|
local size=$1
|
||||||
|
if [ $size -ge 1073741824 ]; then
|
||||||
|
printf "%.2f GB" $(echo "scale=2; $size/1073741824" | bc)
|
||||||
|
elif [ $size -ge 1048576 ]; then
|
||||||
|
printf "%.2f MB" $(echo "scale=2; $size/1048576" | bc)
|
||||||
|
elif [ $size -ge 1024 ]; then
|
||||||
|
printf "%.2f KB" $(echo "scale=2; $size/1024" | bc)
|
||||||
|
else
|
||||||
|
printf "%d B" $size
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to recursively build tree
|
||||||
|
build_tree() {
|
||||||
|
local dir="$1"
|
||||||
|
local prefix="$2"
|
||||||
|
local is_last="$3"
|
||||||
|
|
||||||
|
local entries=()
|
||||||
|
while IFS= read -r -d '' entry; do
|
||||||
|
entries+=("$entry")
|
||||||
|
done < <(find "$dir" -maxdepth 1 -mindepth 1 -print0 | sort -z)
|
||||||
|
|
||||||
|
local count=${#entries[@]}
|
||||||
|
local index=0
|
||||||
|
|
||||||
|
for entry in "${entries[@]}"; do
|
||||||
|
index=$((index + 1))
|
||||||
|
local basename=$(basename "$entry")
|
||||||
|
local is_last_entry=false
|
||||||
|
[ $index -eq $count ] && is_last_entry=true
|
||||||
|
|
||||||
|
if [ -d "$entry" ]; then
|
||||||
|
# Directory
|
||||||
|
if $is_last_entry; then
|
||||||
|
echo "${prefix}└── 📁 $basename/" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$entry" "${prefix} " true
|
||||||
|
else
|
||||||
|
echo "${prefix}├── 📁 $basename/" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$entry" "${prefix}│ " false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# File - check if it's an audio file
|
||||||
|
local ext="${basename##*.}"
|
||||||
|
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
if [[ "$ext" =~ ^(mp3|flac|ogg|m4a|wav|aac|opus|wma)$ ]]; then
|
||||||
|
local duration=$(get_duration "$entry")
|
||||||
|
local size=$(stat -f%z "$entry" 2>/dev/null || stat -c%s "$entry" 2>/dev/null)
|
||||||
|
local size_fmt=$(format_size $size)
|
||||||
|
|
||||||
|
if $is_last_entry; then
|
||||||
|
echo "${prefix}└── 🎵 $basename [$duration] ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
echo "${prefix}├── 🎵 $basename [$duration] ($size_fmt)" >> "$OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Non-audio file
|
||||||
|
if $is_last_entry; then
|
||||||
|
echo "${prefix}└── 📄 $basename" >> "$OUTPUT_FILE"
|
||||||
|
else
|
||||||
|
echo "${prefix}├── 📄 $basename" >> "$OUTPUT_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start generating the tree
|
||||||
|
echo "Generating music library tree..."
|
||||||
|
echo "Music Library Tree" > "$OUTPUT_FILE"
|
||||||
|
echo "==================" >> "$OUTPUT_FILE"
|
||||||
|
echo "Generated: $(date)" >> "$OUTPUT_FILE"
|
||||||
|
echo "Directory: $MUSIC_DIR" >> "$OUTPUT_FILE"
|
||||||
|
echo "" >> "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
# Count total files
|
||||||
|
total_audio=$(find "$MUSIC_DIR" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.ogg" -o -iname "*.m4a" -o -iname "*.wav" -o -iname "*.aac" -o -iname "*.opus" -o -iname "*.wma" \) | wc -l)
|
||||||
|
echo "Total audio files: $total_audio" >> "$OUTPUT_FILE"
|
||||||
|
echo "" >> "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
# Build the tree
|
||||||
|
echo "📁 $(basename "$MUSIC_DIR")/" >> "$OUTPUT_FILE"
|
||||||
|
build_tree "$MUSIC_DIR" "" true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Tree generated successfully!"
|
||||||
|
echo "Output saved to: $OUTPUT_FILE"
|
||||||
|
echo "Total audio files: $total_audio"
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate a tree view of the music library with track durations
|
||||||
|
No external dependencies required - reads file headers directly
|
||||||
|
Usage: python3 music-library-tree-standalone.py [music-directory] [output-file]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
AUDIO_EXTENSIONS = {'.mp3', '.flac', '.ogg', '.m4a', '.wav', '.aac', '.opus', '.wma'}
|
||||||
|
|
||||||
|
def get_mp3_duration(file_path):
|
||||||
|
"""Get MP3 duration by reading frame headers"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
# Skip ID3v2 tag if present
|
||||||
|
header = f.read(10)
|
||||||
|
if header[:3] == b'ID3':
|
||||||
|
size = struct.unpack('>I', b'\x00' + header[6:9])[0]
|
||||||
|
f.seek(size + 10)
|
||||||
|
else:
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
# Read first frame to get bitrate and sample rate
|
||||||
|
frame_header = f.read(4)
|
||||||
|
if len(frame_header) < 4:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Parse MP3 frame header
|
||||||
|
if frame_header[0] != 0xFF or (frame_header[1] & 0xE0) != 0xE0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Bitrate table (MPEG1 Layer III)
|
||||||
|
bitrates = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]
|
||||||
|
bitrate_index = (frame_header[2] >> 4) & 0x0F
|
||||||
|
bitrate = bitrates[bitrate_index] * 1000
|
||||||
|
|
||||||
|
if bitrate == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get file size
|
||||||
|
f.seek(0, 2)
|
||||||
|
file_size = f.tell()
|
||||||
|
|
||||||
|
# Estimate duration
|
||||||
|
duration = (file_size * 8) / bitrate
|
||||||
|
return int(duration)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_flac_duration(file_path):
|
||||||
|
"""Get FLAC duration by reading metadata block"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
# Check FLAC signature
|
||||||
|
if f.read(4) != b'fLaC':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Read metadata blocks
|
||||||
|
while True:
|
||||||
|
block_header = f.read(4)
|
||||||
|
if len(block_header) < 4:
|
||||||
|
return None
|
||||||
|
|
||||||
|
is_last = (block_header[0] & 0x80) != 0
|
||||||
|
block_type = block_header[0] & 0x7F
|
||||||
|
block_size = struct.unpack('>I', b'\x00' + block_header[1:4])[0]
|
||||||
|
|
||||||
|
if block_type == 0: # STREAMINFO
|
||||||
|
streaminfo = f.read(block_size)
|
||||||
|
# Sample rate is at bytes 10-13 (20 bits)
|
||||||
|
sample_rate = (struct.unpack('>I', streaminfo[10:14])[0] >> 12) & 0xFFFFF
|
||||||
|
# Total samples is at bytes 13-17 (36 bits)
|
||||||
|
total_samples = struct.unpack('>Q', b'\x00\x00\x00' + streaminfo[13:18])[0] & 0xFFFFFFFFF
|
||||||
|
|
||||||
|
if sample_rate > 0:
|
||||||
|
duration = total_samples / sample_rate
|
||||||
|
return int(duration)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if is_last:
|
||||||
|
break
|
||||||
|
f.seek(block_size, 1)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_wav_duration(file_path):
|
||||||
|
"""Get WAV duration by reading RIFF header"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
# Check RIFF header
|
||||||
|
if f.read(4) != b'RIFF':
|
||||||
|
return None
|
||||||
|
f.read(4) # File size
|
||||||
|
if f.read(4) != b'WAVE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find fmt chunk
|
||||||
|
while True:
|
||||||
|
chunk_id = f.read(4)
|
||||||
|
if len(chunk_id) < 4:
|
||||||
|
return None
|
||||||
|
chunk_size = struct.unpack('<I', f.read(4))[0]
|
||||||
|
|
||||||
|
if chunk_id == b'fmt ':
|
||||||
|
fmt_data = f.read(chunk_size)
|
||||||
|
sample_rate = struct.unpack('<I', fmt_data[4:8])[0]
|
||||||
|
byte_rate = struct.unpack('<I', fmt_data[8:12])[0]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
f.seek(chunk_size, 1)
|
||||||
|
|
||||||
|
# Find data chunk
|
||||||
|
while True:
|
||||||
|
chunk_id = f.read(4)
|
||||||
|
if len(chunk_id) < 4:
|
||||||
|
return None
|
||||||
|
chunk_size = struct.unpack('<I', f.read(4))[0]
|
||||||
|
|
||||||
|
if chunk_id == b'data':
|
||||||
|
if byte_rate > 0:
|
||||||
|
duration = chunk_size / byte_rate
|
||||||
|
return int(duration)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
f.seek(chunk_size, 1)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_duration(file_path):
|
||||||
|
"""Get duration of audio file by reading file headers"""
|
||||||
|
ext = file_path.suffix.lower()
|
||||||
|
|
||||||
|
if ext == '.mp3':
|
||||||
|
duration_sec = get_mp3_duration(file_path)
|
||||||
|
elif ext == '.flac':
|
||||||
|
duration_sec = get_flac_duration(file_path)
|
||||||
|
elif ext == '.wav':
|
||||||
|
duration_sec = get_wav_duration(file_path)
|
||||||
|
else:
|
||||||
|
# For other formats, we can't easily read without libraries
|
||||||
|
return "--:--"
|
||||||
|
|
||||||
|
if duration_sec is not None:
|
||||||
|
minutes = duration_sec // 60
|
||||||
|
seconds = duration_sec % 60
|
||||||
|
return f"{minutes:02d}:{seconds:02d}"
|
||||||
|
|
||||||
|
return "--:--"
|
||||||
|
|
||||||
|
def format_size(size):
|
||||||
|
"""Format file size in human-readable format"""
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||||
|
if size < 1024.0:
|
||||||
|
return f"{size:.2f} {unit}"
|
||||||
|
size /= 1024.0
|
||||||
|
return f"{size:.2f} TB"
|
||||||
|
|
||||||
|
def build_tree(directory, output_file, prefix="", is_last=True):
|
||||||
|
"""Recursively build tree structure"""
|
||||||
|
try:
|
||||||
|
entries = sorted(Path(directory).iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
||||||
|
except PermissionError:
|
||||||
|
return
|
||||||
|
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
is_last_entry = (i == len(entries) - 1)
|
||||||
|
connector = "└── " if is_last_entry else "├── "
|
||||||
|
|
||||||
|
if entry.is_dir():
|
||||||
|
output_file.write(f"{prefix}{connector}📁 {entry.name}/\n")
|
||||||
|
extension = " " if is_last_entry else "│ "
|
||||||
|
build_tree(entry, output_file, prefix + extension, is_last_entry)
|
||||||
|
else:
|
||||||
|
ext = entry.suffix.lower()
|
||||||
|
if ext in AUDIO_EXTENSIONS:
|
||||||
|
duration = get_duration(entry)
|
||||||
|
size = entry.stat().st_size
|
||||||
|
size_fmt = format_size(size)
|
||||||
|
output_file.write(f"{prefix}{connector}🎵 {entry.name} [{duration}] ({size_fmt})\n")
|
||||||
|
else:
|
||||||
|
output_file.write(f"{prefix}{connector}📄 {entry.name}\n")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
music_dir = sys.argv[1] if len(sys.argv) > 1 else "/home/glenneth/Music"
|
||||||
|
output_path = sys.argv[2] if len(sys.argv) > 2 else "music-library-tree.txt"
|
||||||
|
|
||||||
|
music_path = Path(music_dir)
|
||||||
|
if not music_path.exists():
|
||||||
|
print(f"Error: Music directory '{music_dir}' does not exist")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Generating music library tree...")
|
||||||
|
print("Reading durations from file headers (MP3, FLAC, WAV supported)")
|
||||||
|
|
||||||
|
# Count audio files
|
||||||
|
audio_files = []
|
||||||
|
for ext in AUDIO_EXTENSIONS:
|
||||||
|
audio_files.extend(music_path.rglob(f"*{ext}"))
|
||||||
|
total_audio = len(audio_files)
|
||||||
|
|
||||||
|
# Generate tree
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write("Music Library Tree\n")
|
||||||
|
f.write("==================\n")
|
||||||
|
f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
|
f.write(f"Directory: {music_dir}\n")
|
||||||
|
f.write(f"Duration support: MP3, FLAC, WAV (no external libraries needed)\n")
|
||||||
|
f.write(f"\nTotal audio files: {total_audio}\n\n")
|
||||||
|
f.write(f"📁 {music_path.name}/\n")
|
||||||
|
build_tree(music_path, f, "", True)
|
||||||
|
|
||||||
|
print(f"\nTree generated successfully!")
|
||||||
|
print(f"Output saved to: {output_path}")
|
||||||
|
print(f"Total audio files: {total_audio}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
;;;; test-parenscript.lisp - Test ParenScript compilation
|
||||||
|
|
||||||
|
(ql:quickload :parenscript)
|
||||||
|
|
||||||
|
(defun test-auth-ui-compilation ()
|
||||||
|
"Test compiling the auth-ui ParenScript to JavaScript"
|
||||||
|
(let ((js-code
|
||||||
|
(ps:ps
|
||||||
|
;; Check if user is logged in by calling the API
|
||||||
|
(defun check-auth-status ()
|
||||||
|
(ps:chain
|
||||||
|
(fetch "/api/asteroid/auth-status")
|
||||||
|
(then (lambda (response)
|
||||||
|
(ps:chain response (json))))
|
||||||
|
(then (lambda (result)
|
||||||
|
;; api-output wraps response in {status, message, data}
|
||||||
|
(let ((data (or (ps:@ result data) result)))
|
||||||
|
data)))
|
||||||
|
(catch (lambda (error)
|
||||||
|
(ps:chain console (error "Error checking auth status:" error))
|
||||||
|
(ps:create :logged-in false
|
||||||
|
:is-admin false)))))
|
||||||
|
|
||||||
|
;; Update UI based on authentication status
|
||||||
|
(defun update-auth-ui (auth-status)
|
||||||
|
;; Show/hide elements based on login status
|
||||||
|
(ps:chain document
|
||||||
|
(query-selector-all "[data-show-if-logged-in]")
|
||||||
|
(for-each (lambda (el)
|
||||||
|
(setf (ps:@ el style display)
|
||||||
|
(if (ps:@ auth-status logged-in)
|
||||||
|
"inline-block"
|
||||||
|
"none")))))
|
||||||
|
|
||||||
|
(ps:chain document
|
||||||
|
(query-selector-all "[data-show-if-logged-out]")
|
||||||
|
(for-each (lambda (el)
|
||||||
|
(setf (ps:@ el style display)
|
||||||
|
(if (ps:@ auth-status logged-in)
|
||||||
|
"none"
|
||||||
|
"inline-block")))))
|
||||||
|
|
||||||
|
(ps:chain document
|
||||||
|
(query-selector-all "[data-show-if-admin]")
|
||||||
|
(for-each (lambda (el)
|
||||||
|
(setf (ps:@ el style display)
|
||||||
|
(if (ps:@ auth-status is-admin)
|
||||||
|
"inline-block"
|
||||||
|
"none"))))))
|
||||||
|
|
||||||
|
;; Initialize auth UI on page load
|
||||||
|
(ps:chain document
|
||||||
|
(add-event-listener
|
||||||
|
"DOMContentLoaded"
|
||||||
|
(async lambda ()
|
||||||
|
(ps:chain console (log "Auth UI initializing..."))
|
||||||
|
(let ((auth-status (await (check-auth-status))))
|
||||||
|
(ps:chain console (log "Auth status:" auth-status))
|
||||||
|
(update-auth-ui auth-status)
|
||||||
|
(ps:chain console (log "Auth UI updated")))))))))
|
||||||
|
|
||||||
|
(format t "~%Generated JavaScript:~%~%")
|
||||||
|
(format t "~a~%" js-code)
|
||||||
|
(format t "~%~%")
|
||||||
|
|
||||||
|
;; Write to file for comparison
|
||||||
|
(with-open-file (out "/home/glenn/Projects/Code/asteroid/static/js/auth-ui-generated.js"
|
||||||
|
:direction :output
|
||||||
|
:if-exists :supersede)
|
||||||
|
(write-string js-code out))
|
||||||
|
|
||||||
|
(format t "Wrote generated JavaScript to: static/js/auth-ui-generated.js~%")))
|
||||||
|
|
||||||
|
;; Run the test
|
||||||
|
(test-auth-ui-compilation)
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
;;;; test-ps-compile.lisp - Test ParenScript compilation for auth-ui
|
||||||
|
|
||||||
|
(load "~/quicklisp/setup.lisp")
|
||||||
|
(ql:quickload '(:parenscript) :silent t)
|
||||||
|
|
||||||
|
(format t "~%Testing ParenScript compilation for auth-ui...~%~%")
|
||||||
|
|
||||||
|
;; Load the auth-ui parenscript file
|
||||||
|
(load "parenscript/auth-ui.lisp")
|
||||||
|
|
||||||
|
;; Test compilation
|
||||||
|
(format t "Compiling ParenScript to JavaScript...~%~%")
|
||||||
|
(let ((js-output (asteroid::generate-auth-ui-js)))
|
||||||
|
(format t "Generated JavaScript (~a characters):~%~%" (length js-output))
|
||||||
|
(format t "~a~%~%" js-output)
|
||||||
|
|
||||||
|
;; Write to file
|
||||||
|
(with-open-file (out "static/js/auth-ui-parenscript-output.js"
|
||||||
|
:direction :output
|
||||||
|
:if-exists :supersede)
|
||||||
|
(write-string js-output out))
|
||||||
|
|
||||||
|
(format t "~%✓ JavaScript written to: static/js/auth-ui-parenscript-output.js~%")
|
||||||
|
(format t "✓ Compilation successful!~%~%"))
|
||||||
|
|
||||||
|
(format t "Compare with original:~%")
|
||||||
|
(format t " Original: static/js/auth-ui.js.original~%")
|
||||||
|
(format t " Generated: static/js/auth-ui-parenscript-output.js~%~%")
|
||||||
Loading…
Reference in New Issue