#4594 -- Fix how the Schedule embed displays by allowing calendar return data on public API endpoint.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-09-23 20:13:11 -05:00
parent 09b5e1d06d
commit c7a06c2583
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
8 changed files with 307 additions and 178 deletions

View File

@ -227,7 +227,7 @@ return static function (RouteCollectorProxy $app) {
->setName('api:stations:profile')
->add(new Middleware\Permissions(Acl::STATION_VIEW, true));
$group->get('/schedule', Controller\Api\Stations\ScheduleController::class)
$group->get('/schedule', Controller\Api\Stations\ScheduleAction::class)
->setName('api:stations:schedule');
$group->get('/history', Controller\Api\Stations\HistoryController::class)

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\HasScheduleDisplay;
use App\Entity;
use App\Exception\ValidationException;
use App\Http\Response;
@ -22,6 +23,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
*/
abstract class AbstractScheduledEntityController extends AbstractStationApiCrudController
{
use HasScheduleDisplay;
public function __construct(
protected Entity\Repository\StationScheduleRepository $scheduleRepo,
protected Scheduler $scheduler,
@ -38,54 +41,12 @@ abstract class AbstractScheduledEntityController extends AbstractStationApiCrudC
array $scheduleItems,
callable $rowRender
): ResponseInterface {
$tz = $request->getStation()->getTimezoneObject();
[$startDate, $endDate] = $this->getDateRange($request);
$params = $request->getQueryParams();
$startDateStr = substr($params['start'], 0, 10);
$startDate = CarbonImmutable::createFromFormat('Y-m-d', $startDateStr, $tz);
if (false === $startDate) {
throw new \InvalidArgumentException(sprintf('Could not parse start date: "%s"', $startDateStr));
}
$startDate = $startDate->subDay();
$endDateStr = substr($params['end'], 0, 10);
$endDate = CarbonImmutable::createFromFormat('Y-m-d', $endDateStr, $tz);
if (false === $endDate) {
throw new \InvalidArgumentException(sprintf('Could not parse end date: "%s"', $endDateStr));
}
$events = [];
foreach ($scheduleItems as $scheduleItem) {
/** @var Entity\StationSchedule $scheduleItem */
$i = $startDate;
while ($i <= $endDate) {
$dayOfWeek = $i->dayOfWeekIso;
if (
$this->scheduler->shouldSchedulePlayOnCurrentDate($scheduleItem, $i)
&& $this->scheduler->isScheduleScheduledToPlayToday($scheduleItem, $dayOfWeek)
) {
$rowStart = Entity\StationSchedule::getDateTime($scheduleItem->getStartTime(), $i);
$rowEnd = Entity\StationSchedule::getDateTime($scheduleItem->getEndTime(), $i);
// Handle overnight schedule items
if ($rowEnd < $rowStart) {
$rowEnd = $rowEnd->addDay();
}
$events[] = $rowRender($scheduleItem, $rowStart, $rowEnd);
}
$i = $i->addDay();
}
}
$station = $request->getStation();
$now = CarbonImmutable::now($station->getTimezoneObject());
$events = $this->getEvents($startDate, $endDate, $now, $this->scheduler, $scheduleItems, $rowRender);
return $response->withJson($events);
}

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\HasScheduleDisplay;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\AutoDJ\Scheduler;
use Carbon\CarbonImmutable;
use Doctrine\ORM\EntityManagerInterface;
use OpenApi\Annotations as OA;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Cache\CacheInterface;
class ScheduleAction
{
use HasScheduleDisplay;
/**
* @OA\Get(path="/station/{station_id}/schedule",
* tags={"Stations: Schedules"},
* description="Return upcoming and currently ongoing schedule entries.",
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
* @OA\Parameter(
* name="now",
* description="The date/time to compare schedule items to. Defaults to the current date and time.",
* in="query",
* required=false,
* @OA\Schema(type="string")
* ),
* @OA\Parameter(
* name="rows",
* description="The number of upcoming/ongoing schedule entries to return. Defaults to 5.",
* in="query",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="Success",
* @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/Api_StationSchedule"))
* ),
* @OA\Response(response=404, description="Station not found"),
* @OA\Response(response=403, description="Access denied")
* )
*/
public function __invoke(
ServerRequest $request,
Response $response,
EntityManagerInterface $em,
Scheduler $scheduler,
CacheInterface $cache,
Entity\ApiGenerator\ScheduleApiGenerator $scheduleApiGenerator,
Entity\Repository\StationScheduleRepository $scheduleRepo
): ResponseInterface {
$station = $request->getStation();
$tz = $station->getTimezoneObject();
$queryParams = $request->getQueryParams();
if (isset($queryParams['start'])) {
[$startDate, $endDate] = $this->getDateRange($request);
$cacheKey = 'api_station_' . $station->getId() . '_schedule_'
. $startDate->format('Ymd') . '-'
. $endDate->format('Ymd');
$events = $cache->get(
$cacheKey,
function (CacheItem $item) use (
$station,
$scheduleRepo,
$scheduleApiGenerator,
$scheduler,
$startDate,
$endDate
) {
$item->expiresAfter(1);
$nowTz = CarbonImmutable::now($station->getTimezoneObject());
$events = $scheduleRepo->getAllScheduledItemsForStation($station);
return $this->getEvents(
$startDate,
$endDate,
$nowTz,
$scheduler,
$events,
[$scheduleApiGenerator, '__invoke']
);
}
);
} else {
if (!empty($queryParams['now'])) {
$now = CarbonImmutable::parse($queryParams['now'], $tz);
$cacheKey = 'api_station_' . $station->getId() . '_schedule_' . $now->format('Ymd_gia');
} else {
$now = CarbonImmutable::now($tz);
$cacheKey = 'api_station_' . $station->getId() . '_schedule_upcoming';
}
$events = $cache->get(
$cacheKey,
function (CacheItem $item) use ($scheduleRepo, $station, $now) {
$item->expiresAfter(60);
return $scheduleRepo->getUpcomingSchedule($station, $now);
}
);
$rows = (int)$request->getQueryParam('rows', 5);
$events = array_slice($events, 0, $rows);
}
return $response->withJson($events);
}
}

View File

@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations;
use App\Entity\Repository\StationScheduleRepository;
use App\Http\Response;
use App\Http\ServerRequest;
use Carbon\CarbonImmutable;
use Doctrine\ORM\EntityManagerInterface;
use OpenApi\Annotations as OA;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Cache\CacheInterface;
class ScheduleController extends AbstractStationApiCrudController
{
/**
* @OA\Get(path="/station/{station_id}/schedule",
* tags={"Stations: Schedules"},
* description="Return upcoming and currently ongoing schedule entries.",
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
* @OA\Parameter(
* name="now",
* description="The date/time to compare schedule items to. Defaults to the current date and time.",
* in="query",
* required=false,
* @OA\Schema(type="string")
* ),
* @OA\Parameter(
* name="rows",
* description="The number of upcoming/ongoing schedule entries to return. Defaults to 5.",
* in="query",
* required=false,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="Success",
* @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/Api_StationSchedule"))
* ),
* @OA\Response(response=404, description="Station not found"),
* @OA\Response(response=403, description="Access denied")
* )
*
* @param ServerRequest $request
* @param Response $response
* @param EntityManagerInterface $em
* @param CacheInterface $cache
* @param StationScheduleRepository $scheduleRepo
*/
public function __invoke(
ServerRequest $request,
Response $response,
EntityManagerInterface $em,
CacheInterface $cache,
StationScheduleRepository $scheduleRepo
): ResponseInterface {
$station = $request->getStation();
$tz = $station->getTimezoneObject();
$now = $request->getQueryParam('now');
if (!empty($now)) {
$now = CarbonImmutable::parse($now, $tz);
$cacheKey = 'api_station_' . $station->getId() . '_schedule_' . $now->format('Ymd_gia');
} else {
$now = CarbonImmutable::now($tz);
$cacheKey = 'api_station_' . $station->getId() . '_schedule_upcoming';
}
$events = $cache->get(
$cacheKey,
function (CacheItem $item) use ($scheduleRepo, $station, $now) {
$item->expiresAfter(60);
return $scheduleRepo->getUpcomingSchedule($station, $now);
}
);
$rows = (int)$request->getQueryParam('rows', 5);
$events = array_slice($events, 0, $rows);
return $response->withJson($events);
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Traits;
use App\Entity;
use App\Http\ServerRequest;
use App\Radio\AutoDJ\Scheduler;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
trait HasScheduleDisplay
{
protected function getDateRange(ServerRequest $request): array
{
$tz = $request->getStation()->getTimezoneObject();
$params = $request->getQueryParams();
$startDateStr = substr($params['start'], 0, 10);
$startDate = CarbonImmutable::createFromFormat('Y-m-d', $startDateStr, $tz);
if (false === $startDate) {
throw new \InvalidArgumentException(sprintf('Could not parse start date: "%s"', $startDateStr));
}
$startDate = $startDate->startOf('day');
$endDateStr = substr($params['end'], 0, 10);
$endDate = CarbonImmutable::createFromFormat('Y-m-d', $endDateStr, $tz);
if (false === $endDate) {
throw new \InvalidArgumentException(sprintf('Could not parse end date: "%s"', $endDateStr));
}
$endDate = $endDate->endOf('day');
return [$startDate, $endDate];
}
protected function getEvents(
CarbonInterface $startDate,
CarbonInterface $endDate,
CarbonInterface $now,
Scheduler $scheduler,
array $scheduleItems,
callable $rowRender
): array {
$events = [];
foreach ($scheduleItems as $scheduleItem) {
/** @var Entity\StationSchedule $scheduleItem */
$i = $startDate;
while ($i <= $endDate) {
$dayOfWeek = $i->dayOfWeekIso;
if (
$scheduler->shouldSchedulePlayOnCurrentDate($scheduleItem, $i)
&& $scheduler->isScheduleScheduledToPlayToday($scheduleItem, $dayOfWeek)
) {
$rowStart = Entity\StationSchedule::getDateTime($scheduleItem->getStartTime(), $i);
$rowEnd = Entity\StationSchedule::getDateTime($scheduleItem->getEndTime(), $i);
// Handle overnight schedule items
if ($rowEnd < $rowStart) {
$rowEnd = $rowEnd->addDay();
}
$events[] = $rowRender($scheduleItem, $rowStart, $rowEnd, $now);
}
$i = $i->addDay();
}
}
return $events;
}
}

View File

@ -17,56 +17,54 @@ class StationSchedule
/**
* Unique identifier for this schedule entry.
* @OA\Property(example=1)
* @var int
*/
public int $id;
/**
* The type of this schedule entry.
* @OA\Property(enum={App\Entity\Api\StationSchedule::TYPE_PLAYLIST, App\Entity\Api\StationSchedule::TYPE_STREAMER}, example=App\Entity\Api\StationSchedule::TYPE_PLAYLIST)
* @var string
*/
public string $type;
/**
* Either the playlist or streamer's display name.
* @OA\Property(example="Example Schedule Entry")
* @var string
*/
public string $name;
/**
* The full name of the type and name combined.
* @OA\Property(example="Playlist: Example Schedule Entry")
*/
public string $title;
/**
* The start time of the schedule entry, in UNIX format.
* @OA\Property(example=1609480800)
* @var int
*/
public int $start_timestamp;
/**
* The start time of the schedule entry, in ISO 8601 format.
* @OA\Property(example="020-02-19T03:00:00-06:00")
* @var string
*/
public string $start;
/**
* The end time of the schedule entry, in UNIX format.
* @OA\Property(example=1609480800)
* @var int
*/
public int $end_timestamp;
/**
* The start time of the schedule entry, in ISO 8601 format.
* @OA\Property(example="020-02-19T05:00:00-06:00")
* @var string
*/
public string $end;
/**
* Whether the event is currently ongoing.
* @OA\Property(example=true)
* @var bool
*/
public bool $is_now;
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Entity\ApiGenerator;
use App\Entity;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
class ScheduleApiGenerator
{
public function __invoke(
Entity\StationSchedule $scheduleItem,
?CarbonInterface $start,
?CarbonInterface $end,
?CarbonInterface $now
): Entity\Api\StationSchedule {
$playlist = $scheduleItem->getPlaylist();
$streamer = $scheduleItem->getStreamer();
if (null === $now) {
if (null !== $playlist) {
$station = $playlist->getStation();
} elseif (null !== $streamer) {
$station = $streamer->getStation();
} else {
$station = null;
}
$now = CarbonImmutable::now($station?->getTimezoneObject());
}
if (null === $start || null === $end) {
$start = Entity\StationSchedule::getDateTime($scheduleItem->getStartTime(), $now);
$end = Entity\StationSchedule::getDateTime($scheduleItem->getEndTime(), $now);
// Handle overnight schedule items
if ($end < $start) {
$end = $end->addDay();
}
}
$row = new Entity\Api\StationSchedule();
$row->id = $scheduleItem->getIdRequired();
$row->start_timestamp = $start->getTimestamp();
$row->start = $start->toIso8601String();
$row->end_timestamp = $end->getTimestamp();
$row->end = $end->toIso8601String();
$row->is_now = ($start <= $now && $end >= $now);
if ($playlist instanceof Entity\StationPlaylist) {
$row->type = Entity\Api\StationSchedule::TYPE_PLAYLIST;
$row->name = $playlist->getName();
$row->title = __('Playlist: %s', $row->name);
} elseif ($streamer instanceof Entity\StationStreamer) {
$row->type = Entity\Api\StationSchedule::TYPE_STREAMER;
$row->name = $streamer->getDisplayName();
$row->title = __('Streamer: %s', $row->name);
}
return $row;
}
}

View File

@ -19,18 +19,15 @@ use Symfony\Component\Serializer\Serializer;
*/
class StationScheduleRepository extends Repository
{
protected Scheduler $scheduler;
public function __construct(
ReloadableEntityManagerInterface $em,
Serializer $serializer,
Environment $environment,
LoggerInterface $logger,
Scheduler $scheduler
protected Scheduler $scheduler,
protected Entity\ApiGenerator\ScheduleApiGenerator $scheduleApiGenerator
) {
parent::__construct($em, $serializer, $environment, $logger);
$this->scheduler = $scheduler;
}
/**
@ -85,6 +82,26 @@ class StationScheduleRepository extends Repository
return $this->repository->findBy(['streamer' => $relation]);
}
/**
* @param Entity\Station $station
*
* @return Entity\StationSchedule[]
*/
public function getAllScheduledItemsForStation(Entity\Station $station): array
{
return $this->em->createQuery(
<<<'DQL'
SELECT ssc, sp, sst
FROM App\Entity\StationSchedule ssc
LEFT JOIN ssc.playlist sp
LEFT JOIN ssc.streamer sst
WHERE (sp.station = :station AND sp.is_jingle = 0 AND sp.is_enabled = 1)
OR (sst.station = :station AND sst.is_active = 1)
DQL
)->setParameter('station', $station)
->execute();
}
/**
* @param Entity\Station $station
* @param CarbonInterface|null $now
@ -102,19 +119,7 @@ class StationScheduleRepository extends Repository
$events = [];
$scheduleItems = $this->em->createQuery(
<<<'DQL'
SELECT ssc, sp, sst
FROM App\Entity\StationSchedule ssc
LEFT JOIN ssc.playlist sp
LEFT JOIN ssc.streamer sst
WHERE (sp.station = :station AND sp.is_jingle = 0 AND sp.is_enabled = 1)
OR (sst.station = :station AND sst.is_active = 1)
DQL
)->setParameter('station', $station)
->execute();
foreach ($scheduleItems as $scheduleItem) {
foreach ($this->getAllScheduledItemsForStation($station) as $scheduleItem) {
/** @var Entity\StationSchedule $scheduleItem */
$i = $startDate;
@ -139,26 +144,12 @@ class StationScheduleRepository extends Repository
continue;
}
$row = new Entity\Api\StationSchedule();
$row->id = $scheduleItem->getIdRequired();
$row->start_timestamp = $start->getTimestamp();
$row->start = $start->toIso8601String();
$row->end_timestamp = $end->getTimestamp();
$row->end = $end->toIso8601String();
$row->is_now = $start->lessThanOrEqualTo($now);
$playlist = $scheduleItem->getPlaylist();
$streamer = $scheduleItem->getStreamer();
if ($playlist instanceof Entity\StationPlaylist) {
$row->type = Entity\Api\StationSchedule::TYPE_PLAYLIST;
$row->name = $playlist->getName();
} elseif ($streamer instanceof Entity\StationStreamer) {
$row->type = Entity\Api\StationSchedule::TYPE_STREAMER;
$row->name = $streamer->getDisplayName();
}
$events[] = $row;
$events[] = ($this->scheduleApiGenerator)(
$scheduleItem,
$start,
$end,
$now
);
}
$i = $i->addDay();