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 +21 -0
- baycone-0.7.0/PKG-INFO +124 -0
- baycone-0.7.0/baycone/__init__.py +19 -0
- baycone-0.7.0/baycone/audio.py +128 -0
- baycone-0.7.0/baycone/encoding.py +741 -0
- baycone-0.7.0/baycone/errors.py +18 -0
- baycone-0.7.0/baycone/ffmpeg.py +530 -0
- baycone-0.7.0/baycone/files.py +40 -0
- baycone-0.7.0/baycone/languages.py +213 -0
- baycone-0.7.0/baycone/py.typed +0 -0
- baycone-0.7.0/baycone/settings.py +77 -0
- baycone-0.7.0/baycone/subtitles.py +53 -0
- baycone-0.7.0/baycone_cli/__init__.py +0 -0
- baycone-0.7.0/baycone_cli/__main__.py +5 -0
- baycone-0.7.0/baycone_cli/cli.py +180 -0
- baycone-0.7.0/baycone_cli/py.typed +0 -0
- baycone-0.7.0/docs/pypi-overview.md +92 -0
- baycone-0.7.0/pyproject.toml +142 -0
- baycone-0.7.0/pyproject.toml.orig +130 -0
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
|
+
]
|