Big Visual Waveform Editor Overhaul

- Refactor Flysystem to use constants instead of magic strings for URL prefixes
 - Add audiowaveform installation to both Docker and Ansible installations
 - Use audiowaveform to generate waveforms saved to disk for every track
 - Load these waveforms when visiting the visual waveform editor page instead of using the browser to calculate them
 - Add volume control and use app-wide storage of default volume
 - Remove "beta" tag from waveform editor header
This commit is contained in:
Buster "Silver Eagle" Neece 2020-05-11 19:32:41 -05:00
parent 5102ce89c4
commit 769de19d00
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
36 changed files with 355 additions and 87 deletions

View File

@ -160,6 +160,10 @@ return function (App $app) {
->setName('api:listeners:index')
->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
$group->get('/waveform/{media_id:[a-zA-Z0-9\-]+}.json',
Controller\Api\Stations\Waveform\GetWaveformAction::class)
->setName('api:stations:media:waveform');
$group->get('/art/{media_id:[a-zA-Z0-9\-]+}.jpg', Controller\Api\Stations\Art\GetArtAction::class)
->setName('api:stations:media:art');

View File

@ -68,7 +68,7 @@
<template v-slot:cell(commands)="row">
<template v-if="row.item.media_can_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)">
@click.prevent="edit(row.item.media_edit_url, row.item.media_art_url, row.item.media_play_url, row.item.media_waveform_url)">
{{ langEditButton }}
</b-button>
</template>
@ -258,8 +258,8 @@
rename (path) {
this.$refs.renameModal.open(path);
},
edit (recordUrl, albumArtUrl, audioUrl) {
this.$refs.editModal.open(recordUrl, albumArtUrl, audioUrl);
edit (recordUrl, albumArtUrl, audioUrl, waveformUrl) {
this.$refs.editModal.open(recordUrl, albumArtUrl, audioUrl, waveformUrl);
},
requestConfig (config) {
config.params.file = this.currentDirectory;

View File

@ -1,5 +1,5 @@
<template>
<b-form-group>
<b-form-group class="waveform-controls">
<b-row>
<b-form-group class="col-md-12">
<div class="waveform__container">
@ -7,13 +7,40 @@
<div id="waveform"></div>
</div>
</b-form-group>
<b-form-group class="col-md-12" label-for="waveform-zoom">
<template v-slot:label>
<translate>Waveform Zoom</translate>
</template>
<b-form-input id="waveform-zoom" v-model="zoom" type="range" min="0" max="256"></b-form-input>
</b-form-group>
</b-row>
<b-row class="mt-3">
<b-col md="8">
<div class="d-flex">
<div class="flex-shrink-0">
<label for="waveform-zoom">
<translate>Waveform Zoom</translate>
</label>
</div>
<div class="flex-fill mx-3">
<b-form-input id="waveform-zoom" v-model="zoom" type="range" min="0" max="256" class="w-100"></b-form-input>
</div>
</div>
</b-col>
<b-col md="4">
<div class="inline-volume-controls d-flex align-items-center">
<div class="flex-shrink-0">
<a class="btn btn-sm btn-outline-light py-0 px-3" href="#" @click.prevent="volume = 0">
<i class="material-icons" aria-hidden="true">volume_mute</i>
<span class="sr-only" v-translate>Mute</span>
</a>
</div>
<div class="flex-fill mx-1">
<input type="range" :title="langVolume" class="player-volume-range custom-range w-100" min="0" max="100"
step="1" v-model="volume">
</div>
<div class="flex-shrink-0">
<a class="btn btn-sm btn-outline-light py-0 px-3" href="#" @click.prevent="volume = 100">
<i class="material-icons" aria-hidden="true">volume_up</i>
<span class="sr-only" v-translate>Full Volume</span>
</a>
</div>
</div>
</b-col>
</b-row>
</b-form-group>
</template>
@ -22,16 +49,21 @@
import WaveSurfer from 'wavesurfer.js';
import timeline from 'wavesurfer.js/dist/plugin/wavesurfer.timeline.js';
import regions from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
import axios from 'axios';
import getLogarithmicVolume from '../inc/logarithmic_volume';
import store from 'store';
export default {
name: 'Waveform',
props: {
audioUrl: String
audioUrl: String,
waveformUrl: String
},
data () {
return {
wavesurfer: null,
zoom: 0
zoom: 0,
volume: 0
};
},
mounted () {
@ -58,7 +90,22 @@
this.$emit('ready');
});
this.wavesurfer.load(this.audioUrl);
axios.get(this.waveformUrl).then((resp) => {
this.wavesurfer.load(this.audioUrl, resp.data.data);
}).catch((err) => {
console.error(err);
this.wavesurfer.load(this.audioUrl);
});
// Check webstorage for existing volume preference.
if (store.enabled && store.get('player_volume') !== undefined) {
this.volume = store.get('player_volume', 55);
}
},
computed: {
langVolume () {
return this.$gettext('Volume');
}
},
methods: {
play () {
@ -103,6 +150,13 @@
watch: {
zoom: function (val) {
this.wavesurfer.zoom(Number(val));
},
volume: function (volume) {
this.wavesurfer.setVolume(getLogarithmicVolume(volume));
if (store.enabled) {
store.set('player_volume', volume);
}
}
},
beforeDestroy () {

View File

@ -7,7 +7,7 @@
<media-form-basic-info :form="$v.form"></media-form-basic-info>
<media-form-album-art :album-art-url="albumArtUrl"></media-form-album-art>
<media-form-custom-fields :form="form" :custom-fields="customFields"></media-form-custom-fields>
<media-form-waveform-editor :form="form" :audio-url="audioUrl"></media-form-waveform-editor>
<media-form-waveform-editor :form="form" :audio-url="audioUrl" :waveform-url="waveformUrl"></media-form-waveform-editor>
<media-form-advanced-settings :form="$v.form" :song-length="songLength"></media-form-advanced-settings>
</b-tabs>
<invisible-submit-button/>
@ -53,6 +53,7 @@
loading: true,
recordUrl: null,
albumArtUrl: null,
waveformUrl: null,
audioUrl: null,
songLength: null,
form: this.getBlankForm()
@ -106,11 +107,12 @@
custom_fields: customFields
};
},
open (recordUrl, albumArtUrl, audioUrl) {
open (recordUrl, albumArtUrl, audioUrl, waveformUrl) {
this.loading = true;
this.$refs.modal.show();
this.albumArtUrl = albumArtUrl;
this.waveformUrl = waveformUrl;
this.recordUrl = recordUrl;
this.audioUrl = audioUrl;

View File

@ -3,12 +3,15 @@
<b-form-group>
<b-row>
<div class="col-md-12">
<h4>Visual Cue Editor <sup>BETA</sup></h4>
<p>You are able to set cue points and fades using the visual editor. The timestamps will be saved to
the corresponding fields in the advanced playback settings.</p>
<h4>
<translate>Visual Cue Editor</translate>
</h4>
<p>
<translate>Set cue and fade points using the visual editor. The timestamps will be saved to the corresponding fields in the advanced playback settings.</translate>
</p>
</div>
<b-form-group class="col-md-12">
<waveform ref="waveform" :audio-url="audioUrl" @ready="updateRegions"></waveform>
<waveform ref="waveform" :audio-url="audioUrl" :waveform-url="waveformUrl" @ready="updateRegions"></waveform>
</b-form-group>
<b-form-group class="col-md-12">
<b-button-group>
@ -58,7 +61,8 @@
components: { Waveform },
props: {
form: Object,
audioUrl: String
audioUrl: String,
waveformUrl: String
},
computed: {
langTitle () {

View File

@ -1,11 +1,11 @@
<?php
namespace App\Console\Command\Internal;
use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Message;
use App\MessageQueue;
use App\Radio\Filesystem;
use App\Console\Command\CommandAbstract;
use Doctrine\ORM\EntityManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

View File

@ -2,9 +2,9 @@
namespace App\Controller\Api\Stations\Art;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use Psr\Http\Message\ResponseInterface;
class DeleteArtAction

View File

@ -4,9 +4,9 @@ namespace App\Controller\Api\Stations\Art;
use App\Customization;
use App\Entity\Repository\StationMediaRepository;
use App\Entity\StationMedia;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use OpenApi\Annotations as OA;
use Psr\Http\Message\ResponseInterface;
@ -56,7 +56,8 @@ class GetArtAction
$media_id = explode('-', $media_id)[0];
if (StationMedia::UNIQUE_ID_LENGTH === strlen($media_id)) {
$mediaPath = 'albumart://' . $media_id . '.jpg';
$response = $response->withCacheLifetime(Response::CACHE_ONE_YEAR);
$mediaPath = Filesystem::PREFIX_ALBUM_ART . '://' . $media_id . '.jpg';
} else {
$media = $mediaRepo->find($media_id, $station);
if ($media instanceof StationMedia) {
@ -67,8 +68,7 @@ class GetArtAction
}
if ($fs->has($mediaPath)) {
return $response->withCacheLifetime(Response::CACHE_ONE_YEAR)
->withFlysystemFile($fs, $mediaPath, null, 'inline');
return $response->withFlysystemFile($fs, $mediaPath, null, 'inline');
}
return $defaultArtRedirect;

View File

@ -2,9 +2,9 @@
namespace App\Controller\Api\Stations\Art;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;

View File

@ -2,13 +2,13 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Flysystem\StationFilesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Message\WritePlaylistFileMessage;
use App\MessageQueue;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Filesystem;
use Doctrine\ORM\EntityManager;
use Exception;
use Psr\Http\Message\ResponseInterface;
@ -33,7 +33,7 @@ class BatchAction
$files = [];
foreach ($files_raw as $file) {
$file_path = 'media://' . $file;
$file_path = Filesystem::PREFIX_MEDIA . '://' . $file;
if ($fs->has($file_path)) {
$files[] = $file_path;
@ -223,7 +223,7 @@ class BatchAction
$files_found = count($music_files);
$directory_path = $request->getParam('directory');
$directory_path_full = 'media://' . $directory_path;
$directory_path_full = Filesystem::PREFIX_MEDIA . '://' . $directory_path;
try {
// Verify that you're moving to a directory (if it's not the root dir).

View File

@ -1,9 +1,9 @@
<?php
namespace App\Controller\Api\Stations\Files;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use Psr\Http\Message\ResponseInterface;
class DownloadAction

View File

@ -3,9 +3,9 @@ namespace App\Controller\Api\Stations\Files;
use App\Customization;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use App\Utilities;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
@ -108,14 +108,28 @@ class ListAction
'album' => $media_row['album'],
'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']]),
'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'],
]
),
'can_edit' => true,
'edit_url' => (string)$router->named('api:stations:file',
['station_id' => $station->getId(), 'id' => $media_row['id']]),
'play_url' => (string)$router->named('api:stations:files:download',
'edit_url' => (string)$router->named(
'api:stations:file',
['station_id' => $station->getId(), 'id' => $media_row['id']]
),
'play_url' => (string)$router->named(
'api:stations:files:download',
['station_id' => $station->getId()],
['file' => $media_row['path']], true),
['file' => $media_row['path']],
true
),
'playlists' => $playlists,
] + $custom_fields;
}
@ -134,7 +148,7 @@ class ListAction
$files = [];
if (!empty($search_phrase)) {
foreach ($media_in_dir as $short_path => $media_row) {
$files[] = 'media://' . $short_path;
$files[] = Filesystem::PREFIX_MEDIA . '://' . $short_path;
}
} else {
$files_raw = $fs->listContents($file_path);
@ -144,7 +158,7 @@ class ListAction
}
foreach ($files as $i) {
$short = str_replace('media://', '', $i);
$short = str_replace(Filesystem::PREFIX_MEDIA . '://', '', $i);
$meta = $fs->getMetadata($i);
if ('dir' === $meta['type']) {

View File

@ -2,9 +2,9 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use Psr\Http\Message\ResponseInterface;
class ListDirectoriesAction

View File

@ -2,9 +2,9 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use Psr\Http\Message\ResponseInterface;
class MakeDirectoryAction

View File

@ -2,9 +2,9 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
@ -39,7 +39,7 @@ class RenameAction
$fs = $filesystem->getForStation($station);
$originalPathFull = $request->getAttribute('file_path');
$newPathFull = 'media://' . $newPath;
$newPathFull = Filesystem::PREFIX_MEDIA . '://' . $newPath;
// MountManager::rename's second argument is NOT the full URI >:(
$fs->rename($originalPathFull, $newPath);

View File

@ -3,13 +3,13 @@ namespace App\Controller\Api\Stations;
use App\Entity;
use App\Exception\ValidationException;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Message\WritePlaylistFileMessage;
use App\MessageQueue;
use App\Radio\Adapters;
use App\Radio\Backend\Liquidsoap;
use App\Radio\Filesystem;
use Doctrine\ORM\EntityManager;
use InvalidArgumentException;
use OpenApi\Annotations as OA;
@ -116,7 +116,7 @@ class FilesController extends AbstractStationApiCrudController
$temp_path = $station->getRadioTempDir() . '/' . $api_record->getSanitizedFilename();
file_put_contents($temp_path, $api_record->getFileContents());
$sanitized_path = 'media://' . $api_record->getSanitizedPath();
$sanitized_path = Filesystem::PREFIX_MEDIA . '://' . $api_record->getSanitizedPath();
// Process temp path as regular media record.
$record = $this->media_repo->uploadFile($station, $temp_path, $sanitized_path);
@ -204,7 +204,7 @@ class FilesController extends AbstractStationApiCrudController
return $record;
}
}
return null;
}
@ -236,7 +236,7 @@ class FilesController extends AbstractStationApiCrudController
'path' => function ($new_value, $record) {
// Detect and handle a rename.
if (($record instanceof Entity\StationMedia) && $new_value !== $record->getPath()) {
$path_full = 'media://' . $new_value;
$path_full = Filesystem::PREFIX_MEDIA . '://' . $new_value;
$fs = $this->filesystem->getForStation($record->getStation());
$fs->rename($record->getPathUri(), $path_full);

View File

@ -5,9 +5,9 @@ use App\Controller\Api\AbstractApiCrudController;
use App\Doctrine\Paginator;
use App\Entity;
use App\File;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use App\Utilities;
use Psr\Http\Message\ResponseInterface;
@ -61,7 +61,7 @@ class BroadcastsController extends AbstractApiCrudController
unset($return['recordingPath']);
$recordingPath = $row->getRecordingPath();
$recordingUri = 'recordings://' . $recordingPath;
$recordingUri = Filesystem::PREFIX_RECORDINGS . '://' . $recordingPath;
if ($fs->has($recordingUri)) {
$recordingMeta = $fs->getMetadata($recordingUri);
@ -134,7 +134,7 @@ class BroadcastsController extends AbstractApiCrudController
$fs = $filesystem->getForStation($station);
$filename = basename($recordingPath);
$recordingPath = 'recordings://' . $recordingPath;
$recordingPath = Filesystem::PREFIX_RECORDINGS . '://' . $recordingPath;
return $response->withFlysystemFile(
$fs,
@ -163,7 +163,7 @@ class BroadcastsController extends AbstractApiCrudController
if (!empty($recordingPath)) {
$fs = $filesystem->getForStation($station);
$recordingPath = 'recordings://' . $recordingPath;
$recordingPath = Filesystem::PREFIX_RECORDINGS . '://' . $recordingPath;
$fs->delete($recordingPath);

View File

@ -0,0 +1,51 @@
<?php
namespace App\Controller\Api\Stations\Waveform;
use App\Customization;
use App\Entity\Api\Error;
use App\Entity\Repository\StationMediaRepository;
use App\Entity\StationMedia;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
class GetWaveformAction
{
public function __invoke(
ServerRequest $request,
Response $response,
Customization $customization,
Filesystem $filesystem,
StationMediaRepository $mediaRepo,
$media_id
): ResponseInterface {
$response = $response->withCacheLifetime(Response::CACHE_ONE_YEAR);
$station = $request->getStation();
$fs = $filesystem->getForStation($station);
// If a timestamp delimiter is added, strip it automatically.
$media_id = explode('-', $media_id)[0];
if (StationMedia::UNIQUE_ID_LENGTH === strlen($media_id)) {
$waveformPath = Filesystem::PREFIX_WAVEFORMS . '://' . $media_id . '.json';
if ($fs->has($waveformPath)) {
return $response->withFlysystemFile($fs, $waveformPath, null, 'inline');
}
}
$media = $mediaRepo->findByUniqueId($media_id, $station);
if (!($media instanceof StationMedia)) {
return $response->withStatus(500)->withJson(new Error(500, 'Media not found.'));
}
$waveformPath = $media->getWaveformPath();
if (!$fs->has($waveformPath)) {
$mediaRepo->updateWaveform($media);
}
return $response->withFlysystemFile($fs, $waveformPath, null, 'inline');
}
}

View File

@ -2,9 +2,9 @@
namespace App\Controller\Stations\Reports;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Radio\Filesystem;
use App\Session\Flash;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;

View File

@ -4,7 +4,8 @@ namespace App\Entity\Repository;
use App\Doctrine\Repository;
use App\Entity;
use App\Exception\MediaProcessingException;
use App\Radio\Filesystem;
use App\Flysystem\Filesystem;
use App\Service\AudioWaveform;
use App\Settings;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMException;
@ -49,6 +50,13 @@ class StationMediaRepository extends Repository
*/
public function find($id, Entity\Station $station): ?Entity\StationMedia
{
if (Entity\StationMedia::UNIQUE_ID_LENGTH === strlen($id)) {
$media = $this->findByUniqueId($id, $station);
if ($media instanceof Entity\StationMedia) {
return $media;
}
}
return $this->repository->findOneBy([
'station' => $station,
'id' => $id,
@ -391,6 +399,7 @@ class StationMediaRepository extends Repository
}
$this->loadFromFile($media, $tmp_path);
$this->writeWaveform($media, $tmp_path);
if (null !== $tmp_uri) {
$fs->delete($tmp_uri);
@ -478,6 +487,39 @@ class StationMediaRepository extends Repository
return false;
}
public function updateWaveform(Entity\StationMedia $media): void
{
$fs = $this->filesystem->getForStation($media->getStation());
$mediaUri = $media->getPathUri();
$tmpUri = null;
try {
$tmpPath = $fs->getFullPath($mediaUri);
} catch (InvalidArgumentException $e) {
$tmpUri = $fs->copyToTemp($mediaUri);
$tmpPath = $fs->getFullPath($tmpUri);
}
$this->writeWaveform($media, $tmpPath);
if (null !== $tmpUri) {
$fs->delete($tmpUri);
}
}
public function writeWaveform(Entity\StationMedia $media, string $path): bool
{
$waveform = AudioWaveform::getWaveformFor($path);
$waveformPath = $media->getWaveformPath();
$fs = $this->filesystem->getForStation($media->getStation());
return $fs->put(
$waveformPath,
json_encode($waveform, \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR)
);
}
/**
* Read the contents of the album art from storage (if it exists).
*

View File

@ -652,6 +652,11 @@ class Station
return $this->radio_base_dir . '/album_art';
}
public function getRadioWaveformsDir(): string
{
return $this->radio_base_dir . '/waveforms';
}
public function getRadioTempDir(): string
{
return $this->radio_base_dir . '/temp';
@ -704,6 +709,20 @@ class Station
return $this->radio_base_dir . '/config';
}
public function getAllStationDirectories(): array
{
return [
$this->getRadioBaseDir(),
$this->getRadioMediaDir(),
$this->getRadioAlbumArtDir(),
$this->getRadioWaveformsDir(),
$this->getRadioPlaylistsDir(),
$this->getRadioConfigDir(),
$this->getRadioTempDir(),
$this->getRadioRecordingsDir(),
];
}
public function getNowplaying(): ?Api\NowPlaying
{
if ($this->nowplaying instanceof Api\NowPlaying) {

View File

@ -3,6 +3,7 @@ namespace App\Entity;
use App\Annotations\AuditLog;
use App\ApiUtilities;
use App\Flysystem\Filesystem;
use App\Normalizer\Annotation\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@ -313,7 +314,12 @@ class StationMedia
*/
public function getArtPath(): string
{
return 'albumart://' . $this->unique_id . '.jpg';
return Filesystem::PREFIX_ALBUM_ART . '://' . $this->unique_id . '.jpg';
}
public function getWaveformPath(): string
{
return Filesystem::PREFIX_WAVEFORMS . '://' . $this->unique_id . '.json';
}
public function getIsrc(): ?string
@ -370,7 +376,7 @@ class StationMedia
*/
public function getPathUri(): string
{
return 'media://' . $this->path;
return Filesystem::PREFIX_MEDIA . '://' . $this->path;
}
public function getMtime(): ?int

View File

@ -1,8 +1,7 @@
<?php
namespace App\Radio;
namespace App\Flysystem;
use App\Entity;
use App\Flysystem\StationFilesystem;
use Cache\Prefixed\PrefixedCachePool;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Cached\CachedAdapter;
@ -15,9 +14,17 @@ use Psr\Cache\CacheItemPoolInterface;
*/
class Filesystem
{
public const PREFIX_MEDIA = 'media';
public const PREFIX_ALBUM_ART = 'albumart';
public const PREFIX_WAVEFORMS = 'waveforms';
public const PREFIX_PLAYLISTS = 'playlists';
public const PREFIX_CONFIG = 'config';
public const PREFIX_RECORDINGS = 'recordings';
public const PREFIX_TEMP = 'temp';
protected CacheItemPoolInterface $cachePool;
/** @var StationFilesystem[] All current interfaces managed by this */
/** @var StationFilesystem[] All current interfaces managed by this instance. */
protected array $interfaces = [];
public function __construct(CacheItemPoolInterface $cachePool)
@ -30,12 +37,13 @@ class Filesystem
$station_id = $station->getId();
if (!isset($this->interfaces[$station_id])) {
$aliases = [
'media' => $station->getRadioMediaDir(),
'albumart' => $station->getRadioAlbumArtDir(),
'playlists' => $station->getRadioPlaylistsDir(),
'config' => $station->getRadioConfigDir(),
'recordings' => $station->getRadioRecordingsDir(),
'temp' => $station->getRadioTempDir(),
self::PREFIX_MEDIA => $station->getRadioMediaDir(),
self::PREFIX_ALBUM_ART => $station->getRadioAlbumArtDir(),
self::PREFIX_WAVEFORMS => $station->getRadioWaveformsDir(),
self::PREFIX_PLAYLISTS => $station->getRadioPlaylistsDir(),
self::PREFIX_CONFIG => $station->getRadioConfigDir(),
self::PREFIX_RECORDINGS => $station->getRadioRecordingsDir(),
self::PREFIX_TEMP => $station->getRadioTempDir(),
];
$filesystems = [];

View File

@ -19,7 +19,7 @@ class StationFilesystem extends FilesystemGroup
if (null === $to) {
$random_prefix = substr(md5(random_bytes(8)), 0, 5);
$to = 'temp://' . $random_prefix . '_' . $path_from;
$to = Filesystem::PREFIX_TEMP . '://' . $random_prefix . '_' . $path_from;
}
if ($this->has($to)) {

View File

@ -1,8 +1,9 @@
<?php
namespace App\Middleware\Module;
use App\Http\ServerRequest;
use App\Exception;
use App\Flysystem\Filesystem;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -21,7 +22,7 @@ class StationFiles
$params = $request->getParams();
$file = ltrim($params['file'] ?? '', '/');
$file_path = 'media://' . $file;
$file_path = Filesystem::PREFIX_MEDIA . '://' . $file;
$request = $request->withAttribute('file', $file)
->withAttribute('file_path', $file_path);

View File

@ -5,6 +5,7 @@ use App\Entity;
use App\Event\Radio\AnnotateNextSong;
use App\Event\Radio\BuildQueue;
use App\EventDispatcher;
use App\Flysystem\Filesystem;
use App\Lock\LockManager;
use Cake\Chronos\Chronos;
use DateTimeZone;

View File

@ -78,15 +78,7 @@ class Configuration
}
// Ensure all directories exist.
$radio_dirs = [
$station->getRadioBaseDir(),
$station->getRadioMediaDir(),
$station->getRadioAlbumArtDir(),
$station->getRadioPlaylistsDir(),
$station->getRadioConfigDir(),
$station->getRadioTempDir(),
$station->getRadioRecordingsDir(),
];
$radio_dirs = $station->getAllStationDirectories();
foreach ($radio_dirs as $radio_dir) {
if (!file_exists($radio_dir) && !mkdir($radio_dir, 0777) && !is_dir($radio_dir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $radio_dir));

View File

@ -0,0 +1,50 @@
<?php
namespace App\Service;
use Symfony\Component\Process\Process;
class AudioWaveform
{
public static function getWaveformFor(string $path): array
{
if (!file_exists($path)) {
throw new \InvalidArgumentException(sprintf('File at path "%s" not found.', $path));
}
$jsonOutPath = tempnam(sys_get_temp_dir(), 'awf') . '.json';
$process = new Process([
'audiowaveform',
'-i',
$path,
'-o',
$jsonOutPath,
'--pixels-per-second',
'20',
'--bits',
'8',
]);
$process->setTimeout(60);
$process->setIdleTimeout(3600);
$process->mustRun();
if (!file_exists($jsonOutPath)) {
throw new \RuntimeException('Audio waveform JSON was not generated.');
}
$inputRaw = file_get_contents($jsonOutPath);
$input = json_decode($inputRaw, true, 512, JSON_THROW_ON_ERROR);
// Limit all input to a range from 0 to 1.
$data = $input['data'];
$maxVal = (float)max($data);
$newData = [];
foreach ($data as $row) {
$newData[] = round($row / $maxVal, 2);
}
$input['data'] = $newData;
return $input;
}
}

View File

@ -2,8 +2,8 @@
namespace App\Sync\Task;
use App\Entity;
use App\Flysystem\Filesystem;
use App\MessageQueue;
use App\Radio\Filesystem;
use Doctrine\ORM\EntityManager;
use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Psr\Log\LoggerInterface;
@ -75,7 +75,7 @@ class FolderPlaylists extends AbstractTask
/** @var Entity\StationPlaylistFolder $row */
$path = $row->getPath();
if ($fs->has('media://' . $path)) {
if ($fs->has(Filesystem::PREFIX_MEDIA . '://' . $path)) {
$folders[$path][] = $row->getPlaylist();
} else {
$this->em->remove($row);

View File

@ -2,9 +2,9 @@
namespace App\Sync\Task;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Message;
use App\MessageQueue;
use App\Radio\Filesystem;
use App\Radio\Quota;
use Bernard\Envelope;
use Brick\Math\BigInteger;
@ -109,7 +109,7 @@ class Media extends AbstractTask
$music_files = [];
$total_size = BigInteger::zero();
$fsIterator = $fs->createIterator('media://', [
$fsIterator = $fs->createIterator(Filesystem::PREFIX_MEDIA . '://', [
'filter' => FilterFactory::isFile(),
]);

View File

@ -35,7 +35,7 @@ else
fi
APP_ENV="${APP_ENV:-production}"
UPDATE_REVISION="${UPDATE_REVISION:-52}"
UPDATE_REVISION="${UPDATE_REVISION:-53}"
echo "Updating AzuraCast (Environment: $APP_ENV, Update revision: $UPDATE_REVISION)"

View File

@ -113,3 +113,14 @@
force: yes
when:
- ansible_distribution_release == 'focal'
- name: Add Audiowaveform PPA
apt_repository:
repo: "ppa:chris-needham/ppa"
update_cache: yes
- name: Install Audiowaveform
apt:
name: audiowaveform
state: latest
install_recommends: no

View File

@ -13,7 +13,7 @@
roles:
- init
- azuracast-config
- { role: azuracast-radio, when: update_revision|int < 52 }
- { role: azuracast-radio, when: update_revision|int < 53 }
- { role: supervisord, when: update_revision|int < 13 }
- { role: mariadb, when: update_revision|int < 15 }
- { role: nginx, when: update_revision|int < 49 }

View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
source /bd_build/buildconfig
set -x
add-apt-repository -y ppa:chris-needham/ppa
apt-get update
$minimal_apt_get_install audiowaveform

View File

@ -50,7 +50,7 @@
"dist/light.css": "dist/light-009b7a2804.css",
"dist/material.js": "dist/material-df68dbf23f.js",
"dist/radio_player.js": "dist/radio_player-f88abea4e8.js",
"dist/station_media.js": "dist/station_media-1ac4c7a323.js",
"dist/station_media.js": "dist/station_media-badde0c9c8.js",
"dist/station_playlists.js": "dist/station_playlists-c1c36c2497.js",
"dist/station_streamers.js": "dist/station_streamers-2e160bdd93.js",
"dist/vue_gettext.js": "dist/vue_gettext-2d25ba9686.js",

File diff suppressed because one or more lines are too long