Open Media Library
This commit is contained in:
commit
2ee2bc178a
228 changed files with 85988 additions and 0 deletions
0
oml/__init__.py
Normal file
0
oml/__init__.py
Normal file
16
oml/__main__.py
Normal file
16
oml/__main__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
import app
|
||||
import server
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'server':
|
||||
import server
|
||||
server.run()
|
||||
else:
|
||||
app.manager.run()
|
||||
6
oml/api.py
Normal file
6
oml/api.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import item.api
|
||||
import user.api
|
||||
|
||||
43
oml/app.py
Normal file
43
oml/app.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from flask import Flask
|
||||
from flask.ext.script import Manager
|
||||
from flask.ext.migrate import Migrate, MigrateCommand
|
||||
|
||||
|
||||
import oxflask.api
|
||||
|
||||
import settings
|
||||
from settings import db
|
||||
|
||||
import changelog
|
||||
import item.models
|
||||
import user.models
|
||||
import item.person
|
||||
|
||||
import item.api
|
||||
import user.api
|
||||
|
||||
import item.views
|
||||
import commands
|
||||
|
||||
app = Flask('openmedialibrary', static_folder=settings.static_path)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////%s' % settings.db_path
|
||||
app.register_blueprint(oxflask.api.app)
|
||||
app.register_blueprint(item.views.app)
|
||||
db.init_app(app)
|
||||
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
manager = Manager(app)
|
||||
manager.add_command('db', MigrateCommand)
|
||||
manager.add_command('setup', commands.Setup)
|
||||
manager.add_command('update_static', commands.UpdateStatic)
|
||||
manager.add_command('release', commands.Release)
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/<path:path>')
|
||||
def main(path=None):
|
||||
return app.send_static_file('html/oml.html')
|
||||
|
||||
230
oml/changelog.py
Normal file
230
oml/changelog.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ed25519_utils import valid
|
||||
|
||||
import settings
|
||||
from settings import db
|
||||
import state
|
||||
from websocket import trigger_event
|
||||
|
||||
class Changelog(db.Model):
|
||||
'''
|
||||
additem itemid metadata from file (info) + OLID
|
||||
edititem itemid name->id (i.e. olid-> OL...M)
|
||||
removeitem itemid
|
||||
addlist name
|
||||
editlist name {name: newname}
|
||||
orderlists [name, name, name]
|
||||
removelist name
|
||||
additemtolist listname itemid
|
||||
removeitemfromlist listname itemid
|
||||
editusername username
|
||||
editcontact string
|
||||
addpeer peerid peername
|
||||
removepeer peerid peername
|
||||
'''
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
|
||||
created = db.Column(db.DateTime())
|
||||
|
||||
user_id = db.Column(db.String(43))
|
||||
revision = db.Column(db.BigInteger())
|
||||
data = db.Column(db.Text())
|
||||
sig = db.Column(db.String(96))
|
||||
|
||||
@classmethod
|
||||
def record(cls, user, action, *args):
|
||||
c = cls()
|
||||
c.created = datetime.now()
|
||||
c.user_id = user.id
|
||||
c.revision = cls.query.filter_by(user_id=user.id).count()
|
||||
c.data = json.dumps([action, args])
|
||||
timestamp = c.timestamp
|
||||
_data = str(c.revision) + str(timestamp) + c.data
|
||||
c.sig = settings.sk.sign(_data, encoding='base64')
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
if state.online:
|
||||
state.nodes.queue('online', 'pushChanges', [c.json()])
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return self.created.strftime('%s')
|
||||
|
||||
@classmethod
|
||||
def apply_change(cls, user, change, rebuild=False):
|
||||
revision, timestamp, sig, data = change
|
||||
last = Changelog.query.filter_by(user_id=user.id).order_by('-revision').first()
|
||||
next_revision = last.revision + 1 if last else 0
|
||||
if revision == next_revision:
|
||||
_data = str(revision) + str(timestamp) + data
|
||||
if rebuild:
|
||||
sig = settings.sk.sign(_data, encoding='base64')
|
||||
if valid(user.id, _data, sig):
|
||||
c = cls()
|
||||
c.created = datetime.now()
|
||||
c.user_id = user.id
|
||||
c.revision = revision
|
||||
c.data = data
|
||||
c.sig = sig
|
||||
action, args = json.loads(data)
|
||||
print 'apply change', action
|
||||
if getattr(c, 'action_' + action)(user, timestamp, *args):
|
||||
print 'change applied'
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
return True
|
||||
else:
|
||||
print 'INVLAID SIGNATURE ON CHANGE', change
|
||||
raise Exception, 'invalid signature'
|
||||
else:
|
||||
print 'revsion does not match! got', revision, 'expecting', next_revision
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return self.data
|
||||
|
||||
def verify(self):
|
||||
_data = str(self.revision) + str(self.timestamp) + self.data
|
||||
return valid(self.user_id, _data, self.sig)
|
||||
|
||||
def json(self):
|
||||
return [self.revision, self.timestamp, self.sig, self.data]
|
||||
|
||||
@classmethod
|
||||
def restore(cls, user_id, path=None):
|
||||
from user.models import User
|
||||
user = User.get_or_create(user_id)
|
||||
if not path:
|
||||
path = '/tmp/oml_changelog_%s.json' % user_id
|
||||
with open(path, 'r') as fd:
|
||||
for change in fd:
|
||||
change = json.loads(change)
|
||||
cls.apply_change(user, change, user_id == settings.USER_ID)
|
||||
|
||||
@classmethod
|
||||
def export(cls, user_id, path=None):
|
||||
if not path:
|
||||
path = '/tmp/oml_changelog_%s.json' % user_id
|
||||
with open(path, 'w') as fd:
|
||||
for c in cls.query.filter_by(user_id=user_id).order_by('revision'):
|
||||
fd.write(json.dumps(c.json()) + '\n')
|
||||
|
||||
def action_additem(self, user, timestamp, itemid, info):
|
||||
from item.models import Item
|
||||
i = Item.get(itemid)
|
||||
if i and i.timestamp > timestamp:
|
||||
return True
|
||||
if not i:
|
||||
i = Item.get_or_create(itemid, info)
|
||||
i.users.append(user)
|
||||
i.update()
|
||||
return True
|
||||
|
||||
def action_edititem(self, user, timestamp, itemid, meta):
|
||||
from item.models import Item
|
||||
i = Item.get(itemid)
|
||||
if i.timestamp > timestamp:
|
||||
return True
|
||||
key = meta.keys()[0]
|
||||
if not meta[key] and i.meta.get('mainid') == key:
|
||||
print 'remove id mapping', key, meta[key], 'currently', i.meta[key]
|
||||
i.update_mainid(key, meta[key])
|
||||
elif meta[key] and (i.meta.get('mainid') != key or meta[key] != i.meta.get(key)):
|
||||
print 'new mapping', key, meta[key], 'currently', i.meta.get('mainid'), i.meta.get(i.meta.get('mainid'))
|
||||
i.update_mainid(key, meta[key])
|
||||
return True
|
||||
|
||||
def action_removeitem(self, user, timestamp, itemid):
|
||||
from item.models import Item
|
||||
i = Item.get(itemid)
|
||||
if not i or i.timestamp > timestamp:
|
||||
return True
|
||||
i.users.remove(user)
|
||||
if i.users:
|
||||
i.update()
|
||||
else:
|
||||
db.session.delete(i)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
def action_addlist(self, user, timestamp, name, query=None):
|
||||
from user.models import List
|
||||
l = List.create(user.id, name)
|
||||
return True
|
||||
|
||||
def action_editlist(self, user, timestamp, name, new):
|
||||
from user.models import List
|
||||
l = List.get_or_create(user.id, name)
|
||||
if 'name' in new:
|
||||
l.name = new['name']
|
||||
l.save()
|
||||
return True
|
||||
|
||||
def action_orderlists(self, user, timestamp, lists):
|
||||
from user.models import List
|
||||
position = 0
|
||||
for name in lists:
|
||||
l = List.get_or_create(user.id, name)
|
||||
l.position = position
|
||||
l.save()
|
||||
position += 1
|
||||
return True
|
||||
|
||||
def action_removelist(self, user, timestamp, name):
|
||||
from user.models import List
|
||||
l = List.get(user.id, name)
|
||||
if l:
|
||||
l.remove()
|
||||
return True
|
||||
|
||||
def action_addlistitem(self, user, timestamp, name, itemid):
|
||||
from item.models import Item
|
||||
from user.models import List
|
||||
l = List.get(user.id, name)
|
||||
i = Item.get(itemid)
|
||||
if l and i:
|
||||
i.lists.append(l)
|
||||
i.update()
|
||||
return True
|
||||
|
||||
def action_removelistitem(self, user, timestamp, name, itemid):
|
||||
from item.models import Item
|
||||
from user.models import List
|
||||
l = List.get(user.id, name)
|
||||
i = Item.get(itemid)
|
||||
if l and i:
|
||||
i.lists.remove(l)
|
||||
i.update()
|
||||
return True
|
||||
|
||||
def action_editusername(self, user, timestamp, username):
|
||||
user.info['username'] = username
|
||||
user.save()
|
||||
return True
|
||||
|
||||
def action_editcontact(self, user, timestamp, contact):
|
||||
user.info['contact'] = contact
|
||||
user.save()
|
||||
return True
|
||||
|
||||
def action_adduser(self, user, timestamp, peerid, username):
|
||||
from user.models import User
|
||||
if not 'users' in user.info:
|
||||
user.info['users'] = {}
|
||||
user.info['users'][peerid] = username
|
||||
user.save()
|
||||
User.get_or_create(peerid)
|
||||
#fixme, add username to user?
|
||||
return True
|
||||
|
||||
def action_removeuser(self, user, timestamp, peerid):
|
||||
if 'users' in user.info and peerid in user.info['users']:
|
||||
del user.info['users'][peerid]
|
||||
user.save()
|
||||
#fixme, remove from User table if no other connection exists
|
||||
return True
|
||||
115
oml/commands.py
Normal file
115
oml/commands.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from flask.ext.script import Command
|
||||
|
||||
|
||||
class Setup(Command):
|
||||
"""
|
||||
setup new node
|
||||
"""
|
||||
def run(self):
|
||||
import setup
|
||||
setup.create_default_lists()
|
||||
|
||||
class UpdateStatic(Command):
|
||||
"""
|
||||
setup new node
|
||||
"""
|
||||
def run(self):
|
||||
import subprocess
|
||||
import os
|
||||
import settings
|
||||
|
||||
def r(*cmd):
|
||||
print ' '.join(cmd)
|
||||
return subprocess.call(cmd)
|
||||
|
||||
oxjs = os.path.join(settings.static_path, 'oxjs')
|
||||
if not os.path.exists(oxjs):
|
||||
r('git', 'clone', 'https://git.0x2620.org/oxjs.git', oxjs)
|
||||
r('python', os.path.join(oxjs, 'tools', 'build', 'build.py'))
|
||||
r('python', os.path.join(settings.static_path, 'py', 'build.py'))
|
||||
|
||||
class Release(Command):
|
||||
"""
|
||||
release new version
|
||||
"""
|
||||
def run(self):
|
||||
print 'checking...'
|
||||
import settings
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
import hashlib
|
||||
import ed25519
|
||||
from os.path import join, exists, dirname
|
||||
|
||||
root_dir = dirname(settings.base_dir)
|
||||
os.chdir(root_dir)
|
||||
|
||||
def run(*cmd):
|
||||
p = subprocess.Popen(cmd)
|
||||
p.wait()
|
||||
return p.returncode
|
||||
|
||||
def get(*cmd):
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, error = p.communicate()
|
||||
return stdout
|
||||
|
||||
def version(module):
|
||||
os.chdir(join(root_dir, module))
|
||||
version = get('git', 'log', '-1', '--format=%cd', '--date=iso').split(' ')[0].replace('-', '')
|
||||
version += '-' + get('git', 'rev-list', 'HEAD', '--count').strip()
|
||||
version += '-' + get('git', 'describe', '--always').strip()
|
||||
os.chdir(root_dir)
|
||||
return version
|
||||
|
||||
with open(os.path.expanduser('~/Private/openmedialibrary_release.key')) as fd:
|
||||
SIG_KEY=ed25519.SigningKey(fd.read())
|
||||
SIG_ENCODING='base64'
|
||||
|
||||
def sign(release):
|
||||
value = []
|
||||
for module in sorted(release['modules']):
|
||||
value += ['%s/%s' % (release['modules'][module]['version'], release['modules'][module]['sha1'])]
|
||||
value = '\n'.join(value)
|
||||
sig = SIG_KEY.sign(value, encoding=SIG_ENCODING)
|
||||
release['signature'] = sig
|
||||
|
||||
def sha1sum(path):
|
||||
h = hashlib.sha1()
|
||||
with open(path) as fd:
|
||||
for chunk in iter(lambda: fd.read(128*h.block_size), ''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
MODULES = ['platform', 'openmedialibrary']
|
||||
VERSIONS = {module:version(module) for module in MODULES}
|
||||
|
||||
EXCLUDE=[
|
||||
'--exclude', '.git', '--exclude', '.bzr',
|
||||
'--exclude', '.*.swp', '--exclude', '._*', '--exclude', '.DS_Store'
|
||||
]
|
||||
|
||||
#run('./ctl', 'update_static')
|
||||
for module in MODULES:
|
||||
tar = join('updates', '%s-%s.tar.bz2' % (module, VERSIONS[module]))
|
||||
if not exists(tar):
|
||||
cmd = ['tar', 'cvjf', tar, '%s/' % module] + EXCLUDE
|
||||
if module in ('openmedialibrary', ):
|
||||
cmd += ['--exclude', '*.pyc']
|
||||
if module == 'openmedialibrary':
|
||||
cmd += ['--exclude', 'oxjs/examples', '--exclude', 'gunicorn.pid']
|
||||
run(*cmd)
|
||||
release = {}
|
||||
release['modules'] = {module: {
|
||||
'name': '%s-%s.tar.bz2' % (module, VERSIONS[module]),
|
||||
'version': VERSIONS[module],
|
||||
'sha1': sha1sum(join('updates', '%s-%s.tar.bz2' % (module, VERSIONS[module])))
|
||||
} for module in MODULES}
|
||||
sign(release)
|
||||
with open('updates/release.json', 'w') as fd:
|
||||
json.dump(release, fd, indent=2)
|
||||
print 'signed latest release in updates/release.json'
|
||||
44
oml/directory.py
Normal file
44
oml/directory.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# DHT placeholder
|
||||
|
||||
import requests
|
||||
import ed25519
|
||||
import json
|
||||
import settings
|
||||
|
||||
base = settings.server['directory_service']
|
||||
|
||||
def get(vk):
|
||||
id = vk.to_ascii(encoding='base64')
|
||||
url ='%s/%s' % (base, id)
|
||||
r = requests.get(url)
|
||||
sig = r.headers.get('X-Ed25519-Signature')
|
||||
data = r.content
|
||||
if sig and data:
|
||||
vk = ed25519.VerifyingKey(id, encoding='base64')
|
||||
try:
|
||||
vk.verify(sig, data, encoding='base64')
|
||||
data = json.loads(data)
|
||||
except ed25519.BadSignatureError:
|
||||
print 'invalid signature'
|
||||
data = None
|
||||
return data
|
||||
|
||||
def put(sk, data):
|
||||
id = sk.get_verifying_key().to_ascii(encoding='base64')
|
||||
data = json.dumps(data)
|
||||
sig = sk.sign(data, encoding='base64')
|
||||
url ='%s/%s' % (base, id)
|
||||
headers = {
|
||||
'X-Ed25519-Signature': sig
|
||||
}
|
||||
try:
|
||||
r = requests.put(url, data, headers=headers)
|
||||
except:
|
||||
import traceback
|
||||
print 'directory.put failed:', data
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return r.status_code == 200
|
||||
46
oml/downloads.py
Normal file
46
oml/downloads.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
import state
|
||||
|
||||
class Downloads(Thread):
|
||||
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
self._running = True
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def download_next(self):
|
||||
import item.models
|
||||
for i in item.models.Item.query.filter(
|
||||
item.models.Item.transferadded!=None).filter(
|
||||
item.models.Item.transferprogress<1):
|
||||
print 'DOWNLOAD', i, i.users
|
||||
for p in i.users:
|
||||
if state.nodes.check_online(p.id):
|
||||
r = state.nodes.download(p.id, i)
|
||||
print 'download ok?', r
|
||||
return True
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
time.sleep(2)
|
||||
with self._app.app_context():
|
||||
while self._running:
|
||||
if state.online:
|
||||
self.download_next()
|
||||
time.sleep(10)
|
||||
else:
|
||||
time.sleep(20)
|
||||
|
||||
def join(self):
|
||||
self._running = False
|
||||
self._q.put(None)
|
||||
return Thread.join(self)
|
||||
|
||||
14
oml/ed25519_utils.py
Normal file
14
oml/ed25519_utils.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import ed25519
|
||||
ENCODING='base64'
|
||||
|
||||
def valid(key, value, sig):
|
||||
'''
|
||||
validate that value was signed by key
|
||||
'''
|
||||
vk = ed25519.VerifyingKey(str(key), encoding=ENCODING)
|
||||
try:
|
||||
vk.verify(str(sig), str(value), encoding=ENCODING)
|
||||
#except ed25519.BadSignatureError:
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
0
oml/item/__init__.py
Normal file
0
oml/item/__init__.py
Normal file
19
oml/item/add.py
Normal file
19
oml/item/add.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import base64
|
||||
import models
|
||||
|
||||
import ox
|
||||
|
||||
import scan
|
||||
|
||||
def add(path):
|
||||
info = scan.get_metadata(path)
|
||||
id = info.pop('id')
|
||||
item = models.Item.get_or_create(id)
|
||||
item.path = path
|
||||
item.info = info
|
||||
models.db.session.add(item)
|
||||
models.db.session.commit()
|
||||
|
||||
210
oml/item/api.py
Normal file
210
oml/item/api.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from datetime import datetime
|
||||
|
||||
from flask import json
|
||||
from oxflask.api import actions
|
||||
from oxflask.shortcuts import returns_json
|
||||
|
||||
from oml import utils
|
||||
import query
|
||||
|
||||
import models
|
||||
import settings
|
||||
from changelog import Changelog
|
||||
import re
|
||||
import state
|
||||
|
||||
import utils
|
||||
|
||||
@returns_json
|
||||
def find(request):
|
||||
'''
|
||||
find items
|
||||
'''
|
||||
response = {}
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
q = query.parse(data)
|
||||
if 'group' in q:
|
||||
response['items'] = []
|
||||
'''
|
||||
items = 'items'
|
||||
item_qs = q['qs']
|
||||
order_by = query.order_by_group(q)
|
||||
qs = models.Facet.objects.filter(key=q['group']).filter(item__id__in=item_qs)
|
||||
qs = qs.values('value').annotate(items=Count('id')).order_by(*order_by)
|
||||
|
||||
if 'positions' in q:
|
||||
response['positions'] = {}
|
||||
ids = [j['value'] for j in qs]
|
||||
response['positions'] = utils.get_positions(ids, q['positions'])
|
||||
elif 'range' in data:
|
||||
qs = qs[q['range'][0]:q['range'][1]]
|
||||
response['items'] = [{'name': i['value'], 'items': i[items]} for i in qs]
|
||||
else:
|
||||
response['items'] = qs.count()
|
||||
'''
|
||||
_g = {}
|
||||
key = utils.get_by_id(settings.config['itemKeys'], q['group'])
|
||||
for item in q['qs']:
|
||||
i = item.json()
|
||||
if q['group'] in i:
|
||||
values = i[q['group']]
|
||||
if isinstance(values, basestring):
|
||||
values = [values]
|
||||
for value in values:
|
||||
if key.get('filterMap') and value:
|
||||
value = re.compile(key.get('filterMap')).findall(value)
|
||||
if value:
|
||||
value = value[0]
|
||||
else:
|
||||
continue
|
||||
if value not in _g:
|
||||
_g[value] = 0
|
||||
_g[value] += 1
|
||||
g = [{'name': k, 'items': _g[k]} for k in _g]
|
||||
if 'sort' in data: # parse adds default sort to q!
|
||||
g.sort(key=lambda k: k[q['sort'][0]['key']])
|
||||
if q['sort'][0]['operator'] == '-':
|
||||
g.reverse()
|
||||
if 'positions' in data:
|
||||
response['positions'] = {}
|
||||
ids = [k['name'] for k in g]
|
||||
response['positions'] = utils.get_positions(ids, data['positions'])
|
||||
elif 'range' in data:
|
||||
response['items'] = g[q['range'][0]:q['range'][1]]
|
||||
else:
|
||||
response['items'] = len(g)
|
||||
elif 'position' in data:
|
||||
ids = [i.id for i in q['qs']]
|
||||
response['position'] = utils.get_positions(ids, [data['qs'][0].id])[0]
|
||||
elif 'positions' in data:
|
||||
ids = [i.id for i in q['qs']]
|
||||
response['positions'] = utils.get_positions(ids, data['positions'])
|
||||
elif 'keys' in data:
|
||||
'''
|
||||
qs = qs[q['range'][0]:q['range'][1]]
|
||||
response['items'] = [p.json(data['keys']) for p in qs]
|
||||
'''
|
||||
response['items'] = []
|
||||
for i in q['qs'][q['range'][0]:q['range'][1]]:
|
||||
j = i.json()
|
||||
response['items'].append({k:j[k] for k in j if not data['keys'] or k in data['keys']})
|
||||
else:
|
||||
items = [i.json() for i in q['qs']]
|
||||
response['items'] = len(items)
|
||||
response['size'] = sum([i.get('size',0) for i in items])
|
||||
return response
|
||||
actions.register(find)
|
||||
|
||||
@returns_json
|
||||
def get(request):
|
||||
response = {}
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
item = models.Item.get(data['id'])
|
||||
if item:
|
||||
response = item.json(data['keys'] if 'keys' in data else None)
|
||||
return response
|
||||
actions.register(get)
|
||||
|
||||
@returns_json
|
||||
def edit(request):
|
||||
response = {}
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
print 'edit', data
|
||||
item = models.Item.get(data['id'])
|
||||
keys = filter(lambda k: k in models.Item.id_keys, data.keys())
|
||||
print item, keys
|
||||
if item and keys and item.json()['mediastate'] == 'available':
|
||||
key = keys[0]
|
||||
print 'update mainid', key, data[key]
|
||||
item.update_mainid(key, data[key])
|
||||
response = item.json()
|
||||
else:
|
||||
print 'can only edit available items'
|
||||
response = item.json()
|
||||
return response
|
||||
actions.register(edit, cache=False)
|
||||
|
||||
|
||||
@returns_json
|
||||
def identify(request):
|
||||
'''
|
||||
takes {
|
||||
title: string,
|
||||
author: [string],
|
||||
publisher: string,
|
||||
date: string
|
||||
}
|
||||
returns {
|
||||
title: string,
|
||||
autor: [string],
|
||||
date: string,
|
||||
}
|
||||
'''
|
||||
response = {}
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
response = {
|
||||
'items': [
|
||||
{
|
||||
u'title': u'Cinema',
|
||||
u'author': [u'Gilles Deleuze'],
|
||||
u'date': u'1986-10',
|
||||
u'publisher': u'University of Minnesota Press',
|
||||
u'isbn10': u'0816613990',
|
||||
},
|
||||
{
|
||||
u'title': u'How to Change the World: Reflections on Marx and Marxism',
|
||||
u'author': [u'Eric Hobsbawm'],
|
||||
u'date': u'2011-09-06',
|
||||
u'publisher': u'Yale University Press',
|
||||
u'isbn13': u'9780300176162',
|
||||
}
|
||||
]
|
||||
}
|
||||
return response
|
||||
actions.register(identify)
|
||||
|
||||
@returns_json
|
||||
def download(request):
|
||||
response = {}
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
item = models.Item.get(data['id'])
|
||||
if item:
|
||||
item.transferprogress = 0
|
||||
item.transferadded = datetime.now()
|
||||
p = models.User.get(settings.USER_ID)
|
||||
if p not in item.users:
|
||||
item.users.append(p)
|
||||
item.update()
|
||||
response = {'status': 'queued'}
|
||||
return response
|
||||
actions.register(download, cache=False)
|
||||
|
||||
@returns_json
|
||||
def cancelDownload(request):
|
||||
response = {}
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
item = models.Item.get(data['id'])
|
||||
if item:
|
||||
item.transferprogress = None
|
||||
item.transferadded = None
|
||||
p = models.User.get(settings.USER_ID)
|
||||
if p in item.users:
|
||||
item.users.remove(p)
|
||||
item.update()
|
||||
response = {'status': 'cancelled'}
|
||||
return response
|
||||
actions.register(cancelDownload, cache=False)
|
||||
|
||||
@returns_json
|
||||
def scan(request):
|
||||
state.main.add_callback(state.websockets[0].put, json.dumps(['scan', {}]))
|
||||
return {}
|
||||
actions.register(scan, cache=False)
|
||||
|
||||
@returns_json
|
||||
def _import(request):
|
||||
state.main.add_callback(state.websockets[0].put, json.dumps(['import', {}]))
|
||||
return {}
|
||||
actions.register(_import, 'import', cache=False)
|
||||
74
oml/item/covers.py
Normal file
74
oml/item/covers.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import sqlite3
|
||||
import Image
|
||||
from StringIO import StringIO
|
||||
|
||||
from settings import covers_db_path
|
||||
|
||||
class Covers(dict):
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def connect(self):
|
||||
self.conn = sqlite3.connect(self._db, timeout=10)
|
||||
self.create()
|
||||
|
||||
def create(self):
|
||||
c = self.conn.cursor()
|
||||
c.execute(u'CREATE TABLE IF NOT EXISTS cover (id varchar(64) unique, data blob)')
|
||||
c.execute(u'CREATE TABLE IF NOT EXISTS setting (key varchar(256) unique, value text)')
|
||||
if int(self.get_setting(c, 'version', 0)) < 1:
|
||||
self.set_setting(c, 'version', 1)
|
||||
|
||||
def get_setting(self, c, key, default=None):
|
||||
c.execute(u'SELECT value FROM setting WHERE key = ?', (key, ))
|
||||
for row in c:
|
||||
return row[0]
|
||||
return default
|
||||
|
||||
def set_setting(self, c, key, value):
|
||||
c.execute(u'INSERT OR REPLACE INTO setting values (?, ?)', (key, str(value)))
|
||||
|
||||
def black(self):
|
||||
img = Image.new('RGB', (80, 128))
|
||||
o = StringIO()
|
||||
img.save(o, format='jpeg')
|
||||
data = o.getvalue()
|
||||
o.close()
|
||||
return data
|
||||
|
||||
def __getitem__(self, id, default=None):
|
||||
sql = u'SELECT data FROM cover WHERE id=?'
|
||||
self.connect()
|
||||
c = self.conn.cursor()
|
||||
c.execute(sql, (id, ))
|
||||
data = default
|
||||
for row in c:
|
||||
data = row[0]
|
||||
break
|
||||
c.close()
|
||||
self.conn.close()
|
||||
return data
|
||||
|
||||
def __setitem__(self, id, data):
|
||||
sql = u'INSERT OR REPLACE INTO cover values (?, ?)'
|
||||
self.connect()
|
||||
c = self.conn.cursor()
|
||||
data = sqlite3.Binary(data)
|
||||
c.execute(sql, (id, data))
|
||||
self.conn.commit()
|
||||
c.close()
|
||||
self.conn.close()
|
||||
|
||||
def __delitem__(self, id):
|
||||
sql = u'DELETE FROM cover WHERE id = ?'
|
||||
self.connect()
|
||||
c = self.conn.cursor()
|
||||
c.execute(sql, (id, ))
|
||||
self.conn.commit()
|
||||
c.close()
|
||||
self.conn.close()
|
||||
|
||||
covers = Covers(covers_db_path)
|
||||
13
oml/item/migrate.py
Normal file
13
oml/item/migrate.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import models
|
||||
from copy import deepcopy
|
||||
|
||||
def import_all():
|
||||
for i in models.items:
|
||||
item = models.Item.get_or_create(i['id'])
|
||||
item.path = i['path']
|
||||
item.info = deepcopy(i)
|
||||
del item.info['path']
|
||||
del item.info['id']
|
||||
item.meta = item.info.pop('meta', {})
|
||||
models.db.session.add(item)
|
||||
models.db.session.commit()
|
||||
427
oml/item/models.py
Normal file
427
oml/item/models.py
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
import json
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from StringIO import StringIO
|
||||
|
||||
import Image
|
||||
import ox
|
||||
|
||||
import settings
|
||||
from settings import db, config
|
||||
|
||||
from user.models import User
|
||||
|
||||
from person import get_sort_name
|
||||
|
||||
import media
|
||||
from meta import scraper
|
||||
|
||||
import utils
|
||||
|
||||
from oxflask.db import MutableDict
|
||||
|
||||
from covers import covers
|
||||
from changelog import Changelog
|
||||
from websocket import trigger_event
|
||||
|
||||
class Work(db.Model):
|
||||
|
||||
created = db.Column(db.DateTime())
|
||||
modified = db.Column(db.DateTime())
|
||||
|
||||
id = db.Column(db.String(32), primary_key=True)
|
||||
|
||||
meta = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
|
||||
def __repr__(self):
|
||||
return self.id
|
||||
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.created = datetime.now()
|
||||
self.modified = datetime.now()
|
||||
|
||||
class Edition(db.Model):
|
||||
|
||||
created = db.Column(db.DateTime())
|
||||
modified = db.Column(db.DateTime())
|
||||
|
||||
id = db.Column(db.String(32), primary_key=True)
|
||||
|
||||
meta = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
|
||||
work_id = db.Column(db.String(32), db.ForeignKey('work.id'))
|
||||
work = db.relationship('Work', backref=db.backref('editions', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return self.id
|
||||
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.created = datetime.now()
|
||||
self.modified = datetime.now()
|
||||
|
||||
user_items = db.Table('useritem',
|
||||
db.Column('user_id', db.String(43), db.ForeignKey('user.id')),
|
||||
db.Column('item_id', db.String(32), db.ForeignKey('item.id'))
|
||||
)
|
||||
|
||||
class Item(db.Model):
|
||||
|
||||
created = db.Column(db.DateTime())
|
||||
modified = db.Column(db.DateTime())
|
||||
|
||||
id = db.Column(db.String(32), primary_key=True)
|
||||
|
||||
info = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
meta = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
|
||||
added = db.Column(db.DateTime()) # added to local library
|
||||
accessed = db.Column(db.DateTime())
|
||||
timesaccessed = db.Column(db.Integer())
|
||||
|
||||
transferadded = db.Column(db.DateTime())
|
||||
transferprogress = db.Column(db.Float())
|
||||
|
||||
users = db.relationship('User', secondary=user_items,
|
||||
backref=db.backref('items', lazy='dynamic'))
|
||||
|
||||
edition_id = db.Column(db.String(32), db.ForeignKey('edition.id'))
|
||||
edition = db.relationship('Edition', backref=db.backref('items', lazy='dynamic'))
|
||||
|
||||
work_id = db.Column(db.String(32), db.ForeignKey('work.id'))
|
||||
work = db.relationship('Work', backref=db.backref('items', lazy='dynamic'))
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return self.modified.strftime('%s')
|
||||
|
||||
def __repr__(self):
|
||||
return self.id
|
||||
|
||||
def __init__(self, id):
|
||||
if isinstance(id, list):
|
||||
id = base64.b32encode(hashlib.sha1(''.join(id)).digest())
|
||||
self.id = id
|
||||
self.created = datetime.now()
|
||||
self.modified = datetime.now()
|
||||
self.info = {}
|
||||
self.meta = {}
|
||||
|
||||
@classmethod
|
||||
def get(cls, id):
|
||||
if isinstance(id, list):
|
||||
id = base64.b32encode(hashlib.sha1(''.join(id)).digest())
|
||||
return cls.query.filter_by(id=id).first()
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, id, info=None):
|
||||
if isinstance(id, list):
|
||||
id = base64.b32encode(hashlib.sha1(''.join(id)).digest())
|
||||
item = cls.query.filter_by(id=id).first()
|
||||
if not item:
|
||||
item = cls(id=id)
|
||||
if info:
|
||||
item.info = info
|
||||
db.session.add(item)
|
||||
db.session.commit()
|
||||
return item
|
||||
|
||||
def json(self, keys=None):
|
||||
j = {}
|
||||
j['id'] = self.id
|
||||
j['created'] = self.created
|
||||
j['modified'] = self.modified
|
||||
j['timesaccessed'] = self.timesaccessed
|
||||
j['accessed'] = self.accessed
|
||||
j['added'] = self.added
|
||||
j['transferadded'] = self.transferadded
|
||||
j['transferprogress'] = self.transferprogress
|
||||
j['users'] = map(str, list(self.users))
|
||||
|
||||
if self.info:
|
||||
j.update(self.info)
|
||||
if self.meta:
|
||||
j.update(self.meta)
|
||||
|
||||
for key in self.id_keys + ['mainid']:
|
||||
if key not in self.meta and key in j:
|
||||
del j[key]
|
||||
'''
|
||||
if self.work_id:
|
||||
j['work'] = {
|
||||
'olid': self.work_id
|
||||
}
|
||||
j['work'].update(self.work.meta)
|
||||
'''
|
||||
if keys:
|
||||
for k in j.keys():
|
||||
if k not in keys:
|
||||
del j[k]
|
||||
return j
|
||||
|
||||
def get_path(self):
|
||||
f = self.files.first()
|
||||
prefs = settings.preferences
|
||||
prefix = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||
return os.path.join(prefix, f.path) if f else None
|
||||
|
||||
def update_sort(self):
|
||||
for key in config['itemKeys']:
|
||||
if key.get('sort'):
|
||||
value = self.json().get(key['id'], None)
|
||||
sort_type = key.get('sortType', key['type'])
|
||||
if value:
|
||||
if sort_type == 'integer':
|
||||
value = int(value)
|
||||
elif sort_type == 'float':
|
||||
value = float(value)
|
||||
elif sort_type == 'date':
|
||||
pass
|
||||
elif sort_type == 'name':
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
value = map(get_sort_name, value)
|
||||
value = ox.sort_string(u'\n'.join(value))
|
||||
elif sort_type == 'title':
|
||||
value = utils.sort_title(value).lower()
|
||||
else:
|
||||
if isinstance(value, list):
|
||||
value = u'\n'.join(value)
|
||||
if value:
|
||||
value = unicode(value)
|
||||
value = ox.sort_string(value).lower()
|
||||
setattr(self, 'sort_%s' % key['id'], value)
|
||||
|
||||
def update_find(self):
|
||||
for key in config['itemKeys']:
|
||||
if key.get('find') or key.get('filter'):
|
||||
value = self.json().get(key['id'], None)
|
||||
if key.get('filterMap') and value:
|
||||
value = re.compile(key.get('filterMap')).findall(value)[0]
|
||||
print key['id'], value
|
||||
if value:
|
||||
if isinstance(value, list):
|
||||
Find.query.filter_by(item_id=self.id, key=key['id']).delete()
|
||||
for v in value:
|
||||
f = Find(item_id=self.id, key=key['id'])
|
||||
f.value = v.lower()
|
||||
db.session.add(f)
|
||||
else:
|
||||
f = Find.get_or_create(self.id, key['id'])
|
||||
f.value = value.lower()
|
||||
db.session.add(f)
|
||||
else:
|
||||
f = Find.get(self.id, key['id'])
|
||||
if f:
|
||||
db.session.delete(f)
|
||||
|
||||
def update_lists(self):
|
||||
Find.query.filter_by(item_id=self.id, key='list').delete()
|
||||
for p in self.users:
|
||||
f = Find()
|
||||
f.item_id = self.id
|
||||
f.key = 'list'
|
||||
if p.id == settings.USER_ID:
|
||||
f.value = ':'
|
||||
else:
|
||||
f.value = '%s:' % p.id
|
||||
db.session.add(f)
|
||||
|
||||
def update(self):
|
||||
users = map(str, list(self.users))
|
||||
self.meta['mediastate'] = 'available' # available, unavailable, transferring
|
||||
if self.transferadded and self.transferprogress < 1:
|
||||
self.meta['mediastate'] = 'transferring'
|
||||
else:
|
||||
self.meta['mediastate'] = 'available' if settings.USER_ID in users else 'unavailable'
|
||||
self.update_sort()
|
||||
self.update_find()
|
||||
self.update_lists()
|
||||
self.modified = datetime.now()
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def update_mainid(self, key, id):
|
||||
record = {}
|
||||
if id:
|
||||
self.meta[key] = id
|
||||
self.meta['mainid'] = key
|
||||
record[key] = id
|
||||
else:
|
||||
if key in self.meta:
|
||||
del self.meta[key]
|
||||
if 'mainid' in self.meta:
|
||||
del self.meta['mainid']
|
||||
record[key] = ''
|
||||
for k in self.id_keys:
|
||||
if k != key:
|
||||
if k in self.meta:
|
||||
del self.meta[k]
|
||||
print 'mainid', 'mainid' in self.meta, self.meta.get('mainid')
|
||||
print 'key', key, self.meta.get(key)
|
||||
# get metadata from external resources
|
||||
self.scrape()
|
||||
self.update()
|
||||
self.update_cover()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
user = User.get_or_create(settings.USER_ID)
|
||||
if user in self.users:
|
||||
Changelog.record(user, 'edititem', self.id, record)
|
||||
|
||||
def extract_cover(self):
|
||||
path = self.get_path()
|
||||
if not path:
|
||||
return getattr(media, self.meta['extensions']).cover(path)
|
||||
|
||||
def update_cover(self):
|
||||
cover = None
|
||||
if 'cover' in self.meta:
|
||||
cover = ox.cache.read_url(self.meta['cover'])
|
||||
#covers[self.id] = requests.get(self.meta['cover']).content
|
||||
if cover:
|
||||
covers[self.id] = cover
|
||||
path = self.get_path()
|
||||
if not cover and path:
|
||||
cover = self.extract_cover()
|
||||
if cover:
|
||||
covers[self.id] = cover
|
||||
if cover:
|
||||
img = Image.open(StringIO(cover))
|
||||
self.meta['coverRatio'] = img.size[0]/img.size[1]
|
||||
for p in (':128', ':256'):
|
||||
del covers['%s%s' % (self.id, p)]
|
||||
return cover
|
||||
|
||||
def scrape(self):
|
||||
mainid = self.meta.get('mainid')
|
||||
print 'scrape', mainid, self.meta.get(mainid)
|
||||
if mainid == 'olid':
|
||||
scraper.update_ol(self)
|
||||
scraper.add_lookupbyisbn(self)
|
||||
elif mainid in ('isbn10', 'isbn13'):
|
||||
scraper.add_lookupbyisbn(self)
|
||||
elif mainid == 'lccn':
|
||||
import meta.lccn
|
||||
info = meta.lccn.info(self.meta[mainid])
|
||||
for key in info:
|
||||
self.meta[key] = info[key]
|
||||
else:
|
||||
print 'FIX UPDATE', mainid
|
||||
self.update()
|
||||
|
||||
def save_file(self, content):
|
||||
p = User.get(settings.USER_ID)
|
||||
f = File.get(self.id)
|
||||
if not f:
|
||||
path = 'Downloads/%s.%s' % (self.id, self.info['extension'])
|
||||
f = File.get_or_create(self.id, self.info, path=path)
|
||||
path = self.get_path()
|
||||
if not os.path.exists(path):
|
||||
ox.makedirs(os.path.dirname(path))
|
||||
with open(path, 'wb') as fd:
|
||||
fd.write(content)
|
||||
if p not in self.users:
|
||||
self.users.append(p)
|
||||
self.transferprogress = 1
|
||||
self.added = datetime.now()
|
||||
Changelog.record(p, 'additem', self.id, self.info)
|
||||
self.update()
|
||||
trigger_event('transfer', {
|
||||
'id': self.id, 'progress': 1
|
||||
})
|
||||
return True
|
||||
else:
|
||||
print 'TRIED TO SAVE EXISTING FILE!!!'
|
||||
self.transferprogress = 1
|
||||
self.update()
|
||||
return False
|
||||
|
||||
for key in config['itemKeys']:
|
||||
if key.get('sort'):
|
||||
sort_type = key.get('sortType', key['type'])
|
||||
if sort_type == 'integer':
|
||||
col = db.Column(db.BigInteger(), index=True)
|
||||
elif sort_type == 'float':
|
||||
col = db.Column(db.Float(), index=True)
|
||||
elif sort_type == 'date':
|
||||
col = db.Column(db.DateTime(), index=True)
|
||||
else:
|
||||
col = db.Column(db.String(1000), index=True)
|
||||
setattr(Item, 'sort_%s' % key['id'], col)
|
||||
|
||||
Item.id_keys = ['isbn10', 'isbn13', 'lccn', 'olid', 'oclc']
|
||||
Item.item_keys = config['itemKeys']
|
||||
Item.filter_keys = []
|
||||
|
||||
class Find(db.Model):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
item_id = db.Column(db.String(32), db.ForeignKey('item.id'))
|
||||
item = db.relationship('Item', backref=db.backref('find', lazy='dynamic'))
|
||||
key = db.Column(db.String(200), index=True)
|
||||
value = db.Column(db.Text())
|
||||
|
||||
def __repr__(self):
|
||||
return (u'%s=%s' % (self.key, self.value)).encode('utf-8')
|
||||
|
||||
@classmethod
|
||||
def get(cls, item, key):
|
||||
return cls.query.filter_by(item_id=item, key=key).first()
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, item, key):
|
||||
f = cls.get(item, key)
|
||||
if not f:
|
||||
f = cls(item_id=item, key=key)
|
||||
db.session.add(f)
|
||||
db.session.commit()
|
||||
return f
|
||||
|
||||
class File(db.Model):
|
||||
|
||||
created = db.Column(db.DateTime())
|
||||
modified = db.Column(db.DateTime())
|
||||
|
||||
sha1 = db.Column(db.String(32), primary_key=True)
|
||||
path = db.Column(db.String(2048))
|
||||
|
||||
info = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
|
||||
item_id = db.Column(db.String(32), db.ForeignKey('item.id'))
|
||||
item = db.relationship('Item', backref=db.backref('files', lazy='dynamic'))
|
||||
|
||||
@classmethod
|
||||
def get(cls, sha1):
|
||||
return cls.query.filter_by(sha1=sha1).first()
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, sha1, info=None, path=None):
|
||||
f = cls.get(sha1)
|
||||
if not f:
|
||||
f = cls(sha1=sha1)
|
||||
if info:
|
||||
f.info = info
|
||||
if path:
|
||||
f.path = path
|
||||
f.item_id = Item.get_or_create(id=sha1, info=info).id
|
||||
db.session.add(f)
|
||||
db.session.commit()
|
||||
return f
|
||||
|
||||
def __repr__(self):
|
||||
return self.sha1
|
||||
|
||||
def __init__(self, sha1):
|
||||
self.sha1 = sha1
|
||||
self.created = datetime.now()
|
||||
self.modified = datetime.now()
|
||||
42
oml/item/person.py
Normal file
42
oml/item/person.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import unicodedata
|
||||
|
||||
import ox
|
||||
|
||||
from settings import db
|
||||
|
||||
def get_sort_name(name, sortname=None):
|
||||
name = unicodedata.normalize('NFKD', name).strip()
|
||||
if name:
|
||||
person = Person.get(name)
|
||||
if not person:
|
||||
person = Person(name=name, sortname=sortname)
|
||||
person.save()
|
||||
sortname = unicodedata.normalize('NFKD', person.sortname)
|
||||
else:
|
||||
sortname = u''
|
||||
return sortname
|
||||
|
||||
class Person(db.Model):
|
||||
name = db.Column(db.String(1024), primary_key=True)
|
||||
sortname = db.Column(db.String())
|
||||
numberofnames = db.Column(db.Integer())
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
return cls.query.filter_by(name=name).first()
|
||||
|
||||
def save(self):
|
||||
if not self.sortname:
|
||||
self.sortname = ox.get_sort_name(self.name)
|
||||
self.sortname = unicodedata.normalize('NFKD', self.sortname)
|
||||
self.sortsortname = ox.sort_string(self.sortname)
|
||||
self.numberofnames = len(self.name.split(' '))
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
83
oml/item/query.py
Normal file
83
oml/item/query.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import settings
|
||||
import models
|
||||
import utils
|
||||
import oxflask.query
|
||||
|
||||
from sqlalchemy.sql.expression import nullslast
|
||||
|
||||
def parse(data):
|
||||
query = {}
|
||||
query['range'] = [0, 100]
|
||||
query['sort'] = [{'key':'title', 'operator':'+'}]
|
||||
for key in ('keys', 'group', 'list', 'range', 'sort', 'query'):
|
||||
if key in data:
|
||||
query[key] = data[key]
|
||||
print data
|
||||
query['qs'] = oxflask.query.Parser(models.Item).find(data)
|
||||
if 'query' in query and 'conditions' in query['query'] and query['query']['conditions']:
|
||||
conditions = query['query']['conditions']
|
||||
condition = conditions[0]
|
||||
if condition['key'] == '*':
|
||||
value = condition['value'].lower()
|
||||
query['qs'] = models.Item.query.join(
|
||||
models.Find, models.Find.item_id==models.Item.id).filter(
|
||||
models.Find.value.contains(value))
|
||||
if 'group' in query:
|
||||
query['qs'] = order_by_group(query['qs'], query['sort'])
|
||||
else:
|
||||
query['qs'] = order(query['qs'], query['sort'])
|
||||
return query
|
||||
|
||||
def order(qs, sort, prefix='sort_'):
|
||||
order_by = []
|
||||
if len(sort) == 1:
|
||||
additional_sort = settings.config['user']['ui']['listSort']
|
||||
key = utils.get_by_id(models.Item.item_keys, sort[0]['key'])
|
||||
for s in key.get('additionalSort', additional_sort):
|
||||
if s['key'] not in [e['key'] for e in sort]:
|
||||
sort.append(s)
|
||||
for e in sort:
|
||||
operator = e['operator']
|
||||
if operator != '-':
|
||||
operator = ''
|
||||
else:
|
||||
operator = ' DESC'
|
||||
key = {}.get(e['key'], e['key'])
|
||||
if key not in ('fixme', ):
|
||||
key = "%s%s" % (prefix, key)
|
||||
order = '%s%s' % (key, operator)
|
||||
order_by.append(order)
|
||||
if order_by:
|
||||
#nulllast not supported in sqlite, use IS NULL hack instead
|
||||
#order_by = map(nullslast, order_by)
|
||||
_order_by = []
|
||||
for order in order_by:
|
||||
nulls = "%s IS NULL" % order.split(' ')[0]
|
||||
_order_by.append(nulls)
|
||||
_order_by.append(order)
|
||||
order_by = _order_by
|
||||
qs = qs.order_by(*order_by)
|
||||
return qs
|
||||
|
||||
def order_by_group(qs, sort):
|
||||
return qs
|
||||
if 'sort' in query:
|
||||
if len(query['sort']) == 1 and query['sort'][0]['key'] == 'items':
|
||||
order_by = query['sort'][0]['operator'] == '-' and '-items' or 'items'
|
||||
if query['group'] == "year":
|
||||
secondary = query['sort'][0]['operator'] == '-' and '-sortvalue' or 'sortvalue'
|
||||
order_by = (order_by, secondary)
|
||||
elif query['group'] != "keyword":
|
||||
order_by = (order_by, 'sortvalue')
|
||||
else:
|
||||
order_by = (order_by, 'value')
|
||||
else:
|
||||
order_by = query['sort'][0]['operator'] == '-' and '-sortvalue' or 'sortvalue'
|
||||
order_by = (order_by, 'items')
|
||||
else:
|
||||
order_by = ('-sortvalue', 'items')
|
||||
return order_by
|
||||
|
||||
182
oml/item/scan.py
Normal file
182
oml/item/scan.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
import ox
|
||||
|
||||
from app import app
|
||||
import settings
|
||||
from settings import db
|
||||
from item.models import File
|
||||
from user.models import User
|
||||
|
||||
from changelog import Changelog
|
||||
|
||||
import media
|
||||
from websocket import trigger_event
|
||||
|
||||
def remove_missing():
|
||||
dirty = False
|
||||
with app.app_context():
|
||||
user = User.get_or_create(settings.USER_ID)
|
||||
prefs = settings.preferences
|
||||
prefix = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||
for f in File.query:
|
||||
if not os.path.exists(f.item.get_path()):
|
||||
dirty = True
|
||||
print 'file gone', f, f.item.get_path()
|
||||
f.item.users.remove(user)
|
||||
if not f.item.users:
|
||||
print 'last user, remove'
|
||||
db.session.delete(f.item)
|
||||
else:
|
||||
f.item.update_lists()
|
||||
Changelog.record(user, 'removeitem', f.item.id)
|
||||
db.session.delete(f)
|
||||
if dirty:
|
||||
db.session.commit()
|
||||
|
||||
def run_scan():
|
||||
remove_missing()
|
||||
with app.app_context():
|
||||
prefs = settings.preferences
|
||||
prefix = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||
if not prefix[-1] == '/':
|
||||
prefix += '/'
|
||||
user = User.get_or_create(settings.USER_ID)
|
||||
assert isinstance(prefix, unicode)
|
||||
extensions = ['pdf', 'epub', 'txt']
|
||||
books = []
|
||||
for root, folders, files in os.walk(prefix):
|
||||
for f in files:
|
||||
#if f.startswith('._') or f == '.DS_Store':
|
||||
if f.startswith('.'):
|
||||
continue
|
||||
f = os.path.join(root, f)
|
||||
ext = f.split('.')[-1]
|
||||
if ext in extensions:
|
||||
books.append(f)
|
||||
|
||||
trigger_event('scan', {
|
||||
'path': prefix,
|
||||
'files': len(books)
|
||||
})
|
||||
position = 0
|
||||
added = 0
|
||||
for f in ox.sorted_strings(books):
|
||||
position += 1
|
||||
id = media.get_id(f)
|
||||
file = File.get(id)
|
||||
path = f[len(prefix):]
|
||||
if not file:
|
||||
data = media.metadata(f)
|
||||
ext = f.split('.')[-1]
|
||||
data['extension'] = ext
|
||||
data['size'] = os.stat(f).st_size
|
||||
file = File.get_or_create(id, data, path)
|
||||
item = file.item
|
||||
if 'mainid' in file.info:
|
||||
del file.info['mainid']
|
||||
db.session.add(file)
|
||||
if 'mainid' in item.info:
|
||||
item.meta['mainid'] = item.info.pop('mainid')
|
||||
item.meta[item.meta['mainid']] = item.info[item.meta['mainid']]
|
||||
db.session.add(item)
|
||||
item.users.append(user)
|
||||
Changelog.record(user, 'additem', item.id, item.info)
|
||||
if item.meta.get('mainid'):
|
||||
Changelog.record(user, 'edititem', item.id, {
|
||||
item.meta['mainid']: item.meta[item.meta['mainid']]
|
||||
})
|
||||
item.added = datetime.now()
|
||||
item.scrape()
|
||||
added += 1
|
||||
trigger_event('scan', {
|
||||
'position': position,
|
||||
'length': len(books),
|
||||
'path': path,
|
||||
'progress': position/len(books),
|
||||
'added': added,
|
||||
})
|
||||
trigger_event('scan', {
|
||||
'progress': 1,
|
||||
'added': added,
|
||||
'done': True
|
||||
})
|
||||
|
||||
def run_import():
|
||||
with app.app_context():
|
||||
prefs = settings.preferences
|
||||
prefix = os.path.expanduser(prefs['importPath'])
|
||||
prefix_books = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||
prefix_imported = os.path.join(prefix_books, 'Imported/')
|
||||
if not prefix[-1] == '/':
|
||||
prefix += '/'
|
||||
user = User.get_or_create(settings.USER_ID)
|
||||
assert isinstance(prefix, unicode)
|
||||
extensions = ['pdf', 'epub', 'txt']
|
||||
books = []
|
||||
for root, folders, files in os.walk(prefix):
|
||||
for f in files:
|
||||
#if f.startswith('._') or f == '.DS_Store':
|
||||
if f.startswith('.'):
|
||||
continue
|
||||
f = os.path.join(root, f)
|
||||
ext = f.split('.')[-1]
|
||||
if ext in extensions:
|
||||
books.append(f)
|
||||
|
||||
trigger_event('import', {
|
||||
'path': prefix,
|
||||
'files': len(books)
|
||||
})
|
||||
position = 0
|
||||
added = 0
|
||||
for f in ox.sorted_strings(books):
|
||||
position += 1
|
||||
id = media.get_id(f)
|
||||
file = File.get(id)
|
||||
path = f[len(prefix):]
|
||||
if not file:
|
||||
f_import = f
|
||||
f = f.replace(prefix, prefix_imported)
|
||||
ox.makedirs(os.path.dirname(f))
|
||||
shutil.move(f_import, f)
|
||||
path = f[len(prefix_books):]
|
||||
data = media.metadata(f)
|
||||
ext = f.split('.')[-1]
|
||||
data['extension'] = ext
|
||||
data['size'] = os.stat(f).st_size
|
||||
file = File.get_or_create(id, data, path)
|
||||
item = file.item
|
||||
if 'mainid' in file.info:
|
||||
del file.info['mainid']
|
||||
db.session.add(file)
|
||||
if 'mainid' in item.info:
|
||||
item.meta['mainid'] = item.info.pop('mainid')
|
||||
item.meta[item.meta['mainid']] = item.info[item.meta['mainid']]
|
||||
db.session.add(item)
|
||||
item.users.append(user)
|
||||
Changelog.record(user, 'additem', item.id, item.info)
|
||||
if item.meta.get('mainid'):
|
||||
Changelog.record(user, 'edititem', item.id, {
|
||||
item.meta['mainid']: item.meta[item.meta['mainid']]
|
||||
})
|
||||
item.scrape()
|
||||
added += 1
|
||||
trigger_event('import', {
|
||||
'position': position,
|
||||
'length': len(books),
|
||||
'path': path,
|
||||
'progress': position/len(books),
|
||||
'added': added,
|
||||
})
|
||||
trigger_event('import', {
|
||||
'progress': 1,
|
||||
'added': added,
|
||||
'done': True
|
||||
})
|
||||
101
oml/item/views.py
Normal file
101
oml/item/views.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
import zipfile
|
||||
import mimetypes
|
||||
from StringIO import StringIO
|
||||
import Image
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import json, request, make_response, abort, send_file
|
||||
from covers import covers
|
||||
|
||||
import settings
|
||||
|
||||
from models import Item, db
|
||||
|
||||
from utils import resize_image
|
||||
|
||||
app = Blueprint('item', __name__, static_folder=settings.static_path)
|
||||
|
||||
@app.route('/<string:id>/epub/')
|
||||
@app.route('/<string:id>/epub/<path:filename>')
|
||||
def epub(id, filename=''):
|
||||
item = Item.get(id)
|
||||
if not item or item.info['extension'] != 'epub':
|
||||
abort(404)
|
||||
|
||||
path = item.get_path()
|
||||
z = zipfile.ZipFile(path)
|
||||
if filename == '':
|
||||
return '<br>\n'.join([f.filename for f in z.filelist])
|
||||
if filename not in [f.filename for f in z.filelist]:
|
||||
abort(404)
|
||||
resp = make_response(z.read(filename))
|
||||
resp.content_type = {
|
||||
'xpgt': 'application/vnd.adobe-page-template+xml'
|
||||
}.get(filename.split('.')[0], mimetypes.guess_type(filename)[0]) or 'text/plain'
|
||||
return resp
|
||||
|
||||
@app.route('/<string:id>/get')
|
||||
@app.route('/<string:id>/txt/')
|
||||
@app.route('/<string:id>/pdf')
|
||||
def get(id):
|
||||
item = Item.get(id)
|
||||
if not item:
|
||||
abort(404)
|
||||
path = item.get_path()
|
||||
mimetype={
|
||||
'epub': 'application/epub+zip',
|
||||
'pdf': 'application/pdf',
|
||||
}.get(path.split('.')[-1], None)
|
||||
return send_file(path, mimetype=mimetype)
|
||||
|
||||
@app.route('/<string:id>/cover.jpg')
|
||||
@app.route('/<string:id>/cover<int:size>.jpg')
|
||||
def cover(id, size=None):
|
||||
item = Item.get(id)
|
||||
if not item:
|
||||
abort(404)
|
||||
data = None
|
||||
if size:
|
||||
data = covers['%s:%s' % (id, size)]
|
||||
if data:
|
||||
size = None
|
||||
if not data:
|
||||
data = covers[id]
|
||||
if not data:
|
||||
print 'check for cover', id
|
||||
data = item.update_cover()
|
||||
if not data:
|
||||
data = covers.black()
|
||||
if size:
|
||||
data = covers['%s:%s' % (id, size)] = resize_image(data, size=size)
|
||||
data = str(data)
|
||||
if not 'coverRatio' in item.meta:
|
||||
#img = Image.open(StringIO(str(covers[id])))
|
||||
img = Image.open(StringIO(data))
|
||||
item.meta['coverRatio'] = float(img.size[0])/img.size[1]
|
||||
db.session.add(item)
|
||||
db.session.commit()
|
||||
resp = make_response(data)
|
||||
resp.content_type = "image/jpeg"
|
||||
return resp
|
||||
|
||||
@app.route('/<string:id>/reader/')
|
||||
def reader(id, filename=''):
|
||||
item = Item.get(id)
|
||||
if item.info['extension'] == 'epub':
|
||||
html = 'html/epub.html'
|
||||
elif item.info['extension'] == 'pdf':
|
||||
html = 'html/pdf.html'
|
||||
elif item.info['extension'] == 'txt':
|
||||
html = 'html/txt.html'
|
||||
else:
|
||||
abort(404)
|
||||
item.sort_accessed = item.accessed = datetime.now()
|
||||
item.sort_timesaccessed = item.timesaccessed = (item.timesaccessed or 0) + 1
|
||||
item.save()
|
||||
return app.send_static_file(html)
|
||||
45
oml/media/__init__.py
Normal file
45
oml/media/__init__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import pdf
|
||||
import epub
|
||||
import txt
|
||||
import os
|
||||
import base64
|
||||
import ox
|
||||
|
||||
def get_id(f):
|
||||
return base64.b32encode(ox.sha1sum(f).decode('hex'))
|
||||
|
||||
def metadata(f):
|
||||
ext = f.split('.')[-1]
|
||||
data = {}
|
||||
if ext == 'pdf':
|
||||
info = pdf.info(f)
|
||||
elif ext == 'epub':
|
||||
info = epub.info(f)
|
||||
elif ext == 'txt':
|
||||
info = txt.info(f)
|
||||
|
||||
for key in ('title', 'author', 'date', 'publisher', 'isbn'):
|
||||
if key in info:
|
||||
value = info[key]
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = value.decode('utf-8')
|
||||
except:
|
||||
value = None
|
||||
if value:
|
||||
data[key] = info[key]
|
||||
|
||||
if 'isbn' in data:
|
||||
value = data.pop('isbn')
|
||||
if len(value) == 10:
|
||||
data['isbn10'] = value
|
||||
data['mainid'] = 'isbn10'
|
||||
else:
|
||||
data['isbn13'] = value
|
||||
data['mainid'] = 'isbn13'
|
||||
if not 'title' in data:
|
||||
data['title'] = os.path.splitext(os.path.basename(f))[0]
|
||||
if 'author' in data and isinstance(data['author'], basestring):
|
||||
data['author'] = [data['author']]
|
||||
return data
|
||||
|
||||
63
oml/media/epub.py
Normal file
63
oml/media/epub.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from StringIO import StringIO
|
||||
|
||||
import Image
|
||||
import stdnum.isbn
|
||||
|
||||
from utils import normalize_isbn, find_isbns
|
||||
|
||||
def cover(path):
|
||||
img = Image.new('RGB', (80, 128))
|
||||
o = StringIO()
|
||||
img.save(o, format='jpeg')
|
||||
data = o.getvalue()
|
||||
o.close()
|
||||
return data
|
||||
|
||||
def info(epub):
|
||||
data = {}
|
||||
z = zipfile.ZipFile(epub)
|
||||
opf = [f.filename for f in z.filelist if f.filename.endswith('opf')]
|
||||
if opf:
|
||||
info = ET.fromstring(z.read(opf[0]))
|
||||
metadata = info.findall('{http://www.idpf.org/2007/opf}metadata')[0]
|
||||
for e in metadata.getchildren():
|
||||
if e.text:
|
||||
key = e.tag.split('}')[-1]
|
||||
key = {
|
||||
'creator': 'author',
|
||||
}.get(key, key)
|
||||
value = e.text
|
||||
if key == 'identifier':
|
||||
value = normalize_isbn(value)
|
||||
if stdnum.isbn.is_valid(value):
|
||||
data['isbn'] = value
|
||||
else:
|
||||
data[key] = e.text
|
||||
text = extract_text(epub)
|
||||
data['textsize'] = len(text)
|
||||
if not 'isbn' in data:
|
||||
isbn = extract_isbn(text)
|
||||
if isbn:
|
||||
data['isbn'] = isbn
|
||||
return data
|
||||
|
||||
def extract_text(path):
|
||||
data = ''
|
||||
z = zipfile.ZipFile(path)
|
||||
for f in z.filelist:
|
||||
if f.filename.endswith('html'):
|
||||
data += z.read(f.filename)
|
||||
return data
|
||||
|
||||
def extract_isbn(data):
|
||||
isbns = find_isbns(data)
|
||||
if isbns:
|
||||
return isbns[0]
|
||||
|
||||
140
oml/media/pdf.py
Normal file
140
oml/media/pdf.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
from glob import glob
|
||||
|
||||
from pyPdf import PdfFileReader
|
||||
import stdnum.isbn
|
||||
|
||||
import settings
|
||||
from utils import normalize_isbn, find_isbns
|
||||
|
||||
def cover(pdf):
|
||||
if sys.platform == 'darwin':
|
||||
return ql_cover(pdf)
|
||||
else:
|
||||
return page(pdf, 1)
|
||||
|
||||
def ql_cover(pdf):
|
||||
tmp = tempfile.mkdtemp()
|
||||
cmd = [
|
||||
'qlmanage',
|
||||
'-t',
|
||||
'-s',
|
||||
'1024',
|
||||
'-o',
|
||||
tmp,
|
||||
pdf
|
||||
]
|
||||
p = subprocess.Popen(cmd)
|
||||
p.wait()
|
||||
image = glob('%s/*' % tmp)[0]
|
||||
with open(image, 'rb') as fd:
|
||||
data = fd.read()
|
||||
shutil.rmtree(tmp)
|
||||
return data
|
||||
|
||||
|
||||
def page(pdf, page):
|
||||
image = tempfile.mkstemp('.jpg')[1]
|
||||
cmd = [
|
||||
'gs', '-q',
|
||||
'-dBATCH', '-dSAFER', '-dNOPAUSE', '-dNOPROMPT',
|
||||
'-dMaxBitmap=500000000',
|
||||
'-dAlignToPixels=0', '-dGridFitTT=2',
|
||||
'-sDEVICE=jpeg', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||
'-r72',
|
||||
'-dUseCropBox',
|
||||
'-dFirstPage=%d' % page,
|
||||
'-dLastPage=%d' % page,
|
||||
'-sOutputFile=%s' % image,
|
||||
pdf
|
||||
]
|
||||
p = subprocess.Popen(cmd)
|
||||
p.wait()
|
||||
with open(image, 'rb') as fd:
|
||||
data = fd.read()
|
||||
os.unlink(image)
|
||||
return data
|
||||
|
||||
def info(pdf):
|
||||
data = {}
|
||||
with open(pdf, 'rb') as fd:
|
||||
try:
|
||||
pdfreader = PdfFileReader(fd)
|
||||
info = pdfreader.getDocumentInfo()
|
||||
if info:
|
||||
for key in info:
|
||||
if info[key]:
|
||||
data[key[1:].lower()] = info[key]
|
||||
xmp =pdfreader.getXmpMetadata()
|
||||
if xmp:
|
||||
for key in dir(xmp):
|
||||
if key.startswith('dc_'):
|
||||
value = getattr(xmp, key)
|
||||
if isinstance(value, dict) and 'x-default' in value:
|
||||
value = value['x-default']
|
||||
elif isinstance(value, list):
|
||||
value = [v.strip() for v in value if v.strip()]
|
||||
_key = key[3:]
|
||||
if value and _key not in data:
|
||||
data[_key] = value
|
||||
except:
|
||||
print 'FAILED TO PARSE', pdf
|
||||
import traceback
|
||||
print traceback.print_exc()
|
||||
|
||||
if 'identifier' in data:
|
||||
value = normalize_isbn(data['identifier'])
|
||||
if stdnum.isbn.is_valid(value):
|
||||
data['isbn'] = value
|
||||
del data['identifier']
|
||||
'''
|
||||
cmd = ['pdfinfo', pdf]
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
for line in stdout.strip().split('\n'):
|
||||
parts = line.split(':')
|
||||
key = parts[0].lower().strip()
|
||||
if key:
|
||||
data[key] = ':'.join(parts[1:]).strip()
|
||||
for key in data.keys():
|
||||
if not data[key]:
|
||||
del data[key]
|
||||
'''
|
||||
text = extract_text(pdf)
|
||||
data['textsize'] = len(text)
|
||||
if settings.server['extract_text']:
|
||||
if not 'isbn' in data:
|
||||
isbn = extract_isbn(text)
|
||||
if isbn:
|
||||
data['isbn'] = isbn
|
||||
return data
|
||||
|
||||
'''
|
||||
#possbile alternative with gs
|
||||
tmp = tempfile.mkstemp('.txt')[1]
|
||||
cmd = ['gs', '-dBATCH', '-dNOPAUSE', '-sDEVICE=txtwrite', '-dFirstPage=3', '-dLastPage=5', '-sOutputFile=%s'%tmp, pdf]
|
||||
|
||||
'''
|
||||
def extract_text(pdf):
|
||||
if sys.platform == 'darwin':
|
||||
cmd = ['/usr/bin/mdimport' '-d2', pdf]
|
||||
else:
|
||||
cmd = ['pdftotext', pdf, '-']
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if sys.platform == 'darwin':
|
||||
stdout = stderr.split('kMDItemTextContent = "')[-1].split('\n')[0][:-2]
|
||||
return stdout.strip()
|
||||
|
||||
def extract_isbn(text):
|
||||
isbns = find_isbns(text)
|
||||
if isbns:
|
||||
return isbns[0]
|
||||
41
oml/media/txt.py
Normal file
41
oml/media/txt.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
import sys
|
||||
import os
|
||||
from utils import find_isbns
|
||||
from StringIO import StringIO
|
||||
import Image
|
||||
|
||||
from pdf import ql_cover
|
||||
|
||||
def cover(path):
|
||||
if sys.platform == 'darwin':
|
||||
return ql_cover(path)
|
||||
img = Image.new('RGB', (80, 128))
|
||||
o = StringIO()
|
||||
img.save(o, format='jpeg')
|
||||
data = o.getvalue()
|
||||
o.close()
|
||||
return data
|
||||
|
||||
def info(path):
|
||||
data = {}
|
||||
data['title'] = os.path.splitext(os.path.basename(path))[0]
|
||||
text = extract_text(path)
|
||||
isbn = extract_isbn(text)
|
||||
if isbn:
|
||||
data['isbn'] = isbn
|
||||
data['textsize'] = len(text)
|
||||
return data
|
||||
|
||||
def extract_text(path):
|
||||
with open(path) as fd:
|
||||
data = fd.read()
|
||||
return data
|
||||
|
||||
def extract_isbn(text):
|
||||
isbns = find_isbns(text)
|
||||
if isbns:
|
||||
return isbns[0]
|
||||
0
oml/meta/__init__.py
Normal file
0
oml/meta/__init__.py
Normal file
52
oml/meta/lccn.py
Normal file
52
oml/meta/lccn.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
import ox
|
||||
from ox.cache import read_url
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from utils import normalize_isbn
|
||||
from marc_countries import COUNTRIES
|
||||
|
||||
def info(id):
|
||||
ns = '{http://www.loc.gov/mods/v3}'
|
||||
url = 'http://lccn.loc.gov/%s/mods' % id
|
||||
data = read_url(url)
|
||||
mods = ET.fromstring(data)
|
||||
|
||||
info = {}
|
||||
info['title'] = ''.join([e.text for e in mods.findall(ns + 'titleInfo')[0]])
|
||||
origin = mods.findall(ns + 'originInfo')
|
||||
if origin:
|
||||
info['place'] = []
|
||||
for place in origin[0].findall(ns + 'place'):
|
||||
terms = place.findall(ns + 'placeTerm')
|
||||
if terms and terms[0].attrib['type'] == 'text':
|
||||
e = terms[0]
|
||||
info['place'].append(e.text)
|
||||
elif terms and terms[0].attrib['type'] == 'code':
|
||||
e = terms[0]
|
||||
info['country'] = COUNTRIES.get(e.text, e.text)
|
||||
info['publisher'] = ''.join([e.text for e in origin[0].findall(ns + 'publisher')])
|
||||
info['date'] = ''.join([e.text for e in origin[0].findall(ns + 'dateIssued')])
|
||||
for i in mods.findall(ns + 'identifier'):
|
||||
if i.attrib['type'] == 'oclc':
|
||||
info['oclc'] = i.text.replace('ocn', '')
|
||||
if i.attrib['type'] == 'lccn':
|
||||
info['lccn'] = i.text
|
||||
if i.attrib['type'] == 'isbn':
|
||||
isbn = normalize_isbn(i.text)
|
||||
info['isbn%s'%len(isbn)] = isbn
|
||||
for i in mods.findall(ns + 'classification'):
|
||||
if i.attrib['authority'] == 'ddc':
|
||||
info['classification'] = i.text
|
||||
info['author'] = []
|
||||
for a in mods.findall(ns + 'name'):
|
||||
if a.attrib['usage'] == 'primary':
|
||||
info['author'].append(''.join([e.text for e in a.findall(ns + 'namePart')]))
|
||||
info['author'] = [ox.normalize_name(a[:-1]) for a in info['author']]
|
||||
for key in info.keys():
|
||||
if not info[key]:
|
||||
del info[key]
|
||||
return info
|
||||
409
oml/meta/marc_countries.py
Normal file
409
oml/meta/marc_countries.py
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
COUNTRIES = {
|
||||
"gw": "Germany",
|
||||
"gv": "Guinea",
|
||||
"gu": "Guam",
|
||||
"gt": "Guatemala",
|
||||
"gs": "Georgia (Republic)",
|
||||
"gr": "Greece",
|
||||
"-ge": "Germany (East)",
|
||||
"gp": "Guadeloupe",
|
||||
"mnu": "Minnesota",
|
||||
"gy": "Guyana",
|
||||
"gd": "Grenada",
|
||||
"gb": "Kiribati",
|
||||
"go": "Gabon",
|
||||
"gm": "Gambia",
|
||||
"alu": "Alabama",
|
||||
"gi": "Gibraltar",
|
||||
"gh": "Ghana",
|
||||
"tz": "Tanzania",
|
||||
"tv": "Tuvalu",
|
||||
"tu": "Turkey",
|
||||
"tr": "Trinidad and Tobago",
|
||||
"ts": "United Arab Emirates",
|
||||
"to": "Tonga",
|
||||
"tl": "Tokelau",
|
||||
"tk": "Turkmenistan",
|
||||
"th": "Thailand",
|
||||
"ti": "Tunisia",
|
||||
"tg": "Togo",
|
||||
"tc": "Turks and Caicos Islands",
|
||||
"ta": "Tajikistan",
|
||||
"-gn": "Gilbert and Ellice Islands",
|
||||
"-us": "United States",
|
||||
"-ajr": "Azerbaijan S.S.R.",
|
||||
"-iu": "Israel-Syria Demilitarized Zones",
|
||||
"-iw": "Israel-Jordan Demilitarized Zones",
|
||||
"za": "Zambia",
|
||||
"nbu": "Nebraska",
|
||||
"scu": "South Carolina",
|
||||
"bg": "Bangladesh",
|
||||
"cau": "California",
|
||||
"abc": "Alberta",
|
||||
"xoa": "Northern Territory",
|
||||
"meu": "Maine",
|
||||
"ctu": "Connecticut",
|
||||
"my": "Malaysia",
|
||||
"aku": "Alaska",
|
||||
"gl": "Greenland",
|
||||
"-cn": "Canada",
|
||||
"wiu": "Wisconsin",
|
||||
"-cz": "Canal Zone",
|
||||
"txu": "Texas",
|
||||
"-cs": "Czechoslovakia",
|
||||
"-cp": "Canton and Enderbury Islands",
|
||||
"msu": "Mississippi",
|
||||
"-ln": "Central and Southern Line Islands",
|
||||
"nkc": "New Brunswick",
|
||||
"it": "Italy",
|
||||
"tnu": "Tennessee",
|
||||
"vp": "Various places",
|
||||
"mg": "Madagascar",
|
||||
"mf": "Mauritius",
|
||||
"mc": "Monaco",
|
||||
"-ur": "Soviet Union",
|
||||
"mm": "Malta",
|
||||
"ml": "Mali",
|
||||
"mo": "Montenegro",
|
||||
"flu": "Florida",
|
||||
"deu": "Delaware",
|
||||
"mk": "Oman",
|
||||
"mj": "Montserrat",
|
||||
"mu": "Mauritania",
|
||||
"mw": "Malawi",
|
||||
"mv": "Moldova",
|
||||
"mq": "Martinique",
|
||||
"mp": "Mongolia",
|
||||
"mr": "Morocco",
|
||||
"-ui": "United Kingdom Misc. Islands",
|
||||
"mx": "Mexico",
|
||||
"-uk": "United Kingdom",
|
||||
"mz": "Mozambique",
|
||||
"kyu": "Kentucky",
|
||||
"hiu": "Hawaii",
|
||||
"enk": "England",
|
||||
"nyu": "New York (State)",
|
||||
"fp": "French Polynesia",
|
||||
"fr": "France",
|
||||
"fs": "Terres australes et antarctiques françaises",
|
||||
"mau": "Massachusetts",
|
||||
"snc": "Saskatchewan",
|
||||
"fa": "Faroe Islands",
|
||||
"fg": "French Guiana",
|
||||
"lau": "Louisiana",
|
||||
"fj": "Fiji",
|
||||
"fk": "Falkland Islands",
|
||||
"fm": "Micronesia (Federated States)",
|
||||
"sz": "Switzerland",
|
||||
"sy": "Syria",
|
||||
"sx": "Namibia",
|
||||
"ss": "Western Sahara",
|
||||
"sr": "Surinam",
|
||||
"sq": "Swaziland",
|
||||
"sp": "Spain",
|
||||
"sw": "Sweden",
|
||||
"su": "Saudi Arabia",
|
||||
"st": "Saint-Martin",
|
||||
"sj": "Sudan",
|
||||
"si": "Singapore",
|
||||
"sh": "Spanish North Africa",
|
||||
"so": "Somalia",
|
||||
"sn": "Sint Maarten",
|
||||
"sm": "San Marino",
|
||||
"sl": "Sierra Leone",
|
||||
"sc": "Saint-Barthélemy",
|
||||
"sa": "South Africa",
|
||||
"sg": "Senegal",
|
||||
"sf": "Sao Tome and Principe",
|
||||
"se": "Seychelles",
|
||||
"sd": "South Sudan",
|
||||
"-unr": "Ukraine",
|
||||
"-kgr": "Kirghiz S.S.R.",
|
||||
"le": "Lebanon",
|
||||
"lb": "Liberia",
|
||||
"-hk": "Hong Kong",
|
||||
"lo": "Lesotho",
|
||||
"lh": "Liechtenstein",
|
||||
"li": "Lithuania",
|
||||
"lv": "Latvia",
|
||||
"lu": "Luxembourg",
|
||||
"vtu": "Vermont",
|
||||
"ls": "Laos",
|
||||
"xc": "Maldives",
|
||||
"ly": "Libya",
|
||||
"oku": "Oklahoma",
|
||||
"ye": "Yemen",
|
||||
"-tkr": "Turkmen S.S.R.",
|
||||
"nfc": "Newfoundland and Labrador",
|
||||
"ft": "Djibouti",
|
||||
"em": "Timor-Leste",
|
||||
"eg": "Equatorial Guinea",
|
||||
"ea": "Eritrea",
|
||||
"ec": "Ecuador",
|
||||
"-gsr": "Georgian S.S.R.",
|
||||
"et": "Ethiopia",
|
||||
"es": "El Salvador",
|
||||
"er": "Estonia",
|
||||
"ru": "Russia (Federation)",
|
||||
"rw": "Rwanda",
|
||||
"re": "Réunion",
|
||||
"rb": "Serbia",
|
||||
"rm": "Romania",
|
||||
"rh": "Zimbabwe",
|
||||
"-err": "Estonia",
|
||||
"oru": "Oregon",
|
||||
"quc": "Québec (Province)",
|
||||
"ntc": "Northwest Territories",
|
||||
"wlk": "Wales",
|
||||
"xj": "Saint Helena",
|
||||
"xk": "Saint Lucia",
|
||||
"xh": "Niue",
|
||||
"xn": "Macedonia",
|
||||
"xo": "Slovakia",
|
||||
"xl": "Saint Pierre and Miquelon",
|
||||
"xm": "Saint Vincent and the Grenadines",
|
||||
"xb": "Cocos (Keeling) Islands",
|
||||
"onc": "Ontario",
|
||||
"xa": "Christmas Island (Indian Ocean)",
|
||||
"xf": "Midway Islands",
|
||||
"xd": "Saint Kitts-Nevis",
|
||||
"xe": "Marshall Islands",
|
||||
"nhu": "New Hampshire",
|
||||
"xx": "No place, unknown, or undetermined",
|
||||
"fi": "Finland",
|
||||
"xr": "Czech Republic",
|
||||
"xs": "South Georgia and the South Sandwich Islands",
|
||||
"xp": "Spratly Island",
|
||||
"xv": "Slovenia",
|
||||
"-tt": "Trust Territory of the Pacific Islands",
|
||||
"iau": "Iowa",
|
||||
"ncu": "North Carolina",
|
||||
"stk": "Scotland",
|
||||
"xra": "South Australia",
|
||||
"miu": "Michigan",
|
||||
"kg": "Kyrgyzstan",
|
||||
"ke": "Kenya",
|
||||
"ko": "Korea (South)",
|
||||
"kn": "Korea (North)",
|
||||
"kv": "Kosovo",
|
||||
"ku": "Kuwait",
|
||||
"kz": "Kazakhstan",
|
||||
"-pt": "Portuguese Timor",
|
||||
"ksu": "Kansas",
|
||||
"dm": "Benin",
|
||||
"dk": "Denmark",
|
||||
"-ys": "Yemen (People's Democratic Republic)",
|
||||
"-yu": "Serbia and Montenegro",
|
||||
"-bwr": "Byelorussian S.S.R.",
|
||||
"dr": "Dominican Republic",
|
||||
"dq": "Dominica",
|
||||
"qa": "Qatar",
|
||||
"aru": "Arkansas",
|
||||
"nuc": "Nunavut",
|
||||
"wf": "Wallis and Futuna",
|
||||
"wk": "Wake Island",
|
||||
"wj": "West Bank of the Jordan River",
|
||||
"jm": "Jamaica",
|
||||
"vra": "Victoria",
|
||||
"jo": "Jordan",
|
||||
"ws": "Samoa",
|
||||
"ji": "Johnston Atoll",
|
||||
"-na": "Netherlands Antilles",
|
||||
"ja": "Japan",
|
||||
"cou": "Colorado",
|
||||
"-wb": "West Berlin",
|
||||
"ilu": "Illinois",
|
||||
"-nm": "Northern Mariana Islands",
|
||||
"ck": "Colombia",
|
||||
"cj": "Cayman Islands",
|
||||
"ci": "Croatia",
|
||||
"ch": "China (Republic : 1949- )",
|
||||
"co": "Curaçao",
|
||||
"cm": "Cameroon",
|
||||
"cl": "Chile",
|
||||
"-rur": "Russian S.F.S.R.",
|
||||
"cb": "Cambodia",
|
||||
"ca": "Caribbean Netherlands",
|
||||
"cg": "Congo (Democratic Republic)",
|
||||
"cf": "Congo (Brazzaville)",
|
||||
"-lir": "Lithuania",
|
||||
"cd": "Chad",
|
||||
"cy": "Cyprus",
|
||||
"cx": "Central African Republic",
|
||||
"cr": "Costa Rica",
|
||||
"cq": "Comoros",
|
||||
"cw": "Cook Islands",
|
||||
"cv": "Cape Verde",
|
||||
"cu": "Cuba",
|
||||
"pr": "Puerto Rico",
|
||||
"pp": "Papua New Guinea",
|
||||
"pw": "Palau",
|
||||
"py": "Paraguay",
|
||||
"pc": "Pitcairn Island",
|
||||
"pf": "Paracel Islands",
|
||||
"pg": "Guinea-Bissau",
|
||||
"pe": "Peru",
|
||||
"pk": "Pakistan",
|
||||
"ph": "Philippines",
|
||||
"pn": "Panama",
|
||||
"po": "Portugal",
|
||||
"pl": "Poland",
|
||||
"pic": "Prince Edward Island",
|
||||
"xxu": "United States",
|
||||
"gau": "Georgia",
|
||||
"xxc": "Canada",
|
||||
"xxk": "United Kingdom",
|
||||
"iy": "Iraq-Saudi Arabia Neutral Zone",
|
||||
"vb": "British Virgin Islands",
|
||||
"vc": "Vatican City",
|
||||
"ve": "Venezuela",
|
||||
"iq": "Iraq",
|
||||
"vi": "Virgin Islands of the United States",
|
||||
"is": "Israel",
|
||||
"ir": "Iran",
|
||||
"vm": "Vietnam",
|
||||
"iv": "Côte d'Ivoire",
|
||||
"ii": "India",
|
||||
"-ac": "Ashmore and Cartier Islands",
|
||||
"io": "Indonesia",
|
||||
"-ai": "Anguilla",
|
||||
"ic": "Iceland",
|
||||
"ie": "Ireland",
|
||||
"pau": "Pennsylvania",
|
||||
"-jn": "Jan Mayen",
|
||||
"nik": "Northern Ireland",
|
||||
"wyu": "Wyoming",
|
||||
"-air": "Armenian S.S.R.",
|
||||
"-sv": "Swan Islands",
|
||||
"-mvr": "Moldavian S.S.R.",
|
||||
"-sk": "Sikkim",
|
||||
"riu": "Rhode Island",
|
||||
"-sb": "Svalbard",
|
||||
"-xi": "Saint Kitts-Nevis-Anguilla",
|
||||
"wea": "Western Australia",
|
||||
"cc": "China",
|
||||
"nvu": "Nevada",
|
||||
"mou": "Missouri",
|
||||
"ce": "Sri Lanka",
|
||||
"qea": "Queensland",
|
||||
"-mh": "Macao",
|
||||
"nju": "New Jersey",
|
||||
"ykc": "Yukon Territory",
|
||||
"-vs": "Vietnam, South",
|
||||
"tma": "Tasmania",
|
||||
"-vn": "Vietnam, North",
|
||||
"bd": "Burundi",
|
||||
"be": "Belgium",
|
||||
"bf": "Bahamas",
|
||||
"nmu": "New Mexico",
|
||||
"ba": "Bahrain",
|
||||
"bb": "Barbados",
|
||||
"bl": "Brazil",
|
||||
"bm": "Bermuda Islands",
|
||||
"bn": "Bosnia and Hercegovina",
|
||||
"bo": "Bolivia",
|
||||
"bh": "Belize",
|
||||
"bi": "British Indian Ocean Territory",
|
||||
"bt": "Bhutan",
|
||||
"bu": "Bulgaria",
|
||||
"bv": "Bouvet Island",
|
||||
"bw": "Belarus",
|
||||
"bp": "Solomon Islands",
|
||||
"br": "Burma",
|
||||
"bs": "Botswana",
|
||||
"dcu": "District of Columbia",
|
||||
"bx": "Brunei",
|
||||
"aca": "Australian Capital Territory",
|
||||
"idu": "Idaho",
|
||||
"xna": "New South Wales",
|
||||
"ot": "Mayotte",
|
||||
"ndu": "North Dakota",
|
||||
"nsc": "Nova Scotia",
|
||||
"-kzr": "Kazakh S.S.R.",
|
||||
"mbc": "Manitoba",
|
||||
"-lvr": "Latvia",
|
||||
"-uzr": "Uzbek S.S.R.",
|
||||
"wau": "Washington (State)",
|
||||
"vau": "Virginia",
|
||||
"sdu": "South Dakota",
|
||||
"gz": "Gaza Strip",
|
||||
"ht": "Haiti",
|
||||
"hu": "Hungary",
|
||||
"ho": "Honduras",
|
||||
"hm": "Heard and McDonald Islands",
|
||||
"xga": "Coral Sea Islands Territory",
|
||||
"uy": "Uruguay",
|
||||
"uz": "Uzbekistan",
|
||||
"uv": "Burkina Faso",
|
||||
"up": "United States Misc. Pacific Islands",
|
||||
"mtu": "Montana",
|
||||
"un": "Ukraine",
|
||||
"utu": "Utah",
|
||||
"ug": "Uganda",
|
||||
"ua": "Egypt",
|
||||
"azu": "Arizona",
|
||||
"uc": "United States Misc. Caribbean Islands",
|
||||
"aa": "Albania",
|
||||
"ae": "Algeria",
|
||||
"ag": "Argentina",
|
||||
"af": "Afghanistan",
|
||||
"ai": "Armenia (Republic)",
|
||||
"inu": "Indiana",
|
||||
"uik": "United Kingdom Misc. Islands",
|
||||
"aj": "Azerbaijan",
|
||||
"am": "Anguilla",
|
||||
"ao": "Angola",
|
||||
"an": "Andorra",
|
||||
"aq": "Antigua and Barbuda",
|
||||
"as": "American Samoa",
|
||||
"au": "Austria",
|
||||
"at": "Australia",
|
||||
"aw": "Aruba",
|
||||
"ay": "Antarctica",
|
||||
"ohu": "Ohio",
|
||||
"nl": "New Caledonia",
|
||||
"-ry": "Ryukyu Islands, Southern",
|
||||
"nn": "Vanuatu",
|
||||
"no": "Norway",
|
||||
"ne": "Netherlands",
|
||||
"ng": "Niger",
|
||||
"nx": "Norfolk Island",
|
||||
"nz": "New Zealand",
|
||||
"np": "Nepal",
|
||||
"nq": "Nicaragua",
|
||||
"nr": "Nigeria",
|
||||
"mdu": "Maryland",
|
||||
"nu": "Nauru",
|
||||
"nw": "Northern Mariana Islands",
|
||||
"wvu": "West Virginia",
|
||||
"-xxr": "Soviet Union",
|
||||
"-tar": "Tajik S.S.R.",
|
||||
"bcc": "British Columbia"
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import json
|
||||
import re
|
||||
import ox
|
||||
from ox.cache import read_url
|
||||
|
||||
url = "http://www.loc.gov/marc/countries/countries_code.html"
|
||||
data = read_url(url)
|
||||
countries = dict([
|
||||
[ox.strip_tags(c) for c in r]
|
||||
for r in re.compile('<tr>.*?class="code">(.*?)</td>.*?<td>(.*?)</td>', re.DOTALL).findall(data)
|
||||
])
|
||||
|
||||
data = json.dumps(countries, indent=4, ensure_ascii=False).encode('utf-8')
|
||||
with open(__file__) as f:
|
||||
pydata = f.read()
|
||||
pydata = re.sub(
|
||||
re.compile('\nCOUNTRIES = {.*?}\n\n', re.DOTALL),
|
||||
'\nCOUNTRIES = %s\n\n' % data, pydata)
|
||||
|
||||
with open(__file__, 'w') as f:
|
||||
f.write(pydata)
|
||||
67
oml/meta/ol.py
Normal file
67
oml/meta/ol.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
from ox.cache import read_url
|
||||
import json
|
||||
|
||||
from utils import normalize_isbn
|
||||
from marc_countries import COUNTRIES
|
||||
|
||||
def find(query):
|
||||
url = 'https://openlibrary.org/search.json?q=%s' % query
|
||||
data = json.loads(read_url(url))
|
||||
return data
|
||||
|
||||
def authors(authors):
|
||||
return resolve_names(authors)
|
||||
|
||||
def resolve_names(objects, key='name'):
|
||||
r = []
|
||||
for o in objects:
|
||||
url = 'https://openlibrary.org%s.json' % o['key']
|
||||
data = json.loads(read_url(url))
|
||||
r.append(data[key])
|
||||
return r
|
||||
|
||||
def languages(languages):
|
||||
return resolve_names(languages)
|
||||
|
||||
def info(id):
|
||||
data = {}
|
||||
url = 'https://openlibrary.org/books/%s.json' % id
|
||||
info = json.loads(read_url(url))
|
||||
keys = {
|
||||
'title': 'title',
|
||||
'authors': 'author',
|
||||
'publishers': 'publisher',
|
||||
'languages': 'language',
|
||||
'publish_places': 'place',
|
||||
'publish_country': 'country',
|
||||
'covers': 'cover',
|
||||
'isbn_10': 'isbn10',
|
||||
'isbn_13': 'isbn13',
|
||||
'lccn': 'lccn',
|
||||
'oclc_numbers': 'oclc',
|
||||
'dewey_decimal_class': 'classification',
|
||||
'number_of_pages': 'pages',
|
||||
}
|
||||
for key in keys:
|
||||
if key in info:
|
||||
value = info[key]
|
||||
if key == 'authors':
|
||||
value = authors(value)
|
||||
elif key == 'publish_country':
|
||||
value = COUNTRIES.get(value, value)
|
||||
elif key == 'covers':
|
||||
value = 'https://covers.openlibrary.org/b/id/%s.jpg' % value[0]
|
||||
value = COUNTRIES.get(value, value)
|
||||
elif key == 'languages':
|
||||
value = languages(value)
|
||||
elif isinstance(value, list) and key not in ('publish_places'):
|
||||
value = value[0]
|
||||
if key in ('isbn_10', 'isbn_13'):
|
||||
value = normalize_isbn(value)
|
||||
data[keys[key]] = value
|
||||
return data
|
||||
|
||||
32
oml/meta/scraper.py
Normal file
32
oml/meta/scraper.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import json
|
||||
from ox.cache import read_url
|
||||
import ox.web.lookupbyisbn
|
||||
|
||||
from utils import normalize_isbn
|
||||
|
||||
import ol
|
||||
|
||||
def add_lookupbyisbn(item):
|
||||
isbn = item.meta.get('isbn10', item.meta.get('isbn13'))
|
||||
if isbn:
|
||||
more = ox.web.lookupbyisbn.get_data(isbn)
|
||||
if more:
|
||||
for key in more:
|
||||
if more[key]:
|
||||
value = more[key]
|
||||
if isinstance(value, basestring):
|
||||
value = ox.strip_tags(ox.decode_html(value))
|
||||
elif isinstance(value, list):
|
||||
value = [ox.strip_tags(ox.decode_html(v)) for v in value]
|
||||
item.meta[key] = value
|
||||
|
||||
if 'author' in item.meta and isinstance(item.meta['author'], basestring):
|
||||
item.meta['author'] = [item.meta['author']]
|
||||
if 'isbn' in item.meta:
|
||||
del item.meta['isbn']
|
||||
|
||||
def update_ol(item):
|
||||
info = ol.info(item.meta['olid'])
|
||||
for key in info:
|
||||
item.meta[key] = info[key]
|
||||
|
||||
0
oml/node/__init__.py
Normal file
0
oml/node/__init__.py
Normal file
87
oml/node/api.py
Normal file
87
oml/node/api.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import settings
|
||||
from changelog import Changelog
|
||||
from user.models import User
|
||||
|
||||
import state
|
||||
from websocket import trigger_event
|
||||
|
||||
def api_pullChanges(app, remote_id, user_id=None, from_=None, to=None):
|
||||
if user_id and not from_ and not to:
|
||||
from_ = user_id
|
||||
user_id = None
|
||||
if user_id and from_ and not to:
|
||||
if isinstance(user_id, int):
|
||||
to = from_
|
||||
from_ = user_id
|
||||
user_id = None
|
||||
from_ = from_ or 0
|
||||
if user_id:
|
||||
return []
|
||||
if not user_id:
|
||||
user_id = settings.USER_ID
|
||||
qs = Changelog.query.filter_by(user_id=user_id)
|
||||
if from_:
|
||||
qs = qs.filter(Changelog.revision>=from_)
|
||||
if to:
|
||||
qs = qs.filter(Changelog.revision<to)
|
||||
state.nodes.queue('add', remote_id)
|
||||
return [c.json() for c in qs]
|
||||
|
||||
def api_pushChanges(app, user_id, changes):
|
||||
user = User.get(user_id)
|
||||
for change in changes:
|
||||
if not Changelog.apply_change(user, change):
|
||||
print 'FAILED TO APPLY CHANGE', change
|
||||
state.nodes.queue(user_id, 'pullChanges')
|
||||
return False
|
||||
return True
|
||||
|
||||
def api_requestPeering(app, user_id, username, message):
|
||||
user = User.get_or_create(user_id)
|
||||
if not user.info:
|
||||
user.info = {}
|
||||
if not user.peered:
|
||||
if user.pending == 'sent':
|
||||
user.info['message'] = message
|
||||
user.update_peering(True, username)
|
||||
else:
|
||||
user.pending = 'received'
|
||||
user.info['username'] = username
|
||||
user.info['message'] = message
|
||||
user.save()
|
||||
trigger_event('peering', user.json())
|
||||
return True
|
||||
return False
|
||||
|
||||
def api_acceptPeering(app, user_id, username, message):
|
||||
user = User.get(user_id)
|
||||
if user and user.pending == 'sent':
|
||||
if not user.info:
|
||||
user.info = {}
|
||||
user.info['username'] = username
|
||||
user.info['message'] = message
|
||||
user.update_peering(True, username)
|
||||
trigger_event('peering', user.json())
|
||||
return True
|
||||
return False
|
||||
|
||||
def api_rejectPeering(app, user_id, message):
|
||||
user = User.get(user_id)
|
||||
if user:
|
||||
if not user.info:
|
||||
user.info = {}
|
||||
user.info['message'] = message
|
||||
user.update_peering(False)
|
||||
trigger_event('peering', user.json())
|
||||
return True
|
||||
return False
|
||||
|
||||
def api_removePeering(app, user_id, message):
|
||||
user = User.get(user_id)
|
||||
if user:
|
||||
user.peered = False
|
||||
user.info['message'] = message
|
||||
user.save()
|
||||
trigger_event('peering', {'id': user.id, 'peered': user.peered})
|
||||
return True
|
||||
return False
|
||||
25
oml/node/gencert.py
Normal file
25
oml/node/gencert.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import OpenSSL
|
||||
|
||||
key = OpenSSL.crypto.PKey()
|
||||
key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
|
||||
|
||||
ca = OpenSSL.crypto.X509()
|
||||
ca.set_version(2)
|
||||
ca.set_serial_number(1)
|
||||
ca.get_subject().CN = "put_ed25519_key_here"
|
||||
ca.gmtime_adj_notBefore(0)
|
||||
ca.gmtime_adj_notAfter(24 * 60 * 60)
|
||||
ca.set_issuer(ca.get_subject())
|
||||
ca.set_pubkey(key)
|
||||
ca.add_extensions([
|
||||
OpenSSL.crypto.X509Extension("basicConstraints", True,
|
||||
"CA:TRUE, pathlen:0"),
|
||||
OpenSSL.crypto.X509Extension("keyUsage", True,
|
||||
"keyCertSign, cRLSign"),
|
||||
OpenSSL.crypto.X509Extension("subjectKeyIdentifier", False, "hash",
|
||||
subject=ca),
|
||||
OpenSSL.crypto.X509Extension("authorityKeyIdentifier", False, "keyid:always",issuer=ca)
|
||||
])
|
||||
ca.sign(key, "sha1")
|
||||
open("MyCertificate.crt.bin", "wb").write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, ca))
|
||||
|
||||
119
oml/node/server.py
Normal file
119
oml/node/server.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tornado
|
||||
from tornado.web import StaticFileHandler, Application, FallbackHandler
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
|
||||
import settings
|
||||
|
||||
import directory
|
||||
import utils
|
||||
import state
|
||||
import user
|
||||
|
||||
import json
|
||||
from ed25519_utils import valid
|
||||
import api
|
||||
|
||||
class NodeHandler(tornado.web.RequestHandler):
|
||||
|
||||
def initialize(self, app):
|
||||
self.app = app
|
||||
|
||||
|
||||
def post(self):
|
||||
request = self.request
|
||||
if request.method == 'POST':
|
||||
'''
|
||||
API
|
||||
pullChanges [userid] from [to]
|
||||
pushChanges [index, change]
|
||||
requestPeering username message
|
||||
acceptPeering username message
|
||||
rejectPeering message
|
||||
removePeering message
|
||||
|
||||
ping responds public ip
|
||||
'''
|
||||
key = str(request.headers['X-Ed25519-Key'])
|
||||
sig = str(request.headers['X-Ed25519-Signature'])
|
||||
data = request.body
|
||||
content = {}
|
||||
if valid(key, data, sig):
|
||||
action, args = json.loads(data)
|
||||
print 'action', action, args
|
||||
if action == 'ping':
|
||||
content = {
|
||||
'ip': request.remote_addr
|
||||
}
|
||||
else:
|
||||
with self.app.app_context():
|
||||
if action in (
|
||||
'requestPeering', 'acceptPeering', 'rejectPeering', 'removePeering'
|
||||
) or user.models.User.get(key):
|
||||
content = getattr(api, 'api_' + action)(self.app, key, *args)
|
||||
else:
|
||||
print 'PEER', key, 'IS UNKNOWN SEND 403'
|
||||
self.set_status(403)
|
||||
content = {
|
||||
'status': 'not peered'
|
||||
}
|
||||
content = json.dumps(content)
|
||||
sig = settings.sk.sign(content, encoding='base64')
|
||||
self.set_header('X-Ed25519-Signature', sig)
|
||||
self.write(content)
|
||||
self.finish()
|
||||
|
||||
def get(self):
|
||||
self.write('Open Media Library')
|
||||
self.finish()
|
||||
|
||||
class ShareHandler(tornado.web.RequestHandler):
|
||||
|
||||
def initialize(self, app):
|
||||
self.app = app
|
||||
|
||||
def get(self, id):
|
||||
with self.app.app_context():
|
||||
import item.models
|
||||
i = item.models.Item.get(id)
|
||||
if not i:
|
||||
self.set_status(404)
|
||||
self.finish()
|
||||
path = i.get_path()
|
||||
mimetype = {
|
||||
'epub': 'application/epub+zip',
|
||||
'pdf': 'application/pdf',
|
||||
'txt': 'text/plain',
|
||||
}.get(path.split('.')[-1], None)
|
||||
self.set_header('Content-Type', mimetype)
|
||||
print 'GET file', id
|
||||
with open(path, 'rb') as f:
|
||||
while 1:
|
||||
data = f.read(16384)
|
||||
if not data:
|
||||
break
|
||||
self.write(data)
|
||||
self.finish()
|
||||
|
||||
|
||||
def start(app):
|
||||
http_server = tornado.web.Application([
|
||||
(r"/get/(.*)", ShareHandler, dict(app=app)),
|
||||
(r".*", NodeHandler, dict(app=app)),
|
||||
])
|
||||
|
||||
#tr = WSGIContainer(node_app)
|
||||
#http_server= HTTPServer(tr)
|
||||
http_server.listen(settings.server['node_port'], settings.server['node_address'])
|
||||
host = utils.get_public_ipv4()
|
||||
state.online = directory.put(settings.sk, {
|
||||
'host': host,
|
||||
'port': settings.server['node_port']
|
||||
})
|
||||
return http_server
|
||||
19
oml/node/utils.py
Normal file
19
oml/node/utils.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import socket
|
||||
import requests
|
||||
from urlparse import urlparse
|
||||
|
||||
def get_public_ipv6():
|
||||
host = ('2a01:4f8:120:3201::3', 25519)
|
||||
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
s.connect(host)
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
|
||||
def get_public_ipv4():
|
||||
host = ('10.0.3.1', 25519)
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(host)
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
263
oml/nodes.py
Normal file
263
oml/nodes.py
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
from Queue import Queue
|
||||
from threading import Thread
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
import ox
|
||||
import ed25519
|
||||
import requests
|
||||
|
||||
import settings
|
||||
import user.models
|
||||
from changelog import Changelog
|
||||
|
||||
import directory
|
||||
from websocket import trigger_event
|
||||
|
||||
ENCODING='base64'
|
||||
|
||||
class Node(object):
|
||||
online = False
|
||||
download_speed = 0
|
||||
|
||||
def __init__(self, app, user):
|
||||
self._app = app
|
||||
self.user_id = user.id
|
||||
key = str(user.id)
|
||||
self.vk = ed25519.VerifyingKey(key, encoding=ENCODING)
|
||||
self.go_online()
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if ':' in self.host:
|
||||
url = 'http://[%s]:%s' % (self.host, self.port)
|
||||
else:
|
||||
url = 'http://%s:%s' % (self.host, self.port)
|
||||
return url
|
||||
|
||||
def resolve_host(self):
|
||||
r = directory.get(self.vk)
|
||||
if r:
|
||||
self.host = r['host']
|
||||
if 'port' in r:
|
||||
self.port = r['port']
|
||||
else:
|
||||
self.host = None
|
||||
self.port = 9851
|
||||
|
||||
def request(self, action, *args):
|
||||
if not self.host:
|
||||
self.resolve_host()
|
||||
if not self.host:
|
||||
return None
|
||||
content = json.dumps([action, args])
|
||||
sig = settings.sk.sign(content, encoding=ENCODING)
|
||||
headers = {
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
'Accept': 'text/plain',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Ed25519-Key': settings.USER_ID,
|
||||
'X-Ed25519-Signature': sig,
|
||||
}
|
||||
r = requests.post(self.url, data=content, headers=headers)
|
||||
if r.status_code == 403:
|
||||
print 'REMOTE ENDED PEERING'
|
||||
if self.user.peered:
|
||||
self.user.update_peering(False)
|
||||
data = r.content
|
||||
sig = r.headers.get('X-Ed25519-Signature')
|
||||
if sig and self._valid(data, sig):
|
||||
response = json.loads(data)
|
||||
else:
|
||||
response = None
|
||||
return response
|
||||
|
||||
def _valid(self, data, sig):
|
||||
try:
|
||||
self.vk.verify(sig, data, encoding=ENCODING)
|
||||
#except ed25519.BadSignatureError:
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return user.models.User.get_or_create(self.user_id)
|
||||
|
||||
def go_online(self):
|
||||
self.resolve_host()
|
||||
if self.user.peered:
|
||||
try:
|
||||
self.online = False
|
||||
print 'type to connect to', self.user_id
|
||||
self.pullChanges()
|
||||
print 'connected to', self.user_id
|
||||
self.online = True
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print 'failed to connect to', self.user_id
|
||||
self.online = False
|
||||
else:
|
||||
self.online = False
|
||||
trigger_event('status', {
|
||||
'id': self.user_id,
|
||||
'status': 'online' if self.online else 'offline'
|
||||
})
|
||||
|
||||
def pullChanges(self):
|
||||
with self._app.app_context():
|
||||
last = Changelog.query.filter_by(user_id=self.user_id).order_by('-revision').first()
|
||||
from_revision = last.revision + 1 if last else 0
|
||||
changes = self.request('pullChanges', from_revision)
|
||||
if not changes:
|
||||
return False
|
||||
for change in changes:
|
||||
if not Changelog.apply_change(self.user, change):
|
||||
print 'FAIL', change
|
||||
break
|
||||
return False
|
||||
return True
|
||||
|
||||
def pushChanges(self, changes):
|
||||
print 'pushing changes to', self.user_id, changes
|
||||
try:
|
||||
r = self.request('pushChanges', changes)
|
||||
except:
|
||||
self.online = False
|
||||
trigger_event('status', {
|
||||
'id': self.user_id,
|
||||
'status': 'offline'
|
||||
})
|
||||
r = False
|
||||
print r
|
||||
|
||||
def requestPeering(self, message):
|
||||
p = self.user
|
||||
p.pending = 'sent'
|
||||
p.save()
|
||||
r = self.request('requestPeering', settings.preferences['username'], message)
|
||||
return True
|
||||
|
||||
def acceptPeering(self, message):
|
||||
r = self.request('acceptPeering', settings.preferences['username'], message)
|
||||
p = self.user
|
||||
p.update_peering(True)
|
||||
self.go_online()
|
||||
return True
|
||||
|
||||
def rejectPeering(self, message):
|
||||
r = self.request('rejectPeering', message)
|
||||
p = self.user
|
||||
p.update_peering(False)
|
||||
return True
|
||||
|
||||
def removePeering(self, message):
|
||||
r = self.request('removePeering', message)
|
||||
p = self.user
|
||||
p.update_peering(False)
|
||||
return True
|
||||
|
||||
def download(self, item):
|
||||
url = '%s/get/%s' % (self.url, item.id)
|
||||
headers = {
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
}
|
||||
t1 = datetime.now()
|
||||
print 'GET', url
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
t2 = datetime.now()
|
||||
duration = (t2-t1).total_seconds()
|
||||
if duration:
|
||||
self.download_speed = len(r.content) / duration
|
||||
print 'SPEED', ox.format_bits(self.download_speed)
|
||||
return item.save_file(r.content)
|
||||
else:
|
||||
print 'FAILED', url
|
||||
return False
|
||||
|
||||
def download_upgrade(self):
|
||||
for module in settings.release['modules']:
|
||||
path = os.path.join(settings.update_path, settings.release['modules'][module]['name'])
|
||||
if not os.path.exists(path):
|
||||
url = '%s/oml/%s' % (self.url, settings.release['modules'][module]['name'])
|
||||
sha1 = settings.release['modules'][module]['sha1']
|
||||
headers = {
|
||||
'User-Agent': settings.USER_AGENT,
|
||||
}
|
||||
r = requests.get(url, headers=headers)
|
||||
if r.status_code == 200:
|
||||
with open(path, 'w') as fd:
|
||||
fd.write(r.content)
|
||||
if (ox.sha1sum(path) != sha1):
|
||||
print 'invalid update!'
|
||||
os.unlink(path)
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
class Nodes(Thread):
|
||||
_nodes = {}
|
||||
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
self._q = Queue()
|
||||
self._running = True
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def queue(self, *args):
|
||||
self._q.put(list(args))
|
||||
|
||||
def check_online(self, id):
|
||||
return id in self._nodes and self._nodes[id].online
|
||||
|
||||
def download(self, id, item):
|
||||
return id in self._nodes and self._nodes[id].download(item)
|
||||
|
||||
def _call(self, target, action, *args):
|
||||
print 'call', target, action, args
|
||||
if target == 'all':
|
||||
nodes = self._nodes.values()
|
||||
elif target == 'online':
|
||||
nodes = [n for n in self._nodes.values() if n.online]
|
||||
else:
|
||||
nodes = [self._nodes[target]]
|
||||
for node in nodes:
|
||||
getattr(node, action)(*args)
|
||||
|
||||
def _add_node(self, user_id):
|
||||
if user_id not in self._nodes:
|
||||
from user.models import User
|
||||
self._nodes[user_id] = Node(self._app, User.get_or_create(user_id))
|
||||
else:
|
||||
self._nodes[user_id].online = True
|
||||
trigger_event('status', {
|
||||
'id': user_id,
|
||||
'status': 'online'
|
||||
})
|
||||
|
||||
def run(self):
|
||||
with self._app.app_context():
|
||||
while self._running:
|
||||
args = self._q.get()
|
||||
if args:
|
||||
if args[0] == 'add':
|
||||
self._add_node(args[1])
|
||||
else:
|
||||
print 'next', args
|
||||
self._call(*args)
|
||||
|
||||
def join(self):
|
||||
self._running = False
|
||||
self._q.put(None)
|
||||
return Thread.join(self)
|
||||
0
oml/oxflask/__init__.py
Normal file
0
oml/oxflask/__init__.py
Normal file
154
oml/oxflask/api.py
Normal file
154
oml/oxflask/api.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division, with_statement
|
||||
import inspect
|
||||
import sys
|
||||
import json
|
||||
|
||||
from flask import request, Blueprint
|
||||
from .shortcuts import render_to_json_response, json_response
|
||||
|
||||
app = Blueprint('oxflask', __name__)
|
||||
|
||||
@app.route('/api/', methods=['POST', 'OPTIONS'])
|
||||
def api():
|
||||
if request.method == "OPTIONS":
|
||||
response = render_to_json_response({'status': {'code': 200, 'text': 'use POST'}})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response
|
||||
if not 'action' in request.form:
|
||||
methods = actions.keys()
|
||||
api = []
|
||||
for f in sorted(methods):
|
||||
api.append({'name': f,
|
||||
'doc': actions.doc(f).replace('\n', '<br>\n')})
|
||||
return render_to_json_response(api)
|
||||
action = request.form['action']
|
||||
f = actions.get(action)
|
||||
if f:
|
||||
response = f(request)
|
||||
else:
|
||||
response = render_to_json_response(json_response(status=400,
|
||||
text='Unknown action %s' % action))
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response
|
||||
|
||||
def trim(docstring):
|
||||
if not docstring:
|
||||
return ''
|
||||
# Convert tabs to spaces (following the normal Python rules)
|
||||
# and split into a list of lines:
|
||||
lines = docstring.expandtabs().splitlines()
|
||||
# Determine minimum indentation (first line doesn't count):
|
||||
indent = sys.maxint
|
||||
for line in lines[1:]:
|
||||
stripped = line.lstrip()
|
||||
if stripped:
|
||||
indent = min(indent, len(line) - len(stripped))
|
||||
# Remove indentation (first line is special):
|
||||
trimmed = [lines[0].strip()]
|
||||
if indent < sys.maxint:
|
||||
for line in lines[1:]:
|
||||
trimmed.append(line[indent:].rstrip())
|
||||
# Strip off trailing and leading blank lines:
|
||||
while trimmed and not trimmed[-1]:
|
||||
trimmed.pop()
|
||||
while trimmed and not trimmed[0]:
|
||||
trimmed.pop(0)
|
||||
# Return a single string:
|
||||
return '\n'.join(trimmed)
|
||||
|
||||
|
||||
class ApiActions(dict):
|
||||
properties = {}
|
||||
versions = {}
|
||||
def __init__(self):
|
||||
|
||||
def api(request):
|
||||
'''
|
||||
returns list of all known api actions
|
||||
param data {
|
||||
docs: bool
|
||||
}
|
||||
if docs is true, action properties contain docstrings
|
||||
return {
|
||||
status: {'code': int, 'text': string},
|
||||
data: {
|
||||
actions: {
|
||||
'api': {
|
||||
cache: true,
|
||||
doc: 'recursion'
|
||||
},
|
||||
'hello': {
|
||||
cache: true,
|
||||
..
|
||||
}
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
data = json.loads(request.form.get('data', '{}'))
|
||||
docs = data.get('docs', False)
|
||||
code = data.get('code', False)
|
||||
version = getattr(request, 'version', None)
|
||||
if version:
|
||||
_actions = self.versions.get(version, {}).keys()
|
||||
_actions = list(set(_actions + self.keys()))
|
||||
else:
|
||||
_actions = self.keys()
|
||||
_actions.sort()
|
||||
actions = {}
|
||||
for a in _actions:
|
||||
actions[a] = self.properties[a]
|
||||
if docs:
|
||||
actions[a]['doc'] = self.doc(a, version)
|
||||
if code:
|
||||
actions[a]['code'] = self.code(a, version)
|
||||
response = json_response({'actions': actions})
|
||||
return render_to_json_response(response)
|
||||
self.register(api)
|
||||
|
||||
def doc(self, name, version=None):
|
||||
if version:
|
||||
f = self.versions[version].get(name, self.get(name))
|
||||
else:
|
||||
f = self[name]
|
||||
return trim(f.__doc__)
|
||||
|
||||
def code(self, name, version=None):
|
||||
if version:
|
||||
f = self.versions[version].get(name, self.get(name))
|
||||
else:
|
||||
f = self[name]
|
||||
if name != 'api' and hasattr(f, 'func_closure') and f.func_closure:
|
||||
fc = filter(lambda c: hasattr(c.cell_contents, '__call__'), f.func_closure)
|
||||
f = fc[len(fc)-1].cell_contents
|
||||
info = f.func_code.co_filename
|
||||
info = u'%s:%s' % (info, f.func_code.co_firstlineno)
|
||||
return info, trim(inspect.getsource(f))
|
||||
|
||||
def register(self, method, action=None, cache=True, version=None):
|
||||
if not action:
|
||||
action = method.func_name
|
||||
if version:
|
||||
if not version in self.versions:
|
||||
self.versions[version] = {}
|
||||
self.versions[version][action] = method
|
||||
else:
|
||||
self[action] = method
|
||||
self.properties[action] = {'cache': cache}
|
||||
|
||||
def unregister(self, action):
|
||||
if action in self:
|
||||
del self[action]
|
||||
|
||||
actions = ApiActions()
|
||||
|
||||
def error(request):
|
||||
'''
|
||||
this action is used to test api error codes, it should return a 503 error
|
||||
'''
|
||||
success = error_is_success
|
||||
return render_to_json_response({})
|
||||
actions.register(error)
|
||||
27
oml/oxflask/db.py
Normal file
27
oml/oxflask/db.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from sqlalchemy.ext.mutable import Mutable
|
||||
|
||||
class MutableDict(Mutable, dict):
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
"Convert plain dictionaries to MutableDict."
|
||||
|
||||
if not isinstance(value, MutableDict):
|
||||
if isinstance(value, dict):
|
||||
return MutableDict(value)
|
||||
|
||||
# this call will raise ValueError
|
||||
return Mutable.coerce(key, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"Detect dictionary set events and emit change events."
|
||||
|
||||
dict.__setitem__(self, key, value)
|
||||
self.changed()
|
||||
|
||||
def __delitem__(self, key):
|
||||
"Detect dictionary del events and emit change events."
|
||||
|
||||
dict.__delitem__(self, key)
|
||||
self.changed()
|
||||
245
oml/oxflask/query.py
Normal file
245
oml/oxflask/query.py
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from sqlalchemy.sql.expression import and_, not_, or_, ClauseElement
|
||||
from datetime import datetime
|
||||
import unicodedata
|
||||
from sqlalchemy.sql import operators, extract
|
||||
|
||||
import utils
|
||||
import settings
|
||||
|
||||
def get_operator(op, type='str'):
|
||||
return {
|
||||
'str': {
|
||||
'==': operators.ilike_op,
|
||||
'>': operators.gt,
|
||||
'>=': operators.ge,
|
||||
'<': operators.lt,
|
||||
'<=': operators.le,
|
||||
'^': operators.startswith_op,
|
||||
'$': operators.endswith_op,
|
||||
},
|
||||
'int': {
|
||||
'==': operators.eq,
|
||||
'>': operators.gt,
|
||||
'>=': operators.ge,
|
||||
'<': operators.lt,
|
||||
'<=': operators.le,
|
||||
}
|
||||
}[type].get(op, {
|
||||
'str': operators.contains_op,
|
||||
'int': operators.eq
|
||||
}[type])
|
||||
|
||||
|
||||
class Parser(object):
|
||||
|
||||
def __init__(self, model):
|
||||
self._model = model
|
||||
self._find = model.find.mapper.class_
|
||||
self._user = model.users.mapper.class_
|
||||
self._list = model.lists.mapper.class_
|
||||
self.item_keys = model.item_keys
|
||||
self.filter_keys = model.filter_keys
|
||||
|
||||
def parse_condition(self, condition):
|
||||
'''
|
||||
condition: {
|
||||
value: "war"
|
||||
}
|
||||
or
|
||||
condition: {
|
||||
key: "year",
|
||||
value: [1970, 1980],
|
||||
operator: "="
|
||||
}
|
||||
...
|
||||
'''
|
||||
k = condition.get('key', '*')
|
||||
if not k:
|
||||
k = '*'
|
||||
v = condition['value']
|
||||
op = condition.get('operator')
|
||||
if not op:
|
||||
op = '='
|
||||
if op.startswith('!'):
|
||||
op = op[1:]
|
||||
exclude = True
|
||||
else:
|
||||
exclude = False
|
||||
|
||||
key_type = (utils.get_by_id(self.item_keys, k) or {'type': 'string'}).get('type')
|
||||
if isinstance(key_type, list):
|
||||
key_type = key_type[0]
|
||||
if k == 'list':
|
||||
key_type = ''
|
||||
|
||||
|
||||
if (not exclude and op == '=' or op in ('$', '^')) and v == '':
|
||||
return None
|
||||
elif k == 'resolution':
|
||||
q = self.parse_condition({'key': 'width', 'value': v[0], 'operator': op}) \
|
||||
& self.parse_condition({'key': 'height', 'value': v[1], 'operator': op})
|
||||
if exclude:
|
||||
q = ~q
|
||||
return q
|
||||
elif isinstance(v, list) and len(v) == 2 and op == '=':
|
||||
q = self.parse_condition({'key': k, 'value': v[0], 'operator': '>='}) \
|
||||
& self.parse_condition({'key': k, 'value': v[1], 'operator': '<'})
|
||||
if exclude:
|
||||
q = ~q
|
||||
return q
|
||||
elif key_type == 'boolean':
|
||||
q = getattr(self._model, 'find_%s' % k) == v
|
||||
if exclude:
|
||||
q = ~q
|
||||
return q
|
||||
elif key_type in ("string", "text"):
|
||||
if isinstance(v, unicode):
|
||||
v = unicodedata.normalize('NFKD', v).lower()
|
||||
q = get_operator(op)(self._find.value, v.lower())
|
||||
if k != '*':
|
||||
q &= (self._find.key == k)
|
||||
if exclude:
|
||||
q = ~q
|
||||
return q
|
||||
elif k == 'list':
|
||||
'''
|
||||
q = Q(id=0)
|
||||
l = v.split(":")
|
||||
if len(l) == 1:
|
||||
vqs = Volume.objects.filter(name=v, user=user)
|
||||
if vqs.count() == 1:
|
||||
v = vqs[0]
|
||||
q = Q(files__instances__volume__id=v.id)
|
||||
elif len(l) >= 2:
|
||||
l = (l[0], ":".join(l[1:]))
|
||||
lqs = list(List.objects.filter(name=l[1], user__username=l[0]))
|
||||
if len(lqs) == 1 and lqs[0].accessible(user):
|
||||
l = lqs[0]
|
||||
if l.query.get('static', False) == False:
|
||||
data = l.query
|
||||
q = self.parse_conditions(data.get('conditions', []),
|
||||
data.get('operator', '&'),
|
||||
user, l.user)
|
||||
else:
|
||||
q = Q(id__in=l.items.all())
|
||||
if exclude:
|
||||
q = ~q
|
||||
else:
|
||||
q = Q(id=0)
|
||||
'''
|
||||
l = v.split(":")
|
||||
nickname = l[0]
|
||||
name = ':'.join(l[1:])
|
||||
if nickname:
|
||||
p = self._user.query.filter_by(nickname=nickname).first()
|
||||
v = '%s:%s' % (p.id, name)
|
||||
else:
|
||||
p = self._user.query.filter_by(id=settings.USER_ID).first()
|
||||
v = ':%s' % name
|
||||
#print 'get list:', p.id, name, l, v
|
||||
if name:
|
||||
l = self._list.query.filter_by(user_id=p.id, name=name).first()
|
||||
else:
|
||||
l = None
|
||||
if l and l._query:
|
||||
data = l._query
|
||||
q = self.parse_conditions(data.get('conditions', []),
|
||||
data.get('operator', '&'))
|
||||
else:
|
||||
q = (self._find.key == 'list') & (self._find.value == v)
|
||||
return q
|
||||
elif key_type == 'date':
|
||||
def parse_date(d):
|
||||
while len(d) < 3:
|
||||
d.append(1)
|
||||
return datetime(*[int(i) for i in d])
|
||||
#using sort here since find only contains strings
|
||||
v = parse_date(v.split('-'))
|
||||
vk = getattr(self._model, 'sort_%s' % k)
|
||||
q = get_operator(op, 'int')(vk, v)
|
||||
if exclude:
|
||||
q = ~q
|
||||
return q
|
||||
else: #integer, float, time
|
||||
q = get_operator(op, 'int')(getattr(self._model, 'sort_%s'%k), v)
|
||||
if exclude:
|
||||
q = ~q
|
||||
return q
|
||||
|
||||
def parse_conditions(self, conditions, operator):
|
||||
'''
|
||||
conditions: [
|
||||
{
|
||||
value: "war"
|
||||
}
|
||||
{
|
||||
key: "year",
|
||||
value: "1970-1980,
|
||||
operator: "!="
|
||||
},
|
||||
{
|
||||
key: "country",
|
||||
value: "f",
|
||||
operator: "^"
|
||||
}
|
||||
],
|
||||
operator: "&"
|
||||
'''
|
||||
conn = []
|
||||
for condition in conditions:
|
||||
if 'conditions' in condition:
|
||||
q = self.parse_conditions(condition['conditions'],
|
||||
condition.get('operator', '&'))
|
||||
else:
|
||||
q = self.parse_condition(condition)
|
||||
if isinstance(q, list):
|
||||
conn += q
|
||||
else:
|
||||
conn.append(q)
|
||||
conn = [q for q in conn if not isinstance(q, None.__class__)]
|
||||
if conn:
|
||||
if operator == '|':
|
||||
q = conn[0]
|
||||
for c in conn[1:]:
|
||||
q = q | c
|
||||
q = [q]
|
||||
else:
|
||||
q = conn
|
||||
return q
|
||||
return []
|
||||
|
||||
def find(self, data):
|
||||
'''
|
||||
query: {
|
||||
conditions: [
|
||||
{
|
||||
value: "war"
|
||||
}
|
||||
{
|
||||
key: "year",
|
||||
value: "1970-1980,
|
||||
operator: "!="
|
||||
},
|
||||
{
|
||||
key: "country",
|
||||
value: "f",
|
||||
operator: "^"
|
||||
}
|
||||
],
|
||||
operator: "&"
|
||||
}
|
||||
'''
|
||||
|
||||
#join query with operator
|
||||
qs = self._model.query
|
||||
#only include items that have hard metadata
|
||||
conditions = self.parse_conditions(data.get('query', {}).get('conditions', []),
|
||||
data.get('query', {}).get('operator', '&'))
|
||||
for c in conditions:
|
||||
qs = qs.join(self._find).filter(c)
|
||||
qs = qs.group_by(self._model.id)
|
||||
return qs
|
||||
|
||||
34
oml/oxflask/shortcuts.py
Normal file
34
oml/oxflask/shortcuts.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from functools import wraps
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from flask import Response
|
||||
|
||||
def json_response(data=None, status=200, text='ok'):
|
||||
if not data:
|
||||
data = {}
|
||||
return {'status': {'code': status, 'text': text}, 'data': data}
|
||||
|
||||
def _to_json(python_object):
|
||||
if isinstance(python_object, datetime.datetime):
|
||||
if python_object.year < 1900:
|
||||
tt = python_object.timetuple()
|
||||
return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6])
|
||||
return python_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
raise TypeError(u'%s %s is not JSON serializable' % (repr(python_object), type(python_object)))
|
||||
|
||||
def json_dumps(obj):
|
||||
indent = 2
|
||||
return json.dumps(obj, indent=indent, default=_to_json, ensure_ascii=False).encode('utf-8')
|
||||
|
||||
def render_to_json_response(obj, content_type="text/json", status=200):
|
||||
resp = Response(json_dumps(obj), status=status, content_type=content_type)
|
||||
return resp
|
||||
|
||||
def returns_json(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
r = f(*args, **kwargs)
|
||||
return render_to_json_response(json_response(r))
|
||||
return decorated_function
|
||||
|
||||
8
oml/oxflask/utils.py
Normal file
8
oml/oxflask/utils.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
def get_by_key(objects, key, value):
|
||||
obj = filter(lambda o: o.get(key) == value, objects)
|
||||
return obj and obj[0] or None
|
||||
|
||||
def get_by_id(objects, id):
|
||||
return get_by_key(objects, 'id', id)
|
||||
|
||||
37
oml/pdict.py
Normal file
37
oml/pdict.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
class pdict(dict):
|
||||
def __init__(self, path, defaults=None):
|
||||
self._path = None
|
||||
self._defaults = defaults
|
||||
if os.path.exists(path):
|
||||
with open(path) as fd:
|
||||
_data = json.load(fd)
|
||||
for key in _data:
|
||||
self[key] = _data[key]
|
||||
self._path = path
|
||||
|
||||
def _save(self):
|
||||
if self._path:
|
||||
with open(self._path, 'w') as fd:
|
||||
json.dump(self, fd, indent=1)
|
||||
|
||||
def get(self, key, default=None):
|
||||
if default == None and self._defaults:
|
||||
default = self._defaults.get(key)
|
||||
return dict.get(self, key, default)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self and self._defaults and key in self._defaults:
|
||||
return self._defaults[key]
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key, value)
|
||||
self._save()
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
self._save()
|
||||
|
||||
59
oml/server.py
Normal file
59
oml/server.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import sys
|
||||
from tornado.web import StaticFileHandler, Application, FallbackHandler
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
from app import app
|
||||
import settings
|
||||
import websocket
|
||||
|
||||
import state
|
||||
import node.server
|
||||
|
||||
def run():
|
||||
root_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..'))
|
||||
PID = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
state.main = IOLoop.instance()
|
||||
|
||||
static_path = os.path.join(root_dir, 'static')
|
||||
|
||||
options = {
|
||||
'debug': not PID
|
||||
}
|
||||
|
||||
tr = WSGIContainer(app)
|
||||
|
||||
handlers = [
|
||||
(r'/(favicon.ico)', StaticFileHandler, {'path': static_path}),
|
||||
(r'/static/(.*)', StaticFileHandler, {'path': static_path}),
|
||||
(r'/ws', websocket.Handler),
|
||||
(r".*", FallbackHandler, dict(fallback=tr)),
|
||||
]
|
||||
|
||||
http_server = HTTPServer(Application(handlers, **options))
|
||||
|
||||
http_server.listen(settings.server['port'], settings.server['address'])
|
||||
if PID:
|
||||
with open(PID, 'w') as pid:
|
||||
pid.write('%s' % os.getpid())
|
||||
|
||||
def start_node():
|
||||
import user
|
||||
import downloads
|
||||
import nodes
|
||||
state.node = node.server.start(app)
|
||||
state.nodes = nodes.Nodes(app)
|
||||
state.downloads = downloads.Downloads(app)
|
||||
def add_users(app):
|
||||
with app.app_context():
|
||||
for p in user.models.User.query.filter_by(peered=True):
|
||||
state.nodes.queue('add', p.id)
|
||||
state.main.add_callback(add_users, app)
|
||||
state.main.add_callback(start_node)
|
||||
state.main.start()
|
||||
71
oml/settings.py
Normal file
71
oml/settings.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
import json
|
||||
import os
|
||||
import ed25519
|
||||
|
||||
from pdict import pdict
|
||||
|
||||
base_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..'))
|
||||
static_path = os.path.join(base_dir, 'static')
|
||||
updates_path = os.path.join(base_dir, 'updates')
|
||||
|
||||
oml_config_path = os.path.join(base_dir, 'config.json')
|
||||
|
||||
config_dir = os.path.normpath(os.path.join(base_dir, '..', 'config'))
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
|
||||
db_path = os.path.join(config_dir, 'openmedialibrary.db')
|
||||
covers_db_path = os.path.join(config_dir, 'covers.db')
|
||||
key_path = os.path.join(config_dir, 'node.key')
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
if os.path.exists(oml_config_path):
|
||||
with open(oml_config_path) as fd:
|
||||
config = json.load(fd)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
preferences = pdict(os.path.join(config_dir, 'preferences.json'), config['user']['preferences'])
|
||||
ui = pdict(os.path.join(config_dir, 'ui.json'), config['user']['ui'])
|
||||
|
||||
server = pdict(os.path.join(config_dir, 'server.json'))
|
||||
server_defaults = {
|
||||
'port': 9842,
|
||||
'address': '127.0.0.1',
|
||||
'node_port': 9851,
|
||||
'node_address': '::',
|
||||
'extract_text': True,
|
||||
'directory_service': 'http://[2a01:4f8:120:3201::3]:25519',
|
||||
'lookup_service': 'http://data.openmedialibrary.com',
|
||||
}
|
||||
|
||||
for key in server_defaults:
|
||||
if key not in server:
|
||||
server[key] = server_defaults[key]
|
||||
|
||||
release = pdict(os.path.join(config_dir, 'release.json'))
|
||||
|
||||
if os.path.exists(key_path):
|
||||
with open(key_path) as fd:
|
||||
sk = ed25519.SigningKey(fd.read())
|
||||
vk = sk.get_verifying_key()
|
||||
else:
|
||||
sk, vk = ed25519.create_keypair()
|
||||
with open(key_path, 'w') as fd:
|
||||
os.chmod(key_path, 0600)
|
||||
fd.write(sk.to_bytes())
|
||||
os.chmod(key_path, 0400)
|
||||
|
||||
USER_ID = vk.to_ascii(encoding='base64')
|
||||
|
||||
if 'modules' in release and 'openmedialibrary' in release['modules']:
|
||||
VERSION = release['modules']['openmedialibrary']['version']
|
||||
else:
|
||||
VERSION = 'git'
|
||||
|
||||
USER_AGENT = 'OpenMediaLibrary/%s' % VERSION
|
||||
14
oml/setup.py
Normal file
14
oml/setup.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import settings
|
||||
from user.models import List, User
|
||||
|
||||
def create_default_lists(user_id=None):
|
||||
user_id = user_id or settings.USER_ID
|
||||
user = User.get_or_create(user_id)
|
||||
for list in settings.config['lists']:
|
||||
l = List.get(user_id, list['title'])
|
||||
if not l:
|
||||
l = List.create(user_id, list['title'], list.get('query'))
|
||||
|
||||
9
oml/state.py
Normal file
9
oml/state.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
websockets = []
|
||||
nodes = False
|
||||
main = None
|
||||
online = False
|
||||
|
||||
def user():
|
||||
import settings
|
||||
import user.models
|
||||
return user.models.User.get_or_create(settings.USER_ID)
|
||||
97
oml/update.py
Normal file
97
oml/update.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
|
||||
from __future__ import division
|
||||
#https://github.com/hiroakis/tornado-websocket-example/blob/master/app.py
|
||||
#http://stackoverflow.com/questions/5892895/tornado-websocket-question
|
||||
|
||||
#possibly get https://github.com/methane/wsaccel
|
||||
|
||||
|
||||
#possibly run the full django app throw tornado instead of gunicorn
|
||||
#https://github.com/bdarnell/django-tornado-demo/blob/master/testsite/tornado_main.py
|
||||
#http://stackoverflow.com/questions/7190431/tornado-with-django
|
||||
#http://www.tornadoweb.org/en/stable/wsgi.html
|
||||
|
||||
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application
|
||||
from tornado.websocket import WebSocketHandler
|
||||
from Queue import Queue
|
||||
import urllib2
|
||||
import os
|
||||
from contextlib import closing
|
||||
import json
|
||||
from threading import Thread
|
||||
|
||||
|
||||
class Background:
|
||||
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.q = Queue()
|
||||
|
||||
def worker(self):
|
||||
while True:
|
||||
message = self.q.get()
|
||||
action, data = json.loads(message)
|
||||
if action == 'get':
|
||||
if 'url' in data and data['url'].startswith('http'):
|
||||
self.download(data['url'], '/tmp/test.data')
|
||||
elif action == 'update':
|
||||
self.post({'error': 'not implemented'})
|
||||
else:
|
||||
self.post({'error': 'unknown action'})
|
||||
self.q.task_done()
|
||||
|
||||
def join(self):
|
||||
self.q.join()
|
||||
|
||||
def put(self, data):
|
||||
self.q.put(data)
|
||||
|
||||
def post(self, data):
|
||||
if not isinstance(data, basestring):
|
||||
data = json.dumps(data)
|
||||
main.add_callback(lambda: self.handler.write_message(data))
|
||||
|
||||
def download(self, url, filename):
|
||||
dirname = os.path.dirname(filename)
|
||||
if dirname and not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
with open(filename, 'w') as f:
|
||||
with closing(urllib2.urlopen(url)) as u:
|
||||
size = int(u.headers.get('content-length', 0))
|
||||
done = 0
|
||||
chunk_size = max(min(1024*1024, int(size/100)), 4096)
|
||||
print 'chunksize', chunk_size
|
||||
for data in iter(lambda: u.read(chunk_size), ''):
|
||||
f.write(data)
|
||||
done += len(data)
|
||||
if size:
|
||||
percent = done/size
|
||||
self.post({'url': url, 'size': size, 'done': done, 'percent': percent})
|
||||
|
||||
class Handler(WebSocketHandler):
|
||||
def open(self):
|
||||
print "New connection opened."
|
||||
self.background = Background(self)
|
||||
self.t = Thread(target=self.background.worker)
|
||||
self.t.daemon = True
|
||||
self.t.start()
|
||||
|
||||
#websocket calls
|
||||
def on_message(self, message):
|
||||
self.background.put(message)
|
||||
|
||||
def on_close(self):
|
||||
print "Connection closed."
|
||||
self.background.join()
|
||||
|
||||
print "Server started."
|
||||
HTTPServer(Application([("/", Handler)])).listen(28161)
|
||||
main = IOLoop.instance()
|
||||
main.start()
|
||||
0
oml/user/__init__.py
Normal file
0
oml/user/__init__.py
Normal file
216
oml/user/api.py
Normal file
216
oml/user/api.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
from flask import json
|
||||
from oxflask.api import actions
|
||||
from oxflask.shortcuts import returns_json
|
||||
|
||||
import models
|
||||
from item.models import Item
|
||||
|
||||
from utils import get_position_by_id
|
||||
|
||||
import settings
|
||||
import state
|
||||
from changelog import Changelog
|
||||
|
||||
@returns_json
|
||||
def init(request):
|
||||
'''
|
||||
this is an init request to test stuff
|
||||
'''
|
||||
#print 'init', request
|
||||
response = {}
|
||||
if os.path.exists(settings.oml_config_path):
|
||||
with open(settings.oml_config_path) as fd:
|
||||
config = json.load(fd)
|
||||
else:
|
||||
config = {}
|
||||
response['config'] = config
|
||||
response['user'] = deepcopy(config['user'])
|
||||
if settings.preferences:
|
||||
response['user']['preferences'] = settings.preferences
|
||||
response['user']['id'] = settings.USER_ID
|
||||
response['user']['online'] = state.online
|
||||
if settings.ui:
|
||||
response['user']['ui'] = settings.ui
|
||||
return response
|
||||
actions.register(init)
|
||||
|
||||
def update_dict(root, data):
|
||||
for key in data:
|
||||
keys = map(lambda p: p.replace('\0', '\\.'), key.replace('\\.', '\0').split('.'))
|
||||
value = data[key]
|
||||
p = root
|
||||
while len(keys)>1:
|
||||
key = keys.pop(0)
|
||||
if isinstance(p, list):
|
||||
p = p[get_position_by_id(p, key)]
|
||||
else:
|
||||
if key not in p:
|
||||
p[key] = {}
|
||||
p = p[key]
|
||||
if value == None and keys[0] in p:
|
||||
del p[keys[0]]
|
||||
else:
|
||||
p[keys[0]] = value
|
||||
|
||||
@returns_json
|
||||
def setPreferences(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
update_dict(settings.preferences, data)
|
||||
return settings.preferences
|
||||
actions.register(setPreferences)
|
||||
|
||||
@returns_json
|
||||
def setUI(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
update_dict(settings.ui, data)
|
||||
return settings.ui
|
||||
actions.register(setUI)
|
||||
|
||||
@returns_json
|
||||
def getUsers(request):
|
||||
users = []
|
||||
for u in models.User.query.filter(models.User.id!=settings.USER_ID).all():
|
||||
users.append(u.json())
|
||||
return {
|
||||
"users": users
|
||||
}
|
||||
actions.register(getUsers)
|
||||
|
||||
@returns_json
|
||||
def getLists(request):
|
||||
lists = {}
|
||||
for u in models.User.query.filter((models.User.peered==True)|(models.User.id==settings.USER_ID)):
|
||||
lists[u.id] = u.lists_json()
|
||||
return {
|
||||
'lists': lists
|
||||
}
|
||||
actions.register(getLists)
|
||||
|
||||
@returns_json
|
||||
def addList(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
user_id = settings.USER_ID
|
||||
l = models.List.get(user_id, data['name'])
|
||||
if not l:
|
||||
l = models.List.create(user_id, data['name'], data.get('query'))
|
||||
if 'items' in data:
|
||||
l.add_items(data['items'])
|
||||
return l.json()
|
||||
return {}
|
||||
actions.register(addList, cache=False)
|
||||
|
||||
@returns_json
|
||||
def removeList(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
l = models.List.get(data['id'])
|
||||
if l:
|
||||
l.remove()
|
||||
return {}
|
||||
actions.register(removeList, cache=False)
|
||||
|
||||
@returns_json
|
||||
def editList(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
l = models.List.get_or_create(data['id'])
|
||||
name = l.name
|
||||
if 'name' in data:
|
||||
l.name = data['name']
|
||||
l.type = 'static'
|
||||
if 'query' in data:
|
||||
l._query = data['query']
|
||||
l.type = 'smart'
|
||||
if l.type == 'static' and name != l.name:
|
||||
Changelog.record(state.user(), 'editlist', name, {'name': l.name})
|
||||
l.save()
|
||||
return {}
|
||||
actions.register(editList, cache=False)
|
||||
|
||||
@returns_json
|
||||
def addListItem(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
l = models.List.get_or_create(data['id'])
|
||||
i = Item.get(data['item'])
|
||||
if l and i:
|
||||
l.items.append(i)
|
||||
models.db.session.add(l)
|
||||
i.update()
|
||||
return {}
|
||||
actions.register(addListItem, cache=False)
|
||||
|
||||
@returns_json
|
||||
def removeListItem(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
l = models.List.get(data['id'])
|
||||
i = Item.get(data['item'])
|
||||
if l and i:
|
||||
l.items.remove(i)
|
||||
models.db.session.add(l)
|
||||
i.update()
|
||||
return {}
|
||||
actions.register(removeListItem, cache=False)
|
||||
|
||||
@returns_json
|
||||
def sortLists(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
n = 0
|
||||
print 'sortLists', data
|
||||
for id in data['ids']:
|
||||
l = models.List.get(id)
|
||||
l.position = n
|
||||
n += 1
|
||||
models.db.session.add(l)
|
||||
models.db.session.commit()
|
||||
return {}
|
||||
actions.register(sortLists, cache=False)
|
||||
|
||||
@returns_json
|
||||
def editUser(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
if 'nickname' in data:
|
||||
p = models.User.get_or_create(data['id'])
|
||||
p.set_nickname(data['nickname'])
|
||||
p.save()
|
||||
return {}
|
||||
actions.register(editUser, cache=False)
|
||||
|
||||
@returns_json
|
||||
def requestPeering(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
p = models.User.get_or_create(data['id'])
|
||||
state.nodes.queue('add', p.id)
|
||||
state.nodes.queue(p.id, 'requestPeering', data.get('message', ''))
|
||||
return {}
|
||||
actions.register(requestPeering, cache=False)
|
||||
|
||||
@returns_json
|
||||
def acceptPeering(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
p = models.User.get_or_create(data['id'])
|
||||
state.nodes.queue('add', p.id)
|
||||
state.nodes.queue(p.id, 'acceptPeering', data.get('message', ''))
|
||||
return {}
|
||||
actions.register(acceptPeering, cache=False)
|
||||
|
||||
@returns_json
|
||||
def rejectPeering(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
p = models.User.get_or_create(data['id'])
|
||||
state.nodes.queue('add', p.id)
|
||||
state.nodes.queue(p.id, 'rejectPeering', data.get('message', ''))
|
||||
return {}
|
||||
actions.register(rejectPeering, cache=False)
|
||||
|
||||
@returns_json
|
||||
def removePeering(request):
|
||||
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||
p = models.User.get_or_create(data['id'])
|
||||
state.nodes.queue('add', p.id)
|
||||
state.nodes.queue(p.id, 'removePeering', data.get('message', ''))
|
||||
return {}
|
||||
actions.register(removePeering, cache=False)
|
||||
213
oml/user/models.py
Normal file
213
oml/user/models.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import json
|
||||
|
||||
from oxflask.db import MutableDict
|
||||
import oxflask.query
|
||||
|
||||
from changelog import Changelog
|
||||
import settings
|
||||
from settings import db
|
||||
|
||||
import state
|
||||
|
||||
class User(db.Model):
|
||||
|
||||
created = db.Column(db.DateTime())
|
||||
modified = db.Column(db.DateTime())
|
||||
id = db.Column(db.String(43), primary_key=True)
|
||||
info = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
|
||||
#nickname = db.Column(db.String(256), unique=True)
|
||||
nickname = db.Column(db.String(256))
|
||||
|
||||
pending = db.Column(db.String(64)) # sent|received
|
||||
peered = db.Column(db.Boolean())
|
||||
online = db.Column(db.Boolean())
|
||||
|
||||
def __repr__(self):
|
||||
return self.id
|
||||
|
||||
@classmethod
|
||||
def get(cls, id):
|
||||
return cls.query.filter_by(id=id).first()
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, id):
|
||||
user = cls.get(id)
|
||||
if not user:
|
||||
user = cls(id=id, peered=False, online=False)
|
||||
user.info = {}
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def json(self):
|
||||
j = {}
|
||||
if self.info:
|
||||
j.update(self.info)
|
||||
j['id'] = self.id
|
||||
if self.pending:
|
||||
j['pending'] = self.pending
|
||||
j['peered'] = self.peered
|
||||
j['online'] = self.check_online()
|
||||
j['nickname'] = self.nickname
|
||||
return j
|
||||
|
||||
def check_online(self):
|
||||
return state.nodes.check_online(self.id)
|
||||
|
||||
def lists_json(self):
|
||||
return [l.json() for l in self.lists.order_by('position')]
|
||||
|
||||
def update_peering(self, peered, username=None):
|
||||
if peered:
|
||||
self.pending = ''
|
||||
self.peered = True
|
||||
if username:
|
||||
self.info['username'] = username
|
||||
|
||||
self.set_nickname(self.info.get('username', 'anonymous'))
|
||||
else:
|
||||
self.peered = False
|
||||
self.nickname = None
|
||||
for i in self.items:
|
||||
i.users.remove(self)
|
||||
if not i.users:
|
||||
print 'last user, remove'
|
||||
db.session.delete(i)
|
||||
else:
|
||||
i.update_lists()
|
||||
self.save()
|
||||
|
||||
def set_nickname(self, nickname):
|
||||
username = nickname
|
||||
n = 2
|
||||
while self.query.filter_by(nickname=nickname).filter(User.id!=self.id).first():
|
||||
nickname = '%s [%d]' % (username, n)
|
||||
n += 1
|
||||
self.nickname = nickname
|
||||
|
||||
list_items = db.Table('listitem',
|
||||
db.Column('list_id', db.Integer(), db.ForeignKey('list.id')),
|
||||
db.Column('item_id', db.String(32), db.ForeignKey('item.id'))
|
||||
)
|
||||
|
||||
class List(db.Model):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
name = db.Column(db.String())
|
||||
position = db.Column(db.Integer())
|
||||
|
||||
type = db.Column(db.String(64))
|
||||
_query = db.Column('query', MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||
|
||||
user_id = db.Column(db.String(43), db.ForeignKey('user.id'))
|
||||
user = db.relationship('User', backref=db.backref('lists', lazy='dynamic'))
|
||||
|
||||
items = db.relationship('Item', secondary=list_items,
|
||||
backref=db.backref('lists', lazy='dynamic'))
|
||||
|
||||
@classmethod
|
||||
def get(cls, user_id, name=None):
|
||||
if not name:
|
||||
user_id, name = cls.get_user_name(user_id)
|
||||
return cls.query.filter_by(user_id=user_id, name=name).first()
|
||||
|
||||
@classmethod
|
||||
def get_user_name(cls, user_id):
|
||||
l = user_id.split(':')
|
||||
nickname = l[0]
|
||||
name = ':'.join(l[1:])
|
||||
if nickname:
|
||||
user = User.query.filter_by(nickname=nickname).first()
|
||||
user_id = user.id
|
||||
else:
|
||||
user_id = settings.USER_ID
|
||||
return user_id, name
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, user_id, name=None):
|
||||
if not name:
|
||||
user_id, name = cls.get_user_name(user_id)
|
||||
l = cls.get(user_id, name)
|
||||
if not l:
|
||||
l = cls(name=name, user_id=user_id)
|
||||
db.session.add(l)
|
||||
db.session.commit()
|
||||
return l
|
||||
|
||||
@classmethod
|
||||
def create(cls, user_id, name, query=None):
|
||||
l = cls(name=name, user_id=user_id)
|
||||
l._query = query
|
||||
l.type = 'smart' if l._query else 'static'
|
||||
l.position = cls.query.filter_by(user_id=user_id).count()
|
||||
if user_id == settings.USER_ID:
|
||||
p = User.get(settings.USER_ID)
|
||||
if not l._query:
|
||||
Changelog.record(p, 'addlist', l.name)
|
||||
db.session.add(l)
|
||||
db.session.commit()
|
||||
return l
|
||||
|
||||
def add_items(self, items):
|
||||
from item.models import Item
|
||||
for item_id in items:
|
||||
i = Item.get(item_id)
|
||||
self.items.add(i)
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def remove_items(self, items):
|
||||
from item.models import Item
|
||||
for item_id in items:
|
||||
i = Item.get(item_id)
|
||||
self.items.remove(i)
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def remove(self):
|
||||
if not self._query:
|
||||
for i in self.items:
|
||||
self.items.remove(i)
|
||||
if not self._query:
|
||||
print 'record change: removelist', self.user, self.name
|
||||
Changelog.record(self.user, 'removelist', self.name)
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
@property
|
||||
def public_id(self):
|
||||
id = ''
|
||||
if self.user_id != settings.USER_ID:
|
||||
id += self.user_id
|
||||
id = '%s:%s' % (id, self.name)
|
||||
return id
|
||||
|
||||
def items_count(self):
|
||||
from item.models import Item
|
||||
if self._query:
|
||||
data = self._query
|
||||
return oxflask.query.Parser(Item).find({'query': data}).count()
|
||||
else:
|
||||
return len(self.items)
|
||||
|
||||
def json(self):
|
||||
r = {
|
||||
'id': self.public_id,
|
||||
'name': self.name,
|
||||
'index': self.position,
|
||||
'items': self.items_count(),
|
||||
'type': self.type
|
||||
}
|
||||
if self.type == 'smart':
|
||||
r['query'] = self._query
|
||||
return r
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
95
oml/utils.py
Normal file
95
oml/utils.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import Image
|
||||
from StringIO import StringIO
|
||||
import re
|
||||
import stdnum.isbn
|
||||
|
||||
import ox
|
||||
|
||||
def valid_olid(id):
|
||||
return id.startswith('OL') and id.endswith('M')
|
||||
|
||||
def get_positions(ids, pos):
|
||||
'''
|
||||
>>> get_positions([1,2,3,4], [2,4])
|
||||
{2: 1, 4: 3}
|
||||
'''
|
||||
positions = {}
|
||||
for i in pos:
|
||||
try:
|
||||
positions[i] = ids.index(i)
|
||||
except:
|
||||
pass
|
||||
return positions
|
||||
|
||||
def get_by_key(objects, key, value):
|
||||
obj = filter(lambda o: o.get(key) == value, objects)
|
||||
return obj and obj[0] or None
|
||||
|
||||
def get_by_id(objects, id):
|
||||
return get_by_key(objects, 'id', id)
|
||||
|
||||
def resize_image(data, width=None, size=None):
|
||||
source = Image.open(StringIO(data)).convert('RGB')
|
||||
source_width = source.size[0]
|
||||
source_height = source.size[1]
|
||||
if size:
|
||||
if source_width > source_height:
|
||||
width = size
|
||||
height = int(width / (float(source_width) / source_height))
|
||||
height = height - height % 2
|
||||
else:
|
||||
height = size
|
||||
width = int(height * (float(source_width) / source_height))
|
||||
width = width - width % 2
|
||||
|
||||
else:
|
||||
height = int(width / (float(source_width) / source_height))
|
||||
height = height - height % 2
|
||||
|
||||
width = max(width, 1)
|
||||
height = max(height, 1)
|
||||
|
||||
if width < source_width:
|
||||
resize_method = Image.ANTIALIAS
|
||||
else:
|
||||
resize_method = Image.BICUBIC
|
||||
output = source.resize((width, height), resize_method)
|
||||
o = StringIO()
|
||||
output.save(o, format='jpeg')
|
||||
data = o.getvalue()
|
||||
o.close()
|
||||
return data
|
||||
|
||||
def sort_title(title):
|
||||
|
||||
title = title.replace(u'Æ', 'Ae')
|
||||
if isinstance(title, str):
|
||||
title = unicode(title)
|
||||
title = ox.sort_string(title)
|
||||
|
||||
#title
|
||||
title = re.sub(u'[\'!¿¡,\.;\-"\:\*\[\]]', '', title)
|
||||
return title.strip()
|
||||
|
||||
def normalize_isbn(value):
|
||||
return ''.join([s for s in value if s.isdigit() or s == 'X'])
|
||||
|
||||
def find_isbns(text):
|
||||
matches = re.compile('\d[\d\-X\ ]+').findall(text)
|
||||
matches = [normalize_isbn(value) for value in matches]
|
||||
return [isbn for isbn in matches if stdnum.isbn.is_valid(isbn)
|
||||
and len(isbn) in (10, 13)
|
||||
and isbn not in (
|
||||
'0' * 10,
|
||||
'0' * 13,
|
||||
)]
|
||||
|
||||
def get_position_by_id(list, key):
|
||||
for i in range(0, len(list)):
|
||||
if list[i]['id'] == key:
|
||||
return i
|
||||
return -1
|
||||
|
||||
86
oml/websocket.py
Normal file
86
oml/websocket.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
from __future__ import division
|
||||
|
||||
from tornado.websocket import WebSocketHandler
|
||||
from tornado.ioloop import IOLoop
|
||||
from Queue import Queue
|
||||
import urllib2
|
||||
import os
|
||||
from contextlib import closing
|
||||
import json
|
||||
from threading import Thread
|
||||
|
||||
from oxflask.shortcuts import json_dumps
|
||||
|
||||
import state
|
||||
|
||||
class Background:
|
||||
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.main = IOLoop.instance()
|
||||
self.q = Queue()
|
||||
self.connected = True
|
||||
|
||||
def worker(self):
|
||||
while self.connected:
|
||||
message = self.q.get()
|
||||
action, data = json.loads(message)
|
||||
print action
|
||||
print data
|
||||
import item.scan
|
||||
if action == 'ping':
|
||||
self.post(['pong', data])
|
||||
elif action == 'import':
|
||||
item.scan.run_import()
|
||||
elif action == 'scan':
|
||||
item.scan.run_scan()
|
||||
elif action == 'update':
|
||||
self.post(['error', {'error': 'not implemented'}])
|
||||
else:
|
||||
self.post(['error', {'error': 'unknown action'}])
|
||||
self.q.task_done()
|
||||
|
||||
def join(self):
|
||||
self.q.join()
|
||||
|
||||
def put(self, data):
|
||||
self.q.put(data)
|
||||
|
||||
def post(self, data):
|
||||
if not isinstance(data, basestring):
|
||||
data = json_dumps(data)
|
||||
self.main.add_callback(lambda: self.handler.write_message(data))
|
||||
|
||||
|
||||
class Handler(WebSocketHandler):
|
||||
|
||||
def open(self):
|
||||
print "New connection opened."
|
||||
self.background = Background(self)
|
||||
state.websockets.append(self.background)
|
||||
self.t = Thread(target=self.background.worker)
|
||||
self.t.daemon = True
|
||||
self.t.start()
|
||||
|
||||
#websocket calls
|
||||
def on_message(self, message):
|
||||
self.background.put(message)
|
||||
|
||||
def on_close(self):
|
||||
print "Connection closed."
|
||||
state.websockets.remove(self.background)
|
||||
self.background.connected = False
|
||||
|
||||
def trigger_event(event, data):
|
||||
if len(state.websockets):
|
||||
print 'trigger event', event, data, len(state.websockets)
|
||||
for ws in state.websockets:
|
||||
try:
|
||||
ws.post([event, data])
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print 'failed to send to ws', ws, event, data
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue