From aa51ecd2799224ef384700039f86cd73b84b6f90 Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Sat, 27 Aug 2022 20:02:19 +0200 Subject: [PATCH] Added transcoding of series --- transcoder/transcode_dvd.py | 228 +++++++++++++++++++++++++++++------- 1 file changed, 185 insertions(+), 43 deletions(-) diff --git a/transcoder/transcode_dvd.py b/transcoder/transcode_dvd.py index 1b3eda7..a3ad5da 100755 --- a/transcoder/transcode_dvd.py +++ b/transcoder/transcode_dvd.py @@ -4,6 +4,8 @@ import os import subprocess import shutil from datetime import datetime +import json +import re TMP_DIR = "tmp" RAW_DIR = "raw" @@ -15,30 +17,91 @@ HANDBRAKE_AUDIO_LANG_LIST = "de,en" OUT_VIDEO_FORMAT = ".m4v" MIN_EPISODES_DURATION_SECONDS = 10 * 60 +SERIES_TITLE_REGEX = r"S(\d+)[ _]?E(\d+)-(\d+)$" + def main(): chdir_to_script_dir() mkdirs() for dvd_type in ("movie", "series"): for dvd in filter(is_dvd_files_not_locked, list_ripped_dvds(dvd_type)): - print(f"Transcoding {dvd_type} {dvd}") + print(f"Transcoding {dvd}") # TOOD: nice transcoding process so it does not block server - success = transcode_ripped_dvd(dvd, dvd_type) - dvd_title = os.path.basename(dvd) - if success: - mv_video_from_tmp_to_transcoded_dir(dvd_title, dvd_type) - write_to_logfile(dvd_type, dvd_title, "Success") + output_files = transcode_ripped_dvd(dvd) + if output_files: + mv_videos_from_tmp_to_transcoded_dir(output_files, dvd.dvd_type) + write_to_logfile(dvd, "Success") delete_original_video_files(dvd) delete_transcoding_logfile(dvd) print("Success") else: - write_to_logfile(dvd_type, dvd_title, "FAILURE") - err_log_file = dvd + ".err.log" - print(f"Failed. Please see logs at {err_log_file}") + write_to_logfile(dvd, "FAILURE") + rename_transcoding_logfile_as_error_log(dvd) + print(f"Failed. Please see logs at {dvd.err_log_file}") delete_tmp_dir() +class Dvd: + def __init__(self, dvd_type, dvd_path): + self.dvd_type = dvd_type + self.dvd_path = dvd_path + self.dvd_title = os.path.basename(dvd_path) + + self.log_file = dvd_path + ".log" + self.err_log_file = dvd_path + ".err.log" + + def __str__(self): + return f"{self.dvd_type} {self.dvd_path}" + + def is_movie(self): + return self.dvd_type == "movie" + + def is_series(self): + return self.dvd_type == "series" + + def append_line_to_logfile(self, line): + with open(self.log_file, "a") as log_file: + log_file.write(line + "\n") + + def series_create_episode_title(self, series, episode): + re_matches = re.search(SERIES_TITLE_REGEX, self.dvd_title) + if re_matches is None: + return None + + return self.dvd_title[: re_matches.start()] + f"S{series:02d}_E{episode:02d}" + + def series_get_season_episodes(self): + re_matches = re.search(SERIES_TITLE_REGEX, self.dvd_title) + if re_matches is None: + return None + + series = int(re_matches.group(1)) + episode_from = int(re_matches.group(2)) + episode_to = int(re_matches.group(3)) + + if episode_from >= episode_to: + return None + + episodes = list(range(episode_from, episode_to + 1)) + + return (series, episodes) + + def transcode_command_args_without_title_number(self, output_file): + return [ + "HandBrakeCLI", + "--preset", + HANDBRAKE_PRESET, + "--first-audio", + "--audio-lang-list", + HANDBRAKE_AUDIO_LANG_LIST, + "--input", + self.dvd_path, + "--output", + output_file, + ] + + def chdir_to_script_dir(): os.chdir(os.path.dirname(__file__)) @@ -58,7 +121,9 @@ def list_ripped_dvds(dvd_type): dvd_titles = filter(lambda title: not title.endswith(".lock"), dvd_titles) dvd_titles = filter(lambda title: not title.endswith(".err.log"), dvd_titles) dvd_titles = filter(lambda title: not title.endswith(".log"), dvd_titles) - return map(lambda dvd_title: os.path.join(path, dvd_title), dvd_titles) + return map( + lambda dvd_title: Dvd(dvd_type, os.path.join(path, dvd_title)), dvd_titles + ) except FileNotFoundError: print( f"Directory {path} not found. Running the ripper script will generate it." @@ -70,58 +135,135 @@ def is_dvd_files_not_locked(dvd): return not os.path.exists(dvd + ".lock") and not os.path.exists(dvd + ".err.log") -def transcode_ripped_dvd(ripped_dvd, dvd_type): - if dvd_type == "movie": - return transcode_movie(ripped_dvd) +def transcode_ripped_dvd(dvd): + if dvd.is_movie(): + return transcode_movie(dvd) else: - return transcode_series(ripped_dvd) + return transcode_series(dvd) -def transcode_movie(movie): - video_file_name = os.path.basename(movie) + OUT_VIDEO_FORMAT +def transcode_movie(dvd: Dvd): + video_file_name = dvd.dvd_title + OUT_VIDEO_FORMAT output_file = os.path.join(TMP_DIR, video_file_name) - log_file_path = movie + ".log" - - with open(log_file_path, "w") as log_file: + with open(dvd.log_file, "w") as log_file: proc = subprocess.run( - [ - "HandBrakeCLI", - "--preset", - HANDBRAKE_PRESET, + dvd.transcode_command_args_without_title_number(output_file) + + [ "--main-feature", - "--first-audio", - "--audio-lang-list", - HANDBRAKE_AUDIO_LANG_LIST, - "--input", - movie, - "--output", - output_file, ], stderr=subprocess.STDOUT, stdout=log_file, ) - return proc.returncode == 0 + if proc.returncode != 0: + return False + else: + return [output_file] -def transcode_series(series): - # --min-duration MIN_EPISODES_DURATION - # --title 1 - # --title 2 - # etc - pass +def transcode_series(dvd: Dvd): + title_numbers = find_series_titles(dvd) + if not title_numbers: + dvd.append_line_to_logfile( + "Could not find any titles which could be episodes. Maybe you need to decrease the 'MIN_EPISODES_DURATION_SECONDS'?" + ) + return False + + season_episodes = dvd.series_get_season_episodes() + if season_episodes is None: + dvd.append_line_to_logfile(f"Dvd has non valid series name!! ({dvd})") + return False + + (season, episodes) = season_episodes + if len(episodes) != len(title_numbers): + dvd.append_line_to_logfile( + f"{dvd} should have {len(episodes)} episodes, but handbrake found {len(title_numbers)}. Maybe you need to adjust 'MIN_EPISODES_DURATION_SECONDS'?" + ) + return False + + output_files = [] + + for i in range(len(episodes)): + episode = episodes[i] + title_number = title_numbers[i] + + episode_title = dvd.series_create_episode_title(season, episode) + if episode_title is None: + dvd.append_line_to_logfile(f"Dvd has non valid series name!! ({dvd})") + return False + + video_file_name = episode_title + OUT_VIDEO_FORMAT + output_file = os.path.join(TMP_DIR, video_file_name) + output_files.append(output_file) + + with open(dvd.log_file, "w") as log_file: + proc = subprocess.run( + dvd.transcode_command_args_without_title_number(output_file) + + [ + "--title", + title_number, + "--min-duration", + str(MIN_EPISODES_DURATION_SECONDS), + ], + stderr=subprocess.STDOUT, + stdout=log_file, + ) + if proc.returncode != 0: + return False + + return output_files -def mv_video_from_tmp_to_transcoded_dir(video_title, video_type): - src = os.path.join(TMP_DIR, video_title) - dst = os.path.join(TRANSCODED_DIR, video_type, video_title) +def find_series_titles(dvd: Dvd): + with open(dvd.log_file, "w") as log_file: + proc = subprocess.run( + [ + "HandBrakeCLI", + "--input", + dvd.dvd_path, + "-t", + "0", + "--min-duration", + str(MIN_EPISODES_DURATION_SECONDS), + "--json", + ], + stdout=subprocess.PIPE, + stderr=log_file, + ) - shutil.move(src, dst) + if proc.returncode != 0: + return None + + stdout = proc.stdout.decode("utf-8") + + json_str = "" + in_json = False + for line in stdout.splitlines(): + if not in_json: + if line == "JSON Title Set: {": + in_json = True + json_str = "{\n" + else: + json_str += line + "\n" + if line == "}": + break + + json_obj = json.loads(json_str) + titles = list(map(lambda title: title["Index"], json_obj["TitleList"])) + return titles -def write_to_logfile(dvd_type, dvd_title, tag): +def mv_videos_from_tmp_to_transcoded_dir(video_files, video_type): + for video_file in video_files: + src = os.path.join(TMP_DIR, video_file) + dst = os.path.join(TRANSCODED_DIR, video_type) + + shutil.move(src, dst) + + +def write_to_logfile(dvd: Dvd, tag): date = datetime.now().strftime("%d.%m.%Y %H:%M:%S") - log_line = f'{date} - {dvd_type} - "{dvd_title}" - {tag}' + log_line = f'{date} - {dvd.dvd_type} - "{dvd.dvd_title}" - {tag}' with open(LOGFILE, "a") as file: file.write(log_line + "\n")