#788 -- Implement station management via API endpoint.

This commit is contained in:
Buster Neece 2019-04-08 00:09:53 -05:00
parent bbe1690843
commit 9c5797dc38
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
11 changed files with 450 additions and 22 deletions

8
composer.lock generated
View File

@ -94,12 +94,12 @@
"source": {
"type": "git",
"url": "https://github.com/AzuraCast/azuracore.git",
"reference": "c89d6e57bc39e3614065f63a24c75c6bd0e57036"
"reference": "0d12c580c9aadfb8e48bece4f57ffbf84ae94898"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/AzuraCast/azuracore/zipball/c89d6e57bc39e3614065f63a24c75c6bd0e57036",
"reference": "c89d6e57bc39e3614065f63a24c75c6bd0e57036",
"url": "https://api.github.com/repos/AzuraCast/azuracore/zipball/0d12c580c9aadfb8e48bece4f57ffbf84ae94898",
"reference": "0d12c580c9aadfb8e48bece4f57ffbf84ae94898",
"shasum": ""
},
"require": {
@ -149,7 +149,7 @@
}
],
"description": "A lightweight core application framework.",
"time": "2019-03-23T07:10:05+00:00"
"time": "2019-04-08T04:23:55+00:00"
},
{
"name": "azuracast/azuraforms",

View File

@ -247,6 +247,18 @@ return function(App $app)
$this->put('/settings', Controller\Api\Admin\SettingsController::class.':updateAction');
})->add([Middleware\Permissions::class, Acl::GLOBAL_SETTINGS]);
$this->group('', function() {
/** @var App $this */
$this->get('/stations', Controller\Api\Admin\StationsController::class.':listAction')
->setName('api:admin:stations');
$this->post('/stations', Controller\Api\Admin\StationsController::class.':createAction');
$this->get('/station/{id}', Controller\Api\Admin\StationsController::class.':getAction')
->setName('api:admin:station');
$this->put('/station/{id}', Controller\Api\Admin\StationsController::class.':editAction');
$this->delete('/station/{id}', Controller\Api\Admin\StationsController::class.':deleteAction');
})->add([Middleware\Permissions::class, Acl::GLOBAL_STATIONS]);
});
$this->group('/station/{station}', function () {

View File

@ -90,13 +90,10 @@ abstract class AbstractCrudController
/**
* @param array $data
* @param object|null $record
* @param object $record
* @return object
* @throws \App\Exception\Validation
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
protected function _editRecord($data, $record = null): object
protected function _editRecord($data, $record): object
{
if (null === $data) {
throw new \InvalidArgumentException('Could not parse input data.');

View File

@ -41,8 +41,7 @@ abstract class AbstractGenericCrudController extends AbstractCrudController
*/
public function createAction(Request $request, Response $response): ResponseInterface
{
$record = new $this->entityClass();
$row = $this->_editRecord($request->getParsedBody(), $record);
$row = $this->_createRecord($request->getParsedBody());
$return = $this->_viewRecord($row, $request->getRouter());
return $response->withJson($return);
@ -62,6 +61,16 @@ abstract class AbstractGenericCrudController extends AbstractCrudController
return $response->withJson($return);
}
/**
* @param $data
* @return object
*/
protected function _createRecord($data): object
{
$record = new $this->entityClass();
return $this->_editRecord($data, $record);
}
/**
* @param Request $request
* @param Response $response

View File

@ -0,0 +1,141 @@
<?php
namespace App\Controller\Api\Admin;
use App\Entity;
use App\Controller\Api\AbstractGenericCrudController;
use Azura\Normalizer\DoctrineEntityNormalizer;
use Doctrine\ORM\EntityManager;
use OpenApi\Annotations as OA;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @see \App\Provider\ApiProvider
*/
class StationsController extends AbstractGenericCrudController
{
protected $entityClass = Entity\Station::class;
protected $resourceRouteName = 'api:admin:station';
/** @var Entity\Repository\StationRepository */
protected $station_repo;
public function __construct(EntityManager $em, Serializer $serializer, ValidatorInterface $validator)
{
parent::__construct($em, $serializer, $validator);
$this->station_repo = $em->getRepository(Entity\Station::class);
}
/**
* @OA\Get(path="/admin/stations",
* tags={"Administration: Stations"},
* description="List all current stations in the system.",
* @OA\Response(response=200, description="Success",
* @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/Station"))
* ),
* @OA\Response(response=403, description="Access denied"),
* security={{"api_key": {}}},
* )
*
* @OA\Post(path="/admin/stations",
* tags={"Administration: Stations"},
* description="Create a new station.",
* @OA\RequestBody(
* @OA\JsonContent(ref="#/components/schemas/Station")
* ),
* @OA\Response(response=200, description="Success",
* @OA\JsonContent(ref="#/components/schemas/Station")
* ),
* @OA\Response(response=403, description="Access denied"),
* security={{"api_key": {}}},
* )
*
* @OA\Get(path="/admin/station/{id}",
* tags={"Administration: Stations"},
* description="Retrieve details for a single station.",
* @OA\Parameter(
* name="id",
* in="path",
* description="ID",
* required=true,
* @OA\Schema(type="integer", format="int64")
* ),
* @OA\Response(response=200, description="Success",
* @OA\JsonContent(ref="#/components/schemas/Station")
* ),
* @OA\Response(response=403, description="Access denied"),
* security={{"api_key": {}}},
* )
*
* @OA\Put(path="/admin/station/{id}",
* tags={"Administration: Stations"},
* description="Update details of a single station.",
* @OA\RequestBody(
* @OA\JsonContent(ref="#/components/schemas/Station")
* ),
* @OA\Parameter(
* name="id",
* in="path",
* description="ID",
* required=true,
* @OA\Schema(type="integer", format="int64")
* ),
* @OA\Response(response=200, description="Success",
* @OA\JsonContent(ref="#/components/schemas/Api_Status")
* ),
* @OA\Response(response=403, description="Access denied"),
* security={{"api_key": {}}},
* )
*
* @OA\Delete(path="/admin/station/{id}",
* tags={"Administration: Stations"},
* description="Delete a single station.",
* @OA\Parameter(
* name="id",
* in="path",
* description="ID",
* required=true,
* @OA\Schema(type="integer", format="int64")
* ),
* @OA\Response(response=200, description="Success",
* @OA\JsonContent(ref="#/components/schemas/Api_Status")
* ),
* @OA\Response(response=403, description="Access denied"),
* security={{"api_key": {}}},
* )
*/
/** @inheritDoc */
protected function _normalizeRecord($record, array $context = [])
{
return parent::_normalizeRecord($record, $context + [
DoctrineEntityNormalizer::IGNORED_ATTRIBUTES => [
'adapter_api_key',
'nowplaying',
'nowplaying_timestamp',
'automation_timestamp',
'needs_restart',
'has_started',
],
]);
}
/** @inheritDoc */
protected function _createRecord($data): object
{
return $this->station_repo->create($data);
}
/** @inheritDoc */
protected function _editRecord($data, $record): object
{
return $this->station_repo->edit($data, $record);
}
/** @inheritDoc */
protected function _deleteRecord($record): void
{
$this->station_repo->destroy($record);
}
}

View File

@ -53,9 +53,7 @@ abstract class AbstractStationCrudController extends AbstractCrudController
public function createAction(Request $request, Response $response, $station_id): ResponseInterface
{
$station = $this->_getStation($request);
$record = new $this->entityClass($station);
$row = $this->_editRecord($request->getParsedBody(), $record);
$row = $this->_createRecord($request->getParsedBody(), $station);
$router = $request->getRouter();
$return = $this->_viewRecord($row, $router);
@ -122,6 +120,17 @@ abstract class AbstractStationCrudController extends AbstractCrudController
return $response->withJson(new Entity\Api\Status(true, 'Record deleted successfully.'));
}
/**
* @param $data
* @param Entity\Station $station
* @return object
*/
protected function _createRecord($data, Entity\Station $station): object
{
$record = new $this->entityClass($station);
return $this->_editRecord($data, $record);
}
/**
* @param Entity\Station $station
* @param int|string $record_id

View File

@ -9,6 +9,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use OpenApi\Annotations as OA;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Radio\Frontend\AbstractFrontend;
use App\Radio\Remote\AdapterProxy;
@ -38,84 +39,111 @@ class Station
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*
* @OA\Property(example=1)
* @var int
*/
protected $id;
/**
* @ORM\Column(name="name", type="string", length=100, nullable=true)
*
* @OA\Property(example="AzuraTest Radio")
* @var string|null The full display name of the station.
*/
protected $name;
/**
* @ORM\Column(name="short_name", type="string", length=100, nullable=true)
*
* @OA\Property(example="azuratest_radio")
* @var string|null The URL-friendly name for the station, typically auto-generated from the full station name.
*/
protected $short_name;
/**
* @ORM\Column(name="is_enabled", type="boolean", nullable=false)
*
* @OA\Property(example=true)
* @var bool If set to "false", prevents the station from broadcasting but leaves it in the database.
*/
protected $is_enabled = true;
/**
* @ORM\Column(name="frontend_type", type="string", length=100, nullable=true)
*
* @OA\Property(example="icecast")
* @var string|null The frontend adapter (icecast,shoutcast,remote,etc)
*/
protected $frontend_type;
/**
* @ORM\Column(name="frontend_config", type="json_array", nullable=true)
*
* @OA\Property(@OA\Items())
* @var array|null An array containing station-specific frontend configuration
*/
protected $frontend_config;
/**
* @ORM\Column(name="backend_type", type="string", length=100, nullable=true)
*
* @OA\Property(example="liquidsoap")
* @var string|null The backend adapter (liquidsoap,etc)
*/
protected $backend_type;
/**
* @ORM\Column(name="backend_config", type="json_array", nullable=true)
*
* @OA\Property(@OA\Items())
* @var array|null An array containing station-specific backend configuration
*/
protected $backend_config;
/**
* @ORM\Column(name="adapter_api_key", type="string", length=150, nullable=true)
*
* @var string|null An internal-use API key used for container-to-container communications from Liquidsoap to AzuraCast
*/
protected $adapter_api_key;
/**
* @ORM\Column(name="description", type="text", nullable=true)
*
* @OA\Property(example="A sample radio station.")
* @var string|null
*/
protected $description;
/**
* @ORM\Column(name="url", type="string", length=255, nullable=true)
*
* @OA\Property(example="https://demo.azuracast.com/")
* @var string|null
*/
protected $url;
/**
* @ORM\Column(name="genre", type="string", length=150, nullable=true)
*
* @OA\Property(example="Various")
* @var string|null
*/
protected $genre;
/**
* @ORM\Column(name="radio_base_dir", type="string", length=255, nullable=true)
*
* @OA\Property(example="/var/azuracast/stations/azuratest_radio")
* @var string|null
*/
protected $radio_base_dir;
/**
* @ORM\Column(name="radio_media_dir", type="string", length=255, nullable=true)
*
* @OA\Property(example="/var/azuracast/stations/azuratest_radio/media")
* @var string|null
*/
protected $radio_media_dir;
@ -134,6 +162,8 @@ class Station
/**
* @ORM\Column(name="automation_settings", type="json_array", nullable=true)
*
* @OA\Property(@OA\Items())
* @var array|null
*/
protected $automation_settings;
@ -146,42 +176,56 @@ class Station
/**
* @ORM\Column(name="enable_requests", type="boolean", nullable=false)
*
* @OA\Property(example=true)
* @var bool Whether listeners can request songs to play on this station.
*/
protected $enable_requests = false;
/**
* @ORM\Column(name="request_delay", type="integer", nullable=true)
*
* @OA\Property(example=5)
* @var int|null
*/
protected $request_delay = self::DEFAULT_REQUEST_DELAY;
/**
* @ORM\Column(name="request_threshold", type="integer", nullable=true)
*
* @OA\Property(example=15)
* @var int|null
*/
protected $request_threshold = self::DEFAULT_REQUEST_THRESHOLD;
/**
* @ORM\Column(name="disconnect_deactivate_streamer", type="integer", nullable=true, options={"default":0})
*
* @OA\Property(example=0)
* @var int
*/
protected $disconnect_deactivate_streamer = self::DEFAULT_DISCONNECT_DEACTIVATE_STREAMER;
/**
* @ORM\Column(name="enable_streamers", type="boolean", nullable=false)
*
* @OA\Property(example=false)
* @var bool Whether streamers are allowed to broadcast to this station at all.
*/
protected $enable_streamers = false;
/**
* @ORM\Column(name="is_streamer_live", type="boolean", nullable=false)
*
* @OA\Property(example=false)
* @var bool Whether a streamer is currently active on the station.
*/
protected $is_streamer_live = false;
/**
* @ORM\Column(name="enable_public_page", type="boolean", nullable=false)
*
* @OA\Property(example=true)
* @var bool Whether this station is visible as a public page and in a now-playing API response.
*/
protected $enable_public_page = true;
@ -200,18 +244,24 @@ class Station
/**
* @ORM\Column(name="api_history_items", type="smallint")
*
* @OA\Property(example=5)
* @var int|null The number of "last played" history items to show for a given station in the Now Playing API responses.
*/
protected $api_history_items = self::DEFAULT_API_HISTORY_ITEMS;
/**
* @ORM\Column(name="storage_quota", type="bigint", nullable=true)
*
* @OA\Property(example=52428800)
* @var string|null
*/
protected $storage_quota;
/**
* @ORM\Column(name="storage_used", type="bigint", nullable=true)
*
* @OA\Property(example=1048576)
* @var string|null
*/
protected $storage_used;

View File

@ -90,7 +90,7 @@ class StationForm extends EntityForm
}
if (null !== $record) {
$this->populate($this->_normalizeRecord($record, ['deep' => false]));
$this->populate($this->_normalizeRecord($record));
}
if ($request->isPost() && $this->isValid($request->getParsedBody())) {

View File

@ -100,6 +100,7 @@ class ApiProvider implements ServiceProviderInterface
Api\Admin\UsersController::class,
Api\Admin\RolesController::class,
Api\Admin\SettingsController::class,
Api\Admin\StationsController::class,
Api\Stations\StreamersController::class,
];

View File

@ -57,6 +57,7 @@ use OpenApi\Annotations as OA;
* @OA\Tag(name="Administration: Users")
* @OA\Tag(name="Administration: Roles")
* @OA\Tag(name="Administration: Settings")
* @OA\Tag(name="Administration: Stations")
*
* @OA\Tag(name="Miscellaneous")
*

View File

@ -312,6 +312,127 @@ paths:
security:
-
api_key: []
/admin/stations:
get:
tags:
- 'Administration: Stations'
description: 'List all current stations in the system.'
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Station'
'403':
description: 'Access denied'
security:
-
api_key: []
post:
tags:
- 'Administration: Stations'
description: 'Create a new station.'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Station'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Station'
'403':
description: 'Access denied'
security:
-
api_key: []
'/admin/station/{id}':
get:
tags:
- 'Administration: Stations'
description: 'Retrieve details for a single station.'
parameters:
-
name: id
in: path
description: ID
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Station'
'403':
description: 'Access denied'
security:
-
api_key: []
put:
tags:
- 'Administration: Stations'
description: 'Update details of a single station.'
parameters:
-
name: id
in: path
description: ID
required: true
schema:
type: integer
format: int64
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Station'
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Api_Status'
'403':
description: 'Access denied'
security:
-
api_key: []
delete:
tags:
- 'Administration: Stations'
description: 'Delete a single station.'
parameters:
-
name: id
in: path
description: ID
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/Api_Status'
'403':
description: 'Access denied'
security:
-
api_key: []
/admin/users:
get:
tags:
@ -1051,7 +1172,7 @@ components:
connected_on:
description: 'UNIX timestamp that the user first connected.'
type: integer
example: 1554547255
example: 1554636757
connected_time:
description: 'Number of seconds that the user has been connected.'
type: integer
@ -1147,7 +1268,7 @@ components:
cued_at:
description: 'UNIX timestamp when the item was cued for playback.'
type: integer
example: 1554547255
example: 1554636757
autodj_custom_uri:
description: 'Custom AutoDJ playback URI, if it exists.'
type: string
@ -1202,7 +1323,7 @@ components:
played_at:
description: 'UNIX timestamp when playback started.'
type: integer
example: 1554547255
example: 1554636757
duration:
description: 'Duration of the song in seconds'
type: integer
@ -1333,7 +1454,7 @@ components:
timestamp:
description: 'The current UNIX timestamp'
type: integer
example: 1554547255
example: 1554636757
type: object
Api_Time:
properties:
@ -1394,6 +1515,91 @@ components:
items: { }
type: object
Station:
properties:
id:
type: integer
example: 1
name:
description: 'The full display name of the station.'
type: string
example: 'AzuraTest Radio'
short_name:
description: 'The URL-friendly name for the station, typically auto-generated from the full station name.'
type: string
example: azuratest_radio
is_enabled:
description: 'If set to "false", prevents the station from broadcasting but leaves it in the database.'
type: boolean
example: true
frontend_type:
description: 'The frontend adapter (icecast,shoutcast,remote,etc)'
type: string
example: icecast
frontend_config:
description: 'An array containing station-specific frontend configuration'
type: array
items: { }
backend_type:
description: 'The backend adapter (liquidsoap,etc)'
type: string
example: liquidsoap
backend_config:
description: 'An array containing station-specific backend configuration'
type: array
items: { }
description:
type: string
example: 'A sample radio station.'
url:
type: string
example: 'https://demo.azuracast.com/'
genre:
type: string
example: Various
radio_base_dir:
type: string
example: /var/azuracast/stations/azuratest_radio
radio_media_dir:
type: string
example: /var/azuracast/stations/azuratest_radio/media
automation_settings:
type: array
items: { }
enable_requests:
description: 'Whether listeners can request songs to play on this station.'
type: boolean
example: true
request_delay:
type: integer
example: 5
request_threshold:
type: integer
example: 15
disconnect_deactivate_streamer:
type: integer
example: 0
enable_streamers:
description: 'Whether streamers are allowed to broadcast to this station at all.'
type: boolean
example: false
is_streamer_live:
description: 'Whether a streamer is currently active on the station.'
type: boolean
example: false
enable_public_page:
description: 'Whether this station is visible as a public page and in a now-playing API response.'
type: boolean
example: true
api_history_items:
description: 'The number of "last played" history items to show for a given station in the Now Playing API responses.'
type: integer
example: 5
storage_quota:
type: string
example: 52428800
storage_used:
type: string
example: 1048576
type: object
StationStreamer:
properties:
@ -1417,7 +1623,7 @@ components:
example: true
reactivate_at:
type: integer
example: 1554547255
example: 1554636757
type: object
User:
properties:
@ -1447,10 +1653,10 @@ components:
example: A1B2C3D4
created_at:
type: integer
example: 1554547255
example: 1554636757
updated_at:
type: integer
example: 1554547255
example: 1554636757
roles:
items: { }
type: object
@ -1499,6 +1705,8 @@ tags:
name: 'Administration: Roles'
-
name: 'Administration: Settings'
-
name: 'Administration: Stations'
-
name: Miscellaneous
externalDocs: