545 lines
19 KiB
Python
545 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vi:si:et:sw=4:sts=4:ts=4
|
|
from __future__ import division
|
|
|
|
import os.path
|
|
import re
|
|
import time
|
|
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.contrib.auth.models import User
|
|
from django.conf import settings
|
|
from django.db.models.signals import pre_delete
|
|
|
|
from ox.django import fields
|
|
import ox
|
|
from ox.normalize import canonicalTitle
|
|
import chardet
|
|
|
|
from app.models import site_config
|
|
from item import utils
|
|
from item.models import Item
|
|
from person.models import get_name_sort
|
|
|
|
import extract
|
|
|
|
|
|
class File(models.Model):
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
modified = models.DateTimeField(auto_now=True)
|
|
|
|
active = models.BooleanField(default=False)
|
|
auto = models.BooleanField(default=True)
|
|
|
|
oshash = models.CharField(max_length=16, unique=True)
|
|
item = models.ForeignKey(Item, related_name='files')
|
|
|
|
name = models.CharField(max_length=2048, default="") # canoncial path/file
|
|
folder = models.CharField(max_length=2048, default="") # canoncial path/file
|
|
sort_name = models.CharField(max_length=2048, default="") # sort name
|
|
|
|
type = models.CharField(default="", max_length=255)
|
|
part = models.IntegerField(null=True)
|
|
version = models.CharField(default="", max_length=255) # sort path/file name
|
|
language = models.CharField(default="", max_length=8)
|
|
|
|
season = models.IntegerField(default=-1)
|
|
episode = models.IntegerField(default=-1)
|
|
|
|
size = models.BigIntegerField(default=0)
|
|
duration = models.FloatField(null=True)
|
|
|
|
info = fields.DictField(default={})
|
|
|
|
video_codec = models.CharField(max_length=255)
|
|
pixel_format = models.CharField(max_length=255)
|
|
display_aspect_ratio = models.CharField(max_length=255)
|
|
width = models.IntegerField(default = 0)
|
|
height = models.IntegerField(default = 0)
|
|
framerate = models.CharField(max_length=255)
|
|
|
|
audio_codec = models.CharField(max_length=255)
|
|
channels = models.IntegerField(default=0)
|
|
samplerate = models.IntegerField(default=0)
|
|
|
|
bits_per_pixel = models.FloatField(default=-1)
|
|
pixels = models.BigIntegerField(default=0)
|
|
|
|
#This is true if derivative is available or subtitles where uploaded
|
|
available = models.BooleanField(default = False)
|
|
wanted = models.BooleanField(default = False)
|
|
uploading = models.BooleanField(default = False)
|
|
|
|
is_audio = models.BooleanField(default=False)
|
|
is_video = models.BooleanField(default=False)
|
|
is_subtitle = models.BooleanField(default=False)
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
def set_state(self):
|
|
self.name = self.get_name()
|
|
self.folder = self.get_folder()
|
|
self.sort_name = utils.sort_string(canonicalTitle(self.name))
|
|
|
|
if not os.path.splitext(self.name)[-1] in (
|
|
'.srt', '.rar', '.sub', '.idx', '.txt', '.jpg', '.png', '.nfo') \
|
|
and self.info:
|
|
for key in ('duration', 'size'):
|
|
setattr(self, key, self.info.get(key, 0))
|
|
|
|
if 'video' in self.info and self.info['video'] and \
|
|
'width' in self.info['video'][0]:
|
|
video = self.info['video'][0]
|
|
self.video_codec = video['codec']
|
|
self.width = video['width']
|
|
self.height = video['height']
|
|
self.framerate = video['framerate']
|
|
if 'display_aspect_ratio' in video:
|
|
self.display_aspect_ratio = video['display_aspect_ratio']
|
|
else:
|
|
self.display_aspect_ratio = "%s:%s" % (self.width, self.height)
|
|
self.is_video = True
|
|
self.is_audio = False
|
|
if self.name.endswith('.jpg') or \
|
|
self.name.endswith('.png') or \
|
|
self.duration == 0.04:
|
|
self.is_video = False
|
|
else:
|
|
self.is_video = False
|
|
self.display_aspect_ratio = "4:3"
|
|
self.width = '320'
|
|
self.height = '240'
|
|
if 'audio' in self.info and self.info['audio'] and self.duration > 0:
|
|
audio = self.info['audio'][0]
|
|
self.audio_codec = audio['codec']
|
|
self.samplerate = audio.get('samplerate', 0)
|
|
self.channels = audio.get('channels', 0)
|
|
|
|
if not self.is_video:
|
|
self.is_audio = True
|
|
else:
|
|
self.is_audio = False
|
|
self.audio_codec = ''
|
|
self.sampleate = 0
|
|
self.channels = 0
|
|
|
|
if self.framerate:
|
|
self.pixels = int(self.width * self.height * float(utils.parse_decimal(self.framerate)) * self.duration)
|
|
|
|
else:
|
|
self.is_video = os.path.splitext(self.name)[-1] in ('.avi', '.mkv', '.dv', '.ogv', '.mpeg', '.mov', '.webm')
|
|
self.is_audio = os.path.splitext(self.name)[-1] in ('.mp3', '.wav', '.ogg', '.flac', '.oga')
|
|
self.is_subtitle = os.path.splitext(self.name)[-1] in ('.srt', )
|
|
|
|
if self.name.endswith('.srt'):
|
|
self.is_subtitle = True
|
|
self.is_audio = False
|
|
self.is_video = False
|
|
else:
|
|
self.is_subtitle = False
|
|
|
|
self.type = self.get_type()
|
|
self.language = self.get_language()
|
|
self.part = self.get_part()
|
|
|
|
if self.type not in ('audio', 'video'):
|
|
self.duration = None
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.auto:
|
|
self.set_state()
|
|
if self.is_subtitle:
|
|
self.available = self.data and True or False
|
|
else:
|
|
self.available = not self.uploading and \
|
|
self.streams.filter(source=None, available=True).count() > 0
|
|
super(File, self).save(*args, **kwargs)
|
|
|
|
#upload and data handling
|
|
data = models.FileField(null=True, blank=True,
|
|
upload_to=lambda f, x: f.path('data.bin'))
|
|
|
|
def path(self, name):
|
|
h = self.oshash
|
|
return os.path.join('files', h[:2], h[2:4], h[4:6], h[6:], name)
|
|
|
|
def contents(self):
|
|
if self.data != None:
|
|
self.data.seek(0)
|
|
return self.data.read()
|
|
return None
|
|
|
|
def srt(self, offset=0):
|
|
|
|
def _detectEncoding(fp):
|
|
bomDict={ # bytepattern : name
|
|
(0x00, 0x00, 0xFE, 0xFF): "utf_32_be",
|
|
(0xFF, 0xFE, 0x00, 0x00): "utf_32_le",
|
|
(0xFE, 0xFF, None, None): "utf_16_be",
|
|
(0xFF, 0xFE, None, None): "utf_16_le",
|
|
(0xEF, 0xBB, 0xBF, None): "utf_8",
|
|
}
|
|
|
|
# go to beginning of file and get the first 4 bytes
|
|
oldFP = fp.tell()
|
|
fp.seek(0)
|
|
(byte1, byte2, byte3, byte4) = tuple(map(ord, fp.read(4)))
|
|
|
|
# try bom detection using 4 bytes, 3 bytes, or 2 bytes
|
|
bomDetection = bomDict.get((byte1, byte2, byte3, byte4))
|
|
if not bomDetection:
|
|
bomDetection = bomDict.get((byte1, byte2, byte3, None))
|
|
if not bomDetection:
|
|
bomDetection = bomDict.get((byte1, byte2, None, None))
|
|
|
|
## if BOM detected, we're done :-)
|
|
fp.seek(oldFP)
|
|
if bomDetection:
|
|
return bomDetection
|
|
|
|
encoding = 'latin-1'
|
|
#more character detecting magick using http://chardet.feedparser.org/
|
|
fp.seek(0)
|
|
rawdata = fp.read()
|
|
encoding = chardet.detect(rawdata)['encoding']
|
|
fp.seek(oldFP)
|
|
return encoding
|
|
|
|
def parseTime(t):
|
|
return offset + ox.time2ms(t.replace(',', '.')) / 1000
|
|
|
|
srt = []
|
|
|
|
f = open(self.data.path)
|
|
encoding = _detectEncoding(f)
|
|
data = f.read()
|
|
f.close()
|
|
data = data.replace('\r\n', '\n')
|
|
try:
|
|
data = unicode(data, encoding)
|
|
except:
|
|
try:
|
|
data = unicode(data, 'latin-1')
|
|
except:
|
|
print "failed to detect encoding, giving up"
|
|
return srt
|
|
|
|
srts = re.compile('(\d\d:\d\d:\d\d[,.]\d\d\d)\s*-->\s*(\d\d:\d\d:\d\d[,.]\d\d\d)\s*(.+?)\n\n', re.DOTALL)
|
|
i = 0
|
|
for s in srts.findall(data):
|
|
_s = {'id': str(i),
|
|
'in': parseTime(s[0]), 'out': parseTime(s[1]), 'value': s[2].strip()}
|
|
if srt and srt[-1]['out'] > _s['in']:
|
|
srt[-1]['out'] = _s['in']
|
|
srt.append(_s)
|
|
i += 1
|
|
return srt
|
|
|
|
def editable(self, user):
|
|
#FIXME: check that user has instance of this file
|
|
return True
|
|
|
|
def save_chunk(self, chunk, chunk_id=-1, done=False):
|
|
if not self.available:
|
|
config = site_config()['video']
|
|
stream, created = Stream.objects.get_or_create(
|
|
file=self,
|
|
resolution=config['resolutions'][0],
|
|
format=config['formats'][0])
|
|
if created:
|
|
stream.video.save(stream.name(), chunk)
|
|
else:
|
|
f = open(stream.video.path, 'a')
|
|
#FIXME: should check that chunk_id/offset is right
|
|
f.write(chunk.read())
|
|
f.close()
|
|
if done:
|
|
stream.available = True
|
|
stream.save()
|
|
return True
|
|
return False
|
|
|
|
def json(self, keys=None, user=None):
|
|
if keys and not 'instances' in keys:
|
|
keys.append('instances')
|
|
resolution = (self.width, self.height)
|
|
if resolution == (0, 0):
|
|
resolution = None
|
|
duration = self.duration
|
|
if self.get_type() != 'video':
|
|
duration = None
|
|
data = {
|
|
'available': self.available,
|
|
'duration': duration,
|
|
'framerate': self.framerate,
|
|
#'height': self.height,
|
|
#'width': self.width,
|
|
'resolution': resolution,
|
|
'id': self.oshash,
|
|
'samplerate': self.samplerate,
|
|
'video_codec': self.video_codec,
|
|
'audio_codec': self.audio_codec,
|
|
'name': self.name,
|
|
'size': self.size,
|
|
#'info': self.info,
|
|
'users': list(set([u.username
|
|
for u in User.objects.filter(volumes__files__in=self.instances.all())])),
|
|
'instances': [i.json() for i in self.instances.all()],
|
|
'folder': self.get_folder(),
|
|
'type': self.get_type(),
|
|
'part': self.get_part()
|
|
}
|
|
if keys:
|
|
for k in data.keys():
|
|
if k not in keys:
|
|
del data[k]
|
|
return data
|
|
|
|
def get_part(self):
|
|
#FIXME: this breaks for sub/idx/srt
|
|
if os.path.splitext(self.name)[-1] in ('.sub', '.idx', '.srt'):
|
|
name = os.path.splitext(self.name)[0]
|
|
if self.language:
|
|
name = name[-(len(self.language)+1)]
|
|
qs = self.item.files.filter(Q(is_video=True)|Q(is_audio=True),
|
|
active=True, name__startswith=name)
|
|
if qs.count()>0:
|
|
return qs[0].part
|
|
if self.active:
|
|
files = list(self.item.files.filter(type=self.type, language=self.language,
|
|
active=self.active).order_by('sort_name'))
|
|
if self in files:
|
|
return files.index(self) + 1
|
|
return None
|
|
|
|
def get_type(self):
|
|
if self.is_video:
|
|
return 'video'
|
|
if self.is_audio:
|
|
return 'audio'
|
|
if self.is_subtitle or os.path.splitext(self.name)[-1] in ('.sub', '.idx'):
|
|
return 'subtitle'
|
|
return 'unknown'
|
|
|
|
def get_instance(self):
|
|
#FIXME: what about other instances?
|
|
if self.instances.all().count() > 0:
|
|
return self.instances.all()[0]
|
|
return None
|
|
|
|
def get_folder(self):
|
|
instance = self.get_instance()
|
|
if instance:
|
|
return instance.folder
|
|
name = os.path.splitext(self.get_name())[0]
|
|
name = name.replace('. ', '||').split('.')[0].replace('||', '. ')
|
|
if self.item:
|
|
if settings.USE_IMDB:
|
|
director = self.item.get('director', ['Unknown Director'])
|
|
director = map(get_name_sort, director)
|
|
director = u'; '.join(director)
|
|
director = re.sub(r'[:\\/]', '_', director)
|
|
name = os.path.join(director, name)
|
|
year = self.item.get('year')
|
|
if year:
|
|
name += u' (%s)' % year
|
|
name = os.path.join(name[0].upper(), name)
|
|
return name
|
|
return u''
|
|
|
|
def get_name(self):
|
|
instance = self.get_instance()
|
|
if instance:
|
|
return instance.name
|
|
if self.item:
|
|
name = self.item.get('title', 'Untitled')
|
|
name = re.sub(r'[:\\/]', '_', name)
|
|
if not name:
|
|
name = 'Untitled'
|
|
ext = '.unknown'
|
|
return name + ext
|
|
|
|
def get_language(self):
|
|
language = self.name.split('.')
|
|
if len(language) >= 3 and len(language[-2]) == 2:
|
|
return language[-2]
|
|
return ''
|
|
|
|
def delete_file(sender, **kwargs):
|
|
f = kwargs['instance']
|
|
#FIXME: delete streams here
|
|
if f.data:
|
|
f.data.delete()
|
|
pre_delete.connect(delete_file, sender=File)
|
|
|
|
class Volume(models.Model):
|
|
|
|
class Meta:
|
|
unique_together = ("user", "name")
|
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
modified = models.DateTimeField(auto_now=True)
|
|
|
|
user = models.ForeignKey(User, related_name='volumes')
|
|
name = models.CharField(max_length=1024)
|
|
|
|
def __unicode__(self):
|
|
return u"%s's %s"% (self.user, self.name)
|
|
|
|
class Instance(models.Model):
|
|
|
|
class Meta:
|
|
unique_together = ("name", "folder", "volume")
|
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
modified = models.DateTimeField(auto_now=True)
|
|
|
|
atime = models.IntegerField(default=lambda: int(time.time()), editable=False)
|
|
ctime = models.IntegerField(default=lambda: int(time.time()), editable=False)
|
|
mtime = models.IntegerField(default=lambda: int(time.time()), editable=False)
|
|
|
|
name = models.CharField(max_length=2048)
|
|
folder = models.CharField(max_length=2048)
|
|
extra = models.BooleanField(default=False)
|
|
|
|
file = models.ForeignKey(File, related_name='instances')
|
|
volume = models.ForeignKey(Volume, related_name='files')
|
|
|
|
def __unicode__(self):
|
|
return u"%s's %s <%s>"% (self.volume.user, self.name, self.file.oshash)
|
|
|
|
@property
|
|
def itemId(self):
|
|
return File.objects.get(oshash=self.oshash).itemId
|
|
|
|
def json(self):
|
|
return {
|
|
'user': self.volume.user.username,
|
|
'volume': self.volume.name,
|
|
'folder': self.folder,
|
|
'name': self.name
|
|
}
|
|
|
|
def frame_path(frame, name):
|
|
ext = os.path.splitext(name)[-1]
|
|
name = "%s%s" % (frame.position, ext)
|
|
return frame.file.path(name)
|
|
|
|
|
|
class Frame(models.Model):
|
|
|
|
class Meta:
|
|
unique_together = ("file", "position")
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
modified = models.DateTimeField(auto_now=True)
|
|
file = models.ForeignKey(File, related_name="frames")
|
|
position = models.FloatField()
|
|
frame = models.ImageField(default=None, null=True, upload_to=frame_path)
|
|
|
|
'''
|
|
def save(self, *args, **kwargs):
|
|
name = "%d.jpg" % self.position
|
|
if file.name != name:
|
|
#FIXME: frame path should be renamed on save to match current position
|
|
super(Frame, self).save(*args, **kwargs)
|
|
'''
|
|
|
|
def __unicode__(self):
|
|
return u'%s/%s' % (self.file, self.position)
|
|
|
|
def delete_frame(sender, **kwargs):
|
|
f = kwargs['instance']
|
|
if f.frame:
|
|
f.frame.delete()
|
|
pre_delete.connect(delete_frame, sender=Frame)
|
|
|
|
|
|
class Stream(models.Model):
|
|
|
|
class Meta:
|
|
unique_together = ("file", "resolution", "format")
|
|
|
|
file = models.ForeignKey(File, related_name='streams')
|
|
resolution = models.IntegerField(default=96)
|
|
format = models.CharField(max_length=255, default='webm')
|
|
|
|
video = models.FileField(default=None, blank=True, upload_to=lambda f, x: f.path(x))
|
|
source = models.ForeignKey('Stream', related_name='derivatives', default=None, null=True)
|
|
available = models.BooleanField(default=False)
|
|
info = fields.DictField(default={})
|
|
duration = models.FloatField(default=0)
|
|
aspect_ratio = models.FloatField(default=0)
|
|
|
|
@property
|
|
def timeline_prefix(self):
|
|
return os.path.join(settings.MEDIA_ROOT, self.path(), 'timeline')
|
|
|
|
def name(self):
|
|
return u"%sp.%s" % (self.resolution, self.format)
|
|
|
|
def __unicode__(self):
|
|
return u"%s/%s" % (self.file, self.name())
|
|
|
|
def path(self, name=''):
|
|
return self.file.path(name)
|
|
|
|
def extract_derivatives(self):
|
|
config = site_config()['video']
|
|
for resolution in config['resolutions']:
|
|
for f in config['formats']:
|
|
derivative, created = Stream.objects.get_or_create(file=self.file,
|
|
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()
|
|
derivative.save()
|
|
return True
|
|
|
|
def encode(self):
|
|
if self.source:
|
|
video = self.source.video.path
|
|
target = self.video.path
|
|
info = ox.avinfo(video)
|
|
if extract.stream(video, target, self.name(), info):
|
|
self.available = True
|
|
else:
|
|
self.available = False
|
|
self.save()
|
|
|
|
def make_timeline(self):
|
|
if self.available and not self.source:
|
|
extract.timeline(self.video.path, self.timeline_prefix)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.video and not self.info:
|
|
self.info = ox.avinfo(self.video.path)
|
|
self.duration = self.info.get('duration', 0)
|
|
if 'video' in self.info and self.info['video']:
|
|
self.aspect_ratio = self.info['video'][0]['width'] / self.info['video'][0]['height']
|
|
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:
|
|
base_url = '/%s' % self.file.item.itemId
|
|
else:
|
|
base_url = os.path.dirname(self.video.url)
|
|
return {
|
|
'duration': self.duration,
|
|
'aspectRatio': self.aspect_ratio,
|
|
'baseUrl': base_url
|
|
}
|
|
|
|
def delete_stream(sender, **kwargs):
|
|
f = kwargs['instance']
|
|
if f.video:
|
|
f.video.delete()
|
|
pre_delete.connect(delete_stream, sender=Stream)
|