baycone 0.7.0__py3-none-any.whl
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.
- baycone/__init__.py +19 -0
- baycone/audio.py +128 -0
- baycone/encoding.py +741 -0
- baycone/errors.py +18 -0
- baycone/ffmpeg.py +530 -0
- baycone/files.py +40 -0
- baycone/languages.py +213 -0
- baycone/py.typed +0 -0
- baycone/settings.py +77 -0
- baycone/subtitles.py +53 -0
- baycone-0.7.0.dist-info/METADATA +124 -0
- baycone-0.7.0.dist-info/RECORD +19 -0
- baycone-0.7.0.dist-info/WHEEL +4 -0
- baycone-0.7.0.dist-info/entry_points.txt +3 -0
- baycone-0.7.0.dist-info/licenses/LICENSE +21 -0
- baycone_cli/__init__.py +0 -0
- baycone_cli/__main__.py +5 -0
- baycone_cli/cli.py +180 -0
- baycone_cli/py.typed +0 -0
baycone/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from baycone.encoding import Analyzer, Encoder, FileInfo, Job, ProgressCallback, VideoStream
|
|
2
|
+
from baycone.errors import BayconeError, EncodingError, InsufficientSpaceError, InvalidFileError, SourceChangedError
|
|
3
|
+
from baycone.settings import EncodingSettings, FFmpegTools
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Analyzer",
|
|
7
|
+
"BayconeError",
|
|
8
|
+
"Encoder",
|
|
9
|
+
"EncodingError",
|
|
10
|
+
"EncodingSettings",
|
|
11
|
+
"FFmpegTools",
|
|
12
|
+
"FileInfo",
|
|
13
|
+
"InsufficientSpaceError",
|
|
14
|
+
"InvalidFileError",
|
|
15
|
+
"Job",
|
|
16
|
+
"ProgressCallback",
|
|
17
|
+
"SourceChangedError",
|
|
18
|
+
"VideoStream",
|
|
19
|
+
]
|
baycone/audio.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from baycone.settings import EncodingSettings
|
|
4
|
+
from baycone.languages import BIBLIOGRAPHIC_TO_TERMINOLOGY
|
|
5
|
+
|
|
6
|
+
UNDEFINED_LANGUAGE = "und"
|
|
7
|
+
# Stands for the original track's language in `target_audio_languages`
|
|
8
|
+
ORIGINAL_LANGUAGE = "original"
|
|
9
|
+
|
|
10
|
+
TRACK_NAMES: dict[int, str] = {
|
|
11
|
+
1: "Mono",
|
|
12
|
+
2: "Stereo",
|
|
13
|
+
3: "Stereo 2.1",
|
|
14
|
+
6: "Surround 5.1",
|
|
15
|
+
8: "Surround 7.1",
|
|
16
|
+
9: "Surround 7.2",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Tracks with any of these ffprobe dispositions are never kept; 'dub' is not one of them, dubs are regular languages
|
|
20
|
+
SPECIAL_DISPOSITIONS: tuple[str, ...] = (
|
|
21
|
+
"comment",
|
|
22
|
+
"visual_impaired",
|
|
23
|
+
"descriptions",
|
|
24
|
+
"hearing_impaired",
|
|
25
|
+
"clean_effects",
|
|
26
|
+
"karaoke",
|
|
27
|
+
"lyrics",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# aac bitrate needed for the same quality, per kbps of the source codec
|
|
32
|
+
AAC_EFFICIENCY: dict[str, float] = {"opus": 1.5, "vorbis": 1.1, "mp3": 0.75, "ac3": 0.65, "dts": 0.5}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def normalize_language(code: str) -> str:
|
|
36
|
+
code = code.strip().lower()
|
|
37
|
+
# ISO 639-2/B and /T codes both occur in the wild, MP4 requires /T
|
|
38
|
+
return BIBLIOGRAPHIC_TO_TERMINOLOGY.get(code, code) or UNDEFINED_LANGUAGE
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cap_bitrate(source: int, target: int) -> int:
|
|
42
|
+
"""Never upscales, falls back to `target` when the source bitrate is unknown."""
|
|
43
|
+
return min(source, target) if source > 0 else target
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class AudioStream:
|
|
48
|
+
index: int = 0
|
|
49
|
+
shall_encode: bool = False
|
|
50
|
+
codec: str = ""
|
|
51
|
+
channel_bitrate: int = 0
|
|
52
|
+
channels: int = 0
|
|
53
|
+
lang: str = UNDEFINED_LANGUAGE
|
|
54
|
+
title: str = ""
|
|
55
|
+
original: bool = False
|
|
56
|
+
special: bool = False
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def overall_bitrate(self) -> int:
|
|
60
|
+
return self.channel_bitrate * self.channels
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def aac_channel_bitrate(self) -> int:
|
|
64
|
+
return int(self.channel_bitrate * AAC_EFFICIENCY.get(self.codec, 1.0))
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def aac_overall_bitrate(self) -> int:
|
|
68
|
+
return self.aac_channel_bitrate * self.channels
|
|
69
|
+
|
|
70
|
+
def generate_track_name(self) -> str:
|
|
71
|
+
return TRACK_NAMES.get(self.channels, "")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class AudioTrack:
|
|
76
|
+
stream: AudioStream
|
|
77
|
+
bitrate: int = 0 # kbps, 0 copies the stream
|
|
78
|
+
default: bool = False
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def title(self) -> str:
|
|
82
|
+
return self.stream.generate_track_name()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def disposition(self) -> str:
|
|
86
|
+
flags = [name for name, enabled in (("default", self.default), ("original", self.stream.original)) if enabled]
|
|
87
|
+
return "+".join(flags) or "0"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _pick_by_channels(versions: list[AudioStream], preferred: int) -> AudioStream:
|
|
91
|
+
"""The fewest channels at or above `preferred`, else the most below it; ties keep the source order."""
|
|
92
|
+
at_least = [s for s in versions if s.channels >= preferred]
|
|
93
|
+
if at_least:
|
|
94
|
+
return min(at_least, key=lambda s: s.channels)
|
|
95
|
+
return max(versions, key=lambda s: s.channels)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def select_audio_tracks(streams: list[AudioStream], settings: EncodingSettings) -> list[AudioTrack]:
|
|
99
|
+
"""The configured languages in order, one track per language; the original alone when none of them is found."""
|
|
100
|
+
candidates = [s for s in streams if not s.special]
|
|
101
|
+
|
|
102
|
+
if candidates:
|
|
103
|
+
original = next((s for s in candidates if s.original), candidates[0])
|
|
104
|
+
languages = [
|
|
105
|
+
original.lang if lang.strip().lower() == ORIGINAL_LANGUAGE else normalize_language(lang)
|
|
106
|
+
for lang in settings.target_audio_languages
|
|
107
|
+
]
|
|
108
|
+
selected = []
|
|
109
|
+
for language in dict.fromkeys(languages):
|
|
110
|
+
versions = [s for s in candidates if s.lang == language]
|
|
111
|
+
if versions:
|
|
112
|
+
selected.append(_pick_by_channels(versions, settings.target_audio_channels))
|
|
113
|
+
selected = selected or [original]
|
|
114
|
+
else:
|
|
115
|
+
selected = streams[:1]
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
AudioTrack(
|
|
119
|
+
stream=stream,
|
|
120
|
+
bitrate=(
|
|
121
|
+
cap_bitrate(stream.aac_overall_bitrate, settings.target_audio_channel_bitrate * stream.channels)
|
|
122
|
+
if stream.shall_encode
|
|
123
|
+
else 0
|
|
124
|
+
),
|
|
125
|
+
default=i == 0,
|
|
126
|
+
)
|
|
127
|
+
for i, stream in enumerate(selected)
|
|
128
|
+
]
|