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:
parent
5102ce89c4
commit
769de19d00
|
@ -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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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).
|
||||
*
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = [];
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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)"
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue