From 74eed215b5d6eb3f74a206bd15fa31fd384aff5b Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Sat, 14 Jan 2012 18:27:33 +0530 Subject: [PATCH] add some backend/selectfile ui and api calls --- bin/pandoralocal | 7 +- pandoralocal/__init__.py | 16 +++ pandoralocal/api.py | 83 +++++++++++++ pandoralocal/backend.py | 204 +++++++++++++++++++++++++++++++- pandoralocal/server.py | 2 +- pandoralocal/static/files.html | 12 ++ pandoralocal/static/js/files.js | 189 +++++++++++++++++++++++++++++ pandoralocal/ui.py | 43 +++++++ pandoralocal/utils.py | 77 ++++++++++++ 9 files changed, 623 insertions(+), 10 deletions(-) create mode 100644 pandoralocal/static/files.html create mode 100755 pandoralocal/static/js/files.js create mode 100644 pandoralocal/ui.py create mode 100644 pandoralocal/utils.py 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 @@ + + + + + pandoralocal + + + + + + + diff --git a/pandoralocal/static/js/files.js b/pandoralocal/static/js/files.js new file mode 100755 index 0000000..0ff13c4 --- /dev/null +++ b/pandoralocal/static/js/files.js @@ -0,0 +1,189 @@ +/*** + PandoraLocal +***/ +Ox.load('UI', { + hideScreen: false, + showScreen: true, + theme: 'classic' +}, function() { + +window.pandora = new Ox.App({ + apiURL: '/api/', + init: 'init', +}).bindEvent('load', function(data) { + pandora.site = { + }; + Ox.UI.hideLoadingScreen(); + + pandora.$ui = { + body: $('body'), + document: $(document), + window: $(window) + .bind({ + resize: function() { + //pandora.resizeWindow(); + }, + unload: function() { + //pandora.nloadWindow(); + } + }) + }; + pandora.$ui.volumes = constructList(); + pandora.$ui.files = constructFiles(); + + var $left = new Ox.SplitPanel({ + elements: [ + { + element: new Ox.Element().append(new Ox.Element() + .html('Pandoralocal').css({ + 'padding': '4px', + })).css({ + 'background-color': '#ddd', + 'font-weight': 'bold', + }), + size: 24 + }, + { + element: pandora.$ui.volumes + } + ], + orientation: 'vertical' + }); + var $main = new Ox.SplitPanel({ + elements: [ + { + element: $left, + size: 160 + }, + { + element: pandora.$ui.files, + } + ], + orientation: 'horizontal' + }); + + $main.appendTo(pandora.$ui.body); +}); + +function constructFiles() { + return new Ox.TextList({ + columns: [ + { + align: "left", + id: "path", + operator: "+", + title: "Name", + unique: true, + visible: true, + width: 860 + }, + { + align: "left", + id: "size", + operator: "-", + title: "Size", + visible: true, + width: 120 + }, + { + align: "left", + id: "oshash", + operator: "-", + title: "ID", + visible: true, + width: 120 + }, + ], + columnsMovable: true, + columnsRemovable: true, + items: pandora.api.findFiles, + scrollbarVisible: true, + sort: [ + { + key: "path", + operator: "+" + } + ] + }).bindEvent({ + select: function(data) { + var info = $('
').addClass('OxSelectable'), + hash = '#'; + if(data.ids.length) { + data.ids.forEach(function(id) { + info.append($("

").html(id)); + var $doc =$('
')
+                           .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])
+