parameterize yt-dlp & ffprobe binary paths
This commit is contained in:
parent
ec1fcd6e5e
commit
2cb767229f
5
.env
5
.env
|
@ -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=""
|
||||||
|
|
26
README.md
26
README.md
|
@ -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.
|
||||||
|
|
|
@ -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()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue