<commit_before><?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)
        );
    }
}
<commit_msg>#5870 -- Properly mark reprocessing media as unprocessable.<commit_after><?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->process($storageLocation, $mediaRow, $message->force);
            }
        } 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|StationMedia $pathOrMedia,
        bool $force = false
    ): ?StationMedia {
        if ($pathOrMedia instanceof StationMedia) {
            $record = $pathOrMedia;
            $path = $pathOrMedia->getPath();
        } else {
            $record = null;
            $path = $pathOrMedia;
        }

        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 (null === $record && 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)
        );
    }
}

{'fraction_non_alphanumeric': 0.11305970149253732, 'fraction_numerical': 0.00044776119402985075, 'mean_word_length': 1.7144014583755316, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 114, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6618600', 'n_tokens_mistral': 3456, 'n_tokens_neox': 3297, 'n_words': 650}