Complete initial project setup and core functionality

This commit is contained in:
Ramforth
2025-11-01 11:22:59 +01:00
parent f06797d437
commit 0fbc261e1b
15 changed files with 418 additions and 12 deletions

0
src/__init__.py Normal file
View File

34
src/config.py Normal file
View File

@@ -0,0 +1,34 @@
import os
# Default video and audio settings for DaVinci Resolve compatibility (Free Linux version)
# Based on DaVinci Resolve 18 Supported Codec List and web search recommendations.
# Target 1080p60 and 1440p60
DEFAULT_VIDEO_CODEC = "dnxhd"
DEFAULT_VIDEO_PROFILE_1080P = "HQ" # DNxHD HQ for 1080p
DEFAULT_VIDEO_PROFILE_1440P = "HQ" # DNxHR HQ for 1440p
DEFAULT_VIDEO_PIX_FMT = "yuv422p" # Common for DNxHD/HR
DEFAULT_CONTAINER = ".mov"
DEFAULT_AUDIO_CODEC = "pcm_s16le" # Linear PCM 16-bit little-endian
# FFmpeg global options
FFMPEG_GLOBAL_OPTIONS = [
"-hide_banner",
"-loglevel", "info",
]
# FFprobe global options
FFPROBE_GLOBAL_OPTIONS = [
"-v", "error",
]
# Path to bundled ffmpeg/ffprobe binaries (to be set by PyInstaller hook or similar)
# For development, assume they are in PATH
FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "ffmpeg")
FFPROBE_PATH = os.environ.get("FFPROBE_PATH", "ffprobe")
# Output file naming conventions
OUTPUT_SUFFIX = "_DR_compatible"
OUTPUT_PREFIX = "recoded_"

99
src/converter.py Normal file
View File

@@ -0,0 +1,99 @@
import ffmpeg
import os
import sys
from . import utils
from . import config
def convert_video(
input_file_path,
output_dir,
video_codec=config.DEFAULT_VIDEO_CODEC,
audio_codec=config.DEFAULT_AUDIO_CODEC,
output_suffix=config.OUTPUT_SUFFIX,
output_prefix=config.OUTPUT_PREFIX,
output_ext=config.DEFAULT_CONTAINER
):
"""Converts a video file to a DaVinci Resolve compatible format."""
print(f"Analyzing input file: {input_file_path}")
video_info = utils.get_video_info(input_file_path)
audio_info = utils.get_audio_info(input_file_path)
if not video_info or not audio_info:
print("Error: Could not retrieve full media information. Aborting conversion.", file=sys.stderr)
return False
# Extract relevant video stream info
try:
v_stream = video_info["streams"][0]
width = v_stream["width"]
height = v_stream["height"]
# frame_rate = eval(v_stream["avg_frame_rate"]) # avg_frame_rate is a string like '30/1'
# duration = float(v_stream["duration_ts"]) / frame_rate
# pix_fmt = v_stream["pix_fmt"]
except (KeyError, IndexError) as e:
print(f"Error parsing video stream info: {e}", file=sys.stderr)
return False
# Extract relevant audio stream info
try:
a_stream = audio_info["streams"][0]
# a_codec_name = a_stream["codec_name"]
# sample_rate = a_stream["sample_rate"]
# channels = a_stream["channels"]
except (KeyError, IndexError) as e:
print(f"Error parsing audio stream info: {e}", file=sys.stderr)
# This might be a video-only file, proceed with no audio conversion
audio_codec = None
# Determine video profile based on resolution
video_profile = "dnxhr_sq" # Default to 1080p Standard Quality
if height > 1080:
video_profile = "dnxhr_hq" # For 1440p High Quality
output_file_path = utils.generate_output_path(
input_file_path, output_dir, output_suffix, output_prefix, output_ext
)
print(f"Converting '{input_file_path}' to '{output_file_path}'")
try:
stream = ffmpeg.input(input_file_path)
# Video stream setup
video_stream = stream.video
video_options = {
"c:v": video_codec,
"profile:v": video_profile,
"pix_fmt": config.DEFAULT_VIDEO_PIX_FMT,
}
# Audio stream setup
audio_stream = None
audio_options = {}
if audio_codec:
audio_stream = stream.audio
audio_options = {"c:a": audio_codec}
# Construct output stream
if audio_stream:
out = ffmpeg.output(video_stream, audio_stream, output_file_path, **video_options, **audio_options)
else:
out = ffmpeg.output(video_stream, output_file_path, **video_options)
# Add global options and run
out = ffmpeg.overwrite_output(out)
ffmpeg.run(out, cmd=config.FFMPEG_PATH, capture_stdout=True, capture_stderr=True)
print(f"Successfully converted to '{output_file_path}'")
return True
except ffmpeg.Error as e:
print(f"FFmpeg Error: {e.stderr.decode()}", file=sys.stderr)
return False
except FileNotFoundError:
print("Error: ffmpeg not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr)
return False
except Exception as e:
print(f"An unexpected error occurred during conversion: {e}", file=sys.stderr)
return False

58
src/main.py Normal file
View File

@@ -0,0 +1,58 @@
import argparse
import os
import sys
# Assuming these modules will be created and populated later
from . import converter
from . import utils
from . import config
def main():
parser = argparse.ArgumentParser(
description="Convert video files to DaVinci Resolve compatible formats."
)
parser.add_argument(
"input_file",
help="Path to the input video file.",
type=str
)
parser.add_argument(
"-o", "--output_dir",
help="Optional: Directory to save the converted file. Defaults to the input file's directory.",
type=str,
default=None
)
args = parser.parse_args()
input_file_path = os.path.abspath(args.input_file)
if not os.path.exists(input_file_path):
print(f"Error: Input file not found at '{input_file_path}'", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(input_file_path):
print(f"Error: '{input_file_path}' is not a file.", file=sys.stderr)
sys.exit(1)
output_dir = args.output_dir
if output_dir:
output_dir = os.path.abspath(output_dir)
if not os.path.isdir(output_dir):
print(f"Error: Output directory '{output_dir}' does not exist or is not a directory.", file=sys.stderr)
sys.exit(1)
else:
output_dir = os.path.dirname(input_file_path)
# Perform conversion
print(f"Starting conversion for {input_file_path}...")
success = converter.convert_video(input_file_path, output_dir)
if success:
print("Conversion completed successfully.")
else:
print("Conversion failed.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

59
src/utils.py Normal file
View File

@@ -0,0 +1,59 @@
import os
import json
import subprocess
import sys
def get_video_info(file_path):
"""Uses ffprobe to get detailed information about a video file."""
command = [
'ffprobe',
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=codec_name,width,height,avg_frame_rate,duration_ts,bit_rate,pix_fmt',
'-of', 'json',
file_path
]
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error running ffprobe: {e}", file=sys.stderr)
print(f"Stderr: {e.stderr}", file=sys.stderr)
return None
except FileNotFoundError:
print("Error: ffprobe not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr)
return None
def get_audio_info(file_path):
"""Uses ffprobe to get detailed information about an audio stream in a video file."""
command = [
'ffprobe',
'-v', 'error',
'-select_streams', 'a:0',
'-show_entries', 'stream=codec_name,sample_rate,channels,bit_rate',
'-of', 'json',
file_path
]
try:
result = subprocess.run(command, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error running ffprobe: {e}", file=sys.stderr)
print(f"Stderr: {e.stderr}", file=sys.stderr)
return None
except FileNotFoundError:
print("Error: ffprobe not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr)
return None
def generate_output_path(input_file_path, output_dir, suffix="_DR_compatible", prefix="recoded_", new_extension=".mov"):
"""Generates the output file path based on input, output directory, and naming conventions."""
base_name = os.path.basename(input_file_path)
name_without_ext = os.path.splitext(base_name)[0]
final_name = f"{name_without_ext}{suffix}{new_extension}"
# Apply recoded_ prefix if output_dir is the same as input_file_path's directory
if os.path.abspath(output_dir) == os.path.abspath(os.path.dirname(input_file_path)):
final_name = f"{prefix}{final_name}"
return os.path.join(output_dir, final_name)