diff --git a/README.md b/README.md index bfa2a06..3b9d4ab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -LSBC -==== +# LSBC > Lightweight Symfony Broadcast Client, probably. @@ -15,16 +14,15 @@ Features include: - TODO the API let's you quickly add whole episodes from Youtube links Podcasts follow mostly open standards but the “target” client is the fantastic -[AntennaPod](https://antennapod.org/) +[AntennaPod](https://antennapod.org/), and gPodder was used in development. -Install -------- +## Install This project requires: -- PHP 8.2 +- PHP 8.3 - PostgreSQL 15 and its PHP driver For production: diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 56aee40..4808235 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -18,7 +18,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; #[AsCommand( name: 'app:download', - description: 'Add a short description for your command', + description: 'Download something from the Web to create an episode.', )] class DownloadCommand extends Command { @@ -27,8 +27,7 @@ class DownloadCommand extends Command protected EntityManagerInterface $entityManager, protected PodcastRepository $podcastRepository, protected ParameterBagInterface $parameterBag, - ) - { + ) { parent::__construct(); } @@ -50,24 +49,26 @@ class DownloadCommand extends Command $slug = $input->getArgument('podcast'); $podcast = $this->podcastRepository->findOneBy(['slug' => $slug]); - if ($podcast === null) { + if (null === $podcast) { $io->error("No podcast with slug $slug."); + return Command::FAILURE; } $filepath = $this->downloadService->download($url); - if ($filepath === false) { + if (false === $filepath) { $io->error('Download failed.'); + return Command::FAILURE; } $filename = basename($filepath); $publicFilepath = $this->parameterBag->get('kernel.project_dir') - . '/' - . Constants::BASE_PUBLIC_DIR - . Constants::FILES_BASE_PATH - . $filename; + .'/' + .Constants::BASE_PUBLIC_DIR + .Constants::FILES_BASE_PATH + .$filename; rename($filepath, $publicFilepath); $episode = new Episode(); diff --git a/src/Controller/EpisodeController.php b/src/Controller/EpisodeController.php index a28ff6f..3017ff7 100644 --- a/src/Controller/EpisodeController.php +++ b/src/Controller/EpisodeController.php @@ -26,7 +26,7 @@ class EpisodeController extends AbstractController protected PodcastRepository $podcastRepo, protected EntityManagerInterface $em, protected LoggerInterface $logger, - protected SluggerInterface $slugger + protected SluggerInterface $slugger, ) { } @@ -39,7 +39,8 @@ class EpisodeController extends AbstractController } #[Route('/new', name: 'app_episode_new', methods: ['GET', 'POST'])] - public function new(Request $request): Response { + public function new(Request $request): Response + { $queryPodcastId = $request->query->getInt('podcast', 0); $episode = new Episode(); @@ -59,7 +60,7 @@ class EpisodeController extends AbstractController $podcast = $this->podcastRepo->find($podcastId ?? $queryPodcastId); if ( null === $podcast - || $podcast->getOwner()?->getId() !== $this->getUser()?->getId() + || $podcast->getOwner()?->getUserIdentifier() !== $this->getUser()?->getUserIdentifier() ) { $form->get('podcast')->addError(new FormError('Invalid podcast.')); @@ -91,7 +92,7 @@ class EpisodeController extends AbstractController { return $this->render('episode/show.html.twig', [ 'episode' => $episode, - 'files_path' => Constants::FILES_BASE_PATH + 'files_path' => Constants::FILES_BASE_PATH, ]); } diff --git a/src/Entity/User.php b/src/Entity/User.php index 6140380..7d5d746 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -21,16 +21,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface private ?int $id = null; #[ORM\Column(length: 180, unique: true)] - private ?string $email = null; + private string $email; #[ORM\Column] private array $roles = []; - /** - * @var string The hashed password - */ #[ORM\Column] - private ?string $password = null; + private string $password; #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Podcast::class)] private Collection $podcasts; @@ -40,17 +37,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->podcasts = new ArrayCollection(); } - public function __toString() + public function __toString(): string { return "$this->email ($this->id)"; } - public function getId(): ?int + public function getId(): int { return $this->id; } - public function getEmail(): ?string + public function getEmail(): string { return $this->email; } @@ -84,6 +81,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return array_unique($roles); } + /** + * @param array $roles + */ public function setRoles(array $roles): self { $this->roles = $roles; @@ -106,7 +106,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } - public function eraseCredentials(): void { } diff --git a/src/Service/FeedService.php b/src/Service/FeedService.php index a108744..a76b220 100644 --- a/src/Service/FeedService.php +++ b/src/Service/FeedService.php @@ -4,9 +4,6 @@ namespace App\Service; use App\Constants; use App\Entity\Podcast; -use DOMDocument; -use DOMElement; -use DOMText; use Doctrine\ORM\EntityManagerInterface; use FFMpeg\FFProbe; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; @@ -21,15 +18,14 @@ class FeedService protected EntityManagerInterface $entityManager, protected ParameterBagInterface $parameterBag, UrlGeneratorInterface $urlGenerator, - ) - { + ) { $rootUrl = $urlGenerator->generate( 'app_index', [], UrlGeneratorInterface::ABSOLUTE_URL ); - $this->baseFeedUrl = $rootUrl . Constants::FILES_BASE_PATH; - $this->baseImageUrl = $rootUrl . Constants::IMAGES_BASE_PATH; + $this->baseFeedUrl = $rootUrl.Constants::FILES_BASE_PATH; + $this->baseImageUrl = $rootUrl.Constants::IMAGES_BASE_PATH; } public const XML_NS = 'http://www.w3.org/2000/xmlns/'; @@ -38,7 +34,7 @@ class FeedService public function generate(Podcast $podcast): string|false { - $document = new DOMDocument(version: '1.0', encoding: 'UTF-8'); + $document = new \DOMDocument(version: '1.0', encoding: 'UTF-8'); $rssElement = $document->createElement('rss'); $rssElement->setAttribute('version', '2.0'); @@ -46,20 +42,20 @@ class FeedService $rssElement->setAttributeNS(self::XML_NS, 'xmlns:itunes', self::ITUNES_NS); $document->appendChild($rssElement); - $channelElement = new DOMElement('channel'); + $channelElement = new \DOMElement('channel'); $rssElement->appendChild($channelElement); - $titleElement = new DOMElement('title'); + $titleElement = new \DOMElement('title'); $channelElement->appendChild($titleElement); - $titleElement->appendChild(new DOMText($podcast->getName())); - $channelElement->appendChild(new DOMElement('description', $podcast->getDescription())); - $channelElement->appendChild(new DOMElement('link', $podcast->getWebsite())); + $titleElement->appendChild(new \DOMText($podcast->getName())); + $channelElement->appendChild(new \DOMElement('description', $podcast->getDescription())); + $channelElement->appendChild(new \DOMElement('link', $podcast->getWebsite())); $logoFilename = $podcast->getLogoFilename(); - if ($logoFilename !== null) { - $imageElement = new DOMElement('image'); + if (null !== $logoFilename) { + $imageElement = new \DOMElement('image'); $channelElement->appendChild($imageElement); - $imageElement->appendChild(new DOMElement('url', $this->baseImageUrl . $logoFilename)); - $imageElement->appendChild(new DOMElement('title', 'logo')); - $imageElement->appendChild(new DOMElement('link', $podcast->getWebsite())); + $imageElement->appendChild(new \DOMElement('url', $this->baseImageUrl.$logoFilename)); + $imageElement->appendChild(new \DOMElement('title', 'logo')); + $imageElement->appendChild(new \DOMElement('link', $podcast->getWebsite())); } $episodes = $this->entityManager->createQuery( @@ -68,31 +64,31 @@ class FeedService .' AND e.audioFilename IS NOT NULL' )->setParameter('podcastId', $podcast->getId())->getResult(); foreach ($episodes as $episode) { - $audioUrl = $this->baseFeedUrl . $episode->getAudioFilename(); // Also used as ID. - $itemElement = new DOMElement('item'); + $audioUrl = $this->baseFeedUrl.$episode->getAudioFilename(); // Also used as ID. + $itemElement = new \DOMElement('item'); $channelElement->appendChild($itemElement); - $titleElement = new DOMElement('title'); + $titleElement = new \DOMElement('title'); $itemElement->appendChild($titleElement); - $titleElement->appendChild(new DOMText($episode->getTitle())); - $itemElement->appendChild(new DOMElement('description', $episode->getDescription())); - $itemElement->appendChild(new DOMElement('link', $audioUrl)); - $itemElement->appendChild(new DOMElement('guid', $audioUrl)); + $titleElement->appendChild(new \DOMText($episode->getTitle())); + $itemElement->appendChild(new \DOMElement('description', $episode->getDescription())); + $itemElement->appendChild(new \DOMElement('link', $audioUrl)); + $itemElement->appendChild(new \DOMElement('guid', $audioUrl)); - $filepath = + $filepath = ($this->parameterBag->get('kernel.project_dir') ?: '') - . '/' - . Constants::BASE_PUBLIC_DIR - . Constants::FILES_BASE_PATH - . $episode->getAudioFilename(); + .'/' + .Constants::BASE_PUBLIC_DIR + .Constants::FILES_BASE_PATH + .$episode->getAudioFilename(); // Use FFProbe to get the media duration, probably the easiest way but requires a working FFMpeg install. $duration = floatval(FFProbe::create()->format($filepath)->get('duration')); $duration_minutes = intval($duration / 60); $duration_seconds = str_pad(strval(intval($duration % 60)), 2, '0', STR_PAD_LEFT); $duration_str = "{$duration_minutes}:{$duration_seconds}"; - $itemElement->appendChild(new DOMElement('itunes:duration', $duration_str, self::ITUNES_NS)); + $itemElement->appendChild(new \DOMElement('itunes:duration', $duration_str, self::ITUNES_NS)); - $enclosureElement = new DOMElement('enclosure'); + $enclosureElement = new \DOMElement('enclosure'); $itemElement->appendChild($enclosureElement); $enclosureElement->setAttribute('url', $audioUrl); $enclosureElement->setAttribute('type', mime_content_type($filepath)); diff --git a/src/Service/YoutubeService.php b/src/Service/YoutubeService.php index 90a4206..a852449 100644 --- a/src/Service/YoutubeService.php +++ b/src/Service/YoutubeService.php @@ -2,14 +2,15 @@ namespace App\Service; -use App\Constants; use Psr\Log\LoggerInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; class YoutubeService { - public function __construct(protected LoggerInterface $logger) {} + public function __construct(protected LoggerInterface $logger) + { + } public function download(string $url): string|false { @@ -18,16 +19,16 @@ class YoutubeService '-x', '-O', 'after_move:filepath', '--restrict-filenames', '-P', sys_get_temp_dir(), - $url + escapeshellarg($url), ]); try { $process->mustRun(); - } - catch (ProcessFailedException $exception) { + } catch (ProcessFailedException $exception) { $this->logger->error( 'yt-dlp process failed: {error}', ['error' => $exception->getMessage()] ); + return false; } @@ -36,6 +37,7 @@ class YoutubeService 'Success for URL "{url}": {filepath}', ['url' => $url, 'filepath' => $filepath] ); + return $filepath; } }