diff --git a/README.md b/README.md index f7577d4b..3ea98034 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d 1. Install dependencies by running `pip install --target vendor -r requirements.txt`. 2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window) 3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog -4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button - optionally a google drive can be used to host the calibre library (-> Using Google Drive integration) +4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\ + Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration) 5. Go to Login page **Default admin login:**\ @@ -50,26 +50,35 @@ Python 2.7+, python 3.x+ Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata: -[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including programm name (normally /opt/calibre/ebook-convert, or c:\prgogram files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page. +[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including programm name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page. \*** DEPRECATED \*** Support will be removed in future releases -Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature: -[Download Kindlegen](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder. +[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder. -## Docker images +## Docker Images -Pre-built Docker images based on Alpine Linux are available in these Docker Hub repositories: +Pre-built Docker images are available in these Docker Hub repositories: -**x64** -+ **technosoft2000** at [technosoft2000/calibre-web](https://hub.docker.com/r/technosoft2000/calibre-web/). If you want the option to convert/download ebooks in multiple formats, use this image as it includes Calibre's ebook-convert binary. The "path to convertertool" should be set to /opt/calibre/ebook-convert. -+ **linuxserver.io** at [linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web/). Cannot convert between ebook formats. +#### **Technosoft2000 - x64** ++ Docker Hub - [https://hub.docker.com/r/technosoft2000/calibre-web/](https://hub.docker.com/r/technosoft2000/calibre-web/) ++ Github - [https://github.com/Technosoft2000/docker-calibre-web](https://github.com/Technosoft2000/docker-calibre-web) -**armhf** -+ **linuxserver.io** at [lsioarmhf/calibre-web](https://hub.docker.com/r/lsioarmhf/calibre-web/) + Includes the Calibre `ebook-convert` binary. + + The "path to convertertool" should be set to `/opt/calibre/ebook-convert` -**aarch64** -+ **linuxserver.io** at [lsioarmhf/calibre-web-aarch64](https://hub.docker.com/r/lsioarmhf/calibre-web-aarch64) +#### **LinuxServer - x64, armhf, aarch64** ++ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web/](https://hub.docker.com/r/linuxserver/calibre-web/) ++ Github - [https://github.com/linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web) ++ Github - (Optional Calibre layer) - [https://github.com/linuxserver/docker-calibre-web/tree/calibre](https://github.com/linuxserver/docker-calibre-web/tree/calibre) + + This image has the option to pull in an extra docker manifest layer to include the Calibre `ebook-convert` binary. Just include the environmental variable `DOCKER_MODS=linuxserver/calibre-web:calibre` in your docker run/docker compose file. **(x64 only)** + + If you do not need this functionality then this can be omitted, keeping the image as lightweight as possible. + + Both the Calibre-Web and Calibre-Mod images are rebuilt automatically on new releases of Calibre-Web and Calibre respectively, and on updates to any included base image packages on a weekly basis if required. + + The "path to convertertool" should be set to `/usr/bin/ebook-convert` + + The "path to unrar" should be set to `/usr/bin/unrar` # Wiki diff --git a/cps/admin.py b/cps/admin.py index 01f56b7d..4d220fd0 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -26,6 +26,8 @@ import os import json import time from datetime import datetime, timedelta +# import uuid +import random try: from imp import reload except ImportError: @@ -33,7 +35,7 @@ except ImportError: from babel import Locale as LC from babel.dates import format_datetime -from flask import Blueprint, flash, redirect, url_for, abort, request, make_response +from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory from flask_login import login_required, current_user, logout_user from flask_babel import gettext as _ from sqlalchemy import and_ @@ -542,6 +544,9 @@ def new_user(): to_save = request.form.to_dict() content.default_language = to_save["default_language"] content.mature_content = "Show_mature_content" in to_save + dat = datetime.strptime("1.1.2019", "%d.%m.%Y") + content.id = int(time.time()*100) + # val= int(uuid.uuid4()) if "locale" in to_save: content.locale = to_save["locale"] @@ -551,6 +556,7 @@ def new_user(): val += int(key[5:]) content.sidebar_view = val + if "show_detail_random" in to_save: content.sidebar_view |= constants.DETAIL_RANDOM @@ -758,6 +764,25 @@ def reset_password(user_id): return redirect(url_for('admin.admin')) +@admi.route("/admin/logfile") +@login_required +@admin_required +def view_logfile(): + perpage_p = {0:"30",1:"40",2:"100"} + for key, value in perpage_p.items(): + print(key) + print(value) + return render_title_template("logviewer.html",title=_(u"Logfile viewer"), perpage_p=perpage_p, perpage = 30, + page="logfile") + + +@admi.route("/ajax/accesslog") +@login_required +@admin_required +def send_logfile(): + return send_from_directory(constants.BASE_DIR,"access.log") + + @admi.route("/get_update_status", methods=['GET']) @login_required_if_no_ano def get_update_status(): diff --git a/cps/logger.py b/cps/logger.py index 2073b066..c249cad7 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -88,8 +88,8 @@ def setup(log_file, log_level=None, logger=None): log_file = os.path.join(_BASE_DIR, log_file) log_file = os.path.abspath(log_file) else: - # log_file = LOG_TO_STDERR - log_file = default_file + log_file = LOG_TO_STDERR + # log_file = default_file # print ('%r -- %r' % (log_level, log_file)) if logger != "access" and logger != "tornado.access": diff --git a/cps/static/images/caliblur/blur-dark.png b/cps/static/css/images/caliblur/blur-dark.png similarity index 100% rename from cps/static/images/caliblur/blur-dark.png rename to cps/static/css/images/caliblur/blur-dark.png diff --git a/cps/static/images/caliblur/blur-light.png b/cps/static/css/images/caliblur/blur-light.png similarity index 100% rename from cps/static/images/caliblur/blur-light.png rename to cps/static/css/images/caliblur/blur-light.png diff --git a/cps/static/images/caliblur/blur-noise.png b/cps/static/css/images/caliblur/blur-noise.png similarity index 100% rename from cps/static/images/caliblur/blur-noise.png rename to cps/static/css/images/caliblur/blur-noise.png diff --git a/cps/static/js/io/bitstream.js b/cps/static/js/io/bitstream.js index 77ba260f..af1cad99 100644 --- a/cps/static/js/io/bitstream.js +++ b/cps/static/js/io/bitstream.js @@ -8,6 +8,9 @@ * Copyright(c) 2011 Google Inc. * Copyright(c) 2011 antimatter15 */ + +/* global bitjs, Uint8Array */ + var bitjs = bitjs || {}; bitjs.io = bitjs.io || {}; @@ -30,20 +33,20 @@ bitjs.io = bitjs.io || {}; * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. * @param {boolean} rtl Whether the stream reads bits from the byte starting * from bit 7 to 0 (true) or bit 0 to 7 (false). - * @param {Number} opt_offset The offset into the ArrayBuffer - * @param {Number} opt_length The length of this BitStream + * @param {Number} optOffset The offset into the ArrayBuffer + * @param {Number} optLength The length of this BitStream */ - bitjs.io.BitStream = function(ab, rtl, opt_offset, opt_length) { + bitjs.io.BitStream = function(ab, rtl, optOffset, optLength) { if (!ab || !ab.toString || ab.toString() !== "[object ArrayBuffer]") { throw "Error! BitArray constructed with an invalid ArrayBuffer object"; } - var offset = opt_offset || 0; - var length = opt_length || ab.byteLength; + var offset = optOffset || 0; + var length = optLength || ab.byteLength; this.bytes = new Uint8Array(ab, offset, length); this.bytePtr = 0; // tracks which byte we are on this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) - this.peekBits = rtl ? this.peekBits_rtl : this.peekBits_ltr; + this.peekBits = rtl ? this.peekBitsRtl : this.peekBitsLtr; }; @@ -57,8 +60,8 @@ bitjs.io = bitjs.io || {}; * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - bitjs.io.BitStream.prototype.peekBits_ltr = function(n, movePointers) { - if (n <= 0 || typeof n != typeof 1) { + bitjs.io.BitStream.prototype.peekBitsLtr = function(n, movePointers) { + if (n <= 0 || typeof n !== typeof 1) { return 0; } @@ -77,12 +80,13 @@ bitjs.io = bitjs.io || {}; if (bytePtr >= bytes.length) { throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + bytes.length + ", bitPtr=" + bitPtr; - return -1; + // return -1; } var numBitsLeftInThisByte = (8 - bitPtr); + var mask; if (n >= numBitsLeftInThisByte) { - var mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); + mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); bytePtr++; @@ -90,7 +94,7 @@ bitjs.io = bitjs.io || {}; bitsIn += numBitsLeftInThisByte; n -= numBitsLeftInThisByte; } else { - var mask = (BITMASK[n] << bitPtr); + mask = (BITMASK[n] << bitPtr); result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); bitPtr += n; @@ -118,8 +122,8 @@ bitjs.io = bitjs.io || {}; * @param {boolean=} movePointers Whether to move the pointer, defaults false. * @return {number} The peeked bits, as an unsigned number. */ - bitjs.io.BitStream.prototype.peekBits_rtl = function(n, movePointers) { - if (n <= 0 || typeof n != typeof 1) { + bitjs.io.BitStream.prototype.peekBitsRtl = function(n, movePointers) { + if (n <= 0 || typeof n !== typeof 1) { return 0; } @@ -138,7 +142,7 @@ bitjs.io = bitjs.io || {}; if (bytePtr >= bytes.length) { throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + bytes.length + ", bitPtr=" + bitPtr; - return -1; + // return -1; } var numBitsLeftInThisByte = (8 - bitPtr); @@ -198,19 +202,19 @@ bitjs.io = bitjs.io || {}; * @return {Uint8Array} The subarray. */ bitjs.io.BitStream.prototype.peekBytes = function(n, movePointers) { - if (n <= 0 || typeof n != typeof 1) { + if (n <= 0 || typeof n !== typeof 1) { return 0; } // from http://tools.ietf.org/html/rfc1951#page-11 // "Any bits of input up to the next byte boundary are ignored." - while (this.bitPtr != 0) { + while (this.bitPtr !== 0) { this.readBits(1); } var movePointers = movePointers || false; - var bytePtr = this.bytePtr, - bitPtr = this.bitPtr; + var bytePtr = this.bytePtr; + // bitPtr = this.bitPtr; var result = this.bytes.subarray(bytePtr, bytePtr + n); @@ -230,4 +234,4 @@ bitjs.io = bitjs.io || {}; return this.peekBytes(n, true); }; -})(); \ No newline at end of file +})(); diff --git a/cps/static/js/io/bytebuffer.js b/cps/static/js/io/bytebuffer.js index cb9f955e..2d55a76f 100644 --- a/cps/static/js/io/bytebuffer.js +++ b/cps/static/js/io/bytebuffer.js @@ -8,6 +8,9 @@ * Copyright(c) 2011 Google Inc. * Copyright(c) 2011 antimatter15 */ + +/* global bitjs, Uint8Array */ + var bitjs = bitjs || {}; bitjs.io = bitjs.io || {}; @@ -20,7 +23,7 @@ bitjs.io = bitjs.io || {}; * @constructor */ bitjs.io.ByteBuffer = function(numBytes) { - if (typeof numBytes != typeof 1 || numBytes <= 0) { + if (typeof numBytes !== typeof 1 || numBytes <= 0) { throw "Error! ByteBuffer initialized with '" + numBytes + "'"; } this.data = new Uint8Array(numBytes); @@ -55,14 +58,14 @@ bitjs.io = bitjs.io || {}; */ bitjs.io.ByteBuffer.prototype.writeNumber = function(num, numBytes) { if (numBytes < 1) { - throw 'Trying to write into too few bytes: ' + numBytes; + throw "Trying to write into too few bytes: " + numBytes; } if (num < 0) { - throw 'Trying to write a negative number (' + num + - ') as an unsigned number to an ArrayBuffer'; + throw "Trying to write a negative number (" + num + + ") as an unsigned number to an ArrayBuffer"; } if (num > (Math.pow(2, numBytes * 8) - 1)) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; + throw "Trying to write " + num + " into only " + numBytes + " bytes"; } // Roll 8-bits at a time into an array of bytes. @@ -85,12 +88,12 @@ bitjs.io = bitjs.io || {}; */ bitjs.io.ByteBuffer.prototype.writeSignedNumber = function(num, numBytes) { if (numBytes < 1) { - throw 'Trying to write into too few bytes: ' + numBytes; + throw "Trying to write into too few bytes: " + numBytes; } var HALF = Math.pow(2, (numBytes * 8) - 1); if (num >= HALF || num < -HALF) { - throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; + throw "Trying to write " + num + " into only " + numBytes + " bytes"; } // Roll 8-bits at a time into an array of bytes. @@ -112,10 +115,10 @@ bitjs.io = bitjs.io || {}; for (var i = 0; i < str.length; ++i) { var curByte = str.charCodeAt(i); if (curByte < 0 || curByte > 255) { - throw 'Trying to write a non-ASCII string!'; + throw "Trying to write a non-ASCII string!"; } this.insertByte(curByte); } }; -})(); \ No newline at end of file +})(); diff --git a/cps/static/js/io/bytestream.js b/cps/static/js/io/bytestream.js index 066cc5e8..55b14005 100644 --- a/cps/static/js/io/bytestream.js +++ b/cps/static/js/io/bytestream.js @@ -8,6 +8,9 @@ * Copyright(c) 2011 Google Inc. * Copyright(c) 2011 antimatter15 */ + +/* global bitjs, Uint8Array */ + var bitjs = bitjs || {}; bitjs.io = bitjs.io || {}; @@ -19,13 +22,13 @@ bitjs.io = bitjs.io || {}; * out of an ArrayBuffer. In this buffer, everything must be byte-aligned. * * @param {ArrayBuffer} ab The ArrayBuffer object. - * @param {number=} opt_offset The offset into the ArrayBuffer - * @param {number=} opt_length The length of this BitStream + * @param {number=} optOffset The offset into the ArrayBuffer + * @param {number=} optLength The length of this BitStream * @constructor */ - bitjs.io.ByteStream = function(ab, opt_offset, opt_length) { - var offset = opt_offset || 0; - var length = opt_length || ab.byteLength; + bitjs.io.ByteStream = function(ab, optOffset, optLength) { + var offset = optOffset || 0; + var length = optLength || ab.byteLength; this.bytes = new Uint8Array(ab, offset, length); this.ptr = 0; }; @@ -40,8 +43,9 @@ bitjs.io = bitjs.io || {}; */ bitjs.io.ByteStream.prototype.peekNumber = function(n) { // TODO: return error if n would go past the end of the stream? - if (n <= 0 || typeof n != typeof 1) + if (n <= 0 || typeof n !== typeof 1) { return -1; + } var result = 0; // read from last byte to first byte and roll them in @@ -105,7 +109,7 @@ bitjs.io = bitjs.io || {}; * @return {Uint8Array} The subarray. */ bitjs.io.ByteStream.prototype.peekBytes = function(n, movePointers) { - if (n <= 0 || typeof n != typeof 1) { + if (n <= 0 || typeof n !== typeof 1) { return null; } @@ -135,7 +139,7 @@ bitjs.io = bitjs.io || {}; * @return {string} The next n bytes as a string. */ bitjs.io.ByteStream.prototype.peekString = function(n) { - if (n <= 0 || typeof n != typeof 1) { + if (n <= 0 || typeof n !== typeof 1) { return ""; } diff --git a/cps/static/js/logviewer.js b/cps/static/js/logviewer.js new file mode 100644 index 00000000..77c63a3f --- /dev/null +++ b/cps/static/js/logviewer.js @@ -0,0 +1,609 @@ +var threadregexp = /(?:^| - )(\[[^\]]*\]):/; + +var colors = ["#ffa", "#aaf", "#afa", "#aff", "#faf", "#aaa", "#fd8", "#f80", "#4df", "#4fc", "#76973c", "#7e56d8", "#99593d", "#37778a", "#4068fc"]; +var screenlines = 1; + +var file = null; +var text; + +var current, nextFilterId = 1; +var shl = null; +var hl = []; +var groupwith = false; +var filterswitch = true; + +var selectedlineid = -1; +var selectedthread = null; +var reachedbottom = false; +var reachedtop = false; + + +function wheelscroll(event) +{ + renderincremental(event.deltaY); +} + +function keypress(event) +{ + if (event.key == "PageDown") { + _render(screenlines - 1); + event.preventDefault(); + } + if (event.key == "PageUp") { + _render(-(screenlines - 1)); + event.preventDefault(); + } + if (event.key == "Home" && event.ctrlKey) { + selectedlineid = 0; + render(); + event.preventDefault(); + } + if (event.key == "End" && event.ctrlKey) { + selectedlineid = text.length - 1; + render(); + event.preventDefault(); + } + if (event.key == "ArrowUp") { + renderincremental(-1); + event.preventDefault(); + } + if (event.key == "ArrowDown") { + renderincremental(1); + event.preventDefault(); + } +} + +function init(filename) { + document.addEventListener("wheel", wheelscroll, false); + document.addEventListener("keypress", keypress, false); + window.addEventListener("resize", resize, false); + + _resize(); + + var s = document.getElementById("search"); + s.value = ""; + selectfilter(0); + reload(filename); +} + +function _resize() +{ + var d = document.getElementById("renderer"); + var t = document.getElementById("toobar"); + screenlines = Math.floor((window.innerHeight - t.offsetHeight) / d.firstChild.offsetHeight) - 1; +} + +function resize() +{ + _resize(); + repaint(); +} + +function reload(filename) +{ + if (shl) shl.cache = {}; + for (_hl of hl) _hl.cache = {}; + + var q = filename; + document.title = "Log: " + (q || "none loaded"); + if (!q) + return; + + var d = document.getElementById("renderer"); + d.innerHTML = "loading " + q + "..."; + + var r = new XMLHttpRequest(); + r.open("GET", "/ajax/accesslog", true); + r.responseType = 'text'; + r.onload = function() { + console.log("prepare"); + prepare(r.responseText); + if (selectedlineid > text.length) { + selectedlineid = -1; + } + console.log("render"); + render(); + }; + r.send(); +} + +function _sanitize(t) +{ + t = t + .replace(/&/g, "&") + .replace(/ /g, " ") + .replace(//g, ">"); + + return t; +} + +function _prepare(t) +{ + // sanitization happens in render, since otherwise it eats enormous amount of memory. + /* + var t = t.split('\n'); + for (var i in t) { + t[i] = _sanitize(t[i]); + } + return t; + */ + return /*_sanitize*/(t).split('\n'); +} + +function prepare(t) +{ + text = _prepare(t); +} + +function render() +{ + _render(0, false); // completely redraws the view from the current scroll position +} + +function repaint() +{ + _render(0, true); // completely redraws the view, but centers the selected line +} + +function renderincremental(difference) +{ + _render(difference); // "scrolls" the view +} + +function _render(increment, repaintonly) +{ + var epoch = new Date(); + + var d = document.getElementById("renderer"); + var filter = _gfilteron(); + + function process(i, append) + { + var t = _sanitize(text[i]); + + var lhl = false; + function dohl(_hl) + { + if (_hl.cache[i] === false) { + // lhl is here unaffected + return t; + } + + var t2 = t.replace(new RegExp("(" + _hl.text_r + ")", "g"), "$1"); + var affecting = (t != t2); + _hl.cache[i] = affecting; + lhl = lhl || (affecting && (!filter || _hl.filter)); + return t2; + } + + for (var h in hl) + t = dohl(hl[h]); + if (shl) + t = dohl(shl); + + if (filter && !lhl && i != selectedlineid) { + return false; + } + + lhl = lhl && !filter; + var div = document.createElement("div"); + div.id = i; + if (lhl) div.className = 'lhl'; + div.onclick = function() { selectline(this); }; + div.innerHTML = t; + if (t.match(new RegExp(selectedthread, "g"))) div.className += ' thread'; + + if (append) + d.appendChild(div); + else + d.insertBefore(div, d.firstChild); + + return true; + } + + var lefttodraw = Math.floor(Math.abs(increment)); + + if (increment < 0) { + // scroll up + reachedbottom = false; + if (reachedtop) { + _hint("reached top of the file"); + return; + } + for (var i = parseInt(d.firstChild.id) - 1; lefttodraw && i >= 0 && i < text.length; --i) { + if (process(i, false)) { + --lefttodraw; + if (d.childNodes.length > screenlines) + d.removeChild(d.lastChild); + } + } + if (lefttodraw) { + _hint("reached top of the file"); + reachedtop = true; + } + } else if (increment > 0) { + // scroll down + reachedtop = false; + if (reachedbottom) { + _hint("reached bottom of the file"); + return; + } + for (var i = parseInt(d.lastChild.id) + 1; lefttodraw && i < text.length; ++i) { + if (process(i, true)) { + --lefttodraw; + if (d.childNodes.length > screenlines) + d.removeChild(d.firstChild); + } + } + if (lefttodraw) { + _hint("reached bottom of the file"); + reachedbottom = true; + } + } else { // == 0 + // redraw all + reachedbottom = false; + reachedtop = false; + lefttodraw = screenlines; + var i = repaintonly ? parseInt(d.firstChild.id) : selectedlineid; + if (i < 0) i = 0; + + d.innerHTML = ""; + for (; lefttodraw && i < text.length; ++i) { + if (process(i, true)) { + --lefttodraw; + } + } + + if (!repaintonly && selectedlineid > -1) { + // center the selected line in the middle of screen! + _render(-(screenlines / 2)); + } + } + + selectline(selectedlineid); + + var now = new Date(); + console.log("rendered in " + (now.getTime() - epoch.getTime()) + "ms"); + + var pos = document.getElementById("position"); + pos.textContent = Math.round((parseInt(d.firstChild.id) / text.length) * 1000) / 10 + "%"; +} + +function _hint(h) +{ + document.getElementById("hint").innerHTML = h; +} + +function _gfilteron() +{ + if (!filterswitch) + return false; + + if (shl && shl.filter) + return true; + + for (var h in hl) + { + if (hl[h].filter) + return true; + } + + return false; +} + +function _getfilterelement(filter) +{ + if (filter == 0) + return document.getElementById("search"); + + return document.getElementById("filter" + filter); +} + +function _setfilterelementstate(p0, _hl) +{ + p0.style.textDecoration = _hl.filter ? "underline" : ""; +} + +function _triminput(t) +{ + t = t + .replace(/^\s+/, "") + .replace(/\s+$/, "") + ; + return t; +} + +function _regexpescape(t) +{ + t = t + .replace(/\\/g, "\\\\") + .replace(/\?/g, "\\?") + .replace(/\./g, "\\.") + .replace(/\+/g, "\\+") + .replace(/\*/g, "\\*") + .replace(/\^/g, "\\^") + .replace(/\$/g, "\\$") + .replace(/\(/g, "\\(") + .replace(/\)/g, "\\)") + .replace(/\[/g, "\\[") + .replace(/\]/g, "\\]") + .replace(/\|/g, "\\|") + ; + return t; +} + +function resetuistate() +{ + groupwith = false; + _hint(""); +} + +function newhl(t, p, persistent) +{ + return { + id: persistent ? nextFilterId++ : 0, + text: t, + text_r: _sanitize(_regexpescape(t)), + color: p ? p.color : colors[0], + filter: p ? p.filter : false, + cache: {} + }; +} + +function selectline(id) +{ + var l0 = document.getElementById(selectedlineid); + if (l0) + l0.style.backgroundColor = ""; + + var l1 = null; + if (typeof(id) == "object") { + l1 = id; + id = parseInt(l1.id); + } else { + l1 = document.getElementById(id); + } + + selectedlineid = id; + if (selectedlineid > -1) + _hint("line # " + (selectedlineid + 1)); + + if (l1) { + l1.style.background = "#faa"; + } + + var thread = null; + var m = text[selectedlineid].match(threadregexp); + if (m) thread = _regexpescape(_sanitize(m[1])); + if (thread != selectedthread) { + selectedthread = thread; + repaint(); + } + + return l1; +} + +function mouseup(event) +{ + if (event.ctrlKey) + return; + + resetuistate(); + + var s = window.getSelection(); + var t = _triminput(s.toString()); + if (!t) + return; + + s = document.getElementById("search"); + s.value = t; + + t = _prepare(t)[0]; + + shl = newhl(t, shl); + selectfilter(0); + repaint(); +} + +function persist() +{ + resetuistate(); + + var dorender = false; + if (!shl) + { + _apply(); + dorender = true; + } + + if (!shl) + return; + + selectfilter(0); + + var _hl = newhl(shl.text, shl, true); + _hl.cache = shl.cache; // hope this is right, shl is updated in _apply, that always creates an empty cache + hl.push(_hl); + + var p = document.getElementById("persistents"); + var p0 = document.createElement("div"); + p0.id = "filter" + _hl.id; + p0.className = "persistent"; + p0.style.backgroundColor = _hl.color; + p0.innerHTML = _hl.text; + p0.onclick = function() {selectfilter(_hl.id)}; + _setfilterelementstate(p0, _hl); + p.appendChild(p0); + + _restartshl(); + selectfilter(_hl.id); + if (dorender) + render(); + + colors.push(colors.shift()); +} + +function _apply() +{ + s = document.getElementById("search"); + var t = _triminput(s.value); + + if (!t) + { + shl = null; + } + else + { + t = _prepare(t)[0]; + shl = newhl(t, shl); + } +} + +function highlight() +{ + resetuistate(); + + _apply(); + + repaint(); + selectfilter(0); +} + +function filter() +{ + resetuistate(); + + if ((!shl && !hl.length) || (current == shl)) + { + _apply(); + selectfilter(0); + } + + if (current) { + current.filter = !current.filter; + _setfilterelementstate(_getfilterelement(current.id), current); + if (filterswitch) + render(); + else + repaint(); + } +} + +function group() +{ + resetuistate(); + + // the code it self happens in selectfilter() function + + if (!shl) + { + _hint("press higlight or filter first"); + return; + } + + if (hl.length) + { + groupwith = true; + _hint("-> now select a pinned filter to group the current highlight with"); + } + else + { + _hint("you have to pin a filter with the 'pin' button first"); + } +} + +function _restartshl() +{ + var s = document.getElementById("search"); + s.value = ""; + s.style.backgroundColor = ""; + s.style.textDecoration = ""; + current = shl = null; +} + +function restart() +{ + resetuistate(); + + var filtered = _gfilteron(); // was: = current && current.filter + + if (current == shl) + { + _restartshl(); + } + else + { + var p0 = _getfilterelement(current.id); + + for (var h in hl) + if (hl[h].id == current.id) { + hl.splice(h, 1); + break; + } + + if (current.text) { + shl = newhl(current.text, current); + var s = document.getElementById("search"); + s.value = current.text; + _setfilterelementstate(s, shl); + } + selectfilter(0); + + var p = document.getElementById("persistents"); + p.removeChild(p0); + } + + if (!shl) // means: filter could not be switched back to shl or directly shl was reset + render(); +} + +function selectfilter(filter) +{ + var el0 = _getfilterelement(current ? current.id : 0); + var el1 = _getfilterelement(filter); + + el0.style.border = ""; + el0.style.margin = ""; + el1.style.border = "solid 2px #3ad"; + el1.style.margin = "0px"; + + function filterbyid(id) + { + for (var h in hl) + if (hl[h].id == id) + return hl[h]; + } + + if (groupwith && filter) + { + el1.innerHTML += "+" + shl.text; + var _hl = filterbyid(filter); + _hl.text = ""; // not backward compatible + _hl.text_r += "|" + shl.text_r; + _hl.cache = {}; + resetuistate(); + _restartshl(); + render(); + } + else + resetuistate(); + + current = (filter == 0) ? shl : filterbyid(filter); + + // A bit hacky redraw of the search box color :) + if (filter === 0 && shl) + { + var s = document.getElementById("search"); + s.style.backgroundColor = shl.color; + } +} + +function flipfilter(event) +{ + filterswitch = !filterswitch; + event.target.innerHTML = (filterswitch ? "filters on" : "filters off"); + render(); + + event.stopPropagation(); +} diff --git a/cps/templates/admin.html b/cps/templates/admin.html index b7d156c8..a89193d0 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -106,6 +106,7 @@

{{_('Administration')}}

+
{{_('Reconnect to Calibre DB')}}
{{_('Restart Calibre-Web')}}
{{_('Stop Calibre-Web')}}
diff --git a/cps/templates/logviewer.html b/cps/templates/logviewer.html new file mode 100644 index 00000000..0f01f773 --- /dev/null +++ b/cps/templates/logviewer.html @@ -0,0 +1,141 @@ +{% extends "layout.html" %} +{% block body %} +{% block header %} + +{% endblock %} + +
+ +
apply
+
filters on
+
+
filter
+
+
+
+
+ +
+
+
+ +   + + +
+
+
{{warning}}
+
+
+ +
nothing loaded
+ {% for line in log %} + + {% endfor %} +
{{line.line}}{{line.date}}{{line.level}}{{line.message}}
+
+
+
+ + + +
+
+{% endblock %} +{% block js %} + + + +{% endblock %} diff --git a/setup.cfg b/setup.cfg index 470a13d9..700e6e3d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,6 @@ universal = 1 [metadata] name = calibreweb -version= 0.6.3 url = https://github.com/janeczku/calibre-web project_urls = Bug Tracker = https://github.com/janeczku/calibre-web/issues @@ -38,8 +37,6 @@ console_scripts = cps = calibreweb:main [options] python_requires = >=2.6 -package_dir= - =src include_package_data = True zip_safe = False install_requires = diff --git a/setup.py b/setup.py index 4fc653b0..05dd947c 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,25 @@ # """Calibre-web distribution package setuptools installer.""" from setuptools import setup +import os +import re +import codecs + +here = os.path.abspath(os.path.dirname(__file__)) + +def read(*parts): + with codecs.open(os.path.join(here, *parts), 'r') as fp: + return fp.read() + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^STABLE_VERSION\s+=\s+{['\"]version['\"]:\s*['\"](.*)['\"]}", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") setup( - package_dir = {'': 'src'}) + package_dir = {'': 'src'}, + version=find_version("src", "calibreweb", "cps", "constants.py") +)