Skip to content

Instantly share code, notes, and snippets.

@calzoneman
Last active December 2, 2015 10:16
Show Gist options
  • Save calzoneman/b5ee12cf69863bd3fcc3 to your computer and use it in GitHub Desktop.
Save calzoneman/b5ee12cf69863bd3fcc3 to your computer and use it in GitHub Desktop.
YouTube module for willie - using v3 API
# coding=utf8
"""
youtube.py - Willie YouTube v3 Module
Copyright (c) 2015 Calvin Montgomery, All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer. Redistributions in binary form must
reproduce the above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the
distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
Behavior based on the original YouTube module by the following authors:
Dimitri Molenaars, Tyrope.nl.
Elad Alfassa, <elad@fedoraproject.org>
Edward Powell, embolalia.net
Usage:
1. Go to https://console.developers.google.com/project
2. Create a new API project
3. On the left sidebar, click "Credentials" under "APIs & auth"
4. Click "Create new Key" under "Public API access"
5. Click "Server key"
6. Under "APIs & auth" click "YouTube Data API" and then click "Enable API"
Once the key has been generated, add the following to your willie config
(replace dummy_key with the key obtained from the API console):
[youtube]
api_key = dummy_key
"""
import datetime
import json
import re
import sys
from willie import web, tools
from willie.module import rule, commands, example
URL_REGEX = re.compile(r'(youtube.com/watch\S*v=|youtu.be/)([\w-]+)')
INFO_URL = ('https://www.googleapis.com/youtube/v3/videos'
'?key={}&part=contentDetails,status,snippet,statistics&id={}')
SEARCH_URL = (u'https://www.googleapis.com/youtube/v3/search'
'?key={}&part=id&maxResults=1&q={}&type=video')
class YouTubeError(Exception):
pass
def setup(bot):
if not bot.memory.contains('url_callbacks'):
bot.memory['url_callbacks'] = tools.WillieMemory()
bot.memory['url_callbacks'][URL_REGEX] = youtube_info
def shutdown(bot):
del bot.memory['url_callbacks'][URL_REGEX]
def get_api_key(bot):
if not bot.config.has_option('youtube', 'api_key'):
raise KeyError('Missing YouTube API key')
return bot.config.youtube.api_key
def configure(config):
if config.option('Configure YouTube v3 API', False):
config.interactive_add('youtube', 'api_key', 'Google Developers '
'Console API key (Server key)')
def convert_date(date):
"""Parses an ISO 8601 datestamp and reformats it to be a bit nicer"""
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.000Z')
return date.strftime('%Y-%m-%d %H:%M:%S UTC')
def convert_duration(duration):
"""Converts an ISO 8601 duration to a human-readable duration"""
units = {
'hour': 0,
'minute': 0,
'second': 0
}
for symbol, unit in zip(('H', 'M', 'S'), ('hour', 'minute', 'second')):
match = re.search(r'(\d+)' + symbol, duration)
if match:
units[unit] = int(match.group(1))
time = datetime.time(**units)
output = str(time)
match = re.search('(\d+)D', duration)
if match:
output = match.group(1) + ' days, ' + output
return output
def fetch_video_info(bot, id):
"""Retrieves video metadata from YouTube"""
url = INFO_URL.format(get_api_key(bot), id)
raw, headers = web.get(url, return_headers=True)
if headers['_http_status'] == 403:
bot.say(u'[YouTube Search] Access denied. Check that your API key is '
u'configured to allow access to your IP address.')
return
try:
result = json.loads(raw)
except ValueError as e:
raise YouTubeError(u'Failed to decode: ' + raw)
if 'error' in result:
raise YouTubeError(result['error']['message'])
if len(result['items']) == 0:
raise YouTubeError('YouTube API returned empty result')
video = result['items'][0]
info = {
'title': video['snippet']['title'],
'uploader': video['snippet']['channelTitle'],
'uploaded': convert_date(video['snippet']['publishedAt']),
'duration': convert_duration(video['contentDetails']['duration']),
'views': video['statistics']['viewCount'],
'comments': video['statistics']['commentCount'],
'likes': video['statistics']['likeCount'],
'dislikes': video['statistics']['dislikeCount'],
'link': 'https://youtu.be/' + video['id']
}
return info
def fix_count(count):
"""Adds commas to a number representing a count"""
return '{:,}'.format(int(count))
def format_info(tag, info, include_link=False):
"""Formats video information for sending to IRC.
If include_link is True, then the video link will be included in the
output (this is useful for search results), otherwise it is not (no
reason to include a link if we are simply printing information about
a video that was already linked in chat).
"""
output = [
u'[{}] Title: {}'.format(tag, info['title']),
u'Uploader: ' + info['uploader'],
u'Uploaded: ' + info['uploaded'],
u'Duration: ' + info['duration'],
u'Views: ' + fix_count(info['views']),
u'Comments: ' + fix_count(info['comments']),
u'Likes: ' + fix_count(info['likes']),
u'Dislikes: ' + fix_count(info['dislikes'])
]
if include_link:
output.append(u'Link: ' + info['link'])
return u' | '.join(output)
@rule('.*(youtube.com/watch\S*v=|youtu.be/)([\w-]+).*')
def youtube_info(bot, trigger, found_match=None):
"""Catches youtube links said in chat and fetches video information"""
match = found_match or trigger
try:
info = fetch_video_info(bot, match.group(2))
except YouTubeError as e:
bot.say(u'[YouTube] Lookup failed: {}'.format(e))
return
bot.say(format_info('YouTube', info))
@commands('yt', 'youtube')
@example('.yt Mystery Skulls - Ghost')
def ytsearch(bot, trigger):
"""Allows users to search for YouTube videos with .yt <search query>"""
if not trigger.group(2):
return
# Note that web.get() quotes the query parameters, so the
# trigger is purposely left unquoted (double-quoting breaks things)
url = SEARCH_URL.format(get_api_key(bot), trigger.group(2))
raw, headers = web.get(url, return_headers=True)
if headers['_http_status'] == 403:
bot.say(u'[YouTube Search] Access denied. Check that your API key is '
u'configured to allow access to your IP address.')
return
try:
result = json.loads(raw)
except ValueError as e:
bot.say(u'[YouTube Search] Failed to decode: ' + raw)
return
if 'error' in result:
bot.say(u'[YouTube Search] ' + result['error']['message'])
return
if len(result['items']) == 0:
bot.say(u'[YouTube Search] No results for ' + trigger.group(2))
return
# YouTube v3 API does not include useful video metadata in search results.
# Searching gives us the video ID, now we have to do a regular lookup to
# get the information we want.
try:
info = fetch_video_info(bot, result['items'][0]['id']['videoId'])
except YouTubeError as e:
bot.say(u'[YouTube] Lookup failed: {}'.format(e))
return
bot.say(format_info('YouTube Search', info, include_link=True))
@cubarco
Copy link

cubarco commented Jul 11, 2015

Here's a new error.

KeyError: '_http_status' (file "/usr/lib64/python2.7/rfc822.py", line 388, in __getitem__)

@rtil5
Copy link

rtil5 commented Dec 2, 2015

a KeyError is thrown on line 134 if comments are disabled. there must be an easy fix for this, but i'm not sure what.

@rtil5
Copy link

rtil5 commented Dec 2, 2015

here's this crude workaround for now:

try:
    info = {
        'title': video['snippet']['title'],
        'uploader': video['snippet']['channelTitle'],
        'uploaded': convert_date(video['snippet']['publishedAt']),
        'duration': convert_duration(video['contentDetails']['duration']),
        'views': video['statistics']['viewCount'],
        'comments': video['statistics']['commentCount'],
        'likes': video['statistics']['likeCount'],
        'dislikes': video['statistics']['dislikeCount'],
        'link': 'https://youtu.be/' + video['id']
    }
except KeyError:
    info = {
        'title': video['snippet']['title'],
        'uploader': video['snippet']['channelTitle'],
        'uploaded': convert_date(video['snippet']['publishedAt']),
        'duration': convert_duration(video['contentDetails']['duration']),
        'views': video['statistics']['viewCount'],
        'comments': 0,
        'likes': video['statistics']['likeCount'],
        'dislikes': video['statistics']['dislikeCount'],
        'link': 'https://youtu.be/' + video['id']
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment