Podcast controller overhaul:
- Make public-facing podcast API endpoints separate from the CRUD API used by station operators - Make Podcast a request attribute (like stations) to simplify several controllers - Move API generation for podcasts and episodes into an ApiGenerator class.
This commit is contained in:
parent
85f89d13ba
commit
f379464937
|
@ -68,31 +68,67 @@ return static function (RouteCollectorProxy $group) {
|
|||
$group->get(
|
||||
'/art/{media_id:[a-zA-Z0-9\-]+}[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Art\GetArtAction::class
|
||||
)->setName('api:stations:media:art');
|
||||
)->setName('api:stations:media:art')
|
||||
->add(new Middleware\Cache\SetStaticFileCache());
|
||||
|
||||
// Streamer Art
|
||||
$group->get(
|
||||
'/streamer/{id}/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Streamers\Art\GetArtAction::class
|
||||
)->setName('api:stations:streamer:art');
|
||||
)->setName('api:stations:streamer:art')
|
||||
->add(new Middleware\Cache\SetStaticFileCache());
|
||||
|
||||
// Podcast and Episode Art
|
||||
$group->group(
|
||||
'/podcast/{podcast_id}',
|
||||
'/public',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get(
|
||||
'/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Podcasts\Art\GetArtAction::class
|
||||
)->setName('api:stations:podcast:art');
|
||||
// Podcast Public Pages
|
||||
$group->get('/podcasts', Controller\Api\Stations\Podcasts\ListPodcastsAction::class)
|
||||
->setName('api:stations:public:podcasts');
|
||||
|
||||
$group->get(
|
||||
'/episode/{episode_id}/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Art\GetArtAction::class
|
||||
)->setName('api:stations:podcast:episode:art');
|
||||
$group->group(
|
||||
'/podcast/{podcast_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Api\Stations\Podcasts\GetPodcastAction::class)
|
||||
->setName('api:stations:public:podcast');
|
||||
|
||||
$group->get(
|
||||
'/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Podcasts\Art\GetArtAction::class
|
||||
)->setName('api:stations:public:podcast:art')
|
||||
->add(new Middleware\Cache\SetStaticFileCache());
|
||||
|
||||
$group->get(
|
||||
'/episodes',
|
||||
Controller\Api\Stations\Podcasts\Episodes\ListEpisodesAction::class
|
||||
)->setName('api:stations:public:podcast:episodes');
|
||||
|
||||
$group->group(
|
||||
'/episode/{episode_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get(
|
||||
'',
|
||||
Controller\Api\Stations\Podcasts\Episodes\GetEpisodeAction::class
|
||||
)->setName('api:stations:public:podcast:episode')
|
||||
->add(new Middleware\Cache\SetStaticFileCache());
|
||||
|
||||
$group->get(
|
||||
'/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Art\GetArtAction::class
|
||||
)->setName('api:stations:public:podcast:episode:art')
|
||||
->add(new Middleware\Cache\SetStaticFileCache());
|
||||
|
||||
$group->get(
|
||||
'/download',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Media\GetMediaAction::class
|
||||
)->setName('api:stations:public:podcast:episode:download');
|
||||
}
|
||||
);
|
||||
}
|
||||
)->add(Middleware\RequirePublishedPodcastEpisodeMiddleware::class)
|
||||
->add(Middleware\GetAndRequirePodcast::class);
|
||||
}
|
||||
)->add(Middleware\RequirePublishedPodcastEpisodeMiddleware::class);
|
||||
);
|
||||
}
|
||||
)->add(new Middleware\Cache\SetStaticFileCache())
|
||||
->add(Middleware\RequireStation::class)
|
||||
)->add(Middleware\RequireStation::class)
|
||||
->add(Middleware\GetStation::class);
|
||||
};
|
||||
|
|
|
@ -48,41 +48,7 @@ return static function (RouteCollectorProxy $group) {
|
|||
->add(new Middleware\StationSupportsFeature(StationFeatures::OnDemand))
|
||||
->add(new Middleware\RateLimit('ondemand', 1, 2));
|
||||
|
||||
// Podcast Public Pages
|
||||
$group->get('/podcasts', Controller\Api\Stations\PodcastsController::class . ':listAction')
|
||||
->setName('api:stations:podcasts');
|
||||
|
||||
$group->group(
|
||||
'/podcast/{podcast_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Api\Stations\PodcastsController::class . ':getAction')
|
||||
->setName('api:stations:podcast');
|
||||
|
||||
// See ./api_public for podcast art.
|
||||
|
||||
$group->get(
|
||||
'/episodes',
|
||||
Controller\Api\Stations\PodcastEpisodesController::class . ':listAction'
|
||||
)->setName('api:stations:podcast:episodes');
|
||||
|
||||
$group->group(
|
||||
'/episode/{episode_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get(
|
||||
'',
|
||||
Controller\Api\Stations\PodcastEpisodesController::class . ':getAction'
|
||||
)->setName('api:stations:podcast:episode');
|
||||
|
||||
$group->get(
|
||||
'/download',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Media\GetMediaAction::class
|
||||
)->setName('api:stations:podcast:episode:download');
|
||||
}
|
||||
);
|
||||
}
|
||||
)->add(Middleware\RequirePublishedPodcastEpisodeMiddleware::class);
|
||||
|
||||
// NOTE: See ./api_public.php for media art public path.
|
||||
// NOTE: See ./api_public.php for podcast public pages.
|
||||
|
||||
/*
|
||||
* Authenticated Functions
|
||||
|
@ -148,6 +114,9 @@ return static function (RouteCollectorProxy $group) {
|
|||
$group->group(
|
||||
'',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('/podcasts', Controller\Api\Stations\PodcastsController::class . ':listAction')
|
||||
->setName('api:stations:podcasts');
|
||||
|
||||
$group->post(
|
||||
'/podcasts',
|
||||
Controller\Api\Stations\PodcastsController::class . ':createAction'
|
||||
|
@ -159,6 +128,9 @@ return static function (RouteCollectorProxy $group) {
|
|||
$group->group(
|
||||
'/podcast/{podcast_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Api\Stations\PodcastsController::class . ':getAction')
|
||||
->setName('api:stations:podcast');
|
||||
|
||||
$group->put('', Controller\Api\Stations\PodcastsController::class . ':editAction');
|
||||
|
||||
$group->delete(
|
||||
|
@ -166,16 +138,26 @@ return static function (RouteCollectorProxy $group) {
|
|||
Controller\Api\Stations\PodcastsController::class . ':deleteAction'
|
||||
);
|
||||
|
||||
$group->get(
|
||||
'/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Podcasts\Art\GetArtAction::class
|
||||
)->setName('api:stations:podcast:art');
|
||||
|
||||
$group->post(
|
||||
'/art',
|
||||
Controller\Api\Stations\Podcasts\Art\PostArtAction::class
|
||||
)->setName('api:stations:podcast:art-internal');
|
||||
);
|
||||
|
||||
$group->delete(
|
||||
'/art',
|
||||
Controller\Api\Stations\Podcasts\Art\DeleteArtAction::class
|
||||
);
|
||||
|
||||
$group->get(
|
||||
'/episodes',
|
||||
Controller\Api\Stations\PodcastEpisodesController::class . ':listAction'
|
||||
)->setName('api:stations:podcast:episodes');
|
||||
|
||||
$group->post(
|
||||
'/episodes',
|
||||
Controller\Api\Stations\PodcastEpisodesController::class . ':createAction'
|
||||
|
@ -194,6 +176,11 @@ return static function (RouteCollectorProxy $group) {
|
|||
$group->group(
|
||||
'/episode/{episode_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get(
|
||||
'',
|
||||
Controller\Api\Stations\PodcastEpisodesController::class . ':getAction'
|
||||
)->setName('api:stations:podcast:episode');
|
||||
|
||||
$group->put(
|
||||
'',
|
||||
Controller\Api\Stations\PodcastEpisodesController::class . ':editAction'
|
||||
|
@ -205,20 +192,30 @@ return static function (RouteCollectorProxy $group) {
|
|||
. ':deleteAction'
|
||||
);
|
||||
|
||||
$group->get(
|
||||
'/art[-{timestamp}.jpg]',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Art\GetArtAction::class
|
||||
)->setName('api:stations:podcast:episode:art');
|
||||
|
||||
$group->post(
|
||||
'/art',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Art\PostArtAction::class
|
||||
)->setName('api:stations:podcast:episode:art-internal');
|
||||
);
|
||||
|
||||
$group->delete(
|
||||
'/art',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Art\DeleteArtAction::class
|
||||
);
|
||||
|
||||
$group->get(
|
||||
'/media',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Media\GetMediaAction::class
|
||||
)->setName('api:stations:podcast:episode:media');
|
||||
|
||||
$group->post(
|
||||
'/media',
|
||||
Controller\Api\Stations\Podcasts\Episodes\Media\PostMediaAction::class
|
||||
)->setName('api:stations:podcast:episode:media-internal');
|
||||
);
|
||||
|
||||
$group->delete(
|
||||
'/media',
|
||||
|
@ -227,7 +224,7 @@ return static function (RouteCollectorProxy $group) {
|
|||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
)->add(Middleware\GetAndRequirePodcast::class);
|
||||
}
|
||||
)->add(new Middleware\Permissions(StationPermissions::Podcasts, true));
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ import useStationDateTimeFormatter from "~/functions/useStationDateTimeFormatter
|
|||
|
||||
const {params} = useRoute();
|
||||
|
||||
const podcastUrl = getStationApiUrl(`/podcast/${params.podcast_id}`);
|
||||
const podcastUrl = getStationApiUrl(`/public/podcast/${params.podcast_id}`);
|
||||
|
||||
const {axios} = useAxios();
|
||||
const {state: podcast, isLoading} = useRefreshableAsyncState(
|
||||
|
@ -153,7 +153,7 @@ const {state: podcast, isLoading} = useRefreshableAsyncState(
|
|||
{},
|
||||
);
|
||||
|
||||
const episodesUrl = getStationApiUrl(`/podcast/${params.podcast_id}/episodes`);
|
||||
const episodesUrl = getStationApiUrl(`/public/podcast/${params.podcast_id}/episodes`);
|
||||
|
||||
const {$gettext} = useTranslate();
|
||||
const fields: DataTableField[] = [
|
||||
|
|
|
@ -127,8 +127,8 @@ import useStationDateTimeFormatter from "~/functions/useStationDateTimeFormatter
|
|||
|
||||
const {params} = useRoute();
|
||||
|
||||
const podcastUrl = getStationApiUrl(`/podcast/${params.podcast_id}`);
|
||||
const episodeUrl = getStationApiUrl(`/podcast/${params.podcast_id}/episode/${params.episode_id}`);
|
||||
const podcastUrl = getStationApiUrl(`/public/podcast/${params.podcast_id}`);
|
||||
const episodeUrl = getStationApiUrl(`/public/podcast/${params.podcast_id}/episode/${params.episode_id}`);
|
||||
|
||||
const {axios} = useAxios();
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ import {useTranslate} from "~/vendor/gettext.ts";
|
|||
import {IconRss} from "~/components/Common/icons.ts";
|
||||
import Icon from "~/components/Common/Icon.vue";
|
||||
|
||||
const apiUrl = getStationApiUrl('/podcasts');
|
||||
const apiUrl = getStationApiUrl('/public/podcasts');
|
||||
|
||||
const {$gettext} = useTranslate();
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, toRef, watch} from "vue";
|
||||
import {syncRef} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
import FormGroup from "~/components/Form/FormGroup.vue";
|
||||
import FormFile from "~/components/Form/FormFile.vue";
|
||||
|
@ -49,9 +48,7 @@ const props = defineProps({
|
|||
}
|
||||
});
|
||||
|
||||
const albumArtSrc = ref(null);
|
||||
syncRef(toRef(props, 'albumArtUrl'), albumArtSrc, {direction: 'ltr'});
|
||||
|
||||
const albumArtSrc = ref(props.albumArtUrl);
|
||||
const reloadArt = () => {
|
||||
albumArtSrc.value = props.albumArtUrl + '?' + Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, toRef} from "vue";
|
||||
import {computed, ref, toRef, watch} from "vue";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
import FormGroup from "~/components/Form/FormGroup.vue";
|
||||
import FormFile from "~/components/Form/FormFile.vue";
|
||||
|
@ -54,15 +54,11 @@ import Tab from "~/components/Common/Tab.vue";
|
|||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
artworkSrc: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
editArtUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
newArtUrl: {
|
||||
type: String,
|
||||
|
@ -72,7 +68,12 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const artworkSrc = toRef(props, 'artworkSrc');
|
||||
const artworkSrc = ref(props.artworkSrc);
|
||||
const reloadArt = () => {
|
||||
artworkSrc.value = props.artworkSrc + '?' + Math.floor(Date.now() / 1000);
|
||||
}
|
||||
watch(toRef(props, 'artworkSrc'), reloadArt);
|
||||
|
||||
const localSrc = ref(null);
|
||||
|
||||
const src = computed(() => {
|
||||
|
@ -92,21 +93,24 @@ const uploaded = (file) => {
|
|||
}, false);
|
||||
fileReader.readAsDataURL(file);
|
||||
|
||||
const url = (props.editArtUrl) ? props.editArtUrl : props.newArtUrl;
|
||||
const url = (props.artworkSrc) ? props.artworkSrc : props.newArtUrl;
|
||||
const formData = new FormData();
|
||||
formData.append('art', file);
|
||||
|
||||
axios.post(url, formData).then((resp) => {
|
||||
emit('update:modelValue', resp.data);
|
||||
reloadArt();
|
||||
});
|
||||
};
|
||||
|
||||
const deleteArt = () => {
|
||||
if (props.editArtUrl) {
|
||||
axios.delete(props.editArtUrl).then(() => {
|
||||
if (props.artworkSrc) {
|
||||
axios.delete(props.artworkSrc).then(() => {
|
||||
reloadArt();
|
||||
localSrc.value = null;
|
||||
});
|
||||
} else {
|
||||
reloadArt();
|
||||
localSrc.value = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,14 +18,12 @@
|
|||
:record-has-media="record.has_media"
|
||||
:new-media-url="newMediaUrl"
|
||||
:edit-media-url="record.links.media"
|
||||
:download-url="record.links.download"
|
||||
/>
|
||||
|
||||
<podcast-common-artwork
|
||||
v-model="form.artwork_file"
|
||||
:artwork-src="record.art"
|
||||
:artwork-src="record.links.art"
|
||||
:new-art-url="newArtUrl"
|
||||
:edit-art-url="record.links.art"
|
||||
/>
|
||||
</tabs>
|
||||
</modal-form>
|
||||
|
|
|
@ -32,9 +32,9 @@
|
|||
<template v-if="hasMedia">
|
||||
<div class="block-buttons pt-3">
|
||||
<a
|
||||
v-if="downloadUrl"
|
||||
v-if="editMediaUrl"
|
||||
class="btn btn-block btn-dark"
|
||||
:href="downloadUrl"
|
||||
:href="editMediaUrl"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $gettext('Download') }}
|
||||
|
@ -74,10 +74,6 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
downloadUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
editMediaUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
|
|
|
@ -33,42 +33,42 @@
|
|||
:fields="fields"
|
||||
:api-url="listUrl"
|
||||
>
|
||||
<template #cell(art)="row">
|
||||
<album-art :src="row.item.art" />
|
||||
<template #cell(art)="{item}">
|
||||
<album-art :src="item.art" />
|
||||
</template>
|
||||
<template #cell(title)="row">
|
||||
<template #cell(title)="{item}">
|
||||
<h5 class="m-0">
|
||||
{{ row.item.title }}
|
||||
{{ item.title }}
|
||||
</h5>
|
||||
<a
|
||||
:href="row.item.links.public_episodes"
|
||||
:href="item.links.public_episodes"
|
||||
target="_blank"
|
||||
>{{ $gettext('Public Page') }}</a> •
|
||||
<a
|
||||
:href="row.item.links.public_feed"
|
||||
:href="item.links.public_feed"
|
||||
target="_blank"
|
||||
>{{ $gettext('RSS Feed') }}</a>
|
||||
</template>
|
||||
<template #cell(actions)="row">
|
||||
<template #cell(actions)="{item}">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="doEdit(row.item.links.self)"
|
||||
@click="doEdit(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Edit') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
@click="doDelete(row.item.links.self)"
|
||||
@click="doDelete(item.links.self)"
|
||||
>
|
||||
{{ $gettext('Delete') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="doSelectPodcast(row.item)"
|
||||
@click="doSelectPodcast(item)"
|
||||
>
|
||||
{{ $gettext('Episodes') }}
|
||||
</button>
|
||||
|
@ -88,7 +88,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DataTable, { DataTableField } from '~/components/Common/DataTable.vue';
|
||||
import DataTable, {DataTableField} from '~/components/Common/DataTable.vue';
|
||||
import EditModal from './PodcastEditModal.vue';
|
||||
import AlbumArt from '~/components/Common/AlbumArt.vue';
|
||||
import StationsCommonQuota from "~/components/Stations/Common/Quota.vue";
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
|
||||
<podcast-common-artwork
|
||||
v-model="form.artwork_file"
|
||||
:artwork-src="record.art"
|
||||
:artwork-src="record.links.art"
|
||||
:new-art-url="newArtUrl"
|
||||
:edit-art-url="record.links.art"
|
||||
/>
|
||||
</tabs>
|
||||
</modal-form>
|
||||
|
@ -35,6 +34,7 @@ import {useResettableRef} from "~/functions/useResettableRef";
|
|||
import {useTranslate} from "~/vendor/gettext";
|
||||
import ModalForm from "~/components/Common/ModalForm.vue";
|
||||
import Tabs from "~/components/Common/Tabs.vue";
|
||||
import {map} from "lodash";
|
||||
|
||||
const props = defineProps({
|
||||
...baseEditModalProps,
|
||||
|
@ -59,7 +59,9 @@ const $modal = ref<ModalFormTemplateRef>(null);
|
|||
const {record, reset} = useResettableRef({
|
||||
has_custom_art: false,
|
||||
art: null,
|
||||
links: {}
|
||||
links: {
|
||||
art: null
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -87,6 +89,11 @@ const {
|
|||
reset();
|
||||
},
|
||||
populateForm: (data, formRef) => {
|
||||
data.categories = map(
|
||||
data.categories,
|
||||
(row) => row.category
|
||||
);
|
||||
|
||||
record.value = data;
|
||||
formRef.value = mergeExisting(formRef.value, data);
|
||||
},
|
||||
|
|
|
@ -7,22 +7,16 @@ namespace App\Controller\Api\Stations;
|
|||
use App\Controller\Api\AbstractApiCrudController;
|
||||
use App\Controller\Api\Traits\CanSearchResults;
|
||||
use App\Entity\Api\PodcastEpisode as ApiPodcastEpisode;
|
||||
use App\Entity\Api\PodcastMedia as ApiPodcastMedia;
|
||||
use App\Entity\ApiGenerator\PodcastEpisodeApiGenerator;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Entity\PodcastMedia;
|
||||
use App\Entity\Repository\PodcastEpisodeRepository;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Enums\StationPermissions;
|
||||
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 RuntimeException;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
|
@ -194,8 +188,8 @@ final class PodcastEpisodesController extends AbstractApiCrudController
|
|||
protected string $resourceRouteName = 'api:stations:podcast:episode';
|
||||
|
||||
public function __construct(
|
||||
private readonly PodcastRepository $podcastRepository,
|
||||
private readonly PodcastEpisodeRepository $episodeRepository,
|
||||
private readonly PodcastEpisodeApiGenerator $episodeApiGen,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
) {
|
||||
|
@ -207,12 +201,7 @@ final class PodcastEpisodesController extends AbstractApiCrudController
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $podcastId */
|
||||
$podcastId = $params['podcast_id'];
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
$podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
$queryBuilder = $this->em->createQueryBuilder()
|
||||
->select('e, p, pm')
|
||||
|
@ -223,15 +212,6 @@ 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,
|
||||
|
@ -251,8 +231,8 @@ final class PodcastEpisodesController extends AbstractApiCrudController
|
|||
/** @var string $id */
|
||||
$id = $params['episode_id'];
|
||||
|
||||
return $this->episodeRepository->fetchEpisodeForStation(
|
||||
$request->getStation(),
|
||||
return $this->episodeRepository->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$id
|
||||
);
|
||||
}
|
||||
|
@ -260,17 +240,7 @@ final class PodcastEpisodesController extends AbstractApiCrudController
|
|||
protected function createRecord(ServerRequest $request, array $data): object
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$podcastId = $request->getAttribute('podcast_id');
|
||||
|
||||
$podcast = $this->podcastRepository->fetchPodcastForStation(
|
||||
$station,
|
||||
$podcastId
|
||||
);
|
||||
|
||||
if (null === $podcast) {
|
||||
throw new RuntimeException('Podcast not found.');
|
||||
}
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
$record = $this->editRecord(
|
||||
$data,
|
||||
|
@ -315,87 +285,47 @@ final class PodcastEpisodesController extends AbstractApiCrudController
|
|||
throw new InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass));
|
||||
}
|
||||
|
||||
$isInternal = Types::bool($request->getParam('internal'), false, true);
|
||||
$isInternal = $request->isInternal();
|
||||
$router = $request->getRouter();
|
||||
|
||||
$return = new ApiPodcastEpisode();
|
||||
$return->id = $record->getIdRequired();
|
||||
$return->title = $record->getTitle();
|
||||
$return = $this->episodeApiGen->__invoke($record, $request);
|
||||
|
||||
$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();
|
||||
$return->has_media = ($mediaRow instanceof PodcastMedia);
|
||||
if ($mediaRow instanceof PodcastMedia) {
|
||||
$media = new ApiPodcastMedia();
|
||||
$media->id = $mediaRow->getId();
|
||||
$media->original_name = $mediaRow->getOriginalName();
|
||||
$media->length = $mediaRow->getLength();
|
||||
$media->length_text = $mediaRow->getLengthText();
|
||||
$media->path = $mediaRow->getPath();
|
||||
|
||||
$return->has_media = true;
|
||||
$return->media = $media;
|
||||
} else {
|
||||
$return->has_media = false;
|
||||
$return->media = new ApiPodcastMedia();
|
||||
}
|
||||
|
||||
$return->art_updated_at = $record->getArtUpdatedAt();
|
||||
$return->has_custom_art = (0 !== $return->art_updated_at);
|
||||
|
||||
$routeParams = [
|
||||
'episode_id' => $record->getId(),
|
||||
$baseRouteParams = [
|
||||
'station_id' => $request->getStation()->getIdRequired(),
|
||||
'podcast_id' => $record->getPodcast()->getIdRequired(),
|
||||
'episode_id' => $record->getIdRequired(),
|
||||
];
|
||||
if ($return->has_custom_art) {
|
||||
$routeParams['timestamp'] = $return->art_updated_at;
|
||||
|
||||
$artRouteParams = $baseRouteParams;
|
||||
if (0 !== $return->art_updated_at) {
|
||||
$artRouteParams['timestamp'] = $return->art_updated_at;
|
||||
}
|
||||
|
||||
$return->art = $router->fromHere(
|
||||
$return->art = $router->named(
|
||||
routeName: 'api:stations:podcast:episode:art',
|
||||
routeParams: $routeParams,
|
||||
absolute: true
|
||||
routeParams: $artRouteParams,
|
||||
absolute: !$isInternal
|
||||
);
|
||||
|
||||
$return->links = [
|
||||
...$return->links,
|
||||
'self' => $router->fromHere(
|
||||
routeName: $this->resourceRouteName,
|
||||
routeParams: ['episode_id' => $record->getId()],
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'public' => $router->fromHere(
|
||||
routeName: 'public:podcast:episode',
|
||||
routeParams: ['episode_id' => $record->getId()],
|
||||
'art' => $router->named(
|
||||
routeName: 'api:stations:podcast:episode:art',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'download' => $router->fromHere(
|
||||
routeName: 'api:stations:podcast:episode:download',
|
||||
routeParams: ['episode_id' => $record->getId()],
|
||||
'media' => $router->fromHere(
|
||||
routeName: 'api:stations:podcast:episode:media',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
];
|
||||
|
||||
$acl = $request->getAcl();
|
||||
$station = $request->getStation();
|
||||
|
||||
if ($acl->isAllowed(StationPermissions::Podcasts, $station)) {
|
||||
$return->links['art'] = $router->fromHere(
|
||||
routeName: 'api:stations:podcast:episode:art-internal',
|
||||
routeParams: ['episode_id' => $record->getId()],
|
||||
absolute: !$isInternal
|
||||
);
|
||||
$return->links['media'] = $router->fromHere(
|
||||
routeName: 'api:stations:podcast:episode:media-internal',
|
||||
routeParams: ['episode_id' => $record->getId()],
|
||||
absolute: !$isInternal
|
||||
);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ namespace App\Controller\Api\Stations\Podcasts\Art;
|
|||
|
||||
use App\Container\EntityManagerAwareTrait;
|
||||
use App\Controller\SingleActionInterface;
|
||||
use App\Entity\Api\Error;
|
||||
use App\Entity\Api\Status;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Http\Response;
|
||||
|
@ -52,22 +51,7 @@ final class DeleteArtAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $podcastId */
|
||||
$podcastId = $params['podcast_id'];
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
$podcast = $this->podcastRepo->fetchPodcastForStation($station, $podcastId);
|
||||
|
||||
if ($podcast === null) {
|
||||
return $response->withStatus(404)
|
||||
->withJson(
|
||||
new Error(
|
||||
404,
|
||||
__('Podcast not found!')
|
||||
)
|
||||
);
|
||||
}
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
$this->podcastRepo->removePodcastArt($podcast);
|
||||
$this->em->persist($podcast);
|
||||
|
|
|
@ -53,12 +53,10 @@ final class GetArtAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $podcastId */
|
||||
$podcastId = $params['podcast_id'];
|
||||
|
||||
$podcast = $request->getPodcast();
|
||||
$station = $request->getStation();
|
||||
|
||||
$podcastPath = Podcast::getArtPath($podcastId);
|
||||
$podcastPath = Podcast::getArtPath($podcast->getIdRequired());
|
||||
|
||||
$fsPodcasts = $this->stationFilesystems->getPodcastsFilesystem($station);
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ namespace App\Controller\Api\Stations\Podcasts\Art;
|
|||
|
||||
use App\Container\EntityManagerAwareTrait;
|
||||
use App\Controller\SingleActionInterface;
|
||||
use App\Entity\Api\Error;
|
||||
use App\Entity\Api\Status;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Exception\InvalidRequestAttribute;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
|
@ -53,11 +53,14 @@ final class PostArtAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string|null $podcastId */
|
||||
$podcastId = $params['podcast_id'] ?? null;
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
try {
|
||||
$podcast = $request->getPodcast();
|
||||
} catch (InvalidRequestAttribute) {
|
||||
$podcast = null;
|
||||
}
|
||||
|
||||
$mediaStorage = $station->getPodcastsStorageLocation();
|
||||
$mediaStorage->errorIfFull();
|
||||
|
||||
|
@ -66,14 +69,7 @@ final class PostArtAction implements SingleActionInterface
|
|||
return $flowResponse;
|
||||
}
|
||||
|
||||
if (null !== $podcastId) {
|
||||
$podcast = $this->podcastRepo->fetchPodcastForStation($station, $podcastId);
|
||||
|
||||
if (null === $podcast) {
|
||||
return $response->withStatus(404)
|
||||
->withJson(Error::notFound());
|
||||
}
|
||||
|
||||
if (null !== $podcast) {
|
||||
$this->podcastRepo->writePodcastArt(
|
||||
$podcast,
|
||||
$flowResponse->readAndDeleteUploadedFile()
|
||||
|
|
|
@ -12,6 +12,7 @@ use App\Entity\Repository\PodcastEpisodeRepository;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Utilities\Types;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -70,12 +71,13 @@ final class DeleteArtAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $episodeId */
|
||||
$episodeId = $params['episode_id'];
|
||||
$episodeId = Types::string($params['episode_id'] ?? null);
|
||||
|
||||
$station = $request->getStation();
|
||||
$episode = $this->episodeRepo->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$episodeId
|
||||
);
|
||||
|
||||
$episode = $this->episodeRepo->fetchEpisodeForStation($station, $episodeId);
|
||||
if ($episode === null) {
|
||||
return $response->withStatus(404)
|
||||
->withJson(Error::notFound());
|
||||
|
|
|
@ -12,6 +12,7 @@ use App\Flysystem\StationFilesystems;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Utilities\Types;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -61,12 +62,9 @@ final class GetArtAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $podcastId */
|
||||
$podcastId = $params['podcast_id'];
|
||||
|
||||
/** @var string $episodeId */
|
||||
$episodeId = $params['episode_id'];
|
||||
$episodeId = Types::string($params['episode_id'] ?? null);
|
||||
|
||||
$podcast = $request->getPodcast();
|
||||
$station = $request->getStation();
|
||||
|
||||
$episodeArtPath = PodcastEpisode::getArtPath($episodeId);
|
||||
|
@ -76,8 +74,7 @@ final class GetArtAction implements SingleActionInterface
|
|||
return $response->streamFilesystemFile($fsPodcasts, $episodeArtPath, null, 'inline', false);
|
||||
}
|
||||
|
||||
$podcastArtPath = Podcast::getArtPath($podcastId);
|
||||
|
||||
$podcastArtPath = Podcast::getArtPath($podcast->getIdRequired());
|
||||
if ($fsPodcasts->fileExists($podcastArtPath)) {
|
||||
return $response->streamFilesystemFile($fsPodcasts, $podcastArtPath, null, 'inline', false);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use App\Http\Response;
|
|||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Service\Flow;
|
||||
use App\Utilities\Types;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -60,8 +61,7 @@ final class PostArtAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string|null $episodeId */
|
||||
$episodeId = $params['episode_id'] ?? null;
|
||||
$episodeId = Types::stringOrNull($params['episode_id'] ?? null, true);
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
|
@ -71,7 +71,10 @@ final class PostArtAction implements SingleActionInterface
|
|||
}
|
||||
|
||||
if (null !== $episodeId) {
|
||||
$episode = $this->episodeRepo->fetchEpisodeForStation($station, $episodeId);
|
||||
$episode = $this->episodeRepo->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$episodeId
|
||||
);
|
||||
|
||||
if (null === $episode) {
|
||||
return $response->withStatus(404)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Podcasts\Episodes;
|
||||
|
||||
use App\Controller\SingleActionInterface;
|
||||
use App\Entity\ApiGenerator\PodcastEpisodeApiGenerator;
|
||||
use App\Entity\Repository\PodcastEpisodeRepository;
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\Types;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class GetEpisodeAction implements SingleActionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PodcastEpisodeRepository $episodeRepo,
|
||||
private readonly PodcastEpisodeApiGenerator $episodeApiGen
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequest $request, Response $response, array $params): ResponseInterface
|
||||
{
|
||||
$episodeId = Types::string($params['episode_id'] ?? null);
|
||||
|
||||
$episode = $this->episodeRepo->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$episodeId
|
||||
);
|
||||
|
||||
if (null === $episode) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
return $response->withJson(
|
||||
$this->episodeApiGen->__invoke($episode, $request)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Podcasts\Episodes;
|
||||
|
||||
use App\Container\EntityManagerAwareTrait;
|
||||
use App\Controller\Api\Traits\CanSearchResults;
|
||||
use App\Controller\SingleActionInterface;
|
||||
use App\Entity\ApiGenerator\PodcastEpisodeApiGenerator;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Paginator;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class ListEpisodesAction implements SingleActionInterface
|
||||
{
|
||||
use EntityManagerAwareTrait;
|
||||
use CanSearchResults;
|
||||
|
||||
public function __construct(
|
||||
private readonly PodcastEpisodeApiGenerator $episodeApiGen
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequest $request, Response $response, array $params): ResponseInterface
|
||||
{
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
$queryBuilder = $this->em->createQueryBuilder()
|
||||
->select('e, p, pm')
|
||||
->from(PodcastEpisode::class, 'e')
|
||||
->join('e.podcast', 'p')
|
||||
->leftJoin('e.media', 'pm')
|
||||
->where('e.podcast = :podcast')
|
||||
->setParameter('podcast', $podcast)
|
||||
->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,
|
||||
[
|
||||
'e.title',
|
||||
]
|
||||
);
|
||||
|
||||
$paginator = Paginator::fromQueryBuilder($queryBuilder, $request);
|
||||
$paginator->setPostprocessor(fn($row) => $this->episodeApiGen->__invoke($row, $request));
|
||||
|
||||
return $paginator->write($response);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ use App\Entity\Repository\PodcastEpisodeRepository;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Utilities\Types;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -58,11 +59,12 @@ final class DeleteMediaAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string $episodeId */
|
||||
$episodeId = $params['episode_id'];
|
||||
$episodeId = Types::string($params['episode_id'] ?? null);
|
||||
|
||||
$station = $request->getStation();
|
||||
$episode = $this->episodeRepo->fetchEpisodeForStation($station, $episodeId);
|
||||
$episode = $this->episodeRepo->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$episodeId
|
||||
);
|
||||
|
||||
if (!($episode instanceof PodcastEpisode)) {
|
||||
return $response->withStatus(404)
|
||||
|
|
|
@ -13,6 +13,7 @@ use App\Flysystem\StationFilesystems;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Utilities\Types;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -64,11 +65,13 @@ final class GetMediaAction implements SingleActionInterface
|
|||
): ResponseInterface {
|
||||
set_time_limit(600);
|
||||
|
||||
/** @var string $episodeId */
|
||||
$episodeId = $params['episode_id'];
|
||||
$episodeId = Types::string($params['episode_id'] ?? null);
|
||||
|
||||
$station = $request->getStation();
|
||||
$episode = $this->episodeRepo->fetchEpisodeForStation($station, $episodeId);
|
||||
$episode = $this->episodeRepo->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$episodeId
|
||||
);
|
||||
|
||||
if ($episode instanceof PodcastEpisode) {
|
||||
$podcastMedia = $episode->getMedia();
|
||||
|
|
|
@ -13,6 +13,7 @@ use App\Http\Response;
|
|||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use App\Service\Flow;
|
||||
use App\Utilities\Types;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -59,8 +60,7 @@ final class PostMediaAction implements SingleActionInterface
|
|||
Response $response,
|
||||
array $params
|
||||
): ResponseInterface {
|
||||
/** @var string|null $episodeId */
|
||||
$episodeId = $params['episode_id'] ?? null;
|
||||
$episodeId = Types::stringOrNull($params['episode_id'] ?? null, true);
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
|
@ -70,7 +70,10 @@ final class PostMediaAction implements SingleActionInterface
|
|||
}
|
||||
|
||||
if (null !== $episodeId) {
|
||||
$episode = $this->episodeRepo->fetchEpisodeForStation($station, $episodeId);
|
||||
$episode = $this->episodeRepo->fetchEpisodeForPodcast(
|
||||
$request->getPodcast(),
|
||||
$episodeId
|
||||
);
|
||||
|
||||
if (null === $episode) {
|
||||
return $response->withStatus(404)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Podcasts;
|
||||
|
||||
use App\Controller\SingleActionInterface;
|
||||
use App\Entity\ApiGenerator\PodcastApiGenerator;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class GetPodcastAction implements SingleActionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PodcastApiGenerator $podcastApiGen
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequest $request, Response $response, array $params): ResponseInterface
|
||||
{
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
return $response->withJson(
|
||||
$this->podcastApiGen->__invoke($podcast, $request)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations\Podcasts;
|
||||
|
||||
use App\Container\EntityManagerAwareTrait;
|
||||
use App\Controller\Api\Traits\CanSearchResults;
|
||||
use App\Controller\SingleActionInterface;
|
||||
use App\Entity\ApiGenerator\PodcastApiGenerator;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Paginator;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class ListPodcastsAction implements SingleActionInterface
|
||||
{
|
||||
use EntityManagerAwareTrait;
|
||||
use CanSearchResults;
|
||||
|
||||
public function __construct(
|
||||
private readonly PodcastApiGenerator $podcastApiGen,
|
||||
private readonly PodcastRepository $podcastRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequest $request, Response $response, array $params): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
$queryBuilder = $this->em->createQueryBuilder()
|
||||
->select('p, pc')
|
||||
->from(Podcast::class, 'p')
|
||||
->leftJoin('p.categories', 'pc')
|
||||
->where('p.storage_location = :storageLocation')
|
||||
->setParameter('storageLocation', $station->getPodcastsStorageLocation())
|
||||
->andWhere('p.id IN (:podcastIds)')
|
||||
->setParameter('podcastIds', $this->podcastRepo->getPodcastIdsWithPublishedEpisodes($station))
|
||||
->orderBy('p.title', 'ASC');
|
||||
|
||||
$queryBuilder = $this->searchQueryBuilder(
|
||||
$request,
|
||||
$queryBuilder,
|
||||
[
|
||||
'p.title',
|
||||
]
|
||||
);
|
||||
|
||||
$paginator = Paginator::fromQueryBuilder($queryBuilder, $request);
|
||||
$paginator->setPostprocessor(fn($row) => $this->podcastApiGen->__invoke($row, $request));
|
||||
|
||||
return $paginator->write($response);
|
||||
}
|
||||
}
|
|
@ -7,22 +7,17 @@ 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\ApiGenerator\PodcastApiGenerator;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\PodcastCategory;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Enums\StationPermissions;
|
||||
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;
|
||||
|
||||
|
@ -160,6 +155,7 @@ final class PodcastsController extends AbstractApiCrudController
|
|||
|
||||
public function __construct(
|
||||
private readonly PodcastRepository $podcastRepository,
|
||||
private readonly PodcastApiGenerator $podcastApiGen,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
) {
|
||||
|
@ -238,113 +234,56 @@ final class PodcastsController extends AbstractApiCrudController
|
|||
throw new InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass));
|
||||
}
|
||||
|
||||
$isInternal = Types::bool($request->getParam('internal'), false, true);
|
||||
$isInternal = $request->isInternal();
|
||||
$router = $request->getRouter();
|
||||
$station = $request->getStation();
|
||||
|
||||
$return = new ApiPodcast();
|
||||
$return->id = $record->getIdRequired();
|
||||
$return->storage_location_id = $record->getStorageLocation()->getIdRequired();
|
||||
$return = $this->podcastApiGen->__invoke($record, $request);
|
||||
|
||||
$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) {
|
||||
$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;
|
||||
|
||||
$episodes = [];
|
||||
foreach ($record->getEpisodes() as $episode) {
|
||||
$episodes[] = $episode->getId();
|
||||
}
|
||||
$return->episodes = $episodes;
|
||||
|
||||
$return->has_custom_art = (0 !== $record->getArtUpdatedAt());
|
||||
|
||||
$routeParams = [
|
||||
'podcast_id' => $record->getId(),
|
||||
$baseRouteParams = [
|
||||
'station_id' => $request->getStation()->getIdRequired(),
|
||||
'podcast_id' => $record->getIdRequired(),
|
||||
];
|
||||
if ($return->has_custom_art) {
|
||||
$routeParams['timestamp'] = $record->getArtUpdatedAt();
|
||||
|
||||
$artRouteParams = $baseRouteParams;
|
||||
if (0 !== $return->art_updated_at) {
|
||||
$artRouteParams['timestamp'] = $return->art_updated_at;
|
||||
}
|
||||
|
||||
$return->art = $router->fromHere(
|
||||
$return->art = $router->named(
|
||||
routeName: 'api:stations:podcast:art',
|
||||
routeParams: $routeParams,
|
||||
absolute: true
|
||||
routeParams: $artRouteParams,
|
||||
absolute: !$isInternal
|
||||
);
|
||||
|
||||
$return->links = [
|
||||
'self' => $router->fromHere(
|
||||
...$return->links,
|
||||
'self' => $router->named(
|
||||
routeName: $this->resourceRouteName,
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'episodes' => $router->fromHere(
|
||||
'art' => $router->named(
|
||||
routeName: 'api:stations:podcast:art',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'episodes' => $router->named(
|
||||
routeName: 'api:stations:podcast:episodes',
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'public_episodes' => $router->fromHere(
|
||||
routeName: 'public:podcast',
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
'episode_new_art' => $router->named(
|
||||
routeName: 'api:stations:podcast:episodes:new-art',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'public_feed' => $router->fromHere(
|
||||
routeName: 'public:podcast:feed',
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
'episode_new_media' => $router->named(
|
||||
routeName: 'api:stations:podcast:episodes:new-media',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
];
|
||||
|
||||
$acl = $request->getAcl();
|
||||
|
||||
if ($acl->isAllowed(StationPermissions::Podcasts, $station)) {
|
||||
$return->links['art'] = $router->fromHere(
|
||||
routeName: 'api:stations:podcast:art-internal',
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
absolute: !$isInternal
|
||||
);
|
||||
|
||||
$return->links['episode_new_art'] = $router->fromHere(
|
||||
routeName: 'api:stations:podcast:episodes:new-art',
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
absolute: !$isInternal
|
||||
);
|
||||
$return->links['episode_new_media'] = $router->fromHere(
|
||||
routeName: 'api:stations:podcast:episodes:new-media',
|
||||
routeParams: ['podcast_id' => $record->getId()],
|
||||
absolute: !$isInternal
|
||||
);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,10 +62,4 @@ final class Podcast
|
|||
items: new OA\Items(type: PodcastCategory::class)
|
||||
)]
|
||||
public array $categories = [];
|
||||
|
||||
#[OA\Property(
|
||||
type: 'array',
|
||||
items: new OA\Items(type: 'string')
|
||||
)]
|
||||
public array $episodes = [];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\ApiGenerator;
|
||||
|
||||
use App\Entity\Api\Podcast as ApiPodcast;
|
||||
use App\Entity\Api\PodcastCategory as ApiPodcastCategory;
|
||||
use App\Entity\Podcast;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\Strings;
|
||||
use Symfony\Component\Intl\Exception\MissingResourceException;
|
||||
use Symfony\Component\Intl\Languages;
|
||||
|
||||
final class PodcastApiGenerator
|
||||
{
|
||||
public function __invoke(
|
||||
Podcast $record,
|
||||
ServerRequest $request
|
||||
): ApiPodcast {
|
||||
$router = $request->getRouter();
|
||||
$isInternal = $request->isInternal();
|
||||
$station = $request->getStation();
|
||||
|
||||
$return = new ApiPodcast();
|
||||
$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) {
|
||||
$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;
|
||||
|
||||
$return->art_updated_at = $record->getArtUpdatedAt();
|
||||
$return->has_custom_art = (0 !== $record->getArtUpdatedAt());
|
||||
|
||||
$baseRouteParams = [
|
||||
'station_id' => $station->getIdRequired(),
|
||||
'podcast_id' => $record->getIdRequired(),
|
||||
];
|
||||
|
||||
$artRouteParams = $baseRouteParams;
|
||||
if ($return->has_custom_art) {
|
||||
$artRouteParams['timestamp'] = $record->getArtUpdatedAt();
|
||||
}
|
||||
|
||||
$return->art = $router->named(
|
||||
routeName: 'api:stations:public:podcast:art',
|
||||
routeParams: $artRouteParams,
|
||||
absolute: !$isInternal
|
||||
);
|
||||
|
||||
$return->links = [
|
||||
'self' => $router->named(
|
||||
routeName: 'api:stations:public:podcast',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'episodes' => $router->named(
|
||||
routeName: 'api:stations:public:podcast:episodes',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'public_episodes' => $router->named(
|
||||
routeName: 'public:podcast',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'public_feed' => $router->named(
|
||||
routeName: 'public:podcast:feed',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
];
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\ApiGenerator;
|
||||
|
||||
use App\Entity\Api\PodcastEpisode as ApiPodcastEpisode;
|
||||
use App\Entity\Api\PodcastMedia as ApiPodcastMedia;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Entity\PodcastMedia;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Utilities\Strings;
|
||||
|
||||
final class PodcastEpisodeApiGenerator
|
||||
{
|
||||
public function __invoke(
|
||||
PodcastEpisode $record,
|
||||
ServerRequest $request
|
||||
): ApiPodcastEpisode {
|
||||
$router = $request->getRouter();
|
||||
$isInternal = $request->isInternal();
|
||||
|
||||
$station = $request->getStation();
|
||||
$podcast = $request->getPodcast();
|
||||
|
||||
$return = new ApiPodcastEpisode();
|
||||
$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();
|
||||
$return->has_media = ($mediaRow instanceof PodcastMedia);
|
||||
if ($mediaRow instanceof PodcastMedia) {
|
||||
$media = new ApiPodcastMedia();
|
||||
$media->id = $mediaRow->getId();
|
||||
$media->original_name = $mediaRow->getOriginalName();
|
||||
$media->length = $mediaRow->getLength();
|
||||
$media->length_text = $mediaRow->getLengthText();
|
||||
$media->path = $mediaRow->getPath();
|
||||
|
||||
$return->has_media = true;
|
||||
$return->media = $media;
|
||||
} else {
|
||||
$return->has_media = false;
|
||||
$return->media = new ApiPodcastMedia();
|
||||
}
|
||||
|
||||
$return->art_updated_at = $record->getArtUpdatedAt();
|
||||
$return->has_custom_art = (0 !== $return->art_updated_at);
|
||||
|
||||
$baseRouteParams = [
|
||||
'station_id' => $station->getShortName(),
|
||||
'podcast_id' => $podcast->getIdRequired(),
|
||||
'episode_id' => $record->getIdRequired(),
|
||||
];
|
||||
|
||||
$artRouteParams = $baseRouteParams;
|
||||
if (0 !== $return->art_updated_at) {
|
||||
$artRouteParams['timestamp'] = $return->art_updated_at;
|
||||
}
|
||||
|
||||
$return->art = $router->named(
|
||||
routeName: 'api:stations:public:podcast:episode:art',
|
||||
routeParams: $artRouteParams,
|
||||
absolute: !$isInternal
|
||||
);
|
||||
|
||||
$return->links = [
|
||||
'self' => $router->named(
|
||||
routeName: 'api:stations:public:podcast:episode',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'public' => $router->fromHere(
|
||||
routeName: 'public:podcast:episode',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
'download' => $router->fromHere(
|
||||
routeName: 'api:stations:public:podcast:episode:download',
|
||||
routeParams: $baseRouteParams,
|
||||
absolute: !$isInternal
|
||||
),
|
||||
];
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Entity\Repository;
|
||||
|
||||
use App\Doctrine\Repository;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Entity\PodcastMedia;
|
||||
use App\Entity\Station;
|
||||
|
@ -30,6 +31,14 @@ final class PodcastEpisodeRepository extends Repository
|
|||
) {
|
||||
}
|
||||
|
||||
public function fetchEpisodeForPodcast(Podcast $podcast, string $episodeId): ?PodcastEpisode
|
||||
{
|
||||
return $this->repository->findOneBy([
|
||||
'id' => $episodeId,
|
||||
'podcast' => $podcast,
|
||||
]);
|
||||
}
|
||||
|
||||
public function fetchEpisodeForStation(Station $station, string $episodeId): ?PodcastEpisode
|
||||
{
|
||||
return $this->fetchEpisodeForStorageLocation(
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace App\Entity\Repository;
|
|||
use App\Doctrine\Repository;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\Station;
|
||||
use App\Entity\StorageLocation;
|
||||
use App\Exception\StorageLocationFullException;
|
||||
use App\Flysystem\ExtendedFilesystemInterface;
|
||||
use App\Media\AlbumArt;
|
||||
|
@ -29,21 +28,34 @@ final class PodcastRepository extends Repository
|
|||
|
||||
public function fetchPodcastForStation(Station $station, string $podcastId): ?Podcast
|
||||
{
|
||||
return $this->fetchPodcastForStorageLocation($station->getPodcastsStorageLocation(), $podcastId);
|
||||
}
|
||||
|
||||
public function fetchPodcastForStorageLocation(
|
||||
StorageLocation $storageLocation,
|
||||
string $podcastId
|
||||
): ?Podcast {
|
||||
return $this->repository->findOneBy(
|
||||
[
|
||||
'id' => $podcastId,
|
||||
'storage_location' => $storageLocation,
|
||||
'storage_location' => $station->getPodcastsStorageLocation(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Station $station
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPodcastIdsWithPublishedEpisodes(Station $station): array
|
||||
{
|
||||
return $this->em->createQuery(
|
||||
<<<'DQL'
|
||||
SELECT DISTINCT p.id
|
||||
FROM App\Entity\PodcastEpisode pe
|
||||
JOIN pe.podcast p
|
||||
JOIN pe.media pm
|
||||
WHERE pm.id IS NOT NULL
|
||||
AND (pe.publish_at IS NULL OR pe.publish_at <= :time)
|
||||
DQL
|
||||
)->setParameter('time', time())
|
||||
->enableResultCache(60, 'podcast_ids_' . $station->getIdRequired())
|
||||
->getSingleColumnResult();
|
||||
}
|
||||
|
||||
public function writePodcastArt(
|
||||
Podcast $podcast,
|
||||
string $rawArtworkString,
|
||||
|
|
|
@ -12,7 +12,7 @@ final class NotFoundException extends Exception
|
|||
{
|
||||
public function __construct(
|
||||
string $message = 'Record not found.',
|
||||
int $code = 0,
|
||||
int $code = 404,
|
||||
Throwable $previous = null,
|
||||
Level $loggerLevel = Level::Debug
|
||||
) {
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace App\Http;
|
|||
use App\Acl;
|
||||
use App\Auth;
|
||||
use App\Customization;
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\Station;
|
||||
use App\Entity\User;
|
||||
use App\Enums\SupportedLocales;
|
||||
|
@ -31,6 +32,7 @@ final class ServerRequest extends SlimServerRequest
|
|||
public const ATTR_CUSTOMIZATION = 'customization';
|
||||
public const ATTR_AUTH = 'auth';
|
||||
public const ATTR_STATION = 'station';
|
||||
public const ATTR_PODCAST = 'podcast';
|
||||
public const ATTR_USER = 'user';
|
||||
|
||||
/**
|
||||
|
@ -129,6 +131,14 @@ final class ServerRequest extends SlimServerRequest
|
|||
return $this->getAttributeOfClass(self::ATTR_STATION, Station::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidRequestAttribute
|
||||
*/
|
||||
public function getPodcast(): Podcast
|
||||
{
|
||||
return $this->getAttributeOfClass(self::ATTR_PODCAST, Podcast::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware;
|
||||
|
||||
use App\Entity\Podcast;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Slim\Routing\RouteContext;
|
||||
|
||||
/**
|
||||
* Retrieve the podcast specified in the request parameters.
|
||||
*/
|
||||
final class GetAndRequirePodcast extends AbstractMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PodcastRepository $podcastRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$routeArgs = RouteContext::fromRequest($request)->getRoute()?->getArguments();
|
||||
|
||||
$id = $routeArgs['podcast_id'] ?? $routeArgs['id'] ?? null;
|
||||
|
||||
if (empty($id)) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
$record = $this->podcastRepo->fetchPodcastForStation(
|
||||
$request->getStation(),
|
||||
$id
|
||||
);
|
||||
|
||||
if (!($record instanceof Podcast)) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
$request = $request->withAttribute(ServerRequest::ATTR_PODCAST, $record);
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
|
@ -4,18 +4,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Middleware;
|
||||
|
||||
use App\Acl;
|
||||
use App\Entity\PodcastEpisode;
|
||||
use App\Entity\Repository\PodcastRepository;
|
||||
use App\Entity\Station;
|
||||
use App\Entity\User;
|
||||
use App\Enums\StationPermissions;
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Http\ServerRequest;
|
||||
use Exception;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Slim\Routing\RouteContext;
|
||||
|
||||
/**
|
||||
* Require that the podcast has a published episode for public access
|
||||
|
@ -29,70 +22,15 @@ final class RequirePublishedPodcastEpisodeMiddleware extends AbstractMiddleware
|
|||
|
||||
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$user = $this->getLoggedInUser($request);
|
||||
$station = $request->getStation();
|
||||
|
||||
if ($user !== null) {
|
||||
$acl = $request->getAcl();
|
||||
$publishedPodcastIds = $this->podcastRepository->getPodcastIdsWithPublishedEpisodes($station);
|
||||
|
||||
if ($this->canUserManageStationPodcasts($user, $station, $acl)) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
||||
$podcastId = $this->getPodcastIdFromRequest($request);
|
||||
|
||||
if ($podcastId === null || !$this->checkPodcastHasPublishedEpisodes($station, $podcastId)) {
|
||||
$podcast = $request->getPodcast();
|
||||
if (!in_array($podcast->getIdRequired(), $publishedPodcastIds, true)) {
|
||||
throw NotFoundException::podcast();
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
private function getLoggedInUser(ServerRequest $request): ?User
|
||||
{
|
||||
try {
|
||||
return $request->getUser();
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function canUserManageStationPodcasts(User $user, Station $station, Acl $acl): bool
|
||||
{
|
||||
return $acl->userAllowed($user, StationPermissions::Podcasts, $station->getId());
|
||||
}
|
||||
|
||||
private function getPodcastIdFromRequest(ServerRequest $request): ?string
|
||||
{
|
||||
$routeArgs = RouteContext::fromRequest($request)->getRoute()?->getArguments();
|
||||
|
||||
$podcastId = $routeArgs['id'] ?? null;
|
||||
|
||||
if ($podcastId === null) {
|
||||
$podcastId = $routeArgs['podcast_id'] ?? null;
|
||||
}
|
||||
|
||||
return $podcastId;
|
||||
}
|
||||
|
||||
private function checkPodcastHasPublishedEpisodes(Station $station, string $podcastId): bool
|
||||
{
|
||||
$podcastId = explode('|', $podcastId, 2)[0];
|
||||
|
||||
$podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId);
|
||||
|
||||
if ($podcast === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var PodcastEpisode $episode */
|
||||
foreach ($podcast->getEpisodes() as $episode) {
|
||||
if ($episode->isPublished()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue