From 7fc52f5076d918bd29f87060cc853a78351b8ca0 Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Fri, 14 Oct 2011 11:03:43 +0200 Subject: [PATCH] api/movie --- oxdata/api/__init__.py | 0 oxdata/api/actions.py | 117 +++++++++++++++ oxdata/api/management/__init__.py | 0 oxdata/api/management/commands/__init__.py | 0 oxdata/api/models.py | 3 + oxdata/api/templates/api.html | 10 ++ oxdata/api/urls.py | 10 ++ oxdata/api/views.py | 57 ++++++++ oxdata/movie/__init__.py | 0 oxdata/movie/models.py | 91 ++++++++++++ oxdata/movie/views.py | 89 ++++++++++++ oxdata/settings.py | 25 +++- oxdata/static/js/pandora.api.js | 157 +++++++++++++++++++++ oxdata/urls.py | 5 + requirements.txt | 1 + 15 files changed, 560 insertions(+), 5 deletions(-) create mode 100644 oxdata/api/__init__.py create mode 100644 oxdata/api/actions.py create mode 100644 oxdata/api/management/__init__.py create mode 100644 oxdata/api/management/commands/__init__.py create mode 100644 oxdata/api/models.py create mode 100644 oxdata/api/templates/api.html create mode 100644 oxdata/api/urls.py create mode 100644 oxdata/api/views.py create mode 100644 oxdata/movie/__init__.py create mode 100644 oxdata/movie/models.py create mode 100644 oxdata/movie/views.py create mode 100755 oxdata/static/js/pandora.api.js diff --git a/oxdata/api/__init__.py b/oxdata/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oxdata/api/actions.py b/oxdata/api/actions.py new file mode 100644 index 0000000..f80a29f --- /dev/null +++ b/oxdata/api/actions.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +import sys +import inspect + +from django.conf import settings + +from ox.django.shortcuts import render_to_json_response, json_response +from ox.utils import json + + +def autodiscover(): + #register api actions from all installed apps + from django.utils.importlib import import_module + from django.utils.module_loading import module_has_submodule + for app in settings.INSTALLED_APPS: + if app != 'api': + mod = import_module(app) + try: + import_module('%s.views'%app) + except: + if module_has_submodule(mod, 'views'): + raise + +def trim(docstring): + if not docstring: + return '' + # Convert tabs to spaces (following the normal Python rules) + # and split into a list of lines: + lines = docstring.expandtabs().splitlines() + # Determine minimum indentation (first line doesn't count): + indent = sys.maxint + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < sys.maxint: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + # Return a single string: + return '\n'.join(trimmed) + + +class ApiActions(dict): + properties = {} + def __init__(self): + + def api(request): + ''' + returns list of all known api actions + param data { + docs: bool + } + if docs is true, action properties contain docstrings + return { + status: {'code': int, 'text': string}, + data: { + actions: { + 'api': { + cache: true, + doc: 'recursion' + }, + 'hello': { + cache: true, + .. + } + ... + } + } + } + ''' + data = json.loads(request.POST.get('data', '{}')) + docs = data.get('docs', False) + code = data.get('code', False) + _actions = self.keys() + _actions.sort() + actions = {} + for a in _actions: + actions[a] = self.properties[a] + if docs: + actions[a]['doc'] = self.doc(a) + if code: + actions[a]['code'] = self.code(a) + response = json_response({'actions': actions}) + return render_to_json_response(response) + self.register(api) + + def doc(self, f): + return trim(self[f].__doc__) + + def code(self, name): + f = self[name] + if name != 'api' and hasattr(f, 'func_closure') and f.func_closure: + f = f.func_closure[0].cell_contents + info = f.func_code.co_filename[len(settings.PROJECT_ROOT)+1:] + info = u'%s:%s' % (info, f.func_code.co_firstlineno) + return info, trim(inspect.getsource(f)) + + def register(self, method, action=None, cache=True): + if not action: + action = method.func_name + self[action] = method + self.properties[action] = {'cache': cache} + + def unregister(self, action): + if action in self: + del self[action] + +actions = ApiActions() + diff --git a/oxdata/api/management/__init__.py b/oxdata/api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oxdata/api/management/commands/__init__.py b/oxdata/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oxdata/api/models.py b/oxdata/api/models.py new file mode 100644 index 0000000..04d0a3c --- /dev/null +++ b/oxdata/api/models.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + diff --git a/oxdata/api/templates/api.html b/oxdata/api/templates/api.html new file mode 100644 index 0000000..cabd746 --- /dev/null +++ b/oxdata/api/templates/api.html @@ -0,0 +1,10 @@ + + + + {{sitename}} API + + + + + + diff --git a/oxdata/api/urls.py b/oxdata/api/urls.py new file mode 100644 index 0000000..fb0b40c --- /dev/null +++ b/oxdata/api/urls.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +from django.conf.urls.defaults import * + + +urlpatterns = patterns("api.views", + (r'^$', 'api'), +) + diff --git a/oxdata/api/views.py b/oxdata/api/views.py new file mode 100644 index 0000000..0ce7bdf --- /dev/null +++ b/oxdata/api/views.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division, with_statement + +import os +import copy + +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.conf import settings +from django.db.models import Max, Sum + +from ox.django.shortcuts import render_to_json_response, json_response +from ox.utils import json + +from actions import actions + + +def api(request): + if request.META['REQUEST_METHOD'] == "OPTIONS": + response = render_to_json_response({'status': {'code': 200, + 'text': 'use POST'}}) + response['Access-Control-Allow-Origin'] = '*' + return response + if not 'action' in request.POST: + methods = actions.keys() + api = [] + for f in sorted(methods): + api.append({'name': f, + 'doc': actions.doc(f).replace('\n', '
\n')}) + context = RequestContext(request, {'api': api, + 'sitename': settings.SITENAME}) + return render_to_response('api.html', context) + function = request.POST['action'] + #FIXME: possible to do this in f + #data = json.loads(request.POST['data']) + + f = actions.get(function) + if f: + response = f(request) + else: + response = render_to_json_response(json_response(status=400, + text='Unknown function %s' % function)) + response['Access-Control-Allow-Origin'] = '*' + return response + +def init(request): + return render_to_json_response(json_response()) +actions.register(init) + +def error(request): + ''' + this action is used to test api error codes, it should return a 503 error + ''' + success = error_is_success + return render_to_json_response({}) +actions.register(error) diff --git a/oxdata/movie/__init__.py b/oxdata/movie/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oxdata/movie/models.py b/oxdata/movie/models.py new file mode 100644 index 0000000..98f9c71 --- /dev/null +++ b/oxdata/movie/models.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import re + +from django.db import models +import ox + + +def find(info): + q = Imdb.objects.all() + for key in Imdb.keys: + if key in info and info[key]: + fkey = '%s_iexact' + if isinstance(info[key], list): + q = q.filter(**{fkey: '\n'.join(info[key])}) + else: + q = q.filter(**{fkey:info[key]}) + if q.count() == 1: + return q[0] + return None + +class Imdb(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + imdb = models.CharField(max_length=7, unique=True) + title = models.CharField(max_length=1000, blank=True, default='') + year = models.CharField(max_length=4, blank=True, default='') + director = models.CharField(max_length=1000, blank=True, default='') + + season = models.IntegerField(blank=True, null=True) + episode = models.IntegerField(blank=True, null=True) + episodeTitle = models.CharField(max_length=1000, blank=True, default='') + episodeYear = models.CharField(max_length=4, blank=True, default='') + episodeDirector = models.CharField(max_length=1000, blank=True, default='') + + def __unicode__(self): + return u"%s (%s)" % (self.title, self.imdb) + + keys = ('title', 'director', 'year', 'season', 'episode', + 'episodeTitle', 'episodeYear', 'episodeDirector') + + def update(self): + info = ox.web.imdb.ImdbCombined(self.imdb) + if info: + for key in self.keys: + ikey = { + 'director': 'directors', + 'episodeTitle': 'episode_title', + 'episodeYear': 'episode_year', + 'episodeDirector': 'episode_directors', + }.get(key, key) + if ikey in info: + if ikey in info: + value = info[ikey] + if ikey == 'title' and 'series_title' in info: + value = info['series_title'] + if isinstance(value, list): + value = '\n'.join(value) + '\n' + setattr(self, key, value) + self.save() + + def json(self): + j = {} + j['imdbId'] = self.imdb + for key in self.keys: + j[key] = getattr(self, key) + for key in ('director', 'episodeDirector'): + if j[key].srip(): + j[key] = j[key].strip().split('\n') + else: + del j[key] + for key in j.keys(): + if not j[key]: + del j[key] + return j + +def get_new_ids(timeout=-1): + robot = ox.cache.readUrl('http://www.imdb.com/robots.txt', timeout=timeout) + sitemap_url = re.compile('\nSitemap: (http.+)').findall(robot)[0] + sitemap = ox.cache.readUrl(sitemap_url, timeout=timeout) + urls = re.compile('(.+?)').findall(sitemap) + for url in sorted(urls, reverse=True): + print url + s = ox.cache.readUrl(url, timeout=timeout) + ids = re.compile('http://www.imdb.com/title/tt(\d{7})/combined').findall(s) + for i in ids: + m, created = Imdb.objects.get_or_create(imdb=i) + if created: + m.update() diff --git a/oxdata/movie/views.py b/oxdata/movie/views.py new file mode 100644 index 0000000..bf03a2e --- /dev/null +++ b/oxdata/movie/views.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division + +import re +from urllib import quote + +from django.conf import settings +from ox.django.shortcuts import render_to_json_response, json_response +import ox.web.imdb +from ox.utils import json + +from api.actions import actions + +import models + + +def getId(request): + data = json.loads(request.POST['data']) + response = json_response() + movie = models.find(data) + if movie: + response['data'] = movie.json() + else: + response['status'] = {'text':'not found', 'code': 404} + return render_to_json_response(response) +actions.register(getId) + +def getData(request): + response = json_response() + data = json.loads(request.POST['data']) + id = data['id'] + if len(id) == 7: + data = ox.web.imdb.Imdb(id) + #FIXME: all this should be in ox.web.imdb.Imdb + for key in ('directors', 'writers', 'editors', 'producers', + 'cinematographers', 'languages', 'genres', 'keywords', + 'episode_directors'): + if key in data: + data[key[:-1]] = data.pop(key) + if 'countries' in data: + data['country'] = data.pop('countries') + if 'release date' in data: + data['releasedate'] = data.pop('release date') + if isinstance(data['releasedate'], list): + data['releasedate'] = min(data['releasedate']) + if 'plot' in data: + data['summary'] = data.pop('plot') + if 'cast' in data: + if isinstance(data['cast'][0], basestring): + data['cast'] = [data['cast']] + data['actor'] = [c[0] for c in data['cast']] + data['cast'] = map(lambda x: {'actor': x[0], 'character': x[1]}, data['cast']) + if 'trivia' in data: + def fix_links(t): + def fix_names(m): + return '%s' % ( + quote(m.group(2).encode('utf-8')), m.group(2) + ) + t = re.sub('(.*?)', fix_names, t) + def fix_titles(m): + return '%s' % ( + quote(m.group(2).encode('utf-8')), m.group(2) + ) + t = re.sub('(.*?)', fix_titles, t) + return t + data['trivia'] = [fix_links(t) for t in data['trivia']] + if 'aspectratio' in data: + data['aspectRatio'] = data.pop('aspectratio') + + if 'reviews' in data: + reviews = [] + for r in data['reviews']: + for url in settings.REVIEW_WHITELIST: + if url in r[0]: + reviews.append({ + 'source': settings.REVIEW_WHITELIST[url], + 'url': r[0] + }) + data['reviews'] = reviews + if not data['reviews']: + del data['reviews'] + + response['data'] = data + else: + response['status'] = {'text':'not found', 'code': 404} + + return render_to_json_response(response) +actions.register(getData) diff --git a/oxdata/settings.py b/oxdata/settings.py index faee9c9..db2e557 100644 --- a/oxdata/settings.py +++ b/oxdata/settings.py @@ -4,6 +4,8 @@ import os from os.path import join +SITENAME = 'oxdata' + PROJECT_ROOT = os.path.normpath(os.path.dirname(__file__)) DEBUG = False @@ -72,7 +74,7 @@ ADMIN_MEDIA_PREFIX = '/admin/media/' TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( @@ -95,12 +97,14 @@ INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.humanize', 'south', + 'django_extensions', 'djcelery', - 'oxdata.lookup', -# 'oxdata.movie', - 'oxdata.poster', - 'oxdata.cover', + 'api', + 'lookup', + 'movie', + 'poster', + 'cover', ) LOGIN_REDIRECT_URL='/' @@ -113,6 +117,17 @@ BROKER_USER = "oxdata" BROKER_PASSWORD = "ox" BROKER_VHOST = "/oxdata" +#Movie related settings +REVIEW_WHITELIST = { + u'.filmcritic.com': u'Filmcritic', + u'metacritic.com': u'Metacritic', + u'nytimes.com': u'New York Times', + u'rottentomatoes.com': u'Rotten Tomatoes', + u'salon.com': u'Salon.com', + u'sensesofcinema.com': u'Senses of Cinema', + u'villagevoice.com': u'Village Voice' +} + #overwrite default settings with local settings try: from local_settings import * diff --git a/oxdata/static/js/pandora.api.js b/oxdata/static/js/pandora.api.js new file mode 100755 index 0000000..31b774b --- /dev/null +++ b/oxdata/static/js/pandora.api.js @@ -0,0 +1,157 @@ +/*** + Pandora API +***/ +Ox.load('UI', { + hideScreen: false, + showScreen: true, + theme: 'classic' +}, function() { + +var app = new Ox.App({ + apiURL: '/api/', +}).bindEvent('load', function(data) { + app.default_info = '

Pan.do/ra API Overview

use this api in the browser with Ox.app or use pandora_client it in python. Further description of the api can be found on the wiki
'; + app.$body = $('body'); + app.$document = $(document); + app.$window = $(window); + //app.$body.html(''); + Ox.UI.hideLoadingScreen(); + + app.$ui = {}; + app.$ui.actionList = constructList(); + app.$ui.actionInfo = Ox.Container().css({padding: '16px'}).html(app.default_info); + + app.api.api({docs: true, code: true}, function(results) { + app.actions = results.data.actions; + + if(document.location.hash) { + app.$ui.actionList.triggerEvent('select', {ids: document.location.hash.substring(1).split(',')}); + } + }); + + var $left = new Ox.SplitPanel({ + elements: [ + { + element: new Ox.Element().append(new Ox.Element() + .html('API').css({ + 'padding': '4px', + })).css({ + 'background-color': '#ddd', + 'font-weight': 'bold', + }), + size: 24 + }, + { + element: app.$ui.actionList + } + ], + orientation: 'vertical' + }); + var $main = new Ox.SplitPanel({ + elements: [ + { + element: $left, + size: 160 + }, + { + element: app.$ui.actionInfo, + } + ], + orientation: 'horizontal' + }); + + $main.appendTo(app.$body); +}); + +function constructList() { + return new Ox.TextList({ + columns: [ + { + align: "left", + id: "name", + operator: "+", + title: "Name", + unique: true, + visible: true, + width: 140 + }, + ], + columnsMovable: false, + columnsRemovable: false, + id: 'actionList', + items: function(data, callback) { + function _sort(a, b) { + if(a.name > b.name) + return 1; + else if(a.name == b.name) + return 0; + return -1; + } + if (!data.keys) { + app.api.api(function(results) { + var items = []; + Ox.forEach(results.data.actions, function(v, k) { + items.push({'name': k}) + }); + items.sort(_sort); + var result = {'data': {'items': items.length}}; + callback(result); + }); + } else { + app.api.api(function(results) { + var items = []; + Ox.forEach(results.data.actions, function(v, k) { + items.push({'name': k}) + }); + items.sort(_sort); + var result = {'data': {'items': items}}; + callback(result); + }); + } + }, + scrollbarVisible: true, + sort: [ + { + key: "name", + operator: "+" + } + ] + }).bindEvent({ + select: function(data) { + var info = $('
').addClass('OxSelectable'), + hash = '#'; + if(data.ids.length) + data.ids.forEach(function(id) { + info.append($("

").html(id)); + var $doc =$('
')
+                           .html(app.actions[id].doc.replace('/\n/
\n/g')) + .appendTo(info); + var $code = $('') + .html(app.actions[id].code[1].replace('/\n/
\n/g')) + .hide(); + var $button = new Ox.Button({ + title: [ + {id: "one", title: "right"}, + {id: "two", title: "down"}, + ], + type: "image" + }) + .addClass("margin") + .click(function() { $code.toggle()}) + .appendTo(info) + var f = app.actions[id].code[0]; + $('').html(' View Source ('+f+')').appendTo(info) + $('
').append($code).appendTo(info) 
+
+                hash += id + ','
+              });
+            else
+              info.html(app.default_info);
+
+            document.location.hash = hash.substring(0, hash.length-1);
+            app.$ui.actionInfo.html(info);
+       }
+    });
+}
+});
+
diff --git a/oxdata/urls.py b/oxdata/urls.py
index 3b6dfbe..c826d8f 100644
--- a/oxdata/urls.py
+++ b/oxdata/urls.py
@@ -10,11 +10,16 @@ from django.conf import settings
 from django.contrib import admin
 admin.autodiscover()
 
+from api import actions
+actions.autodiscover()
+
+
 def serve_static_file(path, location, content_type):
     return HttpFileResponse(location, content_type=content_type)
 
 urlpatterns = patterns('',
     (r'^$', 'oxdata.views.index'),
+    (r'^api/$', include('api.urls')),
     (r'^poster/', include('oxdata.poster.urls')),
     (r'^still/$', 'oxdata.poster.views.still'),
     (r'^id/', include('oxdata.lookup.urls')),
diff --git a/requirements.txt b/requirements.txt
index e935c59..f8e6753 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,4 @@ South
 chardet
 django-celery
 gunicorn
+-e git://github.com/bit/django-extensions.git#egg=django_extensions