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 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 #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 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 #8
0
    def __call__(self, **kwargs):
        """Start the clients in parallel"""

        [
            setattr(self, k, kwargs.pop(k)) for k in list(kwargs)
            if k in self.__dict__
        ]
        if len(kwargs):
            raise TypeError(
                "{} got one or more unexpected arguments {}".format(
                    "Clients.__call__", list(kwargs.keys())))

        if self.nclients == 0:
            return

        if not self.nclients:
            self.nclients = cpu_count()

        if not self.logger:
            self.logger = Logger(fmt=FMT_LONG,
                                 name='Clients',
                                 level=self.loglevel)

        self.logger.info("Starting {} Clients".format(self.nclients))

        opts = {
            k: self.__dict__[k]
            for k in ('server', 'password', 'port', 'timeout', 'loglevel')
        }

        self.clients = []

        try:
            for i in range(self.nclients):

                client = Client(**opts)
                client.start()
                self.clients.append(client)

        except:
            exc_info = sys.exc_info()
            self.logger.exception("{}".format(exc_info[1]))
            return 1

        return 0
Example #9
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 #10
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 #11
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 #12
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 #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 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 #15
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 #16
0
    def __call__(self, **kwargs):
        """Run the server

        While parameters may be passed to this method, the intention is that
        the constructor will be used to create a closure setting the arguments.
        This provides a means for the user to adjust the parameters prior to 
        calling the created object to run the server.
        """

        # Process arguments

        [
            setattr(self, k, kwargs.pop(k)) for k in list(kwargs)
            if k in self.__dict__
        ]
        if len(kwargs):
            raise TypeError(
                "{} got one or more unexpected arguments {}".format(
                    "Clients.__call__", list(kwargs.keys())))

        if type(self.password) == str:
            self.password = self.password.encode("utf-8")

        # Setup logging

        if not self.logger:
            self.logger = Logger(fmt=FMT_LONG,
                                 name='Server',
                                 level=self.loglevel)

        # Save server pid

        if 'MAPREDUCE_PID_FILE' in os.environ:
            with open(os.environ['MAPREDUCE_PID_FILE'], 'w') as pf:
                print(os.getpid(), file=pf)

        # Setup signal handling

        def SIGHANDLER(sig, frame):
            "Handler for signal, just sets flag to close processing"
            self.logger.warning(
                "Signal {} detected, waiting for clients to finish".format(
                    sig))
            self.fail = True

        signal.signal(STOP_SIGNAL, SIGHANDLER)

        # Start Clients

        if self.nclients != 0:
            c = Clients(nclients=self.nclients,
                        password=self.password,
                        port=self.port,
                        logger=self.logger,
                        loglevel=self.loglevel)
            if c():
                return 1

        # Start Server

        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.bind(("", self.port))
        self.listen(5)

        try:
            asyncore.loop(map=self.socket_map)
        except:
            self.close()
            raise

        # Close any remaining clients
        if self.nclients != 0:
            c.terminate()

        if self.fail:
            self.logger.warning("Server ending abnormally")
            raise MapReduceError("Server ending abnormally")

        return self.taskmanager.results
Example #17
0
                   help='Number of clients to run',
                   type=int,
                   default=None)

    loggerValues = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']

    p.add_argument('-l',
                   '--loglevel',
                   help='Logger level (debug, info, warning, error, critical)',
                   type=lambda x: x.upper(),
                   choices=loggerValues,
                   default='info')

    opt = p.parse_args()

    log = Logger(level=opt.loglevel, fmt=FMT_LONG, name='Clients')

    log.info("Starting {}".format(sys.argv[0]))

    # Show options

    log.info(__doc__.splitlines()[1])
    log.info("Options specified:")
    for o in sorted(opt.__dict__.keys()):
        log.info("{:12s} = {}".format(o, opt.__dict__[o]))

    # Run the clients
    clients = Clients(logger=log, **vars(opt))

    rc = clients()
    if not rc:
Example #18
0
class Clients(object):
    """Run a set of clients as separate processes:

    nclients is specified:
    None : Run the number of clients sized to the number of cores (default)
    0    : Run none
    >0   : Run the pool of processes sized as specified
    """
    def __init__(self,
                 nclients=None,
                 server=None,
                 password=None,
                 port=None,
                 timeout=None,
                 logger=None,
                 loglevel=None):

        self.server = server if server else DEFAULT_HOST
        self.port = port if port else DEFAULT_PORT
        self.password = password if password else DEFAULT_PASSWORD
        self.timeout = timeout if timeout else DEFAULT_TIMEOUT
        self.nclients = nclients
        self.logger = logger if logger else logging.getLogger()
        self.loglevel = loglevel if loglevel else DEFAULT_LOG_LEVEL

    def __call__(self, **kwargs):
        """Start the clients in parallel"""

        [
            setattr(self, k, kwargs.pop(k)) for k in list(kwargs)
            if k in self.__dict__
        ]
        if len(kwargs):
            raise TypeError(
                "{} got one or more unexpected arguments {}".format(
                    "Clients.__call__", list(kwargs.keys())))

        if self.nclients == 0:
            return

        if not self.nclients:
            self.nclients = cpu_count()

        if not self.logger:
            self.logger = Logger(fmt=FMT_LONG,
                                 name='Clients',
                                 level=self.loglevel)

        self.logger.info("Starting {} Clients".format(self.nclients))

        opts = {
            k: self.__dict__[k]
            for k in ('server', 'password', 'port', 'timeout', 'loglevel')
        }

        self.clients = []

        try:
            for i in range(self.nclients):

                client = Client(**opts)
                client.start()
                self.clients.append(client)

        except:
            exc_info = sys.exc_info()
            self.logger.exception("{}".format(exc_info[1]))
            return 1

        return 0

    def terminate(self):
        """Forceably terminate the clients"""
        [c.terminate for c in self.clients if c.is_alive()]

    def join(self):
        """Wait for client completion"""
        self.logger.debug("Waiting for clients to complete")

        try:
            for c in self.clients:
                c.join()
                self.logger.debug("Client ended with rc {}", c.exitcode)
        except:
            exc_info = sys.exc_info()
            self.logger.exception("{}".format(exc_info[1]))

        self.logger.info("All Clients Completed")
Example #19
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 #20
0
    def login(self):
        Logger.log(r'Plex', r'Logging in...', 1)

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

        Logger.tabs -= 1
Example #21
0
class Server(asyncore.dispatcher, object):
    """Run the map/reduce server

    It is recommended that all the appropriate parameters be passed to
    the constructor.
    If nclients is not specified a set of client prcesses will be started
    equal to the number of CPU's available on the local machine.
    """
    def __init__(self,
                 datasource,
                 mapfn,
                 reducefn,
                 collectfn=None,
                 port=None,
                 password=None,
                 logger=None,
                 loglevel=None,
                 nclients=None):

        self.port = port if port else DEFAULT_PORT
        self.password = password if password else DEFAULT_PASSWORD
        self.logger = logger
        self.nclients = nclients
        self.loglevel = loglevel if loglevel else DEFAULT_LOG_LEVEL

        self.socket_map = {}
        asyncore.dispatcher.__init__(self, map=self.socket_map)

        self.mapfn = mapfn
        self.reducefn = reducefn
        self.collectfn = collectfn
        self.datasource = datasource
        self._fail = False

    def __call__(self, **kwargs):
        """Run the server

        While parameters may be passed to this method, the intention is that
        the constructor will be used to create a closure setting the arguments.
        This provides a means for the user to adjust the parameters prior to 
        calling the created object to run the server.
        """

        # Process arguments

        [
            setattr(self, k, kwargs.pop(k)) for k in list(kwargs)
            if k in self.__dict__
        ]
        if len(kwargs):
            raise TypeError(
                "{} got one or more unexpected arguments {}".format(
                    "Clients.__call__", list(kwargs.keys())))

        if type(self.password) == str:
            self.password = self.password.encode("utf-8")

        # Setup logging

        if not self.logger:
            self.logger = Logger(fmt=FMT_LONG,
                                 name='Server',
                                 level=self.loglevel)

        # Save server pid

        if 'MAPREDUCE_PID_FILE' in os.environ:
            with open(os.environ['MAPREDUCE_PID_FILE'], 'w') as pf:
                print(os.getpid(), file=pf)

        # Setup signal handling

        def SIGHANDLER(sig, frame):
            "Handler for signal, just sets flag to close processing"
            self.logger.warning(
                "Signal {} detected, waiting for clients to finish".format(
                    sig))
            self.fail = True

        signal.signal(STOP_SIGNAL, SIGHANDLER)

        # Start Clients

        if self.nclients != 0:
            c = Clients(nclients=self.nclients,
                        password=self.password,
                        port=self.port,
                        logger=self.logger,
                        loglevel=self.loglevel)
            if c():
                return 1

        # Start Server

        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.bind(("", self.port))
        self.listen(5)

        try:
            asyncore.loop(map=self.socket_map)
        except:
            self.close()
            raise

        # Close any remaining clients
        if self.nclients != 0:
            c.terminate()

        if self.fail:
            self.logger.warning("Server ending abnormally")
            raise MapReduceError("Server ending abnormally")

        return self.taskmanager.results

    _clientId = 0

    def handle_accepted(self, conn, addr):
        sc = ServerChannel(conn, addr, self.socket_map, Server._clientId, self)
        Server._clientId += 1

    def handle_close(self):
        self.close()

    def set_datasource(self, ds):
        self._datasource = ds
        self.taskmanager = TaskManager(self._datasource, self)

    def get_datasource(self):
        return self._datasource

    datasource = property(get_datasource, set_datasource)

    def get_fail(self):
        return self._fail

    def set_fail(self, v):
        self._fail = v
        if v:
            self.taskmanager.state = TaskManager.FINISHED

    fail = property(get_fail, set_fail)