pandora_client/pandora_client/__init__.py

1219 lines
44 KiB
Python
Executable File

#!/usr/bin/python
# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
# GPL 2012-2016
import getpass
from glob import glob
import importlib.util
import json
import math
import os
import socket
import sqlite3
import sys
import tempfile
import time
import pkg_resources
from urllib.parse import urlparse
import ox
from . import extract
from . import utils
DEBUG = False
__version__ = pkg_resources.require("pandora_client")[0].version
socket.setdefaulttimeout(300)
CHUNK_SIZE = 1024*1024*5
default_media_cache = os.environ.get('oxMEDIA', os.path.join(utils.basedir(), 'media'))
DOCUMENT_FORMATS = ('jpg', 'pdf', 'png')
sync_extensions = ()
def get_frames(filename, prefix, info, force=False):
oshash = info['oshash']
cache = os.path.join(prefix, os.path.join(*utils.hash_prefix(oshash)))
frames = []
for pos in utils.video_frame_positions(info['duration']):
frame_name = '%s.png' % pos
frame_f = os.path.join(cache, frame_name)
if force or not os.path.exists(frame_f):
print(frame_f)
extract.frame(filename, frame_f, pos)
frames.append(frame_f)
return frames
def encode(filename, prefix, profile, info=None, extract_frames=True):
if not info:
info = utils.avinfo(filename)
if 'oshash' not in info:
return None
oshash = info['oshash']
cache = os.path.join(prefix, os.path.join(*utils.hash_prefix(oshash)))
if info.get('video') and extract_frames:
frames = get_frames(filename, prefix, info)
else:
frames = []
if info.get('video') or info.get('audio'):
media_f = os.path.join(cache, profile)
if not os.path.exists(media_f) or os.stat(media_f).st_size == 0:
extract.video(filename, media_f, profile, info)
else:
print(info)
print(filename)
return None
return {
'info': info,
'oshash': oshash,
'frames': frames,
'media': media_f
}
def encode_cmd(filename, prefix, profile, info):
if not info:
info = utils.avinfo(filename)
if 'oshash' not in info:
return None
oshash = info['oshash']
cache = os.path.join(prefix, os.path.join(*utils.hash_prefix(oshash)))
media_f = os.path.join(cache, profile)
return extract.video_cmd(filename, media_f, profile, info)
def parse_path(client, path, prefix=None):
'''
args:
path - path without volume prefix
client - Client instance
prefix - volume prefix
return:
return None if file will not be used, dict with parsed item information otherwise
'''
if isinstance(path, bytes):
path = path.decode('utf-8')
path = path.replace(os.sep, '/')
parts = path.split('/')
if len(parts) >= client.folderdepth and parts[client.folderdepth-1] == 'Documents':
info = ox.movie.parse_path(u'/'.join(
parts[:client.folderdepth-1] + [parts[-1]]
))
else:
if len(parts) != client.folderdepth:
return None
info = ox.movie.parse_path(path)
if client.folderdepth == 3:
info['director'] = []
info['directorSort'] = []
return info
def example_path(client):
return '\t' + (client.folderdepth == 4 and 'L/Last, First/Title (Year)/Title.avi' or 'T/Title/Title.dv')
def ignore_file(client, path):
filename = os.path.basename(path)
if filename.startswith('._') \
or filename in ('.DS_Store', 'Thumbs.db') \
or filename.endswith('~') \
or 'Extras' + os.sep in path \
or 'Versions' + os.sep in path \
or not os.path.exists(path) \
or os.stat(path).st_size == 0:
return True
return False
def is_oshash(oshash):
try:
int(oshash, 16)
except:
return False
return len(oshash) == 16
def hide_cursor():
sys.stdout.write("\033[?25l")
sys.stdout.flush()
def show_cursor():
sys.stdout.write("\033[?25h")
sys.stdout.flush()
class Client(object):
_configfile = None
def __init__(self, config, offline=False):
if isinstance(config, str):
self._configfile = os.path.expanduser(config)
with open(config) as f:
try:
self._config = json.load(f)
except ValueError:
print("Failed to parse config at", config)
sys.exit(1)
base = self._config.get('plugin.d', os.path.join(utils.basedir(), 'client.d'))
self.load_plugins(base)
else:
self._config = config
if not self._config['url'].endswith('/'):
self._config['url'] = self._config['url'] + '/'
self.resolutions = list(reversed(sorted(self._config.get('resolutions', [480]))))
self.format = self._config.get('format', 'webm')
self.importFrames = False
if not offline:
self.online()
conn, c = self._conn()
c.execute(u'CREATE TABLE IF NOT EXISTS setting (key varchar(1024) unique, value text)')
if int(self.get('version', 0)) < 1:
self.set('version', 1)
db = [
u'''CREATE TABLE IF NOT EXISTS file (
path varchar(1024) unique,
oshash varchar(16),
atime FLOAT,
ctime FLOAT,
mtime FLOAT,
size INT,
info TEXT,
created INT,
modified INT,
deleted INT)''',
u'CREATE INDEX IF NOT EXISTS path_idx ON file (path)',
u'CREATE INDEX IF NOT EXISTS oshash_idx ON file (oshash)',
]
for i in db:
c.execute(i)
conn.commit()
if int(self.get('version', 0)) < 2:
self.set('version', 2)
db = [
u'''CREATE TABLE IF NOT EXISTS encode (
oshash varchar(16),
site varchar(255))''',
u'CREATE INDEX IF NOT EXISTS upload_site_idx ON encode (site)',
]
for i in db:
c.execute(i)
conn.commit()
if int(self.get('version', 0)) < 3:
self.set('version', 3)
db = [
u'ALTER TABLE file add sha1 varchar(42)'
]
for i in db:
c.execute(i)
conn.commit()
if int(self.get('version', 0)) < 4:
self.set('version', 4)
db = [
u'ALTER TABLE encode add status varchar(255)',
u'CREATE INDEX IF NOT EXISTS encode_status_idx ON encode (status)',
u'ALTER TABLE encode ADD modified INT DEFAULT 0',
]
for i in db:
c.execute(i)
conn.commit()
conn.close()
def load_plugins(self, base=os.path.join(utils.basedir(), 'client.d')):
global parse_path, example_path, ignore_file, sync_extensions, encode
base = os.path.expanduser(base)
if not os.path.exists(base):
return
for name in sorted(os.listdir(base)):
if not name.endswith('.py'):
continue
path = os.path.join(base, name)
module_name = os.path.basename(path).split('.')[0]
spec = importlib.util.spec_from_file_location(module_name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, 'parse_path'):
parse_path = module.parse_path
if hasattr(module, 'example_path'):
example_path = module.example_path
if hasattr(module, 'ignore_file'):
ignore_file = module.ignore_file
if hasattr(module, 'sync_extensions'):
sync_extensions = module.sync_extensions
if hasattr(module, 'encode'):
encode = module.encode
def _conn(self):
db_conn = self._config['cache']
if isinstance(db_conn, bytes):
db_conn = db_conn.decode('utf-8')
db_conn = os.path.expanduser(db_conn)
if not os.path.exists(os.path.dirname(db_conn)):
os.makedirs(os.path.dirname(db_conn))
conn = sqlite3.connect(db_conn, timeout=10)
return conn, conn.cursor()
def media_cache(self):
path = self._config.get('media-cache', default_media_cache)
if path is None:
path = '/tmp/pandora_client_media_cache'
return os.path.expanduser(path)
def get(self, key, default=None):
conn, c = self._conn()
c.execute(u'SELECT value FROM setting WHERE key = ?', (key, ))
value = default
for row in c:
value = row[0]
break
conn.close()
return value
def set(self, key, value):
conn, c = self._conn()
c.execute(u'INSERT OR REPLACE INTO setting values (?, ?)', (key, str(value)))
conn.commit()
conn.close()
def info(self, oshash):
conn, c = self._conn()
c.execute(u'SELECT info FROM file WHERE oshash = ?', (oshash, ))
info = None
for row in c:
info = json.loads(row[0])
break
conn.close()
return info
def get_info(self, oshash, prefix=None):
if prefix:
prefixes = [prefix]
else:
prefixes = self.active_volumes().values()
prefixes = [p.decode('utf-8') if isinstance(prefix, bytes) else p for p in prefixes]
_info = self.info(oshash)
for path in self.path(oshash):
for prefix in prefixes:
if path.startswith(prefix) and os.path.exists(path):
path = path[len(prefix):]
i = parse_path(self, path, prefix)
if i:
_info.update(i)
return _info
else:
print('failed to parse', path)
return
def get_info_for_ids(self, ids, prefix=None):
info = {}
for oshash in ids:
i = self.get_info(oshash, prefix)
if i:
info[oshash] = i
return info
def path(self, oshash):
conn, c = self._conn()
c.execute(u'SELECT path FROM file WHERE oshash = ?', (oshash, ))
paths = set()
for row in c:
path = row[0]
paths.add(path)
conn.close()
return list(paths)
def online(self):
self.api = API(self._config['url'], media_cache=self.media_cache())
self.api.DEBUG = DEBUG
if self.signin():
self.resolutions = list(reversed(sorted(self.api.site['video']['resolutions'])))
self.format = self.api.site['video']['formats'][0]
self.importFrames = self.api.site['media'].get('importFrames')
self.folderdepth = self._config.get('folderdepth', self.api.site['site'].get('folderdepth', 3))
def signin(self):
if 'username' in self._config:
r = self.api.signin(username=self._config['username'], password=self._config['password'])
if r['status']['code'] == 200 and 'errors' not in r['data']:
self.user = r['data']['user']
else:
self.user = False
if DEBUG:
print(r)
print('\nlogin failed! check config\n\n')
sys.exit(1)
r = self.api.init()
if r['status']['code'] == 200:
self.api.site = r['data']['site']
else:
print("\n init failed.", r['status'])
sys.exit(1)
return True
def set_encodes(self, site, files):
conn, c = self._conn()
c.execute(u'DELETE FROM encode WHERE site = ?', (site, ))
conn.commit()
conn.close()
self.add_encodes(site, files)
def get_encodes(self, site, status=''):
conn, c = self._conn()
sql = u'SELECT oshash FROM encode WHERE site = ? AND status = ?'
args = [site, status]
c.execute(sql, tuple(args))
encodes = [row[0] for row in c]
conn.close()
return encodes
def add_encodes(self, site, files):
conn, c = self._conn()
for oshash in files:
c.execute(u'INSERT INTO encode VALUES (?, ?, ?, 0)', (oshash, site, ''))
conn.commit()
conn.close()
def update_encodes(self, add=False):
# send empty list to get updated list of requested info/files/data
site = self._config['url']
post = {'info': {}}
r = self.api.update(post)
files = r['data']['data']
if add:
conn, c = self._conn()
sql = u'SELECT oshash FROM encode WHERE site = ?'
c.execute(sql, (site, ))
known = [row[0] for row in c]
conn.close()
files = list(set(files) - set(known))
if files:
self.add_encodes(site, files)
else:
self.set_encodes(site, files)
def scan_file(self, path, rescan=False):
conn, c = self._conn()
update = True
modified = time.mktime(time.localtime())
created = modified
if isinstance(path, bytes):
path = path.decode('utf-8')
sql = u'SELECT atime, ctime, mtime, size, created, info FROM file WHERE deleted < 0 AND path=?'
c.execute(sql, [path])
stat = os.stat(path)
for row in c:
if stat.st_atime == row[0] and stat.st_ctime == row[1] and stat.st_mtime == row[2] and stat.st_size == row[3]:
created = row[4]
info = json.loads(row[5])
update = False
break
if update or rescan:
info = utils.avinfo(path, cached=not rescan)
if info['size'] > 0:
oshash = info['oshash']
sha1 = None
deleted = -1
t = (path, oshash, stat.st_atime, stat.st_ctime, stat.st_mtime,
stat.st_size, json.dumps(info), created, modified, deleted, sha1)
c.execute(u'INSERT OR REPLACE INTO file values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
t)
conn.commit()
conn.close()
return 'error' not in info
def get_resolution(self, info):
height = info['video'][0]['height'] if info.get('video') else None
for resolution in sorted(self.resolutions):
if height and height <= resolution:
return resolution
return resolution
def profile(self, info):
resolution = self.get_resolution(info)
profile = '%sp.%s' % (resolution, self.format)
return profile
def cmd(self, args):
filename = args[0]
if len(filename) == 16:
path = self.path(filename)
else:
path = [filename]
for p in path:
if os.path.exists(p):
info = utils.avinfo(p)
profile = self.profile(info)
cmds = encode_cmd(p, self.media_cache(), profile, info)
output = []
for cmd in cmds:
cmd = [' ' in c and u'"%s"' % c or c for c in cmd]
output.append(u' '.join(cmd))
print(' && '.join(output))
def save_config(self):
if not self._configfile:
raise Exception('Can not save temporary config')
with open(self._configfile, 'w') as f:
json.dump(self._config, f, indent=2)
def config(self, args):
print("Current Config:\n User: %s\n URL: %s\n" % (self._config['username'], self._config['url']))
print("Leave empty to keep current value\n")
username = input('Username: ')
if username:
self._config['username'] = username
password = getpass.getpass('Password: ')
if password:
self._config['password'] = password
url = input('Pan.do/ra URL (i.e. https://pandora.local/api/): ')
if url:
self._config['url'] = url
self.save_config()
print("\nconfiguration updated.")
def add_volume(self, args):
usage = "Usage: %s add_volume name path" % sys.argv[0]
if len(args) != 2:
print(usage)
sys.exit(1)
name = args[0]
path = os.path.abspath(args[1])
if not path.endswith(os.sep):
path += os.sep
if os.path.isdir(path):
if name in self._config['volumes']:
print("updated %s to %s" % (name, path))
else:
print("added %s %s" % (name, path))
self._config['volumes'][name] = path
self.save_config()
else:
print("'%s' does not exist" % path)
print(usage)
sys.exit(1)
def active_volumes(self):
volumes = {}
for name in sorted(self._config['volumes']):
path = self._config['volumes'][name]
path = os.path.normpath(path)
if not path.endswith(os.sep):
path += os.sep
if isinstance(path, bytes):
path = path.decode('utf-8')
if os.path.exists(path):
volumes[name] = path
return volumes
def scan(self, args):
rescan = 'rescan' in args
print("checking for new files ...")
volumes = self.active_volumes()
for name in sorted(volumes):
path = volumes[name]
conn, c = self._conn()
c.execute(u'SELECT path FROM file WHERE path LIKE ? AND deleted < 0', [u"%s%%" % path])
known_files = [r[0] for r in c.fetchall()]
conn.close()
files = []
unknown = []
ignored = []
unsupported = []
for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
if filenames:
for filename in sorted(filenames):
file_path = os.path.join(dirpath, filename)
if not ignore_file(self, file_path):
files.append(file_path)
else:
ignored.append(file_path)
for f in files:
if not parse_path(self, f[len(path):], path):
unknown.append(f)
files = sorted(set(files) - set(unknown))
for f in files:
if os.path.splitext(f)[-1] not in sync_extensions and not self.scan_file(f, rescan):
unsupported.append(f)
if unknown:
example = example_path(self)
print('Files need to be in a folder structure like this:\n%s\n' % example)
print('The following files do not fit into the folder structure and will not be synced:')
print('\t' + '\n\t'.join([f[len(path):] for f in sorted(unknown)]))
print('')
if unsupported:
files = sorted(set(files) - set(unsupported))
print('The following files are in an unsupported format and will not be synced:')
print('\t' + '\n\t'.join([f[len(path):] for f in sorted(unsupported)]))
print('')
'''
'''
deleted_files = list(filter(lambda f: f not in files, known_files))
new_files = list(filter(lambda f: f not in known_files, files))
conn, c = self._conn()
if deleted_files:
deleted = time.mktime(time.localtime())
for f in deleted_files:
c.execute(u'UPDATE file SET deleted=? WHERE path=?', (deleted, f))
conn.commit()
conn.close()
'''
print("scanned volume %s: %s files, %s new, %s deleted, %s ignored, %s unsupported" % (
name, len(files), len(new_files), len(deleted_files), len(ignored), len(unsupported)))
'''
print("scanned volume %s: %s files, %s new, %s deleted, %s ignored" % (
name, len(files), len(new_files), len(deleted_files), len(ignored)))
def extract(self, args):
if args:
if args[0] == 'offline':
files = self.get_encodes(self._config['url'])
elif args[0] == 'all':
files = []
for name in self._config['volumes']:
path = self._config['volumes'][name]
path = os.path.normpath(path)
if not path.endswith(os.sep):
path += os.sep
if os.path.exists(path):
files += self.files(path)['info']
else:
files = [f if len(f) == 16 else ox.oshash(f) for f in args]
else:
if not self.user:
print("you need to login or run pandora_client extract offline")
return
self.update_encodes()
files = self.get_encodes(self._config['url'])
for oshash in files:
info = self.info(oshash)
if 'error' not in info:
for path in self.path(oshash):
if os.path.exists(path):
profile = self.profile(info)
i = encode(path, self.media_cache(), profile, info,
self.importFrames)
break
def sync(self, args):
if not self.user:
print("you need to login")
return
conn, c = self._conn()
volumes = self.active_volumes()
if not volumes:
print("no active volumes found")
return
for name in sorted(volumes):
prefix = volumes[name]
files = self.files(prefix)
post = {}
post['files'] = files['files']
post['volume'] = name
print('sending list of files in %s (%s total)' % (name, len(post['files'])))
r = self.api.later('update', post)
# send empty list to get updated list of requested info/files/data
post = {'info': {}}
r = self.api.update(post)
if r['data']['info']:
r = self.update_info(r['data']['info'], prefix)
if 'data' not in r:
print(r)
return
if r['data']['data']:
files = []
for f in r['data']['data']:
for path in self.path(f):
if os.path.exists(path):
files.append(path)
break
if files:
print('\ncould encode and upload %s videos:\n' % len(files))
print('\n'.join(files))
if r['data']['file']:
files = []
for f in r['data']['file']:
for path in self.path(f):
if os.path.exists(path):
files.append(path)
break
if files:
print('\ncould upload %s subtitles:\n' % len(files))
print('\n'.join(files))
def upload(self, args):
if not self.user:
print("you need to login")
return
documents = []
if args:
data = []
for arg in args:
if os.path.exists(arg):
oshash = ox.oshash(arg)
self.scan_file(arg)
r = self.api.findMedia({'query': {
'conditions': [{'key': 'oshash', 'value': oshash}]
}})['data']['items']
if r == 0:
info = self.info(oshash)
filename = os.path.basename(arg)
r = self.api.addMedia({
'id': oshash,
'info': info,
'filename': filename
})
data.append(oshash)
elif not is_oshash(arg):
print('file not found "%s"' % arg)
sys.exit(1)
else:
data.append(arg)
files = []
info = []
else:
if not self.active_volumes():
print("no volumes found, mount volumes and run again")
return
# send empty list to get updated list of requested info/files/data
post = {'info': {}}
r = self.api.update(post)
data = r['data']['data']
files = r['data']['file']
info = r['data']['info']
documents = self._get_documents()
if info:
r = self.update_info(info)
data = r['data']['data']
files = r['data']['file']
if files:
print('uploading %s files' % len(files))
for oshash in files:
for path in self.path(oshash):
if os.path.exists(path):
self.api.uploadData(path, oshash)
break
if documents:
_documents = []
for oshash, item in documents:
for path in self.path(oshash):
if os.path.exists(path):
_documents.append([path, item])
break
print('uploading %s documents' % len(_documents))
for path, item in _documents:
self._add_document(path, item)
if data:
print('encoding and uploading %s videos' % len(data))
for oshash in data:
data = {}
info = self.info(oshash)
if info and 'error' not in info:
for path in self.path(oshash):
if os.path.exists(path):
if not self.api.uploadVideo(path, data,
self.profile(info), info):
print('video upload failed, giving up, please try again')
return
if 'rightsLevel' in self._config:
r = self.api.find({'query': {
'conditions': [
{'key': 'oshash', 'value': oshash, 'operator': '=='}
],
'keys': ['id'],
'range': [0, 1]
}})
if r['data']['items']:
item = r['data']['items'][0]['id']
r = self.api.edit({
'item': item,
'rightsLevel': self._config['rightsLevel']
})
break
def update_info(self, info, prefix=None):
if info:
print('sending info for %d files' % len(info))
post = {'info': {}, 'upload': True}
post['info'] = self.get_info_for_ids(info, prefix)
r = self.api.later('update', post)
# send empty list to get updated list of requested info/files/data
post = {'info': {}}
r = self.api.update(post)
return r
def upload_frames(self, args):
if not self.user:
print("you need to login")
return
for oshash in args:
info = self.info(oshash)
if info and 'error' not in info:
for path in self.path(oshash):
if os.path.exists(path):
frames = get_frames(path, self.api.media_cache, info, True)
i = {
'info': info,
'oshash': oshash,
'frames': frames,
}
r = self.api.uploadFrames(i, {})
if r.get('status', {}).get('code') != 200:
print(r)
def _get_documents(self):
files = []
for volume in self.active_volumes():
query = {
'conditions': [
{'key': 'list', 'value': volume, 'operator': '=='},
{
'conditions': [
{'key': 'filename', 'operator': '', 'value': value}
for value in DOCUMENT_FORMATS
],
'operator': '|'
}
],
'operator': '&'
}
n = self.api.findMedia({'query': query})['data']['items']
if n:
o = 0
chunk = 5000
while o < n:
files += [f for f in self.api.findMedia({
'query': query,
'keys': ['item', 'id', 'extension'],
'range': [o, o+chunk]
})['data']['items'] if f['extension'] in DOCUMENT_FORMATS]
o += chunk
missing = list(set((f['id'], f['item']) for f in files))
available = set()
total = len(missing)
ids = [m[0] for m in missing]
o = 0
chunk = 1000
while o < len(ids):
for d in self.api.findDocuments({
'query': {
'conditions': [
{'key': 'oshash', 'operator': '==', 'value': id}
for id in ids[o:o+chunk]
],
'operator': '|'
},
'keys': ['oshash'],
'range': [0, chunk]
})['data']['items']:
available.add(d['oshash'])
o += chunk
missing = [m for m in missing if m[0] not in available]
return missing
def find_document(self, oshash):
r = self.api.findDocuments({
'keys': ['id'],
'query': {
'conditions': [
{'key': 'oshash', 'value': oshash, 'operator': '=='}
],
'operator': '&'
}
})
if r['data']['items']:
return r['data']['items'][0]['id']
return None
def _add_document(self, f, item=None):
if f.split('.')[-1].lower() not in DOCUMENT_FORMATS:
print('skip, not a document', f)
return False
oshash = ox.oshash(f)
did = self.find_document(oshash)
if not did:
url = '%supload/document/' % self._config['url']
did = self.api.upload_chunks(url, f, {
'filename': os.path.basename(f)
})
if did and item:
r = self.api.addDocument({
'id': did,
'item': item
})
return did
def upload_document(self, args):
if not self.user:
print("you need to login")
return
for f in args:
r = self._add_document(f)
if not r:
print('unsupported format', f)
continue
def files(self, prefix):
if not prefix.endswith('/'):
prefix += '/'
conn, c = self._conn()
files = {}
files['info'] = {}
files['files'] = []
sql = u'SELECT path, oshash, info, atime, ctime, mtime FROM file WHERE deleted < 0 AND path LIKE ? ORDER BY path'
t = [u"%s%%" % prefix]
c.execute(sql, t)
for row in c:
path = row[0]
oshash = row[1]
info = json.loads(row[2])
if 'error' not in info:
for key in ('atime', 'ctime', 'mtime', 'path'):
if key in info:
del info[key]
files['info'][oshash] = info
files['files'].append({
'oshash': oshash,
'path': path[len(prefix):],
'atime': row[3],
'ctime': row[4],
'mtime': row[5],
})
conn.close()
return files
def clean(self, args):
if os.path.exists(self.api.media_cache):
if args and args[0] == 'all':
print("remove all cached videos in", self.api.media_cache)
# if os.path.exists(self.api.media_cache):
# shutil.rmtree(self.api.media_cache)
else:
nothing = False
for root, folders, files in os.walk(self.api.media_cache):
for f in sorted(files):
f = os.path.join(root, f)
if f.endswith('.webm'):
oshash = os.path.dirname(f)[len(self.api.media_cache):].replace('/', '')
remove = True
for path in self.path(oshash):
if os.path.exists(path):
remove = False
break
if remove:
nothing = False
os.unlink(f)
if nothing and folders:
print("No unused files found in cache, run \"%s clean all\" to remove the entire cache" % sys.argv[0])
else:
utils.cleanup_tree(self.api.media_cache)
def import_srt(self, args):
'''
import srt as annotations, usage:
pandora_client import_srt ITEMID layername /path/to/transcript.srt
i.e.
pandora_client ipmort_srt ABC transcripts /path/to/transcript.srt
'''
if not args:
print('Usage: pandora_client import_srt ABC transcripts /path/to/transcript.srt')
sys.exit(1)
item = args[0]
layer = args[1]
filename = args[2]
layers = [l['id'] for l in self.api.site['layers']]
if layer not in layers:
print("invalid layer name, choices are: ", ', '.join(layers))
sys.exit(1)
if filename.endswith('.vtt'):
load = ox.vtt.load
else:
load = ox.srt.load
annotations = [{
'in': s['in'],
'out': s['out'],
'value': s['value'].replace('\n', '<br>\n') if layer == 'subtitles' else s['value'],
} for s in load(filename)]
r = self.api.addAnnotations({
'item': item,
'layer': layer,
'annotations': annotations
})
if r['status']['code'] == 400:
print('failed')
sys.exit(1)
if r['status']['code'] == 403:
print('permission deinied')
sys.exit(1)
elif r['status']['code'] == 404:
print('item not found')
sys.exit(1)
def server(self, args):
from . import server
server.run(self, args)
def client(self, args):
threads = [t.split('=')[-1] for t in args if t.startswith('c=')]
if threads:
threads = int(threads[0])
else:
threads = 1
args = [a for a in args if not a.startswith('c=')]
urls = [u for u in args if u.startswith('http:')]
name = [u for u in args if u not in urls]
if not name:
name = '%s-%s' % (socket.gethostname(), int(time.time()))
else:
name = name[0]
if not urls:
from . import localnode
nodes = localnode.LocalNodes()
time.sleep(1)
found = len(nodes)
if not found:
print('usage: %s client <server_url>\n\ti.e. %s client http://192.168.1.1:8789' % (sys.argv[0], sys.argv[0]))
sys.exit(1)
elif found > 1:
print('found multiple servers, please select one, your options are:')
for id, url in nodes.items():
print('\t%s client %s' % (sys.argv[0], url))
sys.exit(1)
else:
for id, url in nodes.items():
break
print('connecting to %s (%s)' % (id, url))
else:
url = urls[0]
from . import client
c = client.DistributedClient(url, name, threads)
c.run()
class API(ox.API):
__name__ = 'pandora_client'
__version__ = __version__
def __init__(self, url, cj=None, media_cache=None):
super(API, self).__init__(url, cj)
self.media_cache = media_cache
if not self.media_cache:
self.media_cache = os.path.expanduser(default_media_cache)
netloc = urlparse(self.url).netloc
tmp = tempfile.gettempdir()
self._resume_file = os.path.join(tmp, 'pandora_client.%s.%s.json' % (os.environ.get('USER'), netloc))
if hasattr(self, 'taskStatus') and not hasattr(self, 'getTaskStatus'):
self.getTaskStatus = self.taskStatus
def later(self, action, data, interval=5):
t = r = getattr(self, action)(data)
if r['status']['code'] == 200:
# wait for async task to finish
if 'taskId' in r['data']:
t = self.getTaskStatus(task_id=r['data']['taskId'])
print('waiting for server ...')
while t['data'].get('status') == 'PENDING':
time.sleep(interval)
t = self.getTaskStatus(task_id=r['data']['taskId'])
return t
def uploadFrames(self, i, data):
# upload frames
if self.site['media'].get('importFrames') and i['frames']:
form = ox.MultiPartForm()
form.add_field('action', 'upload')
form.add_field('id', i['oshash'])
for key in data:
form.add_field(key, data[key])
for frame in i['frames']:
fname = os.path.basename(frame)
if os.path.exists(frame):
form.add_file('frame', fname, open(frame, 'rb'))
r = self._json_request(self.url, form)
return r
def uploadVideo(self, filename, data, profile, info=None):
i = encode(filename, self.media_cache, profile, info,
self.site['media'].get('importFrames'))
if not i:
print("failed")
return
# upload frames
r = self.uploadFrames(i, data)
# upload media
if os.path.exists(i['media']):
size = ox.format_bytes(os.path.getsize(i['media']))
name = os.path.basename(filename)
print(u"uploading %s of %s (%s)" % (profile, name, size))
url = self.url + 'upload/?profile=%s&id=%s' % (profile, i['oshash'])
if not self.upload_chunks(url, i['media'], data):
if DEBUG:
print("failed")
return False
else:
print("Failed")
return False
return True
def uploadData(self, filename, oshash):
if DEBUG:
print('upload', filename)
form = ox.MultiPartForm()
form.add_field('action', 'upload')
form.add_field('id', str(oshash))
fname = os.path.basename(filename)
if not isinstance(fname, bytes):
fname = fname.encode('utf-8')
form.add_file('file', fname, open(filename, 'rb'))
r = self._json_request(self.url, form)
return r
def upload_chunks(self, url, filename, data=None):
form = ox.MultiPartForm()
resume = None
if self._resume_file and os.path.exists(self._resume_file):
with open(self._resume_file) as f:
try:
resume = json.load(f)
except:
resume = {}
if resume.get('chunkUploadUrl') != url:
resume = None
if resume:
data = resume
else:
for key in data:
form.add_field(key, data[key])
data = self._json_request(url, form)
print(filename)
hide_cursor()
def full_url(path):
if path.startswith('/'):
u = urlparse(url)
path = '%s://%s%s' % (u.scheme, u.netloc, path)
return path
result_url = full_url(data.get('url'))
if 'uploadUrl' in data:
uploadUrl = full_url(data['uploadUrl'])
f = open(filename, 'rb')
fsize = os.stat(filename).st_size
done = 0
start = time.mktime(time.localtime())
if 'offset' in data and data['offset'] < fsize:
done = data['offset']
f.seek(done)
resume_offset = done
else:
resume_offset = 0
chunk = f.read(CHUNK_SIZE)
fname = os.path.basename(filename)
if not isinstance(fname, bytes):
fname = fname.encode('utf-8')
while chunk:
elapsed = time.mktime(time.localtime()) - start
remaining = ""
if done:
r = math.ceil((elapsed / (done/(fsize-resume_offset)) - elapsed)/60) * 60 * 1000
r = ox.format_duration(r, milliseconds=False, verbosity=2)
if r:
remaining = ", %s remaining" % r
msg = '%0.2f%% %s of %s done%s' % (
100 * done/fsize, ox.format_bytes(done), ox.format_bytes(fsize), remaining)
print(''.join([msg, ' ' * (80-len(msg)), '\r']), end='')
sys.stdout.flush()
form = ox.MultiPartForm()
form.add_file('chunk', fname, chunk)
if len(chunk) < CHUNK_SIZE or f.tell() == fsize:
form.add_field('done', '1')
form.add_field('offset', str(done))
try:
data = self._json_request(uploadUrl, form)
except KeyboardInterrupt:
print("\ninterrupted by user.")
sys.exit(1)
except:
print("uploading chunk failed, will try again in 5 seconds\r", end='')
sys.stdout.flush()
if DEBUG:
print('\n', uploadUrl)
import traceback
traceback.print_exc()
data = {'result': -1}
time.sleep(5)
if data and 'status' in data:
if data['status']['code'] == 403:
print("login required")
return False
if data['status']['code'] != 200:
print("request returned error, will try again in 5 seconds")
if DEBUG:
print(data)
time.sleep(5)
if data and data.get('result') == 1:
done += len(chunk)
if data.get('offset') not in (None, done):
print('server offset out of sync, continue from', data['offset'])
done = data['offset']
f.seek(done)
if self._resume_file:
with open(self._resume_file, 'w') as r:
json.dump({
'uploadUrl': uploadUrl,
'chunkUploadUrl': url,
'url': result_url,
'offset': done
}, r, indent=2)
chunk = f.read(CHUNK_SIZE)
if self._resume_file and os.path.exists(self._resume_file):
os.unlink(self._resume_file)
resume = None
if result_url:
print(result_url + (' ' * (80-len(result_url))))
else:
print(' ' * 80)
print('')
show_cursor()
if data and 'result' in data and data.get('result') == 1:
return data.get('id', True)
else:
return False
else:
if DEBUG:
if 'status' in data and data['status']['code'] == 401:
print("login required")
else:
print("failed to upload file to", url)
print(data)
return False