forked from 0x2620/pandora
serve frames, timelines and videos, wire up timeline demo
This commit is contained in:
parent
744dfb101e
commit
f3f40f6e2f
10 changed files with 1595 additions and 68 deletions
|
@ -16,6 +16,10 @@ def index(request):
|
||||||
context = RequestContext(request, {'settings':settings})
|
context = RequestContext(request, {'settings':settings})
|
||||||
return render_to_response('index.html', context)
|
return render_to_response('index.html', context)
|
||||||
|
|
||||||
|
def timeline(request):
|
||||||
|
context = RequestContext(request, {'settings':settings})
|
||||||
|
return render_to_response('timeline.html', context)
|
||||||
|
|
||||||
def api_getPage(request):
|
def api_getPage(request):
|
||||||
data = json.loads(request.POST['data'])
|
data = json.loads(request.POST['data'])
|
||||||
name = data['page']
|
name = data['page']
|
||||||
|
|
|
@ -194,34 +194,22 @@ def run_command(cmd, timeout=10):
|
||||||
killedpid, stat = os.waitpid(p.pid, os.WNOHANG)
|
killedpid, stat = os.waitpid(p.pid, os.WNOHANG)
|
||||||
return p.returncode
|
return p.returncode
|
||||||
|
|
||||||
def frame(videoFile, position, baseFolder, width=128, redo=False):
|
def frame(videoFile, frame, position, width=128, redo=False):
|
||||||
'''
|
'''
|
||||||
params:
|
params:
|
||||||
videoFile
|
videoFile input
|
||||||
|
frame output
|
||||||
position as float in seconds
|
position as float in seconds
|
||||||
baseFolder to write frames to
|
|
||||||
width of frame
|
width of frame
|
||||||
redo boolean to extract file even if it exists
|
redo boolean to extract file even if it exists
|
||||||
'''
|
'''
|
||||||
def frame_path(size):
|
|
||||||
return os.path.join(baseFolder, "%s.%s.%s" % (ox.ms2time(position*1000), size, img_extension))
|
|
||||||
|
|
||||||
#not using input file, to slow to extract frame right now
|
|
||||||
base_size = 320
|
|
||||||
frame = frame_path(base_size)
|
|
||||||
|
|
||||||
if exists(videoFile):
|
if exists(videoFile):
|
||||||
|
frameFolder = os.path.dirname(frame)
|
||||||
if redo or not exists(frame):
|
if redo or not exists(frame):
|
||||||
if not exists(baseFolder):
|
if not exists(frameFolder):
|
||||||
os.makedirs(baseFolder)
|
os.makedirs(frameFolder)
|
||||||
cmd = ['oggThumb', '-t', str(position), '-n', frame, '-s', '%dx0'%base_size, videoFile]
|
cmd = ['oxframe', '-i', videoFile, '-o', frame, '-p', str(position), '-x', str(width)]
|
||||||
run_command(cmd)
|
run_command(cmd)
|
||||||
if width != base_size:
|
|
||||||
frame_base = frame
|
|
||||||
frame = frame_path(width)
|
|
||||||
if not exists(frame):
|
|
||||||
resize_image(frame_base, frame, width)
|
|
||||||
return frame
|
|
||||||
|
|
||||||
def resize_image(image_source, image_output, width=None, size=None):
|
def resize_image(image_source, image_output, width=None, size=None):
|
||||||
if exists(image_source):
|
if exists(image_source):
|
||||||
|
|
|
@ -259,9 +259,11 @@ class Movie(models.Model):
|
||||||
self.get('season', ''), self.get('episode', ''))
|
self.get('season', ''), self.get('episode', ''))
|
||||||
|
|
||||||
def frame(self, position, width=128):
|
def frame(self, position, width=128):
|
||||||
#FIXME: compute offset and so on
|
stream = self.streams.filter(profile=settings.VIDEO_PROFILE+'.webm')[0]
|
||||||
f = self.files.all()[0]
|
path = os.path.join(settings.MEDIA_ROOT, 'frame', self.movieId, "%d"%width, "%s.jpg"%position)
|
||||||
return f.frame(position, width)
|
if not os.path.exists(path):
|
||||||
|
extract.frame(stream.video.path, path, position, width)
|
||||||
|
return path
|
||||||
|
|
||||||
def updateFind(self):
|
def updateFind(self):
|
||||||
try:
|
try:
|
||||||
|
@ -411,6 +413,10 @@ class Movie(models.Model):
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
part += 1
|
part += 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeline_prefix(self):
|
||||||
|
return os.path.join('stream', movieid_path(self.movieId), 'timeline')
|
||||||
|
|
||||||
def updateStreams(self):
|
def updateStreams(self):
|
||||||
files = {}
|
files = {}
|
||||||
for f in self.files.filter(is_main=True, video_available=True):
|
for f in self.files.filter(is_main=True, video_available=True):
|
||||||
|
@ -428,11 +434,13 @@ class Movie(models.Model):
|
||||||
else:
|
else:
|
||||||
cmd.append('+')
|
cmd.append('+')
|
||||||
cmd.append(files[f])
|
cmd.append(files[f])
|
||||||
|
if not os.path.exists(os.path.dirname(stream.video.path)):
|
||||||
|
os.makedirs(os.path.dirname(stream.video.path))
|
||||||
cmd = [ 'mkvmerge', '-o', stream.video.path ] + cmd
|
cmd = [ 'mkvmerge', '-o', stream.video.path ] + cmd
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
stream.save()
|
stream.save()
|
||||||
|
|
||||||
extract.timeline(stream.video.path, os.path.join(stream.video.path[:-len(stream.profile)], 'timeline'))
|
extract.timeline(stream.video.path, os.path.join(settings.MEDIA_ROOT, self.timeline_prefix))
|
||||||
stream.extract_derivatives()
|
stream.extract_derivatives()
|
||||||
|
|
||||||
#something with poster
|
#something with poster
|
||||||
|
@ -704,10 +712,13 @@ class Collection(models.Model):
|
||||||
|
|
||||||
def editable(self, user):
|
def editable(self, user):
|
||||||
return self.users.filter(id=user.id).count() > 0
|
return self.users.filter(id=user.id).count() > 0
|
||||||
|
|
||||||
|
|
||||||
|
def movieid_path(h):
|
||||||
|
return os.path.join(h[:2], h[2:4], h[4:6], h[6:])
|
||||||
def stream_path(f):
|
def stream_path(f):
|
||||||
h = f.movie.movieId
|
h = f.movie.movieId
|
||||||
return os.path.join('stream', h[:2], h[2:4], h[4:6], h[6:], f.profile)
|
return os.path.join('stream', movieid_path(h), f.profile)
|
||||||
|
|
||||||
class Stream(models.Model):
|
class Stream(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -5,10 +5,13 @@ from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns("backend.views",
|
urlpatterns = patterns("backend.views",
|
||||||
(r'^frame/(?P<id>.*)/(?P<position>.*)\.(?P<size>\d+).jpg$', 'frame'),
|
(r'^(?P<id>.*)/frame/(?P<size>\d+)/(?P<position>[0-9\.,]+).jpg$', 'frame'),
|
||||||
(r'^stream/(?P<id>.*).(?P<quality>.*).ogv$', 'video'),
|
(r'^(?P<id>.*)/(?P<profile>.*.webm)$', 'video'),
|
||||||
(r'^poster/(?P<id>.*)\.(?P<size>\d+)\.jpg$', 'poster'),
|
(r'^(?P<id>.*)/(?P<profile>.*.mp4)$', 'video'),
|
||||||
(r'^poster/(?P<id>.*)\.jpg$', 'poster'),
|
(r'^(?P<id>.*)/poster\.(?P<size>\d+)\.jpg$', 'poster'),
|
||||||
|
(r'^(?P<id>.*)/poster\.jpg$', 'poster'),
|
||||||
|
(r'^(?P<id>.*)/timelines/timeline\.(?P<size>\d+)\.(?P<position>\d+)\.png$', 'timeline'),
|
||||||
|
(r'^(?P<id>.*)/data/(?P<data>.+)\.json$', 'data'),
|
||||||
(r'^api/$', 'api'),
|
(r'^api/$', 'api'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -494,45 +494,6 @@ def api_getImdbId(request):
|
||||||
response = json_response(status=404, text='not found')
|
response = json_response(status=404, text='not found')
|
||||||
return render_to_json_response(response)
|
return render_to_json_response(response)
|
||||||
|
|
||||||
def poster(request, id, size=None):
|
|
||||||
print id, size
|
|
||||||
movie = get_object_or_404(models.Movie, movieId=id)
|
|
||||||
if movie.poster:
|
|
||||||
if size:
|
|
||||||
size = int(size)
|
|
||||||
poster_path = movie.poster.path.replace('.jpg', '.%d.jpg'%size)
|
|
||||||
if not os.path.exists(poster_path):
|
|
||||||
poster_size = max(movie.poster.width, movie.poster.height)
|
|
||||||
size = min(size, poster_size)
|
|
||||||
poster_path = movie.poster.path.replace('.jpg', '.%d.jpg'%size)
|
|
||||||
extract.resize_image(movie.poster.path, poster_path, size=size)
|
|
||||||
url = movie.poster.url.replace('.jpg', '.%d.jpg'%size)
|
|
||||||
elif movie.poster:
|
|
||||||
url = movie.poster.url
|
|
||||||
else:
|
|
||||||
url = movie.poster_url
|
|
||||||
if not url:
|
|
||||||
url = '/static/png/posterDark.48.png'
|
|
||||||
return redirect(url)
|
|
||||||
|
|
||||||
def video(request, id, quality):
|
|
||||||
movie = get_object_or_404(models.Movie, movieId=id)
|
|
||||||
if quality not in settings.VIDEO_ENCODING:
|
|
||||||
raise Http404
|
|
||||||
stream = getattr(movie, 'stream_'+quality)
|
|
||||||
response = HttpFileResponse(stream.path, content_type='video/ogg')
|
|
||||||
#FIXME: movie needs duration field
|
|
||||||
#response['Content-Duration'] = movie.duration
|
|
||||||
return response
|
|
||||||
|
|
||||||
def frame(request, id, position, size):
|
|
||||||
movie = get_object_or_404(models.Movie, movieId=id)
|
|
||||||
position = ox.time2ms(position)/1000
|
|
||||||
frame = movie.frame(position, int(size))
|
|
||||||
if not frame:
|
|
||||||
raise Http404
|
|
||||||
return HttpFileResponse(frame, content_type='image/jpeg')
|
|
||||||
|
|
||||||
def apidoc(request):
|
def apidoc(request):
|
||||||
'''
|
'''
|
||||||
this is used for online documentation at http://127.0.0.1:8000/api/
|
this is used for online documentation at http://127.0.0.1:8000/api/
|
||||||
|
@ -573,3 +534,53 @@ def apidoc(request):
|
||||||
context = RequestContext(request, {'api': api,
|
context = RequestContext(request, {'api': api,
|
||||||
'sitename': settings.SITENAME,})
|
'sitename': settings.SITENAME,})
|
||||||
return render_to_response('api.html', context)
|
return render_to_response('api.html', context)
|
||||||
|
|
||||||
|
def data(request, id, data):
|
||||||
|
movie = get_object_or_404(models.Movie, movieId=id)
|
||||||
|
response = {}
|
||||||
|
if data == 'video':
|
||||||
|
response = movie.get_stream()
|
||||||
|
return render_to_json_response(response)
|
||||||
|
|
||||||
|
#media delivery
|
||||||
|
def frame(request, id, position, size):
|
||||||
|
movie = get_object_or_404(models.Movie, movieId=id)
|
||||||
|
position = float(position.replace(',', '.'))
|
||||||
|
frame = movie.frame(position, int(size))
|
||||||
|
if not frame:
|
||||||
|
raise Http404
|
||||||
|
return HttpFileResponse(frame, content_type='image/jpeg')
|
||||||
|
|
||||||
|
def poster(request, id, size=128):
|
||||||
|
movie = get_object_or_404(models.Movie, movieId=id)
|
||||||
|
if size == 'large':
|
||||||
|
size = None
|
||||||
|
if movie.poster:
|
||||||
|
if size:
|
||||||
|
size = int(size)
|
||||||
|
poster_path = movie.poster.path.replace('.jpg', '.%d.jpg'%size)
|
||||||
|
if not os.path.exists(poster_path):
|
||||||
|
poster_size = max(movie.poster.width, movie.poster.height)
|
||||||
|
if size > poster_size:
|
||||||
|
return redirect('/%s/poster.large.jpg' % movie.movieId)
|
||||||
|
extract.resize_image(movie.poster.path, poster_path, size=size)
|
||||||
|
else:
|
||||||
|
poster_path = movie.poster.path
|
||||||
|
else:
|
||||||
|
poster_path = os.path.join(settings.STATIC_ROOT, 'png/posterDark.48.png')
|
||||||
|
return HttpFileResponse(poster_path, content_type='image/jpeg')
|
||||||
|
|
||||||
|
def timeline(request, id, size, position):
|
||||||
|
movie = get_object_or_404(models.Movie, movieId=id)
|
||||||
|
timeline = os.path.join(settings.MEDIA_ROOT, '%s.%s.%04d.png' %(movie.timeline_prefix, size, int(position)))
|
||||||
|
return HttpFileResponse(timeline, content_type='image/png')
|
||||||
|
|
||||||
|
def video(request, id, profile):
|
||||||
|
movie = get_object_or_404(models.Movie, movieId=id)
|
||||||
|
stream = get_object_or_404(movie.streams, profile=profile)
|
||||||
|
path = stream.video.path
|
||||||
|
content_type = path.endswith('.mp4') and 'video/mp4' or 'video/webm'
|
||||||
|
#url = 'http://127.0.0.1/pandora_media' + path[len(settings.MEDIA_ROOT):]
|
||||||
|
#return redirect(url)
|
||||||
|
return HttpFileResponse(path, content_type=content_type)
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,21 @@ DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
TEMPLATE_DEBUG = DEBUG
|
||||||
JSON_DEBUG = True
|
JSON_DEBUG = True
|
||||||
|
|
||||||
|
#with apache x-sendfile or lighttpd set this to True
|
||||||
XSENDFILE = False
|
XSENDFILE = False
|
||||||
|
|
||||||
|
XACCELREDIRECT = False
|
||||||
|
# with nginx:
|
||||||
|
#XACCELREDIRECT=[/some/path/, /protected/]
|
||||||
|
'''
|
||||||
|
this assumes the following configuration:
|
||||||
|
location /protected/ {
|
||||||
|
internal;
|
||||||
|
root /some/path/;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
('j', 'j@mailb.org'),
|
('j', 'j@mailb.org'),
|
||||||
)
|
)
|
||||||
|
|
181
pandora/static/css/timeline.css
Normal file
181
pandora/static/css/timeline.css
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#editor {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
#players {
|
||||||
|
//background: rgb(255, 192, 192);
|
||||||
|
}
|
||||||
|
#timelines {
|
||||||
|
background: rgb(192, 192, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.OxEditor .OxVideoPlayer {
|
||||||
|
position: absolute;
|
||||||
|
margin: 4px;
|
||||||
|
//background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.OxTimelineLarge {
|
||||||
|
position: absolute;
|
||||||
|
height: 72px;
|
||||||
|
margin: 0 4px 0 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge > div {
|
||||||
|
position: absolute;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge > div > img {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge .OxCut {
|
||||||
|
position: absolute;
|
||||||
|
top: 66px;
|
||||||
|
width: 2px;
|
||||||
|
height: 4px;
|
||||||
|
margin-left: -1px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge .OxMarkerPointIn {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
margin-left: -5px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge .OxMarkerPointOut {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge .OxMarkerPosition {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
width: 9px;
|
||||||
|
height: 5px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.OxTimelineLarge .OxSubtitle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 9px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
padding: 1px;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 10px;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-shadow: rgba(0, 0, 0, 1) 1px 1px 1px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
cursor: default;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 10;
|
||||||
|
-moz-box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||||
|
-webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.OxTimelineLarge .OxSubtitle.OxHighlight {
|
||||||
|
border-color: rgba(255, 255, 0, 1);
|
||||||
|
}
|
||||||
|
.OxTimelineSmall {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.OxTimelineSmall > div {
|
||||||
|
position: absolute;
|
||||||
|
height: 18px;
|
||||||
|
margin: 3px 4px 3px 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.OxTimelineSmall > div > img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.OxTimelineSmall > div > .OxTimelineSmallImage {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.OxTimelineSmall .OxMarkerPointIn {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
.OxTimelineSmall .OxMarkerPointOut {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OxVideoPlayer > .OxBar .OxInputGroup {
|
||||||
|
//width: 98px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer > .OxBar .OxButton {
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer > .OxBar .OxButton,
|
||||||
|
.OxVideoPlayer > .OxBar .OxInput,
|
||||||
|
.OxVideoPlayer > .OxBar .OxLabel {
|
||||||
|
padding: 0;
|
||||||
|
-moz-border-radius: 0;
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer > .OxBar .OxLabel {
|
||||||
|
//width: 22px;
|
||||||
|
//background: rgb(32, 32, 32);
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerFrame {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 16px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerFrame > div {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerFrame > .OxFrame {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerFrame > .OxPoster {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerPoint {
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerInTop {
|
||||||
|
left: 4px;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerInBottom {
|
||||||
|
left: 4px;
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerOutTop {
|
||||||
|
right: 4px;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxMarkerOutBottom {
|
||||||
|
right: 4px;
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
||||||
|
.OxVideoPlayer .OxSubtitle {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
//text-shadow: rgba(0, 0, 0, 1) 2px 2px 0px;
|
||||||
|
text-shadow: rgba(0, 0, 0, 1) 0 0 4px;
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
1299
pandora/static/js/timeline.js
Normal file
1299
pandora/static/js/timeline.js
Normal file
File diff suppressed because it is too large
Load diff
16
pandora/templates/timeline.html
Normal file
16
pandora/templates/timeline.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>timeline demo</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/oxjs/build/css/ox.ui.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/timeline.css"/>
|
||||||
|
<script type="text/javascript" src="/static/oxjs/build/js/jquery-1.4.2.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/jquery/jquery.videosupport.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/oxjs/build/js/ox.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/oxjs/build/js/ox.data.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/oxjs/build/js/ox.ui.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/timeline.js"></script>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
|
@ -15,6 +15,7 @@ urlpatterns = patterns('',
|
||||||
(r'^pandora.json$', 'app.views.pandora_json'),
|
(r'^pandora.json$', 'app.views.pandora_json'),
|
||||||
(r'^$', 'app.views.intro'),
|
(r'^$', 'app.views.intro'),
|
||||||
(r'^ra$', 'app.views.index'),
|
(r'^ra$', 'app.views.index'),
|
||||||
|
(r'^timeline$', 'app.views.timeline'),
|
||||||
(r'^r/(?P<key>.*)$', 'oxuser.views.recover'),
|
(r'^r/(?P<key>.*)$', 'oxuser.views.recover'),
|
||||||
(r'', include('backend.urls')),
|
(r'', include('backend.urls')),
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue