diff --git a/asteroid.lisp b/asteroid.lisp index 1f4e23a..3d24a2e 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -491,18 +491,26 @@ :default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*) :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 +;; BUT exclude auth-ui.js which is served by ParenScript (define-page static #@"/static/(.*)" (:uri-groups (path)) - (serve-file (merge-pathnames (format nil "static/~a" path) - (asdf:system-source-directory :asteroid)))) + (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) + (asdf:system-source-directory :asteroid))))) ;; Status check functions (defun check-icecast-status () diff --git a/parenscript/auth-ui.lisp b/parenscript/auth-ui.lisp index c3752e1..5faa53c 100644 --- a/parenscript/auth-ui.lisp +++ b/parenscript/auth-ui.lisp @@ -3,8 +3,7 @@ (in-package #:asteroid) -(defun generate-auth-ui-js () - "Generate JavaScript for authentication UI handling" +(defparameter *auth-ui-js* (ps:ps* '(progn @@ -60,3 +59,7 @@ (ps:chain console (log "Auth status:" auth-status)) (update-auth-ui auth-status) (ps:chain console (log "Auth UI updated"))))))))) + +(defun generate-auth-ui-js () + "Return the pre-compiled JavaScript for authentication UI" + *auth-ui-js*) diff --git a/scripts/Asteroid-Low-Orbit-DOCKER.m3u b/scripts/Asteroid-Low-Orbit-DOCKER.m3u new file mode 100644 index 0000000..babf3f8 --- /dev/null +++ b/scripts/Asteroid-Low-Orbit-DOCKER.m3u @@ -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 diff --git a/scripts/Asteroid-Low-Orbit.m3u b/scripts/Asteroid-Low-Orbit.m3u new file mode 100644 index 0000000..7ff29a2 --- /dev/null +++ b/scripts/Asteroid-Low-Orbit.m3u @@ -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 diff --git a/scripts/Asteroid-Low-Orbit.m3u:Zone.Identifier b/scripts/Asteroid-Low-Orbit.m3u:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/scripts/Asteroid-Low-Orbit.m3u:Zone.Identifier differ diff --git a/scripts/README-PLAYLIST.org b/scripts/README-PLAYLIST.org new file mode 100644 index 0000000..ca21ffe --- /dev/null +++ b/scripts/README-PLAYLIST.org @@ -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 diff --git a/scripts/fix-m3u-paths.py b/scripts/fix-m3u-paths.py new file mode 100644 index 0000000..df45128 --- /dev/null +++ b/scripts/fix-m3u-paths.py @@ -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() diff --git a/scripts/music-library-tree-basic.sh b/scripts/music-library-tree-basic.sh new file mode 100644 index 0000000..f5ee52c --- /dev/null +++ b/scripts/music-library-tree-basic.sh @@ -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" diff --git a/scripts/music-library-tree-simple.sh b/scripts/music-library-tree-simple.sh new file mode 100644 index 0000000..dad93d9 --- /dev/null +++ b/scripts/music-library-tree-simple.sh @@ -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" diff --git a/scripts/music-library-tree-vps.sh b/scripts/music-library-tree-vps.sh new file mode 100644 index 0000000..5fb4c78 --- /dev/null +++ b/scripts/music-library-tree-vps.sh @@ -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)" diff --git a/scripts/music-library-tree.py b/scripts/music-library-tree.py new file mode 100644 index 0000000..0f6a88a --- /dev/null +++ b/scripts/music-library-tree.py @@ -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() diff --git a/scripts/music-library-tree.sh b/scripts/music-library-tree.sh new file mode 100644 index 0000000..ac51a12 --- /dev/null +++ b/scripts/music-library-tree.sh @@ -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" diff --git a/scripts/scan.py b/scripts/scan.py new file mode 100644 index 0000000..77afde2 --- /dev/null +++ b/scripts/scan.py @@ -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(' 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() diff --git a/test-parenscript.lisp b/test-parenscript.lisp new file mode 100644 index 0000000..de46b27 --- /dev/null +++ b/test-parenscript.lisp @@ -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) diff --git a/test-ps-compile.lisp b/test-ps-compile.lisp new file mode 100644 index 0000000..ee30b39 --- /dev/null +++ b/test-ps-compile.lisp @@ -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~%~%")