Update media processing to include cover art handling.
This commit is contained in:
parent
468c4fe940
commit
e388541594
|
@ -5,6 +5,11 @@ release channel, you can take advantage of these new features and fixes.
|
|||
|
||||
## New Features/Changes
|
||||
|
||||
- **Cover Art Files Support**: Many users keep the cover art for their media alongside the media in a separate image
|
||||
file. AzuraCast now detects image files in the same folder as your media and uses it as the default album art for that
|
||||
media. Because cover art files are often named a variety of things, we currently will use _any_ image file that exists
|
||||
alongside media. You can also now view cover art via the Media Manager UI.
|
||||
|
||||
## Code Quality/Technical Changes
|
||||
|
||||
## Bug Fixes
|
||||
|
|
|
@ -6,8 +6,9 @@ use App\Sync\Task;
|
|||
use Symfony\Component\Mailer;
|
||||
|
||||
return [
|
||||
Message\AddNewMediaMessage::class => Task\CheckMediaTask::class,
|
||||
Message\ReprocessMediaMessage::class => Task\CheckMediaTask::class,
|
||||
Message\AddNewMediaMessage::class => App\Media\MediaProcessor::class,
|
||||
Message\ReprocessMediaMessage::class => App\Media\MediaProcessor::class,
|
||||
Message\ProcessCoverArtMessage::class => App\Media\MediaProcessor::class,
|
||||
|
||||
Message\WritePlaylistFileMessage::class => Liquidsoap\PlaylistFileWriter::class,
|
||||
|
||||
|
|
|
@ -58,6 +58,9 @@
|
|||
<span class="file-icon" v-if="row.item.is_dir">
|
||||
<icon icon="folder"></icon>
|
||||
</span>
|
||||
<span class="file-icon" v-else-if="row.item.is_cover_art">
|
||||
<icon icon="photo"></icon>
|
||||
</span>
|
||||
<span class="file-icon" v-else>
|
||||
<icon icon="note"></icon>
|
||||
</span>
|
||||
|
@ -90,6 +93,8 @@
|
|||
|
||||
<album-art v-if="row.item.media_art" :src="row.item.media_art"
|
||||
class="flex-shrink-1 pl-2"></album-art>
|
||||
<album-art v-else-if="row.item.is_cover_art" :src="row.item.links_download"
|
||||
class="flex-shrink-1 pl-2"></album-art>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(media_genre)="row">
|
||||
|
|
|
@ -9,6 +9,7 @@ use App\Flysystem\StationFilesystems;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\OpenApi;
|
||||
use Azura\Files\ExtendedFilesystemInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -53,29 +54,59 @@ final class GetArtAction
|
|||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
$fsMedia = (new StationFilesystems($station))->getMediaFilesystem();
|
||||
|
||||
$defaultArtRedirect = $response->withRedirect((string)$this->stationRepo->getDefaultAlbumArtUrl($station), 302);
|
||||
if (str_contains($media_id, '-')) {
|
||||
$response = $response->withCacheLifetime(Response::CACHE_ONE_YEAR);
|
||||
}
|
||||
|
||||
// If a timestamp delimiter is added, strip it automatically.
|
||||
$media_id = explode('-', $media_id, 2)[0];
|
||||
|
||||
$fsMedia = (new StationFilesystems($station))->getMediaFilesystem();
|
||||
|
||||
$mediaPath = $this->getMediaPath($station, $fsMedia, $media_id);
|
||||
if (null !== $mediaPath) {
|
||||
return $response->streamFilesystemFile(
|
||||
$fsMedia,
|
||||
$mediaPath,
|
||||
null,
|
||||
'inline',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return $response->withRedirect((string)$this->stationRepo->getDefaultAlbumArtUrl($station), 302);
|
||||
}
|
||||
|
||||
private function getMediaPath(
|
||||
Entity\Station $station,
|
||||
ExtendedFilesystemInterface $fsMedia,
|
||||
string $media_id
|
||||
): ?string {
|
||||
if (Entity\StationMedia::UNIQUE_ID_LENGTH === strlen($media_id)) {
|
||||
$response = $response->withCacheLifetime(Response::CACHE_ONE_YEAR);
|
||||
$mediaPath = Entity\StationMedia::getArtPath($media_id);
|
||||
} else {
|
||||
$media = $this->mediaRepo->findForStation($media_id, $station);
|
||||
if ($media instanceof Entity\StationMedia) {
|
||||
$mediaPath = Entity\StationMedia::getArtPath($media->getUniqueId());
|
||||
} else {
|
||||
return $defaultArtRedirect;
|
||||
|
||||
if ($fsMedia->fileExists($mediaPath)) {
|
||||
return $mediaPath;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fsMedia->fileExists($mediaPath)) {
|
||||
return $response->streamFilesystemFile($fsMedia, $mediaPath, null, 'inline', false);
|
||||
$media = $this->mediaRepo->findForStation($media_id, $station);
|
||||
if (!($media instanceof Entity\StationMedia)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $defaultArtRedirect;
|
||||
$mediaPath = Entity\StationMedia::getArtPath($media->getUniqueId());
|
||||
if ($fsMedia->fileExists($mediaPath)) {
|
||||
return $mediaPath;
|
||||
}
|
||||
|
||||
$folderPath = Entity\StationMedia::getFolderArtPath(
|
||||
Entity\StationMedia::getFolderHashForPath($media->getPath())
|
||||
);
|
||||
if ($fsMedia->fileExists($folderPath)) {
|
||||
return $folderPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -388,6 +388,7 @@ final class BatchAction
|
|||
|
||||
if (!isset($queuedMediaUpdates[$mediaId])) {
|
||||
$message = new Message\ReprocessMediaMessage();
|
||||
$message->storage_location_id = $storageLocation->getIdRequired();
|
||||
$message->media_id = $mediaId;
|
||||
$message->force = true;
|
||||
|
||||
|
@ -400,7 +401,7 @@ final class BatchAction
|
|||
|
||||
if (!isset($queuedNewFiles[$path])) {
|
||||
$message = new Message\AddNewMediaMessage();
|
||||
$message->storage_location_id = (int)$storageLocation->getId();
|
||||
$message->storage_location_id = $storageLocation->getIdRequired();
|
||||
$message->path = $unprocessable->getPath();
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
|
|
|
@ -9,6 +9,7 @@ use App\Exception\CannotProcessMediaException;
|
|||
use App\Exception\StorageLocationFullException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Media\MediaProcessor;
|
||||
use App\Service\Flow;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
@ -18,7 +19,7 @@ final class FlowUploadAction
|
|||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly Entity\Repository\StationMediaRepository $mediaRepo,
|
||||
private readonly MediaProcessor $mediaProcessor,
|
||||
private readonly Entity\Repository\StationPlaylistMediaRepository $spmRepo,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
|
@ -54,7 +55,13 @@ final class FlowUploadAction
|
|||
}
|
||||
|
||||
try {
|
||||
$stationMedia = $this->mediaRepo->getOrCreate($station, $destPath, $flowResponse->getUploadedPath());
|
||||
$tempPath = $flowResponse->getUploadedPath();
|
||||
|
||||
$stationMedia = $this->mediaProcessor->processAndUpload(
|
||||
$mediaStorage,
|
||||
$destPath,
|
||||
$tempPath
|
||||
);
|
||||
} catch (CannotProcessMediaException $e) {
|
||||
$this->logger->error(
|
||||
$e->getMessageWithPath(),
|
||||
|
@ -67,7 +74,7 @@ final class FlowUploadAction
|
|||
}
|
||||
|
||||
// If the user is looking at a playlist's contents, add uploaded media to that playlist.
|
||||
if (!empty($allParams['searchPhrase'])) {
|
||||
if ($stationMedia instanceof Entity\StationMedia && !empty($allParams['searchPhrase'])) {
|
||||
$search_phrase = $allParams['searchPhrase'];
|
||||
|
||||
if (str_starts_with($search_phrase, 'playlist:')) {
|
||||
|
|
|
@ -10,6 +10,7 @@ use App\Flysystem\StationFilesystems;
|
|||
use App\Http\Response;
|
||||
use App\Http\RouterInterface;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Media\MimeType;
|
||||
use App\Paginator;
|
||||
use App\Utilities;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
|
@ -253,7 +254,11 @@ final class ListAction
|
|||
$files = array_keys($mediaInDir);
|
||||
}
|
||||
} else {
|
||||
$protectedPaths = [Entity\StationMedia::DIR_ALBUM_ART, Entity\StationMedia::DIR_WAVEFORMS];
|
||||
$protectedPaths = [
|
||||
Entity\StationMedia::DIR_ALBUM_ART,
|
||||
Entity\StationMedia::DIR_WAVEFORMS,
|
||||
Entity\StationMedia::DIR_FOLDER_COVERS,
|
||||
];
|
||||
|
||||
$files = $fs->listContents($currentDir, false)->filter(
|
||||
function (StorageAttributes $attributes) use ($currentDir, $protectedPaths) {
|
||||
|
@ -304,6 +309,9 @@ final class ListAction
|
|||
__('File Not Processed: %s'),
|
||||
Utilities\Strings::truncateText($unprocessableMedia[$row->path])
|
||||
);
|
||||
} elseif (MimeType::isPathImage($row->path)) {
|
||||
$row->is_cover_art = true;
|
||||
$row->text = __('Cover Art');
|
||||
} else {
|
||||
$row->text = __('File Processing');
|
||||
}
|
||||
|
@ -337,11 +345,10 @@ final class ListAction
|
|||
// Add processor-intensive data for just this page.
|
||||
$stationId = $station->getIdRequired();
|
||||
$isInternal = (bool)$request->getParam('internal', false);
|
||||
$defaultAlbumArtUrl = (string)$this->stationRepo->getDefaultAlbumArtUrl($station);
|
||||
|
||||
$paginator->setPostprocessor(
|
||||
static function (Entity\Api\FileList $row) use ($router, $stationId, $defaultAlbumArtUrl, $isInternal) {
|
||||
return self::postProcessRow($row, $router, $stationId, $defaultAlbumArtUrl, $isInternal);
|
||||
static function (Entity\Api\FileList $row) use ($router, $stationId, $isInternal) {
|
||||
return self::postProcessRow($row, $router, $stationId, $isInternal);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -397,19 +404,21 @@ final class ListAction
|
|||
Entity\Api\FileList $row,
|
||||
RouterInterface $router,
|
||||
int $stationId,
|
||||
string $defaultAlbumArtUrl,
|
||||
bool $isInternal
|
||||
): Entity\Api\FileList|array {
|
||||
if (null !== $row->media->media_id) {
|
||||
$row->media->art = (0 === $row->media->art_updated_at)
|
||||
? $defaultAlbumArtUrl
|
||||
: (string)$router->named(
|
||||
'api:stations:media:art',
|
||||
[
|
||||
'station_id' => $stationId,
|
||||
'media_id' => $row->media->unique_id . '-' . $row->media->art_updated_at,
|
||||
]
|
||||
);
|
||||
$artMediaId = $row->media->unique_id;
|
||||
if (0 !== $row->media->art_updated_at) {
|
||||
$artMediaId .= '-' . $row->media->art_updated_at;
|
||||
}
|
||||
|
||||
$row->media->art = (string)$router->named(
|
||||
'api:stations:media:art',
|
||||
[
|
||||
'station_id' => $stationId,
|
||||
'media_id' => $artMediaId,
|
||||
]
|
||||
);
|
||||
|
||||
$row->media->links = [
|
||||
'play' => (string)$router->named(
|
||||
|
|
|
@ -24,7 +24,11 @@ final class ListDirectoriesAction
|
|||
|
||||
$fsMedia = (new StationFilesystems($station))->getMediaFilesystem();
|
||||
|
||||
$protectedPaths = [Entity\StationMedia::DIR_ALBUM_ART, Entity\StationMedia::DIR_WAVEFORMS];
|
||||
$protectedPaths = [
|
||||
Entity\StationMedia::DIR_ALBUM_ART,
|
||||
Entity\StationMedia::DIR_WAVEFORMS,
|
||||
Entity\StationMedia::DIR_FOLDER_COVERS,
|
||||
];
|
||||
|
||||
$directoriesRaw = $fsMedia->listContents($currentDir, false)->filter(
|
||||
function (StorageAttributes $attrs) use ($protectedPaths) {
|
||||
|
|
|
@ -10,6 +10,7 @@ use App\Exception\ValidationException;
|
|||
use App\Flysystem\StationFilesystems;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Media\MediaProcessor;
|
||||
use App\Message\WritePlaylistFileMessage;
|
||||
use App\OpenApi;
|
||||
use App\Radio\Adapters;
|
||||
|
@ -156,6 +157,7 @@ final class FilesController extends AbstractStationApiCrudController
|
|||
private readonly Entity\Repository\CustomFieldRepository $customFieldsRepo,
|
||||
private readonly Entity\Repository\StationMediaRepository $mediaRepo,
|
||||
private readonly Entity\Repository\StationPlaylistMediaRepository $playlistMediaRepo,
|
||||
private readonly MediaProcessor $mediaProcessor,
|
||||
ReloadableEntityManagerInterface $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator
|
||||
|
@ -206,13 +208,19 @@ final class FilesController extends AbstractStationApiCrudController
|
|||
}
|
||||
|
||||
// Write file to temp path.
|
||||
$temp_path = $station->getRadioTempDir() . '/' . $api_record->getSanitizedFilename();
|
||||
file_put_contents($temp_path, $api_record->getFileContents());
|
||||
$tempPath = $station->getRadioTempDir() . '/' . $api_record->getSanitizedFilename();
|
||||
file_put_contents($tempPath, $api_record->getFileContents());
|
||||
|
||||
// Process temp path as regular media record.
|
||||
$record = $this->mediaRepo->getOrCreate($station, $api_record->getSanitizedPath(), $temp_path);
|
||||
$record = $this->mediaProcessor->processAndUpload(
|
||||
$mediaStorage,
|
||||
$api_record->getSanitizedPath(),
|
||||
$tempPath
|
||||
);
|
||||
|
||||
$return = $this->viewRecord($record, $request);
|
||||
$return = (null !== $record)
|
||||
? $this->viewRecord($record, $request)
|
||||
: Entity\Api\Status::success();
|
||||
|
||||
return $response->withJson($return);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ final class FileList
|
|||
|
||||
public bool $is_dir = false;
|
||||
|
||||
public bool $is_cover_art = false;
|
||||
|
||||
public FileListMedia $media;
|
||||
|
||||
public array $playlists = [];
|
||||
|
|
|
@ -63,15 +63,18 @@ final class SongApiGenerator
|
|||
): UriInterface {
|
||||
if (null !== $station && $song instanceof Entity\StationMedia) {
|
||||
$mediaUpdatedTimestamp = $song->getArtUpdatedAt();
|
||||
$mediaId = $song->getUniqueId();
|
||||
if (0 !== $mediaUpdatedTimestamp) {
|
||||
return $this->router->named(
|
||||
route_name: 'api:stations:media:art',
|
||||
route_params: [
|
||||
'station_id' => $station->getId(),
|
||||
'media_id' => $song->getUniqueId() . '-' . $mediaUpdatedTimestamp,
|
||||
]
|
||||
);
|
||||
$mediaId .= '-' . $mediaUpdatedTimestamp;
|
||||
}
|
||||
|
||||
return $this->router->named(
|
||||
route_name: 'api:stations:media:art',
|
||||
route_params: [
|
||||
'station_id' => $station->getId(),
|
||||
'media_id' => $mediaId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($allowRemoteArt && $this->remoteAlbumArt->enableForApis()) {
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Entity\Fixture;
|
||||
|
||||
use App\Entity;
|
||||
use App\Media\MediaProcessor;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
@ -13,7 +14,7 @@ use Symfony\Component\Finder\Finder;
|
|||
final class StationMedia extends AbstractFixture implements DependentFixtureInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Entity\Repository\StationMediaRepository $mediaRepo
|
||||
private readonly MediaProcessor $mediaProcessor
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -46,7 +47,11 @@ final class StationMedia extends AbstractFixture implements DependentFixtureInte
|
|||
// Copy the file to the station media directory.
|
||||
$fs->upload($filePath, '/' . $fileBaseName);
|
||||
|
||||
$mediaRow = $this->mediaRepo->getOrCreate($mediaStorage, $fileBaseName);
|
||||
$mediaRow = $this->mediaProcessor->process($mediaStorage, $fileBaseName);
|
||||
if (null === $mediaRow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$manager->persist($mediaRow);
|
||||
|
||||
// Add the file to the playlist.
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace App\Entity\Repository;
|
|||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Doctrine\Repository;
|
||||
use App\Entity;
|
||||
use App\Exception\CannotProcessMediaException;
|
||||
use App\Exception\NotFoundException;
|
||||
use App\Media\AlbumArt;
|
||||
use App\Media\MetadataManager;
|
||||
|
@ -33,8 +32,7 @@ final class StationMediaRepository extends Repository
|
|||
private readonly MetadataManager $metadataManager,
|
||||
private readonly RemoteAlbumArt $remoteAlbumArt,
|
||||
private readonly CustomFieldRepository $customFieldRepo,
|
||||
private readonly StationPlaylistMediaRepository $spmRepo,
|
||||
private readonly UnprocessableMediaRepository $unprocessableMediaRepo
|
||||
private readonly StationPlaylistMediaRepository $spmRepo
|
||||
) {
|
||||
parent::__construct($em);
|
||||
}
|
||||
|
@ -144,102 +142,6 @@ final class StationMediaRepository extends Repository
|
|||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity\Station|Entity\StorageLocation $source
|
||||
* @param string $path
|
||||
* @param string|null $uploadedFrom The original uploaded path (if this is a new upload).
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getOrCreate(
|
||||
Entity\Station|Entity\StorageLocation $source,
|
||||
string $path,
|
||||
?string $uploadedFrom = null
|
||||
): Entity\StationMedia {
|
||||
$record = $this->findByPath($path, $source);
|
||||
$storageLocation = $this->getStorageLocation($source);
|
||||
|
||||
$created = false;
|
||||
if (!($record instanceof Entity\StationMedia)) {
|
||||
$record = new Entity\StationMedia($storageLocation, $path);
|
||||
$created = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$reprocessed = $this->processMedia($record, $created, $uploadedFrom);
|
||||
} catch (CannotProcessMediaException $e) {
|
||||
$this->unprocessableMediaRepo->setForPath(
|
||||
$storageLocation,
|
||||
$path,
|
||||
$e->getMessage()
|
||||
);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($created || $reprocessed) {
|
||||
$this->em->flush();
|
||||
|
||||
$this->unprocessableMediaRepo->clearForPath($storageLocation, $path);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run media through the "processing" steps: loading from file and setting up any missing metadata.
|
||||
*
|
||||
* @param Entity\StationMedia $media
|
||||
* @param bool $force
|
||||
* @param string|null $uploadedPath The uploaded path (if this is a new upload).
|
||||
*
|
||||
* @return bool Whether reprocessing was required for this file.
|
||||
*/
|
||||
public function processMedia(
|
||||
Entity\StationMedia $media,
|
||||
bool $force = false,
|
||||
?string $uploadedPath = null
|
||||
): bool {
|
||||
$fs = $this->getFilesystem($media);
|
||||
$path = $media->getPath();
|
||||
|
||||
if (null !== $uploadedPath) {
|
||||
try {
|
||||
$this->loadFromFile($media, $uploadedPath, $fs);
|
||||
} finally {
|
||||
$fs->uploadAndDeleteOriginal($uploadedPath, $path);
|
||||
}
|
||||
|
||||
$mediaMtime = time();
|
||||
} else {
|
||||
if (!$fs->fileExists($path)) {
|
||||
throw CannotProcessMediaException::forPath(
|
||||
$path,
|
||||
sprintf('Media path "%s" not found.', $path)
|
||||
);
|
||||
}
|
||||
|
||||
$mediaMtime = $fs->lastModified($path);
|
||||
|
||||
// No need to update if all of these conditions are true.
|
||||
if (!$force && !$media->needsReprocessing($mediaMtime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fs->withLocalFile(
|
||||
$path,
|
||||
function ($localPath) use ($media, $fs): void {
|
||||
$this->loadFromFile($media, $localPath, $fs);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$media->setMtime($mediaMtime);
|
||||
$this->em->persist($media);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process metadata information from media file.
|
||||
*
|
||||
|
|
|
@ -36,6 +36,7 @@ class StationMedia implements
|
|||
public const UNIQUE_ID_LENGTH = 24;
|
||||
|
||||
public const DIR_ALBUM_ART = '.albumart';
|
||||
public const DIR_FOLDER_COVERS = '.covers';
|
||||
public const DIR_WAVEFORMS = '.waveforms';
|
||||
|
||||
#[
|
||||
|
@ -533,6 +534,19 @@ class StationMedia implements
|
|||
return self::DIR_ALBUM_ART . '/' . $uniqueId . '.jpg';
|
||||
}
|
||||
|
||||
public static function getFolderArtPath(string $folderHash): string
|
||||
{
|
||||
return self::DIR_FOLDER_COVERS . '/' . $folderHash . '.jpg';
|
||||
}
|
||||
|
||||
public static function getFolderHashForPath(string $path): string
|
||||
{
|
||||
$folder = dirname($path);
|
||||
return (!empty($folder))
|
||||
? md5($folder)
|
||||
: 'base';
|
||||
}
|
||||
|
||||
public static function getWaveformPath(string $uniqueId): string
|
||||
{
|
||||
return self::DIR_WAVEFORMS . '/' . $uniqueId . '.json';
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Entity\Repository\StationMediaRepository;
|
||||
use App\Entity\Repository\UnprocessableMediaRepository;
|
||||
use App\Entity\StationMedia;
|
||||
use App\Entity\StorageLocation;
|
||||
use App\Exception\CannotProcessMediaException;
|
||||
use App\Message\AddNewMediaMessage;
|
||||
use App\Message\ProcessCoverArtMessage;
|
||||
use App\Message\ReprocessMediaMessage;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
final class MediaProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ReloadableEntityManagerInterface $em,
|
||||
private readonly StationMediaRepository $mediaRepo,
|
||||
private readonly UnprocessableMediaRepository $unprocessableMediaRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ReprocessMediaMessage|AddNewMediaMessage|ProcessCoverArtMessage $message
|
||||
): void {
|
||||
$storageLocation = $this->em->find(StorageLocation::class, $message->storage_location_id);
|
||||
if (!($storageLocation instanceof StorageLocation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($message instanceof ReprocessMediaMessage) {
|
||||
$mediaRow = $this->em->find(StationMedia::class, $message->media_id);
|
||||
if ($mediaRow instanceof StationMedia) {
|
||||
$this->processMedia($storageLocation, $mediaRow, $message->force);
|
||||
$this->em->flush();
|
||||
}
|
||||
} else {
|
||||
$this->process($storageLocation, $message->path);
|
||||
}
|
||||
}
|
||||
|
||||
public function processAndUpload(
|
||||
StorageLocation $storageLocation,
|
||||
string $path,
|
||||
string $localPath
|
||||
): ?StationMedia {
|
||||
$fs = $storageLocation->getFilesystem();
|
||||
|
||||
if (!(new Filesystem())->exists($localPath)) {
|
||||
throw CannotProcessMediaException::forPath(
|
||||
$path,
|
||||
sprintf('Local file path "%s" not found.', $localPath)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (MimeType::isFileProcessable($localPath)) {
|
||||
$record = $this->mediaRepo->findByPath($path, $storageLocation);
|
||||
if (!($record instanceof StationMedia)) {
|
||||
$record = new StationMedia($storageLocation, $path);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->mediaRepo->loadFromFile($record, $localPath, $fs);
|
||||
|
||||
$record->setMtime(time());
|
||||
$this->em->persist($record);
|
||||
} catch (CannotProcessMediaException $e) {
|
||||
$this->unprocessableMediaRepo->setForPath(
|
||||
$storageLocation,
|
||||
$path,
|
||||
$e->getMessage()
|
||||
);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->unprocessableMediaRepo->clearForPath($storageLocation, $path);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
if (MimeType::isPathImage($localPath)) {
|
||||
$this->processCoverArt(
|
||||
$storageLocation,
|
||||
$path,
|
||||
file_get_contents($localPath) ?: ''
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw CannotProcessMediaException::forPath(
|
||||
$path,
|
||||
'File type cannot be processed.'
|
||||
);
|
||||
} finally {
|
||||
$fs->uploadAndDeleteOriginal($localPath, $path);
|
||||
}
|
||||
}
|
||||
|
||||
public function process(
|
||||
StorageLocation $storageLocation,
|
||||
string $path,
|
||||
bool $force = false
|
||||
): ?StationMedia {
|
||||
if (MimeType::isPathProcessable($path)) {
|
||||
$record = $this->mediaRepo->findByPath($path, $storageLocation);
|
||||
$created = false;
|
||||
if (!($record instanceof StationMedia)) {
|
||||
$record = new StationMedia($storageLocation, $path);
|
||||
$created = true;
|
||||
}
|
||||
|
||||
try {
|
||||
$reprocessed = $this->processMedia($storageLocation, $record, $force);
|
||||
} catch (CannotProcessMediaException $e) {
|
||||
$this->unprocessableMediaRepo->setForPath(
|
||||
$storageLocation,
|
||||
$path,
|
||||
$e->getMessage()
|
||||
);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($created || $reprocessed) {
|
||||
$this->em->flush();
|
||||
$this->unprocessableMediaRepo->clearForPath($storageLocation, $path);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
if (MimeType::isPathImage($path)) {
|
||||
$this->processCoverArt(
|
||||
$storageLocation,
|
||||
$path
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw CannotProcessMediaException::forPath(
|
||||
$path,
|
||||
'File type cannot be processed.'
|
||||
);
|
||||
}
|
||||
|
||||
public function processMedia(
|
||||
StorageLocation $storageLocation,
|
||||
StationMedia $media,
|
||||
bool $force = false
|
||||
): bool {
|
||||
$fs = $storageLocation->getFilesystem();
|
||||
$path = $media->getPath();
|
||||
|
||||
if (!$fs->fileExists($path)) {
|
||||
throw CannotProcessMediaException::forPath(
|
||||
$path,
|
||||
sprintf('Media path "%s" not found.', $path)
|
||||
);
|
||||
}
|
||||
|
||||
$mediaMtime = $fs->lastModified($path);
|
||||
|
||||
// No need to update if all of these conditions are true.
|
||||
if (!$force && !$media->needsReprocessing($mediaMtime)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fs->withLocalFile(
|
||||
$path,
|
||||
function ($localPath) use ($media, $fs): void {
|
||||
$this->mediaRepo->loadFromFile($media, $localPath, $fs);
|
||||
}
|
||||
);
|
||||
|
||||
$media->setMtime($mediaMtime);
|
||||
$this->em->persist($media);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function processCoverArt(
|
||||
StorageLocation $storageLocation,
|
||||
string $path,
|
||||
?string $contents = null
|
||||
): void {
|
||||
$fs = $storageLocation->getFilesystem();
|
||||
|
||||
if (null === $contents) {
|
||||
if (!$fs->fileExists($path)) {
|
||||
throw CannotProcessMediaException::forPath(
|
||||
$path,
|
||||
sprintf('Cover art path "%s" not found.', $path)
|
||||
);
|
||||
}
|
||||
|
||||
$contents = $fs->read($path);
|
||||
}
|
||||
|
||||
$folderHash = StationMedia::getFolderHashForPath($path);
|
||||
$destPath = StationMedia::getFolderArtPath($folderHash);
|
||||
|
||||
$fs->write(
|
||||
$destPath,
|
||||
AlbumArt::resize($contents)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,39 +8,58 @@ use League\MimeTypeDetection\FinfoMimeTypeDetector;
|
|||
|
||||
final class MimeType
|
||||
{
|
||||
private static FinfoMimeTypeDetector $detector;
|
||||
|
||||
private static array $processableTypes = [
|
||||
'audio/aiff', // aiff (Audio Interchange File Format)
|
||||
'audio/flac', // MIME type used by some FLAC files
|
||||
'audio/mp4', // m4a mp4a
|
||||
'audio/mpeg', // mpga mp2 mp2a mp3 m2a m3a
|
||||
'audio/ogg', // oga ogg spx
|
||||
'audio/s3m', // s3m (ScreamTracker 3 Module)
|
||||
'audio/wav', // wav
|
||||
'audio/xm', // xm
|
||||
'audio/vnd.wave', // alt for wav (RFC 2361)
|
||||
'audio/x-aac', // aac
|
||||
'audio/x-aiff', // alt for aiff
|
||||
'audio/x-flac', // flac
|
||||
'audio/x-m4a', // alt for m4a/mp4a
|
||||
'audio/x-mod', // stm, alt for xm
|
||||
'audio/x-s3m', // alt for s3m
|
||||
'audio/x-wav', // alt for wav
|
||||
'audio/x-ms-wma', // wma (Windows Media Audio)
|
||||
'video/mp4', // some MP4 audio files are recognized as this (#3569)
|
||||
'video/x-ms-asf', // asf / wmv / alt for wma
|
||||
];
|
||||
|
||||
private static array $imageTypes = [
|
||||
'image/gif', // gif
|
||||
'image/jpeg', // jpg/jpeg
|
||||
'image/png', // png
|
||||
];
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getProcessableTypes(): array
|
||||
{
|
||||
return [
|
||||
'audio/aiff', // aiff (Audio Interchange File Format)
|
||||
'audio/flac', // MIME type used by some FLAC files
|
||||
'audio/mp4', // m4a mp4a
|
||||
'audio/mpeg', // mpga mp2 mp2a mp3 m2a m3a
|
||||
'audio/ogg', // oga ogg spx
|
||||
'audio/s3m', // s3m (ScreamTracker 3 Module)
|
||||
'audio/wav', // wav
|
||||
'audio/xm', // xm
|
||||
'audio/vnd.wave', // alt for wav (RFC 2361)
|
||||
'audio/x-aac', // aac
|
||||
'audio/x-aiff', // alt for aiff
|
||||
'audio/x-flac', // flac
|
||||
'audio/x-m4a', // alt for m4a/mp4a
|
||||
'audio/x-mod', // stm, alt for xm
|
||||
'audio/x-s3m', // alt for s3m
|
||||
'audio/x-wav', // alt for wav
|
||||
'audio/x-ms-wma', // wma (Windows Media Audio)
|
||||
'video/mp4', // some MP4 audio files are recognized as this (#3569)
|
||||
'video/x-ms-asf', // asf / wmv / alt for wma
|
||||
];
|
||||
return self::$processableTypes;
|
||||
}
|
||||
|
||||
public static function getMimeTypeDetector(): FinfoMimeTypeDetector
|
||||
{
|
||||
if (!isset(self::$detector)) {
|
||||
self::$detector = new FinfoMimeTypeDetector(
|
||||
extensionMap: new MimeTypeExtensionMap()
|
||||
);
|
||||
}
|
||||
|
||||
return self::$detector;
|
||||
}
|
||||
|
||||
public static function getMimeTypeFromFile(string $path): string
|
||||
{
|
||||
$fileMimeType = (new FinfoMimeTypeDetector(
|
||||
extensionMap: new MimeTypeExtensionMap()
|
||||
))->detectMimeTypeFromFile($path);
|
||||
$fileMimeType = self::getMimeTypeDetector()->detectMimeTypeFromFile($path);
|
||||
|
||||
if ('application/octet-stream' === $fileMimeType) {
|
||||
$fileMimeType = null;
|
||||
|
@ -51,24 +70,28 @@ final class MimeType
|
|||
|
||||
public static function getMimeTypeFromPath(string $path): string
|
||||
{
|
||||
$extensionMap = new MimeTypeExtensionMap();
|
||||
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
return $extensionMap->lookupMimeType($extension) ?? 'application/octet-stream';
|
||||
return self::getMimeTypeDetector()->detectMimeTypeFromPath($path)
|
||||
?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
public static function isPathProcessable(string $path): bool
|
||||
{
|
||||
$mimeType = self::getMimeTypeFromPath($path);
|
||||
|
||||
return in_array($mimeType, self::getProcessableTypes(), true);
|
||||
return in_array($mimeType, self::$processableTypes, true);
|
||||
}
|
||||
|
||||
public static function isPathImage(string $path): bool
|
||||
{
|
||||
$mimeType = self::getMimeTypeFromPath($path);
|
||||
|
||||
return in_array($mimeType, self::$imageTypes, true);
|
||||
}
|
||||
|
||||
public static function isFileProcessable(string $path): bool
|
||||
{
|
||||
$mimeType = self::getMimeTypeFromFile($path);
|
||||
|
||||
return in_array($mimeType, self::getProcessableTypes(), true);
|
||||
return in_array($mimeType, self::$processableTypes, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
use App\MessageQueue\QueueManagerInterface;
|
||||
|
||||
final class ProcessCoverArtMessage extends AbstractUniqueMessage
|
||||
{
|
||||
/** @var int The numeric identifier for the StorageLocation entity. */
|
||||
public int $storage_location_id;
|
||||
|
||||
/** @var string The relative path for the cover file to be processed. */
|
||||
public string $path;
|
||||
|
||||
/** @var string The hash of the folder (used for storing and indexing the cover art). */
|
||||
public string $folder_hash;
|
||||
|
||||
public function getQueue(): string
|
||||
{
|
||||
return QueueManagerInterface::QUEUE_MEDIA;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,9 @@ use App\MessageQueue\QueueManagerInterface;
|
|||
|
||||
final class ReprocessMediaMessage extends AbstractUniqueMessage
|
||||
{
|
||||
/** @var int The numeric identifier for the StorageLocation entity. */
|
||||
public int $storage_location_id;
|
||||
|
||||
/** @var int The numeric identifier for the StationMedia record being processed. */
|
||||
public int $media_id;
|
||||
|
||||
|
|
|
@ -7,10 +7,13 @@ namespace App\Sync\Task;
|
|||
use App\Doctrine\ReloadableEntityManagerInterface;
|
||||
use App\Entity;
|
||||
use App\Media\MimeType;
|
||||
use App\Message;
|
||||
use App\Message\AddNewMediaMessage;
|
||||
use App\Message\ProcessCoverArtMessage;
|
||||
use App\Message\ReprocessMediaMessage;
|
||||
use App\MessageQueue\QueueManagerInterface;
|
||||
use App\Radio\Quota;
|
||||
use Azura\Files\Attributes\FileAttributes;
|
||||
use Azura\Files\ExtendedFilesystemInterface;
|
||||
use Brick\Math\BigInteger;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use League\Flysystem\FilesystemException;
|
||||
|
@ -42,29 +45,6 @@ final class CheckMediaTask extends AbstractTask
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event dispatch.
|
||||
*
|
||||
* @param Message\AbstractMessage $message
|
||||
*/
|
||||
public function __invoke(Message\AbstractMessage $message): void
|
||||
{
|
||||
if ($message instanceof Message\ReprocessMediaMessage) {
|
||||
$mediaRow = $this->em->find(Entity\StationMedia::class, $message->media_id);
|
||||
|
||||
if ($mediaRow instanceof Entity\StationMedia) {
|
||||
$this->mediaRepo->processMedia($mediaRow, $message->force);
|
||||
$this->em->flush();
|
||||
}
|
||||
} elseif ($message instanceof Message\AddNewMediaMessage) {
|
||||
$storageLocation = $this->em->find(Entity\StorageLocation::class, $message->storage_location_id);
|
||||
|
||||
if ($storageLocation instanceof Entity\StorageLocation) {
|
||||
$this->mediaRepo->getOrCreate($storageLocation, $message->path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function run(bool $force = false): void
|
||||
{
|
||||
$storageLocations = $this->iterateStorageLocations(Entity\Enums\StorageLocationTypes::StationMedia);
|
||||
|
@ -93,11 +73,10 @@ final class CheckMediaTask extends AbstractTask
|
|||
'updated' => 0,
|
||||
'created' => 0,
|
||||
'deleted' => 0,
|
||||
'cover_art' => 0,
|
||||
'not_processable' => 0,
|
||||
];
|
||||
|
||||
$musicFiles = [];
|
||||
|
||||
$total_size = BigInteger::zero();
|
||||
|
||||
try {
|
||||
|
@ -105,7 +84,8 @@ final class CheckMediaTask extends AbstractTask
|
|||
function (StorageAttributes $attrs) {
|
||||
return ($attrs->isFile()
|
||||
&& !str_starts_with($attrs->path(), Entity\StationMedia::DIR_ALBUM_ART)
|
||||
&& !str_starts_with($attrs->path(), Entity\StationMedia::DIR_WAVEFORMS));
|
||||
&& !str_starts_with($attrs->path(), Entity\StationMedia::DIR_WAVEFORMS)
|
||||
&& !str_starts_with($attrs->path(), Entity\StationMedia::DIR_FOLDER_COVERS));
|
||||
}
|
||||
);
|
||||
} catch (FilesystemException $e) {
|
||||
|
@ -118,6 +98,9 @@ final class CheckMediaTask extends AbstractTask
|
|||
return;
|
||||
}
|
||||
|
||||
$musicFiles = [];
|
||||
$coverFiles = [];
|
||||
|
||||
/** @var FileAttributes $file */
|
||||
foreach ($fsIterator as $file) {
|
||||
try {
|
||||
|
@ -129,11 +112,23 @@ final class CheckMediaTask extends AbstractTask
|
|||
continue;
|
||||
}
|
||||
|
||||
$pathHash = md5($file->path());
|
||||
$musicFiles[$pathHash] = [
|
||||
StorageAttributes::ATTRIBUTE_PATH => $file->path(),
|
||||
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $file->lastModified(),
|
||||
];
|
||||
if (MimeType::isPathProcessable($file->path())) {
|
||||
$pathHash = md5($file->path());
|
||||
$musicFiles[$pathHash] = [
|
||||
StorageAttributes::ATTRIBUTE_PATH => $file->path(),
|
||||
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $file->lastModified(),
|
||||
];
|
||||
} elseif (MimeType::isPathImage($file->path())) {
|
||||
$stats['cover_art']++;
|
||||
|
||||
$dirHash = Entity\StationMedia::getFolderHashForPath($file->path());
|
||||
$coverFiles[$dirHash] = [
|
||||
StorageAttributes::ATTRIBUTE_PATH => $file->path(),
|
||||
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $file->lastModified(),
|
||||
];
|
||||
} else {
|
||||
$stats['not_processable']++;
|
||||
}
|
||||
}
|
||||
|
||||
$storageLocation->setStorageUsed($total_size);
|
||||
|
@ -146,18 +141,27 @@ final class CheckMediaTask extends AbstractTask
|
|||
// Check queue for existing pending processing entries.
|
||||
$queuedMediaUpdates = [];
|
||||
$queuedNewFiles = [];
|
||||
$queuedCoverArt = [];
|
||||
|
||||
foreach ($this->queueManager->getMessagesInTransport(QueueManagerInterface::QUEUE_MEDIA) as $message) {
|
||||
if ($message instanceof Message\ReprocessMediaMessage) {
|
||||
if ($message instanceof ReprocessMediaMessage) {
|
||||
$queuedMediaUpdates[$message->media_id] = true;
|
||||
} elseif (
|
||||
$message instanceof Message\AddNewMediaMessage
|
||||
$message instanceof AddNewMediaMessage
|
||||
&& $message->storage_location_id === $storageLocation->getId()
|
||||
) {
|
||||
$queuedNewFiles[md5($message->path)] = true;
|
||||
} elseif (
|
||||
$message instanceof ProcessCoverArtMessage
|
||||
&& $message->storage_location_id === $storageLocation->getId()
|
||||
) {
|
||||
$queuedCoverArt[$message->folder_hash] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process cover art.
|
||||
$this->processCoverArt($storageLocation, $fs, $coverFiles, $queuedCoverArt);
|
||||
|
||||
// Check queue for existing pending processing entries.
|
||||
$this->processExistingMediaRows($storageLocation, $queuedMediaUpdates, $musicFiles, $stats);
|
||||
|
||||
|
@ -173,6 +177,41 @@ final class CheckMediaTask extends AbstractTask
|
|||
$this->logger->debug(sprintf('Media processed for "%s".', $storageLocation), $stats);
|
||||
}
|
||||
|
||||
private function processCoverArt(
|
||||
Entity\StorageLocation $storageLocation,
|
||||
ExtendedFilesystemInterface $fs,
|
||||
array $coverFiles,
|
||||
array $queuedCoverArt,
|
||||
): void {
|
||||
$fsIterator = $fs->listContents(Entity\StationMedia::DIR_FOLDER_COVERS, true)->filter(
|
||||
fn(StorageAttributes $attrs) => $attrs->isFile()
|
||||
);
|
||||
|
||||
/** @var FileAttributes $file */
|
||||
foreach ($fsIterator as $file) {
|
||||
$basename = basename($file->path());
|
||||
|
||||
if (!isset($coverFiles[$basename])) {
|
||||
$fs->delete($file->path());
|
||||
} elseif ($file->lastModified() > $coverFiles[$basename][StorageAttributes::ATTRIBUTE_LAST_MODIFIED]) {
|
||||
unset($coverFiles[$basename]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($coverFiles as $folderHash => $coverFile) {
|
||||
if (isset($queuedCoverArt[$folderHash])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$message = new ProcessCoverArtMessage();
|
||||
$message->storage_location_id = $storageLocation->getIdRequired();
|
||||
$message->path = $coverFile[StorageAttributes::ATTRIBUTE_PATH];
|
||||
$message->folder_hash = $folderHash;
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
}
|
||||
}
|
||||
|
||||
private function processExistingMediaRows(
|
||||
Entity\StorageLocation $storageLocation,
|
||||
array $queuedMediaUpdates,
|
||||
|
@ -206,7 +245,8 @@ final class CheckMediaTask extends AbstractTask
|
|||
empty($mediaRow['unique_id'])
|
||||
|| Entity\StationMedia::needsReprocessing($mtime, $mediaRow['mtime'] ?? 0)
|
||||
) {
|
||||
$message = new Message\ReprocessMediaMessage();
|
||||
$message = new ReprocessMediaMessage();
|
||||
$message->storage_location_id = $storageLocation->getIdRequired();
|
||||
$message->media_id = $mediaRow['id'];
|
||||
$message->force = empty($mediaRow['unique_id']);
|
||||
|
||||
|
@ -253,7 +293,7 @@ final class CheckMediaTask extends AbstractTask
|
|||
$mtime = $fileInfo[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? 0;
|
||||
|
||||
if (Entity\UnprocessableMedia::needsReprocessing($mtime, $unprocessableRow['mtime'] ?? 0)) {
|
||||
$message = new Message\AddNewMediaMessage();
|
||||
$message = new AddNewMediaMessage();
|
||||
$message->storage_location_id = $storageLocation->getIdRequired();
|
||||
$message->path = $unprocessableRow['path'];
|
||||
|
||||
|
@ -282,22 +322,10 @@ final class CheckMediaTask extends AbstractTask
|
|||
foreach ($musicFiles as $pathHash => $newMusicFile) {
|
||||
$path = $newMusicFile[StorageAttributes::ATTRIBUTE_PATH];
|
||||
|
||||
if (!MimeType::isPathProcessable($path)) {
|
||||
$mimeType = MimeType::getMimeTypeFromPath($path);
|
||||
|
||||
$this->unprocessableMediaRepo->setForPath(
|
||||
$storageLocation,
|
||||
$path,
|
||||
sprintf('MIME type "%s" is not processable.', $mimeType)
|
||||
);
|
||||
|
||||
$stats['not_processable']++;
|
||||
}
|
||||
|
||||
if (isset($queuedNewFiles[$pathHash])) {
|
||||
$stats['already_queued']++;
|
||||
} else {
|
||||
$message = new Message\AddNewMediaMessage();
|
||||
$message = new AddNewMediaMessage();
|
||||
$message->storage_location_id = $storageLocation->getIdRequired();
|
||||
$message->path = $path;
|
||||
|
||||
|
|
|
@ -102,6 +102,7 @@ final class CleanupStorageTask extends AbstractTask
|
|||
foreach ($fs->listContents($dirBase, true) as $row) {
|
||||
$path = $row->path();
|
||||
|
||||
|
||||
$filename = pathinfo($path, PATHINFO_FILENAME);
|
||||
if (!isset($allUniqueIds[$filename])) {
|
||||
$fs->delete($path);
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Doctrine\ReloadableEntityManagerInterface;
|
|||
use App\Entity;
|
||||
use App\Enums\GlobalPermissions;
|
||||
use App\Environment;
|
||||
use App\Media\MediaProcessor;
|
||||
use App\Security\SplitToken;
|
||||
use App\Tests\Module;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
@ -159,10 +160,10 @@ abstract class CestAbstract
|
|||
$storageFs = $storageLocation->getFilesystem();
|
||||
$storageFs->upload($songSrc, 'test.mp3');
|
||||
|
||||
/** @var Entity\Repository\StationMediaRepository $mediaRepo */
|
||||
$mediaRepo = $this->di->get(Entity\Repository\StationMediaRepository::class);
|
||||
/** @var MediaProcessor $mediaProcessor */
|
||||
$mediaProcessor = $this->di->get(MediaProcessor::class);
|
||||
|
||||
return $mediaRepo->getOrCreate($storageLocation, 'test.mp3');
|
||||
return $mediaProcessor->process($storageLocation, 'test.mp3');
|
||||
}
|
||||
|
||||
protected function _cleanTables(): void
|
||||
|
|
Loading…
Reference in New Issue