diff --git a/bin/pandoralocal b/bin/pandoralocal index f5e3e26..5d82340 100755 --- a/bin/pandoralocal +++ b/bin/pandoralocal @@ -5,7 +5,6 @@ import os import sys -from glob import glob from optparse import OptionParser root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..') @@ -16,10 +15,10 @@ import pandoralocal if __name__ == '__main__': parser = OptionParser() - parser.add_option('-c', '--config', dest='config', help='config file', default='config.json') + parser.add_option('-d', '--db', dest='db', help='settings db', default=pandoralocal.db_path()) (opts, args) = parser.parse_args() - if None in (opts.config, ): + if None in (opts.db, ): parser.print_help() sys.exit() - pandoralocal.main(opts.config) + pandoralocal.main(opts.db) diff --git a/pandoralocal/__init__.py b/pandoralocal/__init__.py index bc94261..6cc38ca 100644 --- a/pandoralocal/__init__.py +++ b/pandoralocal/__init__.py @@ -1,6 +1,13 @@ # encoding: utf-8 # vi:si:et:sw=4:sts=4:ts=4 import os +import sys + +import gobject +gobject.threads_init() + +from twisted.internet import glib2reactor +glib2reactor.install() from twisted.web.server import Site from twisted.internet import reactor @@ -11,6 +18,15 @@ import api from version import __version__ +def db_path(): + #use something like http://pypi.python.org/pypi/appdirs/? + if sys.platform == 'darwin': + path = os.path.expanduser('~/Library/Application Support/pandora/local.db') + elif sys.platform == 'win32': + path = os.path.expanduser('~\\AppData\\pandora\\local.db') + else: + path = os.path.expanduser('~/.local/share/pandora/local.db') + return path def main(config): base = os.path.abspath(os.path.dirname(__file__)) diff --git a/pandoralocal/api.py b/pandoralocal/api.py index 9442b6c..a794410 100644 --- a/pandoralocal/api.py +++ b/pandoralocal/api.py @@ -1,6 +1,9 @@ # encoding: utf-8 # vi:si:et:sw=4:sts=4:ts=4 +import os + from server import actions, json_response +import ui def init(backend, site, data): response = {} @@ -14,3 +17,83 @@ actions.register(echo, cache=False) def site(backend, site, data): return json_response({'site': site}) actions.register(site, cache=False) + +def addVolume(backend, site, data): + print data + path = ui.selectFolder(data) + if path: + name = os.path.basename(path) + volume = backend.add_volume(site, name, path) + return json_response(volume) + return json_response({}) +actions.register(addVolume, cache=False) + +def removeVolume(backend, site, data): + print data + return json_response({}) +actions.register(removeVolume, cache=False) + +def renameVolume(backend, site, data): + volume = backend.rename_volume(site, data['name'], data['newName']) + return json_response(volume) +actions.register(renameVolume, cache=False) + +def findVolumes(backend, site, data): + print site, data + response = {} + response['items'] = backend.volumes(site) + return json_response(response) +actions.register(findVolumes, cache=False) + +def findFiles(backend, site, data): + ''' + implements Ox.List api for files + keys: path, id, size, volume, item, uploaded + ''' + print site, data + response = {} + if not 'keys' in data: + response['items'] = backend.files(site, keys=['count(*)'])[0]['count(*)'] + print response + else: + response['items'] = backend.files(site, keys=data['keys'], limit=data['range']) + + return json_response(response) +actions.register(findFiles, cache=False) + +def uploadFile(backend, site, data): + ''' + uploadFile { + id: + } + upload file with provides id + ''' + print data + return json_response({}) +actions.register(uploadFile, cache=False) + +def encodeFile(backend, site, data): + path = ui.selectFile(data) + oshash = backend.add_file(site, path) + url = '/%s/480p.webm' % oshash + return json_response({ + 'path': path, + 'oshash': oshash, + 'url': url + }) +actions.register(encodeFile, cache=False) + +def cacheVideo(backend, site, data): + print data + return json_response({}) +actions.register(encodeFile, cache=False) + +def selectFile(backend, site, data): + path = ui.selectFile(data) + oshash = backend.add_file(site, path) + return json_response({ + 'path': path, + 'oshash': oshash + }) +actions.register(selectFile, cache=False) + diff --git a/pandoralocal/backend.py b/pandoralocal/backend.py index 563f213..e34ebdb 100644 --- a/pandoralocal/backend.py +++ b/pandoralocal/backend.py @@ -1,15 +1,209 @@ # encoding: utf-8 # vi:si:et:sw=4:sts=4:ts=4 +import json +import sqlite3 +import os + +import ox + +import utils + class Backend: - def __init__(self, config): - self.config = config + def __init__(self, db): + self.db = db - def get_file(self, site, itemId, filename): + conn, c = self._conn() + + c.execute('''CREATE TABLE IF NOT EXISTS setting (key varchar(1024) unique, value text)''') + + if int(self.get('version', 0)) < 1: + self.set('version', 1) + db = [ + '''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)''', + '''CREATE INDEX IF NOT EXISTS path_idx ON file (path)''', + '''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 = [ + '''CREATE TABLE IF NOT EXISTS encode ( + oshash varchar(16), + site varchar(255))''', + '''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 = [ + '''CREATE TABLE IF NOT EXISTS volume ( + name varchar(1024) unique, + path text, + site varchar(255))''', + '''CREATE TABLE IF NOT EXISTS part ( + id varchar(1024), + part int, + oshash varchar(16), + site varchar(255))''', + ] + for i in db: + c.execute(i) + conn.commit() + + self.media_cache = self.get('media_cache') + if not self.media_cache: + self.media_cache = os.path.join(os.path.dirname(self.db), 'media') + self.set('media_cache', self.media_cache) + + def _conn(self): + if not os.path.exists(os.path.dirname(self.db)): + os.makedirs(os.path.dirname(self.db)) + conn = sqlite3.connect(self.db, timeout=10) + conn.text_factory = sqlite3.OptimizedUnicode + return conn, conn.cursor() + + def get(self, key, default=None): + conn, c = self._conn() + c.execute('SELECT value FROM setting WHERE key = ?', (key, )) + for row in c: + return row[0] + return default + + def set(self, key, value): + conn, c = self._conn() + c.execute(u'INSERT OR REPLACE INTO setting VALUES (?, ?)', (key, str(value))) + conn.commit() + + def info(self, oshash): + conn, c = self._conn() + c.execute('SELECT info FROM file WHERE oshash = ?', (oshash, )) + for row in c: + return json.loads(row[0]) + return None + + def path(self, oshash): + conn, c = self._conn() + c.execute('SELECT path FROM file WHERE oshash = ?', (oshash, )) + paths = [] + for row in c: + paths.append(row[0]) + return paths + + def cache_path(self, oshash, profile): + return os.path.join(self.media_cache, os.path.join(*utils.hash_prefix(oshash)), profile) + + def volumes(self, site): + conn, c = self._conn() + c.execute('SELECT name, path FROM volume WHERE site= ?', (site, )) + volumes = [] + for r in c: + volumes.append({'name': r[0], 'path': r[1]}) + return volumes + + def add_volume(self, site, name, path): + volumes = self.volumes(site) + exists = filter(lambda v: v['path'] == path, volumes) + if exists: + return exists[0] + _name = name + n = 2 + while filter(lambda v: v['name'] == _name, volumes): + _name = "%s %d" % (name, n) + name = _name + conn, c = self._conn() + c.execute('INSERT INTO volume (site, name, path) VALUES (?, ?, ?)', (site, name, path)) + conn.commit() + return { + 'name': name, 'path': path + } + + def rename_volume(self, site, name, new_name): + volumes = self.volumes(site) + _name = new_name + n = 2 + while filter(lambda v: v['name'] == _name, volumes): + _name = "%s %d" % (new_name, n) + new_name = _name + conn, c = self._conn() + c.execute('UPDATE volume SET name = ? WHERE name = ? AND site = ?', (new_name, name, site)) + conn.commit() + return { + 'name': new_name, + } + + def files(self, site, keys=[], order='path', limit=None): + conn, c = self._conn() + files = [] + sql = 'SELECT %s FROM file ORDER BY %s '% (','.join(keys), order) + if limit: + sql += ' LIMIT %d, %d' %(limit[0], limit[1]-limit[0]) + print sql + c.execute(sql) + for r in c: + f = {} + for i in range(len(r)): + f[keys[i]] = r[i] + files.append(f) + return files + + def add_file(self, site, filename): + info = utils.avinfo(filename) + return info['oshash'] + + def cache_file(self, site, url, itemId, filename): + conn, c = self._conn() filename, ext = filename.split('.') resolution, part = filename.split('p') print site, itemId, resolution, part, ext + c.execute('SELECT oshash FROM part WHERE site = ? AND id = ? AND part =?', + (site, itemId, part)) path = '' - if resolution == '480' and ext == 'webm': - path = '/home/j/.ox/media/44/c4/b1/11a888e96a/480p.webm' + for r in c: + oshash = r[0] + path = self.cache_path(oshash, '480p.webm') + break + + if path and not os.path.exists(path): + path = '' + if not path: + #FIXME: oshash get oshash for part + path = self.cache_path(oshash, '480p.webm') + #FIXME: need to add cookies + ox.net.saveUrl(url, path) + t = (oshash, part, itemId, site) + c.execute('INSERT INTO part (oshash, part, id, site) VALUES (?, ?, ?, ?)', t) + return path + + def get_file(self, site, itemId, filename): + conn, c = self._conn() + filename, ext = filename.split('.') + resolution, part = filename.split('p') + print site, itemId, resolution, part, ext + if len(itemId) == 16 and itemId.islower() and itemId.isalnum(): + path = self.cache_path(itemId, '480p.webm') + else: + c.execute('SELECT oshash FROM part WHERE site = ? AND id = ? AND part =?', + (site, itemId, part)) + path = '' + for r in c: + path = self.cache_path(r[0], '480p.webm') + break + + if path and not os.path.exists(path): + path = '' return path diff --git a/pandoralocal/server.py b/pandoralocal/server.py index 26e436b..5a63962 100644 --- a/pandoralocal/server.py +++ b/pandoralocal/server.py @@ -110,7 +110,7 @@ class ApiActions(dict): result = self[action](backend, site, data) else: result = json_response(status=404, text='not found') - print result + #print result return json.dumps(result) actions = ApiActions() diff --git a/pandoralocal/static/files.html b/pandoralocal/static/files.html new file mode 100644 index 0000000..698bcea --- /dev/null +++ b/pandoralocal/static/files.html @@ -0,0 +1,12 @@ + + +
+ +') + .html(pandora.actions[id].doc.replace('/\n/
\n/g')) + .appendTo(info); + var $code = $('') + .html(pandora.actions[id].code[1].replace('/\n/
\n/g')) + .hide(); + var f = pandora.actions[id].code[0]; + $('') + .html(' View Source ('+f+')') + .click(function() { $code.toggle();}) + .appendTo(info); + $('').append($code).appendTo(info); + hljs.highlightBlock($code[0], ' '); + + hash += id + ','; + }); + } else { + info.html(pandora.site.default_info); + } + + document.location.hash = hash.substring(0, hash.length-1); + pandora.$ui.actionInfo.html(info); + } + }); +} + +function constructList() { + return new Ox.TextList({ + columns: [ + { + align: "left", + id: "name", + operator: "+", + title: "Name", + unique: true, + visible: true, + width: 140 + }, + ], + columnsMovable: false, + columnsRemovable: false, + id: 'actionList', + items: function(data, callback) { + function _sort(a, b) { + if(a.name > b.name) + return 1; + else if(a.name == b.name) + return 0; + return -1; + } + pandora.api.findVolumes(function(result) { + var items = result.data.items; + items.sort(_sort); + callback({'data': { + 'items': data.keys ? items : items.length + }}); + }); + }, + scrollbarVisible: true, + sort: [ + { + key: "name", + operator: "+" + } + ] + }).bindEvent({ + select: function(data) { + if(data.ids.length) { + var volume = data.ids[0]; + Ox.print("FIXME", volume); + } + } + }); +} +}); + diff --git a/pandoralocal/ui.py b/pandoralocal/ui.py new file mode 100644 index 0000000..349aaa6 --- /dev/null +++ b/pandoralocal/ui.py @@ -0,0 +1,43 @@ +# encoding: utf-8 +# vi:si:et:sw=4:sts=4:ts=4 +import pygtk +pygtk.require('2.0') +import gtk + +def selectFolder(data): + dialog = gtk.FileChooserDialog("Select Folder..", + None, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + + response = dialog.run() + if response == gtk.RESPONSE_OK: + filename = dialog.get_filename() + print filename, 'selected' + elif response == gtk.RESPONSE_CANCEL: + print 'Closed, no files selected' + filename = None + dialog.destroy() + print "done" + return filename + +def selectFile(data): + dialog = gtk.FileChooserDialog("Select File..", + None, + gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + dialog.set_default_response(gtk.RESPONSE_OK) + + response = dialog.run() + if response == gtk.RESPONSE_OK: + filename = dialog.get_filename() + print filename, 'selected' + elif response == gtk.RESPONSE_CANCEL: + print 'Closed, no files selected' + filename = None + dialog.destroy() + print "done" + return filename diff --git a/pandoralocal/utils.py b/pandoralocal/utils.py new file mode 100644 index 0000000..ee81331 --- /dev/null +++ b/pandoralocal/utils.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +# GPL 2010 +from __future__ import division, with_statement + +import fractions +from glob import glob +import json +import os +import re +import sqlite3 +import subprocess +import sys +import shutil +import tempfile +import time + +import ox + + +class AspectRatio(fractions.Fraction): + def __new__(cls, numerator, denominator=None): + if not denominator: + ratio = map(int, numerator.split(':')) + if len(ratio) == 1: ratio.append(1) + numerator = ratio[0] + denominator = ratio[1] + #if its close enough to the common aspect ratios rather use that + if abs(numerator/denominator - 4/3) < 0.03: + numerator = 4 + denominator = 3 + elif abs(numerator/denominator - 16/9) < 0.02: + numerator = 16 + denominator = 9 + return super(AspectRatio, cls).__new__(cls, numerator, denominator) + + @property + def ratio(self): + return "%d:%d" % (self.numerator, self.denominator) + +def avinfo(filename): + if os.path.getsize(filename): + info = ox.avinfo(filename) + if 'video' in info and info['video'] and 'width' in info['video'][0]: + if not 'display_aspect_ratio' in info['video'][0]: + dar = AspectRatio(info['video'][0]['width'], info['video'][0]['height']) + info['video'][0]['display_aspect_ratio'] = dar.ratio + del info['path'] + if os.path.splitext(filename)[-1] in ('.srt', '.sub', '.idx', '.rar') and 'error' in info: + del info['error'] + if 'code' in info and info['code'] == 'badfile': + del info['code'] + return info + return {'path': filename, 'size': 0} + +def hash_prefix(h): + return [h[:2], h[2:4], h[4:6], h[6:]] + +def run_command(cmd, timeout=25): + #print cmd + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + while timeout > 0: + time.sleep(0.2) + timeout -= 0.2 + if p.poll() != None: + return p.returncode + if p.poll() == None: + os.kill(p.pid, 9) + killedpid, stat = os.waitpid(p.pid, os.WNOHANG) + return p.returncode + +def video_frame_positions(duration): + pos = duration / 2 + #return [pos/4, pos/2, pos/2+pos/4, pos, pos+pos/2, pos+pos/2+pos/4] + return map(int, [pos/2, pos, pos+pos/2]) +