From 916d7baec20efd1b26b63a358ea7d6807d1d6871 Mon Sep 17 00:00:00 2001 From: dece Date: Sun, 25 Feb 2024 22:51:24 +0100 Subject: [PATCH] add human-readable duration in feed --- composer.json | 1 + composer.lock | 199 +++++++++++++++++++++++++++++++++++- src/Service/FeedService.php | 32 ++++-- 3 files changed, 223 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 5ff17f2..fbb1819 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-bundle": "^2.9", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.15", + "php-ffmpeg/php-ffmpeg": "^1.2", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.20", "sensio/framework-extra-bundle": "^6.1", diff --git a/composer.lock b/composer.lock index 3579db8..014565e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c241853e026884d05d25f49b85867c16", + "content-hash": "8adb110d727d28dd8b1233bbe99e92de", "packages": [ { "name": "doctrine/annotations", @@ -1463,6 +1463,53 @@ ], "time": "2023-01-14T14:17:03+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, { "name": "monolog/monolog", "version": "3.4.0", @@ -1564,6 +1611,95 @@ ], "time": "2023-06-21T08:46:11+00:00" }, + { + "name": "php-ffmpeg/php-ffmpeg", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git", + "reference": "785a5ba05dd88b3b8146f85f18476b259b23917c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/785a5ba05dd88b3b8146f85f18476b259b23917c", + "reference": "785a5ba05dd88b3b8146f85f18476b259b23917c", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0", + "php": "^8.0 || ^8.1 || ^8.2 || ^8.3", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "spatie/temporary-directory": "^2.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "phpunit/phpunit": "^9.5.10" + }, + "suggest": { + "php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg" + }, + "type": "library", + "autoload": { + "psr-4": { + "FFMpeg\\": "src/FFMpeg", + "Alchemy\\BinaryDriver\\": "src/Alchemy/BinaryDriver" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Romain Neutron", + "email": "imprec@gmail.com", + "homepage": "http://www.lickmychip.com/" + }, + { + "name": "Phraseanet Team", + "email": "info@alchemy.fr", + "homepage": "http://www.phraseanet.com/" + }, + { + "name": "Patrik Karisch", + "email": "patrik@karisch.guru", + "homepage": "http://www.karisch.guru" + }, + { + "name": "Romain Biard", + "email": "romain.biard@gmail.com", + "homepage": "https://www.strime.io/" + }, + { + "name": "Jens Hausdorf", + "email": "hello@jens-hausdorf.de", + "homepage": "https://jens-hausdorf.de" + }, + { + "name": "Pascal Baljet", + "email": "pascal@protone.media", + "homepage": "https://protone.media" + } + ], + "description": "FFMpeg PHP, an Object Oriented library to communicate with AVconv / ffmpeg", + "keywords": [ + "audio", + "audio processing", + "avconv", + "avprobe", + "ffmpeg", + "ffprobe", + "video", + "video processing" + ], + "support": { + "issues": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/issues", + "source": "https://github.com/PHP-FFMpeg/PHP-FFMpeg/tree/v1.2.0" + }, + "time": "2024-01-02T10:37:01+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2115,6 +2251,67 @@ "abandoned": "Symfony", "time": "2023-02-24T14:57:12+00:00" }, + { + "name": "spatie/temporary-directory", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", + "reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.2.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2023-12-25T11:46:58+00:00" + }, { "name": "symfony/asset", "version": "v6.2.7", diff --git a/src/Service/FeedService.php b/src/Service/FeedService.php index f92f45d..a108744 100644 --- a/src/Service/FeedService.php +++ b/src/Service/FeedService.php @@ -8,6 +8,7 @@ use DOMDocument; use DOMElement; use DOMText; use Doctrine\ORM\EntityManagerInterface; +use FFMpeg\FFProbe; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -31,14 +32,20 @@ class FeedService $this->baseImageUrl = $rootUrl . Constants::IMAGES_BASE_PATH; } - public const DOCUMENT_VERSION = '2.0'; + public const XML_NS = 'http://www.w3.org/2000/xmlns/'; + public const ATOM_NS = 'http://www.w3.org/2005/Atom'; + public const ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'; public function generate(Podcast $podcast): string|false { - $document = new DOMDocument(); - $rssElement = new DOMElement('rss'); + $document = new DOMDocument(version: '1.0', encoding: 'UTF-8'); + + $rssElement = $document->createElement('rss'); + $rssElement->setAttribute('version', '2.0'); + $rssElement->setAttributeNS(self::XML_NS, 'xmlns:atom', self::ATOM_NS); + $rssElement->setAttributeNS(self::XML_NS, 'xmlns:itunes', self::ITUNES_NS); $document->appendChild($rssElement); - $rssElement->setAttribute('version', $this::DOCUMENT_VERSION); + $channelElement = new DOMElement('channel'); $rssElement->appendChild($channelElement); $titleElement = new DOMElement('title'); @@ -70,15 +77,24 @@ class FeedService $itemElement->appendChild(new DOMElement('description', $episode->getDescription())); $itemElement->appendChild(new DOMElement('link', $audioUrl)); $itemElement->appendChild(new DOMElement('guid', $audioUrl)); - $enclosureElement = new DOMElement('enclosure'); - $itemElement->appendChild($enclosureElement); - $enclosureElement->setAttribute('url', $audioUrl); + $filepath = - $this->parameterBag->get('kernel.project_dir') + ($this->parameterBag->get('kernel.project_dir') ?: '') . '/' . Constants::BASE_PUBLIC_DIR . 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)); + + $enclosureElement = new DOMElement('enclosure'); + $itemElement->appendChild($enclosureElement); + $enclosureElement->setAttribute('url', $audioUrl); $enclosureElement->setAttribute('type', mime_content_type($filepath)); $enclosureElement->setAttribute('length', (string) filesize($filepath)); }