From beec9f4c5795b2e1d8e9cbbd41ef4e0a46e4be9e Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Wed, 27 Nov 2024 15:05:03 -0600 Subject: [PATCH] Fixes #6362 -- Provide DJ display name in Liquidsoap for scripting purposes, move DJ username/display name to API response and move djon/djoff to low-priority thread calls. --- .../Repository/StationStreamerRepository.php | 33 -------- .../Liquidsoap/Command/DjAuthCommand.php | 59 +++++++++++++- util/docker/stations/liquidsoap/azuracast.liq | 77 +++++++++++-------- 3 files changed, 103 insertions(+), 66 deletions(-) diff --git a/backend/src/Entity/Repository/StationStreamerRepository.php b/backend/src/Entity/Repository/StationStreamerRepository.php index e490bb60e8..e5018cee2b 100644 --- a/backend/src/Entity/Repository/StationStreamerRepository.php +++ b/backend/src/Entity/Repository/StationStreamerRepository.php @@ -9,7 +9,6 @@ use App\Entity\StationStreamerBroadcast; use App\Flysystem\StationFilesystems; use App\Media\AlbumArt; -use App\Radio\AutoDJ\Scheduler; /** * @extends AbstractStationBasedRepository @@ -19,42 +18,10 @@ final class StationStreamerRepository extends AbstractStationBasedRepository protected string $entityClass = StationStreamer::class; public function __construct( - private readonly Scheduler $scheduler, private readonly StationStreamerBroadcastRepository $broadcastRepo ) { } - /** - * Attempt to authenticate a streamer. - * - * @param Station $station - * @param string $username - * @param string $password - */ - public function authenticate( - Station $station, - string $username = '', - string $password = '' - ): bool { - // Extra safety check for the station's streamer status. - if (!$station->getEnableStreamers()) { - return false; - } - - // Allow connections using the exact broadcast source password. - $sourcePw = $station->getFrontendConfig()->getSourcePassword(); - if (!empty($sourcePw) && strcmp($sourcePw, $password) === 0) { - return true; - } - - $streamer = $this->getStreamer($station, $username); - if (!($streamer instanceof StationStreamer)) { - return false; - } - - return $streamer->authenticate($password) && $this->scheduler->canStreamerStreamNow($streamer); - } - public function onConnect(Station $station, string $username = ''): bool { // End all current streamer sessions. diff --git a/backend/src/Radio/Backend/Liquidsoap/Command/DjAuthCommand.php b/backend/src/Radio/Backend/Liquidsoap/Command/DjAuthCommand.php index ce2b7d23df..15598fcb26 100644 --- a/backend/src/Radio/Backend/Liquidsoap/Command/DjAuthCommand.php +++ b/backend/src/Radio/Backend/Liquidsoap/Command/DjAuthCommand.php @@ -6,12 +6,16 @@ use App\Entity\Repository\StationStreamerRepository; use App\Entity\Station; +use App\Radio\AutoDJ\Scheduler; +use App\Utilities\Types; +use InvalidArgumentException; use RuntimeException; final class DjAuthCommand extends AbstractCommand { public function __construct( private readonly StationStreamerRepository $streamerRepo, + private readonly Scheduler $scheduler, ) { } @@ -24,11 +28,60 @@ protected function doRun( throw new RuntimeException('Streamers are disabled on this station.'); } - $user = $payload['user'] ?? ''; - $pass = $payload['password'] ?? ''; + [$user, $pass] = $this->getCredentials($payload); + + // Allow connections using the exact broadcast source password. + if ('source' === $user) { + $sourcePw = $station->getFrontendConfig()->getSourcePassword(); + + if (!empty($sourcePw) && strcmp($sourcePw, $pass) === 0) { + return [ + 'allow' => true, + 'username' => $user, + ]; + } + } + + $streamer = $this->streamerRepo->getStreamer($station, $user); + + if (null === $streamer) { + return [ + 'allow' => false, + ]; + } return [ - 'allow' => $this->streamerRepo->authenticate($station, $user, $pass), + 'allow' => $streamer->authenticate($pass) && $this->scheduler->canStreamerStreamNow($streamer), + 'username' => $streamer->getStreamerUsername(), + 'display_name' => $streamer->getDisplayName(), ]; } + + /** + * @return array{string, string} + */ + private function getCredentials(array $payload = []): array + { + $user = Types::stringOrNull($payload['user'] ?? null, true); + $pass = Types::stringOrNull($payload['password'] ?? null, true); + + if (null === $pass) { + throw new InvalidArgumentException('No credentials provided!'); + } + + if (null === $user || 'source' === $user) { + foreach ([',', ':'] as $separator) { + if (str_contains($pass, $separator)) { + [$user, $pass] = explode($separator, $pass, 2); + return [$user, $pass]; + } + } + } + + if (null === $user) { + throw new InvalidArgumentException('No credentials provided!'); + } + + return [$user, $pass]; + } } diff --git a/util/docker/stations/liquidsoap/azuracast.liq b/util/docker/stations/liquidsoap/azuracast.liq index ba9a4a2f82..5bd45b8093 100644 --- a/util/docker/stations/liquidsoap/azuracast.liq +++ b/util/docker/stations/liquidsoap/azuracast.liq @@ -104,11 +104,13 @@ let azuracast.autodj_is_loading = ref(true) # Number of attempts that have been made to ping the AutoDJ for the next song. let azuracast.autodj_ping_attempts = ref(0) -# The username of the last authenticated DJ. +# The username/display name of the last authenticated DJ. let azuracast.last_authenticated_dj = ref("") +let azuracast.last_authenticated_dj_name = ref("") -# The username of the current live DJ. +# The username/display name of the current live DJ. let azuracast.live_dj = ref("") +let azuracast.live_dj_name = ref("") # Whether Liquidsoap is currently transitioning to a live stream. let azuracast.to_live = ref(false) @@ -440,17 +442,7 @@ Live Streamers/DJs -----># # DJ Authentication -def azuracast.dj_auth(login) = - sep_re = r/([,:])/ - auth_info = - if (login.user == "source" or login.user == "") and (sep_re.test(login.password)) then - sep = sep_re.exec(login.password) - let (u, p) = string.split.first(separator=sep[0], login.password) - {user = u, password = p} - else - {user = login.user, password = login.password} - end - +def azuracast.dj_auth(auth_info) = try api_response = azuracast.api_call( timeout=5.0, @@ -461,14 +453,23 @@ def azuracast.dj_auth(login) = if null.defined(api_response) then let json.parse ( { - allow + allow, + username, + display_name, } : { - allow: bool + allow: bool, + username: string?, + display_name: string?, } ) = null.get(api_response) if allow then - azuracast.last_authenticated_dj := auth_info.user + azuracast.last_authenticated_dj := null.get(default='', username) + azuracast.last_authenticated_dj_name := null.get( + default=settings.azuracast.live_broadcast_text(), + display_name + ) + true else false @@ -489,32 +490,48 @@ end def azuracast.live_connected(header) = dj = azuracast.last_authenticated_dj() + dj_name = azuracast.last_authenticated_dj_name() log( level=2, label="azuracast.dj", - "DJ Source connected! Last authenticated DJ: #{dj} - #{header}" + "DJ Source connected! Last authenticated DJ: #{dj} (#{dj_name}) - #{header}" ) azuracast.live_enabled := true azuracast.live_dj := dj + azuracast.live_dj_name := dj_name - _ = azuracast.api_call( - timeout=5.0, - "djon", - json.stringify({user = dj}) - ) + def f() = + _ = azuracast.api_call( + timeout=5.0, + "djon", + json.stringify({ + user = azuracast.live_dj() + }) + ) + end + + thread.run(fast=false, f) end def azuracast.live_disconnected() = - _ = azuracast.api_call( - timeout=5.0, - "djoff", - json.stringify({user = azuracast.live_dj()}) - ) - azuracast.live_enabled := false - azuracast.live_dj := "" + + def f() = + _ = azuracast.api_call( + timeout=5.0, + "djoff", + json.stringify({ + user = azuracast.live_dj() + }) + ) + + azuracast.live_dj := "" + azuracast.live_dj_name := "" + end + + thread.run(fast=false, f) end #<----- @@ -605,7 +622,7 @@ def azuracast.send_feedback(m) = end end - thread.run(f) + thread.run(fast=false, f) end #<-----