Пример #1
0
    def patch(self, series_slug, episode_slug=None, path_param=None):
        """Patch episode."""
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        data = json_decode(self.request.body)

        # Multi-patch request
        if not episode_slug:
            return self._patch_multi(series, data)

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._bad_request('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        accepted = self._patch_episode(episode, data)

        return self._ok(data=accepted)
Пример #2
0
    def delete(self, series_slug, episode_slug, **kwargs):
        """Delete the episode."""
        if not series_slug:
            return self._method_not_allowed(
                'Deleting multiple series are not allowed')

        identifier = SeriesIdentifier.from_slug(series_slug)
        if not identifier:
            return self._bad_request('Invalid series identifier')

        series = Series.find_by_identifier(identifier)
        if not series:
            return self._not_found('Series not found')

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._bad_request('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        try:
            episode.delete_episode()
        except EpisodeDeletedException:
            return self._no_content()
        else:
            return self._conflict('Unable to delete episode')
Пример #3
0
    def http_delete(self, series_slug, episode_slug, **kwargs):
        """Delete the episode."""
        if not series_slug:
            return self._method_not_allowed('Deleting multiple series are not allowed')

        identifier = SeriesIdentifier.from_slug(series_slug)
        if not identifier:
            return self._bad_request('Invalid series identifier')

        series = Series.find_by_identifier(identifier)
        if not series:
            return self._not_found('Series not found')

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._bad_request('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        try:
            episode.delete_episode()
        except EpisodeDeletedException:
            return self._no_content()
        else:
            return self._conflict('Unable to delete episode')
Пример #4
0
    def http_patch(self, series_slug, episode_slug=None, path_param=None):
        """Patch episode."""
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        data = json_decode(self.request.body)

        # Multi-patch request
        if not episode_slug:
            return self._patch_multi(series, data)

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._bad_request('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        accepted = self._patch_episode(episode, data)

        return self._ok(data=accepted)
Пример #5
0
    def resource_search_missing_subtitles(self):
        """
        Search missing subtitles for multiple episodes at once.

        example: Pass the following structure:
            language: 'all', // Or a three letter language code.
            shows: [
                {
                    'slug': 'tvdb1234',
                    'episodes': ['s01e01', 's02e03', 's10e10']
                },
            ]
        """
        data = json_decode(self.request.body)
        language = data.get('language', 'all')
        shows = data.get('shows', [])

        if language != 'all' and language not in subtitle_code_filter():
            return self._bad_request(
                'You need to provide a valid subtitle code')

        for show in shows:
            # Loop through the shows. Each show should have an array of episode slugs
            series_identifier = SeriesIdentifier.from_slug(show.get('slug'))
            if not series_identifier:
                log.warning(
                    'Could not create a show identifier with slug {slug}',
                    {'slug': show.get('slug')})
                continue

            series = Series.find_by_identifier(series_identifier)
            if not series:
                log.warning(
                    'Could not match to a show in the library with slug {slug}',
                    {'slug': show.get('slug')})
                continue

            for episode_slug in show.get('episodes', []):
                episode_number = EpisodeNumber.from_slug(episode_slug)
                if not episode_number:
                    log.warning('Bad episode number from slug {slug}',
                                {'slug': episode_slug})
                    continue

                episode = Episode.find_by_series_and_episode(
                    series, episode_number)
                if not episode:
                    log.warning('Episode not found with slug {slug}',
                                {'slug': episode_slug})

                episode.download_subtitles(
                    lang=language if language != 'all' else None)

        return self._ok()
Пример #6
0
    def _search_failed(self, data):
        """Queue a failed search.

        :param data:
        :return:
        """
        statuses = {}

        if not data.get('showSlug'):
            return self._bad_request(
                'For a failed search you need to provide a show slug')

        if not data.get('episodes'):
            return self._bad_request(
                'For a failed search you need to provide a list of episodes')

        identifier = SeriesIdentifier.from_slug(data['showSlug'])
        if not identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(identifier)
        if not series:
            return self._not_found('Series not found')

        season_segments = defaultdict(list)
        for episode_slug in data['episodes']:
            episode_number = EpisodeNumber.from_slug(episode_slug)
            if not episode_number:
                statuses[episode_slug] = {'status': 400}
                continue

            episode = Episode.find_by_series_and_episode(
                series, episode_number)
            if not episode:
                statuses[episode_slug] = {'status': 404}
                continue

            season_segments[episode.season].append(episode)

        if not season_segments:
            return self._not_found(
                'Could not find any episode for show {show}. Did you provide the correct format?'
                .format(show=series.name))

        for segment in itervalues(season_segments):
            cur_failed_queue_item = FailedQueueItem(series, segment)
            app.forced_search_queue_scheduler.action.add_item(
                cur_failed_queue_item)

        return self._accepted('Failed search for {0} started'.format(
            data['showSlug']))
Пример #7
0
def test_episode_identifier(p):
    # Given
    slug = p['slug']
    expected = p['expected']

    # When
    actual = EpisodeNumber.from_slug(slug)

    # Then
    if expected is None:
        assert not actual
    else:
        assert actual
        assert expected == actual
        assert slug != actual
Пример #8
0
def test_episode_identifier(p):
    # Given
    slug = p['slug']
    expected = p['expected']

    # When
    actual = EpisodeNumber.from_slug(slug)

    # Then
    if expected is None:
        assert not actual
    else:
        assert actual
        assert expected == actual
        assert slug != actual
Пример #9
0
    def get(self, series_slug, episode_slug, path_param):
        """Query episode information.

        :param series_slug: series slug. E.g.: tvdb1234
        :param episode_number:
        :param path_param:
        """
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        if not episode_slug:
            detailed = self._parse_boolean(
                self.get_argument('detailed', default=False))
            season = self._parse(self.get_argument('season', None), int)
            data = [
                e.to_json(detailed=detailed)
                for e in series.get_all_episodes(season=season)
            ]
            return self._paginate(data, sort='airDate')

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._bad_request('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        detailed = self._parse_boolean(
            self.get_argument('detailed', default=True))
        data = episode.to_json(detailed=detailed)
        if path_param:
            if path_param == 'metadata':
                data = episode.metadata() if episode.is_location_valid(
                ) else {}
            elif path_param in data:
                data = data[path_param]
            else:
                return self._bad_request(
                    "Invalid path parameter '{0}'".format(path_param))

        return self._ok(data=data)
Пример #10
0
    def _get_episode_segments(series, data):
        """
        Create a dict with season number keys and their corresponding episodes as an array of Episode objects.

        The episode objects are created from the "episodes" property passed as json data.
        """
        episode_segments = defaultdict(list)
        if data.get('episodes'):
            for episode_slug in data['episodes']:
                episode_number = EpisodeNumber.from_slug(episode_slug)
                if not episode_number:
                    continue

                episode = Episode.find_by_series_and_episode(series, episode_number)
                if not episode:
                    continue

                episode_segments[episode.season].append(episode)
        return episode_segments
Пример #11
0
    def _patch_multi(self, series, request_data):
        """Patch multiple episodes."""
        statuses = {}

        for slug, data in iteritems(request_data):
            episode_number = EpisodeNumber.from_slug(slug)
            if not episode_number:
                statuses[slug] = {'status': 400}
                continue

            episode = Episode.find_by_series_and_episode(series, episode_number)
            if not episode:
                statuses[slug] = {'status': 404}
                continue

            self._patch_episode(episode, data)

            statuses[slug] = {'status': 200}

        return self._multi_status(data=statuses)
Пример #12
0
    def _patch_multi(self, series, request_data):
        """Patch multiple episodes."""
        statuses = {}

        for slug, data in iteritems(request_data):
            episode_number = EpisodeNumber.from_slug(slug)
            if not episode_number:
                statuses[slug] = {'status': 400}
                continue

            episode = Episode.find_by_series_and_episode(series, episode_number)
            if not episode:
                statuses[slug] = {'status': 404}
                continue

            self._patch_episode(episode, data)

            statuses[slug] = {'status': 200}

        return self._multi_status(data=statuses)
Пример #13
0
    def http_get(self, series_slug, episode_slug, path_param):
        """Query episode information.

        :param series_slug: series slug. E.g.: tvdb1234
        :param episode_number:
        :param path_param:
        """
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        if not episode_slug:
            detailed = self._parse_boolean(self.get_argument('detailed', default=False))
            season = self._parse(self.get_argument('season', None), int)
            data = [e.to_json(detailed=detailed) for e in series.get_all_episodes(season=season)]
            return self._paginate(data, sort='airDate')

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._bad_request('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        detailed = self._parse_boolean(self.get_argument('detailed', default=True))
        data = episode.to_json(detailed=detailed)
        if path_param:
            if path_param == 'metadata':
                data = episode.metadata() if episode.is_location_valid() else {}
            elif path_param in data:
                data = data[path_param]
            else:
                return self._bad_request("Invalid path parameter '{0}'".format(path_param))

        return self._ok(data=data)
Пример #14
0
    def _get_season_segments(series, data):
        """
        Create a dict with season number keys and their corresponding episodes as an array of Episode objects.

        The episode objects are created from the "season" property passed as json data.
        """
        season_segments = defaultdict(list)
        if data.get('season'):
            for season_slug in data['season']:
                # For season packs we still need to provide an episode object. So we choose to provide the first
                episode_slug = '{season}e01'.format(season=season_slug)
                episode_number = EpisodeNumber.from_slug(episode_slug)

                if not episode_number:
                    continue

                episode = Episode.find_by_series_and_episode(series, episode_number)
                if not episode:
                    continue

                season_segments[episode.season].append(episode)
        return season_segments
Пример #15
0
    def get(self, series_slug, episode_slug, path_param):
        """Query episode's history information.

        :param series_slug: series slug. E.g.: tvdb1234
        :param episode_slug: episode slug. E.g.: s01e01
        :param path_param:
        """
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        if not episode_slug:
            return self._bad_request('Invalid episode slug')

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._not_found('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        sql_base = """
            SELECT rowid, date, action, quality,
                   provider, version, resource, size, proper_tags,
                   indexer_id, showid, season, episode, manually_searched, info_hash
            FROM history
            WHERE showid = ? AND indexer_id = ? AND season = ? AND episode = ?
        """

        params = [
            series.series_id, series.indexer, episode.season, episode.episode
        ]

        sql_base += ' ORDER BY date DESC'
        results = db.DBConnection().select(sql_base, params)

        def data_generator():
            """Read history data and normalize key/value pairs."""
            for item in results:
                provider = {}
                release_group = None
                release_name = None
                file_name = None
                subtitle_language = None

                if item['action'] in (SNATCHED, FAILED):
                    provider.update({
                        'id':
                        GenericProvider.make_id(item['provider']),
                        'name':
                        item['provider']
                    })
                    release_name = item['resource']

                if item['action'] == DOWNLOADED:
                    release_group = item['provider']
                    file_name = item['resource']

                if item['action'] == SUBTITLED:
                    subtitle_language = item['resource']
                    provider.update({
                        'id': item['provider'],
                        'name': item['provider']
                    })

                if item['action'] == SUBTITLED:
                    subtitle_language = item['resource']

                yield {
                    'id':
                    item['rowid'],
                    'series':
                    SeriesIdentifier.from_id(item['indexer_id'],
                                             item['showid']).slug,
                    'status':
                    item['action'],
                    'statusName':
                    statusStrings.get(item['action']),
                    'actionDate':
                    item['date'],
                    'quality':
                    item['quality'],
                    'resource':
                    basename(item['resource']),
                    'size':
                    item['size'],
                    'properTags':
                    item['proper_tags'],
                    'season':
                    item['season'],
                    'episode':
                    item['episode'],
                    'manuallySearched':
                    bool(item['manually_searched']),
                    'infoHash':
                    item['info_hash'],
                    'provider':
                    provider,
                    'release_name':
                    release_name,
                    'releaseGroup':
                    release_group,
                    'fileName':
                    file_name,
                    'subtitleLanguage':
                    subtitle_language
                }

        if not results:
            return self._not_found(
                'History data not found for show {show} and episode {episode}'.
                format(show=series.identifier.slug, episode=episode.slug))

        return self._ok(data=list(data_generator()))
Пример #16
0
    def get(self, series_slug, episode_slug, path_param):
        """Query episode's history information.

        :param series_slug: series slug. E.g.: tvdb1234
        :param episode_slug: episode slug. E.g.: s01e01
        :param path_param:
        """
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        if not episode_slug:
            return self._bad_request('Invalid episode slug')

        episode_number = EpisodeNumber.from_slug(episode_slug)
        if not episode_number:
            return self._not_found('Invalid episode number')

        episode = Episode.find_by_series_and_episode(series, episode_number)
        if not episode:
            return self._not_found('Episode not found')

        sql_base = """
            SELECT rowid, date, action, quality,
                   provider, version, resource, size, proper_tags,
                   indexer_id, showid, season, episode, manually_searched
            FROM history
            WHERE showid = ? AND indexer_id = ? AND season = ? AND episode = ?
        """

        params = [
            series.series_id, series.indexer, episode.season, episode.episode
        ]

        sql_base += ' ORDER BY date DESC'
        results = db.DBConnection().select(sql_base, params)

        def data_generator():
            """Read history data and normalize key/value pairs."""
            for item in results:
                d = {}
                d['id'] = item['rowid']
                d['series'] = SeriesIdentifier.from_id(item['indexer_id'],
                                                       item['showid']).slug
                d['status'] = item['action']
                d['actionDate'] = item['date']

                d['resource'] = basename(item['resource'])
                d['size'] = item['size']
                d['properTags'] = item['proper_tags']
                d['statusName'] = statusStrings.get(item['action'])
                d['season'] = item['season']
                d['episode'] = item['episode']
                d['manuallySearched'] = bool(item['manually_searched'])
                d['provider'] = item['provider']

                yield d

        if not results:
            return self._not_found(
                'History data not found for show {show} and episode {episode}'.
                format(show=series.identifier.slug, episode=episode.slug))

        return self._ok(data=list(data_generator()))
Пример #17
0
    def resource_update_episode_status(self):
        """
        Mass update episodes statuses for multiple shows at once.

        example: Pass the following structure:
            status: 3,
            shows: [
                {
                    'slug': 'tvdb1234',
                    'episodes': ['s01e01', 's02e03', 's10e10']
                },
            ]
        """
        data = json_decode(self.request.body)
        status = data.get('status')
        shows = data.get('shows', [])

        if status not in statusStrings:
            return self._bad_request('You need to provide a valid status code')

        ep_sql_l = []
        for show in shows:
            # Loop through the shows. Each show should have an array of episode slugs
            series_identifier = SeriesIdentifier.from_slug(show.get('slug'))
            if not series_identifier:
                log.warning(
                    'Could not create a show identifier with slug {slug}',
                    {'slug': show.get('slug')})
                continue

            series = Series.find_by_identifier(series_identifier)
            if not series:
                log.warning(
                    'Could not match to a show in the library with slug {slug}',
                    {'slug': show.get('slug')})
                continue

            episodes = []
            for episode_slug in show.get('episodes', []):
                episode_number = EpisodeNumber.from_slug(episode_slug)
                if not episode_number:
                    log.warning('Bad episode number from slug {slug}',
                                {'slug': episode_slug})
                    continue

                episode = Episode.find_by_series_and_episode(
                    series, episode_number)
                if not episode:
                    log.warning('Episode not found with slug {slug}',
                                {'slug': episode_slug})

                ep_sql = episode.mass_update_episode_status(status)
                if ep_sql:
                    ep_sql_l.append(ep_sql)

                    # Keep an array of episodes for the trakt sync
                    episodes.append(episode)

            if episodes:
                series.sync_trakt_episodes(status, episodes)

        if ep_sql_l:
            main_db_con = db.DBConnection()
            main_db_con.mass_action(ep_sql_l)

        return self._ok(data={'count': len(ep_sql_l)})
Пример #18
0
    def post(self, series_slug):
        """Query series information.

        :param series_slug: series slug. E.g.: tvdb1234
        """
        series_identifier = SeriesIdentifier.from_slug(series_slug)
        if not series_identifier:
            return self._bad_request('Invalid series slug')

        series = Series.find_by_identifier(series_identifier)
        if not series:
            return self._not_found('Series not found')

        data = json_decode(self.request.body)
        if not data or not all([data.get('type')]):
            return self._bad_request('Invalid request body')

        if data['type'] == 'ARCHIVE_EPISODES':
            if series.set_all_episodes_archived(final_status_only=True):
                return self._created()
            return self._no_content()

        if data['type'] == 'TEST_RENAME':
            try:
                series.validate_location  # @UnusedVariable
            except ShowDirectoryNotFoundException:
                return self._bad_request(
                    "Can't rename episodes when the show dir is missing.")

            ep_obj_list = series.get_all_episodes(has_location=True)
            ep_obj_list = [x for x in ep_obj_list if x.location]
            ep_obj_rename_list = []
            for ep_obj in ep_obj_list:
                has_already = False
                for check in ep_obj.related_episodes + [ep_obj]:
                    if check in ep_obj_rename_list:
                        has_already = True
                        break
                if not has_already:
                    ep_obj_rename_list.append(ep_obj)

            if ep_obj_rename_list:
                ep_obj_rename_list.reverse()
            return self._ok(data=[{
                **ep_obj.to_json(detailed=True),
                **{
                    'selected': False
                }
            } for ep_obj in ep_obj_rename_list])

        if data['type'] == 'RENAME_EPISODES':
            episodes = data.get('episodes', [])
            if not episodes:
                return self._bad_request(
                    'You must provide at least one episode')

            try:
                series.validate_location  # @UnusedVariable
            except ShowDirectoryNotFoundException:
                return self._bad_request(
                    "Can't rename episodes when the show dir is missing.")

            main_db_con = db.DBConnection()
            for episode_slug in episodes:
                episode_number = EpisodeNumber.from_slug(episode_slug)
                if not episode_number:
                    continue

                episode = Episode.find_by_series_and_episode(
                    series, episode_number)
                if not episode:
                    continue

                # this is probably the worst possible way to deal with double eps
                # but I've kinda painted myself into a corner here with this stupid database
                ep_result = main_db_con.select(
                    'SELECT location '
                    'FROM tv_episodes '
                    'WHERE indexer = ? AND showid = ? AND season = ? AND episode = ? AND 5=5',
                    [
                        series.indexer, series.series_id, episode.season,
                        episode.episode
                    ])

                if not ep_result:
                    log.warning(
                        'Unable to find an episode for {episode}, skipping',
                        {'episode': episode})
                    continue

                related_eps_result = main_db_con.select(
                    'SELECT season, episode '
                    'FROM tv_episodes '
                    'WHERE location = ? AND episode != ?',
                    [ep_result[0]['location'], episode.episode])

                root_ep_obj = episode
                root_ep_obj.related_episodes = []

                for cur_related_ep in related_eps_result:
                    related_ep_obj = series.get_episode(
                        cur_related_ep['season'], cur_related_ep['episode'])
                    if related_ep_obj not in root_ep_obj.related_episodes:
                        root_ep_obj.related_episodes.append(related_ep_obj)

                root_ep_obj.rename()
            return self._created()

        # This might also be moved to /notifications/kodi/update?showslug=..
        if data['type'] == 'UPDATE_KODI':
            series_name = quote_plus(series.name.encode('utf-8'))

            if app.KODI_UPDATE_ONLYFIRST:
                host = app.KODI_HOST[0].strip()
            else:
                host = ', '.join(app.KODI_HOST)

            if notifiers.kodi_notifier.update_library(series_name=series_name):
                ui.notifications.message(
                    f'Library update command sent to KODI host(s): {host}')
            else:
                ui.notifications.error(
                    f'Unable to contact one or more KODI host(s): {host}')

            return self._created()

        return self._bad_request('Invalid operation')