Complete initial project setup and core functionality
This commit is contained in:
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
34
src/config.py
Normal file
34
src/config.py
Normal 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
99
src/converter.py
Normal 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
58
src/main.py
Normal 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
59
src/utils.py
Normal 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)
|
||||
Reference in New Issue
Block a user