fix a few warnings and format files

This commit is contained in:
dece 2024-08-31 23:16:39 +02:00
parent 85a5cba500
commit 45ea878f17
6 changed files with 62 additions and 65 deletions

View file

@ -1,5 +1,4 @@
LSBC # LSBC
====
> Lightweight Symfony Broadcast Client, probably. > Lightweight Symfony Broadcast Client, probably.
@ -15,16 +14,15 @@ Features include:
- TODO the API let's you quickly add whole episodes from Youtube links - 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 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: This project requires:
- PHP 8.2 - PHP 8.3
- PostgreSQL 15 and its PHP driver - PostgreSQL 15 and its PHP driver
For production: For production:

View file

@ -18,7 +18,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
#[AsCommand( #[AsCommand(
name: 'app:download', 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 class DownloadCommand extends Command
{ {
@ -27,8 +27,7 @@ class DownloadCommand extends Command
protected EntityManagerInterface $entityManager, protected EntityManagerInterface $entityManager,
protected PodcastRepository $podcastRepository, protected PodcastRepository $podcastRepository,
protected ParameterBagInterface $parameterBag, protected ParameterBagInterface $parameterBag,
) ) {
{
parent::__construct(); parent::__construct();
} }
@ -50,24 +49,26 @@ class DownloadCommand extends Command
$slug = $input->getArgument('podcast'); $slug = $input->getArgument('podcast');
$podcast = $this->podcastRepository->findOneBy(['slug' => $slug]); $podcast = $this->podcastRepository->findOneBy(['slug' => $slug]);
if ($podcast === null) { if (null === $podcast) {
$io->error("No podcast with slug $slug."); $io->error("No podcast with slug $slug.");
return Command::FAILURE; return Command::FAILURE;
} }
$filepath = $this->downloadService->download($url); $filepath = $this->downloadService->download($url);
if ($filepath === false) { if (false === $filepath) {
$io->error('Download failed.'); $io->error('Download failed.');
return Command::FAILURE; return Command::FAILURE;
} }
$filename = basename($filepath); $filename = basename($filepath);
$publicFilepath = $publicFilepath =
$this->parameterBag->get('kernel.project_dir') $this->parameterBag->get('kernel.project_dir')
. '/' .'/'
. Constants::BASE_PUBLIC_DIR .Constants::BASE_PUBLIC_DIR
. Constants::FILES_BASE_PATH .Constants::FILES_BASE_PATH
. $filename; .$filename;
rename($filepath, $publicFilepath); rename($filepath, $publicFilepath);
$episode = new Episode(); $episode = new Episode();

View file

@ -26,7 +26,7 @@ class EpisodeController extends AbstractController
protected PodcastRepository $podcastRepo, protected PodcastRepository $podcastRepo,
protected EntityManagerInterface $em, protected EntityManagerInterface $em,
protected LoggerInterface $logger, 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'])] #[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); $queryPodcastId = $request->query->getInt('podcast', 0);
$episode = new Episode(); $episode = new Episode();
@ -59,7 +60,7 @@ class EpisodeController extends AbstractController
$podcast = $this->podcastRepo->find($podcastId ?? $queryPodcastId); $podcast = $this->podcastRepo->find($podcastId ?? $queryPodcastId);
if ( if (
null === $podcast null === $podcast
|| $podcast->getOwner()?->getId() !== $this->getUser()?->getId() || $podcast->getOwner()?->getUserIdentifier() !== $this->getUser()?->getUserIdentifier()
) { ) {
$form->get('podcast')->addError(new FormError('Invalid podcast.')); $form->get('podcast')->addError(new FormError('Invalid podcast.'));
@ -91,7 +92,7 @@ class EpisodeController extends AbstractController
{ {
return $this->render('episode/show.html.twig', [ return $this->render('episode/show.html.twig', [
'episode' => $episode, 'episode' => $episode,
'files_path' => Constants::FILES_BASE_PATH 'files_path' => Constants::FILES_BASE_PATH,
]); ]);
} }

View file

@ -21,16 +21,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180, unique: true)] #[ORM\Column(length: 180, unique: true)]
private ?string $email = null; private string $email;
#[ORM\Column] #[ORM\Column]
private array $roles = []; private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column] #[ORM\Column]
private ?string $password = null; private string $password;
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: Podcast::class)] #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Podcast::class)]
private Collection $podcasts; private Collection $podcasts;
@ -40,17 +37,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->podcasts = new ArrayCollection(); $this->podcasts = new ArrayCollection();
} }
public function __toString() public function __toString(): string
{ {
return "$this->email ($this->id)"; return "$this->email ($this->id)";
} }
public function getId(): ?int public function getId(): int
{ {
return $this->id; return $this->id;
} }
public function getEmail(): ?string public function getEmail(): string
{ {
return $this->email; return $this->email;
} }
@ -84,6 +81,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return array_unique($roles); return array_unique($roles);
} }
/**
* @param array<int,mixed> $roles
*/
public function setRoles(array $roles): self public function setRoles(array $roles): self
{ {
$this->roles = $roles; $this->roles = $roles;
@ -106,7 +106,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function eraseCredentials(): void public function eraseCredentials(): void
{ {
} }

View file

@ -4,9 +4,6 @@ namespace App\Service;
use App\Constants; use App\Constants;
use App\Entity\Podcast; use App\Entity\Podcast;
use DOMDocument;
use DOMElement;
use DOMText;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use FFMpeg\FFProbe; use FFMpeg\FFProbe;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
@ -21,15 +18,14 @@ class FeedService
protected EntityManagerInterface $entityManager, protected EntityManagerInterface $entityManager,
protected ParameterBagInterface $parameterBag, protected ParameterBagInterface $parameterBag,
UrlGeneratorInterface $urlGenerator, UrlGeneratorInterface $urlGenerator,
) ) {
{
$rootUrl = $urlGenerator->generate( $rootUrl = $urlGenerator->generate(
'app_index', 'app_index',
[], [],
UrlGeneratorInterface::ABSOLUTE_URL UrlGeneratorInterface::ABSOLUTE_URL
); );
$this->baseFeedUrl = $rootUrl . Constants::FILES_BASE_PATH; $this->baseFeedUrl = $rootUrl.Constants::FILES_BASE_PATH;
$this->baseImageUrl = $rootUrl . Constants::IMAGES_BASE_PATH; $this->baseImageUrl = $rootUrl.Constants::IMAGES_BASE_PATH;
} }
public const XML_NS = 'http://www.w3.org/2000/xmlns/'; public const XML_NS = 'http://www.w3.org/2000/xmlns/';
@ -38,7 +34,7 @@ class FeedService
public function generate(Podcast $podcast): string|false 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 = $document->createElement('rss');
$rssElement->setAttribute('version', '2.0'); $rssElement->setAttribute('version', '2.0');
@ -46,20 +42,20 @@ class FeedService
$rssElement->setAttributeNS(self::XML_NS, 'xmlns:itunes', self::ITUNES_NS); $rssElement->setAttributeNS(self::XML_NS, 'xmlns:itunes', self::ITUNES_NS);
$document->appendChild($rssElement); $document->appendChild($rssElement);
$channelElement = new DOMElement('channel'); $channelElement = new \DOMElement('channel');
$rssElement->appendChild($channelElement); $rssElement->appendChild($channelElement);
$titleElement = new DOMElement('title'); $titleElement = new \DOMElement('title');
$channelElement->appendChild($titleElement); $channelElement->appendChild($titleElement);
$titleElement->appendChild(new DOMText($podcast->getName())); $titleElement->appendChild(new \DOMText($podcast->getName()));
$channelElement->appendChild(new DOMElement('description', $podcast->getDescription())); $channelElement->appendChild(new \DOMElement('description', $podcast->getDescription()));
$channelElement->appendChild(new DOMElement('link', $podcast->getWebsite())); $channelElement->appendChild(new \DOMElement('link', $podcast->getWebsite()));
$logoFilename = $podcast->getLogoFilename(); $logoFilename = $podcast->getLogoFilename();
if ($logoFilename !== null) { if (null !== $logoFilename) {
$imageElement = new DOMElement('image'); $imageElement = new \DOMElement('image');
$channelElement->appendChild($imageElement); $channelElement->appendChild($imageElement);
$imageElement->appendChild(new DOMElement('url', $this->baseImageUrl . $logoFilename)); $imageElement->appendChild(new \DOMElement('url', $this->baseImageUrl.$logoFilename));
$imageElement->appendChild(new DOMElement('title', 'logo')); $imageElement->appendChild(new \DOMElement('title', 'logo'));
$imageElement->appendChild(new DOMElement('link', $podcast->getWebsite())); $imageElement->appendChild(new \DOMElement('link', $podcast->getWebsite()));
} }
$episodes = $this->entityManager->createQuery( $episodes = $this->entityManager->createQuery(
@ -68,31 +64,31 @@ class FeedService
.' AND e.audioFilename IS NOT NULL' .' AND e.audioFilename IS NOT NULL'
)->setParameter('podcastId', $podcast->getId())->getResult(); )->setParameter('podcastId', $podcast->getId())->getResult();
foreach ($episodes as $episode) { foreach ($episodes as $episode) {
$audioUrl = $this->baseFeedUrl . $episode->getAudioFilename(); // Also used as ID. $audioUrl = $this->baseFeedUrl.$episode->getAudioFilename(); // Also used as ID.
$itemElement = new DOMElement('item'); $itemElement = new \DOMElement('item');
$channelElement->appendChild($itemElement); $channelElement->appendChild($itemElement);
$titleElement = new DOMElement('title'); $titleElement = new \DOMElement('title');
$itemElement->appendChild($titleElement); $itemElement->appendChild($titleElement);
$titleElement->appendChild(new DOMText($episode->getTitle())); $titleElement->appendChild(new \DOMText($episode->getTitle()));
$itemElement->appendChild(new DOMElement('description', $episode->getDescription())); $itemElement->appendChild(new \DOMElement('description', $episode->getDescription()));
$itemElement->appendChild(new DOMElement('link', $audioUrl)); $itemElement->appendChild(new \DOMElement('link', $audioUrl));
$itemElement->appendChild(new DOMElement('guid', $audioUrl)); $itemElement->appendChild(new \DOMElement('guid', $audioUrl));
$filepath = $filepath =
($this->parameterBag->get('kernel.project_dir') ?: '') ($this->parameterBag->get('kernel.project_dir') ?: '')
. '/' .'/'
. Constants::BASE_PUBLIC_DIR .Constants::BASE_PUBLIC_DIR
. Constants::FILES_BASE_PATH .Constants::FILES_BASE_PATH
. $episode->getAudioFilename(); .$episode->getAudioFilename();
// Use FFProbe to get the media duration, probably the easiest way but requires a working FFMpeg install. // 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 = floatval(FFProbe::create()->format($filepath)->get('duration'));
$duration_minutes = intval($duration / 60); $duration_minutes = intval($duration / 60);
$duration_seconds = str_pad(strval(intval($duration % 60)), 2, '0', STR_PAD_LEFT); $duration_seconds = str_pad(strval(intval($duration % 60)), 2, '0', STR_PAD_LEFT);
$duration_str = "{$duration_minutes}:{$duration_seconds}"; $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); $itemElement->appendChild($enclosureElement);
$enclosureElement->setAttribute('url', $audioUrl); $enclosureElement->setAttribute('url', $audioUrl);
$enclosureElement->setAttribute('type', mime_content_type($filepath)); $enclosureElement->setAttribute('type', mime_content_type($filepath));

View file

@ -2,14 +2,15 @@
namespace App\Service; namespace App\Service;
use App\Constants;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
class YoutubeService class YoutubeService
{ {
public function __construct(protected LoggerInterface $logger) {} public function __construct(protected LoggerInterface $logger)
{
}
public function download(string $url): string|false public function download(string $url): string|false
{ {
@ -18,16 +19,16 @@ class YoutubeService
'-x', '-x',
'-O', 'after_move:filepath', '--restrict-filenames', '-O', 'after_move:filepath', '--restrict-filenames',
'-P', sys_get_temp_dir(), '-P', sys_get_temp_dir(),
$url escapeshellarg($url),
]); ]);
try { try {
$process->mustRun(); $process->mustRun();
} } catch (ProcessFailedException $exception) {
catch (ProcessFailedException $exception) {
$this->logger->error( $this->logger->error(
'yt-dlp process failed: {error}', 'yt-dlp process failed: {error}',
['error' => $exception->getMessage()] ['error' => $exception->getMessage()]
); );
return false; return false;
} }
@ -36,6 +37,7 @@ class YoutubeService
'Success for URL "{url}": {filepath}', 'Success for URL "{url}": {filepath}',
['url' => $url, 'filepath' => $filepath] ['url' => $url, 'filepath' => $filepath]
); );
return $filepath; return $filepath;
} }
} }