baycone 0.7.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.
baycone-0.7.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filip Klopec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
21
+ OR OTHER DEALINGS IN THE SOFTWARE.
baycone-0.7.0/PKG-INFO ADDED
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: baycone
3
+ Version: 0.7.0
4
+ Summary: Transcode videos into streaming-friendly MP4 with ffmpeg, as a Python library or a command
5
+ Keywords: ffmpeg,transcoding,video,mp4,h264,hevc,jellyfin,media-library
6
+ Author: theobaldik
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Natural Language :: English
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Multimedia :: Video
20
+ Classifier: Topic :: Multimedia :: Video :: Conversion
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: charset-normalizer>=3.5.2
23
+ Maintainer: theobaldik
24
+ Requires-Python: >=3.14
25
+ Project-URL: Homepage, https://github.com/theobaldik/baycone
26
+ Project-URL: Source, https://github.com/theobaldik/baycone
27
+ Project-URL: Documentation, https://github.com/theobaldik/baycone#readme
28
+ Project-URL: Changelog, https://github.com/theobaldik/baycone/blob/master/CHANGELOG.md
29
+ Project-URL: Issues, https://github.com/theobaldik/baycone/issues
30
+ Project-URL: Docker image, https://hub.docker.com/r/theobaldik/baycone
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Baycone
34
+
35
+ **Transcode videos into streaming-friendly MP4 with ffmpeg — as a Python library or a command.**
36
+
37
+ Baycone turns a video file into an MP4 that plays everywhere and starts instantly: h264 or hevc video, AAC audio and
38
+ the moov atom up front. It only re-encodes what needs it, picks the audio tracks you want and never leaves a
39
+ half-written file behind.
40
+
41
+ This package is the pipeline behind the [Baycone Docker image](https://hub.docker.com/r/theobaldik/baycone), which
42
+ watches media libraries and processes new files automatically.
43
+
44
+ ## Features
45
+
46
+ - Two-pass `libx264` / `libx265` encode to a target bitrate, never above the source's
47
+ - Copies streams that are already within limits; a container mismatch alone is fixed by a remux
48
+ - Scales down to fit 1920×1080 (configurable), crops black bars and tone maps HDR10 / HLG to SDR
49
+ - Keeps the original audio language plus your preferred ones, one track per language at the preferred channel count
50
+ - Extracts text subtitles to UTF-8 `.srt` files
51
+ - Detects copies that were cut off, kills a hung ffmpeg, and resumes an interrupted encode from its second pass
52
+ - Writes the output in one step after it is safely on disk; the input file is never modified
53
+
54
+ ## Requirements
55
+
56
+ - Python 3.14
57
+ - `ffmpeg` and `ffprobe` on `PATH`, built with `libx264`, `libx265` and `libzimg` (most distribution builds are)
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install baycone
63
+ # or, for the command only
64
+ uv tool install baycone
65
+ ```
66
+
67
+ ## Command line
68
+
69
+ ```bash
70
+ baycone-cli movie.mkv # result in _baycone_processed/movie.mp4 next to the input
71
+ baycone-cli movie.mkv --output /media/out.mp4
72
+ baycone-cli movie.mkv -l cze -l eng -v 1800 # audio languages in order, video bitrate in kbps
73
+ baycone-cli movie.mkv --codec hevc
74
+ baycone-cli movie.mkv --remux-only # never encode, only remux into MP4
75
+ baycone-cli movie.mkv --info # print the analysis as JSON and exit
76
+ ```
77
+
78
+ | Flag | Default | Description |
79
+ |-------------------------|------------------------|---------------------------------------------------------|
80
+ | `--output` | `_baycone_processed/` | Output file |
81
+ | `--work-dir` | `.baycone/cli/` | Temporary files; the default one is removed afterwards |
82
+ | `-l`, `--language` | `original` `eng` `cze` | Audio language to keep, repeatable, in order |
83
+ | `-v`, `--video-bitrate` | `2250` | Target video bitrate in kbps |
84
+ | `-a`, `--audio-bitrate` | `64` | Target audio bitrate per channel in kbps |
85
+ | `--codec` | `h264` | Target video codec, `h264` or `hevc` |
86
+ | `--subtitles` | none | Subtitle language to extract to `.srt`, repeatable |
87
+ | `--remux-only` | off | Copy every stream, only remux into the target container |
88
+ | `--info` | off | Print the analysis as JSON and exit |
89
+ | `--ffmpeg`, `--ffprobe` | `ffmpeg`, `ffprobe` | Binaries to use |
90
+
91
+ Exit codes: `0` success, `1` invalid arguments, `2` the file could not be processed.
92
+
93
+ ## Library
94
+
95
+ ```python
96
+ from pathlib import Path
97
+
98
+ from baycone import BayconeError, Encoder, EncodingSettings, Job
99
+
100
+ settings = EncodingSettings(target_video_bitrate=1800, target_audio_languages=["original", "eng"])
101
+ job = Job(input_file=Path("movie.mkv"), output_file=Path("out/movie.mp4"), work_dir=Path("work"))
102
+
103
+ try:
104
+ Encoder(settings, on_progress=lambda step, percent: print(step, percent)).process(job)
105
+ except BayconeError as e:
106
+ ... # the input is untouched and no output is left behind
107
+ ```
108
+
109
+ - `EncodingSettings` holds every encoding setting: allowed codecs and containers, targets and limits.
110
+ - `Encoder.process()` returns the output path or raises a `BayconeError`: `InsufficientSpaceError` before anything is
111
+ written, `InvalidFileError` for an unreadable or truncated file, `EncodingError` when ffmpeg fails, and
112
+ `SourceChangedError` when the input changed while it was processed.
113
+ - Keep `work_dir` on the same filesystem as `output_file`, otherwise publishing has to copy the file.
114
+ - `Analyzer(settings).analyze(path)` returns the analysis alone, without encoding anything.
115
+ - The library logs through `logging` under `baycone.*` and configures no handlers. It is fully typed.
116
+
117
+ ## Links
118
+
119
+ - [Source code and documentation](https://github.com/theobaldik/baycone)
120
+ - [Changelog](https://github.com/theobaldik/baycone/blob/master/CHANGELOG.md)
121
+ - [Issues](https://github.com/theobaldik/baycone/issues)
122
+ - [Docker image](https://hub.docker.com/r/theobaldik/baycone)
123
+
124
+ Released under the [MIT License](https://github.com/theobaldik/baycone/blob/master/LICENSE).
@@ -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
+ ]
@@ -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
+ ]