render form site

This commit is contained in:
j 2018-02-09 12:20:28 +01:00
parent 8325174995
commit 748e6b3d6d
2 changed files with 240 additions and 85 deletions

165
edit.py
View file

@ -1,20 +1,50 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os from argparse import ArgumentParser
import getpass
import json import json
import math import math
import os
import ox import ox
import ox.web.auth import ox.web.auth
import sys
import urllib.parse
import urllib.request
base_url = None
stream_resolution = 480
prefix = '/mnt'
render = './cache'
pandora_client_config = {}
use_local = False
class API(ox.API):
def save_url(self, url, filename, overwrite=False):
if not os.path.exists(filename) or overwrite:
dirname = os.path.dirname(filename)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
chunk_size = 16 * 1024
request = urllib.request.Request(url)
remote = self._opener.open(request)
with open(filename, 'wb') as f:
for chunk in iter(lambda: remote.read(chunk_size), b''):
f.write(chunk)
base_url = 'https://0xdb.org'
credentials = ox.web.auth.get('0xdb.org')
prefix = '/Cinema'
if os.path.exists('files.json'): if os.path.exists('files.json'):
files = json.load(open('files.json')) files = json.load(open('files.json'))
else: else:
files = {} files = {}
def get_info(api, oshash): def get_volume_prefix(name):
path = pandora_client_config.get('volumes', {}).get(name)
if path:
return path
else:
return os.path.join(prefix, name)
def get_info(api, oshash, item, part):
if oshash not in files: if oshash not in files:
r = api.findMedia({ r = api.findMedia({
'query': { 'query': {
@ -23,12 +53,15 @@ def get_info(api, oshash):
'keys': ['id', 'instances', 'resolution'] 'keys': ['id', 'instances', 'resolution']
})['data'] })['data']
if not r['items'][0]['instances']: if not r['items'][0]['instances']:
print(r) print(r, item, part)
if use_local:
raise Exception('item without instance') raise Exception('item without instance')
files[oshash] = { files[oshash] = {
'path': os.path.join(prefix, r['items'][0]['instances'][0]['path']),
'resolution': r['items'][0]['resolution'] 'resolution': r['items'][0]['resolution']
} }
if r['items'][0]['instances']:
volume_prefix = get_volume_prefix(r['items'][0]['instances'][0]['volume'])
files[oshash]['path'] = os.path.join(volume_prefix, r['items'][0]['instances'][0]['path'])
return files[oshash] return files[oshash]
def normalize(name): def normalize(name):
@ -45,7 +78,7 @@ def sort_clips(edit, sort):
s = reversed(s) s = reversed(s)
elif sort in [ elif sort in [
'id', 'index', 'in', 'out', 'duration', 'id', 'index', 'in', 'out', 'duration',
'title', 'director', 'year', 'videoRatio' 'title', 'director', 'year', 'videoRatio',
]: ]:
s = sorted(clips, key=lambda c: (str(c.get(sort, last)), c['title'], c['in'], c['out'])) s = sorted(clips, key=lambda c: (str(c.get(sort, last)), c['title'], c['in'], c['out']))
if reverse: if reverse:
@ -55,31 +88,91 @@ def sort_clips(edit, sort):
'edit': edit['id'], 'edit': edit['id'],
'sort': [{'key': sort, 'operator': '-' if reverse else '+'}] 'sort': [{'key': sort, 'operator': '-' if reverse else '+'}]
})['data']['clips'] })['data']['clips']
print(set(c['id'] for c in clips) - set(ids)) #print(set(c['id'] for c in clips) - set(ids))
print(set(ids) - set(c['id'] for c in clips)) #print(set(ids) - set(c['id'] for c in clips))
s = sorted(clips, key=lambda c: ids.index(c['id']) if c['id'] in ids else -1) s = sorted(clips, key=lambda c: ids.index(c['id']) if c['id'] in ids else -1)
return s return s
def cache_clips(api, videos):
for clip in videos:
out = '%s/%s.mp4' % (render, clip['oshash'])
if 'path' in clip:
clip['src'] = clip['path']
clip['path'] = out
if not os.path.exists(out):
print(clip['url'], out)
api.save_url(clip['url'], out)
if __name__ == '__main__': if __name__ == '__main__':
import sys usage = "usage: %(prog)s [options] edit-url"
edit_id = sys.argv[1] parser = ArgumentParser(usage=usage)
if len(sys.argv) > 2: parser.add_argument('-p', '--prefix', dest='prefix', type=str,
sort_by = sys.argv[2] help="prefix to use instead of pandora_client config", default='')
else: parser.add_argument('-s', '--source', dest='source', type=str,
sort_by = 'year' help="source, local or site", default='site')
parser.add_argument('-r', '--resolution', dest='stream_resolution', type=int,
help="resolution of streams to download i.e. 480, 240, 96 default 480", default=480)
parser.add_argument('-c', '--config', dest='config',
help='config.json containing config',
default='~/.ox/client.json')
parser.add_argument('url', metavar='url', type=str,
help='edit url')
opts = parser.parse_args()
edit_url = opts.url
use_local = opts.source == 'local'
prefix = opts.prefix
parts = edit_url.split('/')
site = parts[2]
base_url = '/'.join(parts[:3])
edit_id = urllib.parse.unquote(parts[4]).replace('_', ' ').replace('\t', '_')
sort_by = parts[6] if len(parts) >= 7 else 'year'
stream_resolution = opts.stream_resolution
api = ox.API(base_url + '/api/') credentials = None
api.signin(**credentials) config = os.path.expanduser(opts.config)
edit = api.getEdit(id=edit_id)['data'] if config and os.path.exists(config):
with open(config) as fd:
data = json.load(fd)
if data['url'].startswith(base_url):
pandora_client_config = data
credentials = {
'username': data['username'],
'password': data['password']
}
if not credentials:
try:
credentials = ox.web.auth.get(site)
except:
credentials = {}
credentials['username'] = input('Username: ')
credentials['password'] = getpass.getpass('Password: ')
ox.web.auth.update(site, credentials)
api = API(base_url + '/api/')
r = api.signin(**credentials)
if 'errors' in r.get('data', {}):
for kv in r['data']['errors'].items():
print('%s: %s' % kv)
sys.exit(1)
print('Edit:', edit_id, 'Sort:', sort_by)
r = api.getEdit(id=edit_id)
if 'data' not in r:
print(r)
sys.exit(1)
if r.get('status', {}).get('code') == 404:
print(r.get('status', {}).get('text'))
sys.exit(1)
edit = r['data']
videos = [] videos = []
subtitles = [] subtitles = []
position = 0 position = 0
for clip in sort_clips(edit, sort_by): for clip in sort_clips(edit, sort_by):
clip_subtitles = [] clip_subtitles = []
for sub in clip['layers']['subtitles']: for sub in clip['layers'].get('subtitles', []):
subtitles.append({ subtitles.append({
'in': position, 'in': position,
'out': position + (sub['out'] - sub['in']), 'out': position + (sub['out'] - sub['in']),
@ -99,38 +192,53 @@ if __name__ == '__main__':
stream_out = math.ceil(stream_out / (1/25)) * 1/25 stream_out = math.ceil(stream_out / (1/25)) * 1/25
part_pos += duration part_pos += duration
info = get_info(api, clip['streams'][i]) info = get_info(api, clip['streams'][i], clip['item'], i+1)
if stream_out > stream_in:
videos.append({ videos.append({
'oshash': clip['streams'][i], 'oshash': clip['streams'][i],
'path': os.path.join(prefix, info['path']), 'url': '%s/%s/%sp%s.mp4' % (base_url, clip['item'], stream_resolution, i + 1),
'item': clip['item'],
'annotation': clip.get('annotation'),
'resolution': info['resolution'], 'resolution': info['resolution'],
'in': stream_in, 'in': stream_in,
'out': stream_out, 'out': stream_out,
'subtitles': '\n'.join(clip_subtitles)
}) })
if 'path' in info:
videos[-1]['path'] = os.path.join(prefix, info['path'])
if clip_subtitles:
videos[-1]['subtitles'] = '\n'.join(clip_subtitles)
elif clip['out'] > part_pos: elif clip['out'] > part_pos:
stream_in = part_pos stream_in = 0
stream_out = min(clip['out'] - part_pos, duration) stream_out = min(clip['out'] - part_pos, duration)
stream_in = math.ceil(stream_in / (1/25)) * 1/25 stream_in = math.ceil(stream_in / (1/25)) * 1/25
stream_out = math.ceil(stream_out / (1/25)) * 1/25 stream_out = math.ceil(stream_out / (1/25)) * 1/25
part_pos += duration part_pos += duration
info = get_info(api, clip['streams'][i]) info = get_info(api, clip['streams'][i], clip['item'], i+1)
if stream_out > stream_in:
videos.append({ videos.append({
'oshash': clip['streams'][i], 'oshash': clip['streams'][i],
'path': os.path.join(prefix, info['path']), 'url': '%s/%s/%sp%s.mp4' % (base_url, clip['item'], stream_resolution, i),
'resolution': info['resolution'], 'resolution': info['resolution'],
'item': clip['item'],
'annotation': clip.get('annotation'),
'in': stream_in, 'in': stream_in,
'out': stream_out, 'out': stream_out,
'subtitles': '\n'.join(clip_subtitles)
}) })
if 'path' in info:
videos[-1]['path'] = os.path.join(prefix, info['path'])
if clip_subtitles:
videos[-1]['subtitles'] = '\n'.join(clip_subtitles)
position += clip['duration'] position += clip['duration']
position = math.ceil(position / (1/25)) * 1/25 position = math.ceil(position / (1/25)) * 1/25
if not use_local:
cache_clips(api, videos)
name = normalize(edit_id) name = normalize(edit_id)
if sort_by != 'year': if sort_by != 'year':
name += '_' + sort_by name += '_' + sort_by
if subtitles:
with open('%s.srt' % name, 'wb') as fd: with open('%s.srt' % name, 'wb') as fd:
fd.write(ox.srt.encode(subtitles)) fd.write(ox.srt.encode(subtitles))
with open('%s.json' % name, 'w') as fd: with open('%s.json' % name, 'w') as fd:
@ -138,3 +246,4 @@ if __name__ == '__main__':
with open('files.json', 'w') as fd: with open('files.json', 'w') as fd:
json.dump(files, fd, indent=4, ensure_ascii=False) json.dump(files, fd, indent=4, ensure_ascii=False)
print('created: %s' % '%s.json' % name)

View file

@ -1,4 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
from argparse import ArgumentParser
import math import math
import json import json
import os import os
@ -11,13 +12,32 @@ def run(cmd):
subprocess.call(cmd) subprocess.call(cmd)
edit_json = sys.argv[1] usage = "usage: %(prog)s [options] edit.json"
parser = ArgumentParser(usage=usage)
parser.add_argument('-r', '--resolution', dest='height', type=int,
help="output height, default 480", default=480)
parser.add_argument('-a', '--aspect', dest='aspect', type=str,
help="aspect ratio, default 16/9 (float or /)", default='16/9')
parser.add_argument('-c', '--config', dest='config',
help='config.json containing config',
default='~/.ox/client.json')
parser.add_argument('path', metavar='path', type=str,
help='edit.json path')
opts = parser.parse_args()
edit_json = opts.path
edit = json.load(open(edit_json)) edit = json.load(open(edit_json))
render = './cache' render = './cache'
output = os.path.splitext(edit_json)[0] + '.mp4' output = os.path.splitext(edit_json)[0] + '.mp4'
height = 480 height = opts.height
aspect = 16/9 if '/' in opts.aspect:
aspect = [int(p) for p in opts.aspect.split('/')]
aspect = aspect[0] / aspect[1]
else:
aspect = float(opts.aspect)
width = int(height * aspect) width = int(height * aspect)
width -= width % 2 width -= width % 2
@ -31,17 +51,24 @@ position = 0
for clip in edit: for clip in edit:
out = render + '/%s_%0.3f-%0.3f.ts' % (clip['oshash'], clip['in'], clip['out']) out = render + '/%s_%0.3f-%0.3f.ts' % (clip['oshash'], clip['in'], clip['out'])
edit_duration += (clip['out']-clip['in']) edit_duration += (clip['out']-clip['in'])
files.append(out)
if os.path.exists(out): if os.path.exists(out):
try:
duration = ox.avinfo(out)['duration'] duration = ox.avinfo(out)['duration']
if clip['subtitles']: except KeyError:
os.unlink(out)
duration = None
if duration:
files.append(out)
src_duration = clip['out']-clip['in']
if abs(src_duration-duration) > 1:
print(clip.get('annotation', clip['item']), src_duration, duration)
if clip.get('subtitles'):
subtitles.append({ subtitles.append({
'in': position, 'in': position,
'out': position+duration, 'out': position+duration,
'value': clip['subtitles'] 'value': clip['subtitles']
}) })
position += duration position += duration
print(out, duration, (clip['out'] - clip['in']))
continue continue
clip_aspect = clip['resolution'][0] / clip['resolution'][1] clip_aspect = clip['resolution'][0] / clip['resolution'][1]
if clip_aspect < aspect: if clip_aspect < aspect:
@ -65,7 +92,6 @@ for clip in edit:
'-c:v', 'libx264', '-c:v', 'libx264',
'-b:v', '2M', '-b:v', '2M',
'-preset:v', 'medium', '-profile:v', 'high', '-level:v', '4.0', '-preset:v', 'medium', '-profile:v', 'high', '-level:v', '4.0',
'-map', '0:0,0:0', '-map', '0:1,0:1',
'-r', '25', '-r', '25',
'-c:a', 'aac', '-c:a', 'aac',
'-ar', '48000', '-ar', '48000',
@ -73,19 +99,37 @@ for clip in edit:
'-b:a', '192k', '-b:a', '192k',
] ]
clip_duration = math.ceil((clip['out'] - clip['in']) / (1/25)) * 1/25 clip_duration = math.ceil((clip['out'] - clip['in']) / (1/25)) * 1/25
if not clip_duration:
print('skip empty clip', clip)
else:
files.append(out)
src_info = ox.avinfo(clip['path'])
if not src_info['audio']:
audio = ['-f', 'lavfi', '-i', 'anullsrc=channel_layout=stereo:sample_rate=48000']
audio_map = []
else:
audio = []
audio_map = ['-map', '0:0,0:0', '-map', '0:1,0:1']
cmd = [ cmd = [
'ffmpeg', 'ffmpeg',
'-nostats', '-loglevel', 'error', '-nostats', '-loglevel', 'error',
'-ss', str(clip['in']), '-ss', str(clip['in']),
] + audio + [
'-i', clip['path'] '-i', clip['path']
] + options + [ ] + audio_map + options + [
'-t', str(clip_duration), '-t', str(clip_duration),
out out
] ]
run(cmd) run(cmd)
try:
duration = ox.avinfo(out)['duration'] duration = ox.avinfo(out)['duration']
if clip['subtitles']: except:
print('invalid file:', out)
print('try again?:')
print(' '.join(cmd).replace('-nostats -loglevel error', ''))
sys.exit(1)
print(clip, duration, clip['out']-clip['in'])
if clip.get('subtitles'):
subtitles.append({ subtitles.append({
'in': position, 'in': position,
'out': position+duration, 'out': position+duration,
@ -97,15 +141,17 @@ for clip in edit:
txt = output + '.txt' txt = output + '.txt'
with open(txt, 'w') as fd: with open(txt, 'w') as fd:
fd.write('file ' + '\nfile '.join(files)) fd.write('file ' + '\nfile '.join(files))
cmd = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', txt, '-c', 'copy', output] cmd = ['ffmpeg',
'-nostats', '-loglevel', 'error',
'-y', '-f', 'concat', '-safe', '0', '-i', txt, '-c', 'copy', output]
run(cmd) run(cmd)
os.unlink(txt) os.unlink(txt)
srt = output.replace('.mp4', '.srt') if subtitles:
srt = output.replace('.mp4', '.srt')
with open(srt, 'wb') as fd: with open(srt, 'wb') as fd:
fd.write(ox.srt.encode(subtitles)) fd.write(ox.srt.encode(subtitles))
duration = ox.avinfo(output)['duration'] duration = ox.avinfo(output)['duration']
print('file is %d, edit should be %d' % (duration, edit_duration)) print('file duration is %d, edit should be %d' % (duration, edit_duration))
print('created:', output)