parameterize yt-dlp & ffprobe binary paths

This commit is contained in:
dece 2024-09-01 22:23:59 +02:00
parent ec1fcd6e5e
commit 2cb767229f
5 changed files with 68 additions and 17 deletions

5
.env
View file

@ -39,3 +39,8 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###> symfony/mailer ### ###> symfony/mailer ###
# MAILER_DSN=null://null # MAILER_DSN=null://null
###< symfony/mailer ### ###< symfony/mailer ###
# Specify paths to the external programs you use to avoid path issues.
YTDLP_PATH=""
YTDLP_COOKIES_FILE=""
FFPROBE_PATH=""

View file

@ -2,7 +2,7 @@
> Lightweight Symfony Broadcast Client, probably. > 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: Features include:
@ -30,3 +30,27 @@ For production:
4. Create a database and its owner, then set appropriate database credentials in the config file. 4. Create a database and its owner, then set appropriate database credentials in the config file.
5. Install dependencies: `composer install --no-dev`. 5. Install dependencies: `composer install --no-dev`.
6. Apply database migrations: `php bin/console doctrine:migrations:migrate` 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.

View file

@ -36,5 +36,7 @@ class DownloadRequestHandler
$this->em->persist($episode); $this->em->persist($episode);
$this->em->flush(); $this->em->flush();
$this->logger->info('Episode saved.', ['title' => $episode->getTitle()]);
} }
} }

View file

@ -6,6 +6,7 @@ use App\Constants;
use App\Entity\Podcast; use App\Entity\Podcast;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use FFMpeg\FFProbe; use FFMpeg\FFProbe;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@ -17,6 +18,7 @@ class FeedService
public function __construct( public function __construct(
protected EntityManagerInterface $entityManager, protected EntityManagerInterface $entityManager,
protected ParameterBagInterface $parameterBag, protected ParameterBagInterface $parameterBag,
#[Autowire('%env(FFPROBE_PATH)%')] protected string $ffprobePath,
UrlGeneratorInterface $urlGenerator, UrlGeneratorInterface $urlGenerator,
) { ) {
$rootUrl = $urlGenerator->generate( $rootUrl = $urlGenerator->generate(
@ -82,12 +84,8 @@ class FeedService
.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. $duration = $this->getFileDuration($filepath);
$duration = floatval(FFProbe::create()->format($filepath)->get('duration')); $itemElement->appendChild(new \DOMElement('itunes:duration', $duration, self::ITUNES_NS));
$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));
$enclosureElement = new \DOMElement('enclosure'); $enclosureElement = new \DOMElement('enclosure');
$itemElement->appendChild($enclosureElement); $itemElement->appendChild($enclosureElement);
@ -98,4 +96,19 @@ class FeedService
return $document->saveXML(); 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;
}
} }

View file

@ -3,31 +3,38 @@
namespace App\Service; namespace App\Service;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
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,
#[Autowire('%env(YTDLP_PATH)%')] protected string $ytdlpPath,
#[Autowire('%env(YTDLP_COOKIES_PATH)%')] protected string $ytdlpCookiesPath,
) {
} }
public function download(string $url): string|false public function download(string $url): string|false
{ {
$process = new Process([ $processArgs = [
'yt-dlp', $this->ytdlpPath ?: 'yt-dlp',
'-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, ];
]); if ($this->ytdlpCookiesPath) {
$processArgs[] = '--cookies';
$processArgs[] = $this->ytdlpCookiesPath;
}
$processArgs[] = $url;
$process = new Process($processArgs);
try { try {
$process->mustRun(); $process->mustRun();
} catch (ProcessFailedException $exception) { } catch (ProcessFailedException $exception) {
$this->logger->error( $this->logger->error('yt-dlp process failed: {error}', ['error' => $exception->getMessage()]);
'yt-dlp process failed: {error}',
['error' => $exception->getMessage()]
);
return false; return false;
} }