Example #1
0
    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
Example #2
0
	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
Example #3
0
    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()
Example #4
0
    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
Example #5
0
    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))
Example #6
0
    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.')
Example #7
0
    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
Example #8
0
	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)
Example #9
0
    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.')
Example #10
0
    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
Example #11
0
	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
Example #12
0
    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']
Example #13
0
    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']
Example #14
0
    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
Example #15
0
    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
Example #16
0
    def login(self):
        Logger.log(r'Plex', r'Logging in...', 1)

        self.session = PlexServer(self.url, self.token)

        Logger.tabs -= 1