Compare commits
2
Commits
ff2ac0a236
...
97ef0e972c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97ef0e972c | ||
|
|
7e72611b1c |
Generated
+151
-7
@@ -67,6 +67,18 @@ files = [
|
|||||||
{file = "ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010"},
|
{file = "ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "astroid"
|
||||||
|
version = "4.0.4"
|
||||||
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"},
|
||||||
|
{file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.7.22"
|
version = "2026.7.22"
|
||||||
@@ -275,17 +287,39 @@ files = [
|
|||||||
markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
|
markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coloredlog"
|
name = "coloredlogs"
|
||||||
version = "0.2.5"
|
version = "15.0.1"
|
||||||
description = "Colorize your console output."
|
description = "Colored terminal output for Python's logging module"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "coloredlog-0.2.5-py3-none-any.whl", hash = "sha256:9334d73103cad8cb296248961710a8ce5a09ab63905905a81635fd7980538bb9"},
|
{file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"},
|
||||||
{file = "coloredlog-0.2.5.tar.gz", hash = "sha256:f17d3b4ed8acb4df346edb4a5b3c619769cdc88d2b2f3c5023e3f9d0b869d732"},
|
{file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
humanfriendly = ">=9.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cron = ["capturer (>=2.4)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dill"
|
||||||
|
version = "0.4.1"
|
||||||
|
description = "serialize all of Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"},
|
||||||
|
{file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
graph = ["objgraph (>=1.7.2)"]
|
||||||
|
profile = ["gprof2dot (>=2022.7.29)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hexdump"
|
name = "hexdump"
|
||||||
version = "3.3"
|
version = "3.3"
|
||||||
@@ -297,6 +331,21 @@ files = [
|
|||||||
{file = "hexdump-3.3.zip", hash = "sha256:d781a43b0c16ace3f9366aade73e8ad3a7bd5137d58f0b45ab2d3f54876f20db"},
|
{file = "hexdump-3.3.zip", hash = "sha256:d781a43b0c16ace3f9366aade73e8ad3a7bd5137d58f0b45ab2d3f54876f20db"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humanfriendly"
|
||||||
|
version = "10.0"
|
||||||
|
description = "Human friendly output for text interfaces using Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"},
|
||||||
|
{file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.19"
|
version = "3.19"
|
||||||
@@ -336,6 +385,21 @@ files = [
|
|||||||
{file = "iso639_lang-2.6.3.tar.gz", hash = "sha256:078ddb7cd0182dcc04367691acc8022ddf7158b6cb09f08f798af823fa864265"},
|
{file = "iso639_lang-2.6.3.tar.gz", hash = "sha256:078ddb7cd0182dcc04367691acc8022ddf7158b6cb09f08f798af823fa864265"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "8.0.1"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"},
|
||||||
|
{file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "librt"
|
name = "librt"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@@ -484,6 +548,18 @@ files = [
|
|||||||
{file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"},
|
{file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy"
|
name = "mypy"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -604,6 +680,18 @@ hyperscan = ["hyperscan (>=0.7)"]
|
|||||||
optional = ["typing-extensions (>=4)"]
|
optional = ["typing-extensions (>=4)"]
|
||||||
re2 = ["google-re2 (>=1.1)"]
|
re2 = ["google-re2 (>=1.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.11.5"
|
||||||
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b"},
|
||||||
|
{file = "platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -635,6 +723,50 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
windows-terminal = ["colorama (>=0.4.6)"]
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylint"
|
||||||
|
version = "4.0.7"
|
||||||
|
description = "python code static checker"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pylint-4.0.7-py3-none-any.whl", hash = "sha256:be4a3111557a614411ed1fc89347ce4a8e1013a59e1f33d11485227a02e3304d"},
|
||||||
|
{file = "pylint-4.0.7.tar.gz", hash = "sha256:9b2d1d15791c84b77a4fe2aafe8f0d9570717e2dea06d53b19c105cf60275a52"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
astroid = ">=4.0.2,<=4.1.dev0"
|
||||||
|
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||||
|
dill = [
|
||||||
|
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
|
||||||
|
{version = ">=0.3.6", markers = "python_version == \"3.11\""},
|
||||||
|
]
|
||||||
|
isort = ">=5,<5.13 || >5.13,<9"
|
||||||
|
mccabe = ">=0.6,<0.8"
|
||||||
|
platformdirs = ">=2.2"
|
||||||
|
tomlkit = ">=0.10.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
spelling = ["pyenchant (>=3.2,<4.0)"]
|
||||||
|
testutils = ["gitpython (>3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyreadline3"
|
||||||
|
version = "3.5.6"
|
||||||
|
description = "A python implementation of GNU readline."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "sys_platform == \"win32\""
|
||||||
|
files = [
|
||||||
|
{file = "pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"},
|
||||||
|
{file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["build", "flake8", "mypy", "pytest", "twine"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.1.1"
|
version = "9.1.1"
|
||||||
@@ -707,6 +839,18 @@ files = [
|
|||||||
{file = "ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b"},
|
{file = "ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomlkit"
|
||||||
|
version = "0.15.1"
|
||||||
|
description = "Style preserving TOML library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"},
|
||||||
|
{file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tqdm"
|
name = "tqdm"
|
||||||
version = "4.70.0"
|
version = "4.70.0"
|
||||||
@@ -776,4 +920,4 @@ test = ["pytest", "pytest-cov"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.11,<4"
|
python-versions = ">=3.11,<4"
|
||||||
content-hash = "cd4d9345c3a1c6ebd52426674ba386023c52defe06528b723305cf0d8980de5c"
|
content-hash = "bb1383a0bd739449d830d9146085c9dcd7f67d7ddaf93bf3e371b90f2d6c069c"
|
||||||
|
|||||||
+5
-3
@@ -8,13 +8,14 @@ authors = [
|
|||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11,<4"
|
requires-python = ">=3.11,<4"
|
||||||
dependencies = ["requests", "xmltodict", "coloredlog", "tqdm", "iso639-lang", "hexdump"]
|
dependencies = ["requests", "xmltodict", "coloredlogs", "tqdm", "iso639-lang", "hexdump"]
|
||||||
|
[tool.poetry]
|
||||||
packages = [
|
packages = [
|
||||||
{ include = "", from = "src" }
|
{ include = "tscut", from = "src" }
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
volvo-sensus-sync = "tscut:main"
|
tscut = "tscut:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
@@ -24,4 +25,5 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
ruff = "^0.16.2"
|
ruff = "^0.16.2"
|
||||||
mypy = "^2.3.0"
|
mypy = "^2.3.0"
|
||||||
pytest = "^9.1.1"
|
pytest = "^9.1.1"
|
||||||
|
pylint = "^4.0.7"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#
|
||||||
|
# Copyright (C) 2026 Frédéric Tronel
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
A tool to cut and reassemble parts of a video at the precision of a single frame.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.9.0"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#
|
||||||
|
# Copyright (C) 2026 Frédéric Tronel
|
||||||
|
|
||||||
|
"""Entry point for ``python -m tscut``."""
|
||||||
|
|
||||||
|
from tscut.cli import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#
|
||||||
|
# Copyright (C) 2026 Frédéric Tronel
|
||||||
|
|
||||||
|
"""Command-line interface for tscut."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from os import unlink
|
||||||
|
import os.path
|
||||||
|
from sys import exit
|
||||||
|
from datetime import datetime,timedelta
|
||||||
|
from shutil import copyfile, move
|
||||||
|
|
||||||
|
import coloredlogs
|
||||||
|
import hexdump
|
||||||
|
|
||||||
|
from .tscut import check_required_tools, parse_time_interval, cmp_to_key, get_format,\
|
||||||
|
SupportedFormat, compare_time_interval, ffmpeg_convert, parse_codec_private,\
|
||||||
|
dump_codec_private_data, get_nearest_iframe, get_movie_duration, extract_all_streams,\
|
||||||
|
extract_mkv_part, extract_srt, concatenate_h264_parts, concatenate_h264_ts_parts,\
|
||||||
|
do_ocr, get_frame_rate, get_streams, get_ts_frame, remove_video_tracks_from_mkv,\
|
||||||
|
merge_mkvs, extract_track_from_mkv, get_avc_config_from_h264,\
|
||||||
|
get_codec_private_data_from_mkv, find_subtitles_tracks, change_codec_private_data,\
|
||||||
|
get_tesseract_supported_lang, remux_srt_subtitles
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Run the tscut command-line interface."""
|
||||||
|
|
||||||
|
coloredlogs.install()
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-i", "--input", dest='input_file', type=str, required=True,
|
||||||
|
help="Input file to process (can be .ts, .mp4 or .mkv).")
|
||||||
|
parser.add_argument("-o", "--output", dest='output_file', type=str, required=True,
|
||||||
|
help="Output MKV file to produce.")
|
||||||
|
parser.add_argument("-p", "--part", dest='parts', nargs='+', required=False, action='append',
|
||||||
|
metavar="hh:mm:ss[.mmm]-hh:mm:ss[.mmm]",
|
||||||
|
help="Extract this exact part of the original file.")
|
||||||
|
parser.add_argument("-k", "--keep", action='store_true',
|
||||||
|
help="Do not cleanup temporary files after processing.")
|
||||||
|
parser.add_argument("-t", "--threshold", action='store', type=int,
|
||||||
|
help="Suppress headers and trailers that are smaller than the threshold.")
|
||||||
|
parser.add_argument("-c", "--coarse", action='store_true', dest='coarse',
|
||||||
|
help="Do not take trailers and headers into account.")
|
||||||
|
parser.add_argument("--dump-memory", action='store_true', dest='dump',
|
||||||
|
help="For debug purpose, dump all memory mapping of headers (and trailers)\
|
||||||
|
before (after) each part. They are kept in memory only otherwise.")
|
||||||
|
parser.add_argument("-s","--srt", action='store_true', dest='srt',
|
||||||
|
help="Dump subtitles and make OCR and finally remux them in the movie\
|
||||||
|
(as SRT).")
|
||||||
|
parser.add_argument("-v","--verbose", action='store_true', dest='verbose', help="Debug.")
|
||||||
|
parser.add_argument("-f","--framerate", action='store', type=int,
|
||||||
|
help="Override frame rate estimator.")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
logger.info('Arguments: %s', args)
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
logger.info('Setting logging to debug mode')
|
||||||
|
coloredlogs.set_level(level=logging.DEBUG)
|
||||||
|
|
||||||
|
logger.debug('Arguments: %s', args)
|
||||||
|
|
||||||
|
if args.coarse and args.threshold is not None:
|
||||||
|
logger.error('--coarse and threshold arguments are exclusive.')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
if (not args.coarse) and args.threshold is None:
|
||||||
|
args.threshold = 0
|
||||||
|
|
||||||
|
all_optional_tools, paths = check_required_tools()
|
||||||
|
|
||||||
|
# Flatten args.parts
|
||||||
|
intervals = []
|
||||||
|
if args.parts is not None:
|
||||||
|
for part in args.parts:
|
||||||
|
for subpart in part:
|
||||||
|
intervals.append(subpart)
|
||||||
|
|
||||||
|
parts=[]
|
||||||
|
# Parse each interval
|
||||||
|
for interval in intervals:
|
||||||
|
ts1, ts2 = parse_time_interval(interval)
|
||||||
|
if ts1 is None or ts2 is None:
|
||||||
|
logger.error("Illegal time interval: %s", interval)
|
||||||
|
exit(-1)
|
||||||
|
parts.append((ts1,ts2))
|
||||||
|
|
||||||
|
# Sort intervals
|
||||||
|
parts.sort(key=cmp_to_key(compare_time_interval))
|
||||||
|
|
||||||
|
# Check that no intervals are overlapping
|
||||||
|
prevts = timedelta(0)
|
||||||
|
for part in parts:
|
||||||
|
ts1, ts2 = part
|
||||||
|
if prevts > ts1:
|
||||||
|
logger.error('Intervals are overlapping')
|
||||||
|
exit(-1)
|
||||||
|
prevts = ts2
|
||||||
|
|
||||||
|
nb_parts = len(parts)
|
||||||
|
temporaries = []
|
||||||
|
|
||||||
|
basename = os.path.splitext(os.path.basename(args.input_file))[0]
|
||||||
|
mp4filename = basename+'.mp4'
|
||||||
|
mkvfilename = basename+'.mkv'
|
||||||
|
|
||||||
|
try:
|
||||||
|
input_file = open(args.input_file, mode='rb')
|
||||||
|
logger.debug("Type of input file: %s", type(input_file))
|
||||||
|
except IOError:
|
||||||
|
logger.error("Impossible to open %s", args.input_file)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
format_of_file = get_format(paths['ffprobe'], input_file)
|
||||||
|
|
||||||
|
if format_of_file is None:
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
duration = timedelta(seconds=float(format_of_file['duration']))
|
||||||
|
logger.info("Durée de l'enregistrement: %s", duration)
|
||||||
|
|
||||||
|
if args.framerate is None:
|
||||||
|
framerate = get_frame_rate(paths['ffprobe'], input_file)
|
||||||
|
if framerate is None:
|
||||||
|
logger.error('Impossible to estimate frame rate !')
|
||||||
|
exit(-1)
|
||||||
|
else:
|
||||||
|
framerate = args.framerate
|
||||||
|
|
||||||
|
logger.info('Frame rate: %.1f fps', framerate)
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for f in SupportedFormat:
|
||||||
|
if 'format_name' in format_of_file:
|
||||||
|
if format_of_file['format_name'] == str(f):
|
||||||
|
found = True
|
||||||
|
format_of_file = f
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
logger.error('Unsupported format of file')
|
||||||
|
|
||||||
|
if format_of_file == SupportedFormat.TS:
|
||||||
|
logger.info("Converting TS to MP4 (to fix timestamps).")
|
||||||
|
try:
|
||||||
|
with open(mp4filename, 'wb+') as mp4:
|
||||||
|
ffmpeg_convert(paths['ffmpeg'], paths['ffprobe'], input_file, 'mpegts', mp4, 'mp4',
|
||||||
|
duration)
|
||||||
|
temporaries.append(mp4)
|
||||||
|
logger.info("Converting MP4 to MKV.")
|
||||||
|
try:
|
||||||
|
mkv = open(mkvfilename, 'wb+')
|
||||||
|
except IOError:
|
||||||
|
logger.error('')
|
||||||
|
|
||||||
|
ffmpeg_convert(paths['ffmpeg'], paths['ffprobe'], mp4, 'mp4', mkv, 'matroska',
|
||||||
|
duration)
|
||||||
|
if nb_parts > 0:
|
||||||
|
temporaries.append(mkv)
|
||||||
|
except IOError:
|
||||||
|
logger.error('')
|
||||||
|
|
||||||
|
elif format_of_file == SupportedFormat.MP4:
|
||||||
|
logger.info("Converting MP4 to MKV")
|
||||||
|
try:
|
||||||
|
mkv = open(mkvfilename, 'wb+')
|
||||||
|
except IOError:
|
||||||
|
logger.error('')
|
||||||
|
ffmpeg_convert(paths['ffmpeg'], paths['ffprobe'], input_file, 'mp4', mkv, 'matroska',
|
||||||
|
duration)
|
||||||
|
if nb_parts > 0:
|
||||||
|
temporaries.append(mkv)
|
||||||
|
else:
|
||||||
|
logger.info("Already in MKV")
|
||||||
|
mkv = input_file
|
||||||
|
|
||||||
|
streams = get_streams(paths['ffprobe'], mkv)
|
||||||
|
|
||||||
|
logger.debug('Streams: %s', streams)
|
||||||
|
main_video = None
|
||||||
|
nb_videos = 0
|
||||||
|
for stream in streams:
|
||||||
|
if stream['codec_type'] == 'video':
|
||||||
|
if stream['disposition']['default'] == 1:
|
||||||
|
main_video = stream
|
||||||
|
width = stream['width']
|
||||||
|
height = stream['height']
|
||||||
|
break
|
||||||
|
nb_videos+=1
|
||||||
|
if nb_videos == 1:
|
||||||
|
main_video = stream
|
||||||
|
width = stream['width']
|
||||||
|
height = stream['height']
|
||||||
|
else:
|
||||||
|
main_video = None
|
||||||
|
|
||||||
|
if main_video is None:
|
||||||
|
logger.error('Impossible to find main video stream.')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
# We retrieve the main private codec data
|
||||||
|
_, main_codec_private_data = get_codec_private_data_from_mkv(mkvinfo_path=paths['mkvinfo'],
|
||||||
|
input_file=mkv)
|
||||||
|
logger.debug('Main video stream has following private data: %s',
|
||||||
|
hexdump.dump(main_codec_private_data, sep=':'))
|
||||||
|
|
||||||
|
# We parse them
|
||||||
|
main_avc_config = parse_codec_private(main_codec_private_data)
|
||||||
|
logger.debug('AVC configuration: %s', main_avc_config)
|
||||||
|
|
||||||
|
# We check if the parse and dump operations are idempotent.
|
||||||
|
private_data = dump_codec_private_data(main_avc_config)
|
||||||
|
logger.debug('Redump AVC configuration: %s', hexdump.dump(private_data, sep=':'))
|
||||||
|
# In rare occasion, the PPS has trailing zeroes that do not seem to be related to useful data
|
||||||
|
# but they differ from the private data we generate that do not contain them.
|
||||||
|
# In that case we try to redecode our own private data to see if both AVC configurations are
|
||||||
|
# the same.
|
||||||
|
if main_codec_private_data != private_data:
|
||||||
|
logger.warning('Difference detected in bitstream !!')
|
||||||
|
iso_avc_config = parse_codec_private(private_data)
|
||||||
|
logger.debug('Reread AVC configuration: %s', iso_avc_config)
|
||||||
|
# If there exists a difference between our own reconstructed AVC configuration and the
|
||||||
|
# original one, we abandon
|
||||||
|
if iso_avc_config != main_avc_config:
|
||||||
|
logger.error('AVC configurations are different: %s\n%s\n', main_avc_config,
|
||||||
|
iso_avc_config)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
# Pour chaque portion
|
||||||
|
partnum = 0
|
||||||
|
mkvparts = []
|
||||||
|
h264parts = []
|
||||||
|
h264_ts = []
|
||||||
|
checks = []
|
||||||
|
pos = timedelta()
|
||||||
|
|
||||||
|
other_avc_configs = []
|
||||||
|
|
||||||
|
for ts1, ts2 in parts:
|
||||||
|
# TODO: translate comment in english
|
||||||
|
# Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début
|
||||||
|
# de la portion.
|
||||||
|
# Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin
|
||||||
|
# de la portion.
|
||||||
|
# On a alors
|
||||||
|
# debut ----- trame --------- trame --------- fin fin+1
|
||||||
|
# 'B/P' 'B/P'* 'I' 'I' 'B/P'* 'B/P' 'I/B/P'
|
||||||
|
# Si la trame de début est déjà 'I', il n'y a rien à faire.
|
||||||
|
# Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse.
|
||||||
|
# Si la trame de fin précède une trame I, on n'a rien à faire.
|
||||||
|
# Sinon on extrait toutes les trames depuis la dernière trame I jusqu'à la trame de fin.
|
||||||
|
|
||||||
|
partnum = partnum + 1
|
||||||
|
|
||||||
|
# Get the nearest I-frame whose timestamp is greater or equal to the beginning.
|
||||||
|
head_frames = get_nearest_iframe(paths['ffprobe'], mkv, ts1, before=False)
|
||||||
|
if head_frames is None:
|
||||||
|
logger.error('Impossible to retrieve I-frame')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
# Get the nearest I-frame whose timestamp ...
|
||||||
|
# TODO: wrong here ...
|
||||||
|
tail_frames = get_nearest_iframe(paths['ffprobe'], mkv, ts2, before=True)
|
||||||
|
if tail_frames is None:
|
||||||
|
logger.error('Impossible to retrieve I-frame')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
nb_head_frames, head_iframe = head_frames
|
||||||
|
nb_tail_frames, tail_iframe = tail_frames
|
||||||
|
|
||||||
|
logger.info("Found %d frames between beginning of current part and first I-frame",
|
||||||
|
nb_head_frames)
|
||||||
|
logger.info("Found %d frames between last I-frame and end of current part",
|
||||||
|
nb_tail_frames)
|
||||||
|
|
||||||
|
head_iframe_ts = get_ts_frame(head_iframe)
|
||||||
|
if head_iframe_ts is None:
|
||||||
|
exit(-1)
|
||||||
|
tail_iframe_ts = get_ts_frame(tail_iframe)
|
||||||
|
if tail_iframe_ts is None:
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
checks.append(pos+head_iframe_ts-ts1)
|
||||||
|
|
||||||
|
subparts = []
|
||||||
|
|
||||||
|
# TODO: separate pipeline processing between coarse and not fine grain options.
|
||||||
|
|
||||||
|
# if args.coarse:
|
||||||
|
# do_coarse_processing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], input_file=mkv,
|
||||||
|
# begin=ts1, end=head_iframe_ts, nb_frames=nb_head_frames-1,
|
||||||
|
# frameRate=frameRate, files_prefix='part-%d-head' % (partnum),
|
||||||
|
# streams=streams, width=width, height=height,
|
||||||
|
# temporaries=temporaries, dump_mem_fd=args.dump)
|
||||||
|
# else:
|
||||||
|
# doFineGrainProcessing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'],
|
||||||
|
# input_file=mkv, begin=ts1, end=head_iframe_ts,
|
||||||
|
# nb_frames=nb_head_frames-1, frameRate=frameRate,
|
||||||
|
# files_prefix='part-%d-head' % (partnum), streams=streams,
|
||||||
|
# width=width, height=height, temporaries=temporaries,
|
||||||
|
# dump_mem_fd=args.dump)
|
||||||
|
|
||||||
|
if (not args.coarse) and (nb_head_frames > args.threshold):
|
||||||
|
# We extract all frames between the beginning upto the frame that immediately preceeds
|
||||||
|
# the I-frame.
|
||||||
|
h264_head, h264_head_ts, mkv_head = extract_all_streams(ffmpeg_path=paths['ffmpeg'],
|
||||||
|
ffprobe_path=paths['ffprobe'],
|
||||||
|
input_file=mkv, begin=ts1,
|
||||||
|
end=head_iframe_ts,
|
||||||
|
nb_frames=nb_head_frames-1,
|
||||||
|
framerate=framerate,
|
||||||
|
files_prefix=f'part-{partnum:d}-head',
|
||||||
|
streams=streams, width=width,
|
||||||
|
height=height,
|
||||||
|
temporaries=temporaries,
|
||||||
|
dump_mem_fd=args.dump)
|
||||||
|
|
||||||
|
# If we are not at an exact boundary:
|
||||||
|
if mkv_head is not None:
|
||||||
|
subparts.append(mkv_head)
|
||||||
|
if h264_head is not None:
|
||||||
|
avcconfig = get_avc_config_from_h264(h264_head)
|
||||||
|
other_avc_configs.append(avcconfig)
|
||||||
|
h264parts.append(h264_head)
|
||||||
|
if h264_head_ts is not None:
|
||||||
|
h264_ts.append(h264_head_ts)
|
||||||
|
|
||||||
|
# Creating MKV file that corresponds to current part between I-frames
|
||||||
|
# Internal video with all streams (video, audio and subtitles)
|
||||||
|
internal_mkv_name = f'part-{partnum:d}-internal.mkv'
|
||||||
|
# Internal video stream as a raw H264 stream
|
||||||
|
internal_h264_name = f'part-{partnum:d}-internal.h264'
|
||||||
|
# Internal video timestamps
|
||||||
|
internal_h264_ts_name = f'part-{partnum:d}-internal-ts.txt'
|
||||||
|
# Internal video with only audio and subtitles streams
|
||||||
|
internal_novideo_mkv_name = f'part-{partnum:d}-internal-novideo.mkv'
|
||||||
|
|
||||||
|
try:
|
||||||
|
internal_mkv = open(internal_mkv_name, 'wb+')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to create file: %s', internal_mkv_name)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
internal_novideo_mkv = open(internal_novideo_mkv_name, 'wb+')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to create file: %s', internal_novideo_mkv_name)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
internal_h264 = open(internal_h264_name, 'wb+')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to create file: %s', internal_h264_name)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
internal_h264_ts = open(internal_h264_ts_name, 'w+', encoding='utf8')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to create file: %s', internal_h264_ts_name)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
# logger.info('Merge header, middle and trailer subpart into: %s' % internal_mkv_name)
|
||||||
|
# Extract internal part of MKV
|
||||||
|
extract_mkv_part(mkvmerge_path=paths['mkvmerge'], input_file=mkv, output_file=internal_mkv,
|
||||||
|
begin=head_iframe_ts, end=tail_iframe_ts)
|
||||||
|
|
||||||
|
# Extract video stream of internal part as a raw H264 and its timestamps.
|
||||||
|
logger.info('Extract video track as raw H264 file.')
|
||||||
|
extract_track_from_mkv(mkvextract_path=paths['mkvextract'], input_file=internal_mkv,
|
||||||
|
index=0, output_file=internal_h264, timestamps=internal_h264_ts)
|
||||||
|
|
||||||
|
# Remove video track from internal part of MKV
|
||||||
|
logger.info('Remove video track from %s', internal_mkv_name)
|
||||||
|
remove_video_tracks_from_mkv(mkvmerge_path=paths['mkvmerge'], input_file=internal_mkv,
|
||||||
|
output_file=internal_novideo_mkv)
|
||||||
|
|
||||||
|
temporaries.append(internal_mkv)
|
||||||
|
temporaries.append(internal_h264)
|
||||||
|
temporaries.append(internal_h264_ts)
|
||||||
|
temporaries.append(internal_novideo_mkv)
|
||||||
|
|
||||||
|
h264parts.append(internal_h264)
|
||||||
|
h264_ts.append(internal_h264_ts)
|
||||||
|
subparts.append(internal_novideo_mkv)
|
||||||
|
|
||||||
|
if (not args.coarse) and (nb_tail_frames > args.threshold):
|
||||||
|
# We extract all frames between the I-frame (including it) upto the end.
|
||||||
|
h264_tail, h264_tail_ts, mkv_tail = extract_all_streams(ffmpeg_path=paths['ffmpeg'],
|
||||||
|
ffprobe_path=paths['ffprobe'],
|
||||||
|
input_file=mkv, begin=tail_iframe_ts,
|
||||||
|
end=ts2, nb_frames=nb_tail_frames,
|
||||||
|
framerate=framerate,
|
||||||
|
files_prefix=f'part-{partnum:d}-tail',
|
||||||
|
streams=streams,
|
||||||
|
width=width, height=height,
|
||||||
|
temporaries=temporaries,
|
||||||
|
dump_mem_fd=args.dump)
|
||||||
|
|
||||||
|
if mkv_tail is not None:
|
||||||
|
subparts.append(mkv_tail)
|
||||||
|
if h264_tail is not None:
|
||||||
|
avcconfig = get_avc_config_from_h264(h264_tail)
|
||||||
|
other_avc_configs.append(avcconfig)
|
||||||
|
h264parts.append(h264_tail)
|
||||||
|
if h264_tail_ts is not None:
|
||||||
|
h264_ts.append(h264_tail_ts)
|
||||||
|
|
||||||
|
logger.info('Merging MKV: %s', subparts)
|
||||||
|
|
||||||
|
part = merge_mkvs(mkvmerge_path=paths['mkvmerge'], inputs=subparts,
|
||||||
|
output_name=f'part-{partnum:d}.mkv', concatenate=True)
|
||||||
|
mkvparts.append(part)
|
||||||
|
temporaries.append(part)
|
||||||
|
|
||||||
|
pos = pos+tail_iframe_ts-ts1
|
||||||
|
|
||||||
|
# We need to check the end also
|
||||||
|
checks.append(pos)
|
||||||
|
|
||||||
|
# When using coarse option there is a single AVC configuration.
|
||||||
|
for avc_config in other_avc_configs:
|
||||||
|
main_avc_config.merge(avc_config)
|
||||||
|
logger.debug('Merged AVC configuration: %s', main_avc_config)
|
||||||
|
|
||||||
|
nb_mkv_parts = len(mkvparts)
|
||||||
|
if nb_mkv_parts > 0:
|
||||||
|
try:
|
||||||
|
full_h264 = open(f'{basename}-full.h264', 'wb+')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to create file full H264 stream.')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
logger.info('Merging all H264 tracks')
|
||||||
|
concatenate_h264_parts(h264parts=h264parts, output=full_h264)
|
||||||
|
temporaries.append(full_h264)
|
||||||
|
|
||||||
|
try:
|
||||||
|
full_h264_ts = open(f'{basename}-ts.txt', 'w+', encoding='utf8')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to create file containing all video timestamps.')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
logger.info('Merging H264 timestamps')
|
||||||
|
concatenate_h264_ts_parts(h264_ts_parts=h264_ts, output=full_h264_ts)
|
||||||
|
temporaries.append(full_h264_ts)
|
||||||
|
|
||||||
|
final_novideo_name = f'{basename}-novideo.mkv'
|
||||||
|
final_with_video_name = f'{basename}-video.mkv'
|
||||||
|
|
||||||
|
if nb_mkv_parts > 1:
|
||||||
|
logger.info('Merging all audio and subtitles parts: %s', mkvparts)
|
||||||
|
merge_mkvs(mkvmerge_path=paths['mkvmerge'], inputs=mkvparts, output_name=final_novideo_name,
|
||||||
|
concatenate=True)
|
||||||
|
elif nb_mkv_parts == 1:
|
||||||
|
copyfile('part-1.mkv', final_novideo_name)
|
||||||
|
else:
|
||||||
|
logger.info("Nothing else to do.")
|
||||||
|
copyfile(mkvfilename, final_with_video_name)
|
||||||
|
|
||||||
|
if nb_mkv_parts >=1 :
|
||||||
|
try:
|
||||||
|
final_novideo = open(final_novideo_name, 'rb')
|
||||||
|
except IOError:
|
||||||
|
logger.error('Impossible to open file: %s.', final_novideo_name)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
temporaries.append(final_novideo)
|
||||||
|
|
||||||
|
full_h264_ts.seek(0)
|
||||||
|
|
||||||
|
logger.info('Merging final video track and all other tracks together')
|
||||||
|
final_with_video = merge_mkvs(mkvmerge_path=paths['mkvmerge'], inputs=[full_h264,
|
||||||
|
final_novideo],
|
||||||
|
output_name=final_with_video_name, concatenate=False,
|
||||||
|
timestamps={0: full_h264_ts})
|
||||||
|
final_codec_private_data = dump_codec_private_data(main_avc_config)
|
||||||
|
logger.debug('Final codec private data: %s', hexdump.dump(final_codec_private_data,
|
||||||
|
sep=':'))
|
||||||
|
logger.info('Changing codec private data with the new one.')
|
||||||
|
change_codec_private_data(paths['mkvinfo'], final_with_video, final_codec_private_data)
|
||||||
|
|
||||||
|
if args.srt:
|
||||||
|
if not all_optional_tools:
|
||||||
|
logger.warning("Missing tools for extracting subtitles.")
|
||||||
|
move(final_with_video_name, args.output_file)
|
||||||
|
else:
|
||||||
|
# Final cut is not any more the final step.
|
||||||
|
temporaries.append(final_with_video)
|
||||||
|
duration = get_movie_duration(paths['ffprobe'], final_with_video)
|
||||||
|
supported_langs = get_tesseract_supported_lang(paths['tesseract'])
|
||||||
|
logger.info('Supported lang: %s', supported_langs)
|
||||||
|
logger.info('Find subtitles tracks and language.')
|
||||||
|
subtitles = find_subtitles_tracks(paths['ffprobe'], final_with_video)
|
||||||
|
logger.info(subtitles)
|
||||||
|
sts = {}
|
||||||
|
for subtitle in subtitles:
|
||||||
|
index = subtitle['index']
|
||||||
|
if 'tags' in subtitle:
|
||||||
|
if 'language' in subtitle['tags']:
|
||||||
|
lang = subtitle['tags']['language']
|
||||||
|
if lang in sts:
|
||||||
|
sts[lang].append(index)
|
||||||
|
else:
|
||||||
|
sts[lang] = [index]
|
||||||
|
else:
|
||||||
|
logger.error("Dropping subtitle: %s because it is missing language\
|
||||||
|
indication", subtitle)
|
||||||
|
else:
|
||||||
|
logger.error("Dropping subtitle: %s because it is missing language indication",
|
||||||
|
subtitle)
|
||||||
|
|
||||||
|
logger.info(sts)
|
||||||
|
if len(sts) > 0:
|
||||||
|
logger.info('Supported languages: %s', supported_langs)
|
||||||
|
list_of_subtitles = extract_srt(paths['mkvextract'], final_with_video_name, sts,
|
||||||
|
supported_langs)
|
||||||
|
logger.info(list_of_subtitles)
|
||||||
|
for idx_name, sub_name, _, _ in list_of_subtitles:
|
||||||
|
try:
|
||||||
|
idx = open(idx_name,'rb')
|
||||||
|
except IOError:
|
||||||
|
logger.error("Impossible to open %s.", idx_name)
|
||||||
|
exit(-1)
|
||||||
|
try:
|
||||||
|
sub = open(sub_name,'rb')
|
||||||
|
except IOError:
|
||||||
|
logger.error("Impossible to open %s.", sub_name)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
temporaries.append(idx)
|
||||||
|
temporaries.append(sub)
|
||||||
|
|
||||||
|
ocr = do_ocr(paths['vobsubocr'], list_of_subtitles, duration, temporaries,
|
||||||
|
args.dump)
|
||||||
|
logger.info(ocr)
|
||||||
|
|
||||||
|
# Remux SRT subtitles
|
||||||
|
remux_srt_subtitles(paths['mkvmerge'], final_with_video, args.output_file, ocr)
|
||||||
|
else:
|
||||||
|
copyfile(final_with_video_name, args.output_file)
|
||||||
|
else:
|
||||||
|
move(final_with_video_name, args.output_file)
|
||||||
|
|
||||||
|
if not args.keep:
|
||||||
|
logger.info("Cleaning temporary files")
|
||||||
|
for f in temporaries:
|
||||||
|
path = os.path.realpath(f.name)
|
||||||
|
logger.info("Removing: %s", path)
|
||||||
|
f.close()
|
||||||
|
unlink(path)
|
||||||
|
|
||||||
|
d = datetime(1,1,1)
|
||||||
|
for c in checks:
|
||||||
|
logger.info("Please check cut smoothness at %s", (c+d).strftime("%H:%M:%S"))
|
||||||
|
|
||||||
|
return 0
|
||||||
@@ -3295,535 +3295,3 @@ def do_coarse_processing(ffmpeg_path:str, ffprobe_path:str, mkvmerge_path:str,
|
|||||||
begin=begin, end=end)
|
begin=begin, end=end)
|
||||||
|
|
||||||
temporaries.append(internal_mkv)
|
temporaries.append(internal_mkv)
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
coloredlogs.install()
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-i", "--input", dest='input_file', type=str, required=True,
|
|
||||||
help="Input file to process (can be .ts, .mp4 or .mkv).")
|
|
||||||
parser.add_argument("-o", "--output", dest='output_file', type=str, required=True,
|
|
||||||
help="Output MKV file to produce.")
|
|
||||||
parser.add_argument("-p", "--part", dest='parts', nargs='+', required=False, action='append',
|
|
||||||
metavar="hh:mm:ss[.mmm]-hh:mm:ss[.mmm]",
|
|
||||||
help="Extract this exact part of the original file.")
|
|
||||||
parser.add_argument("-k", "--keep", action='store_true',
|
|
||||||
help="Do not cleanup temporary files after processing.")
|
|
||||||
parser.add_argument("-t", "--threshold", action='store', type=int,
|
|
||||||
help="Suppress headers and trailers that are smaller than the threshold.")
|
|
||||||
parser.add_argument("-c", "--coarse", action='store_true', dest='coarse',
|
|
||||||
help="Do not take trailers and headers into account.")
|
|
||||||
parser.add_argument("--dump-memory", action='store_true', dest='dump',
|
|
||||||
help="For debug purpose, dump all memory mapping of headers (and trailers)\
|
|
||||||
before (after) each part. They are kept in memory only otherwise.")
|
|
||||||
parser.add_argument("-s","--srt", action='store_true', dest='srt',
|
|
||||||
help="Dump subtitles and make OCR and finally remux them in the movie\
|
|
||||||
(as SRT).")
|
|
||||||
parser.add_argument("-v","--verbose", action='store_true', dest='verbose', help="Debug.")
|
|
||||||
parser.add_argument("-f","--framerate", action='store', type=int,
|
|
||||||
help="Override frame rate estimator.")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
logger.info('Arguments: %s', args)
|
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
logger.info('Setting logging to debug mode')
|
|
||||||
coloredlogs.set_level(level=logging.DEBUG)
|
|
||||||
|
|
||||||
logger.debug('Arguments: %s', args)
|
|
||||||
|
|
||||||
if args.coarse and args.threshold is not None:
|
|
||||||
logger.error('--coarse and threshold arguments are exclusive.')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
if (not args.coarse) and args.threshold is None:
|
|
||||||
args.threshold = 0
|
|
||||||
|
|
||||||
all_optional_tools, paths = check_required_tools()
|
|
||||||
|
|
||||||
# Flatten args.parts
|
|
||||||
intervals = []
|
|
||||||
if args.parts is not None:
|
|
||||||
for part in args.parts:
|
|
||||||
for subpart in part:
|
|
||||||
intervals.append(subpart)
|
|
||||||
|
|
||||||
parts=[]
|
|
||||||
# Parse each interval
|
|
||||||
for interval in intervals:
|
|
||||||
ts1, ts2 = parse_time_interval(interval)
|
|
||||||
if ts1 is None or ts2 is None:
|
|
||||||
logger.error("Illegal time interval: %s", interval)
|
|
||||||
exit(-1)
|
|
||||||
parts.append((ts1,ts2))
|
|
||||||
|
|
||||||
# Sort intervals
|
|
||||||
parts.sort(key=cmp_to_key(compare_time_interval))
|
|
||||||
|
|
||||||
# Check that no intervals are overlapping
|
|
||||||
prevts = timedelta(0)
|
|
||||||
for part in parts:
|
|
||||||
ts1, ts2 = part
|
|
||||||
if prevts > ts1:
|
|
||||||
logger.error('Intervals are overlapping')
|
|
||||||
exit(-1)
|
|
||||||
prevts = ts2
|
|
||||||
|
|
||||||
nb_parts = len(parts)
|
|
||||||
temporaries = []
|
|
||||||
|
|
||||||
basename = os.path.splitext(os.path.basename(args.input_file))[0]
|
|
||||||
mp4filename = basename+'.mp4'
|
|
||||||
mkvfilename = basename+'.mkv'
|
|
||||||
|
|
||||||
try:
|
|
||||||
input_file = open(args.input_file, mode='rb')
|
|
||||||
logger.debug("Type of input file: %s", type(input_file))
|
|
||||||
except IOError:
|
|
||||||
logger.error("Impossible to open %s", args.input_file)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
format_of_file = get_format(paths['ffprobe'], input_file)
|
|
||||||
|
|
||||||
if format_of_file is None:
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
duration = timedelta(seconds=float(format_of_file['duration']))
|
|
||||||
logger.info("Durée de l'enregistrement: %s", duration)
|
|
||||||
|
|
||||||
if args.framerate is None:
|
|
||||||
framerate = get_frame_rate(paths['ffprobe'], input_file)
|
|
||||||
if framerate is None:
|
|
||||||
logger.error('Impossible to estimate frame rate !')
|
|
||||||
exit(-1)
|
|
||||||
else:
|
|
||||||
framerate = args.framerate
|
|
||||||
|
|
||||||
logger.info('Frame rate: %.1f fps', framerate)
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for f in SupportedFormat:
|
|
||||||
if 'format_name' in format_of_file:
|
|
||||||
if format_of_file['format_name'] == str(f):
|
|
||||||
found = True
|
|
||||||
format_of_file = f
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
logger.error('Unsupported format of file')
|
|
||||||
|
|
||||||
if format_of_file == SupportedFormat.TS:
|
|
||||||
logger.info("Converting TS to MP4 (to fix timestamps).")
|
|
||||||
try:
|
|
||||||
with open(mp4filename, 'wb+') as mp4:
|
|
||||||
ffmpeg_convert(paths['ffmpeg'], paths['ffprobe'], input_file, 'mpegts', mp4, 'mp4',
|
|
||||||
duration)
|
|
||||||
temporaries.append(mp4)
|
|
||||||
logger.info("Converting MP4 to MKV.")
|
|
||||||
try:
|
|
||||||
mkv = open(mkvfilename, 'wb+')
|
|
||||||
except IOError:
|
|
||||||
logger.error('')
|
|
||||||
|
|
||||||
ffmpeg_convert(paths['ffmpeg'], paths['ffprobe'], mp4, 'mp4', mkv, 'matroska',
|
|
||||||
duration)
|
|
||||||
if nb_parts > 0:
|
|
||||||
temporaries.append(mkv)
|
|
||||||
except IOError:
|
|
||||||
logger.error('')
|
|
||||||
|
|
||||||
elif format_of_file == SupportedFormat.MP4:
|
|
||||||
logger.info("Converting MP4 to MKV")
|
|
||||||
try:
|
|
||||||
mkv = open(mkvfilename, 'wb+')
|
|
||||||
except IOError:
|
|
||||||
logger.error('')
|
|
||||||
ffmpeg_convert(paths['ffmpeg'], paths['ffprobe'], input_file, 'mp4', mkv, 'matroska',
|
|
||||||
duration)
|
|
||||||
if nb_parts > 0:
|
|
||||||
temporaries.append(mkv)
|
|
||||||
else:
|
|
||||||
logger.info("Already in MKV")
|
|
||||||
mkv = input_file
|
|
||||||
|
|
||||||
streams = get_streams(paths['ffprobe'], mkv)
|
|
||||||
|
|
||||||
logger.debug('Streams: %s', streams)
|
|
||||||
main_video = None
|
|
||||||
nb_videos = 0
|
|
||||||
for stream in streams:
|
|
||||||
if stream['codec_type'] == 'video':
|
|
||||||
if stream['disposition']['default'] == 1:
|
|
||||||
main_video = stream
|
|
||||||
width = stream['width']
|
|
||||||
height = stream['height']
|
|
||||||
break
|
|
||||||
nb_videos+=1
|
|
||||||
if nb_videos == 1:
|
|
||||||
main_video = stream
|
|
||||||
width = stream['width']
|
|
||||||
height = stream['height']
|
|
||||||
else:
|
|
||||||
main_video = None
|
|
||||||
|
|
||||||
if main_video is None:
|
|
||||||
logger.error('Impossible to find main video stream.')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
# We retrieve the main private codec data
|
|
||||||
_, main_codec_private_data = get_codec_private_data_from_mkv(mkvinfo_path=paths['mkvinfo'],
|
|
||||||
input_file=mkv)
|
|
||||||
logger.debug('Main video stream has following private data: %s',
|
|
||||||
hexdump.dump(main_codec_private_data, sep=':'))
|
|
||||||
|
|
||||||
# We parse them
|
|
||||||
main_avc_config = parse_codec_private(main_codec_private_data)
|
|
||||||
logger.debug('AVC configuration: %s', main_avc_config)
|
|
||||||
|
|
||||||
# We check if the parse and dump operations are idempotent.
|
|
||||||
private_data = dump_codec_private_data(main_avc_config)
|
|
||||||
logger.debug('Redump AVC configuration: %s', hexdump.dump(private_data, sep=':'))
|
|
||||||
# In rare occasion, the PPS has trailing zeroes that do not seem to be related to useful data
|
|
||||||
# but they differ from the private data we generate that do not contain them.
|
|
||||||
# In that case we try to redecode our own private data to see if both AVC configurations are
|
|
||||||
# the same.
|
|
||||||
if main_codec_private_data != private_data:
|
|
||||||
logger.warning('Difference detected in bitstream !!')
|
|
||||||
iso_avc_config = parse_codec_private(private_data)
|
|
||||||
logger.debug('Reread AVC configuration: %s', iso_avc_config)
|
|
||||||
# If there exists a difference between our own reconstructed AVC configuration and the
|
|
||||||
# original one, we abandon
|
|
||||||
if iso_avc_config != main_avc_config:
|
|
||||||
logger.error('AVC configurations are different: %s\n%s\n', main_avc_config,
|
|
||||||
iso_avc_config)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
# Pour chaque portion
|
|
||||||
partnum = 0
|
|
||||||
mkvparts = []
|
|
||||||
h264parts = []
|
|
||||||
h264_ts = []
|
|
||||||
checks = []
|
|
||||||
pos = timedelta()
|
|
||||||
|
|
||||||
other_avc_configs = []
|
|
||||||
|
|
||||||
for ts1, ts2 in parts:
|
|
||||||
# TODO: translate comment in english
|
|
||||||
# Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début
|
|
||||||
# de la portion.
|
|
||||||
# Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin
|
|
||||||
# de la portion.
|
|
||||||
# On a alors
|
|
||||||
# debut ----- trame --------- trame --------- fin fin+1
|
|
||||||
# 'B/P' 'B/P'* 'I' 'I' 'B/P'* 'B/P' 'I/B/P'
|
|
||||||
# Si la trame de début est déjà 'I', il n'y a rien à faire.
|
|
||||||
# Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse.
|
|
||||||
# Si la trame de fin précède une trame I, on n'a rien à faire.
|
|
||||||
# Sinon on extrait toutes les trames depuis la dernière trame I jusqu'à la trame de fin.
|
|
||||||
|
|
||||||
partnum = partnum + 1
|
|
||||||
|
|
||||||
# Get the nearest I-frame whose timestamp is greater or equal to the beginning.
|
|
||||||
head_frames = get_nearest_iframe(paths['ffprobe'], mkv, ts1, before=False)
|
|
||||||
if head_frames is None:
|
|
||||||
logger.error('Impossible to retrieve I-frame')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
# Get the nearest I-frame whose timestamp ...
|
|
||||||
# TODO: wrong here ...
|
|
||||||
tail_frames = get_nearest_iframe(paths['ffprobe'], mkv, ts2, before=True)
|
|
||||||
if tail_frames is None:
|
|
||||||
logger.error('Impossible to retrieve I-frame')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
nb_head_frames, head_iframe = head_frames
|
|
||||||
nb_tail_frames, tail_iframe = tail_frames
|
|
||||||
|
|
||||||
logger.info("Found %d frames between beginning of current part and first I-frame",
|
|
||||||
nb_head_frames)
|
|
||||||
logger.info("Found %d frames between last I-frame and end of current part",
|
|
||||||
nb_tail_frames)
|
|
||||||
|
|
||||||
head_iframe_ts = get_ts_frame(head_iframe)
|
|
||||||
if head_iframe_ts is None:
|
|
||||||
exit(-1)
|
|
||||||
tail_iframe_ts = get_ts_frame(tail_iframe)
|
|
||||||
if tail_iframe_ts is None:
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
checks.append(pos+head_iframe_ts-ts1)
|
|
||||||
|
|
||||||
subparts = []
|
|
||||||
|
|
||||||
# TODO: separate pipeline processing between coarse and not fine grain options.
|
|
||||||
|
|
||||||
# if args.coarse:
|
|
||||||
# do_coarse_processing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], input_file=mkv,
|
|
||||||
# begin=ts1, end=head_iframe_ts, nb_frames=nb_head_frames-1,
|
|
||||||
# frameRate=frameRate, files_prefix='part-%d-head' % (partnum),
|
|
||||||
# streams=streams, width=width, height=height,
|
|
||||||
# temporaries=temporaries, dump_mem_fd=args.dump)
|
|
||||||
# else:
|
|
||||||
# doFineGrainProcessing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'],
|
|
||||||
# input_file=mkv, begin=ts1, end=head_iframe_ts,
|
|
||||||
# nb_frames=nb_head_frames-1, frameRate=frameRate,
|
|
||||||
# files_prefix='part-%d-head' % (partnum), streams=streams,
|
|
||||||
# width=width, height=height, temporaries=temporaries,
|
|
||||||
# dump_mem_fd=args.dump)
|
|
||||||
|
|
||||||
if (not args.coarse) and (nb_head_frames > args.threshold):
|
|
||||||
# We extract all frames between the beginning upto the frame that immediately preceeds
|
|
||||||
# the I-frame.
|
|
||||||
h264_head, h264_head_ts, mkv_head = extract_all_streams(ffmpeg_path=paths['ffmpeg'],
|
|
||||||
ffprobe_path=paths['ffprobe'],
|
|
||||||
input_file=mkv, begin=ts1,
|
|
||||||
end=head_iframe_ts,
|
|
||||||
nb_frames=nb_head_frames-1,
|
|
||||||
framerate=framerate,
|
|
||||||
files_prefix=f'part-{partnum:d}-head',
|
|
||||||
streams=streams, width=width,
|
|
||||||
height=height,
|
|
||||||
temporaries=temporaries,
|
|
||||||
dump_mem_fd=args.dump)
|
|
||||||
|
|
||||||
# If we are not at an exact boundary:
|
|
||||||
if mkv_head is not None:
|
|
||||||
subparts.append(mkv_head)
|
|
||||||
if h264_head is not None:
|
|
||||||
avcconfig = get_avc_config_from_h264(h264_head)
|
|
||||||
other_avc_configs.append(avcconfig)
|
|
||||||
h264parts.append(h264_head)
|
|
||||||
if h264_head_ts is not None:
|
|
||||||
h264_ts.append(h264_head_ts)
|
|
||||||
|
|
||||||
# Creating MKV file that corresponds to current part between I-frames
|
|
||||||
# Internal video with all streams (video, audio and subtitles)
|
|
||||||
internal_mkv_name = f'part-{partnum:d}-internal.mkv'
|
|
||||||
# Internal video stream as a raw H264 stream
|
|
||||||
internal_h264_name = f'part-{partnum:d}-internal.h264'
|
|
||||||
# Internal video timestamps
|
|
||||||
internal_h264_ts_name = f'part-{partnum:d}-internal-ts.txt'
|
|
||||||
# Internal video with only audio and subtitles streams
|
|
||||||
internal_novideo_mkv_name = f'part-{partnum:d}-internal-novideo.mkv'
|
|
||||||
|
|
||||||
try:
|
|
||||||
internal_mkv = open(internal_mkv_name, 'wb+')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to create file: %s', internal_mkv_name)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
internal_novideo_mkv = open(internal_novideo_mkv_name, 'wb+')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to create file: %s', internal_novideo_mkv_name)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
internal_h264 = open(internal_h264_name, 'wb+')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to create file: %s', internal_h264_name)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
internal_h264_ts = open(internal_h264_ts_name, 'w+', encoding='utf8')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to create file: %s', internal_h264_ts_name)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
# logger.info('Merge header, middle and trailer subpart into: %s' % internal_mkv_name)
|
|
||||||
# Extract internal part of MKV
|
|
||||||
extract_mkv_part(mkvmerge_path=paths['mkvmerge'], input_file=mkv, output_file=internal_mkv,
|
|
||||||
begin=head_iframe_ts, end=tail_iframe_ts)
|
|
||||||
|
|
||||||
# Extract video stream of internal part as a raw H264 and its timestamps.
|
|
||||||
logger.info('Extract video track as raw H264 file.')
|
|
||||||
extract_track_from_mkv(mkvextract_path=paths['mkvextract'], input_file=internal_mkv,
|
|
||||||
index=0, output_file=internal_h264, timestamps=internal_h264_ts)
|
|
||||||
|
|
||||||
# Remove video track from internal part of MKV
|
|
||||||
logger.info('Remove video track from %s', internal_mkv_name)
|
|
||||||
remove_video_tracks_from_mkv(mkvmerge_path=paths['mkvmerge'], input_file=internal_mkv,
|
|
||||||
output_file=internal_novideo_mkv)
|
|
||||||
|
|
||||||
temporaries.append(internal_mkv)
|
|
||||||
temporaries.append(internal_h264)
|
|
||||||
temporaries.append(internal_h264_ts)
|
|
||||||
temporaries.append(internal_novideo_mkv)
|
|
||||||
|
|
||||||
h264parts.append(internal_h264)
|
|
||||||
h264_ts.append(internal_h264_ts)
|
|
||||||
subparts.append(internal_novideo_mkv)
|
|
||||||
|
|
||||||
if (not args.coarse) and (nb_tail_frames > args.threshold):
|
|
||||||
# We extract all frames between the I-frame (including it) upto the end.
|
|
||||||
h264_tail, h264_tail_ts, mkv_tail = extract_all_streams(ffmpeg_path=paths['ffmpeg'],
|
|
||||||
ffprobe_path=paths['ffprobe'],
|
|
||||||
input_file=mkv, begin=tail_iframe_ts,
|
|
||||||
end=ts2, nb_frames=nb_tail_frames,
|
|
||||||
framerate=framerate,
|
|
||||||
files_prefix=f'part-{partnum:d}-tail',
|
|
||||||
streams=streams,
|
|
||||||
width=width, height=height,
|
|
||||||
temporaries=temporaries,
|
|
||||||
dump_mem_fd=args.dump)
|
|
||||||
|
|
||||||
if mkv_tail is not None:
|
|
||||||
subparts.append(mkv_tail)
|
|
||||||
if h264_tail is not None:
|
|
||||||
avcconfig = get_avc_config_from_h264(h264_tail)
|
|
||||||
other_avc_configs.append(avcconfig)
|
|
||||||
h264parts.append(h264_tail)
|
|
||||||
if h264_tail_ts is not None:
|
|
||||||
h264_ts.append(h264_tail_ts)
|
|
||||||
|
|
||||||
logger.info('Merging MKV: %s', subparts)
|
|
||||||
|
|
||||||
part = merge_mkvs(mkvmerge_path=paths['mkvmerge'], inputs=subparts,
|
|
||||||
output_name=f'part-{partnum:d}.mkv', concatenate=True)
|
|
||||||
mkvparts.append(part)
|
|
||||||
temporaries.append(part)
|
|
||||||
|
|
||||||
pos = pos+tail_iframe_ts-ts1
|
|
||||||
|
|
||||||
# We need to check the end also
|
|
||||||
checks.append(pos)
|
|
||||||
|
|
||||||
# When using coarse option there is a single AVC configuration.
|
|
||||||
for avc_config in other_avc_configs:
|
|
||||||
main_avc_config.merge(avc_config)
|
|
||||||
logger.debug('Merged AVC configuration: %s', main_avc_config)
|
|
||||||
|
|
||||||
nb_mkv_parts = len(mkvparts)
|
|
||||||
if nb_mkv_parts > 0:
|
|
||||||
try:
|
|
||||||
full_h264 = open(f'{basename}-full.h264', 'wb+')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to create file full H264 stream.')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
logger.info('Merging all H264 tracks')
|
|
||||||
concatenate_h264_parts(h264parts=h264parts, output=full_h264)
|
|
||||||
temporaries.append(full_h264)
|
|
||||||
|
|
||||||
try:
|
|
||||||
full_h264_ts = open(f'{basename}-ts.txt', 'w+', encoding='utf8')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to create file containing all video timestamps.')
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
logger.info('Merging H264 timestamps')
|
|
||||||
concatenate_h264_ts_parts(h264_ts_parts=h264_ts, output=full_h264_ts)
|
|
||||||
temporaries.append(full_h264_ts)
|
|
||||||
|
|
||||||
final_novideo_name = f'{basename}-novideo.mkv'
|
|
||||||
final_with_video_name = f'{basename}-video.mkv'
|
|
||||||
|
|
||||||
if nb_mkv_parts > 1:
|
|
||||||
logger.info('Merging all audio and subtitles parts: %s', mkvparts)
|
|
||||||
merge_mkvs(mkvmerge_path=paths['mkvmerge'], inputs=mkvparts, output_name=final_novideo_name,
|
|
||||||
concatenate=True)
|
|
||||||
elif nb_mkv_parts == 1:
|
|
||||||
copyfile('part-1.mkv', final_novideo_name)
|
|
||||||
else:
|
|
||||||
logger.info("Nothing else to do.")
|
|
||||||
copyfile(mkvfilename, final_with_video_name)
|
|
||||||
|
|
||||||
if nb_mkv_parts >=1 :
|
|
||||||
try:
|
|
||||||
final_novideo = open(final_novideo_name, 'rb')
|
|
||||||
except IOError:
|
|
||||||
logger.error('Impossible to open file: %s.', final_novideo_name)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
temporaries.append(final_novideo)
|
|
||||||
|
|
||||||
full_h264_ts.seek(0)
|
|
||||||
|
|
||||||
logger.info('Merging final video track and all other tracks together')
|
|
||||||
final_with_video = merge_mkvs(mkvmerge_path=paths['mkvmerge'], inputs=[full_h264,
|
|
||||||
final_novideo],
|
|
||||||
output_name=final_with_video_name, concatenate=False,
|
|
||||||
timestamps={0: full_h264_ts})
|
|
||||||
final_codec_private_data = dump_codec_private_data(main_avc_config)
|
|
||||||
logger.debug('Final codec private data: %s', hexdump.dump(final_codec_private_data,
|
|
||||||
sep=':'))
|
|
||||||
logger.info('Changing codec private data with the new one.')
|
|
||||||
change_codec_private_data(paths['mkvinfo'], final_with_video, final_codec_private_data)
|
|
||||||
|
|
||||||
if args.srt:
|
|
||||||
if not all_optional_tools:
|
|
||||||
logger.warning("Missing tools for extracting subtitles.")
|
|
||||||
move(final_with_video_name, args.output_file)
|
|
||||||
else:
|
|
||||||
# Final cut is not any more the final step.
|
|
||||||
temporaries.append(final_with_video)
|
|
||||||
duration = get_movie_duration(paths['ffprobe'], final_with_video)
|
|
||||||
supported_langs = get_tesseract_supported_lang(paths['tesseract'])
|
|
||||||
logger.info('Supported lang: %s', supported_langs)
|
|
||||||
logger.info('Find subtitles tracks and language.')
|
|
||||||
subtitles = find_subtitles_tracks(paths['ffprobe'], final_with_video)
|
|
||||||
logger.info(subtitles)
|
|
||||||
sts = {}
|
|
||||||
for subtitle in subtitles:
|
|
||||||
index = subtitle['index']
|
|
||||||
if 'tags' in subtitle:
|
|
||||||
if 'language' in subtitle['tags']:
|
|
||||||
lang = subtitle['tags']['language']
|
|
||||||
if lang in sts:
|
|
||||||
sts[lang].append(index)
|
|
||||||
else:
|
|
||||||
sts[lang] = [index]
|
|
||||||
else:
|
|
||||||
logger.error("Dropping subtitle: %s because it is missing language\
|
|
||||||
indication")
|
|
||||||
else:
|
|
||||||
logger.error("Dropping subtitle: %s because it is missing language indication",
|
|
||||||
subtitle)
|
|
||||||
|
|
||||||
logger.info(sts)
|
|
||||||
if len(sts) > 0:
|
|
||||||
logger.info('Supported languages: %s', supported_langs)
|
|
||||||
list_of_subtitles = extract_srt(paths['mkvextract'], final_with_video_name, sts,
|
|
||||||
supported_langs)
|
|
||||||
logger.info(list_of_subtitles)
|
|
||||||
for idx_name, sub_name, _, _ in list_of_subtitles:
|
|
||||||
try:
|
|
||||||
idx = open(idx_name,'rb')
|
|
||||||
except IOError:
|
|
||||||
logger.error("Impossible to open %s.", idx_name)
|
|
||||||
exit(-1)
|
|
||||||
try:
|
|
||||||
sub = open(sub_name,'rb')
|
|
||||||
except IOError:
|
|
||||||
logger.error("Impossible to open %s.", sub_name)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
temporaries.append(idx)
|
|
||||||
temporaries.append(sub)
|
|
||||||
|
|
||||||
ocr = do_ocr(paths['vobsubocr'], list_of_subtitles, duration, temporaries,
|
|
||||||
args.dump)
|
|
||||||
logger.info(ocr)
|
|
||||||
|
|
||||||
# Remux SRT subtitles
|
|
||||||
remux_srt_subtitles(paths['mkvmerge'], final_with_video, args.output_file, ocr)
|
|
||||||
else:
|
|
||||||
copyfile(final_with_video_name, args.output_file)
|
|
||||||
else:
|
|
||||||
move(final_with_video_name, args.output_file)
|
|
||||||
|
|
||||||
if not args.keep:
|
|
||||||
logger.info("Cleaning temporary files")
|
|
||||||
for f in temporaries:
|
|
||||||
path = os.path.realpath(f.name)
|
|
||||||
logger.info("Removing: %s", path)
|
|
||||||
f.close()
|
|
||||||
unlink(path)
|
|
||||||
|
|
||||||
d = datetime(1,1,1)
|
|
||||||
for c in checks:
|
|
||||||
logger.info("Please check cut smoothness at %s", (c+d).strftime("%H:%M:%S"))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user