twotone 1.0.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.
- twotone-1.0.0/LICENSE +8 -0
- twotone-1.0.0/PKG-INFO +27 -0
- twotone-1.0.0/README.md +115 -0
- twotone-1.0.0/pyproject.toml +37 -0
- twotone-1.0.0/setup.cfg +4 -0
- twotone-1.0.0/twotone/__init__.py +0 -0
- twotone-1.0.0/twotone/__main__.py +5 -0
- twotone-1.0.0/twotone/tools/__init__.py +0 -0
- twotone-1.0.0/twotone/tools/concatenate.py +201 -0
- twotone-1.0.0/twotone/tools/language_fixer.py +704 -0
- twotone-1.0.0/twotone/tools/melt/__init__.py +7 -0
- twotone-1.0.0/twotone/tools/melt/attachments_picker.py +19 -0
- twotone-1.0.0/twotone/tools/melt/debug_routines.py +68 -0
- twotone-1.0.0/twotone/tools/melt/duplicates_source.py +14 -0
- twotone-1.0.0/twotone/tools/melt/jellyfin.py +182 -0
- twotone-1.0.0/twotone/tools/melt/melt.py +12 -0
- twotone-1.0.0/twotone/tools/melt/melt_analyzer.py +431 -0
- twotone-1.0.0/twotone/tools/melt/melt_cache.py +205 -0
- twotone-1.0.0/twotone/tools/melt/melt_common.py +81 -0
- twotone-1.0.0/twotone/tools/melt/melt_performer.py +1086 -0
- twotone-1.0.0/twotone/tools/melt/melt_plan.py +148 -0
- twotone-1.0.0/twotone/tools/melt/melt_tool.py +234 -0
- twotone-1.0.0/twotone/tools/melt/pair_matcher.py +1718 -0
- twotone-1.0.0/twotone/tools/melt/phash_cache.py +22 -0
- twotone-1.0.0/twotone/tools/melt/static_source.py +36 -0
- twotone-1.0.0/twotone/tools/melt/streams_picker.py +306 -0
- twotone-1.0.0/twotone/tools/merge.py +401 -0
- twotone-1.0.0/twotone/tools/subtitles_fixer.py +266 -0
- twotone-1.0.0/twotone/tools/tool.py +42 -0
- twotone-1.0.0/twotone/tools/transcode.py +454 -0
- twotone-1.0.0/twotone/tools/utilities.py +162 -0
- twotone-1.0.0/twotone/tools/utils/files_utils.py +78 -0
- twotone-1.0.0/twotone/tools/utils/generic_utils.py +138 -0
- twotone-1.0.0/twotone/tools/utils/image_utils.py +32 -0
- twotone-1.0.0/twotone/tools/utils/language_utils.py +42 -0
- twotone-1.0.0/twotone/tools/utils/process_utils.py +107 -0
- twotone-1.0.0/twotone/tools/utils/requirements_utils.py +239 -0
- twotone-1.0.0/twotone/tools/utils/subtitles_utils.py +274 -0
- twotone-1.0.0/twotone/tools/utils/tmdb_cache.py +98 -0
- twotone-1.0.0/twotone/tools/utils/video_utils.py +1026 -0
- twotone-1.0.0/twotone/twotone.py +283 -0
- twotone-1.0.0/twotone.egg-info/PKG-INFO +27 -0
- twotone-1.0.0/twotone.egg-info/SOURCES.txt +45 -0
- twotone-1.0.0/twotone.egg-info/dependency_links.txt +1 -0
- twotone-1.0.0/twotone.egg-info/entry_points.txt +2 -0
- twotone-1.0.0/twotone.egg-info/requires.txt +22 -0
- twotone-1.0.0/twotone.egg-info/top_level.txt +1 -0
twotone-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
Copyright 2023 Michał Walenciak
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
twotone-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twotone
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Tools for batch video file manipulations
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: argcomplete>=3.6.3
|
|
8
|
+
Requires-Dist: faust_cchardet>=2.1.19
|
|
9
|
+
Requires-Dist: ImageHash>=4.3.2
|
|
10
|
+
Requires-Dist: numpy>=2.2.6
|
|
11
|
+
Requires-Dist: opencv_python>=4.11.0.86
|
|
12
|
+
Requires-Dist: overrides>=7.7.0
|
|
13
|
+
Requires-Dist: Pillow>=12.2.0
|
|
14
|
+
Requires-Dist: platformdirs>=4.9.6
|
|
15
|
+
Requires-Dist: py3langid>=0.3.0
|
|
16
|
+
Requires-Dist: pycountry>=24.6.1
|
|
17
|
+
Requires-Dist: pysubs2>=1.8.0
|
|
18
|
+
Requires-Dist: Requests>=2.33.1
|
|
19
|
+
Requires-Dist: scikit_learn>=1.7.2
|
|
20
|
+
Requires-Dist: scikit-image>=0.25.2
|
|
21
|
+
Requires-Dist: scipy>=1.15.3
|
|
22
|
+
Requires-Dist: tqdm>=4.67.1
|
|
23
|
+
Provides-Extra: ocr
|
|
24
|
+
Requires-Dist: pytesseract; extra == "ocr"
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: parameterized>=0.9.0; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
twotone-1.0.0/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
## Tools for Batch Video File Manipulations
|
|
2
|
+
|
|
3
|
+
### Overview
|
|
4
|
+
|
|
5
|
+
**TwoTone** is a versatile tool with various subtools for batch video file manipulations.
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This installs TwoTone in editable mode: the `twotone` command appears on PATH, and any code changes take effect immediately without reinstalling.
|
|
14
|
+
|
|
15
|
+
### Running TwoTone
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
twotone <global options> <tool-name> <tool-specific options>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Shell Autocompletion (bash)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
twotone --install-completion
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open a new terminal. Tab-completion now works:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
twotone <TAB> → concatenate, language_fix, melt, merge, ...
|
|
31
|
+
twotone --<TAB> → --verbose, --quiet, --no-dry-run, ...
|
|
32
|
+
twotone merge --<TAB> → merge-specific arguments
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To remove: `twotone --uninstall-completion`
|
|
36
|
+
|
|
37
|
+
### Getting Help
|
|
38
|
+
|
|
39
|
+
To see a list of global options, available tools, and their descriptions:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
twotone --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To get help for a specific tool:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
twotone <tool-name> --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Important Notes
|
|
52
|
+
|
|
53
|
+
Dry Run Mode: all tools run in a dry run mode by default (no files are being modified). Use the -r or --no-dry-run global option to perform actual operation. In live run mode most tools remove their input files after successful processing.<br/>
|
|
54
|
+
It is safe to stop execution with ctrl+c. All tools handle proper signal and will stop as soon as possible.<br/>
|
|
55
|
+
Working Directory: use -w or --working-dir to specify where temporary and working files are created. By default the directory is taken from the operating system's user data location for the application.<br/>
|
|
56
|
+
|
|
57
|
+
Data Safety: Always back up your data before using any tool, as source files may be deleted during processing.
|
|
58
|
+
|
|
59
|
+
### Available Tools
|
|
60
|
+
#### Merge Video Files with Subtitles into MKV Files
|
|
61
|
+
|
|
62
|
+
The merge tool searches for movie and subtitle files and merges them into a single MKV file.
|
|
63
|
+
|
|
64
|
+
By default, subtitles are added without a language label. You can specify a language with the --language option.
|
|
65
|
+
|
|
66
|
+
For a full list of options:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
twotone merge --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Fix Missing Track Languages
|
|
73
|
+
|
|
74
|
+
The language_fix tool scans MKV files and fills missing language metadata for subtitle tracks by extracting them and running language detection. It can also infer audio track language from the track name (use --audio to enable; heuristic may be inaccurate).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
twotone language_fix --help
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Automatic Video transcoding
|
|
81
|
+
|
|
82
|
+
The transcode tool transcodes videos using the x265 codec.
|
|
83
|
+
|
|
84
|
+
It takes a video directory as an input and determines the optimal CRF for each video by comparing the original and encoded versions.
|
|
85
|
+
The script aims to achieve a quality level where SSIM ≈ 0.98 (by default).
|
|
86
|
+
|
|
87
|
+
For a full list of options:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
twotone transcode --help
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Movies concatenation
|
|
94
|
+
|
|
95
|
+
The concatenate tool looks for movie files which seem to be split into a few files (like CD1, CD2 etc) and glues them into one file.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
twotone concatenate --help
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Combine Duplicate Videos (melt)
|
|
102
|
+
|
|
103
|
+
The melt tool scans for duplicate video files and creates a single output using the best quality segments from each copy. Duplicates can be provided manually or taken from a Jellyfin server. Input files are kept intact by default.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
twotone melt --help
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Miscellaneous utilities
|
|
110
|
+
|
|
111
|
+
The utilities tool groups smaller helpers. Currently it provides the *scenes* subtool for extracting frames from a video and saving them into per-scene folders.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
twotone utilities scenes --help
|
|
115
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "twotone"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Tools for batch video file manipulations"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"argcomplete>=3.6.3",
|
|
12
|
+
"faust_cchardet>=2.1.19",
|
|
13
|
+
"ImageHash>=4.3.2",
|
|
14
|
+
"numpy>=2.2.6",
|
|
15
|
+
"opencv_python>=4.11.0.86",
|
|
16
|
+
"overrides>=7.7.0",
|
|
17
|
+
"Pillow>=12.2.0",
|
|
18
|
+
"platformdirs>=4.9.6",
|
|
19
|
+
"py3langid>=0.3.0",
|
|
20
|
+
"pycountry>=24.6.1",
|
|
21
|
+
"pysubs2>=1.8.0",
|
|
22
|
+
"Requests>=2.33.1",
|
|
23
|
+
"scikit_learn>=1.7.2",
|
|
24
|
+
"scikit-image>=0.25.2",
|
|
25
|
+
"scipy>=1.15.3",
|
|
26
|
+
"tqdm>=4.67.1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
ocr = ["pytesseract"]
|
|
31
|
+
test = ["parameterized>=0.9.0"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
twotone = "twotone.twotone:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
include = ["twotone*"]
|
twotone-1.0.0/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
|
|
2
|
+
import argparse
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from overrides import override
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
from .tool import EmptyPlan, Plan, Tool
|
|
12
|
+
from twotone.tools.utils import generic_utils, process_utils, video_utils, files_utils
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Concatenate(generic_utils.InterruptibleProcess):
|
|
16
|
+
def __init__(self, logger: logging.Logger, working_dir: str):
|
|
17
|
+
super().__init__()
|
|
18
|
+
|
|
19
|
+
self.logger = logger
|
|
20
|
+
self.working_dir = working_dir
|
|
21
|
+
|
|
22
|
+
def analyze(self, path: str, ignore_warnings: bool = False) -> dict[str, list[tuple[str, int]]] | None:
|
|
23
|
+
self.logger.info(f"Collecting video files from path {path}")
|
|
24
|
+
video_files = video_utils.collect_video_files(path, self)
|
|
25
|
+
|
|
26
|
+
self.logger.info("Finding splitted videos")
|
|
27
|
+
parts_regex = re.compile("(.*[^0-9a-z]+)(cd\\d+)([^0-9a-z]+.*)", re.IGNORECASE)
|
|
28
|
+
|
|
29
|
+
splitted = []
|
|
30
|
+
for video_file in video_files:
|
|
31
|
+
if parts_regex.match(video_file):
|
|
32
|
+
splitted.append(video_file)
|
|
33
|
+
else:
|
|
34
|
+
self.logger.debug(f"File {video_file} does not match pattern")
|
|
35
|
+
|
|
36
|
+
self.logger.info("Matching videos")
|
|
37
|
+
matched_videos: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
|
38
|
+
for video in splitted:
|
|
39
|
+
match = parts_regex.search(video)
|
|
40
|
+
if match is None:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
path = match.group(1)
|
|
44
|
+
if path[-1] == os.sep:
|
|
45
|
+
# movie path is like: /dir/movie/cd1.mp4
|
|
46
|
+
# repeat last dir name as a base for output video file
|
|
47
|
+
last_dir_name = os.path.basename(os.path.normpath(path))
|
|
48
|
+
name_without_part_number = os.path.join(path, last_dir_name) + match.group(3)
|
|
49
|
+
else:
|
|
50
|
+
# movie path is like: /dir/movie/movie cd1.mp4.
|
|
51
|
+
# remove last char before CDXXX as it is most likely space or hyphen and use it as a base for output video file
|
|
52
|
+
name_without_part_number = path[:-1] + match.group(3)
|
|
53
|
+
|
|
54
|
+
# ffmpeg does not support rmvb container, use mp4
|
|
55
|
+
dir, name, extn = files_utils.split_path(name_without_part_number)
|
|
56
|
+
if extn.lower() == "rmvb":
|
|
57
|
+
name_without_part_number = os.path.join(dir, name + ".mkv")
|
|
58
|
+
|
|
59
|
+
part = match.group(2)
|
|
60
|
+
partNo = int(part[2:]) # drop 'CD'
|
|
61
|
+
matched_videos[name_without_part_number].append((video, partNo))
|
|
62
|
+
|
|
63
|
+
self.logger.info("Processing groups")
|
|
64
|
+
warnings = False
|
|
65
|
+
sorted_videos: dict[str, list[tuple[str, int]]] = {}
|
|
66
|
+
valid_videos: dict[str, list[tuple[str, int]]] = {}
|
|
67
|
+
for common_name, details in matched_videos.items():
|
|
68
|
+
|
|
69
|
+
# sort parts by part number [1]
|
|
70
|
+
details = sorted(details, key = lambda detail: detail[1])
|
|
71
|
+
sorted_videos[common_name] = details
|
|
72
|
+
group_has_warning = False
|
|
73
|
+
|
|
74
|
+
# collect all part numbers
|
|
75
|
+
parts = []
|
|
76
|
+
for _, partNo in details:
|
|
77
|
+
parts.append(partNo)
|
|
78
|
+
|
|
79
|
+
if len(parts) < 2:
|
|
80
|
+
self.logger.warning(f"There are less than two parts for video represented under a common name: {common_name}")
|
|
81
|
+
warnings = True
|
|
82
|
+
group_has_warning = True
|
|
83
|
+
|
|
84
|
+
# expect parts to be numbered from 1 to N
|
|
85
|
+
for i, value in enumerate(parts):
|
|
86
|
+
if i + 1 != value:
|
|
87
|
+
self.logger.warning(f"There is a mismatch in CD numbers for a group of files represented under a common name: {common_name}")
|
|
88
|
+
warnings = True
|
|
89
|
+
group_has_warning = True
|
|
90
|
+
|
|
91
|
+
if group_has_warning and ignore_warnings:
|
|
92
|
+
continue
|
|
93
|
+
valid_videos[common_name] = details
|
|
94
|
+
|
|
95
|
+
if warnings and not ignore_warnings:
|
|
96
|
+
self.logger.error("Fix above warnings and try again")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if ignore_warnings:
|
|
100
|
+
return valid_videos
|
|
101
|
+
return sorted_videos
|
|
102
|
+
|
|
103
|
+
def perform(self, sorted_videos: dict[str, list[tuple[str, int]]]) -> None:
|
|
104
|
+
self.logger.info("Starting concatenation")
|
|
105
|
+
for output, details in tqdm(sorted_videos.items(), desc="Concatenating", unit="movie", **generic_utils.get_tqdm_defaults()):
|
|
106
|
+
self.check_for_stop()
|
|
107
|
+
|
|
108
|
+
input_files = [video for video, _ in details]
|
|
109
|
+
|
|
110
|
+
audio_codec = "copy"
|
|
111
|
+
for input_file in input_files:
|
|
112
|
+
file_details = video_utils.get_video_data(input_file)
|
|
113
|
+
audio_streams = file_details.get("audio", [])
|
|
114
|
+
for audio_stream in audio_streams:
|
|
115
|
+
codec = audio_stream.get("codec")
|
|
116
|
+
if codec and codec.lower() == "cook":
|
|
117
|
+
audio_codec = "aac"
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
def escape_path(path: str) -> str:
|
|
121
|
+
return path.replace("'", "'\\''")
|
|
122
|
+
|
|
123
|
+
input_file_content = [f"file '{escape_path(input_file)}'" for input_file in input_files]
|
|
124
|
+
with files_utils.TempFileManager("\n".join(input_file_content), "txt", directory=self.working_dir) as input_file:
|
|
125
|
+
ffmpeg_args = ["-f", "concat", "-safe", "0", "-i", input_file, "-c:v", "copy", "-c:a", audio_codec, output]
|
|
126
|
+
|
|
127
|
+
self.logger.info(f"Concatenating files into {output} file")
|
|
128
|
+
status = process_utils.start_process("ffmpeg", ffmpeg_args)
|
|
129
|
+
if status.returncode == 0:
|
|
130
|
+
for input_file in input_files:
|
|
131
|
+
os.remove(input_file)
|
|
132
|
+
else:
|
|
133
|
+
self.logger.error(f"Problems with concatenation, skipping file {output}")
|
|
134
|
+
self.logger.debug(status.stdout)
|
|
135
|
+
self.logger.debug(status.stderr)
|
|
136
|
+
if os.path.exists(output):
|
|
137
|
+
os.remove(output)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ConcatenateTool(Tool):
|
|
141
|
+
def __init__(self) -> None:
|
|
142
|
+
super().__init__()
|
|
143
|
+
|
|
144
|
+
@override
|
|
145
|
+
def setup_parser(self, parser: argparse.ArgumentParser):
|
|
146
|
+
parser.description = (
|
|
147
|
+
"Concatenate is a tool for concatenating video files splitted into many files into one.\n"
|
|
148
|
+
"For example if you have movie consisting of two files: movie-cd1.avi and movie-cd2.avi\n"
|
|
149
|
+
"then 'concatenate' tool will glue them into one file 'movie.avi'.\n"
|
|
150
|
+
"If your files come with subtitle files, you may want to use 'merge' tool first\n"
|
|
151
|
+
"to merge video files with corresponding subtitle files.\n"
|
|
152
|
+
"Otherwise you will end up with one video file and two subtitle files for cd1 and cd2 which will be useless now"
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument('videos_path',
|
|
155
|
+
nargs=1,
|
|
156
|
+
help='Path with videos to concatenate.')
|
|
157
|
+
parser.add_argument('--ignore-warnings',
|
|
158
|
+
action='store_true',
|
|
159
|
+
help='Skip videos with warnings and continue with valid groups.')
|
|
160
|
+
|
|
161
|
+
@override
|
|
162
|
+
def analyze(self, args, logger: logging.Logger, working_dir: str) -> Plan:
|
|
163
|
+
concatenator = Concatenate(logger, working_dir=working_dir)
|
|
164
|
+
analysis = concatenator.analyze(args.videos_path[0], ignore_warnings=args.ignore_warnings)
|
|
165
|
+
if analysis is None:
|
|
166
|
+
return EmptyPlan()
|
|
167
|
+
return ConcatenatePlan(items=analysis)
|
|
168
|
+
|
|
169
|
+
@override
|
|
170
|
+
def perform(self, args, logger: logging.Logger, working_dir: str, plan: Plan) -> None:
|
|
171
|
+
if not isinstance(plan, ConcatenatePlan):
|
|
172
|
+
raise TypeError(f"Expected ConcatenatePlan, got {type(plan).__name__}")
|
|
173
|
+
|
|
174
|
+
concatenator = Concatenate(logger, working_dir)
|
|
175
|
+
concatenator.perform(plan.items)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class ConcatenatePlan:
|
|
180
|
+
items: dict[str, list[tuple[str, int]]]
|
|
181
|
+
|
|
182
|
+
def is_empty(self) -> bool:
|
|
183
|
+
return not self.items
|
|
184
|
+
|
|
185
|
+
def render(self, logger: logging.Logger) -> None:
|
|
186
|
+
if not self.items:
|
|
187
|
+
logger.info("No videos to concatenate.")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
logger.info("Planned concatenations: %d", len(self.items))
|
|
191
|
+
for output, details in self.items.items():
|
|
192
|
+
paths = [path for path, _ in details]
|
|
193
|
+
common_path = os.path.commonpath(paths) if paths else ""
|
|
194
|
+
if common_path:
|
|
195
|
+
logger.info("Files from %s:", common_path)
|
|
196
|
+
for path, _ in details:
|
|
197
|
+
logger.info(" %s", os.path.relpath(path, common_path))
|
|
198
|
+
else:
|
|
199
|
+
for path, part in details:
|
|
200
|
+
logger.info(" part %d: %s", part, path)
|
|
201
|
+
logger.info(" -> %s", output)
|