From 8a3d92b04b2c00a791e10825fc69b19037a88b63 Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Tue, 28 Feb 2023 14:44:31 -0800 Subject: [PATCH] ffmpeg + ffprobe paths --- .gitignore | 3 +- src/ytdl_sub/config/config_validator.py | 36 +++++++++++++++++-- src/ytdl_sub/plugins/split_by_chapters.py | 5 ++- .../subscriptions/subscription_download.py | 7 ++++ .../subscription_ytdl_options.py | 2 ++ src/ytdl_sub/utils/chapters.py | 6 ++-- src/ytdl_sub/utils/ffmpeg.py | 26 ++++++++++---- 7 files changed, 73 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index f7230e46..4d638c33 100644 --- a/.gitignore +++ b/.gitignore @@ -144,4 +144,5 @@ docker/root/defaults/examples .local/ -ffmpeg.exe \ No newline at end of file +ffmpeg.exe +ffprobe.exe \ No newline at end of file diff --git a/src/ytdl_sub/config/config_validator.py b/src/ytdl_sub/config/config_validator.py index 88b13dac..cf5ad38b 100644 --- a/src/ytdl_sub/config/config_validator.py +++ b/src/ytdl_sub/config/config_validator.py @@ -1,3 +1,4 @@ +import tempfile from typing import Any from typing import Dict from typing import Optional @@ -5,14 +6,24 @@ from typing import Optional from mergedeep import mergedeep from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS +from ytdl_sub.utils.system import IS_WINDOWS from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.validators import LiteralDictValidator from ytdl_sub.validators.validators import StringValidator +if IS_WINDOWS: + _DEFAULT_LOCK_DIRECTORY = tempfile.TemporaryDirectory().name + _DEFAULT_FFMPEG_PATH = ".\\ffmpeg.exe" + _DEFAULT_FFPROBE_PATH = ".\\ffprobe.exe" +else: + _DEFAULT_LOCK_DIRECTORY = "/tmp" + _DEFAULT_FFMPEG_PATH = "/usr/bin/ffmpeg" + _DEFAULT_FFPROBE_PATH = "/usr/bin/ffprobe" + class ConfigOptions(StrictDictValidator): _required_keys = {"working_directory"} - _optional_keys = {"umask", "dl_aliases", "lock_directory"} + _optional_keys = {"umask", "dl_aliases", "lock_directory", "ffmpeg_path", "ffprobe_path"} def __init__(self, name: str, value: Any): super().__init__(name, value) @@ -27,7 +38,14 @@ class ConfigOptions(StrictDictValidator): key="dl_aliases", validator=LiteralDictValidator ) self._lock_directory = self._validate_key( - key="lock_directory", validator=StringValidator, default="/tmp" + key="lock_directory", validator=StringValidator, default=_DEFAULT_LOCK_DIRECTORY + ) + # TODO: Validate these exist + self._ffmpeg_path = self._validate_key( + key="ffmpeg_path", validator=StringValidator, default=_DEFAULT_FFMPEG_PATH + ) + self._ffprobe_path = self._validate_key( + key="ffprobe_path", validator=StringValidator, default=_DEFAULT_FFPROBE_PATH ) @property @@ -82,6 +100,20 @@ class ConfigOptions(StrictDictValidator): """ return self._lock_directory.value + @property + def ffmpeg_path(self) -> str: + """ + TODO: Fill out! + """ + return self._ffmpeg_path.value + + @property + def ffprobe_path(self) -> str: + """ + TODO: Fill out! + """ + return self._ffprobe_path.value + class ConfigValidator(StrictDictValidator): _required_keys = {"configuration", "presets"} diff --git a/src/ytdl_sub/plugins/split_by_chapters.py b/src/ytdl_sub/plugins/split_by_chapters.py index 5f531c40..447d6f25 100644 --- a/src/ytdl_sub/plugins/split_by_chapters.py +++ b/src/ytdl_sub/plugins/split_by_chapters.py @@ -168,7 +168,10 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]): if self.is_dry_run: chapters = Chapters.from_entry_chapters(entry=entry) else: - chapters = Chapters.from_embedded_chapters(file_path=entry.get_download_file_path()) + chapters = Chapters.from_embedded_chapters( + ffprobe_path=FFMPEG.ffprobe_path(), + file_path=entry.get_download_file_path(), + ) # If no chapters, do not split anything if not chapters.contains_any_chapters(): diff --git a/src/ytdl_sub/subscriptions/subscription_download.py b/src/ytdl_sub/subscriptions/subscription_download.py index 05460ba1..1e9fb43c 100644 --- a/src/ytdl_sub/subscriptions/subscription_download.py +++ b/src/ytdl_sub/subscriptions/subscription_download.py @@ -11,6 +11,7 @@ from ytdl_sub.subscriptions.base_subscription import BaseSubscription from ytdl_sub.subscriptions.subscription_ytdl_options import SubscriptionYTDLOptions from ytdl_sub.utils.datetime import to_date_range from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.ffmpeg import FFMPEG from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.file_handler import FileMetadata @@ -265,6 +266,12 @@ class SubscriptionDownload(BaseSubscription, ABC): If true, do not download any video/audio files or move anything to the output directory. """ + # Set ffmpeg paths + FFMPEG.set_paths( + ffmpeg_path=self._config_options.ffmpeg_path, + ffprobe_path=self._config_options.ffprobe_path, + ) + self._enhanced_download_archive.reinitialize(dry_run=dry_run) plugins = self._initialize_plugins() diff --git a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py index e922f1ed..420e4661 100644 --- a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +++ b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py @@ -15,6 +15,7 @@ from ytdl_sub.plugins.file_convert import FileConvertPlugin from ytdl_sub.plugins.match_filters import MatchFiltersPlugin from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.subtitles import SubtitlesPlugin +from ytdl_sub.utils.ffmpeg import FFMPEG from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive PluginT = TypeVar("PluginT", bound=Plugin) @@ -57,6 +58,7 @@ class SubscriptionYTDLOptions: "outtmpl": str(Path(self._working_directory) / "%(id)s.%(ext)s"), # Always write thumbnails "writethumbnail": True, + "ffmpeg_location": FFMPEG.ffmpeg_path(), } return ytdl_options diff --git a/src/ytdl_sub/utils/chapters.py b/src/ytdl_sub/utils/chapters.py index 48ab2753..b3557774 100644 --- a/src/ytdl_sub/utils/chapters.py +++ b/src/ytdl_sub/utils/chapters.py @@ -221,10 +221,12 @@ class Chapters: return Chapters(timestamps=timestamps, titles=titles) @classmethod - def from_embedded_chapters(cls, file_path: str) -> "Chapters": + def from_embedded_chapters(cls, ffprobe_path: str, file_path: str) -> "Chapters": """ Parameters ---------- + ffprobe_path + Path to ffprobe executable file_path File to read ffmpeg chapter metadata from @@ -234,7 +236,7 @@ class Chapters: """ proc = subprocess.run( [ - "ffprobe", + ffprobe_path, "-loglevel", "quiet", "-print_format", diff --git a/src/ytdl_sub/utils/ffmpeg.py b/src/ytdl_sub/utils/ffmpeg.py index ef40a3e6..35a7b9c4 100644 --- a/src/ytdl_sub/utils/ffmpeg.py +++ b/src/ytdl_sub/utils/ffmpeg.py @@ -10,7 +10,6 @@ from ytdl_sub.utils.chapters import Chapters from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.system import IS_WINDOWS logger = Logger.get(name="ffmpeg") @@ -26,13 +25,28 @@ def _ffmpeg_metadata_escape(str_to_escape: str) -> str: class FFMPEG: + _FFMPEG_PATH: str = "" + _FFPROBE_PATH: str = "" + + @classmethod + def set_paths(cls, ffmpeg_path: str, ffprobe_path: str) -> None: + cls._FFMPEG_PATH = ffmpeg_path + cls._FFPROBE_PATH = ffprobe_path + + @classmethod + def ffmpeg_path(cls) -> str: + assert cls._FFMPEG_PATH, "ffmpeg has not been set" + return cls._FFMPEG_PATH + + @classmethod + def ffprobe_path(cls) -> str: + assert cls._FFPROBE_PATH, "ffprobe has not been set" + return cls._FFPROBE_PATH + @classmethod def _ensure_installed(cls): try: - if IS_WINDOWS: - subprocess.check_output([".\\ffmpeg", "-version"]) - else: - subprocess.check_output(["which", "ffmpeg"]) + subprocess.check_output([cls.ffmpeg_path(), "-version"]) except subprocess.CalledProcessError as subprocess_error: raise ValidationException( "Trying to use a feature which requires ffmpeg, but it cannot be found" @@ -69,7 +83,7 @@ class FFMPEG: """ cls._ensure_installed() - cmd = [".\\ffmpeg.exe" if IS_WINDOWS else "ffmpeg"] + cmd = [cls.ffmpeg_path()] cmd.extend(ffmpeg_args) logger.debug("Running %s", " ".join(cmd)) with Logger.handle_external_logs(name="ffmpeg"):