changeset 60:a0d3bc630ebd

For issue #8, create a videos application to randomize videos in the playlist. This commit now adds a dependency to the Google Python GData library. The admin enters a playlist URL in the admin. Then the admin uses an admin action to synchronize the playlist with YouTube. This reads the playlist title and retrieves the video list from YouTube. The view function reads all the playlist objects to get the complete list of videos, then shuffles them up. The template generates Javascript to create a YouTube player with the shuffled list. A fixture is included for convenience and for the tests. I also committed a test tool I wrote to prove out this idea in case it is useful for future enhancements or experimentation.
author Brian Neal <bgneal@gmail.com>
date Sat, 19 Nov 2011 14:19:00 -0600
parents f8858447adda
children 1d2473f4bcaa
files bns_website/core/tests/view_tests.py bns_website/settings/base.py bns_website/templates/videos.html bns_website/templates/videos/index.html bns_website/urls.py bns_website/videos/__init__.py bns_website/videos/admin.py bns_website/videos/fixtures/playlist.json bns_website/videos/models.py bns_website/videos/tests/__init__.py bns_website/videos/tests/view_tests.py bns_website/videos/urls.py bns_website/videos/views.py tools/get_vids.py
diffstat 13 files changed, 291 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/bns_website/core/tests/view_tests.py	Thu Nov 17 20:17:14 2011 -0600
+++ b/bns_website/core/tests/view_tests.py	Sat Nov 19 14:19:00 2011 -0600
@@ -26,15 +26,6 @@
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, 'music.html')
 
-    def test_video(self):
-        """
-        Tests the video page to ensure it displays without errors.
-
-        """
-        response = self.client.get(reverse('videos'))
-        self.assertEqual(response.status_code, 200)
-        self.assertTemplateUsed(response, 'videos.html')
-
     def test_buy(self):
         """
         Tests the buy page to ensure it displays without errors.
--- a/bns_website/settings/base.py	Thu Nov 17 20:17:14 2011 -0600
+++ b/bns_website/settings/base.py	Sat Nov 19 14:19:00 2011 -0600
@@ -128,6 +128,7 @@
     'bands',
     'news',
     'reviews',
+    'videos',
 ]
 
 # A sample logging configuration. The only tangible logging
--- a/bns_website/templates/videos.html	Thu Nov 17 20:17:14 2011 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-{% extends 'base.html' %}
-{% load core_tags %}
-{% block title %}Watch{% endblock %}
-{% block content %}
-{% navbar 'videos' %}
-<h1>Watch</h1>
-<p>Enjoy some videos from the bands that performed on the <em>Brave New Surf</em> compilation.</p>
-
-<iframe width="853" height="480" src="http://www.youtube-nocookie.com/embed/videoseries?list=PL26E22C14D94D323F&amp;hl=en_US" frameborder="0" allowfullscreen></iframe>
-
-{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/templates/videos/index.html	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+{% load core_tags %}
+{% block title %}Watch{% endblock %}
+{% block content %}
+{% navbar 'videos' %}
+<h1>Watch</h1>
+
+{% if videos %}
+<p>
+Please enjoy this {{ videos|length }} video playlist of the bands that performed on the 
+<em>Brave New Surf</em> compilation. You can use the button that looks like a widescreen TV
+at the bottom of the player to scroll through all the videos.
+</p>
+<div id="player"></div>
+
+<script>
+var tag = document.createElement('script');
+tag.src = "http://www.youtube.com/player_api";
+var firstScriptTag = document.getElementsByTagName('script')[0];
+firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+
+var player;
+function onYouTubePlayerAPIReady() {
+  player = new YT.Player('player', {
+    videoId: '{{ videos|first }}',
+    {% if playlist %}
+    playerVars: { playlist: [
+       {% for video in videos|slice:"1:" %}
+         {% if not forloop.first %},{% endif %}'{{ video }}'
+       {% endfor %} ]},
+    {% endif %}
+    width: '853',
+    height: '480'
+  });
+}
+</script>
+
+{% else %}
+   <p>Videos of the bands are coming soon. Please check back later.</p>
+{% endif %}
+{% endblock %}
--- a/bns_website/urls.py	Thu Nov 17 20:17:14 2011 -0600
+++ b/bns_website/urls.py	Sat Nov 19 14:19:00 2011 -0600
@@ -25,17 +25,10 @@
     url(r'^listen/$',
         TemplateView.as_view(template_name="music.html"),
         name="music"),
-    url(r'^watch/$',
-        TemplateView.as_view(template_name="videos.html"),
-        name="videos"),
+    url(r'^watch/$', include('videos.urls')),
     url(r'^buy/$',
         TemplateView.as_view(template_name="buy.html"),
         name="buy"),
-
-    # Examples:
-    # url(r'^$', 'bns_website.views.home', name='home'),
-    # url(r'^bns_website/', include('bns_website.foo.urls')),
-
     url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
     url(r'^admin/', include(admin.site.urls)),
 )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/admin.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,90 @@
+"""
+Automatic admin definitions for the videos application.
+
+"""
+import datetime
+import urlparse
+
+from django.contrib import admin
+from django.contrib import messages
+from gdata.youtube.service import YouTubeService
+
+from videos.models import Playlist
+
+
+class PlaylistAdmin(admin.ModelAdmin):
+    list_display = ['__unicode__', 'playlist_url', 'sync_date']
+    readonly_fields = ['playlist_title', 'video_list', 'sync_date']
+    actions = ['sync']
+
+    def sync(self, request, queryset):
+        for playlist in queryset:
+            self.sync_playlist(request, playlist)
+
+    sync.short_description = 'Synchronize with YouTube'
+
+    def sync_playlist(self, request, playlist):
+        """
+        Retrieve the title and list of videos for a
+        YouTube playlist.
+
+        """
+        # Find the playlist ID:
+        parts = urlparse.urlparse(playlist.playlist_url)
+        query = urlparse.parse_qs(parts.query)
+        if 'list' not in query:
+            messages.error(request, 'Invalid playlist %s' %
+                    playlist.playlist_url)
+            return
+
+        playlist_id = query['list'][0]
+        if not playlist_id.startswith('PL'):
+            messages.error(request, 'Invalid playlist ID in %s' %
+                    playlist.playlist_url)
+            return
+        playlist_id = playlist_id[2:]
+
+        # Get the playlist feed:
+        yt = YouTubeService()
+        feed = yt.GetYouTubePlaylistVideoFeed(playlist_id=playlist_id)
+        feed_title = feed.title.text
+        expected_count = int(feed.total_results.text)
+
+        # Get all the videos in the feed; this may take multiple requests:
+        vids = []
+        while True:
+            vids.extend(feed.entry)
+            next_link = feed.GetNextLink()
+            if not next_link:
+                break
+            feed = yt.Query(next_link.href)
+
+        if len(vids) != expected_count:
+            messages.error(request, "%s: expected %d videos, got %d" %
+                    (playlist.playlist_url, expected_count, len(vids)))
+
+        # Find the video ID for each video
+
+        vid_ids = []
+        for vid in vids:
+            for link in vid.link:
+                parts = urlparse.urlparse(link.href)
+                query = urlparse.parse_qs(parts.query)
+                if 'v' in query:
+                    vid_ids.append(query['v'][0])
+                    break
+            else:
+                messages.error(request, "%s: video id not found for %s" %
+                        (playlist.playlist_url, vid.title.text))
+
+        # Okay, save what we got
+        playlist.playlist_title = feed_title
+        playlist.video_list = ",".join(vid_ids)
+        playlist.sync_date = datetime.datetime.now()
+        playlist.save()
+
+        messages.info(request, "Synchronized %s (%s)" % (feed_title,
+            playlist.playlist_url))
+
+
+admin.site.register(Playlist, PlaylistAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/fixtures/playlist.json	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,12 @@
+[
+    {
+        "pk": 1, 
+        "model": "videos.playlist", 
+        "fields": {
+            "playlist_url": "http://www.youtube.com/playlist?list=PL26E22C14D94D323F", 
+            "sync_date": "2011-11-19 12:28:01", 
+            "video_list": "jTJE2PDKkLM,RsWH6Hvk0ec,mBj3BKwyF50,xDYT0Vu0Xrk,XiNjzIBLwSE,kG626uATPcM,J618hE-GUXY,MKU8mgMwkyY,0LGUqEZcjtU,HACgijBE0JM,NFKwh967CQg,l2mTHH1WEFY,e_y6qWBkZvA,d_WvX06VNKU,sAItSEypYrA,ZfcXla0Rppc,HugPW5XZWQ4,PNkxPAReT5I,env8hOFFn44,_mDxMVQy3UY,u5gDyFMluio,sGDm3Wx2hNQ,am59GF0Dc3s,-JKKZaTSEAk,KUzsXvHVTjU,8b1r7t4LT1g,SXtr1u6Yizw,OxunllVGQQo,YJNzGRvI_kU,IFnyaCPyJSk,hIEGYQjkn0I,i9J0-OQkfRc,F8H8w0cLSS8,yxqC67op4gc,EQok0Jsi85k,p5R3HOv5S5k,RiHOgdLjD7g,CHGeikLt-bc,cVBm-XljTuM,Pt3lStrELhc,HgT8LJvdbL0,HACgijBE0JM,QKG0Fu8o7-w,z6bwirjUTTI,JwgfzvNyHoU,R7FEfqy1Xg8,i9J0-OQkfRc,2EI1JVoajBw,oASw2F86koo,chK776zOK-M", 
+            "playlist_title": "Brave New Surf Bands"
+        }
+    }
+]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/models.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,38 @@
+"""
+Models for the videos app.
+
+"""
+from django.db import models
+
+
+# The whole purpose of this application is to shuffle the videos in
+# a YouTube playlist.
+#
+# We'd like to embed a video player to show videos from all the bands that
+# participated in the compilation. So we create a YouTube playlist with a long
+# list of videos. YouTube no longer offers a way to shuffle the playlist via
+# the embed code. Instead we will use the YouTube Javascript API to accomplish
+# this. However, the Javascript API works off of video IDs. So we now need
+# a way to obtain all the video IDs from a YouTube playlist. This model
+# stores the playlist URL and an admin function can be run to retrieve the
+# video ID list via the Python GData YouTube API. The video list is also
+# stored in this model. The view function can then read the video ID list,
+# randomize it, and give it to the template to build the appropriate 
+# Javascript.
+
+class Playlist(models.Model):
+    """
+    This model stores a YouTube playlist URL and a list of video IDs that
+    make up the playlist.
+
+    """
+    playlist_title = models.CharField(max_length=128, blank=True)
+    playlist_url = models.URLField()
+    video_list = models.TextField(blank=True)
+    sync_date = models.DateTimeField(null=True, blank=True)
+
+    def __unicode__(self):
+        if self.playlist_title:
+            return self.playlist_title
+
+        return u"(Please sync with YouTube to retrieve the videos)"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/tests/__init__.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,1 @@
+from view_tests import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/tests/view_tests.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,33 @@
+"""
+View tests for the videos application.
+
+"""
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+
+
+class NoVideosTest(TestCase):
+
+    def test_index(self):
+        """
+        Test that the page displays without any videos in the database.
+
+        """
+        response = self.client.get(reverse('videos'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.context['videos']), 0)
+        self.assertTemplateUsed(response, 'videos/index.html')
+
+
+class SomeVideosTest(TestCase):
+    fixtures = ['playlist.json']
+
+    def test_index(self):
+        """
+        Test that the page displays with videos in the database.
+
+        """
+        response = self.client.get(reverse('videos'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.context['videos']), 50)
+        self.assertTemplateUsed(response, 'videos/index.html')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/urls.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,9 @@
+"""
+URLs for the videos application.
+
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('videos.views',
+    url(r'^$', 'index', name='videos'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bns_website/videos/views.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,20 @@
+"""
+Views for the videos application.
+
+"""
+import random
+from django.shortcuts import render
+from videos.models import Playlist
+
+
+def index(request):
+    qs = Playlist.objects.all()
+    videos = []
+    for playlist in qs:
+        videos.extend(playlist.video_list.split(','))
+
+    random.shuffle(videos)
+
+    return render(request, 'videos/index.html', {
+        'videos': videos,
+        })
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/get_vids.py	Sat Nov 19 14:19:00 2011 -0600
@@ -0,0 +1,45 @@
+"""
+Quick & dirty Python script to retrieve the video IDs of all the videos in a
+playlist on YouTube.
+
+"""
+import urlparse
+
+from gdata.youtube.service import YouTubeService
+
+
+PLAYLIST_ID = '26E22C14D94D323F'
+
+yt = YouTubeService()
+feed = yt.GetYouTubePlaylistVideoFeed(playlist_id=PLAYLIST_ID)
+
+print "Feed contains %s videos" % feed.total_results.text
+
+vids = []
+while True:
+    vids.extend(feed.entry)
+    next_link = feed.GetNextLink()
+    if not next_link:
+        break
+    feed = yt.Query(next_link.href)
+
+print "Got %d videos" % len(vids)
+
+vid_ids = []
+problems = []
+for vid in vids:
+    for link in vid.link:
+        url_data = urlparse.urlparse(link.href)
+        query = urlparse.parse_qs(url_data.query)
+        if 'v' in query:
+            vid_ids.append(query['v'][0])
+            break
+    else:
+        print "Video id not found for %s" % vid.title.text
+
+video_id = vid_ids[0]
+playlist = vid_ids[1:]
+
+print "videoId: '%s'," % video_id
+print "playerVars: { playlist: [ %s ] }," % ','.join("'%s'" % v for v in
+        playlist)