Make the media list API endpoint return a standardized API response.
This commit is contained in:
parent
a5cf4309cf
commit
5ff1f442b3
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
Loading…
Reference in New Issue