|
|
@ -6,7 +6,9 @@ from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
from .exceptions import ExtractionError
|
|
|
|
from .exceptions import ExtractionError
|
|
|
|
from .exceptions import InvalidSource
|
|
|
|
from .exceptions import InvalidSource
|
|
|
@ -23,6 +25,45 @@ FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "ffmpeg")
|
|
|
|
FFMPEG_STATS = True
|
|
|
|
FFMPEG_STATS = True
|
|
|
|
FF_LOG_LEVEL = "quiet"
|
|
|
|
FF_LOG_LEVEL = "quiet"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_PROGRESS_RE = re.compile(
|
|
|
|
|
|
|
|
r"size=\s*(\d+\w*B|N/A)\s+time=(\d+:\d+:\d+\.\d+)\s+bitrate=\s*([\d\.]+(?:e[\+\-]?\d+)?\w*bits/s|N/A)\s+speed=([\d\.]+(?:e[\+\-]?\d+)?x|N/A)"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ffmpeg_call(command, log_callback=None, progress_callback=None, timeout=10000):
|
|
|
|
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
|
|
|
|
command,
|
|
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
|
|
universal_newlines=True,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
log_callback = log_callback or logger.debug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
line = proc.stderr.readline()
|
|
|
|
|
|
|
|
if not line:
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if line:
|
|
|
|
|
|
|
|
log_callback("ffmpeg: %s", line.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if progress_callback is not None:
|
|
|
|
|
|
|
|
match = _PROGRESS_RE.search(line)
|
|
|
|
|
|
|
|
if match:
|
|
|
|
|
|
|
|
size, time_, bitrate, speed = match.groups()
|
|
|
|
|
|
|
|
info = {"size": size, "time": time_, "bitrate": bitrate, "speed": speed}
|
|
|
|
|
|
|
|
progress_callback(info)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if timeout is not None and time.time() - start > timeout:
|
|
|
|
|
|
|
|
proc.kill()
|
|
|
|
|
|
|
|
raise subprocess.TimeoutExpired(command, timeout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return_code = proc.wait()
|
|
|
|
|
|
|
|
if return_code != 0:
|
|
|
|
|
|
|
|
raise subprocess.CalledProcessError(return_code, command)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FFprobeVideoContainer:
|
|
|
|
class FFprobeVideoContainer:
|
|
|
|
def __init__(self, path: str):
|
|
|
|
def __init__(self, path: str):
|
|
|
@ -81,6 +122,8 @@ class FFprobeVideoContainer:
|
|
|
|
overwrite=True,
|
|
|
|
overwrite=True,
|
|
|
|
timeout=600,
|
|
|
|
timeout=600,
|
|
|
|
convert_format=None,
|
|
|
|
convert_format=None,
|
|
|
|
|
|
|
|
basename_callback=None,
|
|
|
|
|
|
|
|
progress_callback=None,
|
|
|
|
):
|
|
|
|
):
|
|
|
|
"""Extracts a list of subtitles converting them. Returns a dictionary of the
|
|
|
|
"""Extracts a list of subtitles converting them. Returns a dictionary of the
|
|
|
|
extracted filenames by index.
|
|
|
|
extracted filenames by index.
|
|
|
@ -95,6 +138,9 @@ class FFprobeVideoContainer:
|
|
|
|
:param timeout: subprocess timeout in seconds (default: 600)
|
|
|
|
:param timeout: subprocess timeout in seconds (default: 600)
|
|
|
|
:param convert_format: format to convert selected subtitles. Defaults to
|
|
|
|
:param convert_format: format to convert selected subtitles. Defaults to
|
|
|
|
srt
|
|
|
|
srt
|
|
|
|
|
|
|
|
:param basename_callback: a callback that takes the filename path. Only used if
|
|
|
|
|
|
|
|
custom_dir is set. Defaults to `os.path.basename`
|
|
|
|
|
|
|
|
:progress_callback: a callback that takes a dict
|
|
|
|
:raises: ExtractionError, UnsupportedCodec, OSError
|
|
|
|
:raises: ExtractionError, UnsupportedCodec, OSError
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
extract_command = [FFMPEG_PATH, "-v", FF_LOG_LEVEL]
|
|
|
|
extract_command = [FFMPEG_PATH, "-v", FF_LOG_LEVEL]
|
|
|
@ -116,7 +162,8 @@ class FFprobeVideoContainer:
|
|
|
|
f"{os.path.splitext(self.path)[0]}.{subtitle.suffix}.{extension_to_use}"
|
|
|
|
f"{os.path.splitext(self.path)[0]}.{subtitle.suffix}.{extension_to_use}"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if custom_dir is not None:
|
|
|
|
if custom_dir is not None:
|
|
|
|
sub_path = os.path.join(custom_dir, os.path.basename(sub_path))
|
|
|
|
basename_callback = basename_callback or os.path.basename
|
|
|
|
|
|
|
|
sub_path = os.path.join(custom_dir, basename_callback(sub_path))
|
|
|
|
|
|
|
|
|
|
|
|
if not overwrite and sub_path in collected_paths:
|
|
|
|
if not overwrite and sub_path in collected_paths:
|
|
|
|
sub_path = f"{os.path.splitext(sub_path)[0]}.{len(collected_paths):02}.{extension_to_use}"
|
|
|
|
sub_path = f"{os.path.splitext(sub_path)[0]}.{len(collected_paths):02}.{extension_to_use}"
|
|
|
@ -139,7 +186,10 @@ class FFprobeVideoContainer:
|
|
|
|
logger.debug("Extracting subtitle with command %s", " ".join(extract_command))
|
|
|
|
logger.debug("Extracting subtitle with command %s", " ".join(extract_command))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
subprocess.run(extract_command, timeout=timeout, check=True)
|
|
|
|
# subprocess.run(extract_command, timeout=timeout, check=True)
|
|
|
|
|
|
|
|
_ffmpeg_call(
|
|
|
|
|
|
|
|
extract_command, timeout=timeout, progress_callback=progress_callback
|
|
|
|
|
|
|
|
)
|
|
|
|
except (subprocess.SubprocessError, FileNotFoundError) as error:
|
|
|
|
except (subprocess.SubprocessError, FileNotFoundError) as error:
|
|
|
|
raise ExtractionError(f"Error calling ffmpeg: {error}") from error
|
|
|
|
raise ExtractionError(f"Error calling ffmpeg: {error}") from error
|
|
|
|
|
|
|
|
|
|
|
@ -156,6 +206,8 @@ class FFprobeVideoContainer:
|
|
|
|
overwrite=True,
|
|
|
|
overwrite=True,
|
|
|
|
timeout=600,
|
|
|
|
timeout=600,
|
|
|
|
fallback_to_convert=True,
|
|
|
|
fallback_to_convert=True,
|
|
|
|
|
|
|
|
basename_callback=None,
|
|
|
|
|
|
|
|
progress_callback=None,
|
|
|
|
):
|
|
|
|
):
|
|
|
|
"""Extracts a list of subtitles with ffmpeg's copy method. Returns a dictionary
|
|
|
|
"""Extracts a list of subtitles with ffmpeg's copy method. Returns a dictionary
|
|
|
|
of the extracted filenames by index.
|
|
|
|
of the extracted filenames by index.
|
|
|
@ -167,6 +219,9 @@ class FFprobeVideoContainer:
|
|
|
|
:param timeout: subprocess timeout in seconds (default: 600)
|
|
|
|
:param timeout: subprocess timeout in seconds (default: 600)
|
|
|
|
:param fallback_to_convert: fallback to stream's default convert format if it is
|
|
|
|
:param fallback_to_convert: fallback to stream's default convert format if it is
|
|
|
|
incompatible with copy
|
|
|
|
incompatible with copy
|
|
|
|
|
|
|
|
:param basename_callback: a callback that takes the filename path. Only used if
|
|
|
|
|
|
|
|
custom_dir is set. Defaults to `os.path.basename`
|
|
|
|
|
|
|
|
:progress_callback: a callback that takes a dict
|
|
|
|
:raises: ExtractionError, UnsupportedCodec, OSError
|
|
|
|
:raises: ExtractionError, UnsupportedCodec, OSError
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
extract_command = [FFMPEG_PATH, "-v", FF_LOG_LEVEL]
|
|
|
|
extract_command = [FFMPEG_PATH, "-v", FF_LOG_LEVEL]
|
|
|
@ -184,7 +239,8 @@ class FFprobeVideoContainer:
|
|
|
|
for subtitle in subtitles:
|
|
|
|
for subtitle in subtitles:
|
|
|
|
sub_path = f"{os.path.splitext(self.path)[0]}.{subtitle.suffix}.{subtitle.extension}"
|
|
|
|
sub_path = f"{os.path.splitext(self.path)[0]}.{subtitle.suffix}.{subtitle.extension}"
|
|
|
|
if custom_dir is not None:
|
|
|
|
if custom_dir is not None:
|
|
|
|
sub_path = os.path.join(custom_dir, os.path.basename(sub_path))
|
|
|
|
basename_callback = basename_callback or os.path.basename
|
|
|
|
|
|
|
|
sub_path = os.path.join(custom_dir, basename_callback(sub_path))
|
|
|
|
|
|
|
|
|
|
|
|
if not overwrite and sub_path in collected_paths:
|
|
|
|
if not overwrite and sub_path in collected_paths:
|
|
|
|
sub_path = f"{os.path.splitext(sub_path)[0]}.{len(collected_paths):02}.{subtitle.extension}"
|
|
|
|
sub_path = f"{os.path.splitext(sub_path)[0]}.{len(collected_paths):02}.{subtitle.extension}"
|
|
|
@ -216,7 +272,10 @@ class FFprobeVideoContainer:
|
|
|
|
logger.debug("Extracting subtitle with command %s", " ".join(extract_command))
|
|
|
|
logger.debug("Extracting subtitle with command %s", " ".join(extract_command))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
subprocess.run(extract_command, timeout=timeout, check=True)
|
|
|
|
# subprocess.run(extract_command, timeout=timeout, check=True)
|
|
|
|
|
|
|
|
_ffmpeg_call(
|
|
|
|
|
|
|
|
extract_command, timeout=timeout, progress_callback=progress_callback
|
|
|
|
|
|
|
|
)
|
|
|
|
except (subprocess.SubprocessError, FileNotFoundError) as error:
|
|
|
|
except (subprocess.SubprocessError, FileNotFoundError) as error:
|
|
|
|
raise ExtractionError(f"Error calling ffmpeg: {error}") from error
|
|
|
|
raise ExtractionError(f"Error calling ffmpeg: {error}") from error
|
|
|
|
|
|
|
|
|
|
|
|