Example #1
0
    def id(self, source, key, identifier=None, resolve_mappings=True):
        # Retrieve mapping from database
        supported, match = self.map(
            source, key,
            identifier=identifier,
            resolve_mappings=resolve_mappings
        )

        if not supported:
            return False, (None, None)

        if not match or not match.valid:
            return True, (None, None)

        # Find valid identifier
        for id_service, id_key in match.identifiers.items():
            if id_service == source:
                continue

            # Strip media from identifier key
            id_service_parts = id_service.split(':', 1)

            if len(id_service_parts) == 2:
                id_service, _ = tuple(id_service_parts)

            if id_service in ['tvdb', 'tmdb', 'tvrage']:
                id_key = try_convert(id_key, int, id_key)

            return True, (id_service, id_key)

        log.info('[%s/%s] - Unable to find valid identifier in %r', source, key, match.identiifers)
        return True, (None, None)
Example #2
0
    def process_period(self, period, group):
        policy = BACKUP_RETENTION[period]

        # Retrieve options
        p_files = policy.get('files')

        if p_files is None:
            raise ValueError('Policy "%s" is missing the "files" attribute')

        # Build lists of revisions, grouped by period
        revisions_grouped = {}

        for base_path, dirs, files in os.walk(group.path):
            # Strip UNC prefix from `base_path`
            if base_path.startswith('\\\\?\\'):
                base_path = base_path[4:]

            # Ensure directory starts with a year
            rel_path = os.path.relpath(base_path, group.path)

            try:
                year = rel_path[:rel_path.index(os.path.sep)]
            except ValueError:
                year = rel_path

            if len(year) != 4 or try_convert(year, int) is None:
                continue

            # Search for revision metadata files
            for name in files:
                # Ensure file name matches the policy "files" filter
                if not fnmatch(name, p_files):
                    continue

                # Build path
                path = os.path.join(base_path, name)

                # Match revision metadata against regex pattern
                if not BACKUP_NAME_REGEX.match(name):
                    continue

                # Load metadata from file
                try:
                    revision = BackupRevision.load(path)
                except Exception, ex:
                    log.warn('Unable to load revision at %r: %s', path, ex, exc_info=True)
                    continue

                # Retrieve timestamp period
                key = self.timestamp_period(period, revision.timestamp)

                if key == self.timestamp_period(period, self.now):
                    # Backup occurred in the current period
                    continue

                # Store details in `revisions_grouped` dictionary
                if key not in revisions_grouped:
                    revisions_grouped[key] = []

                revisions_grouped[key].append(revision)
Example #3
0
    def _parse(cls, lines):
        processors = {}
        extra = {}

        # Parse lines into `processors` and `extra`
        section = None
        current = {}

        for line in lines:
            # Handle section break
            if line == '':
                # Store current attributes
                if section == 'processor':
                    num = try_convert(current.pop('processor', None), int)

                    if num is None:
                        num = len(processors)

                    processors[num] = current
                elif section == 'extra':
                    extra.update(current)
                elif current:
                    log.debug('Discarding unknown attributes: %r', current)

                # Reset state
                section = None
                current = {}

                # Continue with next line
                continue

            # Parse attribute from line
            parts = [part.strip() for part in line.split(':', 1)]

            if len(parts) < 2:
                log.debug('Unable to parse attribute from line: %r', line)
                continue

            # Retrieve attribute components
            key, value = parts[0], parts[1]

            if not key:
                log.debug('Invalid key returned for line: %r', line)
                continue

            # Transform `key`
            key = key.lower()
            key = key.replace(' ', '_')

            # Check for section-identifier
            if not section:
                if key == 'processor':
                    section = 'processor'
                else:
                    section = 'extra'

            # Store attribute in current dictionary
            current[key] = value

        # Store any leftover extra attributes
        if section == 'extra' and current:
            extra.update(current)

        # Return result
        return processors, extra
Example #4
0
    def _parse(cls, lines):
        processors = {}
        extra = {}

        # Parse lines into `processors` and `extra`
        section = None
        current = {}

        for line in lines:
            # Handle section break
            if line == '':
                # Store current attributes
                if section == 'processor':
                    num = try_convert(current.pop('processor', None), int)

                    if num is None:
                        num = len(processors)

                    processors[num] = current
                elif section == 'extra':
                    extra.update(current)
                elif current:
                    log.debug('Discarding unknown attributes: %r', current)

                # Reset state
                section = None
                current = {}

                # Continue with next line
                continue

            # Parse attribute from line
            parts = [part.strip() for part in line.split(':', 1)]

            if len(parts) < 2:
                log.debug('Unable to parse attribute from line: %r', line)
                continue

            # Retrieve attribute components
            key, value = parts[0], parts[1]

            if not key:
                log.debug('Invalid key returned for line: %r', line)
                continue

            # Transform `key`
            key = key.lower()
            key = key.replace(' ', '_')

            # Check for section-identifier
            if not section:
                if key == 'processor':
                    section = 'processor'
                else:
                    section = 'extra'

            # Store attribute in current dictionary
            current[key] = value

        # Store any leftover extra attributes
        if section == 'extra' and current:
            extra.update(current)

        # Return result
        return processors, extra
Example #5
0
    def _build_request(self, match, item, episode=None):
        if not match:
            log.warn('Invalid value provided for "match" parameter')
            return None

        if not item:
            log.warn('Invalid value provided for "item" parameter')
            return None

        # Retrieve identifier
        id_service = match.identifiers.keys()[0]
        id_key = try_convert(match.identifiers[id_service], int, match.identifiers[id_service])

        if type(id_key) not in [int, str]:
            log.info('Unsupported key: %r', id_key)
            return None

        # Determine media type
        if isinstance(match, MovieMatch):
            media = 'movie'
        elif isinstance(match, EpisodeMatch):
            media = 'show'
        else:
            log.warn('Unknown match: %r', match)
            return None

        # Strip media from identifier key
        id_service_parts = id_service.split(':', 1)

        if len(id_service_parts) == 2:
            id_service, id_media = tuple(id_service_parts)
        else:
            id_media = None

        if id_media and id_media != media:
            log.warn('Identifier mismatch, [%s: %r] doesn\'t match %r', id_service, id_key, media)
            return None

        # Build request
        request = {
            media: {
                'title': item.title,

                'ids': {
                    id_service: id_key
                }
            }
        }

        if item.year:
            request[media]['year'] = item.year
        elif episode and episode.year:
            request[media]['year'] = episode.year
        else:
            log.warn('Missing "year" parameter on %r', item)

        # Add episode parameters
        if isinstance(match, EpisodeMatch):
            if match.absolute_num is not None:
                log.info('Absolute mappings are not supported')
                return None

            if match.season_num is None or match.episode_num is None:
                log.warn('Missing season or episode number in %r', match)
                return None

            request['episode'] = {
                'season': match.season_num,
                'number': match.episode_num
            }

            if episode:
                request['episode']['title'] = episode.title

        return request
Example #6
0
    def map_items(self, key, store, media=None):
        # Retrieve key map
        if media is not None:
            keys = self.keys(media)
            table = self.table(media)

            if keys is None or table is None:
                log.debug('[%-38s] Collection has been ignored (unknown/unsupported media)', '/'.join(key))
                return
        else:
            keys = None
            table = None

        # Map each item in store
        log.debug('[%-38s] Building table from collection...', '/'.join(key))

        for pk, item in store.iteritems():
            # Trim `pk` season/episode values
            if len(pk) > 2:
                pk = tuple(pk[:2])

            if pk[0] not in GUID_SERVICES:
                log.info('Ignoring item %r with an unknown primary agent: %r', item, pk)
                continue

            # Detect media type from `item`
            if media is not None:
                i_media = media
                i_keys = keys
                i_table = table
            else:
                i_media = self.media(item)
                i_keys = self.keys(i_media)
                i_table = self.table(i_media)

            # Store `pk` in `keys
            if i_keys is not None:
                i_keys.add(pk)

            # Map `item.keys` -> `pk`
            for key in item.keys:
                # Expand `key`
                if type(key) is not tuple or len(key) != 2:
                    continue

                service, id = key

                # Check if agent is supported
                if service not in GUID_SERVICES:
                    continue

                # Cast service id to integer
                if service in ['tvdb', 'tmdb', 'tvrage']:
                    id = try_convert(id, int, id)

                # Store key in table
                key = (service, id)

                if key in i_table:
                    continue

                i_table[key] = pk

            # Map episodes in show
            if i_media == 'episodes':
                if type(item) is trakt_objects.Show:
                    if pk not in self.episode_keys:
                        self.episode_keys[pk] = set()

                    for identifier, _ in item.episodes():
                        self.episode_keys[pk].add(identifier)
                elif type(item) is trakt_objects.Episode:
                    # TODO
                    pass
                else:
                    log.debug('Unknown episode item: %r', item)

        # Task checkpoint
        self.task.checkpoint()
Example #7
0
class GuidParser(object):
    @classmethod
    def parse(cls, guid, episode=None):
        media = (GuidMatch.Media.Episode if episode else GuidMatch.Media.Movie)

        # Ensure guid is valid
        if not guid or not guid.valid:
            return GuidMatch(media, guid, invalid=True)

        # Process guid episode identifier overrides
        if episode and len(episode) == 2:
            season_num, episode_num = episode

            if guid.season is not None:
                episode = guid.season, episode_num

        # Process natively supported guid services
        if guid.service in GUID_SERVICES:
            episodes = None

            if episode and len(episode) == 2:
                episodes = [episode]

            return GuidMatch(media,
                             guid,
                             episodes=episodes,
                             supported=True,
                             found=True)

        # Process episode
        if episode:
            return cls.parse_episode(guid, episode)

        # Process shows + movies
        supported, (service,
                    key) = ModuleManager['mapper'].id(guid.service,
                                                      guid.id,
                                                      resolve_mappings=False)

        # Validate match
        if not supported:
            return GuidMatch(media, guid)

        if not service or not key:
            return GuidMatch(media, guid, supported=True)

        # Validate identifier
        if type(key) is list:
            log.info('[%s/%s] - List keys are not supported', guid.service,
                     guid.id)
            return GuidMatch(media, guid, supported=True)

        if type(key) not in [int, str]:
            log.info('[%s/%s] - Unsupported key: %r', guid.service, guid.id,
                     key)
            return GuidMatch(media, guid, supported=True)

        log.debug('[%s/%s] - Mapped to: %s/%s', guid.service, guid.id, service,
                  key)

        # Return movie/show match
        return GuidMatch(media,
                         Guid.construct(service, key, matched=True),
                         supported=True,
                         found=True)

    @classmethod
    def parse_episode(cls, guid, (season_num, episode_num)):
        episodes = [(season_num, episode_num)]

        # Map episode to a supported service (via OEM)
        supported, match = ModuleManager['mapper'].map_episode(
            guid, season_num, episode_num, resolve_mappings=False)

        # Validate match
        if not supported:
            return GuidMatch(GuidMatch.Media.Episode, guid, episodes=episodes)

        if not match or not match.identifiers:
            log.debug('Unable to find mapping for %r S%02dE%02d', guid,
                      season_num, episode_num)
            return GuidMatch(GuidMatch.Media.Episode,
                             guid,
                             episodes=episodes,
                             supported=True)

        # Retrieve identifier
        service = match.identifiers.keys()[0]
        key = match.identifiers[service]

        if type(key) is list:
            log.info('[%s/%s] - List keys are not supported', guid.service,
                     guid.id)
            return GuidMatch(GuidMatch.Media.Episode,
                             guid,
                             episodes=episodes,
                             supported=True)

        # Cast `key` numbers to integers
        key = try_convert(key, int, key)

        # Validate show identifier
        if type(key) not in [int, str]:
            log.info('[%s/%s] - Unsupported key: %r', guid.service, guid.id,
                     key)
            return GuidMatch(GuidMatch.Media.Episode,
                             guid,
                             episodes=episodes,
                             supported=True)

        # Process episode matches
        if isinstance(match, EpisodeMatch):
            # Ensure match doesn't include an absolute number
            if match.absolute_num is not None:
                log.info(
                    '[%s/%s] - Episode mappings with absolute numbers are not supported',
                    guid.service, guid.id)
                return GuidMatch(GuidMatch.Media.Episode,
                                 guid,
                                 episodes=episodes,
                                 supported=True)

            # Update `episodes` list
            if match.mappings:
                # Use match mappings
                episodes = []

                for mapping in match.mappings:
                    log.debug('[%s/%s] (S%02dE%02d) - Mapped to: %r',
                              guid.service, guid.id, season_num, episode_num,
                              mapping)
                    episodes.append((int(mapping.season), int(mapping.number)))
            else:
                # Use match identifier
                log.debug('[%s/%s] (S%02dE%02d) - Mapped to: %r', guid.service,
                          guid.id, season_num, episode_num, match)
                episodes = [(int(match.season_num), int(match.episode_num))]

            # Return episode match
            return GuidMatch(GuidMatch.Media.Episode,
                             Guid.construct(service, key, matched=True),
                             episodes=episodes,
                             supported=True,
                             found=True)

        # Process movie matches
        if isinstance(match, MovieMatch):
            log.debug('[%s/%s] (S%02dE%02d) - Mapped to: %r', guid.service,
                      guid.id, season_num, episode_num, match)

            # Return movie match
            return GuidMatch(GuidMatch.Media.Movie,
                             Guid.construct(service, key, matched=True),
                             supported=True,
                             found=True)

        # Unknown value for `match` returned
        log.warn('Unknown match returned: %r', match)
        return GuidMatch(GuidMatch.Media.Episode,
                         guid,
                         episodes=episodes,
                         supported=True)