View
@@ -402,6 +402,10 @@ def get_video_url(self):
def get_or_create_for_url(cls, video_url=None, vt=None, user=None, timestamp=None):
assert video_url or vt, 'should be video URL or VideoType'
from types.base import VideoTypeError
from videos.tasks import (
save_thumbnail_in_s3,
add_amara_description_credit_to_youtube_video
)
try:
vt = vt or video_type_registrar.video_type_for_url(video_url)
@@ -433,9 +437,7 @@ def get_or_create_for_url(cls, video_url=None, vt=None, user=None, timestamp=Non
obj.user = user
obj.save()
from videos.tasks import save_thumbnail_in_s3
save_thumbnail_in_s3.delay(obj.pk)
Action.create_video_handler(obj, user)
#Save video url
@@ -467,6 +469,12 @@ def get_or_create_for_url(cls, video_url=None, vt=None, user=None, timestamp=Non
if hasattr(vt, 'username'):
video_url_obj.owner_username = vt.username
video_url_obj.save()
if vt.abbreviation == VIDEO_TYPE_YOUTUBE:
# Only try to update the Youtube description once we have made sure
# that we have set the owner_username.
add_amara_description_credit_to_youtube_video.delay(video.video_id)
return video, created
@property
@@ -1233,6 +1241,19 @@ def first_version_with_status(self, status):
def first_approved_version(self):
return self.first_version_with_status(APPROVED)
@property
def is_imported_from_youtube_and_not_worked_on(self):
versions = self.subtitleversion_set.all()
if versions.count() > 1 or versions.count() == 0:
return False
version = versions[0]
if version.note == 'From youtube':
return True
return False
models.signals.m2m_changed.connect(User.sl_followers_change_handler, sender=SubtitleLanguage.followers.through)
@@ -2552,6 +2573,8 @@ class VideoFeed(models.Model):
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, blank=True, null=True)
YOUTUBE_PAGE_SIZE = 25
def __unicode__(self):
return self.url
@@ -2567,11 +2590,44 @@ def update(self):
except (IndexError, KeyError):
pass
if not last_link and 'youtube' in self.url:
# This is a newly added Youtube feed. Let's make sure that we get
# all the videos even if we have to page the results.
video_count = feed_parser.feed.feed.opensearch_totalresults
if video_count > self.YOUTUBE_PAGE_SIZE:
pages = self._get_pages(video_count)
for x in range(1, pages + 1):
start = (x - 1) * self.YOUTUBE_PAGE_SIZE + 1
url = '{0}?start-index={1}&max-results={2}'.format(
self.url, start, self.YOUTUBE_PAGE_SIZE)
feed_parser = FeedParser(url)
checked_entries += self._create_videos(feed_parser,
last_link)
else:
checked_entries += self._create_videos(feed_parser, last_link)
return checked_entries
def _get_pages(self, total):
pages = float(total) / float(self.YOUTUBE_PAGE_SIZE)
if pages > int(pages):
pages = int(pages) + 1
else:
pages = int(pages)
return pages
def _create_videos(self, feed_parser, last_link):
checked_entries = 0
_iter = feed_parser.items(reverse=True, until=last_link, ignore_error=True)
for vt, info, entry in _iter:
vt and Video.get_or_create_for_url(vt=vt, user=self.user)
checked_entries += 1
return checked_entries
View
@@ -35,7 +35,11 @@
from messages.models import Message
from utils import send_templated_email, DEFAULT_PROTOCOL
from utils.metrics import Gauge, Meter
from videos.models import VideoFeed, SubtitleLanguage, Video, Subtitle, SubtitleVersion
from videos.models import (
VideoFeed, SubtitleLanguage, Video, Subtitle, SubtitleVersion,
VIDEO_TYPE_YOUTUBE, VideoUrl
)
from videos.types import video_type_registrar
from videos.feed_parser import FeedParser
celery_logger = logging.getLogger('celery.task')
@@ -513,7 +517,7 @@ def _save_video_feed(feed_url, last_entry_url, user):
vf.save()
@periodic_task(run_every=timedelta(seconds=5))
@periodic_task(run_every=timedelta(seconds=60))
def gauge_videos():
Gauge('videos.Video').report(Video.objects.count())
Gauge('videos.Video-captioned').report(Video.objects.exclude(subtitlelanguage=None).count())
@@ -524,3 +528,50 @@ def gauge_videos():
def gauge_videos_long():
Gauge('videos.Subtitle').report(Subtitle.objects.count())
@task
def _add_amara_description_credit_to_youtube_vurl(vurl_pk):
from accountlinker.models import ThirdPartyAccount
try:
vurl = VideoUrl.objects.get(pk=vurl_pk)
except VideoUrl.DoesNotExist:
celery_logger.error("vurl not found", extra={
'vurl_pk': vurl_pk})
return
vt = video_type_registrar.video_type_for_url(vurl.url)
try:
account = ThirdPartyAccount.objects.get(username=vurl.owner_username)
except ThirdPartyAccount.DoesNotExist:
celery_logger.info("TPA not found for {0}".format(vurl.owner_username))
return
bridge = vt._get_bridge(account)
return bridge.add_credit_to_description(vurl.video)
@task
def add_amara_description_credit_to_youtube_video(video_id):
try:
video = Video.objects.get(video_id=video_id)
except Video.DoesNotExist:
celery_logger.error("video_id not found", extra={
'video_id': video_id})
return
if video.get_team_video():
celery_logger.info('team video, skipping', extra={
'video_id': video_id})
return
youtube_urls = video.videourl_set.filter(type=VIDEO_TYPE_YOUTUBE)
if not youtube_urls.exists():
celery_logger.warning("Not a youtube video", extra={
'video_id': video_id})
return
for vurl in youtube_urls:
_add_amara_description_credit_to_youtube_vurl.delay(vurl.pk)
View
@@ -28,7 +28,6 @@
from django.conf import settings
from django.utils.http import urlquote
from django.utils.translation import ugettext_lazy as _
from django.contrib.sites.models import Site
from gdata.service import RequestError
from gdata.youtube.service import YouTubeService
from lxml import etree
@@ -37,18 +36,32 @@
from base import VideoType, VideoTypeError
from utils.subtitles import YoutubeXMLParser
from utils.translation import SUPPORTED_LANGUAGE_CODES
from utils.metrics import Meter, Occurrence
from libs.unilangs.unilangs import LanguageCode
logger = logging.getLogger("youtube")
YOUTUBE_API_SECRET = getattr(settings, "YOUTUBE_API_SECRET", None)
YOUTUBE_ALWAYS_PUSH_USERNAME = getattr(settings,
'YOUTUBE_ALWAYS_PUSH_USERNAME', None)
_('Private video')
_('Undefined error')
class TooManyRecentCallsException(Exception):
"""
Raised when the Youtube API responds with yt:quota too_many_recent_calls.
"""
def __init__(self, *args, **kwargs):
super(TooManyRecentCallsException, self).__init__(*args, **kwargs)
Occurrence('youtube.api_too_many_calls').mark()
def get_youtube_service():
"""
Gets instance of youtube service with the proper developer key
@@ -58,8 +71,10 @@ def get_youtube_service():
yt_service.ssl = False
return yt_service
yt_service = get_youtube_service()
@task
def save_subtitles_for_lang(lang, video_pk, youtube_id):
from videos.models import Video
@@ -143,10 +158,17 @@ def save_subtitles_for_lang(lang, video_pk, youtube_id):
from videos.tasks import video_changed_tasks
video_changed_tasks.delay(video.pk)
Meter('youtube.lang_imported').inc()
def should_add_credit(subtitle_version):
def should_add_credit(subtitle_version=None, video=None):
# Only add credit to non-team videos
video = subtitle_version.language.video
if not video and not subtitle_version:
raise Exception("You need to pass in at least one argument")
if not video:
video = subtitle_version.language.video
return not video.get_team_video()
@@ -157,7 +179,7 @@ def add_credit(subtitle_version, subs):
if len(subs) == 0:
return subs
if not should_add_credit(subtitle_version):
if not should_add_credit(subtitle_version=subtitle_version):
return subs
from accountlinker.models import get_amara_credit_text
@@ -250,6 +272,8 @@ def set_values(self, video_obj):
video_obj.small_thumbnail = 'http://i.ytimg.com/vi/%s/default.jpg' % self.video_id
video_obj.save()
Meter('youtube.video_imported').inc()
try:
self.get_subtitles(video_obj)
except :
@@ -258,6 +282,7 @@ def set_values(self, video_obj):
return video_obj
def _get_entry(self, video_id):
Meter('youtube.api_request').inc()
try:
return yt_service.GetYouTubeVideoEntry(video_id=str(video_id))
except RequestError, e:
@@ -327,10 +352,12 @@ def get_subtitles(self, video_obj):
save_subtitles_for_lang.delay(item, video_obj.pk, self.video_id)
def _get_bridge(self, third_party_account):
# Because somehow Django's ORM is case insensitive on CharFields.
is_always = third_party_account.username.lower() == \
YOUTUBE_ALWAYS_PUSH_USERNAME.lower()
return YouTubeApiBridge(third_party_account.oauth_access_token,
third_party_account.oauth_refresh_token,
self.videoid)
third_party_account.oauth_refresh_token, self.videoid, is_always)
def update_subtitles(self, subtitle_version, third_party_account):
"""
@@ -351,7 +378,8 @@ class YouTubeApiBridge(gdata.youtube.client.YouTubeClient):
upload_uri_base = 'http://gdata.youtube.com/feeds/api/users/default/uploads/%s'
def __init__(self, access_token, refresh_token, youtube_video_id):
def __init__(self, access_token, refresh_token, youtube_video_id,
is_always_push_account=False):
"""
A wrapper around the gdata client, to make life easier.
In order to edit captions for a video, the oauth credentials
@@ -371,6 +399,21 @@ def __init__(self, access_token, refresh_token, youtube_video_id):
)
self.token.authorize(self)
self.youtube_video_id = youtube_video_id
self.is_always_push_account = is_always_push_account
def request(self, *args, **kwargs):
"""
Override the very low-level request method to catch possible
too_many_recent_calls errors.
"""
Meter('youtube.api_request').inc()
try:
return super(YouTubeApiBridge, self).request(*args, **kwargs)
except gdata.client.RequestError, e:
if 'too_many_recent_calls' in str(e):
raise TooManyRecentCallsException
else:
raise e
def refresh(self):
"""
@@ -441,8 +484,9 @@ def upload_captions(self, subtitle_version):
handler = GenerateSubtitlesHandler.get('srt')
subs = [x.for_generator() for x in subtitle_version.ordered_subtitles()]
subs = add_credit(subtitle_version, subs)
self.add_credit_to_description(subtitle_version)
if not self.is_always_push_account:
subs = add_credit(subtitle_version, subs)
self.add_credit_to_description(subtitle_version.language.video)
content = unicode(handler(subs, subtitle_version.language.video )).encode('utf-8')
title = ""
@@ -454,11 +498,13 @@ def upload_captions(self, subtitle_version):
if lang in self.captions:
self._delete_track(self.captions[lang]['track'])
return self.create_track(self.youtube_video_id, title, lang, content,
res = self.create_track(self.youtube_video_id, title, lang, content,
settings.YOUTUBE_CLIENT_ID, settings.YOUTUBE_API_SECRET,
self.token, {'fmt':'srt'})
Meter('youtube.subs_pushed').inc()
return res
def add_credit_to_description(self, subtitle_version):
def add_credit_to_description(self, video):
"""
Get the entry information from Youtube, extract the original
description, prepend the description with Amara credits and push it
@@ -470,10 +516,11 @@ def add_credit_to_description(self, subtitle_version):
If the existing description starts with the credit text, we just
return.
"""
if not should_add_credit(subtitle_version):
if not should_add_credit(video=video):
return False
from accountlinker.models import add_amara_description_credit
from apps.videos.templatetags.videos_tags import shortlink_for_video
uri = self.upload_uri_base % self.youtube_video_id
entry = self.GetVideoEntry(uri=uri)
@@ -482,12 +529,7 @@ def add_credit_to_description(self, subtitle_version):
old_description = entry.media.description.text
video = subtitle_version.language.video
current_site = Site.objects.get_current()
video_url = video.get_absolute_url()
video_url = u"http://%s%s" % (unicode(current_site.domain),
video_url)
video_url = shortlink_for_video(video)
language_code = video.language
@@ -510,18 +552,24 @@ def add_credit_to_description(self, subtitle_version):
status_code = self._make_update_request(uri, entry)
if status_code == 200:
Meter('youtube.description_changed').inc()
return True
return False
def _make_update_request(self, uri, entry):
Meter('youtube.api_request').inc()
headers = {
'Content-Type': 'application/atom+xml',
'Authorization': 'Bearer %s' % self.access_token,
'GData-Version': '2',
'X-GData-Key': 'key=%s' % YOUTUBE_API_SECRET
}
r = requests.put(uri, data=entry, headers=headers)
if r.status_code == 403 and 'too_many_recent_calls' in r.content:
raise TooManyRecentCallsException
return r.status_code
def _delete_track(self, track):
View
@@ -22,6 +22,9 @@
LANGUAGES_MAP = dict(LANGUAGES)
# Handle blank language codes, for now. Eventually these should be going away.
LANGUAGES_MAP[''] = u'Original'
class BaseRpc:
def _make_subtitles_dict(self, subtitles, language_code, language_pk, is_original, is_complete, version, is_latest, is_forked, base_language, title, description, language_is_rtl, is_moderated):
return {
View
@@ -53,3 +53,4 @@ rauth==0.4.12
bernhard==0.0.2
-e git+https://github.com/jsocol/bleach.git@105b4cfc2f00cc1954bcab3b39b16fbfaf8863e0#egg=bleach
markdown2==2.0.0
pytz==2012f
View
@@ -0,0 +1,80 @@
Youtube syncing
===============
Most of the Youtube syncing logic is contained in three places:
* ``videos.types.youtube``
* ``videos.tasks.add_amara_description_credit_to_youtube_video``
* ``accountlinker.models``
The central place for Youtube API interaction is the
``video.types.youtube.YouTubeAPIBridge`` class.
Latest work was done on the **staging** branch (as opposed to **dev**).
What we do
----------
Here is a list of things that we do when we interact with the Youtube API:
* Import Youtube videos (one-off and periodical)
* Import subtitles from Youtube videos
* Push Amara subtitles to Youtube
* Add Amara credit to Youtube video descriptions
* Add Amara credit as the last subtitle of a Youtube video's language
* OAuth authorization
How linking works
-----------------
Whenever a user links their Youtube account to Amara, we add their feed to the
system so that all existing videos are added to Amara immediately and any
future videos are added automatically. When a user unlinks their Youtube
account, we should remove the feed as well.
Whenever any Youtube video is added to Amara outside of a team context we
should check if the video's Youtube owner has a linked account on Amara. If
so, we should immediately add the Amara link to the video's description on
Youtube.
Credits
-------
Credits should be localized based on the video language or the subtitle
language.
Subtitle credits are inserted into the last three seconds of the video. If
there is less than 3 seconds left, we take up whatever space there is.
The description credit is inserted above the existing video description. There
is a ``remove_youtube_credit`` management command that can be used to fix a
video that had the credit applied by accident.
API quota issues
----------------
We have had some issues with rate limiting. If we make too many calls within
a short period of time, we get blocked for a few minutes. We seem to be able
to make an unlimited amount of calls as long as they are spaced out.
To give you an idea how much we can do, importing a video feed with 100 videos
will only import about 60 videos before choking.
Currently, we raise a ``videos.types.youtube.TooManyRecentCallsException`` when
we hit the quota error.
We are working with someone from the Youtube API support team to resolve this
issue.
Metrics
-------
You can have a look at the rates in Graphite. All of the Youtube stuff is
namespaced under ``youtube``. Things that we currently measure:
* API calls
* Descriptions changed
* Languages imported
* Subtitles pushed
* Videos imported
* Too many recent calls exception thrown (occurrence)
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1619,9 +1619,13 @@ body.account {
textarea.api-key-holder {
height: 1.5em;
}
#youtube-modal img {
display: block;
margin: 0 auto 1em;
#youtube-modal {
margin-top: -300px;
img {
display: block;
margin: 0 auto 1em;
}
}
}
.profile .submit.remove {
@@ -2766,6 +2770,8 @@ a.truncated-expand {
padding: 2em 200px 2em 185px;
background: #f2f2f2;
@include border-radius(2px);
@include box-shadow(0,1px,6px,#ddd);
border: 1px solid #ddd;
img {
position: absolute;
View
@@ -503,7 +503,7 @@ var Site = function(Site) {
$('#youtube-prompt a.hide').click(function() {
$('#youtube-prompt').hide();
$.cookie('hide-yt-prompt', 'yes', { expires: 365 });
$.cookie('hide-yt-prompt', 'yes', { path: '/', expires: 365 });
return false;
});
},
View
@@ -1 +1 @@
aec6eae679cabe61cfa5fb8e75f2d2fd2ac5c574
cf49717359db7172b4a9cde0979fa0b88b3c6f7a
View
@@ -6,7 +6,7 @@
<html {% if LANGUAGE_BIDI %}dir="rtl"{% endif %} xmlns:og="http://opengraphprotocol.org/schema/" xmlns:fb="http://www.facebook.com/2008/fbml" xml:lang="en" lang="en" {% block html_attrs %}{% endblock %} class="base">
<head>
<title>{% block title %}Amara - {% trans 'Universal subtitling: caption, translate, subtitle and transcribe video.' %}{% endblock %}</title>
<title>{% block title %}Amara - {% trans 'Caption, translate, subtitle and transcribe video.' %}{% endblock %}</title>
<meta name="description" content="The easiest way to caption and translate any video, with crowdsourcing, volunteers, and professional services." />
@@ -88,17 +88,17 @@ <h1><a href="/">Amara</a></h1>
</div>
<div class="content container_12 wrapper clearfix">
{% comment %}
{% if request|show_youtube_prompt and not hide_prompt %}
<div id="youtube-prompt">
<img src="{{ STATIC_URL }}images/partners/youtube.png" alt="YouTube"/>
<h2>{% trans "Got a YouTube account?<" %}/h2>
<p>{% trans "You can automatically sync subtitles from Amara to your YouTube channel!" %}</p>
<ul>
<li><a href="{% url profiles:account %}?prompt=true" class="proceed">{% trans "Enable Sync" %}</a></li>
<li><a href="#" class="hide">{% trans "No thanks" %}</a></li>
</ul>
</div>
{% endif %}
{% if request|show_youtube_prompt and not hide_prompt %}
<div id="youtube-prompt">
<img src="{{ STATIC_URL }}images/partners/youtube.png" alt="YouTube"/>
<h2>{% trans "Got a YouTube account?<" %}/h2>
<p>{% trans "New: enable viewer-created translations and captions on your YouTube channel!" %}</p>
<ul>
<li><a href="{% url profiles:account %}?prompt=true" class="proceed">{% trans "Connect to YouTube" %}</a></li>
<li><a href="#" class="hide">{% trans "No thanks" %}</a></li>
</ul>
</div>
{% endif %}
{% endcomment %}
{% if messages %}
<div id="messages">
@@ -117,7 +117,7 @@ <h2 class="{% if message.tags %}{{ message.tags }}{% endif %}">{{ message|safe }
<div class="floatright">
<a href="/about">{% trans 'About' %}</a>
<a href="http://www.pculture.org/pcf/jobs/">{% trans 'Jobs' %}</a>
<a href="http://www.facebook.com/pages/Universal-Subtitles/112574762094219">Facebook</a>
<a href="http://www.facebook.com/Amara.Community/">Facebook</a>
<a href="http://twitter.com/amarasubs">Twitter</a>
<a href="http://support.universalsubtitles.org/solution/categories/13504/folders/40766/articles/35517--i-have-a-non-technical-question-about-amara">{% trans 'Contact Us' %}</a>
<a href="http://www.pculture.org/pcf/websites-privacy/">{% trans 'Privacy Policy' %}</a>
@@ -154,7 +154,7 @@ <h2 class="{% if message.tags %}{{ message.tags }}{% endif %}">{{ message|safe }
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{ GOOGLE_ANALYTICS_NUMBER }}']);
_gaq.push(['_setDomainName', '.universalsubtitles.org']);
_gaq.push(['_setDomainName', '.amara.org']);
_gaq.push(['_trackPageview']);
{% if not RUN_LOCALLY %}
{% block analytics %}{% endblock %}
@@ -165,6 +165,7 @@ <h2 class="{% if message.tags %}{{ message.tags }}{% endif %}">{{ message|safe }
})();
{% endif %}
</script>
<script src="//cdn.optimizely.com/js/12019399.js"></script>
{% else %}
<script src="/site_media/js/highlight.min.js"></script>
<link rel="stylesheet" href="/site_media/css/github.min.css">
View
@@ -25,7 +25,7 @@ <h3>{% trans "User Details" %}</h3>
</fieldset>
<div class="grid_8 alpha third-party">
<fieldset>
<h3>{% trans "Sync subtitles" %}</h3>
<h3>{% trans "YouTube Account" %}</h3>
<div class="callout">
{% if third_party %}
{% for account in third_party %}
@@ -34,7 +34,7 @@ <h3>{% trans "Sync subtitles" %}</h3>
</a>
<p>
<img src="{{ STATIC_URL }}images/partners/youtube.png" alt="YouTube"/>
<strong>{% trans "Sync is active" %}</strong>
<strong>{% trans "Link is active" %}</strong>
<span> for {{ account.username }}.</span>
</p>
{% endfor %}
@@ -43,13 +43,13 @@ <h3>{% trans "Sync subtitles" %}</h3>
<span>Connect to</span>
<img src="{{ STATIC_URL }}images/partners/youtube.png" alt="YouTube"/>
</a>
<p>{% trans "Send Amara-created subtitles right to your YouTube videos!" %}</p>
<p>{% trans "Enable Amara crowd subtitles on your personal YouTube channel (beta)" %}</p>
{% endif %}
</div>
</fieldset>
<fieldset>
<h3>{% trans "Link Accounts" %}</h3>
<h3>{% trans "Other Accounts" %}</h3>
<div class="callout">
<p>{% trans "Sign in to Amara with Twitter, Facebook, and other accounts" %}</p>
<a href="#account-modal" class="open-modal button reduced">{% trans "Add an account" %}</a>
@@ -155,7 +155,7 @@ <h3>{% trans 'Add a linked account' %}</h3>
<div class="modal" id="youtube-modal">
<div class="modal-header">
<a href="#" class="close">x</a>
<h3>{% trans 'Connect to YouTube' %}</h3>
<h3>{% trans 'Enable Crowd Subtitles on your YouTube Channel (beta)' %}</h3>
</div>
<form method="get" action="{% url profiles:add-third-party %}">
@@ -165,46 +165,51 @@ <h3>{% trans 'Connect to YouTube' %}</h3>
<img src="{{ STATIC_URL }}images/partners/youtube.png" alt="YouTube" height="65"/>
<input type="hidden" name="account_type" value="youtube"/>
<h5>{% trans "What gets synced" %}</h5>
<h5>{% trans "How We Enable Crowd Subtitles" %}</h5>
<ul>
<li>
{% blocktrans %}
Existing, completed Amara subtitles will be <strong>sent to YouTube immediately</strong>.
We automatically <strong>add your YouTube videos to Amara</strong> (and import their subtitles).
{% endblocktrans %}
</li>
<li>
{% blocktrans %}
While linked, <strong>any newly edited, complete subtitles</strong> will be sent to YouTube.
We <strong>invite your viewers to subtitle</strong> by adding a link to your video descriptions.
{% endblocktrans %}
</li>
<li>
{% blocktrans %}
Sync is <strong>one-way only</strong>. Changes made on YouTube will not be reflected on Amara.
We <strong>send subtitles to YouTube</strong> whenever they're completed on Amara.
{% endblocktrans %}
</li>
</ul>
<h5>{% trans "Your YouTube data" %}</h5>
<h5>{% trans "When We Send Subtitles to YouTube" %}</h5>
<ul>
<li>
{% blocktrans %}
Amara subtitles <strong>will overwrite</strong> existing YouTube subtitles if both exist.
The link is <strong>one-way only</strong>. Changes made on YouTube won't be reflected on Amara and may be overwritten. Use Amara to edit subtitles.
{% endblocktrans %}
</li>
<li>
{% blocktrans %}
Amara <strong>invites your viewers to subtitle</strong> by adding your video's URL to its YouTube description and appending a final subtitle (if space allows).
Amara will <strong>append a credit</strong> to the subtitles if space allows.
{% endblocktrans %}
<a href="http://support.universalsubtitles.org/solution/articles/40227-syncing-to-youtube">{% trans "See what it looks like" %}</a>.
</li>
</ul>
<h5>{% trans "Step 1: Enable your YouTube account by creating a channel" %}</h5>
<p><a href="https://accounts.google.com/ServiceLogin?passive=true&continue=http%3A%2F%2Fwww.youtube.com/create_channel" target="_blank" class="btn">{% trans "Create a channel" %}</a> (opens in a new window)</p>
<p><a href="http://support.universalsubtitles.org/solution/articles/40227-syncing-to-youtube" target="_blank">{% trans "Learn more" %}</a>{% trans " about Crowd Subtitles and YouTube" %}</p>
<h5>{% trans "Step 2: Link accounts" %}</h5>
<input type="submit" class="btn btn-primary" value="{% trans 'Connect and Sync' %}" />
<h5>{% trans "Step 1: Create/Confirm your YouTube Channel" %}</h5>
<p><a href="https://accounts.google.com/ServiceLogin?passive=true&continue=http%3A%2F%2Fwww.youtube.com/create_channel" target="_blank" class="btn">{% trans "Enable Channel" %}</a> (opens in a new window)</p>
<h5>{% trans "Step 2: Authorize" %}</h5>
<input type="submit" class="btn btn-primary" value="{% trans 'Link accounts' %}" />
</fieldset>
<p>
{% trans "Organizations/companies: interested in YouTube sync and crowd subtitling? " %}
<a href="mailto:enterprise@amara.org">{% trans "Contact us" %}</a>.
</p>
</div>
</form>
</div>
View
@@ -13,8 +13,8 @@ <h3>1. Your Acceptance</h3>
Subtitles products, software, data feeds, and services provided to you on,
from, or through the Amara website (known collectively as the
"Service") you signify your agreement to (1) these terms and conditions (the
"Terms of Service"), and (2) Universal Subtitle' privacy policy, found at
http://universalsubtitles.org/w3c/privacy.html and incorporated herein by
"Terms of Service"), and (2) Amara's privacy policy, found at
http://amara.org/w3c/privacy.html and incorporated herein by
reference. If you do not agree to any of these terms, or the Universal
Subtitles privacy policy, please do not use the Service.</p>
View
@@ -0,0 +1,56 @@
"""
Video crop
==========
Given a video, create 100 small 10 second videos from that video.
Usage: $ video_crop.py FILENAME
Depends on ffmpeg. `brew install ffmpeg`
"""
import os
from fabric.api import local
def pad(n):
if n < 10:
return '0' + str(n)
return str(n)
def crop(src_filename):
if not os.path.exists(src_filename):
print "File doesn't exist"
return
command = "ffmpeg -ss {0} -t 00:00:10 -i {1} -acodec mp3 -vcodec copy {2}"
minute = 0
second = 0
for n in range(1, 101):
dest = 'test-video-{0}.avi'.format(pad(n))
if minute == 0 and second == 0:
start = '00:00:00'
else:
if second == 60:
minute += 1
second = 0
start = '00:{0}:{1}'.format(pad(minute), pad(second))
second += 10
cmd = command.format(start, src_filename, dest)
local(cmd)
if __name__ == '__main__':
import sys
try:
crop(sys.argv[1])
except IndexError:
print 'Missing argument'
print 'Usage: video_crop.py FILENAME'