def files_unrar(abs_path, ext=None): """ Unzip rar files Arguments: abs_path {string} -- Path to the directory containg rar files """ stderr = "" exts = ext if isinstance(ext, str) else tuple(ext) for rar_file in files_find_ext(abs_path, "rar"): files = [] if (exts): process = Popen(["unrar", "lb", rar_file], stdout=PIPE, stderr=PIPE) stdout, stderr = process.communicate() if len(stderr) > 1: break files = stdout.decode("UTF-8").split("\n") files = [f for f in files if f.endswith(exts)] if (files or exts == None): infomsg("Found RAR archive in source directory, extract it", "Postprocessing", (rar_file, )) process = Popen(["unrar", "x", "-o+", rar_file, abs_path], stdout=PIPE, stderr=PIPE) stderr = process.communicate()[1] if len(stderr) > 1: break if len(stderr) > 1: errmsg("Unrar archive failed with the following error message") print(stderr) exit()
def client(scope, port, source_host, output_host=None, original_host=None, original_mode=0): ## Get the URL to the Syno-Index server and add the arguments url = client_get_url(scope, port) if not output_host: query_vars = {'source_host': source_host} else: query_vars = { "source_host": source_host, "output_host": output_host, "original_host": original_host } query_vars["original_mode"] = original_mode ## Call the url and get the answer of the server url = url + urlencode(query_vars) infomsg(" Source", "SynoClient", (source_host, )) infomsg(" Handbrake Output", "SynoClient", (output_host, )) infomsg(" Original", "SynoClient", (original_host, )) infomsg(" Original mode", "SynoClient", (original_mode, )) try: contents = urlopen(url).read() debugmsg("SynoIndex-Server answered with", "SynoClient", (contents.decode("UTF-8"), )) except urllib.error.URLError: errmsg( "Server is not started yet, start the Triggered Task with the right mode" ) exit()
def main(): """ Name: VS-Handbrake (Part of the VS-Package) Summary: After handbrake finished the convertion process, the script is called and handles the right naming for the video file and relocates it according to its video type (episode or movie). """ ## Get the shell arguments args = argparse.Namespace() parser = argparse.ArgumentParser(description='Naming and locate script for Handbrake docker container') parser.add_argument('-f','--file', help='Path to the video file', required=True) args = parser.parse_args() args.script_dir = cur_dir args.scope = scope_get() ## Check whether the file exists if not os.path.isfile(args.file): errmsg("File does not exist, check the path", "Postprocessing") ## Parse the config config_file = os.path.join(cur_dir, "config.txt") cfg = parse_cfg(config_file, "vs-handbrake", "docker") ## Initialize the logging init_logging(args, cfg) ## Print the date and the file infomsg("-" * 35, "Postprocessing") infomsg("Handbrake finished converting file", "Postprocessing", (args.file,)) ## Check for the source file, continue if convert file doesnt exist args = get_convert_source_path(args) ## Check which media type it is and process it accordingly processing_file(cfg, args)
def parse_cfg_transmission(cfg, scope): mapping, host_admin, host_watch_dir = (None for _ in range(3)) ## Parse the config in case the script is running within a docker container if (scope == "docker"): mapping = parse_docker_mappings() handbrake = [m[0] for m in mapping if "handbrake" in m[0]] if (len(handbrake) > 0): handbrake = handbrake[0] else: errmsg("Define the handbrake mount in the container settings") exit() ## [Hostsystem] Parse the config in case the script is running in host system else: handbrake = parse_strlist(cfg.get("Host", "host_handbrake"), True)[0] host_watch_dir = parse_strlist(cfg.get("Host", "host_watch_dir"), True) host_admin = parse_hostadmin(cfg.get("Host", "host_admin")) codecs = enum(cfg.get("Transmission", "codecs")) extensions = enum(cfg.get("Transmission", "extensions")) port = parse_dig(cfg.get("SynoIndex", "synoindex_port"), 1, 65535) handbrake_exclude = parse_strlist(cfg.get("Handbrake", "handbrake_exclude")) handbrake_4k = parse_dig(cfg.get("Handbrake", "handbrake_4k"), 1, 2) log_level = parse_loglevel(cfg.get("Logging", "log_level")) log_dir = parse_strlist(cfg.get("Logging", "log_dir"))[0] return (mapping, codecs, extensions, port, handbrake, host_watch_dir, host_admin, handbrake_exclude, handbrake_4k, log_level, log_dir)
def get_resolution(file_name): ## Call mediainfo via command line cmds = ['mediainfo', file_name] p = subprocess.Popen(cmds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (stdout, stderr) = p.communicate() if (len(stderr) > 1): errmsg(stderr) ## Parse the mediainfo output as dictionary mediainfo_output = stdout.decode("UTF-8").split("\n") mediainfo_output = [ ' '.join(m.split()).split(" : ") for m in mediainfo_output ] mediainfo_output = dict([tuple(m) for m in mediainfo_output if len(m) > 1]) ## Parse the height of the video resolution res = mediainfo_output["Height"] res = int(res.replace(" ", "").replace("pixels", "")) ## Round the real resolution of the video file to the common resolution if (res == 0): return -1 if (0 < res and res <= 360): return "360p" elif (360 < res and res <= 480): return "480p" elif (480 < res and res <= 720): return "720p" elif (720 < res and res <= 1080): return "1080p" elif (1080 < res and res <= 2160): return "2160p" else: return -1
def processing_file(cfg, args): ## Check the file type file_dst = None if any(file_type in args.root_host.split(os.sep) for file_type in cfg.series): infomsg("Type: Episode", "Postprocessing") file_dst = naming_episode(args, cfg) elif any(file_type in args.root_host.split(os.sep) for file_type in cfg.movies): infomsg("Type: Movie", "Postprocessing") file_dst = naming_movie(args) else: exit("Error: Unsupported type of converted file") ## Delete the corresponding convert and watch file and add to synoindex if file_dst: msg = "Add converted file to synoindex and {} original file".format(switch_original(cfg.original)) infomsg(msg, "Postprocessing") client(args.scope, cfg.port, file_dst, args.output_host, args.source_host, cfg.original) watch_file = scope_reverse_map_path(cfg, args, args.watch_host) debugmsg("Delete watch file", "Postprocessing", (watch_file,)) try: os.remove(watch_file) except FileNotFoundError: errmsg("Could not find watch file at", "Postprocessing", (watch_file,)); exit() debugmsg("Delete convert file", "Postprocessing", (args.convert_path,)) os.remove(args.convert_path)
def get_convert_source_path(args): """ Get the convert file path and the source path inside it. Arguments: args {Namespace} -- Namespace containing the shell arguments. Returns: tuple -- (convert file path, source file path) """ ## check whether the convert directory exists file_name = os.path.basename(args.file) convert_file = "%s.txt" % os.path.splitext(file_name)[0] convert_path = os.path.join(os.sep, "convert") if not os.path.isdir(convert_path): errmsg("Seems like the data mount does not exist, [VS-Handbrake] -> /convert", "Postprocessing"); exit() ## check whether the convert file exists args.convert_path = os.path.join(convert_path, convert_file) if not os.path.isfile(args.convert_path): errmsg("Convert file does not exist"); exit() ## Get the source path with open(args.convert_path, "r") as f: lines = f.readlines() convert_args = [l.replace("\n", "").split(":")[1] for l in lines] args.root_host, args.source_host, args.output_host, args.watch_host = convert_args return args
def get_season_desc(cfg): if (cfg.language == "DE"): return "Staffel" elif (cfg.language == "EN"): return "Season" else: errmsg("Invalid language in configuration", "Naming") exit()
def connect(): try: conn = psycopg2.connect("dbname='video_metadata' user='******'") except: errmsg("Could not connect to the database, try to run as follows: sudo -u postgres python") return (None,None) cur = conn.cursor() return (conn, cur)
def parse_hostadmin(username): ''' Parse the host admin username from config ''' try: gid = pwd.getpwnam(username).pw_gid uid = pwd.getpwnam(username).pw_uid except KeyError: errmsg("Configured host admin does not exist", "Parsing", (username, )) exit() return (uid, gid)
def parse_loglevel(log_level): allowed = [10, 20, 40] log_level = enum(log_level)[0] try: if int(log_level) in allowed: return int(log_level) else: errmsg("Invalid log level in config") exit() except ValueError: errmsg("Invalid log level in config") exit()
def parse_language(language): allowed = ["DE", "EN"] language = enum(language)[0] try: if language in allowed: return language else: errmsg("Invalid language in config") exit() except ValueError: errmsg("Invalid language in config") exit()
def daemon_items_same(conn, cur, user_collection, admin_collection): ## Get all items of the collection user_items = get_all_items_of_collection(cur, user_collection) admin_items = get_all_items_of_collection(cur, admin_collection) if (user_items == None or admin_items == None): errmsg("Could not get items of admin collection") close_connection(conn, cur) exit(-1) if (set([u[0] for u in user_items]) == set([u[0] for u in admin_items])): return True return False
def daemon_mode(args): debugmsg("Started execution", "Daemon mode") ## Connect to Video Station database conn, cur = connect() if (conn == None): errmsg("Could not connect to database") exit(-1) debugmsg("Connected successfully to Synology Video Station database", "Daemon mode") ## Get the admin ID users, admin = users_get_selection(1) ## Get all collection information of the admin admin_collections = get_collections_of_user(cur, admin[1]) if (admin_collections == None): close_connection(conn, cur) exit(-1) changes = False for user in users: ## Check whether user has all admin collections user_collections = get_collections_of_user(cur, user[1]) new_collections = daemon_collections_same(user_collections, admin_collections) for collection_info in new_collections: daemon_collection_add_to_user(conn, cur, collection_info, user) debugmsg("Add another playlist to the user", "Daemon mode", (user[0], collection_info[2])) changes = True ## Check whether all items in every collection is synced for collection_info in admin_collections: user_collection = [ uc for uc in user_collections if uc[2] == collection_info[2] ] if user_collection: user_collection = user_collection[0] if not daemon_items_same(conn, cur, user_collection, collection_info): delete_all_items_of_collection(conn, cur, user_collection) delete_collection(conn, cur, user_collection) daemon_collection_add_to_user(conn, cur, collection_info, user) debugmsg( "Renew playlist due to admin's playlist was updated", "Daemon mode", (user[0], collection_info[2])) changes = True if not changes: debugmsg("Finish daemon mode without any changes", "Daemon mode") return
def parse_dig(dig, imin, imax): ''' Parse a digit ''' dig = enum(dig)[0] try: if imin <= int(dig) <= imax: return int(dig) else: errmsg("Invalid digit in config") exit() except ValueError: errmsg("Invalid digit in config") exit()
def mail_get_addresses(args): """ Get all users and mail address of the current system Arguments: args {Namespace} -- Namespace containing all arguments. Returns: string -- List of tuples with users and mail addresses """ ## Get the username, real name and the mail address of each user admin = getpass.getuser() user_file = os.path.join(os.sep, "volume1", "homes", admin, "users.csv") user_mails = os.path.join(args.scriptdir, "user_mails.sh") p = Popen(["/bin/bash", user_mails, admin], stdout=PIPE, stderr=PIPE) stderr = p.communicate()[1] if p.returncode != 0: errmsg("Could not get user mail addresses", (stderr, )) exit() try: with open(user_file, 'r', encoding='utf-8', errors='replace') as f: users = f.read() except EnvironmentError: errmsg("Could not read the user mail addresses file", (user_file, )) exit() os.remove(user_file) ## Parse user names and separate the admin users = ("".join(i for i in users if ord(i) < 128)).split("\n") users = [u.replace("\n", "").replace("\"", "") for u in users] users = [u.replace(" (user)", "").replace(" (admin)", "") for u in users] users = list(filter(None, users)) fs_admin = users_get_selection(2, False) admin = [u for u in users if u.split(',')[0] == fs_admin] users = list(set(users) - set(admin)) ## Get the mail tuples fs_admin = users_get_selection(2, False) user_mails = [ "{} <{}>".format(u.split(",")[1], u.split(",")[2]) for u in users ] admin_mails = [ "{} <{}>".format(a.split(",")[1], a.split(",")[2]) for a in admin ] mails = admin_mails if args.receivers == "admin" else user_mails if args.receivers == "all": mails = admin_mails + user_mails infomsg("Selected list of mail addresses by receivers argument", "Mail", (",".join(mails), )) return mails
def parse_cfg(config_file, config_type, scope): ''' Parse all configuration options of the config file. ''' ## Read the config file config = ConfigParser() config.read(config_file) ## VS-Handbrake if (config_type == "vs-handbrake"): sections = ["Handbrake", "SynoIndex", "Logging"] fields = [ "mapping", "movies", "series", "original", "language", "port", "log_level", "log_dir" ] ## VS-Transmission elif (config_type == "vs-transmission"): sections = [ "Transmission", "SynoIndex", "Handbrake", "Host", "Logging" ] fields = [ "mapping", "codecs", "extensions", "port", "handbrake", "watch_directories", "host_admin", "exclude", "hb_4k", "log_level", "log_dir" ] else: errmsg("Config type not supported") exit() ## Check whether all sections are present and initialize config Namespace _ = [ exit('Error: Section (%s) missing' % s) for s in sections if not config.has_section(s) ] cfg = namedtuple('cfg', " ".join(fields)) cfg.__new__.__defaults__ = (None, ) * len(cfg._fields) ## VS-Handbrake if (config_type == "vs-handbrake"): (mpg, movies, series, original, lang, port, level, log) = parse_cfg_handbrake(config, scope) parsed_cfg = cfg(mpg, movies, series, original, lang, port, level, log) ## VS-Transmission elif (config_type == "vs-transmission"): (mpg, cds, exts, port, hb, dirs, had, excls, h4k, lvl, log) = parse_cfg_transmission(config, scope) parsed_cfg = cfg(mpg, cds, exts, port, hb, dirs, had, excls, h4k, lvl, log) return parsed_cfg
def add_all_items_to_collection(conn, cur, items, new_collection_id): items = [(mapper_id, new_collection_id) for mapper_id, collection_id in items] try: values = ', '.join(map(str, items)) sql_query = ( "INSERT INTO collection_map (mapper_id, collection_id) VALUES {}" ).format(values) cur.execute(sql.SQL(sql_query)) conn.commit() except psycopg2.IntegrityError: errmsg("Could not insert the items to the new playlist") return None
def get_all_items_of_collection(cur, collection_info): collection_id = collection_info[0] ## Get all items cur.execute( sql.SQL("SELECT * FROM collection_map WHERE collection_id = %s;"), [collection_id]) result = cur.fetchall() ## Parse the mapper_id and collection_id result = [r[1:3] for r in result] if (result == None or len(result) == 0): errmsg("The playlist is empty") return None return (result)
def main(): ## Parse the shell arguments args = parse_arguments() ## Parse the config config_file = os.path.join(cur_dir, "config.txt") cfg = parse_cfg(config_file, "vs-transmission", args.scope) ## Check for userid and groupid in docker scope if (args.scope == "docker" and (args.userid == 0 or args.groupid == 0)): errmsg("Docker scope requires userid and groupid", "Post-Processing") elif (args.scope == "host"): args.userid, args.groupid = cfg.host_admin ## Post Processing post_processing(args, cfg)
def get_collections_of_user(cur, user): if (user == None): errmsg("get_collections_of_user() failed - User missing") exit() else: cur.execute( sql.SQL( "SELECT * FROM collection WHERE uid = %s AND (title != %s AND title != %s AND title != %s);" ), [ str(user), "syno_watchlist", "syno_favorite", "syno_default_shared" ]) result = cur.fetchall() if (result == None): errmsg("User does not have any playlist information") return None return (result)
def get_collection(cur, playlist, user=None): if (user == None): cur.execute( sql.SQL( "SELECT * FROM collection WHERE title = %s AND (title != %s AND title != %s);" ), [playlist, "syno_watchlist", "syno_favorite"]) result = cur.fetchall() else: cur.execute( sql.SQL( "SELECT * FROM collection WHERE title = %s AND uid = %s AND (title != %s AND title != %s);" ), [playlist, str(user), "syno_watchlist", "syno_favorite"]) result = cur.fetchone() if (result == None): errmsg("Could not find the playlist") return None return (result)
def parse_arguments(): """ Parse the shell arguments Returns: Namespace -- Namespace containing all arguments """ args = argparse.Namespace() parser = argparse.ArgumentParser( description='Post Processing of torrents via transmission') parser.add_argument('-n', '--name', help='Name of the torrent', required=True) parser.add_argument('-d', '--directory', help='Directory of the torrent', required=True) parser.add_argument('-u', '--userid', help='ID of the user (PUID)', default=0, type=int, nargs='?') parser.add_argument('-g', '--groupid', help='ID of the group (PGID)', default=0, type=int, nargs='?') args = parser.parse_args() args.script_dir = cur_dir args.scope = scope_get() ## Check whether the passed name and directory are valid if not os.path.isdir(args.directory): errmsg("Passed torrent directory does not exist", "Parsing", (args.directory, )) exit() full_path = os.path.join(args.directory, args.name) if (not os.path.isdir(full_path)) and (not os.path.isfile(full_path)): errmsg("Passed torrent does not exist", "Parsing", (full_path, )) exit() return args
def create_new_collection(conn, cur, collection_info, userid): if (int(userid) == int(collection_info[1])): errmsg("The ownerID and the userID are equal") return None ## Insert the collection collection_name = collection_info[2] try: cur.execute( sql.SQL("INSERT INTO collection (uid,title) VALUES (%s,%s);"), [userid, collection_name]) conn.commit() except psycopg2.IntegrityError: errmsg( "Could not insert the new playlist, the playlist already exists (%s)", userid) return None ## Get the ID of the new created collection cur.execute( sql.SQL("SELECT * FROM collection WHERE uid = %s AND title = %s;"), [userid, collection_name]) result = cur.fetchone() if (result == None): errmsg( "Could not insert the new playlist, the playlist already exists") return None return result[0]
def parse_strlist(strlist, paths=False): ''' Parse a stringlist which may be a list of paths ''' try: strlist = list(filter(None, strlist.split(','))) strlist = [s.strip() for s in strlist] except ValueError: errmsg("Invalid string list in config") exit() if not strlist: errmsg("Invalid string list in config") exit() if paths: paths = [p.rstrip(os.sep).lstrip() for p in strlist] non_paths = [p for p in paths if not os.path.isdir(p)] paths = [p for p in paths if os.path.isdir(p)] if not paths: infomsg("Config contains list of non-existent file paths", "Parsing", (paths, )) if non_paths: infomsg("Config contains path which does not exist", "Parsing", (non_paths, )) return paths return strlist
def analyze_movie(movie): ## Get the extensions of the season movie.extension = os.path.splitext(movie.file)[1] ## Analyze resolution of the season movie.resolution = get_resolution(movie.file) if (movie.resolution == -1): errmsg("The resolution of the movie was invalid", "Naming") exit() ## Get the real movie name of the file movie.name_bk = movie.original.replace(movie.movies_path, "").split(os.sep)[1] movie.name = [] for token in movie.name_bk.split(movie.delim): if (token.isdigit() and int(token) > 1925 and int(token) < 2050): break movie.name.append(token) movie.name = " ".join(movie.name) ## Get the destination directory movie.dst = os.path.join(movie.movies_path, movie.name_bk) return movie
def daemon_collection_add_to_user(conn, cur, collection_info, user): new_collection_id = create_new_collection(conn, cur, collection_info, str(user[1])) if (new_collection_id == None): errmsg("Could not find collection of admin") close_connection(conn, cur) exit(-1) ## Get all items of the collection items = get_all_items_of_collection(cur, collection_info) if (items == None): errmsg("Could not get items of admin collection") close_connection(conn, cur) exit(-1) ## Add all items to the new collection add_all_items_to_collection(conn, cur, items, new_collection_id) if (items == None): close_connection(conn, cur) errmsg("Could not add items to user collection") exit(-1)
def analyze_series(cfg, series): ## Get the extensions of the season series.file_base, series.extension = os.path.splitext(series.file) ## Analyze resolution of the season series.resolution = get_resolution(series.file) if (series.resolution == -1): errmsg("The resolution of the series episode was invalid", "Naming") exit() ## Analyze the current season number splitted = series.file_base.split(series.delim) try: debugmsg("Check for regular naming scheme (s01e01)", "Naming") season_ep = [ f for f in splitted if "s0" in f.lower() or "s1" in f.lower() ][0] series.season = "{}".format(re.split('s|e', season_ep.lower())[1]) series.episode = "S%sE%s" % (series.season, re.split('s|e', season_ep.lower())[2]) series.season = "%s %s" % (get_season_desc(cfg), series.season) except IndexError: debugmsg( "Regular naming scheme not found, check for alternative naming scheme (101|1201|122324)", "Naming") season_ep = splitted[-1] if (season_ep.isdigit()): if (int(season_ep) > 100 and int(season_ep) < 400000): ## Single episode - 101, 923 if (int(season_ep) > 100 and int(season_ep) < 1000): series.season = "{:02d}".format(int(season_ep[0])) series.episode = "S{}E{}".format(series.season, season_ep[1:]) ## Single episode - 1001,9023 elif (int(season_ep) > 1000 and int(season_ep) < 4000): series.season = "{:02d}".format(int(season_ep[:2])) series.episode = "S{}E{}".format(series.season, season_ep[-2:]) ## Double episode - 100102, 12324, 92324 elif (int(season_ep) > 10000 and int(season_ep) < 40000): series.season = "{:02d}".format(int(season_ep[0])) series.episode = "S{}E{}".format(series.season, season_ep[1:3]) ## Double episode - 122324 elif (int(season_ep) > 100000 and int(season_ep) < 400000): series.season = "{:02d}".format(int(season_ep[:2])) series.episode = "S{}E{}".format(series.season, season_ep[2:4]) else: errmsg("Undefined naming scheme for episode", "Naming", (series.file, )) exit() series.season = "{} {}".format(get_season_desc(cfg), series.season) debugmsg("Alternative naming scheme found", "Naming") else: errmsg("Undefined naming scheme for episode", "Naming", (series.file, )) exit() else: debugmsg( "Alternative naming scheme not found, check for unusual naming scheme (Ep 01)", "Naming") season = None for part in splitted: if (part.isdigit()): season = series.original.replace( series.series_path, "").split(os.sep)[2].split()[1] series.season = "{:02d}".format(int(season)) series.episode = "S{}E{}".format(series.season, part) series.season = "{} {}".format(get_season_desc(cfg), series.season) debugmsg("Unusual naming scheme found", "Naming") break if not season: errmsg("Undefined naming scheme for episode", "Naming", (series.file, )) exit() ## Get the series name of the file series.name_bk = series.original.replace(series.series_path, "").split(os.sep)[1] series.name = series.name_bk.replace(" ", "-") ## Get the destination directory series.dst = os.path.join(series.series_path, series.name_bk, series.season) return series
def copy_file_to_handbrake(args, cfg, source, source_host, root_host): """ Copy file to the handbrake watch directory and change owner. Arguments: args {Namespace} -- Namespace containing all shell arguments. cfg {Namespace} -- Namespace containing all configurations. source {string} -- Path to the source within docker container. source_host {string} -- Path to the source file on the host system. root_host {string} -- Path to the top mount containing the file. """ ## Get all media info about the file video_info = ffprobe_file(source) codec = video_info["video_codec"] resolution = video_info["resolutionY"] debugmsg("Analyse the video file for codec and resolution", "Mediainfo", (resolution, codec)) ## Check whether it is one codec of the config is present if codec not in cfg.codecs: infomsg("Codec is not watched in file", "Postprocessing", (source, codec)) return ## Only copy files which match no exclude string if any(exclude in source for exclude in cfg.exclude): infomsg("Source file excluded for handbrake by config", "Postprocessing", (source, )) return ## Switch the watch directory depending on the 4K mode and the resolution watch_host = os.path.join(cfg.handbrake, "watch") if (cfg.hb_4k == 1 and int(resolution) > 2000): infomsg("4K mode enabled - file is copied to separate watch directory", "Postprocessing", (watch_host, )) watch_host = os.path.join(cfg.handbrake, "watch2") if not os.path.isdir(watch_host): create_path_directories(watch_host) os.chown(watch_host, cfg.host_admin[0], cfg.host_admin[1]) ## Copy the video file to the handbrake watch directory infomsg("Copying file to handbrake watch directory", "Postprocessing", (watch_host, )) watch_file = file_copy(source, watch_host, args) if not watch_file: errmsg("Could not copy file to handbrake watch directory", "Postprocessing", (watch_host, )) return infomsg("Finished copying file", "Postprocessing", (watch_file, )) ## Convert the output file into host system path output_file = os.path.join(cfg.handbrake, "output", os.path.basename(watch_file)) output_host = scope_map_path(cfg, args, output_file)[0] if output_host == -1: errmsg("Could not get the host path of file", "Postprocessing", (output_file)) return ## Convert the watch file into host system path watch_file = os.path.join(watch_host, os.path.basename(watch_file)) watch_host = scope_map_path(cfg, args, watch_file)[0] if watch_host == -1: errmsg("Could not get the host path of watch file", "Postprocessing", (watch_file)) return ## Write the convert file with all necessary information write_convert_file(cfg, source, source_host, root_host, output_host, watch_host)