+
{{ $gettext('Song-based') }}
@@ -60,31 +60,31 @@
{{ $gettext('Jingle Mode') }}
{{ $gettext('Sequential') }}
{{ $gettext('On-Demand') }}
{{ $gettext('Scheduled') }}
{{ $gettext('Disabled') }}
diff --git a/frontend/src/js/pages/Public/Podcasts.js b/frontend/src/js/pages/Public/Podcasts.js
new file mode 100644
index 000000000..1be8e3d1d
--- /dev/null
+++ b/frontend/src/js/pages/Public/Podcasts.js
@@ -0,0 +1,24 @@
+import initApp from "~/layout";
+import {h} from "vue";
+import {createRouter, createWebHistory} from "vue-router";
+import {useAzuraCast} from "~/vendor/azuracast";
+import {installRouter} from "~/vendor/router";
+import PodcastsLayout from "~/components/Public/Podcasts/PodcastsLayout.vue";
+import usePodcastRoutes from "~/components/Public/Podcasts/routes";
+
+initApp({
+ render() {
+ return h(PodcastsLayout);
+ }
+}, async (vueApp) => {
+ const routes = usePodcastRoutes();
+ const {componentProps} = useAzuraCast();
+
+ installRouter(
+ createRouter({
+ history: createWebHistory(componentProps.baseUrl),
+ routes
+ }),
+ vueApp
+ );
+});
diff --git a/src/Controller/Api/Stations/PodcastEpisodesController.php b/src/Controller/Api/Stations/PodcastEpisodesController.php
index 27b3ffe55..49700cca7 100644
--- a/src/Controller/Api/Stations/PodcastEpisodesController.php
+++ b/src/Controller/Api/Stations/PodcastEpisodesController.php
@@ -17,6 +17,7 @@ use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Service\Flow\UploadedFile;
+use App\Utilities\Strings;
use App\Utilities\Types;
use InvalidArgumentException;
use OpenApi\Attributes as OA;
@@ -222,6 +223,15 @@ final class PodcastEpisodesController extends AbstractApiCrudController
->orderBy('e.created_at', 'DESC')
->setParameter('podcast', $podcast);
+ $acl = $request->getAcl();
+ if (!$acl->isAllowed(StationPermissions::Podcasts, $station)) {
+ $queryBuilder = $queryBuilder
+ ->andWhere('e.publish_at IS NULL OR e.publish_at <= :publishTime')
+ ->setParameter('publishTime', time())
+ ->andWhere('pm.id IS NOT NULL')
+ ->orderBy('e.publish_at', 'DESC');
+ }
+
$queryBuilder = $this->searchQueryBuilder(
$request,
$queryBuilder,
@@ -309,10 +319,14 @@ final class PodcastEpisodesController extends AbstractApiCrudController
$router = $request->getRouter();
$return = new ApiPodcastEpisode();
- $return->id = $record->getId();
+ $return->id = $record->getIdRequired();
$return->title = $record->getTitle();
+
$return->description = $record->getDescription();
+ $return->description_short = Strings::truncateText($return->description, 100);
+
$return->explicit = $record->getExplicit();
+ $return->created_at = $record->getCreatedAt();
$return->publish_at = $record->getPublishAt();
$mediaRow = $record->getMedia();
diff --git a/src/Controller/Api/Stations/PodcastsController.php b/src/Controller/Api/Stations/PodcastsController.php
index 78e965337..522460486 100644
--- a/src/Controller/Api/Stations/PodcastsController.php
+++ b/src/Controller/Api/Stations/PodcastsController.php
@@ -7,6 +7,7 @@ namespace App\Controller\Api\Stations;
use App\Controller\Api\AbstractApiCrudController;
use App\Controller\Api\Traits\CanSearchResults;
use App\Entity\Api\Podcast as ApiPodcast;
+use App\Entity\Api\PodcastCategory as ApiPodcastCategory;
use App\Entity\Podcast;
use App\Entity\PodcastCategory;
use App\Entity\Repository\PodcastRepository;
@@ -15,10 +16,13 @@ use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use App\Service\Flow\UploadedFile;
+use App\Utilities\Strings;
use App\Utilities\Types;
use InvalidArgumentException;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
+use Symfony\Component\Intl\Exception\MissingResourceException;
+use Symfony\Component\Intl\Languages;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -239,18 +243,40 @@ final class PodcastsController extends AbstractApiCrudController
$station = $request->getStation();
$return = new ApiPodcast();
- $return->id = $record->getId();
- $return->storage_location_id = $record->getStorageLocation()->getId();
+ $return->id = $record->getIdRequired();
+ $return->storage_location_id = $record->getStorageLocation()->getIdRequired();
+
$return->title = $record->getTitle();
$return->link = $record->getLink();
+
$return->description = $record->getDescription();
+ $return->description_short = Strings::truncateText($return->description, 200);
+
$return->language = $record->getLanguage();
+ try {
+ $locale = $request->getCustomization()->getLocale();
+ $return->language_name = Languages::getName(
+ $return->language,
+ $locale->value
+ );
+ } catch (MissingResourceException) {
+ }
+
$return->author = $record->getAuthor();
$return->email = $record->getEmail();
$categories = [];
foreach ($record->getCategories() as $category) {
- $categories[] = $category->getCategory();
+ $categoryRow = new ApiPodcastCategory();
+ $categoryRow->category = $category->getCategory();
+ $categoryRow->title = $category->getTitle();
+ $categoryRow->subtitle = $category->getSubTitle();
+
+ $categoryRow->text = (!empty($categoryRow->subtitle))
+ ? $categoryRow->title . ' - ' . $categoryRow->subtitle
+ : $categoryRow->title;
+
+ $categories[] = $categoryRow;
}
$return->categories = $categories;
@@ -287,7 +313,7 @@ final class PodcastsController extends AbstractApiCrudController
absolute: !$isInternal
),
'public_episodes' => $router->fromHere(
- routeName: 'public:podcast:episodes',
+ routeName: 'public:podcast',
routeParams: ['podcast_id' => $record->getId()],
absolute: !$isInternal
),
diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php b/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php
deleted file mode 100644
index 45e6d48fc..000000000
--- a/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php
+++ /dev/null
@@ -1,85 +0,0 @@
-getRouter();
- $station = $request->getStation();
-
- if (!$station->getEnablePublicPage()) {
- throw NotFoundException::station();
- }
-
- $podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
-
- if ($podcast === null) {
- throw NotFoundException::podcast();
- }
-
- $episode = $this->episodeRepository->fetchEpisodeForStation($station, $episodeId);
-
- $podcastEpisodesLink = $router->named(
- 'public:podcast:episodes',
- [
- 'station_id' => $station->getId(),
- 'podcast_id' => $podcastId,
- ]
- );
-
- if (!($episode instanceof PodcastEpisode) || !$episode->isPublished()) {
- $request->getFlash()->error(__('Episode not found.'));
- return $response->withRedirect($podcastEpisodesLink);
- }
-
- $feedLink = $router->named(
- 'public:podcast:feed',
- [
- 'station_id' => $station->getId(),
- 'podcast_id' => $podcast->getId(),
- ]
- );
-
- return $request->getView()->renderToResponse(
- $response
- ->withHeader('X-Frame-Options', '*')
- ->withHeader('X-Robots-Tag', 'index, nofollow'),
- 'frontend/public/podcast-episode',
- [
- 'episode' => $episode,
- 'feedLink' => $feedLink,
- 'podcast' => $podcast,
- 'podcastEpisodesLink' => $podcastEpisodesLink,
- 'station' => $station,
- ]
- );
- }
-}
diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php b/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php
deleted file mode 100644
index 53b05b9d8..000000000
--- a/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php
+++ /dev/null
@@ -1,95 +0,0 @@
-getRouter();
- $station = $request->getStation();
-
- if (!$station->getEnablePublicPage()) {
- throw NotFoundException::station();
- }
-
- $podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
-
- if ($podcast === null) {
- throw NotFoundException::podcast();
- }
-
- $publishedEpisodes = $this->episodeRepository->fetchPublishedEpisodesForPodcast($podcast);
-
- // Reverse sort order according to the calculated publishing timestamp
- usort(
- $publishedEpisodes,
- static function ($prevEpisode, $nextEpisode) {
- /** @var PodcastEpisode $prevEpisode */
- /** @var PodcastEpisode $nextEpisode */
-
- $prevPublishedAt = $prevEpisode->getPublishAt() ?? $prevEpisode->getCreatedAt();
- $nextPublishedAt = $nextEpisode->getPublishAt() ?? $nextEpisode->getCreatedAt();
-
- return ($nextPublishedAt <=> $prevPublishedAt);
- }
- );
-
- $podcastsLink = $router->fromHere(
- 'public:podcasts',
- [
- 'station_id' => $station->getId(),
- ]
- );
-
- if (count($publishedEpisodes) === 0) {
- $request->getFlash()->error(__('No episodes found.'));
- return $response->withRedirect($podcastsLink);
- }
-
- $feedLink = $router->named(
- 'public:podcast:feed',
- [
- 'station_id' => $station->getId(),
- 'podcast_id' => $podcast->getId(),
- ]
- );
-
- return $request->getView()->renderToResponse(
- $response
- ->withHeader('X-Frame-Options', '*')
- ->withHeader('X-Robots-Tag', 'index, nofollow'),
- 'frontend/public/podcast-episodes',
- [
- 'episodes' => $publishedEpisodes,
- 'feedLink' => $feedLink,
- 'podcast' => $podcast,
- 'podcastsLink' => $podcastsLink,
- 'station' => $station,
- ]
- );
- }
-}
diff --git a/src/Controller/Frontend/PublicPages/PodcastsAction.php b/src/Controller/Frontend/PublicPages/PodcastsAction.php
index 5622d52e8..f01fcaf24 100644
--- a/src/Controller/Frontend/PublicPages/PodcastsAction.php
+++ b/src/Controller/Frontend/PublicPages/PodcastsAction.php
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Controller\Frontend\PublicPages;
+use App\Controller\Frontend\PublicPages\Traits\IsEmbeddable;
use App\Controller\SingleActionInterface;
-use App\Entity\Repository\PodcastRepository;
use App\Exception\NotFoundException;
use App\Http\Response;
use App\Http\ServerRequest;
@@ -13,10 +13,7 @@ use Psr\Http\Message\ResponseInterface;
final class PodcastsAction implements SingleActionInterface
{
- public function __construct(
- private readonly PodcastRepository $podcastRepository
- ) {
- }
+ use IsEmbeddable;
public function __invoke(
ServerRequest $request,
@@ -29,17 +26,37 @@ final class PodcastsAction implements SingleActionInterface
throw NotFoundException::station();
}
- $publishedPodcasts = $this->podcastRepository->fetchPublishedPodcastsForStation($station);
+ $pageClass = 'podcasts station-' . $station->getShortName();
+ if ($this->isEmbedded($request, $params)) {
+ $pageClass .= ' embed';
+ }
- return $request->getView()->renderToResponse(
- $response
+ $router = $request->getRouter();
+ $view = $request->getView();
+
+ // Add station public code.
+ $view->fetch(
+ 'frontend/public/partials/station-custom',
+ ['station' => $station]
+ );
+
+ return $view->renderVuePage(
+ response: $response
->withHeader('X-Frame-Options', '*')
->withHeader('X-Robots-Tag', 'index, nofollow'),
- 'frontend/public/podcasts',
- [
- 'podcasts' => $publishedPodcasts,
- 'station' => $station,
- ]
+ component: 'Public/Podcasts',
+ id: 'podcast',
+ layout: 'minimal',
+ title: 'Podcasts - ' . $station->getName(),
+ layoutParams: [
+ 'page_class' => $pageClass,
+ 'hide_footer' => true,
+ ],
+ props: [
+ 'baseUrl' => $router->named('public:index', [
+ 'station_id' => $station->getShortName(),
+ ]),
+ ],
);
}
}
diff --git a/src/Entity/Api/Podcast.php b/src/Entity/Api/Podcast.php
index 00a74ade4..76d7357fd 100644
--- a/src/Entity/Api/Podcast.php
+++ b/src/Entity/Api/Podcast.php
@@ -16,41 +16,50 @@ final class Podcast
use HasLinks;
#[OA\Property]
- public ?string $id = null;
+ public string $id;
#[OA\Property]
- public ?int $storage_location_id = null;
+ public int $storage_location_id;
#[OA\Property]
- public ?string $title = null;
+ public string $title;
#[OA\Property]
public ?string $link = null;
#[OA\Property]
- public ?string $description = null;
+ public string $description;
#[OA\Property]
- public ?string $language = null;
+ public string $description_short;
#[OA\Property]
- public ?string $author = null;
+ public string $language;
#[OA\Property]
- public ?string $email = null;
+ public string $language_name;
+
+ #[OA\Property]
+ public string $author;
+
+ #[OA\Property]
+ public string $email;
#[OA\Property]
public bool $has_custom_art = false;
#[OA\Property]
- public ?string $art = null;
+ public string $art;
#[OA\Property]
public int $art_updated_at = 0;
+ /**
+ * @var PodcastCategory[]
+ */
#[OA\Property(
type: 'array',
- items: new OA\Items(type: 'string')
+ items: new OA\Items(type: PodcastCategory::class)
)]
public array $categories = [];
diff --git a/src/Entity/Api/PodcastCategory.php b/src/Entity/Api/PodcastCategory.php
new file mode 100644
index 000000000..fd391f9c3
--- /dev/null
+++ b/src/Entity/Api/PodcastCategory.php
@@ -0,0 +1,26 @@
+getOneOrNullResult();
}
- /**
- * @return PodcastEpisode[]
- */
- public function fetchPublishedEpisodesForPodcast(Podcast $podcast): array
- {
- $episodes = $this->em->createQueryBuilder()
- ->select('pe')
- ->from(PodcastEpisode::class, 'pe')
- ->where('pe.podcast = :podcast')
- ->setParameter('podcast', $podcast)
- ->orderBy('pe.created_at', 'DESC')
- ->getQuery()
- ->getResult();
-
- return array_filter(
- $episodes,
- static function (PodcastEpisode $episode) {
- return $episode->isPublished();
- }
- );
- }
-
public function writeEpisodeArt(
PodcastEpisode $episode,
string $rawArtworkString
diff --git a/src/Entity/Repository/PodcastRepository.php b/src/Entity/Repository/PodcastRepository.php
index b66a044d6..8b0acb7f8 100644
--- a/src/Entity/Repository/PodcastRepository.php
+++ b/src/Entity/Repository/PodcastRepository.php
@@ -44,35 +44,6 @@ final class PodcastRepository extends Repository
);
}
- /**
- * @return Podcast[]
- */
- public function fetchPublishedPodcastsForStation(Station $station): array
- {
- $podcasts = $this->em->createQuery(
- <<<'DQL'
- SELECT p, pe
- FROM App\Entity\Podcast p
- LEFT JOIN p.episodes pe
- WHERE p.storage_location = :storageLocation
- DQL
- )->setParameter('storageLocation', $station->getPodcastsStorageLocation())
- ->getResult();
-
- return array_filter(
- $podcasts,
- static function (Podcast $podcast) {
- foreach ($podcast->getEpisodes() as $episode) {
- if ($episode->isPublished()) {
- return true;
- }
- }
-
- return false;
- }
- );
- }
-
public function writePodcastArt(
Podcast $podcast,
string $rawArtworkString,
diff --git a/templates/frontend/public/podcast-episode.phtml b/templates/frontend/public/podcast-episode.phtml
deleted file mode 100644
index 6203b7463..000000000
--- a/templates/frontend/public/podcast-episode.phtml
+++ /dev/null
@@ -1,95 +0,0 @@
-layout('minimal', [
- 'page_class' => 'podcasts station-' . $station->getShortName(),
- 'title' => 'Podcasts - ' . $station->getName(),
- 'hide_footer' => true,
-]);
-
-$this->fetch('frontend/public/partials/station-custom', ['station' => $station]);
-
-$episodeAudioSrc = $router->named(
- 'api:stations:podcast:episode:download',
- [
- 'station_id' => $station->getId(),
- 'podcast_id' => $episode->getPodcast()->getId(),
- 'episode_id' => $episode->getId(),
- ],
- [],
- true
-);
-
-$publishedAt = CarbonImmutable::createFromTimestamp($episode->getCreatedAt());
-
-if ($episode->getPublishAt() !== null) {
- $publishedAt = CarbonImmutable::createFromTimestamp($episode->getPublishAt());
-}
-
-$sections->append(
- 'head',
- <<
- HTML
-);
-?>
-
-
-
-
-
-
- = $this->e($podcast->getTitle()) ?>
-
-
- = $this->e($episode->getTitle()) ?>
-
-
-
-
- = $publishedAt->format('d. M. Y') ?>
-
-
- getExplicit()) : ?>
- = __('Explicit') ?>
-
-
-
-
= $this->e($episode->getDescription()) ?>
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/templates/frontend/public/podcast-episodes.phtml b/templates/frontend/public/podcast-episodes.phtml
deleted file mode 100644
index 0f66adf86..000000000
--- a/templates/frontend/public/podcast-episodes.phtml
+++ /dev/null
@@ -1,116 +0,0 @@
-layout(
- 'minimal',
- [
- 'page_class' => 'podcasts station-' . $station->getShortName(),
- 'title' => 'Podcasts - ' . $station->getName(),
- 'hide_footer' => true,
- ]
-);
-
-$this->fetch('frontend/public/partials/station-custom', ['station' => $station]);
-
-$sections->append(
- 'head',
- <<
- HTML
-);
-?>
-
-
-
-
-
- = $this->e($podcast->getTitle()) ?>
-
-
- = __('Episodes') ?>
-
-
-
-
-
- named(
- 'public:podcast:episode',
- [
- 'station_id' => $station->getId(),
- 'podcast_id' => $podcast->getId(),
- 'episode_id' => $episode->getId(),
- ]
- ) ?>
-
-
-
-
= $this->e($episode->getTitle()) ?>
-
= $this->e($episode->getDescription()) ?>
-
- getExplicit()) : ?>
-
- = __('Contains explicit content') ?>
-
-
-
-
- getCreatedAt()
- ); ?>
- getPublishAt() !== null) : ?>
- getPublishAt()
- ); ?>
-
- = $publishedAt->format('d. M. Y') ?>
-
-
-
-
-
-
-
-
-
diff --git a/templates/frontend/public/podcasts.phtml b/templates/frontend/public/podcasts.phtml
deleted file mode 100644
index 651dd5fa2..000000000
--- a/templates/frontend/public/podcasts.phtml
+++ /dev/null
@@ -1,92 +0,0 @@
-layout('minimal', [
- 'page_class' => 'podcasts station-' . $station->getShortName(),
- 'title' => 'Podcasts - ' . $station->getName(),
- 'hide_footer' => true,
-]);
-
-$this->fetch('frontend/public/partials/station-custom', ['station' => $station]);
-
-?>
-
-
-
-
- = $this->e($station->getName()) ?>
-
-
-
- named(
- 'public:podcast:episodes',
- [
- 'station_id' => $station->getId(),
- 'podcast_id' => $podcast->getId(),
- ]
- ) ?>
- named(
- 'public:podcast:feed',
- ['station_id' => $station->getId(), 'podcast_id' => $podcast->getId()]
- ) ?>
-
-
-
-
= $this->e($podcast->getTitle()) ?>
-
= $this->e($podcast->getDescription()) ?>
-
-
- = __('Language') ?>: = strtoupper(
- $podcast->getLanguage()
- ) ?>
-
- = __('Categories') ?>: = implode(
- $podcast->getCategories()->map(
- function ($category) {
- $title = $category->getTitle();
- $subtitle = $category->getSubTitle();
-
- return (!empty($subtitle))
- ? $title . ' - ' . $subtitle
- : $title;
- }
- )->getValues()
- ); ?>
-
-
-
-
-
-
-
-
-
= __('No entries found.') ?>
-
-
-
-
diff --git a/templates/icons/rss.phtml b/templates/icons/rss.phtml
deleted file mode 100644
index 58cc4b415..000000000
--- a/templates/icons/rss.phtml
+++ /dev/null
@@ -1,7 +0,0 @@
-