diff --git a/pandora/0xdb.json b/pandora/0xdb.json index e0aa3a22..36b3e36d 100644 --- a/pandora/0xdb.json +++ b/pandora/0xdb.json @@ -133,6 +133,13 @@ "group": true, "sort": "person" }, + { + "id": "parts", + "title": "Parts", + "type": "integer", + "columnWidth": 60, + "rightsLevel": 1 + }, { "id": "numberofactors", "title": "Number of Actors", @@ -561,6 +568,7 @@ }, "userLevels": ["guest", "member", "staff", "admin"], "video": { + "download": false, "formats": ["webm", "mp4"], "resolutions": [96] } diff --git a/pandora/archive/models.py b/pandora/archive/models.py index 42ea3556..9cfed69f 100644 --- a/pandora/archive/models.py +++ b/pandora/archive/models.py @@ -161,6 +161,7 @@ class File(models.Model): def save(self, *args, **kwargs): if self.auto: self.set_state() + self.available = self.streams.filter(source=None, available=True).count() > 0 super(File, self).save(*args, **kwargs) #upload and data handling @@ -498,6 +499,7 @@ class Stream(models.Model): resolution=resolution, format=f) if created: derivative.source = self + derivative.save() name = derivative.name() derivative.video.name = os.path.join(os.path.dirname(self.video.name), name) derivative.encode() @@ -528,6 +530,8 @@ class Stream(models.Model): else: self.aspect_ratio = 128/80 super(Stream, self).save(*args, **kwargs) + if self.available and not self.file.available: + self.file.save() def json(self): if settings.XSENDFILE or settings.XACCELREDIRECT: diff --git a/pandora/archive/tasks.py b/pandora/archive/tasks.py index 67259873..0b621c62 100644 --- a/pandora/archive/tasks.py +++ b/pandora/archive/tasks.py @@ -91,10 +91,11 @@ def update_files(user, volume, files): @task(queue="encoding") def process_stream(fileId): - file = models.Stream.objects.get(id=fileId) + file = models.File.objects.get(id=fileId) streams = file.streams.filter(source=None) - if streams.count() >0: + if streams.count() > 0: stream = streams[0] stream.make_timeline() stream.extract_derivatives() + file.item.update_timeline() return True diff --git a/pandora/item/models.py b/pandora/item/models.py index 73dfba2b..1ecc18b1 100644 --- a/pandora/item/models.py +++ b/pandora/item/models.py @@ -13,7 +13,7 @@ import unicodedata from urllib import quote from django.db import models -from django.db.models import Sum, Count +from django.db.models import Count, Q, Sum from django.core.files.base import ContentFile from django.utils import simplejson as json from django.conf import settings @@ -30,14 +30,15 @@ import ox.image import managers import utils import tasks -from archive import extract +from .timelines import join_timelines +from archive import extract from annotation.models import Annotation, Layer from person.models import get_name_sort from app.models import site_config -def get_item(info, user=None): +def get_item(info, user=None, async=False): ''' info dict with: imdbId, title, director, episode_title, season, series @@ -56,7 +57,10 @@ def get_item(info, user=None): } item.user = user item.save() - tasks.update_external.delay(item.itemId) + if async: + tasks.update_external.delay(item.itemId) + else: + item.update_external() else: q = Item.objects.all() for key in ('title', 'director', 'year'): @@ -789,17 +793,23 @@ class Item(models.Model): self.save() def streams(self): - return [video.streams.filter(source=None)[0] for video in self.main_videos()] + return [video.streams.filter(source=None, available=True)[0] + for video in self.main_videos()] def update_timeline(self, force=False): + config = site_config() streams = self.streams() self.make_timeline() self.data['cuts'] = extract.cuts(self.timeline_prefix) self.data['color'] = extract.average_color(self.timeline_prefix) #extract.timeline_strip(self, self.data['cuts'], stream.info, self.timeline_prefix[:-8]) + self.select_frame() self.make_local_poster() self.make_poster() self.make_icon() + if config['video']['download']: + self.make_torrent() + self.load_subtitles() self.rendered = streams != [] self.save() @@ -855,7 +865,8 @@ class Item(models.Model): def make_timeline(self): streams = self.streams() if len(streams) > 1: - print "FIXME, needs to build timeline from parts" + timelines = [s.timeline_prefix for s in self.streams()] + join_timelines(timelines, self.timeline_prefix) def make_poster(self, force=False): if not self.poster or force: @@ -960,6 +971,43 @@ class Item(models.Model): os.unlink(f) return icon + def load_subtitles(self): + layer = Layer.objects.get(name='subtitles') + Annotation.objects.filter(layer=layer,item=self).delete() + offset = 0 + language = '' + languages = [f.language for f in self.files.filter(is_main=True, is_subtitle=True, + available=True)] + if languages: + if 'en' in languages: + language = 'en' + elif '' in languages: + language = '' + else: + language = languages[0] + for f in self.files.filter(is_main=True, is_subtitle=True, + available=True, language=language).order_by('part'): + user = f.instances.all()[0].volume.user + for data in f.srt(offset): + annotation = Annotation( + item=f.item, + layer=layer, + start=data['in'], + end=data['out'], + value=data['value'], + user=user + ) + annotation.save() + duration = self.files.filter(Q(is_audio=True)|Q(is_video=True)) \ + .filter(is_main=True, available=True, part=f.part) + if duration: + duration = duration[0].duration + else: + Annotation.objects.filter(layer=layer,item=self).delete() + break + offset += duration + self.update_find() + def delete_item(sender, **kwargs): i = kwargs['instance'] i.delete_files() diff --git a/pandora/item/tasks.py b/pandora/item/tasks.py index 06a47d33..1d71eaac 100644 --- a/pandora/item/tasks.py +++ b/pandora/item/tasks.py @@ -3,7 +3,6 @@ from datetime import timedelta from celery.decorators import task, periodic_task -from django.db.models import Q import models @@ -32,39 +31,5 @@ def update_timeline(itemId): def load_subtitles(itemId): item = models.Item.objects.get(itemId=itemId) - layer = models.Layer.objects.get(name='subtitles') - models.Annotation.objects.filter(layer=layer,item=item).delete() - offset = 0 - language = '' - languages = [f.language for f in item.files.filter(is_main=True, is_subtitle=True, - available=True)] - if languages: - if 'en' in languages: - language = 'en' - elif '' in languages: - language = '' - else: - language = languages[0] - for f in item.files.filter(is_main=True, is_subtitle=True, - available=True, language=language).order_by('part'): - user = f.instances.all()[0].volume.user - for data in f.srt(offset): - annotation = models.Annotation( - item=f.item, - layer=layer, - start=data['in'], - end=data['out'], - value=data['value'], - user=user - ) - annotation.save() - duration = item.files.filter(Q(is_audio=True)|Q(is_video=True)) \ - .filter(is_main=True, available=True, part=f.part) - if duration: - duration = duration[0].duration - else: - models.Annotation.objects.filter(layer=layer,item=item).delete() - break - offset += duration - item.update_find() + item.load_subtitles() diff --git a/pandora/item/timelines.py b/pandora/item/timelines.py new file mode 100644 index 00000000..8e799beb --- /dev/null +++ b/pandora/item/timelines.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +from __future__ import division, with_statement + +from glob import glob + +import Image + +def loadTimeline(timeline_prefix, height=64): + files = sorted(glob('%s.%s.*.png' % (timeline_prefix, height))) + f = Image.open(files[0]) + width = f.size[0] + f = Image.open(files[-1]) + duration = f.size[0] + (len(files)-1)*width + timeline = Image.new("RGB", (duration, height)) + pos = 0 + for f in files: + part = Image.open(f) + timeline.paste(part, (pos, 0, pos + part.size[0], height)) + pos += part.size[0] + return timeline + +def makeTiles(timeline_prefix, height=16, width=3600): + files = glob('%s.64.*.png' % timeline_prefix) + fps = 25 + part_step = 60 + output_width = width + width = len(files) * part_step + timeline = Image.new("RGB", (width, height)) + + pos = 0 + for f in sorted(files): + part = Image.open(f) + part_width = int(part.size[0] / fps) + part = part.resize((part_width, height), Image.ANTIALIAS) + timeline.paste(part, (pos, 0, pos+part_width, height)) + pos += part_width + + timeline = timeline.crop((0, 0, pos, height)) + + pos = 0 + i = 0 + while pos < timeline.size[0]: + end = min(pos+output_width, timeline.size[0]) + timeline.crop((pos, 0, end, timeline.size[1])).save('%s.%s.%04d.png' % (timeline_prefix, timeline.size[1], i)) + pos += output_width + i += 1 + +def makeTimelineOverview(timeline_prefix, width, inpoint=0, outpoint=0, duration=-1, height=16): + input_scale = 25 + + timeline_file = '%s.%s.png' % (timeline_prefix, height) + if outpoint > 0: + timeline_file = '%s.overview.%s.%d-%d.png' % (timeline_prefix, height, inpoint, outpoint) + + timeline = loadTimeline(timeline_prefix) + duration = timeline.size[0] + + if inpoint<=0: + inpoint = 0 + else: + inpoint = inpoint * input_scale + if outpoint<=0: + outpoint = duration + else: + outpoint = outpoint * input_scale + + timeline = timeline.crop((inpoint, 0, outpoint, timeline.size[1])).resize((width, height), Image.ANTIALIAS) + timeline.save(timeline_file) + + +def join_timelines(timelines, prefix): + height = 64 + width = 1500 + + tiles = [] + for timeline in timelines: + tiles += sorted(glob('%s.%s.*.png'%(timeline, height))) + + tiles = map(Image.open, tiles) + duration = sum(map(lambda i: i.size[0], tiles)) + timeline = Image.new("RGB", (duration, height)) + pos = 0 + for tile in tiles: + timeline.paste(tile, (pos, 0, pos+tile.size[0], height)) + pos += tile.size[0] + + pos = 0 + i = 0 + while pos < timeline.size[0]: + end = min(pos+width, timeline.size[0]) + timeline_name = '%s.%s.%04d.png' % (prefix, timeline.size[1], i) + timeline.crop((pos, 0, end, timeline.size[1])).save(timeline_name) + pos += width + i += 1 + + makeTiles(prefix, 16, 3600) + makeTimelineOverview(prefix, 1920, height=16) + makeTimelineOverview(prefix, 1920, height=64) + diff --git a/pandora/padma.json b/pandora/padma.json index 4d19c792..d83fac99 100644 --- a/pandora/padma.json +++ b/pandora/padma.json @@ -446,7 +446,8 @@ }, "userLevels": ["guest", "member", "staff", "admin"], "video": { - "formats": ["webm", "h264"], + "download": true, + "formats": ["webm", "mp4"], "resolutions": [480, 240, 96] } } diff --git a/static/jquery b/static/jquery deleted file mode 120000 index b88f0d53..00000000 --- a/static/jquery +++ /dev/null @@ -1 +0,0 @@ -js/jquery/ \ No newline at end of file diff --git a/static/js/jquery/jquery.tmpl.min.js b/static/js/jquery/jquery.tmpl.min.js deleted file mode 100644 index f08e81dc..00000000 --- a/static/js/jquery/jquery.tmpl.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},h=0,c=0,l=[];function g(e,d,g,i){var c={data:i||(d?d.data:{}),_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};e&&a.extend(c,e,{nodes:[],parent:d});if(g){c.tmpl=g;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++h;(l.length?f:b)[h]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a.fn[d].apply(a(i[h]),k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,l,j){if(d[0]&&d[0].nodeType){var f=a.makeArray(arguments),g=d.length,i=0,h;while(i1)f[0]=[a.makeArray(d)];if(h&&c)f[2]=function(b){a.tmpl.afterManip(this,b,j)};r.apply(this,f)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var j,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(i(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);j=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(i(c,null,j)):j},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function i(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:i(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=j(c).concat(b);if(d)b=b.concat(j(d))});return b?b:j(c)}function j(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,j,d,b,c,e){var i=a.tmpl.tag[j],h,f,g;if(!i)throw"Template command not found: "+j;h=i._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=k(b);e=e?","+k(e)+")":c?")":"";f=c?b.indexOf(".")>-1?b+c:"("+b+").call($item"+e:b;g=c?f:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else g=f=h.$1||"null";d=k(d);return"');"+i[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(g).split("$1").join(f).split("$2").join(d?d.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(d,c,b,a){a=a?","+a+")":b?")":"";return a?"("+c+").call($item"+a:d}):h.$2||"")+"_.push('"})+"');}return _;")}function n(c,b){c._wrap=i(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function k(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,i;for(e=0,p=o.length;e=0;i--)m(j[i]);m(k)}function m(j){var p,i=j,k,e,m;if(m=j.getAttribute(d)){while(i.parentNode&&(i=i.parentNode).nodeType===1&&!(p=i.getAttribute(d)));if(p!==m){i=i.parentNode?i.nodeType===11?0:i.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[i]||f[i],null,true);e.key=++h;b[h]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;i=a.data(j.parentNode,"tmplItem");i=i?i.key:0}if(e){k=e;while(k&&k.key!=i){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent,null,true)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery) \ No newline at end of file diff --git a/static/js/pandora/ui/infoView.js b/static/js/pandora/ui/infoView.js index a89b40ce..20f2c9b4 100644 --- a/static/js/pandora/ui/infoView.js +++ b/static/js/pandora/ui/infoView.js @@ -502,7 +502,7 @@ pandora.ui.infoView = function(data) { $reflectionIcon.attr({src: src}); iconSize = iconSize == 256 ? 512 : 256; iconRatio = pandora.user.ui.icons == 'posters' - ? data.poster.width / data.poster.height : 1; + ? data.posterRatio : 1; toggleIconSize(); pandora.user.level == 'admin' && $list.replaceWith($list = renderList()); }; @@ -515,4 +515,4 @@ pandora.ui.infoView = function(data) { return that; -} \ No newline at end of file +}