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