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 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
+ ]