Open Media Library

This commit is contained in:
j 2014-05-04 19:26:43 +02:00
commit 2ee2bc178a
228 changed files with 85988 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
env
*.pyc
*.gz
*.swp
*.min.js
static/oxjs
*~
*.db
._*

19
README Normal file
View file

@ -0,0 +1,19 @@
Open Media Library
== Install ==
soon
== Development ==
mkdir client
cd client
git clone https://git.0x2620.org/openmedialibrary.git
git clone https://git.0x2620.org/openmedialibrary_platform.git platform
ln -s openmedialibrary/ctl ctl
./ctl update_static
./ctl db upgrade
./ctl setup
# and start it
./ctl debug

366
config.json Normal file
View file

@ -0,0 +1,366 @@
{
"coverRatio": 0.75,
"itemKeys": [
{
"id": "*",
"title": "All",
"type": "text",
"find": true
},
{
"id": "title",
"title": "Title",
"type": "string",
"autocomplete": true,
"columnRequired": true,
"columnWidth": 256,
"find": true,
"sort": true,
"sortType": "title"
},
{
"id": "author",
"title": "Author",
"type": ["string"],
"autocomplete": true,
"columnRequired": true,
"columnWidth": 192,
"filter": true,
"sort": true,
"sortType": "person"
},
{
"id": "edition",
"title": "Edition",
"type": "string",
"columnWidth": 128,
"find": true
},
{
"id": "publisher",
"title": "Publisher",
"type": "string",
"autocomplete": true,
"columnWidth": 128,
"filter": true,
"find": true,
"sort": true
},
{
"id": "place",
"title": "Place",
"type": ["string"],
"columnWidth": 128,
"sort": true
},
{
"id": "country",
"title": "Country",
"type": "string",
"columnWidth": 128,
"filter": true,
"find": true,
"sort": true
},
{
"id": "date",
"title": "Date",
"type": "string",
"columnWidth": 96,
"filter": true,
"filterMap": "(-?\\d+)",
"sort": true
},
{
"id": "language",
"title": "Language",
"type": "string",
"columnWidth": 128,
"filter": true,
"find": true,
"sort": true
},
{
"id": "pages",
"title": "Pages",
"type": "integer",
"columnWidth": 96,
"format": {"type": "unit", "args": ["pages"]},
"sort": true
},
{
"id": "classification",
"title": "Classification",
"type": "string",
"autocomplete": true,
"columnWidth": 192,
"find": true,
"filter": true,
"sort": true
},
{
"id": "description",
"title": "Description",
"type": "text",
"find": true
},
{
"id": "extension",
"title": "Extension",
"type": "string",
"columnWidth": 80,
"sort": true
},
{
"id": "size",
"title": "Size",
"type": "integer",
"columnWidth": 64,
"format": {"type": "value", "args": ["B"]},
"sort": true
},
{
"id": "created",
"title": "First Seen",
"type": "date",
"columnWidth": 144,
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
"sort": true
},
{
"id": "added",
"title": "Date Added",
"type": "date",
"columnWidth": 144,
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
"sort": true
},
{
"id": "modified",
"title": "Last Modified",
"type": "date",
"columnWidth": 144,
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
"sort": true
},
{
"id": "accessed",
"title": "Last Read",
"type": "date",
"columnWidth": 144,
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
"sort": true
},
{
"id": "timesaccessed",
"title": "Times Accessed",
"type": "integer",
"columnWidth": 64,
"format": {"type": "number", "args": []},
"sort": true
},
{
"id": "mediastate",
"title": "Media State",
"type": "string",
"find": true,
"sort": true
},
{
"id": "transferadded",
"title": "Added",
"type": "date",
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
"sort": true
},
{
"id": "transferprogress",
"title": "Progress",
"type": "float",
"format": {"type": "percent", "args": [1, 0]},
"sort": true
},
{
"id": "id",
"title": "ID",
"type": "string",
"columnWidth": 96,
"sort": true
},
{
"id": "isbn10",
"title": "ISBN-10",
"type": "string",
"columnWidth": 96,
"sort": true
},
{
"id": "isbn13",
"title": "ISBN-13",
"type": "string",
"columnWidth": 96,
"sort": true
},
{
"id": "lccn",
"title": "LCCN",
"type": "string",
"columnWidth": 96,
"sort": true
},
{
"id": "olid",
"title": "OLID",
"type": "string",
"columnWidth": 96,
"sort": true
},
{
"id": "oclc",
"title": "OCLC",
"type": "string",
"columnWidth": 96,
"sort": true
},
{
"id": "mainid",
"title": "Main ID",
"type": "string"
},
{
"id": "random",
"title": "Random",
"type": "integer",
"sort": true
}
],
"itemViews": [
{"id": "info", "title": "Info"},
{"id": "book", "title": "Book"}
],
"lists": [
{"title": "Reading List"},
{"title": "1968", "query": {
"conditions": [{"key": "*", "operator": "=", "value": "1968"}],
"operator": "&"
}}
],
"listViews": [
{"id": "list", "title": "List"},
{"id": "grid", "title": "Grid"}
],
"locales": ["en", "ar", "hi"],
"pages": [
{"id": "welcome", "title": "Welcome"},
{"id": "app", "title": "Application", "parts": [
{"id": "about", "title": "About Open Media Library"},
{"id": "faq", "title": "Frequently Asked Questions"},
{"id": "terms", "title": "Terms and Conditions"},
{"id": "development", "title": "Software Development"},
{"id": "contact", "title": "Send Feedback"},
{"id": "update", "title": "Software Update"}
]},
{"id": "preferences", "title": "Preferences", "parts": [
{"id": "account", "title": "Account"},
{"id": "library", "title": "Library"},
{"id": "peering", "title": "Peering"},
{"id": "network", "title": "Network"},
{"id": "appearance", "title": "Appearance"},
{"id": "extensions", "title": "Extensions"},
{"id": "advanced", "title": "Advanced"}
]},
{"id": "users", "title": "Users"},
{"id": "devices", "title": "Devices"},
{"id": "notifications", "title": "Notifications"},
{"id": "transfers", "title": "Transfers"},
{"id": "gettingstarted", "title": "Getting Started"},
{"id": "help", "title": "Help", "parts": [
{"id": "introduction", "title": "Introduction"},
{"id": "accounts", "title": "Accounts"},
{"id": "navigaion", "title": "Navigation"},
{"id": "views", "title": "Views"},
{"id": "lists", "title": "Lists"}
]},
{"id": "documentation", "title": "Documentation"}
],
"themes": ["oxlight", "oxmedium", "oxdark"],
"totals": [
{"id": "items"},
{"id": "size"}
],
"user": {
"preferences": {
"acceptMessage": "",
"contact": "",
"downloadRate": null,
"extensions": "",
"importPath": "~/Documents/Open Media Library/Import/",
"libraryPath": "~/Documents/Open Media Library/",
"receivedRequests": "notify",
"rejectMessage": "",
"sendDiagnostics": false,
"sendRequests": "manually",
"uploadRate": null,
"username": ""
},
"ui": {
"fileInfo": "extension",
"filters": [
{"id": "author", "sort": [{"key": "items", "operator": "-"}]},
{"id": "publisher", "sort": [{"key": "items", "operator": "-"}]},
{"id": "date", "sort": [{"key": "name", "operator": "-"}]},
{"id": "language", "sort": [{"key": "items", "operator": "-"}]},
{"id": "classification", "sort": [{"key": "items", "operator": "-"}]}
],
"filtersSize": 176,
"find": {"conditions": [], "operator": "&"},
"icons": "cover",
"item": "",
"itemView": "info",
"listColumns": ["title", "author", "publisher", "date"],
"listColumnWidth": {},
"lists": {},
"listSelection": [],
"listSort": [
{"key": "author", "operator": "+"},
{"key": "date", "operator": "+"},
{"key": "title", "operator": "+"}
],
"listView": "grid",
"locale": "en",
"mediaState": {},
"page": "welcome",
"part": {
"app": "about",
"preferences": "account",
"help": "introduction"
},
"section": "books",
"showBrowser": true,
"showDebugMenu": false,
"showFileInfo": true,
"showFolder": {},
"showFilters": true,
"showInfo": true,
"showSection": {
"notifications": {
"received": true,
"sent": true
},
"transfers": {
"active": true,
"queued": true
},
"users": {
"peers": true,
"pending": true,
"others": true
}
},
"showSidebar": true,
"sidebarSize": 256,
"theme": "oxlight",
"usersSelection": []
}
}
}

77
ctl Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
HOST="127.0.0.1:9842"
NAME="openmedialibrary"
PID="/tmp/$NAME.pid"
cd `dirname "$0"`
if [ -e oml ]; then
cd ..
fi
BASE=`pwd`
SYSTEM=`uname -s`
export PLATFORM_ENV="$BASE/platform/$SYSTEM"
if [ $SYSTEM == "Darwin" ]; then
export DYLD_FALLBACK_LIBRARY_PATH="$PLATFORM_ENV/lib"
fi
PATH="$PLATFORM_ENV/bin:$PATH"
SHARED_ENV="$BASE/platform/Shared"
export SHARED_ENV
PATH="$SHARED_ENV/bin:$PATH"
export PATH
PYTHONPATH="$PLATFORM_ENV/lib/python2.7/site-packages:$SHARED_ENV/lib/python2.7/site-packages:$BASE/$NAME"
export PYTHONPATH
#must be called to update commands in $PATH
hash -r 2>/dev/null
if [ "$1" == "start" ]; then
cd $BASE/$NAME
if [ -e $PID ]; then
echo openmedialibrary already running
exit 1
fi
python oml server PID &
exit $?
fi
if [ "$1" == "debug" ]; then
cd $BASE/$NAME
if [ -e $PID ]; then
echo openmedialibrary already running
exit 1
fi
shift
echo Open browser at http://$HOST
python oml server $@
exit $?
fi
if [ "$1" == "stop" ]; then
test -e $PID && kill `cat $PID`
test -e $PID && rm $PID
exit $?
fi
if [ "$1" == "restart" ]; then
if [ -e $PID ]; then
$0 stop
$0 start
exit $?
else
exit 1
fi
fi
if [ "$1" == "open" ]; then
#time to switch to python and use webbrowser.open_tab?
if [ $SYSTEM == "Darwin" ]; then
open http://$HOST/
else
xdg-open http://$HOST/
fi
exit 0
fi
cd $BASE/$NAME
python oml $@
exit $?

93
install.py Executable file
View file

@ -0,0 +1,93 @@
#!/usr/bin/env python
from __future__ import with_statement
from contextlib import closing
import json
import os
import sys
import tarfile
import urllib2
release_url = "http://downloads.openmedialibrary.com/release.json"
release_url = "http://c.local/oml/release.json"
def get_release():
with closing(urllib2.urlopen(release_url)) as u:
data = json.load(u)
return data
def download(url, filename):
dirname = os.path.dirname(filename)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
print url, filename
with open(filename, 'w') as f:
with closing(urllib2.urlopen(url)) as u:
data = u.read(4096)
while data:
f.write(data)
data = u.read(4096)
def install_launchd(base):
plist = os.path.expanduser('~/Library/LaunchAgents/com.openmedialibrary.loginscript.plist')
with open(plist, 'w') as f:
f.write('''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openmedialibrary.loginscript</string>
<key>ProgramArguments</key>
<array>
<string>%s/ctl</string>
<string>start</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>''' % base)
os.system('launchctl load "%s"' % plist)
os.system('launchctl start com.openmedialibrary.loginscript')
if __name__ == '__main__':
if len(sys.argv) == 1:
target = os.path.join(os.curdir, 'openmedialibrary')
elif len(sys.argv) != 2:
print "usage: %s target" % sys.argv[0]
sys.exit(1)
else:
target = sys.argv[1]
target = os.path.normpath(os.path.join(os.path.abspath(target)))
if not os.path.exists(target):
os.makedirs(target)
os.chdir(target)
release = get_release()
packages = ['contrib', 'openmedialibrary']
if sys.platform == 'darwin':
packages.append('platform')
for package in packages:
package_tar = '%s.tar.bz2' % package
download(release[package]['url'], package_tar)
tar = tarfile.open(package_tar)
tar.extractall()
tar.close()
os.unlink(package_tar)
os.symlink('openmedialibrary/ctl', 'ctl')
with open('release.json', 'w') as fd:
json.dump(release, fd, indent=2)
if sys.platform == 'darwin':
cmd = 'Open OpenMediaLibrary.command'
with open(cmd, 'w') as fd:
fd.write('''#!/bin/sh
cd `dirname "$0"`
./ctl start
./ctl open
''')
os.chmod(cmd, 0755)
install_launchd(target)
elif sys.platform == 'linux2':
#fixme, do only if on debian/ubuntu
os.sysrem('sudo apt-get install python-imaging python-setproctitle python-simplejson')

1
migrations/README Executable file
View file

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini Normal file
View file

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

73
migrations/env.py Normal file
View file

@ -0,0 +1,73 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

22
migrations/script.py.mako Executable file
View file

@ -0,0 +1,22 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View file

@ -0,0 +1,26 @@
"""empty message
Revision ID: 1ead68a53597
Revises: 348720abe06e
Create Date: 2014-05-11 17:12:04.427336
"""
# revision identifiers, used by Alembic.
revision = '1ead68a53597'
down_revision = '348720abe06e'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
pass
### end Alembic commands ###

View file

@ -0,0 +1,214 @@
"""empty message
Revision ID: 348720abe06e
Revises: None
Create Date: 2014-05-11 12:24:57.346130
"""
# revision identifiers, used by Alembic.
revision = '348720abe06e'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('modified', sa.DateTime(), nullable=True),
sa.Column('id', sa.String(length=43), nullable=False),
sa.Column('info', sa.PickleType(), nullable=True),
sa.Column('nickname', sa.String(length=256), nullable=True),
sa.Column('pending', sa.String(length=64), nullable=True),
sa.Column('peered', sa.Boolean(), nullable=True),
sa.Column('online', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('person',
sa.Column('name', sa.String(length=1024), nullable=False),
sa.Column('sortname', sa.String(), nullable=True),
sa.Column('numberofnames', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('name')
)
op.create_table('work',
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('modified', sa.DateTime(), nullable=True),
sa.Column('id', sa.String(length=32), nullable=False),
sa.Column('meta', sa.PickleType(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('changelog',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('user_id', sa.String(length=43), nullable=True),
sa.Column('revision', sa.BigInteger(), nullable=True),
sa.Column('data', sa.Text(), nullable=True),
sa.Column('sig', sa.String(length=96), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('list',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('position', sa.Integer(), nullable=True),
sa.Column('type', sa.String(length=64), nullable=True),
sa.Column('query', sa.PickleType(), nullable=True),
sa.Column('user_id', sa.String(length=43), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('edition',
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('modified', sa.DateTime(), nullable=True),
sa.Column('id', sa.String(length=32), nullable=False),
sa.Column('meta', sa.PickleType(), nullable=True),
sa.Column('work_id', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['work_id'], ['work.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('item',
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('modified', sa.DateTime(), nullable=True),
sa.Column('id', sa.String(length=32), nullable=False),
sa.Column('info', sa.PickleType(), nullable=True),
sa.Column('meta', sa.PickleType(), nullable=True),
sa.Column('added', sa.DateTime(), nullable=True),
sa.Column('accessed', sa.DateTime(), nullable=True),
sa.Column('timesaccessed', sa.Integer(), nullable=True),
sa.Column('transferadded', sa.DateTime(), nullable=True),
sa.Column('transferprogress', sa.Float(), nullable=True),
sa.Column('edition_id', sa.String(length=32), nullable=True),
sa.Column('work_id', sa.String(length=32), nullable=True),
sa.Column('sort_title', sa.String(length=1000), nullable=True),
sa.Column('sort_author', sa.String(length=1000), nullable=True),
sa.Column('sort_language', sa.String(length=1000), nullable=True),
sa.Column('sort_publisher', sa.String(length=1000), nullable=True),
sa.Column('sort_place', sa.String(length=1000), nullable=True),
sa.Column('sort_country', sa.String(length=1000), nullable=True),
sa.Column('sort_date', sa.String(length=1000), nullable=True),
sa.Column('sort_pages', sa.BigInteger(), nullable=True),
sa.Column('sort_classification', sa.String(length=1000), nullable=True),
sa.Column('sort_id', sa.String(length=1000), nullable=True),
sa.Column('sort_isbn10', sa.String(length=1000), nullable=True),
sa.Column('sort_isbn13', sa.String(length=1000), nullable=True),
sa.Column('sort_lccn', sa.String(length=1000), nullable=True),
sa.Column('sort_olid', sa.String(length=1000), nullable=True),
sa.Column('sort_oclc', sa.String(length=1000), nullable=True),
sa.Column('sort_extension', sa.String(length=1000), nullable=True),
sa.Column('sort_size', sa.BigInteger(), nullable=True),
sa.Column('sort_created', sa.DateTime(), nullable=True),
sa.Column('sort_added', sa.DateTime(), nullable=True),
sa.Column('sort_modified', sa.DateTime(), nullable=True),
sa.Column('sort_accessed', sa.DateTime(), nullable=True),
sa.Column('sort_timesaccessed', sa.BigInteger(), nullable=True),
sa.Column('sort_mediastate', sa.String(length=1000), nullable=True),
sa.Column('sort_transferadded', sa.DateTime(), nullable=True),
sa.Column('sort_transferprogress', sa.Float(), nullable=True),
sa.Column('sort_random', sa.BigInteger(), nullable=True),
sa.ForeignKeyConstraint(['edition_id'], ['edition.id'], ),
sa.ForeignKeyConstraint(['work_id'], ['work.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_item_sort_accessed'), 'item', ['sort_accessed'], unique=False)
op.create_index(op.f('ix_item_sort_added'), 'item', ['sort_added'], unique=False)
op.create_index(op.f('ix_item_sort_author'), 'item', ['sort_author'], unique=False)
op.create_index(op.f('ix_item_sort_classification'), 'item', ['sort_classification'], unique=False)
op.create_index(op.f('ix_item_sort_country'), 'item', ['sort_country'], unique=False)
op.create_index(op.f('ix_item_sort_created'), 'item', ['sort_created'], unique=False)
op.create_index(op.f('ix_item_sort_date'), 'item', ['sort_date'], unique=False)
op.create_index(op.f('ix_item_sort_extension'), 'item', ['sort_extension'], unique=False)
op.create_index(op.f('ix_item_sort_id'), 'item', ['sort_id'], unique=False)
op.create_index(op.f('ix_item_sort_isbn10'), 'item', ['sort_isbn10'], unique=False)
op.create_index(op.f('ix_item_sort_isbn13'), 'item', ['sort_isbn13'], unique=False)
op.create_index(op.f('ix_item_sort_language'), 'item', ['sort_language'], unique=False)
op.create_index(op.f('ix_item_sort_lccn'), 'item', ['sort_lccn'], unique=False)
op.create_index(op.f('ix_item_sort_mediastate'), 'item', ['sort_mediastate'], unique=False)
op.create_index(op.f('ix_item_sort_modified'), 'item', ['sort_modified'], unique=False)
op.create_index(op.f('ix_item_sort_oclc'), 'item', ['sort_oclc'], unique=False)
op.create_index(op.f('ix_item_sort_olid'), 'item', ['sort_olid'], unique=False)
op.create_index(op.f('ix_item_sort_pages'), 'item', ['sort_pages'], unique=False)
op.create_index(op.f('ix_item_sort_place'), 'item', ['sort_place'], unique=False)
op.create_index(op.f('ix_item_sort_publisher'), 'item', ['sort_publisher'], unique=False)
op.create_index(op.f('ix_item_sort_random'), 'item', ['sort_random'], unique=False)
op.create_index(op.f('ix_item_sort_size'), 'item', ['sort_size'], unique=False)
op.create_index(op.f('ix_item_sort_timesaccessed'), 'item', ['sort_timesaccessed'], unique=False)
op.create_index(op.f('ix_item_sort_title'), 'item', ['sort_title'], unique=False)
op.create_index(op.f('ix_item_sort_transferadded'), 'item', ['sort_transferadded'], unique=False)
op.create_index(op.f('ix_item_sort_transferprogress'), 'item', ['sort_transferprogress'], unique=False)
op.create_table('useritem',
sa.Column('user_id', sa.String(length=43), nullable=True),
sa.Column('item_id', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
)
op.create_table('find',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('item_id', sa.String(length=32), nullable=True),
sa.Column('key', sa.String(length=200), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_find_key'), 'find', ['key'], unique=False)
op.create_table('file',
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('modified', sa.DateTime(), nullable=True),
sa.Column('sha1', sa.String(length=32), nullable=False),
sa.Column('path', sa.String(length=2048), nullable=True),
sa.Column('info', sa.PickleType(), nullable=True),
sa.Column('item_id', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.PrimaryKeyConstraint('sha1')
)
op.create_table('listitem',
sa.Column('list_id', sa.Integer(), nullable=True),
sa.Column('item_id', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
sa.ForeignKeyConstraint(['list_id'], ['list.id'], )
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('listitem')
op.drop_table('file')
op.drop_index(op.f('ix_find_key'), table_name='find')
op.drop_table('find')
op.drop_table('useritem')
op.drop_index(op.f('ix_item_sort_transferprogress'), table_name='item')
op.drop_index(op.f('ix_item_sort_transferadded'), table_name='item')
op.drop_index(op.f('ix_item_sort_title'), table_name='item')
op.drop_index(op.f('ix_item_sort_timesaccessed'), table_name='item')
op.drop_index(op.f('ix_item_sort_size'), table_name='item')
op.drop_index(op.f('ix_item_sort_random'), table_name='item')
op.drop_index(op.f('ix_item_sort_publisher'), table_name='item')
op.drop_index(op.f('ix_item_sort_place'), table_name='item')
op.drop_index(op.f('ix_item_sort_pages'), table_name='item')
op.drop_index(op.f('ix_item_sort_olid'), table_name='item')
op.drop_index(op.f('ix_item_sort_oclc'), table_name='item')
op.drop_index(op.f('ix_item_sort_modified'), table_name='item')
op.drop_index(op.f('ix_item_sort_mediastate'), table_name='item')
op.drop_index(op.f('ix_item_sort_lccn'), table_name='item')
op.drop_index(op.f('ix_item_sort_language'), table_name='item')
op.drop_index(op.f('ix_item_sort_isbn13'), table_name='item')
op.drop_index(op.f('ix_item_sort_isbn10'), table_name='item')
op.drop_index(op.f('ix_item_sort_id'), table_name='item')
op.drop_index(op.f('ix_item_sort_extension'), table_name='item')
op.drop_index(op.f('ix_item_sort_date'), table_name='item')
op.drop_index(op.f('ix_item_sort_created'), table_name='item')
op.drop_index(op.f('ix_item_sort_country'), table_name='item')
op.drop_index(op.f('ix_item_sort_classification'), table_name='item')
op.drop_index(op.f('ix_item_sort_author'), table_name='item')
op.drop_index(op.f('ix_item_sort_added'), table_name='item')
op.drop_index(op.f('ix_item_sort_accessed'), table_name='item')
op.drop_table('item')
op.drop_table('edition')
op.drop_table('list')
op.drop_table('changelog')
op.drop_table('work')
op.drop_table('person')
op.drop_table('user')
### end Alembic commands ###

0
oml/__init__.py Normal file
View file

16
oml/__main__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

19
oml/item/add.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

52
oml/meta/lccn.py Normal file
View 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
View 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&ccedil;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&eacute;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&eacute;union",
"rb": "Serbia",
"rm": "Romania",
"rh": "Zimbabwe",
"-err": "Estonia",
"oru": "Oregon",
"quc": "Qu&eacute;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&ccedil;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&ocirc;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
View 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
View 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
View file

87
oml/node/api.py Normal file
View 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
View 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
View 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
View 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
View 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
View file

154
oml/oxflask/api.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

216
oml/user/api.py Normal file
View 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
View 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
View 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
View 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

5877
static/epub.js/epub.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

318
static/epub.js/hooks.js Normal file
View file

@ -0,0 +1,318 @@
EPUBJS.Hooks.register("beforeChapterDisplay").endnotes = function(callback, renderer){
var notes = renderer.contents.querySelectorAll('a[href]'),
items = Array.prototype.slice.call(notes), //[].slice.call()
attr = "epub:type",
type = "noteref",
folder = EPUBJS.core.folder(location.pathname),
cssPath = (folder + EPUBJS.cssPath) || folder,
popups = {};
EPUBJS.core.addCss(cssPath + "popup.css", false, renderer.render.document.head);
items.forEach(function(item){
var epubType = item.getAttribute(attr),
href,
id,
el,
pop,
pos,
left,
top,
txt;
if(epubType != type) return;
href = item.getAttribute("href");
id = href.replace("#", '');
el = renderer.render.document.getElementById(id);
item.addEventListener("mouseover", showPop, false);
item.addEventListener("mouseout", hidePop, false);
function showPop(){
var poppos,
iheight = renderer.height,
iwidth = renderer.width,
tip,
pop,
maxHeight = 225,
itemRect;
if(!txt) {
pop = el.cloneNode(true);
txt = pop.querySelector("p");
}
// chapter.replaceLinks.bind(this) //TODO:Fred - update?
//-- create a popup with endnote inside of it
if(!popups[id]) {
popups[id] = document.createElement("div");
popups[id].setAttribute("class", "popup");
pop_content = document.createElement("div");
popups[id].appendChild(pop_content);
pop_content.appendChild(txt);
pop_content.setAttribute("class", "pop_content");
renderer.render.document.body.appendChild(popups[id]);
//-- TODO: will these leak memory? - Fred
popups[id].addEventListener("mouseover", onPop, false);
popups[id].addEventListener("mouseout", offPop, false);
//-- Add hide on page change
// chapter.book.listenUntil("book:pageChanged", "book:chapterDestroy", hidePop);
// chapter.book.listenUntil("book:pageChanged", "book:chapterDestroy", offPop);
renderer.on("renderer:pageChanged", hidePop, this);
renderer.on("renderer:pageChanged", offPop, this);
// chapter.book.on("renderer:chapterDestroy", hidePop, this);
}
pop = popups[id];
//-- get location of item
itemRect = item.getBoundingClientRect();
left = itemRect.left;
top = itemRect.top;
//-- show the popup
pop.classList.add("show");
//-- locations of popup
popRect = pop.getBoundingClientRect();
//-- position the popup
pop.style.left = left - popRect.width / 2 + "px";
pop.style.top = top + "px";
//-- Adjust max height
if(maxHeight > iheight / 2.5) {
maxHeight = iheight / 2.5;
pop_content.style.maxHeight = maxHeight + "px";
}
//-- switch above / below
if(popRect.height + top >= iheight - 25) {
pop.style.top = top - popRect.height + "px";
pop.classList.add("above");
}else{
pop.classList.remove("above");
}
//-- switch left
if(left - popRect.width <= 0) {
pop.style.left = left + "px";
pop.classList.add("left");
}else{
pop.classList.remove("left");
}
//-- switch right
if(left + popRect.width / 2 >= iwidth) {
//-- TEMP MOVE: 300
pop.style.left = left - 300 + "px";
popRect = pop.getBoundingClientRect();
pop.style.left = left - popRect.width + "px";
//-- switch above / below again
if(popRect.height + top >= iheight - 25) {
pop.style.top = top - popRect.height + "px";
pop.classList.add("above");
}else{
pop.classList.remove("above");
}
pop.classList.add("right");
}else{
pop.classList.remove("right");
}
}
function onPop(){
popups[id].classList.add("on");
}
function offPop(){
popups[id].classList.remove("on");
}
function hidePop(){
setTimeout(function(){
popups[id].classList.remove("show");
}, 100);
}
});
if(callback) callback();
}
EPUBJS.Hooks.register("beforeChapterDisplay").mathml = function(callback, renderer){
// check of currentChapter properties contains 'mathml'
if(renderer.currentChapter.manifestProperties.indexOf("mathml") !== -1 ){
// Assign callback to be inside iframe window
renderer.iframe.contentWindow.mathmlCallback = callback;
// add MathJax config script tag to the renderer body
var s = document.createElement("script");
s.type = 'text/x-mathjax-config';
s.innerHTML = '\
MathJax.Hub.Register.StartupHook("End",function () { \
window.mathmlCallback(); \
});\
MathJax.Hub.Config({jax: ["input/TeX","input/MathML","output/SVG"],extensions: ["tex2jax.js","mml2jax.js","MathEvents.js"],TeX: {extensions: ["noErrors.js","noUndefined.js","autoload-all.js"]},MathMenu: {showRenderer: false},menuSettings: {zoom: "Click"},messageStyle: "none"}); \
';
renderer.doc.body.appendChild(s);
// add MathJax.js to renderer head
EPUBJS.core.addScript("http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML", null, renderer.doc.head);
} else {
if(callback) callback();
}
}
EPUBJS.Hooks.register("beforeChapterDisplay").smartimages = function(callback, renderer){
var images = renderer.contents.querySelectorAll('img'),
items = Array.prototype.slice.call(images),
iheight = renderer.height,//chapter.bodyEl.clientHeight,//chapter.doc.body.getBoundingClientRect().height,
oheight;
if(renderer.layoutSettings.layout != "reflowable") {
callback();
return; //-- Only adjust images for reflowable text
}
items.forEach(function(item){
function size() {
var itemRect = item.getBoundingClientRect(),
rectHeight = itemRect.height,
top = itemRect.top,
oHeight = item.getAttribute('data-height'),
height = oHeight || rectHeight,
newHeight,
fontSize = Number(getComputedStyle(item, "").fontSize.match(/(\d*(\.\d*)?)px/)[1]),
fontAdjust = fontSize ? fontSize / 2 : 0;
iheight = renderer.contents.clientHeight;
if(top < 0) top = 0;
if(height + top >= iheight) {
if(top < iheight/2) {
// Remove top and half font-size from height to keep container from overflowing
newHeight = iheight - top - fontAdjust;
item.style.maxHeight = newHeight + "px";
item.style.width= "auto";
}else{
if(height > iheight) {
item.style.maxHeight = iheight + "px";
item.style.width= "auto";
itemRect = item.getBoundingClientRect();
height = itemRect.height;
}
item.style.display = "block";
item.style["WebkitColumnBreakBefore"] = "always";
item.style["breakBefore"] = "column";
}
item.setAttribute('data-height', newHeight);
}else{
item.style.removeProperty('max-height');
item.style.removeProperty('margin-top');
}
}
item.addEventListener('load', size, false);
renderer.on("renderer:resized", size);
renderer.on("renderer:chapterUnloaded", function(){
item.removeEventListener('load', size);
renderer.off("renderer:resized", size);
});
size();
});
if(callback) callback();
}
EPUBJS.Hooks.register("beforeChapterDisplay").transculsions = function(callback, renderer){
/*
<aside ref="http://www.youtube.com/embed/DUL6MBVKVLI?html5=1" transclusion="video" width="560" height="315">
<a href="http://www.youtube.com/embed/DUL6MBVKVLI"> Watch the National Geographic: The Last Roll of Kodachrome</a>
</aside>
*/
var trans = renderer.contents.querySelectorAll('[transclusion]'),
items = Array.prototype.slice.call(trans);
items.forEach(function(item){
var src = item.getAttribute("ref"),
iframe = document.createElement('iframe'),
orginal_width = item.getAttribute("width"),
orginal_height = item.getAttribute("height"),
parent = item.parentNode,
width = orginal_width,
height = orginal_height,
ratio;
function size() {
width = orginal_width;
height = orginal_height;
if(width > chapter.colWidth){
ratio = chapter.colWidth / width;
width = chapter.colWidth;
height = height * ratio;
}
iframe.width = width;
iframe.height = height;
}
size();
//-- resize event
renderer.listenUntil("renderer:resized", "renderer:chapterUnloaded", size);
iframe.src = src;
//<iframe width="560" height="315" src="http://www.youtube.com/embed/DUL6MBVKVLI" frameborder="0" allowfullscreen="true"></iframe>
parent.replaceChild(iframe, item);
});
if(callback) callback();
}
//# sourceMappingURL=hooks.js.map

View file

@ -0,0 +1,12 @@
{
"version": 3,
"file": "hooks.js",
"sources": [
"hooks/default/endnotes.js",
"hooks/default/mathml.js",
"hooks/default/smartimages.js",
"hooks/default/transculsions.js"
],
"names": [],
"mappings": "AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1116
static/epub.js/reader.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

103
static/html/epub.html Normal file
View file

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>epub.js</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- EPUBJS Renderer -->
<script src="/static/epub.js/epub.min.js"></script>
<style type="text/css">
body {
overflow: hidden;
background: #fff;
}
#main {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
#area {
position: absolute;
right: 44px;
left: 44px;
top: 32px;
bottom: 32px;
}
#area iframe {
border: none;
}
#prev {
top: 0;
left: 0;
width: 36px;
bottom: 0;
}
#next {
right: 0;
top: 0;
width: 36px;
bottom: 0;
}
.arrow {
position: absolute;
}
.arrow:hover {
background: #eee;
}
.arrow:active {
background: #fff;
}
</style>
<script>
"use strict";
function bookUrl() {
return document.location.pathname.replace(/\/reader\//, '/epub/');
}
var Book = ePub({store: false});
Book.open(bookUrl());
Book.getMetadata().then(function(meta) {
document.title = meta.bookTitle + " " + meta.creator;
document.addEventListener("keydown", function(event) {
if (event.keyCode == 39 || event.keyCode == 40) {
Book.nextPage();
} else if (event.keyCode == 37 || event.keyCode == 38) {
Book.prevPage();
}
}, false);
});
</script>
</head>
<body>
<div id="main">
<div id="prev" onclick="Book.prevPage();" class="arrow"></div>
<div id="area"></div>
<div id="next" onclick="Book.nextPage();"class="arrow"></div>
</div>
<script>
Book.renderTo("area");
</script>
</body>
</html>

11
static/html/oml.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Open Media Library</title>
<meta charset="UTF-8"/>
<link href="/static/png/oml.png" rel="icon" type="image/png">
<script src="/static/js/oml.js?1" type="text/javascript"></script>
<meta name="google" value="notranslate"/>
</head>
<body></body>
</html>

355
static/html/pdf.html Normal file
View file

@ -0,0 +1,355 @@
<!DOCTYPE html>
<!--
Copyright 2012 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html dir="ltr" mozdisallowselectionprint moznomarginboxes>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="google" content="notranslate">
<title></title>
<link rel="stylesheet" href="/static/pdf.js/viewer.css"/>
<script type="text/javascript" src="/static/oxjs/build/Ox.js"></script>
<script type="text/javascript" src="/static/oxjs/source/Ox.UI/js/Core/Message.js"></script>
<script type="text/javascript" src="/static/pdf.js/compatibility.js"></script>
<!-- This snippet is used in production, see Makefile -->
<link rel="resource" type="application/l10n" href="/static/pdf.js/locale/locale.properties"/>
<script type="text/javascript" src="/static/pdf.js/l10n.js"></script>
<script type="text/javascript" src="/static/pdf.js/pdf.js"></script>
<script type="text/javascript" src="/static/pdf.js/debugger.js"></script>
<script type="text/javascript" src="/static/pdf.js/embeds.js"></script>
<script type="text/javascript" src="/static/pdf.js/viewer.js"></script>
<link rel="stylesheet" type="text/css" href="/static/pdf.js/css/videopdf.css"/>
</head>
<body tabindex="1">
<div id="outerContainer" class="loadingInProgress">
<div id="sidebarContainer">
<div id="toolbarSidebar">
<div class="splitToolbarButton toggled">
<button id="viewThumbnail" class="toolbarButton group toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs">
<span data-l10n-id="thumbs_label">Thumbnails</span>
</button>
<button id="viewOutline" class="toolbarButton group" title="Show Document Outline" tabindex="3" data-l10n-id="outline">
<span data-l10n-id="outline_label">Document Outline</span>
</button>
</div>
</div>
<div id="sidebarContent">
<div id="thumbnailView">
</div>
<div id="outlineView" class="hidden">
</div>
</div>
</div> <!-- sidebarContainer -->
<div id="mainContainer">
<div class="findbar hidden doorHanger hiddenSmallView" id="findbar">
<label for="findInput" class="toolbarLabel" data-l10n-id="find_label">Find:</label>
<input id="findInput" class="toolbarField" tabindex="41">
<div class="splitToolbarButton">
<button class="toolbarButton findPrevious" title="" id="findPrevious" tabindex="42" data-l10n-id="find_previous">
<span data-l10n-id="find_previous_label">Previous</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button class="toolbarButton findNext" title="" id="findNext" tabindex="43" data-l10n-id="find_next">
<span data-l10n-id="find_next_label">Next</span>
</button>
</div>
<input type="checkbox" id="findHighlightAll" class="toolbarField">
<label for="findHighlightAll" class="toolbarLabel" tabindex="44" data-l10n-id="find_highlight">Highlight all</label>
<input type="checkbox" id="findMatchCase" class="toolbarField">
<label for="findMatchCase" class="toolbarLabel" tabindex="45" data-l10n-id="find_match_case_label">Match case</label>
<span id="findMsg" class="toolbarLabel"></span>
</div> <!-- findbar -->
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
<div id="secondaryToolbarButtonContainer">
<button id="secondaryPresentationMode" class="secondaryToolbarButton presentationMode visibleLargeView" title="Switch to Presentation Mode" tabindex="18" data-l10n-id="presentation_mode">
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
</button>
<button id="secondaryOpenFile" class="secondaryToolbarButton openFile visibleLargeView" title="Open File" tabindex="19" data-l10n-id="open_file">
<span data-l10n-id="open_file_label">Open</span>
</button>
<button id="secondaryPrint" class="secondaryToolbarButton print visibleMediumView" title="Print" tabindex="20" data-l10n-id="print">
<span data-l10n-id="print_label">Print</span>
</button>
<button id="secondaryDownload" class="secondaryToolbarButton download visibleMediumView" title="Download" tabindex="21" data-l10n-id="download">
<span data-l10n-id="download_label">Download</span>
</button>
<a href="#" id="secondaryViewBookmark" class="secondaryToolbarButton bookmark visibleSmallView" title="Current view (copy or open in new window)" tabindex="22" data-l10n-id="bookmark">
<span data-l10n-id="bookmark_label">Current View</span>
</a>
<div class="horizontalToolbarSeparator visibleLargeView"></div>
<button id="firstPage" class="secondaryToolbarButton firstPage" title="Go to First Page" tabindex="23" data-l10n-id="first_page">
<span data-l10n-id="first_page_label">Go to First Page</span>
</button>
<button id="lastPage" class="secondaryToolbarButton lastPage" title="Go to Last Page" tabindex="24" data-l10n-id="last_page">
<span data-l10n-id="last_page_label">Go to Last Page</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<button id="pageRotateCw" class="secondaryToolbarButton rotateCw" title="Rotate Clockwise" tabindex="25" data-l10n-id="page_rotate_cw">
<span data-l10n-id="page_rotate_cw_label">Rotate Clockwise</span>
</button>
<button id="pageRotateCcw" class="secondaryToolbarButton rotateCcw" title="Rotate Counterclockwise" tabindex="26" data-l10n-id="page_rotate_ccw">
<span data-l10n-id="page_rotate_ccw_label">Rotate Counterclockwise</span>
</button>
<div class="horizontalToolbarSeparator"></div>
<button id="toggleHandTool" class="secondaryToolbarButton handTool" title="Enable hand tool" tabindex="27" data-l10n-id="hand_tool_enable">
<span data-l10n-id="hand_tool_enable_label">Enable hand tool</span>
</button>
</div>
</div> <!-- secondaryToolbar -->
<div class="toolbar">
<div id="toolbarContainer">
<div id="toolbarViewer">
<div id="toolbarViewerLeft">
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="4" data-l10n-id="toggle_sidebar">
<span data-l10n-id="toggle_sidebar_label">Toggle Sidebar</span>
</button>
<div class="toolbarButtonSpacer"></div>
<button id="viewFind" class="toolbarButton group hiddenSmallView" title="Find in Document" tabindex="5" data-l10n-id="findbar">
<span data-l10n-id="findbar_label">Find</span>
</button>
<div class="splitToolbarButton">
<button class="toolbarButton pageUp" title="Previous Page" id="previous" tabindex="6" data-l10n-id="previous">
<span data-l10n-id="previous_label">Previous</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button class="toolbarButton pageDown" title="Next Page" id="next" tabindex="7" data-l10n-id="next">
<span data-l10n-id="next_label">Next</span>
</button>
</div>
<label id="pageNumberLabel" class="toolbarLabel" for="pageNumber" data-l10n-id="page_label">Page: </label>
<input type="number" id="pageNumber" class="toolbarField pageNumber" value="1" size="4" min="1" tabindex="8">
<span id="numPages" class="toolbarLabel"></span>
</div>
<div id="toolbarViewerRight">
<button id="presentationMode" class="toolbarButton presentationMode hiddenLargeView" title="Switch to Presentation Mode" tabindex="12" data-l10n-id="presentation_mode">
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
</button>
<button id="openFile" class="toolbarButton openFile hiddenLargeView" title="Open File" tabindex="13" data-l10n-id="open_file">
<span data-l10n-id="open_file_label">Open</span>
</button>
<button id="print" class="toolbarButton print hiddenMediumView" title="Print" tabindex="14" data-l10n-id="print">
<span data-l10n-id="print_label">Print</span>
</button>
<button id="download" class="toolbarButton download hiddenMediumView" title="Download" tabindex="15" data-l10n-id="download">
<span data-l10n-id="download_label">Download</span>
</button>
<!-- <div class="toolbarButtonSpacer"></div> -->
<a href="#" id="viewBookmark" class="toolbarButton bookmark hiddenSmallView" title="Current view (copy or open in new window)" tabindex="16" data-l10n-id="bookmark">
<span data-l10n-id="bookmark_label">Current View</span>
</a>
<div class="verticalToolbarSeparator hiddenSmallView"></div>
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="17" data-l10n-id="tools">
<span data-l10n-id="tools_label">Tools</span>
</button>
</div>
<div class="outerCenter">
<div class="innerCenter" id="toolbarViewerMiddle">
<div class="splitToolbarButton">
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out" tabindex="9" data-l10n-id="zoom_out">
<span data-l10n-id="zoom_out_label">Zoom Out</span>
</button>
<div class="splitToolbarButtonSeparator"></div>
<button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In" tabindex="10" data-l10n-id="zoom_in">
<span data-l10n-id="zoom_in_label">Zoom In</span>
</button>
</div>
<span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" title="Zoom" tabindex="11" data-l10n-id="zoom">
<option id="pageAutoOption" value="auto" selected="selected" data-l10n-id="page_scale_auto">Automatic Zoom</option>
<option id="pageActualOption" value="page-actual" data-l10n-id="page_scale_actual">Actual Size</option>
<option id="pageFitOption" value="page-fit" data-l10n-id="page_scale_fit">Fit Page</option>
<option id="pageWidthOption" value="page-width" data-l10n-id="page_scale_width">Full Width</option>
<option id="customScaleOption" value="custom"></option>
<option value="0.5">50%</option>
<option value="0.75">75%</option>
<option value="1">100%</option>
<option value="1.25">125%</option>
<option value="1.5">150%</option>
<option value="2">200%</option>
</select>
</span>
</div>
</div>
</div>
<div id="loadingBar">
<div class="progress">
<div class="glimmer">
</div>
</div>
</div>
</div>
</div>
<menu type="context" id="viewerContextMenu">
<menuitem id="contextFirstPage" label="First Page"
data-l10n-id="first_page"></menuitem>
<menuitem id="contextLastPage" label="Last Page"
data-l10n-id="last_page"></menuitem>
<menuitem id="contextPageRotateCw" label="Rotate Clockwise"
data-l10n-id="page_rotate_cw"></menuitem>
<menuitem id="contextPageRotateCcw" label="Rotate Counter-Clockwise"
data-l10n-id="page_rotate_ccw"></menuitem>
</menu>
<div id="viewerContainer" tabindex="0">
<div id="viewer"></div>
</div>
<div id="errorWrapper" hidden='true'>
<div id="errorMessageLeft">
<span id="errorMessage"></span>
<button id="errorShowMore" data-l10n-id="error_more_info">
More Information
</button>
<button id="errorShowLess" data-l10n-id="error_less_info" hidden='true'>
Less Information
</button>
</div>
<div id="errorMessageRight">
<button id="errorClose" data-l10n-id="error_close">
Close
</button>
</div>
<div class="clearBoth"></div>
<textarea id="errorMoreInfo" hidden='true' readonly="readonly"></textarea>
</div>
</div> <!-- mainContainer -->
<div id="overlayContainer" class="hidden">
<div id="promptContainer">
<div id="passwordContainer" class="prompt doorHanger">
<div class="row">
<p id="passwordText" data-l10n-id="password_label">Enter the password to open this PDF file:</p>
</div>
<div class="row">
<input type="password" id="password" class="toolbarField" />
</div>
<div class="row">
<button id="passwordCancel" class="promptButton"><span data-l10n-id="password_cancel">Cancel</span></button>
<button id="passwordSubmit" class="promptButton"><span data-l10n-id="password_ok">OK</span></button>
</div>
</div>
</div>
</div>
</div> <!-- outerContainer -->
<div id="printContainer"></div>
<div id="mozPrintCallback-shim" hidden>
<style scoped>
#mozPrintCallback-shim {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 9999999;
display: block;
text-align: center;
background-color: rgba(0, 0, 0, 0.5);
}
#mozPrintCallback-shim[hidden] {
display: none;
}
@media print {
#mozPrintCallback-shim {
display: none;
}
}
#mozPrintCallback-shim .mozPrintCallback-dialog-box {
display: inline-block;
margin: -50px auto 0;
position: relative;
top: 45%;
left: 0;
min-width: 220px;
max-width: 400px;
padding: 9px;
border: 1px solid hsla(0, 0%, 0%, .5);
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
background-color: #474747;
color: hsl(0, 0%, 85%);
font-size: 16px;
line-height: 20px;
}
#mozPrintCallback-shim .progress-row {
clear: both;
padding: 1em 0;
}
#mozPrintCallback-shim progress {
width: 100%;
}
#mozPrintCallback-shim .relative-progress {
clear: both;
float: right;
}
#mozPrintCallback-shim .progress-actions {
clear: both;
}
</style>
<div class="mozPrintCallback-dialog-box">
<!-- TODO: Localise the following strings -->
Preparing document for printing...
<div class="progress-row">
<progress value="0" max="100"></progress>
<span class="relative-progress">0%</span>
</div>
<div class="progress-actions">
<input type="button" value="Cancel" class="mozPrintCallback-cancel">
</div>
</div>
</div>
</body>
</html>

12
static/html/txt.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="/static/oxjs/build/Ox.js" type="text/javascript"></script>
<script src="/static/txt.js/txt.js" type="text/javascript"></script>
<script>
txtjs.open(document.location.pathname.replace(/\/reader\//, '/txt/'));
</script>
</head>
<body></body>
</html>

41
static/js/Preferences.js Normal file
View file

@ -0,0 +1,41 @@
'use strict';
oml.Preferences = (function() {
var that = {};
that.set = function() {
var args = Ox.isObject(arguments[0])
? args
: Ox.makeObject([arguments[0], arguments[1]]),
set = {},
preferences = oml.user.preferences,
previousPreferences = Ox.clone(preferences, true);
Ox.forEach(args, function(value, key) {
if (!Ox.isEqual(preferences[key], value)) {
preferences[key] = value;
set[key] = value;
}
});
if (Ox.len(set)) {
oml.api.setPreferences(set);
Ox.forEach(set, function(value, key) {
Ox.forEach(oml.$ui, function($element) {
if (Ox.UI.isElement($element)) {
$element.triggerEvent('oml_' + key.toLowerCase(), {
value: value,
previousValue: previousPreferences[key]
});
}
});
});
}
};
return that;
}());

181
static/js/UI.js Normal file
View file

@ -0,0 +1,181 @@
'use strict';
oml.UI = (function() {
var previousUI = {},
that = {};
that.encode = function(value) {
return value.replace(/\./g, '\\.');
};
that.reset = function() {
var ui = oml.user.ui;
oml.api.resetUI({}, function() {
ui = oml.config.user.ui;
ui._list = oml.getListState(ui.find);
ui._filterState = oml.getFilterState(ui.find);
ui._findState = oml.getFindState(ui.find);
Ox.Theme(ui.theme);
oml.$ui.appPanel.reload();
});
};
// sets oml.user.ui.key to value
// key foo.bar.baz sets oml.user.ui.foo.bar.baz
// value null removes a key
that.set = function(/* {key: value}[, flag] or key, value[, flag] */) {
var add = {},
args,
item,
list,
listSettings = oml.config.listSettings,
listView,
set = {},
trigger = {},
triggerEvents,
ui = oml.user.ui;
if (Ox.isObject(arguments[0])) {
args = arguments[0];
triggerEvents = Ox.isUndefined(arguments[1]) ? true : arguments[1];
} else {
args = Ox.makeObject([arguments[0], arguments[1]]);
triggerEvents = Ox.isUndefined(arguments[2]) ? true : arguments[1];
}
Ox.print('UI SET', JSON.stringify(args));
previousUI = Ox.clone(ui, true);
previousUI._list = oml.getListState(previousUI.find);
if ('find' in args) {
// the challenge here is that find may change list,
// and list may then change listSort and listView,
// which we don't want to trigger, since find triggers
// (values we put in add will be changed, but won't trigger)
list = oml.getListState(args.find);
ui._list = list;
ui._filterState = oml.getFilterState(args.find);
ui._findState = oml.getFindState(args.find);
if (oml.$ui.appPanel && !oml.stayInItemView) {
// if we're not on page load, and if find isn't a context change
// caused by an edit, then switch from item view to list view
args.item = '';
}
if (list != previousUI._list) {
// if find has changed list
Ox.forEach(listSettings, function(listSetting, setting) {
// then for each setting that corresponds to a list setting
if (!ui.lists[list]) {
// either add the default setting
add[setting] = oml.config.user.ui[setting];
} else {
// or the existing list setting
add[setting] = ui.lists[list][listSetting]
}
});
} else {
list = previousUI._list;
}
// it is important to check for find first, so that
// if find changes list, list is correct here
item = args.item || ui.item;
listView = add.listView || args.listView;
if (!ui.lists[list]) {
add['lists.' + that.encode(list)] = {};
}
Ox.forEach(listSettings, function(listSetting, setting) {
// for each setting that corresponds to a list setting
// set that list setting to
var key = 'lists.' + that.encode(list) + '.' + listSetting;
if (setting in args) {
// the setting passed to UI.set
add[key] = args[setting];
} else if (setting in add) {
// or the setting changed via find
add[key] = add[setting];
} else if (!ui.lists[list]) {
// or the default setting
add[key] = oml.config.user.ui[setting];
}
});
if (args.item) {
// when switching to an item, update list selection
add['listSelection'] = [args.item];
add['lists.' + that.encode(list) + '.selection'] = [args.item];
if (
!args.itemView
&& ui.itemView == 'book'
&& !ui.mediaState[item]
&& !args['mediaState.' + item]
) {
// if the item view doesn't change, remains a media view,
// media state doesn't exist yet, and won't be set, add
// default media state
add['mediaState.' + item] = {position: 0, zoom: 1};
}
}
if (
args.itemView == 'book'
&& !ui.mediaState[item]
&& !args['mediaState.' + item]
) {
// when switching to a media view, media state doesn't exist
// yet, and won't be set, add default media state
add['mediaState.' + item] = {position: 0, zoom: 1};
}
}
// items in args trigger events, items in add do not
[args, add].forEach(function(object, isAdd) {
Ox.forEach(object, function(value, key) {
// make sure to not split at escaped dots ('\.')
var keys = key.replace(/\\\./g, '\n').split('.').map(function(key) {
return key.replace(/\n/g, '.');
}),
ui_ = ui;
while (keys.length > 1) {
ui_ = ui_[keys.shift()];
}
if (!Ox.isEqual(ui_[keys[0]], value)) {
if (value === null) {
delete ui_[keys[0]];
} else {
ui_[keys[0]] = value;
}
set[key] = value;
if (!isAdd) {
trigger[key] = value;
}
}
});
});
if (Ox.len(set)) {
oml.api.setUI(set);
}
if (triggerEvents) {
Ox.forEach(trigger, function(value, key) {
Ox.print('UI TRIGGER', 'oml_' + key.toLowerCase(), value);
Ox.forEach(oml.$ui, function($elements) {
Ox.makeArray($elements).forEach(function($element) {
$element.triggerEvent('oml_' + key.toLowerCase(), {
value: value,
previousValue: previousUI[key]
});
});
});
});
}
oml.URL.update(Object.keys(!oml.$ui.appPanel ? args : trigger));
};
return that;
}());

349
static/js/URL.js Normal file
View file

@ -0,0 +1,349 @@
'use strict';
oml.URL = (function() {
var self = {}, that = {};
function getHash(state, callback) {
callback();
}
function getItem(state, string, callback) {
oml.api.get({id: string, keys: ['id']}, function(result) {
if (result.status.code == 200) {
state.item = result.data.id;
}
callback();
});
}
function getPart(state, string, callback) {
var parts = Ox.getObjectById(oml.config.pages, state.page).parts || [];
if (Ox.contains(parts, string)) {
state.part = string;
}
callback();
}
function getSort(state, value, callback) {
callback();
}
function getSpan(state, value, callback) {
callback();
}
// translates UI settings to URL state
function getState() {
var state = {},
ui = oml.user.ui;
if (ui.page) {
state.page = ui.page;
if (Ox.contains(Object.keys(oml.config.user.ui.part), state.page)) {
state.part = ui.part[state.page];
}
} else {
state.type = ui.section;
state.item = ui.item;
if (ui.section == 'books') {
if (!ui.item) {
state.view = ui.listView;
state.sort = [ui.listSort[0]];
state.find = ui.find;
} else {
state.view = ui.itemView;
if (ui.itemView == 'book') {
state.span = ui.mediaState[state.item] || [0, 1];
}
}
}
}
return state;
}
function getURLOptions() {
var sortKeys = {},
ui = oml.user.ui,
views = {};
views['books'] = {
// ui.listView is the default view
list: [ui.listView].concat(
oml.config.listViews.filter(function(view) {
return view.id != ui.listView;
}).map(function(view) {
return view.id;
})
),
// ui.itemView is the default view,
item: [ui.itemView].concat(
oml.config.itemViews.filter(function(view) {
return view.id != ui.itemView;
}).map(function(view) {
return view.id;
})
)
};
sortKeys['books'] = {list: {}, item: {}};
views['books'].list.forEach(function(view) {
sortKeys['books'].list[view] = [].concat(
// ui.listSort[0].key is the default sort key
Ox.getObjectById(oml.config.sortKeys, ui.listSort[0].key),
oml.config.sortKeys.filter(function(key) {
return key.id != ui.listSort[0].key;
})
);
});
return {
findKeys: [{id: 'list', type: 'string'}].concat(
oml.config.itemKeys
),
pages: oml.config.pages.map(function(page) {
return page.id;
}),
spanType: {
books: {
list: {},
item: {
book: 'FIXME, no idea'
}
}
},
sortKeys: sortKeys,
types: ['books'],
views: views
};
}
// translates URL state to UI settings
function setState(state, callback) {
var set = {},
ui = oml.user.ui;
ui._list = oml.getListState(ui.find);
ui._filterState = oml.getFilterState(ui.find);
ui._findState = oml.getFindState(ui.find);
if (Ox.isEmpty(state)) {
callback && callback();
} else {
if (state.page) {
set.page = state.page;
if (
Ox.contains(Object.keys(oml.config.user.ui.part), state.page)
&& state.part
) {
set['part.' + state.page] = state.part;
}
oml.UI.set(set);
callback && callback();
} else {
set.page = '';
if (state.type) {
set.section = state.type;
set.item = state.item;
}
if (set.section == 'books') {
if (state.view) {
set[!state.item ? 'listView' : 'itemView'] = state.view;
}
if (state.sort) {
set[!state.item ? 'listSort' : 'itemSort'] = state.sort;
}
if (state.span) {
if (state.view == 'book') {
set['mediaState.' + state.item] = {
position: state.span[0],
zoom: state.span[1]
};
}
}
if (!state.item) {
if (state.find) {
set.find = state.find;
} else if (!oml.$ui.appPanel) {
// when loading results without find, clear find, so that
// removing a query and reloading works as expected
set.find = oml.config.user.ui.find;
}
}
}
Ox.Request.cancel();
if (!oml.$ui.appPanel && state.item && ui.find) {
// on page load, if item is set and there was a query,
// we have to check if the item actually matches the query,
// and otherwise reset find
oml.api.find({
query: ui.find,
positions: [state.item],
sort: [{key: 'id', operator: ''}]
}, function(result) {
if (Ox.isUndefined(result.data.positions[state.item])) {
set.find = oml.config.user.ui.find
}
oml.UI.set(set);
callback && callback();
});
} else {
oml.UI.set(set);
callback && callback();
}
}
}
}
that.init = function() {
self.URL = Ox.URL(Ox.extend({
getHash: getHash,
getItem: getItem,
getPart: getPart,
getSort: getSort,
getSpan: getSpan,
}, getURLOptions()));
window.addEventListener('hashchange', function() {
Ox.Request.cancel();
that.parse();
});
window.addEventListener('popstate', function(e) {
Ox.Request.cancel();
self.isPopState = true;
$('.OxDialog:visible').each(function() {
Ox.UI.elements[$(this).data('oxid')].close();
});
if (e.state && !Ox.isEmpty(e.state)) {
document.title = Ox.decodeHTMLEntities(e.state.title);
setState(e.state);
} else {
that.parse();
}
});
return that;
};
// on page load, this sets the state from the URL
// can also be used to parse a URL
that.parse = function(url, callback) {
if (arguments.length == 2) {
self.URL.parse(url, callback);
} else {
callback = arguments[0];
url = null;
if (document.location.pathname.slice(0, 4) == 'url=') {
document.location.href = Ox.decodeURI(document.location.pathname.slice(4));
} else {
self.URL.parse(function(state) {
// setState -> UI.set -> URL.update
setState(state, callback);
});
}
}
return that;
};
// sets the URL to the previous URL
that.pop = function() {
self.URL.pop() || that.update();
return that;
};
// pushes a new URL (as string or from state)
that.push = function(stateOrURL, expandURL) {
var state,
title = oml.getPageTitle(stateOrURL)
url;
oml.replaceURL = expandURL;
if (Ox.isObject(stateOrURL)) {
state = stateOrURL;
} else {
url = stateOrURL;
}
self.URL.push(state, title, url, setState);
return that;
};
// replaces the current URL (as string or from state)
that.replace = function(stateOrURL, title) {
var state,
title = oml.getPageTitle(stateOrURL)
url;
if (Ox.isObject(stateOrURL)) {
state = stateOrURL;
} else {
url = stateOrURL;
}
self.URL.push(state, title, url, setState);
return that;
};
// this gets called from oml.UI
that.update = function(keys) {
var action, state;
if (keys.some(function(key) {
return Ox.contains(['itemView', 'listSort', 'listView'], key);
})) {
self.URL.options(getURLOptions());
}
if (self.isPopState) {
self.isPopState = false;
} else {
if (
!oml.$ui.appPanel
|| oml.replaceURL
|| keys.every(function(key) {
return Ox.contains([
'listColumnWidth', 'listColumns', 'listSelection'
], key) || /^mediaState/.test(key);
})
) {
action = 'replace';
} else {
action = 'push';
}
state = getState();
self.URL[action](
state,
oml.getPageTitle(state)
);
oml.replaceURL = false;
}
};
return that;
}());

69
static/js/allItems.js Normal file
View file

@ -0,0 +1,69 @@
'use strict';
oml.ui.allItems = function(user) {
var ui = oml.user.ui,
that = Ox.TableList({
columns: [
{
format: function() {
return $('<img>')
.attr({
src: Ox.UI.getImageURL(user ? 'symbolUser' : 'symbolData')
})
.css({
width: '10px',
height: '10px',
margin: '2px -2px 2px 0'
});
},
id: 'id',
title: 'ID',
visible: true,
width: 16
},
{
id: 'title',
title: 'Title',
visible: true,
width: ui.sidebarSize - 58,
},
{
align: 'right',
format: function(value) {
return value > -1
? '<span class="OxLight">'
+ Ox.formatNumber(value)
+ '</span>'
: '';
},
id: 'items',
title: 'Items',
visible: true,
width: 42
}
],
items: [
{
id: '',
title: Ox._(user ? 'Library' : 'All Libraries'),
items: -1
}
],
sort: [{key: 'id', operator: '+'}],
selected: [],
unique: 'id'
})
.css({
width: ui.sidebarSize + 'px',
height: '16px'
});
that.resizeElement = function() {
// ...
};
return that;
};

108
static/js/appDialog.js Normal file
View file

@ -0,0 +1,108 @@
'use strict';
oml.ui.appDialog = function() {
var ui = oml.user.ui,
tabs = Ox.getObjectById(oml.config.pages, 'app').parts.map(function(tab) {
return {
id: tab.id,
title: tab.title.replace(/ Open Media Library$/, ''),
selected: tab.id == ui.part.app
};
}),
$panel = Ox.TabPanel({
content: function(id) {
var $logo = Ox.Element(),
$text = Ox.Element()
.addClass('OxTextPage'),
title = Ox.getObjectById(
Ox.getObjectById(oml.config.pages, 'app').parts,
id
).title;
$('<img>')
.attr({
src: '/static/png/oml.png'
})
.css({
position: 'absolute',
left: '16px',
top: '16px',
width: '192px',
height: '192px'
})
.appendTo($logo);
$('<div>')
.css({
position: 'absolute',
left: '16px',
right: '24px',
top: '24px',
overflowY: 'auto'
})
.html(
'<h1><b>' + title + '</b></h1>'
+ '<p>The lazy brown fox jumped over the lazy black fox, but otherwise not really much happened here since you last checked.'
)
.appendTo($text);
return Ox.SplitPanel({
elements: [
{
element: $logo,
size: 208
},
{
element: $text
}
],
orientation: 'horizontal'
});
},
tabs: tabs
})
.bindEvent({
change: function(data) {
oml.UI.set({'part.app': data.selected});
}
}),
that = Ox.Dialog({
buttons: [
Ox.Button({
id: 'close',
title: Ox._('Close')
}).bindEvent({
click: function() {
that.close();
}
})
],
closeButton: true,
content: $panel,
fixedSize: true,
height: 384,
removeOnClose: true,
title: 'Open Media Library',
width: 768
})
.bindEvent({
close: function() {
if (ui.page == 'app') {
oml.UI.set({page: ''});
}
},
'oml_part.app': function() {
if (ui.page == 'app') {
that.update();
}
}
});
that.update = function(section) {
$panel.selectTab(section);
};
return that;
};

51
static/js/appPanel.js Normal file
View file

@ -0,0 +1,51 @@
'use strict';
oml.ui.appPanel = function() {
var ui = oml.user.ui,
that = Ox.SplitPanel({
elements: [
{
element: oml.$ui.mainMenu = oml.ui.mainMenu(),
size: 20
},
{
element: oml.$ui.mainPanel = oml.ui.mainPanel()
}
],
orientation: 'vertical'
})
.bindEvent({
oml_page: function(data) {
setPage(data.value);
}
});
setPage(ui.page);
function setPage(page) {
// close dialogs
$('.OxDialog:visible').each(function() {
Ox.UI.elements[$(this).data('oxid')].close();
});
// open dialog
if (Ox.contains([
'welcome', 'app', 'preferences', 'users',
'notifications', 'transfers', 'help'
], page)) {
oml.$ui[page + 'Dialog'] = oml.ui[page + 'Dialog']().open();
}
}
that.reload = function() {
Ox.Request.cancel();
Ox.Request.clearCache();
oml.$ui.appPanel.remove();
oml.$ui.appPanel = oml.ui.appPanel().appendTo(Ox.$body);
return that;
};
return that;
};

25
static/js/backButton.js Normal file
View file

@ -0,0 +1,25 @@
'use strict';
oml.ui.backButton = function() {
var ui = oml.user.ui,
that = Ox.Button({
style: 'squared',
title: 'arrowLeft',
tooltip: Ox._('Back to Books'),
type: 'image'
})
.css({
float: 'left',
margin: '4px 2px 4px 4px'
})
.bindEvent({
click: function() {
oml.UI.set({item: ''});
}
});
return that;
};

106
static/js/browser.js Normal file
View file

@ -0,0 +1,106 @@
'use strict';
oml.ui.browser = function() {
var ui = oml.user.ui,
that = Ox.IconList({
centered: true,
defaultRatio: oml.config.coverRatio,
draggable: true,
item: function(data, sort, size) {
var color = oml.getFileTypeColor(data).map(function(rgb) {
return rgb.concat(0.8)
}),
ratio = data.coverRatio || oml.config.coverRatio,
width = Math.round(ratio >= 1 ? size : size * ratio),
height = Math.round(ratio <= 1 ? size : size / ratio),
sortKey = sort[0].key,
info = Ox.getObjectById(oml.config.sortKeys, sortKey).format(
Ox.contains(['title', 'random'], sortKey)
? (data.author || '') : data[sortKey]
);
size = size || 64;
return {
extra: ui.showFileInfo ? $('<div>')
.css({
width: width + 'px',
height: Math.round(size / 12.8) + 'px',
borderWidth: Math.round(size / 64) + 'px 0',
borderStyle: 'solid',
borderColor: 'rgba(' + color[1].join(', ') + ')',
margin: Math.round(size / 18) + 'px ' + Math.round(width / 3) + 'px',
fontSize: Math.round(size / 16) + 'px',
textAlign: 'center',
color: 'rgba(' + color[1].join(', ') + ')',
backgroundColor: 'rgba(' + color[0].join(', ') + ')',
WebkitTransform: 'rotate(45deg)'
})
.html(
ui.fileInfo == 'extension'
? data.extension.toUpperCase()
: Ox.formatValue(data.size, 'B')
) : null,
height: height,
id: data.id,
info: info,
title: data.title,
url: '/' + data.id + '/cover128.jpg',
width: width
};
},
items: function(data, callback) {
oml.api.find(Ox.extend(data, {
query: ui.find
}), callback);
},
keys: [
'author', 'coverRatio', 'extension',
'id', 'size', 'textsize', 'title'
],
max: 1,
min: 1,
orientation: 'horizontal',
// FIXME: is this correct?:
selected: ui.item ? [ui.item]
: ui.listSelection.length ? [ui.listSelection[0]]
: [],
size: 64,
sort: ui.listSort,
unique: 'id'
})
.bindEvent({
open: function() {
oml.UI.set({itemView: 'book'});
},
select: function(data) {
oml.UI.set({
item: data.ids[0],
itemView: 'info',
listSelection: data.ids
});
},
oml_find: function() {
that.reloadList();
},
oml_item: function(data) {
if (data.value && !data.previousValue) {
that.gainFocus();
}
},
oml_listselection: function(data) {
if (data.value.length) {
that.options({selected: [data.value[0]]});
}
},
oml_listsort: function(data) {
that.options({sort: data.value});
},
oml_sidebarsize: function(data) {
that.size(); // FIXME: DOESN'T CENTER
}
});
return that;
};

148
static/js/columnView.js Normal file
View file

@ -0,0 +1,148 @@
'use strict';
oml.ui.columnView = function() {
var ui = oml.user.ui,
that = Ox.CustomColumnList({
columns: [
{
id: 'authors',
item: getItemFunction('authors'),
itemHeight: 32,
items: function(data, selected, callback) {
oml.api.find(Ox.extend({
group: 'author',
query: {conditions: [], operator: '&'}
}, data), callback);
},
keys: ['name', 'items'],
max: -1,
sort: [{key: 'name', operator: '+'}],
selected: [],
title: Ox._('Authors'),
unique: 'name'
},
{
id: 'items',
item: getItemFunction('items'),
itemHeight: 32,
items: function(data, selected, callback) {
if (selected[0].length) {
oml.api.find(Ox.extend({
query: {
conditions: selected[0].map(function(name) {
return {
key: 'author',
operator: '==',
value: name
};
}),
operator: '|'
}
}, data), callback);
} else {
callback({
data: {
items: data.keys ? [] : 0
}
});
}
},
keys: ['author', 'title', 'date'],
max: -1,
selected: [],
sort: [{key: 'title', operator: '+'}],
title: Ox._('Items'),
unique: 'id'
},
{
id: 'files',
item: getItemFunction('files'),
itemHeight: 32,
items: function(data, selected, callback) {
oml.api.find(Ox.extend({
query: {
conditions: selected[0].map(function(name) {
return {
key: 'author',
operator: '==',
value: name
};
}),
operator: '|'
}
}, data), callback);
},
keys: ['id', 'name'],
selected: [],
sort: [{key: 'name', operator: '+'}],
title: Ox._('Files'),
unique: 'id'
}
],
width: window.innerWidth - (ui.showSidebar * ui.sidebarSize) - 1
});
function getItemFunction(id) {
return function(data, width) {
var $item = $('<div>')
.css({
height: '32px',
width: width + 'px'
})
if (!Ox.isEmpty(data)) {
$('<img>')
.attr({
src: '/static/png/oml.png'
})
.css({
position: 'relative',
display: 'inline-block',
left: '2px',
top: '2px',
width: '26px',
height: '26px',
border: '1px solid rgb(192, 192, 192)',
backgroundImage: '-webkit-linear-gradient(top, rgb(255, 255, 255), rgb(224, 224, 224))'
})
.appendTo($item);
$('<div>')
.css({
position: 'relative',
left: '34px',
top: '-28px',
width: width - 36 + 'px',
height: '16px',
fontSize: '13px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
cursor: 'default'
})
.html(id == 'authors' ? data.name : 'foo')
.appendTo($item);
$('<div>')
.addClass('OxLight')
.css({
position: 'relative',
left: '34px',
top: '-28px',
width: width - 36 + 'px',
height: '12px',
fontSize: '9px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
cursor: 'default'
})
.html(id == 'authors' ? Ox.formatCount(data.items, 'Item') : 'bar')
.appendTo($item);
}
return $item;
};
}
return that;
};

View file

@ -0,0 +1,23 @@
'use strict';
oml.ui.confirmDialog = function(options, callback) {
options = Ox.extend(options, {
buttons: options.buttons.map(function($button, index) {
return $button
.options({id: index ? 'yes' : 'no'})
.bindEvent({
click: function() {
that.close();
index && callback();
}
});
}),
keys: {enter: 'yes', escape: 'no'}
});
var that = oml.ui.iconDialog(options);
return that.open();
}

View file

@ -0,0 +1,33 @@
'use strict';
oml.ui.connectionButton = function() {
var that = Ox.Element({
tooltip: Ox._('Disconnected')
})
.css({
marginRight: '3px'
})
.bindEvent({
// ...
});
/*
oml.ui.statusIcon(oml.user.online ? 'connected' : 'disconnected')
.css({float: 'left'})
.appendTo(that);
*/
Ox.Element()
.addClass('OxLight')
.css({
float: 'left',
marginTop: '2px',
fontSize: '9px'
})
.html('&darr;0K/&uarr;0K')
.appendTo(that);
return that;
};

76
static/js/errorDialog.js Normal file
View file

@ -0,0 +1,76 @@
'use strict';
oml.ui.errorDialog = function() {
var ui = oml.user.ui,
that = oml.ui.iconDialog({
buttons: getButtons(),
content: Ox.Element(),
keys: {enter: 'close', escape: 'close'}
})
.addClass('OxErrorDialog')
.bindEvent({
oml_enabledebugmenu: function() {
that.options({buttons: getButtons()});
}
}),
open = that.open;
function getButtons() {
return (ui.enableDebugMenu ? [
Ox.Button({
title: Ox._('View Error Logs...')
})
.bindEvent({
click: function() {
that.close();
oml.UI.set({page: 'errorlogs'});
}
}),
{}
] : []).concat([
Ox.Button({
id: 'close',
title: Ox._('Close')
})
.bindEvent({
click: function() {
that.close();
}
})
]);
}
that.open = function() {
// on window unload, pending request will time out, so
// in order to keep the dialog from appearing, delay it
setTimeout(function() {
if ($('.OxErrorDialog').length == 0 && !oml.isUnloading) {
open();
}
}, 250);
return that;
};
that.update = function(data) {
// 0 (timeout) or 500 (error)
var error = data.status.code == 0 ? 'a timeout' : 'an error',
title = data.status.code == 0 ? 'Timeout' : 'Error';
that.options({
content: Ox.Element().html(
Ox._(
'Sorry, {0} occured while handling your request.'
+ ' In case this happens repeatedly, you may want to file a bug report.'
+ ' Otherwise, please try again later.', [error]
)
),
title: title
});
return that;
}
return that;
};

192
static/js/filter.js Normal file
View file

@ -0,0 +1,192 @@
'use strict';
oml.ui.filter = function(id) {
var ui = oml.user.ui,
filter = Ox.getObjectById(oml.config.filters, id),
filterIndex = Ox.getIndexById(ui.filters, id),
filterSize = oml.getFilterSizes()[filterIndex],
that = Ox.TableList({
_selected: !ui.showFilters
? ui._filterState[filterIndex].selected
: false,
columns: [
{
id: 'name',
operator: '+',
title: Ox._(filter.title),
visible: true,
width: filterSize - 44 - Ox.UI.SCROLLBAR_SIZE
},
{
align: 'right',
format: function(value) {
return Ox.formatNumber(value);
},
id: 'items',
operator: '-',
title: '#',
visible: true,
width: 44
}
],
columnsVisible: true,
items: function(data, callback) {
if (ui.showFilters) {
delete data.keys;
return oml.api.find(Ox.extend(data, {
group: filter.id,
query: ui._filterState[filterIndex].find
}), callback);
} else {
callback({
data: {items: data.keys ? [] : 0}
});
}
},
scrollbarVisible: true,
selected: ui.showFilters
? ui._filterState[filterIndex].selected
: [],
sort: Ox.clone(ui.filters[filterIndex].sort, true),
unique: 'name'
})
.bindEvent({
init: function(data) {
that.setColumnTitle(
'name',
Ox._(filter.title)
+ '<div class="OxColumnStatus OxLight">'
+ Ox.formatNumber(data.items) + '</div>'
);
},
select: function(data) {
Ox.print('UI FILTER STATE', ui._filterState)
// fixme: cant index be an empty array, instead of -1?
// FIXME: this is still incorrect when deselecting a filter item
// makes a selected item in another filter disappear
var conditions = data.ids.map(function(value) {
return {
key: id,
value: value,
operator: '=='
};
}),
index = ui._filterState[filterIndex].index,
find = Ox.clone(ui.find, true);
if (Ox.isArray(index)) {
// this filter had multiple selections and the | query
// was on the top level, i.e. not bracketed
find = {
conditions: conditions,
operator: conditions.length > 1 ? '|' : '&'
}
} else {
if (index == -1) {
// this filter had no selection, i.e. no query
index = find.conditions.length;
if (find.operator == '|') {
find = {
conditions: [find],
operator: '&'
};
index = 1;
} else {
find.operator = '&';
}
}
if (conditions.length == 0) {
// nothing selected
find.conditions.splice(index, 1);
if (find.conditions.length == 1) {
if (find.conditions[0].conditions) {
// unwrap single remaining bracketed query
find = {
conditions: find.conditions[0].conditions,
operator: '|'
};
} else {
find.operator = '&';
}
}
} else if (conditions.length == 1) {
// one item selected
find.conditions[index] = conditions[0];
} else {
// multiple items selected
if (ui.find.conditions.length == 1) {
find = {
conditions: conditions,
operator: '|'
};
} else {
find.conditions[index] = {
conditions: conditions,
operator: '|'
};
}
}
}
oml.UI.set({find: find});
oml.updateFilterMenus();
},
sort: function(data) {
var filters = Ox.clone(ui.filters, true);
filters[filterIndex].sort = [Ox.clone(data)];
oml.UI.set({filters: filters});
},
oml_find: function() {
Ox.print('%%%%', 'RELOADING FILTER')
that.reloadList(true);
}
}),
$menu = Ox.MenuButton({
items: [
{id: 'clearFilter', title: Ox._('Clear Filter'), keyboard: 'shift control a'},
{id: 'clearFilters', title: Ox._('Clear All Filters'), keyboard: 'shift alt control a'},
{},
{group: 'filter', max: 1, min: 1, items: oml.config.filters.map(function(filter) {
return Ox.extend({checked: filter.id == id}, filter);
})}
],
type: 'image',
})
.css(Ox.UI.SCROLLBAR_SIZE == 16 ? {
right: 0,
width: '14px'
} : {
right: '-1px',
width: '8px',
})
.bindEvent({
change: function(data) {
},
click: function(data) {
}
})
.appendTo(that.$bar.$element);
if (Ox.UI.SCROLLBAR_SIZE < 16) {
$($menu.find('input')[0]).css({
marginRight: '-3px',
marginTop: '1px',
width: '8px',
height: '8px'
});
}
that.disableMenuItem = function(id) {
$menu.disableItem(id);
};
that.enableMenuItem = function(id) {
$menu.enableItem(id);
};
return that;
};

View file

@ -0,0 +1,26 @@
'use strict';
oml.ui.filtersInnerPanel = function() {
var filterSizes = oml.getFilterSizes(),
that = Ox.SplitPanel({
elements: [
{
element: oml.$ui.filters[1],
size: filterSizes[1]
},
{
element: oml.$ui.filters[2]
},
{
element: oml.$ui.filters[3],
size: filterSizes[3]
}
],
orientation: 'horizontal'
});
return that;
};

View file

@ -0,0 +1,82 @@
'use strict';
oml.ui.filtersOuterPanel = function() {
var ui = oml.user.ui,
$filters = oml.$ui.filters = ui.filters.map(function(filter) {
return oml.ui.filter(filter.id);
}),
filterSizes = oml.getFilterSizes(),
that = Ox.SplitPanel({
elements: [
{
element: $filters[0],
size: filterSizes[0]
},
{
element: oml.$ui.filtersInnerPanel = oml.ui.filtersInnerPanel()
},
{
element: $filters[4],
size: filterSizes[4]
},
],
orientation: 'horizontal'
})
.bindEvent({
resize: function() {
oml.$ui.filters.forEach(function($filter) {
$filter.size();
});
},
resizeend: function(data) {
oml.UI.set({filtersSize: data.size});
},
toggle: function(data) {
if (data.collapsed) {
oml.$ui.list.gainFocus();
}
oml.UI.set({showFilters: !data.collapsed});
if (!data.collapsed) {
oml.$ui.filters.forEach(function($filter) {
var selected = $filter.options('_selected');
if (selected) {
$filter.bindEventOnce({
load: function() {
$filter.options({
_selected: false,
selected: selected
});
}
}).reloadList();
}
});
oml.updateFilterMenus();
}
}
});
oml.updateFilterMenus();
that.update = function() {
var filterSizes = oml.getFilterSizes();
that.size(0, filterSizes[0])
.size(2, filterSizes[4]);
oml.$ui.filtersInnerPanel
.size(0, filterSizes[1])
.size(2, filterSizes[3]);
oml.$ui.filters.forEach(function($filter, index) {
$filter.resizeColumn(
'name',
filterSizes[index] - 44 - Ox.UI.SCROLLBAR_SIZE
);
});
return that;
};
return that;
};

203
static/js/findElement.js Normal file
View file

@ -0,0 +1,203 @@
'use strict';
oml.ui.findElement = function() {
var ui = oml.user.ui,
findIndex = ui._findState.index,
findKey = ui._findState.key,
findValue = ui._findState.value,
hasPressedClear = false,
previousFindKey = findKey,
that = Ox.FormElementGroup({
elements: [
oml.$ui.findScopeSelect = renderFindScopeSelect(),
oml.$ui.findSelect = Ox.Select({
id: 'select',
items: [].concat(
oml.config.findKeys.map(function(key) {
return {
id: key.id,
title: Ox._('Find: {0}', [Ox._(key.title)])
};
}),
[{}, {
id: 'advanced',
title: Ox._('Find: Advanced...')
}]
),
overlap: 'right',
style: 'squared',
value: findKey,
width: 160
})
.bindEvent({
change: function(data) {
if (data.value == 'advanced') {
that.updateElement();
//oml.$ui.mainMenu.checkItem('findMenu_find_' + previousFindKey);
oml.$ui.filterDialog = oml.ui.filterDialog().open();
} else {
//oml.$ui.mainMenu.checkItem('findMenu_find_' + data.value);
oml.$ui.findInput.options({
autocomplete: getAutocomplete(),
placeholder: ''
}).focusInput(true);
previousFindKey = data.value;
}
}
}),
oml.$ui.findInput = Ox.Input({
autocomplete: getAutocomplete(),
autocompleteSelect: true,
autocompleteSelectHighlight: true,
autocompleteSelectMaxWidth: 256,
autocompleteSelectSubmit: true,
clear: true,
clearTooltip: Ox._('Click to clear or doubleclick to reset query'),
id: 'input',
placeholder: findKey == 'advanced' ? Ox._('Edit Query...') : '',
style: 'squared',
value: findValue,
width: 240
})
.bindEvent({
clear: function() {
hasPressedClear = true;
},
focus: function(data) {
if (oml.$ui.findSelect.value() == 'advanced') {
if (hasPressedClear) {
oml.UI.set({find: oml.site.user.ui.find});
that.updateElement();
hasPressedClear = false;
}
oml.$ui.findInput.blurInput();
oml.$ui.filterDialog = oml.ui.filterDialog().open();
}
},
submit: function(data) {
var scope = oml.$ui.findScopeSelect.value(),
key = oml.$ui.findSelect.value(),
conditions = [].concat(
scope == 'list' ? [{
key: 'list',
value: ui._list,
operator: '=='
}] : [],
scope == 'user' ? [{
key: 'list',
value: ui._list.split(':')[0],
operator: '=='
}] : [],
data.value ? [{
key: key,
value: data.value,
operator: '='
}] : []
);
oml.UI.set({
find: {
conditions: conditions,
operator: '&'
}
});
}
})
]
})
.css({
float: 'right',
margin: '4px 4px 4px 2px'
})
.bindEvent({
oml_find: function() {
that.replaceElement(
0, oml.$ui.findScopeSelect = renderFindScopeSelect()
);
}
});
function getAutocomplete() {
var key = !that
? ui._findState.key
: that.value()[ui._list ? 1 : 0],
findKey = Ox.getObjectById(oml.config.findKeys, key);
return findKey && findKey.autocomplete ? function(value, callback) {
oml.api.autocomplete({
key: key,
query: {
conditions: ui._list
&& oml.$ui.findScopeSelect.value() == 'list'
? [{
key: 'list',
operator: '==',
value: ui._list
}]
: [],
operator: '&'
},
range: [0, 20],
sort: findKey.autocompleteSort,
value: value
}, function(result) {
callback(result.data.items.map(function(item) {
return Ox.decodeHTMLEntities(item);
}));
});
} : null;
}
function renderFindScopeSelect() {
var scope = !ui._list ? 'all'
: Ox.endsWith(ui._list, ':') ? 'user'
: 'list';
return Ox.Select({
items: [
{id: 'all', title: Ox._('Find: All Libraries')},
].concat(scope != 'all' ? [
{id: 'user', title: Ox._('Find: This Library')},
] : []).concat(scope == 'list' ? [
{id: 'list', title: Ox._('Find: This List')}
] : []),
overlap: 'right',
style: 'squared',
title: scope == 'all' ? 'data' : scope,
type: 'image',
tooltip: Ox._('Find: FIXME'),
value: scope
})
.bindEvent({
change: function(data) {
oml.$ui.findScopeSelect.options({
title: data.value == 'all' ? 'data' : data.value,
tooltip: data.title
});
oml.$ui.findInput.focusInput(true);
}
});
}
that.updateElement = function() {
var findState = ui._findState;
oml.$ui.findSelect.value(findState.key);
oml.$ui.findInput.options(
findState.key == 'advanced' ? {
placeholder: Ox._('Edit Query...'),
value: ''
} : {
autocomplete: getAutocomplete(),
placeholder: '',
value: findState.value
}
);
};
return that;
};

67
static/js/folderList.js Normal file
View file

@ -0,0 +1,67 @@
'use strict';
oml.ui.folderList = function(options) {
var ui = oml.user.ui,
that = Ox.TableList({
columns: [
{
format: function(value) {
return $('<img>')
.attr({
src: Ox.UI.getImageURL(
value == 'libraries' ? 'symbolData'
: value == 'library' ? 'symbolUser'
: value == 'static' ? 'symbolClick'
: 'symbolFind'
)
})
.css({
width: '10px',
height: '10px',
margin: '2px -2px 2px 0'
});
},
id: 'type',
visible: true,
width: 16
},
{
id: 'name',
visible: true,
width: ui.sidebarSize - 58,
},
{
align: 'right',
format: function(value) {
return value > -1
? '<span class="OxLight">'
+ Ox.formatNumber(value)
+ '</span>'
: '';
},
id: 'items',
visible: true,
width: 42
}
],
draggable: options.draggable,
items: options.items,
sort: [{key: 'index', operator: '+'}],
sortable: options.sortable,
selected: [],
unique: 'id'
})
.css({
width: ui.sidebarSize + 'px',
height: '16px'
});
that.resizeElement = function() {
// ...
};
return that;
};

View file

@ -0,0 +1,20 @@
'use strict';
// FIXME: UNUSED
oml.ui.folderPlaceholder = function(text) {
var that = Ox.Element()
.addClass('OxLight')
.css({
height: '14px',
padding: '1px 4px',
});
that.updateText = function(text) {
return that.html(text);
};
return that.updateText(text);
};

286
static/js/folders.js Normal file
View file

@ -0,0 +1,286 @@
'use strict';
oml.ui.folders = function() {
var ui = oml.user.ui,
userIndex = {},
$lists = [],
that = Ox.Element()
.css({
//overflowX: 'hidden',
//overflowY: 'auto',
})
.bindEvent({
oml_find: selectList
});
$lists.push(
oml.$ui.librariesList = oml.ui.folderList({
items: [
{
id: '',
name: Ox._('All Libraries'),
type: 'libraries',
items: -1
}
]
})
.bindEvent({
load: function() {
oml.api.find({query: getFind()}, function(result) {
oml.$ui.librariesList.value('', 'items', result.data.items);
});
},
open: function() {
},
select: function() {
oml.UI.set({find: getFind('')});
oml.$ui.librariesList.options({selected: ['']});
},
selectnext: function() {
oml.UI.set(Ox.extend(
{find: getFind(':')},
'showFolder.' + oml.user.preferences.username,
true
));
},
})
.css({height: '16px'})
.appendTo(that)
);
oml.$ui.librariesList.$body.css({height: '16px'}); // FIXME!
oml.$ui.folder = [];
oml.$ui.libraryList = [];
oml.$ui.folderList = [];
oml.api.getUsers(function(result) {
var peers = result.data.users.filter(function(user) {
return user.peered;
});
oml.api.getLists(function(result) {
Ox.print('GOT LISTS', result.data);
var users = [
{
id: oml.user.id,
nickname: oml.user.preferences.username,
online: oml.user.online
}
].concat(peers),
lists = result.data.lists;
users.forEach(function(user, index) {
var $content,
libraryId = (!index ? '' : user.nickname) + ':';
userIndex[user.nickname] = index;
oml.$ui.folder[index] = Ox.CollapsePanel({
collapsed: false,
extras: [
oml.ui.statusIcon(
!oml.user.online ? 'unknown'
: user.online ? 'connected'
: 'disconnected'
),
{},
Ox.Button({
style: 'symbol',
title: 'info',
tooltip: Ox._(!index ? 'Preferences' : 'Profile'),
type: 'image'
})
.bindEvent({
click: function() {
if (!index) {
oml.UI.set({
page: 'preferences',
'part.preferences': 'account'
});
} else {
oml.UI.set({page: 'users'})
}
}
})
],
title: Ox.encodeHTMLEntities(user.nickname)
})
.css({
width: ui.sidebarSize
})
.bindEvent({
toggle: function(data) {
oml.UI.set('showFolder.' + user.nickname, !data.collapsed);
}
})
.bindEvent(
'oml_showfolder.' + user.nickname.toLowerCase(),
function(data) {
oml.$ui.folder[index].options({collapsed: !data.value});
}
)
.appendTo(that);
$content = oml.$ui.folder[index].$content
.css({
height: (1 + lists[user.id].length) * 16 + 'px'
});
$lists.push(
oml.$ui.libraryList[index] = oml.ui.folderList({
items: [
{
id: libraryId,
name: Ox._('Library'),
type: 'library',
items: -1
}
]
})
.bindEvent({
add: function() {
!index && oml.addList();
},
load: function() {
oml.api.find({
query: getFind(libraryId)
}, function(result) {
oml.$ui.libraryList[index].value(
libraryId, 'items', result.data.items
);
});
},
open: function() {
oml.$ui.listDialog = oml.ui.listDialog().open();
},
select: function(data) {
oml.UI.set({find: getFind(data.ids[0])});
},
selectnext: function() {
oml.UI.set({find: getFind(lists[user.id][0].id)});
},
selectprevious: function() {
var userId = !index ? null : users[index - 1].id,
set = {
find: getFind(
!index
? ''
: Ox.last(lists[userId]).id
)
};
if (userId) {
Ox.extend(set, 'showFolder.' + userId, true);
}
oml.UI.set(set);
}
})
.appendTo($content)
);
$lists.push(
oml.$ui.folderList[index] = oml.ui.folderList({
draggable: !!index,
items: lists[user.id],
sortable: true
})
.bindEvent({
add: function() {
!index && oml.addList();
},
'delete': function() {
!index && oml.deleteList();
},
key_control_d: function() {
oml.addList(ui._list);
},
load: function() {
// ...
},
move: function(data) {
lists[user.id] = data.ids.map(function(listId) {
return Ox.getObjectById(lists[user.id], listId);
});
oml.api.sortLists({
ids: data.ids,
user: user.id
}, function(result) {
// ...
});
},
open: function() {
oml.ui.listDialog().open();
},
select: function(data) {
oml.UI.set({find: getFind(data.ids[0])});
},
selectnext: function() {
if (index < users.length - 1) {
oml.UI.set(Ox.extend(
{find: getFind(users[index + 1].nickname + ':')},
'showFolder.' + users[index + 1].nickname,
true
));
}
},
selectprevious: function() {
oml.UI.set({find: getFind(libraryId)});
}
})
.bindEvent(function(data, event) {
if (!index) {
Ox.print('LIST EVENT', event, data);
}
})
.css({height: lists[user.id].length * 16 + 'px'})
.appendTo($content)
);
oml.$ui.folderList[index].$body.css({top: '16px'});
});
selectList();
});
});
function getFind(list) {
return {
conditions: list ? [{
key: 'list',
operator: '==',
value: list
}] : [],
operator: '&'
};
}
function selectList() {
var split = ui._list.split(':'),
index = userIndex[split[0] || oml.user.preferences.username],
list = split[1],
$selectedList = !ui._list ? oml.$ui.librariesList
: !list ? oml.$ui[!list ? 'libraryList' : 'folderList'][index]
: oml.$ui.folderList[index];
$lists.forEach(function($list) {
if ($list == $selectedList) {
$list.options({selected: [ui._list]}).gainFocus();
} else {
$list.options({selected: []})
}
});
}
return that;
};

View file

@ -0,0 +1,34 @@
'use strict';
oml.ui.fullscreenButton = function() {
var ui = oml.user.ui,
that = Ox.Button({
style: 'squared',
title: 'grow',
tooltip: Ox._('Enter Fullscreen'),
type: 'image'
})
.css({
float: 'left',
margin: '4px 2px'
})
.bindEvent({
click: function() {
Ox.Fullscreen.enter(oml.$ui.viewer.find('iframe')[0]);
},
oml_itemview: function() {
that.update();
}
});
that.update = function() {
return that.options({
disabled: ui.itemView != 'book'
});
};
return that.update();
};

Some files were not shown because too many files have changed in this diff Show more