Compare commits
5 Commits
0da8101f63
...
5f9dc80ac8
| Author | SHA1 | Date |
|---|---|---|
|
|
5f9dc80ac8 | |
|
|
8700724f81 | |
|
|
61266647a9 | |
|
|
6fd8071a05 | |
|
|
1e5a7e75f3 |
|
|
@ -49,7 +49,8 @@
|
||||||
(:file "template-utils")
|
(:file "template-utils")
|
||||||
(:file "parenscript-utils")
|
(:file "parenscript-utils")
|
||||||
(:module :parenscript
|
(:module :parenscript
|
||||||
:components ((:file "recently-played")
|
:components ((:file "parenscript-utils")
|
||||||
|
(:file "recently-played")
|
||||||
(:file "auth-ui")
|
(:file "auth-ui")
|
||||||
(:file "front-page")
|
(:file "front-page")
|
||||||
(:file "profile")
|
(:file "profile")
|
||||||
|
|
|
||||||
|
|
@ -825,22 +825,24 @@
|
||||||
"Main front page"
|
"Main front page"
|
||||||
;; Register this visitor for geo stats (captures real IP from X-Forwarded-For)
|
;; Register this visitor for geo stats (captures real IP from X-Forwarded-For)
|
||||||
(register-web-listener)
|
(register-web-listener)
|
||||||
(clip:process-to-string
|
(let ((now-playing-stats (icecast-now-playing *stream-base-url*)))
|
||||||
(load-template "front-page")
|
(clip:process-to-string
|
||||||
:title "ASTEROID RADIO"
|
(load-template "front-page")
|
||||||
:station-name "ASTEROID RADIO"
|
:title "ASTEROID RADIO"
|
||||||
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
:station-name "ASTEROID RADIO"
|
||||||
:listeners "0"
|
:status-message "🟢 LIVE - Broadcasting asteroid music for hackers"
|
||||||
:stream-quality "128kbps MP3"
|
:listeners "0"
|
||||||
:stream-base-url *stream-base-url*
|
:connection-error (not now-playing-stats)
|
||||||
:curated-channel-name (get-curated-channel-name)
|
:stream-quality "128kbps MP3"
|
||||||
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-encoding "audio/aac"
|
:curated-channel-name (get-curated-channel-name)
|
||||||
:default-stream-encoding-desc "AAC 96kbps Stereo"
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:now-playing-artist "The Void"
|
:default-stream-encoding "audio/aac"
|
||||||
:now-playing-track "Silence"
|
:default-stream-encoding-desc "AAC 96kbps Stereo"
|
||||||
:now-playing-album "Startup Sounds"
|
:now-playing-artist "The Void"
|
||||||
:now-playing-duration "∞"))
|
:now-playing-track "Silence"
|
||||||
|
:now-playing-album "Startup Sounds"
|
||||||
|
:now-playing-duration "∞")))
|
||||||
|
|
||||||
;; Frameset wrapper for persistent player mode
|
;; Frameset wrapper for persistent player mode
|
||||||
(define-page frameset-wrapper #@"/frameset" ()
|
(define-page frameset-wrapper #@"/frameset" ()
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
(error (e)
|
(error (e)
|
||||||
(declare (ignore e))
|
(declare (ignore e))
|
||||||
nil))))
|
nil))))
|
||||||
|
|
||||||
(defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3"))
|
(defun icecast-now-playing (icecast-base-url &optional (mount "asteroid.mp3"))
|
||||||
"Fetch now-playing information from Icecast server.
|
"Fetch now-playing information from Icecast server.
|
||||||
|
|
||||||
|
|
@ -89,7 +90,8 @@
|
||||||
`((:listenurl . ,(format nil "~a/~a" *stream-base-url* mount))
|
`((:listenurl . ,(format nil "~a/~a" *stream-base-url* mount))
|
||||||
(:title . ,title)
|
(:title . ,title)
|
||||||
(:listeners . ,total-listeners)
|
(:listeners . ,total-listeners)
|
||||||
(:track-id . ,(find-track-by-title title))))))))
|
(:track-id . ,(find-track-by-title title))
|
||||||
|
(:favorite-count . ,(or (get-track-favorite-count title) 1))))))))
|
||||||
|
|
||||||
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
|
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
|
||||||
"Get Partial HTML with live status from Icecast server.
|
"Get Partial HTML with live status from Icecast server.
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@
|
||||||
nil)))))
|
nil)))))
|
||||||
|
|
||||||
;; Update now playing info from API
|
;; Update now playing info from API
|
||||||
(defun update-now-playing ()
|
(defun update-now-playing()
|
||||||
(let ((mount (get-current-mount)))
|
(let ((mount (get-current-mount)))
|
||||||
(ps:chain
|
(ps:chain
|
||||||
(fetch (+ "/api/asteroid/partial/now-playing?mount=" mount))
|
(fetch (+ "/api/asteroid/partial/now-playing?mount=" mount))
|
||||||
|
|
@ -250,19 +250,25 @@
|
||||||
;; Check if this track is in user's favorites
|
;; Check if this track is in user's favorites
|
||||||
(check-favorite-status)
|
(check-favorite-status)
|
||||||
;; Update favorite count display
|
;; Update favorite count display
|
||||||
(let ((count-el (ps:chain document (get-element-by-id "favorite-count-display")))
|
(update-favorite-information)
|
||||||
(count-val-el (ps:chain document (get-element-by-id "favorite-count-value"))))
|
(update-media-session new-title)))))))))
|
||||||
(when (and count-el count-val-el)
|
|
||||||
(let ((fav-count (parse-int (or (ps:@ count-val-el value) "0") 10)))
|
|
||||||
(if (> fav-count 0)
|
|
||||||
(setf (ps:@ count-el text-content)
|
|
||||||
(if (= fav-count 1)
|
|
||||||
"1 person loves this track ❤️"
|
|
||||||
(+ fav-count " people love this track ❤️")))
|
|
||||||
(setf (ps:@ count-el text-content) "")))))))))))))
|
|
||||||
(catch (lambda (error)
|
(catch (lambda (error)
|
||||||
(ps:chain console (log "Could not fetch stream status:" error)))))))
|
(ps:chain console (log "Could not fetch stream status:" error)))))))
|
||||||
|
|
||||||
|
;; Update favorite count display
|
||||||
|
(defun update-favorite-information ()
|
||||||
|
(let ((count-el (ps:chain document (get-element-by-id "favorite-count-display")))
|
||||||
|
(count-val-el (ps:chain document (get-element-by-id "favorite-count-value"))))
|
||||||
|
(when (and count-el count-val-el)
|
||||||
|
(let ((fav-count (parse-int (or (ps:@ count-val-el value) "0") 10)))
|
||||||
|
(if (> fav-count 0)
|
||||||
|
(setf (ps:@ count-el text-content)
|
||||||
|
(if (= fav-count 1)
|
||||||
|
"1 person loves this track ❤️"
|
||||||
|
(+ fav-count " people love this track ❤️")))
|
||||||
|
(setf (ps:@ count-el text-content) ""))))))
|
||||||
|
|
||||||
|
|
||||||
;; Update stream information
|
;; Update stream information
|
||||||
(defun update-stream-information ()
|
(defun update-stream-information ()
|
||||||
(let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel"))
|
(let* ((channel-selector (or (ps:chain document (get-element-by-id "stream-channel"))
|
||||||
|
|
@ -635,6 +641,7 @@
|
||||||
|
|
||||||
;; Load user's favorites for highlight feature
|
;; Load user's favorites for highlight feature
|
||||||
(load-favorites-cache)
|
(load-favorites-cache)
|
||||||
|
(update-favorite-information)
|
||||||
|
|
||||||
;; Update now playing
|
;; Update now playing
|
||||||
(update-now-playing)
|
(update-now-playing)
|
||||||
|
|
@ -864,4 +871,6 @@
|
||||||
|
|
||||||
(defun generate-front-page-js ()
|
(defun generate-front-page-js ()
|
||||||
"Return the pre-compiled JavaScript for front page"
|
"Return the pre-compiled JavaScript for front page"
|
||||||
*front-page-js*)
|
(ps-join
|
||||||
|
*common-player-js*
|
||||||
|
*front-page-js*))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
;;;; parenscript-utils.lisp - ParenScript utility functions
|
||||||
|
|
||||||
|
(in-package #:asteroid)
|
||||||
|
|
||||||
|
(defmacro ps-join (&body forms)
|
||||||
|
`(format nil "~{~A~^~%~%~}" (list ,@forms)))
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -524,6 +524,7 @@
|
||||||
(setf (ps:@ el text-content) title)
|
(setf (ps:@ el text-content) title)
|
||||||
;; Check if this track is in user's favorites
|
;; Check if this track is in user's favorites
|
||||||
(check-favorite-status-mini))
|
(check-favorite-status-mini))
|
||||||
|
(update-media-session title)
|
||||||
(when track-id-el
|
(when track-id-el
|
||||||
(let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id))))
|
(let ((track-id (or (ps:@ data data track_id) (ps:@ data track_id))))
|
||||||
(setf (ps:@ track-id-el value) (or track-id ""))))
|
(setf (ps:@ track-id-el value) (or track-id ""))))
|
||||||
|
|
@ -634,7 +635,8 @@
|
||||||
(when title-el
|
(when title-el
|
||||||
(setf (ps:@ title-el text-content) (ps:chain track-text (trim))))
|
(setf (ps:@ title-el text-content) (ps:chain track-text (trim))))
|
||||||
(when artist-el
|
(when artist-el
|
||||||
(setf (ps:@ artist-el text-content) "Asteroid Radio"))))))))
|
(setf (ps:@ artist-el text-content) "Asteroid Radio")))))
|
||||||
|
(update-media-session track-text))))
|
||||||
(catch (lambda (error)
|
(catch (lambda (error)
|
||||||
(ps:chain console (error "Error updating now playing:" error)))))))
|
(ps:chain console (error "Error updating now playing:" error)))))))
|
||||||
|
|
||||||
|
|
@ -1082,4 +1084,6 @@
|
||||||
|
|
||||||
(defun generate-stream-player-js ()
|
(defun generate-stream-player-js ()
|
||||||
"Generate JavaScript code for the stream player"
|
"Generate JavaScript code for the stream player"
|
||||||
*stream-player-js*)
|
(ps-join
|
||||||
|
*common-player-js*
|
||||||
|
*stream-player-js*))
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@
|
||||||
;; Step 1: Reload the playlist file in Liquidsoap
|
;; Step 1: Reload the playlist file in Liquidsoap
|
||||||
(dotimes (attempt max-retries)
|
(dotimes (attempt max-retries)
|
||||||
(let ((result (liquidsoap-command "stream-queue_m3u.reload")))
|
(let ((result (liquidsoap-command "stream-queue_m3u.reload")))
|
||||||
(format t "~&[SCHEDULER] Reload attempt ~a/~a: ~a~%"
|
|
||||||
(1+ attempt) max-retries (string-trim '(#\Space #\Newline #\Return) result))
|
|
||||||
(when (liquidsoap-command-succeeded-p result)
|
(when (liquidsoap-command-succeeded-p result)
|
||||||
(setf reload-ok t)
|
(setf reload-ok t)
|
||||||
(return)))
|
(return)))
|
||||||
|
|
@ -62,8 +60,6 @@
|
||||||
(sleep 1)) ; Brief pause after reload before skipping
|
(sleep 1)) ; Brief pause after reload before skipping
|
||||||
(dotimes (attempt max-retries)
|
(dotimes (attempt max-retries)
|
||||||
(let ((result (liquidsoap-command "stream-queue_m3u.skip")))
|
(let ((result (liquidsoap-command "stream-queue_m3u.skip")))
|
||||||
(format t "~&[SCHEDULER] Skip attempt ~a/~a: ~a~%"
|
|
||||||
(1+ attempt) max-retries (string-trim '(#\Space #\Newline #\Return) result))
|
|
||||||
(when (liquidsoap-command-succeeded-p result)
|
(when (liquidsoap-command-succeeded-p result)
|
||||||
(setf skip-ok t)
|
(setf skip-ok t)
|
||||||
(return)))
|
(return)))
|
||||||
|
|
@ -76,30 +72,23 @@
|
||||||
(let ((playlist-path (merge-pathnames playlist-name (get-playlists-directory))))
|
(let ((playlist-path (merge-pathnames playlist-name (get-playlists-directory))))
|
||||||
(if (probe-file playlist-path)
|
(if (probe-file playlist-path)
|
||||||
(progn
|
(progn
|
||||||
(format t "~&[SCHEDULER] Loading playlist: ~a~%" playlist-name)
|
|
||||||
(copy-playlist-to-stream-queue playlist-path)
|
(copy-playlist-to-stream-queue playlist-path)
|
||||||
(load-queue-from-m3u-file)
|
(load-queue-from-m3u-file)
|
||||||
(multiple-value-bind (skip-ok reload-ok)
|
(multiple-value-bind (skip-ok reload-ok)
|
||||||
(liquidsoap-reload-and-skip)
|
(liquidsoap-reload-and-skip)
|
||||||
(cond
|
(if (and reload-ok skip-ok)
|
||||||
((and reload-ok skip-ok)
|
(log:info "Scheduler loaded ~a" playlist-name)
|
||||||
(format t "~&[SCHEDULER] Playlist ~a loaded and crossfade triggered successfully~%" playlist-name))
|
(log:error "Scheduler failed to switch to ~a (reload:~a skip:~a)"
|
||||||
(skip-ok
|
playlist-name reload-ok skip-ok)))
|
||||||
(format t "~&[SCHEDULER] WARNING: Reload failed but skip succeeded for ~a~%" playlist-name))
|
|
||||||
(reload-ok
|
|
||||||
(format t "~&[SCHEDULER] WARNING: Reload OK but skip failed for ~a - track may not change immediately~%" playlist-name))
|
|
||||||
(t
|
|
||||||
(format t "~&[SCHEDULER] ERROR: Both reload and skip failed for ~a - Liquidsoap may be unresponsive~%" playlist-name))))
|
|
||||||
t)
|
t)
|
||||||
(progn
|
(progn
|
||||||
(format t "~&[SCHEDULER] Error: Playlist not found: ~a~%" playlist-name)
|
(log:error "Scheduler playlist not found: ~a" playlist-name)
|
||||||
nil))))
|
nil))))
|
||||||
|
|
||||||
(defun scheduled-playlist-loader (hour playlist-name)
|
(defun scheduled-playlist-loader (hour playlist-name)
|
||||||
"Create a function that loads a specific playlist. Used by cl-cron jobs."
|
"Create a function that loads a specific playlist. Used by cl-cron jobs."
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(when *scheduler-enabled*
|
(when *scheduler-enabled*
|
||||||
(format t "~&[SCHEDULER] Triggered at hour ~a UTC - loading ~a~%" hour playlist-name)
|
|
||||||
(load-scheduled-playlist playlist-name))))
|
(load-scheduled-playlist playlist-name))))
|
||||||
|
|
||||||
;;; Cron Job Management
|
;;; Cron Job Management
|
||||||
|
|
@ -107,30 +96,25 @@
|
||||||
(defun setup-playlist-cron-jobs ()
|
(defun setup-playlist-cron-jobs ()
|
||||||
"Set up cl-cron jobs for all scheduled playlists."
|
"Set up cl-cron jobs for all scheduled playlists."
|
||||||
(unless *scheduler-running*
|
(unless *scheduler-running*
|
||||||
(format t "~&[SCHEDULER] Setting up playlist schedule:~%")
|
|
||||||
(dolist (entry *playlist-schedule*)
|
(dolist (entry *playlist-schedule*)
|
||||||
(let ((hour (car entry))
|
(let ((hour (car entry))
|
||||||
(playlist (cdr entry)))
|
(playlist (cdr entry)))
|
||||||
(format t "~&[SCHEDULER] ~2,'0d:00 UTC -> ~a~%" hour playlist)
|
|
||||||
(cl-cron:make-cron-job
|
(cl-cron:make-cron-job
|
||||||
(scheduled-playlist-loader hour playlist)
|
(scheduled-playlist-loader hour playlist)
|
||||||
:minute 0
|
:minute 0
|
||||||
:hour hour)))
|
:hour hour)))
|
||||||
(setf *scheduler-running* t)
|
(setf *scheduler-running* t)))
|
||||||
(format t "~&[SCHEDULER] Playlist schedule configured~%")))
|
|
||||||
|
|
||||||
(defun start-playlist-scheduler ()
|
(defun start-playlist-scheduler ()
|
||||||
"Start the playlist scheduler. Sets up cron jobs and starts cl-cron."
|
"Start the playlist scheduler. Sets up cron jobs and starts cl-cron."
|
||||||
(setup-playlist-cron-jobs)
|
(setup-playlist-cron-jobs)
|
||||||
(cl-cron:start-cron)
|
(cl-cron:start-cron)
|
||||||
(format t "~&[SCHEDULER] Playlist scheduler started~%")
|
|
||||||
t)
|
t)
|
||||||
|
|
||||||
(defun stop-playlist-scheduler ()
|
(defun stop-playlist-scheduler ()
|
||||||
"Stop the playlist scheduler."
|
"Stop the playlist scheduler."
|
||||||
(cl-cron:stop-cron)
|
(cl-cron:stop-cron)
|
||||||
(setf *scheduler-running* nil)
|
(setf *scheduler-running* nil)
|
||||||
(format t "~&[SCHEDULER] Playlist scheduler stopped~%")
|
|
||||||
t)
|
t)
|
||||||
|
|
||||||
(defun restart-playlist-scheduler ()
|
(defun restart-playlist-scheduler ()
|
||||||
|
|
@ -150,10 +134,9 @@
|
||||||
(mapcar (lambda (row)
|
(mapcar (lambda (row)
|
||||||
(cons (first row) (second row)))
|
(cons (first row) (second row)))
|
||||||
rows))
|
rows))
|
||||||
(format t "~&[SCHEDULER] Loaded ~a schedule entries from database~%" (length rows)))))
|
(log:info "Scheduler loaded ~a entries from database" (length rows)))))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "~&[SCHEDULER] Warning: Could not load schedule from DB: ~a~%" e)
|
(log:warn "Scheduler DB load failed, using defaults: ~a" e))))
|
||||||
(format t "~&[SCHEDULER] Using default schedule~%"))))
|
|
||||||
|
|
||||||
(defun save-schedule-entry-to-db (hour playlist-name)
|
(defun save-schedule-entry-to-db (hour playlist-name)
|
||||||
"Save or update a schedule entry in the database."
|
"Save or update a schedule entry in the database."
|
||||||
|
|
@ -172,7 +155,7 @@
|
||||||
(format nil "INSERT INTO playlist_schedule (hour, playlist, updated_at) VALUES (~a, '~a', NOW()) ON CONFLICT (hour) DO UPDATE SET playlist = '~a', updated_at = NOW()"
|
(format nil "INSERT INTO playlist_schedule (hour, playlist, updated_at) VALUES (~a, '~a', NOW()) ON CONFLICT (hour) DO UPDATE SET playlist = '~a', updated_at = NOW()"
|
||||||
hour playlist-name playlist-name)))
|
hour playlist-name playlist-name)))
|
||||||
(error (e2)
|
(error (e2)
|
||||||
(format t "~&[SCHEDULER] Warning: Could not save schedule entry: ~a~%" e2))))))
|
(log:warn "Scheduler could not save schedule entry: ~a" e2))))))
|
||||||
|
|
||||||
(defun delete-schedule-entry-from-db (hour)
|
(defun delete-schedule-entry-from-db (hour)
|
||||||
"Delete a schedule entry from the database."
|
"Delete a schedule entry from the database."
|
||||||
|
|
@ -180,7 +163,7 @@
|
||||||
(with-db
|
(with-db
|
||||||
(postmodern:query (:delete-from 'playlist_schedule :where (:= 'hour hour))))
|
(postmodern:query (:delete-from 'playlist_schedule :where (:= 'hour hour))))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "~&[SCHEDULER] Warning: Could not delete schedule entry: ~a~%" e))))
|
(log:warn "Scheduler could not delete schedule entry: ~a" e))))
|
||||||
|
|
||||||
(defun add-scheduled-playlist (hour playlist-name)
|
(defun add-scheduled-playlist (hour playlist-name)
|
||||||
"Add or update a playlist in the schedule (persists to database)."
|
"Add or update a playlist in the schedule (persists to database)."
|
||||||
|
|
@ -352,17 +335,13 @@
|
||||||
|
|
||||||
(define-trigger db:connected ()
|
(define-trigger db:connected ()
|
||||||
"Start the playlist scheduler after database connection is established"
|
"Start the playlist scheduler after database connection is established"
|
||||||
(format t "~&[SCHEDULER] Database connected, starting playlist scheduler...~%")
|
|
||||||
(handler-case
|
(handler-case
|
||||||
(progn
|
(progn
|
||||||
;; Load schedule from database first
|
|
||||||
(load-schedule-from-db)
|
(load-schedule-from-db)
|
||||||
(start-playlist-scheduler)
|
(start-playlist-scheduler)
|
||||||
;; Load the current scheduled playlist on startup
|
|
||||||
(let ((current-playlist (get-current-scheduled-playlist)))
|
(let ((current-playlist (get-current-scheduled-playlist)))
|
||||||
(when current-playlist
|
(when current-playlist
|
||||||
(format t "~&[SCHEDULER] Loading current scheduled playlist: ~a~%" current-playlist)
|
|
||||||
(load-scheduled-playlist current-playlist)))
|
(load-scheduled-playlist current-playlist)))
|
||||||
(format t "~&[SCHEDULER] Scheduler auto-started successfully~%"))
|
(log:info "Playlist scheduler started"))
|
||||||
(error (e)
|
(error (e)
|
||||||
(format t "~&[SCHEDULER] Warning: Could not auto-start scheduler: ~a~%" e))))
|
(log:error "Scheduler failed to start: ~a" e))))
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
|
|
@ -117,7 +117,9 @@
|
||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="now-playing" class="now-playing"></div>
|
<div id="now-playing" class="now-playing">
|
||||||
|
<c:h>(asteroid::load-template "partial/now-playing")</c:h>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Recently Played Tracks -->
|
<!-- Recently Played Tracks -->
|
||||||
<div id="recently-played-panel" class="recently-played-panel">
|
<div id="recently-played-panel" class="recently-played-panel">
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@
|
||||||
<span class="star-icon">☆</span>
|
<span class="star-icon">☆</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p>Listeners: <span lquery="(text listeners)">1</span></p>
|
<p>Listeners: <span id="current-listeners" lquery="(text listeners)">1</span></p>
|
||||||
|
<input type="hidden" id="current-track-id" lquery="(val track-id)" value="">
|
||||||
|
<input type="hidden" id="favorite-count-value" lquery="(val favorite-count)" value="0">
|
||||||
|
<p class="favorite-count" id="favorite-count-display"></p>
|
||||||
</c:using>
|
</c:using>
|
||||||
<input type="hidden" id="current-track-id" lquery="(val track-id)" value="">
|
|
||||||
<input type="hidden" id="favorite-count-value" lquery="(val favorite-count)" value="0">
|
|
||||||
<p class="favorite-count" id="favorite-count-display"></p>
|
|
||||||
</c:then>
|
</c:then>
|
||||||
<c:else>
|
<c:else>
|
||||||
<c:if test="connection-error">
|
<c:if test="connection-error">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue