diff --git a/tv_audio_encoder.py b/tv_audio_encoder.py index 6e0d028..8acb987 100644 --- a/tv_audio_encoder.py +++ b/tv_audio_encoder.py @@ -8,6 +8,17 @@ import json from datetime import datetime from pathlib import Path +class Tee: + def __init__(self, *files): + self.files = files + def write(self, obj): + for f in self.files: + f.write(obj) + f.flush() + def flush(self): + for f in self.files: + f.flush() + REQUIRED_TOOLS_MAP = { "ffmpeg": "extra/ffmpeg", "ffprobe": "extra/ffmpeg", # Part of ffmpeg package @@ -20,6 +31,7 @@ REQUIRED_TOOLS_MAP = { } DIR_COMPLETED = Path("completed") DIR_ORIGINAL = Path("original") +DIR_LOGS = Path("conv_logs") REMUX_CODECS = {"aac", "opus"} # Using a set for efficient lookups # Removed CONVERT_CODECS, now all non-remux codecs will be converted @@ -151,6 +163,7 @@ def main(no_downmix=False): check_tools() DIR_COMPLETED.mkdir(exist_ok=True, parents=True) DIR_ORIGINAL.mkdir(exist_ok=True, parents=True) + DIR_LOGS.mkdir(exist_ok=True, parents=True) current_dir = Path(".") @@ -165,150 +178,165 @@ def main(no_downmix=False): break file_path = files_to_process[0] - print("-" * shutil.get_terminal_size(fallback=(80, 24)).columns) - print(f"Starting full processing for: {file_path.name}") - date = datetime.now() - input_file_abs = file_path.resolve() - intermediate_output_file = current_dir / f"output-{file_path.name}" - audio_temp_dir = None # Initialize to None - created_ut_video_path = None - created_encoded_video_path = None + + # Setup logging + log_file_path = DIR_LOGS / f"{file_path.name}.log" + log_file = open(log_file_path, 'w', encoding='utf-8') + original_stdout = sys.stdout + original_stderr = sys.stderr + sys.stdout = Tee(original_stdout, log_file) + sys.stderr = Tee(original_stderr, log_file) try: - audio_temp_dir = tempfile.mkdtemp(prefix="tv_audio_") # UUID is not strictly needed for uniqueness - print(f"Audio temporary directory created at: {audio_temp_dir}") - print(f"Analyzing file: {input_file_abs}") + print("-" * shutil.get_terminal_size(fallback=(80, 24)).columns) + print(f"Starting full processing for: {file_path.name}") + date = datetime.now() + input_file_abs = file_path.resolve() + intermediate_output_file = current_dir / f"output-{file_path.name}" + audio_temp_dir = None # Initialize to None + created_ut_video_path = None + created_encoded_video_path = None - ffprobe_info_json = run_cmd([ - "ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", str(input_file_abs) - ], capture_output=True) - ffprobe_info = json.loads(ffprobe_info_json) + try: + audio_temp_dir = tempfile.mkdtemp(prefix="tv_audio_") # UUID is not strictly needed for uniqueness + print(f"Audio temporary directory created at: {audio_temp_dir}") + print(f"Analyzing file: {input_file_abs}") - mkvmerge_info_json = run_cmd([ - "mkvmerge", "-J", str(input_file_abs) - ], capture_output=True) - mkv_info = json.loads(mkvmerge_info_json) + ffprobe_info_json = run_cmd([ + "ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", str(input_file_abs) + ], capture_output=True) + ffprobe_info = json.loads(ffprobe_info_json) - mediainfo_json = run_cmd([ - "mediainfo", "--Output=JSON", "-f", str(input_file_abs) - ], capture_output=True) - media_info = json.loads(mediainfo_json) + mkvmerge_info_json = run_cmd([ + "mkvmerge", "-J", str(input_file_abs) + ], capture_output=True) + mkv_info = json.loads(mkvmerge_info_json) - created_ut_video_path, created_encoded_video_path = convert_video(file_path.stem, str(input_file_abs)) + mediainfo_json = run_cmd([ + "mediainfo", "--Output=JSON", "-f", str(input_file_abs) + ], capture_output=True) + media_info = json.loads(mediainfo_json) - print("--- Starting Audio Processing ---") - processed_audio_files = [] - audio_tracks_to_remux = [] - audio_streams = [s for s in ffprobe_info.get("streams", []) if s.get("codec_type") == "audio"] + created_ut_video_path, created_encoded_video_path = convert_video(file_path.stem, str(input_file_abs)) - # Build mkvmerge audio track list - mkv_audio_tracks_list = [t for t in mkv_info.get("tracks", []) if t.get("type") == "audio"] + print("--- Starting Audio Processing ---") + processed_audio_files = [] + audio_tracks_to_remux = [] + audio_streams = [s for s in ffprobe_info.get("streams", []) if s.get("codec_type") == "audio"] - # Build mediainfo track mapping by StreamOrder - media_tracks_data = media_info.get("media", {}).get("track", []) - mediainfo_audio_tracks = {int(t.get("StreamOrder", -1)): t for t in media_tracks_data if t.get("@type") == "Audio"} + # Build mkvmerge audio track list + mkv_audio_tracks_list = [t for t in mkv_info.get("tracks", []) if t.get("type") == "audio"] - for audio_idx, stream in enumerate(audio_streams): - stream_index = stream["index"] - codec = stream.get("codec_name") - channels = stream.get("channels", 2) - language = stream.get("tags", {}).get("language", "und") + # Build mediainfo track mapping by StreamOrder + media_tracks_data = media_info.get("media", {}).get("track", []) + mediainfo_audio_tracks = {int(t.get("StreamOrder", -1)): t for t in media_tracks_data if t.get("@type") == "Audio"} - # More robustly find the mkvmerge track by matching ffprobe's stream index - # to mkvmerge's 'stream_id' property. - mkv_track = next((t for t in mkv_info.get("tracks", []) if t.get("properties", {}).get("stream_id") == stream_index), None) - if not mkv_track: - # Fallback to the less reliable index-based method if stream_id isn't found - mkv_track = mkv_audio_tracks_list[audio_idx] if audio_idx < len(mkv_audio_tracks_list) else {} + for audio_idx, stream in enumerate(audio_streams): + stream_index = stream["index"] + codec = stream.get("codec_name") + channels = stream.get("channels", 2) + language = stream.get("tags", {}).get("language", "und") - track_id = mkv_track.get("id", -1) - track_title = mkv_track.get("properties", {}).get("track_name", "") - track_delay = 0 - audio_track_info = mediainfo_audio_tracks.get(stream_index) - delay_raw = audio_track_info.get("Video_Delay") if audio_track_info else None - if delay_raw is not None: - try: - delay_val = float(delay_raw) - if delay_val < 1: - track_delay = int(round(delay_val * 1000)) - else: - track_delay = int(round(delay_val)) - except Exception: - track_delay = 0 + # More robustly find the mkvmerge track by matching ffprobe's stream index + # to mkvmerge's 'stream_id' property. + mkv_track = next((t for t in mkv_info.get("tracks", []) if t.get("properties", {}).get("stream_id") == stream_index), None) + if not mkv_track: + # Fallback to the less reliable index-based method if stream_id isn't found + mkv_track = mkv_audio_tracks_list[audio_idx] if audio_idx < len(mkv_audio_tracks_list) else {} - print(f"Processing Audio Stream #{stream_index} (TID: {track_id}, Codec: {codec}, Channels: {channels})") - if codec in REMUX_CODECS: - audio_tracks_to_remux.append(str(track_id)) + track_id = mkv_track.get("id", -1) + track_title = mkv_track.get("properties", {}).get("track_name", "") + track_delay = 0 + audio_track_info = mediainfo_audio_tracks.get(stream_index) + delay_raw = audio_track_info.get("Video_Delay") if audio_track_info else None + if delay_raw is not None: + try: + delay_val = float(delay_raw) + if delay_val < 1: + track_delay = int(round(delay_val * 1000)) + else: + track_delay = int(round(delay_val)) + except Exception: + track_delay = 0 + + print(f"Processing Audio Stream #{stream_index} (TID: {track_id}, Codec: {codec}, Channels: {channels})") + if codec in REMUX_CODECS: + audio_tracks_to_remux.append(str(track_id)) + else: + opus_file = convert_audio_track( + stream_index, channels, language, audio_temp_dir, str(input_file_abs), not no_downmix + ) + processed_audio_files.append({ + "Path": opus_file, + "Language": language, + "Title": track_title, + "Delay": track_delay + }) + + print("--- Finished Audio Processing ---") + + # Final mux + print("Assembling final file with mkvmerge...") + mkvmerge_args = ["mkvmerge", "-o", str(intermediate_output_file), str(created_encoded_video_path)] + + for file_info in processed_audio_files: + mkvmerge_args.extend(["--language", f"0:{file_info['Language']}"]) + if file_info['Title']: # Only add track name if it exists + mkvmerge_args.extend(["--track-name", f"0:{file_info['Title']}"]) + if file_info['Delay']: + mkvmerge_args.extend(["--sync", f"0:{file_info['Delay']}"]) + mkvmerge_args.append(str(file_info["Path"])) + + source_copy_args = ["--no-video"] + + if audio_tracks_to_remux: + source_copy_args += ["--audio-tracks", ",".join(audio_tracks_to_remux)] else: - opus_file = convert_audio_track( - stream_index, channels, language, audio_temp_dir, str(input_file_abs), not no_downmix - ) - processed_audio_files.append({ - "Path": opus_file, - "Language": language, - "Title": track_title, - "Delay": track_delay - }) + source_copy_args += ["--no-audio"] + mkvmerge_args += source_copy_args + [str(input_file_abs)] + run_cmd(mkvmerge_args) - print("--- Finished Audio Processing ---") + # Move files + print("Moving files to final destinations...") + shutil.move(str(file_path), DIR_ORIGINAL / file_path.name) + shutil.move(str(intermediate_output_file), DIR_COMPLETED / file_path.name) - # Final mux - print("Assembling final file with mkvmerge...") - mkvmerge_args = ["mkvmerge", "-o", str(intermediate_output_file), str(created_encoded_video_path)] + except Exception as e: + print(f"An error occurred while processing '{file_path.name}': {e}", file=sys.stderr) + finally: + print("--- Starting Cleanup ---") + if audio_temp_dir and Path(audio_temp_dir).exists(): + print(" - Cleaning up disposable audio temporary directory...") + shutil.rmtree(audio_temp_dir, ignore_errors=True) - for file_info in processed_audio_files: - mkvmerge_args.extend(["--language", f"0:{file_info['Language']}"]) - if file_info['Title']: # Only add track name if it exists - mkvmerge_args.extend(["--track-name", f"0:{file_info['Title']}"]) - if file_info['Delay']: - mkvmerge_args.extend(["--sync", f"0:{file_info['Delay']}"]) - mkvmerge_args.append(str(file_info["Path"])) + if intermediate_output_file.exists(): + print(" - Cleaning up intermediate output file...") + intermediate_output_file.unlink() - source_copy_args = ["--no-video"] + print(" - Cleaning up persistent video temporary files...") + if created_ut_video_path and created_ut_video_path.exists(): + print(f" - Deleting UT video file: {created_ut_video_path}") + created_ut_video_path.unlink() + if created_encoded_video_path and created_encoded_video_path.exists(): + print(f" - Deleting encoded video temp file: {created_encoded_video_path}") + created_encoded_video_path.unlink() - if audio_tracks_to_remux: - source_copy_args += ["--audio-tracks", ",".join(audio_tracks_to_remux)] - else: - source_copy_args += ["--no-audio"] - mkvmerge_args += source_copy_args + [str(input_file_abs)] - run_cmd(mkvmerge_args) + alabama_dirs = list(current_dir.glob('.alabamatemp-*')) + if alabama_dirs: + print(" - Cleaning up AlabamaEncoder temporary directories...") + for temp_dir_alabama in alabama_dirs: + if temp_dir_alabama.is_dir(): + shutil.rmtree(temp_dir_alabama, ignore_errors=True) + print("--- Finished Cleanup ---") - # Move files - print("Moving files to final destinations...") - shutil.move(str(file_path), DIR_ORIGINAL / file_path.name) - shutil.move(str(intermediate_output_file), DIR_COMPLETED / file_path.name) - - except Exception as e: - print(f"An error occurred while processing '{file_path.name}': {e}", file=sys.stderr) + runtime = datetime.now() - date + runtime_str = str(runtime).split('.')[0] # Format to remove milliseconds + print(f"Total runtime for {file_path.name}: {runtime_str}") finally: - print("--- Starting Cleanup ---") - if audio_temp_dir and Path(audio_temp_dir).exists(): - print(" - Cleaning up disposable audio temporary directory...") - shutil.rmtree(audio_temp_dir, ignore_errors=True) - - if intermediate_output_file.exists(): - print(" - Cleaning up intermediate output file...") - intermediate_output_file.unlink() - - print(" - Cleaning up persistent video temporary files...") - if created_ut_video_path and created_ut_video_path.exists(): - print(f" - Deleting UT video file: {created_ut_video_path}") - created_ut_video_path.unlink() - if created_encoded_video_path and created_encoded_video_path.exists(): - print(f" - Deleting encoded video temp file: {created_encoded_video_path}") - created_encoded_video_path.unlink() - - alabama_dirs = list(current_dir.glob('.alabamatemp-*')) - if alabama_dirs: - print(" - Cleaning up AlabamaEncoder temporary directories...") - for temp_dir_alabama in alabama_dirs: - if temp_dir_alabama.is_dir(): - shutil.rmtree(temp_dir_alabama, ignore_errors=True) - print("--- Finished Cleanup ---") - - runtime = datetime.now() - date - runtime_str = str(runtime).split('.')[0] # Format to remove milliseconds - print(f"Total runtime for {file_path.name}: {runtime_str}") + # Restore stdout/stderr and close log file + sys.stdout = original_stdout + sys.stderr = original_stderr + log_file.close() if __name__ == "__main__": import argparse