This repository has been archived by the owner on Dec 5, 2018. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite YouTube wrapper to not use gdata
Summary: The gdata library wasn't flexible enough and did things like losing the privacy settings on update. Test Plan: Ran v = YouTubeVideo.get('v3U2h-vXl0s', read_only=False) v.update(description='monkey') and verified that the description on YouTube was updated and that the video remains unlisted. Reviewers: benkomalo Reviewed By: benkomalo Differential Revision: http://phabricator.khanacademy.org/D770
- Loading branch information
1 parent
b0420b9
commit 7b54353
Showing
3 changed files
with
117 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import gdata.gauth | ||
|
||
import secrets | ||
|
||
token = gdata.gauth.OAuth2Token( | ||
client_id=secrets.oauth_client_id, | ||
client_secret=secrets.oauth_client_secret, | ||
scope='https://gdata.youtube.com', | ||
user_agent='') | ||
|
||
print token.generate_authorize_url() | ||
code = raw_input("Enter the resulting code: ") | ||
token.get_access_token(code) | ||
print "Access token: %s" % token.access_token |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
gdata==2.0.17 | ||
lxml==3.0alpha2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,125 @@ | ||
"""This file contains some lightweight tools for interacting with the YouTube | ||
API. Currently included are: | ||
get_video: Return a YouTubeVideo with a given video ID. | ||
YouTubeVideo: A class to represent a YouTube video. | ||
""" | ||
|
||
import gdata.gauth | ||
import gdata.youtube.service | ||
import urllib2 | ||
import urlparse | ||
from lxml import etree | ||
|
||
import secrets | ||
|
||
_service = gdata.youtube.service.YouTubeService() | ||
_service.email, _service.password = secrets.get_youtube_password() | ||
_service.developer_key = secrets.youtube_developer_key | ||
_service.ProgrammaticLogin() | ||
|
||
def _youtube_api_urlopen(path, data=None, method='GET', | ||
content_type='application/x-www-form-urlencoded'): | ||
headers = { | ||
'X-GData-Key': 'key=' + secrets.youtube_developer_key, | ||
|
||
# TODO(alpert): Access tokens expire after an hour -- figure out | ||
# how to use refresh tokens properly | ||
'Authorization': 'Bearer ' + secrets.youtube_access_token, | ||
|
||
'Content-Type': content_type, | ||
} | ||
|
||
req = urllib2.Request( | ||
urlparse.urljoin('https://gdata.youtube.com/', path), | ||
data, headers=headers) | ||
|
||
# Ugh -- from http://stackoverflow.com/a/9023005/49485. | ||
req.get_method = lambda: method | ||
|
||
return urllib2.urlopen(req) | ||
|
||
|
||
class YouTubeVideo(object): | ||
"""A light wrapper around gdata.youtube.YouTubeVideoEntry so we can work | ||
with a cleaner API that hides the XML format of the GData API output. | ||
"""A wrapper around the YouTube API for retrieving and updating video | ||
properties. | ||
""" | ||
|
||
def __init__(self, video_entry): | ||
self.video_entry = video_entry | ||
|
||
def __repr__(self): | ||
return "<YouTubeVideo title: %r>" % self.title | ||
def __init__(self, xml): | ||
self.root = etree.fromstring(xml) | ||
|
||
# This seems necessary to avoid having to prefix tag names with the | ||
# full namespace URL every time | ||
self._xpath_eval = etree.XPathEvaluator(self.root) | ||
for prefix, url in self.root.nsmap.iteritems(): | ||
# lxml and xpath don't like empty prefixes, so we give a name to | ||
# the Atom namespace | ||
prefix = prefix or "atom" | ||
self._xpath_eval.register_namespace(prefix, url) | ||
|
||
@classmethod | ||
def get(cls, video_id, read_only=True): | ||
"""Return a YouTubeVideo with the given video ID. If the authenticated | ||
user owns the video, pass read_only=False to fetch from the user's | ||
uploads in order to allow editing. | ||
""" | ||
if read_only: | ||
format = '/feeds/api/videos/%s?v=2' | ||
else: | ||
format = '/feeds/api/users/default/uploads/%s?v=2' | ||
|
||
resp = _youtube_api_urlopen(format % video_id) | ||
return cls(resp.read()) | ||
|
||
@property | ||
def title(self): | ||
return self.video_entry.media.title.text | ||
els = self._xpath_eval('media:group/media:title') | ||
assert len(els) == 1 | ||
return els[0].text | ||
|
||
@property | ||
def description(self): | ||
return self.video_entry.media.description.text | ||
|
||
@description.setter # @Nolint | ||
def description(self, value): | ||
self.video_entry.media.description.text = value | ||
els = self._xpath_eval('media:group/media:description') | ||
assert len(els) == 1 | ||
return els[0].text | ||
|
||
@property | ||
def duration(self): | ||
return int(self.video_entry.media.duration.seconds) | ||
|
||
def put(self): | ||
assert any(link.rel == 'edit' for link in self.video_entry.link), \ | ||
'Video entry is not editable' | ||
result = _service.UpdateVideoEntry(self.video_entry) | ||
assert result, 'Video entry update failed' | ||
|
||
def _edit_url(self): | ||
"""Return the URL to which a PUT or PATCH request to update the video | ||
should be sent. | ||
""" | ||
els = self._xpath_eval('atom:link[@rel="edit"]') | ||
if len(els) == 1: | ||
return els[0].get('href') | ||
|
||
def update(self, **attributes): | ||
"""Update a video's attributes. | ||
Example: | ||
video.update(title='monkey') | ||
Currently only title and description are supported. | ||
""" | ||
edit_url = self._edit_url | ||
assert edit_url is not None, 'Video is not editable' | ||
|
||
# Copy over the xmlns attributes | ||
nsmap = self.root.nsmap | ||
entry = etree.Element('entry', nsmap=nsmap) | ||
entry.set("{%s}fields" % nsmap['gd'], | ||
'media:group(media:title,media:keywords)') | ||
|
||
group = etree.Element("{%s}group" % nsmap['media']) | ||
entry.append(group) | ||
|
||
title = etree.Element("{%s}title" % nsmap['media']) | ||
title.set('type', 'plain') | ||
title.text = attributes.get('title', self.title) | ||
group.append(title) | ||
|
||
description = etree.Element("{%s}description" % nsmap['media']) | ||
description.set('type', 'plain') | ||
description.text = attributes.get('description', self.description) | ||
group.append(description) | ||
|
||
xml = etree.tostring(entry) | ||
resp = _youtube_api_urlopen( | ||
edit_url, data=xml, content_type='application/xml', method='PATCH') | ||
resp.read() | ||
|
||
def get_video(video_id, read_only=True): | ||
"""Return a YouTubeVideo with the given video ID. If the authenticated user | ||
owns the video, use read_only=False to fetch from the user's uploads in | ||
order to allow editing. | ||
""" | ||
if read_only: | ||
format = '/feeds/api/videos/%s' | ||
else: | ||
format = '/feeds/api/users/default/uploads/%s' | ||
|
||
entry = _service.GetYouTubeVideoEntry(format % video_id) | ||
return YouTubeVideo(entry) | ||
|
||
# TODO(alpert): Figure out why the privacy settings are lost when a video entry | ||
# is saved. | ||
def __repr__(self): | ||
return "<YouTubeVideo title: %r description: %r>" % ( | ||
self.title, self.description) |