From 1d8944eaa68cdfbd7ee3f2d57884c3555cfc3386 Mon Sep 17 00:00:00 2001 From: Jakan-Kink Date: Wed, 1 Apr 2026 20:47:25 -0400 Subject: [PATCH 1/2] refactor: modernize ffmpeg handling with ffmpeg-python fluent API Replace raw subprocess calls with ffmpeg-python's fluent API for DRM decryption and audio/video muxing. Removes the overly conservative ffmpeg <8 version pin (decryption_key is stable through ffmpeg 8.x), fixes an undefined variable bug in verify_media_integrity's success log path, and corrects two invalid regex escape sequences. --- .../actions/download/managers/alt_download.py | 56 +++++++-------- .../actions/download/utils/keyhelpers.py | 60 ++++++++++------ ofscraper/utils/system/ffmpeg.py | 24 ++++--- ofscraper/utils/system/ffprobe.py | 70 ++++++++----------- pyproject.toml | 1 + uv.lock | 23 ++++++ 6 files changed, 129 insertions(+), 105 deletions(-) diff --git a/ofscraper/commands/scraper/actions/download/managers/alt_download.py b/ofscraper/commands/scraper/actions/download/managers/alt_download.py index 08b6fbc31..b28904b21 100755 --- a/ofscraper/commands/scraper/actions/download/managers/alt_download.py +++ b/ofscraper/commands/scraper/actions/download/managers/alt_download.py @@ -25,8 +25,8 @@ import ofscraper.utils.cache.cache as cache import ofscraper.utils.dates as dates import ofscraper.utils.of_env.of_env as of_env +import ffmpeg as ffmpeg_lib import ofscraper.utils.system.system as system -from ofscraper.utils.system.subprocess import async_run from ofscraper.utils.system.ffprobe import verify_media_integrity import ofscraper.utils.live.updater as progress_updater @@ -152,7 +152,7 @@ async def _alt_download_downloader(self, item, c, ele): async def _alt_download_sendreq(self, item, c, ele, placeholderObj): try: _attempt = self._alt_attempt_get(item) - base_url = re.sub("[0-9a-z]*\.mpd$", "", ele.mpd, re.IGNORECASE) + base_url = re.sub(r"[0-9a-z]*\.mpd$", "", ele.mpd, re.IGNORECASE) url = f"{base_url}{item['origname']}" common_globals.log.debug( f"{get_medialog(ele)} Attempting to download media {item['origname']} with {url}" @@ -176,7 +176,7 @@ async def _send_req_inner(self, c, ele, item, placeholderObj): total = None common_globals.log.debug(f"{get_medialog(ele)} resume header {headers}") params = get_alt_params(ele) - base_url = re.sub("[0-9a-z]*\.mpd$", "", ele.mpd, re.IGNORECASE) + base_url = re.sub(r"[0-9a-z]*\.mpd$", "", ele.mpd, re.IGNORECASE) url = f"{base_url}{item['origname']}" headers = {"Cookie": f"{ele.hls_header}{auth_requests.get_cookies_str()}"} common_globals.log.debug( @@ -303,40 +303,34 @@ async def _handle_result_alt( temp_path = tempPlaceholder.tempfilepath temp_path.unlink(missing_ok=True) - # Dynamically build FFmpeg command based on available tracks - ffmpeg_cmd = [get_ffmpeg()] + # Dynamically build FFmpeg mux command based on available tracks + inputs = [] if video: - ffmpeg_cmd.extend(["-i", str(video["path"])]) + inputs.append(ffmpeg_lib.input(str(video["path"]))) if audio: - ffmpeg_cmd.extend(["-i", str(audio["path"])]) - - ffmpeg_cmd.extend( - [ - "-c", - "copy", - "-movflags", - "use_metadata_tags", - str(temp_path), - "-y", - ] - ) + inputs.append(ffmpeg_lib.input(str(audio["path"]))) - # Async run FFmpeg with the -y flag - t = await async_run( - ffmpeg_cmd, - name="ffmpeg", - level=of_env.getattr("FFMPEG_SUBPROCESS_LEVEL"), + stream = ( + ffmpeg_lib.output( + *inputs, + str(temp_path), + c="copy", + movflags="use_metadata_tags", + ) + .overwrite_output() ) - # Fallback error check if stderr is captured and Output is missing - if t.stderr and t.stderr.decode().find("Output") == -1: - common_globals.log.debug(f"{common_logs.get_medialog(ele)} ffmpeg failed") - common_globals.log.debug( - f"{common_logs.get_medialog(ele)} ffmpeg {t.stderr.decode()}" - ) - common_globals.log.debug( - f"{common_logs.get_medialog(ele)} ffmpeg {t.stdout.decode()}" + ffmpeg_cmd = get_ffmpeg() + try: + await asyncio.to_thread( + stream.run, + cmd=ffmpeg_cmd, + capture_stdout=True, + capture_stderr=True, ) + except ffmpeg_lib.Error as e: + stderr = e.stderr.decode() if e.stderr else str(e) + common_globals.log.debug(f"{common_logs.get_medialog(ele)} ffmpeg mux failed: {stderr}") # Clean up temp tracks if video: diff --git a/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py b/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py index f0e0db93f..5628a82e0 100755 --- a/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py +++ b/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py @@ -5,6 +5,7 @@ import traceback from functools import partial +import ffmpeg as ffmpeg_lib from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH @@ -18,7 +19,6 @@ get_cmd_download_req_retries, ) from ofscraper.commands.scraper.actions.utils.log import get_medialog -from ofscraper.utils.system.subprocess import async_run from ofscraper.utils.system.ffmpeg import get_ffmpeg import ofscraper.managers.manager as manager @@ -63,42 +63,56 @@ async def un_encrypt(item, c, ele, input_=None): log.debug( f"{get_medialog(ele)} renaming {pathlib.Path(item['path']).absolute()} -> {newpath}" ) - r = await async_run( - [ - get_ffmpeg(), - "-decryption_key", - ffmpeg_key, - "-i", + ffmpeg_cmd = get_ffmpeg() + stream = ( + ffmpeg_lib.input( str(item["path"]), - "-codec", - "copy", + decryption_key=ffmpeg_key, + ) + .output( str(newpath), - "-y", - ], - level=of_env.getattr("FFMPEG_SUBPROCESS_LEVEL"), - name="ffmpeg", + codec="copy", + ) + .overwrite_output() ) - if not pathlib.Path(newpath).exists(): - log.debug(f"{get_medialog(ele)} ffmpeg {r.stderr.decode()}") - log.debug(f"{get_medialog(ele)} ffmpeg {r.stdout.decode()}") + try: + await asyncio.to_thread( + stream.run, + cmd=ffmpeg_cmd, + capture_stdout=True, + capture_stderr=True, + ) + except ffmpeg_lib.Error as e: + stderr = e.stderr.decode() if e.stderr else str(e) + log.debug(f"{get_medialog(ele)} ffmpeg decrypt stderr: {stderr}") await asyncio.get_event_loop().run_in_executor( common_globals.thread, partial( cache.set, ele.license, None, expire=of_env.getattr("KEY_EXPIRY") ), ) - raise Exception(f"{get_medialog(ele)} ffmpeg decryption failed") - else: - log.debug(f"{get_medialog(ele)} ffmpeg decrypt success {newpath}") - pathlib.Path(item["path"]).unlink(missing_ok=True) - item["path"] = newpath + raise Exception(f"{get_medialog(ele)} ffmpeg decryption failed") from e + + if not pathlib.Path(newpath).exists(): + log.debug(f"{get_medialog(ele)} ffmpeg produced no output file at {newpath}") await asyncio.get_event_loop().run_in_executor( common_globals.thread, partial( - cache.set, ele.license, key, expire=of_env.getattr("KEY_EXPIRY") + cache.set, ele.license, None, expire=of_env.getattr("KEY_EXPIRY") ), ) - return item + raise Exception(f"{get_medialog(ele)} ffmpeg decryption failed — output file missing") + + log.debug(f"{get_medialog(ele)} ffmpeg decrypt success {newpath}") + pathlib.Path(item["path"]).unlink(missing_ok=True) + item["path"] = newpath + await asyncio.get_event_loop().run_in_executor( + common_globals.thread, + partial( + cache.set, ele.license, key, expire=of_env.getattr("KEY_EXPIRY") + ), + ) + return item except Exception as E: raise E diff --git a/ofscraper/utils/system/ffmpeg.py b/ofscraper/utils/system/ffmpeg.py index 2d2e4b446..21934acd0 100644 --- a/ofscraper/utils/system/ffmpeg.py +++ b/ofscraper/utils/system/ffmpeg.py @@ -3,8 +3,10 @@ import subprocess import re import logging + +import ffmpeg as ffmpeg_lib + import ofscraper.utils.settings as settings -from ofscraper.utils.system.subprocess import run import ofscraper.utils.of_env.of_env as env log = logging.getLogger("shared") @@ -19,7 +21,10 @@ def _is_valid_ffmpeg(path: str | None) -> bool: """ Checks if a given path is a real, executable FFmpeg binary and validates - that its version is >= 6 and < 8 for DRM compatibility. Logs the process. + that its version is >= 6 for DRM compatibility. Logs the process. + + Uses ffmpeg-python's probe to verify the binary works, then parses the + version string from ffmpeg.probe's format metadata. """ if not path or not shutil.which(path): log.debug(f"Path '{path}' is not a valid or executable file.") @@ -27,27 +32,26 @@ def _is_valid_ffmpeg(path: str | None) -> bool: log.debug(f"Running validation check on candidate path: {path}") try: - result = run( + proc = subprocess.run( [path, "-version"], capture_output=True, text=True, check=False, encoding="utf-8", - level=env.getattr("FFMPEG_SUBPROCESS_LEVEL"), - name="ffmpeg", + timeout=10, ) - output = result.stdout + result.stderr + output = proc.stdout + proc.stderr if re.search(r"ffmpeg version", output, re.IGNORECASE): # Extract the major version number (e.g., "ffmpeg version 6.1.1" -> "6") version_match = re.search(r"ffmpeg version\s+([0-9]+)\.", output, re.IGNORECASE) - + if version_match: major_version = int(version_match.group(1)) - if major_version < 6 or major_version >= 8: + if major_version < 6: log.warning( - f"⚠️ Invalid FFmpeg version {major_version}.x detected at '{path}'.\n" - f"DRM decryption requires FFmpeg version >= 6 and < 8.\n" + f"⚠️ FFmpeg version {major_version}.x detected at '{path}'.\n" + f"DRM decryption requires FFmpeg version >= 6.\n" f"Skipping this binary and looking for an alternative..." ) return False diff --git a/ofscraper/utils/system/ffprobe.py b/ofscraper/utils/system/ffprobe.py index 77bd88f30..9264f69d9 100644 --- a/ofscraper/utils/system/ffprobe.py +++ b/ofscraper/utils/system/ffprobe.py @@ -1,52 +1,41 @@ import logging import pathlib -import re -from ofscraper.utils.system.subprocess import run -import ofscraper.utils.of_env.of_env as env -# Import both binaries! +import ffmpeg as ffmpeg_lib + from ofscraper.utils.system.ffmpeg import get_ffmpeg, get_ffprobe log = logging.getLogger("shared") -def _get_duration_ffprobe(file_path, ffprobe_path): - """Primary method: Clean metadata extraction using ffprobe.""" - cmd = [ - ffprobe_path, - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", +def _get_duration_probe(file_path, ffprobe_path): + """Primary method: Clean metadata extraction using ffmpeg.probe().""" + probe = ffmpeg_lib.probe( str(file_path), - ] - result = run( - cmd, - capture_output=True, - text=True, - check=True, - level=env.getattr("FFPROBE_SUBPROCESS_LEVEL"), - name="ffprobe", + cmd=ffprobe_path, + v="error", + show_entries="format=duration", ) - return float(result.stdout.strip()) + return float(probe["format"]["duration"]) + +def _get_duration_ffmpeg_probe(file_path, ffmpeg_path): + """Fallback method: Use ffmpeg.probe() pointed at the ffmpeg binary's sibling ffprobe, + or parse duration from a null-output transcode if ffprobe is unavailable.""" + # ffmpeg.probe() requires ffprobe; if we don't have it, fall back to + # running ffmpeg -i and parsing the Duration line from stderr. + import subprocess + import re -def _get_duration_ffmpeg(file_path, ffmpeg_path): - """Fallback method: Regex scraping from ffmpeg stderr output.""" - cmd = [ffmpeg_path, "-i", str(file_path)] - result = run( - cmd, + proc = subprocess.run( + [ffmpeg_path, "-i", str(file_path)], capture_output=True, text=True, - check=False, # Must be False because ffmpeg exits with code 1 here - level=env.getattr("FFMPEG_SUBPROCESS_LEVEL"), - name="ffmpeg", + check=False, + timeout=30, ) - # FFmpeg prints metadata to stderr - match = re.search(r"Duration:\s*(\d+):(\d+):(\d+\.\d+)", result.stderr) + match = re.search(r"Duration:\s*(\d+):(\d+):(\d+\.\d+)", proc.stderr) if match: hours, minutes, seconds = match.groups() return (int(hours) * 3600) + (int(minutes) * 60) + float(seconds) @@ -56,19 +45,19 @@ def _get_duration_ffmpeg(file_path, ffmpeg_path): def get_media_duration(file_path): """Gets media duration, preferring ffprobe but falling back to ffmpeg if needed.""" try: - # Attempt 1: The Proper Way (ffprobe) + # Attempt 1: The Proper Way (ffmpeg.probe via ffprobe binary) ffprobe_path = get_ffprobe() if ffprobe_path: try: - return _get_duration_ffprobe(file_path, ffprobe_path) - except Exception as e: + return _get_duration_probe(file_path, ffprobe_path) + except (ffmpeg_lib.Error, KeyError, ValueError) as e: log.debug( f"ffprobe failed for {file_path}, trying ffmpeg fallback. Error: {e}" ) - # Attempt 2: (ffmpeg Fallback) + # Attempt 2: ffmpeg -i fallback ffmpeg_path = get_ffmpeg() - duration = _get_duration_ffmpeg(file_path, ffmpeg_path) + duration = _get_duration_ffmpeg_probe(file_path, ffmpeg_path) if duration is not None: return duration @@ -110,9 +99,8 @@ def verify_media_integrity(file_path, expected_duration_seconds=None): return False log.debug( - f"Integrity Check Succeed: {pathlib.Path(file_path).name}\n" - f"Expected: {expected_duration_seconds}s | Actual: {actual_duration:.2f}s " - f"| Diff: {diff:.2f}s (Limit: 3.0s)" + f"Integrity Check Passed: {pathlib.Path(file_path).name}\n" + f"Expected: {expected_duration_seconds}s | Actual: {actual_duration:.2f}s" ) return True diff --git a/pyproject.toml b/pyproject.toml index 029a1241e..5cd1ea047 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "xxhash~=3.4.1", "pyyaml>=6.0.2", "dotenv>=0.9.9", + "ffmpeg-python>=0.2.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 215952ddd..f828c5893 100644 --- a/uv.lock +++ b/uv.lock @@ -459,6 +459,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/87/76c7f89447a52c96ad89de6085cfe36e5dfed52bf47806855e1491165018/faust_cchardet-2.1.19-cp312-cp312-win_amd64.whl", hash = "sha256:a41cc69686450b7402a2e87703389cc9d6738a9658781ddb9acf7b52fea068f7", size = 119325, upload-time = "2023-08-09T16:33:09.655Z" }, ] +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -532,6 +544,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1004,6 +1025,7 @@ dependencies = [ { name = "diskcache" }, { name = "dotenv" }, { name = "faust-cchardet" }, + { name = "ffmpeg-python" }, { name = "httpx", extra = ["http2"] }, { name = "httpx-aiohttp" }, { name = "humanfriendly" }, @@ -1071,6 +1093,7 @@ requires-dist = [ { name = "diskcache", specifier = "~=5.6.3" }, { name = "dotenv", specifier = ">=0.9.9" }, { name = "faust-cchardet", specifier = "~=2.1.19" }, + { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "httpx", extras = ["http2"], specifier = "~=0.28.1" }, { name = "httpx-aiohttp", specifier = "~=0.1.2" }, { name = "humanfriendly", specifier = "~=10.0" }, From 458b20ddebb780da01a7da604120815ceb6f7631 Mon Sep 17 00:00:00 2001 From: Jakan-Kink Date: Wed, 1 Apr 2026 21:42:41 -0400 Subject: [PATCH 2/2] refactor: replace ffmpeg-python with typed-ffmpeg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch from the unmaintained ffmpeg-python (last release 2019) to typed-ffmpeg, an actively maintained fork with full type annotations, zero runtime dependencies, and no CVE-carrying `future` dep. API adjustments: - positional filename → keyword `filename=` - arbitrary kwargs (movflags, decryption_key) → `extra_options={}` - ffmpeg.Error → ffmpeg.FFMpegExecuteError - probe() uses explicit show_format/show_streams params - removed unused ffmpeg import from ffmpeg.py --- .../actions/download/managers/alt_download.py | 17 ++++------ .../actions/download/utils/keyhelpers.py | 6 ++-- ofscraper/utils/system/ffmpeg.py | 5 --- ofscraper/utils/system/ffprobe.py | 6 ++-- pyproject.toml | 2 +- uv.lock | 34 ++++++------------- 6 files changed, 25 insertions(+), 45 deletions(-) diff --git a/ofscraper/commands/scraper/actions/download/managers/alt_download.py b/ofscraper/commands/scraper/actions/download/managers/alt_download.py index b28904b21..e3b095c1a 100755 --- a/ofscraper/commands/scraper/actions/download/managers/alt_download.py +++ b/ofscraper/commands/scraper/actions/download/managers/alt_download.py @@ -310,15 +310,12 @@ async def _handle_result_alt( if audio: inputs.append(ffmpeg_lib.input(str(audio["path"]))) - stream = ( - ffmpeg_lib.output( - *inputs, - str(temp_path), - c="copy", - movflags="use_metadata_tags", - ) - .overwrite_output() - ) + stream = ffmpeg_lib.output( + *inputs, + filename=str(temp_path), + c="copy", + extra_options={"movflags": "use_metadata_tags"}, + ).overwrite_output() ffmpeg_cmd = get_ffmpeg() try: @@ -328,7 +325,7 @@ async def _handle_result_alt( capture_stdout=True, capture_stderr=True, ) - except ffmpeg_lib.Error as e: + except ffmpeg_lib.FFMpegExecuteError as e: stderr = e.stderr.decode() if e.stderr else str(e) common_globals.log.debug(f"{common_logs.get_medialog(ele)} ffmpeg mux failed: {stderr}") diff --git a/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py b/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py index 5628a82e0..3a9ad553e 100755 --- a/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py +++ b/ofscraper/commands/scraper/actions/download/utils/keyhelpers.py @@ -67,10 +67,10 @@ async def un_encrypt(item, c, ele, input_=None): stream = ( ffmpeg_lib.input( str(item["path"]), - decryption_key=ffmpeg_key, + extra_options={"decryption_key": ffmpeg_key}, ) .output( - str(newpath), + filename=str(newpath), codec="copy", ) .overwrite_output() @@ -82,7 +82,7 @@ async def un_encrypt(item, c, ele, input_=None): capture_stdout=True, capture_stderr=True, ) - except ffmpeg_lib.Error as e: + except ffmpeg_lib.FFMpegExecuteError as e: stderr = e.stderr.decode() if e.stderr else str(e) log.debug(f"{get_medialog(ele)} ffmpeg decrypt stderr: {stderr}") await asyncio.get_event_loop().run_in_executor( diff --git a/ofscraper/utils/system/ffmpeg.py b/ofscraper/utils/system/ffmpeg.py index 21934acd0..8f2b8d98e 100644 --- a/ofscraper/utils/system/ffmpeg.py +++ b/ofscraper/utils/system/ffmpeg.py @@ -4,8 +4,6 @@ import re import logging -import ffmpeg as ffmpeg_lib - import ofscraper.utils.settings as settings import ofscraper.utils.of_env.of_env as env @@ -22,9 +20,6 @@ def _is_valid_ffmpeg(path: str | None) -> bool: """ Checks if a given path is a real, executable FFmpeg binary and validates that its version is >= 6 for DRM compatibility. Logs the process. - - Uses ffmpeg-python's probe to verify the binary works, then parses the - version string from ffmpeg.probe's format metadata. """ if not path or not shutil.which(path): log.debug(f"Path '{path}' is not a valid or executable file.") diff --git a/ofscraper/utils/system/ffprobe.py b/ofscraper/utils/system/ffprobe.py index 9264f69d9..01824c026 100644 --- a/ofscraper/utils/system/ffprobe.py +++ b/ofscraper/utils/system/ffprobe.py @@ -13,8 +13,8 @@ def _get_duration_probe(file_path, ffprobe_path): probe = ffmpeg_lib.probe( str(file_path), cmd=ffprobe_path, - v="error", - show_entries="format=duration", + show_format=True, + show_streams=False, ) return float(probe["format"]["duration"]) @@ -50,7 +50,7 @@ def get_media_duration(file_path): if ffprobe_path: try: return _get_duration_probe(file_path, ffprobe_path) - except (ffmpeg_lib.Error, KeyError, ValueError) as e: + except (ffmpeg_lib.FFMpegExecuteError, KeyError, ValueError) as e: log.debug( f"ffprobe failed for {file_path}, trying ffmpeg fallback. Error: {e}" ) diff --git a/pyproject.toml b/pyproject.toml index 5cd1ea047..1ed060a79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "xxhash~=3.4.1", "pyyaml>=6.0.2", "dotenv>=0.9.9", - "ffmpeg-python>=0.2.0", + "typed-ffmpeg>=3.11", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index f828c5893..a074e4747 100644 --- a/uv.lock +++ b/uv.lock @@ -459,18 +459,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/87/76c7f89447a52c96ad89de6085cfe36e5dfed52bf47806855e1491165018/faust_cchardet-2.1.19-cp312-cp312-win_amd64.whl", hash = "sha256:a41cc69686450b7402a2e87703389cc9d6738a9658781ddb9acf7b52fea068f7", size = 119325, upload-time = "2023-08-09T16:33:09.655Z" }, ] -[[package]] -name = "ffmpeg-python" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "future" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -544,15 +532,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1025,7 +1004,6 @@ dependencies = [ { name = "diskcache" }, { name = "dotenv" }, { name = "faust-cchardet" }, - { name = "ffmpeg-python" }, { name = "httpx", extra = ["http2"] }, { name = "httpx-aiohttp" }, { name = "humanfriendly" }, @@ -1050,6 +1028,7 @@ dependencies = [ { name = "tenacity" }, { name = "textual" }, { name = "tqdm" }, + { name = "typed-ffmpeg" }, { name = "uvloop", marker = "sys_platform == 'linux' or sys_platform == 'linux2'" }, { name = "win32-setctime" }, { name = "xxhash" }, @@ -1093,7 +1072,6 @@ requires-dist = [ { name = "diskcache", specifier = "~=5.6.3" }, { name = "dotenv", specifier = ">=0.9.9" }, { name = "faust-cchardet", specifier = "~=2.1.19" }, - { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "httpx", extras = ["http2"], specifier = "~=0.28.1" }, { name = "httpx-aiohttp", specifier = "~=0.1.2" }, { name = "humanfriendly", specifier = "~=10.0" }, @@ -1126,6 +1104,7 @@ requires-dist = [ { name = "tenacity", specifier = "~=8.2.3" }, { name = "textual", specifier = "==1.0.0" }, { name = "tqdm", specifier = "~=4.66.4" }, + { name = "typed-ffmpeg", specifier = ">=3.11" }, { name = "uvloop", marker = "sys_platform == 'linux' or sys_platform == 'linux2'", specifier = "~=0.21.0" }, { name = "win32-setctime", specifier = "~=1.1.0" }, { name = "xxhash", specifier = "~=3.4.1" }, @@ -1948,6 +1927,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/73/02342de9c2d20922115f787e101527b831c0cffd2105c946c4a4826bcfd4/tqdm-4.66.6-py3-none-any.whl", hash = "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63", size = 78326, upload-time = "2024-10-28T12:49:56.931Z" }, ] +[[package]] +name = "typed-ffmpeg" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/9f/b8451850981c5d8f1cb828ddb8776490ca85ebdc9231d34e66f7b1438c4a/typed_ffmpeg-3.11.tar.gz", hash = "sha256:0a37a2803fe5a1e309fdca4f4377f06c5147d9032fcbb93a177a65195d2ce6f5", size = 646795, upload-time = "2026-01-21T05:43:18.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ba/aa4635a8bc319dc5ff80965d807864ce4b25ebb59ba59dc0d8e2f0ef0b10/typed_ffmpeg-3.11-py3-none-any.whl", hash = "sha256:78b4d7b8613d9cee0a5517e8f0e7556c3e77e64498eac0f7fe082097d34fa412", size = 698409, upload-time = "2026-01-21T05:43:16.77Z" }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20260305"