def rename_file(self, path: str, template: str, sanitize: bool = True): """ Renames a given file according to a given template. Args: path (str): Path to the file to rename. template (str): Output template to rename the file by. sanitize (bool, optional): Defaults to True. True to replace path delimiters within the substituted template to avoid unexpected directories. Returns: str: The renamed path to the file. """ if not path.exists(): return info = FileSystem(path = path) replaced_template = self.replace_values(info, template) if sanitize: replaced_template = replaced_template.replace('/', '_').replace('\\', '_') new_filename = path.with_name(replaced_template) path.rename(new_filename) Logger.log(r'PostProcessor', r'{} => {}'.format(path.name, replaced_template)) return new_filename
def construct_command(self, subscription: Subscription) -> str: """ Builds the youtube-dl command for the given subscription. Args: subscription (Subscription): The subscription to process. Returns: str: The youtube-dl command with all desired arguments. """ command = r'youtube-dl' # Add the youtube-dl config path. if subscription.youtubedl_config.config: config_path = os.path.join(os.getenv('youtubedl_config_directory'), subscription.youtubedl_config.config) command += r' --config-location "{}"'.format(config_path) # Add the metadata-from-title pattern. if subscription.youtubedl_config.metadata_format: command += r' --metadata-from-title "{}"'.format(subscription.youtubedl_config.metadata_format) # Add the output pattern. if subscription.youtubedl_config.output_format: output_format = subscription.staging_directory + '/staging_area/' + subscription.youtubedl_config.output_format command += r' -o "{}"'.format(output_format) # Add the path to the video ID archive. if subscription.youtubedl_config.archive: archive_path = os.path.join(subscription.staging_directory, subscription.youtubedl_config.archive) command += r' --download-archive "{}"'.format(archive_path) # Add any extra arguments this sub has. if subscription.youtubedl_config.extra_commands: command += " " + subscription.youtubedl_config.extra_commands # Add the subscription URL. command += r' "{}"'.format(subscription.url) # Construct the post-processing call back into # Chrysalis to be run after each successful download. if subscription.post_processing: command += ' --exec \'"{}" "{}" --postprocess {{}} --subscription "{}"\''.format( sys.executable, __file__, subscription.name ) # Construct the stdout redirect to the log file. if subscription.logging.path: command += r' {} "{}"'.format( '>>' if subscription.logging.append == True else '>', subscription.logging.path ) Logger.log(r'Chrysalis', r'Command to be run: [{}]'.format(command)) return command
def run(self, postprocessor): Logger.log(r'Plex', r'Processing {}...'.format(postprocessor.path_info.parent), 1) self.postprocessor = postprocessor self.login() if "metadata" in self.postprocessor.settings.post_processing.destination: self.set_metadata()
def run(self): """ Runs all post-processing on the file. Returns: str: Path to the post-processed output directory. """ self.path_info = pathlib.Path(self.file) Logger.log(r'PostProcessor', r'Processing {}...'.format(self.path_info.parent), 1) if self.settings.post_processing.repositories is not None and 'tvdb' in self.settings.post_processing.repositories: self.get_api_info() if self.settings.post_processing.pattern is not None: self.special_values = re.match(self.settings.post_processing.pattern, str(self.path_info)) if self.settings.post_processing.subtitle is not None: self.rename_subtitles() if self.settings.post_processing.thumbnail is not None: self.rename_thumbnails() if self.settings.post_processing.description is not None: self.rename_description() if self.settings.post_processing.video is not None: self.path_info = self.rename_video() if self.settings.post_processing.episode_folder is not None: new_folder = self.rename_folder() else: new_folder = self.path_info.parent self.path_info = new_folder / self.path_info.name if self.settings.post_processing.output_directory: self.move_from_staging_area(new_folder) if self.settings.post_processing.real_destinations: destination = self.settings.post_processing.real_destinations[0] if destination: destination.run(self) Logger.tabs -= 1 return new_folder
def move_from_staging_area(self, current_path): """ Moves the post-processed folder from the staging area to the final path specified by the subscription. """ out_dir = self.settings.post_processing.output_directory pathlib.Path(out_dir).mkdir(parents=True, exist_ok=True) final_dir = out_dir + '/' + current_path.name current_path.rename(final_dir) self.path_info = pathlib.Path(final_dir) / self.path_info.name Logger.log(r'PostProcessor', r'Refoldered to {}'.format(final_dir))
def try_description_file(self): description_file_path = self.postprocessor.path_info.with_suffix( '.description') try: with open(description_file_path, 'r') as myfile: self.metadata['summary']['found value'] = myfile.read() except: Logger.log(r'Plex', r'Couldn\'t read from *.description file.') description_file_path = self.postprocessor.path_info.parent / 'description.txt' try: with open(description_file_path, 'r') as myfile: self.metadata['summary']['found value'] = myfile.read() except: Logger.log(r'Plex', r'Couldn\'t read from description.txt file.')
def get_series_episodes(self, series_id: int): """ Query episode information for series. Automatically retrieves all pages. Args: series_id (int): Unique ID of the series. Returns: list: List of dicts for episodes. """ Logger.log(r'API', r'Querying episodes...') page = 1 episodes = [] while True: response = self.http.request( 'GET', self.base_url + 'series/' + str(series_id) + '/episodes?page=' + str(page), headers={ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + self.jwt }) response_data = json.loads(response.data.decode('utf-8')) episodes.extend(response_data['data']) if response_data['links']['next'] is None or page == response_data[ 'links']['last']: break page += 1 episodes = list({ v['episodeName']: v for v in episodes if v['episodeName'] is not None and v['episodeName'] != '' }.values()) return episodes
def process_subscription(self, subscription: Subscription): """ Runs youtube-dl and the post-processing for the given subscription. Parameters: subscription (Subscription): The subscription to process. """ if not subscription.enabled: return Logger.log(r'Chrysalis', r'Processing "{}"...'.format(subscription.name)) self.setup_staging_directory(subscription) if subscription.logging and subscription.logging.path: pathlib.Path(subscription.logging.path).parent.mkdir(parents=True, exist_ok=True) command = self.construct_command(subscription) subprocess.run(command, shell=True)
def try_file_metadata(self): try: file_metadata = ffmpeg.probe(str(self.postprocessor.path_info)) except: None if file_metadata: if self.metadata['summary']['wanted']: try: self.metadata['summary']['found value'] = file_metadata[ 'format']['tags']['DESCRIPTION'] except: Logger.log(r'Plex', r'Couldn\'t read description from tags.') if self.metadata['originallyAvailableAt']['wanted']: try: temp_date = file_metadata['format']['tags']['DATE'] self.metadata['originallyAvailableAt']['found value'] = temp_date[0:4] \ + '-' + temp_date[4:6] + '-' + temp_date[6:8] except: Logger.log(r'Plex', r'Couldn\'t read date from tags.')
def get_api_info(self): """ Retrieves the API info for the file. """ from Repositories.TVDB import TVDB Logger.log(r'API', r'Querying...', 1) api = TVDB() api.login() self.series = api.get_series(self.settings.post_processing.series_id) episodes = api.get_series_episodes(self.settings.post_processing.series_id) if episodes is None or len(episodes) == 0: Logger.log(r'API', r'No episodes returned!', -1) return -1 file_matches = re.match(self.settings.post_processing.pattern, str(self.path_info)) self.episode = api.match_episode(episodes, file_matches.group('episodeName')) Logger.tabs -= 1
def postprocess(self, file: str, subscription: Subscription) -> str: """ Runs the post-processing for the given youtube-dl output file. Args: file (str): Absolute path to the youtube-dl output file. subscription (Subscription): The settings to process the file under. Returns: str: The absolute path to the folder where all the files were moved. """ from PostProcessor import PostProcessor Logger.log(r'Crysalis', r'Starting PostProcessor for {}'.format(file), 1) postprocessor = PostProcessor( file = file, settings = subscription ) postprocessor.run() Logger.tabs -= 1
def get_series(self, series_id: int): """ Query series information. Args: series_id (int): Unique ID of the series. Returns: dict: Series infomation. """ Logger.log(r'API', r'Querying series...') response = self.http.request('GET', self.base_url + 'series/' + str(series_id), headers={ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + self.jwt }) response_data = json.loads(response.data.decode('utf-8')) return response_data['data']
def login(self): """ Pass the user credentials to receive a JWT. """ Logger.log(r'API', r'Logging in...') request_data = { 'username': self.username, 'userkey': self.user_key, 'apikey': self.api_key, } encoded_data = json.dumps(request_data).encode('utf-8') response = self.http.request( 'POST', self.base_url + 'login', body=encoded_data, headers={'Content-Type': 'application/json'}) response_data = json.loads(response.data.decode('utf-8')) self.jwt = response_data['token']
def set_metadata(self): Logger.log(r'Plex', r'Setting metadata...', 1) self.parse_metadata_settings() episode = self.get_episode() if not episode: return -1 # Try to pull the metadata out of the file itself, first. self.try_file_metadata() # If we couldn't get the description from the file, try a .description file. if not self.metadata['summary']['found value'] and self.metadata[ 'summary']['wanted']: self.try_description_file() values = {} for item in self.metadata: if self.metadata[item]['found value']: Logger.log( r'Plex', r'Setting metadata: {} = {}...'.format( item, self.metadata[item]['found value'][:20])) values[item + '.value'] = self.metadata[item]['found value'] values[item + '.locked'] = 1 # Plex seems to have a race condition if you set values before it's # finished the library scan. If the scan finishes afterwards, it seems # like the values get reset. episode.section().cancelUpdate() time.sleep(10) episode.edit(**values) Logger.log(r'Plex', r'Metadata set.') Logger.tabs -= 1
def get_episode(self): destination_settings = self.postprocessor.settings.post_processing.destination section_name = destination_settings["section"] series_name = destination_settings[ "series"] or self.postprocessor.settings.name if not section_name: Logger.log(r'Plex', r'No "section" set!') return None if not series_name: Logger.log(r'Plex', r'No "series" set!') return None section = self.session.library.section(section_name) if not section: Logger.log(r'Plex', r'"{}" section not found!'.format(section_name)) return None # Make sure Plex knows about our new video. section.update() time.sleep(3) series = None tries = 0 # Grab the show. while not series and tries < 10: try: series = section.get(series_name) except exceptions.NotFound: # It can take plex a few seconds to # find the show if it's new. tries += 1 section.update() time.sleep(10 * tries) if not series: Logger.log(r'Plex', r'"{}" series not found!'.format(series_name)) return None episode = None tries = 0 while not episode and tries < 10: try: episodes = series.episodes() episodes = [episode for episode in episodes \ if pathlib.Path(episode.locations[0]).name == self.postprocessor.path_info.name] episode = episodes[0] except: # It can take plex a few seconds to # find the episode. tries += 1 section.update() time.sleep(10 * tries) if not episode: Logger.log('Plex', "Couldn't find episode!") raise Exception("Couldn't find episode!") return None return episode
def login(self): Logger.log(r'Plex', r'Logging in...', 1) self.session = PlexServer(self.url, self.token) Logger.tabs -= 1