Compare commits

...
7 Commits
Author SHA1 Message Date
Esa Kataja a45407384e chore: release v1.0.0 2025-04-17 13:58:54 +03:00
Esa Kataja 708c7dc4cd Add future features section with Streamlit UI preview plan 2025-04-17 13:51:27 +03:00
Esa Kataja 21383a391a Update README 2025-04-17 13:49:25 +03:00
Esa Kataja 97e40731b2 Refactor app.py: extract SOLID helper functions and simplify main command
Moved ffprobe call into run_ffprobe
Introduced prepare_video_file, apply_stream_settings, and confirm_and_write_script helpers
Streamlined main() to delegate to new helpers
2025-04-17 13:49:14 +03:00
Esa Kataja 65777071f4 Add error handling for ffprobe 2025-04-17 13:19:22 +03:00
Esa Kataja 404b61aa00 Add IMDb ID validation with regex pattern matching for tt followed by 7-8 digits 2025-04-17 13:16:22 +03:00
Esa Kataja cf4b7fa106 Add confirmation prompt before overwriting existing encode.sh script 2025-04-17 13:00:40 +03:00
4 changed files with 191 additions and 54 deletions
+29
View File
@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.0.0] - 2025-04-17
### Added
- Initial release of **videnc-reeffed** CLI for encoding videos.
- Support for:
- Specifying input video file and IMDb ID validation.
- Removing subtitles with `--nosubtitles`.
- Encoding presets (`--preset`) and CRF settings (`--crf`).
- Selecting original media type (`--original-media-type`).
- Adjusting film grain amount (`--film-grain`).
- Implemented core functions:
- `run_ffprobe` for extracting stream metadata.
- `parse_streams` to model video, audio, and subtitle streams.
- `apply_stream_settings` to apply encoding parameters.
- `confirm_and_write_script` to generate and confirm overwrite of `encode.sh` script.
- Integrated modules and helpers:
- `models/streams` data models: `VideoFile`, `VideoStream`, `AudioStream`, `SubtitleStream`, `OriginalMediaType`.
- `movie_details.get_movie_details` for enriching metadata.
- `lib/fs.write_shell_script` for file system operations.
### Changed
- Adopted `src` directory layout according to standard Python packaging.
### Fixed
- None (initial release).
+94 -25
View File
@@ -1,33 +1,102 @@
# Video Encoding App
# VidEnc: Video Encoding with OMDB Metadata
## Overview
This app takes a video file and an IMDb ID as input, generates a shell script to encode the video streams to AV1 and audio streams to Opus format, converts anamorphic video to SAR 1:1, and adds metadata from OMDB while stripping all other metadata.
VidEnc streamlines complex video transcoding workflows into a single CLI command by generating a portable FFmpeg-based shell script that:
- Converts video streams to AV1 for maximum compression efficiency
- Encodes audio tracks to Opus for reduced file size without sacrificing quality
- Adjusts anamorphic sources to a correct 1:1 sample aspect ratio automatically
- Retrieves and embeds movie title and release date from OMDB using your IMDb ID
- Strips all other metadata to produce a clean, optimized output
## Input Requirements
1. **Video File**: A valid video file in a supported format.
2. **IMDb ID**: A valid IMDb ID to fetch metadata from OMDB.
Designed for media archivists, home theater enthusiasts, and content creators, VidEnc offers customizable presets (CRF, film grain strength, speed), original media type tagging, optional subtitle handling, and shell completion support for a seamless CLI experience.
## Optional CLI Arguments
1. **`-ns`/`--nosubtitles`**: Disables subtitle processing.
2. **`-p`/`--preset`**: Sets the encoding preset for the video. Default is `2`.
3. **`-c`/`--crf`**: Sets the CRF value for the video. Default is `30`.
4. **`-o`/`--original-media-type`**: Sets the ORIGINAL_MEDIA_TYPE metadata. Default is `DVD`.
---
## Output Details
The app generates a shell script that performs the following tasks:
1. Encodes the video to AV1 format.
2. Encodes the audio to Opus format.
3. Converts anamorphic video to SAR 1:1.
4. Adds metadata from OMDB using the provided IMDb ID.
5. Strips all other metadata from the video.
## 🚀 Features
## Metadata Handling
- **Added Metadata**: Metadata fetched from OMDB using the IMDb ID.
- **Stripped Metadata**: All other metadata is removed from the video.
- Encode video streams to AV1 and audio streams to Opus
- Convert anamorphic video to 1:1 pixel aspect ratio
- Fetch and inject movie title and release date from OMDB
- Strip all other metadata to produce a clean output
- Configurable presets, CRF, film grain, media type, and subtitle handling
- Automatic generation of a reusable `encode.sh` script
- Shell completion support
## Anamorphic Video Conversion
Anamorphic videos are converted to SAR (Sample Aspect Ratio) 1:1 to ensure proper display on all devices.
## 🔮 Future Features
## Example Usage
- Streamlit UI for interactive configuration and metadata preview.
## 🔧 Prerequisites
- **Python** >= 3.12
- **FFmpeg** (including `ffprobe`) installed and available in your `PATH`
- **OMDB API key**: Sign up at https://www.omdbapi.com/ and set `API_KEY` as an environment variable or in a `.env` file
## ⚙️ Installation
1. Clone the repository (or navigate to your project folder):
```bash
./encode_video.sh input_video.mp4 -i tt1234567
git clone https://github.com/Kessinen/VidEnc.git
cd VidEnc
```
2. Install Python dependencies using **uv**:
```bash
uv install
```
3. Create a `.env` file with your OMDB API key:
```bash
echo "API_KEY=your_omdb_api_key" > .env
```
## 📝 Usage
### Generate the encoding script
```bash
# via uv:
uv run src/app.py path/to/video.mp4 --imdb tt1234567
# or directly:
python src/app.py path/to/video.mp4 -i tt1234567
```
This will probe the input, fetch metadata, and write an executable `encode.sh` next to your video.
### Execute the encoding script
```bash
bash encode.sh
```
## ⚙️ CLI Options
| Option | Description | Default |
|-------------------------------|----------------------------------------------------------------------|-------------|
| -ns, --nosubtitles | Disable subtitle processing | false |
| -p, --preset INT | Encoding preset (110 scale; higher = faster, lower quality) | 2 |
| -c, --crf INT | Constant Rate Factor for video quality | 30 |
| -o, --original-media-type | Source media type (DVD, BluRay, TVRip, WebRip, Other) | DVD |
| -fg, --film-grain INT | Film grain strength | 20 |
| --install-completion | Install shell completion for your current shell | — |
| --show-completion | Display shell completion script for customization | — |
```bash
# Install bash completion (example):
python src/app.py --install-completion
```
## 🔍 How It Works
1. **Probe** the input file with `ffprobe` to collect stream data.
2. **Parse** video, audio, and subtitle streams into a structured model.
3. **Fetch** title and release date from OMDB using the provided IMDb ID.
4. **Configure** each video stream with presets, CRF, film grain, and media type.
5. **Generate** a single FFmpeg command to re-encode streams, convert SAR, embed metadata, and strip all other metadata.
6. **Write** the command into `encode.sh`, prompting before overwrite if it exists.
## 💡 Contributing
Contributions, issues, and feature requests are welcome! Feel free to open a PR or issue in the [GitHub repository](https://github.com/Kessinen/VidEnc).
## 📄 License
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "videnc-reeffed"
version = "0.1.0"
version = "1.0.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
+67 -28
View File
@@ -2,6 +2,8 @@ import json
import subprocess
import typer
from pathlib import Path
import re
import click
from models.streams import VideoFile, VideoStream, AudioStream, SubtitleStream, OriginalMediaType
from movie_details import get_movie_details
@@ -23,45 +25,82 @@ def parse_streams(stream_data: dict, VideoFile: VideoFile) -> VideoFile:
return VideoFile
def validate_imdb(ctx: click.Context, param: click.Parameter, value: str) -> str:
"""Validate that an IMDb ID matches 'tt' followed by 7-8 digits."""
if value is None:
return value
if not re.match(r"^tt\d{7,8}$", value):
raise click.BadParameter("IMDb ID must be 'tt' followed by 7-8 digits (e.g. tt1234567)")
return value
# Helper functions for SOLID refactoring
def run_ffprobe(filename: Path) -> dict:
"""Execute ffprobe and return parsed JSON data."""
cmd = [
"ffprobe", "-v", "quiet",
"-print_format", "json",
"-show_format", "-show_streams",
filename,
]
try:
result = subprocess.run(cmd, capture_output=True, check=True)
except FileNotFoundError:
typer.secho("Error: ffprobe not found. Please install FFmpeg.", fg="red", err=True)
raise typer.Exit(code=1)
except subprocess.CalledProcessError as e:
typer.secho("Error: ffprobe failed to probe the file.", fg="red", err=True)
error_output = e.stderr.decode() if e.stderr else str(e)
typer.secho(error_output, fg="red", err=True)
raise typer.Exit(code=1)
return json.loads(result.stdout)
def prepare_video_file(filename: Path, imdb: str, nosubtitles: bool) -> VideoFile:
"""Initialize VideoFile with metadata."""
return VideoFile(imdb=imdb, video_path=filename, allow_subtitle=not nosubtitles)
def apply_stream_settings(video_details: VideoFile, preset: int, crf: int, original_media_type: OriginalMediaType, film_grain: int) -> VideoFile:
"""Apply encoding settings to video streams."""
video_details.original_media_type = original_media_type
for stream in video_details.video_streams:
stream.preset = preset
stream.crf = crf
stream.original_media_type = original_media_type
stream.film_grain = film_grain
return video_details
def confirm_and_write_script(export_data: str, export_path: Path):
"""Confirm overwrite and write shell script."""
if export_path.exists():
if not typer.confirm(f"'{export_path}' already exists. Overwrite? "):
typer.echo("Aborting.")
raise typer.Exit()
export_path.unlink()
write_shell_script(export_data, export_path)
@app.command()
def main(
filename: Path = typer.Argument(..., exists=True, help="Input video file"),
imdb: str = typer.Option(None, "-i", "--imdb", help="IMDB ID"),
imdb: str = typer.Option(None, "-i", "--imdb", callback=validate_imdb, help="IMDB ID"),
nosubtitles: bool = typer.Option(False, "-ns", "--nosubtitles", help="Remove subtitles"),
preset: int = typer.Option(2, "-p", "--preset", help="Encoding preset"),
crf: int = typer.Option(30, "-c", "--crf", help="CRF value"),
original_media_type: OriginalMediaType = typer.Option(OriginalMediaType.DVD, "-o", "--original-media-type", case_sensitive=False, help="Source of the original media."),
film_grain: int = typer.Option(20, "-fg", "--film-grain", help="Film grain amount (default: 20)")
):
video_details = VideoFile(
imdb=imdb, video_path=filename, allow_subtitle=not nosubtitles
)
video_details = prepare_video_file(filename, imdb, nosubtitles)
video_details = get_movie_details(video_details)
cmd = [
"ffprobe",
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
"-show_streams",
filename,
]
result = subprocess.run(cmd, capture_output=True, check=True)
data = parse_streams(json.loads(result.stdout), video_details)
data.original_media_type = original_media_type
for stream in data.video_streams:
stream.preset = preset
stream.crf = crf
stream.original_media_type = original_media_type
stream.film_grain = film_grain
export_data = " ".join(data.cmd())
export_path = Path(filename).parent / "encode.sh"
write_shell_script(export_data, export_path)
print(Path(filename).parent)
print(" ".join(data.cmd()))
# json.dump(data, sys.stdout, indent=4)
stream_data = run_ffprobe(filename)
video_details = parse_streams(stream_data, video_details)
video_details = apply_stream_settings(video_details, preset, crf, original_media_type, film_grain)
export_data = " ".join(video_details.cmd())
export_path = filename.parent / "encode.sh"
confirm_and_write_script(export_data, export_path)
typer.echo(str(filename.parent))
typer.echo(export_data)
if __name__ == "__main__":