diff --git a/config/routes/api_station.php b/config/routes/api_station.php
index 86175b017..7ce2f9ce5 100644
--- a/config/routes/api_station.php
+++ b/config/routes/api_station.php
@@ -40,10 +40,12 @@ return static function (RouteCollectorProxy $group) {
// On-Demand Streaming
$group->get('/ondemand', Controller\Api\Stations\OnDemand\ListAction::class)
- ->setName('api:stations:ondemand:list');
+ ->setName('api:stations:ondemand:list')
+ ->add(new Middleware\StationSupportsFeature(StationFeatures::OnDemand));
$group->get('/ondemand/download/{media_id}', Controller\Api\Stations\OnDemand\DownloadAction::class)
->setName('api:stations:ondemand:download')
+ ->add(new Middleware\StationSupportsFeature(StationFeatures::OnDemand))
->add(new Middleware\RateLimit('ondemand', 1, 2));
// Podcast Public Pages
diff --git a/config/routes/public.php b/config/routes/public.php
index 00ad458e7..55d3437c2 100644
--- a/config/routes/public.php
+++ b/config/routes/public.php
@@ -38,7 +38,8 @@ return static function (RouteCollectorProxy $app) {
->setName('public:manifest');
$group->get('/embed-requests', Controller\Frontend\PublicPages\RequestsAction::class)
- ->setName('public:embedrequests');
+ ->setName('public:embedrequests')
+ ->add(new Middleware\StationSupportsFeature(App\Enums\StationFeatures::Requests));
$group->get('/playlist[.{format}]', Controller\Frontend\PublicPages\PlaylistAction::class)
->setName('public:playlist');
@@ -50,7 +51,8 @@ return static function (RouteCollectorProxy $app) {
->setName('public:dj');
$group->get('/ondemand[/{embed:embed}]', Controller\Frontend\PublicPages\OnDemandAction::class)
- ->setName('public:ondemand');
+ ->setName('public:ondemand')
+ ->add(new Middleware\StationSupportsFeature(App\Enums\StationFeatures::OnDemand));
$group->get('/schedule[/{embed:embed}]', Controller\Frontend\PublicPages\ScheduleAction::class)
->setName('public:schedule');
diff --git a/frontend/src/vendor/axios.ts b/frontend/src/vendor/axios.ts
index cafb4c806..6949f16f4 100644
--- a/frontend/src/vendor/axios.ts
+++ b/frontend/src/vendor/axios.ts
@@ -37,8 +37,9 @@ export default function installAxios(vueApp: App) {
let notifyMessage = $gettext('An error occurred and your request could not be completed.');
if (error.response) {
// Request made and server responded
- notifyMessage = error.response.data.message;
- console.error(notifyMessage);
+ const responseJson = error.response.data ?? {};
+ notifyMessage = responseJson.message ?? notifyMessage;
+ console.error(responseJson);
} else if (error.request) {
// The request was made but no response was received
console.error(error.request);
diff --git a/src/Controller/Api/Admin/Backups/AbstractFileAction.php b/src/Controller/Api/Admin/Backups/AbstractFileAction.php
index a2121ccff..cd531abb5 100644
--- a/src/Controller/Api/Admin/Backups/AbstractFileAction.php
+++ b/src/Controller/Api/Admin/Backups/AbstractFileAction.php
@@ -36,7 +36,7 @@ abstract class AbstractFileAction implements SingleActionInterface
->getFilesystem();
if (!$fs->fileExists($path)) {
- throw new NotFoundException(__('Backup not found.'));
+ throw NotFoundException::file();
}
return [$path, $fs];
diff --git a/src/Controller/Api/Admin/Debug/TelnetAction.php b/src/Controller/Api/Admin/Debug/TelnetAction.php
index a7c52ab65..41f757dc6 100644
--- a/src/Controller/Api/Admin/Debug/TelnetAction.php
+++ b/src/Controller/Api/Admin/Debug/TelnetAction.php
@@ -6,7 +6,6 @@ namespace App\Controller\Api\Admin\Debug;
use App\Container\LoggerAwareTrait;
use App\Controller\SingleActionInterface;
-use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
@@ -32,11 +31,7 @@ final class TelnetAction implements SingleActionInterface
$this->logger->pushHandler($testHandler);
$station = $request->getStation();
- $backend = $this->adapters->getBackendAdapter($station);
-
- if (null === $backend) {
- throw new StationUnsupportedException();
- }
+ $backend = $this->adapters->requireBackendAdapter($station);
$command = $request->getParam('command');
diff --git a/src/Controller/Api/Stations/AbstractSearchableListAction.php b/src/Controller/Api/Stations/AbstractSearchableListAction.php
index 09ddbeaa3..0be92ddb3 100644
--- a/src/Controller/Api/Stations/AbstractSearchableListAction.php
+++ b/src/Controller/Api/Stations/AbstractSearchableListAction.php
@@ -11,7 +11,6 @@ use App\Entity\StationMedia;
use App\Http\ServerRequest;
use App\Paginator;
use Psr\Cache\CacheItemPoolInterface;
-use RuntimeException;
abstract class AbstractSearchableListAction implements SingleActionInterface
{
@@ -31,10 +30,6 @@ abstract class AbstractSearchableListAction implements SingleActionInterface
ServerRequest $request,
array $playlists
): Paginator {
- if (empty($playlists)) {
- throw new RuntimeException('This station has no qualifying playlists for this feature.');
- }
-
$station = $request->getStation();
$queryParams = $request->getQueryParams();
diff --git a/src/Controller/Api/Stations/OnDemand/DownloadAction.php b/src/Controller/Api/Stations/OnDemand/DownloadAction.php
index 971fcc422..a575df0bd 100644
--- a/src/Controller/Api/Stations/OnDemand/DownloadAction.php
+++ b/src/Controller/Api/Stations/OnDemand/DownloadAction.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations\OnDemand;
use App\Controller\SingleActionInterface;
-use App\Entity\Api\Error;
use App\Entity\Repository\StationMediaRepository;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
@@ -30,12 +29,6 @@ final class DownloadAction implements SingleActionInterface
$station = $request->getStation();
- // Verify that the station supports on-demand streaming.
- if (!$station->getEnableOnDemand()) {
- return $response->withStatus(403)
- ->withJson(new Error(403, __('This station does not support on-demand streaming.')));
- }
-
$media = $this->mediaRepo->requireByUniqueId($mediaId, $station);
$fsMedia = $this->stationFilesystems->getMediaFilesystem($station);
diff --git a/src/Controller/Api/Stations/OnDemand/ListAction.php b/src/Controller/Api/Stations/OnDemand/ListAction.php
index 5d62c6f3a..a4080be5c 100644
--- a/src/Controller/Api/Stations/OnDemand/ListAction.php
+++ b/src/Controller/Api/Stations/OnDemand/ListAction.php
@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations\OnDemand;
use App\Controller\Api\Stations\AbstractSearchableListAction;
-use App\Entity\Api\Error;
use App\Entity\Api\StationOnDemand;
use App\Entity\Station;
use App\Entity\StationMedia;
+use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -22,16 +22,12 @@ final class ListAction extends AbstractSearchableListAction
): ResponseInterface {
$station = $request->getStation();
- // Verify that the station supports on-demand streaming.
- if (!$station->getEnableOnDemand()) {
- return $response->withStatus(403)
- ->withJson(new Error(403, __('This station does not support on-demand streaming.')));
+ $playlists = $this->getPlaylists($station);
+ if (empty($playlists)) {
+ throw StationUnsupportedException::onDemand();
}
- $paginator = $this->getPaginator(
- $request,
- $this->getPlaylists($station)
- );
+ $paginator = $this->getPaginator($request, $playlists);
$router = $request->getRouter();
diff --git a/src/Controller/Api/Stations/Requests/ListAction.php b/src/Controller/Api/Stations/Requests/ListAction.php
index 1926074d5..bd7aee850 100644
--- a/src/Controller/Api/Stations/Requests/ListAction.php
+++ b/src/Controller/Api/Stations/Requests/ListAction.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations\Requests;
use App\Controller\Api\Stations\AbstractSearchableListAction;
-use App\Entity\Api\Error;
use App\Entity\Api\StationRequest;
use App\Entity\ApiGenerator\SongApiGenerator;
use App\Entity\Station;
use App\Entity\StationMedia;
use App\Entity\StationPlaylist;
+use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
@@ -61,16 +61,12 @@ final class ListAction extends AbstractSearchableListAction
): ResponseInterface {
$station = $request->getStation();
- // Verify that the station supports on-demand streaming.
- if (!$station->getEnableRequests()) {
- return $response->withStatus(403)
- ->withJson(new Error(403, __('This station does not support requests.')));
+ $playlists = $this->getPlaylists($station);
+ if (empty($playlists)) {
+ throw StationUnsupportedException::requests();
}
- $paginator = $this->getPaginator(
- $request,
- $this->getPlaylists($station)
- );
+ $paginator = $this->getPaginator($request, $playlists);
$router = $request->getRouter();
diff --git a/src/Controller/Api/Stations/Requests/SubmitAction.php b/src/Controller/Api/Stations/Requests/SubmitAction.php
index 321f5927c..1bbe020c5 100644
--- a/src/Controller/Api/Stations/Requests/SubmitAction.php
+++ b/src/Controller/Api/Stations/Requests/SubmitAction.php
@@ -9,7 +9,6 @@ use App\Controller\SingleActionInterface;
use App\Entity\Api\Status;
use App\Entity\Repository\StationRequestRepository;
use App\Entity\User;
-use App\Exception;
use App\Exception\InvalidRequestAttribute;
use App\Http\Response;
use App\Http\ServerRequest;
@@ -66,25 +65,18 @@ final class SubmitAction implements SingleActionInterface
$user = null;
}
- $isAuthenticated = ($user instanceof User);
+ $ip = $this->readSettings()->getIp($request);
- try {
- $ip = $this->readSettings()->getIp($request);
+ $this->requestRepo->submit(
+ $station,
+ $mediaId,
+ ($user instanceof User),
+ $ip,
+ $request->getHeaderLine('User-Agent')
+ );
- $this->requestRepo->submit(
- $station,
- $mediaId,
- $isAuthenticated,
- $ip,
- $request->getHeaderLine('User-Agent')
- );
-
- return $response->withJson(
- new Status(true, __('Your request has been submitted and will be played soon.'))
- );
- } catch (Exception $e) {
- return $response->withStatus(400)
- ->withJson(new Status(false, $e->getMessage()));
- }
+ return $response->withJson(
+ new Status(true, __('Your request has been submitted and will be played soon.'))
+ );
}
}
diff --git a/src/Controller/Api/Stations/ServicesController.php b/src/Controller/Api/Stations/ServicesController.php
index 15efaeee2..2b78b9903 100644
--- a/src/Controller/Api/Stations/ServicesController.php
+++ b/src/Controller/Api/Stations/ServicesController.php
@@ -8,7 +8,6 @@ use App\Container\EntityManagerAwareTrait;
use App\Entity\Api\Error;
use App\Entity\Api\StationServiceStatus;
use App\Entity\Api\Status;
-use App\Exception\StationUnsupportedException;
use App\Exception\Supervisor\NotRunningException;
use App\Http\Response;
use App\Http\ServerRequest;
@@ -196,11 +195,7 @@ final class ServicesController
$do = $params['do'] ?? 'restart';
$station = $request->getStation();
- $frontend = $this->adapters->getFrontendAdapter($station);
-
- if (null === $frontend) {
- throw new StationUnsupportedException();
- }
+ $frontend = $this->adapters->requireFrontendAdapter($station);
switch ($do) {
case 'stop':
@@ -242,11 +237,7 @@ final class ServicesController
$do = $params['do'] ?? 'restart';
$station = $request->getStation();
- $backend = $this->adapters->getBackendAdapter($station);
-
- if (null === $backend) {
- throw new StationUnsupportedException();
- }
+ $backend = $this->adapters->requireBackendAdapter($station);
switch ($do) {
case 'skip':
diff --git a/src/Controller/Api/Stations/UpdateMetadataAction.php b/src/Controller/Api/Stations/UpdateMetadataAction.php
index 91fb0efa5..ea7643882 100644
--- a/src/Controller/Api/Stations/UpdateMetadataAction.php
+++ b/src/Controller/Api/Stations/UpdateMetadataAction.php
@@ -6,7 +6,6 @@ namespace App\Controller\Api\Stations;
use App\Controller\SingleActionInterface;
use App\Entity\Api\Status;
-use App\Exception\StationUnsupportedException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
@@ -28,11 +27,7 @@ final class UpdateMetadataAction implements SingleActionInterface
): ResponseInterface {
$station = $request->getStation();
- $backend = $this->adapters->getBackendAdapter($station);
-
- if (null === $backend) {
- throw new StationUnsupportedException();
- }
+ $backend = $this->adapters->requireBackendAdapter($station);
$allowedMetaFields = [
'title',
diff --git a/src/Controller/Api/Traits/HasLogViewer.php b/src/Controller/Api/Traits/HasLogViewer.php
index 95821574d..2f54a92ff 100644
--- a/src/Controller/Api/Traits/HasLogViewer.php
+++ b/src/Controller/Api/Traits/HasLogViewer.php
@@ -24,7 +24,7 @@ trait HasLogViewer
clearstatcache();
if (!is_file($logPath)) {
- throw new NotFoundException('Log file not found!');
+ throw NotFoundException::file();
}
if (!$tailFile) {
diff --git a/src/Controller/Frontend/PWA/AppManifestAction.php b/src/Controller/Frontend/PWA/AppManifestAction.php
index 838b37ed0..7d52e0d53 100644
--- a/src/Controller/Frontend/PWA/AppManifestAction.php
+++ b/src/Controller/Frontend/PWA/AppManifestAction.php
@@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PWA;
use App\Controller\SingleActionInterface;
use App\Enums\SupportedThemes;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -21,7 +21,7 @@ final class AppManifestAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$customization = $request->getCustomization();
diff --git a/src/Controller/Frontend/PublicPages/HistoryAction.php b/src/Controller/Frontend/PublicPages/HistoryAction.php
index fc5ce2961..61b810d95 100644
--- a/src/Controller/Frontend/PublicPages/HistoryAction.php
+++ b/src/Controller/Frontend/PublicPages/HistoryAction.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\VueComponent\NowPlayingComponent;
@@ -26,7 +26,7 @@ final class HistoryAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$view = $request->getView();
diff --git a/src/Controller/Frontend/PublicPages/OEmbedAction.php b/src/Controller/Frontend/PublicPages/OEmbedAction.php
index b667e3bfe..cb1746a95 100644
--- a/src/Controller/Frontend/PublicPages/OEmbedAction.php
+++ b/src/Controller/Frontend/PublicPages/OEmbedAction.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Xml\Writer;
@@ -21,7 +21,7 @@ final class OEmbedAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$format = $params['format'] ?? 'json';
diff --git a/src/Controller/Frontend/PublicPages/OnDemandAction.php b/src/Controller/Frontend/PublicPages/OnDemandAction.php
index 5191211ea..93e59f551 100644
--- a/src/Controller/Frontend/PublicPages/OnDemandAction.php
+++ b/src/Controller/Frontend/PublicPages/OnDemandAction.php
@@ -6,8 +6,7 @@ namespace App\Controller\Frontend\PublicPages;
use App\Container\EntityManagerAwareTrait;
use App\Controller\SingleActionInterface;
-use App\Exception\StationNotFoundException;
-use App\Exception\StationUnsupportedException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -27,11 +26,7 @@ final class OnDemandAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
- }
-
- if (!$station->getEnableOnDemand()) {
- throw new StationUnsupportedException();
+ throw NotFoundException::station();
}
// Get list of custom fields.
diff --git a/src/Controller/Frontend/PublicPages/PlayerAction.php b/src/Controller/Frontend/PublicPages/PlayerAction.php
index 2eef6cd28..8ebe1aecf 100644
--- a/src/Controller/Frontend/PublicPages/PlayerAction.php
+++ b/src/Controller/Frontend/PublicPages/PlayerAction.php
@@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
use App\Entity\Repository\CustomFieldRepository;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\VueComponent\NowPlayingComponent;
@@ -35,7 +35,7 @@ final class PlayerAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
// Build Vue props.
diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php b/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php
index b30184622..45e6d48fc 100644
--- a/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php
+++ b/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php
@@ -8,8 +8,7 @@ use App\Controller\SingleActionInterface;
use App\Entity\PodcastEpisode;
use App\Entity\Repository\PodcastEpisodeRepository;
use App\Entity\Repository\PodcastRepository;
-use App\Exception\PodcastNotFoundException;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -37,13 +36,13 @@ final class PodcastEpisodeAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
if ($podcast === null) {
- throw new PodcastNotFoundException();
+ throw NotFoundException::podcast();
}
$episode = $this->episodeRepository->fetchEpisodeForStation($station, $episodeId);
diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php b/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php
index 98933f5aa..53b05b9d8 100644
--- a/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php
+++ b/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php
@@ -8,8 +8,7 @@ use App\Controller\SingleActionInterface;
use App\Entity\PodcastEpisode;
use App\Entity\Repository\PodcastEpisodeRepository;
use App\Entity\Repository\PodcastRepository;
-use App\Exception\PodcastNotFoundException;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -34,13 +33,13 @@ final class PodcastEpisodesAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
if ($podcast === null) {
- throw new PodcastNotFoundException();
+ throw NotFoundException::podcast();
}
$publishedEpisodes = $this->episodeRepository->fetchPublishedEpisodesForPodcast($podcast);
diff --git a/src/Controller/Frontend/PublicPages/PodcastFeedAction.php b/src/Controller/Frontend/PublicPages/PodcastFeedAction.php
index 3ee5ac8af..9a35ea2a8 100644
--- a/src/Controller/Frontend/PublicPages/PodcastFeedAction.php
+++ b/src/Controller/Frontend/PublicPages/PodcastFeedAction.php
@@ -11,8 +11,7 @@ use App\Entity\PodcastEpisode;
use App\Entity\Repository\PodcastRepository;
use App\Entity\Repository\StationRepository;
use App\Entity\Station;
-use App\Exception\PodcastNotFoundException;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\RouterInterface;
@@ -65,17 +64,17 @@ final class PodcastFeedAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
if ($podcast === null) {
- throw new PodcastNotFoundException();
+ throw NotFoundException::podcast();
}
if (!$this->checkHasPublishedEpisodes($podcast)) {
- throw new PodcastNotFoundException();
+ throw NotFoundException::podcast();
}
$generatedRss = $this->generateRssFeed($podcast, $station, $request);
diff --git a/src/Controller/Frontend/PublicPages/PodcastsAction.php b/src/Controller/Frontend/PublicPages/PodcastsAction.php
index d1b7109ac..5622d52e8 100644
--- a/src/Controller/Frontend/PublicPages/PodcastsAction.php
+++ b/src/Controller/Frontend/PublicPages/PodcastsAction.php
@@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
use App\Entity\Repository\PodcastRepository;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -26,7 +26,7 @@ final class PodcastsAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$publishedPodcasts = $this->podcastRepository->fetchPublishedPodcastsForStation($station);
diff --git a/src/Controller/Frontend/PublicPages/RequestsAction.php b/src/Controller/Frontend/PublicPages/RequestsAction.php
index b83ddc690..42bd2a5b6 100644
--- a/src/Controller/Frontend/PublicPages/RequestsAction.php
+++ b/src/Controller/Frontend/PublicPages/RequestsAction.php
@@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
use App\Entity\Repository\CustomFieldRepository;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -26,7 +26,7 @@ final class RequestsAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$router = $request->getRouter();
diff --git a/src/Controller/Frontend/PublicPages/ScheduleAction.php b/src/Controller/Frontend/PublicPages/ScheduleAction.php
index a5f7664ec..d1dccb419 100644
--- a/src/Controller/Frontend/PublicPages/ScheduleAction.php
+++ b/src/Controller/Frontend/PublicPages/ScheduleAction.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@@ -23,7 +23,7 @@ final class ScheduleAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
$router = $request->getRouter();
diff --git a/src/Controller/Frontend/PublicPages/WebDjAction.php b/src/Controller/Frontend/PublicPages/WebDjAction.php
index f2493d8ff..77666b734 100644
--- a/src/Controller/Frontend/PublicPages/WebDjAction.php
+++ b/src/Controller/Frontend/PublicPages/WebDjAction.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Controller\Frontend\PublicPages;
use App\Controller\SingleActionInterface;
-use App\Exception\StationNotFoundException;
-use App\Exception\StationUnsupportedException;
+use App\Enums\StationFeatures;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Adapters;
@@ -27,17 +27,12 @@ final class WebDjAction implements SingleActionInterface
$station = $request->getStation();
if (!$station->getEnablePublicPage()) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
- if (!$station->getEnableStreamers()) {
- throw new StationUnsupportedException();
- }
+ StationFeatures::Streamers->assertSupportedForStation($station);
- $backend = $this->adapters->getBackendAdapter($station);
- if (null === $backend) {
- throw new StationUnsupportedException();
- }
+ $backend = $this->adapters->requireBackendAdapter($station);
$wssUrl = (string)$backend->getWebStreamingUrl($station, $request->getRouter()->getBaseUrl());
diff --git a/src/Doctrine/Repository.php b/src/Doctrine/Repository.php
index aa31df0d6..e189af238 100644
--- a/src/Doctrine/Repository.php
+++ b/src/Doctrine/Repository.php
@@ -58,7 +58,7 @@ class Repository
{
$record = $this->find($id);
if (null === $record) {
- throw new NotFoundException();
+ throw NotFoundException::generic();
}
return $record;
}
diff --git a/src/Entity/Api/Error.php b/src/Entity/Api/Error.php
index 1ce88703b..9e456cf7a 100644
--- a/src/Entity/Api/Error.php
+++ b/src/Entity/Api/Error.php
@@ -96,21 +96,22 @@ final class Error
$className = (new ReflectionClass($e))->getShortName();
- $errorHeader = $className . ' at ' . $e->getFile() . ' L' . $e->getLine();
- $message = $errorHeader . ': ' . $e->getMessage();
-
if ($e instanceof Exception) {
- $messageFormatted = '' . $errorHeader . ': ' . $e->getFormattedMessage();
+ $messageFormatted = $e->getFormattedMessage();
$extraData = $e->getExtraData();
} else {
- $messageFormatted = '' . $errorHeader . ': ' . $e->getMessage();
+ $messageFormatted = $e->getMessage();
$extraData = [];
}
+ $extraData['class'] = $className;
+ $extraData['file'] = $e->getFile();
+ $extraData['line'] = $e->getLine();
+
if ($includeTrace) {
$extraData['trace'] = $e->getTrace();
}
- return new self($code, $message, $messageFormatted, $extraData, $className);
+ return new self($code, $e->getMessage(), $messageFormatted, $extraData, $className);
}
}
diff --git a/src/Entity/Repository/AbstractStationBasedRepository.php b/src/Entity/Repository/AbstractStationBasedRepository.php
index 1064c79df..0566fbdc9 100644
--- a/src/Entity/Repository/AbstractStationBasedRepository.php
+++ b/src/Entity/Repository/AbstractStationBasedRepository.php
@@ -36,7 +36,7 @@ abstract class AbstractStationBasedRepository extends Repository
{
$record = $this->findForStation($id, $station);
if (null === $record) {
- throw new NotFoundException();
+ throw NotFoundException::generic();
}
return $record;
}
diff --git a/src/Entity/Repository/PodcastEpisodeRepository.php b/src/Entity/Repository/PodcastEpisodeRepository.php
index 687708ce1..edf557abb 100644
--- a/src/Entity/Repository/PodcastEpisodeRepository.php
+++ b/src/Entity/Repository/PodcastEpisodeRepository.php
@@ -10,11 +10,11 @@ use App\Entity\PodcastEpisode;
use App\Entity\PodcastMedia;
use App\Entity\Station;
use App\Entity\StorageLocation;
-use App\Exception\InvalidPodcastMediaFileException;
use App\Exception\StorageLocationFullException;
use App\Flysystem\ExtendedFilesystemInterface;
use App\Media\AlbumArt;
use App\Media\MetadataManager;
+use http\Exception\InvalidArgumentException;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToRetrieveMetadata;
@@ -152,8 +152,8 @@ final class PodcastEpisodeRepository extends Repository
$metadata = $this->metadataManager->read($uploadPath);
if (!in_array($metadata->getMimeType(), ['audio/x-m4a', 'audio/mpeg'])) {
- throw new InvalidPodcastMediaFileException(
- 'Invalid Podcast Media mime type: ' . $metadata->getMimeType()
+ throw new InvalidArgumentException(
+ sprintf('Invalid Podcast Media mime type: %s', $metadata->getMimeType())
);
}
diff --git a/src/Entity/Repository/StationMediaRepository.php b/src/Entity/Repository/StationMediaRepository.php
index 6d8d15ea2..2d497a03b 100644
--- a/src/Entity/Repository/StationMediaRepository.php
+++ b/src/Entity/Repository/StationMediaRepository.php
@@ -69,7 +69,7 @@ final class StationMediaRepository extends Repository
{
$record = $this->findForStation($id, $station);
if (null === $record) {
- throw new NotFoundException();
+ throw NotFoundException::generic();
}
return $record;
}
@@ -136,7 +136,7 @@ final class StationMediaRepository extends Repository
): StationMedia {
$record = $this->findByUniqueId($uniqueId, $source);
if (null === $record) {
- throw new NotFoundException();
+ throw NotFoundException::generic();
}
return $record;
}
diff --git a/src/Entity/Repository/StationRequestRepository.php b/src/Entity/Repository/StationRequestRepository.php
index aeb19d1d4..66477308c 100644
--- a/src/Entity/Repository/StationRequestRepository.php
+++ b/src/Entity/Repository/StationRequestRepository.php
@@ -8,6 +8,7 @@ use App\Entity\Api\StationPlaylistQueue;
use App\Entity\Station;
use App\Entity\StationMedia;
use App\Entity\StationRequest;
+use App\Enums\StationFeatures;
use App\Exception;
use App\Radio\AutoDJ;
use App\Radio\Frontend\Blocklist\BlocklistParser;
@@ -62,27 +63,31 @@ final class StationRequestRepository extends AbstractStationBasedRepository
string $userAgent
): int {
// Verify that the station supports requests.
- if (!$station->getEnableRequests()) {
- throw new Exception(__('This station does not accept requests currently.'));
- }
+ StationFeatures::Requests->assertSupportedForStation($station);
// Forbid web crawlers from using this feature.
$dd = $this->deviceDetector->parse($userAgent);
if ($dd->isBot) {
- throw new Exception(__('Search engine crawlers are not permitted to use this feature.'));
+ throw Exception\CannotCompleteActionException::submitRequest(
+ __('Search engine crawlers are not permitted to use this feature.')
+ );
}
// Check frontend blocklist and apply it to requests.
if (!$this->blocklistParser->isAllowed($station, $ip, $userAgent)) {
- throw new Exception(__('You are not permitted to submit requests.'));
+ throw Exception\CannotCompleteActionException::submitRequest(
+ __('You are not permitted to submit requests.')
+ );
}
// Verify that Track ID exists with station.
$mediaItem = $this->mediaRepo->requireByUniqueId($trackId, $station);
if (!$mediaItem->isRequestable()) {
- throw new Exception(__('The song ID you specified cannot be requested for this station.'));
+ throw Exception\CannotCompleteActionException::submitRequest(
+ __('This track is not requestable.')
+ );
}
// Check if the song is already enqueued as a request.
@@ -112,7 +117,7 @@ final class StationRequestRepository extends AbstractStationBasedRepository
->getSingleScalarResult();
if ($recentRequests > 0) {
- throw new Exception(
+ throw Exception\CannotCompleteActionException::submitRequest(
__('You have submitted a request too recently! Please wait before submitting another one.')
);
}
@@ -158,7 +163,9 @@ final class StationRequestRepository extends AbstractStationBasedRepository
}
if ($pendingRequest > 0) {
- throw new Exception(__('Duplicate request: this song was already requested and will play soon.'));
+ throw Exception\CannotCompleteActionException::submitRequest(
+ __('This song was already requested and will play soon.')
+ );
}
return true;
@@ -236,7 +243,7 @@ final class StationRequestRepository extends AbstractStationBasedRepository
$isDuplicate = (null === $this->duplicatePrevention->getDistinctTrack([$eligibleTrack], $recentTracks));
if ($isDuplicate) {
- throw new Exception(
+ throw Exception\CannotCompleteActionException::submitRequest(
__('This song or artist has been played too recently. Wait a while before requesting it again.')
);
}
diff --git a/src/Enums/StationFeatures.php b/src/Enums/StationFeatures.php
index 8b8768333..287d4d5d4 100644
--- a/src/Enums/StationFeatures.php
+++ b/src/Enums/StationFeatures.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Enums;
use App\Entity\Station;
+use App\Exception\StationUnsupportedException;
enum StationFeatures
{
@@ -17,6 +18,7 @@ enum StationFeatures
case Streamers;
case Webhooks;
case Podcasts;
+ case OnDemand;
case Requests;
public function supportedForStation(Station $station): bool
@@ -30,7 +32,24 @@ enum StationFeatures
self::MountPoints => $station->getFrontendType()->supportsMounts(),
self::HlsStreams => $backendEnabled && $station->getEnableHls(),
self::Requests => $backendEnabled && $station->getEnableRequests(),
+ self::OnDemand => $station->getEnableOnDemand(),
self::Webhooks, self::Podcasts, self::RemoteRelays => true,
};
}
+
+ /**
+ * @param Station $station
+ * @return void
+ * @throws StationUnsupportedException
+ */
+ public function assertSupportedForStation(Station $station): void
+ {
+ if (!$this->supportedForStation($station)) {
+ throw match ($this) {
+ self::Requests => StationUnsupportedException::requests(),
+ self::OnDemand => StationUnsupportedException::onDemand(),
+ default => StationUnsupportedException::generic(),
+ };
+ }
+ }
}
diff --git a/src/Exception.php b/src/Exception.php
index 71356d6ad..99a1409c5 100644
--- a/src/Exception.php
+++ b/src/Exception.php
@@ -5,37 +5,28 @@ declare(strict_types=1);
namespace App;
use Exception as PhpException;
-use Psr\Log\LogLevel;
+use Monolog\Level;
use Throwable;
class Exception extends PhpException
{
- /** @var string The logging severity of the exception. */
- protected string $loggerLevel;
-
/** @var array Any additional data that can be displayed in debugging. */
protected array $extraData = [];
/** @var array Additional data supplied to the logger class when handling the exception. */
protected array $loggingContext = [];
- /** @var string|null */
protected ?string $formattedMessage;
public function __construct(
string $message = '',
int $code = 0,
Throwable $previous = null,
- string $loggerLevel = LogLevel::ERROR
+ protected Level $loggerLevel = Level::Error
) {
parent::__construct($message, $code, $previous);
-
- $this->loggerLevel = $loggerLevel;
}
- /**
- * @param string $message
- */
public function setMessage(string $message): void
{
$this->message = $message;
@@ -52,31 +43,22 @@ class Exception extends PhpException
/**
* Set a display-formatted message (if one exists).
- *
- * @param string|null $message
*/
public function setFormattedMessage(?string $message): void
{
$this->formattedMessage = $message;
}
- public function getLoggerLevel(): string
+ public function getLoggerLevel(): Level
{
return $this->loggerLevel;
}
- /**
- * @param string $loggerLevel
- */
- public function setLoggerLevel(string $loggerLevel): void
+ public function setLoggerLevel(Level $loggerLevel): void
{
$this->loggerLevel = $loggerLevel;
}
- /**
- * @param int|string $legend
- * @param mixed $data
- */
public function addExtraData(int|string $legend, mixed $data): void
{
if (is_array($data)) {
@@ -92,18 +74,11 @@ class Exception extends PhpException
return $this->extraData;
}
- /**
- * @param int|string $key
- * @param mixed $data
- */
public function addLoggingContext(int|string $key, mixed $data): void
{
$this->loggingContext[$key] = $data;
}
- /**
- * @return mixed[]
- */
public function getLoggingContext(): array
{
return $this->loggingContext;
diff --git a/src/Exception/BootstrapException.php b/src/Exception/BootstrapException.php
index 415f786be..3bf12243f 100644
--- a/src/Exception/BootstrapException.php
+++ b/src/Exception/BootstrapException.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Exception;
use App\Exception;
-use Psr\Log\LogLevel;
+use Monolog\Level;
use Throwable;
final class BootstrapException extends Exception
@@ -14,7 +14,7 @@ final class BootstrapException extends Exception
string $message = '',
int $code = 0,
Throwable $previous = null,
- string $loggerLevel = LogLevel::ALERT
+ Level $loggerLevel = Level::Alert
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
diff --git a/src/Exception/CannotCompleteActionException.php b/src/Exception/CannotCompleteActionException.php
new file mode 100644
index 000000000..83451fd64
--- /dev/null
+++ b/src/Exception/CannotCompleteActionException.php
@@ -0,0 +1,31 @@
+loggerLevel = $exception->getLoggerLevel();
} elseif ($exception instanceof HttpException) {
- $this->loggerLevel = LogLevel::INFO;
+ $this->loggerLevel = Level::Info;
}
$this->showDetailed = $this->environment->showDetailedErrors();
diff --git a/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php b/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php
index a63a1597e..eea394162 100644
--- a/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php
+++ b/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php
@@ -10,7 +10,7 @@ use App\Entity\Repository\PodcastRepository;
use App\Entity\Station;
use App\Entity\User;
use App\Enums\StationPermissions;
-use App\Exception\PodcastNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
use Exception;
@@ -44,7 +44,7 @@ final class RequirePublishedPodcastEpisodeMiddleware extends AbstractMiddleware
$podcastId = $this->getPodcastIdFromRequest($request);
if ($podcastId === null || !$this->checkPodcastHasPublishedEpisodes($station, $podcastId)) {
- throw new PodcastNotFoundException();
+ throw NotFoundException::podcast();
}
$response = $handler->handle($request);
diff --git a/src/Middleware/RequireStation.php b/src/Middleware/RequireStation.php
index c011a8f03..4b4cf5df6 100644
--- a/src/Middleware/RequireStation.php
+++ b/src/Middleware/RequireStation.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Middleware;
-use App\Exception\StationNotFoundException;
+use App\Exception\NotFoundException;
use App\Http\ServerRequest;
use Exception;
use Psr\Http\Message\ResponseInterface;
@@ -20,7 +20,7 @@ final class RequireStation extends AbstractMiddleware
try {
$request->getStation();
} catch (Exception) {
- throw new StationNotFoundException();
+ throw NotFoundException::station();
}
return $handler->handle($request);
diff --git a/src/Middleware/StationSupportsFeature.php b/src/Middleware/StationSupportsFeature.php
index e08788130..503bb270c 100644
--- a/src/Middleware/StationSupportsFeature.php
+++ b/src/Middleware/StationSupportsFeature.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Middleware;
use App\Enums\StationFeatures;
-use App\Exception;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -19,9 +18,7 @@ final class StationSupportsFeature extends AbstractMiddleware
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
- if (!$this->feature->supportedForStation($request->getStation())) {
- throw new Exception\StationUnsupportedException();
- }
+ $this->feature->assertSupportedForStation($request->getStation());
return $handler->handle($request);
}
diff --git a/src/Radio/Adapters.php b/src/Radio/Adapters.php
index 783040189..1b68f455f 100644
--- a/src/Radio/Adapters.php
+++ b/src/Radio/Adapters.php
@@ -8,6 +8,7 @@ use App\Container\ContainerAwareTrait;
use App\Entity\Station;
use App\Entity\StationRemote;
use App\Exception\NotFoundException;
+use App\Exception\StationUnsupportedException;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Enums\AdapterTypeInterface;
use App\Radio\Enums\BackendAdapters;
@@ -30,6 +31,20 @@ final class Adapters
: null;
}
+ /**
+ * @throws StationUnsupportedException
+ */
+ public function requireFrontendAdapter(Station $station): Frontend\AbstractFrontend
+ {
+ $frontend = $this->getFrontendAdapter($station);
+
+ if (null === $frontend) {
+ throw StationUnsupportedException::generic();
+ }
+
+ return $frontend;
+ }
+
/**
* @param bool $checkInstalled
* @return mixed[]
@@ -48,6 +63,20 @@ final class Adapters
: null;
}
+ /**
+ * @throws StationUnsupportedException
+ */
+ public function requireBackendAdapter(Station $station): Liquidsoap
+ {
+ $backend = $this->getBackendAdapter($station);
+
+ if (null === $backend) {
+ throw StationUnsupportedException::generic();
+ }
+
+ return $backend;
+ }
+
/**
* @param bool $checkInstalled
* @return mixed[]
@@ -64,7 +93,9 @@ final class Adapters
return $this->di->get($className);
}
- throw new NotFoundException('Adapter not found: ' . $className);
+ throw new NotFoundException(
+ sprintf('Adapter not found: %s', $className)
+ );
}
/**