From 2cb767229f633872d3b64d1790fe61c70a74cca4 Mon Sep 17 00:00:00 2001 From: dece Date: Sun, 1 Sep 2024 22:23:59 +0200 Subject: [PATCH] parameterize yt-dlp & ffprobe binary paths --- .env | 5 ++++ README.md | 26 +++++++++++++++++- src/MessageHandler/DownloadRequestHandler.php | 2 ++ src/Service/FeedService.php | 25 ++++++++++++----- src/Service/YoutubeService.php | 27 ++++++++++++------- 5 files changed, 68 insertions(+), 17 deletions(-) diff --git a/.env b/.env index 764b688..782280b 100644 --- a/.env +++ b/.env @@ -39,3 +39,8 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### # MAILER_DSN=null://null ###< symfony/mailer ### + +# Specify paths to the external programs you use to avoid path issues. +YTDLP_PATH="" +YTDLP_COOKIES_FILE="" +FFPROBE_PATH="" diff --git a/README.md b/README.md index c1cc54a..6fe1e06 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Lightweight Symfony Broadcast Client, probably. -A small platform to create podcasts and episodes, host the audio files and share the RSS feed, with external sources download abilities. +A small platform to create podcasts and episodes, host the audio files and share the RSS feed, with external sources download abilities. The end goal is to have a _minimalist_ platform to post stuff from the Web and mostly to enable posting from bots though the API, for shits and giggles. Features include: @@ -30,3 +30,27 @@ For production: 4. Create a database and its owner, then set appropriate database credentials in the config file. 5. Install dependencies: `composer install --no-dev`. 6. Apply database migrations: `php bin/console doctrine:migrations:migrate` + + + +## Usage + +Once installed visit the website and everything should be straightforward. Use `php bin/console list` from the command-line to check available commands. + +### Youtube and cookies + +As the Youtube download service relies on yt-dlp to download stuff and you might want to run LSBC on a server, Youtube will probably ask you to login, and providing a username and a password is not enough for them. yt-dlp will fail with an error message. + +What you need is a cookies export file. To produce one, run on your own computer `yt-dlp --cookies-from-browser firefox --cookies cookies.txt` to export your cookies to a text file. You can redact cookies that aren't related to Youtube, i.e. all lines not starting with ".youtube.com" (except the comments at the top, they are mandatory for some reason). Push that to your server and set the `YTDLP_COOKIES_FILE` env var accordingly. + + + +## About + +### Is it for me? + +Probably not. If you need a self-hosted podcasting platform, look at some great projects such as [Castopod](https://castopod.com/). LSBC is really not meant to do much. + +### License + +GPLv3. diff --git a/src/MessageHandler/DownloadRequestHandler.php b/src/MessageHandler/DownloadRequestHandler.php index 72e8ce4..1304a60 100644 --- a/src/MessageHandler/DownloadRequestHandler.php +++ b/src/MessageHandler/DownloadRequestHandler.php @@ -36,5 +36,7 @@ class DownloadRequestHandler $this->em->persist($episode); $this->em->flush(); + + $this->logger->info('Episode saved.', ['title' => $episode->getTitle()]); } } diff --git a/src/Service/FeedService.php b/src/Service/FeedService.php index c89c4a6..44d4338 100644 --- a/src/Service/FeedService.php +++ b/src/Service/FeedService.php @@ -6,6 +6,7 @@ use App\Constants; use App\Entity\Podcast; use Doctrine\ORM\EntityManagerInterface; use FFMpeg\FFProbe; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -17,6 +18,7 @@ class FeedService public function __construct( protected EntityManagerInterface $entityManager, protected ParameterBagInterface $parameterBag, + #[Autowire('%env(FFPROBE_PATH)%')] protected string $ffprobePath, UrlGeneratorInterface $urlGenerator, ) { $rootUrl = $urlGenerator->generate( @@ -82,12 +84,8 @@ class FeedService .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)); + $duration = $this->getFileDuration($filepath); + $itemElement->appendChild(new \DOMElement('itunes:duration', $duration, self::ITUNES_NS)); $enclosureElement = new \DOMElement('enclosure'); $itemElement->appendChild($enclosureElement); @@ -98,4 +96,19 @@ class FeedService return $document->saveXML(); } + + /** Use FFProbe to get the media duration, probably the easiest way but requires a working FFMpeg install. */ + protected static function getFileDuration(string $filepath): string + { + $ffprobeConfig = []; + if ($this->ffprobePath) { + $ffprobeConfig['ffprobe.binaries'] = $this->ffprobePath; + } + $ffprobe = FFProbe::create($ffprobeConfig); + $duration = floatval($ffprobe->format($filepath)->get('duration')); + $durationMinutes = intval($duration / 60); + $durationSeconds = str_pad(strval(intval($duration % 60)), 2, '0', STR_PAD_LEFT); + $durationStr = "{$durationMinutes}:{$durationSeconds}"; + return $durationStr; + } } diff --git a/src/Service/YoutubeService.php b/src/Service/YoutubeService.php index 748b046..7d01008 100644 --- a/src/Service/YoutubeService.php +++ b/src/Service/YoutubeService.php @@ -3,31 +3,38 @@ namespace App\Service; use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; 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, + #[Autowire('%env(YTDLP_PATH)%')] protected string $ytdlpPath, + #[Autowire('%env(YTDLP_COOKIES_PATH)%')] protected string $ytdlpCookiesPath, + ) { } public function download(string $url): string|false { - $process = new Process([ - 'yt-dlp', + $processArgs = [ + $this->ytdlpPath ?: 'yt-dlp', '-x', '-O', 'after_move:filepath', '--restrict-filenames', '-P', sys_get_temp_dir(), - $url, - ]); + ]; + if ($this->ytdlpCookiesPath) { + $processArgs[] = '--cookies'; + $processArgs[] = $this->ytdlpCookiesPath; + } + $processArgs[] = $url; + + $process = new Process($processArgs); try { $process->mustRun(); } catch (ProcessFailedException $exception) { - $this->logger->error( - 'yt-dlp process failed: {error}', - ['error' => $exception->getMessage()] - ); + $this->logger->error('yt-dlp process failed: {error}', ['error' => $exception->getMessage()]); return false; }