diff --git a/README.md b/README.md index 34990a1..914b4d9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ using [HTTP API](https://gitea.brno.mraveniste.cc/turbotraktor/ladylilia.com/src * `!bellagram`, `!bellapics`, `!instabella`, `!instagram` - posts a link to a random Instagram picture of Bella * `!yt QUERY` - queries Lady Lilia's Youtube channel and posts a link to the most relevant result - `QUERY` can contain `|` (logical **or**) and `-` (logical **not**) operators, for example: `!yt oblivion -nehrim` +* `!clip PATTERN` - searches for Twitch clips matching `PATTERN` and in case multiple matches are found, returns one of them randomly + - `PATTERN` has to be at least 3 characters long and it can be enclosed in double quotes in case it contains spaces ### Twitch-only commands diff --git a/clients/discord.py b/clients/discord.py index 68a603d..492c04a 100644 --- a/clients/discord.py +++ b/clients/discord.py @@ -37,6 +37,7 @@ class DiscordClient(discord.Client): self.supported_commands = [ (re.compile(r'^(?P!|\?)(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), (re.compile(r'^(?P!|\?)yt\s+(?P")?(?P.+)(?(q)")$'), self._do_yt), + (re.compile(r'^(?P!|\?)clip\s+(?P")?(?P.+)(?(q)")$'), self._do_clip), ] super(DiscordClient, self).__init__() @@ -90,3 +91,18 @@ class DiscordClient(discord.Client): await self.send_message(message.channel, embed=embed) elif prefix == '?': await self.send_message(message.channel, result['url']) + + @cooldown(retries=3, timeout=5*60, failure=_cooldown_failure) + async def _do_clip(self, user, message, filter, prefix, **kwargs): + try: + result = self.commands.find_clip(filter) + except CommandError as e: + await self.send_message(message.channel, 'Sorry {0}, {1}'.format(message.author.mention, e)) + else: + if prefix == '!': + embed = discord.Embed(title=result['title'], url=result['url'], color=0xff5733) + embed.set_thumbnail(url=result['thumbnail_medium']) + embed.set_author(name=result['curator_display_name'], icon_url=result['curator_logo']) + await self.send_message(message.channel, embed=embed) + elif prefix == '?': + await self.send_message(message.channel, result['url']) diff --git a/clients/twitch.py b/clients/twitch.py index 2b32aec..dc46997 100644 --- a/clients/twitch.py +++ b/clients/twitch.py @@ -48,6 +48,7 @@ class TwitchClient(irc.bot.SingleServerIRCBot): (re.compile(r'^!syncquotes$'), self._do_syncquotes), (re.compile(r'^!(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), (re.compile(r'^!yt\s+(?P")?(?P.+)(?(q)")$'), self._do_yt), + (re.compile(r'^!clip\s+(?P")?(?P.+)(?(q)")$'), self._do_clip), ] server = self.config['IRC'].get('server') port = self.config['IRC'].getint('port') @@ -143,9 +144,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot): respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) return try: - messages = self.commands.get_twitch_messages() + messages = self.commands.get_quote_messages() except CommandError as e: - self.logger.error('Failed to get Twitch messages: %s', e) + self.logger.error('Failed to get quote messages: %s', e) else: for message in messages: for pattern, action in self.patterns: @@ -168,3 +169,11 @@ class TwitchClient(irc.bot.SingleServerIRCBot): send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) else: send_response('{0}: {1}'.format(result['title'], result['url'])) + + def _do_clip(self, tags, send_response, filter, **kwargs): + try: + result = self.commands.find_clip(filter) + except CommandError as e: + send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) + else: + send_response('{0}: {1}'.format(result['title'], result['url'])) diff --git a/commands.py b/commands.py index 05ead63..c824ff4 100644 --- a/commands.py +++ b/commands.py @@ -4,10 +4,12 @@ import dateutil.parser import requests from services.instagram import Instagram, InstagramError -from services.twitch import Twitch, TwitchError from services.youtube import Youtube, YoutubeError +CLIPS_BASE_URL = 'https://clips.twitch.tv' + + class CommandError(Exception): pass @@ -44,15 +46,15 @@ class Commands(object): except requests.exceptions.HTTPError as e: raise CommandError(e) - def get_twitch_messages(self): - api_url = self.config['Twitch Logs'].get('api_url') - params = dict(commenter='bellateeny', term='quote') + def get_quote_messages(self): try: - r = requests.get('{0}/search'.format(api_url), params=params) - r.raise_for_status() - except requests.exceptions.HTTPError: + messages = self._get_messages(dict( + commenter='bellateeny', + term='quote')) + except requests.exceptions.HTTPError as e: raise CommandError(e) - return [m.get('message_body', '') for m in r.json()] + else: + return [m.get('message_body', '') for m in messages] def last_quote(self): try: @@ -96,6 +98,21 @@ class Commands(object): else: return result + def find_clip(self, filter): + if len(filter) < 3: + raise CommandError('the search phrase is too short') + try: + clips = self._get_clips(dict( + filter=filter, + sort_order='random', + page_size=1)).json() + clip = clips.pop(0) + clip['url'] = '{0}/{1}'.format(CLIPS_BASE_URL, clip['slug']) + except (requests.exceptions.HTTPError, IndexError): + raise CommandError('no clips found') + else: + return clip + def _get_quotes(self, params): api_url = self.config['Quotes'].get('api_url') r = requests.get('{0}/quotes'.format(api_url), params=params) @@ -118,6 +135,18 @@ class Commands(object): r.raise_for_status() return r + def _get_messages(self, params): + api_url = self.config['Twitch'].get('cache_api_url') + r = requests.get('{0}/search'.format(api_url), params=params) + r.raise_for_status() + return r + + def _get_clips(self, params): + api_url = self.config['Twitch'].get('cache_api_url') + r = requests.get('{0}/clips'.format(api_url), params=params) + r.raise_for_status() + return r + def _collect_bellagrams(self): username = self.config['Instagram'].get('username') keywords = self.config['Instagram'].get('keywords').split(',') diff --git a/services/twitch.py b/services/twitch.py deleted file mode 100644 index 528f38e..0000000 --- a/services/twitch.py +++ /dev/null @@ -1,83 +0,0 @@ -import dateutil.parser -import requests - - -class TwitchError(Exception): - pass - - -class Twitch(object): - def __init__(self, api_url, client_id): - self.api_url = api_url - self.client_id = client_id - - def _get_videos(self, channel_id): - def request(offset, limit): - url = '{0}/channels/{1}/videos'.format(self.api_url, channel_id) - params = dict(client_id=self.client_id, offset=offset, limit=limit) - r = requests.get(url, params=params) - r.raise_for_status() - return r.json() - result = [] - data = request(0, 1) - total = data.get('_total', 0) - limit = 100 - for offset in range(0, total, limit): - data = request(offset, limit) - for vid in data.get('videos', []): - result.append(dict( - id=int(vid['_id'].lstrip('v')), - title=vid['title'], - date=dateutil.parser.parse(vid['recorded_at']).date())) - return result - - def _get_comments(self, video_id): - def request(cursor): - url = '{0}/videos/{1}/comments'.format(self.api_url, video_id) - params = dict(client_id=self.client_id, cursor=cursor) - r = requests.get(url, params=params) - r.raise_for_status() - return r.json() - result = [] - cursor = '' - while True: - data = request(cursor) - for comment in data.get('comments', []): - result.append(comment['message']['body']) - cursor = data.get('_next') - if not cursor: - break - return result - - def _get_stream_info(self, channel_id): - def request(): - url = '{0}/streams/{1}'.format(self.api_url, channel_id) - params = dict(client_id=self.client_id) - r = requests.get(url, params=params) - r.raise_for_status() - return r.json() - data = request() - if data['stream'] is None: - return None - return dict( - id=int(data['stream']['_id']), - game=data['stream']['game'], - viewers=int(data['stream']['viewers'])) - - def get_messages(self, channel_id, since): - try: - videos = self._get_videos(channel_id) - result = [] - for video in [v for v in videos if v['date'] >= since]: - comments = self._get_comments(video['id']) - result.extend(comments) - except requests.exceptions.HTTPError as e: - raise TwitchError('Failed to retrieve VOD messages: {0}'.format(e)) - else: - return result - - def get_stream_info(self, channel_id): - try: - return self._get_stream_info(channel_id) - except requests.exceptions.HTTPError as e: - raise TwitchError('Failed to get stream info: {0}'.format(e)) diff --git a/services/youtube.py b/services/youtube.py index 2b0aa3f..95f8616 100644 --- a/services/youtube.py +++ b/services/youtube.py @@ -64,7 +64,7 @@ class Youtube(object): try: return self._search(channel_id, query, playlists, limit) except googleapiclient.errors.HttpError as e: - raise YoutubeError('Failed to query Youtube API: {}'.format(e)) + raise YoutubeError('Failed to query Youtube API: {0}'.format(e)) def find_best_match(self, channel_ids, query): results = [] @@ -73,7 +73,7 @@ class Youtube(object): results.extend(self._search(channel_id, query, playlists=True, limit=1)) results.extend(self._search(channel_id, query, playlists=False, limit=1)) except googleapiclient.errors.HttpError as e: - raise YoutubeError('Failed to query Youtube API: {}'.format(e)) + raise YoutubeError('Failed to query Youtube API: {0}'.format(e)) if not results: return None tokens = [t for t in query.split('|') if not t.strip().startswith('-')] or [''] diff --git a/settings.cfg.example b/settings.cfg.example index b809962..5f811a9 100644 --- a/settings.cfg.example +++ b/settings.cfg.example @@ -8,11 +8,8 @@ channels = lilialil token = __DISCORD_TOKEN__ [Twitch] -api_url = https://api.twitch.tv/v5 -client_id = __TWITCH_CLIENT_ID__ +cache_api_url = https://ladylilia.com/twitch-cache/api token = oauth:__TWITCH_OAUTH_TOKEN__ -# lilialil -channel_id = 92737529 # nikola_f master_user_id = 210957066 @@ -28,6 +25,3 @@ channel_ids = UC5970RJMoEcRNZl0MNp8tlQ,UCHNjavmkFUf2n0uzWSgj16w [Quotes] api_url = https://ladylilia.com/quotes/api api_key = __QUOTES_API_KEY__ - -[Twitch Logs] -api_url = https://ladylilia.com/twitch-logs/api