transcode.py: --replace mode
This commit is contained in:
+96
-30
@@ -123,15 +123,39 @@ def get_ffmpeg_command(input_path, output_path):
|
|||||||
]
|
]
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def transcode_file(input_file, output_file, skip_av1=True):
|
def transcode_file(input_file, output_file=None, skip_av1=True, replace_mode=False):
|
||||||
input_path = Path(input_file)
|
input_path = Path(input_file)
|
||||||
output_path = Path(output_file)
|
|
||||||
|
|
||||||
logging.debug(f"Processing request: {input_path} -> {output_path}")
|
logging.debug(f"Processing request for: {input_path}")
|
||||||
|
|
||||||
# 1. Check existence logic
|
target_path = None
|
||||||
if output_path.exists():
|
use_temp_file = False
|
||||||
logging.info(f"SKIP: Output file already exists: {output_path.name}")
|
|
||||||
|
if replace_mode:
|
||||||
|
logging.warning("Policy: --replace is enabled. --output-dir is ignored. Transcoding in-place.")
|
||||||
|
target_path = input_path.with_suffix('.mp4')
|
||||||
|
|
||||||
|
# If the target is the same as input (e.g. input is already .mp4), use a temp file
|
||||||
|
if target_path == input_path:
|
||||||
|
use_temp_file = True
|
||||||
|
transcode_output_path = input_path.with_suffix('.tmp.mp4')
|
||||||
|
logging.debug(f"Input and output filenames match. Using temp file: {transcode_output_path}")
|
||||||
|
else:
|
||||||
|
transcode_output_path = target_path
|
||||||
|
else:
|
||||||
|
if not output_file:
|
||||||
|
logging.error("No output file specified and not in replace mode.")
|
||||||
|
return
|
||||||
|
target_path = Path(output_file)
|
||||||
|
transcode_output_path = target_path
|
||||||
|
|
||||||
|
logging.debug(f"Final transcode target: {transcode_output_path}")
|
||||||
|
|
||||||
|
# 1. Check existence logic (Skip if target exists, unless we are using a temp file for replacement)
|
||||||
|
# If replace_mode is True and extensions differ, we still want to respect -n (no overwrite)
|
||||||
|
# for the destination file if it already exists.
|
||||||
|
if transcode_output_path.exists():
|
||||||
|
logging.info(f"SKIP: Output file already exists: {transcode_output_path.name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Check if file is ready (simple size stability check)
|
# 2. Check if file is ready (simple size stability check)
|
||||||
@@ -167,9 +191,11 @@ def transcode_file(input_file, output_file, skip_av1=True):
|
|||||||
ffmpeg_log_file = ffmpeg_logs_dir / f"{input_path.name}.log"
|
ffmpeg_log_file = ffmpeg_logs_dir / f"{input_path.name}.log"
|
||||||
|
|
||||||
logging.info(f"START: Transcoding {input_path.name}")
|
logging.info(f"START: Transcoding {input_path.name}")
|
||||||
|
if replace_mode:
|
||||||
|
logging.info(f" Outputting to temporary file: {input_path.name}")
|
||||||
logging.info(f" FFmpeg details logging to: {ffmpeg_log_file}")
|
logging.info(f" FFmpeg details logging to: {ffmpeg_log_file}")
|
||||||
|
|
||||||
cmd = get_ffmpeg_command(input_path, output_path)
|
cmd = get_ffmpeg_command(input_path, transcode_output_path)
|
||||||
logging.debug(f"Executing FFmpeg command: {' '.join(cmd)}")
|
logging.debug(f"Executing FFmpeg command: {' '.join(cmd)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -182,7 +208,24 @@ def transcode_file(input_file, output_file, skip_av1=True):
|
|||||||
result = subprocess.run(cmd, stdout=f_log, stderr=subprocess.STDOUT, text=True)
|
result = subprocess.run(cmd, stdout=f_log, stderr=subprocess.STDOUT, text=True)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
logging.info(f"DONE: Successfully created {output_path.name}")
|
logging.info(f"DONE: Successfully created {transcode_output_path.name}")
|
||||||
|
|
||||||
|
if replace_mode:
|
||||||
|
if use_temp_file:
|
||||||
|
# Rename temp file to overwrite original
|
||||||
|
try:
|
||||||
|
transcode_output_path.replace(input_path)
|
||||||
|
logging.info(f"REPLACE: Overwrote original file {input_path.name} with new version.")
|
||||||
|
except OSError as e:
|
||||||
|
logging.error(f"Failed to replace original file: {e}")
|
||||||
|
else:
|
||||||
|
# Different extensions (e.g. mkv -> mp4). Delete original.
|
||||||
|
try:
|
||||||
|
input_path.unlink()
|
||||||
|
logging.info(f"DELETE: Removed original file {input_path.name}")
|
||||||
|
except OSError as e:
|
||||||
|
logging.error(f"Failed to delete original file: {e}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.error(f"FFmpeg failed for {input_path.name}. See log at {ffmpeg_log_file}")
|
logging.error(f"FFmpeg failed for {input_path.name}. See log at {ffmpeg_log_file}")
|
||||||
|
|
||||||
@@ -193,9 +236,10 @@ def transcode_file(input_file, output_file, skip_av1=True):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
class NewFileHandler(FileSystemEventHandler):
|
class NewFileHandler(FileSystemEventHandler):
|
||||||
def __init__(self, output_dir, skip_av1=True):
|
def __init__(self, output_dir=None, skip_av1=True, replace_mode=False):
|
||||||
self.output_dir = Path(output_dir)
|
self.output_dir = Path(output_dir) if output_dir else None
|
||||||
self.skip_av1 = skip_av1
|
self.skip_av1 = skip_av1
|
||||||
|
self.replace_mode = replace_mode
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
if event.is_directory:
|
if event.is_directory:
|
||||||
@@ -217,10 +261,12 @@ class NewFileHandler(FileSystemEventHandler):
|
|||||||
logging.debug(f"Ignored non-video file: {input_path.name}")
|
logging.debug(f"Ignored non-video file: {input_path.name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
new_filename = input_path.stem + ".mp4"
|
output_path = None
|
||||||
output_path = self.output_dir / new_filename
|
if not self.replace_mode and self.output_dir:
|
||||||
|
new_filename = input_path.stem + ".mp4"
|
||||||
|
output_path = self.output_dir / new_filename
|
||||||
|
|
||||||
transcode_file(input_path, output_path, skip_av1=self.skip_av1)
|
transcode_file(input_path, output_path, skip_av1=self.skip_av1, replace_mode=self.replace_mode)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
setup_logging()
|
setup_logging()
|
||||||
@@ -232,21 +278,38 @@ def main():
|
|||||||
parser.add_argument("--watch-dir", type=str, help="Directory to monitor for new files")
|
parser.add_argument("--watch-dir", type=str, help="Directory to monitor for new files")
|
||||||
parser.add_argument("--output-dir", type=str, help="Output directory")
|
parser.add_argument("--output-dir", type=str, help="Output directory")
|
||||||
parser.add_argument("--no-skip-av1", action="store_true", help="Force transcoding even if input is already AV1")
|
parser.add_argument("--no-skip-av1", action="store_true", help="Force transcoding even if input is already AV1")
|
||||||
|
parser.add_argument("--replace", action="store_true", help="Replace original files with transcoded versions (Ignores --output-dir)")
|
||||||
|
|
||||||
# Set defaults from config
|
# Set defaults from config
|
||||||
# We accept both hyphenated and underscore keys from JSON for user convenience
|
# We accept both hyphenated and underscore keys from JSON for user convenience
|
||||||
default_watch = config.get("watch-dir") or config.get("watch_dir")
|
default_watch = config.get("watch-dir") or config.get("watch_dir")
|
||||||
default_output = config.get("output-dir") or config.get("output_dir")
|
default_output = config.get("output-dir") or config.get("output_dir")
|
||||||
default_no_skip = config.get("no-skip-av1") or config.get("no_skip_av1") or False
|
default_no_skip = config.get("no-skip-av1") or config.get("no_skip_av1") or False
|
||||||
|
default_replace = config.get("replace") or False
|
||||||
|
|
||||||
parser.set_defaults(
|
parser.set_defaults(
|
||||||
watch_dir=default_watch,
|
watch_dir=default_watch,
|
||||||
output_dir=default_output,
|
output_dir=default_output,
|
||||||
no_skip_av1=default_no_skip
|
no_skip_av1=default_no_skip,
|
||||||
|
replace=default_replace
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
skip_av1 = not args.no_skip_av1
|
||||||
|
|
||||||
|
# --- Initial Policy Logging ---
|
||||||
|
if args.replace:
|
||||||
|
logging.info("Policy: REPLACE mode enabled. Original files will be overwritten/deleted.")
|
||||||
|
if args.output_dir:
|
||||||
|
logging.warning("Warning: --output-dir is specified but will be IGNORED due to --replace mode.")
|
||||||
|
|
||||||
|
if args.no_skip_av1:
|
||||||
|
logging.info("Policy: Force transcoding all files (including AV1).")
|
||||||
|
else:
|
||||||
|
logging.info("Policy: Skipping files that are already AV1.")
|
||||||
|
|
||||||
|
|
||||||
# --- Mode 1: Single File ---
|
# --- Mode 1: Single File ---
|
||||||
if args.input:
|
if args.input:
|
||||||
input_path = Path(args.input)
|
input_path = Path(args.input)
|
||||||
@@ -254,42 +317,45 @@ def main():
|
|||||||
logging.critical(f"Input file '{args.input}' not found.")
|
logging.critical(f"Input file '{args.input}' not found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.output_dir:
|
output_path = None
|
||||||
out_dir = Path(args.output_dir)
|
if not args.replace:
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
if args.output_dir:
|
||||||
output_path = out_dir / (input_path.stem + ".mp4")
|
out_dir = Path(args.output_dir)
|
||||||
else:
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
output_path = input_path.parent / (input_path.stem + "_av1.mp4")
|
output_path = out_dir / (input_path.stem + ".mp4")
|
||||||
|
else:
|
||||||
|
output_path = input_path.parent / (input_path.stem + "_av1.mp4")
|
||||||
|
|
||||||
transcode_file(input_path, output_path, skip_av1=not args.no_skip_av1)
|
transcode_file(input_path, output_path, skip_av1=skip_av1, replace_mode=args.replace)
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Mode 2: Watch Directory ---
|
# --- Mode 2: Watch Directory ---
|
||||||
if args.watch_dir:
|
if args.watch_dir:
|
||||||
if not args.output_dir:
|
# If replace mode is OFF, output-dir is required.
|
||||||
|
if not args.replace and not args.output_dir:
|
||||||
logging.critical("Output directory is not specified in CLI (--output-dir) or Config.")
|
logging.critical("Output directory is not specified in CLI (--output-dir) or Config.")
|
||||||
|
logging.critical("Either specify --output-dir OR enable --replace mode.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
watch_dir = Path(args.watch_dir)
|
watch_dir = Path(args.watch_dir)
|
||||||
output_dir = Path(args.output_dir)
|
|
||||||
|
|
||||||
if not watch_dir.exists():
|
if not watch_dir.exists():
|
||||||
logging.critical(f"Watch directory '{watch_dir}' does not exist.")
|
logging.critical(f"Watch directory '{watch_dir}' does not exist.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
logging.info(f"Monitoring {watch_dir}...")
|
logging.info(f"Monitoring {watch_dir}...")
|
||||||
logging.info(f"Outputting to {output_dir}")
|
|
||||||
|
|
||||||
if args.no_skip_av1:
|
output_dir_path = None
|
||||||
logging.info("Policy: Force transcoding all files (including AV1).")
|
if not args.replace:
|
||||||
|
output_dir_path = Path(args.output_dir)
|
||||||
|
output_dir_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
logging.info(f"Outputting to {output_dir_path}")
|
||||||
else:
|
else:
|
||||||
logging.info("Policy: Skipping files that are already AV1.")
|
logging.info("Outputting in-place (replacing originals).")
|
||||||
|
|
||||||
logging.info("Press Ctrl+C to stop.")
|
logging.info("Press Ctrl+C to stop.")
|
||||||
|
|
||||||
event_handler = NewFileHandler(output_dir, skip_av1=not args.no_skip_av1)
|
event_handler = NewFileHandler(output_dir_path, skip_av1=skip_av1, replace_mode=args.replace)
|
||||||
observer = Observer()
|
observer = Observer()
|
||||||
observer.schedule(event_handler, str(watch_dir), recursive=False)
|
observer.schedule(event_handler, str(watch_dir), recursive=False)
|
||||||
observer.start()
|
observer.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user