AzuraCast/src/Controller/Api/Stations/Files/BatchAction.php

376 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations\Files;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Media\BatchUtilities;
use App\Message;
use App\MessageQueue\QueueManager;
use App\Radio\Backend\Liquidsoap;
use App\Utilities\File;
use Azura\Files\ExtendedFilesystemInterface;
use Exception;
use InvalidArgumentException;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Messenger\MessageBus;
use Throwable;
class BatchAction
{
public function __construct(
protected BatchUtilities $batchUtilities,
protected ReloadableEntityManagerInterface $em,
protected MessageBus $messageBus,
protected QueueManager $queueManager,
protected Entity\Repository\StationPlaylistMediaRepository $playlistMediaRepo,
protected Entity\Repository\StationPlaylistFolderRepository $playlistFolderRepo,
) {
}
public function __invoke(
ServerRequest $request,
Response $response
): ResponseInterface {
$station = $request->getStation();
$storageLocation = $station->getMediaStorageLocation();
$fsMedia = (new StationFilesystems($station))->getMediaFilesystem();
$result = match ($request->getParam('do')) {
'delete' => $this->doDelete($request, $station, $storageLocation, $fsMedia),
'playlist' => $this->doPlaylist($request, $station, $storageLocation, $fsMedia),
'move' => $this->doMove($request, $station, $storageLocation, $fsMedia),
'queue' => $this->doQueue($request, $station, $storageLocation, $fsMedia),
'reprocess' => $this->doReprocess($request, $station, $storageLocation, $fsMedia),
default => throw new InvalidArgumentException('Invalid batch action specified.')
};
if ($this->em->isOpen()) {
$this->em->clear(Entity\StationMedia::class);
$this->em->clear(Entity\StationPlaylist::class);
$this->em->clear(Entity\StationPlaylistMedia::class);
$this->em->clear(Entity\UnprocessableMedia::class);
$this->em->clear(Entity\StationRequest::class);
}
return $response->withJson($result);
}
public function doDelete(
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
ExtendedFilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
foreach ($result->files as $file) {
try {
$fs->delete($file);
} catch (Throwable $e) {
$result->errors[] = $file . ': ' . $e->getMessage();
}
}
foreach ($result->directories as $dir) {
try {
$fs->deleteDirectory($dir);
} catch (Throwable $e) {
$result->errors[] = $dir . ': ' . $e->getMessage();
}
}
$affectedPlaylists = $this->batchUtilities->handleDelete(
$result->files,
$result->directories,
$storageLocation,
$fs
);
$this->writePlaylistChanges($request, $affectedPlaylists);
return $result;
}
public function doPlaylist(
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
ExtendedFilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
/** @var Entity\StationPlaylist[] $playlists */
$playlists = [];
$playlistWeights = [];
$affectedPlaylists = [];
foreach ($request->getParam('playlists') as $playlistId) {
if ('new' === $playlistId) {
$playlist = new Entity\StationPlaylist($station);
$playlist->setName($request->getParam('new_playlist_name'));
$this->em->persist($playlist);
$this->em->flush();
$result->responseRecord = [
'id' => $playlist->getId(),
'name' => $playlist->getName(),
];
$affectedPlaylists[$playlist->getId()] = $playlist->getId();
$playlists[$playlist->getId()] = $playlist;
$playlistWeights[$playlist->getId()] = 0;
} else {
$playlist = $this->em->getRepository(Entity\StationPlaylist::class)->findOneBy(
[
'station_id' => $station->getId(),
'id' => (int)$playlistId,
]
);
if ($playlist instanceof Entity\StationPlaylist) {
$affectedPlaylists[$playlist->getId()] = $playlist->getId();
$playlists[$playlist->getId()] = $playlist;
$playlistWeights[$playlist->getId()] = $this->playlistMediaRepo->getHighestSongWeight($playlist);
}
}
}
/*
* NOTE: This iteration clears the entity manager.
*/
foreach ($this->batchUtilities->iterateMedia($storageLocation, $result->files) as $media) {
try {
$mediaPlaylists = $this->playlistMediaRepo->clearPlaylistsFromMedia($media, $station);
foreach ($mediaPlaylists as $playlistId => $playlistRecord) {
if (!isset($affectedPlaylists[$playlistId])) {
$affectedPlaylists[$playlistId] = $playlistRecord;
}
}
$this->em->flush();
foreach ($playlists as $playlistRecord) {
/** @var Entity\StationPlaylist $playlist */
$playlist = $this->em->refetchAsReference($playlistRecord);
$playlistWeights[$playlist->getId()]++;
$weight = $playlistWeights[$playlist->getId()];
$this->playlistMediaRepo->addMediaToPlaylist($media, $playlist, $weight);
}
} catch (Exception $e) {
$result->errors[] = $media->getPath() . ': ' . $e->getMessage();
throw $e;
}
}
/** @var Entity\Station $station */
$station = $this->em->refetch($station);
foreach ($result->directories as $dir) {
try {
$this->playlistFolderRepo->setPlaylistsForFolder($station, $playlists, $dir);
} catch (Exception $e) {
$result->errors[] = $dir . ': ' . $e->getMessage();
}
}
$this->em->flush();
$this->writePlaylistChanges($request, $affectedPlaylists);
return $result;
}
public function doMove(
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
ExtendedFilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs);
$from = $request->getParam('currentDirectory', '');
$to = $request->getParam('directory', '');
$toMove = [
$this->batchUtilities->iterateMedia($storageLocation, $result->files),
$this->batchUtilities->iterateUnprocessableMedia($storageLocation, $result->files),
];
foreach ($toMove as $iterator) {
foreach ($iterator as $record) {
/** @var Entity\Interfaces\PathAwareInterface $record */
$oldPath = $record->getPath();
$newPath = File::renameDirectoryInPath($oldPath, $from, $to);
try {
$fs->move($oldPath, $newPath);
$record->setPath($newPath);
$this->em->persist($record);
} catch (Throwable $e) {
$result->errors[] = $oldPath . ': ' . $e->getMessage();
}
}
}
foreach ($result->directories as $dirPath) {
$newDirPath = File::renameDirectoryInPath($dirPath, $from, $to);
$fs->move($dirPath, $newDirPath);
$toMove = [
$this->batchUtilities->iterateMediaInDirectory($storageLocation, $dirPath),
$this->batchUtilities->iterateUnprocessableMediaInDirectory($storageLocation, $dirPath),
$this->batchUtilities->iteratePlaylistFoldersInDirectory($storageLocation, $dirPath),
];
foreach ($toMove as $iterator) {
foreach ($iterator as $record) {
/** @var Entity\Interfaces\PathAwareInterface $record */
try {
$record->setPath(
File::renameDirectoryInPath($record->getPath(), $from, $to)
);
$this->em->persist($record);
} catch (Throwable $e) {
$result->errors[] = $record->getPath() . ': ' . $e->getMessage();
}
}
}
}
return $result;
}
public function doQueue(
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
ExtendedFilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
foreach ($this->batchUtilities->iterateMedia($storageLocation, $result->files) as $media) {
try {
/** @var Entity\Station $stationRef */
$stationRef = $this->em->getReference(Entity\Station::class, $station->getId());
$newQueue = Entity\StationQueue::fromMedia($stationRef, $media);
$newQueue->setTimestampCued(time());
$this->em->persist($newQueue);
} catch (Throwable $e) {
$result->errors[] = $media->getPath() . ': ' . $e->getMessage();
}
}
return $result;
}
public function doReprocess(
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
ExtendedFilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
// Get existing queue items
$queuedMediaUpdates = [];
$queuedNewFiles = [];
foreach ($this->queueManager->getMessagesInTransport(QueueManager::QUEUE_MEDIA) as $message) {
if ($message instanceof Message\ReprocessMediaMessage) {
$queuedMediaUpdates[$message->media_id] = true;
} elseif (
$message instanceof Message\AddNewMediaMessage
&& $message->storage_location_id === $storageLocation->getId()
) {
$queuedNewFiles[$message->path] = true;
}
}
foreach ($this->batchUtilities->iterateMedia($storageLocation, $result->files) as $media) {
$mediaId = (int)$media->getId();
if (!isset($queuedMediaUpdates[$mediaId])) {
$message = new Message\ReprocessMediaMessage();
$message->media_id = $mediaId;
$message->force = true;
$this->messageBus->dispatch($message);
}
}
foreach ($this->batchUtilities->iterateUnprocessableMedia($storageLocation, $result->files) as $unprocessable) {
$path = $unprocessable->getPath();
if (!isset($queuedNewFiles[$path])) {
$message = new Message\AddNewMediaMessage();
$message->storage_location_id = (int)$storageLocation->getId();
$message->path = $unprocessable->getPath();
$this->messageBus->dispatch($message);
}
}
return $result;
}
protected function parseRequest(
ServerRequest $request,
ExtendedFilesystemInterface $fs,
bool $recursive = false
): Entity\Api\BatchResult {
$files = array_values((array)$request->getParam('files', []));
$directories = array_values((array)$request->getParam('dirs', []));
if ($recursive) {
foreach ($directories as $dir) {
$dirIterator = $fs->listContents($dir, true)->filter(
function (StorageAttributes $attrs) {
return $attrs->isFile();
}
);
foreach ($dirIterator as $subDirMeta) {
$files[] = $subDirMeta['path'];
}
}
}
$result = new Entity\Api\BatchResult();
$result->files = $files;
$result->directories = $directories;
return $result;
}
protected function writePlaylistChanges(
ServerRequest $request,
array $playlists
): void {
// Write new PLS playlist configuration.
$backend = $request->getStationBackend();
if ($backend instanceof Liquidsoap) {
foreach ($playlists as $playlistId => $playlistRow) {
// Instruct the message queue to start a new "write playlist to file" task.
$message = new Message\WritePlaylistFileMessage();
$message->playlist_id = $playlistId;
$this->messageBus->dispatch($message);
}
}
}
}