Make the media list API endpoint return a standardized API response.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-01-02 03:05:44 -06:00
parent a5cf4309cf
commit 5ff1f442b3
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
8 changed files with 231 additions and 176 deletions

View File

@ -20,9 +20,10 @@
v-if="row.item.media_art" data-fancybox="gallery">
<img class="media_manager_album_art" :alt="langAlbumArt" :src="row.item.media_art">
</a>
<template v-if="row.item.media_is_playable">
<a class="file-icon btn-audio" href="#" :data-url="row.item.media_play_url"
@click.prevent="playAudio(row.item.media_play_url)" :title="langPlayPause">
<a class="file-icon btn-audio" href="#" :data-url="row.item.media_links_play"
@click.prevent="playAudio(row.item.media_links_play)" :title="langPlayPause">
<i class="material-icons" aria-hidden="true">play_circle_filled</i>
</a>
</template>
@ -32,25 +33,26 @@
<i class="material-icons" aria-hidden="true" v-else>note</i>
</span>
</template>
<template v-if="row.item.is_dir">
<a class="name" href="#" @click.prevent="changeDirectory(row.item.path)"
:title="row.item.name">
{{ row.item.text }}
{{ row.item.path_short }}
</a>
</template>
<template v-else-if="row.item.media_play_url">
<a class="name" :href="row.item.media_play_url" target="_blank" :title="row.item.name">
{{ row.item.media_name }}
<template v-else-if="row.item.media_is_playable">
<a class="name" :href="row.item.media_links_play" target="_blank" :title="row.item.name">
{{ row.item.text }}
</a>
</template>
<template v-else>
<a class="name" :href="row.item.download_url" target="_blank" :title="row.item.text">
{{ row.item.text }}
<a class="name" :href="row.item.links_download" target="_blank" :title="row.item.text">
{{ row.item.path_short }}
</a>
</template>
<br>
<small v-if="row.item.media_play_url">{{ row.item.text }}</small>
<small v-else>{{ row.item.media_name }}</small>
<small v-if="row.item.media_is_playable">{{ row.item.path_short }}</small>
<small v-else>{{ row.item.text }}</small>
</div>
</template>
<template v-slot:cell(media_genre)="row">
@ -66,16 +68,16 @@
</template>
</template>
<template v-slot:cell(playlists)="row">
<template v-for="(playlist, index) in row.item.media_playlists">
<template v-for="(playlist, index) in row.item.playlists">
<a class="btn-search" href="#" @click.prevent="filter('playlist:'+playlist.name)"
:title="langPlaylistSelect">{{ playlist.name }}</a>
<span v-if="index+1 < row.item.media_playlists.length">, </span>
<span v-if="index+1 < row.item.playlists.length">, </span>
</template>
</template>
<template v-slot:cell(commands)="row">
<template v-if="row.item.media_edit_url">
<template v-if="row.item.media_links_edit">
<b-button size="sm" variant="primary"
@click.prevent="edit(row.item.media_edit_url, row.item.media_art_url, row.item.media_play_url, row.item.media_waveform_url)">
@click.prevent="edit(row.item.media_links_edit, row.item.media_links_art, row.item.media_links_play, row.item.media_links_waveform)">
{{ langEditButton }}
</b-button>
</template>
@ -204,7 +206,7 @@ export default {
fields.push(
{ key: 'size', label: this.$gettext('Size'), sortable: true, selectable: true, visible: true },
{
key: 'mtime',
key: 'timestamp',
label: this.$gettext('Modified'),
sortable: true,
formatter: (value, key, item) => {

View File

@ -8,15 +8,14 @@ use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
use App\Utilities;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr;
use Jhofm\FlysystemIterator\Options\Options;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;
use const SORT_ASC;
use const SORT_DESC;
class ListAction
{
public function __invoke(
@ -41,6 +40,8 @@ class ListAction
$currentDir = $request->getParam('currentDirectory', '');
$searchPhrase = trim($request->getParam('searchPhrase', ''));
$isInternal = (bool)$request->getParam('internal', false);
$cacheKeyParts = [
'files_list',
$station->getId(),
@ -125,6 +126,56 @@ class ListAction
$mediaInDir = [];
foreach ($media_in_dir_raw as $media_row) {
$media = new Entity\Api\FileListMedia();
$media->title = (string)$media_row['title'];
$media->artist = (string)$media_row['artist'];
$media->text = $media_row['artist'] . ' - ' . $media_row['title'];
$media->album = (string)$media_row['album'];
$media->genre = (string)$media_row['genre'];
$media->is_playable = ($media_row['length'] !== 0);
$media->length = $media_row['length'];
$media->length_text = $media_row['length_text'];
$media->art = (0 === $media_row['art_updated_at'])
? (string)$stationRepo->getDefaultAlbumArtUrl($station)
: (string)$router->named(
'api:stations:media:art',
[
'station_id' => $station->getId(),
'media_id' => $media_row['unique_id'] . '-' . $media_row['art_updated_at'],
]
);
foreach ($media_row['custom_fields'] as $custom_field) {
$media->custom_fields[$custom_field['field_id']] = $custom_field['value'];
}
$media->links = [
'play' => (string)$router->named(
'api:stations:files:play',
['station_id' => $station->getId(), 'id' => $media_row['id']],
[],
true
),
'edit' => (string)$router->named(
'api:stations:file',
['station_id' => $station->getId(), 'id' => $media_row['id']]
),
'art' => (string)$router->named(
'api:stations:media:art-internal',
['station_id' => $station->getId(), 'media_id' => $media_row['id']]
),
'waveform' => (string)$router->named(
'api:stations:media:waveform',
[
'station_id' => $station->getId(),
'media_id' => $media_row['unique_id'] . '-' . $media_row['art_updated_at'],
]
),
];
$playlists = [];
foreach ($media_row['playlists'] as $playlist_row) {
if (isset($playlist_row['playlist'])) {
@ -135,54 +186,10 @@ class ListAction
}
}
$custom_fields = [];
foreach ($media_row['custom_fields'] as $custom_field) {
$custom_fields['custom_' . $custom_field['field_id']] = $custom_field['value'];
}
$artImgSrc = (0 === $media_row['art_updated_at'])
? (string)$stationRepo->getDefaultAlbumArtUrl($station)
: (string)$router->named(
'api:stations:media:art',
[
'station_id' => $station->getId(),
'media_id' => $media_row['unique_id'] . '-' . $media_row['art_updated_at'],
]
);
$mediaInDir[$media_row['path']] = [
'is_playable' => ($media_row['length'] !== 0),
'length' => $media_row['length'],
'length_text' => $media_row['length_text'],
'artist' => $media_row['artist'],
'title' => $media_row['title'],
'album' => $media_row['album'],
'genre' => $media_row['genre'],
'name' => $media_row['artist'] . ' - ' . $media_row['title'],
'art' => $artImgSrc,
'art_url' => (string)$router->named(
'api:stations:media:art-internal',
['station_id' => $station->getId(), 'media_id' => $media_row['id']]
),
'waveform_url' => (string)$router->named(
'api:stations:media:waveform',
[
'station_id' => $station->getId(),
'media_id' => $media_row['unique_id'] . '-' . $media_row['art_updated_at'],
]
),
'edit_url' => (string)$router->named(
'api:stations:file',
['station_id' => $station->getId(), 'id' => $media_row['id']]
),
'play_url' => (string)$router->named(
'api:stations:files:play',
['station_id' => $station->getId(), 'id' => $media_row['id']],
[],
true
),
'playlists' => $playlists,
] + $custom_fields;
'media' => $media,
'playlists' => $playlists,
];
}
$folders_in_dir = [];
@ -229,84 +236,95 @@ class ListAction
foreach ($files as $path) {
$meta = $fs->getMetadata($path);
if ('dir' === $meta['type']) {
$media = ['name' => __('Directory'), 'playlists' => [], 'is_playable' => false];
if (isset($folders_in_dir[$path])) {
$media['playlists'] = $folders_in_dir[$path]['playlists'];
}
} elseif (isset($mediaInDir[$path])) {
$media = $mediaInDir[$path];
} elseif (isset($unprocessableMedia[$path])) {
$media = [
'name' => __(
'File Not Processed: %s',
Utilities\Strings::truncateText($unprocessableMedia[$path])
),
];
} else {
$media = [
'name' => __('File Processing'),
];
}
$media['playlists'] ??= [];
$media['is_playable'] ??= false;
$row = new Entity\Api\FileList();
$row->path = $path;
$max_length = 60;
$shortname = $meta['basename'];
if (mb_strlen($shortname) > $max_length) {
$shortname = mb_substr($shortname, 0, $max_length - 15) . '...' . mb_substr($shortname, -12);
}
$row->path_short = $shortname;
$result_row = [
'mtime' => $meta['timestamp'],
'size' => $meta['size'],
'name' => $path,
'path' => $path,
'text' => $shortname,
'is_dir' => ('dir' === $meta['type']),
'download_url' => (string)$router->named(
$row->timestamp = $meta['timestamp'];
$row->size = $meta['size'];
$row->is_dir = ('dir' === $meta['type']);
$row->media = new Entity\Api\FileListMedia();
if (isset($mediaInDir[$path])) {
$row->media = $mediaInDir[$path]['media'];
$row->text = $row->media->text;
$row->playlists = (array)$mediaInDir[$path]['playlists'];
} elseif ('dir' === $meta['type']) {
$row->text = __('Directory');
if (isset($folders_in_dir[$path])) {
$row->playlists = (array)$folders_in_dir[$path]['playlists'];
}
} elseif (isset($unprocessableMedia[$path])) {
$row->text = __(
'File Not Processed: %s',
Utilities\Strings::truncateText($unprocessableMedia[$path])
);
} else {
$row->text = __('File Processing');
}
$row->links = [
'download' => (string)$router->named(
'api:stations:files:download',
['station_id' => $station->getId()],
['file' => $path]
),
'rename_url' => (string)$router->named(
'rename' => (string)$router->named(
'api:stations:files:rename',
['station_id' => $station->getId()],
['file' => $path]
),
];
foreach ($media as $media_key => $media_val) {
$result_row['media_' . $media_key] = $media_val;
}
$result[] = $result_row;
$result[] = $row;
}
$cache->set($cacheKey, $result, 300);
}
// Apply sorting
$sort = $request->getParam('sort');
$sortOrder = ('desc' === strtolower($request->getParam('sortOrder', 'asc')))
? SORT_DESC
: SORT_ASC;
// Apply array flattening for internal results
if ($isInternal) {
$result = array_map(
function (Entity\Api\FileList $row) {
$playlists = $row->playlists;
$row->playlists = [];
$sortBy = ['is_dir', SORT_DESC];
$row = Utilities\Arrays::flattenArray($row, '_');
$row['playlists'] = $playlists;
if (!empty($sort)) {
$sortBy[] = $sort;
$sortBy[] = $sortOrder;
} else {
$sortBy[] = 'name';
$sortBy[] = SORT_ASC;
return $row;
},
$result
);
}
$result = Utilities\Arrays::arrayOrderBy($result, $sortBy);
// Apply sorting
$resultCollection = new ArrayCollection($result);
$paginator = Paginator::fromArray($result, $request);
$sort = $request->getParam('sort');
$sortOrder = ('desc' === strtolower($request->getParam('sortOrder', 'asc')))
? Criteria::DESC
: Criteria::ASC;
$sortBy = ['is_dir' => Criteria::DESC];
if (!empty($sort)) {
$sortBy[$sort] = $sortOrder;
} else {
$sortBy['path'] = Criteria::ASC;
}
$resultCollection = $resultCollection->matching(Criteria::create()->orderBy($sortBy));
$paginator = Paginator::fromCollection($resultCollection, $request);
return $paginator->write($response);
}
}

View File

@ -49,7 +49,7 @@ class FilesController
$custom_fields = [];
foreach ($custom_fields_raw as $row) {
$custom_fields[] = [
'display_key' => 'media_custom_' . $row['id'],
'display_key' => 'media_custom_fields_' . $row['id'],
'key' => $row['short_name'],
'label' => $row['name'],
];
@ -57,15 +57,19 @@ class FilesController
$mediaStorage = $station->getMediaStorageLocation();
return $request->getView()->renderToResponse($response, 'stations/files/index', [
'show_sftp' => SftpGo::isSupportedForStation($station),
'playlists' => $playlists,
'custom_fields' => $custom_fields,
'mime_types' => MimeType::getProcessableTypes(),
'space_used' => $mediaStorage->getStorageUsed(),
'space_total' => $mediaStorage->getStorageAvailable(),
'space_percent' => $mediaStorage->getStorageUsePercentage(),
'files_count' => $files_count,
]);
return $request->getView()->renderToResponse(
$response,
'stations/files/index',
[
'show_sftp' => SftpGo::isSupportedForStation($station),
'playlists' => $playlists,
'custom_fields' => $custom_fields,
'mime_types' => MimeType::getProcessableTypes(),
'space_used' => $mediaStorage->getStorageUsed(),
'space_total' => $mediaStorage->getStorageAvailable(),
'space_percent' => $mediaStorage->getStorageUsePercentage(),
'files_count' => $files_count,
]
);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Entity\Api;
use App\Entity\Api\Traits\HasLinks;
class FileList
{
use HasLinks;
public string $path;
public string $path_short;
public string $text = '';
public int $timestamp = 0;
public ?int $size = null;
public bool $is_dir = false;
public FileListMedia $media;
public array $playlists = [];
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Entity\Api;
use App\Entity\Api\Traits\HasLinks;
class FileListMedia extends Song
{
use HasLinks;
public bool $is_playable = false;
public ?int $length = null;
public ?string $length_text = null;
}

View File

@ -17,7 +17,7 @@ class Song implements ResolvableUrlInterface
* @OA\Property(example="9f33bbc912c19603e51be8e0987d076b")
* @var string
*/
public string $id;
public string $id = '';
/**
* The song title, usually "Artist - Title"
@ -25,7 +25,7 @@ class Song implements ResolvableUrlInterface
* @OA\Property(example="Chet Porter - Aluko River")
* @var string
*/
public string $text;
public string $text = '';
/**
* The song artist.
@ -33,7 +33,7 @@ class Song implements ResolvableUrlInterface
* @OA\Property(example="Chet Porter")
* @var string
*/
public string $artist;
public string $artist = '';
/**
* The song title.
@ -41,7 +41,7 @@ class Song implements ResolvableUrlInterface
* @OA\Property(example="Aluko River")
* @var string
*/
public string $title;
public string $title = '';
/**
* The song album.

View File

@ -4,39 +4,6 @@ namespace App\Utilities;
class Arrays
{
/**
* Sort a supplied array (the first argument) by one or more indices, specified in this format:
* arrayOrderBy($data, [ 'index_name', SORT_ASC, 'index2_name', SORT_DESC ])
*
* Internally uses array_multisort().
*
* @param array $data
* @param array $args
*
* @return mixed
*/
public static function arrayOrderBy($data, array $args = [])
{
if (empty($args)) {
return $data;
}
foreach ($args as $n => $field) {
if (is_string($field)) {
$tmp = [];
foreach ($data as $key => $row) {
$tmp[$key] = $row[$field];
}
$args[$n] = $tmp;
}
}
$args[] = &$data;
array_multisort(...$args);
return array_pop($args);
}
/**
* Flatten an array from format:
* [
@ -65,7 +32,7 @@ class Arrays
if (!is_array($array)) {
if (is_object($array)) {
// Quick and dirty conversion from object to array.
$array = json_decode(json_encode($array, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
$array = self::objectToArray($array);
} else {
return $array;
}
@ -84,4 +51,19 @@ class Arrays
return $return;
}
/**
* @param object $source
*
* @return mixed[]
*/
public static function objectToArray(object $source): array
{
return json_decode(
json_encode($source, JSON_THROW_ON_ERROR),
true,
512,
JSON_THROW_ON_ERROR
);
}
}

View File

@ -15,21 +15,28 @@ class C02_Station_MediaCest extends CestAbstract
// Upload test song
$test_song_orig = $this->environment->getBaseDirectory() . '/resources/error.mp3';
$I->sendPOST('/api/station/' . $station_id . '/files', [
'path' => 'error.mp3',
'file' => base64_encode(file_get_contents($test_song_orig)),
]);
$I->sendPOST(
'/api/station/' . $station_id . '/files',
[
'path' => 'error.mp3',
'file' => base64_encode(file_get_contents($test_song_orig)),
]
);
$I->seeResponseContainsJson([
'title' => 'AzuraCast is Live!',
'artist' => 'AzuraCast.com',
]);
$I->seeResponseContainsJson(
[
'title' => 'AzuraCast is Live!',
'artist' => 'AzuraCast.com',
]
);
$I->sendGET('/api/station/' . $station_id . '/files/list');
$I->seeResponseContainsJson([
'media_name' => 'AzuraCast.com - AzuraCast is Live!',
]);
$I->seeResponseContainsJson(
[
'text' => 'AzuraCast.com - AzuraCast is Live!',
]
);
$I->amOnPage('/station/' . $station_id . '/files');