Exemple #1
0
    def __init__(self, media_tweet: TweetCache, trigger: str = TRIGGER_SELF):
        self._log = logging.getLogger(__name__)
        self._trigger = trigger
        self.tweet_cache = media_tweet
        self.media = TweetManager.extract_media(media_tweet.tweet) or []
        self._downloads_enabled = config.getboolean('SauceNao', 'download_files', fallback=False)
        self._previews_enabled = config.getboolean('TraceMoe', 'enabled', fallback=False)

        # SauceNao
        self.minsim_mentioned = float(config.get('SauceNao', 'min_similarity_mentioned', fallback=50.0))
        self.minsim_monitored = float(config.get('SauceNao', 'min_similarity_monitored', fallback=65.0))
        self.minsim_searching = float(config.get('SauceNao', 'min_similarity_searching', fallback=70.0))
        self.persistent = config.getboolean('Twitter', 'enable_persistence', fallback=False)
        self.anime_link = config.get('SauceNao', 'source_link', fallback='anidb').lower()
        self.sauce = SauceNao(
                api_key=config.get('SauceNao', 'api_key', fallback=None),
                min_similarity=min(self.minsim_mentioned, self.minsim_monitored, self.minsim_searching),
                priority=[21, 22, 5, 37, 25]
        )

        # Twython
        self.twython = Twython(config.get('Twitter', 'consumer_key'), config.get('Twitter', 'consumer_secret'),
                               config.get('Twitter', 'access_token'), config.get('Twitter', 'access_secret'))

        self._sauce_cache = {}
Exemple #2
0
    def _determine_requested_index(self, tweet, media_cache: TweetSauceCache) -> int:
        """
        Determined the requested sauce lookup for multi-image tweets
        """
        media = TweetManager.extract_media(media_cache.tweet)
        request_text = tweet.full_text.lower().strip()

        # If there's only one item, that's all we can return
        if len(media) == 1:
            return 0

        # Right / Left image parsing
        if len(media) == 2:
            if 'right' in request_text:
                self.log.debug("User requested the right image")
                return 1

            if 'left' in request_text:
                self.log.debug("User requested the left image")
                return 0

        if len(media) == 4:
            if 'top left' in request_text:
                self.log.debug("User requested the top left image")
                return 0
            if 'top right' in request_text:
                self.log.debug("User requested the top right image")
                return 1
            if 'bottom left' in request_text:
                self.log.debug("User requested the bottom left image")
                return 2
            if 'bottom right' in request_text:
                self.log.debug("User requested the bottom right image")
                return 3

        # First / last image parsing
        if 'first' in request_text:
            self.log.debug("User requested the first image")
            return 0
        if 'last' in request_text:
            self.log.debug("User requested the last image")
            return len(media) - 1

        # Otherwise, try parsing specific ordinals
        if request_text[-2:] == ' 1':
            self.log.debug("User explicitly requested index 0")
            return 0
        if (request_text[-2:] == ' 2' or 'second' in request_text) and len(media) >= 2:
            self.log.debug("User explicitly requested index 1")
            return 1
        if (request_text[-2:] == ' 3' or 'third' in request_text) and len(media) >= 3:
            self.log.debug("User explicitly requested index 2")
            return 2
        if (request_text[-2:] == ' 4' or 'fourth' in request_text) and len(media) == 4:
            self.log.debug("User explicitly requested index 3")
            return 3

        return 0
    async def _get_sauce(self, index: int) -> typing.Optional[TweetSauceCache]:
        cache = TweetSauceCache.fetch(self.tweet_cache.tweet_id, index)
        if cache:
            return cache

        media = TweetManager.extract_media(self.tweet_cache.tweet)[index]

        file = media
        if self._downloads_enabled:
            file = await self._download_media(media)

        if self._downloads_enabled:
            sauce_results = await self.sauce.from_file(io.BytesIO(file))
            self._log.info(f"Performing saucenao lookup via file upload")
        else:
            self._log.info(f"Performing saucenao lookup via URL {file}")
            sauce_results = await self.sauce.from_url(file)

        # No results?
        if not sauce_results:
            sauce_cache = TweetSauceCache.set(self.tweet_cache, sauce_results,
                                              index, self._trigger)
            return sauce_cache

        best_result = sauce_results[0]

        # Attempt to download a video preview, if it's an anime result
        video_preview = None
        if self._previews_enabled and isinstance(best_result, AnimeSource):
            file = io.BytesIO(file) if self._downloads_enabled else file
            is_url = not self._downloads_enabled
            video_preview = await self._video_preview(best_result, file,
                                                      is_url)

        # If we have a video preview, upload it now!
        media_id = None
        if video_preview:
            video_preview = io.BytesIO(video_preview)
            media_id = await self._upload_video(video_preview)

        return TweetSauceCache.set(self.tweet_cache, sauce_results, index,
                                   self._trigger, media_id)
Exemple #4
0
    async def send_reply(self,
                         tweet_cache: TweetCache,
                         media_cache: TweetCache,
                         sauce_cache: TweetSauceCache,
                         requested: bool = True,
                         blocked: bool = False) -> None:
        """
        Return the source of the image
        Args:
            tweet_cache (TweetCache): The tweet to reply to
            media_cache (TweetCache): The tweet containing media elements
            sauce_cache (Optional[GenericSource]): The sauce found (or None if nothing was found)
            requested (bool): True if the lookup was requested, or False if this is a monitored user account
            blocked (bool): If True, the account posting this has blocked the SauceBot

        Returns:
            None
        """
        tweet = tweet_cache.tweet
        sauce = sauce_cache.sauce

        if sauce and self.ignored_indexes and (int(sauce.index_id)
                                               in self.ignored_indexes):
            self.log.info(
                f"Ignoring result from ignored index ID {sauce.index_id}")
            sauce = None

        if sauce is None:
            if self.failed_responses and requested:
                media = TweetManager.extract_media(media_cache.tweet)
                if not media:
                    return

                yandex_url = f"https://yandex.com/images/search?url={media[sauce_cache.index_no]}&rpt=imageview"
                ascii_url = f"https://ascii2d.net/search/url/{media[sauce_cache.index_no]}"
                google_url = f"https://www.google.com/searchbyimage?image_url={media[sauce_cache.index_no]}&safe=off"

                message = lang('Errors',
                               'no_results', {
                                   'yandex_url': yandex_url,
                                   'ascii_url': ascii_url,
                                   'google_url': google_url
                               },
                               user=tweet.author)
                self._post(msg=message, to=tweet.id)
            return

        # Get the artists Twitter handle if possible
        twitter_sauce = None
        if isinstance(sauce, PixivSource):
            twitter_sauce = self.pixiv.get_author_twitter(
                sauce.data['member_id'])

        # If we're requesting sauce from the original artist, just say so
        if twitter_sauce and twitter_sauce.lstrip(
                '@').lower() == media_cache.tweet.author.screen_name.lower():
            self.log.info(
                "User requested sauce from a post by the original artist")
            message = lang('Errors', 'sauced_the_artist')
            self._post(message, to=tweet.id)
            return

        # Lines with priority attributes incase we need to shorten them
        lines = []

        # Add additional sauce URL's if available
        sauce_urls = []
        if isinstance(sauce, AnimeSource):
            await sauce.load_ids()

            if self.anime_link in ['myanimelist', 'animal', 'all'
                                   ] and sauce.mal_url:
                sauce_urls.append(sauce.mal_url)

            if self.anime_link in ['anilist', 'animal', 'all'
                                   ] and sauce.anilist_url:
                sauce_urls.append(sauce.anilist_url)

            if self.anime_link in ['anidb', 'all']:
                sauce_urls.append(sauce.url)

        # Only add Twitter source URL's for booru's, otherwise we may link to something that angers the Twitter gods
        if isinstance(sauce, BooruSource):
            for url in sauce.urls:
                if 'twitter.com' in url:
                    sauce_urls.append(url)

            if 'twitter.com' in sauce.source_url:
                sauce_urls.append(sauce.source_url)

        # For limiting the length of the title/author
        _repr = reprlib.Repr()
        _repr.maxstring = 32

        # H-Misc doesn't have a source to link to, so we need to try and provide the full title
        if sauce.index not in ['H-Misc', 'E-Hentai', 'H-Anime']:
            title = _repr.repr(sauce.title).strip("'")
        else:
            _repr.maxstring = 128
            title = _repr.repr(sauce.title).strip("'")

        # Format the similarity string
        similarity = lang('Accuracy', 'prefix',
                          {'similarity': sauce.similarity})
        if sauce.similarity >= 95:
            similarity = similarity + " " + lang('Accuracy', 'exact')
        elif sauce.similarity >= 85.0:
            similarity = similarity + " " + lang('Accuracy', 'high')
        elif sauce.similarity >= 70.0:
            similarity = similarity + " " + lang('Accuracy', 'medium')
        elif sauce.similarity >= 60.0:
            similarity = similarity + " " + lang('Accuracy', 'low')
        else:
            similarity = similarity + " " + lang('Accuracy', 'very_low')

        if requested:
            if sauce.similarity >= 60.0:
                reply = lang('Results',
                             'requested_found', {'index': sauce.index},
                             user=tweet.author) + "\n"
                lines.append(ReplyLine(reply, 1))
            else:
                reply = lang('Results',
                             'requested_found_low_accuracy',
                             {'index': sauce.index},
                             user=tweet.author) + "\n"
                lines.append(ReplyLine(reply, 1))
        else:
            if sauce.similarity >= 60.0:
                reply = lang('Results',
                             'other_found', {'index': sauce.index},
                             user=tweet.author) + "\n"
                lines.append(ReplyLine(reply, 1))
            else:
                reply = lang('Results',
                             'other_found_low_accuracy',
                             {'index': sauce.index},
                             user=tweet.author)
                lines.append(ReplyLine(reply, 1))

        # If it's a Pixiv source, try and get their Twitter handle (this is considered most important and displayed first)
        if twitter_sauce:
            reply = lang('Results', 'twitter', {'twitter': twitter_sauce})
            lines.append(ReplyLine(reply, newlines=1))

        # Print the author name if available
        if sauce.author_name:
            author = _repr.repr(sauce.author_name).strip("'")
            reply = lang('Results', 'author', {'author': author})
            lines.append(ReplyLine(reply, newlines=1))

        # Omit the title for Pixiv results since it's usually always non-romanized Japanese and not very helpful
        if not isinstance(sauce, PixivSource):
            reply = lang('Results', 'title', {'title': title})
            lines.append(ReplyLine(reply, 10, newlines=1))

        # Add the episode number and timestamp for video sources
        if isinstance(sauce, VideoSource) and sauce.episode:
            reply = lang('Results', 'episode', {'episode': sauce.episode})
            if sauce.timestamp:
                reply += " " + lang('Results', 'timestamp',
                                    {'timestamp': sauce.timestamp})

            lines.append(ReplyLine(reply, 5, newlines=1))

        # Add character and material info for booru results
        if isinstance(sauce, BooruSource):
            if sauce.material:
                reply = lang('Results', 'material',
                             {'material': sauce.material[0].title()})
                lines.append(ReplyLine(reply, 5, newlines=1))

            if sauce.characters:
                reply = lang('Results', 'character',
                             {'character': sauce.characters[0].title()})
                lines.append(ReplyLine(reply, 4, newlines=1))

        # Add the chapter for manga sources
        if isinstance(sauce, MangaSource) and sauce.chapter:
            reply = lang('Results', 'chapter', {'chapter': sauce.chapter})
            lines.append(ReplyLine(reply, 5, newlines=1))

        # Display our confidence rating
        lines.append(ReplyLine(similarity, 2, newlines=1))

        # Source URL's are not available in some indexes
        if sauce.index not in [
                'H-Misc', 'H-Anime', 'H-Magazines', 'H-Game CG', 'Mangadex'
        ]:
            if sauce_urls:
                reply = "\n".join(sauce_urls)
                lines.append(ReplyLine(reply, newlines=2))
            elif sauce.source_url and not isinstance(sauce, BooruSource):
                lines.append(ReplyLine(sauce.source_url, newlines=2))

        # Try and append bot instructions with monitored posts. This might make our post too long, though.
        if not requested:
            promo_footer = lang('Results', 'other_footer')
            if promo_footer:
                lines.append(ReplyLine(promo_footer, 0, newlines=2))
        elif config.getboolean('System', 'display_patreon'):
            lines.append(
                ReplyLine(
                    "Support SauceBot!\nhttps://www.patreon.com/saucebot",
                    3,
                    newlines=2))

        # trace.moe time! Let's get a video preview
        if sauce_cache.media_id:
            comment = self._post(msg=lines,
                                 to=tweet.id,
                                 media_ids=[sauce_cache.media_id])

        # This was hentai and we want to avoid uploading hentai clips to this account
        else:
            comment = self._post(msg=lines, to=tweet.id)

        # If we've been blocked by this user and have the artists Twitter handle, send the artist a DMCA guide
        if blocked and twitter_sauce:
            self.log.info(f"Sending {twitter_sauce} DMCA takedown advice")
            message = lang('Errors', 'blocked_dmca',
                           {'twitter_artist': twitter_sauce})
            # noinspection PyUnboundLocalVariable
            self._post(msg=message, to=comment.id)
Exemple #5
0
    async def send_reply(self,
                         tweet_cache: TweetCache,
                         media_cache: TweetCache,
                         sauce_cache: TweetSauceCache,
                         tracemoe_sauce: Optional[dict] = None,
                         requested: bool = True,
                         blocked: bool = False) -> None:
        """
        Return the source of the image
        Args:
            tweet_cache (TweetCache): The tweet to reply to
            media_cache (TweetCache): The tweet containing media elements
            sauce_cache (Optional[GenericSource]): The sauce found (or None if nothing was found)
            tracemoe_sauce (Optional[dict]): Tracemoe sauce query, if enabled
            requested (bool): True if the lookup was requested, or False if this is a monitored user account
            blocked (bool): If True, the account posting this has blocked the SauceBot

        Returns:
            None
        """
        tweet = tweet_cache.tweet
        sauce = sauce_cache.sauce

        if sauce is None:
            if requested:
                media = TweetManager.extract_media(media_cache.tweet)
                if not media:
                    return

                yandex_url = f"https://yandex.com/images/search?url={media[sauce_cache.index_no]}&rpt=imageview"
                tinyeye_url = f"https://www.tineye.com/search?url={media[sauce_cache.index_no]}"
                google_url = f"https://www.google.com/searchbyimage?image_url={media[sauce_cache.index_no]}&safe=off"

                message = f"@{tweet.author.screen_name} Sorry, I couldn't find anything (●´ω`●)ゞ\nYour image may be cropped too much, or the artist may simply not exist in any of SauceNao's databases.\n\nTry checking one of these search engines!\n{yandex_url}\n{google_url}\n{tinyeye_url}"
                self._post(msg=message, to=tweet.id)
            return

        # Add additional sauce URL's if available
        sauce_urls = []
        if isinstance(sauce, AnimeSource):
            await sauce.load_ids()

            if self.anime_link in ['anilist', 'animal', 'all'
                                   ] and sauce.anilist_url:
                sauce_urls.append(sauce.anilist_url)

            if self.anime_link in ['myanimelist', 'animal', 'all'
                                   ] and sauce.mal_url:
                sauce_urls.append(sauce.mal_url)

            if self.anime_link in ['anidb', 'all']:
                sauce_urls.append(sauce.url)

        # For limiting the length of the title/author
        repr = reprlib.Repr()
        repr.maxstring = 32

        # H-Misc doesn't have a source to link to, so we need to try and provide the full title
        if sauce.index not in ['H-Misc', 'E-Hentai']:
            title = repr.repr(sauce.title).strip("'")
        else:
            repr.maxstring = 128
            title = repr.repr(sauce.title).strip("'")

        # Format the similarity string
        similarity = f'𝗔𝗰𝗰𝘂𝗿𝗮𝗰𝘆: {sauce.similarity}% ( '
        if sauce.similarity >= 95:
            similarity = similarity + '🟢 Exact Match )'
        elif sauce.similarity >= 85.0:
            similarity = similarity + '🔵 High )'
        elif sauce.similarity >= 70.0:
            similarity = similarity + '🟡 Medium )'
        elif sauce.similarity >= 60.0:
            similarity = similarity + '🟠 Low )'
        else:
            similarity = similarity + '🔴 Very Low )'

        if requested:
            if sauce.similarity >= 60.0:
                reply = f"@{tweet.author.screen_name} I found this in the {sauce.index} database!\n"
            else:
                reply = f"@{tweet.author.screen_name} The accuracy for this {sauce.index} result is very low, so it might be wrong!\n"
        else:
            if sauce.similarity >= 60.0:
                reply = f"Need the sauce? I found it in the {sauce.index} database!\n"
            else:
                reply = f"I found something in the {sauce.index} database that might be related, but the accuracy is low. Sorry if it's not helpful!\n"

        # If it's a Pixiv source, try and get their Twitter handle (this is considered most important and displayed first)
        twitter_sauce = None
        if isinstance(sauce, PixivSource):
            twitter_sauce = self.pixiv.get_author_twitter(
                sauce.data['member_id'])
            if twitter_sauce:
                reply += f"\n𝗔𝗿𝘁𝗶𝘀𝘁𝘀 𝗧𝘄𝗶𝘁𝘁𝗲𝗿: {twitter_sauce}"

        # Print the author name if available
        if sauce.author_name:
            author = repr.repr(sauce.author_name).strip("'")
            reply += f"\n𝗔𝘂𝘁𝗵𝗼𝗿: {author}"

        # Omit the title for Pixiv results since it's usually always non-romanized Japanese and not very helpful
        if not isinstance(sauce, PixivSource):
            reply += f"\n𝗧𝗶𝘁𝗹𝗲: {title}"

        # Add the episode number and timestamp for video sources
        if isinstance(sauce, VideoSource):
            if sauce.episode:
                reply += f"\n𝗘𝗽𝗶𝘀𝗼𝗱𝗲: {sauce.episode}"
            if sauce.timestamp:
                reply += f" ( ⏱️ {sauce.timestamp} )"

        # Add the chapter for manga sources
        if isinstance(sauce, MangaSource):
            if sauce.chapter:
                reply += f"\n𝗖𝗵𝗮𝗽𝘁𝗲𝗿: {sauce.chapter}"

        # Display our confidence rating
        reply += f"\n{similarity}"

        # Source URL's are not available in some indexes
        if sauce_urls:
            reply += "\n\n"
            reply += "\n".join(sauce_urls)
        elif sauce.source_url:
            reply += f"\n\n{sauce.source_url}"

        # Some Booru posts have bad source links cited, so we should always provide a Booru link with the source URL
        if isinstance(sauce, BooruSource) and sauce.source_url != sauce.url:
            reply += f"\n{sauce.url}"

        # Try and append bot instructions with monitored posts. This might make our post too long, though.
        if not requested:
            _reply = reply
            reply += f"\n\nNeed sauce elsewhere? Just follow and (@)mention me in a reply and I'll be right over!"

        try:
            # trace.moe time! Let's get a video preview
            if tracemoe_sauce and tracemoe_sauce[
                    'is_adult'] and not self.nsfw_previews:
                self.log.info(
                    f'NSFW video previews are disabled, skipping preview of `{sauce.title}`'
                )
            elif tracemoe_sauce:
                try:
                    # Attempt to upload our preview video
                    tw_response = self.twython.upload_video(
                        media=io.BytesIO(tracemoe_sauce['preview']),
                        media_type='video/mp4')
                    comment = self._post(msg=reply,
                                         to=tweet.id,
                                         media_ids=[tw_response['media_id']],
                                         sensitive=tracemoe_sauce['is_adult'])
                # Likely a connection error
                except twython.exceptions.TwythonError as error:
                    self.log.error(
                        f"An error occurred while uploading a video preview: {error.msg}"
                    )
                    comment = self._post(msg=reply, to=tweet.id)

            # This was hentai and we want to avoid uploading hentai clips to this account
            else:
                comment = self._post(msg=reply, to=tweet.id)

        # Try and handle any tweet too long errors
        except tweepy.TweepError as error:
            if error.api_code == 186 and not requested:
                self.log.info(
                    "Post is too long; scrubbing bot instructions from message"
                )
                # noinspection PyUnboundLocalVariable
                comment = self._post(msg=_reply, to=tweet.id)
            else:
                raise error

        # If we've been blocked by this user and have the artists Twitter handle, send the artist a DMCA guide
        if blocked:
            if twitter_sauce:
                self.log.warning(
                    f"Sending {twitter_sauce} DMCA takedown advice")
                message = f"""{twitter_sauce} This account has stolen your artwork and blocked me for crediting you. このアカウントはあなたの絵を盗んで、私があなたを明記したらブロックされちゃいました
    https://github.com/FujiMakoto/twitter-saucenao/blob/master/DMCA.md
    https://help.twitter.com/forms/dmca"""
                # noinspection PyUnboundLocalVariable
                self._post(msg=message, to=comment.id)
Exemple #6
0
    async def get_sauce(
        self,
        tweet_cache: TweetCache,
        index_no: int = 0,
        log_index: Optional[str] = None,
        trigger: str = TRIGGER_MENTION
    ) -> Tuple[TweetSauceCache, Optional[bytes]]:
        """
        Get the sauce of a media tweet
        """
        log_index = log_index or 'SYSTEM'

        # Have we cached the sauce already?
        cache = TweetSauceCache.fetch(tweet_cache.tweet_id, index_no)
        if cache:
            return cache, None

        media = TweetManager.extract_media(tweet_cache.tweet)[index_no]

        # Execute a Tracemoe search query for anime results
        async def tracemoe_search(_sauce_results, _path: str,
                                  is_url: bool) -> Optional[dict]:
            if not self.tracemoe:
                return None

            if _sauce_results and isinstance(_sauce_results[0], AnimeSource):
                # noinspection PyBroadException
                try:
                    _tracemoe_sauce = await self.tracemoe.search(_path,
                                                                 is_url=is_url)
                except Exception:
                    self.log.warning(
                        f"[{log_index}] Tracemoe returned an exception, aborting search query"
                    )
                    return None
                if not _tracemoe_sauce.get('docs'):
                    return None

                # Make sure our search results match
                if await _sauce_results[0].load_ids():
                    if _sauce_results[0].anilist_id != _tracemoe_sauce['docs'][
                            0]['anilist_id']:
                        self.log.info(
                            f"[{log_index}] saucenao and trace.moe provided mismatched anilist entries: `{_sauce_results[0].anilist_id}` vs. `{_tracemoe_sauce['docs'][0]['anilist_id']}`"
                        )
                        return None

                    self.log.info(
                        f'[{log_index}] Downloading video preview for AniList entry {_sauce_results[0].anilist_id} from trace.moe'
                    )
                    _tracemoe_preview = await self.tracemoe.video_preview_natural(
                        _tracemoe_sauce)
                    _tracemoe_sauce['docs'][0]['preview'] = _tracemoe_preview
                    return _tracemoe_sauce['docs'][0]

            return None

        # Look up the sauce
        try:
            if config.getboolean('SauceNao', 'download_files', fallback=False):
                self.log.debug(f"[{log_index}] Downloading image from Twitter")
                fd, path = tempfile.mkstemp()
                try:
                    with os.fdopen(fd, 'wb') as tmp:
                        async with aiohttp.ClientSession(
                                raise_for_status=True) as session:
                            try:
                                async with await session.get(media
                                                             ) as response:
                                    image = await response.read()
                                    tmp.write(image)
                                    if not image:
                                        self.log.error(
                                            f"[{log_index}] Empty file received from Twitter"
                                        )
                                        sauce_cache = TweetSauceCache.set(
                                            tweet_cache,
                                            index_no=index_no,
                                            trigger=trigger)
                                        return sauce_cache
                            except aiohttp.ClientResponseError as error:
                                self.log.warning(
                                    f"[{log_index}] Twitter returned a {error.status} error when downloading from tweet {tweet_cache.tweet_id}"
                                )
                                sauce_cache = TweetSauceCache.set(
                                    tweet_cache,
                                    index_no=index_no,
                                    trigger=trigger)
                                return sauce_cache

                        sauce_results = await self.sauce.from_file(path)
                        tracemoe_sauce = await tracemoe_search(sauce_results,
                                                               _path=path,
                                                               is_url=False)
                finally:
                    os.remove(path)
            else:
                self.log.debug(f"[{log_index}] Performing remote URL lookup")
                sauce_results = await self.sauce.from_url(media)
                tracemoe_sauce = await tracemoe_search(sauce_results,
                                                       _path=media,
                                                       is_url=True)

            if not sauce_results:
                sauce_cache = TweetSauceCache.set(tweet_cache,
                                                  sauce_results,
                                                  index_no,
                                                  trigger=trigger)
                return sauce_cache, None
        except ShortLimitReachedException:
            self.log.warning(
                f"[{log_index}] Short API limit reached, throttling for 30 seconds"
            )
            await asyncio.sleep(30.0)
            return await self.get_sauce(tweet_cache, index_no, log_index)
        except DailyLimitReachedException:
            self.log.error(
                f"[{log_index}] Daily API limit reached, throttling for 15 minutes. Please consider upgrading your API key."
            )
            await asyncio.sleep(900.0)
            return await self.get_sauce(tweet_cache, index_no, log_index)
        except SauceNaoException as e:
            self.log.error(f"[{log_index}] SauceNao exception raised: {e}")
            sauce_cache = TweetSauceCache.set(tweet_cache,
                                              index_no=index_no,
                                              trigger=trigger)
            return sauce_cache, None

        sauce_cache = TweetSauceCache.set(tweet_cache,
                                          sauce_results,
                                          index_no,
                                          trigger=trigger)
        return sauce_cache, tracemoe_sauce
Exemple #7
0
    async def get_sauce(
        self,
        tweet_cache: TweetCache,
        index_no: int = 0,
        log_index: Optional[str] = None,
        trigger: str = TRIGGER_MENTION
    ) -> Tuple[TweetSauceCache, Optional[bytes]]:
        """
        Get the sauce of a media tweet
        """
        log_index = log_index or 'SYSTEM'

        # Have we cached the sauce already?
        cache = TweetSauceCache.fetch(tweet_cache.tweet_id, index_no)
        if cache:
            return cache, None

        media = TweetManager.extract_media(tweet_cache.tweet)[index_no]

        # Execute a Tracemoe search query for anime results
        async def tracemoe_search(_sauce, is_url: bool) -> Optional[dict]:
            if not self.tracemoe:
                return None

            if _sauce.results and _sauce.results[0].index_id in [21, 22]:
                # noinspection PyBroadException
                try:
                    _tracemoe_sauce = await self.tracemoe.search(path,
                                                                 is_url=is_url)
                except Exception:
                    self.log.warning(
                        f"[{log_index}] Tracemoe returned an exception, aborting search query"
                    )
                    return None
                if not _tracemoe_sauce.get('docs'):
                    return None

                # Check for an exactly title match first, then fallback to a similarity check.
                # Obviously, this is not perfect. Titles don't always match, but sometimes tracemoe returns an accurate
                # result with a lower similarity, so we just.. try and guess the best we can for now.
                if _tracemoe_sauce['docs'][0]['similarity'] < 0.85:
                    if _tracemoe_sauce['docs'][0]['title_english'].lower(
                    ) != sauce.results[0].title.lower():
                        if _tracemoe_sauce['docs'][0]['title_romaji'].lower(
                        ) != sauce.results[0].title.lower():
                            return None

                _tracemoe_preview = await self.tracemoe.video_preview_natural(
                    _tracemoe_sauce)
                _tracemoe_sauce['docs'][0]['preview'] = _tracemoe_preview
                return _tracemoe_sauce['docs'][0]

            return None

        # Look up the sauce
        try:
            if config.getboolean('SauceNao', 'download_files', fallback=False):
                self.log.debug(f"[{log_index}] Downloading image from Twitter")
                fd, path = tempfile.mkstemp()
                try:
                    with os.fdopen(fd, 'wb') as tmp:
                        async with aiohttp.ClientSession(
                                raise_for_status=True) as session:
                            try:
                                async with await session.get(media
                                                             ) as response:
                                    image = await response.read()
                                    tmp.write(image)
                                    if not image:
                                        self.log.error(
                                            f"[{log_index}] Empty file received from Twitter"
                                        )
                                        sauce_cache = TweetSauceCache.set(
                                            tweet_cache,
                                            index_no=index_no,
                                            trigger=trigger)
                                        return sauce_cache
                            except aiohttp.ClientResponseError as error:
                                self.log.warning(
                                    f"[{log_index}] Twitter returned a {error.status} error when downloading from tweet {tweet_cache.tweet_id}"
                                )
                                sauce_cache = TweetSauceCache.set(
                                    tweet_cache,
                                    index_no=index_no,
                                    trigger=trigger)
                                return sauce_cache

                        sauce = await self.sauce.from_file(path)
                        tracemoe_sauce = await tracemoe_search(sauce,
                                                               is_url=False)
                finally:
                    os.remove(path)
            else:
                self.log.debug(f"[{log_index}] Performing remote URL lookup")
                sauce = await self.sauce.from_url(media)
                tracemoe_sauce = await tracemoe_search(sauce, is_url=True)

            if not sauce.results:
                sauce_cache = TweetSauceCache.set(tweet_cache,
                                                  sauce,
                                                  index_no,
                                                  trigger=trigger)
                return sauce_cache, None
        except ShortLimitReachedException:
            self.log.warning(
                f"[{log_index}] Short API limit reached, throttling for 30 seconds"
            )
            await asyncio.sleep(30.0)
            return await self.get_sauce(tweet_cache, index_no, log_index)
        except DailyLimitReachedException:
            self.log.error(
                f"[{log_index}] Daily API limit reached, throttling for 15 minutes. Please consider upgrading your API key."
            )
            await asyncio.sleep(900.0)
            return await self.get_sauce(tweet_cache, index_no, log_index)
        except SauceNaoException as e:
            self.log.error(f"[{log_index}] SauceNao exception raised: {e}")
            sauce_cache = TweetSauceCache.set(tweet_cache,
                                              index_no=index_no,
                                              trigger=trigger)
            return sauce_cache, None

        sauce_cache = TweetSauceCache.set(tweet_cache,
                                          sauce,
                                          index_no,
                                          trigger=trigger)
        return sauce_cache, tracemoe_sauce