muxtools 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- muxtools-0.3.0/.github/workflows/publish.yml +26 -0
- muxtools-0.3.0/.gitignore +167 -0
- muxtools-0.3.0/.gitmodules +3 -0
- muxtools-0.3.0/.vscode/settings.json +10 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/PKG-INFO +17 -17
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/__init__.py +10 -2
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/encoders.py +16 -2
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/extractors.py +26 -14
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/tools.py +1 -1
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/cli.py +23 -2
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/functions.py +16 -10
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/main.py +33 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/misc/chapters.py +78 -42
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/muxing/mux.py +2 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/muxing/tmdb.py +30 -19
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/subtitle/basesub.py +111 -5
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/subtitle/styles.py +72 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/subtitle/sub.py +128 -59
- muxtools-0.3.0/muxtools/utils/convert.py +178 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/dataclass.py +16 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/download.py +35 -15
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/files.py +46 -5
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/log.py +7 -1
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/parsing.py +18 -8
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/types.py +54 -1
- {muxtools-0.2.2 → muxtools-0.3.0}/pyproject.toml +22 -7
- muxtools-0.3.0/tests/test_full_merge.py +69 -0
- muxtools-0.3.0/tests/test_sub_mismatch_warn.py +64 -0
- muxtools-0.3.0/tests/test_sub_shifting.py +61 -0
- muxtools-0.3.0/tests/test_tmdb.py +16 -0
- muxtools-0.3.0/uv.lock +1160 -0
- muxtools-0.2.2/muxtools/utils/convert.py +0 -172
- muxtools-0.2.2/muxtools.egg-info/PKG-INFO +0 -47
- muxtools-0.2.2/muxtools.egg-info/SOURCES.txt +0 -47
- muxtools-0.2.2/muxtools.egg-info/dependency_links.txt +0 -1
- muxtools-0.2.2/muxtools.egg-info/entry_points.txt +0 -2
- muxtools-0.2.2/muxtools.egg-info/requires.txt +0 -10
- muxtools-0.2.2/muxtools.egg-info/top_level.txt +0 -1
- muxtools-0.2.2/setup.cfg +0 -4
- {muxtools-0.2.2 → muxtools-0.3.0}/LICENSE +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/README.md +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/__main__.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/__init__.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/audioutils.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/memecoders.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/audio/preprocess.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/misc/__init__.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/muxing/__init__.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/muxing/muxfiles.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/muxing/tracks.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/py.typed +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/subtitle/__init__.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/subtitle/font.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/subtitle/subutils.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/__init__.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/env.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/format.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/glob.py +0 -0
- {muxtools-0.2.2 → muxtools-0.3.0}/muxtools/utils/subprogress.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish releases when new tag
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- v[0-9]+**
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
package_build:
|
|
9
|
+
name: Build and push to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Prep Python
|
|
15
|
+
uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.11"
|
|
18
|
+
|
|
19
|
+
- name: Build
|
|
20
|
+
run: pipx run build
|
|
21
|
+
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
with:
|
|
25
|
+
user: __token__
|
|
26
|
+
password: ${{ secrets.AUTH_TOKEN }}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Specific to this package
|
|
2
|
+
_binaries/
|
|
3
|
+
premux/
|
|
4
|
+
_workdir/
|
|
5
|
+
config.ini
|
|
6
|
+
/test.py
|
|
7
|
+
|
|
8
|
+
# Byte-compiled / optimized / DLL files
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
*$py.class
|
|
12
|
+
|
|
13
|
+
# C extensions
|
|
14
|
+
*.so
|
|
15
|
+
|
|
16
|
+
# Distribution / packaging
|
|
17
|
+
.Python
|
|
18
|
+
build/
|
|
19
|
+
develop-eggs/
|
|
20
|
+
dist/
|
|
21
|
+
downloads/
|
|
22
|
+
eggs/
|
|
23
|
+
.eggs/
|
|
24
|
+
lib/
|
|
25
|
+
lib64/
|
|
26
|
+
parts/
|
|
27
|
+
sdist/
|
|
28
|
+
var/
|
|
29
|
+
wheels/
|
|
30
|
+
share/python-wheels/
|
|
31
|
+
*.egg-info/
|
|
32
|
+
.installed.cfg
|
|
33
|
+
*.egg
|
|
34
|
+
MANIFEST
|
|
35
|
+
|
|
36
|
+
# PyInstaller
|
|
37
|
+
# Usually these files are written by a python script from a template
|
|
38
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
39
|
+
*.manifest
|
|
40
|
+
*.spec
|
|
41
|
+
|
|
42
|
+
# Installer logs
|
|
43
|
+
pip-log.txt
|
|
44
|
+
pip-delete-this-directory.txt
|
|
45
|
+
|
|
46
|
+
# Unit test / coverage reports
|
|
47
|
+
htmlcov/
|
|
48
|
+
.tox/
|
|
49
|
+
.nox/
|
|
50
|
+
.coverage
|
|
51
|
+
.coverage.*
|
|
52
|
+
.cache
|
|
53
|
+
nosetests.xml
|
|
54
|
+
coverage.xml
|
|
55
|
+
*.cover
|
|
56
|
+
*.py,cover
|
|
57
|
+
.hypothesis/
|
|
58
|
+
.pytest_cache/
|
|
59
|
+
cover/
|
|
60
|
+
|
|
61
|
+
# Translations
|
|
62
|
+
*.mo
|
|
63
|
+
*.pot
|
|
64
|
+
|
|
65
|
+
# Django stuff:
|
|
66
|
+
*.log
|
|
67
|
+
local_settings.py
|
|
68
|
+
db.sqlite3
|
|
69
|
+
db.sqlite3-journal
|
|
70
|
+
|
|
71
|
+
# Flask stuff:
|
|
72
|
+
instance/
|
|
73
|
+
.webassets-cache
|
|
74
|
+
|
|
75
|
+
# Scrapy stuff:
|
|
76
|
+
.scrapy
|
|
77
|
+
|
|
78
|
+
# Sphinx documentation
|
|
79
|
+
docs/_build/
|
|
80
|
+
|
|
81
|
+
# PyBuilder
|
|
82
|
+
.pybuilder/
|
|
83
|
+
target/
|
|
84
|
+
|
|
85
|
+
# Jupyter Notebook
|
|
86
|
+
.ipynb_checkpoints
|
|
87
|
+
|
|
88
|
+
# IPython
|
|
89
|
+
profile_default/
|
|
90
|
+
ipython_config.py
|
|
91
|
+
|
|
92
|
+
# pyenv
|
|
93
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
94
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
95
|
+
# .python-version
|
|
96
|
+
|
|
97
|
+
# pipenv
|
|
98
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
99
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
100
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
101
|
+
# install all needed dependencies.
|
|
102
|
+
#Pipfile.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
#poetry.lock
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
#pdm.lock
|
|
114
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
115
|
+
# in version control.
|
|
116
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
117
|
+
.pdm.toml
|
|
118
|
+
|
|
119
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
120
|
+
__pypackages__/
|
|
121
|
+
|
|
122
|
+
# Celery stuff
|
|
123
|
+
celerybeat-schedule
|
|
124
|
+
celerybeat.pid
|
|
125
|
+
|
|
126
|
+
# SageMath parsed files
|
|
127
|
+
*.sage.py
|
|
128
|
+
|
|
129
|
+
# Environments
|
|
130
|
+
.env
|
|
131
|
+
.venv
|
|
132
|
+
env/
|
|
133
|
+
venv/
|
|
134
|
+
ENV/
|
|
135
|
+
env.bak/
|
|
136
|
+
venv.bak/
|
|
137
|
+
|
|
138
|
+
# Spyder project settings
|
|
139
|
+
.spyderproject
|
|
140
|
+
.spyproject
|
|
141
|
+
|
|
142
|
+
# Rope project settings
|
|
143
|
+
.ropeproject
|
|
144
|
+
|
|
145
|
+
# mkdocs documentation
|
|
146
|
+
/site
|
|
147
|
+
|
|
148
|
+
# mypy
|
|
149
|
+
.mypy_cache/
|
|
150
|
+
.dmypy.json
|
|
151
|
+
dmypy.json
|
|
152
|
+
|
|
153
|
+
# Pyre type checker
|
|
154
|
+
.pyre/
|
|
155
|
+
|
|
156
|
+
# pytype static type analyzer
|
|
157
|
+
.pytype/
|
|
158
|
+
|
|
159
|
+
# Cython debug symbols
|
|
160
|
+
cython_debug/
|
|
161
|
+
|
|
162
|
+
# PyCharm
|
|
163
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
164
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
165
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
166
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
167
|
+
.idea/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"[python]": {
|
|
3
|
+
"editor.formatOnType": false,
|
|
4
|
+
"editor.formatOnSave": true,
|
|
5
|
+
"editor.defaultFormatter": "charliermarsh.ruff"
|
|
6
|
+
},
|
|
7
|
+
"ruff.lint.enable": true,
|
|
8
|
+
"ruff.lint.args": ["--config", "./pyproject.toml"],
|
|
9
|
+
"ruff.format.args": ["--config", "./pyproject.toml"]
|
|
10
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: muxtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A library for various muxing and automation tools for anything and everything fansubbing related
|
|
5
|
-
Author-email: Vodes <vodes.imp@gmail.com>
|
|
6
|
-
License: MPL 2.0
|
|
7
5
|
Project-URL: Source Code, https://github.com/Vodes/muxtools
|
|
8
|
-
|
|
6
|
+
Author-email: Vodes <vodes.imp@gmail.com>
|
|
7
|
+
License-Expression: MPL-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
10
10
|
Classifier: Intended Audience :: End Users/Desktop
|
|
11
|
-
Classifier:
|
|
11
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
12
|
+
Classifier: Natural Language :: English
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier:
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
15
|
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
15
16
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
|
|
16
17
|
Classifier: Topic :: Multimedia :: Video
|
|
17
18
|
Classifier: Topic :: Multimedia :: Video :: Conversion
|
|
18
|
-
Classifier:
|
|
19
|
+
Classifier: Typing :: Typed
|
|
19
20
|
Requires-Python: >=3.10
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
|
-
Requires-Dist: requests>=2.31.0
|
|
23
|
-
Requires-Dist: fontcollector>=3.0.0
|
|
24
21
|
Requires-Dist: ass>=0.5.3
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: fontcollector>=3.0.0
|
|
23
|
+
Requires-Dist: psutil>=5.6.4
|
|
24
|
+
Requires-Dist: pydantic>=2.5.0
|
|
27
25
|
Requires-Dist: pymediainfo>=6.0.1
|
|
28
|
-
Requires-Dist: rich>=13.1.0
|
|
29
26
|
Requires-Dist: pyparsebluray>=0.1.4
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
27
|
+
Requires-Dist: requests>=2.31.0
|
|
28
|
+
Requires-Dist: rich>=13.1.0
|
|
29
|
+
Requires-Dist: videotimestamps>=0.2.0
|
|
30
|
+
Requires-Dist: wget>=3.2
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
33
|
# muxtools
|
|
34
34
|
|
|
@@ -12,7 +12,7 @@ from .functions import *
|
|
|
12
12
|
|
|
13
13
|
def entry_point():
|
|
14
14
|
import sys
|
|
15
|
-
from .cli import install_libraries, install_dependencies
|
|
15
|
+
from .cli import install_libraries, install_dependencies, generate_videometa
|
|
16
16
|
|
|
17
17
|
if sys.argv:
|
|
18
18
|
if sys.argv[-1].lower() in ["libs", "libraries"]:
|
|
@@ -21,8 +21,16 @@ def entry_point():
|
|
|
21
21
|
elif sys.argv[-1].lower() in ["install", "deps", "dependencies"]:
|
|
22
22
|
install_dependencies()
|
|
23
23
|
sys.exit(0)
|
|
24
|
+
else:
|
|
25
|
+
if len(sys.argv) > 1:
|
|
26
|
+
if sys.argv[1].lower() in ["gen-vm", "generate-videometa"]:
|
|
27
|
+
file_in = None if len(sys.argv) < 3 else sys.argv[2]
|
|
28
|
+
file_out = None if len(sys.argv) < 4 else sys.argv[3]
|
|
29
|
+
generate_videometa(file_in, file_out)
|
|
30
|
+
sys.exit(0)
|
|
24
31
|
|
|
25
32
|
error(
|
|
26
33
|
"No arguments passed.\nYou can use [b]libs[/] or [b]libraries[/] to install/update libraries of qaac and eac3to."
|
|
27
|
-
+ "\nYou can use [b]install[/], [b]deps[/] or [b]dependencies[/] to install all sorts of executables
|
|
34
|
+
+ "\nYou can use [b]install[/], [b]deps[/] or [b]dependencies[/] to install all sorts of executables.\n\n"
|
|
35
|
+
+ "You can also use [b]generate-videometa[/] or [b]gen-vm[/] to generate a VideoMeta file for a video."
|
|
28
36
|
)
|
|
@@ -2,6 +2,7 @@ from collections.abc import Sequence
|
|
|
2
2
|
from pydantic.dataclasses import dataclass, Field
|
|
3
3
|
import subprocess
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
|
|
6
7
|
from .tools import Encoder, LosslessEncoder
|
|
7
8
|
from .preprocess import Preprocessor, Resample, Downmix
|
|
@@ -26,6 +27,8 @@ class FLAC(LosslessEncoder):
|
|
|
26
27
|
:param compression_level: Any int value from 0 to 8 (Higher = better but slower)
|
|
27
28
|
:param preprocess: Any amount of preprocessors to run before passing it to the encoder.
|
|
28
29
|
:param verify: Make the encoder verify each encoded sample while encoding to ensure valid output.
|
|
30
|
+
:param threads: Number of threads to use for encoding. (Requires FLAC 1.5.0)
|
|
31
|
+
`None` will choose whatever your CPU has with a max of 8.
|
|
29
32
|
:param output: Custom output. Can be a dir or a file.
|
|
30
33
|
Do not specify an extension unless you know what you're doing.
|
|
31
34
|
"""
|
|
@@ -33,12 +36,14 @@ class FLAC(LosslessEncoder):
|
|
|
33
36
|
compression_level: int = 8
|
|
34
37
|
preprocess: Preprocessor | Sequence[Preprocessor] | None = Field(default_factory=Resample)
|
|
35
38
|
verify: bool = True
|
|
39
|
+
threads: int | None = None
|
|
36
40
|
output: PathLike | None = None
|
|
37
41
|
|
|
38
42
|
def encode_audio(self, fileIn: AudioFile | PathLike, quiet: bool = True, **kwargs) -> AudioFile:
|
|
39
43
|
if not isinstance(fileIn, AudioFile):
|
|
40
44
|
fileIn = AudioFile.from_file(fileIn, self)
|
|
41
45
|
flac = get_executable("flac")
|
|
46
|
+
version = get_binary_version(flac, r"flac .+? version (\d\.\d+\.\d+)")
|
|
42
47
|
output = make_output(fileIn.file, "flac", "libflac", self.output)
|
|
43
48
|
source = ensure_valid_in(fileIn, preprocess=self.preprocess, caller=self, valid_type=ValidInputType.W64_OR_FLAC, supports_pipe=False)
|
|
44
49
|
info(f"Encoding '{fileIn.file.stem}' to FLAC using libFLAC...", self)
|
|
@@ -46,12 +51,21 @@ class FLAC(LosslessEncoder):
|
|
|
46
51
|
args = [flac, f"-{self.compression_level}", "-o", str(output)] + self.get_custom_args()
|
|
47
52
|
if self.verify:
|
|
48
53
|
args.append("--verify")
|
|
54
|
+
|
|
55
|
+
if self.threads or self.threads is None:
|
|
56
|
+
if re.search(r"1\.[2|3|4]\.\d+?", version):
|
|
57
|
+
warn("Using outdated FLAC encoder that does not support threading!", self)
|
|
58
|
+
else:
|
|
59
|
+
if self.threads is None:
|
|
60
|
+
self.threads = min(os.cpu_count(), 8)
|
|
61
|
+
args.append(f"--threads={self.threads}")
|
|
62
|
+
|
|
49
63
|
args.append(str(source.file.resolve()) if isinstance(source, AudioFile) else "-")
|
|
50
64
|
|
|
51
65
|
stdin = subprocess.DEVNULL if isinstance(source, AudioFile) else source.stdout
|
|
52
66
|
|
|
53
67
|
if not run_cmd_pb(args, quiet, ProgressBarConfig("Encoding..."), shell=False, stdin=stdin):
|
|
54
|
-
tags =
|
|
68
|
+
tags = dict(ENCODER=f"FLAC {version}", ENCODER_SETTINGS=self.get_mediainfo_settings(args))
|
|
55
69
|
clean_temp_files()
|
|
56
70
|
return AudioFile(output, fileIn.container_delay, fileIn.source, tags=tags)
|
|
57
71
|
else:
|
|
@@ -304,7 +318,7 @@ class FDK_AAC(Encoder):
|
|
|
304
318
|
exe = get_executable("ffmpeg") if not self.use_binary else get_executable("fdkaac")
|
|
305
319
|
|
|
306
320
|
if os.name == "nt":
|
|
307
|
-
warn("It is strongly recommended to use qAAC on windows. See docs.", self,
|
|
321
|
+
warn("It is strongly recommended to use qAAC on windows. See docs.", self, 1)
|
|
308
322
|
info(f"Encoding '{fileIn.file.stem}' to AAC using libFDK...", self)
|
|
309
323
|
|
|
310
324
|
tags = dict[str, str]()
|
|
@@ -14,10 +14,10 @@ from ..utils.log import error, warn, debug, info
|
|
|
14
14
|
from ..utils.download import get_executable
|
|
15
15
|
from ..utils.parsing import parse_audioinfo
|
|
16
16
|
from ..utils.files import get_absolute_track
|
|
17
|
-
from ..utils.types import Trim, PathLike, TrackType
|
|
17
|
+
from ..utils.types import Trim, PathLike, TrackType, TimeSourceT, TimeScaleT, TimeScale
|
|
18
18
|
from ..utils.env import get_temp_workdir, run_commandline, communicate_stdout
|
|
19
19
|
from ..utils.subprogress import run_cmd_pb, ProgressBarConfig
|
|
20
|
-
from ..utils.convert import
|
|
20
|
+
from ..utils.convert import format_timedelta, resolve_timesource_and_scale, TimeType
|
|
21
21
|
|
|
22
22
|
__all__ = ["Eac3to", "Sox", "FFMpeg", "MkvExtract"]
|
|
23
23
|
|
|
@@ -63,7 +63,7 @@ class Eac3to(Extractor):
|
|
|
63
63
|
info(f"Extracting audio track {self.track} from '{input.stem}'...", self)
|
|
64
64
|
|
|
65
65
|
out = make_output(input, extension, f"extracted_{self.track}", self.output)
|
|
66
|
-
code, stdout = communicate_stdout(f'"{eac3to}" "{input}" {track.track_id+1}: "{out}" {self.append}')
|
|
66
|
+
code, stdout = communicate_stdout(f'"{eac3to}" "{input}" {track.track_id + 1}: "{out}" {self.append}')
|
|
67
67
|
if code == 0:
|
|
68
68
|
if not out.exists():
|
|
69
69
|
pattern_str = rf"{re.escape(out.stem)} DELAY.*\.{extension}"
|
|
@@ -171,9 +171,11 @@ class FFMpeg(HasExtractor, HasTrimmer):
|
|
|
171
171
|
If you're working with lossless files it is strongly recommended to use SoX instead.
|
|
172
172
|
|
|
173
173
|
:param trim: Can be a single trim or a sequence of trims.
|
|
174
|
-
:param trim_use_ms: Will use milliseconds instead of frame numbers
|
|
175
|
-
:param fps: Fps fraction that will be used for the conversion. Also accepts a timecode (v2) file.
|
|
176
174
|
:param preserve_delay: Will preserve existing container delay
|
|
175
|
+
:param trim_use_ms: Will use milliseconds instead of frame numbers
|
|
176
|
+
:param timesource: The source of timestamps/timecodes. For details check the docstring on the type.
|
|
177
|
+
:param timescale: Unit of time (in seconds) in terms of which frame timestamps are represented.\n
|
|
178
|
+
For details check the docstring on the type.
|
|
177
179
|
:param num_frames: Total number of frames used for calculations
|
|
178
180
|
:param output: Custom output. Can be a dir or a file.
|
|
179
181
|
Do not specify an extension unless you know what you're doing.
|
|
@@ -182,7 +184,8 @@ class FFMpeg(HasExtractor, HasTrimmer):
|
|
|
182
184
|
trim: Trim | list[Trim] | None = None
|
|
183
185
|
preserve_delay: bool = False
|
|
184
186
|
trim_use_ms: bool = False
|
|
185
|
-
|
|
187
|
+
timesource: TimeSourceT = Fraction(24000, 1001)
|
|
188
|
+
timescale: TimeScaleT = TimeScale.MKV
|
|
186
189
|
num_frames: int = 0
|
|
187
190
|
output: PathLike | None = None
|
|
188
191
|
|
|
@@ -208,18 +211,21 @@ class FFMpeg(HasExtractor, HasTrimmer):
|
|
|
208
211
|
if self.trim_use_ms:
|
|
209
212
|
arg += f" -ss {format_timedelta(timedelta(milliseconds=trim[0]))}"
|
|
210
213
|
else:
|
|
211
|
-
|
|
214
|
+
millis = self.resolved_ts.frame_to_time(trim[0], TimeType.EXACT, 3)
|
|
215
|
+
arg += f" -ss {format_timedelta(timedelta(milliseconds=millis))}"
|
|
212
216
|
if trim[1] is not None and trim[1] != 0:
|
|
213
217
|
end_frame = self.num_frames + trim[1] if trim[1] < 0 else trim[1]
|
|
214
218
|
if self.trim_use_ms:
|
|
215
219
|
arg += f" -to {format_timedelta(timedelta(milliseconds=trim[1]))}"
|
|
216
220
|
else:
|
|
217
|
-
|
|
221
|
+
millis = self.resolved_ts.frame_to_time(end_frame, TimeType.EXACT, 3)
|
|
222
|
+
arg += f" -to {format_timedelta(timedelta(milliseconds=millis))}"
|
|
218
223
|
return arg
|
|
219
224
|
|
|
220
225
|
def trim_audio(self, input: AudioFile, quiet: bool = True) -> AudioFile:
|
|
221
226
|
if not isinstance(input, AudioFile):
|
|
222
227
|
input = AudioFile.from_file(input, self)
|
|
228
|
+
self.resolved_ts = resolve_timesource_and_scale(self.timesource, self.timescale, caller=self)
|
|
223
229
|
self.trim = sanitize_trims(self.trim, self.num_frames, not self.trim_use_ms, caller=self)
|
|
224
230
|
minfo = input.get_mediainfo()
|
|
225
231
|
form = format_from_track(minfo)
|
|
@@ -248,7 +254,7 @@ class FFMpeg(HasExtractor, HasTrimmer):
|
|
|
248
254
|
args.append(str(out.resolve()))
|
|
249
255
|
if not run_commandline(args, quiet):
|
|
250
256
|
if tr[0] and lossy:
|
|
251
|
-
ms = tr[0] if self.trim_use_ms else
|
|
257
|
+
ms = tr[0] if self.trim_use_ms else self.resolved_ts.frame_to_time(tr[0], TimeType.EXACT, 3)
|
|
252
258
|
cont_delay = self._calc_delay(ms, ainfo.num_samples(), getattr(minfo, "sampling_rate", 48000))
|
|
253
259
|
debug(f"Additional delay of {cont_delay} ms will be applied to fix remaining sync", self)
|
|
254
260
|
if self.preserve_delay:
|
|
@@ -269,8 +275,10 @@ class FFMpeg(HasExtractor, HasTrimmer):
|
|
|
269
275
|
if lossy:
|
|
270
276
|
nArgs[2:1] = splitcommand(self._targs(tr))
|
|
271
277
|
if first:
|
|
272
|
-
|
|
273
|
-
|
|
278
|
+
if tr[0]:
|
|
279
|
+
ms = tr[0] if self.trim_use_ms else self.resolved_ts.frame_to_time(tr[0], TimeType.EXACT, 3)
|
|
280
|
+
cont_delay = self._calc_delay(ms, ainfo.num_samples(), getattr(minfo, "sampling_rate", 48000))
|
|
281
|
+
debug(f"Additional delay of {cont_delay} ms will be applied to fix remaining sync", self)
|
|
274
282
|
first = False
|
|
275
283
|
else:
|
|
276
284
|
nArgs.extend(splitcommand(self._targs(tr)))
|
|
@@ -366,7 +374,9 @@ class Sox(Trimmer):
|
|
|
366
374
|
:param trim: List of Trims or a single Trim, which is a Tuple of two frame numbers or milliseconds
|
|
367
375
|
:param preserve_delay: Keeps existing container delay if True
|
|
368
376
|
:param trim_use_ms: Will use milliseconds instead of frame numbers
|
|
369
|
-
:param
|
|
377
|
+
:param timesource: The source of timestamps/timecodes. For details check the docstring on the type.
|
|
378
|
+
:param timescale: Unit of time (in seconds) in terms of which frame timestamps are represented.\n
|
|
379
|
+
For details check the docstring on the type.
|
|
370
380
|
:param num_frames: Total number of frames used for calculations
|
|
371
381
|
:param output: Custom output. Can be a dir or a file.
|
|
372
382
|
Do not specify an extension unless you know what you're doing.
|
|
@@ -375,7 +385,8 @@ class Sox(Trimmer):
|
|
|
375
385
|
trim: Trim | list[Trim] | None = None
|
|
376
386
|
preserve_delay: bool = False
|
|
377
387
|
trim_use_ms: bool = False
|
|
378
|
-
|
|
388
|
+
timesource: TimeSourceT = Fraction(24000, 1001)
|
|
389
|
+
timescale: TimeScaleT = TimeScale.MKV
|
|
379
390
|
num_frames: int = 0
|
|
380
391
|
output: PathLike | None = None
|
|
381
392
|
|
|
@@ -386,7 +397,7 @@ class Sox(Trimmer):
|
|
|
386
397
|
if self.trim_use_ms:
|
|
387
398
|
return abs(val) / 1000
|
|
388
399
|
else:
|
|
389
|
-
return
|
|
400
|
+
return self.resolved_ts.frame_to_time(abs(val), TimeType.EXACT).__float__()
|
|
390
401
|
|
|
391
402
|
def trim_audio(self, input: AudioFile, quiet: bool = True) -> AudioFile:
|
|
392
403
|
import sox
|
|
@@ -394,6 +405,7 @@ class Sox(Trimmer):
|
|
|
394
405
|
if not isinstance(input, AudioFile):
|
|
395
406
|
input = AudioFile.from_file(input, self)
|
|
396
407
|
out = make_output(input.file, "flac", "trimmed", self.output)
|
|
408
|
+
self.resolved_ts = resolve_timesource_and_scale(self.timesource, self.timescale, caller=self)
|
|
397
409
|
self.trim = sanitize_trims(self.trim, self.num_frames, not self.trim_use_ms, allow_negative_start=True, caller=self)
|
|
398
410
|
source = ensure_valid_in(input, caller=self, supports_pipe=False)
|
|
399
411
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
import sys
|
|
3
4
|
import wget
|
|
4
5
|
import shlex
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
|
-
from .utils.log import info, warn
|
|
10
|
+
from .utils.log import info, warn, error
|
|
10
11
|
from .utils.download import unpack_all
|
|
11
12
|
from .utils.env import get_temp_workdir
|
|
12
|
-
from .utils.files import clean_temp_files
|
|
13
|
+
from .utils.files import clean_temp_files, ensure_path_exists, ensure_path, is_video_file
|
|
14
|
+
from .utils.convert import get_timemeta_from_video
|
|
13
15
|
|
|
14
16
|
CONF = "([green bold]Y[/] | [red]n[/])"
|
|
15
17
|
|
|
@@ -212,3 +214,22 @@ def _run_powershell(args: str | list[str], quiet: bool = False) -> int:
|
|
|
212
214
|
p.communicate()
|
|
213
215
|
print("")
|
|
214
216
|
return p.returncode
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def generate_videometa(file: str | None = None, output: str | None = None):
|
|
220
|
+
if not file:
|
|
221
|
+
error("You have to pass an input video file!", None)
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
|
|
224
|
+
in_path = ensure_path_exists(file, None)
|
|
225
|
+
if not is_video_file(in_path):
|
|
226
|
+
error(f'"{in_path.name}" is not a video file!', None)
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
if not output:
|
|
230
|
+
info("Generating VideoMeta file in your current work directory.", None)
|
|
231
|
+
output = ensure_path(os.getcwd(), None) / f"{in_path.stem}_meta.json"
|
|
232
|
+
else:
|
|
233
|
+
output = ensure_path(output, None)
|
|
234
|
+
|
|
235
|
+
get_timemeta_from_video(in_path, output, None)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from fractions import Fraction
|
|
2
2
|
from datetime import timedelta
|
|
3
|
-
from collections.abc import Sequence
|
|
4
3
|
|
|
5
4
|
from .utils.log import warn, error, info, danger
|
|
5
|
+
from .utils.types import TimeScaleT, TimeScale, TimeSourceT
|
|
6
6
|
from .muxing.muxfiles import AudioFile
|
|
7
7
|
from .audio.audioutils import is_fancy_codec
|
|
8
8
|
from .audio.encoders import Opus
|
|
@@ -19,9 +19,10 @@ def do_audio(
|
|
|
19
19
|
fileIn: PathLike | list[PathLike],
|
|
20
20
|
track: int = 0,
|
|
21
21
|
trims: Trim | list[Trim] | None = None,
|
|
22
|
-
|
|
22
|
+
timesource: TimeSourceT = Fraction(24000, 1001),
|
|
23
|
+
timescale: TimeScaleT = TimeScale.MKV,
|
|
23
24
|
num_frames: int = 0,
|
|
24
|
-
extractor: Extractor = FFMpeg.Extractor(),
|
|
25
|
+
extractor: Extractor | None = FFMpeg.Extractor(),
|
|
25
26
|
trimmer: Trimmer | None = AutoTrimmer(),
|
|
26
27
|
encoder: Encoder | None = AutoEncoder(),
|
|
27
28
|
quiet: bool = True,
|
|
@@ -33,14 +34,16 @@ def do_audio(
|
|
|
33
34
|
:param fileIn: Input file
|
|
34
35
|
:param track: Audio track number
|
|
35
36
|
:param trims: Frame ranges to trim and/or combine, e.g. (24, -24) or [(24, 500), (700, 900)]
|
|
36
|
-
:param
|
|
37
|
+
:param timesource: The source of timestamps/timecodes. For details check the docstring on the type.
|
|
38
|
+
:param timescale: Unit of time (in seconds) in terms of which frame timestamps are represented.\n
|
|
39
|
+
For details check the docstring on the type.
|
|
37
40
|
:param num_frames: Total number of frames, used for negative numbers in trims
|
|
38
41
|
:param extractor: Tool used to extract the audio
|
|
39
42
|
:param trimmer: Tool used to trim the audio
|
|
40
43
|
AutoTrimmer means it will choose ffmpeg for lossy and Sox for lossless
|
|
41
44
|
|
|
42
45
|
:param encoder: Tool used to encode the audio
|
|
43
|
-
AutoEncoder means it won't reencode lossy and choose opus otherwise
|
|
46
|
+
AutoEncoder means it won't reencode lossy and choose opus otherwise.
|
|
44
47
|
|
|
45
48
|
:param quiet: Whether the tool output should be visible
|
|
46
49
|
:param output: Custom output file or directory, extensions will be automatically added
|
|
@@ -77,6 +80,8 @@ def do_audio(
|
|
|
77
80
|
audio = FFMpeg.Concat(extracted).concat_audio()
|
|
78
81
|
else:
|
|
79
82
|
audio = extractor.extract_audio(fileIn, quiet)
|
|
83
|
+
else:
|
|
84
|
+
audio = ensure_path_exists(fileIn, do_audio)
|
|
80
85
|
|
|
81
86
|
if not isinstance(audio, AudioFile):
|
|
82
87
|
audio = AudioFile.from_file(audio, do_audio)
|
|
@@ -89,22 +94,23 @@ def do_audio(
|
|
|
89
94
|
else:
|
|
90
95
|
trimmer = Sox()
|
|
91
96
|
|
|
97
|
+
mediainfo = audio.get_mediainfo()
|
|
98
|
+
|
|
92
99
|
if isinstance(encoder, AutoEncoder):
|
|
93
100
|
if lossy:
|
|
94
101
|
encoder = None
|
|
95
|
-
elif is_fancy_codec(
|
|
102
|
+
elif is_fancy_codec(mediainfo):
|
|
96
103
|
encoder = None
|
|
97
104
|
warn("Audio will not be reencoded due to having Atmos or special DTS features.", do_audio, 2)
|
|
98
105
|
else:
|
|
99
106
|
encoder = Opus()
|
|
100
107
|
|
|
101
108
|
if trimmer and trims:
|
|
102
|
-
if not isinstance(fps, str) and isinstance(fps, Sequence):
|
|
103
|
-
fps = Fraction(*fps)
|
|
104
109
|
setattr(trimmer, "trim", trims)
|
|
105
|
-
setattr(trimmer, "
|
|
110
|
+
setattr(trimmer, "timesource", timesource)
|
|
111
|
+
setattr(trimmer, "timescale", timescale)
|
|
106
112
|
setattr(trimmer, "num_frames", num_frames)
|
|
107
|
-
if not
|
|
113
|
+
if not encoder:
|
|
108
114
|
setattr(trimmer, "output", output)
|
|
109
115
|
trimmed = trimmer.trim_audio(audio, quiet)
|
|
110
116
|
ensure_path(audio.file, do_audio).unlink(missing_ok=True)
|