Add an episode from Youtube over the command-line
This commit is contained in:
parent
852b5f3e17
commit
40f91c8d1d
31
migrations/Version20230512222044.php
Normal file
31
migrations/Version20230512222044.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20230512222044 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
}
|
||||
}
|
32
migrations/Version20230512222100.php
Normal file
32
migrations/Version20230512222100.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20230512222100 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE episode ALTER description DROP NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SCHEMA public');
|
||||
$this->addSql('ALTER TABLE episode ALTER description SET NOT NULL');
|
||||
}
|
||||
}
|
86
src/Command/DownloadCommand.php
Normal file
86
src/Command/DownloadCommand.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Constants;
|
||||
use App\Entity\Episode;
|
||||
use App\Repository\PodcastRepository;
|
||||
use App\Service\DownloadService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:download',
|
||||
description: 'Add a short description for your command',
|
||||
)]
|
||||
class DownloadCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
protected DownloadService $downloadService,
|
||||
protected EntityManagerInterface $entityManager,
|
||||
protected PodcastRepository $podcastRepository,
|
||||
protected ParameterBagInterface $parameterBag,
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('url', InputArgument::REQUIRED, 'URL to download')
|
||||
->addArgument('podcast', InputArgument::REQUIRED, 'Podcast slug')
|
||||
->addOption('title', '-t', InputOption::VALUE_REQUIRED, 'Episode title')
|
||||
->addOption('description', '-d', InputOption::VALUE_REQUIRED, 'Episode description')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$url = $input->getArgument('url');
|
||||
$io->info(sprintf('Downloading: %s', $url));
|
||||
|
||||
$slug = $input->getArgument('podcast');
|
||||
$podcast = $this->podcastRepository->findOneBy(['slug' => $slug]);
|
||||
if ($podcast === null) {
|
||||
$io->error("No podcast with slug $slug.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$filepath = $this->downloadService->download($url);
|
||||
if ($filepath === false) {
|
||||
$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;
|
||||
rename($filepath, $publicFilepath);
|
||||
|
||||
$episode = new Episode();
|
||||
$episode->setTitle($input->getOption('title') ?: $filename);
|
||||
$episode->setDescription($input->getOption('description'));
|
||||
$episode->setAudioFilename($filename);
|
||||
$episode->setPodcast($podcast);
|
||||
$episode->setPublicationDate(date_create());
|
||||
$this->entityManager->persist($episode);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success("New Episode '{$episode->getTitle()}' added.");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ class PodcastCrudController extends AbstractCrudController
|
|||
{
|
||||
return [
|
||||
TextField::new('name'),
|
||||
TextField::new('slug'),
|
||||
UrlField::new('website'),
|
||||
TextEditorField::new('description'),
|
||||
TextField::new('author'),
|
||||
|
|
|
@ -9,7 +9,6 @@ use Doctrine\DBAL\Types\Types;
|
|||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EpisodeRepository::class)]
|
||||
##[Vich\Uploadable]
|
||||
class Episode
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
@ -20,8 +19,8 @@ class Episode
|
|||
#[ORM\Column(length: 255)]
|
||||
private string $title;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $description;
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $audioFilename;
|
||||
|
@ -50,12 +49,12 @@ class Episode
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): self
|
||||
public function setDescription(?string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@ use Doctrine\Common\Collections\ArrayCollection;
|
|||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PodcastRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[UniqueEntity('slug')]
|
||||
class Podcast
|
||||
{
|
||||
#[ORM\Id]
|
||||
|
@ -81,7 +82,6 @@ class Podcast
|
|||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function generateSlug(SluggerInterface $slugger): void
|
||||
{
|
||||
$this->slug = $this->id . '-' . $slugger->slug($this->name)->lower();
|
||||
|
|
26
src/EntityListener/PodcastEntityListener.php
Normal file
26
src/EntityListener/PodcastEntityListener.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\EntityListener;
|
||||
|
||||
use App\Entity\Podcast;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\Persistence\Event\LifecycleEventArgs;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
|
||||
#[AsEntityListener(event: Events::prePersist, entity: Podcast::class)]
|
||||
#[AsEntityListener(event: Events::preUpdate, entity: Podcast::class)]
|
||||
class PodcastEntityListener
|
||||
{
|
||||
public function __construct(private SluggerInterface $slugger) {}
|
||||
|
||||
public function prePersist(Podcast $podcast, LifecycleEventArgs $event)
|
||||
{
|
||||
$podcast->generateSlug($this->slugger);
|
||||
}
|
||||
|
||||
public function preUpdate(Podcast $podcast, LifecycleEventArgs $event)
|
||||
{
|
||||
$podcast->generateSlug($this->slugger);
|
||||
}
|
||||
}
|
24
src/Service/DownloadService.php
Normal file
24
src/Service/DownloadService.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Episode;
|
||||
use App\Entity\Podcast;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class DownloadService
|
||||
{
|
||||
public function __construct(
|
||||
protected LoggerInterface $logger,
|
||||
protected YoutubeService $youtubeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Download the audio from this URL and return its filepath.
|
||||
*/
|
||||
public function download(string $url): string|false
|
||||
{
|
||||
// For now assume every URL is a Youtube URL.
|
||||
return $this->youtubeService->download($url);
|
||||
}
|
||||
}
|
|
@ -13,18 +13,14 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|||
|
||||
class FeedService
|
||||
{
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected ParameterBagInterface $parameterBag;
|
||||
protected string $baseFeedUrl;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
ParameterBagInterface $parameterBag,
|
||||
protected EntityManagerInterface $entityManager,
|
||||
protected ParameterBagInterface $parameterBag,
|
||||
UrlGeneratorInterface $urlGenerator,
|
||||
)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->parameterBag = $parameterBag;
|
||||
$this->baseFeedUrl = $urlGenerator->generate(
|
||||
'app_index',
|
||||
[],
|
||||
|
|
41
src/Service/YoutubeService.php
Normal file
41
src/Service/YoutubeService.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
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 download(string $url): string|false
|
||||
{
|
||||
$process = new Process([
|
||||
'yt-dlp',
|
||||
'-x',
|
||||
'-O', 'after_move:filepath', '--restrict-filenames',
|
||||
'-P', sys_get_temp_dir(),
|
||||
$url
|
||||
]);
|
||||
try {
|
||||
$process->mustRun();
|
||||
}
|
||||
catch (ProcessFailedException $exception) {
|
||||
$this->logger->error(
|
||||
'yt-dlp process failed: {error}',
|
||||
['error' => $exception->getMessage()]
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$filepath = trim($process->getOutput());
|
||||
$this->logger->info(
|
||||
'Success for URL "{url}": {filepath}',
|
||||
['url' => $url, 'filepath' => $filepath]
|
||||
);
|
||||
return $filepath;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue