From c81d4edb7da0d736aa77dcebca1c4ed9f34c0307 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Mon, 10 Jun 2019 16:38:29 +0200 Subject: [PATCH 01/29] Fix for #923 --- cps/static/js/get_meta.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js index 7c5dbe4f..95a28042 100644 --- a/cps/static/js/get_meta.js +++ b/cps/static/js/get_meta.js @@ -20,16 +20,16 @@ * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only) */ /* global _, i18nMsg, tinymce */ -var dbResults = []; +// var dbResults = []; var ggResults = []; $(function () { var msg = i18nMsg; /*var douban = "https://api.douban.com"; var dbSearch = "/v2/book/search";*/ - var dbDone = true; + // var dbDone = true; - var google = "https://www.googleapis.com/"; + var google = "https://www.googleapis.com"; var ggSearch = "/books/v1/volumes"; var ggDone = false; @@ -56,11 +56,9 @@ $(function () { if (showFlag === 1) { $("#meta-info").html(""); } - if (ggDone && dbDone) { - if (!ggResults && !dbResults) { - $("#meta-info").html("

" + msg.no_result + "

"); - return; - } + if (!ggDone) { + $("#meta-info").html("

" + msg.no_result + "

"); + return; } if (ggDone && ggResults.length > 0) { ggResults.forEach(function(result) { @@ -137,10 +135,12 @@ $(function () { dataType: "jsonp", jsonp: "callback", success: function success(data) { - ggResults = data.items; + if ("items" in data) { + ggResults = data.items; + ggDone = true; + } }, complete: function complete() { - ggDone = true; showResult(); $("#show-google").trigger("change"); } From f5e3ed26b935730149c0c39f861b51103b14dd67 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Mon, 10 Jun 2019 19:31:13 +0200 Subject: [PATCH 02/29] Fix for #935 --- cps/web.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cps/web.py b/cps/web.py index 3fc27807..1a3849dd 100644 --- a/cps/web.py +++ b/cps/web.py @@ -3246,10 +3246,16 @@ def edit_user(user_id): if request.method == "POST": to_save = request.form.to_dict() if "delete" in to_save: - ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session.commit() - flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") - return redirect(url_for('admin')) + if ub.session.query(ub.User).filter(and_(ub.User.role.op('&') + (ub.ROLE_ADMIN)== ub.ROLE_ADMIN, + ub.User.id != content.id)).count(): + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session.commit() + flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") + return redirect(url_for('admin')) + else: + flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error") + return redirect(url_for('admin')) else: if "password" in to_save and to_save["password"]: content.password = generate_password_hash(to_save["password"]) From 3f5c6c1fa51134c6fb47e214e1c95bee5b797969 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Thu, 13 Jun 2019 20:01:37 +0200 Subject: [PATCH 03/29] Fix #937 --- cps/static/{ => css}/images/caliblur/blur-dark.png | Bin cps/static/{ => css}/images/caliblur/blur-light.png | Bin cps/static/{ => css}/images/caliblur/blur-noise.png | Bin 3 files changed, 0 insertions(+), 0 deletions(-) rename cps/static/{ => css}/images/caliblur/blur-dark.png (100%) rename cps/static/{ => css}/images/caliblur/blur-light.png (100%) rename cps/static/{ => css}/images/caliblur/blur-noise.png (100%) 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 From e67d707867655ae7046d080b8ac179ba62ab171f Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 15 Jun 2019 11:55:46 +0200 Subject: [PATCH 04/29] Fix for #588 Merge remote-tracking branch 'linuxserver/master' --- cps/ub.py | 30 +++++++++++++++++++++++++++++- readme.md | 30 ++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/cps/ub.py b/cps/ub.py index 61787e57..18cd6975 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -179,6 +179,7 @@ class UserBase: # User Base (all access methods are declared there) class User(UserBase, Base): __tablename__ = 'user' + __table_args__ = {'sqlite_autoincrement': True} id = Column(Integer, primary_key=True) nickname = Column(String(64), unique=True) @@ -715,7 +716,34 @@ def migrate_Database(): conn = engine.connect() conn.execute("ALTER TABLE Settings ADD column `config_updatechannel` INTEGER DEFAULT 0") session.commit() - + try: + # check if one table with autoincrement is existing (should be user table) + conn = engine.connect() + conn.execute("SELECT COUNT(*) FROM sqlite_sequence WHERE name='user'") + except exc.OperationalError: + # Create new table user_id and copy contents of table user into it + conn = engine.connect() + conn.execute("CREATE TABLE user_id (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," + "nickname VARCHAR(64)," + "email VARCHAR(120)," + "role SMALLINT," + "password VARCHAR," + "kindle_mail VARCHAR(120)," + "locale VARCHAR(2)," + "sidebar_view INTEGER," + "default_language VARCHAR(3)," + "mature_content BOOLEAN," + "UNIQUE (nickname)," + "UNIQUE (email)," + "CHECK (mature_content IN (0, 1)))") + conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," + "sidebar_view, default_language, mature_content) " + "SELECT id, nickname, email, role, password, kindle_mail, locale," + "sidebar_view, default_language, mature_content FROM user") + # delete old user table and rename new user_id table to user: + conn.execute("DROP TABLE user") + conn.execute("ALTER TABLE user_id RENAME TO user") + session.commit() # Remove login capability of user Guest conn = engine.connect() diff --git a/readme.md b/readme.md index f06d50da..bc314c02 100644 --- a/readme.md +++ b/readme.md @@ -56,20 +56,30 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe [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 -For further informations, How To's and FAQ please check the ![Wiki](../../wiki) +For further informations, How To's and FAQ please check the ![Wiki](../../wiki) \ No newline at end of file From cd546eb6d4163fb4faffe1de366ae7e5eb626151 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Mon, 17 Jun 2019 19:48:17 +0200 Subject: [PATCH 05/29] Update comic reader js --- cps/static/js/archive/archive.js | 47 ++++--- cps/static/js/archive/rarvm.js | 218 ++++++++++++++++--------------- cps/static/js/archive/unrar.js | 133 ++++++++++--------- cps/static/js/archive/untar.js | 23 ++-- cps/static/js/archive/unzip.js | 36 +++-- cps/static/js/io/bitstream.js | 42 +++--- cps/static/js/io/bytebuffer.js | 21 +-- cps/static/js/io/bytestream.js | 20 +-- cps/static/js/kthoom.js | 63 ++++----- 9 files changed, 311 insertions(+), 292 deletions(-) mode change 100644 => 100755 cps/static/js/io/bitstream.js diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js index 0694ea22..331997d9 100644 --- a/cps/static/js/archive/archive.js +++ b/cps/static/js/archive/archive.js @@ -8,7 +8,7 @@ * Copyright(c) 2011 Google Inc. */ -/* global bitjs */ +/* global bitjs, Uint8Array */ var bitjs = bitjs || {}; bitjs.archive = bitjs.archive || {}; @@ -17,7 +17,7 @@ bitjs.archive = bitjs.archive || {}; // =========================================================================== // Stolen from Closure because it's the best way to do Java-like inheritance. - bitjs.base = function(me, opt_methodName, var_args) { + bitjs.base = function(me, optMethodName, varArgs) { var caller = arguments.callee.caller; if (caller.superClass_) { // This is a constructor. Call the superclass constructor. @@ -28,10 +28,10 @@ bitjs.archive = bitjs.archive || {}; var args = Array.prototype.slice.call(arguments, 2); var foundCaller = false; for (var ctor = me.constructor; ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) { - if (ctor.prototype[opt_methodName] === caller) { + if (ctor.prototype[optMethodName] === caller) { foundCaller = true; } else if (foundCaller) { - return ctor.prototype[opt_methodName].apply(me, args); + return ctor.prototype[optMethodName].apply(me, args); } } @@ -39,8 +39,8 @@ bitjs.archive = bitjs.archive || {}; // then one of two things happened: // 1) The caller is an instance method. // 2) This method was not called by the right caller. - if (me[opt_methodName] === caller) { - return me.constructor.prototype[opt_methodName].apply(me, args); + if (me[optMethodName] === caller) { + return me.constructor.prototype[optMethodName].apply(me, args); } else { throw Error( "goog.base called from a method of one name " + @@ -49,10 +49,10 @@ bitjs.archive = bitjs.archive || {}; }; bitjs.inherits = function(childCtor, parentCtor) { /** @constructor */ - function tempCtor() {}; - tempCtor.prototype = parentCtor.prototype; + function TempCtor() {} + TempCtor.prototype = parentCtor.prototype; childCtor.superClass_ = parentCtor.prototype; - childCtor.prototype = new tempCtor(); + childCtor.prototype = new TempCtor(); childCtor.prototype.constructor = childCtor; }; // =========================================================================== @@ -188,10 +188,10 @@ bitjs.archive = bitjs.archive || {}; * Base class for all Unarchivers. * * @param {ArrayBuffer} arrayBuffer The Array Buffer. - * @param {string} opt_pathToBitJS Optional string for where the BitJS files are located. + * @param {string} optPathToBitJS Optional string for where the BitJS files are located. * @constructor */ - bitjs.archive.Unarchiver = function(arrayBuffer, opt_pathToBitJS) { + bitjs.archive.Unarchiver = function(arrayBuffer, optPathToBitJS) { /** * The ArrayBuffer object. * @type {ArrayBuffer} @@ -204,7 +204,7 @@ bitjs.archive = bitjs.archive || {}; * @type {string} * @private */ - this.pathToBitJS_ = opt_pathToBitJS || "/"; + this.pathToBitJS_ = optPathToBitJS || "/"; /** * A map from event type to an array of listeners. @@ -319,8 +319,8 @@ bitjs.archive = bitjs.archive || {}; * @extends {bitjs.archive.Unarchiver} * @constructor */ - bitjs.archive.Unzipper = function(arrayBuffer, opt_pathToBitJS) { - bitjs.base(this, arrayBuffer, opt_pathToBitJS); + bitjs.archive.Unzipper = function(arrayBuffer, optPathToBitJS) { + bitjs.base(this, arrayBuffer, optPathToBitJS); }; bitjs.inherits(bitjs.archive.Unzipper, bitjs.archive.Unarchiver); bitjs.archive.Unzipper.prototype.getScriptFileName = function() { @@ -332,8 +332,8 @@ bitjs.archive = bitjs.archive || {}; * @extends {bitjs.archive.Unarchiver} * @constructor */ - bitjs.archive.Unrarrer = function(arrayBuffer, opt_pathToBitJS) { - bitjs.base(this, arrayBuffer, opt_pathToBitJS); + bitjs.archive.Unrarrer = function(arrayBuffer, optPathToBitJS) { + bitjs.base(this, arrayBuffer, optPathToBitJS); }; bitjs.inherits(bitjs.archive.Unrarrer, bitjs.archive.Unarchiver); bitjs.archive.Unrarrer.prototype.getScriptFileName = function() { @@ -345,8 +345,8 @@ bitjs.archive = bitjs.archive || {}; * @extends {bitjs.archive.Unarchiver} * @constructor */ - bitjs.archive.Untarrer = function(arrayBuffer, opt_pathToBitJS) { - bitjs.base(this, arrayBuffer, opt_pathToBitJS); + bitjs.archive.Untarrer = function(arrayBuffer, optPathToBitJS) { + bitjs.base(this, arrayBuffer, optPathToBitJS); }; bitjs.inherits(bitjs.archive.Untarrer, bitjs.archive.Unarchiver); bitjs.archive.Untarrer.prototype.getScriptFileName = function() { @@ -357,20 +357,19 @@ bitjs.archive = bitjs.archive || {}; * Factory method that creates an unarchiver based on the byte signature found * in the arrayBuffer. * @param {ArrayBuffer} ab - * @param {string=} opt_pathToBitJS Path to the unarchiver script files. + * @param {string=} optPathToBitJS Path to the unarchiver script files. * @return {bitjs.archive.Unarchiver} */ - bitjs.archive.GetUnarchiver = function(ab, opt_pathToBitJS) { + bitjs.archive.GetUnarchiver = function(ab, optPathToBitJS) { var unarchiver = null; - var pathToBitJS = opt_pathToBitJS || ''; + var pathToBitJS = optPathToBitJS || ""; var h = new Uint8Array(ab, 0, 10); - if (h[0] == 0x52 && h[1] == 0x61 && h[2] == 0x72 && h[3] == 0x21) { // Rar! + if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { // Rar! unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); - } else if (h[0] == 80 && h[1] == 75) { // PK (Zip) + } else if (h[0] === 80 && h[1] === 75) { // PK (Zip) unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); } else { // Try with tar - console.log('geter'); unarchiver = new bitjs.archive.Untarrer(ab, pathToBitJS); } return unarchiver; diff --git a/cps/static/js/archive/rarvm.js b/cps/static/js/archive/rarvm.js index 769c25be..44e09330 100644 --- a/cps/static/js/archive/rarvm.js +++ b/cps/static/js/archive/rarvm.js @@ -9,9 +9,12 @@ /** * CRC Implementation. */ +/* global Uint8Array, Uint32Array, bitjs, DataView */ +/* exported MAXWINMASK, UnpackFilter */ + var CRCTab = new Array(256).fill(0); -function InitCRC() { +function initCRC() { for (var i = 0; i < 256; ++i) { var c = i; for (var j = 0; j < 8; ++j) { @@ -30,8 +33,8 @@ function InitCRC() { * @return {number} */ function CRC(startCRC, arr) { - if (CRCTab[1] == 0) { - InitCRC(); + if (CRCTab[1] === 0) { + initCRC(); } /* @@ -84,7 +87,7 @@ var MAXWINMASK = (MAXWINSIZE - 1); /** */ -var VM_Commands = { +var VmCommands = { VM_MOV: 0, VM_CMP: 1, VM_ADD: 2, @@ -141,7 +144,7 @@ var VM_Commands = { /** */ -var VM_StandardFilters = { +var VmStandardFilters = { VMSF_NONE: 0, VMSF_E8: 1, VMSF_E8E9: 2, @@ -154,7 +157,7 @@ var VM_StandardFilters = { /** */ -var VM_Flags = { +var VmFlags = { VM_FC: 1, VM_FZ: 2, VM_FS: 0x80000000, @@ -162,7 +165,7 @@ var VM_Flags = { /** */ -var VM_OpType = { +var VmOpType = { VM_OPREG: 0, VM_OPINT: 1, VM_OPREGMEM: 2, @@ -186,15 +189,15 @@ function findKeyForValue(obj, val) { } function getDebugString(obj, val) { - var s = 'Unknown.'; - if (obj === VM_Commands) { - s = 'VM_Commands.'; - } else if (obj === VM_StandardFilters) { - s = 'VM_StandardFilters.'; - } else if (obj === VM_Flags) { - s = 'VM_OpType.'; - } else if (obj === VM_OpType) { - s = 'VM_OpType.'; + var s = "Unknown."; + if (obj === VmCommands) { + s = "VmCommands."; + } else if (obj === VmStandardFilters) { + s = "VmStandardFilters."; + } else if (obj === VmFlags) { + s = "VmOpType."; + } else if (obj === VmOpType) { + s = "VmOpType."; } return s + findKeyForValue(obj, val); @@ -204,8 +207,8 @@ function getDebugString(obj, val) { * @struct * @constructor */ -var VM_PreparedOperand = function() { - /** @type {VM_OpType} */ +var VmPreparedOperand = function() { + /** @type {VmOpType} */ this.Type; /** @type {number} */ @@ -220,58 +223,58 @@ var VM_PreparedOperand = function() { }; /** @return {string} */ -VM_PreparedOperand.prototype.toString = function() { +VmPreparedOperand.prototype.toString = function() { if (this.Type === null) { - return 'Error: Type was null in VM_PreparedOperand'; + return "Error: Type was null in VmPreparedOperand"; } - return '{ ' + - 'Type: ' + getDebugString(VM_OpType, this.Type) + - ', Data: ' + this.Data + - ', Base: ' + this.Base + - ' }'; + return "{ " + + "Type: " + getDebugString(VmOpType, this.Type) + + ", Data: " + this.Data + + ", Base: " + this.Base + + " }"; }; /** * @struct * @constructor */ -var VM_PreparedCommand = function() { - /** @type {VM_Commands} */ +var VmPreparedCommand = function() { + /** @type {VmCommands} */ this.OpCode; /** @type {boolean} */ this.ByteMode = false; - /** @type {VM_PreparedOperand} */ - this.Op1 = new VM_PreparedOperand(); + /** @type {VmPreparedOperand} */ + this.Op1 = new VmPreparedOperand(); - /** @type {VM_PreparedOperand} */ - this.Op2 = new VM_PreparedOperand(); + /** @type {VmPreparedOperand} */ + this.Op2 = new VmPreparedOperand(); }; /** @return {string} */ -VM_PreparedCommand.prototype.toString = function(indent) { +VmPreparedCommand.prototype.toString = function(indent) { if (this.OpCode === null) { - return 'Error: OpCode was null in VM_PreparedCommand'; + return "Error: OpCode was null in VmPreparedCommand"; } - indent = indent || ''; - return indent + '{\n' + - indent + ' OpCode: ' + getDebugString(VM_Commands, this.OpCode) + ',\n' + - indent + ' ByteMode: ' + this.ByteMode + ',\n' + - indent + ' Op1: ' + this.Op1.toString() + ',\n' + - indent + ' Op2: ' + this.Op2.toString() + ',\n' + - indent + '}'; + indent = indent || ""; + return indent + "{\n" + + indent + " OpCode: " + getDebugString(VmCommands, this.OpCode) + ",\n" + + indent + " ByteMode: " + this.ByteMode + ",\n" + + indent + " Op1: " + this.Op1.toString() + ",\n" + + indent + " Op2: " + this.Op2.toString() + ",\n" + + indent + "}"; }; /** * @struct * @constructor */ -var VM_PreparedProgram = function() { - /** @type {Array} */ +var VmPreparedProgram = function() { + /** @type {Array} */ this.Cmd = []; - /** @type {Array} */ + /** @type {Array} */ this.AltCmd = null; /** @type {Uint8Array} */ @@ -291,14 +294,14 @@ var VM_PreparedProgram = function() { }; /** @return {string} */ -VM_PreparedProgram.prototype.toString = function() { - var s = '{\n Cmd: [\n'; +VmPreparedProgram.prototype.toString = function() { + var s = "{\n Cmd: [\n"; for (var i = 0; i < this.Cmd.length; ++i) { - s += this.Cmd[i].toString(' ') + ',\n'; + s += this.Cmd[i].toString(" ") + ",\n"; } - s += '],\n'; + s += "],\n"; // TODO: Dump GlobalData, StaticData, InitR? - s += ' }\n'; + s += " }\n"; return s; }; @@ -324,8 +327,8 @@ var UnpackFilter = function() { /** @type {number} */ this.ParentFilter = null; - /** @type {VM_PreparedProgram} */ - this.Prg = new VM_PreparedProgram(); + /** @type {VmPreparedProgram} */ + this.Prg = new VmPreparedProgram(); }; var VMCF_OP0 = 0; @@ -338,7 +341,7 @@ var VMCF_PROC = 16; var VMCF_USEFLAGS = 32; var VMCF_CHFLAGS = 64; -var VM_CmdFlags = [ +var VmCmdFlags = [ /* VM_MOV */ VMCF_OP2 | VMCF_BYTEMODE, /* VM_CMP */ @@ -425,7 +428,7 @@ var VM_CmdFlags = [ /** * @param {number} length * @param {number} crc - * @param {VM_StandardFilters} type + * @param {VmStandardFilters} type * @struct * @constructor */ @@ -436,7 +439,7 @@ var StandardFilterSignature = function(length, crc, type) { /** @type {number} */ this.CRC = crc; - /** @type {VM_StandardFilters} */ + /** @type {VmStandardFilters} */ this.Type = type; }; @@ -444,13 +447,13 @@ var StandardFilterSignature = function(length, crc, type) { * @type {Array} */ var StdList = [ - new StandardFilterSignature(53, 0xad576887, VM_StandardFilters.VMSF_E8), - new StandardFilterSignature(57, 0x3cd7e57e, VM_StandardFilters.VMSF_E8E9), - new StandardFilterSignature(120, 0x3769893f, VM_StandardFilters.VMSF_ITANIUM), - new StandardFilterSignature(29, 0x0e06077d, VM_StandardFilters.VMSF_DELTA), - new StandardFilterSignature(149, 0x1c2c5dc8, VM_StandardFilters.VMSF_RGB), - new StandardFilterSignature(216, 0xbc85e701, VM_StandardFilters.VMSF_AUDIO), - new StandardFilterSignature(40, 0x46b9c560, VM_StandardFilters.VMSF_UPCASE), + new StandardFilterSignature(53, 0xad576887, VmStandardFilters.VMSF_E8), + new StandardFilterSignature(57, 0x3cd7e57e, VmStandardFilters.VMSF_E8E9), + new StandardFilterSignature(120, 0x3769893f, VmStandardFilters.VMSF_ITANIUM), + new StandardFilterSignature(29, 0x0e06077d, VmStandardFilters.VMSF_DELTA), + new StandardFilterSignature(149, 0x1c2c5dc8, VmStandardFilters.VMSF_RGB), + new StandardFilterSignature(216, 0xbc85e701, VmStandardFilters.VMSF_AUDIO), + new StandardFilterSignature(40, 0x46b9c560, VmStandardFilters.VMSF_UPCASE), ]; /** @@ -478,33 +481,34 @@ RarVM.prototype.init = function() { /** * @param {Uint8Array} code - * @return {VM_StandardFilters} + * @return {VmStandardFilters} */ RarVM.prototype.isStandardFilter = function(code) { var codeCRC = (CRC(0xffffffff, code, code.length) ^ 0xffffffff) >>> 0; for (var i = 0; i < StdList.length; ++i) { - if (StdList[i].CRC == codeCRC && StdList[i].Length == code.length) + if (StdList[i].CRC === codeCRC && StdList[i].Length === code.length) { return StdList[i].Type; + } } - return VM_StandardFilters.VMSF_NONE; + return VmStandardFilters.VMSF_NONE; }; /** - * @param {VM_PreparedOperand} op + * @param {VmPreparedOperand} op * @param {boolean} byteMode * @param {bitjs.io.BitStream} bstream A rtl bit stream. */ RarVM.prototype.decodeArg = function(op, byteMode, bstream) { var data = bstream.peekBits(16); if (data & 0x8000) { - op.Type = VM_OpType.VM_OPREG; // Operand is register (R[0]..R[7]) + op.Type = VmOpType.VM_OPREG; // Operand is register (R[0]..R[7]) bstream.readBits(1); // 1 flag bit and... op.Data = bstream.readBits(3); // ... 3 register number bits - op.Addr = [this.R_[op.Data]] // TODO &R[Op.Data] // Register address + op.Addr = [this.R_[op.Data]]; // TODO &R[Op.Data] // Register address } else { - if ((data & 0xc000) == 0) { - op.Type = VM_OpType.VM_OPINT; // Operand is integer + if ((data & 0xc000) === 0) { + op.Type = VmOpType.VM_OPINT; // Operand is integer bstream.readBits(2); // 2 flag bits if (byteMode) { op.Data = bstream.readBits(8); // Byte integer. @@ -513,8 +517,8 @@ RarVM.prototype.decodeArg = function(op, byteMode, bstream) { } } else { // Operand is data addressed by register data, base address or both. - op.Type = VM_OpType.VM_OPREGMEM; - if ((data & 0x2000) == 0) { + op.Type = VmOpType.VM_OPREGMEM; + if ((data & 0x2000) === 0) { bstream.readBits(3); // 3 flag bits // Base address is zero, just use the address from register. op.Data = bstream.readBits(3); // (Data>>10)&7 @@ -522,7 +526,7 @@ RarVM.prototype.decodeArg = function(op, byteMode, bstream) { op.Base = 0; } else { bstream.readBits(4); // 4 flag bits - if ((data & 0x1000) == 0) { + if ((data & 0x1000) === 0) { // Use both register and base address. op.Data = bstream.readBits(3); op.Addr = [this.R_[op.Data]]; // TODO &R[op.Data] @@ -537,7 +541,7 @@ RarVM.prototype.decodeArg = function(op, byteMode, bstream) { }; /** - * @param {VM_PreparedProgram} prg + * @param {VmPreparedProgram} prg */ RarVM.prototype.execute = function(prg) { this.R_.set(prg.InitR); @@ -558,7 +562,7 @@ RarVM.prototype.execute = function(prg) { var preparedCodes = prg.AltCmd ? prg.AltCmd : prg.Cmd; if (prg.Cmd.length > 0 && !this.executeCode(preparedCodes)) { // Invalid VM program. Let's replace it with 'return' command. - preparedCode.OpCode = VM_Commands.VM_RET; + preparedCodes.OpCode = VmCommands.VM_RET; } var dataView = new DataView(this.mem_.buffer, VM_GLOBALMEMADDR); @@ -573,7 +577,7 @@ RarVM.prototype.execute = function(prg) { var dataSize = Math.min(dataView.getUint32(0x30), (VM_GLOBALMEMSIZE - VM_FIXEDGLOBALSIZE)); - if (dataSize != 0) { + if (dataSize !== 0) { var len = dataSize + VM_FIXEDGLOBALSIZE; prg.GlobalData = new Uint8Array(len); prg.GlobalData.set(mem.subarray(VM_GLOBALMEMADDR, VM_GLOBALMEMADDR + len)); @@ -581,7 +585,7 @@ RarVM.prototype.execute = function(prg) { }; /** - * @param {Array} preparedCodes + * @param {Array} preparedCodes * @return {boolean} */ RarVM.prototype.executeCode = function(preparedCodes) { @@ -591,7 +595,7 @@ RarVM.prototype.executeCode = function(preparedCodes) { // when a VM_RET is hit? while (1) { switch (cmd.OpCode) { - case VM_Commands.VM_RET: + case VmCommands.VM_RET: if (this.R_[7] >= VM_MEMSIZE) { return true; } @@ -599,12 +603,12 @@ RarVM.prototype.executeCode = function(preparedCodes) { this.R_[7] += 4; continue; - case VM_Commands.VM_STANDARD: + case VmCommands.VM_STANDARD: this.executeStandardFilter(cmd.Op1.Data); break; default: - console.error('RarVM OpCode not supported: ' + getDebugString(VM_Commands, cmd.OpCode)); + console.error("RarVM OpCode not supported: " + getDebugString(VmCommands, cmd.OpCode)); break; } // switch (cmd.OpCode) codeIndex++; @@ -617,7 +621,7 @@ RarVM.prototype.executeCode = function(preparedCodes) { */ RarVM.prototype.executeStandardFilter = function(filterType) { switch (filterType) { - case VM_StandardFilters.VMSF_DELTA: + case VmStandardFilters.VMSF_DELTA: var dataSize = this.R_[4]; var channels = this.R_[0]; var srcPos = 0; @@ -644,17 +648,19 @@ RarVM.prototype.executeStandardFilter = function(filterType) { break; default: - console.error('RarVM Standard Filter not supported: ' + getDebugString(VM_StandardFilters, filterType)); + console.error("RarVM Standard Filter not supported: " + getDebugString(VmStandardFilters, filterType)); break; } }; /** * @param {Uint8Array} code - * @param {VM_PreparedProgram} prg + * @param {VmPreparedProgram} prg */ RarVM.prototype.prepare = function(code, prg) { var codeSize = code.length; + var i; + var curCmd; //InitBitInput(); //memcpy(InBuf,Code,Min(CodeSize,BitInput::MAX_SIZE)); @@ -662,7 +668,7 @@ RarVM.prototype.prepare = function(code, prg) { // Calculate the single byte XOR checksum to check validity of VM code. var xorSum = 0; - for (var i = 1; i < codeSize; ++i) { + for (i = 1; i < codeSize; ++i) { xorSum ^= code[i]; } @@ -671,20 +677,20 @@ RarVM.prototype.prepare = function(code, prg) { prg.Cmd = []; // TODO: Is this right? I don't see it being done in rarvm.cpp. // VM code is valid if equal. - if (xorSum == code[0]) { + if (xorSum === code[0]) { var filterType = this.isStandardFilter(code); - if (filterType != VM_StandardFilters.VMSF_NONE) { + if (filterType !== VmStandardFilters.VMSF_NONE) { // VM code is found among standard filters. - var curCmd = new VM_PreparedCommand(); + curCmd = new VmPreparedCommand(); prg.Cmd.push(curCmd); - curCmd.OpCode = VM_Commands.VM_STANDARD; + curCmd.OpCode = VmCommands.VM_STANDARD; curCmd.Op1.Data = filterType; // TODO: Addr=&CurCmd->Op1.Data curCmd.Op1.Addr = [curCmd.Op1.Data]; curCmd.Op2.Addr = [null]; // &CurCmd->Op2.Data; - curCmd.Op1.Type = VM_OpType.VM_OPNONE; - curCmd.Op2.Type = VM_OpType.VM_OPNONE; + curCmd.Op1.Type = VmOpType.VM_OPNONE; + curCmd.Op2.Type = VmOpType.VM_OPNONE; codeSize = 0; } @@ -696,7 +702,7 @@ RarVM.prototype.prepare = function(code, prg) { if (dataFlag & 0x8000) { var dataSize = RarVM.readData(bstream) + 1; // TODO: This accesses the byte pointer of the bstream directly. Is that ok? - for (var i = 0; i < bstream.bytePtr < codeSize && i < dataSize; ++i) { + for (i = 0; i < bstream.bytePtr < codeSize && i < dataSize; ++i) { // Append a byte to the program's static data. var newStaticData = new Uint8Array(prg.StaticData.length + 1); newStaticData.set(prg.StaticData); @@ -706,7 +712,7 @@ RarVM.prototype.prepare = function(code, prg) { } while (bstream.bytePtr < codeSize) { - var curCmd = new VM_PreparedCommand(); + curCmd = new VmPreparedCommand(); prg.Cmd.push(curCmd); // Prg->Cmd.Add(1) var flag = bstream.peekBits(1); if (!flag) { // (Data&0x8000)==0 @@ -715,22 +721,22 @@ RarVM.prototype.prepare = function(code, prg) { curCmd.OpCode = (bstream.readBits(6) - 24); } - if (VM_CmdFlags[curCmd.OpCode] & VMCF_BYTEMODE) { - curCmd.ByteMode = (bstream.readBits(1) != 0); + if (VmCmdFlags[curCmd.OpCode] & VMCF_BYTEMODE) { + curCmd.ByteMode = (bstream.readBits(1) !== 0); } else { curCmd.ByteMode = 0; } - curCmd.Op1.Type = VM_OpType.VM_OPNONE; - curCmd.Op2.Type = VM_OpType.VM_OPNONE; - var opNum = (VM_CmdFlags[curCmd.OpCode] & VMCF_OPMASK); + curCmd.Op1.Type = VmOpType.VM_OPNONE; + curCmd.Op2.Type = VmOpType.VM_OPNONE; + var opNum = (VmCmdFlags[curCmd.OpCode] & VMCF_OPMASK); curCmd.Op1.Addr = null; curCmd.Op2.Addr = null; if (opNum > 0) { this.decodeArg(curCmd.Op1, curCmd.ByteMode, bstream); // reading the first operand - if (opNum == 2) { + if (opNum === 2) { this.decodeArg(curCmd.Op2, curCmd.ByteMode, bstream); // reading the second operand } else { - if (curCmd.Op1.Type == VM_OpType.VM_OPINT && (VM_CmdFlags[curCmd.OpCode] & (VMCF_JUMP | VMCF_PROC))) { + if (curCmd.Op1.Type === VmOpType.VM_OPINT && (VmCmdFlags[curCmd.OpCode] & (VMCF_JUMP | VMCF_PROC))) { // Calculating jump distance. var distance = curCmd.Op1.Data; if (distance >= 256) { @@ -756,26 +762,26 @@ RarVM.prototype.prepare = function(code, prg) { } // while ((uint)InAddrOp1.Data curCmd.Op1.Addr = [curCmd.Op1.Data]; curCmd.Op2.Addr = [curCmd.Op2.Data]; - curCmd.Op1.Type = VM_OpType.VM_OPNONE; - curCmd.Op2.Type = VM_OpType.VM_OPNONE; + curCmd.Op1.Type = VmOpType.VM_OPNONE; + curCmd.Op2.Type = VmOpType.VM_OPNONE; // If operand 'Addr' field has not been set by DecodeArg calls above, // let's set it to point to operand 'Data' field. It is necessary for // VM_OPINT type operands (usual integers) or maybe if something was // not set properly for other operands. 'Addr' field is required // for quicker addressing of operand data. - for (var i = 0; i < prg.Cmd.length; ++i) { + for (i = 0; i < prg.Cmd.length; ++i) { var cmd = prg.Cmd[i]; - if (cmd.Op1.Addr == null) { + if (cmd.Op1.Addr === null) { cmd.Op1.Addr = [cmd.Op1.Data]; } - if (cmd.Op2.Addr == null) { + if (cmd.Op2.Addr === null) { cmd.Op2.Addr = [cmd.Op2.Data]; } } @@ -833,7 +839,7 @@ RarVM.readData = function(bstream) { case 1: // 0x4000 // 0x3c00 => 0011 1100 0000 0000 - if (bstream.peekBits(4) == 0) { // (Data&0x3c00)==0 + if (bstream.peekBits(4) === 0) { // (Data&0x3c00)==0 // Skip the 4 zero bits. bstream.readBits(4); // Read in the next 8 and pad with 1s to 32 bits. @@ -855,4 +861,4 @@ RarVM.readData = function(bstream) { } }; -// ============================================================================================== // \ No newline at end of file +// ============================================================================================== // diff --git a/cps/static/js/archive/unrar.js b/cps/static/js/archive/unrar.js index 1094a42b..89263b83 100644 --- a/cps/static/js/archive/unrar.js +++ b/cps/static/js/archive/unrar.js @@ -10,13 +10,14 @@ * * http://kthoom.googlecode.com/hg/docs/unrar.html */ -/* global bitjs, importScripts */ +/* global bitjs, importScripts, RarVM, Uint8Array, UnpackFilter */ +/* global VM_FIXEDGLOBALSIZE, VM_GLOBALMEMSIZE, MAXWINMASK, VM_GLOBALMEMADDR, MAXWINSIZE */ // This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bitstream.js'); -importScripts('../io/bytebuffer.js'); -importScripts('archive.js'); -importScripts('rarvm.js'); +importScripts("../io/bitstream.js"); +importScripts("../io/bytebuffer.js"); +importScripts("archive.js"); +importScripts("rarvm.js"); // Progress variables. var currentFilename = ""; @@ -296,6 +297,12 @@ var rBuffer; */ var wBuffer; +var lowDistRepCount = 0; +var prevLowDist = 0; + +var rOldDist = [0, 0, 0, 0]; +var lastDist; +var lastLength; /** * In unpack.cpp, UnpPtr keeps track of what bytes have been unpacked @@ -401,15 +408,15 @@ function rarDecodeNumber(bstream, dec) { ((bitField < DecodeLen[1]) ? 1 : 2) : ((bitField < DecodeLen[3]) ? 3 : 4)) : (bitField < DecodeLen[6]) ? - ((bitField < DecodeLen[5]) ? 5 : 6) : - ((bitField < DecodeLen[7]) ? 7 : 8)) : + ((bitField < DecodeLen[5]) ? 5 : 6) : + ((bitField < DecodeLen[7]) ? 7 : 8)) : ((bitField < DecodeLen[12]) ? ((bitField < DecodeLen[10]) ? ((bitField < DecodeLen[9]) ? 9 : 10) : ((bitField < DecodeLen[11]) ? 11 : 12)) : (bitField < DecodeLen[14]) ? - ((bitField < DecodeLen[13]) ? 13 : 14) : - 15)); + ((bitField < DecodeLen[13]) ? 13 : 14) : + 15)); bstream.readBits(bits); var N = DecodePos[bits] + ((bitField - DecodeLen[bits - 1]) >>> (16 - bits)); @@ -449,7 +456,7 @@ function rarMakeDecodeTables(BitLength, offset, dec, size) { TmpPos[I] = DecodePos[I]; } for (I = 0; I < size; ++I) { - if (BitLength[I + offset] != 0) { + if (BitLength[I + offset] !== 0) { DecodeNum[TmpPos[BitLength[offset + I] & 0xF]++] = I; } } @@ -524,7 +531,7 @@ function unpack20(bstream) { //, Solid) { if (Distance >= 0x101) { Length++; if (Distance >= 0x2000) { - Length++ + Length++; if (Distance >= 0x40000) { Length++; } @@ -611,13 +618,6 @@ function rarReadTables20(bstream) { } } -var lowDistRepCount = 0; -var prevLowDist = 0; - -var rOldDist = [0, 0, 0, 0]; -var lastDist; -var lastLength; - // ============================================================================================== // // Unpack code specific to RarVM @@ -644,7 +644,7 @@ var OldFilterLengths = []; var LastFilter = 0; -function InitFilters() { +function initFilters() { OldFilterLengths = []; LastFilter = 0; Filters = []; @@ -658,13 +658,14 @@ function InitFilters() { */ function rarAddVMCode(firstByte, vmCode) { VM.init(); + var i; var bstream = new bitjs.io.BitStream(vmCode.buffer, true /* rtl */ ); var filtPos; if (firstByte & 0x80) { filtPos = RarVM.readData(bstream); - if (filtPos == 0) { - InitFilters(); + if (filtPos === 0) { + initFilters(); } else { filtPos--; } @@ -677,7 +678,7 @@ function rarAddVMCode(firstByte, vmCode) { } LastFilter = filtPos; - var newFilter = (filtPos == Filters.length); + var newFilter = (filtPos === Filters.length); // new filter for PrgStack var stackFilter = new UnpackFilter(); @@ -701,10 +702,10 @@ function rarAddVMCode(firstByte, vmCode) { } var emptyCount = 0; - for (var i = 0; i < PrgStack.length; ++i) { + for (i = 0; i < PrgStack.length; ++i) { PrgStack[i - emptyCount] = PrgStack[i]; - if (PrgStack[i] == null) { + if (PrgStack[i] === null) { emptyCount++; } if (emptyCount > 0) { @@ -712,7 +713,7 @@ function rarAddVMCode(firstByte, vmCode) { } } - if (emptyCount == 0) { + if (emptyCount === 0) { PrgStack.push(null); //PrgStack.Add(1); emptyCount = 1; } @@ -734,12 +735,12 @@ function rarAddVMCode(firstByte, vmCode) { OldFilterLengths[filtPos] : 0; } - stackFilter.NextWindow = (wBuffer.ptr != rBuffer.ptr) && + stackFilter.NextWindow = (wBuffer.ptr !== rBuffer.ptr) && (((wBuffer.ptr - rBuffer.ptr) & MAXWINMASK) <= blockStart); OldFilterLengths[filtPos] = stackFilter.BlockLength; - for (var i = 0; i < 7; ++i) { + for (i = 0; i < 7; ++i) { stackFilter.Prg.InitR[i] = 0; } stackFilter.Prg.InitR[3] = VM_GLOBALMEMADDR; @@ -749,7 +750,7 @@ function rarAddVMCode(firstByte, vmCode) { // set registers to optional parameters if any if (firstByte & 0x10) { var initMask = bstream.readBits(7); - for (var i = 0; i < 7; ++i) { + for (i = 0; i < 7; ++i) { if (initMask & (1 << i)) { stackFilter.Prg.InitR[i] = RarVM.readData(bstream); } @@ -758,11 +759,11 @@ function rarAddVMCode(firstByte, vmCode) { if (newFilter) { var vmCodeSize = RarVM.readData(bstream); - if (vmCodeSize >= 0x10000 || vmCodeSize == 0) { + if (vmCodeSize >= 0x10000 || vmCodeSize === 0) { return false; } - var vmCode = new Uint8Array(vmCodeSize); - for (var i = 0; i < vmCodeSize; ++i) { + vmCode = new Uint8Array(vmCodeSize); + for (i = 0; i < vmCodeSize; ++i) { //if (Inp.Overflow(3)) // return(false); vmCode[i] = bstream.readBits(8); @@ -775,7 +776,7 @@ function rarAddVMCode(firstByte, vmCode) { var staticDataSize = filter.Prg.StaticData.length; if (staticDataSize > 0 && staticDataSize < VM_GLOBALMEMSIZE) { // read statically defined data contained in DB commands - for (var i = 0; i < staticDataSize; ++i) { + for (i = 0; i < staticDataSize; ++i) { stackFilter.Prg.StaticData[i] = filter.Prg.StaticData[i]; } } @@ -785,14 +786,14 @@ function rarAddVMCode(firstByte, vmCode) { } var globalData = stackFilter.Prg.GlobalData; - for (var i = 0; i < 7; ++i) { + for (i = 0; i < 7; ++i) { VM.setLowEndianValue(globalData, stackFilter.Prg.InitR[i], i * 4); } VM.setLowEndianValue(globalData, stackFilter.BlockLength, 0x1c); VM.setLowEndianValue(globalData, 0, 0x20); VM.setLowEndianValue(globalData, stackFilter.ExecCount, 0x2c); - for (var i = 0; i < 16; ++i) { + for (i = 0; i < 16; ++i) { globalData[0x30 + i] = 0; } @@ -816,7 +817,7 @@ function rarAddVMCode(firstByte, vmCode) { globalData = newGlobalData; } //byte *GlobalData=&StackFilter->Prg.GlobalData[VM_FIXEDGLOBALSIZE]; - for (var i = 0; i < dataSize; ++i) { + for (i = 0; i < dataSize; ++i) { //if (Inp.Overflow(3)) // return(false); globalData[VM_FIXEDGLOBALSIZE + i] = bstream.readBits(8); @@ -833,9 +834,9 @@ function rarAddVMCode(firstByte, vmCode) { function rarReadVMCode(bstream) { var firstByte = bstream.readBits(8); var length = (firstByte & 7) + 1; - if (length == 7) { + if (length === 7) { length = bstream.readBits(8) + 7; - } else if (length == 8) { + } else if (length === 8) { length = bstream.readBits(16); } @@ -845,7 +846,7 @@ function rarReadVMCode(bstream) { // Do something here with checking readbuf. vmCode[i] = bstream.readBits(8); } - return RarAddVMCode(firstByte, vmCode); + return rarAddVMCode(firstByte, vmCode); } /** @@ -946,7 +947,7 @@ function unpack29(bstream) { continue; } if (num === 258) { - if (lastLength != 0) { + if (lastLength !== 0) { rarCopyString(lastLength, lastDist); } continue; @@ -955,8 +956,8 @@ function unpack29(bstream) { var DistNum = num - 259; Distance = rOldDist[DistNum]; - for (var I = DistNum; I > 0; I--) { - rOldDist[I] = rOldDist[I - 1]; + for (var I2 = DistNum; I2 > 0; I2--) { + rOldDist[I2] = rOldDist[I2 - 1]; } rOldDist[0] = Distance; @@ -990,10 +991,11 @@ function unpack29(bstream) { */ function rarWriteBuf() { var writeSize = (rBuffer.ptr & MAXWINMASK); - + var j; + var flt; for (var i = 0; i < PrgStack.length; ++i) { - var flt = PrgStack[i]; - if (flt == null) { + flt = PrgStack[i]; + if (flt === null) { continue; } @@ -1004,17 +1006,18 @@ function rarWriteBuf() { var blockStart = flt.BlockStart; var blockLength = flt.BlockLength; + var parentPrg; // WrittenBorder = wBuffer.ptr if (((blockStart - wBuffer.ptr) & MAXWINMASK) < writeSize) { - if (wBuffer.ptr != blockStart) { + if (wBuffer.ptr !== blockStart) { // Copy blockStart bytes from rBuffer into wBuffer. rarWriteArea(wBuffer.ptr, blockStart); writeSize = (rBuffer.ptr - wBuffer.ptr) & MAXWINMASK; } if (blockLength <= writeSize) { var blockEnd = (blockStart + blockLength) & MAXWINMASK; - if (blockStart < blockEnd || blockEnd == 0) { + if (blockStart < blockEnd || blockEnd === 0) { VM.setMemory(0, rBuffer.data.subarray(blockStart, blockStart + blockLength), blockLength); } else { var firstPartLength = MAXWINSIZE - blockStart; @@ -1022,7 +1025,7 @@ function rarWriteBuf() { VM.setMemory(firstPartLength, rBuffer.data, blockEnd); } - var parentPrg = Filters[flt.ParentFilter].Prg; + parentPrg = Filters[flt.ParentFilter].Prg; var prg = flt.Prg; if (parentPrg.GlobalData.length > VM_FIXEDGLOBALSIZE) { @@ -1031,10 +1034,11 @@ function rarWriteBuf() { } rarExecuteCode(prg); + var globalDataLen; if (prg.GlobalData.length > VM_FIXEDGLOBALSIZE) { // Save global data for next script execution. - var globalDataLen = prg.GlobalData.length; + globalDataLen = prg.GlobalData.length; if (parentPrg.GlobalData.length < globalDataLen) { parentPrg.GlobalData = new Uint8Array(globalDataLen); } @@ -1050,8 +1054,8 @@ function rarWriteBuf() { PrgStack[i] = null; while (i + 1 < PrgStack.length) { var nextFilter = PrgStack[i + 1]; - if (nextFilter == null || nextFilter.BlockStart != blockStart || - nextFilter.BlockLength != filteredData.length || nextFilter.NextWindow) { + if (nextFilter === null || nextFilter.BlockStart !== blockStart || + nextFilter.BlockLength !== filteredData.length || nextFilter.NextWindow) { break; } @@ -1059,10 +1063,10 @@ function rarWriteBuf() { VM.setMemory(0, filteredData, filteredData.length); - var parentPrg = Filters[nextFilter.ParentFilter].Prg; + parentPrg = Filters[nextFilter.ParentFilter].Prg; var nextPrg = nextFilter.Prg; - var globalDataLen = parentPrg.GlobalData.length; + globalDataLen = parentPrg.GlobalData.length; if (globalDataLen > VM_FIXEDGLOBALSIZE) { // Copy global data from previous script execution if any. nextPrg.GlobalData = new Uint8Array(globalDataLen); @@ -1073,7 +1077,7 @@ function rarWriteBuf() { if (nextPrg.GlobalData.length > VM_GLOBALMEMSIZE) { // Save global data for next script execution. - var globalDataLen = nextPrg.GlobalData.length; + globalDataLen = nextPrg.GlobalData.length; if (parentPrg.GlobalData.length < globalDataLen) { parentPrg.GlobalData = new Uint8Array(globalDataLen); } @@ -1089,15 +1093,14 @@ function rarWriteBuf() { PrgStack[i] = null; } // while (i + 1 < PrgStack.length) - for (var j = 0; j < filteredData.length; ++j) { + for (j = 0; j < filteredData.length; ++j) { wBuffer.insertByte(filteredData[j]); } writeSize = (rBuffer.ptr - wBuffer.ptr) & MAXWINMASK; - } // if (blockLength <= writeSize) - else { - for (var j = i; j < PrgStack.length; ++j) { - var flt = PrgStack[j]; - if (flt != null && flt.NextWindow) { + } else { // if (blockLength <= writeSize) + for (j = i; j < PrgStack.length; ++j) { + flt = PrgStack[j]; + if (flt !== null && flt.NextWindow) { flt.NextWindow = false; } } @@ -1121,7 +1124,7 @@ function rarWriteBuf() { */ function rarWriteArea(startPtr, endPtr) { if (endPtr < startPtr) { - console.error('endPtr < startPtr, endPtr=' + endPtr + ', startPtr=' + startPtr); + console.error("endPtr < startPtr, endPtr=" + endPtr + ", startPtr=" + startPtr); // rarWriteData(startPtr, -(int)StartPtr & MAXWINMASK); // RarWriteData(0, endPtr); return; @@ -1173,7 +1176,7 @@ function rarReadEndOfBlock(bstream) { NewTable = !!bstream.readBits(1); } //tablesRead = !NewTable; - return !(NewFile || NewTable && !rarReadTables(bstream)); + return !(NewFile || (NewTable && !rarReadTables(bstream))); } function rarInsertLastMatch(length, distance) { @@ -1220,8 +1223,8 @@ function rarCopyString(length, distance) { function unpack(v) { // TODO: implement what happens when unpVer is < 15 var Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer; - var Solid = v.header.LHD_SOLID; - var bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */ , v.fileData.byteOffset, v.fileData.byteLength); + // var Solid = v.header.LHD_SOLID; + var bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength); rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize); @@ -1252,7 +1255,7 @@ var RarLocalFile = function(bstream) { this.header = new RarVolumeHeader(bstream); this.filename = this.header.filename; - if (this.header.headType != FILE_HEAD && this.header.headType != ENDARC_HEAD) { + if (this.header.headType !== FILE_HEAD && this.header.headType !== ENDARC_HEAD) { this.isValid = false; info("Error! RAR Volume did not include a FILE_HEAD header "); } else { @@ -1306,7 +1309,7 @@ var unrar = function(arrayBuffer) { info("Found RAR signature"); var mhead = new RarVolumeHeader(bstream); - if (mhead.headType != MAIN_HEAD) { + if (mhead.headType !== MAIN_HEAD) { info("Error! RAR did not include a MAIN_HEAD header"); } else { var localFiles = []; diff --git a/cps/static/js/archive/untar.js b/cps/static/js/archive/untar.js index d6ef3018..d9a1fdfd 100644 --- a/cps/static/js/archive/untar.js +++ b/cps/static/js/archive/untar.js @@ -10,9 +10,11 @@ * TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html */ +/* global bitjs, importScripts, Uint8Array */ + // This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bytestream.js'); -importScripts('archive.js'); +importScripts("../io/bytestream.js"); +importScripts("archive.js"); // Progress variables. var currentFilename = ""; @@ -21,6 +23,7 @@ var currentBytesUnarchivedInFile = 0; var currentBytesUnarchived = 0; var totalUncompressedBytesInArchive = 0; var totalFilesInArchive = 0; +var allLocalFiles = []; // Helper functions. var info = function(str) { @@ -44,8 +47,8 @@ var postProgress = function() { currentBytesUnarchivedInFile, currentBytesUnarchived, totalUncompressedBytesInArchive, - totalFilesInArchive, - )); + totalFilesInArchive + )); }; // takes a ByteStream and parses out the local file information @@ -66,7 +69,7 @@ var TarLocalFile = function(bstream) { this.linkname = readCleanString(bstream, 100); this.maybeMagic = readCleanString(bstream, 6); - if (this.maybeMagic == "ustar") { + if (this.maybeMagic === "ustar") { this.version = readCleanString(bstream, 2); this.uname = readCleanString(bstream, 32); this.gname = readCleanString(bstream, 32); @@ -103,14 +106,14 @@ var TarLocalFile = function(bstream) { } // Round up to 512-byte blocks. - var remaining = 512 - bytesRead % 512; + var remaining = 512 - (bytesRead % 512); if (remaining > 0 && remaining < 512) { bstream.readBytes(remaining); } } else if (this.typeflag == 5) { info(" This is a directory."); } -} +}; var untar = function(arrayBuffer) { postMessage(new bitjs.archive.UnarchiveStartEvent()); @@ -125,7 +128,7 @@ var untar = function(arrayBuffer) { var bstream = new bitjs.io.ByteStream(arrayBuffer); postProgress(); // While we don't encounter an empty block, keep making TarLocalFiles. - while (bstream.peekNumber(4) != 0) { + while (bstream.peekNumber(4) !== 0) { var oneLocalFile = new TarLocalFile(bstream); if (oneLocalFile && oneLocalFile.isValid) { // If we make it to this point and haven't thrown an error, we have successfully @@ -159,8 +162,8 @@ onmessage = function(event) { // Overrun the buffer. // unarchiveState = UnarchiveState.WAITING; } else { - console.error("Found an error while untarring"); - console.log(e); + err("Found an error while untarring"); + err(e); throw e; } } diff --git a/cps/static/js/archive/unzip.js b/cps/static/js/archive/unzip.js index 45cb38ff..f8de27f7 100644 --- a/cps/static/js/archive/unzip.js +++ b/cps/static/js/archive/unzip.js @@ -14,10 +14,10 @@ /* global bitjs, importScripts, Uint8Array*/ // This file expects to be invoked as a Worker (see onmessage below). -importScripts('../io/bitstream.js'); -importScripts('../io/bytebuffer.js'); -importScripts('../io/bytestream.js'); -importScripts('archive.js'); +importScripts("../io/bitstream.js"); +importScripts("../io/bytebuffer.js"); +importScripts("../io/bytestream.js"); +importScripts("archive.js"); // Progress variables. var currentFilename = ""; @@ -118,13 +118,11 @@ ZipLocalFile.prototype.unzip = function() { currentBytesUnarchivedInFile = this.compressedSize; currentBytesUnarchived += this.compressedSize; this.fileData = zeroCompression(this.fileData, this.uncompressedSize); - } - // version == 20, compression method == 8 (DEFLATE) - else if (this.compressionMethod === 8) { + } else if (this.compressionMethod === 8) { + // version == 20, compression method == 8 (DEFLATE) info("ZIP v2.0, DEFLATE: " + this.filename + " (" + this.compressedSize + " bytes)"); this.fileData = inflate(this.fileData, this.uncompressedSize); - } - else { + } else { err("UNSUPPORTED VERSION/FORMAT: ZIP v" + this.version + ", compression method=" + this.compressionMethod + ": " + this.filename + " (" + this.compressedSize + " bytes)"); this.fileData = null; } @@ -497,13 +495,11 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { // copy literal byte to output buffer.insertByte(symbol); blockSize++; - } - else { + } else { // end of block reached if (symbol === 256) { break; - } - else { + } else { var lengthLookup = LengthLookupTable[symbol - 257], length = lengthLookup[1] + bstream.readBits(lengthLookup[0]), distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)], @@ -566,7 +562,7 @@ function inflate(compressedData, numDecompressedBytes) { blockSize = 0; // ++numBlocks; // no compression - if (bType == 0) { + if (bType === 0) { // skip remaining bits in this byte while (bstream.bitPtr !== 0) bstream.readBits(1); var len = bstream.readBits(16); @@ -575,13 +571,11 @@ function inflate(compressedData, numDecompressedBytes) { if (len > 0) buffer.insertBytes(bstream.readBytes(len)); blockSize = len; - } - // fixed Huffman codes - else if (bType === 1) { + } else if (bType === 1) { + // fixed Huffman codes blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer); - } - // dynamic Huffman codes - else if (bType === 2) { + } else if (bType === 2) { + // dynamic Huffman codes var numLiteralLengthCodes = bstream.readBits(5) + 257; var numDistanceCodes = bstream.readBits(5) + 1, numCodeLengthCodes = bstream.readBits(4) + 4; @@ -664,4 +658,4 @@ function inflate(compressedData, numDecompressedBytes) { // event.data.file has the ArrayBuffer. onmessage = function(event) { unzip(event.data.file, true); -}; \ No newline at end of file +}; diff --git a/cps/static/js/io/bitstream.js b/cps/static/js/io/bitstream.js old mode 100644 new mode 100755 index 77ba260f..af1cad99 --- 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/kthoom.js b/cps/static/js/kthoom.js index 9146d603..33a2ac0e 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -15,7 +15,10 @@ * Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6 */ -/* global screenfull, bitjs */ +/* global screenfull, bitjs, Uint8Array, opera */ +/* exported init, event */ + + if (window.opera) { window.console.log = function(str) { opera.postError(str); @@ -101,12 +104,12 @@ kthoom.setSettings = function() { var createURLFromArray = function(array, mimeType) { var offset = array.byteOffset; var len = array.byteLength; - var url; + // var url; var blob; - if (mimeType === 'image/xml+svg') { - const xmlStr = new TextDecoder('utf-8').decode(array); - return 'data:image/svg+xml;UTF-8,' + encodeURIComponent(xmlStr); + if (mimeType === "image/xml+svg") { + var xmlStr = new TextDecoder("utf-8").decode(array); + return "data:image/svg+xml;UTF-8," + encodeURIComponent(xmlStr); } // TODO: Move all this browser support testing to a common place @@ -140,7 +143,7 @@ kthoom.ImageFile = function(file) { var fileExtension = file.filename.split(".").pop().toLowerCase(); this.mimeType = fileExtension === "png" ? "image/png" : (fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" : - fileExtension === "gif" ? "image/gif" : fileExtension == 'svg' ? 'image/xml+svg' : undefined; + fileExtension === "gif" ? "image/gif" : fileExtension === "svg" ? "image/xml+svg" : undefined; if ( this.mimeType !== undefined) { this.dataURI = createURLFromArray(file.fileData, this.mimeType); this.data = file; @@ -154,17 +157,18 @@ function initProgressClick() { currentImage = page; updatePage(); }); -}; +} function loadFromArrayBuffer(ab) { var start = (new Date).getTime(); var h = new Uint8Array(ab, 0, 10); var pathToBitJS = "../../static/js/archive/"; + var lastCompletion = 0; if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); } else if (h[0] === 80 && h[1] === 75) { //PK (Zip) unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); - } else if (h[0] == 255 && h[1] == 216) { // JPEG + } else if (h[0] === 255 && h[1] === 216) { // JPEG // ToDo: check updateProgress(100); lastCompletion = 100; @@ -180,12 +184,12 @@ function loadFromArrayBuffer(ab) { if (totalImages === 0) { totalImages = e.totalFilesInArchive; } - updateProgress(percentage *100); + updateProgress(percentage * 100); lastCompletion = percentage * 100; }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.INFO, function(e) { - // console.log(e.msg); 77 Enable debug output here + // console.log(e.msg); // Enable debug output here }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT, function(e) { @@ -211,8 +215,7 @@ function loadFromArrayBuffer(ab) { if (imageFiles.length === currentImage + 1) { updatePage(lastCompletion); } - } - else { + } else { totalImages--; } } @@ -231,22 +234,22 @@ function loadFromArrayBuffer(ab) { function scrollTocToActive() { // Scroll to the thumbnail in the TOC on page change - $('#tocView').stop().animate({ - scrollTop: $('#tocView a.active').position().top + $("#tocView").stop().animate({ + scrollTop: $("#tocView a.active").position().top }, 200); } function updatePage() { - $('.page').text((currentImage + 1 ) + "/" + totalImages); + $(".page").text((currentImage + 1 ) + "/" + totalImages); // Mark the current page in the TOC - $('#tocView a[data-page]') + $("#tocView a[data-page]") // Remove the currently active thumbnail - .removeClass('active') + .removeClass("active") // Find the new one - .filter('[data-page='+ (currentImage + 1) +']') + .filter("[data-page=" + (currentImage + 1) + "]") // Set it to active - .addClass('active'); + .addClass("active"); scrollTocToActive(); updateProgress(); @@ -270,8 +273,8 @@ function updateProgress(loadPercentage) { if (loadPercentage === 100) { $("#progress") - .removeClass('loading') - .find(".load").text(''); + .removeClass("loading") + .find(".load").text(""); } } @@ -326,7 +329,7 @@ function setImage(url) { xhr.onload = function() { $("#mainText").css("display", ""); $("#mainText").innerHTML(""); - } + }; xhr.send(null); } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { xhr.open("GET", url, true); @@ -378,17 +381,17 @@ function setImage(url) { function showLeftPage() { if (settings.direction === 0) { - showPrevPage() + showPrevPage(); } else { - showNextPage() + showNextPage(); } } function showRightPage() { if (settings.direction === 0) { - showNextPage() + showNextPage(); } else { - showPrevPage() + showPrevPage(); } } @@ -504,7 +507,7 @@ function keyHandler(evt) { updateScale(false); break; case kthoom.Key.SPACE: - var container = $('#mainContent'); + var container = $("#mainContent"); var atTop = container.scrollTop() === 0; var atBottom = container.scrollTop() >= container[0].scrollHeight - container.height(); @@ -577,9 +580,9 @@ function init(filename) { $(this).toggleClass("icon-menu icon-right"); // We need this in a timeout because if we call it during the CSS transition, IE11 shakes the page ¯\_(ツ)_/¯ - setTimeout(function(){ + setTimeout(function() { // Focus on the TOC or the main content area, depending on which is open - $('#main:not(.closed) #mainContent, #sidebar.open #tocView').focus(); + $("#main:not(.closed) #mainContent, #sidebar.open #tocView").focus(); scrollTocToActive(); }, 500); }); @@ -630,7 +633,7 @@ function init(filename) { } // Focus the scrollable area so that keyboard scrolling work as expected - $('#mainContent').focus(); + $("#mainContent").focus(); $("#mainImage").click(function(evt) { // Firefox does not support offsetX/Y so we have to manually calculate From d9f69ca264f1c9ab8c56f1585574cceb93df9646 Mon Sep 17 00:00:00 2001 From: Heimen Stoffels Date: Mon, 17 Jun 2019 22:38:32 +0200 Subject: [PATCH 06/29] Updated Dutch translation --- cps/translations/nl/LC_MESSAGES/messages.mo | Bin 47296 -> 35023 bytes cps/translations/nl/LC_MESSAGES/messages.po | 720 ++++++++++---------- 2 files changed, 357 insertions(+), 363 deletions(-) diff --git a/cps/translations/nl/LC_MESSAGES/messages.mo b/cps/translations/nl/LC_MESSAGES/messages.mo index 6aa3e5f080fbac12572ad79e71031983e17628d4..fc45b5b4b1c134698f7102e93cbe595fcd4ecfb0 100644 GIT binary patch literal 35023 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 diff --git a/cps/translations/nl/LC_MESSAGES/messages.po b/cps/translations/nl/LC_MESSAGES/messages.po index 4862c608..699c442b 100644 --- a/cps/translations/nl/LC_MESSAGES/messages.po +++ b/cps/translations/nl/LC_MESSAGES/messages.po @@ -6,18 +6,19 @@ # FIRST AUTHOR , 2017. msgid "" msgstr "" -"Project-Id-Version: Calibre-Web dutch translation by Ed Driesen (GPL V3)\n" +"Project-Id-Version: Calibre-Web (GPLV3)\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2019-05-30 09:06+0200\n" -"PO-Revision-Date: 2018-12-09 15:07+0100\n" -"Last-Translator: \n" +"PO-Revision-Date: 2019-06-17 22:37+0200\n" +"Last-Translator: Heimen Stoffels \n" "Language: nl\n" -"Language-Team: ed.driesen@telenet.be\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"Language-Team: Dutch , \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.6.0\n" +"X-Generator: Poedit 2.2.3\n" #: cps/book_formats.py:199 cps/book_formats.py:200 cps/book_formats.py:204 #: cps/book_formats.py:208 cps/book_formats.py:212 cps/converter.py:29 @@ -27,11 +28,11 @@ msgstr "niet geïnstalleerd" #: cps/converter.py:40 cps/converter.py:56 msgid "Excecution permissions missing" -msgstr "Rechten om uit te voeren ontbreken" +msgstr "Machtigingen om uit te voeren ontbreken" #: cps/converter.py:66 msgid "not configured" -msgstr "Niet geconfigureerd" +msgstr "niet ingesteld" #: cps/helper.py:79 #, python-format @@ -41,16 +42,16 @@ msgstr "%(format)s formaat niet gevonden voor boek met id: %(book)d" #: cps/helper.py:91 #, python-format msgid "%(format)s not found on Google Drive: %(fn)s" -msgstr "%(format)s niet gevonden op Google Drive: %(fn)s" +msgstr "%(format)s niet aangetroffen op Google Drive: %(fn)s" #: cps/helper.py:98 cps/helper.py:204 cps/templates/detail.html:45 #: cps/templates/detail.html:49 msgid "Send to Kindle" -msgstr "Stuur naar Kindle" +msgstr "Versturen naar Kindle" #: cps/helper.py:99 cps/helper.py:117 cps/helper.py:206 msgid "This e-mail has been sent via Calibre-Web." -msgstr "Deze email werd verzonden via Calibre-Web." +msgstr "Deze e-mail is verstuurd via Calibre-Web." #: cps/helper.py:110 #, python-format @@ -59,11 +60,11 @@ msgstr "%(format)s niet gevonden %(fn)s" #: cps/helper.py:115 msgid "Calibre-Web test e-mail" -msgstr "Calibre-Web test email" +msgstr "Calibre-Web - test-e-mail" #: cps/helper.py:116 msgid "Test e-mail" -msgstr "Test email" +msgstr "Test-e-mail" #: cps/helper.py:132 msgid "Get Started with Calibre-Web" @@ -72,64 +73,64 @@ msgstr "Aan de slag met Calibre-Web" #: cps/helper.py:133 #, python-format msgid "Registration e-mail for user: %(name)s" -msgstr "Registratie email voor gebruiker: %(name)s" +msgstr "Registratie-e-mailadres van gebruiker: %(name)s" #: cps/helper.py:146 cps/helper.py:148 cps/helper.py:150 cps/helper.py:158 #: cps/helper.py:160 cps/helper.py:162 #, python-format msgid "Send %(format)s to Kindle" -msgstr "" +msgstr "%(format)s versturen naar Kindle" #: cps/helper.py:166 #, python-format msgid "Convert %(orig)s to %(format)s and send to Kindle" -msgstr "" +msgstr "%(orig)s converteren naar %(format)s en versturen naar Kindle" #: cps/helper.py:205 #, python-format msgid "E-mail: %(book)s" -msgstr "Email: %(book)s" +msgstr "E-mail: %(book)s" #: cps/helper.py:208 msgid "The requested file could not be read. Maybe wrong permissions?" -msgstr "Het gevraagde bestand kon niet worden gelezen. Misschien niet de juiste permissies?" +msgstr "Het opgevraagde bestand kan niet worden uitgelezen. Ben je hiertoe gemachtigd?" #: cps/helper.py:316 #, python-format msgid "Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s" -msgstr "Hernoemen van titel: '%(src)s' naar '%(dest)s' faade met fout: %(error)s" +msgstr "Kan de naam '%(src)s' niet wijzigen in '%(dest)s': %(error)s" #: cps/helper.py:326 #, python-format msgid "Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s" -msgstr "Hernoemen van de auteur: '%(src)s' naar '%(dest)s' faalde met fout: %(error)s" +msgstr "Kan de auteursnaam '%(src)s' niet wijzigen in '%(dest)s': %(error)s" #: cps/helper.py:340 #, python-format msgid "Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s" -msgstr "" +msgstr "Kan de naam van het bestand in '%(src)s' niet wijzigen in '%(dest)s': %(error)s" #: cps/helper.py:366 cps/helper.py:376 cps/helper.py:384 #, python-format msgid "File %(file)s not found on Google Drive" -msgstr "Bestand %(file)s niet gevonden op Google Drive" +msgstr "Bestand '%(file)s' niet aangetroffen op Google Drive" #: cps/helper.py:405 #, python-format msgid "Book path %(path)s not found on Google Drive" -msgstr "Boek pad %(path)s niet gevonden op Google Drive" +msgstr "Boekpad '%(path)s' niet aangetroffen op Google Drive" #: cps/helper.py:556 msgid "Error excecuting UnRar" -msgstr "Fout bij het uitvoeren van UnRar" +msgstr "Kan UnRar niet uitvoeren" #: cps/helper.py:558 msgid "Unrar binary file not found" -msgstr "Unrar uitvoeringsbestand niet gevonden" +msgstr "Kan uitvoerbaar bestand van UnRar niet vinden" #: cps/helper.py:585 msgid "Waiting" -msgstr "Wachten" +msgstr "Aan het wachten" #: cps/helper.py:587 msgid "Failed" @@ -149,31 +150,31 @@ msgstr "Onbekende status" #: cps/helper.py:598 msgid "E-mail: " -msgstr "Email:" +msgstr "E-mailadres: " #: cps/helper.py:600 cps/helper.py:604 msgid "Convert: " -msgstr "Converteer:" +msgstr "Converteren: " #: cps/helper.py:602 msgid "Upload: " -msgstr "Upload:" +msgstr "Uploaden: " #: cps/helper.py:606 msgid "Unknown Task: " -msgstr "Onbekende taak:" +msgstr "Onbekende taak: " #: cps/updater.py:251 cps/updater.py:410 cps/updater.py:423 msgid "Unexpected data while reading update information" -msgstr "Onverwachte data tijdens het lezen van de update informatie" +msgstr "Onverwachte gegevens tijdens het uitlezen van de update-informatie" #: cps/updater.py:258 cps/updater.py:416 msgid "No update available. You already have the latest version installed" -msgstr "Geen update beschikbaar. Je hebt reeds de laatste versie geïnstalleerd" +msgstr "Geen update beschikbaar; je beschikt al over de nieuwste versie" #: cps/updater.py:270 cps/updater.py:501 cps/updater.py:503 cps/web.py:1206 msgid "HTTP Error" -msgstr "HTTP fout" +msgstr "HTTP-fout" #: cps/updater.py:272 cps/updater.py:505 cps/web.py:1207 msgid "Connection error" @@ -181,7 +182,7 @@ msgstr "Verbindingsfout" #: cps/updater.py:274 cps/updater.py:507 cps/web.py:1208 msgid "Timeout while establishing connection" -msgstr "Time-out bij het maken van de verbinding" +msgstr "Time-out tijdens maken van verbinding" #: cps/updater.py:276 cps/updater.py:509 cps/web.py:1209 msgid "General error" @@ -189,20 +190,20 @@ msgstr "Algemene fout" #: cps/updater.py:283 cps/updater.py:341 cps/updater.py:468 msgid "A new update is available. Click on the button below to update to the latest version." -msgstr "Een nieuwe update is beschikbaar. Click op de knop hier onder op te updaten naar de laatste versie." +msgstr "Er is een update beschikbaar. Klik op de knop hieronder om te updaten naar de nieuwste versie." #: cps/updater.py:335 msgid "Could not fetch update information" -msgstr "De update informatie kon niet gelezen worden" +msgstr "De update-informatie kan niet worden opgehaald" #: cps/updater.py:403 msgid "No release information available" -msgstr "" +msgstr "Geen wijzigingslog beschikbaar" #: cps/updater.py:449 cps/updater.py:458 #, python-format msgid "A new update is available. Click on the button below to update to version: %(version)s" -msgstr "" +msgstr "Er is een update beschikbaar. Klik op de knop hieronder om te updaten naar de nieuwste versie: %(version)s" #: cps/updater.py:491 cps/web.py:2795 msgid "Unknown" @@ -210,35 +211,35 @@ msgstr "Onbekend" #: cps/web.py:1199 msgid "Requesting update package" -msgstr "Update pakket wordt aangevraagd" +msgstr "Bezig met opvragen van updatepakket" #: cps/web.py:1200 msgid "Downloading update package" -msgstr "Update pakket wordt gedownload" +msgstr "Bezig met downloaden van updatepakket" #: cps/web.py:1201 msgid "Unzipping update package" -msgstr "Update pakket wordt uitgepakt" +msgstr "Bezig met uitpakken van updatepakket" #: cps/web.py:1202 msgid "Replacing files" -msgstr "Bestanden aan het vervangen" +msgstr "Bezig met bestandsvervanging" #: cps/web.py:1203 msgid "Database connections are closed" -msgstr "Database verbindingen zijn gesloten" +msgstr "Databankverbindingen zijn gesloten" #: cps/web.py:1204 msgid "Stopping server" -msgstr "Server aan het stoppen" +msgstr "Bezig met stoppen van server" #: cps/web.py:1205 msgid "Update finished, please press okay and reload page" -msgstr "Update voltooid, klik op ok en herlaad de pagina" +msgstr "Update voltooid; klik op 'Oké' en vernieuw de pagina" #: cps/web.py:1206 cps/web.py:1207 cps/web.py:1208 cps/web.py:1209 msgid "Update failed:" -msgstr "Update gefaald:" +msgstr "Update mislukt:" #: cps/web.py:1235 msgid "Recently Added Books" @@ -258,11 +259,11 @@ msgstr "Boeken (A-Z)" #: cps/web.py:1280 msgid "Books (Z-A)" -msgstr "Boeken (A-Z)" +msgstr "Boeken (Z-A)" #: cps/web.py:1309 msgid "Hot Books (most downloaded)" -msgstr "Populaire boeken (meeste downloads)" +msgstr "Populaire boeken (vaakst gedownload)" #: cps/web.py:1322 msgid "Best rated books" @@ -274,7 +275,7 @@ msgstr "Willekeurige boeken" #: cps/web.py:1362 cps/web.py:1618 cps/web.py:2161 msgid "Error opening eBook. File does not exist or file is not accessible:" -msgstr "Fout bij openen van het boek. Bestand bestaat niet of is niet toegankelijk:" +msgstr "Kan e-boek niet openen. Het bestand bestaat niet of is niet toegankelijk:" #: cps/web.py:1391 msgid "Publisher list" @@ -287,7 +288,7 @@ msgstr "Uitgever: %(name)s" #: cps/templates/index.xml:83 cps/web.py:1438 msgid "Series list" -msgstr "Serie lijst" +msgstr "Serielijst" #: cps/web.py:1452 #, python-format @@ -305,7 +306,7 @@ msgstr "Taal: %(name)s" #: cps/templates/index.xml:76 cps/web.py:1509 msgid "Category list" -msgstr "Categorie lijst" +msgstr "Categorielijst" #: cps/web.py:1523 #, python-format @@ -322,19 +323,19 @@ msgstr "Statistieken" #: cps/web.py:1750 msgid "Google Drive setup not completed, try to deactivate and activate Google Drive again" -msgstr "" +msgstr "Het instellen van Google Drive is niet afgerond; heractiveer Google Drive" #: cps/web.py:1795 msgid "Callback domain is not verified, please follow steps to verify domain in google developer console" -msgstr "Het callback domein is niet geverifieerd, volg de stappen in de google ontwikkelaars console om het domein te verifiëren" +msgstr "Het callback-domein is niet geverifieerd. Volg de stappen in de Google-ontwikkelaarsconsole om het domein te verifiëren." #: cps/web.py:1871 msgid "Server restarted, please reload page" -msgstr "Server herstart, gelieve de pagina herladen" +msgstr "De server is herstart; vernieuw de pagina" #: cps/web.py:1874 msgid "Performing shutdown of server, please close window" -msgstr "Bezig met het stoppen van de server, gelieve venster te sluiten" +msgstr "Bezig het stoppen van server; sluit het venster" #: cps/web.py:1953 msgid "Published after " @@ -342,31 +343,31 @@ msgstr "Gepubliceerd na " #: cps/web.py:1960 msgid "Published before " -msgstr "Gepubliceerd voor " +msgstr "Gepubliceerd vóór " #: cps/web.py:1974 #, python-format msgid "Rating <= %(rating)s" -msgstr "Waardering <= %(rating)s" +msgstr "Beoordeling <= %(rating)s" #: cps/web.py:1976 #, python-format msgid "Rating >= %(rating)s" -msgstr "Waardering >= %(rating)s" +msgstr "Beoordeling >= %(rating)s" #: cps/web.py:2036 cps/web.py:2045 msgid "search" -msgstr "zoek" +msgstr "zoeken" #: cps/templates/index.xml:47 cps/templates/index.xml:51 #: cps/templates/layout.html:148 cps/web.py:2116 msgid "Read Books" -msgstr "Gelezen Boeken" +msgstr "Gelezen boeken" #: cps/templates/index.xml:55 cps/templates/index.xml:59 #: cps/templates/layout.html:150 cps/web.py:2119 msgid "Unread Books" -msgstr "Ongelezen Boeken" +msgstr "Ongelezen boeken" #: cps/web.py:2171 cps/web.py:2173 cps/web.py:2175 cps/web.py:2187 msgid "Read a Book" @@ -374,32 +375,32 @@ msgstr "Lees een boek" #: cps/web.py:2199 msgid "Error opening eBook. Fileformat is not supported." -msgstr "" +msgstr "Kan boek niet openen: het bestandsformaat wordt niet ondersteund." #: cps/web.py:2249 cps/web.py:3170 msgid "Please fill out all fields!" -msgstr "Gelieve alle velden in te vullen!" +msgstr "Vul alle velden in!" #: cps/web.py:2250 cps/web.py:2272 cps/web.py:2276 cps/web.py:2281 #: cps/web.py:2283 msgid "register" -msgstr "registreer" +msgstr "registreren" #: cps/web.py:2271 cps/web.py:3389 msgid "An unknown error occurred. Please try again later." -msgstr "Er was een onbekende fout. Gelieve later nog eens te proberen." +msgstr "Er is een onbekende fout opgetreden. Probeer het later nog eens." #: cps/web.py:2274 msgid "Your e-mail is not allowed to register" -msgstr "Het is niet toegestaan om te registreren met jou email" +msgstr "Dit e-mailadres mag niet worden gebruikt voor registratie" #: cps/web.py:2277 msgid "Confirmation e-mail was send to your e-mail account." -msgstr "Bevestigings email werd verzonden naar jou email account." +msgstr "Er is een bevestigingse-mail verstuurd naar je e-mailadres." #: cps/web.py:2280 msgid "This username or e-mail address is already in use." -msgstr "Deze gebruikersnaam of email adres is reeds in gebruik." +msgstr "Deze gebruikersnaam of e-mailadres is al in gebruik." #: cps/web.py:2297 cps/web.py:2393 #, python-format @@ -408,19 +409,19 @@ msgstr "je bent nu ingelogd als: '%(nickname)s'" #: cps/web.py:2302 msgid "Wrong Username or Password" -msgstr "Verkeerde gebruikersnaam of Wachtwoord" +msgstr "Verkeerde gebruikersnaam of wachtwoord" #: cps/web.py:2308 cps/web.py:2329 msgid "login" -msgstr "login" +msgstr "inloggen" #: cps/web.py:2341 cps/web.py:2372 msgid "Token not found" -msgstr "Token niet gevonden" +msgstr "Toegangssleutel niet gevonden" #: cps/web.py:2349 cps/web.py:2380 msgid "Token has expired" -msgstr "Token is verlopen" +msgstr "Toegangssleutel is verlopen" #: cps/web.py:2357 msgid "Success! Please return to your device" @@ -428,110 +429,110 @@ msgstr "Gelukt! Ga terug naar je apparaat" #: cps/web.py:2407 msgid "Please configure the SMTP mail settings first..." -msgstr "Gelieve de SMTP mail instellingen eerst te configureren..." +msgstr "Stel eerst SMTP-mail in..." #: cps/web.py:2412 #, python-format msgid "Book successfully queued for sending to %(kindlemail)s" -msgstr "Boek met succes in de wachtrij geplaatst om te verzenden naar %(kindlemail)s" +msgstr "Het boek is in de wachtrij geplaatst om te worden verstuurd aan %(kindlemail)s" #: cps/web.py:2416 #, python-format msgid "There was an error sending this book: %(res)s" -msgstr "Er trad een fout op bij het versturen van dit boek: %(res)s" +msgstr "Er is een fout opgetreden bij het versturen van dit boek: %(res)s" #: cps/web.py:2418 cps/web.py:3223 msgid "Please configure your kindle e-mail address first..." -msgstr "Gelieve eerst je kindle mailadres te configureren..." +msgstr "Stel eerst je kindle-mailadres in..." #: cps/web.py:2429 cps/web.py:2481 msgid "Invalid shelf specified" -msgstr "Ongeldige boekenplank gespecificeerd" +msgstr "Ongeldige boekenplank opgegeven" #: cps/web.py:2436 #, python-format msgid "Sorry you are not allowed to add a book to the the shelf: %(shelfname)s" -msgstr "Sorry, jij mag geen boeken toe voegen aan boekenplank: %(shelfname)s" +msgstr "Sorry, je mag geen boeken toevoegen aan de boekenplank '%(shelfname)s'" #: cps/web.py:2444 msgid "You are not allowed to edit public shelves" -msgstr "Jij mag geen publieke boekenplanken bewerken" +msgstr "Je mag openbare boekenplanken niet aanpassen" #: cps/web.py:2453 #, python-format msgid "Book is already part of the shelf: %(shelfname)s" -msgstr "Dit boek maakt al deel uit van boekenplank: %(shelfname)s" +msgstr "Dit boek maakt al deel uit van de boekenplank '%(shelfname)s'" #: cps/web.py:2467 #, python-format msgid "Book has been added to shelf: %(sname)s" -msgstr "Boek werd toegevoegd aan boekenplank: %(sname)s" +msgstr "Het boek is toegevoegd aan de boekenplank '%(sname)s'" #: cps/web.py:2486 #, python-format msgid "You are not allowed to add a book to the the shelf: %(name)s" -msgstr "Jij mag geen boeken plaatsen op boekenplank: %(name)s" +msgstr "Je mag geen boeken plaatsen op de boekenplank '%(name)s'" #: cps/web.py:2491 msgid "User is not allowed to edit public shelves" -msgstr "Gebruiker is niet toegestaan om publieke boekenplanken te bewerken" +msgstr "Gebruiker is niet toegestaan om openbare boekenplanken aan te passen" #: cps/web.py:2509 #, python-format msgid "Books are already part of the shelf: %(name)s" -msgstr "Deze boeken maken reeds deel uit van boekenplank: %(name)s" +msgstr "Deze boeken maken al deel uit van de boekenplank '%(name)s'" #: cps/web.py:2523 #, python-format msgid "Books have been added to shelf: %(sname)s" -msgstr "De boeken werden toegevoegd aan boekenplank: %(sname)s" +msgstr "De boeken zijn toegevoegd aan de boekenplank '%(sname)s'" #: cps/web.py:2525 #, python-format msgid "Could not add books to shelf: %(sname)s" -msgstr "Kon geen boeken toevoegen aan boekenplank: %(sname)s" +msgstr "Kan boeken niet toevoegen aan boekenplank '%(sname)s'" #: cps/web.py:2562 #, python-format msgid "Book has been removed from shelf: %(sname)s" -msgstr "Boek werd verwijderd van boekenplank: %(sname)s" +msgstr "Het boek werd verwijderd van de boekenplank '%(sname)s'" #: cps/web.py:2568 #, python-format msgid "Sorry you are not allowed to remove a book from this shelf: %(sname)s" -msgstr "Sorry, jij mag geen boeken verwijderen van deze boekenplank: %(sname)s" +msgstr "Sorry, je mag geen boeken verwijderen van deze boekenplank: %(sname)s" #: cps/web.py:2589 cps/web.py:2613 #, python-format msgid "A shelf with the name '%(title)s' already exists." -msgstr "Een boekenplank met de naam '%(title)s' bestaat reeds." +msgstr "Er bestaat al een boekenplank met de naam '%(title)s'." #: cps/web.py:2594 #, python-format msgid "Shelf %(title)s created" -msgstr "Boekenplank %(title)s aangemaakt" +msgstr "Boekenplank '%(title)s' is gecreëerd" #: cps/web.py:2596 cps/web.py:2624 msgid "There was an error" -msgstr "Er deed zich een fout voor" +msgstr "Er is een fout opgetreden" #: cps/web.py:2597 cps/web.py:2599 msgid "create a shelf" -msgstr "maak een boekenplank" +msgstr "creëer een boekenplank" #: cps/web.py:2622 #, python-format msgid "Shelf %(title)s changed" -msgstr "Boekenplank %(title)s gewijzigd" +msgstr "Boekenplank '%(title)s' is aangepast" #: cps/web.py:2625 cps/web.py:2627 msgid "Edit a shelf" -msgstr "Bewerk een boekenplank" +msgstr "Pas een boekenplank aan" #: cps/web.py:2648 #, python-format msgid "successfully deleted shelf %(name)s" -msgstr "boekenplank %(name)s succesvol gewist" +msgstr "boekenplank '%(name)s' is verwijderd" #: cps/web.py:2675 #, python-format @@ -540,16 +541,16 @@ msgstr "Boekenplank: '%(name)s'" #: cps/web.py:2678 msgid "Error opening shelf. Shelf does not exist or is not accessible" -msgstr "Fout bij openen boekenplank. Boekenplank bestaat niet of is niet toegankelijk" +msgstr "Kan boekenplank niet openen: de boekenplank bestaat niet of is ontoegankelijk" #: cps/web.py:2709 #, python-format msgid "Change order of Shelf: '%(name)s'" -msgstr "Verander volgorde van Boekenplank: '%(name)s'" +msgstr "Volgorde aanpassen van boekenplank '%(name)s'" #: cps/web.py:2738 cps/web.py:3176 msgid "E-mail is not from valid domain" -msgstr "Email is niet van een geldig domein" +msgstr "Het e-mailadres bevat geen geldige domeinnaam" #: cps/web.py:2740 cps/web.py:2782 cps/web.py:2785 #, python-format @@ -558,23 +559,23 @@ msgstr "%(name)s's profiel" #: cps/web.py:2780 msgid "Found an existing account for this e-mail address." -msgstr "Een bestaand account met dit email adres werd gevonden." +msgstr "Er is een bestaand account met dit e-mailadres aangetroffen." #: cps/web.py:2783 msgid "Profile updated" -msgstr "Profiel aangepast" +msgstr "Profiel bijgewerkt" #: cps/web.py:2814 msgid "Admin page" -msgstr "Administratie pagina" +msgstr "Administratiepagina" #: cps/web.py:2899 cps/web.py:3079 msgid "Calibre-Web configuration updated" -msgstr "Calibre-Web configuratie aangepast" +msgstr "Calibre-Web-configuratie bijgewerkt" #: cps/templates/admin.html:100 cps/web.py:2913 msgid "UI Configuration" -msgstr "Gebruikersinterface configuratie" +msgstr "Uiterlijke instellingen" #: cps/web.py:2931 msgid "Import of optional Google Drive requirements missing" @@ -604,93 +605,93 @@ msgstr "Certificatiebestand (\"certfile\") locatie ongeldig, gelieve het correct #: cps/web.py:3051 msgid "Logfile location is not valid, please enter correct path" -msgstr "Log bestand (\"logfile\") locatie ongeldig, gelieve het correcte pad in te geven" +msgstr "De locatie met logbestanden is ongeldig; geef het juiste pad op" #: cps/web.py:3092 msgid "DB location is not valid, please enter correct path" -msgstr "DB locatie is niet geldig, gelieve het correcte pad in te geven" +msgstr "De DB-locatie is ongeldig; geef het juiste pad op" #: cps/templates/admin.html:33 cps/web.py:3172 cps/web.py:3178 cps/web.py:3194 msgid "Add new user" -msgstr "Voeg nieuwe gebruiker toe" +msgstr "Nieuwe gebruiker toevoegen" #: cps/web.py:3184 #, python-format msgid "User '%(user)s' created" -msgstr "Gebruiker '%(user)s' aangemaakt" +msgstr "Gebruiker '%(user)s' is gecreëerd" #: cps/web.py:3188 msgid "Found an existing account for this e-mail address or nickname." -msgstr "Een bestaande account gevonden met dit email adres of gebruikersnaam." +msgstr "Er is een bestaand account met dit e-mailadres of deze gebruikersnaam aangetroffen." #: cps/web.py:3218 #, python-format msgid "Test e-mail successfully send to %(kindlemail)s" -msgstr "Test email met succes verzonden naar %(kindlemail)s" +msgstr "De test-e-mail is verstuurd naar %(kindlemail)s" #: cps/web.py:3221 #, python-format msgid "There was an error sending the Test e-mail: %(res)s" -msgstr "Er was een fout bij het verzenden van test email: %(res)s" +msgstr "Er is een fout opgetreden bij het versturen van de test-e-mail: %(res)s" #: cps/web.py:3225 msgid "E-mail server settings updated" -msgstr "Email server instellingen aangepast" +msgstr "E-mailserverinstellingen bijgewerkt" #: cps/web.py:3226 msgid "Edit e-mail server settings" -msgstr "Bewerk email server instellingen" +msgstr "E-mailserverinstellingen bewerken" #: cps/web.py:3251 #, python-format msgid "User '%(nick)s' deleted" -msgstr "Gebruiker '%(nick)s' verwijderd" +msgstr "Gebruiker '%(nick)s' is verwijderd" #: cps/web.py:3364 #, python-format msgid "User '%(nick)s' updated" -msgstr "Gebruiker '%(nick)s' aangepast" +msgstr "Gebruiker '%(nick)s' is bijgewerkt" #: cps/web.py:3367 msgid "An unknown error occured." -msgstr "Een onbekende fout deed zich voor." +msgstr "Er is een onbekende fout opgetreden." #: cps/web.py:3369 #, python-format msgid "Edit User %(nick)s" -msgstr "Bewerk gebruiker '%(nick)s" +msgstr "Gebruiker '%(nick)s' bewerken" #: cps/web.py:3386 #, python-format msgid "Password for user %(user)s reset" -msgstr "Wachtwoord voor gebruiker %(user)s gereset" +msgstr "Wachtwoord voor gebruiker %(user)s is hersteld" #: cps/web.py:3400 cps/web.py:3592 msgid "Error opening eBook. File does not exist or file is not accessible" -msgstr "Fout bij openen eBook. Het bestand bestaat niet of is niet toegankelijk" +msgstr "Kan e-boek niet openen: het bestand bestaat niet of is ontoegankelijk" #: cps/web.py:3428 msgid "edit metadata" -msgstr "bewerk metadata" +msgstr "metagegevens bewerken" #: cps/web.py:3521 cps/web.py:3754 #, python-format msgid "File extension '%(ext)s' is not allowed to be uploaded to this server" -msgstr "Het uploaden van bestandsextensie '%(ext)s' is niet toegestaan op deze server" +msgstr "De bestandsextensie '%(ext)s' is niet toegestaan op deze server" #: cps/web.py:3525 cps/web.py:3757 msgid "File to be uploaded must have an extension" -msgstr "Up te loaden bestanden dienen een extensie te hebben" +msgstr "Het te uploaden bestand moet voorzien zijn van een extensie" #: cps/web.py:3537 cps/web.py:3776 #, python-format msgid "Failed to create path %(path)s (Permission denied)." -msgstr "Het pad %(path)s aanmaken mislukt (Geen toestemming)." +msgstr "Kan het pad '%(path)s' niet creëren (niet gemachtigd)." #: cps/web.py:3542 #, python-format msgid "Failed to store file %(file)s." -msgstr "Bestand opslaan niet gelukt voor %(file)s." +msgstr "Kan %(file)s niet opslaan." #: cps/web.py:3559 #, python-format @@ -699,7 +700,7 @@ msgstr "Bestandsformaat %(ext)s toegevoegd aan %(book)s" #: cps/web.py:3573 cps/web.py:3646 msgid "Cover is not a supported imageformat (jpg/png/webp), can't save" -msgstr "" +msgstr "Het omslagbestand is een niet-ondersteund afbeeldingsformaat (jpg/png/webp); kan niet opslaan" #: cps/web.py:3605 cps/web.py:3614 msgid "unknown" @@ -712,50 +713,50 @@ msgstr "%(langname)s is geen geldige taal" #: cps/web.py:3725 msgid "Metadata successfully updated" -msgstr "Metadata succesvol geüpdatet" +msgstr "De metagegevens zijn bijgewerkt" #: cps/web.py:3734 msgid "Error editing book, please check logfile for details" -msgstr "Fout bij het bewerken van het boek, gelieve logfile controleren" +msgstr "Kan het boek niet bewerken; controleer het logbestand" #: cps/web.py:3780 #, python-format msgid "Failed to store file %(file)s (Permission denied)." -msgstr "Bestand %(file)s opslaan mislukt (Geen toestemming)." +msgstr "Kan %(file)s niet opslaan (niet gemachtigd)." #: cps/web.py:3785 #, python-format msgid "Failed to delete file %(file)s (Permission denied)." -msgstr "Bestand %(file)s wissen mislukt (Geen toestemming)." +msgstr "Kan %(file)s niet verwijderen (niet gemachtigd)." #: cps/web.py:3867 #, python-format msgid "File %(title)s" -msgstr "" +msgstr "Bestand %(title)s" #: cps/web.py:3896 msgid "Source or destination format for conversion missing" -msgstr "Bron of doel formaat voor conversie ontbreekt" +msgstr "Bron- of doelformaat ontbreekt voor conversie" #: cps/web.py:3906 #, python-format msgid "Book successfully queued for converting to %(book_format)s" -msgstr "Boek succesvol in de wachtrij geplaatst voor conversie naar %(book_format)s" +msgstr "Het boek is in de wachtrij geplaatst voor conversie naar %(book_format)s" #: cps/web.py:3910 #, python-format msgid "There was an error converting this book: %(res)s" -msgstr "Er trad een fout op bij het converteren van dit boek: %(res)s" +msgstr "Er is een fout opgetreden bij het converteren van dit boek: %(res)s" #: cps/worker.py:305 #, python-format msgid "Ebook-converter failed: %(error)s" -msgstr "Ebook conversie mislukt: %(error)s" +msgstr "E-boek-conversie mislukt: %(error)s" #: cps/worker.py:316 #, python-format msgid "Kindlegen failed with Error %(error)s. Message: %(message)s" -msgstr "Kindlegen gefaald met Error %(error)s. Bericht: %(message)s" +msgstr "Kindlegen mislukt; fout: %(error)s. Bericht: %(message)s" #: cps/templates/admin.html:6 msgid "User list" @@ -767,7 +768,7 @@ msgstr "Gebruikersnaam" #: cps/templates/admin.html:10 msgid "E-mail" -msgstr "Email" +msgstr "E-mailadres" #: cps/templates/admin.html:11 msgid "Kindle" @@ -784,27 +785,27 @@ msgstr "Administratie" #: cps/templates/admin.html:14 cps/templates/detail.html:22 #: cps/templates/detail.html:31 msgid "Download" -msgstr "Download" +msgstr "Downloaden" #: cps/templates/admin.html:15 cps/templates/layout.html:65 msgid "Upload" -msgstr "Upload" +msgstr "Uploaden" #: cps/templates/admin.html:16 msgid "Edit" -msgstr "Bewerk" +msgstr "Bewerken" #: cps/templates/admin.html:39 msgid "SMTP e-mail server settings" -msgstr "SMTP email server instellingen" +msgstr "SMTP-serverinstellingen" #: cps/templates/admin.html:42 cps/templates/email_edit.html:11 msgid "SMTP hostname" -msgstr "SMTP hostnaam" +msgstr "SMTP-hostnaam" #: cps/templates/admin.html:43 msgid "SMTP port" -msgstr "SMTP poort" +msgstr "SMTP-poort" #: cps/templates/admin.html:44 msgid "SSL" @@ -812,27 +813,27 @@ msgstr "SSL" #: cps/templates/admin.html:45 cps/templates/email_edit.html:27 msgid "SMTP login" -msgstr "SMTP login" +msgstr "SMTP-gebruikersnaam" #: cps/templates/admin.html:46 msgid "From mail" -msgstr "Van mail" +msgstr "Van e-mail" #: cps/templates/admin.html:56 msgid "Change SMTP settings" -msgstr "Bewerk SMTP instellingen" +msgstr "SMTP-instellingen bewerken" #: cps/templates/admin.html:62 msgid "Configuration" -msgstr "Configuratie" +msgstr "Instellingen" #: cps/templates/admin.html:65 msgid "Calibre DB dir" -msgstr "Calibre DB map" +msgstr "Calibre DB-map" #: cps/templates/admin.html:69 msgid "Log level" -msgstr "Log niveau" +msgstr "Logniveau" #: cps/templates/admin.html:73 msgid "Port" @@ -840,11 +841,11 @@ msgstr "Poort" #: cps/templates/admin.html:79 cps/templates/config_view_edit.html:23 msgid "Books per page" -msgstr "Boeken per pagina" +msgstr "Aantal boeken per pagina" #: cps/templates/admin.html:83 msgid "Uploading" -msgstr "Aan het uploaden" +msgstr "Bezig met uploaden" #: cps/templates/admin.html:87 msgid "Anonymous browsing" @@ -852,11 +853,11 @@ msgstr "Anoniem verkennen" #: cps/templates/admin.html:91 msgid "Public registration" -msgstr "Publieke registratie" +msgstr "Openbare registratie" #: cps/templates/admin.html:95 cps/templates/remote_login.html:4 msgid "Remote login" -msgstr "Login op afstand" +msgstr "Inloggen op afstand" #: cps/templates/admin.html:106 msgid "Administration" @@ -864,19 +865,19 @@ msgstr "Administratie" #: cps/templates/admin.html:107 msgid "Reconnect to Calibre DB" -msgstr "Herverbinden met calibre DB" +msgstr "Opnieuw verbinden met Calibre DB" #: cps/templates/admin.html:108 msgid "Restart Calibre-Web" -msgstr "Herstart Calibre-Web" +msgstr "Calibre-Web herstarten" #: cps/templates/admin.html:109 msgid "Stop Calibre-Web" -msgstr "Stop Calibre-Web" +msgstr "Calibre-Web stoppen" #: cps/templates/admin.html:115 msgid "Update" -msgstr "Update" +msgstr "Bijwerken" #: cps/templates/admin.html:119 msgid "Version" @@ -892,20 +893,20 @@ msgstr "Huidige versie" #: cps/templates/admin.html:132 msgid "Check for update" -msgstr "Controleer voor update" +msgstr "Controleren op updates" #: cps/templates/admin.html:133 msgid "Perform Update" -msgstr "Voer update uit" +msgstr "Update uitvoeren" #: cps/templates/admin.html:145 msgid "Do you really want to restart Calibre-Web?" -msgstr "Wil je Calibre-Web echt herstarten?" +msgstr "Weet je zeker dat je Calibre-Web wilt herstarten?" #: cps/templates/admin.html:150 cps/templates/admin.html:164 #: cps/templates/admin.html:184 cps/templates/shelf.html:73 msgid "Ok" -msgstr "Ok" +msgstr "Oké" #: cps/templates/admin.html:151 cps/templates/admin.html:165 #: cps/templates/book_edit.html:178 cps/templates/book_edit.html:200 @@ -919,11 +920,11 @@ msgstr "Terug" #: cps/templates/admin.html:163 msgid "Do you really want to stop Calibre-Web?" -msgstr "Wil je Calibre-Web echt stoppen?" +msgstr "Weet je zeker dat je Calibre-Web wilt stoppen?" #: cps/templates/admin.html:175 msgid "Updating, please do not reload page" -msgstr "Aan het updaten, gelieve de pagina niet te herladen" +msgstr "Bezig met bijwerken; vernieuw de pagina niet" #: cps/templates/author.html:15 msgid "via" @@ -931,14 +932,14 @@ msgstr "via" #: cps/templates/author.html:23 msgid "In Library" -msgstr "In Bibliotheek" +msgstr "In bibliotheek" #: cps/templates/author.html:50 cps/templates/author.html:97 #: cps/templates/discover.html:28 cps/templates/index.html:31 #: cps/templates/index.html:86 cps/templates/search.html:55 #: cps/templates/shelf.html:37 msgid "reduce" -msgstr "" +msgstr "beperken" #: cps/templates/author.html:81 msgid "More by" @@ -946,40 +947,40 @@ msgstr "Meer van" #: cps/templates/book_edit.html:16 msgid "Delete Book" -msgstr "Wis boek" +msgstr "Boek verwijderen" #: cps/templates/book_edit.html:19 msgid "Delete formats:" -msgstr "Wis formaten:" +msgstr "Formaten verwijderen:" #: cps/templates/book_edit.html:22 cps/templates/book_edit.html:199 #: cps/templates/email_edit.html:73 cps/templates/email_edit.html:74 msgid "Delete" -msgstr "Wis" +msgstr "Verwijderen" #: cps/templates/book_edit.html:30 msgid "Convert book format:" -msgstr "Converteer boek formaat:" +msgstr "Boekformaat converteren:" #: cps/templates/book_edit.html:34 msgid "Convert from:" -msgstr "Converteer van:" +msgstr "Converteren van:" #: cps/templates/book_edit.html:36 cps/templates/book_edit.html:43 msgid "select an option" -msgstr "selecteer een optie" +msgstr "kies een optie" #: cps/templates/book_edit.html:41 msgid "Convert to:" -msgstr "Converteer naar:" +msgstr "Converteren naar:" #: cps/templates/book_edit.html:50 msgid "Convert book" -msgstr "Converteer boek" +msgstr "Boek converteren" #: cps/templates/book_edit.html:59 cps/templates/search_form.html:6 msgid "Book Title" -msgstr "Boek titel" +msgstr "Boektitel" #: cps/templates/book_edit.html:63 cps/templates/book_edit.html:259 #: cps/templates/book_edit.html:277 cps/templates/search_form.html:10 @@ -993,16 +994,16 @@ msgstr "Omschrijving" #: cps/templates/book_edit.html:71 cps/templates/search_form.html:33 msgid "Tags" -msgstr "Tags" +msgstr "Labels" #: cps/templates/book_edit.html:75 cps/templates/layout.html:159 #: cps/templates/search_form.html:53 msgid "Series" -msgstr "Series" +msgstr "Serie" #: cps/templates/book_edit.html:79 msgid "Series id" -msgstr "Series id" +msgstr "Serie-id" #: cps/templates/book_edit.html:83 msgid "Rating" @@ -1010,15 +1011,15 @@ msgstr "Beoordeling" #: cps/templates/book_edit.html:87 msgid "Cover URL (jpg, cover is downloaded and stored in database, field is afterwards empty again)" -msgstr "Boekomslag URL (jpg, omslag wordt gedownload en opgeslagen in database, invulveld is nadien terug leeg)" +msgstr "Boekomslag-url (jpg - de omslag wordt gedownload en opgeslagen in de databank; het invoerveld is nadien leeg)" #: cps/templates/book_edit.html:91 msgid "Upload Cover from local drive" -msgstr "Upload cover van lokale schijf" +msgstr "Omslag uploaden vanaf computer" #: cps/templates/book_edit.html:96 cps/templates/detail.html:148 msgid "Publishing date" -msgstr "Publicatie datum" +msgstr "Publicatiedatum" #: cps/templates/book_edit.html:103 cps/templates/book_edit.html:261 #: cps/templates/book_edit.html:278 cps/templates/detail.html:139 @@ -1040,15 +1041,15 @@ msgstr "Nee" #: cps/templates/book_edit.html:164 msgid "Upload format" -msgstr "Upload type" +msgstr "Uploadformaat" #: cps/templates/book_edit.html:173 msgid "view book after edit" -msgstr "bekijk boek na bewerking" +msgstr "boek inkijken na bewerking" #: cps/templates/book_edit.html:176 cps/templates/book_edit.html:212 msgid "Get metadata" -msgstr "Verkrijg metadata" +msgstr "Metagegevens ophalen" #: cps/templates/book_edit.html:177 cps/templates/config_edit.html:224 #: cps/templates/config_view_edit.html:178 cps/templates/login.html:20 @@ -1059,11 +1060,11 @@ msgstr "Opslaan" #: cps/templates/book_edit.html:191 msgid "Are you really sure?" -msgstr "Ben je zeker?" +msgstr "Weet je het zeker?" #: cps/templates/book_edit.html:194 msgid "Book will be deleted from Calibre database" -msgstr "Boek wordt nu gewist uit de Calibre database" +msgstr "Het boek wordt verwijderd uit de Calibre-databank" #: cps/templates/book_edit.html:195 msgid "and from hard disk" @@ -1071,28 +1072,28 @@ msgstr "en van de harde schijf" #: cps/templates/book_edit.html:215 msgid "Keyword" -msgstr "Zoekwoord" +msgstr "Trefwoord" #: cps/templates/book_edit.html:216 msgid " Search keyword " -msgstr " Zoek sleutelwoord " +msgstr " Trefwoord zoeken " #: cps/templates/book_edit.html:218 cps/templates/layout.html:47 msgid "Go!" -msgstr "Start!" +msgstr "Ga!" #: cps/templates/book_edit.html:222 msgid "Click the cover to load metadata to the form" -msgstr "Klik op de omslag om de metatadata in het formulier te laden" +msgstr "Klik op de omslag om de metagegevens in het formulier te laden" #: cps/templates/book_edit.html:234 cps/templates/book_edit.html:274 msgid "Loading..." -msgstr "Aan het laden..." +msgstr "Bezig met laden..." #: cps/templates/book_edit.html:239 cps/templates/layout.html:226 #: cps/templates/layout.html:258 msgid "Close" -msgstr "Sluit" +msgstr "Sluiten" #: cps/templates/book_edit.html:266 cps/templates/book_edit.html:280 msgid "Source" @@ -1100,19 +1101,19 @@ msgstr "Bron" #: cps/templates/book_edit.html:275 msgid "Search error!" -msgstr "Zoek fout!" +msgstr "Zoekfout!" #: cps/templates/book_edit.html:276 msgid "No Result(s) found! Please try aonther keyword." -msgstr "Geen resultaten gevonden! Gebruik alsjeblieft een ander sleutelwoord." +msgstr "Geen resultaten gevonden! Gebruik een ander trefwoord." #: cps/templates/config_edit.html:12 msgid "Library Configuration" -msgstr "Bibliotheek configuratie" +msgstr "Bibliotheekinstellingen" #: cps/templates/config_edit.html:19 msgid "Location of Calibre database" -msgstr "Locatie van de Calibre database" +msgstr "Locatie van de Calibre-databank" #: cps/templates/config_edit.html:24 msgid "Use Google Drive?" @@ -1120,75 +1121,75 @@ msgstr "Google Drive gebruiken?" #: cps/templates/config_edit.html:30 msgid "Google Drive config problem" -msgstr "Google Drive configuratie probleem" +msgstr "Google Drive-instelprobleem" #: cps/templates/config_edit.html:36 msgid "Authenticate Google Drive" -msgstr "Verifieer Google Drive" +msgstr "Google Drive goedkeuren" #: cps/templates/config_edit.html:40 msgid "Please hit submit to continue with setup" -msgstr "" +msgstr "Druk op 'Opslaan' om door te gaan met instellen" #: cps/templates/config_edit.html:43 msgid "Please finish Google Drive setup after login" -msgstr "Gelieve Google Drive setup te voltooien na login" +msgstr "Voltooi na het inloggen de Google Drive-instelwizard" #: cps/templates/config_edit.html:48 msgid "Google Drive Calibre folder" -msgstr "Google Drive Calibre folder" +msgstr "Google Drive Calibre-map" #: cps/templates/config_edit.html:56 msgid "Metadata Watch Channel ID" -msgstr "Metadata Watch Channel ID" +msgstr "Metagegevens Watch Channel ID" #: cps/templates/config_edit.html:59 msgid "Revoke" -msgstr "Terugtrekken" +msgstr "Intrekken" #: cps/templates/config_edit.html:78 msgid "Server Configuration" -msgstr "Server configuratie" +msgstr "Serverinstellingen" #: cps/templates/config_edit.html:85 msgid "Server Port" -msgstr "Server poort" +msgstr "Serverpoort" #: cps/templates/config_edit.html:89 msgid "SSL certfile location (leave it empty for non-SSL Servers)" -msgstr "SSL certificaat (\"certfile\") bestand locatie (laat leeg voor niet-SSL servers)" +msgstr "SSL-certificaatlocatie ('certfile' - laat leeg voor niet-SSL-servers)" #: cps/templates/config_edit.html:93 msgid "SSL Keyfile location (leave it empty for non-SSL Servers)" -msgstr "SSL sleutel (\"keyfile\") bestand (laat leeg voor niet-SSL servers)" +msgstr "SSL-sleutellocatie ('keyfile' - laat leeg voor niet-SSL-servers)" #: cps/templates/config_edit.html:97 msgid "Update channel" -msgstr "" +msgstr "Updatekanaal" #: cps/templates/config_edit.html:99 msgid "Stable" -msgstr "" +msgstr "Stabiel" #: cps/templates/config_edit.html:100 msgid "Stable (Automatic)" -msgstr "" +msgstr "Stabiel (automatisch)" #: cps/templates/config_edit.html:101 msgid "Nightly" -msgstr "" +msgstr "Bèta" #: cps/templates/config_edit.html:102 msgid "Nightly (Automatic)" -msgstr "" +msgstr "Bèta (automatisch)" #: cps/templates/config_edit.html:113 msgid "Logfile Configuration" -msgstr "Logbestand configuratie" +msgstr "Logbestand-instellingen" #: cps/templates/config_edit.html:120 msgid "Log Level" -msgstr "Log niveau" +msgstr "Logniveau" #: cps/templates/config_edit.html:129 msgid "Location and name of logfile (calibre-web.log for no entry)" @@ -1196,39 +1197,39 @@ msgstr "Locatie en naam van logbestand (calibre-web.log indien leeg)" #: cps/templates/config_edit.html:140 msgid "Feature Configuration" -msgstr "Voorzieningen configuratie" +msgstr "Mogelijkheden" #: cps/templates/config_edit.html:148 msgid "Enable uploading" -msgstr "Uploaden aanzetten" +msgstr "Uploaden inschakelen" #: cps/templates/config_edit.html:152 msgid "Enable anonymous browsing" -msgstr "Anoniem verkennen aanzetten" +msgstr "Anoniem verkennen inschakelen" #: cps/templates/config_edit.html:156 msgid "Enable public registration" -msgstr "Publieke registratie aanzetten" +msgstr "Openbare registratie inschakelen" #: cps/templates/config_edit.html:160 msgid "Enable remote login (\"magic link\")" -msgstr "Maak op afstand ionloggen mogelijk (\"magic link\")" +msgstr "Inloggen op afstand inschakelen ('magic link')" #: cps/templates/config_edit.html:165 msgid "Use" -msgstr "Gebruik" +msgstr "Gebruiken" #: cps/templates/config_edit.html:166 msgid "Obtain an API Key" -msgstr "Verkrijg een API sleutel" +msgstr "API-sleutel verkrijgen" #: cps/templates/config_edit.html:170 msgid "Goodreads API Key" -msgstr "Goodreads API sleutel" +msgstr "Goodreads API-sleutel" #: cps/templates/config_edit.html:174 msgid "Goodreads API Secret" -msgstr "Goodreads API geheim" +msgstr "Goodreads API-geheim" #: cps/templates/config_edit.html:187 msgid "External binaries" @@ -1236,36 +1237,36 @@ msgstr "Externe bibliotheken" #: cps/templates/config_edit.html:195 msgid "No converter" -msgstr "Geen conversie programma" +msgstr "Geen conversieprogramma" #: cps/templates/config_edit.html:197 msgid "Use Kindlegen" -msgstr "Gebruik Kindlegen" +msgstr "Kindlegen gebruiken" #: cps/templates/config_edit.html:199 msgid "Use calibre's ebook converter" -msgstr "Gebruik calibre's ebook converter" +msgstr "Calibre's e-boekconversie gebruiken" #: cps/templates/config_edit.html:203 msgid "E-Book converter settings" -msgstr "E-book conversie instellingen" +msgstr "Conversie-instellingen" #: cps/templates/config_edit.html:207 msgid "Path to convertertool" -msgstr "Pad naar conversietool" +msgstr "Pad naar conversieprogramma" #: cps/templates/config_edit.html:213 msgid "Location of Unrar binary" -msgstr "Locatie van Unrar programma" +msgstr "Locatie van Unrar-programma" #: cps/templates/config_edit.html:229 cps/templates/layout.html:84 #: cps/templates/login.html:4 msgid "Login" -msgstr "Login" +msgstr "Inloggen" #: cps/templates/config_view_edit.html:12 msgid "View Configuration" -msgstr "Bekijk Configuratie" +msgstr "Instellingen bekijken" #: cps/templates/config_view_edit.html:19 cps/templates/layout.html:135 #: cps/templates/layout.html:136 cps/templates/shelf_edit.html:7 @@ -1274,11 +1275,11 @@ msgstr "Titel" #: cps/templates/config_view_edit.html:27 msgid "No. of random books to show" -msgstr "Aantal boeken te tonen" +msgstr "Aantal te tonen willekeurige boeken" #: cps/templates/config_view_edit.html:31 msgid "No. of authors to show before hiding (0=disable hiding)" -msgstr "" +msgstr "Aantal te tonen auteurs alvorens te verbergen (0=nooit verbergen)" #: cps/templates/config_view_edit.html:35 cps/templates/readcbr.html:118 msgid "Theme" @@ -1290,7 +1291,7 @@ msgstr "Standaard thema" #: cps/templates/config_view_edit.html:38 msgid "caliBlur! Dark Theme" -msgstr "caliBlur! Donker Thema" +msgstr "caliBlur! donker thema" #: cps/templates/config_view_edit.html:42 msgid "Regular expression for ignoring columns" @@ -1298,23 +1299,23 @@ msgstr "Reguliere expressie om kolommen te negeren" #: cps/templates/config_view_edit.html:46 msgid "Link read/unread status to Calibre column" -msgstr "Koppel gelezen/ongelezen status aan Calibre kolom" +msgstr "Gelezen/Ongelezen-status koppelen aan Calibre-kolom" #: cps/templates/config_view_edit.html:55 msgid "Regular expression for title sorting" -msgstr "Rguliere expressie op titels te sorteren" +msgstr "Reguliere expressie voor het sorteren op titel" #: cps/templates/config_view_edit.html:59 msgid "Tags for Mature Content" -msgstr "Tags voor Volwassen Inhoud" +msgstr "Labels voor 18+-inhoud" #: cps/templates/config_view_edit.html:73 msgid "Default settings for new users" -msgstr "Standaard instellingen voor nieuwe gebruikers" +msgstr "Standaardinstellingen voor nieuwe gebruikers" #: cps/templates/config_view_edit.html:81 cps/templates/user_edit.html:104 msgid "Admin user" -msgstr "Administratie gebruiker" +msgstr "Admin-gebruiker" #: cps/templates/config_view_edit.html:85 cps/templates/user_edit.html:113 msgid "Allow Downloads" @@ -1330,7 +1331,7 @@ msgstr "Bewerken toestaan" #: cps/templates/config_view_edit.html:97 cps/templates/user_edit.html:125 msgid "Allow Delete books" -msgstr "Het wissen van boeken toestaan" +msgstr "Verwijderen van boeken toestaan" #: cps/templates/config_view_edit.html:101 cps/templates/user_edit.html:130 msgid "Allow Changing Password" @@ -1338,7 +1339,7 @@ msgstr "Wachtwoord wijzigen toestaan" #: cps/templates/config_view_edit.html:105 cps/templates/user_edit.html:134 msgid "Allow Editing Public Shelfs" -msgstr "Publieke boekenplanken bewerken toestaan" +msgstr "Bewerken van openbare boekenplanken toestaan" #: cps/templates/config_view_edit.html:115 msgid "Default visibilities for new users" @@ -1346,59 +1347,59 @@ msgstr "Standaard zichtbaar voor nieuwe gebruikers" #: cps/templates/config_view_edit.html:123 cps/templates/user_edit.html:50 msgid "Show random books" -msgstr "Toon willekeurige boeken" +msgstr "Willekeurige boeken tonen" #: cps/templates/config_view_edit.html:127 cps/templates/user_edit.html:54 msgid "Show recent books" -msgstr "Toon recente boeken" +msgstr "Recente boeken tonen" #: cps/templates/config_view_edit.html:131 cps/templates/user_edit.html:58 msgid "Show sorted books" -msgstr "Toon gesorteerde boeken" +msgstr "Gesorteerde boeken tonen" #: cps/templates/config_view_edit.html:135 cps/templates/user_edit.html:62 msgid "Show hot books" -msgstr "Toon populaire boeken" +msgstr "Populaire boeken tonen" #: cps/templates/config_view_edit.html:139 cps/templates/user_edit.html:66 msgid "Show best rated books" -msgstr "Toon best beoordeelde boeken" +msgstr "Best beoordeelde boeken tonen" #: cps/templates/config_view_edit.html:143 cps/templates/user_edit.html:70 msgid "Show language selection" -msgstr "Toon taal selectie" +msgstr "Taalkeuze tonen" #: cps/templates/config_view_edit.html:147 cps/templates/user_edit.html:74 msgid "Show series selection" -msgstr "Toon serie selectie" +msgstr "Seriekeuze tonen" #: cps/templates/config_view_edit.html:151 cps/templates/user_edit.html:78 msgid "Show category selection" -msgstr "Toon categorie selectie" +msgstr "Categoriekeuze tonen" #: cps/templates/config_view_edit.html:155 cps/templates/user_edit.html:82 msgid "Show author selection" -msgstr "Toon auteur selectie" +msgstr "Auteurkeuze tonen" #: cps/templates/config_view_edit.html:159 cps/templates/user_edit.html:86 msgid "Show publisher selection" -msgstr "Toon uitgevers selectie" +msgstr "Uitgeverskeuze tonen" #: cps/templates/config_view_edit.html:163 cps/templates/user_edit.html:91 msgid "Show read and unread" -msgstr "Toon gelezen en ongelezen" +msgstr "Gelezen/Ongelezen tonen" #: cps/templates/config_view_edit.html:167 cps/templates/user_edit.html:96 msgid "Show random books in detail view" -msgstr "Toon willekeurige boeken in gedetailleerd zicht" +msgstr "Willekeurige boeken tonen in gedetailleerde weergave" #: cps/templates/config_view_edit.html:171 cps/templates/user_edit.html:109 msgid "Show mature content" -msgstr "Toon Volwassen Inhoud" +msgstr "18+-inhoud tonen" #: cps/templates/detail.html:63 msgid "Read in browser" -msgstr "Lees in browser" +msgstr "Lezen in webbrowser" #: cps/templates/detail.html:100 msgid "Book" @@ -1414,15 +1415,15 @@ msgstr "taal" #: cps/templates/detail.html:185 msgid "Mark As Unread" -msgstr "" +msgstr "Markeren als ongelezen" #: cps/templates/detail.html:185 msgid "Mark As Read" -msgstr "" +msgstr "Markeren als gelezen" #: cps/templates/detail.html:186 msgid "Read" -msgstr "Lees" +msgstr "Lezen" #: cps/templates/detail.html:196 msgid "Description:" @@ -1430,19 +1431,19 @@ msgstr "Omschrijving:" #: cps/templates/detail.html:209 cps/templates/search.html:14 msgid "Add to shelf" -msgstr "Voeg toe aan boekenplank" +msgstr "Toevoegen aan boekenplank" #: cps/templates/detail.html:271 msgid "Edit metadata" -msgstr "Bewerk metadata" +msgstr "Metagegevens bewerken" #: cps/templates/email_edit.html:15 msgid "SMTP port (usually 25 for plain SMTP and 465 for SSL and 587 for STARTTLS)" -msgstr "SMTP poort (meestal 25 voor normale SMTP en 465 voor SSL en 587 voor STARTTLS)" +msgstr "SMTP-poort (meestal 25 voor normale SMTP, 465 voor SSL en 587 voor STARTTLS)" #: cps/templates/email_edit.html:19 msgid "Encryption" -msgstr "Encryptie" +msgstr "Versleuteling" #: cps/templates/email_edit.html:21 msgid "None" @@ -1458,19 +1459,19 @@ msgstr "SSL/TLS" #: cps/templates/email_edit.html:31 msgid "SMTP password" -msgstr "SMTP wachtwoord" +msgstr "SMTP-wachtwoord" #: cps/templates/email_edit.html:35 msgid "From e-mail" -msgstr "Van email" +msgstr "Van e-mailadres" #: cps/templates/email_edit.html:38 msgid "Save settings" -msgstr "Bewaar instelling" +msgstr "Instellingen opslaan" #: cps/templates/email_edit.html:39 msgid "Save settings and send Test E-Mail" -msgstr "Bewaar instellingen en stuur test email" +msgstr "Instellingen opslaan en test-e-mail versturen" #: cps/templates/email_edit.html:43 msgid "Allowed domains for registering" @@ -1482,15 +1483,15 @@ msgstr "Voer domeinnaam in" #: cps/templates/email_edit.html:55 msgid "Add Domain" -msgstr "Voeg Domein toe" +msgstr "Domein toevoegen" #: cps/templates/email_edit.html:58 msgid "Add" -msgstr "Voeg toe" +msgstr "Toevoegen" #: cps/templates/email_edit.html:72 msgid "Do you really want to delete this domain rule?" -msgstr "Wil je werkelijk deze domein regel verwijderen?" +msgstr "Weet je zeker dat je deze domeinregel wilt verwijderen?" #: cps/templates/feed.xml:21 cps/templates/layout.html:210 msgid "Next" @@ -1499,39 +1500,39 @@ msgstr "Volgende" #: cps/templates/feed.xml:33 cps/templates/index.xml:11 #: cps/templates/layout.html:44 cps/templates/layout.html:45 msgid "Search" -msgstr "Zoek" +msgstr "Zoeken" #: cps/templates/http_error.html:23 msgid "Back to home" -msgstr "" +msgstr "Terug naar startpagina" #: cps/templates/index.html:5 msgid "Discover (Random Books)" -msgstr "Ontdek (Willekeurige Boeken)" +msgstr "Verkennen (willekeurige boeken)" #: cps/templates/index.xml:6 msgid "Start" -msgstr "Start" +msgstr "Starten" #: cps/templates/index.xml:18 cps/templates/layout.html:141 msgid "Hot Books" -msgstr "Populaire Boeken" +msgstr "Populaire boeken" #: cps/templates/index.xml:22 msgid "Popular publications from this catalog based on Downloads." -msgstr "Populaire publicaties van deze cataloog gebaseerd op Downloads." +msgstr "Populaire publicaties uit deze catalogus, gebaseerd op Downloads." #: cps/templates/index.xml:25 cps/templates/layout.html:144 msgid "Best rated Books" -msgstr "Best beoordeeld" +msgstr "Best beoordeelde boeken" #: cps/templates/index.xml:29 msgid "Popular publications from this catalog based on Rating." -msgstr "Populaire publicaties van deze cataloog gebaseerd op Beoordeling." +msgstr "Populaire publicaties uit deze catalogus, gebaseerd op Beoordeling." #: cps/templates/index.xml:32 msgid "New Books" -msgstr "Nieuwe Boeken" +msgstr "Nieuwe boeken" #: cps/templates/index.xml:36 msgid "The latest Books" @@ -1539,7 +1540,7 @@ msgstr "Recentste boeken" #: cps/templates/index.xml:43 msgid "Show Random Books" -msgstr "Toon Willekeurige Boeken" +msgstr "Willekeurige boeken tonen" #: cps/templates/index.xml:62 cps/templates/layout.html:162 msgid "Authors" @@ -1547,7 +1548,7 @@ msgstr "Auteurs" #: cps/templates/index.xml:66 msgid "Books ordered by Author" -msgstr "Boeken gesorteerd op Auteur" +msgstr "Boeken gesorteerd op auteur" #: cps/templates/index.xml:69 cps/templates/layout.html:165 msgid "Publishers" @@ -1559,35 +1560,35 @@ msgstr "Boeken gesorteerd op uitgever" #: cps/templates/index.xml:80 msgid "Books ordered by category" -msgstr "Boeken gesorteerd op Categorie" +msgstr "Boeken gesorteerd op categorie" #: cps/templates/index.xml:87 msgid "Books ordered by series" -msgstr "Boeken gesorteerd op Serie" +msgstr "Boeken gesorteerd op serie" #: cps/templates/index.xml:90 cps/templates/layout.html:171 msgid "Public Shelves" -msgstr "Publieke Boekenplanken" +msgstr "Openbare boekenplanken" #: cps/templates/index.xml:94 msgid "Books organized in public shelfs, visible to everyone" -msgstr "Boeken georganiseerd in publieke boekenplanken, zichtbaar voor iedereen" +msgstr "Boeken georganiseerd op openbare boekenplanken, zichtbaar voor iedereen" #: cps/templates/index.xml:98 cps/templates/layout.html:175 msgid "Your Shelves" -msgstr "Jou Boekenplanken" +msgstr "Jouw boekenplanken" #: cps/templates/index.xml:102 msgid "User's own shelfs, only visible to the current user himself" -msgstr "Eigen boekenplanken, enkel zichtbaar voor de huidige gebruiker zelf" +msgstr "Eigen boekenplanken, enkel zichtbaar voor de huidige gebruiker" #: cps/templates/layout.html:28 msgid "Home" -msgstr "" +msgstr "Startpagina" #: cps/templates/layout.html:34 msgid "Toggle navigation" -msgstr "Kies navigatie" +msgstr "Navigatie aanpassen" #: cps/templates/layout.html:55 msgid "Advanced Search" @@ -1600,23 +1601,23 @@ msgstr "Instellingen" #: cps/templates/layout.html:78 msgid "Account" -msgstr "" +msgstr "Account" #: cps/templates/layout.html:80 msgid "Logout" -msgstr "Log uit" +msgstr "Uitloggen" #: cps/templates/layout.html:85 cps/templates/register.html:14 msgid "Register" -msgstr "Registreer" +msgstr "Registreren" #: cps/templates/layout.html:111 cps/templates/layout.html:257 msgid "Uploading..." -msgstr "Aan het uploaden..." +msgstr "Bezig met uploaden..." #: cps/templates/layout.html:112 msgid "please don't refresh the page" -msgstr "gelieve de pagina niet te herladen" +msgstr "vernieuw de pagina niet" #: cps/templates/layout.html:122 msgid "Browse" @@ -1624,11 +1625,11 @@ msgstr "Verkennen" #: cps/templates/layout.html:124 msgid "Recently Added" -msgstr "Recent Toegevoegd" +msgstr "Recent toegevoegd" #: cps/templates/layout.html:129 msgid "Sorted Books" -msgstr "Gesorteerde Boeken" +msgstr "Gesorteerde boeken" #: cps/templates/layout.html:133 cps/templates/layout.html:134 #: cps/templates/layout.html:135 cps/templates/layout.html:136 @@ -1653,7 +1654,7 @@ msgstr "Aflopend" #: cps/templates/layout.html:153 msgid "Discover" -msgstr "Ontdek" +msgstr "Verkennen" #: cps/templates/layout.html:156 msgid "Categories" @@ -1665,7 +1666,7 @@ msgstr "Talen" #: cps/templates/layout.html:180 msgid "Create a Shelf" -msgstr "Maak een boekenplank" +msgstr "Creëer een boekenplank" #: cps/templates/layout.html:181 cps/templates/stats.html:3 msgid "About" @@ -1677,15 +1678,15 @@ msgstr "Vorige" #: cps/templates/layout.html:222 msgid "Book Details" -msgstr "Boek Details" +msgstr "Boekgegevens" #: cps/templates/layout.html:256 msgid "Upload done, processing, please wait..." -msgstr "" +msgstr "Uploaden voltooid; bezig met verwerken..." #: cps/templates/layout.html:259 msgid "Error" -msgstr "" +msgstr "Fout" #: cps/templates/login.html:8 cps/templates/login.html:9 #: cps/templates/register.html:7 cps/templates/user_edit.html:8 @@ -1699,7 +1700,7 @@ msgstr "Wachtwoord" #: cps/templates/login.html:17 msgid "Remember me" -msgstr "Onthoumij" +msgstr "Onthouden" #: cps/templates/login.html:22 msgid "Log in with magic link" @@ -1707,11 +1708,11 @@ msgstr "Inloggen met magische koppeling" #: cps/templates/osd.xml:5 msgid "Calibre-Web ebook catalog" -msgstr "Calibre-Web ebook cataloog" +msgstr "Calibre-Web - e-boekcatalogus" #: cps/templates/read.html:74 msgid "Reflow text when sidebars are open." -msgstr "Herschuif tekst waneer het zijpaneel open staat." +msgstr "Tekstindeling automatisch aanpassen als het zijpaneel geopend is." #: cps/templates/readcbr.html:94 msgid "Keyboard Shortcuts" @@ -1719,39 +1720,39 @@ msgstr "Sneltoetsen" #: cps/templates/readcbr.html:97 msgid "Previous Page" -msgstr "Vorige Pagina" +msgstr "Vorige pagina" #: cps/templates/readcbr.html:98 msgid "Next Page" -msgstr "Volgende Pagina" +msgstr "Volgende pagina" #: cps/templates/readcbr.html:99 msgid "Scale to Best" -msgstr "Optimaal schalen" +msgstr "Optimaal inpassen" #: cps/templates/readcbr.html:100 msgid "Scale to Width" -msgstr "Schalen naar breedte" +msgstr "Aanpassen aan breedte" #: cps/templates/readcbr.html:101 msgid "Scale to Height" -msgstr "Schalen naar hoogte" +msgstr "Aanpassen aan hoogte" #: cps/templates/readcbr.html:102 msgid "Scale to Native" -msgstr "Schalen op ware grootte" +msgstr "Ware grootte" #: cps/templates/readcbr.html:103 msgid "Rotate Right" -msgstr "Draai rechtsom" +msgstr "Naar rechts draaien" #: cps/templates/readcbr.html:104 msgid "Rotate Left" -msgstr "Draai linksom" +msgstr "Naar links draaien" #: cps/templates/readcbr.html:105 msgid "Flip Image" -msgstr "Keer beeld om" +msgstr "Afbeelding omdraaien" #: cps/templates/readcbr.html:121 msgid "Light" @@ -1783,11 +1784,11 @@ msgstr "Ware grootte" #: cps/templates/readcbr.html:138 msgid "Rotate" -msgstr "Draai" +msgstr "Draaien" #: cps/templates/readcbr.html:149 msgid "Flip" -msgstr "Keer" +msgstr "Omdraaien" #: cps/templates/readcbr.html:152 msgid "Horizontal" @@ -1799,27 +1800,27 @@ msgstr "Verticaal" #: cps/templates/readcbr.html:158 msgid "Direction" -msgstr "" +msgstr "Richting" #: cps/templates/readcbr.html:161 msgid "Left to Right" -msgstr "" +msgstr "Links-naar-rechts" #: cps/templates/readcbr.html:162 msgid "Right to Left" -msgstr "" +msgstr "Rechts-naar-links" #: cps/templates/readpdf.html:29 msgid "PDF.js viewer" -msgstr "PDF.js viewer" +msgstr "PDF.js-weergave" #: cps/templates/readtxt.html:6 msgid "Basic txt Reader" -msgstr "Basis txt Lezer" +msgstr "Basis tekstlezer" #: cps/templates/register.html:4 msgid "Register a new account" -msgstr "Registreer een nieuwe gebruiker" +msgstr "Nieuw account registreren" #: cps/templates/register.html:8 msgid "Choose a username" @@ -1827,31 +1828,31 @@ msgstr "Kies een gebruikersnaam" #: cps/templates/register.html:11 cps/templates/user_edit.html:13 msgid "E-mail address" -msgstr "Email adres" +msgstr "E-mailadres" #: cps/templates/register.html:12 msgid "Your email address" -msgstr "Jou email adres" +msgstr "Je e-mailadres" #: cps/templates/remote_login.html:6 msgid "Use your other device, login and visit " -msgstr "" +msgstr "Pak je andere apparaat, log in en ga naar " #: cps/templates/remote_login.html:9 msgid "Once you do so, you will automatically get logged in on this device." -msgstr "Eenmaal gedaan wordt je automagisch op dit apparaat ingelogd." +msgstr "Daarna wordt je automatisch op dit apparaat ingelogd." #: cps/templates/remote_login.html:12 msgid "The link will expire after 10 minutes." -msgstr "" +msgstr "De link vervalt na 10 minuten." #: cps/templates/search.html:5 msgid "No Results for:" -msgstr "Geen resultaat voor:" +msgstr "Geen resultaten voor:" #: cps/templates/search.html:6 msgid "Please try a different search" -msgstr "Gelieve een ander zoekwoord proberen" +msgstr "Probeer andere zoektermen" #: cps/templates/search.html:8 msgid "Results for:" @@ -1859,87 +1860,87 @@ msgstr "Resultaten voor:" #: cps/templates/search_form.html:19 msgid "Publishing date from" -msgstr "Publicatie datum van" +msgstr "Publicatiedatum van" #: cps/templates/search_form.html:26 msgid "Publishing date to" -msgstr "Publicatie datum tot" +msgstr "Publicatiedatum tot" #: cps/templates/search_form.html:43 msgid "Exclude Tags" -msgstr "Sluit Tags uit" +msgstr "Labels uitsluiten" #: cps/templates/search_form.html:63 msgid "Exclude Series" -msgstr "Sluit Series uit" +msgstr "Series uitsluiten" #: cps/templates/search_form.html:84 msgid "Exclude Languages" -msgstr "Sluit Talen uit" +msgstr "Talen uitsluiten" #: cps/templates/search_form.html:97 msgid "Rating bigger than" -msgstr "Waardering meer dan" +msgstr "Met beoordeling hoger dan" #: cps/templates/search_form.html:101 msgid "Rating less than" -msgstr "Waardering minder dan" +msgstr "Met beoordeling lager dan" #: cps/templates/shelf.html:7 msgid "Delete this Shelf" -msgstr "Wis deze boekenplank" +msgstr "Deze boekenplank verwijderen" #: cps/templates/shelf.html:8 msgid "Edit Shelf" -msgstr "Bewerk Boekenplank" +msgstr "Boekenplank aanpassen" #: cps/templates/shelf.html:9 cps/templates/shelf_order.html:11 msgid "Change order" -msgstr "Verander volgorde" +msgstr "Volgorde veranderen" #: cps/templates/shelf.html:68 msgid "Do you really want to delete the shelf?" -msgstr "Wil je echt deze boekenplank verwijderen?" +msgstr "Weet je zeker dat je deze boekenplank wilt verwijderen?" #: cps/templates/shelf.html:71 msgid "Shelf will be lost for everybody and forever!" -msgstr "Boekenplank zal verdwijnen voor iedereen en altijd!" +msgstr "De boekenplank wordt permanent verwijderd voor iedereen!" #: cps/templates/shelf_edit.html:13 msgid "should the shelf be public?" -msgstr "mag deze boekenplank publiek zijn?" +msgstr "moet de boekenplank openbaar zijn?" #: cps/templates/shelf_order.html:5 msgid "Drag 'n drop to rearrange order" -msgstr "Sleep en laat vallen om de volgorde te veranderen" +msgstr "Verander de volgorde middels slepen-en-neerzetten" #: cps/templates/stats.html:7 msgid "Calibre library statistics" -msgstr "Calibre bibliotheek statistieken" +msgstr "Calibre-bibliotheekstatistieken" #: cps/templates/stats.html:12 msgid "Books in this Library" -msgstr "Boeken in deze Bibliotheek" +msgstr "Boeken in deze bibliotheek" #: cps/templates/stats.html:16 msgid "Authors in this Library" -msgstr "Auteurs in deze Bibliotheek" +msgstr "Auteurs in deze bibliotheek" #: cps/templates/stats.html:20 msgid "Categories in this Library" -msgstr "Categorieën in deze Bibliotheek" +msgstr "Categorieën in deze bibliotheek" #: cps/templates/stats.html:24 msgid "Series in this Library" -msgstr "Series in deze Bibliotheek" +msgstr "Series in deze bibliotheek" #: cps/templates/stats.html:28 msgid "Linked libraries" -msgstr "Gelinkte bibliotheken" +msgstr "Gekoppelde bibliotheken" #: cps/templates/stats.html:32 msgid "Program library" -msgstr "Programma bibliotheek" +msgstr "Programmabibliotheek" #: cps/templates/stats.html:33 msgid "Installed Version" @@ -1947,7 +1948,7 @@ msgstr "Geïnstalleerde versie" #: cps/templates/tasks.html:7 msgid "Tasks list" -msgstr "Taaklijst" +msgstr "Takenlijst" #: cps/templates/tasks.html:12 msgid "User" @@ -1963,7 +1964,7 @@ msgstr "Status" #: cps/templates/tasks.html:16 msgid "Progress" -msgstr "Vooruitgang" +msgstr "Voortgang" #: cps/templates/tasks.html:17 msgid "Runtime" @@ -1971,39 +1972,39 @@ msgstr "Looptijd" #: cps/templates/tasks.html:18 msgid "Starttime" -msgstr "Start tijd" +msgstr "Begintijd" #: cps/templates/tasks.html:24 msgid "Delete finished tasks" -msgstr "Verwijder voltooide taken" +msgstr "Afgeronde taken verwijderen" #: cps/templates/tasks.html:25 msgid "Hide all tasks" -msgstr "Verberg alle taken" +msgstr "Alle taken verbergen" #: cps/templates/user_edit.html:18 msgid "Reset user Password" -msgstr "Reset gebruikers wachtwoord" +msgstr "Gebruikerswachtwoord herstellen" #: cps/templates/user_edit.html:27 msgid "Kindle E-Mail" -msgstr "Kindle email" +msgstr "Kindle-e-mailadres" #: cps/templates/user_edit.html:39 msgid "Show books with language" -msgstr "Toon boeken met taal" +msgstr "Boeken tonen met taal" #: cps/templates/user_edit.html:41 msgid "Show all" -msgstr "Toon alles" +msgstr "Alle tonen" #: cps/templates/user_edit.html:141 msgid "Delete this user" -msgstr "Wis deze gebruiker" +msgstr "Deze gebruiker verwijderen" #: cps/templates/user_edit.html:156 msgid "Recent Downloads" -msgstr "Recente Downloads" +msgstr "Recente downloads" #~ msgid "Afar" #~ msgstr "Afar; Hamitisch" @@ -3271,15 +3272,8 @@ msgstr "Recente Downloads" #~ msgid "Cover is not a jpg file, can't save" #~ msgstr "Boekomslag is geen jpg bestand, opslaan niet mogelijk" -#~ msgid "Preparing document for printing..." -#~ msgstr "" - #~ msgid "Using your another device, visit" #~ msgstr "Bezoek met je andere apparaat" #~ msgid "and log in" #~ msgstr "en log in" - -#~ msgid "Using your another device, login and visit " -#~ msgstr "" - From c1d5f77fe81518bb60ddf8e05637ef06778cc483 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 20 Jun 2019 10:38:32 +0200 Subject: [PATCH 07/29] Wiki link fixed --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index bc314c02..32afff07 100644 --- a/readme.md +++ b/readme.md @@ -82,4 +82,4 @@ Pre-built Docker images are available in these Docker Hub repositories: # Wiki -For further informations, How To's and FAQ please check the ![Wiki](../../wiki) \ No newline at end of file +For further informations, How To's and FAQ please check the [Wiki](../../wiki) From f79d549910ac2707a593c2df7f775a41ab34e6a0 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 30 Jun 2019 11:20:36 +0200 Subject: [PATCH 08/29] Fix duplicate user and email (now case insensitive #948) Fix sorting in comics (#950) Fix log error on Calibre converter error (#953) Fix long running tasks (#954) --- cps/helper.py | 30 +- cps/static/js/archive/archive.js | 64 ++++ cps/static/js/archive/unrar.js | 7 +- cps/static/js/archive/untar.js | 19 +- cps/static/js/archive/unzip.js | 69 +++-- cps/static/js/io.js | 483 ------------------------------- cps/web.py | 35 ++- cps/worker.py | 36 +-- 8 files changed, 184 insertions(+), 559 deletions(-) delete mode 100644 cps/static/js/io.js diff --git a/cps/helper.py b/cps/helper.py index 9b789f78..114eeccd 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -34,7 +34,8 @@ from flask import send_from_directory, make_response, redirect, abort from flask_babel import gettext as _ from flask_login import current_user from babel.dates import format_datetime -from datetime import datetime +from babel.units import format_unit +from datetime import datetime, timedelta import shutil import requests try: @@ -566,8 +567,33 @@ def json_serial(obj): if isinstance(obj, (datetime)): return obj.isoformat() + if isinstance(obj, (timedelta)): + return { + '__type__': 'timedelta', + 'days': obj.days, + 'seconds': obj.seconds, + 'microseconds': obj.microseconds, + } raise TypeError ("Type %s not serializable" % type(obj)) + +# helper function for displaying the runtime of tasks +def format_runtime(runtime): + retVal = "" + if runtime.days: + retVal = format_unit(runtime.days, 'duration-day', length="long", locale=web.get_locale()) + ', ' + mins, seconds = divmod(runtime.seconds, 60) + hours, minutes = divmod(mins, 60) + # ToDo: locale.number_symbols._data['timeSeparator'] -> localize time separator ? + if hours: + retVal += '{:d}:{:02d}:{:02d}s'.format(hours, minutes, seconds) + elif minutes: + retVal += '{:2d}:{:02d}s'.format(minutes, seconds) + else: + retVal += '{:2d}s'.format(seconds) + return retVal + + # helper function to apply localize status information in tasklist entries def render_task_status(tasklist): renderedtasklist=list() @@ -579,6 +605,8 @@ def render_task_status(tasklist): if 'starttime' not in task: task['starttime'] = "" + task['runtime'] = format_runtime(task['formRuntime']) + # localize the task status if isinstance( task['stat'], int ): if task['stat'] == worker.STAT_WAITING: diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js index 331997d9..cfc7bd40 100644 --- a/cps/static/js/archive/archive.js +++ b/cps/static/js/archive/archive.js @@ -1,3 +1,67 @@ +/* alphanum.js (C) Brian Huisman + * Based on the Alphanum Algorithm by David Koelle + * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com + * + * Distributed under same license as original + * + * Released under the MIT License - https://opensource.org/licenses/MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + /* ******************************************************************** + * Alphanum sort() function version - case insensitive + * - Slower, but easier to modify for arrays of objects which contain + * string properties + * + */ +function alphanumCase(a, b) { + function chunkify(t) { + var tz = new Array(); + var x = 0, y = -1, n = 0, i, j; + + while (i = (j = t.charAt(x++)).charCodeAt(0)) { + var m = (i == 46 || (i >=48 && i <= 57)); + if (m !== n) { + tz[++y] = ""; + n = m; + } + tz[y] += j; + } + return tz; + } + + var aa = chunkify(a.filename.toLowerCase()); + var bb = chunkify(b.filename.toLowerCase()); + + for (x = 0; aa[x] && bb[x]; x++) { + if (aa[x] !== bb[x]) { + var c = Number(aa[x]), d = Number(bb[x]); + if (c == aa[x] && d == bb[x]) { + return c - d; + } else return (aa[x] > bb[x]) ? 1 : -1; + } + } + return aa.length - bb.length; +} +// =========================================================================== + + /** * archive.js * diff --git a/cps/static/js/archive/unrar.js b/cps/static/js/archive/unrar.js index 89263b83..fadb791e 100644 --- a/cps/static/js/archive/unrar.js +++ b/cps/static/js/archive/unrar.js @@ -1332,12 +1332,7 @@ var unrar = function(arrayBuffer) { totalFilesInArchive = localFiles.length; // now we have all information but things are unpacked - // TODO: unpack - localFiles = localFiles.sort(function(a, b) { - var aname = a.filename.toLowerCase(); - var bname = b.filename.toLowerCase(); - return aname > bname ? 1 : -1; - }); + localFiles.sort(alphanumCase); info(localFiles.map(function(a) { return a.filename; diff --git a/cps/static/js/archive/untar.js b/cps/static/js/archive/untar.js index d9a1fdfd..cc1499ef 100644 --- a/cps/static/js/archive/untar.js +++ b/cps/static/js/archive/untar.js @@ -115,6 +115,7 @@ var TarLocalFile = function(bstream) { } }; + var untar = function(arrayBuffer) { postMessage(new bitjs.archive.UnarchiveStartEvent()); currentFilename = ""; @@ -127,14 +128,22 @@ var untar = function(arrayBuffer) { var bstream = new bitjs.io.ByteStream(arrayBuffer); postProgress(); - // While we don't encounter an empty block, keep making TarLocalFiles. + /* + // go through whole file, read header of each block and memorize, filepointer + */ while (bstream.peekNumber(4) !== 0) { - var oneLocalFile = new TarLocalFile(bstream); + var localFile = new TarLocalFile(bstream); + allLocalFiles.push(localFile); + postProgress(); + } + // got all local files, now sort them + allLocalFiles.sort(alphanumCase); + + allLocalFiles.forEach(function(oneLocalFile) { + // While we don't encounter an empty block, keep making TarLocalFiles. if (oneLocalFile && oneLocalFile.isValid) { // If we make it to this point and haven't thrown an error, we have successfully // read in the data for a local file, so we can update the actual bytestream. - - allLocalFiles.push(oneLocalFile); totalUncompressedBytesInArchive += oneLocalFile.size; // update progress @@ -145,7 +154,7 @@ var untar = function(arrayBuffer) { postMessage(new bitjs.archive.UnarchiveExtractEvent(oneLocalFile)); postProgress(); } - } + }); totalFilesInArchive = allLocalFiles.length; postProgress(); diff --git a/cps/static/js/archive/unzip.js b/cps/static/js/archive/unzip.js index f8de27f7..a4cec8d0 100644 --- a/cps/static/js/archive/unzip.js +++ b/cps/static/js/archive/unzip.js @@ -72,23 +72,10 @@ var ZipLocalFile = function(bstream) { this.filename = bstream.readString(this.fileNameLength); } - info("Zip Local File Header:"); - info(" version=" + this.version); - info(" general purpose=" + this.generalPurpose); - info(" compression method=" + this.compressionMethod); - info(" last mod file time=" + this.lastModFileTime); - info(" last mod file date=" + this.lastModFileDate); - info(" crc32=" + this.crc32); - info(" compressed size=" + this.compressedSize); - info(" uncompressed size=" + this.uncompressedSize); - info(" file name length=" + this.fileNameLength); - info(" extra field length=" + this.extraFieldLength); - info(" filename = '" + this.filename + "'"); - this.extraField = null; if (this.extraFieldLength > 0) { - this.extraField = bstream.readString(this.extraFieldLength); - info(" extra field=" + this.extraField); + this.extraField = bstream.readString(this.extraFieldLength); + info(" extra field=" + this.extraField); } // read in the compressed data @@ -107,6 +94,21 @@ var ZipLocalFile = function(bstream) { this.compressedSize = bstream.readNumber(4); this.uncompressedSize = bstream.readNumber(4); } + + // Now that we have all the bytes for this file, we can print out some information. + info("Zip Local File Header:"); + info(" version=" + this.version); + info(" general purpose=" + this.generalPurpose); + info(" compression method=" + this.compressionMethod); + info(" last mod file time=" + this.lastModFileTime); + info(" last mod file date=" + this.lastModFileDate); + info(" crc32=" + this.crc32); + info(" compressed size=" + this.compressedSize); + info(" uncompressed size=" + this.uncompressedSize); + info(" file name length=" + this.fileNameLength); + info(" extra field length=" + this.extraFieldLength); + info(" filename = '" + this.filename + "'"); + }; // determine what kind of compressed data we have and decompress @@ -132,6 +134,7 @@ ZipLocalFile.prototype.unzip = function() { // Takes an ArrayBuffer of a zip file in // returns null on error // returns an array of DecompressedFile objects on success +// ToDo This function differs var unzip = function(arrayBuffer) { postMessage(new bitjs.archive.UnarchiveStartEvent()); @@ -159,11 +162,7 @@ var unzip = function(arrayBuffer) { totalFilesInArchive = localFiles.length; // got all local files, now sort them - localFiles.sort(function(a, b) { - var aname = a.filename.toLowerCase(); - var bname = b.filename.toLowerCase(); - return aname > bname ? 1 : -1; - }); + localFiles.sort(alphanumCase); // archive extra data record if (bstream.peekNumber(4) === zArchiveExtraDataSignature) { @@ -253,9 +252,9 @@ function getHuffmanCodes(bitLengths) { } // Reference: http://tools.ietf.org/html/rfc1951#page-8 - var numLengths = bitLengths.length, - blCount = [], - MAX_BITS = 1; + var numLengths = bitLengths.length; + var blCount = []; + var MAX_BITS = 1; // Step 1: count up how many codes of each length we have for (var i = 0; i < numLengths; ++i) { @@ -274,8 +273,8 @@ function getHuffmanCodes(bitLengths) { } // Step 2: Find the numerical value of the smallest code for each code length - var nextCode = [], - code = 0; + var nextCode = []; + var code = 0; for (var bits = 1; bits <= MAX_BITS; ++bits) { var length2 = bits - 1; // ensure undefined lengths are zero @@ -285,8 +284,8 @@ function getHuffmanCodes(bitLengths) { } // Step 3: Assign numerical values to all codes - var table = {}, - tableLength = 0; + var table = {}; + var tableLength = 0; for (var n = 0; n < numLengths; ++n) { var len = bitLengths[n]; if (len !== 0) { @@ -353,7 +352,8 @@ function getFixedDistanceTable() { // extract one bit at a time until we find a matching Huffman Code // then return that symbol function decodeSymbol(bstream, hcTable) { - var code = 0, len = 0; + var code = 0; + var len = 0; // loop until we match for (;;) { @@ -364,7 +364,6 @@ function decodeSymbol(bstream, hcTable) { // check against Huffman Code table and break if found if (hcTable.hasOwnProperty(code) && hcTable[code].length === len) { - break; } if (len > hcTable.maxLength) { @@ -500,10 +499,10 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { if (symbol === 256) { break; } else { - var lengthLookup = LengthLookupTable[symbol - 257], - length = lengthLookup[1] + bstream.readBits(lengthLookup[0]), - distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)], - distance = distLookup[1] + bstream.readBits(distLookup[0]); + var lengthLookup = LengthLookupTable[symbol - 257]; + var length = lengthLookup[1] + bstream.readBits(lengthLookup[0]); + var distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)]; + var distance = distLookup[1] + bstream.readBits(distLookup[0]); // now apply length and distance appropriately and copy to output @@ -634,8 +633,8 @@ function inflate(compressedData, numDecompressedBytes) { var distanceCodeLengths = literalCodeLengths.splice(numLiteralLengthCodes, numDistanceCodes); // now generate the true Huffman Code tables using these code lengths - var hcLiteralTable = getHuffmanCodes(literalCodeLengths), - hcDistanceTable = getHuffmanCodes(distanceCodeLengths); + var hcLiteralTable = getHuffmanCodes(literalCodeLengths); + var hcDistanceTable = getHuffmanCodes(distanceCodeLengths); blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer); } else { // error diff --git a/cps/static/js/io.js b/cps/static/js/io.js deleted file mode 100644 index 292f5f95..00000000 --- a/cps/static/js/io.js +++ /dev/null @@ -1,483 +0,0 @@ -/* - * io.js - * - * Provides readers for bit/byte streams (reading) and a byte buffer (writing). - * - * Licensed under the MIT License - * - * Copyright(c) 2011 Google Inc. - * Copyright(c) 2011 antimatter15 - */ - -/* global bitjs, Uint8Array */ - -var bitjs = bitjs || {}; -bitjs.io = bitjs.io || {}; - -(function() { - - // mask for getting the Nth bit (zero-based) - bitjs.BIT = [ 0x01, 0x02, 0x04, 0x08, - 0x10, 0x20, 0x40, 0x80, - 0x100, 0x200, 0x400, 0x800, - 0x1000, 0x2000, 0x4000, 0x8000]; - - // mask for getting N number of bits (0-8) - var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF ]; - - - /** - * This bit stream peeks and consumes bits out of a binary stream. - * - * @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} optOffset The offset into the ArrayBuffer - * @param {Number} optLength The length of this BitStream - */ - 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 = 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.peekBitsRtl : this.peekBitsLtr; - }; - - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit0 of byte0 and moves left until it reaches - * bit7 of byte0, then jumps to bit0 of byte1, etc. - * @param {number} n The number of bits to peek. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @return {number} The peeked bits, as an unsigned number. - */ - bitjs.io.BitStream.prototype.peekBitsLtr = function(n, movePointers) { - if (n <= 0 || typeof n !== typeof 1) { - return 0; - } - - var movePointers = movePointers || false; - var bytePtr = this.bytePtr; - var bitPtr = this.bitPtr; - var result = 0; - var bitsIn = 0; - var bytes = this.bytes; - - // keep going until we have no more bits left to peek at - // TODO: Consider putting all bits from bytes we will need into a variable and then - // shifting/masking it to just extract the bits we want. - // This could be considerably faster when reading more than 3 or 4 bits at a time. - while (n > 0) { - if (bytePtr >= bytes.length) { - throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + - bytes.length + ", bitPtr=" + bitPtr; - } - - var numBitsLeftInThisByte = (8 - bitPtr); - var mask; - if (n >= numBitsLeftInThisByte) { - mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bytePtr++; - bitPtr = 0; - bitsIn += numBitsLeftInThisByte; - n -= numBitsLeftInThisByte; - } else { - mask = (BITMASK[n] << bitPtr); - result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); - - bitPtr += n; - bitsIn += n; - n = 0; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - } - - return result; - }; - - - /** - * byte0 byte1 byte2 byte3 - * 7......0 | 7......0 | 7......0 | 7......0 - * - * The bit pointer starts at bit7 of byte0 and moves right until it reaches - * bit0 of byte0, then goes to bit7 of byte1, etc. - * @param {number} n The number of bits to peek. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @return {number} The peeked bits, as an unsigned number. - */ - bitjs.io.BitStream.prototype.peekBitsRtl = function(n, movePointers) { - if (n <= 0 || typeof n !== typeof 1) { - return 0; - } - - var movePointers = movePointers || false; - var bytePtr = this.bytePtr; - var bitPtr = this.bitPtr; - var result = 0; - var bytes = this.bytes; - - // keep going until we have no more bits left to peek at - // TODO: Consider putting all bits from bytes we will need into a variable and then - // shifting/masking it to just extract the bits we want. - // This could be considerably faster when reading more than 3 or 4 bits at a time. - while (n > 0) { - - if (bytePtr >= bytes.length) { - throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + - bytes.length + ", bitPtr=" + bitPtr; - // return -1; - } - - var numBitsLeftInThisByte = (8 - bitPtr); - if (n >= numBitsLeftInThisByte) { - result <<= numBitsLeftInThisByte; - result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); - bytePtr++; - bitPtr = 0; - n -= numBitsLeftInThisByte; - } else { - result <<= n; - result |= ((bytes[bytePtr] & (BITMASK[n] << (8 - n - bitPtr))) >> (8 - n - bitPtr)); - - bitPtr += n; - n = 0; - } - } - - if (movePointers) { - this.bitPtr = bitPtr; - this.bytePtr = bytePtr; - } - - return result; - }; - - - /** - * Some voodoo magic. - */ - bitjs.io.BitStream.prototype.getBits = function() { - return (((((this.bytes[this.bytePtr] & 0xff) << 16) + - ((this.bytes[this.bytePtr + 1] & 0xff) << 8) + - ((this.bytes[this.bytePtr + 2] & 0xff))) >>> (8 - this.bitPtr)) & 0xffff); - }; - - - /** - * Reads n bits out of the stream, consuming them (moving the bit pointer). - * @param {number} n The number of bits to read. - * @return {number} The read bits, as an unsigned number. - */ - bitjs.io.BitStream.prototype.readBits = function(n) { - return this.peekBits(n, true); - }; - - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. Only use this for uncompressed blocks as this throws away remaining - * bits in the current byte. - * @param {number} n The number of bytes to peek. - * @param {boolean=} movePointers Whether to move the pointer, defaults false. - * @return {Uint8Array} The subarray. - */ - bitjs.io.BitStream.prototype.peekBytes = function(n, movePointers) { - 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) { - this.readBits(1); - } - - movePointers = movePointers || false; - var bytePtr = this.bytePtr; - // var bitPtr = this.bitPtr; - - var result = this.bytes.subarray(bytePtr, bytePtr + n); - - if (movePointers) { - this.bytePtr += n; - } - - return result; - }; - - - /** - * @param {number} n The number of bytes to read. - * @return {Uint8Array} The subarray. - */ - bitjs.io.BitStream.prototype.readBytes = function(n) { - return this.peekBytes(n, true); - }; - - - /** - * This object allows you to peek and consume bytes as numbers and strings - * out of an ArrayBuffer. In this buffer, everything must be byte-aligned. - * - * @param {ArrayBuffer} ab The ArrayBuffer object. - * @param {number=} optOffset The offset into the ArrayBuffer - * @param {number=} optLength The length of this BitStream - * @constructor - */ - 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; - }; - - - /** - * Peeks at the next n bytes as an unsigned number but does not advance the - * pointer - * TODO: This apparently cannot read more than 4 bytes as a number? - * @param {number} n The number of bytes to peek at. - * @return {number} The n bytes interpreted as an unsigned number. - */ - 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) { - return -1; - } - - var result = 0; - // read from last byte to first byte and roll them in - var curByte = this.ptr + n - 1; - while (curByte >= this.ptr) { - result <<= 8; - result |= this.bytes[curByte]; - --curByte; - } - return result; - }; - - - /** - * Returns the next n bytes as an unsigned number (or -1 on error) - * and advances the stream pointer n bytes. - * @param {number} n The number of bytes to read. - * @return {number} The n bytes interpreted as an unsigned number. - */ - bitjs.io.ByteStream.prototype.readNumber = function(n) { - var num = this.peekNumber( n ); - this.ptr += n; - return num; - }; - - - /** - * Returns the next n bytes as a signed number but does not advance the - * pointer. - * @param {number} n The number of bytes to read. - * @return {number} The bytes interpreted as a signed number. - */ - bitjs.io.ByteStream.prototype.peekSignedNumber = function(n) { - var num = this.peekNumber(n); - var HALF = Math.pow(2, (n * 8) - 1); - var FULL = HALF * 2; - - if (num >= HALF) num -= FULL; - - return num; - }; - - - /** - * Returns the next n bytes as a signed number and advances the stream pointer. - * @param {number} n The number of bytes to read. - * @return {number} The bytes interpreted as a signed number. - */ - bitjs.io.ByteStream.prototype.readSignedNumber = function(n) { - var num = this.peekSignedNumber(n); - this.ptr += n; - return num; - }; - - - /** - * This returns n bytes as a sub-array, advancing the pointer if movePointers - * is true. - * @param {number} n The number of bytes to read. - * @param {boolean} movePointers Whether to move the pointers. - * @return {Uint8Array} The subarray. - */ - bitjs.io.ByteStream.prototype.peekBytes = function(n, movePointers) { - if (n <= 0 || typeof n != typeof 1) { - return null; - } - - var result = this.bytes.subarray(this.ptr, this.ptr + n); - - if (movePointers) { - this.ptr += n; - } - - return result; - }; - - - /** - * Reads the next n bytes as a sub-array. - * @param {number} n The number of bytes to read. - * @return {Uint8Array} The subarray. - */ - bitjs.io.ByteStream.prototype.readBytes = function(n) { - return this.peekBytes(n, true); - }; - - - /** - * Peeks at the next n bytes as a string but does not advance the pointer. - * @param {number} n The number of bytes to peek at. - * @return {string} The next n bytes as a string. - */ - bitjs.io.ByteStream.prototype.peekString = function(n) { - if (n <= 0 || typeof n != typeof 1) { - return ""; - } - - var result = ""; - for (var p = this.ptr, end = this.ptr + n; p < end; ++p) { - result += String.fromCharCode(this.bytes[p]); - } - return result; - }; - - - /** - * Returns the next n bytes as an ASCII string and advances the stream pointer - * n bytes. - * @param {number} n The number of bytes to read. - * @return {string} The next n bytes as a string. - */ - bitjs.io.ByteStream.prototype.readString = function(n) { - var strToReturn = this.peekString(n); - this.ptr += n; - return strToReturn; - }; - - - /** - * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. - * @param {number} numBytes The number of bytes to allocate. - * @constructor - */ - bitjs.io.ByteBuffer = function(numBytes) { - if (typeof numBytes !== typeof 1 || numBytes <= 0) { - throw "Error! ByteBuffer initialized with '" + numBytes + "'"; - } - this.data = new Uint8Array(numBytes); - this.ptr = 0; - }; - - - /** - * @param {number} b The byte to insert. - */ - bitjs.io.ByteBuffer.prototype.insertByte = function(b) { - // TODO: throw if byte is invalid? - this.data[this.ptr++] = b; - }; - - - /** - * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. - */ - bitjs.io.ByteBuffer.prototype.insertBytes = function(bytes) { - // TODO: throw if bytes is invalid? - this.data.set(bytes, this.ptr); - this.ptr += bytes.length; - }; - - - /** - * Writes an unsigned number into the next n bytes. If the number is too large - * to fit into n bytes or is negative, an error is thrown. - * @param {number} num The unsigned number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - bitjs.io.ByteBuffer.prototype.writeNumber = function(num, numBytes) { - if (numBytes < 1) { - 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"; - } - if (num > (Math.pow(2, numBytes * 8) - 1)) { - throw "Trying to write " + num + " into only " + numBytes + " bytes"; - } - - // Roll 8-bits at a time into an array of bytes. - var bytes = []; - while (numBytes-- > 0) { - var eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - }; - - - /** - * Writes a signed number into the next n bytes. If the number is too large - * to fit into n bytes, an error is thrown. - * @param {number} num The signed number to write. - * @param {number} numBytes The number of bytes to write the number into. - */ - bitjs.io.ByteBuffer.prototype.writeSignedNumber = function(num, numBytes) { - if (numBytes < 1) { - 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"; - } - - // Roll 8-bits at a time into an array of bytes. - var bytes = []; - while (numBytes-- > 0) { - var eightBits = num & 255; - bytes.push(eightBits); - num >>= 8; - } - - this.insertBytes(bytes); - }; - - - /** - * @param {string} str The ASCII string to write. - */ - bitjs.io.ByteBuffer.prototype.writeASCIIString = function(str) { - 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!"; - } - this.insertByte(curByte); - } - }; -})(); diff --git a/cps/web.py b/cps/web.py index 1a3849dd..3b19148c 100644 --- a/cps/web.py +++ b/cps/web.py @@ -3171,13 +3171,22 @@ def new_user(): return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) - content.nickname = to_save["nickname"] - if config.config_public_reg and not check_valid_domain(to_save["email"]): - flash(_(u"E-mail is not from valid domain"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - title=_(u"Add new user")) + existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\ + .first() + existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower())\ + .first() + if not existing_user and not existing_email: + content.nickname = to_save["nickname"] + if config.config_public_reg and not check_valid_domain(to_save["email"]): + flash(_(u"E-mail is not from valid domain"), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + title=_(u"Add new user")) + else: + content.email = to_save["email"] else: - content.email = to_save["email"] + flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + languages=languages, title=_(u"Add new user"), page="newuser") try: ub.session.add(content) ub.session.commit() @@ -3362,14 +3371,24 @@ def edit_user(user_id): if "locale" in to_save and to_save["locale"]: content.locale = to_save["locale"] if to_save["email"] and to_save["email"] != content.email: - content.email = to_save["email"] + existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ + .first() + if not existing_email: + content.email = to_save["email"] + else: + flash(_(u"Found an existing account for this e-mail address."), category="error") + return render_title_template("user_edit.html", translations=translations, languages=languages, + new_user=0, content=content, downloads=downloads, + title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: content.kindle_mail = to_save["kindle_mail"] try: ub.session.commit() flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") - except IntegrityError: + except IntegrityError as e: ub.session.rollback() + print(e) flash(_(u"An unknown error occured."), category="error") return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, content=content, downloads=downloads, title=_(u"Edit User %(nick)s", diff --git a/cps/worker.py b/cps/worker.py index 2cc2b337..02f46ce6 100644 --- a/cps/worker.py +++ b/cps/worker.py @@ -20,7 +20,7 @@ from __future__ import print_function import smtplib import threading -from datetime import datetime +from datetime import datetime, timedelta import logging import time import socket @@ -221,8 +221,10 @@ class WorkerThread(threading.Thread): if self.UIqueue[self.current]['stat'] == STAT_STARTED: if self.queue[self.current]['taskType'] == TASK_EMAIL: self.UIqueue[self.current]['progress'] = self.get_send_status() - self.UIqueue[self.current]['runtime'] = self._formatRuntime( - datetime.now() - self.queue[self.current]['starttime']) + self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime'] + self.UIqueue[self.current]['rt'] = self.UIqueue[self.current]['formRuntime'].days*24*60 \ + + self.UIqueue[self.current]['formRuntime'].seconds \ + + self.UIqueue[self.current]['formRuntime'].microseconds return self.UIqueue def _convert_any_format(self): @@ -259,7 +261,8 @@ class WorkerThread(threading.Thread): self._handleSuccess() return file_path + format_new_ext else: - web.app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", bookid, format_new_ext) + web.app.logger.info("Book id %d - target format of %s does not exist. Moving forward with convert.", + bookid, format_new_ext) # check if converter-executable is existing if not os.path.exists(web.ub.config.config_converterpath): @@ -300,7 +303,7 @@ class WorkerThread(threading.Thread): if sys.version_info < (3, 0): command = [x.encode(sys.getfilesystemencoding()) for x in command] - p = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) except OSError as e: self._handleError(_(u"Ebook-converter failed: %(error)s", error=e)) return @@ -328,6 +331,11 @@ class WorkerThread(threading.Thread): # process returncode check = p.returncode + calibre_traceback = p.stderr.readlines() + for ele in calibre_traceback: + web.app.logger.debug(ele.strip('\n')) + if not ele.startswith('Traceback') and not ele.startswith(' File'): + error_message = "Calibre failed with error: %s" % ele.strip('\n') # kindlegen returncodes # 0 = Info(prcgen):I1036: Mobi file built successfully @@ -481,31 +489,17 @@ class WorkerThread(threading.Thread): self._handleError(u'Error sending email: ' + e.strerror) return None - def _formatRuntime(self, runtime): - self.UIqueue[self.current]['rt'] = runtime.total_seconds() - val = re.split('\:|\.', str(runtime))[0:3] - erg = list() - for v in val: - if int(v) > 0: - erg.append(v) - retVal = (':'.join(erg)).lstrip('0') + ' s' - if retVal == ' s': - retVal = '0 s' - return retVal - def _handleError(self, error_message): web.app.logger.error(error_message) self.UIqueue[self.current]['stat'] = STAT_FAIL self.UIqueue[self.current]['progress'] = "100 %" - self.UIqueue[self.current]['runtime'] = self._formatRuntime( - datetime.now() - self.queue[self.current]['starttime']) + self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime'] self.UIqueue[self.current]['message'] = error_message def _handleSuccess(self): self.UIqueue[self.current]['stat'] = STAT_FINISH_SUCCESS self.UIqueue[self.current]['progress'] = "100 %" - self.UIqueue[self.current]['runtime'] = self._formatRuntime( - datetime.now() - self.queue[self.current]['starttime']) + self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime'] # Enable logging of smtp lib debug output From b1cb7123a389d723feaf789bb5385d2438ec54f0 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Thu, 11 Jul 2019 20:37:03 +0200 Subject: [PATCH 09/29] Fix for #959 --- cps/helper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cps/helper.py b/cps/helper.py index 114eeccd..1b233cad 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -605,7 +605,10 @@ def render_task_status(tasklist): if 'starttime' not in task: task['starttime'] = "" - task['runtime'] = format_runtime(task['formRuntime']) + if 'formRuntime' not in task: + task['runtime'] = "" + else: + task['runtime'] = format_runtime(task['formRuntime']) # localize the task status if isinstance( task['stat'], int ): From 792367e35e01139426234f7621e9290a8aa1a83f Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 13 Jul 2019 20:27:32 +0200 Subject: [PATCH 10/29] Version update --- cps/updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/updater.py b/cps/updater.py index 7592b326..335e90d2 100644 --- a/cps/updater.py +++ b/cps/updater.py @@ -237,7 +237,7 @@ class Updater(threading.Thread): return False def _stable_version_info(self): - return {'version': '0.6.4 Beta'} # Current version + return {'version': '0.6.4'} # Current version def _nightly_available_updates(self, request_method): tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) From 37736e11d5976e35192e0a15bee354f1767bc759 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 13 Jul 2019 20:32:39 +0200 Subject: [PATCH 11/29] Updated dependencies --- optional-requirements.txt | 2 +- requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/optional-requirements.txt b/optional-requirements.txt index dd478553..e2e936bc 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -20,4 +20,4 @@ Pillow>=4.0.0 rarfile>=2.7 # other natsort>=2.2.0 - +git+https://github.com/decentral1se/comicapi.git@packaging-fix/remove-python-requires#egg=comicapi diff --git a/requirements.txt b/requirements.txt index 7f2776d7..3fb23ea3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,3 @@ SQLAlchemy>=1.1.0 tornado>=4.1 Wand>=0.4.4 unidecode>=0.04.19 -git+https://github.com/wildthyme/comicapi.git@cb279168f9c5cec742b5a05ac8326b9c168a8a91#egg=comicapi From 4708347c16fcabc4a620cd9489984106c5794890 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 13 Jul 2019 20:45:48 +0200 Subject: [PATCH 12/29] Merge branch 'Develop' # Conflicts: # MANIFEST.in # README.md # cps/helper.py # cps/static/js/archive/archive.js # cps/translations/nl/LC_MESSAGES/messages.mo # cps/translations/nl/LC_MESSAGES/messages.po # cps/ub.py # cps/updater.py # cps/web.py # cps/worker.py # optional-requirements.txt --- .gitattributes | 2 +- .gitignore | 2 + MANIFEST.in | 4 +- readme.md => README.md | 4 +- cps.py | 71 +- cps/__init__.py | 147 + cps/about.py | 78 + cps/admin.py | 695 ++ cps/book_formats.py | 217 - cps/cache_buster.py | 12 +- cps/cli.py | 95 +- cps/comic.py | 16 +- cps/config_sql.py | 287 + cps/constants.py | 131 + cps/converter.py | 28 +- cps/db.py | 131 +- cps/editbooks.py | 711 ++ cps/epub.py | 10 +- cps/fb2.py | 6 +- cps/gdrive.py | 158 + cps/gdriveutils.py | 107 +- cps/helper.py | 362 +- cps/isoLanguages.py | 50 +- cps/jinjia.py | 117 + cps/logger.py | 164 + cps/oauth.py | 140 + cps/oauth_bb.py | 344 + cps/opds.py | 328 + cps/pagination.py | 77 + cps/redirect.py | 2 + cps/reverseproxy.py | 3 + cps/server.py | 269 +- cps/services/__init__.py | 36 + cps/services/goodreads.py | 106 + cps/services/simpleldap.py | 82 + cps/shelf.py | 331 + cps/static/css/images/black-10.png | Bin 0 -> 88 bytes cps/static/css/images/black-25.png | Bin 0 -> 88 bytes cps/static/css/images/black-33.png | Bin 0 -> 88 bytes cps/static/css/images/icomoon/credits.txt | 6 + .../icomoon/entypo-25px-000000/PNG/arrow.png | Bin 0 -> 160 bytes .../icomoon/entypo-25px-000000/PNG/cart.png | Bin 0 -> 230 bytes .../icomoon/entypo-25px-000000/PNG/first.png | Bin 0 -> 172 bytes .../icomoon/entypo-25px-000000/PNG/last.png | Bin 0 -> 167 bytes .../icomoon/entypo-25px-000000/PNG/list.png | Bin 0 -> 117 bytes .../icomoon/entypo-25px-000000/PNG/list2.png | Bin 0 -> 98 bytes .../icomoon/entypo-25px-000000/PNG/loop.png | Bin 0 -> 190 bytes .../icomoon/entypo-25px-000000/PNG/music.png | Bin 0 -> 191 bytes .../icomoon/entypo-25px-000000/PNG/pause.png | Bin 0 -> 152 bytes .../icomoon/entypo-25px-000000/PNG/play.png | Bin 0 -> 162 bytes .../entypo-25px-000000/PNG/shuffle.png | Bin 0 -> 280 bytes .../icomoon/entypo-25px-000000/PNG/volume.png | Bin 0 -> 169 bytes .../icomoon/entypo-25px-000000/SVG/arrow.svg | 8 + .../icomoon/entypo-25px-000000/SVG/cart.svg | 8 + .../icomoon/entypo-25px-000000/SVG/first.svg | 8 + .../icomoon/entypo-25px-000000/SVG/last.svg | 8 + .../icomoon/entypo-25px-000000/SVG/list.svg | 8 + .../icomoon/entypo-25px-000000/SVG/list2.svg | 8 + .../icomoon/entypo-25px-000000/SVG/loop.svg | 8 + .../icomoon/entypo-25px-000000/SVG/music.svg | 8 + .../icomoon/entypo-25px-000000/SVG/pause.svg | 8 + .../icomoon/entypo-25px-000000/SVG/play.svg | 8 + .../entypo-25px-000000/SVG/shuffle.svg | 8 + .../icomoon/entypo-25px-000000/SVG/volume.svg | 8 + .../icomoon/entypo-25px-ffffff/PNG/arrow.png | Bin 0 -> 165 bytes .../icomoon/entypo-25px-ffffff/PNG/cart.png | Bin 0 -> 236 bytes .../icomoon/entypo-25px-ffffff/PNG/first.png | Bin 0 -> 179 bytes .../icomoon/entypo-25px-ffffff/PNG/last.png | Bin 0 -> 170 bytes .../icomoon/entypo-25px-ffffff/PNG/list.png | Bin 0 -> 117 bytes .../icomoon/entypo-25px-ffffff/PNG/list2.png | Bin 0 -> 99 bytes .../icomoon/entypo-25px-ffffff/PNG/loop.png | Bin 0 -> 201 bytes .../icomoon/entypo-25px-ffffff/PNG/music.png | Bin 0 -> 195 bytes .../icomoon/entypo-25px-ffffff/PNG/pause.png | Bin 0 -> 154 bytes .../icomoon/entypo-25px-ffffff/PNG/play.png | Bin 0 -> 166 bytes .../entypo-25px-ffffff/PNG/shuffle.png | Bin 0 -> 291 bytes .../icomoon/entypo-25px-ffffff/PNG/volume.png | Bin 0 -> 165 bytes .../icomoon/entypo-25px-ffffff/SVG/arrow.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/cart.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/first.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/last.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/list.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/list2.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/loop.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/music.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/pause.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/play.svg | 8 + .../entypo-25px-ffffff/SVG/shuffle.svg | 8 + .../icomoon/entypo-25px-ffffff/SVG/volume.svg | 8 + .../icomoon/free-25px-000000/PNG/spinner.png | Bin 0 -> 293 bytes .../icomoon/free-25px-000000/SVG/spinner.svg | 8 + .../icomoon/free-25px-ffffff/PNG/spinner.png | Bin 0 -> 299 bytes .../icomoon/free-25px-ffffff/SVG/spinner.svg | 8 + cps/static/css/images/patterns/credits.txt | 2 + .../patterns/pinstriped_suit_vertical.png | Bin 0 -> 10828 bytes cps/static/css/images/patterns/pool_table.png | Bin 0 -> 40692 bytes .../css/images/patterns/rubber_grip.png | Bin 0 -> 123 bytes .../css/images/patterns/tasky_pattern.png | Bin 0 -> 104 bytes .../css/images/patterns/textured_paper.png | Bin 0 -> 130482 bytes cps/static/css/images/patterns/tweed.png | Bin 0 -> 21309 bytes .../css/images/patterns/wood_pattern.png | Bin 0 -> 103832 bytes .../css/images/patterns/wood_pattern_dark.png | Bin 0 -> 33072 bytes cps/static/css/images/patterns/woven.png | Bin 0 -> 1165 bytes cps/static/css/libs/bar-ui.css | 1001 +++ cps/static/css/listen.css | 114 + cps/static/css/style.css | 16 + cps/static/js/archive/archive.js | 61 +- cps/static/js/archive/unzip.js | 4 +- cps/static/js/filter_list.js | 195 + cps/static/js/get_meta.js | 1 + cps/static/js/io/bitstream.js | 0 cps/static/js/io/bytestream.js | 29 + cps/static/js/libs/bar-ui.js | 1745 +++++ cps/static/js/libs/plugins.js | 75 +- cps/static/js/libs/soundmanager2.js | 6294 +++++++++++++++++ cps/static/js/logviewer.js | 74 + cps/static/js/main.js | 29 +- cps/static/js/table.js | 6 +- cps/static/js/uploadprogress.js | 11 +- cps/subproc_wrapper.py | 55 + cps/templates/admin.html | 17 +- cps/templates/author.html | 31 +- cps/templates/book_edit.html | 16 +- cps/templates/config_edit.html | 189 +- cps/templates/config_view_edit.html | 110 +- cps/templates/detail.html | 69 +- cps/templates/discover.html | 11 +- cps/templates/email_edit.html | 9 +- cps/templates/feed.xml | 14 +- cps/templates/http_error.html | 4 +- cps/templates/index.html | 48 +- cps/templates/index.xml | 57 +- cps/templates/json.txt | 52 +- cps/templates/languages.html | 2 +- cps/templates/layout.html | 93 +- cps/templates/list.html | 55 +- cps/templates/listenmp3.html | 238 + cps/templates/login.html | 19 +- cps/templates/logviewer.html | 15 + cps/templates/osd.xml | 4 +- cps/templates/read.html | 13 +- cps/templates/readcbr.html | 4 +- cps/templates/readpdf.html | 8 +- cps/templates/readtxt.html | 6 +- cps/templates/register.html | 15 + cps/templates/remote_login.html | 4 +- cps/templates/search.html | 33 +- cps/templates/search_form.html | 2 +- cps/templates/shelf.html | 23 +- cps/templates/shelf_edit.html | 2 +- cps/templates/shelf_order.html | 4 +- cps/templates/shelfdown.html | 82 + cps/templates/tasks.html | 4 +- cps/templates/user_edit.html | 91 +- cps/translations/de/LC_MESSAGES/messages.mo | Bin 48405 -> 52969 bytes cps/translations/de/LC_MESSAGES/messages.po | 2064 +++--- cps/translations/es/LC_MESSAGES/messages.mo | Bin 48145 -> 44274 bytes cps/translations/es/LC_MESSAGES/messages.po | 2054 +++--- cps/translations/fr/LC_MESSAGES/messages.mo | Bin 49829 -> 47514 bytes cps/translations/fr/LC_MESSAGES/messages.po | 2054 +++--- cps/translations/hu/LC_MESSAGES/messages.mo | Bin 48657 -> 44426 bytes cps/translations/hu/LC_MESSAGES/messages.po | 2056 +++--- cps/translations/it/LC_MESSAGES/messages.mo | Bin 47969 -> 46797 bytes cps/translations/it/LC_MESSAGES/messages.po | 2056 +++--- cps/translations/ja/LC_MESSAGES/messages.mo | Bin 52473 -> 47594 bytes cps/translations/ja/LC_MESSAGES/messages.po | 2056 +++--- cps/translations/km/LC_MESSAGES/messages.mo | Bin 58406 -> 32544 bytes cps/translations/km/LC_MESSAGES/messages.po | 2048 +++--- cps/translations/nl/LC_MESSAGES/messages.mo | Bin 35023 -> 46991 bytes cps/translations/nl/LC_MESSAGES/messages.po | 2052 +++--- cps/translations/pl/LC_MESSAGES/messages.mo | Bin 46799 -> 28814 bytes cps/translations/pl/LC_MESSAGES/messages.po | 2048 +++--- cps/translations/ru/LC_MESSAGES/messages.mo | Bin 59026 -> 52816 bytes cps/translations/ru/LC_MESSAGES/messages.po | 2054 +++--- cps/translations/sv/LC_MESSAGES/messages.mo | Bin 47724 -> 44899 bytes cps/translations/sv/LC_MESSAGES/messages.po | 2054 +++--- cps/translations/uk/LC_MESSAGES/messages.mo | Bin 56594 -> 42578 bytes cps/translations/uk/LC_MESSAGES/messages.po | 2050 +++--- .../zh_Hans_CN/LC_MESSAGES/messages.mo | Bin 46504 -> 42762 bytes .../zh_Hans_CN/LC_MESSAGES/messages.po | 2054 +++--- cps/ub.py | 673 +- cps/updater.py | 211 +- cps/uploader.py | 219 +- cps/web.py | 3804 ++-------- cps/worker.py | 168 +- messages.pot | 1998 +++--- optional-requirements.txt | 13 +- setup.cfg | 19 +- setup.py | 22 +- test/Calibre-Web TestSummary.html | 615 +- 189 files changed, 33354 insertions(+), 17681 deletions(-) rename readme.md => README.md (97%) create mode 100644 cps/about.py create mode 100644 cps/admin.py delete mode 100644 cps/book_formats.py create mode 100644 cps/config_sql.py create mode 100644 cps/constants.py create mode 100644 cps/editbooks.py create mode 100644 cps/gdrive.py create mode 100644 cps/jinjia.py create mode 100644 cps/logger.py create mode 100644 cps/oauth.py create mode 100644 cps/oauth_bb.py create mode 100644 cps/opds.py create mode 100644 cps/pagination.py create mode 100644 cps/services/__init__.py create mode 100644 cps/services/goodreads.py create mode 100644 cps/services/simpleldap.py create mode 100644 cps/shelf.py create mode 100644 cps/static/css/images/black-10.png create mode 100644 cps/static/css/images/black-25.png create mode 100644 cps/static/css/images/black-33.png create mode 100644 cps/static/css/images/icomoon/credits.txt create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/arrow.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/cart.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/first.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/last.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/list.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/list2.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/loop.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/music.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/pause.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/play.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/shuffle.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/PNG/volume.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/arrow.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/cart.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/first.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/last.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/list.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/list2.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/loop.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/music.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/pause.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/play.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/shuffle.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-000000/SVG/volume.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/arrow.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/cart.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/first.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/last.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/list.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/list2.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/loop.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/music.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/pause.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/play.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/shuffle.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/volume.png create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/arrow.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/cart.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/first.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/last.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list2.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/loop.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/music.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/pause.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/play.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/shuffle.svg create mode 100644 cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/volume.svg create mode 100644 cps/static/css/images/icomoon/free-25px-000000/PNG/spinner.png create mode 100644 cps/static/css/images/icomoon/free-25px-000000/SVG/spinner.svg create mode 100644 cps/static/css/images/icomoon/free-25px-ffffff/PNG/spinner.png create mode 100644 cps/static/css/images/icomoon/free-25px-ffffff/SVG/spinner.svg create mode 100644 cps/static/css/images/patterns/credits.txt create mode 100644 cps/static/css/images/patterns/pinstriped_suit_vertical.png create mode 100644 cps/static/css/images/patterns/pool_table.png create mode 100644 cps/static/css/images/patterns/rubber_grip.png create mode 100644 cps/static/css/images/patterns/tasky_pattern.png create mode 100644 cps/static/css/images/patterns/textured_paper.png create mode 100644 cps/static/css/images/patterns/tweed.png create mode 100644 cps/static/css/images/patterns/wood_pattern.png create mode 100644 cps/static/css/images/patterns/wood_pattern_dark.png create mode 100644 cps/static/css/images/patterns/woven.png create mode 100644 cps/static/css/libs/bar-ui.css create mode 100644 cps/static/css/listen.css create mode 100644 cps/static/js/filter_list.js mode change 100755 => 100644 cps/static/js/io/bitstream.js create mode 100644 cps/static/js/libs/bar-ui.js create mode 100644 cps/static/js/libs/soundmanager2.js create mode 100644 cps/static/js/logviewer.js create mode 100644 cps/subproc_wrapper.py create mode 100644 cps/templates/listenmp3.html create mode 100644 cps/templates/logviewer.html create mode 100644 cps/templates/shelfdown.html diff --git a/.gitattributes b/.gitattributes index f4bb7a9f..92739fe9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -updater.py ident export-subst +constants.py ident export-subst /test export-ignore cps/static/css/libs/* linguist-vendored cps/static/js/libs/* linguist-vendored diff --git a/.gitignore b/.gitignore index 09bf3faa..981158fe 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ __pycache__/ .Python env/ eggs/ +dist/ +build/ .eggs/ *.egg-info/ .installed.cfg diff --git a/MANIFEST.in b/MANIFEST.in index f4dcc845..b667159c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1 @@ -include cps/static/* -include cps/templates/* -include cps/translations/* +graft src/calibreweb diff --git a/readme.md b/README.md similarity index 97% rename from readme.md rename to README.md index 32afff07..3ea98034 100644 --- a/readme.md +++ b/README.md @@ -4,7 +4,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d *This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.* -![Main screen](../../wiki/images/main_screen.png) +![Main screen](https://github.com/janeczku/calibre-web/wiki/images/main_screen.png) ## Features @@ -82,4 +82,4 @@ Pre-built Docker images are available in these Docker Hub repositories: # Wiki -For further informations, How To's and FAQ please check the [Wiki](../../wiki) +For further informations, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki) diff --git a/cps.py b/cps.py index 055c0ffe..ca7d7230 100755 --- a/cps.py +++ b/cps.py @@ -1,21 +1,68 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2012-2019 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import absolute_import, division, print_function, unicode_literals import sys +import os + -base_path = os.path.dirname(os.path.abspath(__file__)) # Insert local directories into path -sys.path.append(base_path) -sys.path.append(os.path.join(base_path, 'cps')) -sys.path.append(os.path.join(base_path, 'vendor')) +if sys.version_info < (3, 0): + sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8')))) + sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor')) +else: + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor')) + + +from cps import create_app +from cps import web_server +from cps.opds import opds +from cps.web import web +from cps.jinjia import jinjia +from cps.about import about +from cps.shelf import shelf +from cps.admin import admi +from cps.gdrive import gdrive +from cps.editbooks import editbook +try: + from cps.oauth_bb import oauth + oauth_available = True +except ImportError: + oauth_available = False + + +def main(): + app = create_app() + app.register_blueprint(web) + app.register_blueprint(opds) + app.register_blueprint(jinjia) + app.register_blueprint(about) + app.register_blueprint(shelf) + app.register_blueprint(admi) + app.register_blueprint(gdrive) + app.register_blueprint(editbook) + if oauth_available: + app.register_blueprint(oauth) + success = web_server.start() + sys.exit(0 if success else 1) -from cps.server import Server if __name__ == '__main__': - Server.startServer() - - - - - + main() diff --git a/cps/__init__.py b/cps/__init__.py index faa18be5..5808f8ae 100755 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -1,2 +1,149 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import sys +import os +import mimetypes + +from babel import Locale as LC +from babel import negotiate_locale +from babel.core import UnknownLocaleError +from flask import Flask, request, g +from flask_login import LoginManager +from flask_babel import Babel +from flask_principal import Principal + +from . import logger, cache_buster, cli, config_sql, ub +from .reverseproxy import ReverseProxied + + +mimetypes.init() +mimetypes.add_type('application/xhtml+xml', '.xhtml') +mimetypes.add_type('application/epub+zip', '.epub') +mimetypes.add_type('application/fb2+zip', '.fb2') +mimetypes.add_type('application/x-mobipocket-ebook', '.mobi') +mimetypes.add_type('application/x-mobipocket-ebook', '.prc') +mimetypes.add_type('application/vnd.amazon.ebook', '.azw') +mimetypes.add_type('application/x-cbr', '.cbr') +mimetypes.add_type('application/x-cbz', '.cbz') +mimetypes.add_type('application/x-cbt', '.cbt') +mimetypes.add_type('image/vnd.djvu', '.djvu') +mimetypes.add_type('application/mpeg', '.mpeg') +mimetypes.add_type('application/mpeg', '.mp3') +mimetypes.add_type('application/mp4', '.m4a') +mimetypes.add_type('application/mp4', '.m4b') +mimetypes.add_type('application/ogg', '.ogg') +mimetypes.add_type('application/ogg', '.oga') + +app = Flask(__name__) + +lm = LoginManager() +lm.login_view = 'web.login' +lm.anonymous_user = ub.Anonymous + + +ub.init_db(cli.settingspath) +config = config_sql.load_configuration(ub.session) +from . import db, services + +searched_ids = {} + +from .worker import WorkerThread +global_WorkerThread = WorkerThread() + +from .server import WebServer +web_server = WebServer() + +babel = Babel() +_BABEL_TRANSLATIONS = set() + +log = logger.create() + + +def create_app(): + app.wsgi_app = ReverseProxied(app.wsgi_app) + # For python2 convert path to unicode + if sys.version_info < (3, 0): + app.static_folder = app.static_folder.decode('utf-8') + app.root_path = app.root_path.decode('utf-8') + app.instance_path = app.instance_path .decode('utf-8') + + cache_buster.init_cache_busting(app) + + log.info('Starting Calibre Web...') + Principal(app) + lm.init_app(app) + app.secret_key = os.getenv('SECRET_KEY', 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT') + + web_server.init_app(app, config) + db.setup_db(config) + + babel.init_app(app) + _BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations()) + _BABEL_TRANSLATIONS.add('en') + + if services.ldap: + services.ldap.init_app(app, config) + if services.goodreads: + services.goodreads.connect(config.config_goodreads_api_key, config.config_goodreads_api_secret, config.config_use_goodreads) + + global_WorkerThread.start() + return app + +@babel.localeselector +def negociate_locale(): + # if a user is logged in, use the locale from the user settings + user = getattr(g, 'user', None) + # user = None + if user is not None and hasattr(user, "locale"): + if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings + return user.locale + + preferred = set() + if request.accept_languages: + for x in request.accept_languages.values(): + try: + preferred.add(str(LC.parse(x.replace('-', '_')))) + except (UnknownLocaleError, ValueError) as e: + log.warning('Could not parse locale "%s": %s', x, e) + # preferred.append('en') + + return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS) + + +def get_locale(): + return request._locale + + +@babel.timezoneselector +def get_timezone(): + user = getattr(g, 'user', None) + if user is not None: + return user.timezone + +from .updater import Updater +updater_thread = Updater() + + +__all__ = ['app'] diff --git a/cps/about.py b/cps/about.py new file mode 100644 index 00000000..42ffe559 --- /dev/null +++ b/cps/about.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import sys +import requests + +from flask import Blueprint +from flask import __version__ as flaskVersion +from flask_babel import gettext as _ +from flask_principal import __version__ as flask_principalVersion +from flask_login import login_required +try: + from flask_login import __version__ as flask_loginVersion +except ImportError: + from flask_login.__about__ import __version__ as flask_loginVersion +from werkzeug import __version__ as werkzeugVersion + +from babel import __version__ as babelVersion +from jinja2 import __version__ as jinja2Version +from pytz import __version__ as pytzVersion +from sqlalchemy import __version__ as sqlalchemyVersion + +from . import db, converter, uploader +from .isoLanguages import __version__ as iso639Version +from .server import VERSION as serverVersion +from .web import render_title_template + + +about = Blueprint('about', __name__) + + +@about.route("/stats") +@login_required +def stats(): + counter = db.session.query(db.Books).count() + authors = db.session.query(db.Authors).count() + categorys = db.session.query(db.Tags).count() + series = db.session.query(db.Series).count() + versions = uploader.get_versions() + versions['Babel'] = 'v' + babelVersion + versions['Sqlalchemy'] = 'v' + sqlalchemyVersion + versions['Werkzeug'] = 'v' + werkzeugVersion + versions['Jinja2'] = 'v' + jinja2Version + versions['Flask'] = 'v' + flaskVersion + versions['Flask Login'] = 'v' + flask_loginVersion + versions['Flask Principal'] = 'v' + flask_principalVersion + versions['Iso 639'] = 'v' + iso639Version + versions['pytz'] = 'v' + pytzVersion + + versions['Requests'] = 'v' + requests.__version__ + versions['pySqlite'] = 'v' + db.session.bind.dialect.dbapi.version + versions['Sqlite'] = 'v' + db.session.bind.dialect.dbapi.sqlite_version + versions.update(converter.versioncheck()) + versions.update(serverVersion) + versions['Python'] = sys.version + return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, + categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat") diff --git a/cps/admin.py b/cps/admin.py new file mode 100644 index 00000000..69aee9d5 --- /dev/null +++ b/cps/admin.py @@ -0,0 +1,695 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import os +import base64 +import json +import time +from datetime import datetime, timedelta +# try: +# from imp import reload +# except ImportError: +# pass + +from babel import Locale as LC +from babel.dates import format_datetime +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_ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.sql.expression import func +from werkzeug.security import generate_password_hash + +from . import constants, logger, helper, services +from . import db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils +from .helper import speaking_language, check_valid_domain, send_test_mail, generate_random_password, send_registration_mail +from .gdriveutils import is_gdrive_ready, gdrive_support +from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano + +feature_support = { + 'ldap': bool(services.ldap), + 'goodreads': bool(services.goodreads) + } + +# try: +# import rarfile +# feature_support['rar'] = True +# except ImportError: +# feature_support['rar'] = False + +try: + from .oauth_bb import oauth_check + feature_support['oauth'] = True +except ImportError: + feature_support['oauth'] = False + oauth_check = {} + + +feature_support['gdrive'] = gdrive_support +admi = Blueprint('admin', __name__) +log = logger.create() + + +@admi.route("/admin") +@login_required +def admin_forbidden(): + abort(403) + + +@admi.route("/shutdown") +@login_required +@admin_required +def shutdown(): + task = int(request.args.get("parameter").strip()) + if task in (0, 1): # valid commandos received + # close all database connections + db.dispose() + ub.dispose() + + showtext = {} + if task == 0: + showtext['text'] = _(u'Server restarted, please reload page') + else: + showtext['text'] = _(u'Performing shutdown of server, please close window') + # stop gevent/tornado server + web_server.stop(task == 0) + return json.dumps(showtext) + + if task == 2: + log.warning("reconnecting to calibre database") + db.setup_db(config) + return '{}' + + abort(404) + + +@admi.route("/admin/view") +@login_required +@admin_required +def admin(): + version = updater_thread.get_current_version_info() + if version is False: + commit = _(u'Unknown') + else: + if 'datetime' in version: + commit = version['datetime'] + + tz = timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) + form_date = datetime.strptime(commit[:19], "%Y-%m-%dT%H:%M:%S") + if len(commit) > 19: # check if string has timezone + if commit[19] == '+': + form_date -= timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) + elif commit[19] == '-': + form_date += timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) + commit = format_datetime(form_date - tz, format='short', locale=get_locale()) + else: + commit = version['version'] + + allUser = ub.session.query(ub.User).all() + email_settings = config.get_mail_settings() + return render_title_template("admin.html", allUser=allUser, email=email_settings, config=config, commit=commit, + title=_(u"Admin page"), page="admin") + + +@admi.route("/admin/config", methods=["GET", "POST"]) +@login_required +@admin_required +def configuration(): + if request.method == "POST": + return _configuration_update_helper() + return _configuration_result() + + +@admi.route("/admin/viewconfig") +@login_required +@admin_required +def view_configuration(): + readColumn = db.session.query(db.Custom_Columns)\ + .filter(and_(db.Custom_Columns.datatype == 'bool',db.Custom_Columns.mark_for_delete == 0)).all() + return render_title_template("config_view_edit.html", conf=config, readColumns=readColumn, + title=_(u"UI Configuration"), page="uiconfig") + + +@admi.route("/admin/viewconfig", methods=["POST"]) +@login_required +@admin_required +def update_view_configuration(): + reboot_required = False + to_save = request.form.to_dict() + + _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) + _config_int = lambda x: config.set_from_dictionary(to_save, x, int) + + _config_string("config_calibre_web_title") + _config_string("config_columns_to_ignore") + _config_string("config_mature_content_tags") + reboot_required |= _config_string("config_title_regex") + + _config_int("config_read_column") + _config_int("config_theme") + _config_int("config_random_books") + _config_int("config_books_per_page") + _config_int("config_authors_max") + + config.config_default_role = constants.selected_roles(to_save) + config.config_default_role &= ~constants.ROLE_ANONYMOUS + + config.config_default_show = sum(int(k[5:]) for k in to_save if k.startswith('show_')) + if "Show_mature_content" in to_save: + config.config_default_show |= constants.MATURE_CONTENT + + config.save() + flash(_(u"Calibre-Web configuration updated"), category="success") + before_request() + if reboot_required: + db.dispose() + ub.dispose() + web_server.stop(True) + + return view_configuration() + + +@admi.route("/ajax/editdomain", methods=['POST']) +@login_required +@admin_required +def edit_domain(): + # POST /post + # name: 'username', //name of field (column in db) + # pk: 1 //primary key (record id) + # value: 'superuser!' //new value + vals = request.form.to_dict() + answer = ub.session.query(ub.Registration).filter(ub.Registration.id == vals['pk']).first() + # domain_name = request.args.get('domain') + answer.domain = vals['value'].replace('*', '%').replace('?', '_').lower() + ub.session.commit() + return "" + + +@admi.route("/ajax/adddomain", methods=['POST']) +@login_required +@admin_required +def add_domain(): + domain_name = request.form.to_dict()['domainname'].replace('*', '%').replace('?', '_').lower() + check = ub.session.query(ub.Registration).filter(ub.Registration.domain == domain_name).first() + if not check: + new_domain = ub.Registration(domain=domain_name) + ub.session.add(new_domain) + ub.session.commit() + return "" + + +@admi.route("/ajax/deletedomain", methods=['POST']) +@login_required +@admin_required +def delete_domain(): + domain_id = request.form.to_dict()['domainid'].replace('*', '%').replace('?', '_').lower() + ub.session.query(ub.Registration).filter(ub.Registration.id == domain_id).delete() + ub.session.commit() + # If last domain was deleted, add all domains by default + if not ub.session.query(ub.Registration).count(): + new_domain = ub.Registration(domain="%.%") + ub.session.add(new_domain) + ub.session.commit() + return "" + + +@admi.route("/ajax/domainlist") +@login_required +@admin_required +def list_domain(): + answer = ub.session.query(ub.Registration).all() + json_dumps = json.dumps([{"domain": r.domain.replace('%', '*').replace('_', '?'), "id": r.id} for r in answer]) + js = json.dumps(json_dumps.replace('"', "'")).lstrip('"').strip('"') + response = make_response(js.replace("'", '"')) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response + + +@admi.route("/config", methods=["GET", "POST"]) +@unconfigured +def basic_configuration(): + logout_user() + if request.method == "POST": + return _configuration_update_helper() + return _configuration_result() + + +def _configuration_update_helper(): + reboot_required = False + db_change = False + to_save = request.form.to_dict() + + _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) + _config_int = lambda x: config.set_from_dictionary(to_save, x, int) + _config_checkbox = lambda x: config.set_from_dictionary(to_save, x, lambda y: y == "on", False) + _config_checkbox_int = lambda x: config.set_from_dictionary(to_save, x, lambda y: 1 if (y == "on") else 0, 0) + + db_change |= _config_string("config_calibre_dir") + + # Google drive setup + if not os.path.isfile(gdriveutils.SETTINGS_YAML): + config.config_use_google_drive = False + + gdrive_secrets = {} + gdriveError = gdriveutils.get_error_text(gdrive_secrets) + if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdriveError: + if not gdrive_secrets: + return _configuration_result('client_secrets.json is not configured for web application') + gdriveutils.update_settings( + gdrive_secrets['client_id'], + gdrive_secrets['client_secret'], + gdrive_secrets['redirect_uris'][0] + ) + + # always show google drive settings, but in case of error deny support + config.config_use_google_drive = (not gdriveError) and ("config_use_google_drive" in to_save) + if _config_string("config_google_drive_folder"): + gdriveutils.deleteDatabaseOnChange() + + reboot_required |= _config_int("config_port") + + reboot_required |= _config_string("config_keyfile") + if config.config_keyfile and not os.path.isfile(config.config_keyfile): + return _configuration_result('Keyfile location is not valid, please enter correct path', gdriveError) + + reboot_required |= _config_string("config_certfile") + if config.config_certfile and not os.path.isfile(config.config_certfile): + return _configuration_result('Certfile location is not valid, please enter correct path', gdriveError) + + _config_checkbox_int("config_uploading") + _config_checkbox_int("config_anonbrowse") + _config_checkbox_int("config_public_reg") + + _config_int("config_ebookconverter") + _config_string("config_calibre") + _config_string("config_converterpath") + + if _config_int("config_login_type"): + reboot_required |= config.config_login_type != constants.LOGIN_STANDARD + + #LDAP configurator, + if config.config_login_type == constants.LOGIN_LDAP: + _config_string("config_ldap_provider_url") + _config_int("config_ldap_port") + _config_string("config_ldap_schema") + _config_string("config_ldap_dn") + _config_string("config_ldap_user_object") + if not config.config_ldap_provider_url or not config.config_ldap_port or not config.config_ldap_dn or not config.config_ldap_user_object: + return _configuration_result('Please enter a LDAP provider, port, DN and user object identifier', gdriveError) + + _config_string("config_ldap_serv_username") + if not config.config_ldap_serv_username or "config_ldap_serv_password" not in to_save: + return _configuration_result('Please enter a LDAP service account and password', gdriveError) + config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode) + + _config_checkbox("config_ldap_use_ssl") + _config_checkbox("config_ldap_use_tls") + _config_checkbox("config_ldap_openldap") + _config_checkbox("config_ldap_require_cert") + _config_string("config_ldap_cert_path") + if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path): + return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError) + + # Remote login configuration + _config_checkbox("config_remote_login") + if not config.config_remote_login: + ub.session.query(ub.RemoteAuthToken).delete() + + # Goodreads configuration + _config_checkbox("config_use_goodreads") + _config_string("config_goodreads_api_key") + _config_string("config_goodreads_api_secret") + if services.goodreads: + services.goodreads.connect(config.config_goodreads_api_key, config.config_goodreads_api_secret, config.config_use_goodreads) + + _config_int("config_updatechannel") + + # GitHub OAuth configuration + if config.config_login_type == constants.LOGIN_OAUTH_GITHUB: + _config_string("config_github_oauth_client_id") + _config_string("config_github_oauth_client_secret") + if not config.config_github_oauth_client_id or not config.config_github_oauth_client_secret: + return _configuration_result('Please enter Github oauth credentials', gdriveError) + + # Google OAuth configuration + if config.config_login_type == constants.LOGIN_OAUTH_GOOGLE: + _config_string("config_google_oauth_client_id") + _config_string("config_google_oauth_client_secret") + if not config.config_google_oauth_client_id or not config.config_google_oauth_client_secret: + return _configuration_result('Please enter Google oauth credentials', gdriveError) + + _config_int("config_log_level") + _config_string("config_logfile") + if not logger.is_valid_logfile(config.config_logfile): + return _configuration_result('Logfile location is not valid, please enter correct path', gdriveError) + + reboot_required |= _config_checkbox_int("config_access_log") + reboot_required |= _config_string("config_access_logfile") + if not logger.is_valid_logfile(config.config_access_logfile): + return _configuration_result('Access Logfile location is not valid, please enter correct path', gdriveError) + + # Rarfile Content configuration + _config_string("config_rarfile_location") + unrar_status = helper.check_unrar(config.config_rarfile_location) + if unrar_status: + return _configuration_result(unrar_status, gdriveError) + + try: + metadata_db = os.path.join(config.config_calibre_dir, "metadata.db") + if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db): + gdriveutils.downloadFile(None, "metadata.db", metadata_db) + db_change = True + except Exception as e: + return _configuration_result('%s' % e, gdriveError) + + if db_change: + # reload(db) + if not db.setup_db(config): + return _configuration_result('DB location is not valid, please enter correct path', gdriveError) + + config.save() + flash(_(u"Calibre-Web configuration updated"), category="success") + if reboot_required: + web_server.stop(True) + + return _configuration_result(None, gdriveError) + + +def _configuration_result(error_flash=None, gdriveError=None): + gdrive_authenticate = not is_gdrive_ready() + gdrivefolders = [] + if gdriveError is None: + gdriveError = gdriveutils.get_error_text() + if gdriveError: + gdriveError = _(gdriveError) + else: + gdrivefolders = gdriveutils.listRootFolders() + + show_back_button = current_user.is_authenticated + show_login_button = config.db_configured and not current_user.is_authenticated + if error_flash: + config.load() + flash(_(error_flash), category="error") + show_login_button = False + + return render_title_template("config_edit.html", config=config, + show_back_button=show_back_button, show_login_button=show_login_button, + show_authenticate_google_drive=gdrive_authenticate, + gdriveError=gdriveError, gdrivefolders=gdrivefolders, feature_support=feature_support, + title=_(u"Basic Configuration"), page="config") + + +@admi.route("/admin/user/new", methods=["GET", "POST"]) +@login_required +@admin_required +def new_user(): + content = ub.User() + languages = speaking_language() + translations = [LC('en')] + babel.list_translations() + if request.method == "POST": + to_save = request.form.to_dict() + content.default_language = to_save["default_language"] + content.mature_content = "Show_mature_content" in to_save + content.locale = to_save.get("locale", content.locale) + + content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_')) + if "show_detail_random" in to_save: + content.sidebar_view |= constants.DETAIL_RANDOM + + content.role = constants.selected_roles(to_save) + + if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: + flash(_(u"Please fill out all fields!"), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + registered_oauth=oauth_check, title=_(u"Add new user")) + content.password = generate_password_hash(to_save["password"]) + existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\ + .first() + existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower())\ + .first() + if not existing_user and not existing_email: + content.nickname = to_save["nickname"] + if config.config_public_reg and not check_valid_domain(to_save["email"]): + flash(_(u"E-mail is not from valid domain"), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + registered_oauth=oauth_check, title=_(u"Add new user")) + else: + content.email = to_save["email"] + else: + flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + languages=languages, title=_(u"Add new user"), page="newuser", + registered_oauth=oauth_check) + try: + ub.session.add(content) + ub.session.commit() + flash(_(u"User '%(user)s' created", user=content.nickname), category="success") + return redirect(url_for('admin.admin')) + except IntegrityError: + ub.session.rollback() + flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + else: + content.role = config.config_default_role + content.sidebar_view = config.config_default_show + content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + languages=languages, title=_(u"Add new user"), page="newuser", + registered_oauth=oauth_check) + + +@admi.route("/admin/mailsettings") +@login_required +@admin_required +def edit_mailsettings(): + content = config.get_mail_settings() + # log.debug("edit_mailsettings %r", content) + return render_title_template("email_edit.html", content=content, title=_(u"Edit e-mail server settings"), + page="mailset") + + +@admi.route("/admin/mailsettings", methods=["POST"]) +@login_required +@admin_required +def update_mailsettings(): + to_save = request.form.to_dict() + log.debug("update_mailsettings %r", to_save) + + _config_string = lambda x: config.set_from_dictionary(to_save, x, lambda y: y.strip() if y else y) + _config_int = lambda x: config.set_from_dictionary(to_save, x, int) + + _config_string("mail_server") + _config_int("mail_port") + _config_int("mail_use_ssl") + _config_string("mail_login") + _config_string("mail_password") + _config_string("mail_from") + config.save() + + if to_save.get("test"): + if current_user.kindle_mail: + result = send_test_mail(current_user.kindle_mail, current_user.nickname) + if result is None: + flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.kindle_mail), + category="success") + else: + flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") + else: + flash(_(u"Please configure your kindle e-mail address first..."), category="error") + else: + flash(_(u"E-mail server settings updated"), category="success") + + return edit_mailsettings() + + +@admi.route("/admin/user/", methods=["GET", "POST"]) +@login_required +@admin_required +def edit_user(user_id): + content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User + downloads = list() + languages = speaking_language() + translations = babel.list_translations() + [LC('en')] + for book in content.downloads: + downloadbook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + if downloadbook: + downloads.append(downloadbook) + else: + ub.delete_download(book.book_id) + # ub.session.query(ub.Downloads).filter(book.book_id == ub.Downloads.book_id).delete() + # ub.session.commit() + if request.method == "POST": + to_save = request.form.to_dict() + if "delete" in to_save: + if ub.session.query(ub.User).filter(and_(ub.User.role.op('&') + (constants.ROLE_ADMIN)== constants.ROLE_ADMIN, + ub.User.id != content.id)).count(): + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session.commit() + flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") + return redirect(url_for('admin.admin')) + else: + flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error") + return redirect(url_for('admin.admin')) + else: + if "password" in to_save and to_save["password"]: + content.password = generate_password_hash(to_save["password"]) + + anonymous = content.is_anonymous + content.role = constants.selected_roles(to_save) + if anonymous: + content.role |= constants.ROLE_ANONYMOUS + else: + content.role &= ~constants.ROLE_ANONYMOUS + + val = [int(k[5:]) for k in to_save if k.startswith('show_')] + sidebar = ub.get_sidebar_config() + for element in sidebar: + value = element['visibility'] + if value in val and not content.check_visibility(value): + content.sidebar_view |= value + elif not value in val and content.check_visibility(value): + content.sidebar_view &= ~value + + if "Show_detail_random" in to_save: + content.sidebar_view |= constants.DETAIL_RANDOM + else: + content.sidebar_view &= ~constants.DETAIL_RANDOM + + content.mature_content = "Show_mature_content" in to_save + + if "default_language" in to_save: + content.default_language = to_save["default_language"] + if "locale" in to_save and to_save["locale"]: + content.locale = to_save["locale"] + if to_save["email"] and to_save["email"] != content.email: + existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ + .first() + if not existing_email: + content.email = to_save["email"] + else: + flash(_(u"Found an existing account for this e-mail address."), category="error") + return render_title_template("user_edit.html", translations=translations, languages=languages, + new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + + if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: + content.kindle_mail = to_save["kindle_mail"] + try: + ub.session.commit() + flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") + except IntegrityError: + ub.session.rollback() + flash(_(u"An unknown error occured."), category="error") + return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, + content=content, downloads=downloads, registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + + +@admi.route("/admin/resetpassword/") +@login_required +@admin_required +def reset_password(user_id): + if not config.config_public_reg: + abort(404) + if current_user is not None and current_user.is_authenticated: + existing_user = ub.session.query(ub.User).filter(ub.User.id == user_id).first() + password = generate_random_password() + existing_user.password = generate_password_hash(password) + try: + ub.session.commit() + send_registration_mail(existing_user.email, existing_user.nickname, password, True) + flash(_(u"Password for user %(user)s reset", user=existing_user.nickname), category="success") + except Exception: + ub.session.rollback() + flash(_(u"An unknown error occurred. Please try again later."), category="error") + return redirect(url_for('admin.admin')) + + +@admi.route("/admin/logfile") +@login_required +@admin_required +def view_logfile(): + logfiles = {} + logfiles[0] = logger.get_logfile(config.config_logfile) + logfiles[1] = logger.get_accesslogfile(config.config_access_logfile) + return render_title_template("logviewer.html",title=_(u"Logfile viewer"), accesslog_enable=config.config_access_log, + logfiles=logfiles, page="logfile") + + +@admi.route("/ajax/log/") +@login_required +@admin_required +def send_logfile(logtype): + if logtype == 1: + logfile = logger.get_accesslogfile(config.config_access_logfile) + return send_from_directory(os.path.dirname(logfile), + os.path.basename(logfile)) + if logtype == 0: + logfile = logger.get_logfile(config.config_logfile) + return send_from_directory(os.path.dirname(logfile), + os.path.basename(logfile)) + else: + return "" + + +@admi.route("/get_update_status", methods=['GET']) +@login_required_if_no_ano +def get_update_status(): + return updater_thread.get_available_updates(request.method) + + +@admi.route("/get_updater_status", methods=['GET', 'POST']) +@login_required +@admin_required +def get_updater_status(): + status = {} + if request.method == "POST": + commit = request.form.to_dict() + if "start" in commit and commit['start'] == 'True': + text = { + "1": _(u'Requesting update package'), + "2": _(u'Downloading update package'), + "3": _(u'Unzipping update package'), + "4": _(u'Replacing files'), + "5": _(u'Database connections are closed'), + "6": _(u'Stopping server'), + "7": _(u'Update finished, please press okay and reload page'), + "8": _(u'Update failed:') + u' ' + _(u'HTTP Error'), + "9": _(u'Update failed:') + u' ' + _(u'Connection error'), + "10": _(u'Update failed:') + u' ' + _(u'Timeout while establishing connection'), + "11": _(u'Update failed:') + u' ' + _(u'General error') + } + status['text'] = text + updater_thread.status = 0 + updater_thread.start() + status['status'] = updater_thread.get_update_status() + elif request.method == "GET": + try: + status['status'] = updater_thread.get_update_status() + if status['status'] == -1: + status['status'] = 7 + except Exception: + status['status'] = 11 + return json.dumps(status) diff --git a/cps/book_formats.py b/cps/book_formats.py deleted file mode 100644 index 125e0b99..00000000 --- a/cps/book_formats.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) -# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski OzzieIsaacs -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import logging -import uploader -import os -from flask_babel import gettext as _ -import comic - -try: - from lxml.etree import LXML_VERSION as lxmlversion -except ImportError: - lxmlversion = None - -__author__ = 'lemmsh' - -logger = logging.getLogger("book_formats") - -try: - from wand.image import Image - from wand import version as ImageVersion - from wand.exceptions import PolicyError - use_generic_pdf_cover = False -except (ImportError, RuntimeError) as e: - logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) - use_generic_pdf_cover = True -try: - from PyPDF2 import PdfFileReader - from PyPDF2 import __version__ as PyPdfVersion - use_pdf_meta = True -except ImportError as e: - logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e) - use_pdf_meta = False - -try: - import epub - use_epub_meta = True -except ImportError as e: - logger.warning('cannot import epub, extracting epub metadata will not work: %s', e) - use_epub_meta = False - -try: - import fb2 - use_fb2_meta = True -except ImportError as e: - logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) - use_fb2_meta = False - -try: - from PIL import Image - from PIL import __version__ as PILversion - use_PIL = True -except ImportError: - use_PIL = False - - -def process(tmp_file_path, original_file_name, original_file_extension): - meta = None - try: - if ".PDF" == original_file_extension.upper(): - meta = pdf_meta(tmp_file_path, original_file_name, original_file_extension) - if ".EPUB" == original_file_extension.upper() and use_epub_meta is True: - meta = epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension) - if ".FB2" == original_file_extension.upper() and use_fb2_meta is True: - meta = fb2.get_fb2_info(tmp_file_path, original_file_extension) - if original_file_extension.upper() in ['.CBZ', '.CBT']: - meta = comic.get_comic_info(tmp_file_path, original_file_name, original_file_extension) - - except Exception as ex: - logger.warning('cannot parse metadata, using default: %s', ex) - - if meta and meta.title.strip() and meta.author.strip(): - return meta - else: - return default_meta(tmp_file_path, original_file_name, original_file_extension) - - -def default_meta(tmp_file_path, original_file_name, original_file_extension): - return uploader.BookMeta( - file_path=tmp_file_path, - extension=original_file_extension, - title=original_file_name, - author=u"Unknown", - cover=None, - description="", - tags="", - series="", - series_id="", - languages="") - - -def pdf_meta(tmp_file_path, original_file_name, original_file_extension): - - if use_pdf_meta: - pdf = PdfFileReader(open(tmp_file_path, 'rb'), strict=False) - doc_info = pdf.getDocumentInfo() - else: - doc_info = None - - if doc_info is not None: - author = doc_info.author if doc_info.author else u"Unknown" - title = doc_info.title if doc_info.title else original_file_name - subject = doc_info.subject - else: - author = u"Unknown" - title = original_file_name - subject = "" - return uploader.BookMeta( - file_path=tmp_file_path, - extension=original_file_extension, - title=title, - author=author, - cover=pdf_preview(tmp_file_path, original_file_name), - description=subject, - tags="", - series="", - series_id="", - languages="") - - -def pdf_preview(tmp_file_path, tmp_dir): - if use_generic_pdf_cover: - return None - else: - if use_PIL: - try: - input1 = PdfFileReader(open(tmp_file_path, 'rb'), strict=False) - page0 = input1.getPage(0) - xObject = page0['/Resources']['/XObject'].getObject() - - for obj in xObject: - if xObject[obj]['/Subtype'] == '/Image': - size = (xObject[obj]['/Width'], xObject[obj]['/Height']) - data = xObject[obj]._data # xObject[obj].getData() - if xObject[obj]['/ColorSpace'] == '/DeviceRGB': - mode = "RGB" - else: - mode = "P" - if '/Filter' in xObject[obj]: - if xObject[obj]['/Filter'] == '/FlateDecode': - img = Image.frombytes(mode, size, data) - cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png" - img.save(filename=os.path.join(tmp_dir, cover_file_name)) - return cover_file_name - # img.save(obj[1:] + ".png") - elif xObject[obj]['/Filter'] == '/DCTDecode': - cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" - img = open(cover_file_name, "wb") - img.write(data) - img.close() - return cover_file_name - elif xObject[obj]['/Filter'] == '/JPXDecode': - cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jp2" - img = open(cover_file_name, "wb") - img.write(data) - img.close() - return cover_file_name - else: - img = Image.frombytes(mode, size, data) - cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png" - img.save(filename=os.path.join(tmp_dir, cover_file_name)) - return cover_file_name - except Exception as ex: - print(ex) - try: - cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" - with Image(filename=tmp_file_path + "[0]", resolution=150) as img: - img.compression_quality = 88 - img.save(filename=os.path.join(tmp_dir, cover_file_name)) - return cover_file_name - except PolicyError as ex: - logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex) - return None - except Exception as ex: - logger.warning('Cannot extract cover image, using default: %s', ex) - return None - -def get_versions(): - if not use_generic_pdf_cover: - IVersion = ImageVersion.MAGICK_VERSION - WVersion = ImageVersion.VERSION - else: - IVersion = _(u'not installed') - WVersion = _(u'not installed') - if use_pdf_meta: - PVersion='v'+PyPdfVersion - else: - PVersion=_(u'not installed') - if lxmlversion: - XVersion = 'v'+'.'.join(map(str, lxmlversion)) - else: - XVersion = _(u'not installed') - if use_PIL: - PILVersion = 'v' + PILversion - else: - PILVersion = _(u'not installed') - return {'Image Magick': IVersion, - 'PyPdf': PVersion, - 'lxml':XVersion, - 'Wand': WVersion, - 'Pillow': PILVersion} diff --git a/cps/cache_buster.py b/cps/cache_buster.py index edd73cec..02aa7187 100644 --- a/cps/cache_buster.py +++ b/cps/cache_buster.py @@ -17,8 +17,14 @@ # Inspired by https://github.com/ChrisTM/Flask-CacheBust # Uses query strings so CSS font files are found without having to resort to absolute URLs -import hashlib +from __future__ import division, print_function, unicode_literals import os +import hashlib + +from . import logger + + +log = logger.create() def init_cache_busting(app): @@ -34,7 +40,7 @@ def init_cache_busting(app): hash_table = {} # map of file hashes - app.logger.debug('Computing cache-busting values...') + log.debug('Computing cache-busting values...') # compute file hashes for dirpath, __, filenames in os.walk(static_folder): for filename in filenames: @@ -47,7 +53,7 @@ def init_cache_busting(app): file_path = rooted_filename.replace(static_folder, "") file_path = file_path.replace("\\", "/") # Convert Windows path to web path hash_table[file_path] = file_hash - app.logger.debug('Finished computing cache-busting values') + log.debug('Finished computing cache-busting values') def bust_filename(filename): return hash_table.get(filename, "") diff --git a/cps/cli.py b/cps/cli.py index 26741c57..de12be5a 100644 --- a/cps/cli.py +++ b/cps/cli.py @@ -18,30 +18,87 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import argparse -import os +from __future__ import division, print_function, unicode_literals import sys +import os +import argparse + +from .constants import CONFIG_DIR as _CONFIG_DIR +from .constants import STABLE_VERSION as _STABLE_VERSION +from .constants import NIGHTLY_VERSION as _NIGHTLY_VERSION + +VALID_CHARACTERS = 'ABCDEFabcdef:0123456789' + +ipv6 = False + + +def version_info(): + if _NIGHTLY_VERSION[1].startswith('$Format'): + return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version'] + else: + return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'],_NIGHTLY_VERSION[1]) + + +def validate_ip4(address): + address_list = address.split('.') + if len(address_list) != 4: + return False + for val in address_list: + if not val.isdigit(): + return False + i = int(val) + if i < 0 or i > 255: + return False + return True + + +def validate_ip6(address): + address_list = address.split(':') + return ( + len(address_list) == 8 + and all(len(current) <= 4 for current in address_list) + and all(current in VALID_CHARACTERS for current in address) + ) + + +def validate_ip(address): + if validate_ip4(address) or ipv6: + return address + print("IP address is invalid. Exiting") + sys.exit(1) + parser = argparse.ArgumentParser(description='Calibre Web is a web app' ' providing a interface for browsing, reading and downloading eBooks\n', prog='cps.py') parser.add_argument('-p', metavar='path', help='path and name to settings db, e.g. /opt/cw.db') parser.add_argument('-g', metavar='path', help='path and name to gdrive db, e.g. /opt/gd.db') -parser.add_argument('-c', metavar='path', help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile') -parser.add_argument('-k', metavar='path', help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile') +parser.add_argument('-c', metavar='path', + help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile') +parser.add_argument('-k', metavar='path', + help='path and name to SSL keyfile, e.g. /opt/test.key, works only in combination with certfile') +parser.add_argument('-v', '--version', action='version', help='Shows version number and exits Calibre-web', + version=version_info()) +parser.add_argument('-i', metavar='ip-adress', help='Server IP-Adress to listen') +parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password') args = parser.parse_args() -generalPath = os.path.normpath(os.getenv("CALIBRE_DBPATH", - os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep)) -if args.p: - settingspath = args.p -else: - settingspath = os.path.join(generalPath, "app.db") +if sys.version_info < (3, 0): + if args.p: + args.p = args.p.decode('utf-8') + if args.g: + args.g = args.g.decode('utf-8') + if args.k: + args.k = args.k.decode('utf-8') + if args.c: + args.c = args.c.decode('utf-8') + if args.s: + args.s = args.s.decode('utf-8') -if args.g: - gdpath = args.g -else: - gdpath = os.path.join(generalPath, "gdrive.db") +settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db") +gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db") + +# handle and check parameter for ssl encryption certfilepath = None keyfilepath = None if args.c: @@ -67,3 +124,13 @@ if (args.k and not args.c) or (not args.k and args.c): if args.k is "": keyfilepath = "" + +# handle and check ipadress argument +if args.i: + ipv6 = validate_ip6(args.i) + ipadress = validate_ip(args.i) +else: + ipadress = None + +# handle and check user password argument +user_password = args.s or None diff --git a/cps/comic.py b/cps/comic.py index 0b7d4f1f..738b2a89 100755 --- a/cps/comic.py +++ b/cps/comic.py @@ -17,19 +17,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals import os -import uploader -import logging -from iso639 import languages as isoLanguages + +from . import logger, isoLanguages +from .constants import BookMeta -logger = logging.getLogger("book_formats") +log = logger.create() + try: from comicapi.comicarchive import ComicArchive, MetaDataStyle use_comic_meta = True except ImportError as e: - logger.warning('cannot import comicapi, extracting comic metadata will not work: %s', e) + log.warning('cannot import comicapi, extracting comic metadata will not work: %s', e) import zipfile import tarfile use_comic_meta = False @@ -96,7 +98,7 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension): else: loadedMetadata.language = "" - return uploader.BookMeta( + return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=loadedMetadata.title or original_file_name, @@ -109,7 +111,7 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension): languages=loadedMetadata.language) else: - return uploader.BookMeta( + return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=original_file_name, diff --git a/cps/config_sql.py b/cps/config_sql.py new file mode 100644 index 00000000..37ea77e5 --- /dev/null +++ b/cps/config_sql.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2019 OzzieIsaacs, pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from __future__ import division, print_function, unicode_literals +import os +import json + +from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean +from sqlalchemy.ext.declarative import declarative_base + +from . import constants, cli, logger + + +log = logger.create() +_Base = declarative_base() + + +# Baseclass for representing settings in app.db with email server settings and Calibre database settings +# (application settings) +class _Settings(_Base): + __tablename__ = 'settings' + + id = Column(Integer, primary_key=True) + mail_server = Column(String, default='mail.example.org') + mail_port = Column(Integer, default=25) + mail_use_ssl = Column(SmallInteger, default=0) + mail_login = Column(String, default='mail@example.com') + mail_password = Column(String, default='mypassword') + mail_from = Column(String, default='automailer ') + config_calibre_dir = Column(String) + config_port = Column(Integer, default=constants.DEFAULT_PORT) + config_certfile = Column(String) + config_keyfile = Column(String) + config_calibre_web_title = Column(String, default=u'Calibre-Web') + config_books_per_page = Column(Integer, default=60) + config_random_books = Column(Integer, default=4) + config_authors_max = Column(Integer, default=0) + config_read_column = Column(Integer, default=0) + config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+') + config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL) + config_access_log = Column(SmallInteger, default=0) + config_uploading = Column(SmallInteger, default=0) + config_anonbrowse = Column(SmallInteger, default=0) + config_public_reg = Column(SmallInteger, default=0) + config_default_role = Column(SmallInteger, default=0) + config_default_show = Column(SmallInteger, default=6143) + config_columns_to_ignore = Column(String) + config_use_google_drive = Column(Boolean, default=False) + config_google_drive_folder = Column(String) + config_google_drive_watch_changes_response = Column(String) + config_remote_login = Column(Boolean, default=False) + config_use_goodreads = Column(Boolean, default=False) + config_goodreads_api_key = Column(String) + config_goodreads_api_secret = Column(String) + config_login_type = Column(Integer, default=0) + # config_use_ldap = Column(Boolean) + config_ldap_provider_url = Column(String) + config_ldap_dn = Column(String) + # config_use_github_oauth = Column(Boolean) + config_github_oauth_client_id = Column(String) + config_github_oauth_client_secret = Column(String) + # config_use_google_oauth = Column(Boolean) + config_google_oauth_client_id = Column(String) + config_google_oauth_client_secret = Column(String) + config_ldap_provider_url = Column(String, default='localhost') + config_ldap_port = Column(SmallInteger, default=389) + config_ldap_schema = Column(String, default='ldap') + config_ldap_serv_username = Column(String) + config_ldap_serv_password = Column(String) + config_ldap_use_ssl = Column(Boolean, default=False) + config_ldap_use_tls = Column(Boolean, default=False) + config_ldap_require_cert = Column(Boolean, default=False) + config_ldap_cert_path = Column(String) + config_ldap_dn = Column(String) + config_ldap_user_object = Column(String) + config_ldap_openldap = Column(Boolean, default=False) + config_mature_content_tags = Column(String, default='') + config_logfile = Column(String) + config_access_logfile = Column(String) + config_ebookconverter = Column(Integer, default=0) + config_converterpath = Column(String) + config_calibre = Column(String) + config_rarfile_location = Column(String) + config_theme = Column(Integer, default=0) + config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE) + + def __repr__(self): + return self.__class__.__name__ + + +# Class holds all application specific settings in calibre-web +class _ConfigSQL(object): + # pylint: disable=no-member + def __init__(self, session): + self._session = session + self._settings = None + self.db_configured = None + self.config_calibre_dir = None + self.load() + + def _read_from_storage(self): + if self._settings is None: + log.debug("_ConfigSQL._read_from_storage") + self._settings = self._session.query(_Settings).first() + return self._settings + + def get_config_certfile(self): + if cli.certfilepath: + return cli.certfilepath + if cli.certfilepath == "": + return None + return self.config_certfile + + def get_config_keyfile(self): + if cli.keyfilepath: + return cli.keyfilepath + if cli.certfilepath == "": + return None + return self.config_keyfile + + def get_config_ipaddress(self): + return cli.ipadress or "" + + def get_ipaddress_type(self): + return cli.ipv6 + + def _has_role(self, role_flag): + return constants.has_flag(self.config_default_role, role_flag) + + def role_admin(self): + return self._has_role(constants.ROLE_ADMIN) + + def role_download(self): + return self._has_role(constants.ROLE_DOWNLOAD) + + def role_viewer(self): + return self._has_role(constants.ROLE_VIEWER) + + def role_upload(self): + return self._has_role(constants.ROLE_UPLOAD) + + def role_edit(self): + return self._has_role(constants.ROLE_EDIT) + + def role_passwd(self): + return self._has_role(constants.ROLE_PASSWD) + + def role_edit_shelfs(self): + return self._has_role(constants.ROLE_EDIT_SHELFS) + + def role_delete_books(self): + return self._has_role(constants.ROLE_DELETE_BOOKS) + + def show_element_new_user(self, value): + return constants.has_flag(self.config_default_show, value) + + def show_detail_random(self): + return self.show_element_new_user(constants.DETAIL_RANDOM) + + def show_mature_content(self): + return self.show_element_new_user(constants.MATURE_CONTENT) + + def mature_content_tags(self): + mct = self.config_mature_content_tags.split(",") + return [t.strip() for t in mct] + + def get_log_level(self): + return logger.get_level_name(self.config_log_level) + + def get_mail_settings(self): + return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')} + + def set_from_dictionary(self, dictionary, field, convertor=None, default=None): + '''Possibly updates a field of this object. + The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor. + + :returns: `True` if the field has changed value + ''' + new_value = dictionary.get(field, default) + if new_value is None: + # log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field) + return False + + if field not in self.__dict__: + log.warning("_ConfigSQL trying to set unknown field '%s' = %r", field, new_value) + return False + + if convertor is not None: + new_value = convertor(new_value) + + current_value = self.__dict__.get(field) + if current_value == new_value: + return False + + # log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value) + setattr(self, field, new_value) + return True + + def load(self): + '''Load all configuration values from the underlying storage.''' + s = self._read_from_storage() # type: _Settings + for k, v in s.__dict__.items(): + if k[0] != '_': + if v is None: + # if the storage column has no value, apply the (possible) default + column = s.__class__.__dict__.get(k) + if column.default is not None: + v = column.default.arg + setattr(self, k, v) + + if self.config_google_drive_watch_changes_response: + self.config_google_drive_watch_changes_response = json.loads(self.config_google_drive_watch_changes_response) + self.db_configured = (self.config_calibre_dir and + (not self.config_use_google_drive or os.path.exists(self.config_calibre_dir + '/metadata.db'))) + logger.setup(self.config_logfile, self.config_log_level) + + def save(self): + '''Apply all configuration values to the underlying storage.''' + s = self._read_from_storage() # type: _Settings + + for k, v in self.__dict__.items(): + if k[0] == '_': + continue + if hasattr(s, k): # and getattr(s, k, None) != v: + # log.debug("_Settings save '%s' = %r", k, v) + setattr(s, k, v) + + log.debug("_ConfigSQL updating storage") + self._session.merge(s) + self._session.commit() + self.load() + + def invalidate(self): + log.warning("invalidating configuration") + self.db_configured = False + self.config_calibre_dir = None + self.save() + + +def _migrate_table(session, orm_class): + changed = False + + for column_name, column in orm_class.__dict__.items(): + if column_name[0] != '_': + try: + session.query(column).first() + except exc.OperationalError as err: + log.debug("%s: %s", column_name, err) + column_default = "" if column.default is None else ("DEFAULT %r" % column.default.arg) + alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__, column_name, column.type, column_default) + session.execute(alter_table) + changed = True + + if changed: + session.commit() + + +def _migrate_database(session): + # make sure the table is created, if it does not exist + _Base.metadata.create_all(session.bind) + _migrate_table(session, _Settings) + + +def load_configuration(session): + _migrate_database(session) + + if not session.query(_Settings).count(): + session.add(_Settings()) + session.commit() + + return _ConfigSQL(session) diff --git a/cps/constants.py b/cps/constants.py new file mode 100644 index 00000000..8d0002f1 --- /dev/null +++ b/cps/constants.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2019 OzzieIsaacs, pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import sys +import os +from collections import namedtuple + + +# Base dir is parent of current file, necessary if called from different folder +if sys.version_info < (3, 0): + BASE_DIR = os.path.abspath(os.path.join( + os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8') +else: + BASE_DIR = os.path.abspath(os.path.join( + os.path.dirname(os.path.abspath(__file__)),os.pardir)) +STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static') +TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates') +TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations') +CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR) + + +ROLE_USER = 0 << 0 +ROLE_ADMIN = 1 << 0 +ROLE_DOWNLOAD = 1 << 1 +ROLE_UPLOAD = 1 << 2 +ROLE_EDIT = 1 << 3 +ROLE_PASSWD = 1 << 4 +ROLE_ANONYMOUS = 1 << 5 +ROLE_EDIT_SHELFS = 1 << 6 +ROLE_DELETE_BOOKS = 1 << 7 +ROLE_VIEWER = 1 << 8 + +ALL_ROLES = { + "admin_role": ROLE_ADMIN, + "download_role": ROLE_DOWNLOAD, + "upload_role": ROLE_UPLOAD, + "edit_role": ROLE_EDIT, + "passwd_role": ROLE_PASSWD, + "edit_shelf_role": ROLE_EDIT_SHELFS, + "delete_role": ROLE_DELETE_BOOKS, + "viewer_role": ROLE_VIEWER, + } + +DETAIL_RANDOM = 1 << 0 +SIDEBAR_LANGUAGE = 1 << 1 +SIDEBAR_SERIES = 1 << 2 +SIDEBAR_CATEGORY = 1 << 3 +SIDEBAR_HOT = 1 << 4 +SIDEBAR_RANDOM = 1 << 5 +SIDEBAR_AUTHOR = 1 << 6 +SIDEBAR_BEST_RATED = 1 << 7 +SIDEBAR_READ_AND_UNREAD = 1 << 8 +SIDEBAR_RECENT = 1 << 9 +SIDEBAR_SORTED = 1 << 10 +MATURE_CONTENT = 1 << 11 +SIDEBAR_PUBLISHER = 1 << 12 +SIDEBAR_RATING = 1 << 13 +SIDEBAR_FORMAT = 1 << 14 + +ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_EDIT_SHELFS & ~ROLE_ANONYMOUS +ADMIN_USER_SIDEBAR = (SIDEBAR_FORMAT << 1) - 1 + +UPDATE_STABLE = 0 << 0 +AUTO_UPDATE_STABLE = 1 << 0 +UPDATE_NIGHTLY = 1 << 1 +AUTO_UPDATE_NIGHTLY = 1 << 2 + +LOGIN_STANDARD = 0 +LOGIN_LDAP = 1 +LOGIN_OAUTH_GITHUB = 2 +LOGIN_OAUTH_GOOGLE = 3 + + +DEFAULT_PASSWORD = "admin123" +DEFAULT_PORT = 8083 +try: + env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT) + DEFAULT_PORT = int(env_CALIBRE_PORT) +except ValueError: + print('Environment variable CALIBRE_PORT has invalid value (%s), faling back to default (8083)' % env_CALIBRE_PORT) +del env_CALIBRE_PORT + + +EXTENSIONS_AUDIO = {'mp3', 'm4a', 'm4b'} +EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt'} +EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', + 'fb2', 'html', 'rtf', 'odt', 'mp3', 'm4a', 'm4b'} +# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + +# (['rar','cbr'] if feature_support['rar'] else [])) + + +def has_flag(value, bit_flag): + return bit_flag == (bit_flag & (value or 0)) + +def selected_roles(dictionary): + return sum(v for k, v in ALL_ROLES.items() if k in dictionary) + + +# :rtype: BookMeta +BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' + 'series_id, languages') + +STABLE_VERSION = {'version': '0.6.4 Beta'} + +NIGHTLY_VERSION = {} +NIGHTLY_VERSION[0] = '$Format:%H$' +NIGHTLY_VERSION[1] = '$Format:%cI$' +# NIGHTLY_VERSION[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57' +# NIGHTLY_VERSION[1] = '2018-09-09T10:13:08+02:00' + + +# clean-up the module namespace +del sys, os, namedtuple + diff --git a/cps/converter.py b/cps/converter.py index bfcf0879..6dc44383 100644 --- a/cps/converter.py +++ b/cps/converter.py @@ -17,23 +17,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - +from __future__ import division, print_function, unicode_literals import os -import subprocess -import ub import re + from flask_babel import gettext as _ +from . import config +from .subproc_wrapper import process_wait + def versionKindle(): versions = _(u'not installed') - if os.path.exists(ub.config.config_converterpath): + if os.path.exists(config.config_converterpath): try: - p = subprocess.Popen(ub.config.config_converterpath, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.wait() - for lines in p.stdout.readlines(): - if isinstance(lines, bytes): - lines = lines.decode('utf-8') + for lines in process_wait(config.config_converterpath): if re.search('Amazon kindlegen\(', lines): versions = lines except Exception: @@ -43,13 +41,9 @@ def versionKindle(): def versionCalibre(): versions = _(u'not installed') - if os.path.exists(ub.config.config_converterpath): + if os.path.exists(config.config_converterpath): try: - p = subprocess.Popen([ub.config.config_converterpath, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.wait() - for lines in p.stdout.readlines(): - if isinstance(lines, bytes): - lines = lines.decode('utf-8') + for lines in process_wait([config.config_converterpath, '--version']): if re.search('ebook-convert.*\(calibre', lines): versions = lines except Exception: @@ -58,9 +52,9 @@ def versionCalibre(): def versioncheck(): - if ub.config.config_ebookconverter == 1: + if config.config_ebookconverter == 1: return versionKindle() - elif ub.config.config_ebookconverter == 2: + elif config.config_ebookconverter == 2: return versionCalibre() else: return {'ebook_converter':_(u'not configured')} diff --git a/cps/db.py b/cps/db.py index 688f7fde..edcdef63 100755 --- a/cps/db.py +++ b/cps/db.py @@ -18,40 +18,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from sqlalchemy import * -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import * +from __future__ import division, print_function, unicode_literals +import sys import os import re import ast -from ub import config -import ub -import sys -import unidecode + +from sqlalchemy import create_engine +from sqlalchemy import Table, Column, ForeignKey +from sqlalchemy import String, Integer, Boolean +from sqlalchemy.orm import relationship, sessionmaker, scoped_session +from sqlalchemy.ext.declarative import declarative_base + session = None cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] -cc_classes = None -engine = None - - -# user defined sort function for calibre databases (Series, etc.) -def title_sort(title): - # calibre sort stuff - title_pat = re.compile(config.config_title_regex, re.IGNORECASE) - match = title_pat.search(title) - if match: - prep = match.group(1) - title = title.replace(prep, '') + ', ' + prep - return title.strip() - - -def lcase(s): - return unidecode.unidecode(s.lower()) - - -def ucase(s): - return s.upper() +cc_classes = {} Base = declarative_base() @@ -329,37 +311,45 @@ class Custom_Columns(Base): return display_dict -def setup_db(): - global engine - global session - global cc_classes +def update_title_sort(config, conn=None): + # user defined sort function for calibre databases (Series, etc.) + def _title_sort(title): + # calibre sort stuff + title_pat = re.compile(config.config_title_regex, re.IGNORECASE) + match = title_pat.search(title) + if match: + prep = match.group(1) + title = title.replace(prep, '') + ', ' + prep + return title.strip() - if config.config_calibre_dir is None or config.config_calibre_dir == u'': - content = ub.session.query(ub.Settings).first() - content.config_calibre_dir = None - content.db_configured = False - ub.session.commit() - config.loadSettings() + conn = conn or session.connection().connection.connection + conn.create_function("title_sort", 1, _title_sort) + + +def setup_db(config): + dispose() + + if not config.config_calibre_dir: + config.invalidate() return False dbpath = os.path.join(config.config_calibre_dir, "metadata.db") - try: - if not os.path.exists(dbpath): - raise - engine = create_engine('sqlite:///' + dbpath, echo=False, isolation_level="SERIALIZABLE", connect_args={'check_same_thread': False}) - conn = engine.connect() - except Exception: - content = ub.session.query(ub.Settings).first() - content.config_calibre_dir = None - content.db_configured = False - ub.session.commit() - config.loadSettings() + if not os.path.exists(dbpath): + config.invalidate() return False - content = ub.session.query(ub.Settings).first() - content.db_configured = True - ub.session.commit() - config.loadSettings() - conn.connection.create_function('title_sort', 1, title_sort) + + try: + engine = create_engine('sqlite:///{0}'.format(dbpath), + echo=False, + isolation_level="SERIALIZABLE", + connect_args={'check_same_thread': False}) + conn = engine.connect() + except: + config.invalidate() + return False + + config.db_configured = True + update_title_sort(config, conn.connection) # conn.connection.create_function('lower', 1, lcase) # conn.connection.create_function('upper', 1, ucase) @@ -368,7 +358,6 @@ def setup_db(): cc_ids = [] books_custom_column_links = {} - cc_classes = {} for row in cc: if row.datatype not in cc_exceptions: books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata, @@ -393,7 +382,7 @@ def setup_db(): ccdict = {'__tablename__': 'custom_column_' + str(row.id), 'id': Column(Integer, primary_key=True), 'value': Column(String)} - cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict) + cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict) for cc_id in cc_ids: if (cc_id[1] == 'bool') or (cc_id[1] == 'int'): @@ -407,8 +396,38 @@ def setup_db(): backref='books')) + global session Session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) session = Session() return True + + +def dispose(): + global session + + engine = None + if session: + engine = session.bind + try: session.close() + except: pass + session = None + + if engine: + try: engine.dispose() + except: pass + + for attr in list(Books.__dict__.keys()): + if attr.startswith("custom_column_"): + delattr(Books, attr) + + for db_class in cc_classes.values(): + Base.metadata.remove(db_class.__table__) + cc_classes.clear() + + for table in reversed(Base.metadata.sorted_tables): + name = table.key + if name.startswith("custom_column_") or name.startswith("books_custom_column_"): + if table is not None: + Base.metadata.remove(table) diff --git a/cps/editbooks.py b/cps/editbooks.py new file mode 100644 index 00000000..7f850254 --- /dev/null +++ b/cps/editbooks.py @@ -0,0 +1,711 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import os +import datetime +import json +from shutil import move, copyfile +from uuid import uuid4 + +from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response +from flask_babel import gettext as _ +from flask_login import current_user + +from . import constants, logger, isoLanguages, gdriveutils, uploader, helper +from . import config, get_locale, db, ub, global_WorkerThread +from .helper import order_authors, common_filters +from .web import login_required_if_no_ano, render_title_template, edit_required, upload_required, login_required + + +editbook = Blueprint('editbook', __name__) +log = logger.create() + + +# Modifies different Database objects, first check if elements have to be added to database, than check +# if elements have to be deleted, because they are no longer used +def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): + # passing input_elements not as a list may lead to undesired results + if not isinstance(input_elements, list): + raise TypeError(str(input_elements) + " should be passed as a list") + + input_elements = [x for x in input_elements if x != ''] + # we have all input element (authors, series, tags) names now + # 1. search for elements to remove + del_elements = [] + for c_elements in db_book_object: + found = False + if db_type == 'languages': + type_elements = c_elements.lang_code + elif db_type == 'custom': + type_elements = c_elements.value + else: + type_elements = c_elements.name + for inp_element in input_elements: + if inp_element.lower() == type_elements.lower(): + # if inp_element == type_elements: + found = True + break + # if the element was not found in the new list, add it to remove list + if not found: + del_elements.append(c_elements) + # 2. search for elements that need to be added + add_elements = [] + for inp_element in input_elements: + found = False + for c_elements in db_book_object: + if db_type == 'languages': + type_elements = c_elements.lang_code + elif db_type == 'custom': + type_elements = c_elements.value + else: + type_elements = c_elements.name + if inp_element == type_elements: + found = True + break + if not found: + add_elements.append(inp_element) + # if there are elements to remove, we remove them now + if len(del_elements) > 0: + for del_element in del_elements: + db_book_object.remove(del_element) + if len(del_element.books) == 0: + db_session.delete(del_element) + # if there are elements to add, we add them now! + if len(add_elements) > 0: + if db_type == 'languages': + db_filter = db_object.lang_code + elif db_type == 'custom': + db_filter = db_object.value + else: + db_filter = db_object.name + for add_element in add_elements: + # check if a element with that name exists + db_element = db_session.query(db_object).filter(db_filter == add_element).first() + # if no element is found add it + # if new_element is None: + if db_type == 'author': + new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") + elif db_type == 'series': + new_element = db_object(add_element, add_element) + elif db_type == 'custom': + new_element = db_object(value=add_element) + elif db_type == 'publisher': + new_element = db_object(add_element, None) + else: # db_type should be tag or language + new_element = db_object(add_element) + if db_element is None: + db_session.add(new_element) + db_book_object.append(new_element) + else: + if db_type == 'custom': + if db_element.value != add_element: + new_element.value = add_element + # new_element = db_element + elif db_type == 'languages': + if db_element.lang_code != add_element: + db_element.lang_code = add_element + # new_element = db_element + elif db_type == 'series': + if db_element.name != add_element: + db_element.name = add_element # = add_element # new_element = db_object(add_element, add_element) + db_element.sort = add_element + # new_element = db_element + elif db_type == 'author': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = add_element.replace('|', ',') + # new_element = db_element + elif db_type == 'publisher': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = None + # new_element = db_element + elif db_element.name != add_element: + db_element.name = add_element + # new_element = db_element + # add element to book + db_book_object.append(db_element) + + +@editbook.route("/delete//", defaults={'book_format': ""}) +@editbook.route("/delete///") +@login_required +def delete_book(book_id, book_format): + if current_user.role_delete_books(): + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + if book: + helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper()) + if not book_format: + # delete book from Shelfs, Downloads, Read list + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete() + ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete() + ub.delete_download(book_id) + ub.session.commit() + + # check if only this book links to: + # author, language, series, tags, custom columns + modify_database_object([u''], book.authors, db.Authors, db.session, 'author') + modify_database_object([u''], book.tags, db.Tags, db.session, 'tags') + modify_database_object([u''], book.series, db.Series, db.session, 'series') + modify_database_object([u''], book.languages, db.Languages, db.session, 'languages') + modify_database_object([u''], book.publishers, db.Publishers, db.session, 'publishers') + + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + for c in cc: + cc_string = "custom_column_" + str(c.id) + if not c.is_multiple: + if len(getattr(book, cc_string)) > 0: + if c.datatype == 'bool' or c.datatype == 'integer': + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + elif c.datatype == 'rating': + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + db.session.delete(del_cc) + else: + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + else: + modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], + db.session, 'custom') + db.session.query(db.Books).filter(db.Books.id == book_id).delete() + else: + db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete() + db.session.commit() + else: + # book not found + log.error('Book with id "%s" could not be deleted: not found', book_id) + if book_format: + return redirect(url_for('editbook.edit_book', book_id=book_id)) + else: + return redirect(url_for('web.index')) + + +def render_edit_book(book_id): + db.update_title_sort(config) + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + book = db.session.query(db.Books)\ + .filter(db.Books.id == book_id).filter(common_filters()).first() + + if not book: + flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") + return redirect(url_for("web.index")) + + for lang in book.languages: + lang.language_name = isoLanguages.get_language_name(get_locale(), lang.lang_code) + + book = order_authors(book) + + author_names = [] + for authr in book.authors: + author_names.append(authr.name.replace('|', ',')) + + # Option for showing convertbook button + valid_source_formats=list() + if config.config_ebookconverter == 2: + for file in book.data: + if file.format.lower() in constants.EXTENSIONS_CONVERT: + valid_source_formats.append(file.format.lower()) + + # Determine what formats don't already exist + allowed_conversion_formats = constants.EXTENSIONS_CONVERT.copy() + for file in book.data: + try: + allowed_conversion_formats.remove(file.format.lower()) + except Exception: + log.warning('%s already removed from list.', file.format.lower()) + + return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, + title=_(u"edit metadata"), page="editbook", + conversion_formats=allowed_conversion_formats, + source_formats=valid_source_formats) + + +def edit_cc_data(book_id, book, to_save): + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + for c in cc: + cc_string = "custom_column_" + str(c.id) + if not c.is_multiple: + if len(getattr(book, cc_string)) > 0: + cc_db_value = getattr(book, cc_string)[0].value + else: + cc_db_value = None + if to_save[cc_string].strip(): + if c.datatype == 'int' or c.datatype == 'bool': + if to_save[cc_string] == 'None': + to_save[cc_string] = None + elif c.datatype == 'bool': + to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0 + + if to_save[cc_string] != cc_db_value: + if cc_db_value is not None: + if to_save[cc_string] is not None: + setattr(getattr(book, cc_string)[0], 'value', to_save[cc_string]) + else: + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + db.session.delete(del_cc) + else: + cc_class = db.cc_classes[c.id] + new_cc = cc_class(value=to_save[cc_string], book=book_id) + db.session.add(new_cc) + + else: + if c.datatype == 'rating': + to_save[cc_string] = str(int(float(to_save[cc_string]) * 2)) + if to_save[cc_string].strip() != cc_db_value: + if cc_db_value is not None: + # remove old cc_val + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + db.session.delete(del_cc) + cc_class = db.cc_classes[c.id] + new_cc = db.session.query(cc_class).filter( + cc_class.value == to_save[cc_string].strip()).first() + # if no cc val is found add it + if new_cc is None: + new_cc = cc_class(value=to_save[cc_string].strip()) + db.session.add(new_cc) + db.session.flush() + new_cc = db.session.query(cc_class).filter( + cc_class.value == to_save[cc_string].strip()).first() + # add cc value to book + getattr(book, cc_string).append(new_cc) + else: + if cc_db_value is not None: + # remove old cc_val + del_cc = getattr(book, cc_string)[0] + getattr(book, cc_string).remove(del_cc) + if len(del_cc.books) == 0: + db.session.delete(del_cc) + else: + input_tags = to_save[cc_string].split(',') + input_tags = list(map(lambda it: it.strip(), input_tags)) + modify_database_object(input_tags, getattr(book, cc_string), db.cc_classes[c.id], db.session, + 'custom') + return cc + +def upload_single_file(request, book, book_id): + # Check and handle Uploaded file + if 'btn-upload-format' in request.files: + requested_file = request.files['btn-upload-format'] + # check for empty request + if requested_file.filename != '': + if '.' in requested_file.filename: + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in constants.EXTENSIONS_UPLOAD: + flash(_("File extension '%(ext)s' is not allowed to be uploaded to this server", ext=file_ext), + category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + else: + flash(_('File to be uploaded must have an extension'), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + + file_name = book.path.rsplit('/', 1)[-1] + filepath = os.path.normpath(os.path.join(config.config_calibre_dir, book.path)) + saved_filename = os.path.join(filepath, file_name + '.' + file_ext) + + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file + if not os.path.exists(filepath): + try: + os.makedirs(filepath) + except OSError: + flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + try: + requested_file.save(saved_filename) + except OSError: + flash(_(u"Failed to store file %(file)s.", file=saved_filename), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + + file_size = os.path.getsize(saved_filename) + is_format = db.session.query(db.Data).filter(db.Data.book == book_id).\ + filter(db.Data.format == file_ext.upper()).first() + + # Format entry already exists, no need to update the database + if is_format: + log.warning('Book format %s already existing', file_ext.upper()) + else: + db_format = db.Data(book_id, file_ext.upper(), file_size, file_name) + db.session.add(db_format) + db.session.commit() + db.update_title_sort(config) + + # Queue uploader info + uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) + global_WorkerThread.add_upload(current_user.nickname, + "" + uploadText + "") + + +def upload_cover(request, book): + if 'btn-upload-cover' in request.files: + requested_file = request.files['btn-upload-cover'] + # check for empty request + if requested_file.filename != '': + if helper.save_cover(requested_file, book.path) is True: + return True + else: + # ToDo Message not always coorect + flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error") + return False + return None + + +@editbook.route("/admin/book/", methods=['GET', 'POST']) +@login_required_if_no_ano +@edit_required +def edit_book(book_id): + # Show form + if request.method != 'POST': + return render_edit_book(book_id) + + # create the function for sorting... + db.update_title_sort(config) + book = db.session.query(db.Books)\ + .filter(db.Books.id == book_id).filter(common_filters()).first() + + # Book not found + if not book: + flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error") + return redirect(url_for("web.index")) + + upload_single_file(request, book, book_id) + if upload_cover(request, book) is True: + book.has_cover = 1 + try: + to_save = request.form.to_dict() + # Update book + edited_books_id = None + #handle book title + if book.title != to_save["book_title"].rstrip().strip(): + if to_save["book_title"] == '': + to_save["book_title"] = _(u'unknown') + book.title = to_save["book_title"].rstrip().strip() + edited_books_id = book.id + + # handle author(s) + input_authors = to_save["author_name"].split('&') + input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) + # we have all author names now + if input_authors == ['']: + input_authors = [_(u'unknown')] # prevent empty Author + + modify_database_object(input_authors, book.authors, db.Authors, db.session, 'author') + + # Search for each author if author is in database, if not, authorname and sorted authorname is generated new + # everything then is assembled for sorted author field in database + sort_authors_list = list() + for inp in input_authors: + stored_author = db.session.query(db.Authors).filter(db.Authors.name == inp).first() + if not stored_author: + stored_author = helper.get_sorted_author(inp) + else: + stored_author = stored_author.sort + sort_authors_list.append(helper.get_sorted_author(stored_author)) + sort_authors = ' & '.join(sort_authors_list) + if book.author_sort != sort_authors: + edited_books_id = book.id + book.author_sort = sort_authors + + + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + + error = False + if edited_books_id: + error = helper.update_dir_stucture(edited_books_id, config.config_calibre_dir, input_authors[0]) + + if not error: + if to_save["cover_url"]: + if helper.save_cover_from_url(to_save["cover_url"], book.path) is True: + book.has_cover = 1 + else: + flash(_(u"Cover is not a jpg file, can't save"), category="error") + + if book.series_index != to_save["series_index"]: + book.series_index = to_save["series_index"] + + # Handle book comments/description + if len(book.comments): + book.comments[0].text = to_save["description"] + else: + book.comments.append(db.Comments(text=to_save["description"], book=book.id)) + + # Handle book tags + input_tags = to_save["tags"].split(',') + input_tags = list(map(lambda it: it.strip(), input_tags)) + modify_database_object(input_tags, book.tags, db.Tags, db.session, 'tags') + + # Handle book series + input_series = [to_save["series"].strip()] + input_series = [x for x in input_series if x != ''] + modify_database_object(input_series, book.series, db.Series, db.session, 'series') + + if to_save["pubdate"]: + try: + book.pubdate = datetime.datetime.strptime(to_save["pubdate"], "%Y-%m-%d") + except ValueError: + book.pubdate = db.Books.DEFAULT_PUBDATE + else: + book.pubdate = db.Books.DEFAULT_PUBDATE + + if to_save["publisher"]: + publisher = to_save["publisher"].rstrip().strip() + if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name): + modify_database_object([publisher], book.publishers, db.Publishers, db.session, 'publisher') + elif len(book.publishers): + modify_database_object([], book.publishers, db.Publishers, db.session, 'publisher') + + + # handle book languages + input_languages = to_save["languages"].split(',') + unknown_languages = [] + input_l = isoLanguages.get_language_codes(get_locale(), input_languages, unknown_languages) + for l in unknown_languages: + log.error('%s is not a valid language', l) + flash(_(u"%(langname)s is not a valid language", langname=l), category="error") + modify_database_object(list(input_l), book.languages, db.Languages, db.session, 'languages') + + # handle book ratings + if to_save["rating"].strip(): + old_rating = False + if len(book.ratings) > 0: + old_rating = book.ratings[0].rating + ratingx2 = int(float(to_save["rating"]) * 2) + if ratingx2 != old_rating: + is_rating = db.session.query(db.Ratings).filter(db.Ratings.rating == ratingx2).first() + if is_rating: + book.ratings.append(is_rating) + else: + new_rating = db.Ratings(rating=ratingx2) + book.ratings.append(new_rating) + if old_rating: + book.ratings.remove(book.ratings[0]) + else: + if len(book.ratings) > 0: + book.ratings.remove(book.ratings[0]) + + # handle cc data + edit_cc_data(book_id, book, to_save) + + db.session.commit() + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + if "detail_view" in to_save: + return redirect(url_for('web.show_book', book_id=book.id)) + else: + flash(_("Metadata successfully updated"), category="success") + return render_edit_book(book_id) + else: + db.session.rollback() + flash(error, category="error") + return render_edit_book(book_id) + except Exception as e: + log.exception(e) + db.session.rollback() + flash(_("Error editing book, please check logfile for details"), category="error") + return redirect(url_for('web.show_book', book_id=book.id)) + + +@editbook.route("/upload", methods=["GET", "POST"]) +@login_required_if_no_ano +@upload_required +def upload(): + if not config.config_uploading: + abort(404) + if request.method == 'POST' and 'btn-upload' in request.files: + for requested_file in request.files.getlist("btn-upload"): + # create the function for sorting... + db.update_title_sort(config) + db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) + + # check if file extension is correct + if '.' in requested_file.filename: + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in constants.EXTENSIONS_UPLOAD: + flash( + _("File extension '%(ext)s' is not allowed to be uploaded to this server", + ext=file_ext), category="error") + return redirect(url_for('web.index')) + else: + flash(_('File to be uploaded must have an extension'), category="error") + return redirect(url_for('web.index')) + + # extract metadata from file + meta = uploader.upload(requested_file) + title = meta.title + authr = meta.author + tags = meta.tags + series = meta.series + series_index = meta.series_id + title_dir = helper.get_valid_filename(title) + author_dir = helper.get_valid_filename(authr) + filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir) + saved_filename = os.path.join(filepath, title_dir + meta.extension.lower()) + + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file + if not os.path.exists(filepath): + try: + os.makedirs(filepath) + except OSError: + flash(_(u"Failed to create path %(path)s (Permission denied).", path=filepath), category="error") + return redirect(url_for('web.index')) + try: + copyfile(meta.file_path, saved_filename) + except OSError: + flash(_(u"Failed to store file %(file)s (Permission denied).", file=saved_filename), category="error") + return redirect(url_for('web.index')) + try: + os.unlink(meta.file_path) + except OSError: + flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path), + category="warning") + + if meta.cover is None: + has_cover = 0 + copyfile(os.path.join(constants.STATIC_DIR, 'generic_cover.jpg'), + os.path.join(filepath, "cover.jpg")) + else: + has_cover = 1 + move(meta.cover, os.path.join(filepath, "cover.jpg")) + + # handle authors + is_author = db.session.query(db.Authors).filter(db.Authors.name == authr).first() + if is_author: + db_author = is_author + else: + db_author = db.Authors(authr, helper.get_sorted_author(authr), "") + db.session.add(db_author) + + # handle series + db_series = None + is_series = db.session.query(db.Series).filter(db.Series.name == series).first() + if is_series: + db_series = is_series + elif series != '': + db_series = db.Series(series, "") + db.session.add(db_series) + + # add language actually one value in list + input_language = meta.languages + db_language = None + if input_language != "": + input_language = isoLanguages.get(name=input_language).part3 + hasLanguage = db.session.query(db.Languages).filter(db.Languages.lang_code == input_language).first() + if hasLanguage: + db_language = hasLanguage + else: + db_language = db.Languages(input_language) + db.session.add(db_language) + + # combine path and normalize path from windows systems + path = os.path.join(author_dir, title_dir).replace('\\', '/') + db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), + series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language) + db_book.authors.append(db_author) + if db_series: + db_book.series.append(db_series) + if db_language is not None: + db_book.languages.append(db_language) + file_size = os.path.getsize(saved_filename) + db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, title_dir) + + # handle tags + input_tags = tags.split(',') + input_tags = list(map(lambda it: it.strip(), input_tags)) + if input_tags[0] !="": + modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags') + + # flush content, get db_book.id available + db_book.data.append(db_data) + db.session.add(db_book) + db.session.flush() + + # add comment + book_id = db_book.id + upload_comment = Markup(meta.description).unescape() + if upload_comment != "": + db.session.add(db.Comments(upload_comment, book_id)) + + # save data to database, reread data + db.session.commit() + db.update_title_sort(config) + book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() + + # upload book to gdrive if nesseccary and add "(bookid)" to folder name + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + error = helper.update_dir_stucture(book.id, config.config_calibre_dir) + db.session.commit() + if config.config_use_google_drive: + gdriveutils.updateGdriveCalibreFromLocal() + if error: + flash(error, category="error") + uploadText=_(u"File %(file)s uploaded", file=book.title) + global_WorkerThread.add_upload(current_user.nickname, + "" + uploadText + "") + + # create data for displaying display Full language name instead of iso639.part3language + if db_language is not None: + book.languages[0].language_name = _(meta.languages) + author_names = [] + for author in db_book.authors: + author_names.append(author.name) + if len(request.files.getlist("btn-upload")) < 2: + if current_user.role_edit() or current_user.role_admin(): + resp = {"location": url_for('editbook.edit_book', book_id=db_book.id)} + return Response(json.dumps(resp), mimetype='application/json') + else: + resp = {"location": url_for('web.show_book', book_id=db_book.id)} + return Response(json.dumps(resp), mimetype='application/json') + return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + + +@editbook.route("/admin/book/convert/", methods=['POST']) +@login_required_if_no_ano +@edit_required +def convert_bookformat(book_id): + # check to see if we have form fields to work with - if not send user back + book_format_from = request.form.get('book_format_from', None) + book_format_to = request.form.get('book_format_to', None) + + if (book_format_from is None) or (book_format_to is None): + flash(_(u"Source or destination format for conversion missing"), category="error") + return redirect(request.environ["HTTP_REFERER"]) + + log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) + rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), + book_format_to.upper(), current_user.nickname) + + if rtn is None: + flash(_(u"Book successfully queued for converting to %(book_format)s", + book_format=book_format_to), + category="success") + else: + flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") + return redirect(request.environ["HTTP_REFERER"]) diff --git a/cps/epub.py b/cps/epub.py index 913feaca..d9129646 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -17,11 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals +import os import zipfile from lxml import etree -import os -import uploader -import isoLanguages + +from . import isoLanguages +from .constants import BookMeta def extractCover(zipFile, coverFile, coverpath, tmp_file_name): @@ -125,7 +127,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): else: title = epub_metadata['title'] - return uploader.BookMeta( + return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=title.encode('utf-8').decode('utf-8'), diff --git a/cps/fb2.py b/cps/fb2.py index adcac758..cd61b511 100644 --- a/cps/fb2.py +++ b/cps/fb2.py @@ -17,8 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals from lxml import etree -import uploader + +from .constants import BookMeta def get_fb2_info(tmp_file_path, original_file_extension): @@ -66,7 +68,7 @@ def get_fb2_info(tmp_file_path, original_file_extension): else: description = u'' - return uploader.BookMeta( + return BookMeta( file_path=tmp_file_path, extension=original_file_extension, title=title.decode('utf-8'), diff --git a/cps/gdrive.py b/cps/gdrive.py new file mode 100644 index 00000000..263c829b --- /dev/null +++ b/cps/gdrive.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import os +import hashlib +import json +import tempfile +from uuid import uuid4 +from time import time +from shutil import move, copyfile + +from flask import Blueprint, flash, request, redirect, url_for, abort +from flask_babel import gettext as _ +from flask_login import login_required + +try: + from googleapiclient.errors import HttpError +except ImportError: + pass + +from . import logger, gdriveutils, config, db +from .web import admin_required + + +gdrive = Blueprint('gdrive', __name__) +log = logger.create() + +current_milli_time = lambda: int(round(time() * 1000)) + +gdrive_watch_callback_token = 'target=calibreweb-watch_files' + + +@gdrive.route("/gdrive/authenticate") +@login_required +@admin_required +def authenticate_google_drive(): + try: + authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl() + except gdriveutils.InvalidConfigError: + flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'), + category="error") + return redirect(url_for('web.index')) + return redirect(authUrl) + + +@gdrive.route("/gdrive/callback") +def google_drive_callback(): + auth_code = request.args.get('code') + if not auth_code: + abort(403) + try: + credentials = gdriveutils.Gauth.Instance().auth.flow.step2_exchange(auth_code) + with open(gdriveutils.CREDENTIALS, 'w') as f: + f.write(credentials.to_json()) + except ValueError as error: + log.error(error) + return redirect(url_for('admin.configuration')) + + +@gdrive.route("/gdrive/watch/subscribe") +@login_required +@admin_required +def watch_gdrive(): + if not config.config_google_drive_watch_changes_response: + with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: + filedata = json.load(settings) + if filedata['web']['redirect_uris'][0].endswith('/'): + filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-((len('/gdrive/callback')+1))] + else: + filedata['web']['redirect_uris'][0] = filedata['web']['redirect_uris'][0][:-(len('/gdrive/callback'))] + address = '%s/gdrive/watch/callback' % filedata['web']['redirect_uris'][0] + notification_id = str(uuid4()) + try: + result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id, + 'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000) + config.config_google_drive_watch_changes_response = json.dumps(result) + # after save(), config_google_drive_watch_changes_response will be a json object, not string + config.save() + except HttpError as e: + reason=json.loads(e.content)['error']['errors'][0] + if reason['reason'] == u'push.webhookUrlUnauthorized': + flash(_(u'Callback domain is not verified, please follow steps to verify domain in google developer console'), category="error") + else: + flash(reason['message'], category="error") + + return redirect(url_for('admin.configuration')) + + +@gdrive.route("/gdrive/watch/revoke") +@login_required +@admin_required +def revoke_watch_gdrive(): + last_watch_response = config.config_google_drive_watch_changes_response + if last_watch_response: + try: + gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], + last_watch_response['resourceId']) + except HttpError: + pass + config.config_google_drive_watch_changes_response = None + config.save() + return redirect(url_for('admin.configuration')) + + +@gdrive.route("/gdrive/watch/callback", methods=['GET', 'POST']) +def on_received_watch_confirmation(): + log.debug('%r', request.headers) + if request.headers.get('X-Goog-Channel-Token') == gdrive_watch_callback_token \ + and request.headers.get('X-Goog-Resource-State') == 'change' \ + and request.data: + + data = request.data + + def updateMetaData(): + log.info('Change received from gdrive') + log.debug('%r', data) + try: + j = json.loads(data) + log.info('Getting change details') + response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id']) + log.debug('%r', response) + if response: + dbpath = os.path.join(config.config_calibre_dir, "metadata.db") + if not response['deleted'] and response['file']['title'] == 'metadata.db' and response['file']['md5Checksum'] != hashlib.md5(dbpath): + tmpDir = tempfile.gettempdir() + log.info('Database file updated') + copyfile(dbpath, os.path.join(tmpDir, "metadata.db_" + str(current_milli_time()))) + log.info('Backing up existing and downloading updated metadata.db') + gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmpDir, "tmp_metadata.db")) + log.info('Setting up new DB') + # prevent error on windows, as os.rename does on exisiting files + move(os.path.join(tmpDir, "tmp_metadata.db"), dbpath) + db.setup_db(config) + except Exception as e: + log.exception(e) + updateMetaData() + return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index cacddfbd..4ec7f68e 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -17,24 +17,37 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals +import os +import json +import shutil + +from flask import Response, stream_with_context +from sqlalchemy import create_engine +from sqlalchemy import Column, UniqueConstraint +from sqlalchemy import String, Integer +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.ext.declarative import declarative_base + try: from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive - from pydrive.auth import RefreshError, InvalidConfigError + from pydrive.auth import RefreshError from apiclient import errors gdrive_support = True except ImportError: gdrive_support = False -import os -from ub import config -import cli -import shutil -from flask import Response, stream_with_context -from sqlalchemy import * -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import * -import web +from . import logger, cli, config +from .constants import BASE_DIR as _BASE_DIR + + +SETTINGS_YAML = os.path.join(_BASE_DIR, 'settings.yaml') +CREDENTIALS = os.path.join(_BASE_DIR, 'gdrive_credentials') +CLIENT_SECRETS = os.path.join(_BASE_DIR, 'client_secrets.json') + +log = logger.create() + class Singleton: """ @@ -67,6 +80,9 @@ class Singleton: except AttributeError: self._instance = self._decorated() return self._instance + except ImportError as e: + log.debug(e) + return None def __call__(self): raise TypeError('Singletons must be accessed through `Instance()`.') @@ -78,7 +94,7 @@ class Singleton: @Singleton class Gauth: def __init__(self): - self.auth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml')) + self.auth = GoogleAuth(settings_file=SETTINGS_YAML) @Singleton @@ -86,6 +102,9 @@ class Gdrive: def __init__(self): self.drive = getDrive(gauth=Gauth.Instance().auth) +def is_gdrive_ready(): + return os.path.exists(SETTINGS_YAML) and os.path.exists(CREDENTIALS) + engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False) Base = declarative_base() @@ -146,17 +165,17 @@ migrate() def getDrive(drive=None, gauth=None): if not drive: if not gauth: - gauth = GoogleAuth(settings_file=os.path.join(config.get_main_dir,'settings.yaml')) + gauth = GoogleAuth(settings_file=SETTINGS_YAML) # Try to load saved client credentials - gauth.LoadCredentialsFile(os.path.join(config.get_main_dir,'gdrive_credentials')) + gauth.LoadCredentialsFile(CREDENTIALS) if gauth.access_token_expired: # Refresh them if expired try: gauth.Refresh() except RefreshError as e: - web.app.logger.error("Google Drive error: " + e.message) + log.error("Google Drive error: %s", e) except Exception as e: - web.app.logger.exception(e) + log.exception(e) else: # Initialize the saved creds gauth.Authorize() @@ -166,7 +185,7 @@ def getDrive(drive=None, gauth=None): try: drive.auth.Refresh() except RefreshError as e: - web.app.logger.error("Google Drive error: " + e.message) + log.error("Google Drive error: %s", e) return drive def listRootFolders(): @@ -203,7 +222,7 @@ def getEbooksFolderId(drive=None): try: gDriveId.gdrive_id = getEbooksFolder(drive)['id'] except Exception: - web.app.logger.error('Error gDrive, root ID not found') + log.error('Error gDrive, root ID not found') gDriveId.path = '/' session.merge(gDriveId) session.commit() @@ -443,10 +462,10 @@ def getChangeById (drive, change_id): change = drive.auth.service.changes().get(changeId=change_id).execute() return change except (errors.HttpError) as error: - web.app.logger.info(error.message) + log.error(error) return None except Exception as e: - web.app.logger.info(e) + log.error(e) return None @@ -516,6 +535,54 @@ def do_gdrive_download(df, headers): if resp.status == 206: yield content else: - web.app.logger.info('An error occurred: %s' % resp) + log.warning('An error occurred: %s', resp) return return Response(stream_with_context(stream()), headers=headers) + + +_SETTINGS_YAML_TEMPLATE = """ +client_config_backend: settings +client_config_file: %(client_file)s +client_config: + client_id: %(client_id)s + client_secret: %(client_secret)s + redirect_uri: %(redirect_uri)s + +save_credentials: True +save_credentials_backend: file +save_credentials_file: %(credential)s + +get_refresh_token: True + +oauth_scope: + - https://www.googleapis.com/auth/drive +""" + +def update_settings(client_id, client_secret, redirect_uri): + if redirect_uri.endswith('/'): + redirect_uri = redirect_uri[:-1] + config_params = { + 'client_file': CLIENT_SECRETS, + 'client_id': client_id, + 'client_secret': client_secret, + 'redirect_uri': redirect_uri, + 'credential': CREDENTIALS + } + + with open(SETTINGS_YAML, 'w') as f: + f.write(_SETTINGS_YAML_TEMPLATE % config_params) + + +def get_error_text(client_secrets=None): + if not gdrive_support: + return 'Import of optional Google Drive requirements missing' + + if not os.path.isfile(CLIENT_SECRETS): + return 'client_secrets.json is missing or not readable' + + with open(CLIENT_SECRETS, 'r') as settings: + filedata = json.load(settings) + if 'web' not in filedata: + return 'client_secrets.json is not configured for web application' + if client_secrets: + client_secrets.update(filedata['web']) diff --git a/cps/helper.py b/cps/helper.py index 1b233cad..1ceeb0b8 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -18,33 +18,35 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -import db -import ub -from flask import current_app as app -from tempfile import gettempdir +from __future__ import division, print_function, unicode_literals import sys -import io import os +import io +import json +import mimetypes +import random import re -import unicodedata -import worker +import shutil import time +import unicodedata +from datetime import datetime, timedelta +from tempfile import gettempdir + +import requests +from babel import Locale as LC +from babel.core import UnknownLocaleError +from babel.dates import format_datetime +from babel.units import format_unit from flask import send_from_directory, make_response, redirect, abort from flask_babel import gettext as _ from flask_login import current_user -from babel.dates import format_datetime -from babel.units import format_unit -from datetime import datetime, timedelta -import shutil -import requests +from sqlalchemy.sql.expression import true, false, and_, or_, text, func +from werkzeug.datastructures import Headers + try: - import gdriveutils as gd + from urllib.parse import quote except ImportError: - pass -import web -import random -import subprocess + from urllib import quote try: import unidecode @@ -58,10 +60,16 @@ try: except ImportError: use_PIL = False -# Global variables -# updater_thread = None -global_WorkerThread = worker.WorkerThread() -global_WorkerThread.start() +from . import logger, config, global_WorkerThread, get_locale, db, ub, isoLanguages +from . import gdriveutils as gd +from .constants import STATIC_DIR as _STATIC_DIR +from .pagination import Pagination +from .subproc_wrapper import process_wait +from .worker import STAT_WAITING, STAT_FAIL, STAT_STARTED, STAT_FINISH_SUCCESS +from .worker import TASK_EMAIL, TASK_CONVERT, TASK_UPLOAD, TASK_CONVERT_ANY + + +log = logger.create() def update_download(book_id, user_id): @@ -78,9 +86,9 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == old_book_format).first() if not data: error_message = _(u"%(format)s format not found for book id: %(book)d", format=old_book_format, book=book_id) - app.logger.error("convert_book_format: " + error_message) + log.error("convert_book_format: %s", error_message) return error_message - if ub.config.config_use_google_drive: + if config.config_use_google_drive: df = gd.getFileFromEbooksFolder(book.path, data.name + "." + old_book_format.lower()) if df: datafile = os.path.join(calibrepath, book.path, data.name + u"." + old_book_format.lower()) @@ -95,7 +103,7 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, if os.path.exists(file_path + "." + old_book_format.lower()): # read settings and append converter task to queue if kindle_mail: - settings = ub.get_mail_settings() + settings = config.get_mail_settings() settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail settings['body'] = _(u'This e-mail has been sent via Calibre-Web.') # text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title) @@ -113,7 +121,7 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format, def send_test_mail(kindle_mail, user_name): - global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(), + global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, config.get_mail_settings(), kindle_mail, user_name, _(u"Test e-mail"), _(u'This e-mail has been sent via Calibre-Web.')) return @@ -130,17 +138,18 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False): text += "Don't forget to change your password after first login.\r\n" text += "Sincerely\r\n\r\n" text += "Your Calibre-Web team" - global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(), + global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, config.get_mail_settings(), e_mail, None, _(u"Registration e-mail for user: %(name)s", name=user_name), text) return + def check_send_to_kindle(entry): """ returns all available book formats for sending to Kindle """ if len(entry.data): bookformats=list() - if ub.config.config_ebookconverter == 0: + if config.config_ebookconverter == 0: # no converter - only for mobi and pdf formats for ele in iter(entry.data): if 'MOBI' in ele.format: @@ -161,17 +170,17 @@ def check_send_to_kindle(entry): bookformats.append({'format': 'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')}) if 'PDF' in formats: bookformats.append({'format': 'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')}) - if ub.config.config_ebookconverter >= 1: + if config.config_ebookconverter >= 1: if 'EPUB' in formats and not 'MOBI' in formats: bookformats.append({'format': 'Mobi','convert':1, 'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Mobi')}) - '''if ub.config.config_ebookconverter == 2: + '''if config.config_ebookconverter == 2: if 'EPUB' in formats and not 'AZW3' in formats: bookformats.append({'format': 'Azw3','convert':1, 'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Azw3')})''' return bookformats else: - app.logger.error(u'Cannot find book entry %d', entry.id) + log.error(u'Cannot find book entry %d', entry.id) return None @@ -202,7 +211,7 @@ def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id): for entry in iter(book.data): if entry.format.upper() == book_format.upper(): result = entry.name + '.' + book_format.lower() - global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(), + global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, config.get_mail_settings(), kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title), _(u'This e-mail has been sent via Calibre-Web.')) return @@ -256,8 +265,8 @@ def get_sorted_author(value): value2 = value[-1] + ", " + " ".join(value[:-1]) else: value2 = value - except Exception: - web.app.logger.error("Sorting author " + str(value) + "failed") + except Exception as ex: + log.error("Sorting author %s failed: %s", value, ex) value2 = value return value2 @@ -274,13 +283,12 @@ def delete_book_file(book, calibrepath, book_format=None): else: if os.path.isdir(path): if len(next(os.walk(path))[1]): - web.app.logger.error( - "Deleting book " + str(book.id) + " failed, path has subfolders: " + book.path) + log.error("Deleting book %s failed, path has subfolders: %s", book.id, book.path) return False shutil.rmtree(path, ignore_errors=True) return True else: - web.app.logger.error("Deleting book " + str(book.id) + " failed, book path not valid: " + book.path) + log.error("Deleting book %s failed, book path not valid: %s", book.id, book.path) return False @@ -303,16 +311,16 @@ def update_dir_structure_file(book_id, calibrepath, first_author): if not os.path.exists(new_title_path): os.renames(path, new_title_path) else: - web.app.logger.info("Copying title: " + path + " into existing: " + new_title_path) - for dir_name, subdir_list, file_list in os.walk(path): + log.info("Copying title: %s into existing: %s", path, new_title_path) + for dir_name, __, file_list in os.walk(path): for file in file_list: os.renames(os.path.join(dir_name, file), os.path.join(new_title_path + dir_name[len(path):], file)) path = new_title_path localbook.path = localbook.path.split('/')[0] + '/' + new_titledir except OSError as ex: - web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex)) - web.app.logger.debug(ex, exc_info=True) + log.error("Rename title from: %s to %s: %s", path, new_title_path, ex) + log.debug(ex, exc_info=True) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex)) if authordir != new_authordir: @@ -321,8 +329,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): os.renames(path, new_author_path) localbook.path = new_authordir + '/' + localbook.path.split('/')[1] except OSError as ex: - web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex)) - web.app.logger.debug(ex, exc_info=True) + log.error("Rename author from: %s to %s: %s", path, new_author_path, ex) + log.debug(ex, exc_info=True) return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex)) # Rename all files from old names to new names @@ -335,8 +343,8 @@ def update_dir_structure_file(book_id, calibrepath, first_author): os.path.join(path_name, new_name + '.' + file_format.format.lower())) file_format.name = new_name except OSError as ex: - web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex)) - web.app.logger.debug(ex, exc_info=True) + log.error("Rename file in path %s to %s: %s", path, new_name, ex) + log.debug(ex, exc_info=True) return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_name, error=str(ex)) return False @@ -415,37 +423,45 @@ def generate_random_password(): ################################## External interface def update_dir_stucture(book_id, calibrepath, first_author = None): - if ub.config.config_use_google_drive: + if config.config_use_google_drive: return update_dir_structure_gdrive(book_id, first_author) else: return update_dir_structure_file(book_id, calibrepath, first_author) def delete_book(book, calibrepath, book_format): - if ub.config.config_use_google_drive: + if config.config_use_google_drive: return delete_book_gdrive(book, book_format) else: return delete_book_file(book, calibrepath, book_format) -def get_book_cover(cover_path): - if ub.config.config_use_google_drive: - try: - if not web.is_gdrive_ready(): - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") - path=gd.get_cover_via_gdrive(cover_path) - if path: - return redirect(path) +def get_book_cover(book_id): + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + if book.has_cover: + + if config.config_use_google_drive: + try: + if not gd.is_gdrive_ready(): + return send_from_directory(_STATIC_DIR, "generic_cover.jpg") + path=gd.get_cover_via_gdrive(book.path) + if path: + return redirect(path) + else: + log.error('%s/cover.jpg not found on Google Drive', book.path) + return send_from_directory(_STATIC_DIR, "generic_cover.jpg") + except Exception as e: + log.exception(e) + # traceback.print_exc() + return send_from_directory(_STATIC_DIR,"generic_cover.jpg") + else: + cover_file_path = os.path.join(config.config_calibre_dir, book.path) + if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): + return send_from_directory(cover_file_path, "cover.jpg") else: - web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive') - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") - except Exception as e: - web.app.logger.error("Error Message: " + e.message) - web.app.logger.exception(e) - # traceback.print_exc() - return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") + return send_from_directory(_STATIC_DIR,"generic_cover.jpg") else: - return send_from_directory(os.path.join(ub.config.config_calibre_dir, cover_path), "cover.jpg") + return send_from_directory(_STATIC_DIR,"generic_cover.jpg") # saves book cover from url @@ -455,7 +471,7 @@ def save_cover_from_url(url, book_path): def save_cover_from_filestorage(filepath, saved_filename, img): - if hasattr(img,'_content'): + if hasattr(img, '_content'): f = open(os.path.join(filepath, saved_filename), "wb") f.write(img._content) f.close() @@ -465,15 +481,15 @@ def save_cover_from_filestorage(filepath, saved_filename, img): try: os.makedirs(filepath) except OSError: - web.app.logger.error(u"Failed to create path for cover") + log.error(u"Failed to create path for cover") return False try: img.save(os.path.join(filepath, saved_filename)) - except OSError: - web.app.logger.error(u"Failed to store cover-file") - return False except IOError: - web.app.logger.error(u"Cover-file is not a valid image file") + log.error(u"Cover-file is not a valid image file") + return False + except OSError: + log.error(u"Failed to store cover-file") return False return True @@ -484,7 +500,7 @@ def save_cover(img, book_path): if use_PIL: if content_type not in ('image/jpeg', 'image/png', 'image/webp'): - web.app.logger.error("Only jpg/jpeg/png/webp files are supported as coverfile") + log.error("Only jpg/jpeg/png/webp files are supported as coverfile") return False # convert to jpg because calibre only supports jpg if content_type in ('image/png', 'image/webp'): @@ -498,7 +514,7 @@ def save_cover(img, book_path): img._content = tmp_bytesio.getvalue() else: if content_type not in ('image/jpeg'): - web.app.logger.error("Only jpg/jpeg files are supported as coverfile") + log.error("Only jpg/jpeg files are supported as coverfile") return False if ub.config.config_use_google_drive: @@ -506,29 +522,29 @@ def save_cover(img, book_path): if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True: gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, "uploaded_cover.jpg")) - web.app.logger.info("Cover is saved on Google Drive") + log.info("Cover is saved on Google Drive") return True else: return False else: - return save_cover_from_filestorage(os.path.join(ub.config.config_calibre_dir, book_path), "cover.jpg", img) + return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) def do_download_file(book, book_format, data, headers): - if ub.config.config_use_google_drive: + if config.config_use_google_drive: startTime = time.time() df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) - web.app.logger.debug(time.time() - startTime) + log.debug('%s', time.time() - startTime) if df: return gd.do_gdrive_download(df, headers) else: abort(404) else: - filename = os.path.join(ub.config.config_calibre_dir, book.path) + filename = os.path.join(config.config_calibre_dir, book.path) if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)): # ToDo: improve error handling - web.app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format)) + log.error('File not found: %s', os.path.join(filename, data.name + "." + book_format)) response = make_response(send_from_directory(filename, data.name + "." + book_format)) response.headers = headers return response @@ -538,27 +554,23 @@ def do_download_file(book, book_format, data, headers): def check_unrar(unrarLocation): - error = False - if os.path.exists(unrarLocation): - try: - if sys.version_info < (3, 0): - unrarLocation = unrarLocation.encode(sys.getfilesystemencoding()) - p = subprocess.Popen(unrarLocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.wait() - for lines in p.stdout.readlines(): - if isinstance(lines, bytes): - lines = lines.decode('utf-8') - value=re.search('UNRAR (.*) freeware', lines) - if value: - version = value.group(1) - except OSError as e: - error = True - web.app.logger.exception(e) - version =_(u'Error excecuting UnRar') - else: - version = _(u'Unrar binary file not found') - error=True - return (error, version) + if not unrarLocation: + return + + if not os.path.exists(unrarLocation): + return 'Unrar binary file not found' + + try: + if sys.version_info < (3, 0): + unrarLocation = unrarLocation.encode(sys.getfilesystemencoding()) + for lines in process_wait(unrarLocation): + value = re.search('UNRAR (.*) freeware', lines) + if value: + version = value.group(1) + log.debug("unrar version %s", version) + except OSError as err: + log.exception(err) + return 'Error excecuting UnRar' @@ -574,6 +586,7 @@ def json_serial(obj): 'seconds': obj.seconds, 'microseconds': obj.microseconds, } + # return obj.isoformat() raise TypeError ("Type %s not serializable" % type(obj)) @@ -581,7 +594,7 @@ def json_serial(obj): def format_runtime(runtime): retVal = "" if runtime.days: - retVal = format_unit(runtime.days, 'duration-day', length="long", locale=web.get_locale()) + ', ' + retVal = format_unit(runtime.days, 'duration-day', length="long", locale=get_locale()) + ', ' mins, seconds = divmod(runtime.seconds, 60) hours, minutes = divmod(mins, 60) # ToDo: locale.number_symbols._data['timeSeparator'] -> localize time separator ? @@ -600,7 +613,8 @@ def render_task_status(tasklist): for task in tasklist: if task['user'] == current_user.nickname or current_user.role_admin(): if task['formStarttime']: - task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale()) + task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=get_locale()) + # task2['formStarttime'] = "" else: if 'starttime' not in task: task['starttime'] = "" @@ -612,26 +626,26 @@ def render_task_status(tasklist): # localize the task status if isinstance( task['stat'], int ): - if task['stat'] == worker.STAT_WAITING: + if task['stat'] == STAT_WAITING: task['status'] = _(u'Waiting') - elif task['stat'] == worker.STAT_FAIL: + elif task['stat'] == STAT_FAIL: task['status'] = _(u'Failed') - elif task['stat'] == worker.STAT_STARTED: + elif task['stat'] == STAT_STARTED: task['status'] = _(u'Started') - elif task['stat'] == worker.STAT_FINISH_SUCCESS: + elif task['stat'] == STAT_FINISH_SUCCESS: task['status'] = _(u'Finished') else: task['status'] = _(u'Unknown Status') # localize the task type if isinstance( task['taskType'], int ): - if task['taskType'] == worker.TASK_EMAIL: + if task['taskType'] == TASK_EMAIL: task['taskMessage'] = _(u'E-mail: ') + task['taskMess'] - elif task['taskType'] == worker.TASK_CONVERT: + elif task['taskType'] == TASK_CONVERT: task['taskMessage'] = _(u'Convert: ') + task['taskMess'] - elif task['taskType'] == worker.TASK_UPLOAD: + elif task['taskType'] == TASK_UPLOAD: task['taskMessage'] = _(u'Upload: ') + task['taskMess'] - elif task['taskType'] == worker.TASK_CONVERT_ANY: + elif task['taskType'] == TASK_CONVERT_ANY: task['taskMessage'] = _(u'Convert: ') + task['taskMess'] else: task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess'] @@ -639,3 +653,135 @@ def render_task_status(tasklist): renderedtasklist.append(task) return renderedtasklist + + +# Language and content filters for displaying in the UI +def common_filters(): + if current_user.filter_language() != "all": + lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) + else: + lang_filter = true() + content_rating_filter = false() if current_user.mature_content else \ + db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags())) + return and_(lang_filter, ~content_rating_filter) + + +# Creates for all stored languages a translated speaking name in the array for the UI +def speaking_language(languages=None): + if not languages: + languages = db.session.query(db.Languages).all() + for lang in languages: + try: + cur_l = LC.parse(lang.lang_code) + lang.name = cur_l.get_language_name(get_locale()) + except UnknownLocaleError: + lang.name = _(isoLanguages.get(part3=lang.lang_code).name) + return languages + +# checks if domain is in database (including wildcards) +# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; +# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ +def check_valid_domain(domain_text): + domain_text = domain_text.split('@', 1)[-1].lower() + sql = "SELECT * FROM registration WHERE :domain LIKE domain;" + result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all() + return len(result) + + +# Orders all Authors in the list according to authors sort +def order_authors(entry): + sort_authors = entry.author_sort.split('&') + authors_ordered = list() + error = False + for auth in sort_authors: + # ToDo: How to handle not found authorname + result = db.session.query(db.Authors).filter(db.Authors.sort == auth.lstrip().strip()).first() + if not result: + error = True + break + authors_ordered.append(result) + if not error: + entry.authors = authors_ordered + return entry + + +# Fill indexpage with all requested data from database +def fill_indexpage(page, database, db_filter, order, *join): + if current_user.show_detail_random(): + randm = db.session.query(db.Books).filter(common_filters())\ + .order_by(func.random()).limit(config.config_random_books) + else: + randm = false() + off = int(int(config.config_books_per_page) * (page - 1)) + pagination = Pagination(page, config.config_books_per_page, + len(db.session.query(database).filter(db_filter).filter(common_filters()).all())) + entries = db.session.query(database).join(*join, isouter=True).filter(db_filter).filter(common_filters()).\ + order_by(*order).offset(off).limit(config.config_books_per_page).all() + for book in entries: + book = order_authors(book) + return entries, randm, pagination + + +def get_typeahead(database, query, replace=('','')): + db.session.connection().connection.connection.create_function("lower", 1, lcase) + entries = db.session.query(database).filter(func.lower(database.name).ilike("%" + query + "%")).all() + json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries]) + return json_dumps + +# read search results from calibre-database and return it (function is used for feed and simple search +def get_search_results(term): + db.session.connection().connection.connection.create_function("lower", 1, lcase) + q = list() + authorterms = re.split("[, ]+", term) + for authorterm in authorterms: + q.append(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + authorterm + "%"))) + + db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + term + "%")) + + return db.session.query(db.Books).filter(common_filters()).filter( + or_(db.Books.tags.any(func.lower(db.Tags.name).ilike("%" + term + "%")), + db.Books.series.any(func.lower(db.Series.name).ilike("%" + term + "%")), + db.Books.authors.any(and_(*q)), + db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + term + "%")), + func.lower(db.Books.title).ilike("%" + term + "%") + )).all() + +def get_cc_columns(): + tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + if config.config_columns_to_ignore: + cc = [] + for col in tmpcc: + r = re.compile(config.config_columns_to_ignore) + if r.match(col.label): + cc.append(col) + else: + cc = tmpcc + return cc + +def get_download_link(book_id, book_format): + book_format = book_format.split(".")[0] + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + data = db.session.query(db.Data).filter(db.Data.book == book.id)\ + .filter(db.Data.format == book_format.upper()).first() + if data: + # collect downloaded books only for registered user and not for anonymous user + if current_user.is_authenticated: + ub.update_download(book_id, int(current_user.id)) + file_name = book.title + if len(book.authors) > 0: + file_name = book.authors[0].name + '_' + file_name + file_name = get_valid_filename(file_name) + headers = Headers() + headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") + headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')), + book_format) + return do_download_file(book, book_format, data, headers) + else: + abort(404) + + + +############### Database Helper functions + +def lcase(s): + return unidecode.unidecode(s.lower()) diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index 31ef341e..808d3761 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -17,6 +17,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, print_function, unicode_literals +import sys +import os +try: + import cPickle +except ImportError: + import pickle as cPickle + +from .constants import TRANSLATIONS_DIR as _TRANSLATIONS_DIR + + try: from iso639 import languages, __version__ get = languages.get @@ -30,14 +41,43 @@ except ImportError: __version__ = "? (PyCountry)" def _copy_fields(l): - l.part1 = l.alpha_2 - l.part3 = l.alpha_3 + l.part1 = getattr(l, 'alpha_2', None) + l.part3 = getattr(l, 'alpha_3', None) return l def get(name=None, part1=None, part3=None): - if (part3 is not None): + if part3 is not None: return _copy_fields(pyc_languages.get(alpha_3=part3)) - if (part1 is not None): + if part1 is not None: return _copy_fields(pyc_languages.get(alpha_2=part1)) - if (name is not None): + if name is not None: return _copy_fields(pyc_languages.get(name=name)) + + +try: + with open(os.path.join(_TRANSLATIONS_DIR, 'iso639.pickle'), 'rb') as f: + _LANGUAGES = cPickle.load(f) +except cPickle.UnpicklingError as error: + print("Can't read file cps/translations/iso639.pickle: %s" % error) + sys.exit(1) + + +def get_language_names(locale): + return _LANGUAGES.get(locale) + + +def get_language_name(locale, lang_code): + return get_language_names(locale)[lang_code] + + +def get_language_codes(locale, language_names, remainder=None): + language_names = set(x.strip().lower() for x in language_names if x) + + for k, v in get_language_names(locale).items(): + v = v.lower() + if v in language_names: + language_names.remove(v) + yield k + + if remainder is not None: + remainder.extend(language_names) diff --git a/cps/jinjia.py b/cps/jinjia.py new file mode 100644 index 00000000..ffd6832c --- /dev/null +++ b/cps/jinjia.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# custom jinja filters + +from __future__ import division, print_function, unicode_literals +import datetime +import mimetypes +import re + +from babel.dates import format_date +from flask import Blueprint, request, url_for +from flask_babel import get_locale +from flask_login import current_user + +from . import logger + + +jinjia = Blueprint('jinjia', __name__) +log = logger.create() + + +# pagination links in jinja +@jinjia.app_template_filter('url_for_other_page') +def url_for_other_page(page): + args = request.view_args.copy() + args['page'] = page + return url_for(request.endpoint, **args) + + +# shortentitles to at longest nchar, shorten longer words if necessary +@jinjia.app_template_filter('shortentitle') +def shortentitle_filter(s, nchar=20): + text = s.split() + res = "" # result + suml = 0 # overall length + for line in text: + if suml >= 60: + res += '...' + break + # if word longer than 20 chars truncate line and append '...', otherwise add whole word to result + # string, and summarize total length to stop at chars given by nchar + if len(line) > nchar: + res += line[:(nchar-3)] + '[..] ' + suml += nchar+3 + else: + res += line + ' ' + suml += len(line) + 1 + return res.strip() + + +@jinjia.app_template_filter('mimetype') +def mimetype_filter(val): + return mimetypes.types_map.get('.' + val, 'application/octet-stream') + + +@jinjia.app_template_filter('formatdate') +def formatdate_filter(val): + try: + conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) + formatdate = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") + return format_date(formatdate, format='medium', locale=get_locale()) + except AttributeError as e: + log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, current_user.nickname) + return formatdate + +@jinjia.app_template_filter('formatdateinput') +def format_date_input(val): + conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', val) + date_obj = datetime.datetime.strptime(conformed_timestamp[:15], "%Y%m%d %H%M%S") + input_date = date_obj.isoformat().split('T', 1)[0] # Hack to support dates <1900 + return '' if input_date == "0101-01-01" else input_date + + +@jinjia.app_template_filter('strftime') +def timestamptodate(date, fmt=None): + date = datetime.datetime.fromtimestamp( + int(date)/1000 + ) + native = date.replace(tzinfo=None) + if fmt: + time_format = fmt + else: + time_format = '%d %m %Y - %H:%S' + return native.strftime(time_format) + + +@jinjia.app_template_filter('yesno') +def yesno(value, yes, no): + return yes if value else no + + +'''@jinjia.app_template_filter('canread') +def canread(ext): + if isinstance(ext, db.Data): + ext = ext.format + return ext.lower() in EXTENSIONS_READER''' diff --git a/cps/logger.py b/cps/logger.py new file mode 100644 index 00000000..3a540683 --- /dev/null +++ b/cps/logger.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2019 pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import os +import inspect +import logging +from logging import Formatter, StreamHandler +from logging.handlers import RotatingFileHandler + +from .constants import BASE_DIR as _BASE_DIR + + +ACCESS_FORMATTER_GEVENT = Formatter("%(message)s") +ACCESS_FORMATTER_TORNADO = Formatter("[%(asctime)s] %(message)s") + +FORMATTER = Formatter("[%(asctime)s] %(levelname)5s {%(name)s:%(lineno)d} %(message)s") +DEFAULT_LOG_LEVEL = logging.INFO +DEFAULT_LOG_FILE = os.path.join(_BASE_DIR, "calibre-web.log") +DEFAULT_ACCESS_LOG = os.path.join(_BASE_DIR, "access.log") +LOG_TO_STDERR = '/dev/stderr' + +logging.addLevelName(logging.WARNING, "WARN") +logging.addLevelName(logging.CRITICAL, "CRIT") + + +def get(name=None): + return logging.getLogger(name) + + +def create(): + parent_frame = inspect.stack(0)[1] + if hasattr(parent_frame, 'frame'): + parent_frame = parent_frame.frame + else: + parent_frame = parent_frame[0] + parent_module = inspect.getmodule(parent_frame) + return get(parent_module.__name__) + + +def is_debug_enabled(): + return logging.root.level <= logging.DEBUG + +def is_info_enabled(logger): + return logging.getLogger(logger).level <= logging.INFO + + +def get_level_name(level): + return logging.getLevelName(level) + + +def is_valid_logfile(file_path): + if not file_path: + return True + if os.path.isdir(file_path): + return False + log_dir = os.path.dirname(file_path) + return (not log_dir) or os.path.isdir(log_dir) + + +def _absolute_log_file(log_file, default_log_file): + if log_file: + if not os.path.dirname(log_file): + log_file = os.path.join(_BASE_DIR, log_file) + return os.path.abspath(log_file) + + return default_log_file + + +def get_logfile(log_file): + return _absolute_log_file(log_file, DEFAULT_LOG_FILE) + + +def get_accesslogfile(log_file): + return _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) + + +def setup(log_file, log_level=None): + ''' + Configure the logging output. + May be called multiple times. + ''' + log_file = _absolute_log_file(log_file, DEFAULT_LOG_FILE) + + r = logging.root + r.setLevel(log_level or DEFAULT_LOG_LEVEL) + + previous_handler = r.handlers[0] if r.handlers else None + if previous_handler: + # if the log_file has not changed, don't create a new handler + if getattr(previous_handler, 'baseFilename', None) == log_file: + return + r.debug("logging to %s level %s", log_file, r.level) + + if log_file == LOG_TO_STDERR: + file_handler = StreamHandler() + file_handler.baseFilename = LOG_TO_STDERR + else: + try: + file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2) + except IOError: + if log_file == DEFAULT_LOG_FILE: + raise + file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2) + file_handler.setFormatter(FORMATTER) + + for h in r.handlers: + r.removeHandler(h) + h.close() + r.addHandler(file_handler) + + +def create_access_log(log_file, log_name, formatter): + ''' + One-time configuration for the web server's access log. + ''' + log_file = _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) + logging.debug("access log: %s", log_file) + + access_log = logging.getLogger(log_name) + access_log.propagate = False + access_log.setLevel(logging.INFO) + + file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2) + file_handler.setFormatter(formatter) + access_log.addHandler(file_handler) + return access_log + + +# Enable logging of smtp lib debug output +class StderrLogger(object): + def __init__(self, name=None): + self.log = get(name or self.__class__.__name__) + self.buffer = '' + + def write(self, message): + try: + if message == '\n': + self.log.debug(self.buffer.replace('\n', '\\n')) + self.buffer = '' + else: + self.buffer += message + except Exception: + self.log.debug("Logging Error") + + +# if debugging, start logging to stderr immediately +if os.environ.get('FLASK_DEBUG', None): + setup(LOG_TO_STDERR, logging.DEBUG) diff --git a/cps/oauth.py b/cps/oauth.py new file mode 100644 index 00000000..35362dbf --- /dev/null +++ b/cps/oauth.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import division, print_function, unicode_literals +from flask import session + + +try: + from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user + from sqlalchemy.orm.exc import NoResultFound + + class OAuthBackend(SQLAlchemyBackend): + """ + Stores and retrieves OAuth tokens using a relational database through + the `SQLAlchemy`_ ORM. + + .. _SQLAlchemy: http://www.sqlalchemy.org/ + """ + def __init__(self, model, session, + user=None, user_id=None, user_required=None, anon_user=None, + cache=None): + super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) + + def get(self, blueprint, user=None, user_id=None): + if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '': + return session[blueprint.name + '_oauth_token'] + # check cache + cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) + token = self.cache.get(cache_key) + if token: + return token + + # if not cached, make database queries + query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + use_provider_user_id = False + if blueprint.name + '_oauth_user_id' in session and session[blueprint.name + '_oauth_user_id'] != '': + query = query.filter_by(provider_user_id=session[blueprint.name + '_oauth_user_id']) + use_provider_user_id = True + + if self.user_required and not u and not uid and not use_provider_user_id: + #raise ValueError("Cannot get OAuth token without an associated user") + return None + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + try: + token = query.one().token + except NoResultFound: + token = None + + # cache the result + self.cache.set(cache_key, token) + + return token + + def set(self, blueprint, token, user=None, user_id=None): + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot set OAuth token without an associated user") + + # if there was an existing model, delete it + existing_query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + # check for user ID + has_user_id = hasattr(self.model, "user_id") + if has_user_id and uid: + existing_query = existing_query.filter_by(user_id=uid) + # check for user (relationship property) + has_user = hasattr(self.model, "user") + if has_user and u: + existing_query = existing_query.filter_by(user=u) + # queue up delete query -- won't be run until commit() + existing_query.delete() + # create a new model for this token + kwargs = { + "provider": blueprint.name, + "token": token, + } + if has_user_id and uid: + kwargs["user_id"] = uid + if has_user and u: + kwargs["user"] = u + self.session.add(self.model(**kwargs)) + # commit to delete and add simultaneously + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id + )) + + def delete(self, blueprint, user=None, user_id=None): + query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot delete OAuth token without an associated user") + + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + query.delete() + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id, + )) + +except ImportError: + pass diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py new file mode 100644 index 00000000..39777911 --- /dev/null +++ b/cps/oauth_bb.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +from __future__ import division, print_function, unicode_literals +import json +from functools import wraps + +from flask import session, request, make_response, abort +from flask import Blueprint, flash, redirect, url_for +from flask_babel import gettext as _ +from flask_dance.consumer import oauth_authorized, oauth_error +from flask_dance.contrib.github import make_github_blueprint, github +from flask_dance.contrib.google import make_google_blueprint, google +from flask_login import login_user, current_user +from sqlalchemy.orm.exc import NoResultFound + +from . import constants, logger, config, app, ub +from .web import login_required +from .oauth import OAuthBackend +# from .web import github_oauth_required + + +oauth_check = {} +oauth = Blueprint('oauth', __name__) +log = logger.create() + + +def github_oauth_required(f): + @wraps(f) + def inner(*args, **kwargs): + if config.config_login_type == constants.LOGIN_OAUTH_GITHUB: + return f(*args, **kwargs) + if request.is_xhr: + data = {'status': 'error', 'message': 'Not Found'} + response = make_response(json.dumps(data, ensure_ascii=False)) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response, 404 + abort(404) + + return inner + + +def google_oauth_required(f): + @wraps(f) + def inner(*args, **kwargs): + if config.config_use_google_oauth == constants.LOGIN_OAUTH_GOOGLE: + return f(*args, **kwargs) + if request.is_xhr: + data = {'status': 'error', 'message': 'Not Found'} + response = make_response(json.dumps(data, ensure_ascii=False)) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response, 404 + abort(404) + + return inner + + +def register_oauth_blueprint(blueprint, show_name): + if blueprint.name != "": + oauth_check[blueprint.name] = show_name + + +def register_user_with_oauth(user=None): + all_oauth = {} + for oauth in oauth_check.keys(): + if oauth + '_oauth_user_id' in session and session[oauth + '_oauth_user_id'] != '': + all_oauth[oauth] = oauth_check[oauth] + if len(all_oauth.keys()) == 0: + return + if user is None: + flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success") + else: + for oauth in all_oauth.keys(): + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=oauth, + provider_user_id=session[oauth + "_oauth_user_id"], + ) + try: + oauth = query.one() + oauth.user_id = user.id + except NoResultFound: + # no found, return error + return + try: + ub.session.commit() + except Exception as e: + log.exception(e) + ub.session.rollback() + + +def logout_oauth_user(): + for oauth in oauth_check.keys(): + if oauth + '_oauth_user_id' in session: + session.pop(oauth + '_oauth_user_id') + +if ub.oauth_support: + github_blueprint = make_github_blueprint( + client_id=config.config_github_oauth_client_id, + client_secret=config.config_github_oauth_client_secret, + redirect_to="oauth.github_login",) + + google_blueprint = make_google_blueprint( + client_id=config.config_google_oauth_client_id, + client_secret=config.config_google_oauth_client_secret, + redirect_to="oauth.google_login", + scope=[ + "https://www.googleapis.com/auth/plus.me", + "https://www.googleapis.com/auth/userinfo.email", + ] + ) + + app.register_blueprint(google_blueprint, url_prefix="/login") + app.register_blueprint(github_blueprint, url_prefix='/login') + + github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) + google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) + + + if config.config_login_type == constants.LOGIN_OAUTH_GITHUB: + register_oauth_blueprint(github_blueprint, 'GitHub') + if config.config_login_type == constants.LOGIN_OAUTH_GOOGLE: + register_oauth_blueprint(google_blueprint, 'Google') + + + @oauth_authorized.connect_via(github_blueprint) + def github_logged_in(blueprint, token): + if not token: + flash(_(u"Failed to log in with GitHub."), category="error") + return False + + resp = blueprint.session.get("/user") + if not resp.ok: + flash(_(u"Failed to fetch user info from GitHub."), category="error") + return False + + github_info = resp.json() + github_user_id = str(github_info["id"]) + return oauth_update_token(blueprint, token, github_user_id) + + + @oauth_authorized.connect_via(google_blueprint) + def google_logged_in(blueprint, token): + if not token: + flash(_(u"Failed to log in with Google."), category="error") + return False + + resp = blueprint.session.get("/oauth2/v2/userinfo") + if not resp.ok: + flash(_(u"Failed to fetch user info from Google."), category="error") + return False + + google_info = resp.json() + google_user_id = str(google_info["id"]) + + return oauth_update_token(blueprint, token, google_user_id) + + + def oauth_update_token(blueprint, token, provider_user_id): + session[blueprint.name + "_oauth_user_id"] = provider_user_id + session[blueprint.name + "_oauth_token"] = token + + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=blueprint.name, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # update token + oauth.token = token + except NoResultFound: + oauth = ub.OAuth( + provider=blueprint.name, + provider_user_id=provider_user_id, + token=token, + ) + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + log.exception(e) + ub.session.rollback() + + # Disable Flask-Dance's default behavior for saving the OAuth token + return False + + + def bind_oauth_or_register(provider, provider_user_id, redirect_url): + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # already bind with user, just login + if oauth.user: + login_user(oauth.user) + return redirect(url_for('web.index')) + else: + # bind to current user + if current_user and current_user.is_authenticated: + oauth.user = current_user + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + log.exception(e) + ub.session.rollback() + return redirect(url_for('web.login')) + #if config.config_public_reg: + # return redirect(url_for('web.register')) + #else: + # flash(_(u"Public registration is not enabled"), category="error") + # return redirect(url_for(redirect_url)) + except NoResultFound: + return redirect(url_for(redirect_url)) + + + def get_oauth_status(): + status = [] + query = ub.session.query(ub.OAuth).filter_by( + user_id=current_user.id, + ) + try: + oauths = query.all() + for oauth in oauths: + status.append(oauth.provider) + return status + except NoResultFound: + return None + + + def unlink_oauth(provider): + if request.host_url + 'me' != request.referrer: + pass + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + user_id=current_user.id, + ) + try: + oauth = query.one() + if current_user and current_user.is_authenticated: + oauth.user = current_user + try: + ub.session.delete(oauth) + ub.session.commit() + logout_oauth_user() + flash(_(u"Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") + except Exception as e: + log.exception(e) + ub.session.rollback() + flash(_(u"Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") + except NoResultFound: + log.warning("oauth %s for user %d not fount", provider, current_user.id) + flash(_(u"Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") + return redirect(url_for('web.profile')) + + + # notify on OAuth provider error + @oauth_error.connect_via(github_blueprint) + def github_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + u"OAuth error from {name}! " + u"error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) # ToDo: Translate + flash(msg, category="error") + + + @oauth.route('/github') + @github_oauth_required + def github_login(): + if not github.authorized: + return redirect(url_for('github.login')) + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + return redirect(url_for('web.login')) + + + @oauth.route('/unlink/github', methods=["GET"]) + @login_required + def github_login_unlink(): + return unlink_oauth(github_blueprint.name) + + + @oauth.route('/login/google') + @google_oauth_required + def google_login(): + if not google.authorized: + return redirect(url_for("google.login")) + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login') + flash(_(u"Google Oauth error, please retry later."), category="error") + return redirect(url_for('web.login')) + + + @oauth_error.connect_via(google_blueprint) + def google_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + u"OAuth error from {name}! " + u"error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) # ToDo: Translate + flash(msg, category="error") + + + @oauth.route('/unlink/google', methods=["GET"]) + @login_required + def google_login_unlink(): + return unlink_oauth(google_blueprint.name) diff --git a/cps/opds.py b/cps/opds.py new file mode 100644 index 00000000..657b3861 --- /dev/null +++ b/cps/opds.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import sys +import datetime +from functools import wraps + +from flask import Blueprint, request, render_template, Response, g, make_response +from flask_login import current_user +from sqlalchemy.sql.expression import func, text, or_, and_ +from werkzeug.security import check_password_hash + +from . import constants, logger, config, db, ub, services +from .helper import fill_indexpage, get_download_link, get_book_cover +from .pagination import Pagination +from .web import common_filters, get_search_results, render_read_books, download_required + + +opds = Blueprint('opds', __name__) + +log = logger.create() + + +def requires_basic_auth_if_no_ano(f): + @wraps(f) + def decorated(*args, **kwargs): + auth = request.authorization + if config.config_anonbrowse != 1: + if not auth or not check_auth(auth.username, auth.password): + return authenticate() + return f(*args, **kwargs) + if config.config_login_type == constants.LOGIN_LDAP and services.ldap: + return services.ldap.basic_auth_required(f) + return decorated + + +@opds.route("/opds/") +@requires_basic_auth_if_no_ano +def feed_index(): + return render_xml_template('index.xml') + + +@opds.route("/opds/osd") +@requires_basic_auth_if_no_ano +def feed_osd(): + return render_xml_template('osd.xml', lang='en-EN') + + +@opds.route("/opds/search", defaults={'query': ""}) +@opds.route("/opds/search/") +@requires_basic_auth_if_no_ano +def feed_cc_search(query): + return feed_search(query.strip()) + + +@opds.route("/opds/search", methods=["GET"]) +@requires_basic_auth_if_no_ano +def feed_normal_search(): + return feed_search(request.args.get("query").strip()) + + +@opds.route("/opds/new") +@requires_basic_auth_if_no_ano +def feed_new(): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, True, [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/discover") +@requires_basic_auth_if_no_ano +def feed_discover(): + entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\ + .limit(config.config_books_per_page) + pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/rated") +@requires_basic_auth_if_no_ano +def feed_best_rated(): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/hot") +@requires_basic_auth_if_no_ano +def feed_hot(): + off = request.args.get("offset") or 0 + all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by( + func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) + hot_books = all_books.offset(off).limit(config.config_books_per_page) + entries = list() + for book in hot_books: + downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first() + if downloadBook: + entries.append( + db.session.query(db.Books).filter(common_filters()) + .filter(db.Books.id == book.Downloads.book_id).first() + ) + else: + ub.delete_download(book.Downloads.book_id) + # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() + # ub.session.commit() + numBooks = entries.__len__() + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), + config.config_books_per_page, numBooks) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/author") +@requires_basic_auth_if_no_ano +def feed_authorindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\ + .group_by(text('books_authors_link.author')).order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Authors).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination) + + +@opds.route("/opds/author/") +@requires_basic_auth_if_no_ano +def feed_author(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.authors.any(db.Authors.id == book_id), [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/publisher") +@requires_basic_auth_if_no_ano +def feed_publisherindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\ + .group_by(text('books_publishers_link.publisher')).order_by(db.Publishers.sort).limit(config.config_books_per_page).offset(off) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Publishers).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination) + + +@opds.route("/opds/publisher/") +@requires_basic_auth_if_no_ano +def feed_publisher(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.publishers.any(db.Publishers.id == book_id), + [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/category") +@requires_basic_auth_if_no_ano +def feed_categoryindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\ + .group_by(text('books_tags_link.tag')).order_by(db.Tags.name).offset(off).limit(config.config_books_per_page) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Tags).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination) + + +@opds.route("/opds/category/") +@requires_basic_auth_if_no_ano +def feed_category(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.tags.any(db.Tags.id == book_id), [db.Books.timestamp.desc()]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/series") +@requires_basic_auth_if_no_ano +def feed_seriesindex(): + off = request.args.get("offset") or 0 + entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\ + .group_by(text('books_series_link.series')).order_by(db.Series.sort).offset(off).all() + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(db.session.query(db.Series).all())) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination) + + +@opds.route("/opds/series/") +@requires_basic_auth_if_no_ano +def feed_series(book_id): + off = request.args.get("offset") or 0 + entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), + db.Books, db.Books.series.any(db.Series.id == book_id), [db.Books.series_index]) + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + +@opds.route("/opds/shelfindex/", defaults={'public': 0}) +@opds.route("/opds/shelfindex/") +@requires_basic_auth_if_no_ano +def feed_shelfindex(public): + off = request.args.get("offset") or 0 + if public is not 0: + shelf = g.public_shelfes + number = len(shelf) + else: + shelf = g.user.shelf + number = shelf.count() + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + number) + return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination) + + +@opds.route("/opds/shelf/") +@requires_basic_auth_if_no_ano +def feed_shelf(book_id): + off = request.args.get("offset") or 0 + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == book_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == book_id))).first() + result = list() + # user is allowed to access shelf + if shelf: + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by( + ub.BookShelf.order.asc()).all() + for book in books_in_shelf: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + result.append(cur_book) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(result)) + return render_xml_template('feed.xml', entries=result, pagination=pagination) + + +@opds.route("/opds/download///") +@requires_basic_auth_if_no_ano +@download_required +def opds_download_link(book_id, book_format): + return get_download_link(book_id,book_format) + + +@opds.route("/ajax/book/") +@requires_basic_auth_if_no_ano +def get_metadata_calibre_companion(uuid): + entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() + if entry is not None: + js = render_template('json.txt', entry=entry) + response = make_response(js) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response + else: + return "" + + +def feed_search(term): + if term: + term = term.strip().lower() + entries = get_search_results( term) + entriescount = len(entries) if len(entries) > 0 else 1 + pagination = Pagination(1, entriescount, entriescount) + return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) + else: + return render_xml_template('feed.xml', searchterm="") + +def check_auth(username, password): + if sys.version_info.major == 3: + username=username.encode('windows-1252') + user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == + username.decode('utf-8').lower()).first() + return bool(user and check_password_hash(user.password, password)) + + +def authenticate(): + return Response( + 'Could not verify your access level for that URL.\n' + 'You have to login with proper credentials', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + + +def render_xml_template(*args, **kwargs): + #ToDo: return time in current timezone similar to %z + currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00") + xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs) + response = make_response(xml) + response.headers["Content-Type"] = "application/atom+xml; charset=utf-8" + return response + +@opds.route("/opds/thumb_240_240/") +@opds.route("/opds/cover_240_240/") +@opds.route("/opds/cover_90_90/") +@opds.route("/opds/cover/") +@requires_basic_auth_if_no_ano +def feed_get_cover(book_id): + return get_book_cover(book_id) + +@opds.route("/opds/readbooks/") +@requires_basic_auth_if_no_ano +def feed_read_books(): + off = request.args.get("offset") or 0 + return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) + + +@opds.route("/opds/unreadbooks/") +@requires_basic_auth_if_no_ano +def feed_unread_books(): + off = request.args.get("offset") or 0 + return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) diff --git a/cps/pagination.py b/cps/pagination.py new file mode 100644 index 00000000..0a138a64 --- /dev/null +++ b/cps/pagination.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +from math import ceil + + +# simple pagination for the feed +class Pagination(object): + def __init__(self, page, per_page, total_count): + self.page = int(page) + self.per_page = int(per_page) + self.total_count = int(total_count) + + @property + def next_offset(self): + return int(self.page * self.per_page) + + @property + def previous_offset(self): + return int((self.page - 2) * self.per_page) + + @property + def last_offset(self): + last = int(self.total_count) - int(self.per_page) + if last < 0: + last = 0 + return int(last) + + @property + def pages(self): + return int(ceil(self.total_count / float(self.per_page))) + + @property + def has_prev(self): + return self.page > 1 + + @property + def has_next(self): + return self.page < self.pages + + # right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shwn + # left_edge: first left_edges count of all pages are shown as number -> 1,2 shwn + # left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shwn + # left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shwn + def iter_pages(self, left_edge=2, left_current=2, + right_current=4, right_edge=2): + last = 0 + left_current = self.page - left_current - 1 + right_current = self.page + right_current + 1 + right_edge = self.pages - right_edge + for num in range(1, (self.pages + 1)): + if num <= left_edge or (left_current < num < right_current) or num > right_edge: + if last + 1 != num: + yield None + yield num + last = num diff --git a/cps/redirect.py b/cps/redirect.py index 7b3981c4..324c4b20 100644 --- a/cps/redirect.py +++ b/cps/redirect.py @@ -28,10 +28,12 @@ # http://flask.pocoo.org/snippets/62/ +from __future__ import division, print_function, unicode_literals try: from urllib.parse import urlparse, urljoin except ImportError: from urlparse import urlparse, urljoin + from flask import request, url_for, redirect diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py index 8a44062a..25bbe77b 100644 --- a/cps/reverseproxy.py +++ b/cps/reverseproxy.py @@ -37,6 +37,9 @@ # # Inspired by http://flask.pocoo.org/snippets/35/ +from __future__ import division, print_function, unicode_literals + + class ReverseProxied(object): """Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind diff --git a/cps/server.py b/cps/server.py index 98baddf3..1d564824 100644 --- a/cps/server.py +++ b/cps/server.py @@ -17,149 +17,190 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -from socket import error as SocketError +from __future__ import division, print_function, unicode_literals import sys import os +import errno import signal -import web +import socket try: from gevent.pywsgi import WSGIServer from gevent.pool import Pool - from gevent import __version__ as geventVersion - gevent_present = True + from gevent import __version__ as _version + VERSION = {'Gevent': 'v' + _version} + _GEVENT = True except ImportError: from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop - from tornado import version as tornadoVersion - gevent_present = False + from tornado import version as _version + VERSION = {'Tornado': 'v' + _version} + _GEVENT = False + +from . import logger, global_WorkerThread +log = logger.create() -class server: - wsgiserver = None - restart= False +class WebServer: def __init__(self): - signal.signal(signal.SIGINT, self.killServer) - signal.signal(signal.SIGTERM, self.killServer) + signal.signal(signal.SIGINT, self._killServer) + signal.signal(signal.SIGTERM, self._killServer) - def start_gevent(self): + self.wsgiserver = None + self.access_logger = None + self.restart = False + self.app = None + self.listen_address = None + self.listen_port = None + self.IPV6 = False + self.unix_socket_file = None + self.ssl_args = None + + def init_app(self, application, config): + self.app = application + self.listen_address = config.get_config_ipaddress() + self.IPV6 = config.get_ipaddress_type() + self.listen_port = config.config_port + + if config.config_access_log: + log_name = "gevent.access" if _GEVENT else "tornado.access" + formatter = logger.ACCESS_FORMATTER_GEVENT if _GEVENT else logger.ACCESS_FORMATTER_TORNADO + self.access_logger = logger.create_access_log(config.config_access_logfile, log_name, formatter) + else: + if not _GEVENT: + logger.get('tornado.access').disabled = True + + certfile_path = config.get_config_certfile() + keyfile_path = config.get_config_keyfile() + if certfile_path and keyfile_path: + if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): + self.ssl_args = {"certfile": certfile_path, + "keyfile": keyfile_path} + else: + log.warning('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl.') + log.warning('Cert path: %s', certfile_path) + log.warning('Key path: %s', keyfile_path) + + def _make_gevent_unix_socket(self, socket_file): + # the socket file must not exist prior to bind() + if os.path.exists(socket_file): + # avoid nuking regular files and symbolic links (could be a mistype or security issue) + if os.path.isfile(socket_file) or os.path.islink(socket_file): + raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), socket_file) + os.remove(socket_file) + + unix_sock = WSGIServer.get_listener(socket_file, family=socket.AF_UNIX) + self.unix_socket_file = socket_file + + # ensure current user and group have r/w permissions, no permissions for other users + # this way the socket can be shared in a semi-secure manner + # between the user running calibre-web and the user running the fronting webserver + os.chmod(socket_file, 0o660) + + return unix_sock + + def _make_gevent_socket(self): + if os.name != 'nt': + unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET") + if unix_socket_file: + output = "socket:" + unix_socket_file + ":" + str(self.listen_port) + return self._make_gevent_unix_socket(unix_socket_file), output + + if self.listen_address: + return (self.listen_address, self.listen_port), self._get_readable_listen_address() + + if os.name == 'nt': + self.listen_address = '0.0.0.0' + return (self.listen_address, self.listen_port), self._get_readable_listen_address() + + address = ('', self.listen_port) try: - ssl_args = dict() - certfile_path = web.ub.config.get_config_certfile() - keyfile_path = web.ub.config.get_config_keyfile() - if certfile_path and keyfile_path: - if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): - ssl_args = {"certfile": certfile_path, - "keyfile": keyfile_path} - else: - web.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path)) - if os.name == 'nt': - self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) - else: - self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) - web.py3_gevent_link = self.wsgiserver + sock = WSGIServer.get_listener(address, family=socket.AF_INET6) + output = self._get_readable_listen_address(True) + except socket.error as ex: + log.error('%s', ex) + log.warning('Unable to listen on "", trying on IPv4 only...') + output = self._get_readable_listen_address(False) + sock = WSGIServer.get_listener(address, family=socket.AF_INET) + return sock, output + + def _start_gevent(self): + ssl_args = self.ssl_args or {} + + try: + sock, output = self._make_gevent_socket() + log.info('Starting Gevent server on %s', output) + self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, spawn=Pool(), **ssl_args) self.wsgiserver.serve_forever() + finally: + if self.unix_socket_file: + os.remove(self.unix_socket_file) + self.unix_socket_file = None - except SocketError: - try: - web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...') - self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) - web.py3_gevent_link = self.wsgiserver - self.wsgiserver.serve_forever() - except (OSError, SocketError) as e: - web.app.logger.info("Error starting server: %s" % e.strerror) - print("Error starting server: %s" % e.strerror) - web.helper.global_WorkerThread.stop() - sys.exit(1) - except Exception: - web.app.logger.info("Unknown error while starting gevent") + def _start_tornado(self): + log.info('Starting Tornado server on %s', self._get_readable_listen_address()) - def startServer(self): - if gevent_present: - web.app.logger.info('Starting Gevent server') - # leave subprocess out to allow forking for fetchers and processors - self.start_gevent() + # Max Buffersize set to 200MB ) + http_server = HTTPServer(WSGIContainer(self.app), + max_buffer_size = 209700000, + ssl_options=self.ssl_args) + http_server.listen(self.listen_port, self.listen_address) + self.wsgiserver=IOLoop.instance() + self.wsgiserver.start() + # wait for stop signal + self.wsgiserver.close(True) + + def _get_readable_listen_address(self, ipV6=False): + if self.listen_address == "": + listen_string = '""' else: - try: - ssl = None - web.app.logger.info('Starting Tornado server') - certfile_path = web.ub.config.get_config_certfile() - keyfile_path = web.ub.config.get_config_keyfile() - if certfile_path and keyfile_path: - if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path): - ssl = {"certfile": certfile_path, - "keyfile": keyfile_path} - else: - web.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path)) + ipV6 = self.IPV6 + listen_string = self.listen_address + if ipV6: + adress = "[" + listen_string + "]" + else: + adress = listen_string + return adress + ":" + str(self.listen_port) - # Max Buffersize set to 200MB - http_server = HTTPServer(WSGIContainer(web.app), - max_buffer_size = 209700000, - ssl_options=ssl) - http_server.listen(web.ub.config.config_port) - self.wsgiserver=IOLoop.instance() - web.py3_gevent_link = self.wsgiserver - self.wsgiserver.start() - # wait for stop signal - self.wsgiserver.close(True) - except SocketError as e: - web.app.logger.info("Error starting server: %s" % e.strerror) - print("Error starting server: %s" % e.strerror) - web.helper.global_WorkerThread.stop() - sys.exit(1) - - # ToDo: Somehow caused by circular import under python3 refactor - if sys.version_info > (3, 0): - self.restart = web.py3_restart_Typ - if self.restart == True: - web.app.logger.info("Performing restart of Calibre-Web") - web.helper.global_WorkerThread.stop() - if os.name == 'nt': - arguments = ["\"" + sys.executable + "\""] - for e in sys.argv: - arguments.append("\"" + e + "\"") - os.execv(sys.executable, arguments) + def start(self): + try: + if _GEVENT: + # leave subprocess out to allow forking for fetchers and processors + self._start_gevent() else: - os.execl(sys.executable, sys.executable, *sys.argv) - else: - web.app.logger.info("Performing shutdown of Calibre-Web") - web.helper.global_WorkerThread.stop() - sys.exit(0) + self._start_tornado() + except Exception as ex: + log.error("Error starting server: %s", ex) + print("Error starting server: %s" % ex) + return False + finally: + self.wsgiserver = None + global_WorkerThread.stop() - def setRestartTyp(self,starttyp): - self.restart = starttyp - # ToDo: Somehow caused by circular import under python3 refactor - web.py3_restart_Typ = starttyp + if not self.restart: + log.info("Performing shutdown of Calibre-Web") + return True - def killServer(self, signum, frame): - self.stopServer() + log.info("Performing restart of Calibre-Web") + arguments = list(sys.argv) + arguments.insert(0, sys.executable) + if os.name == 'nt': + arguments = ["\"%s\"" % a for a in arguments] + os.execv(sys.executable, arguments) + return True - def stopServer(self): - # ToDo: Somehow caused by circular import under python3 refactor - if sys.version_info > (3, 0): - if not self.wsgiserver: - # if gevent_present: - self.wsgiserver = web.py3_gevent_link - #else: - # self.wsgiserver = IOLoop.instance() + def _killServer(self, signum, frame): + self.stop() + + def stop(self, restart=False): + log.info("webserver stop (restart=%s)", restart) + self.restart = restart if self.wsgiserver: - if gevent_present: + if _GEVENT: self.wsgiserver.close() else: self.wsgiserver.add_callback(self.wsgiserver.stop) - - @staticmethod - def getNameVersion(): - if gevent_present: - return {'Gevent':'v'+geventVersion} - else: - return {'Tornado':'v'+tornadoVersion} - - -# Start Instance of Server -Server=server() diff --git a/cps/services/__init__.py b/cps/services/__init__.py new file mode 100644 index 00000000..90607160 --- /dev/null +++ b/cps/services/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2019 pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals + +from .. import logger + + +log = logger.create() + + +try: from . import goodreads +except ImportError as err: + log.warning("goodreads: %s", err) + goodreads = None + + +try: from . import simpleldap as ldap +except ImportError as err: + log.warning("simpleldap: %s", err) + ldap = None diff --git a/cps/services/goodreads.py b/cps/services/goodreads.py new file mode 100644 index 00000000..55161c7a --- /dev/null +++ b/cps/services/goodreads.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import time +from functools import reduce + +from goodreads.client import GoodreadsClient + +try: import Levenshtein +except ImportError: Levenshtein = False + +from .. import logger + + +log = logger.create() +_client = None # type: GoodreadsClient + +# GoodReads TOS allows for 24h caching of data +_CACHE_TIMEOUT = 23 * 60 * 60 # 23 hours (in seconds) +_AUTHORS_CACHE = {} + + +def connect(key=None, secret=None, enabled=True): + global _client + + if not enabled or not key or not secret: + _client = None + return + + if _client: + # make sure the configuration has not changed since last we used the client + if _client.client_key != key or _client.client_secret != secret: + _client = None + + if not _client: + _client = GoodreadsClient(key, secret) + + +def get_author_info(author_name): + now = time.time() + author_info = _AUTHORS_CACHE.get(author_name, None) + if author_info: + if now < author_info._timestamp + _CACHE_TIMEOUT: + return author_info + # clear expired entries + del _AUTHORS_CACHE[author_name] + + if not _client: + log.warning("failed to get a Goodreads client") + return + + try: + author_info = _client.find_author(author_name=author_name) + except Exception as ex: + # Skip goodreads, if site is down/inaccessible + log.warning('Goodreads website is down/inaccessible? %s', ex) + return + + if author_info: + author_info._timestamp = now + _AUTHORS_CACHE[author_name] = author_info + return author_info + + +def get_other_books(author_info, library_books=None): + # Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates + # Note: Not all images will be shown, even though they're available on Goodreads.com. + # See https://www.goodreads.com/topic/show/18213769-goodreads-book-images + + if not author_info: + return + + identifiers = [] + library_titles = [] + if library_books: + identifiers = list(reduce(lambda acc, book: acc + [i.val for i in book.identifiers if i.val], library_books, [])) + library_titles = [book.title for book in library_books] + + for book in author_info.books: + if book.isbn in identifiers: + continue + if book.gid["#text"] in identifiers: + continue + + if Levenshtein and library_titles: + goodreads_title = book._book_dict['title_without_series'] + if any(Levenshtein.ratio(goodreads_title, title) > 0.7 for title in library_titles): + continue + + yield book diff --git a/cps/services/simpleldap.py b/cps/services/simpleldap.py new file mode 100644 index 00000000..f9d0dfff --- /dev/null +++ b/cps/services/simpleldap.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, pwr +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import base64 + +from flask_simpleldap import LDAP, LDAPException + +from .. import constants, logger + + +log = logger.create() +_ldap = LDAP() + + +def init_app(app, config): + global _ldap + + if config.config_login_type != constants.LOGIN_LDAP: + _ldap = None + return + + app.config['LDAP_HOST'] = config.config_ldap_provider_url + app.config['LDAP_PORT'] = config.config_ldap_port + app.config['LDAP_SCHEMA'] = config.config_ldap_schema + app.config['LDAP_USERNAME'] = config.config_ldap_user_object.replace('%s', config.config_ldap_serv_username)\ + + ',' + config.config_ldap_dn + app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) + app.config['LDAP_REQUIRE_CERT'] = bool(config.config_ldap_require_cert) + if config.config_ldap_require_cert: + app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path + app.config['LDAP_BASE_DN'] = config.config_ldap_dn + app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object + app.config['LDAP_USE_SSL'] = bool(config.config_ldap_use_ssl) + app.config['LDAP_USE_TLS'] = bool(config.config_ldap_use_tls) + app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap) + + # app.config['LDAP_BASE_DN'] = 'ou=users,dc=yunohost,dc=org' + # app.config['LDAP_USER_OBJECT_FILTER'] = '(uid=%s)' + _ldap.init_app(app) + + + +def basic_auth_required(func): + return _ldap.basic_auth_required(func) + + +def bind_user(username, password): + # ulf= _ldap.get_object_details('admin') + '''Attempts a LDAP login. + + :returns: True if login succeeded, False if login failed, None if server unavailable. + ''' + try: + result = _ldap.bind_user(username, password) + log.debug("LDAP login '%s': %r", username, result) + return result is not None + except LDAPException as ex: + if ex.message == 'Invalid credentials': + log.info("LDAP login '%s' failed: %s", username, ex) + return False + if ex.message == "Can't contact LDAP server": + log.warning('LDAP Server down: %s', ex) + return None + else: + log.warning('LDAP Server error: %s', ex.message) + return None diff --git a/cps/shelf.py b/cps/shelf.py new file mode 100644 index 00000000..a34dbfed --- /dev/null +++ b/cps/shelf.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals + +from flask import Blueprint, request, flash, redirect, url_for +from flask_babel import gettext as _ +from flask_login import login_required, current_user +from sqlalchemy.sql.expression import func, or_, and_ + +from . import logger, ub, searched_ids, db +from .web import render_title_template + + +shelf = Blueprint('shelf', __name__) +log = logger.create() + + +@shelf.route("/shelf/add//") +@login_required +def add_to_shelf(shelf_id, book_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if shelf is None: + log.error("Invalid shelf specified: %s", shelf_id) + if not request.is_xhr: + flash(_(u"Invalid shelf specified"), category="error") + return redirect(url_for('web.index')) + return "Invalid shelf specified", 400 + + if not shelf.is_public and not shelf.user_id == int(current_user.id): + log.error("User %s not allowed to add a book to %s", current_user, shelf) + if not request.is_xhr: + flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name), + category="error") + return redirect(url_for('web.index')) + return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403 + + if shelf.is_public and not current_user.role_edit_shelfs(): + log.info("User %s not allowed to edit public shelves", current_user) + if not request.is_xhr: + flash(_(u"You are not allowed to edit public shelves"), category="error") + return redirect(url_for('web.index')) + return "User is not allowed to edit public shelves", 403 + + book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, + ub.BookShelf.book_id == book_id).first() + if book_in_shelf: + log.error("Book %s is already part of %s", book_id, shelf) + if not request.is_xhr: + flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error") + return redirect(url_for('web.index')) + return "Book is already part of the shelf: %s" % shelf.name, 400 + + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() + if maxOrder[0] is None: + maxOrder = 0 + else: + maxOrder = maxOrder[0] + + ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1) + ub.session.add(ins) + ub.session.commit() + if not request.is_xhr: + flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success") + if "HTTP_REFERER" in request.environ: + return redirect(request.environ["HTTP_REFERER"]) + else: + return redirect(url_for('web.index')) + return "", 204 + + +@shelf.route("/shelf/massadd/") +@login_required +def search_to_shelf(shelf_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if shelf is None: + log.error("Invalid shelf specified: %s", shelf_id) + flash(_(u"Invalid shelf specified"), category="error") + return redirect(url_for('web.index')) + + if not shelf.is_public and not shelf.user_id == int(current_user.id): + log.error("User %s not allowed to add a book to %s", current_user, shelf) + flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error") + return redirect(url_for('web.index')) + + if shelf.is_public and not current_user.role_edit_shelfs(): + log.error("User %s not allowed to edit public shelves", current_user) + flash(_(u"User is not allowed to edit public shelves"), category="error") + return redirect(url_for('web.index')) + + if current_user.id in searched_ids and searched_ids[current_user.id]: + books_for_shelf = list() + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all() + if books_in_shelf: + book_ids = list() + for book_id in books_in_shelf: + book_ids.append(book_id.book_id) + for searchid in searched_ids[current_user.id]: + if searchid not in book_ids: + books_for_shelf.append(searchid) + else: + books_for_shelf = searched_ids[current_user.id] + + if not books_for_shelf: + log.error("Books are already part of %s", shelf) + flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error") + return redirect(url_for('web.index')) + + maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first() + if maxOrder[0] is None: + maxOrder = 0 + else: + maxOrder = maxOrder[0] + + for book in books_for_shelf: + maxOrder = maxOrder + 1 + ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder) + ub.session.add(ins) + ub.session.commit() + flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success") + else: + flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error") + return redirect(url_for('web.index')) + + +@shelf.route("/shelf/remove//") +@login_required +def remove_from_shelf(shelf_id, book_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if shelf is None: + log.error("Invalid shelf specified: %s", shelf_id) + if not request.is_xhr: + return redirect(url_for('web.index')) + return "Invalid shelf specified", 400 + + # if shelf is public and use is allowed to edit shelfs, or if shelf is private and user is owner + # allow editing shelfs + # result shelf public user allowed user owner + # false 1 0 x + # true 1 1 x + # true 0 x 1 + # false 0 x 0 + + if (not shelf.is_public and shelf.user_id == int(current_user.id)) \ + or (shelf.is_public and current_user.role_edit_shelfs()): + book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id, + ub.BookShelf.book_id == book_id).first() + + if book_shelf is None: + log.error("Book %s already removed from %s", book_id, shelf) + if not request.is_xhr: + return redirect(url_for('web.index')) + return "Book already removed from shelf", 410 + + ub.session.delete(book_shelf) + ub.session.commit() + + if not request.is_xhr: + flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success") + return redirect(request.environ["HTTP_REFERER"]) + return "", 204 + else: + log.error("User %s not allowed to remove a book from %s", current_user, shelf) + if not request.is_xhr: + flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), + category="error") + return redirect(url_for('web.index')) + return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403 + + + +@shelf.route("/shelf/create", methods=["GET", "POST"]) +@login_required +def create_shelf(): + shelf = ub.Shelf() + if request.method == "POST": + to_save = request.form.to_dict() + if "is_public" in to_save: + shelf.is_public = 1 + shelf.name = to_save["title"] + shelf.user_id = int(current_user.id) + existing_shelf = ub.session.query(ub.Shelf).filter( + or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), + (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).first() + if existing_shelf: + flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") + else: + try: + ub.session.add(shelf) + ub.session.commit() + flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success") + except Exception: + flash(_(u"There was an error"), category="error") + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate") + else: + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"), page="shelfcreate") + + +@shelf.route("/shelf/edit/", methods=["GET", "POST"]) +@login_required +def edit_shelf(shelf_id): + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + if request.method == "POST": + to_save = request.form.to_dict() + existing_shelf = ub.session.query(ub.Shelf).filter( + or_((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1), + (ub.Shelf.name == to_save["title"]) & (ub.Shelf.user_id == int(current_user.id)))).filter( + ub.Shelf.id != shelf_id).first() + if existing_shelf: + flash(_(u"A shelf with the name '%(title)s' already exists.", title=to_save["title"]), category="error") + else: + shelf.name = to_save["title"] + if "is_public" in to_save: + shelf.is_public = 1 + else: + shelf.is_public = 0 + try: + ub.session.commit() + flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success") + except Exception: + flash(_(u"There was an error"), category="error") + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") + else: + return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit") + + +@shelf.route("/shelf/delete/") +@login_required +def delete_shelf(shelf_id): + cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() + deleted = None + if current_user.role_admin(): + deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete() + else: + if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \ + or (cur_shelf.is_public and current_user.role_edit_shelfs()): + deleted = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).delete() + + if deleted: + ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete() + ub.session.commit() + log.info("successfully deleted %s", cur_shelf) + return redirect(url_for('web.index')) + +# @shelf.route("/shelfdown/") +@shelf.route("/shelf/", defaults={'shelf_type': 1}) +@shelf.route("/shelf//") +@login_required +def show_shelf(shelf_type, shelf_id): + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() + result = list() + # user is allowed to access shelf + if shelf: + page = "shelf.html" if shelf_type == 1 else 'shelfdown.html' + + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() + for book in books_in_shelf: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + if cur_book: + result.append(cur_book) + else: + log.info('Not existing book %s in %s deleted', book.book_id, shelf) + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() + ub.session.commit() + return render_title_template(page, entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, page="shelf") + else: + flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") + return redirect(url_for("web.index")) + + + +@shelf.route("/shelf/order/", methods=["GET", "POST"]) +@login_required +def order_shelf(shelf_id): + if request.method == "POST": + to_save = request.form.to_dict() + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() + counter = 0 + for book in books_in_shelf: + setattr(book, 'order', to_save[str(book.book_id)]) + counter += 1 + ub.session.commit() + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() + result = list() + if shelf: + books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \ + .order_by(ub.BookShelf.order.asc()).all() + for book in books_in_shelf2: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + result.append(cur_book) + return render_title_template('shelf_order.html', entries=result, + title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, page="shelforder") diff --git a/cps/static/css/images/black-10.png b/cps/static/css/images/black-10.png new file mode 100644 index 0000000000000000000000000000000000000000..fe545ed85188384926cd9b5d0b6a751c52b12d77 GIT binary patch literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4U@}4e^As)w*6DBMX=#w(YIPc)I$ztaD0e0sx6Y7?c12 literal 0 HcmV?d00001 diff --git a/cps/static/css/images/black-25.png b/cps/static/css/images/black-25.png new file mode 100644 index 0000000000000000000000000000000000000000..a498b981b42e14fb2377a9bd88a6f06445121143 GIT binary patch literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4U@}4e^As)w*6E-Xt=#w(YIk4U@}4e^As)w*6F3$M^hp_H9azD* gn5_rGIDD3YVFOQqfJ?nCJ5VWur>mdKI;Vst090od6#xJL literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/credits.txt b/cps/static/css/images/icomoon/credits.txt new file mode 100644 index 00000000..34c4b3ab --- /dev/null +++ b/cps/static/css/images/icomoon/credits.txt @@ -0,0 +1,6 @@ +SVG icons via Icomoon +https://icomoon.io/app + +Icons used from the following sets: +* Entypo - Creative Commons BY-SA 3.0 http://creativecommons.org/licenses/by-sa/3.0/us/ +* IcoMoon - Free (GPL) http://www.gnu.org/licenses/gpl.html \ No newline at end of file diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/arrow.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..e77449bcc9d54970735c8d516faff515ee6f6cdb GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#A>7Fi*Ar*|t4UK^b2Lc>swK24P z=iyzF;gRrzzvZ*!n?fhn5MJ(vQ=Er?YM2RdGGEA5mdK II;Vst01VhRCjbBd literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/cart.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/cart.png new file mode 100644 index 0000000000000000000000000000000000000000..70e74a1444fd242bf43e7f64f55792694fd70b23 GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AYdu{YLn;{OPLf>EmLR}zTyy!H zYd~DL{JiG!n7DWMcI}x{q}l%Nci!xWd#C5SELkk4ANcBG!}KE>HzkrBR>ZGdqw!WT zi@)=Y;zHXao7)}o_&rzTpY;?l+cEnN-@}#%Cixx5d5`Z|FK4p*bhXN|WeR%@)7(>H zKK)><%DT5|Z6B|bqi-Way7+Hqi~axXqwB6Let5O(^l8q!H}^&HM}O2j9%9EX)AhJx e!WORhOoq3g#4cCMoYw|AoWax8&t;ucLK6VY(P5$h literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/first.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/first.png new file mode 100644 index 0000000000000000000000000000000000000000..0947734aed5b50faa56d261a104b1d95bfdec5c1 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#A#hxyXAr*{o&jj)|8wfN!^mkHK zSQw<0DpbJWH-q^V!yF#==y#=&)9zW<%ir4lMEk(pwCyW}_C9{GPg~5r&1C(J{z)h0 zb{85Q|NnT2Rg#qIQK9ubGasLum~>WX#W|hEc5Uw&UvwIuof7K0`ZC@3d&u9{@0h($ WH$JQ=eN+#$p25@A&t;ucLK6UxphUj_ literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/last.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/last.png new file mode 100644 index 0000000000000000000000000000000000000000..3621d331019ffff3d979d210c53cb65e9820df69 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#9o-U3d6^zLT_}v;A4_CGQF5w4%+YN?@N4Cxko>2=5?3%TWkD z&2lMcr^BpxI|GpkVoA~--P8G(9R0`I#<}CNM5bmXbAa@jcYzEH?@b#I3QFv82HMQv M>FVdQ&MBb@0Fy*Cl>h($ literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/list.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/list.png new file mode 100644 index 0000000000000000000000000000000000000000..2684aaf3ecbf9b6ad12a2df9219d48bd94f7527a GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AHl8kyAr*|t3er7ItUSLZuNiAJ z++=kyvzpVt?qK6XdkM3K%Ksak*nOh;400N77cnsFNqn_?nQ-OzVg`oiMj4ymX3XLT Pn#17f>gTe~DWM4f3$!G9 literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/list2.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/list2.png new file mode 100644 index 0000000000000000000000000000000000000000..601413b7ea2e36e7e6bd58e3e71378e14b8570c6 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#Anw~C>Ar*|t3er7ItUSLz9{Tx0 vN=D+Ja0}mzZ88VA#e1seml(YI8_2*A=}|9tsY&4|P!ofvtDnm{r-UW|tsorM literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/loop.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/loop.png new file mode 100644 index 0000000000000000000000000000000000000000..6f9aba040320806ae6c60a5b2e71c1b89881123d GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#At)4E9Ar*{o`yF`?8}PWww>U_& z7~N3lJ;8tr#~-FqATK0!(RkjbgpirnAbTz^hr*zt4M z0~NC=FVZebi8);8yQG$OOk?hXNlrnxmV7FB`(pnMw_eYKj)HZX&pQv~9J)Vg?)8<` pK5g4hT1~VuR488H%x@$fuT-|%EOXaUcA$e8JYD@<);T3K0RWc^N~iz; literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/music.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/music.png new file mode 100644 index 0000000000000000000000000000000000000000..6e1ae083acd70babd3c1e036b1f9e8c8757dd5de GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AZJsWUAr*{!FFOhzG7xBe7=Osi z!AaGHE9jczbw;s*3ljnPoO})&7cw%unmB#;`|S%=fmShiy85}Sb4q9e08uA0 AyZ`_I literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/PNG/play.png b/cps/static/css/images/icomoon/entypo-25px-000000/PNG/play.png new file mode 100644 index 0000000000000000000000000000000000000000..6fcf777042ce56ef31e68605572e22239a53ab0c GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AnVv3=Ar*|t4y=nE8Xwv^+cz_4 zM#)J0V}4lM#xQA%!^1i@)}XC_*sV6stI*}BkkfFA;AWe1Ucq6HSc~vu&g_FNTuU;4 z9+==TOPAZ@^J&&gPH&iP000>X1ONa4Zs1Mm0002rNklxf&dS0ELXGE+VsdBT5ImSLa6Hh)L=)ZY_w~exz%3g{7W%62 zL*$y(Y^Wy&3!N3d5lcKRWy!#9uR&qN@q*0?ZxTykLwE~sqldnNmQ~a1Ey7ZLn+at( zJF&!?wQFIs$8v5vGoQ?8_6q>CGJrj!bw-YrCleSW&+|K$iV;tXCmjkKL(gAKNeJ{* eB3(GH{3*Y;sJb)%Wx&$_0000aX6u;7v>@)$1C|9JRhk1C7+y!ue0Q(C R{3_6H22WQ%mvv4FO#rHwHr@aL literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/arrow.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/arrow.svg new file mode 100644 index 00000000..e6f2a0bd --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/arrow.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/cart.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/cart.svg new file mode 100644 index 00000000..590ffa8c --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/cart.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/first.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/first.svg new file mode 100644 index 00000000..e69482d1 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/first.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/last.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/last.svg new file mode 100644 index 00000000..9a958b23 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/last.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/list.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/list.svg new file mode 100644 index 00000000..88c39810 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/list2.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/list2.svg new file mode 100644 index 00000000..0c9ea62f --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/list2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/loop.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/loop.svg new file mode 100644 index 00000000..7b0c90ce --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/loop.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/music.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/music.svg new file mode 100644 index 00000000..135ccded --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/music.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/pause.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/pause.svg new file mode 100644 index 00000000..d08ab5ad --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/pause.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/play.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/play.svg new file mode 100644 index 00000000..352ccad2 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/play.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/shuffle.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/shuffle.svg new file mode 100644 index 00000000..a6fc25f4 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/shuffle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-000000/SVG/volume.svg b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/volume.svg new file mode 100644 index 00000000..dcc4a3c2 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-000000/SVG/volume.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/arrow.png b/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..2452d288026c194dec15e6e11bd02c19029cf90a GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AIi4<#Ar_~T6C_v#KHPs$&zLF2 z^l$(F`wm@gE&mk^{_nT-ThXv8@nC(@ftnKO4o1c$w?4@qT^}%8>`(atM#HTQ&Tp9C z2^?OM6$_s3KlHzfNx-?A`DCNd7BS{aTWo&JH<)$1k%56@WyuV`s2#U~ P_A+?7`njxgN@xNAzotOZ literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/cart.png b/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/cart.png new file mode 100644 index 0000000000000000000000000000000000000000..611a39661f86e1f4d21c349dfffb1fd61d812094 GIT binary patch literal 236 zcmVP000>X1ONa4Zs1Mm00029Nkl=6ob#LAIDjwm_ONRUN1IbtZwVd&0?&f7-6e~v|;w5y7fSF~Fg(79|k z9#HU1g>8&ij;zSb1lZ)}aNn!GE&vbsCGEIlkM%k3{`2zw&MK8FamZ$<<9hvcsS53~ m`~qzpnvN1&voz6sN%R8wTvwp^FLagw0000jRhi^+K2fN%EoIWD-|e0%Ov<+}tE#8H dp0xi4XR?<>C&Ub@{yz^#u9GO568R zjTe<&XKdAEiudhuSnelyPxDfGv{%{bM^kHaPSivieebqAb0cvQ`(?*T?^nogXOXZB TaCI&KTF&6<>gTe~DWM4f#FVdQ&MBb@056X!4FCWD literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/list2.png b/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/list2.png new file mode 100644 index 0000000000000000000000000000000000000000..a8892c26705ef10d5e0f905121e1578b192bb37a GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#ATAnVBAr_~T6C_wun*MYDdw-n! w`Qq$?AN~>+3Ac(E^kWQO{SAEI&vc2AK}}n>Z&UmF5TGswPgg&ebxsLQ01$f~l>h($ literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/loop.png b/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/loop.png new file mode 100644 index 0000000000000000000000000000000000000000..60b071a65f36b297a6963db693c2cdc91bfdc0b8 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#A6FglULn;{GUUC#XWFX-7(4&Jz z_<3XFDMM)F8wfL%VEDZ-`z1g4EFrrHuwDnzr*&zGdJx2C;84!(!zYT zZO7}$ljGP`UDT?4wWaRsPfhuDN`KMPUXH(97X>msA1+$mx$ktD_B0Mh_vgl^4Gj;d zKg>>fR<`4&(EhfbntP?njYpd9J8i1pv|nD?-)i6DtYmeOqd>PYc)I$ztaD0e0svsq BP)q;- literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/music.png b/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/music.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a64a6c83814d60bfc48b00e0ac57be62726ee2 GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AU7jwEAr*{UFFOhzG7xBeICX>2 zgaFQoO;buQSR7#DlVT5Xl65(BzVU#fapRuEU;jRr-`;B9ws{k8bm5z+AM>97mb3Nz zb8pe)5+=?bXLV0?w--k&O^S}6lxp-@u_PcQ$Wiso#EfujU#9HXG|yiXUhC|9$Q$xh u$NbgL_1%p`#+OM z4Wq(+x%q9}{~aIvt!LGUXFS2y_<4s3$Dj5_mK7chF)obL)g&?+tcBTR+_v^)NoMG> zN<{H|cs`xq~2%4pAMoKkhq3?U7N?{U$$Q(lL>Sb2A-+yyPMo7_48t`FnHEP000>X1ONa4Zs1Mm0002$Nkl2z;7>w!n0ZJPwr3be0BST@|NZ zQ1t$6D1bha7>0qtTMKRbu6CI^Zgc?LHzZWM2Ogb%hw_od+p!Bve8-G^bZokmZ>zLL ppQT(eFkjBfAv*OU*T4PSya0j~tWU#whb#a9002ovPDHLkV1hUFa-skL literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/volume.png b/cps/static/css/images/icomoon/entypo-25px-ffffff/PNG/volume.png new file mode 100644 index 0000000000000000000000000000000000000000..7c251ddd90aacbd87669785d31fb8e619cb58aa6 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%0wnVu_`U#AIi4<#Ar*{oPdkb>1xU06D*q9T zn?`jRDg-adJf)jXv{-^0_`+3S?b-HHjbP2Vi!`;=PmcaOs*&W*?9>?@$X N44$rjF6*2UngCC}I!6Ei literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/arrow.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/arrow.svg new file mode 100644 index 00000000..ea2a59ae --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/arrow.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/cart.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/cart.svg new file mode 100644 index 00000000..4b08a94b --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/cart.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/first.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/first.svg new file mode 100644 index 00000000..ac3cf397 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/first.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/last.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/last.svg new file mode 100644 index 00000000..4e3b833d --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/last.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list.svg new file mode 100644 index 00000000..fa2f7174 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list2.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list2.svg new file mode 100644 index 00000000..7cec36cb --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/list2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/loop.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/loop.svg new file mode 100644 index 00000000..79c89573 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/loop.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/music.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/music.svg new file mode 100644 index 00000000..9a6fd461 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/music.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/pause.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/pause.svg new file mode 100644 index 00000000..77ca91bb --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/pause.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/play.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/play.svg new file mode 100644 index 00000000..b98385f7 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/play.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/shuffle.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/shuffle.svg new file mode 100644 index 00000000..13a4007d --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/shuffle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/volume.svg b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/volume.svg new file mode 100644 index 00000000..9d708435 --- /dev/null +++ b/cps/static/css/images/icomoon/entypo-25px-ffffff/SVG/volume.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/icomoon/free-25px-000000/PNG/spinner.png b/cps/static/css/images/icomoon/free-25px-000000/PNG/spinner.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6d1a4c981f1e10b67ab47c593c4857d442500f GIT binary patch literal 293 zcmV+=0owkFP)P000>X1ONa4Zs1Mm0002&NklczO)0h6G?`V?@wM0LVl}c#R|wGBX3pMJ(vTGs2Ly zba7+MtNerxHp-#CNsKWV)I`QILh4J3^qCUb`v^UrMr6Ts0+tquCzdl4!hyiXl~0y{ raKe#TxO1uAn!rqla3hrpc>cpL+I4O;PO-yk00000NkvXXu0mjf> + + + + + + + diff --git a/cps/static/css/images/icomoon/free-25px-ffffff/PNG/spinner.png b/cps/static/css/images/icomoon/free-25px-ffffff/PNG/spinner.png new file mode 100644 index 0000000000000000000000000000000000000000..7db552586aa2d4bb4cea9f0d19b8b6c0c0525b33 GIT binary patch literal 299 zcmV+`0o4A9P)P000>X1ONa4Zs1Mm0002;Nklj8tJSYpf9C84&#pplQ>7Qip*Sg1fJ#ThCC=#LZLU>;YHEx~te z(V>kS$Ocet*I*rY&>RG(MSWLE)*wfOwILor%a{PVhm;zWkkLG(RA3b<)DI~M2nmaL z2+J`EH1QzYzWh`k6q2;v@Jt?3@?n#@N7mRX+ja8&zQiFqE9tgy7QCXro{{;*D6|H% xi`PecHEfk)<;(Qou4(UfO}0?R(W>JU{03gt8K_0A002ovPDHLkV1m)?d?Ek< literal 0 HcmV?d00001 diff --git a/cps/static/css/images/icomoon/free-25px-ffffff/SVG/spinner.svg b/cps/static/css/images/icomoon/free-25px-ffffff/SVG/spinner.svg new file mode 100644 index 00000000..eac5df26 --- /dev/null +++ b/cps/static/css/images/icomoon/free-25px-ffffff/SVG/spinner.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cps/static/css/images/patterns/credits.txt b/cps/static/css/images/patterns/credits.txt new file mode 100644 index 00000000..fb3d6673 --- /dev/null +++ b/cps/static/css/images/patterns/credits.txt @@ -0,0 +1,2 @@ +Patterns from subtlepatterns.com. +"If you need more, that's where to get 'em." \ No newline at end of file diff --git a/cps/static/css/images/patterns/pinstriped_suit_vertical.png b/cps/static/css/images/patterns/pinstriped_suit_vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..26d547a62f7ef619981c51fd92f115760ddc7cfa GIT binary patch literal 10828 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 F{XcKz)Ib0L literal 0 HcmV?d00001 diff --git a/cps/static/css/images/patterns/pool_table.png b/cps/static/css/images/patterns/pool_table.png new file mode 100644 index 0000000000000000000000000000000000000000..4183efa53b765f23288e7f93bd22f5b1fc29f9c5 GIT binary patch literal 40692 zcmV(#K;*xPP)Bw{}*X+$AjIxJ~LC1XD) zV?ZKcJt<{DB40ZtV?a82RXciCE^9|1UpppZKPF^AENez1UpydOIW1~NBVj%#WkDZX zI3r&>DQ80{WI!lpLK90WEo?^5aJSu5JB49ioSvD_hNE1mYAzwT)a!wvvI2u$jDP=+`WXp9TR9q4GZs!O z5=JBwN+>dLO&nD+6;3P{Pb?W#F*I{eAYVElTsj?EHa&b;6G$f*QZF4>H6vm_9ab|w zeOVw|I4WgAGI2~6QZE%uD;Q8N9auIqa!wUbEgV=hF>XsUaZMRhFd9=ZENev^SvDzW zLSalB2><|`qDe$SRCrz${o~TE+qNw1)$7cR+qP}nJjT6e zM2UzO^_z^EfD|oDq+0 zE2a%nJw~dp<jF?D%0k5^<8CJ$mF;w^E8p_7~Pc?XqKJ zdLH%Q&GDSP6 zArLSNv_7ZR!8#lVFy4P&*)kJE`D)Xu8v%}5LWtK2wayj2%GEfa!f+AOKX%B0;fKh) zbJ4YqV^XtvM#)tMhwn*yW9h=426n0-6COR$e7^3f4h>bB&t*t3o}==tek@gzOsF`9 zn%3H{s^`fTsb056k`3@89t-~@{4aQ{C1iD99KL*nDY50Vs5M=ai6rTn2U=Rs!TaQ` zn@K{F2ea`3mY}}hQ-0tGUX`1G8L%Q*?p4m*4(Kg%5Xo`BKl7^B)l`*c6(1-o`2rL~ z2TjDuD?76@PBefk^A_Hlnp=lTynpk1`h4NlK$sV@ttYal!@YWi;QQjl4N%0-%qo~K zs5!;~iM}F>E8rdbW(WPgwfgf5STQKOw@n+P!abi41HG(5=nfq=h_i)_QRmOh%c@ANnhsWDmVW+JWDUM8~ld8C5|B z0%`4PQ38Osz}T+00$^gj6PE_cadxHa!E3R$auxG_K^olNKCYu)w~Xr^k5J+|twh_h zwhW|Qvto2B4eLkA(XOj?aI{|NR&Sif&{Ib*Kn3hMEF@b3nDB9Haq1t;DHpqL9Wf~6 z<4Yltsa1iRf8>#%`I@1>+gc{}ngJZxU!GNj9r(b;NH{(s?O;-;qNi`k{^;*2B&j0; z_hI1nk)dxLz;I2qa+7<)N=*)*Q#;tGaf5LezdM(}bT3eqQbgkIt09jO5u~kODiTq- zwU)I?fAA6(PKm1{Z{V2eSd!KF7|@j)OxiBdPNJqJElL+HVZM@hsv#T%saCAUgd<&{ zDpD+YJt}AmsPsp_{_1hBNq~$2=77ySu^;pG^XD8prAMP?%XulVfxF9_2bX5YljAhw6)QdRueZod9}jfo z8o9=)%G@liXIh+Q)u}{SkCY@U%|j>vO{#@(l%lKBmNiw%S=GUzC5*G=EFsTi`>eWmdAa2zE|s`tb9BcHmSadp=>oZ?oPnm#twCX-)# zW%guUwB_htXIvTF$_5;TZ5>zMpK3>y{b-f}qghx6E#jmqmk6x?fOIEYXf?_VQp9QJnqyOk`RUIm4j-nDTPZ#&<8F4>g;K^4ijL1 zI%z~54xpqJ8AG4Ub1=!brAm*sK5MknoBy?>jDAQo?n?gtP8C!p$j_@69CeK6fZ~g< z0^=ze|0u4NAmXC=KJ9EGm4Lk_xpMHi-J)k6-0DI=QkEo#4sT+4}YI2pCXQ_|!*7 zusJv~dsT!wSKW7ueBMeg=7&()ff`u5nW^O4O-b?gxN<@0QRT;GmVsv?>qUnEOs{Ex z1$gd(Yb6)i=gy_KO+7);G=lZxw?A}i*CTM*`GtkXN{^e72{m(oniNKo#BV*lyG2}^ z8RcMhWNndP%g5{Ux9oQHi(ao(6Kyn&b1Ov+D)kiEN2znQuOL<-?uAb*UJ=aJH~nZN z@5gWbqSkJu&hhi!H*%ypD|JlJ)Km5D9oC?%2H;9@BUoY?5RfTuot5T<;PZzbv!a>> z169Z(6lSSLBV(ypp{4sCSC;m@I_cbF<0NZZX}`QGKL z6HoM};zG7d$6tMlwnrh$8s}RXwtcjE(Y`>Q&yuM{yAV@oTn#fHkyDEHd^#c`ZpDy&PW0O*SXjHtKVh~V zc9F;7w-N>z^!@9Yl0YeLz2#nnWcu?Lbac0rl(6o?oQ^y06{nAdmQ^A5sK_m%jw-|H zasFb5Y#*5;S4u-msxjXyAyB?4XpdvE%{zxATF>ogB?((T6I^`2*&oHe{XDj^ot$dIgplCvL4#3pR~))g zPYKnkuVfKdAS>%BM~&7i&~P|6ni<_bHq)S7>7NPZ#JS4l2ho$aGmTLrG-rdLI`7F- zE5iXRN258;@|2>U;pdlTMr+`Dp0RZU^eUH4ZZIqAMUpt0?_)rwrCq~T%1v#T0`>HE z`g36k=q2H0LAjlm<#1-R?9U8r*$$4}=4%itN2kQ1(TtQB{-vw2jIr!u0fVZyYkE_z zM?$DMYT7${iDVfG* zQFBH)-&7-8zXcMOmJV^(e04$8(56^NBDA80+SYPK4vJn!M$1%P*oKGZ>wLU5W3WOw zp2LWN!qn`siPqz_UOag?r*}j^h?bn=>&CT><9raa``@}<(QlV-!65^3qD;U*!}|r` z^Ufa1qyvSW+sVjnK*Lf@st^m6d42u#kt`mNf78L zX2jxD4CWG++HR-RbF&()7xKfcU6)$PJ(ULZ_{c-rhEPWxJ}Y3iG+-hKD%zR#I(s83 zP;yJ}^En&X5<+xh(x0}4*AqV1f-k)TwVORF* zG*`$m^2e4b^wxXUaU^X-nHtrJNOS(P8TmS|EEP(N^1pp|o3G@DUbzP@Uc+>GL1;ix z;=*}+t6|X!GJvR;A-ilwdTmmH${8|;r@EHIF)KRA|9cTwj7{$P@D3lWCtpFpjZE$d=N3Tzj!f8N#?f?N5 zVWk*5VgBA7m2o7w-zQ@d#NIL>f-5^D9B8aRo*dDfvZUe7v4 zxPtNe2mV3hItO7bIc~RGbM7jU(U}}`i)U?wst{rOUW>^>!hhiZ@lUoUtQIn6-q*h4 zv6oiPPLCfw6RE*k5Cz=M6Ns$m90FiQ>vsN$MV#sYaHevamIKU6LvhT3s`i_rLGq6o zQwyd{w>O%Ibn>biR$w@th;G%4@VzUN5=$l|69*+TN%hhn0>6B-DkpvgyPlO)+rLUiMFZvl#c| zo}?9YoNBO7tVU^qMLQWe!G$Hy(KTrh%8u?tM9G@$2wHNj{le&vM|I7inz2H`DLnCN z#HLN9j&G<_%XUU)=Ab3ZVI75>@@0hee4{Etu|Ih`wZ$QiB0}$#CGD7LV2sb?k#n4_ z)*ih)zd7&EqiqcTk*aY_RF_OmU8p~7I6}!S6Dh0;a6A+2lf!Fnwpn>xRRG|{iD?}5 zYQW};F3HHgBV1dA&G-3G7eccAwF^?v-E*Y(m2yUXoFw9OZ51iHN?i`eOm*yhf1lKN zuiJ-sReIb$)0Gi%Ofd-5^q5d+MA7`Yjx3-SNiEuBsO^-dk9wgUU)Od-` zWN$q8&m#;9KpHY9hogujIg3qitOgGb@R3jCrmvOi8^ZNCfBVN}V^0I^w&^cJM!8Z& zM{smcdRHNlAl*2Xxp(pW^iR%m2gaECCU zI$9?r(_3T$)U#atI`a1ifB&hUEzFIN)pmW~7=Q`18tb2OA z(3uX*#N$|T70AlSNjP}CNbkfIsao}gC9dr{bFQA>9%uNlu|IjxEm)EaJ*o&4O7~x3 z8fyUN%!bD@D%?!apDrBbig(z0&lr|IF_U$x{?;hAImhe$u8oJ{ebzdDfz&qe1a!b7 z61K#TEQS#NJNSJ=%7K{~STS+ysN8%83HMYh(%F4h&;J3NkTZ;5hBbV884HQasqIL z|E$f>8m0O?SkQP(64I*eYHJsV+k@r=t@}N?szOpb8|9k~;G7$<%*rW!Z{p63po-K3 zE1tRaT-cp5vwtoKHbydju3F_h&2vcGZiKyjzUpGB@docqiq8}$O`guVq-bZRDwlzF z8(I&vHg>4bLp3|(1}=h+AA$MGX2F(!r z#O-@_O|NvFrCIsNAZ9Qnr3%c9HE_udYA#d-$khnzekZ0dGT^!-c!W40wOr&yzxn%4 z@YMOhU{F8s5o1naN7Lx$SfQPIOkh8Dv+;RfbtV||a3i(r@&mUH8TdRUeHFjAhGbC3 z#jR1l=^9~m=~c;(gs`ZT;C2G*^)L$JTq&!}|M%HIT;J?$sx12U)`^)TFQNKe3B(O| zk1?=!gpUVOW4>(u%dv&oS*oA64KQDHS}wYe-b!y#u6#Zojs5;wNdgnePC1{qb``9s zip+J~>Acubl<@lT(UAKYHDm2X4ZP>xnd$lSnC~?-eU1#GA4_U|Ca`>#j03h?gTxo4))MCZ^Qdv-NRkr4^@vuk zkE?IBcS??>WbNN?C|7>ViB3Q2c#eE4cs7k(;=EURik<}{cNOvAY}c*l{&kfmL*8b& zC~vK74f9rLCgD1tt33L3WBR(jG3o8s3y!$~? z3F|i*DY<`JbriWS2*I6om1XLuma-OYkj~n_55>Ck-t#ztqXOx_> zP>!w3vHXofp2@x8dM087N@oGM&QiuvqxHpUNtnI$#uG7aFhti2GCt+pvaMvKa(q^5 z1?CV_cOITM<=iriqnumjz2^3I#_iE&tyok9fuTkr;oL#&tvwefiGD&IUM?o0)zZ}S zY(+3{IPc=Py&2^OW+Ig7TMRc@_L22u0Ae1?U4z}&&o|X2ZL2Axl$de*0k7YAQBX_0 zX^uEL=Tbx+fslMs~~Y^NvBW4mYdq$|8=2`;bwSHW&9a zr&Qlc|GJqSAWX~wbaN*eQ}*|Wq83yv%%7{yCxn&{<7ZW@#6Gdsd~3G`X$Y{8iwjsJRB!;kU1_Ii z*vO(^z_@>#g7}~QFFr~cPp~7&SQ3v18CEJy1gMNBo~7Ay&sW`R-gF+oXK%`u#<45z zP7R<9Rn4Tc&(8sck@9MMPnDru1cC&R-vyn>_Cr`MNP#b+%3q$2y+w$wDL41Nc}n=8M%Kn z-MlhDU_N=(l>P`CqawHbQ_HQXxWcYs6jG7&cI)Taj6i#+Za4L~h!8YNYuwnSXPHz= zneoGQsfGtFp?*|mE3AMp3%7)1E7GZ z63+eLjJ0o}lq2MA%Qffh23EW#8Nk=wZi6KO9rT#F_E2A(sOU*Bq#

>p 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 literal 0 HcmV?d00001 diff --git a/cps/static/css/images/patterns/rubber_grip.png b/cps/static/css/images/patterns/rubber_grip.png new file mode 100644 index 0000000000000000000000000000000000000000..076b9606f513440b964bb4b5d5d3d78a175eabe3 GIT binary patch literal 123 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 Eg4qg2P5=M^ literal 0 HcmV?d00001 diff --git a/cps/static/css/images/patterns/wood_pattern.png b/cps/static/css/images/patterns/wood_pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..47903e43b1e84e5826fd2026473d6dc798a826f0 GIT binary patch literal 103832 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 z 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

pdANuD<9Sl`$~z@ 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 literal 0 HcmV?d00001 diff --git a/cps/static/css/images/patterns/wood_pattern_dark.png b/cps/static/css/images/patterns/wood_pattern_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..1f5fc7fef391f38156d453489d8ea81e1f47a59f GIT binary patch literal 33072 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∾kbY6 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"); + /* IE 8 inverse filter, may only match pure black/white */ + /* filter: xray; */ + /* pending W3 standard */ + filter: invert(1); + /* not you, IE < 10. */ + filter: none\9; +} + +.sm2-bar-ui .bd a { + text-decoration: none; +} + +.sm2-bar-ui .bd .sm2-button-element:hover { + background-color: rgba(0,0,0,0.1); + background-image: url(../images/black-10.png); + background-image: none, none; +} + +.sm2-bar-ui .bd .sm2-button-element:active { + background-color: rgba(0,0,0,0.25); + background-image: url(../images/black-25.png); + background-image: none, none; +} + +.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active .sm2-inline-button, +.sm2-bar-ui .bd .active .sm2-inline-button/*, +.sm2-bar-ui.playlist-open .sm2-menu a */{ + -ms-transform: scale(0.9); + -webkit-transform: scale(0.9); + -webkit-transform-origin: 50% 50%; + /* firefox doesn't scale quite right. */ + transform: scale(0.9); + transform-origin: 50% 50%; + /* firefox doesn't scale quite right. */ + -moz-transform: none; +} + +.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:hover, +.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active, +.sm2-bar-ui .bd .active { + background-color: rgba(0,0,0,0.1); + background-image: url(../images/black-10.png); + background-image: none, none; +} + +.sm2-bar-ui .bd .sm2-extra-controls .sm2-button-element:active { + /* box shadow is excessive on smaller elements. */ + box-shadow: none; +} + +.sm2-bar-ui { + /* base font size */ + font-size: 15px; + text-shadow: none; +} + +.sm2-bar-ui .sm2-inline-element { + position: relative; + display: inline-block; + vertical-align: middle; + padding: 0px; + overflow: hidden; +} + +.sm2-bar-ui .sm2-inline-element, +.sm2-bar-ui .sm2-button-element .sm2-button-bd { + position: relative; + /** + * .sm2-button-bd exists because of a Firefox bug from 2000 + * re: nested relative / absolute elements inside table cells. + * https://bugzilla.mozilla.org/show_bug.cgi?id=63895 + */ +} + +.sm2-bar-ui .sm2-inline-element, +.sm2-bar-ui .sm2-button-element .sm2-button-bd { + /** + * if you play with UI width/height, these are the important ones. + * NOTE: match these values if you want square UI buttons. + */ + min-width: 2.8em; + min-height: 2.8em; +} + +.sm2-bar-ui .sm2-inline-button { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; +} + +.sm2-bar-ui .sm2-extra-controls .bd { + /* don't double-layer. */ + background-image: none; + background-color: rgba(0,0,0,0.15); +} + +.sm2-bar-ui .sm2-extra-controls .sm2-inline-element { + width: 25px; /* bare minimum */ + min-height: 1.75em; + min-width: 2.5em; +} + +.sm2-bar-ui .sm2-inline-status { + line-height: 100%; + /* how much to allow before truncating song artist / title with ellipsis */ + display: inline-block; + min-width: 200px; + max-width: 20em; + /* a little more spacing */ + padding-left: 0.75em; + padding-right: 0.75em; +} + +.sm2-bar-ui .sm2-inline-element { + /* extra-small em scales up nicely, vs. 1px which gets fat */ + border-right: 0.075em dotted #666; /* legacy */ + border-right: 0.075em solid rgba(0,0,0,0.1); +} + +.sm2-bar-ui .sm2-inline-element.noborder { + border-right: none; +} + +.sm2-bar-ui .sm2-inline-element.compact { + min-width: 2em; + padding: 0px 0.25em; +} + +.sm2-bar-ui .sm2-inline-element:first-of-type { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + overflow: hidden; +} + +.sm2-bar-ui .sm2-inline-element:last-of-type { + border-right: none; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.sm2-bar-ui .sm2-inline-status a:hover { + background-color: transparent; + text-decoration: underline; +} + +.sm2-inline-time, +.sm2-inline-duration { + display: table-cell; + width: 1%; + font-size: 75%; + line-height: 0.9em; + min-width: 2em; /* if you have sounds > 10:00 in length, make this bigger. */ + vertical-align: middle; +} + +.sm2-bar-ui .sm2-playlist { + position: relative; + height: 1.45em; +} + +.sm2-bar-ui .sm2-playlist-target { + /* initial render / empty case */ + position: relative; + min-height: 1em; +} + +.sm2-bar-ui .sm2-playlist ul { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + list-style-type: none; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.sm2-bar-ui p, +.sm2-bar-ui .sm2-playlist ul, +.sm2-bar-ui .sm2-playlist ul li { + margin: 0px; + padding: 0px; +} + +.sm2-bar-ui .sm2-playlist ul li { + position: relative; +} + +.sm2-bar-ui .sm2-playlist ul li, +.sm2-bar-ui .sm2-playlist ul li a { + position: relative; + display: block; + /* prevent clipping of characters like "g" */ + height: 1.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + +.sm2-row { + position: relative; + display: table-row; +} + +.sm2-progress-bd { + /* spacing between progress track/ball and time (position) */ + padding: 0px 0.8em; +} + +.sm2-progress .sm2-progress-track, +.sm2-progress .sm2-progress-ball, +.sm2-progress .sm2-progress-bar { + position: relative; + width: 100%; + height: 0.65em; + border-radius: 0.65em; +} + +.sm2-progress .sm2-progress-bar { + /* element which follows the progres "ball" as it moves */ + position: absolute; + left: 0px; + top: 0px; + width: 0px; + background-color: rgba(0,0,0,0.33); + background-image: url(../images/black-33.png); + background-image: none, none; +} + +.volume-shade, +.playing .sm2-progress .sm2-progress-track, +.paused .sm2-progress .sm2-progress-track { + cursor: pointer; +} + +.playing .sm2-progress .sm2-progress-ball { + cursor: -moz-grab; + cursor: -webkit-grab; + cursor: grab; +} + +.sm2-progress .sm2-progress-ball { + position: absolute; + top: 0px; + left: 0px; + width: 1em; + height: 1em; + margin: -0.2em 0px 0px -0.5em; + width: 14px; + height: 14px; + margin: -2px 0px 0px -7px; + width: 0.9333em; + height: 0.9333em; + margin: -0.175em 0px 0px -0.466em; + background-color: #fff; + padding: 0px; +/* + z-index: 1; +*/ + transition: transform 0.15s ease-in-out; +} + +/* +.sm2-bar-ui.dark-text .sm2-progress .sm2-progress-ball { + background-color: #000; +} +*/ + +.sm2-progress .sm2-progress-track { + background-color: rgba(0,0,0,0.4); + background-image: url(../images/black-33.png); /* legacy */ + background-image: none, none; /* modern browsers */ +} + +/* scrollbar rules have to be separate, browsers not supporting this syntax will skip them when combined. */ +.sm2-playlist-wrapper ul::-webkit-scrollbar-track { + background-color: rgba(0,0,0,0.4); +} + +.playing.grabbing .sm2-progress .sm2-progress-track, +.playing.grabbing .sm2-progress .sm2-progress-ball { + cursor: -moz-grabbing; + cursor: -webkit-grabbing; + cursor: grabbing; +} + +.sm2-bar-ui.grabbing .sm2-progress .sm2-progress-ball { + -webkit-transform: scale(1.15); + transform: scale(1.15); +} + +.sm2-inline-button { + background-position: 50% 50%; + background-repeat: no-repeat; + /* hide inner text */ + line-height: 10em; + /** + * image-rendering seems to apply mostly to Firefox in this case. Use with caution. + * https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#Browser_compatibility + */ + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + -ms-interpolation-mode: nearest-neighbor; + -ms-interpolation-mode: bicubic; +} + +.sm2-icon-play-pause, +.sm2-icon-play-pause:hover, +.paused .sm2-icon-play-pause:hover { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/play.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/play.svg); + background-size: 67.5%; + background-position: 40% 53%; +} + +.playing .sm2-icon-play-pause { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/pause.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/pause.svg); + background-size: 57.6%; + background-position: 50% 53%; +} + +.sm2-volume-control { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/volume.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/volume.svg); +} + +.sm2-volume-control, +.sm2-volume-shade { + background-position: 42% 50%; + background-size: 56%; +} + +.volume-shade { + filter: alpha(opacity=33); /* <= IE 8 */ + opacity: 0.33; +/* -webkit-filter: invert(1);*/ + background-image: url(../images/icomoon/entypo-25px-000000/PNG/volume.png); + background-image: none, url(../images/icomoon/entypo-25px-000000/SVG/volume.svg); +} + +.sm2-icon-menu { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/list2.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/list2.svg); + background-size: 58%; + background-position: 54% 51%; +} + +.sm2-icon-previous { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/first.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/first.svg); +} + +.sm2-icon-next { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/last.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/last.svg); +} + +.sm2-icon-previous, +.sm2-icon-next { + background-size: 49.5%; + background-position: 50% 50%; +} + + +.sm2-extra-controls .sm2-icon-previous, +.sm2-extra-controls .sm2-icon-next { + backgound-size: 53%; +} + +.sm2-icon-shuffle { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/shuffle.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/shuffle.svg); + background-size: 45%; + background-position: 50% 50%; +} + +.sm2-icon-repeat { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/loop.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/loop.svg); + background-position: 50% 43%; + background-size: 54%; +} + +.sm2-extra-controls .sm2-icon-repeat { + background-position: 50% 45%; +} + +.sm2-playlist-wrapper ul li .sm2-row { + display: table; + width: 100%; +} + +.sm2-playlist-wrapper ul li .sm2-col { + display: table-cell; + vertical-align: top; + /* by default, collapse. */ + width: 0%; +} + +.sm2-playlist-wrapper ul li .sm2-col.sm2-wide { + /* take 100% width. */ + width: 100%; +} + +.sm2-playlist-wrapper ul li .sm2-icon { + display: inline-block; + overflow: hidden; + width: 2em; + color: transparent !important; /* hide text */ + white-space: nowrap; /* don't let text affect height */ + padding-left: 0px; + padding-right: 0px; + text-indent: 2em; /* IE 8, mostly */ +} + +.sm2-playlist-wrapper ul li .sm2-icon, +.sm2-playlist-wrapper ul li:hover .sm2-icon, +.sm2-playlist-wrapper ul li.selected .sm2-icon { + background-size: 55%; + background-position: 50% 50%; + background-repeat: no-repeat; +} + +.sm2-playlist-wrapper ul li .sm2-col { + /* sibling table cells get borders. */ + border-right: 1px solid rgba(0,0,0,0.075); +} + +.sm2-playlist-wrapper ul li.selected .sm2-col { + border-color: rgba(255,255,255,0.075); +} + +.sm2-playlist-wrapper ul li .sm2-col:last-of-type { + border-right: none; +} + +.sm2-playlist-wrapper ul li .sm2-cart, +.sm2-playlist-wrapper ul li:hover .sm2-cart, +.sm2-playlist-wrapper ul li.selected .sm2-cart { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/cart.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/cart.svg); + /* slight alignment tweak */ + background-position: 48% 50%; +} + +.sm2-playlist-wrapper ul li .sm2-music, +.sm2-playlist-wrapper ul li:hover .sm2-music, +.sm2-playlist-wrapper ul li.selected .sm2-music { + background-image: url(../images/icomoon/entypo-25px-ffffff/PNG/music.png); + background-image: none, url(../images/icomoon/entypo-25px-ffffff/SVG/music.svg); +} + +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-cart, +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover .sm2-cart, +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-cart { + background-image: url(../images/icomoon/entypo-25px-000000/PNG/cart.png); + background-image: none, url(../images/icomoon/entypo-25px-000000/SVG/cart.svg); +} + +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-music, +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover .sm2-music, +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-music { + background-image: url(../images/icomoon/entypo-25px-000000/PNG/music.png); + background-image: none, url(../images/icomoon/entypo-25px-000000/SVG/music.svg); +} + + +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li .sm2-col { + border-left-color: rgba(0,0,0,0.15); +} + +.sm2-playlist-wrapper ul li .sm2-icon:hover { + background-color: rgba(0,0,0,0.33); +} + +.sm2-bar-ui .sm2-playlist-wrapper ul li .sm2-icon:hover { + background-color: rgba(0,0,0,0.45); +} + +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li.selected .sm2-icon:hover { + background-color: rgba(255,255,255,0.25); + border-color: rgba(0,0,0,0.125); +} + +.sm2-progress-ball .icon-overlay { + position: absolute; + width: 100%; + height: 100%; + top: 0px; + left: 0px; + background: none, url(../image/icomoon/free-25px-000000/SVG/spinner.svg); + background-size: 72%; + background-position: 50%; + background-repeat: no-repeat; + display: none; +} + +.playing.buffering .sm2-progress-ball .icon-overlay { + display: block; + -webkit-animation: spin 0.6s linear infinite; + animation: spin 0.6s linear infinite; +} + +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} + +@-moz-keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.sm2-element ul { + font-size: 95%; + list-style-type: none; +} + +.sm2-element ul, +.sm2-element ul li { + margin: 0px; + padding: 0px; +} + +.bd.sm2-playlist-drawer { + /* optional: absolute positioning */ + /* position: absolute; */ + z-index: 3; + border-radius: 0px; + width: 100%; + height: 0px; + border: none; + background-image: none; + display: block; + overflow: hidden; + transition: height 0.2s ease-in-out; +} + +.sm2-bar-ui.fixed .bd.sm2-playlist-drawer, +.sm2-bar-ui.bottom .bd.sm2-playlist-drawer { + position: absolute; +} + +.sm2-bar-ui.fixed .sm2-playlist-wrapper, +.sm2-bar-ui.bottom .sm2-playlist-wrapper { + padding-bottom: 0px; +} + +.sm2-bar-ui.fixed .bd.sm2-playlist-drawer, +.sm2-bar-ui.bottom .bd.sm2-playlist-drawer { + /* show playlist on top */ + bottom: 2.8em; +} + +.sm2-bar-ui .bd.sm2-playlist-drawer { + opacity: 0.5; + /* redraw fix for Chrome, background color doesn't always draw when playlist drawer open. */ + transform: translateZ(0); +} + +/* experimental, may not perform well. */ +/* +.sm2-bar-ui .bd.sm2-playlist-drawer a { + -webkit-filter: blur(5px); +} +*/ + +.sm2-bar-ui.playlist-open .bd.sm2-playlist-drawer { + height: auto; + opacity: 1; +} + +.sm2-bar-ui.playlist-open .bd.sm2-playlist-drawer a { + -webkit-filter: none; /* blur(0px) was still blurred on retina displays, as of 07/2014 */ +} + +.sm2-bar-ui.fixed.playlist-open .bd.sm2-playlist-drawer .sm2-playlist-wrapper, +.sm2-bar-ui.bottom.playlist-open .bd.sm2-playlist-drawer .sm2-playlist-wrapper { + /* extra padding when open */ + padding-bottom: 0.5em; + box-shadow: none; +} + +.sm2-bar-ui .bd.sm2-playlist-drawer { + transition: all 0.2s ease-in-out; + transition-property: transform, height, opacity, background-color, -webkit-filter; +} + +.sm2-bar-ui .bd.sm2-playlist-drawer a { + transition: -webkit-filter 0.2s ease-in-out; +} + +.sm2-bar-ui .bd.sm2-playlist-drawer .sm2-inline-texture { + /* negative offset for height of top bar, so background is seamless. */ + background-position: 0px -2.8em; +} + +.sm2-box-shadow { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + box-shadow: inset 0px 1px 6px rgba(0,0,0,0.15); +} + +.sm2-playlist-wrapper { + position: relative; + padding: 0.5em 0.5em 0.5em 0.25em; + background-image: none, none; +} + +.sm2-playlist-wrapper ul { + max-height: 9.25em; + overflow: auto; +} + +.sm2-playlist-wrapper ul li { + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +.sm2-playlist-wrapper ul li:nth-child(odd) { + background-color: rgba(255,255,255,0.03); +} + +.sm2-playlist-wrapper ul li a { + display: block; + padding: 0.5em 0.25em 0.5em 0.75em; + margin-right: 0px; + font-size: 90%; + vertical-align: middle; +} + +.sm2-playlist-wrapper ul li a.sm2-exclude { + display: inline-block; +} + +.sm2-playlist-wrapper ul li a.sm2-exclude .label { + font-size: 95%; + line-height: 1em; + margin-left: 0px; + padding: 2px 4px; +} + +.sm2-playlist-wrapper ul li:hover a { + background-color: rgba(0,0,0,0.20); + background-image: url(../images/black-20.png); + background-image: none, none; +} + +.sm2-bar-ui.dark-text .sm2-playlist-wrapper ul li:hover a { + background-color: rgba(255,255,255,0.1); + background-image: url(../images/black-10.png); + background-image: none, none; +} + +.sm2-playlist-wrapper ul li.selected a { + background-color: rgba(0,0,0,0.25); + background-image: url(../images/black-20.png); + background-image: none, none; +} + +.sm2-bar-ui.dark-text ul li.selected a { + background-color: rgba(255,255,255,0.1); + background-image: url(../images/black-10.png); + background-image: none, none; +} + +.sm2-bar-ui .disabled { + filter: alpha(opacity=33); /* <= IE 8 */ + opacity: 0.33; +} + +.sm2-bar-ui .bd .sm2-button-element.disabled:hover { + background-color: transparent; +} + +.sm2-bar-ui .active, +/*.sm2-bar-ui.playlist-open .sm2-menu,*/ +.sm2-bar-ui.playlist-open .sm2-menu:hover { + /* depressed / "on" state */ + box-shadow: inset 0px 0px 2px rgba(0,0,0,0.1); + background-image: none; +} + +.firefox-fix { + /** + * This exists because of a Firefox bug from 2000 + * re: nested relative / absolute elements inside table cells. + * https://bugzilla.mozilla.org/show_bug.cgi?id=63895 + */ + position: relative; + display: inline-block; + width: 100%; + height: 100%; +} + +/* some custom scrollbar trickery, where supported */ + +.sm2-playlist-wrapper ul::-webkit-scrollbar { + width: 10px; +} + +.sm2-playlist-wrapper ul::-webkit-scrollbar-track { + background: rgba(0,0,0,0.33); + border-radius: 10px; +} + +.sm2-playlist-wrapper ul::-webkit-scrollbar-thumb { + border-radius: 10px; + background: #fff; +} + +.sm2-extra-controls { + font-size: 0px; + text-align: center; +} + +.sm2-bar-ui .label { + position: relative; + display: inline-block; + font-size: 0.7em; + margin-left: 0.25em; + vertical-align: top; + background-color: rgba(0,0,0,0.25); + border-radius: 3px; + padding: 0px 3px; + box-sizing: padding-box; +} + +.sm2-bar-ui.dark-text .label { + background-color: rgba(0,0,0,0.1); + background-image: url(../images/black-10.png); + background-image: none, none; +} + +.sm2-bar-ui .sm2-playlist-drawer .label { + font-size: 0.8em; + padding: 0px 3px; +} + +/* --- full width stuff --- */ + +.sm2-bar-ui .sm2-inline-element { + display: table-cell; +} + +.sm2-bar-ui .sm2-inline-element { + /* collapse */ + width: 1%; +} + +.sm2-bar-ui .sm2-inline-status { + /* full width */ + width: 100%; + min-width: 100%; + max-width: 100%; +} + +.sm2-bar-ui > .bd { + width: 100%; +} + +.sm2-bar-ui .sm2-playlist-drawer { + /* re-hide playlist */ + display: block; + overflow: hidden; +} diff --git a/cps/static/css/listen.css b/cps/static/css/listen.css new file mode 100644 index 00000000..b08cc33c --- /dev/null +++ b/cps/static/css/listen.css @@ -0,0 +1,114 @@ +.sm2-bar-ui { + font-size: 20px; + } + + .sm2-bar-ui.compact { + max-width: 90%; + } + + .sm2-progress .sm2-progress-ball { + width: .5333em; + height: 1.9333em; + border-radius: 0em; + } + + .sm2-progress .sm2-progress-track { + height: 0.15em; + background: white; + } + + .sm2-bar-ui .sm2-main-controls, + .sm2-bar-ui .sm2-playlist-drawer { + background-color: transparent; + } + + .sm2-bar-ui .sm2-inline-texture { + background: transparent; + } + + .rating .glyphicon-star { + color: gray; + } + + .rating .glyphicon-star.good { + color: white; + } + + body { + overflow: hidden; + background: #272B30; + color: #aaa; + } + + #main { + position: absolute; + width: 100%; + height: 100%; + } + + #area { + width: 80%; + height: 80%; + margin: 5% auto; + max-width: 1250px; + } + + #area iframe { + border: none; + } + + #prev { + left: 40px; + } + + #next { + right: 40px; + } + + .arrow { + position: absolute; + top: 50%; + margin-top: -32px; + font-size: 64px; + color: #E2E2E2; + font-family: arial, sans-serif; + font-weight: bold; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } + + .arrow:hover { + color: #777; + } + + .arrow:active { + color: #000; + } + + xmp, + pre, + plaintext { + display: block; + font-family: -moz-fixed; + white-space: pre; + margin: 1em 0; + } + + #area { + overflow: hidden; + } + + pre { + white-space: pre-wrap; + word-wrap: break-word; + font-family: -moz-fixed; + column-count: 2; + -webkit-columns: 2; + -moz-columns: 2; + column-gap: 20px; + -moz-column-gap: 20px; + -webkit-column-gap: 20px; + position: relative; + } \ No newline at end of file diff --git a/cps/static/css/style.css b/cps/static/css/style.css index c4a9b502..1880207a 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -1,3 +1,6 @@ + +.tooltip.bottom .tooltip-inner{font-size:13px;font-family:Open Sans Semibold,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;padding:3px 10px;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 4px 10px 0 rgba(0,0,0,.35);box-shadow:0 4px 10px 0 rgba(0,0,0,.35);opacity:1;white-space:nowrap;margin-top:-16px!important;line-height:1.71428571;color:#ddd} + @font-face { font-family: 'Grand Hotel'; font-style: normal; @@ -136,7 +139,20 @@ input.pill:not(:checked) + label .glyphicon { .editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;} .editable-submit { margin-bottom: 0px !important;} +.filterheader { margin-bottom: 20px; } + .modal-body .comments { max-height:300px; overflow-y: auto; } + +div.log { + font-family: Courier New; + font-size: 12px; + box-sizing: border-box; + height: 700px; + overflow-y: scroll; + border: 1px solid #ddd; + white-space: nowrap; + padding: 0.5em; +} diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js index cfc7bd40..13e1d183 100644 --- a/cps/static/js/archive/archive.js +++ b/cps/static/js/archive/archive.js @@ -24,40 +24,45 @@ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE * USE OR OTHER DEALINGS IN THE SOFTWARE. */ - /* ******************************************************************** - * Alphanum sort() function version - case insensitive - * - Slower, but easier to modify for arrays of objects which contain - * string properties - * - */ +/* ******************************************************************** +* Alphanum sort() function version - case insensitive +* - Slower, but easier to modify for arrays of objects which contain +* string properties +* +*/ +/* exported alphanumCase */ + + function alphanumCase(a, b) { - function chunkify(t) { - var tz = new Array(); - var x = 0, y = -1, n = 0, i, j; + function chunkify(t) { + var tz = new Array(); + var x = 0, y = -1, n = 0, i, j; - while (i = (j = t.charAt(x++)).charCodeAt(0)) { - var m = (i == 46 || (i >=48 && i <= 57)); - if (m !== n) { - tz[++y] = ""; - n = m; - } - tz[y] += j; + while (i = (j = t.charAt(x++)).charCodeAt(0)) { + var m = (i === 46 || (i >= 48 && i <= 57)); + if (m !== n) { + tz[++y] = ""; + n = m; + } + tz[y] += j; + } + return tz; } - return tz; - } - var aa = chunkify(a.filename.toLowerCase()); - var bb = chunkify(b.filename.toLowerCase()); + var aa = chunkify(a.filename.toLowerCase()); + var bb = chunkify(b.filename.toLowerCase()); - for (x = 0; aa[x] && bb[x]; x++) { - if (aa[x] !== bb[x]) { - var c = Number(aa[x]), d = Number(bb[x]); - if (c == aa[x] && d == bb[x]) { - return c - d; - } else return (aa[x] > bb[x]) ? 1 : -1; + for (var x = 0; aa[x] && bb[x]; x++) { + if (aa[x] !== bb[x]) { + var c = Number(aa[x]), d = Number(bb[x]); + if (c === aa[x] && d === bb[x]) { + return c - d; + } else { + return (aa[x] > bb[x]) ? 1 : -1; + } + } } - } - return aa.length - bb.length; + return aa.length - bb.length; } // =========================================================================== diff --git a/cps/static/js/archive/unzip.js b/cps/static/js/archive/unzip.js index a4cec8d0..886f4b80 100644 --- a/cps/static/js/archive/unzip.js +++ b/cps/static/js/archive/unzip.js @@ -74,8 +74,8 @@ var ZipLocalFile = function(bstream) { this.extraField = null; if (this.extraFieldLength > 0) { - this.extraField = bstream.readString(this.extraFieldLength); - info(" extra field=" + this.extraField); + this.extraField = bstream.readString(this.extraFieldLength); + info(" extra field=" + this.extraField); } // read in the compressed data diff --git a/cps/static/js/filter_list.js b/cps/static/js/filter_list.js new file mode 100644 index 00000000..0610bb86 --- /dev/null +++ b/cps/static/js/filter_list.js @@ -0,0 +1,195 @@ +/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) + * Copyright (C) 2018 OzzieIsaacs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var direction = 0; // Descending order +var sort = 0; // Show sorted entries + +$("#sort_name").click(function() { + var count = 0; + var index = 0; + var store; + // Append 2nd half of list to first half for easier processing + var cnt = $("#second").contents(); + $("#list").append(cnt); + // Count no of elements + var listItems = $("#list").children(".row"); + var listlength = listItems.length; + // check for each element if its Starting character matches + $(".row").each(function() { + if ( sort === 1) { + store = this.attributes["data-name"]; + } else { + store = this.attributes["data-id"]; + } + $(this).find("a").html(store.value); + if ($(this).css("display") !== "none") { + count++; + } + }); + /*listItems.sort(function(a,b){ + return $(a).children()[1].innerText.localeCompare($(b).children()[1].innerText) + });*/ + // Find count of middle element + if (count > 20) { + var middle = parseInt(count / 2) + (count % 2); + // search for the middle of all visibe elements + $(".row").each(function() { + index++; + if ($(this).css("display") !== "none") { + middle--; + if (middle <= 0) { + return false; + } + } + }); + // Move second half of visible elements + $("#second").append(listItems.slice(index, listlength)); + } + sort = (sort + 1) % 2; +}); + +$("#desc").click(function() { + if (direction === 0) { + return; + } + var index = 0; + var list = $("#list"); + var second = $("#second"); + // var cnt = ; + list.append(second.contents()); + var listItems = list.children(".row"); + var reversed, elementLength, middle; + reversed = listItems.get().reverse(); + elementLength = reversed.length; + // Find count of middle element + var count = $(".row:visible").length; + if (count > 20) { + middle = parseInt(count / 2) + (count % 2); + + //var middle = parseInt(count / 2) + (count % 2); + // search for the middle of all visible elements + $(reversed).each(function() { + index++; + if ($(this).css("display") !== "none") { + middle--; + if (middle <= 0) { + return false; + } + } + }); + + list.append(reversed.slice(0, index)); + second.append(reversed.slice(index, elementLength)); + } else { + list.append(reversed.slice(0, elementLength)); + } + direction = 0; +}); + + +$("#asc").click(function() { + if (direction === 1) { + return; + } + var index = 0; + var list = $("#list"); + var second = $("#second"); + list.append(second.contents()); + var listItems = list.children(".row"); + var reversed = listItems.get().reverse(); + var elementLength = reversed.length; + + // Find count of middle element + var count = $(".row:visible").length; + if (count > 20) { + var middle = parseInt(count / 2) + (count % 2); + + //var middle = parseInt(count / 2) + (count % 2); + // search for the middle of all visible elements + $(reversed).each(function() { + index++; + if ($(this).css("display") !== "none") { + middle--; + if (middle <= 0) { + return false; + } + } + }); + + // middle = parseInt(elementLength / 2) + (elementLength % 2); + + list.append(reversed.slice(0, index)); + second.append(reversed.slice(index, elementLength)); + } else { + list.append(reversed.slice(0, elementLength)); + } + direction = 1; +}); + +$("#all").click(function() { + var cnt = $("#second").contents(); + $("#list").append(cnt); + // Find count of middle element + var listItems = $("#list").children(".row"); + var listlength = listItems.length; + var middle = parseInt(listlength / 2) + (listlength % 2); + // go through all elements and make them visible + listItems.each(function() { + $(this).show(); + }); + // Move second half of all elements + if (listlength > 20) { + $("#second").append(listItems.slice(middle, listlength)); + } +}); + +$(".char").click(function() { + var character = this.innerText; + var count = 0; + var index = 0; + // Append 2nd half of list to first half for easier processing + var cnt = $("#second").contents(); + $("#list").append(cnt); + // Count no of elements + var listItems = $("#list").children(".row"); + var listlength = listItems.length; + // check for each element if its Starting character matches + $(".row").each(function() { + if (this.attributes["data-id"].value.charAt(0).toUpperCase() !== character) { + $(this).hide(); + } else { + $(this).show(); + count++; + } + }); + if (count > 20) { + // Find count of middle element + var middle = parseInt(count / 2) + (count % 2); + // search for the middle of all visibe elements + $(".row").each(function() { + index++; + if ($(this).css("display") !== "none") { + middle--; + if (middle <= 0) { + return false; + } + } + }); + // Move second half of visible elements + $("#second").append(listItems.slice(index, listlength)); + } +}); diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js index 95a28042..cf079ba7 100644 --- a/cps/static/js/get_meta.js +++ b/cps/static/js/get_meta.js @@ -141,6 +141,7 @@ $(function () { } }, complete: function complete() { + ggDone = true; showResult(); $("#show-google").trigger("change"); } diff --git a/cps/static/js/io/bitstream.js b/cps/static/js/io/bitstream.js old mode 100755 new mode 100644 diff --git a/cps/static/js/io/bytestream.js b/cps/static/js/io/bytestream.js index 55b14005..9372f648 100644 --- a/cps/static/js/io/bytestream.js +++ b/cps/static/js/io/bytestream.js @@ -101,6 +101,35 @@ bitjs.io = bitjs.io || {}; }; + /** + * ToDo: Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. + * @return {number} The bytes interpreted as a signed number. + */ + bitjs.io.ByteStream.prototype.movePointer = function(n) { + this.ptr += n; + // end of buffer reached + if ((this.bytes.byteLength - this.ptr) < 0 ) { + this.ptr = this.bytes.byteLength; + } + } + + /** + * ToDo: Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. + * @return {number} The bytes interpreted as a signed number. + */ + bitjs.io.ByteStream.prototype.moveTo = function(n) { + if ( n < 0 ) { + n = 0; + } + this.ptr = n; + // end of buffer reached + if ((this.bytes.byteLength - this.ptr) < 0 ) { + this.ptr = this.bytes.byteLength; + } + } + /** * This returns n bytes as a sub-array, advancing the pointer if movePointers * is true. diff --git a/cps/static/js/libs/bar-ui.js b/cps/static/js/libs/bar-ui.js new file mode 100644 index 00000000..e0d4b85c --- /dev/null +++ b/cps/static/js/libs/bar-ui.js @@ -0,0 +1,1745 @@ +(function (window) { + + /** + * SoundManager 2: "Bar UI" player + * Copyright (c) 2014, Scott Schiller. All rights reserved. + * http://www.schillmania.com/projects/soundmanager2/ + * Code provided under BSD license. + * http://schillmania.com/projects/soundmanager2/license.txt + */ + + /* global console, document, navigator, soundManager, window */ + + 'use strict'; + + var Player, + players = [], + // CSS selector that will get us the top-level DOM node for the player UI. + playerSelector = '.sm2-bar-ui', + playerOptions, + utils; + + /** + * The following are player object event callback examples. + * Override globally by setting window.sm2BarPlayers.on = {}, or individually by window.sm2BarPlayers[0].on = {} etc. + * soundObject is provided for whileplaying() etc., but playback control should be done via the player object. + */ + players.on = { + /* + play: function(player, soundObject) { + console.log('playing', player); + }, + whileplaying: function(player, soundObject) { + console.log('whileplaying', player, soundObject); + }, + finish: function(player, soundObject) { + // each sound + console.log('finish', player); + }, + pause: function(player, soundObject) { + console.log('pause', player); + }, + error: function(player, soundObject) { + console.log('error', player); + }, + end: function(player, soundObject) { + // end of playlist + console.log('end', player); + } + */ + }; + + playerOptions = { + // useful when multiple players are in use, or other SM2 sounds are active etc. + stopOtherSounds: true, + // CSS class to let the browser load the URL directly e.g., download foo.mp3 + excludeClass: 'sm2-exclude' + }; + + soundManager.setup({ + // trade-off: higher UI responsiveness (play/progress bar), but may use more CPU. + html5PollingInterval: 50, + flashVersion: 9 + }); + + soundManager.onready(function () { + + var nodes, i, j; + + nodes = utils.dom.getAll(playerSelector); + + if (nodes && nodes.length) { + for (i = 0, j = nodes.length; i < j; i++) { + players.push(new Player(nodes[i])); + } + } + + }); + + /** + * player bits + */ + + Player = function (playerNode) { + + var css, dom, extras, playlistController, soundObject, actions, actionData, defaultItem, defaultVolume, firstOpen, exports; + + css = { + disabled: 'disabled', + selected: 'selected', + active: 'active', + legacy: 'legacy', + noVolume: 'no-volume', + playlistOpen: 'playlist-open' + }; + + dom = { + o: null, + playlist: null, + playlistTarget: null, + playlistContainer: null, + time: null, + player: null, + progress: null, + progressTrack: null, + progressBar: null, + duration: null, + volume: null + }; + + // prepended to tracks when a sound fails to load/play + extras = { + loadFailedCharacter: '' + }; + + function stopOtherSounds() { + + if (playerOptions.stopOtherSounds) { + soundManager.stopAll(); + } + + } + + function callback(method, oSound) { + if (method) { + // fire callback, passing current player and sound objects + if (exports.on && exports.on[method]) { + exports.on[method](exports, oSound); + } else if (players.on[method]) { + players.on[method](exports, oSound); + } + } + } + + function getTime(msec, useString) { + + // convert milliseconds to hh:mm:ss, return as object literal or string + + var nSec = Math.floor(msec / 1000), + hh = Math.floor(nSec / 3600), + min = Math.floor(nSec / 60) - Math.floor(hh * 60), + sec = Math.floor(nSec - (hh * 3600) - (min * 60)); + + // if (min === 0 && sec === 0) return null; // return 0:00 as null + + return (useString ? ((hh ? hh + ':' : '') + (hh && min < 10 ? '0' + min : min) + ':' + (sec < 10 ? '0' + sec : sec)) : { min: min, sec: sec }); + + } + + function setTitle(item) { + + // given a link, update the "now playing" UI. + + // if this is an

  • with an inner link, grab and use the text from that. + var links = item.getElementsByTagName('a'); + + if (links.length) { + item = links[0]; + } + + // remove any failed character sequence, also + dom.playlistTarget.innerHTML = '
    • ' + item.innerHTML.replace(extras.loadFailedCharacter, '') + '
    '; + + if (dom.playlistTarget.getElementsByTagName('li')[0].scrollWidth > dom.playlistTarget.offsetWidth) { + // this item can use , in fact. + dom.playlistTarget.innerHTML = '
    • ' + item.innerHTML + '
    '; + } + + } + + function makeSound(url) { + + var sound = soundManager.createSound({ + + url: url, + + volume: defaultVolume, + + whileplaying: function () { + + + //This sends a bookmark update to calibreweb every 30 seconds. + if (this.progressBuffer == undefined) { + this.progressBuffer = 0; + } + + if (this.progressBuffer <= this.position) { + + $.ajax(calibre.bookmarkUrl, { + method: "post", + data: { bookmark: this.position } + }).fail(function (xhr, status, error) { + console.error(error); + }); + + this.progressBuffer = this.progressBuffer + 30000; + } + + var progressMaxLeft = 100, + left, + width; + + left = Math.min(progressMaxLeft, Math.max(0, (progressMaxLeft * (this.position / this.durationEstimate)))) + '%'; + width = Math.min(100, Math.max(0, (100 * (this.position / this.durationEstimate)))) + '%'; + + if (this.duration) { + + dom.progress.style.left = left; + dom.progressBar.style.width = width; + + // TODO: only write changes + dom.time.innerHTML = getTime(this.position, true); + + } + + callback('whileplaying', this); + + }, + + onbufferchange: function (isBuffering) { + + if (isBuffering) { + utils.css.add(dom.o, 'buffering'); + } else { + utils.css.remove(dom.o, 'buffering'); + } + + }, + + onplay: function () { + utils.css.swap(dom.o, 'paused', 'playing'); + callback('play', this); + }, + + onpause: function () { + + $.ajax(calibre.bookmarkUrl, { + method: "post", + data: { bookmark: this.position } + }).fail(function (xhr, status, error) { + console.error(error); + }); + + utils.css.swap(dom.o, 'playing', 'paused'); + callback('pause', this); + }, + + onresume: function () { + utils.css.swap(dom.o, 'paused', 'playing'); + }, + + whileloading: function () { + + if (!this.isHTML5) { + dom.duration.innerHTML = getTime(this.durationEstimate, true); + } + + }, + + onload: function (ok) { + + sound.setPosition(calibre.bookmark); + + if (ok) { + dom.duration.innerHTML = getTime(this.duration, true); + + } else if (this._iO && this._iO.onerror) { + + this._iO.onerror(); + + } + + }, + + onerror: function () { + + // sound failed to load. + var item, element, html; + + item = playlistController.getItem(); + + if (item) { + + // note error, delay 2 seconds and advance? + // playlistTarget.innerHTML = '
    • ' + item.innerHTML + '
    '; + + if (extras.loadFailedCharacter) { + dom.playlistTarget.innerHTML = dom.playlistTarget.innerHTML.replace('
  • ', '
  • ' + extras.loadFailedCharacter + ' '); + if (playlistController.data.playlist && playlistController.data.playlist[playlistController.data.selectedIndex]) { + element = playlistController.data.playlist[playlistController.data.selectedIndex].getElementsByTagName('a')[0]; + html = element.innerHTML; + if (html.indexOf(extras.loadFailedCharacter) === -1) { + element.innerHTML = extras.loadFailedCharacter + ' ' + html; + } + } + } + + } + + callback('error', this); + + // load next, possibly with delay. + + if (navigator.userAgent.match(/mobile/i)) { + // mobile will likely block the next play() call if there is a setTimeout() - so don't use one here. + actions.next(); + } else { + if (playlistController.data.timer) { + window.clearTimeout(playlistController.data.timer); + } + playlistController.data.timer = window.setTimeout(actions.next, 2000); + } + + }, + + onstop: function () { + + $.ajax(calibre.bookmarkUrl, { + method: "post", + data: { bookmark: this.position } + }).fail(function (xhr, status, error) { + console.error(error); + }); + + utils.css.remove(dom.o, 'playing'); + + }, + + onfinish: function () { + + $.ajax(calibre.bookmarkUrl, { + method: "post", + data: { bookmark: this.position } + }).fail(function (xhr, status, error) { + console.error(error); + }); + + var lastIndex, item; + + utils.css.remove(dom.o, 'playing'); + + dom.progress.style.left = '0%'; + + lastIndex = playlistController.data.selectedIndex; + + callback('finish', this); + + // next track? + item = playlistController.getNext(); + + // don't play the same item over and over again, if at end of playlist (excluding single item case.) + if (item && (playlistController.data.selectedIndex !== lastIndex || (playlistController.data.playlist.length === 1 && playlistController.data.loopMode))) { + + playlistController.select(item); + + setTitle(item); + + stopOtherSounds(); + + // play next + this.play({ + url: playlistController.getURL() + }); + + } else { + + // end of playlist case + + // explicitly stop? + // this.stop(); + + callback('end', this); + + } + + } + + }); + + return sound; + + } + + function playLink(link) { + + // if a link is OK, play it. + + if (soundManager.canPlayURL(link.href)) { + + // if there's a timer due to failure to play one track, cancel it. + // catches case when user may use previous/next after an error. + if (playlistController.data.timer) { + window.clearTimeout(playlistController.data.timer); + playlistController.data.timer = null; + } + + if (!soundObject) { + soundObject = makeSound(link.href); + } + + // required to reset pause/play state on iOS so whileplaying() works? odd. + soundObject.stop(); + + playlistController.select(link.parentNode); + + setTitle(link.parentNode); + + // reset the UI + // TODO: function that also resets/hides timing info. + dom.progress.style.left = '0px'; + dom.progressBar.style.width = '0px'; + + stopOtherSounds(); + + soundObject.play({ + url: link.href, + position: 0 + }); + + } + + } + + function PlaylistController() { + + var data; + + data = { + + // list of nodes? + playlist: [], + + // NOTE: not implemented yet. + // shuffledIndex: [], + // shuffleMode: false, + + // selection + selectedIndex: 0, + + loopMode: false, + + timer: null + + }; + + function getPlaylist() { + + return data.playlist; + + } + + function getItem(offset) { + + var list, + item; + + // given the current selection (or an offset), return the current item. + + // if currently null, may be end of list case. bail. + if (data.selectedIndex === null) { + return offset; + } + + list = getPlaylist(); + + // use offset if provided, otherwise take default selected. + offset = (offset !== undefined ? offset : data.selectedIndex); + + // safety check - limit to between 0 and list length + offset = Math.max(0, Math.min(offset, list.length)); + + item = list[offset]; + + return item; + + } + + function findOffsetFromItem(item) { + + // given an
  • item, find it in the playlist array and return the index. + var list, + i, + j, + offset; + + offset = -1; + + list = getPlaylist(); + + if (list) { + + for (i = 0, j = list.length; i < j; i++) { + if (list[i] === item) { + offset = i; + break; + } + } + + } + + return offset; + + } + + function getNext() { + + // don't increment if null. + if (data.selectedIndex !== null) { + data.selectedIndex++; + } + + if (data.playlist.length > 1) { + + if (data.selectedIndex >= data.playlist.length) { + + if (data.loopMode) { + + // loop to beginning + data.selectedIndex = 0; + + } else { + + // no change + data.selectedIndex--; + + // end playback + // data.selectedIndex = null; + + } + + } + + } else { + + data.selectedIndex = null; + + } + + return getItem(); + + } + + function getPrevious() { + + data.selectedIndex--; + + if (data.selectedIndex < 0) { + // wrapping around beginning of list? loop or exit. + if (data.loopMode) { + data.selectedIndex = data.playlist.length - 1; + } else { + // undo + data.selectedIndex++; + } + } + + return getItem(); + + } + + function resetLastSelected() { + + // remove UI highlight(s) on selected items. + var items, + i, j; + + items = utils.dom.getAll(dom.playlist, '.' + css.selected); + + for (i = 0, j = items.length; i < j; i++) { + utils.css.remove(items[i], css.selected); + } + + } + + function select(item) { + + var offset, + itemTop, + itemBottom, + containerHeight, + scrollTop, + itemPadding, + liElement; + + // remove last selected, if any + resetLastSelected(); + + if (item) { + + liElement = utils.dom.ancestor('li', item); + + utils.css.add(liElement, css.selected); + + itemTop = item.offsetTop; + itemBottom = itemTop + item.offsetHeight; + containerHeight = dom.playlistContainer.offsetHeight; + scrollTop = dom.playlist.scrollTop; + itemPadding = 8; + + if (itemBottom > containerHeight + scrollTop) { + // bottom-align + dom.playlist.scrollTop = (itemBottom - containerHeight) + itemPadding; + } else if (itemTop < scrollTop) { + // top-align + dom.playlist.scrollTop = item.offsetTop - itemPadding; + } + + } + + // update selected offset, too. + offset = findOffsetFromItem(liElement); + + data.selectedIndex = offset; + + } + + function playItemByOffset(offset) { + + var item; + + offset = (offset || 0); + + item = getItem(offset); + + if (item) { + playLink(item.getElementsByTagName('a')[0]); + } + + } + + function getURL() { + + // return URL of currently-selected item + var item, url; + + item = getItem(); + + if (item) { + url = item.getElementsByTagName('a')[0].href; + } + + return url; + + } + + function refreshDOM() { + + // get / update playlist from DOM + + if (!dom.playlist) { + if (window.console && console.warn) { + console.warn('refreshDOM(): playlist node not found?'); + } + return; + } + + data.playlist = dom.playlist.getElementsByTagName('li'); + + } + + function initDOM() { + + dom.playlistTarget = utils.dom.get(dom.o, '.sm2-playlist-target'); + dom.playlistContainer = utils.dom.get(dom.o, '.sm2-playlist-drawer'); + dom.playlist = utils.dom.get(dom.o, '.sm2-playlist-bd'); + + } + + function initPlaylistController() { + + // inherit the default SM2 volume + defaultVolume = soundManager.defaultOptions.volume; + + initDOM(); + refreshDOM(); + + // animate playlist open, if HTML classname indicates so. + if (utils.css.has(dom.o, css.playlistOpen)) { + // hackish: run this after API has returned + window.setTimeout(function () { + actions.menu(true); + }, 1); + } + + } + + initPlaylistController(); + + return { + data: data, + refresh: refreshDOM, + getNext: getNext, + getPrevious: getPrevious, + getItem: getItem, + getURL: getURL, + playItemByOffset: playItemByOffset, + select: select + }; + + } + + function isRightClick(e) { + + // only pay attention to left clicks. old IE differs where there's no e.which, but e.button is 1 on left click. + if (e && ((e.which && e.which === 2) || (e.which === undefined && e.button !== 1))) { + // http://www.quirksmode.org/js/events_properties.html#button + return true; + } + + return false; + + } + + function getActionData(target) { + + // DOM measurements for volume slider + + if (!target) { + return; + } + + actionData.volume.x = utils.position.getOffX(target); + actionData.volume.y = utils.position.getOffY(target); + + actionData.volume.width = target.offsetWidth; + actionData.volume.height = target.offsetHeight; + + // potentially dangerous: this should, but may not be a percentage-based value. + actionData.volume.backgroundSize = parseInt(utils.style.get(target, 'background-size'), 10); + + // IE gives pixels even if background-size specified as % in CSS. Boourns. + if (window.navigator.userAgent.match(/msie|trident/i)) { + actionData.volume.backgroundSize = (actionData.volume.backgroundSize / actionData.volume.width) * 100; + } + + } + + function handleMouseDown(e) { + + var links, + target; + + target = e.target || e.srcElement; + + if (isRightClick(e)) { + return; + } + + // normalize to , if applicable. + if (target.nodeName.toLowerCase() !== 'a') { + + links = target.getElementsByTagName('a'); + if (links && links.length) { + target = target.getElementsByTagName('a')[0]; + } + + } + + if (utils.css.has(target, 'sm2-volume-control')) { + + // drag case for volume + + getActionData(target); + + utils.events.add(document, 'mousemove', actions.adjustVolume); + utils.events.add(document, 'touchmove', actions.adjustVolume); + utils.events.add(document, 'mouseup', actions.releaseVolume); + utils.events.add(document, 'touchend', actions.releaseVolume); + + // and apply right away + actions.adjustVolume(e); + + } + + } + + function handleMouse(e) { + + var target, barX, barWidth, x, clientX, newPosition, sound; + + target = dom.progressTrack; + + barX = utils.position.getOffX(target); + barWidth = target.offsetWidth; + clientX = utils.events.getClientX(e); + + x = (clientX - barX); + + newPosition = (x / barWidth); + + sound = soundObject; + + if (sound && sound.duration) { + + sound.setPosition(sound.duration * newPosition); + + // a little hackish: ensure UI updates immediately with current position, even if audio is buffering and hasn't moved there yet. + if (sound._iO && sound._iO.whileplaying) { + sound._iO.whileplaying.apply(sound); + } + + } + + if (e.preventDefault) { + e.preventDefault(); + } + + return false; + + } + + function releaseMouse(e) { + + utils.events.remove(document, 'mousemove', handleMouse); + utils.events.remove(document, 'touchmove', handleMouse); + + utils.css.remove(dom.o, 'grabbing'); + + utils.events.remove(document, 'mouseup', releaseMouse); + utils.events.remove(document, 'touchend', releaseMouse); + + utils.events.preventDefault(e); + + return false; + + } + + function handleProgressMouseDown(e) { + + if (isRightClick(e)) { + return; + } + + utils.css.add(dom.o, 'grabbing'); + + utils.events.add(document, 'mousemove', handleMouse); + utils.events.add(document, 'touchmove', handleMouse); + utils.events.add(document, 'mouseup', releaseMouse); + utils.events.add(document, 'touchend', releaseMouse); + + handleMouse(e); + + } + + function handleClick(e) { + + var evt, + target, + offset, + targetNodeName, + methodName, + href, + handled; + + evt = (e || window.event); + + target = evt.target || evt.srcElement; + + if (target && target.nodeName) { + + targetNodeName = target.nodeName.toLowerCase(); + + if (targetNodeName !== 'a') { + + // old IE (IE 8) might return nested elements inside the , eg., etc. Try to find the parent . + + if (target.parentNode) { + + do { + target = target.parentNode; + targetNodeName = target.nodeName.toLowerCase(); + } while (targetNodeName !== 'a' && target.parentNode); + + if (!target) { + // something went wrong. bail. + return false; + } + + } + + } + + if (targetNodeName === 'a') { + + // yep, it's a link. + + href = target.href; + + if (soundManager.canPlayURL(href)) { + + // not excluded + if (!utils.css.has(target, playerOptions.excludeClass)) { + + // find this in the playlist + + playLink(target); + + handled = true; + + } + + } else { + + // is this one of the action buttons, eg., play/pause, volume, etc.? + offset = target.href.lastIndexOf('#'); + + if (offset !== -1) { + + methodName = target.href.substr(offset + 1); + + if (methodName && actions[methodName]) { + handled = true; + actions[methodName](e); + } + + } + + } + + // fall-through case + + if (handled) { + // prevent browser fall-through + return utils.events.preventDefault(evt); + } + + } + + } + + return true; + + } + + function init() { + + // init DOM? + + if (!playerNode && window.console && console.warn) { + console.warn('init(): No playerNode element?'); + } + + dom.o = playerNode; + + // are we dealing with a crap browser? apply legacy CSS if so. + if (window.navigator.userAgent.match(/msie [678]/i)) { + utils.css.add(dom.o, css.legacy); + } + + if (window.navigator.userAgent.match(/mobile/i)) { + // majority of mobile devices don't let HTML5 audio set volume. + utils.css.add(dom.o, css.noVolume); + } + + dom.progress = utils.dom.get(dom.o, '.sm2-progress-ball'); + + dom.progressTrack = utils.dom.get(dom.o, '.sm2-progress-track'); + + dom.progressBar = utils.dom.get(dom.o, '.sm2-progress-bar'); + + dom.volume = utils.dom.get(dom.o, 'a.sm2-volume-control'); + + // measure volume control dimensions + if (dom.volume) { + getActionData(dom.volume); + } + + dom.duration = utils.dom.get(dom.o, '.sm2-inline-duration'); + + dom.time = utils.dom.get(dom.o, '.sm2-inline-time'); + + playlistController = new PlaylistController(); + + defaultItem = playlistController.getItem(0); + + playlistController.select(defaultItem); + + if (defaultItem) { + setTitle(defaultItem); + } + + utils.events.add(dom.o, 'mousedown', handleMouseDown); + utils.events.add(dom.o, 'touchstart', handleMouseDown); + utils.events.add(dom.o, 'click', handleClick); + utils.events.add(dom.progressTrack, 'mousedown', handleProgressMouseDown); + utils.events.add(dom.progressTrack, 'touchstart', handleProgressMouseDown); + + } + + // --- + + actionData = { + + volume: { + x: 0, + y: 0, + width: 0, + height: 0, + backgroundSize: 0 + } + + }; + + actions = { + + play: function (offsetOrEvent) { + + /** + * This is an overloaded function that takes mouse/touch events or offset-based item indices. + * Remember, "auto-play" will not work on mobile devices unless this function is called immediately from a touch or click event. + * If you have the link but not the offset, you can also pass a fake event object with a target of an inside the playlist - e.g. { target: someMP3Link } + */ + + var target, + href, + e; + + if (offsetOrEvent !== undefined && !isNaN(offsetOrEvent)) { + // smells like a number. + playlistController.playItemByOffset(offsetOrEvent); + return; + } + + // DRY things a bit + e = offsetOrEvent; + + if (e && e.target) { + + target = e.target || e.srcElement; + + href = target.href; + + } + + // haaaack - if null due to no event, OR '#' due to play/pause link, get first link from playlist + if (!href || href.indexOf('#') !== -1) { + href = dom.playlist.getElementsByTagName('a')[0].href; + } + + if (!soundObject) { + soundObject = makeSound(href); + } + + // edge case: if the current sound is not playing, stop all others. + if (!soundObject.playState) { + stopOtherSounds(); + } + + // TODO: if user pauses + unpauses a sound that had an error, try to play next? + soundObject.togglePause(); + + // special case: clear "play next" timeout, if one exists. + // edge case: user pauses after a song failed to load. + if (soundObject.paused && playlistController.data.timer) { + window.clearTimeout(playlistController.data.timer); + playlistController.data.timer = null; + } + + }, + + pause: function () { + + if (soundObject && soundObject.readyState) { + soundObject.pause(); + } + + }, + + resume: function () { + + if (soundObject && soundObject.readyState) { + soundObject.resume(); + } + + }, + + stop: function () { + + // just an alias for pause, really. + // don't actually stop because that will mess up some UI state, i.e., dragging the slider. + return actions.pause(); + + }, + + next: function (/* e */) { + + var item, lastIndex; + + // special case: clear "play next" timeout, if one exists. + if (playlistController.data.timer) { + window.clearTimeout(playlistController.data.timer); + playlistController.data.timer = null; + } + + lastIndex = playlistController.data.selectedIndex; + + item = playlistController.getNext(true); + + // don't play the same item again + if (item && playlistController.data.selectedIndex !== lastIndex) { + playLink(item.getElementsByTagName('a')[0]); + } + + }, + + prev: function (/* e */) { + + var item, lastIndex; + + lastIndex = playlistController.data.selectedIndex; + + item = playlistController.getPrevious(); + + // don't play the same item again + if (item && playlistController.data.selectedIndex !== lastIndex) { + playLink(item.getElementsByTagName('a')[0]); + } + + }, + + shuffle: function (e) { + + // NOTE: not implemented yet. + + var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.shuffle')); + + if (target && !utils.css.has(target, css.disabled)) { + utils.css.toggle(target.parentNode, css.active); + playlistController.data.shuffleMode = !playlistController.data.shuffleMode; + } + + }, + + repeat: function (e) { + + var target = (e ? e.target || e.srcElement : utils.dom.get(dom.o, '.repeat')); + + if (target && !utils.css.has(target, css.disabled)) { + utils.css.toggle(target.parentNode, css.active); + playlistController.data.loopMode = !playlistController.data.loopMode; + } + + }, + + menu: function (ignoreToggle) { + + var isOpen; + + isOpen = utils.css.has(dom.o, css.playlistOpen); + + // hackish: reset scrollTop in default first open case. odd, but some browsers have a non-zero scroll offset the first time the playlist opens. + if (playlistController && !playlistController.data.selectedIndex && !firstOpen) { + dom.playlist.scrollTop = 0; + firstOpen = true; + } + + // sniff out booleans from mouse events, as this is referenced directly by event handlers. + if (typeof ignoreToggle !== 'boolean' || !ignoreToggle) { + + if (!isOpen) { + // explicitly set height:0, so the first closed -> open animation runs properly + dom.playlistContainer.style.height = '0px'; + } + + isOpen = utils.css.toggle(dom.o, css.playlistOpen); + + } + + // playlist + dom.playlistContainer.style.height = (isOpen ? dom.playlistContainer.scrollHeight : 0) + 'px'; + + }, + + adjustVolume: function (e) { + + /** + * NOTE: this is the mousemove() event handler version. + * Use setVolume(50), etc., to assign volume directly. + */ + + var backgroundMargin, + pixelMargin, + target, + value, + volume; + + value = 0; + + target = dom.volume; + + // safety net + if (e === undefined) { + return false; + } + + // normalize between mouse and touch events + var clientX = utils.events.getClientX(e); + + if (!e || clientX === undefined) { + // called directly or with a non-mouseEvent object, etc. + // proxy to the proper method. + if (arguments.length && window.console && window.console.warn) { + console.warn('Bar UI: call setVolume(' + e + ') instead of adjustVolume(' + e + ').'); + } + return actions.setVolume.apply(this, arguments); + } + + // based on getStyle() result + // figure out spacing around background image based on background size, eg. 60% background size. + // 60% wide means 20% margin on each side. + backgroundMargin = (100 - actionData.volume.backgroundSize) / 2; + + // relative position of mouse over element + value = Math.max(0, Math.min(1, (clientX - actionData.volume.x) / actionData.volume.width)); + + target.style.clip = 'rect(0px, ' + (actionData.volume.width * value) + 'px, ' + actionData.volume.height + 'px, ' + (actionData.volume.width * (backgroundMargin / 100)) + 'px)'; + + // determine logical volume, including background margin + pixelMargin = ((backgroundMargin / 100) * actionData.volume.width); + + volume = Math.max(0, Math.min(1, ((clientX - actionData.volume.x) - pixelMargin) / (actionData.volume.width - (pixelMargin * 2)))) * 100; + + // set volume + if (soundObject) { + soundObject.setVolume(volume); + } + + defaultVolume = volume; + + return utils.events.preventDefault(e); + + }, + + releaseVolume: function (/* e */) { + + utils.events.remove(document, 'mousemove', actions.adjustVolume); + utils.events.remove(document, 'touchmove', actions.adjustVolume); + utils.events.remove(document, 'mouseup', actions.releaseVolume); + utils.events.remove(document, 'touchend', actions.releaseVolume); + + }, + + setVolume: function (volume) { + + // set volume (0-100) and update volume slider UI. + + var backgroundSize, + backgroundMargin, + backgroundOffset, + target, + from, + to; + + if (volume === undefined || isNaN(volume)) { + return; + } + + if (dom.volume) { + + target = dom.volume; + + // based on getStyle() result + backgroundSize = actionData.volume.backgroundSize; + + // figure out spacing around background image based on background size, eg. 60% background size. + // 60% wide means 20% margin on each side. + backgroundMargin = (100 - backgroundSize) / 2; + + // margin as pixel value relative to width + backgroundOffset = actionData.volume.width * (backgroundMargin / 100); + + from = backgroundOffset; + to = from + ((actionData.volume.width - (backgroundOffset * 2)) * (volume / 100)); + + target.style.clip = 'rect(0px, ' + to + 'px, ' + actionData.volume.height + 'px, ' + from + 'px)'; + + } + + // apply volume to sound, as applicable + if (soundObject) { + soundObject.setVolume(volume); + } + + defaultVolume = volume; + + } + + }; + + init(); + + // TODO: mixin actions -> exports + + exports = { + // Per-instance events: window.sm2BarPlayers[0].on = { ... } etc. See global players.on example above for reference. + on: null, + actions: actions, + dom: dom, + playlistController: playlistController + }; + + return exports; + + }; + + // barebones utilities for logic, CSS, DOM, events etc. + + utils = { + + array: (function () { + + function compare(property) { + + var result; + + return function (a, b) { + + if (a[property] < b[property]) { + result = -1; + } else if (a[property] > b[property]) { + result = 1; + } else { + result = 0; + } + return result; + }; + + } + + function shuffle(array) { + + // Fisher-Yates shuffle algo + + var i, j, temp; + + for (i = array.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + return array; + + } + + return { + compare: compare, + shuffle: shuffle + }; + + }()), + + css: (function () { + + function hasClass(o, cStr) { + + return (o.className !== undefined ? new RegExp('(^|\\s)' + cStr + '(\\s|$)').test(o.className) : false); + + } + + function addClass(o, cStr) { + + if (!o || !cStr || hasClass(o, cStr)) { + return; // safety net + } + o.className = (o.className ? o.className + ' ' : '') + cStr; + + } + + function removeClass(o, cStr) { + + if (!o || !cStr || !hasClass(o, cStr)) { + return; + } + o.className = o.className.replace(new RegExp('( ' + cStr + ')|(' + cStr + ')', 'g'), ''); + + } + + function swapClass(o, cStr1, cStr2) { + + var tmpClass = { + className: o.className + }; + + removeClass(tmpClass, cStr1); + addClass(tmpClass, cStr2); + + o.className = tmpClass.className; + + } + + function toggleClass(o, cStr) { + + var found, + method; + + found = hasClass(o, cStr); + + method = (found ? removeClass : addClass); + + method(o, cStr); + + // indicate the new state... + return !found; + + } + + return { + has: hasClass, + add: addClass, + remove: removeClass, + swap: swapClass, + toggle: toggleClass + }; + + }()), + + dom: (function () { + + function getAll(param1, param2) { + + var node, + selector, + results; + + if (arguments.length === 1) { + + // .selector case + node = document.documentElement; + // first param is actually the selector + selector = param1; + + } else { + + // node, .selector + node = param1; + selector = param2; + + } + + // sorry, IE 7 users; IE 8+ required. + if (node && node.querySelectorAll) { + + results = node.querySelectorAll(selector); + + } + + return results; + + } + + function get(/* parentNode, selector */) { + + var results = getAll.apply(this, arguments); + + // hackish: if an array, return the last item. + if (results && results.length) { + return results[results.length - 1]; + } + + // handle "not found" case + return results && results.length === 0 ? null : results; + + } + + function ancestor(nodeName, element, checkCurrent) { + + if (!element || !nodeName) { + return element; + } + + nodeName = nodeName.toUpperCase(); + + // return if current node matches. + if (checkCurrent && element && element.nodeName === nodeName) { + return element; + } + + while (element && element.nodeName !== nodeName && element.parentNode) { + element = element.parentNode; + } + + return (element && element.nodeName === nodeName ? element : null); + + } + + return { + ancestor: ancestor, + get: get, + getAll: getAll + }; + + }()), + + position: (function () { + + function getOffX(o) { + + // http://www.xs4all.nl/~ppk/js/findpos.html + var curleft = 0; + + if (o.offsetParent) { + + while (o.offsetParent) { + + curleft += o.offsetLeft; + + o = o.offsetParent; + + } + + } else if (o.x) { + + curleft += o.x; + + } + + return curleft; + + } + + function getOffY(o) { + + // http://www.xs4all.nl/~ppk/js/findpos.html + var curtop = 0; + + if (o.offsetParent) { + + while (o.offsetParent) { + + curtop += o.offsetTop; + + o = o.offsetParent; + + } + + } else if (o.y) { + + curtop += o.y; + + } + + return curtop; + + } + + return { + getOffX: getOffX, + getOffY: getOffY + }; + + }()), + + style: (function () { + + function get(node, styleProp) { + + // http://www.quirksmode.org/dom/getstyles.html + var value; + + if (node.currentStyle) { + + value = node.currentStyle[styleProp]; + + } else if (window.getComputedStyle) { + + value = document.defaultView.getComputedStyle(node, null).getPropertyValue(styleProp); + + } + + return value; + + } + + return { + get: get + }; + + }()), + + events: (function () { + + var add, remove, preventDefault, getClientX; + + add = function (o, evtName, evtHandler) { + // return an object with a convenient detach method. + var eventObject = { + detach: function () { + return remove(o, evtName, evtHandler); + } + }; + if (window.addEventListener) { + o.addEventListener(evtName, evtHandler, false); + } else { + o.attachEvent('on' + evtName, evtHandler); + } + return eventObject; + }; + + remove = (window.removeEventListener !== undefined ? function (o, evtName, evtHandler) { + return o.removeEventListener(evtName, evtHandler, false); + } : function (o, evtName, evtHandler) { + return o.detachEvent('on' + evtName, evtHandler); + }); + + preventDefault = function (e) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + e.cancelBubble = true; + } + return false; + }; + + getClientX = function (e) { + // normalize between desktop (mouse) and touch (mobile/tablet/?) events. + // note pageX for touch, which normalizes zoom/scroll/pan vs. clientX. + return (e && (e.clientX || (e.touches && e.touches[0] && e.touches[0].pageX))); + }; + + return { + add: add, + preventDefault: preventDefault, + remove: remove, + getClientX: getClientX + }; + + }()), + + features: (function () { + + var getAnimationFrame, + localAnimationFrame, + localFeatures, + prop, + styles, + testDiv, + transform; + + testDiv = document.createElement('div'); + + /** + * hat tip: paul irish + * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * https://gist.github.com/838785 + */ + + localAnimationFrame = (window.requestAnimationFrame + || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame + || window.oRequestAnimationFrame + || window.msRequestAnimationFrame + || null); + + // apply to window, avoid "illegal invocation" errors in Chrome + getAnimationFrame = localAnimationFrame ? function () { + return localAnimationFrame.apply(window, arguments); + } : null; + + function has(propName) { + + // test for feature support + return (testDiv.style[propName] !== undefined ? propName : null); + + } + + // note local scope. + localFeatures = { + + transform: { + ie: has('-ms-transform'), + moz: has('MozTransform'), + opera: has('OTransform'), + webkit: has('webkitTransform'), + w3: has('transform'), + prop: null // the normalized property value + }, + + rotate: { + has3D: false, + prop: null + }, + + getAnimationFrame: getAnimationFrame + + }; + + localFeatures.transform.prop = ( + localFeatures.transform.w3 || + localFeatures.transform.moz || + localFeatures.transform.webkit || + localFeatures.transform.ie || + localFeatures.transform.opera + ); + + function attempt(style) { + + try { + testDiv.style[transform] = style; + } catch (e) { + // that *definitely* didn't work. + return false; + } + // if we can read back the style, it should be cool. + return !!testDiv.style[transform]; + + } + + if (localFeatures.transform.prop) { + + // try to derive the rotate/3D support. + transform = localFeatures.transform.prop; + styles = { + css_2d: 'rotate(0deg)', + css_3d: 'rotate3d(0,0,0,0deg)' + }; + + if (attempt(styles.css_3d)) { + localFeatures.rotate.has3D = true; + prop = 'rotate3d'; + } else if (attempt(styles.css_2d)) { + prop = 'rotate'; + } + + localFeatures.rotate.prop = prop; + + } + + testDiv = null; + + return localFeatures; + + }()) + + }; + + // --- + + // expose to global + window.sm2BarPlayers = players; + window.sm2BarPlayerOptions = playerOptions; + window.SM2BarPlayer = Player; + +}(window)); diff --git a/cps/static/js/libs/plugins.js b/cps/static/js/libs/plugins.js index 0ef33898..f69561a1 100644 --- a/cps/static/js/libs/plugins.js +++ b/cps/static/js/libs/plugins.js @@ -42,4 +42,77 @@ * Copyright 2018 Metafizzy */ -!function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,r,l){function a(t,e,n){var o,r="$()."+i+'("'+e+'")';return t.each(function(t,a){var h=l.data(a,i);if(!h)return void s(i+" not initialized. Cannot call methods, i.e. "+r);var c=h[e];if(!c||"_"==e.charAt(0))return void s(r+" is not a valid method");var u=c.apply(h,n);o=void 0===o?u:o}),void 0!==o?o:t}function h(t,e){t.each(function(t,n){var o=l.data(n,i);o?(o.option(e),o._init()):(o=new r(n,e),l.data(n,i,o))})}l=l||e||t.jQuery,l&&(r.prototype.option||(r.prototype.option=function(t){l.isPlainObject(t)&&(this.options=l.extend(!0,this.options,t))}),l.fn[i]=function(t){if("string"==typeof t){var e=o.call(arguments,1);return a(this,t,e)}return h(this,t),this},n(l))}function n(t){!t||t&&t.bridget||(t.bridget=i)}var o=Array.prototype.slice,r=t.console,s="undefined"==typeof r?function(){}:function(t){r.error(t)};return n(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return n.indexOf(e)==-1&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return n!=-1&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var n=this._onceEvents&&this._onceEvents[t],o=0;o=0,this.isPrefilling?(this.log("prefill"),this.loadNextPage()):this.stopPrefill()},s.getPrefillDistance=function(){return this.options.elementScroll?this.scroller.clientHeight-this.scroller.scrollHeight:this.windowHeight-this.element.clientHeight},s.stopPrefill=function(){this.log("stopPrefill"),this.off("append",this.prefill)},e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/scroll-watch",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){var n=e.prototype;return e.defaults.scrollThreshold=400,e.create.scrollWatch=function(){this.pageScrollHandler=this.onPageScroll.bind(this),this.resizeHandler=this.onResize.bind(this);var t=this.options.scrollThreshold,e=t||0===t;e&&this.enableScrollWatch()},e.destroy.scrollWatch=function(){this.disableScrollWatch()},n.enableScrollWatch=function(){this.isScrollWatching||(this.isScrollWatching=!0,this.updateMeasurements(),this.updateScroller(),this.on("last",this.disableScrollWatch),this.bindScrollWatchEvents(!0))},n.disableScrollWatch=function(){this.isScrollWatching&&(this.bindScrollWatchEvents(!1),delete this.isScrollWatching)},n.bindScrollWatchEvents=function(e){var i=e?"addEventListener":"removeEventListener";this.scroller[i]("scroll",this.pageScrollHandler),t[i]("resize",this.resizeHandler)},n.onPageScroll=e.throttle(function(){var t=this.getBottomDistance();t<=this.options.scrollThreshold&&this.dispatchEvent("scrollThreshold")}),n.getBottomDistance=function(){return this.options.elementScroll?this.getElementBottomDistance():this.getWindowBottomDistance()},n.getWindowBottomDistance=function(){var e=this.top+this.element.clientHeight,i=t.pageYOffset+this.windowHeight;return e-i},n.getElementBottomDistance=function(){var t=this.scroller.scrollHeight,e=this.scroller.scrollTop+this.scroller.clientHeight;return t-e},n.onResize=function(){this.updateMeasurements()},i.debounceMethod(e,"onResize",150),e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/history",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){var n=e.prototype;e.defaults.history="replace";var o=document.createElement("a");return e.create.history=function(){if(this.options.history){o.href=this.getAbsolutePath();var t=o.origin||o.protocol+"//"+o.host,e=t==location.origin;return e?void(this.options.append?this.createHistoryAppend():this.createHistoryPageLoad()):void console.error("[InfiniteScroll] cannot set history with different origin: "+o.origin+" on "+location.origin+" . History behavior disabled.")}},n.createHistoryAppend=function(){this.updateMeasurements(),this.updateScroller(),this.scrollPages=[{top:0,path:location.href,title:document.title}],this.scrollPageIndex=0,this.scrollHistoryHandler=this.onScrollHistory.bind(this),this.unloadHandler=this.onUnload.bind(this),this.scroller.addEventListener("scroll",this.scrollHistoryHandler),this.on("append",this.onAppendHistory),this.bindHistoryAppendEvents(!0)},n.bindHistoryAppendEvents=function(e){var i=e?"addEventListener":"removeEventListener";this.scroller[i]("scroll",this.scrollHistoryHandler),t[i]("unload",this.unloadHandler)},n.createHistoryPageLoad=function(){this.on("load",this.onPageLoadHistory)},e.destroy.history=n.destroyHistory=function(){var t=this.options.history&&this.options.append;t&&this.bindHistoryAppendEvents(!1)},n.onAppendHistory=function(t,e,i){if(i&&i.length){var n=i[0],r=this.getElementScrollY(n);o.href=e,this.scrollPages.push({top:r,path:o.href,title:t.title})}},n.getElementScrollY=function(t){return this.options.elementScroll?this.getElementElementScrollY(t):this.getElementWindowScrollY(t)},n.getElementWindowScrollY=function(e){var i=e.getBoundingClientRect();return i.top+t.pageYOffset},n.getElementElementScrollY=function(t){return t.offsetTop-this.top},n.onScrollHistory=function(){for(var t,e,i=this.getScrollViewY(),n=0;n=i)break;t=n,e=o}t!=this.scrollPageIndex&&(this.scrollPageIndex=t,this.setHistory(e.title,e.path))},i.debounceMethod(e,"onScrollHistory",150),n.getScrollViewY=function(){return this.options.elementScroll?this.scroller.scrollTop+this.scroller.clientHeight/2:t.pageYOffset+this.windowHeight/2},n.setHistory=function(t,e){var i=this.options.history,n=i&&history[i+"State"];n&&(history[i+"State"](null,t,e),this.options.historyTitle&&(document.title=t),this.dispatchEvent("history",null,[t,e]))},n.onUnload=function(){var e=this.scrollPageIndex;if(0!==e){var i=this.scrollPages[e],n=t.pageYOffset-i.top+this.top;this.destroyHistory(),scrollTo(0,n)}},n.onPageLoadHistory=function(t,e){this.setHistory(t.title,e)},e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/button",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){function n(t,e){this.element=t,this.infScroll=e,this.clickHandler=this.onClick.bind(this),this.element.addEventListener("click",this.clickHandler),e.on("request",this.disable.bind(this)),e.on("load",this.enable.bind(this)),e.on("error",this.hide.bind(this)),e.on("last",this.hide.bind(this))}return e.create.button=function(){var t=i.getQueryElement(this.options.button);if(t)return void(this.button=new n(t,this))},e.destroy.button=function(){this.button&&this.button.destroy()},n.prototype.onClick=function(t){t.preventDefault(),this.infScroll.loadNextPage()},n.prototype.enable=function(){this.element.removeAttribute("disabled")},n.prototype.disable=function(){this.element.disabled="disabled"},n.prototype.hide=function(){this.element.style.display="none"},n.prototype.destroy=function(){this.element.removeEventListener("click",this.clickHandler)},e.Button=n,e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/status",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){function n(t){r(t,"none")}function o(t){r(t,"block")}function r(t,e){t&&(t.style.display=e)}var s=e.prototype;return e.create.status=function(){var t=i.getQueryElement(this.options.status);t&&(this.statusElement=t,this.statusEventElements={request:t.querySelector(".infinite-scroll-request"),error:t.querySelector(".infinite-scroll-error"),last:t.querySelector(".infinite-scroll-last")},this.on("request",this.showRequestStatus),this.on("error",this.showErrorStatus),this.on("last",this.showLastStatus),this.bindHideStatus("on"))},s.bindHideStatus=function(t){var e=this.options.append?"append":"load";this[t](e,this.hideAllStatus)},s.showRequestStatus=function(){this.showStatus("request")},s.showErrorStatus=function(){this.showStatus("error")},s.showLastStatus=function(){this.showStatus("last"),this.bindHideStatus("off")},s.showStatus=function(t){o(this.statusElement),this.hideStatusEventElements();var e=this.statusEventElements[t];o(e)},s.hideAllStatus=function(){n(this.statusElement),this.hideStatusEventElements()},s.hideStatusEventElements=function(){for(var t in this.statusEventElements){var e=this.statusEventElements[t];n(e)}},e}),function(t,e){"function"==typeof define&&define.amd?define(["infinite-scroll/js/core","infinite-scroll/js/page-load","infinite-scroll/js/scroll-watch","infinite-scroll/js/history","infinite-scroll/js/button","infinite-scroll/js/status"],e):"object"==typeof module&&module.exports&&(module.exports=e(require("./core"),require("./page-load"),require("./scroll-watch"),require("./history"),require("./button"),require("./status")))}(window,function(t){return t}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("imagesloaded/imagesloaded",["ev-emitter/ev-emitter"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}("undefined"!=typeof window?window:this,function(t,e){function i(t,e){for(var i in e)t[i]=e[i];return t}function n(t){if(Array.isArray(t))return t;var e="object"==typeof t&&"number"==typeof t.length;return e?h.call(t):[t]}function o(t,e,r){if(!(this instanceof o))return new o(t,e,r);var s=t;return"string"==typeof t&&(s=document.querySelectorAll(t)),s?(this.elements=n(s),this.options=i({},this.options),"function"==typeof e?r=e:i(this.options,e),r&&this.on("always",r),this.getImages(),l&&(this.jqDeferred=new l.Deferred),void setTimeout(this.check.bind(this))):void a.error("Bad element for imagesLoaded "+(s||t))}function r(t){this.img=t}function s(t,e){this.url=t,this.element=e,this.img=new Image}var l=t.jQuery,a=t.console,h=Array.prototype.slice;o.prototype=Object.create(e.prototype),o.prototype.options={},o.prototype.getImages=function(){this.images=[],this.elements.forEach(this.addElementImages,this)},o.prototype.addElementImages=function(t){"IMG"==t.nodeName&&this.addImage(t),this.options.background===!0&&this.addElementBackgroundImages(t);var e=t.nodeType;if(e&&c[e]){for(var i=t.querySelectorAll("img"),n=0;n=0,this.isPrefilling?(this.log("prefill"),this.loadNextPage()):this.stopPrefill()},s.getPrefillDistance=function(){return this.options.elementScroll?this.scroller.clientHeight-this.scroller.scrollHeight:this.windowHeight-this.element.clientHeight},s.stopPrefill=function(){this.log("stopPrefill"),this.off("append",this.prefill)},e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/scroll-watch",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){var n=e.prototype;return e.defaults.scrollThreshold=400,e.create.scrollWatch=function(){this.pageScrollHandler=this.onPageScroll.bind(this),this.resizeHandler=this.onResize.bind(this);var t=this.options.scrollThreshold,e=t||0===t;e&&this.enableScrollWatch()},e.destroy.scrollWatch=function(){this.disableScrollWatch()},n.enableScrollWatch=function(){this.isScrollWatching||(this.isScrollWatching=!0,this.updateMeasurements(),this.updateScroller(),this.on("last",this.disableScrollWatch),this.bindScrollWatchEvents(!0))},n.disableScrollWatch=function(){this.isScrollWatching&&(this.bindScrollWatchEvents(!1),delete this.isScrollWatching)},n.bindScrollWatchEvents=function(e){var i=e?"addEventListener":"removeEventListener";this.scroller[i]("scroll",this.pageScrollHandler),t[i]("resize",this.resizeHandler)},n.onPageScroll=e.throttle(function(){var t=this.getBottomDistance();t<=this.options.scrollThreshold&&this.dispatchEvent("scrollThreshold")}),n.getBottomDistance=function(){return this.options.elementScroll?this.getElementBottomDistance():this.getWindowBottomDistance()},n.getWindowBottomDistance=function(){var e=this.top+this.element.clientHeight,i=t.pageYOffset+this.windowHeight;return e-i},n.getElementBottomDistance=function(){var t=this.scroller.scrollHeight,e=this.scroller.scrollTop+this.scroller.clientHeight;return t-e},n.onResize=function(){this.updateMeasurements()},i.debounceMethod(e,"onResize",150),e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/history",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){var n=e.prototype;e.defaults.history="replace";var o=document.createElement("a");return e.create.history=function(){if(this.options.history){o.href=this.getAbsolutePath();var t=o.origin||o.protocol+"//"+o.host,e=t==location.origin;return e?void(this.options.append?this.createHistoryAppend():this.createHistoryPageLoad()):void console.error("[InfiniteScroll] cannot set history with different origin: "+o.origin+" on "+location.origin+" . History behavior disabled.")}},n.createHistoryAppend=function(){this.updateMeasurements(),this.updateScroller(),this.scrollPages=[{top:0,path:location.href,title:document.title}],this.scrollPageIndex=0,this.scrollHistoryHandler=this.onScrollHistory.bind(this),this.unloadHandler=this.onUnload.bind(this),this.scroller.addEventListener("scroll",this.scrollHistoryHandler),this.on("append",this.onAppendHistory),this.bindHistoryAppendEvents(!0)},n.bindHistoryAppendEvents=function(e){var i=e?"addEventListener":"removeEventListener";this.scroller[i]("scroll",this.scrollHistoryHandler),t[i]("unload",this.unloadHandler)},n.createHistoryPageLoad=function(){this.on("load",this.onPageLoadHistory)},e.destroy.history=n.destroyHistory=function(){var t=this.options.history&&this.options.append;t&&this.bindHistoryAppendEvents(!1)},n.onAppendHistory=function(t,e,i){if(i&&i.length){var n=i[0],r=this.getElementScrollY(n);o.href=e,this.scrollPages.push({top:r,path:o.href,title:t.title})}},n.getElementScrollY=function(t){return this.options.elementScroll?this.getElementElementScrollY(t):this.getElementWindowScrollY(t)},n.getElementWindowScrollY=function(e){var i=e.getBoundingClientRect();return i.top+t.pageYOffset},n.getElementElementScrollY=function(t){return t.offsetTop-this.top},n.onScrollHistory=function(){for(var t,e,i=this.getScrollViewY(),n=0;n=i)break;t=n,e=o}t!=this.scrollPageIndex&&(this.scrollPageIndex=t,this.setHistory(e.title,e.path))},i.debounceMethod(e,"onScrollHistory",150),n.getScrollViewY=function(){return this.options.elementScroll?this.scroller.scrollTop+this.scroller.clientHeight/2:t.pageYOffset+this.windowHeight/2},n.setHistory=function(t,e){var i=this.options.history,n=i&&history[i+"State"];n&&(history[i+"State"](null,t,e),this.options.historyTitle&&(document.title=t),this.dispatchEvent("history",null,[t,e]))},n.onUnload=function(){var e=this.scrollPageIndex;if(0!==e){var i=this.scrollPages[e],n=t.pageYOffset-i.top+this.top;this.destroyHistory(),scrollTo(0,n)}},n.onPageLoadHistory=function(t,e){this.setHistory(t.title,e)},e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/button",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){function n(t,e){this.element=t,this.infScroll=e,this.clickHandler=this.onClick.bind(this),this.element.addEventListener("click",this.clickHandler),e.on("request",this.disable.bind(this)),e.on("load",this.enable.bind(this)),e.on("error",this.hide.bind(this)),e.on("last",this.hide.bind(this))}return e.create.button=function(){var t=i.getQueryElement(this.options.button);if(t)return void(this.button=new n(t,this))},e.destroy.button=function(){this.button&&this.button.destroy()},n.prototype.onClick=function(t){t.preventDefault(),this.infScroll.loadNextPage()},n.prototype.enable=function(){this.element.removeAttribute("disabled")},n.prototype.disable=function(){this.element.disabled="disabled"},n.prototype.hide=function(){this.element.style.display="none"},n.prototype.destroy=function(){this.element.removeEventListener("click",this.clickHandler)},e.Button=n,e}),function(t,e){"function"==typeof define&&define.amd?define("infinite-scroll/js/status",["./core","fizzy-ui-utils/utils"],function(i,n){return e(t,i,n)}):"object"==typeof module&&module.exports?module.exports=e(t,require("./core"),require("fizzy-ui-utils")):e(t,t.InfiniteScroll,t.fizzyUIUtils)}(window,function(t,e,i){function n(t){r(t,"none")}function o(t){r(t,"block")}function r(t,e){t&&(t.style.display=e)}var s=e.prototype;return e.create.status=function(){var t=i.getQueryElement(this.options.status);t&&(this.statusElement=t,this.statusEventElements={request:t.querySelector(".infinite-scroll-request"),error:t.querySelector(".infinite-scroll-error"),last:t.querySelector(".infinite-scroll-last")},this.on("request",this.showRequestStatus),this.on("error",this.showErrorStatus),this.on("last",this.showLastStatus),this.bindHideStatus("on"))},s.bindHideStatus=function(t){var e=this.options.append?"append":"load";this[t](e,this.hideAllStatus)},s.showRequestStatus=function(){this.showStatus("request")},s.showErrorStatus=function(){this.showStatus("error")},s.showLastStatus=function(){this.showStatus("last"),this.bindHideStatus("off")},s.showStatus=function(t){o(this.statusElement),this.hideStatusEventElements();var e=this.statusEventElements[t];o(e)},s.hideAllStatus=function(){n(this.statusElement),this.hideStatusEventElements()},s.hideStatusEventElements=function(){for(var t in this.statusEventElements){var e=this.statusEventElements[t];n(e)}},e}),function(t,e){"function"==typeof define&&define.amd?define(["infinite-scroll/js/core","infinite-scroll/js/page-load","infinite-scroll/js/scroll-watch","infinite-scroll/js/history","infinite-scroll/js/button","infinite-scroll/js/status"],e):"object"==typeof module&&module.exports&&(module.exports=e(require("./core"),require("./page-load"),require("./scroll-watch"),require("./history"),require("./button"),require("./status")))}(window,function(t){return t}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("imagesloaded/imagesloaded",["ev-emitter/ev-emitter"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}("undefined"!=typeof window?window:this,function(t,e){function i(t,e){for(var i in e)t[i]=e[i];return t}function n(t){if(Array.isArray(t))return t;var e="object"==typeof t&&"number"==typeof t.length;return e?h.call(t):[t]}function o(t,e,r){if(!(this instanceof o))return new o(t,e,r);var s=t;return"string"==typeof t&&(s=document.querySelectorAll(t)),s?(this.elements=n(s),this.options=i({},this.options),"function"==typeof e?r=e:i(this.options,e),r&&this.on("always",r),this.getImages(),l&&(this.jqDeferred=new l.Deferred),void setTimeout(this.check.bind(this))):void a.error("Bad element for imagesLoaded "+(s||t))}function r(t){this.img=t}function s(t,e){this.url=t,this.element=e,this.img=new Image}var l=t.jQuery,a=t.console,h=Array.prototype.slice;o.prototype=Object.create(e.prototype),o.prototype.options={},o.prototype.getImages=function(){this.images=[],this.elements.forEach(this.addElementImages,this)},o.prototype.addElementImages=function(t){"IMG"==t.nodeName&&this.addImage(t),this.options.background===!0&&this.addElementBackgroundImages(t);var e=t.nodeType;if(e&&c[e]){for(var i=t.querySelectorAll("img"),n=0;n this.isotope.size.innerHeight ) { + this.y = 0; + this.x = this.maxX; + } + + var position = { + x: this.x, + y: this.y + }; + + this.maxX = Math.max( this.maxX, this.x + item.size.outerWidth ); + this.y += item.size.outerHeight; + + return position; + }; + + proto._getContainerSize = function() { + return { width: this.maxX }; + }; + + proto.needsResizeLayout = function() { + return this.needsVerticalResizeLayout(); + }; + + return FitColumns; + +})); diff --git a/cps/static/js/libs/soundmanager2.js b/cps/static/js/libs/soundmanager2.js new file mode 100644 index 00000000..87a751d3 --- /dev/null +++ b/cps/static/js/libs/soundmanager2.js @@ -0,0 +1,6294 @@ +/** @license + * + * SoundManager 2: JavaScript Sound for the Web + * ---------------------------------------------- + * http://schillmania.com/projects/soundmanager2/ + * + * Copyright (c) 2007, Scott Schiller. All rights reserved. + * Code provided under the BSD License: + * http://schillmania.com/projects/soundmanager2/license.txt + * + * V2.97a.20170601 + */ + +/** + * About this file + * ------------------------------------------------------------------------------------- + * This is the fully-commented source version of the SoundManager 2 API, + * recommended for use during development and testing. + * + * See soundmanager2-nodebug-jsmin.js for an optimized build (~11KB with gzip.) + * http://schillmania.com/projects/soundmanager2/doc/getstarted/#basic-inclusion + * Alternately, serve this file with gzip for 75% compression savings (~30KB over HTTP.) + * + * You may notice and comments in this source; these are delimiters for + * debug blocks which are removed in the -nodebug builds, further optimizing code size. + * + * Also, as you may note: Whoa, reliable cross-platform/device audio support is hard! ;) + */ + +(function SM2(window, _undefined) { + +/* global Audio, document, window, navigator, define, module, SM2_DEFER, opera, setTimeout, setInterval, clearTimeout, sm2Debugger */ + +'use strict'; + +if (!window || !window.document) { + + // Don't cross the [environment] streams. SM2 expects to be running in a browser, not under node.js etc. + // Additionally, if a browser somehow manages to fail this test, as Egon said: "It would be bad." + + throw new Error('SoundManager requires a browser with window and document objects.'); + +} + +var soundManager = null; + +/** + * The SoundManager constructor. + * + * @constructor + * @param {string} smURL Optional: Path to SWF files + * @param {string} smID Optional: The ID to use for the SWF container element + * @this {SoundManager} + * @return {SoundManager} The new SoundManager instance + */ + +function SoundManager(smURL, smID) { + + /** + * soundManager configuration options list + * defines top-level configuration properties to be applied to the soundManager instance (eg. soundManager.flashVersion) + * to set these properties, use the setup() method - eg., soundManager.setup({url: '/swf/', flashVersion: 9}) + */ + + this.setupOptions = { + + url: (smURL || null), // path (directory) where SoundManager 2 SWFs exist, eg., /path/to/swfs/ + flashVersion: 8, // flash build to use (8 or 9.) Some API features require 9. + debugMode: true, // enable debugging output (console.log() with HTML fallback) + debugFlash: false, // enable debugging output inside SWF, troubleshoot Flash/browser issues + useConsole: true, // use console.log() if available (otherwise, writes to #soundmanager-debug element) + consoleOnly: true, // if console is being used, do not create/write to #soundmanager-debug + waitForWindowLoad: false, // force SM2 to wait for window.onload() before trying to call soundManager.onload() + bgColor: '#ffffff', // SWF background color. N/A when wmode = 'transparent' + useHighPerformance: false, // position:fixed flash movie can help increase js/flash speed, minimize lag + flashPollingInterval: null, // msec affecting whileplaying/loading callback frequency. If null, default of 50 msec is used. + html5PollingInterval: null, // msec affecting whileplaying() for HTML5 audio, excluding mobile devices. If null, native HTML5 update events are used. + flashLoadTimeout: 1000, // msec to wait for flash movie to load before failing (0 = infinity) + wmode: null, // flash rendering mode - null, 'transparent', or 'opaque' (last two allow z-index to work) + allowScriptAccess: 'always', // for scripting the SWF (object/embed property), 'always' or 'sameDomain' + useFlashBlock: false, // *requires flashblock.css, see demos* - allow recovery from flash blockers. Wait indefinitely and apply timeout CSS to SWF, if applicable. + useHTML5Audio: true, // use HTML5 Audio() where API is supported (most Safari, Chrome versions), Firefox (MP3/MP4 support varies.) Ideally, transparent vs. Flash API where possible. + forceUseGlobalHTML5Audio: false, // if true, a single Audio() object is used for all sounds - and only one can play at a time. + ignoreMobileRestrictions: false, // if true, SM2 will not apply global HTML5 audio rules to mobile UAs. iOS > 7 and WebViews may allow multiple Audio() instances. + html5Test: /^(probably|maybe)$/i, // HTML5 Audio() format support test. Use /^probably$/i; if you want to be more conservative. + preferFlash: false, // overrides useHTML5audio, will use Flash for MP3/MP4/AAC if present. Potential option if HTML5 playback with these formats is quirky. + noSWFCache: false, // if true, appends ?ts={date} to break aggressive SWF caching. + idPrefix: 'sound' // if an id is not provided to createSound(), this prefix is used for generated IDs - 'sound0', 'sound1' etc. + + }; + + this.defaultOptions = { + + /** + * the default configuration for sound objects made with createSound() and related methods + * eg., volume, auto-load behaviour and so forth + */ + + autoLoad: false, // enable automatic loading (otherwise .load() will be called on demand with .play(), the latter being nicer on bandwidth - if you want to .load yourself, you also can) + autoPlay: false, // enable playing of file as soon as possible (much faster if "stream" is true) + from: null, // position to start playback within a sound (msec), default = beginning + loops: 1, // how many times to repeat the sound (position will wrap around to 0, setPosition() will break out of loop when >0) + onid3: null, // callback function for "ID3 data is added/available" + onerror: null, // callback function for "load failed" (or, playback/network/decode error under HTML5.) + onload: null, // callback function for "load finished" + whileloading: null, // callback function for "download progress update" (X of Y bytes received) + onplay: null, // callback for "play" start + onpause: null, // callback for "pause" + onresume: null, // callback for "resume" (pause toggle) + whileplaying: null, // callback during play (position update) + onposition: null, // object containing times and function callbacks for positions of interest + onstop: null, // callback for "user stop" + onfinish: null, // callback function for "sound finished playing" + multiShot: true, // let sounds "restart" or layer on top of each other when played multiple times, rather than one-shot/one at a time + multiShotEvents: false, // fire multiple sound events (currently onfinish() only) when multiShot is enabled + position: null, // offset (milliseconds) to seek to within loaded sound data. + pan: 0, // "pan" settings, left-to-right, -100 to 100 + playbackRate: 1, // rate at which to play the sound (HTML5-only) + stream: true, // allows playing before entire file has loaded (recommended) + to: null, // position to end playback within a sound (msec), default = end + type: null, // MIME-like hint for file pattern / canPlay() tests, eg. audio/mp3 + usePolicyFile: false, // enable crossdomain.xml request for audio on remote domains (for ID3/waveform access) + volume: 100 // self-explanatory. 0-100, the latter being the max. + + }; + + this.flash9Options = { + + /** + * flash 9-only options, + * merged into defaultOptions if flash 9 is being used + */ + + onfailure: null, // callback function for when playing fails (Flash 9, MovieStar + RTMP-only) + isMovieStar: null, // "MovieStar" MPEG4 audio mode. Null (default) = auto detect MP4, AAC etc. based on URL. true = force on, ignore URL + usePeakData: false, // enable left/right channel peak (level) data + useWaveformData: false, // enable sound spectrum (raw waveform data) - NOTE: May increase CPU load. + useEQData: false, // enable sound EQ (frequency spectrum data) - NOTE: May increase CPU load. + onbufferchange: null, // callback for "isBuffering" property change + ondataerror: null // callback for waveform/eq data access error (flash playing audio in other tabs/domains) + + }; + + this.movieStarOptions = { + + /** + * flash 9.0r115+ MPEG4 audio options, + * merged into defaultOptions if flash 9+movieStar mode is enabled + */ + + bufferTime: 3, // seconds of data to buffer before playback begins (null = flash default of 0.1 seconds - if AAC playback is gappy, try increasing.) + serverURL: null, // rtmp: FMS or FMIS server to connect to, required when requesting media via RTMP or one of its variants + onconnect: null, // rtmp: callback for connection to flash media server + duration: null // rtmp: song duration (msec) + + }; + + this.audioFormats = { + + /** + * determines HTML5 support + flash requirements. + * if no support (via flash and/or HTML5) for a "required" format, SM2 will fail to start. + * flash fallback is used for MP3 or MP4 if HTML5 can't play it (or if preferFlash = true) + */ + + mp3: { + type: ['audio/mpeg; codecs="mp3"', 'audio/mpeg', 'audio/mp3', 'audio/MPA', 'audio/mpa-robust'], + required: true + }, + + mp4: { + related: ['aac', 'm4a', 'm4b'], // additional formats under the MP4 container + type: ['audio/mp4; codecs="mp4a.40.2"', 'audio/aac', 'audio/x-m4a', 'audio/MP4A-LATM', 'audio/mpeg4-generic'], + required: false + }, + + ogg: { + type: ['audio/ogg; codecs=vorbis'], + required: false + }, + + opus: { + type: ['audio/ogg; codecs=opus', 'audio/opus'], + required: false + }, + + wav: { + type: ['audio/wav; codecs="1"', 'audio/wav', 'audio/wave', 'audio/x-wav'], + required: false + }, + + flac: { + type: ['audio/flac'], + required: false + } + + }; + + // HTML attributes (id + class names) for the SWF container + + this.movieID = 'sm2-container'; + this.id = (smID || 'sm2movie'); + + this.debugID = 'soundmanager-debug'; + this.debugURLParam = /([#?&])debug=1/i; + + // dynamic attributes + + this.versionNumber = 'V2.97a.20170601'; + this.version = null; + this.movieURL = null; + this.altURL = null; + this.swfLoaded = false; + this.enabled = false; + this.oMC = null; + this.sounds = {}; + this.soundIDs = []; + this.muted = false; + this.didFlashBlock = false; + this.filePattern = null; + + this.filePatterns = { + flash8: /\.mp3(\?.*)?$/i, + flash9: /\.mp3(\?.*)?$/i + }; + + // support indicators, set at init + + this.features = { + buffering: false, + peakData: false, + waveformData: false, + eqData: false, + movieStar: false + }; + + // flash sandbox info, used primarily in troubleshooting + + this.sandbox = { + // + type: null, + types: { + remote: 'remote (domain-based) rules', + localWithFile: 'local with file access (no internet access)', + localWithNetwork: 'local with network (internet access only, no local access)', + localTrusted: 'local, trusted (local+internet access)' + }, + description: null, + noRemote: null, + noLocal: null + // + }; + + /** + * format support (html5/flash) + * stores canPlayType() results based on audioFormats. + * eg. { mp3: boolean, mp4: boolean } + * treat as read-only. + */ + + this.html5 = { + usingFlash: null // set if/when flash fallback is needed + }; + + // file type support hash + this.flash = {}; + + // determined at init time + this.html5Only = false; + + // used for special cases (eg. iPad/iPhone/palm OS?) + this.ignoreFlash = false; + + /** + * a few private internals (OK, a lot. :D) + */ + + var SMSound, + sm2 = this, globalHTML5Audio = null, flash = null, sm = 'soundManager', smc = sm + ': ', h5 = 'HTML5::', id, ua = navigator.userAgent, wl = window.location.href.toString(), doc = document, doNothing, setProperties, init, fV, on_queue = [], debugOpen = true, debugTS, didAppend = false, appendSuccess = false, didInit = false, disabled = false, windowLoaded = false, _wDS, wdCount = 0, initComplete, mixin, assign, extraOptions, addOnEvent, processOnEvents, initUserOnload, delayWaitForEI, waitForEI, rebootIntoHTML5, setVersionInfo, handleFocus, strings, initMovie, domContentLoaded, winOnLoad, didDCLoaded, getDocument, createMovie, catchError, setPolling, initDebug, debugLevels = ['log', 'info', 'warn', 'error'], defaultFlashVersion = 8, disableObject, failSafely, normalizeMovieURL, oRemoved = null, oRemovedHTML = null, str, flashBlockHandler, getSWFCSS, swfCSS, toggleDebug, loopFix, policyFix, complain, idCheck, waitingForEI = false, initPending = false, startTimer, stopTimer, timerExecute, h5TimerCount = 0, h5IntervalTimer = null, parseURL, messages = [], + canIgnoreFlash, needsFlash = null, featureCheck, html5OK, html5CanPlay, html5ErrorCodes, html5Ext, html5Unload, domContentLoadedIE, testHTML5, event, slice = Array.prototype.slice, useGlobalHTML5Audio = false, lastGlobalHTML5URL, hasFlash, detectFlash, badSafariFix, html5_events, showSupport, flushMessages, wrapCallback, idCounter = 0, didSetup, msecScale = 1000, + is_iDevice = ua.match(/(ipad|iphone|ipod)/i), isAndroid = ua.match(/android/i), isIE = ua.match(/msie|trident/i), + isWebkit = ua.match(/webkit/i), + isSafari = (ua.match(/safari/i) && !ua.match(/chrome/i)), + isOpera = (ua.match(/opera/i)), + mobileHTML5 = (ua.match(/(mobile|pre\/|xoom)/i) || is_iDevice || isAndroid), + isBadSafari = (!wl.match(/usehtml5audio/i) && !wl.match(/sm2-ignorebadua/i) && isSafari && !ua.match(/silk/i) && ua.match(/OS\sX\s10_6_([3-7])/i)), // Safari 4 and 5 (excluding Kindle Fire, "Silk") occasionally fail to load/play HTML5 audio on Snow Leopard 10.6.3 through 10.6.7 due to bug(s) in QuickTime X and/or other underlying frameworks. :/ Confirmed bug. https://bugs.webkit.org/show_bug.cgi?id=32159 + hasConsole = (window.console !== _undefined && console.log !== _undefined), + isFocused = (doc.hasFocus !== _undefined ? doc.hasFocus() : null), + tryInitOnFocus = (isSafari && (doc.hasFocus === _undefined || !doc.hasFocus())), + okToDisable = !tryInitOnFocus, + flashMIME = /(mp3|mp4|mpa|m4a|m4b)/i, + emptyURL = 'about:blank', // safe URL to unload, or load nothing from (flash 8 + most HTML5 UAs) + emptyWAV = 'data:audio/wave;base64,/UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAD//w==', // tiny WAV for HTML5 unloading + overHTTP = (doc.location ? doc.location.protocol.match(/http/i) : null), + http = (!overHTTP ? '//' : ''), + // mp3, mp4, aac etc. + netStreamMimeTypes = /^\s*audio\/(?:x-)?(?:mpeg4|aac|flv|mov|mp4|m4v|m4a|m4b|mp4v|3gp|3g2)\s*(?:$|;)/i, + // Flash v9.0r115+ "moviestar" formats + netStreamTypes = ['mpeg4', 'aac', 'flv', 'mov', 'mp4', 'm4v', 'f4v', 'm4a', 'm4b', 'mp4v', '3gp', '3g2'], + netStreamPattern = new RegExp('\\.(' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i'); + + this.mimePattern = /^\s*audio\/(?:x-)?(?:mp(?:eg|3))\s*(?:$|;)/i; // default mp3 set + + // use altURL if not "online" + this.useAltURL = !overHTTP; + + swfCSS = { + swfBox: 'sm2-object-box', + swfDefault: 'movieContainer', + swfError: 'swf_error', // SWF loaded, but SM2 couldn't start (other error) + swfTimedout: 'swf_timedout', + swfLoaded: 'swf_loaded', + swfUnblocked: 'swf_unblocked', // or loaded OK + sm2Debug: 'sm2_debug', + highPerf: 'high_performance', + flashDebug: 'flash_debug' + }; + + /** + * HTML5 error codes, per W3C + * Error code 1, MEDIA_ERR_ABORTED: Client aborted download at user's request. + * Error code 2, MEDIA_ERR_NETWORK: A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable. + * Error code 3, MEDIA_ERR_DECODE: An error of some description occurred while decoding the media resource, after the resource was established to be usable. + * Error code 4, MEDIA_ERR_SRC_NOT_SUPPORTED: Media (audio file) not supported ("not usable.") + * Reference: https://html.spec.whatwg.org/multipage/embedded-content.html#error-codes + */ + html5ErrorCodes = [ + null, + 'MEDIA_ERR_ABORTED', + 'MEDIA_ERR_NETWORK', + 'MEDIA_ERR_DECODE', + 'MEDIA_ERR_SRC_NOT_SUPPORTED' + ]; + + /** + * basic HTML5 Audio() support test + * try...catch because of IE 9 "not implemented" nonsense + * https://github.com/Modernizr/Modernizr/issues/224 + */ + + this.hasHTML5 = (function() { + try { + // new Audio(null) for stupid Opera 9.64 case, which throws not_enough_arguments exception otherwise. + return (Audio !== _undefined && (isOpera && opera !== _undefined && opera.version() < 10 ? new Audio(null) : new Audio()).canPlayType !== _undefined); + } catch(e) { + return false; + } + }()); + + /** + * Public SoundManager API + * ----------------------- + */ + + /** + * Configures top-level soundManager properties. + * + * @param {object} options Option parameters, eg. { flashVersion: 9, url: '/path/to/swfs/' } + * onready and ontimeout are also accepted parameters. call soundManager.setup() to see the full list. + */ + + this.setup = function(options) { + + var noURL = (!sm2.url); + + // warn if flash options have already been applied + + if (options !== _undefined && didInit && needsFlash && sm2.ok() && (options.flashVersion !== _undefined || options.url !== _undefined || options.html5Test !== _undefined)) { + complain(str('setupLate')); + } + + // TODO: defer: true? + + assign(options); + + if (!useGlobalHTML5Audio) { + + if (mobileHTML5) { + + // force the singleton HTML5 pattern on mobile, by default. + if (!sm2.setupOptions.ignoreMobileRestrictions || sm2.setupOptions.forceUseGlobalHTML5Audio) { + messages.push(strings.globalHTML5); + useGlobalHTML5Audio = true; + } + + } else if (sm2.setupOptions.forceUseGlobalHTML5Audio) { + + // only apply singleton HTML5 on desktop if forced. + messages.push(strings.globalHTML5); + useGlobalHTML5Audio = true; + + } + + } + + if (!didSetup && mobileHTML5) { + + if (sm2.setupOptions.ignoreMobileRestrictions) { + + messages.push(strings.ignoreMobile); + + } else { + + // prefer HTML5 for mobile + tablet-like devices, probably more reliable vs. flash at this point. + + // + if (!sm2.setupOptions.useHTML5Audio || sm2.setupOptions.preferFlash) { + // notify that defaults are being changed. + sm2._wD(strings.mobileUA); + } + // + + sm2.setupOptions.useHTML5Audio = true; + sm2.setupOptions.preferFlash = false; + + if (is_iDevice) { + + // no flash here. + sm2.ignoreFlash = true; + + } else if ((isAndroid && !ua.match(/android\s2\.3/i)) || !isAndroid) { + + /** + * Android devices tend to work better with a single audio instance, specifically for chained playback of sounds in sequence. + * Common use case: exiting sound onfinish() -> createSound() -> play() + * Presuming similar restrictions for other mobile, non-Android, non-iOS devices. + */ + + // + sm2._wD(strings.globalHTML5); + // + + useGlobalHTML5Audio = true; + + } + + } + + } + + // special case 1: "Late setup". SM2 loaded normally, but user didn't assign flash URL eg., setup({url:...}) before SM2 init. Treat as delayed init. + + if (options) { + + if (noURL && didDCLoaded && options.url !== _undefined) { + sm2.beginDelayedInit(); + } + + // special case 2: If lazy-loading SM2 (DOMContentLoaded has already happened) and user calls setup() with url: parameter, try to init ASAP. + + if (!didDCLoaded && options.url !== _undefined && doc.readyState === 'complete') { + setTimeout(domContentLoaded, 1); + } + + } + + didSetup = true; + + return sm2; + + }; + + this.ok = function() { + + return (needsFlash ? (didInit && !disabled) : (sm2.useHTML5Audio && sm2.hasHTML5)); + + }; + + this.supported = this.ok; // legacy + + this.getMovie = function(movie_id) { + + // safety net: some old browsers differ on SWF references, possibly related to ExternalInterface / flash version + return id(movie_id) || doc[movie_id] || window[movie_id]; + + }; + + /** + * Creates a SMSound sound object instance. Can also be overloaded, e.g., createSound('mySound', '/some.mp3'); + * + * @param {object} oOptions Sound options (at minimum, url parameter is required.) + * @return {object} SMSound The new SMSound object. + */ + + this.createSound = function(oOptions, _url) { + + var cs, cs_string, options, oSound = null; + + // + cs = sm + '.createSound(): '; + cs_string = cs + str(!didInit ? 'notReady' : 'notOK'); + // + + if (!didInit || !sm2.ok()) { + complain(cs_string); + return false; + } + + if (_url !== _undefined) { + // function overloading in JS! :) ... assume simple createSound(id, url) use case. + oOptions = { + id: oOptions, + url: _url + }; + } + + // inherit from defaultOptions + options = mixin(oOptions); + + options.url = parseURL(options.url); + + // generate an id, if needed. + if (options.id === _undefined) { + options.id = sm2.setupOptions.idPrefix + (idCounter++); + } + + // + if (options.id.toString().charAt(0).match(/^[0-9]$/)) { + sm2._wD(cs + str('badID', options.id), 2); + } + + sm2._wD(cs + options.id + (options.url ? ' (' + options.url + ')' : ''), 1); + // + + if (idCheck(options.id, true)) { + sm2._wD(cs + options.id + ' exists', 1); + return sm2.sounds[options.id]; + } + + function make() { + + options = loopFix(options); + sm2.sounds[options.id] = new SMSound(options); + sm2.soundIDs.push(options.id); + return sm2.sounds[options.id]; + + } + + if (html5OK(options)) { + + oSound = make(); + // + if (!sm2.html5Only) { + sm2._wD(options.id + ': Using HTML5'); + } + // + oSound._setup_html5(options); + + } else { + + if (sm2.html5Only) { + sm2._wD(options.id + ': No HTML5 support for this sound, and no Flash. Exiting.'); + return make(); + } + + // TODO: Move HTML5/flash checks into generic URL parsing/handling function. + + if (sm2.html5.usingFlash && options.url && options.url.match(/data:/i)) { + // data: URIs not supported by Flash, either. + sm2._wD(options.id + ': data: URIs not supported via Flash. Exiting.'); + return make(); + } + + if (fV > 8) { + if (options.isMovieStar === null) { + // attempt to detect MPEG-4 formats + options.isMovieStar = !!(options.serverURL || (options.type ? options.type.match(netStreamMimeTypes) : false) || (options.url && options.url.match(netStreamPattern))); + } + // + if (options.isMovieStar) { + sm2._wD(cs + 'using MovieStar handling'); + if (options.loops > 1) { + _wDS('noNSLoop'); + } + } + // + } + + options = policyFix(options, cs); + oSound = make(); + + if (fV === 8) { + flash._createSound(options.id, options.loops || 1, options.usePolicyFile); + } else { + flash._createSound(options.id, options.url, options.usePeakData, options.useWaveformData, options.useEQData, options.isMovieStar, (options.isMovieStar ? options.bufferTime : false), options.loops || 1, options.serverURL, options.duration || null, options.autoPlay, true, options.autoLoad, options.usePolicyFile); + if (!options.serverURL) { + // We are connected immediately + oSound.connected = true; + if (options.onconnect) { + options.onconnect.apply(oSound); + } + } + } + + if (!options.serverURL && (options.autoLoad || options.autoPlay)) { + // call load for non-rtmp streams + oSound.load(options); + } + + } + + // rtmp will play in onconnect + if (!options.serverURL && options.autoPlay) { + oSound.play(); + } + + return oSound; + + }; + + /** + * Destroys a SMSound sound object instance. + * + * @param {string} sID The ID of the sound to destroy + */ + + this.destroySound = function(sID, _bFromSound) { + + // explicitly destroy a sound before normal page unload, etc. + + if (!idCheck(sID)) return false; + + var oS = sm2.sounds[sID], i; + + oS.stop(); + + // Disable all callbacks after stop(), when the sound is being destroyed + oS._iO = {}; + + oS.unload(); + + for (i = 0; i < sm2.soundIDs.length; i++) { + if (sm2.soundIDs[i] === sID) { + sm2.soundIDs.splice(i, 1); + break; + } + } + + if (!_bFromSound) { + // ignore if being called from SMSound instance + oS.destruct(true); + } + + oS = null; + delete sm2.sounds[sID]; + + return true; + + }; + + /** + * Calls the load() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @param {object} oOptions Optional: Sound options + */ + + this.load = function(sID, oOptions) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].load(oOptions); + + }; + + /** + * Calls the unload() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + */ + + this.unload = function(sID) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].unload(); + + }; + + /** + * Calls the onPosition() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @param {number} nPosition The position to watch for + * @param {function} oMethod The relevant callback to fire + * @param {object} oScope Optional: The scope to apply the callback to + * @return {SMSound} The SMSound object + */ + + this.onPosition = function(sID, nPosition, oMethod, oScope) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].onposition(nPosition, oMethod, oScope); + + }; + + // legacy/backwards-compability: lower-case method name + this.onposition = this.onPosition; + + /** + * Calls the clearOnPosition() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @param {number} nPosition The position to watch for + * @param {function} oMethod Optional: The relevant callback to fire + * @return {SMSound} The SMSound object + */ + + this.clearOnPosition = function(sID, nPosition, oMethod) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].clearOnPosition(nPosition, oMethod); + + }; + + /** + * Calls the play() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @param {object} oOptions Optional: Sound options + * @return {SMSound} The SMSound object + */ + + this.play = function(sID, oOptions) { + + var result = null, + // legacy function-overloading use case: play('mySound', '/path/to/some.mp3'); + overloaded = (oOptions && !(oOptions instanceof Object)); + + if (!didInit || !sm2.ok()) { + complain(sm + '.play(): ' + str(!didInit ? 'notReady' : 'notOK')); + return false; + } + + if (!idCheck(sID, overloaded)) { + + // no sound found for the given ID. Bail. + if (!overloaded) return false; + + if (overloaded) { + oOptions = { + url: oOptions + }; + } + + if (oOptions && oOptions.url) { + // overloading use case, create+play: .play('someID', {url:'/path/to.mp3'}); + sm2._wD(sm + '.play(): Attempting to create "' + sID + '"', 1); + oOptions.id = sID; + result = sm2.createSound(oOptions).play(); + } + + } else if (overloaded) { + + // existing sound object case + oOptions = { + url: oOptions + }; + + } + + if (result === null) { + // default case + result = sm2.sounds[sID].play(oOptions); + } + + return result; + + }; + + // just for convenience + this.start = this.play; + + /** + * Calls the setPlaybackRate() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.setPlaybackRate = function(sID, rate, allowOverride) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].setPlaybackRate(rate, allowOverride); + + }; + + /** + * Calls the setPosition() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @param {number} nMsecOffset Position (milliseconds) + * @return {SMSound} The SMSound object + */ + + this.setPosition = function(sID, nMsecOffset) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].setPosition(nMsecOffset); + + }; + + /** + * Calls the stop() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.stop = function(sID) { + + if (!idCheck(sID)) return false; + + sm2._wD(sm + '.stop(' + sID + ')', 1); + + return sm2.sounds[sID].stop(); + + }; + + /** + * Stops all currently-playing sounds. + */ + + this.stopAll = function() { + + var oSound; + sm2._wD(sm + '.stopAll()', 1); + + for (oSound in sm2.sounds) { + if (sm2.sounds.hasOwnProperty(oSound)) { + // apply only to sound objects + sm2.sounds[oSound].stop(); + } + } + + }; + + /** + * Calls the pause() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.pause = function(sID) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].pause(); + + }; + + /** + * Pauses all currently-playing sounds. + */ + + this.pauseAll = function() { + + var i; + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + sm2.sounds[sm2.soundIDs[i]].pause(); + } + + }; + + /** + * Calls the resume() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.resume = function(sID) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].resume(); + + }; + + /** + * Resumes all currently-paused sounds. + */ + + this.resumeAll = function() { + + var i; + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + sm2.sounds[sm2.soundIDs[i]].resume(); + } + + }; + + /** + * Calls the togglePause() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.togglePause = function(sID) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].togglePause(); + + }; + + /** + * Calls the setPan() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @param {number} nPan The pan value (-100 to 100) + * @return {SMSound} The SMSound object + */ + + this.setPan = function(sID, nPan) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].setPan(nPan); + + }; + + /** + * Calls the setVolume() method of a SMSound object by ID + * Overloaded case: pass only volume argument eg., setVolume(50) to apply to all sounds. + * + * @param {string} sID The ID of the sound + * @param {number} nVol The volume value (0 to 100) + * @return {SMSound} The SMSound object + */ + + this.setVolume = function(sID, nVol) { + + // setVolume(50) function overloading case - apply to all sounds + + var i, j; + + if (sID !== _undefined && !isNaN(sID) && nVol === _undefined) { + for (i = 0, j = sm2.soundIDs.length; i < j; i++) { + sm2.sounds[sm2.soundIDs[i]].setVolume(sID); + } + return false; + } + + // setVolume('mySound', 50) case + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].setVolume(nVol); + + }; + + /** + * Calls the mute() method of either a single SMSound object by ID, or all sound objects. + * + * @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.) + */ + + this.mute = function(sID) { + + var i = 0; + + if (sID instanceof String) { + sID = null; + } + + if (!sID) { + + sm2._wD(sm + '.mute(): Muting all sounds'); + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + sm2.sounds[sm2.soundIDs[i]].mute(); + } + sm2.muted = true; + + } else { + + if (!idCheck(sID)) return false; + + sm2._wD(sm + '.mute(): Muting "' + sID + '"'); + return sm2.sounds[sID].mute(); + + } + + return true; + + }; + + /** + * Mutes all sounds. + */ + + this.muteAll = function() { + + sm2.mute(); + + }; + + /** + * Calls the unmute() method of either a single SMSound object by ID, or all sound objects. + * + * @param {string} sID Optional: The ID of the sound (if omitted, all sounds will be used.) + */ + + this.unmute = function(sID) { + + var i; + + if (sID instanceof String) { + sID = null; + } + + if (!sID) { + + sm2._wD(sm + '.unmute(): Unmuting all sounds'); + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + sm2.sounds[sm2.soundIDs[i]].unmute(); + } + sm2.muted = false; + + } else { + + if (!idCheck(sID)) return false; + + sm2._wD(sm + '.unmute(): Unmuting "' + sID + '"'); + + return sm2.sounds[sID].unmute(); + + } + + return true; + + }; + + /** + * Unmutes all sounds. + */ + + this.unmuteAll = function() { + + sm2.unmute(); + + }; + + /** + * Calls the toggleMute() method of a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.toggleMute = function(sID) { + + if (!idCheck(sID)) return false; + + return sm2.sounds[sID].toggleMute(); + + }; + + /** + * Retrieves the memory used by the flash plugin. + * + * @return {number} The amount of memory in use + */ + + this.getMemoryUse = function() { + + // flash-only + var ram = 0; + + if (flash && fV !== 8) { + ram = parseInt(flash._getMemoryUse(), 10); + } + + return ram; + + }; + + /** + * Undocumented: NOPs soundManager and all SMSound objects. + */ + + this.disable = function(bNoDisable) { + + // destroy all functions + var i; + + if (bNoDisable === _undefined) { + bNoDisable = false; + } + + // already disabled? + if (disabled) return false; + + disabled = true; + + _wDS('shutdown', 1); + + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + disableObject(sm2.sounds[sm2.soundIDs[i]]); + } + + disableObject(sm2); + + // fire "complete", despite fail + initComplete(bNoDisable); + + event.remove(window, 'load', initUserOnload); + + return true; + + }; + + /** + * Determines playability of a MIME type, eg. 'audio/mp3'. + */ + + this.canPlayMIME = function(sMIME) { + + var result; + + if (sm2.hasHTML5) { + result = html5CanPlay({ + type: sMIME + }); + } + + if (!result && needsFlash) { + // if flash 9, test netStream (movieStar) types as well. + result = (sMIME && sm2.ok() ? !!((fV > 8 ? sMIME.match(netStreamMimeTypes) : null) || sMIME.match(sm2.mimePattern)) : null); // TODO: make less "weird" (per JSLint) + } + + return result; + + }; + + /** + * Determines playability of a URL based on audio support. + * + * @param {string} sURL The URL to test + * @return {boolean} URL playability + */ + + this.canPlayURL = function(sURL) { + + var result; + + if (sm2.hasHTML5) { + result = html5CanPlay({ + url: sURL + }); + } + + if (!result && needsFlash) { + result = (sURL && sm2.ok() ? !!(sURL.match(sm2.filePattern)) : null); + } + + return result; + + }; + + /** + * Determines playability of an HTML DOM <a> object (or similar object literal) based on audio support. + * + * @param {object} oLink an HTML DOM <a> object or object literal including href and/or type attributes + * @return {boolean} URL playability + */ + + this.canPlayLink = function(oLink) { + + if (oLink.type !== _undefined && oLink.type && sm2.canPlayMIME(oLink.type)) return true; + + return sm2.canPlayURL(oLink.href); + + }; + + /** + * Retrieves a SMSound object by ID. + * + * @param {string} sID The ID of the sound + * @return {SMSound} The SMSound object + */ + + this.getSoundById = function(sID, _suppressDebug) { + + if (!sID) return null; + + var result = sm2.sounds[sID]; + + // + if (!result && !_suppressDebug) { + sm2._wD(sm + '.getSoundById(): Sound "' + sID + '" not found.', 2); + } + // + + return result; + + }; + + /** + * Queues a callback for execution when SoundManager has successfully initialized. + * + * @param {function} oMethod The callback method to fire + * @param {object} oScope Optional: The scope to apply to the callback + */ + + this.onready = function(oMethod, oScope) { + + var sType = 'onready', + result = false; + + if (typeof oMethod === 'function') { + + // + if (didInit) { + sm2._wD(str('queue', sType)); + } + // + + if (!oScope) { + oScope = window; + } + + addOnEvent(sType, oMethod, oScope); + processOnEvents(); + + result = true; + + } else { + + throw str('needFunction', sType); + + } + + return result; + + }; + + /** + * Queues a callback for execution when SoundManager has failed to initialize. + * + * @param {function} oMethod The callback method to fire + * @param {object} oScope Optional: The scope to apply to the callback + */ + + this.ontimeout = function(oMethod, oScope) { + + var sType = 'ontimeout', + result = false; + + if (typeof oMethod === 'function') { + + // + if (didInit) { + sm2._wD(str('queue', sType)); + } + // + + if (!oScope) { + oScope = window; + } + + addOnEvent(sType, oMethod, oScope); + processOnEvents({ type: sType }); + + result = true; + + } else { + + throw str('needFunction', sType); + + } + + return result; + + }; + + /** + * Writes console.log()-style debug output to a console or in-browser element. + * Applies when debugMode = true + * + * @param {string} sText The console message + * @param {object} nType Optional log level (number), or object. Number case: Log type/style where 0 = 'info', 1 = 'warn', 2 = 'error'. Object case: Object to be dumped. + */ + + this._writeDebug = function(sText, sTypeOrObject) { + + // pseudo-private console.log()-style output + // + + var sDID = 'soundmanager-debug', o, oItem; + + if (!sm2.setupOptions.debugMode) return false; + + if (hasConsole && sm2.useConsole) { + if (sTypeOrObject && typeof sTypeOrObject === 'object') { + // object passed; dump to console. + console.log(sText, sTypeOrObject); + } else if (debugLevels[sTypeOrObject] !== _undefined) { + console[debugLevels[sTypeOrObject]](sText); + } else { + console.log(sText); + } + if (sm2.consoleOnly) return true; + } + + o = id(sDID); + + if (!o) return false; + + oItem = doc.createElement('div'); + + if (++wdCount % 2 === 0) { + oItem.className = 'sm2-alt'; + } + + if (sTypeOrObject === _undefined) { + sTypeOrObject = 0; + } else { + sTypeOrObject = parseInt(sTypeOrObject, 10); + } + + oItem.appendChild(doc.createTextNode(sText)); + + if (sTypeOrObject) { + if (sTypeOrObject >= 2) { + oItem.style.fontWeight = 'bold'; + } + if (sTypeOrObject === 3) { + oItem.style.color = '#ff3333'; + } + } + + // top-to-bottom + // o.appendChild(oItem); + + // bottom-to-top + o.insertBefore(oItem, o.firstChild); + + o = null; + // + + return true; + + }; + + // + // last-resort debugging option + if (wl.indexOf('sm2-debug=alert') !== -1) { + this._writeDebug = function(sText) { + window.alert(sText); + }; + } + // + + // alias + this._wD = this._writeDebug; + + /** + * Provides debug / state information on all SMSound objects. + */ + + this._debug = function() { + + // + var i, j; + _wDS('currentObj', 1); + + for (i = 0, j = sm2.soundIDs.length; i < j; i++) { + sm2.sounds[sm2.soundIDs[i]]._debug(); + } + // + + }; + + /** + * Restarts and re-initializes the SoundManager instance. + * + * @param {boolean} resetEvents Optional: When true, removes all registered onready and ontimeout event callbacks. + * @param {boolean} excludeInit Options: When true, does not call beginDelayedInit() (which would restart SM2). + * @return {object} soundManager The soundManager instance. + */ + + this.reboot = function(resetEvents, excludeInit) { + + // reset some (or all) state, and re-init unless otherwise specified. + + // + if (sm2.soundIDs.length) { + sm2._wD('Destroying ' + sm2.soundIDs.length + ' SMSound object' + (sm2.soundIDs.length !== 1 ? 's' : '') + '...'); + } + // + + var i, j, k; + + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + sm2.sounds[sm2.soundIDs[i]].destruct(); + } + + // trash ze flash (remove from the DOM) + + if (flash) { + + try { + + if (isIE) { + oRemovedHTML = flash.innerHTML; + } + + oRemoved = flash.parentNode.removeChild(flash); + + } catch(e) { + + // Remove failed? May be due to flash blockers silently removing the SWF object/embed node from the DOM. Warn and continue. + + _wDS('badRemove', 2); + + } + + } + + // actually, force recreate of movie. + + oRemovedHTML = oRemoved = needsFlash = flash = null; + + sm2.enabled = didDCLoaded = didInit = waitingForEI = initPending = didAppend = appendSuccess = disabled = useGlobalHTML5Audio = sm2.swfLoaded = false; + + sm2.soundIDs = []; + sm2.sounds = {}; + + idCounter = 0; + didSetup = false; + + if (!resetEvents) { + // reset callbacks for onready, ontimeout etc. so that they will fire again on re-init + for (i in on_queue) { + if (on_queue.hasOwnProperty(i)) { + for (j = 0, k = on_queue[i].length; j < k; j++) { + on_queue[i][j].fired = false; + } + } + } + } else { + // remove all callbacks entirely + on_queue = []; + } + + // + if (!excludeInit) { + sm2._wD(sm + ': Rebooting...'); + } + // + + // reset HTML5 and flash canPlay test results + + sm2.html5 = { + usingFlash: null + }; + + sm2.flash = {}; + + // reset device-specific HTML/flash mode switches + + sm2.html5Only = false; + sm2.ignoreFlash = false; + + window.setTimeout(function() { + + // by default, re-init + + if (!excludeInit) { + sm2.beginDelayedInit(); + } + + }, 20); + + return sm2; + + }; + + this.reset = function() { + + /** + * Shuts down and restores the SoundManager instance to its original loaded state, without an explicit reboot. All onready/ontimeout handlers are removed. + * After this call, SM2 may be re-initialized via soundManager.beginDelayedInit(). + * @return {object} soundManager The soundManager instance. + */ + + _wDS('reset'); + + return sm2.reboot(true, true); + + }; + + /** + * Undocumented: Determines the SM2 flash movie's load progress. + * + * @return {number or null} Percent loaded, or if invalid/unsupported, null. + */ + + this.getMoviePercent = function() { + + /** + * Interesting syntax notes... + * Flash/ExternalInterface (ActiveX/NPAPI) bridge methods are not typeof "function" nor instanceof Function, but are still valid. + * Furthermore, using (flash && flash.PercentLoaded) causes IE to throw "object doesn't support this property or method". + * Thus, 'in' syntax must be used. + */ + + return (flash && 'PercentLoaded' in flash ? flash.PercentLoaded() : null); + + }; + + /** + * Additional helper for manually invoking SM2's init process after DOM Ready / window.onload(). + */ + + this.beginDelayedInit = function() { + + windowLoaded = true; + domContentLoaded(); + + setTimeout(function() { + + if (initPending) return false; + + createMovie(); + initMovie(); + initPending = true; + + return true; + + }, 20); + + delayWaitForEI(); + + }; + + /** + * Destroys the SoundManager instance and all SMSound instances. + */ + + this.destruct = function() { + + sm2._wD(sm + '.destruct()'); + sm2.disable(true); + + }; + + /** + * SMSound() (sound object) constructor + * ------------------------------------ + * + * @param {object} oOptions Sound options (id and url are required attributes) + * @return {SMSound} The new SMSound object + */ + + SMSound = function(oOptions) { + + var s = this, resetProperties, add_html5_events, remove_html5_events, stop_html5_timer, start_html5_timer, attachOnPosition, onplay_called = false, onPositionItems = [], onPositionFired = 0, detachOnPosition, applyFromTo, lastURL = null, lastHTML5State, urlOmitted; + + lastHTML5State = { + // tracks duration + position (time) + duration: null, + time: null + }; + + this.id = oOptions.id; + + // legacy + this.sID = this.id; + + this.url = oOptions.url; + this.options = mixin(oOptions); + + // per-play-instance-specific options + this.instanceOptions = this.options; + + // short alias + this._iO = this.instanceOptions; + + // assign property defaults + this.pan = this.options.pan; + this.volume = this.options.volume; + + // whether or not this object is using HTML5 + this.isHTML5 = false; + + // internal HTML5 Audio() object reference + this._a = null; + + // for flash 8 special-case createSound() without url, followed by load/play with url case + urlOmitted = (!this.url); + + /** + * SMSound() public methods + * ------------------------ + */ + + this.id3 = {}; + + /** + * Writes SMSound object parameters to debug console + */ + + this._debug = function() { + + // + sm2._wD(s.id + ': Merged options:', s.options); + // + + }; + + /** + * Begins loading a sound per its *url*. + * + * @param {object} options Optional: Sound options + * @return {SMSound} The SMSound object + */ + + this.load = function(options) { + + var oSound = null, instanceOptions; + + if (options !== _undefined) { + s._iO = mixin(options, s.options); + } else { + options = s.options; + s._iO = options; + if (lastURL && lastURL !== s.url) { + _wDS('manURL'); + s._iO.url = s.url; + s.url = null; + } + } + + if (!s._iO.url) { + s._iO.url = s.url; + } + + s._iO.url = parseURL(s._iO.url); + + // ensure we're in sync + s.instanceOptions = s._iO; + + // local shortcut + instanceOptions = s._iO; + + sm2._wD(s.id + ': load (' + instanceOptions.url + ')'); + + if (!instanceOptions.url && !s.url) { + sm2._wD(s.id + ': load(): url is unassigned. Exiting.', 2); + return s; + } + + // + if (!s.isHTML5 && fV === 8 && !s.url && !instanceOptions.autoPlay) { + // flash 8 load() -> play() won't work before onload has fired. + sm2._wD(s.id + ': Flash 8 load() limitation: Wait for onload() before calling play().', 1); + } + // + + if (instanceOptions.url === s.url && s.readyState !== 0 && s.readyState !== 2) { + _wDS('onURL', 1); + // if loaded and an onload() exists, fire immediately. + if (s.readyState === 3 && instanceOptions.onload) { + // assume success based on truthy duration. + wrapCallback(s, function() { + instanceOptions.onload.apply(s, [(!!s.duration)]); + }); + } + return s; + } + + // reset a few state properties + + s.loaded = false; + s.readyState = 1; + s.playState = 0; + s.id3 = {}; + + // TODO: If switching from HTML5 -> flash (or vice versa), stop currently-playing audio. + + if (html5OK(instanceOptions)) { + + oSound = s._setup_html5(instanceOptions); + + if (!oSound._called_load) { + + s._html5_canplay = false; + + // TODO: review called_load / html5_canplay logic + + // if url provided directly to load(), assign it here. + + if (s.url !== instanceOptions.url) { + + sm2._wD(_wDS('manURL') + ': ' + instanceOptions.url); + + s._a.src = instanceOptions.url; + + // TODO: review / re-apply all relevant options (volume, loop, onposition etc.) + + // reset position for new URL + s.setPosition(0); + + } + + // given explicit load call, try to preload. + + // early HTML5 implementation (non-standard) + s._a.autobuffer = 'auto'; + + // standard property, values: none / metadata / auto + // reference: http://msdn.microsoft.com/en-us/library/ie/ff974759%28v=vs.85%29.aspx + s._a.preload = 'auto'; + + s._a._called_load = true; + + } else { + + sm2._wD(s.id + ': Ignoring request to load again'); + + } + + } else { + + if (sm2.html5Only) { + sm2._wD(s.id + ': No flash support. Exiting.'); + return s; + } + + if (s._iO.url && s._iO.url.match(/data:/i)) { + // data: URIs not supported by Flash, either. + sm2._wD(s.id + ': data: URIs not supported via Flash. Exiting.'); + return s; + } + + try { + s.isHTML5 = false; + s._iO = policyFix(loopFix(instanceOptions)); + // if we have "position", disable auto-play as we'll be seeking to that position at onload(). + if (s._iO.autoPlay && (s._iO.position || s._iO.from)) { + sm2._wD(s.id + ': Disabling autoPlay because of non-zero offset case'); + s._iO.autoPlay = false; + } + // re-assign local shortcut + instanceOptions = s._iO; + if (fV === 8) { + flash._load(s.id, instanceOptions.url, instanceOptions.stream, instanceOptions.autoPlay, instanceOptions.usePolicyFile); + } else { + flash._load(s.id, instanceOptions.url, !!(instanceOptions.stream), !!(instanceOptions.autoPlay), instanceOptions.loops || 1, !!(instanceOptions.autoLoad), instanceOptions.usePolicyFile); + } + } catch(e) { + _wDS('smError', 2); + debugTS('onload', false); + catchError({ + type: 'SMSOUND_LOAD_JS_EXCEPTION', + fatal: true + }); + } + + } + + // after all of this, ensure sound url is up to date. + s.url = instanceOptions.url; + + return s; + + }; + + /** + * Unloads a sound, canceling any open HTTP requests. + * + * @return {SMSound} The SMSound object + */ + + this.unload = function() { + + // Flash 8/AS2 can't "close" a stream - fake it by loading an empty URL + // Flash 9/AS3: Close stream, preventing further load + // HTML5: Most UAs will use empty URL + + if (s.readyState !== 0) { + + sm2._wD(s.id + ': unload()'); + + if (!s.isHTML5) { + + if (fV === 8) { + flash._unload(s.id, emptyURL); + } else { + flash._unload(s.id); + } + + } else { + + stop_html5_timer(); + + if (s._a) { + + s._a.pause(); + + // update empty URL, too + lastURL = html5Unload(s._a); + + } + + } + + // reset load/status flags + resetProperties(); + + } + + return s; + + }; + + /** + * Unloads and destroys a sound. + */ + + this.destruct = function(_bFromSM) { + + sm2._wD(s.id + ': Destruct'); + + if (!s.isHTML5) { + + // kill sound within Flash + // Disable the onfailure handler + s._iO.onfailure = null; + flash._destroySound(s.id); + + } else { + + stop_html5_timer(); + + if (s._a) { + s._a.pause(); + html5Unload(s._a); + if (!useGlobalHTML5Audio) { + remove_html5_events(); + } + // break obvious circular reference + s._a._s = null; + s._a = null; + } + + } + + if (!_bFromSM) { + // ensure deletion from controller + sm2.destroySound(s.id, true); + } + + }; + + /** + * Begins playing a sound. + * + * @param {object} options Optional: Sound options + * @return {SMSound} The SMSound object + */ + + this.play = function(options, _updatePlayState) { + + var fN, allowMulti, a, onready, + audioClone, onended, oncanplay, + startOK = true; + + // + fN = s.id + ': play(): '; + // + + // default to true + _updatePlayState = (_updatePlayState === _undefined ? true : _updatePlayState); + + if (!options) { + options = {}; + } + + // first, use local URL (if specified) + if (s.url) { + s._iO.url = s.url; + } + + // mix in any options defined at createSound() + s._iO = mixin(s._iO, s.options); + + // mix in any options specific to this method + s._iO = mixin(options, s._iO); + + s._iO.url = parseURL(s._iO.url); + + s.instanceOptions = s._iO; + + // RTMP-only + if (!s.isHTML5 && s._iO.serverURL && !s.connected) { + if (!s.getAutoPlay()) { + sm2._wD(fN + ' Netstream not connected yet - setting autoPlay'); + s.setAutoPlay(true); + } + // play will be called in onconnect() + return s; + } + + if (html5OK(s._iO)) { + s._setup_html5(s._iO); + start_html5_timer(); + } + + if (s.playState === 1 && !s.paused) { + + allowMulti = s._iO.multiShot; + + if (!allowMulti) { + + sm2._wD(fN + 'Already playing (one-shot)', 1); + + if (s.isHTML5) { + // go back to original position. + s.setPosition(s._iO.position); + } + + return s; + + } + + sm2._wD(fN + 'Already playing (multi-shot)', 1); + + } + + // edge case: play() with explicit URL parameter + if (options.url && options.url !== s.url) { + + // special case for createSound() followed by load() / play() with url; avoid double-load case. + if (!s.readyState && !s.isHTML5 && fV === 8 && urlOmitted) { + + urlOmitted = false; + + } else { + + // load using merged options + s.load(s._iO); + + } + + } + + if (!s.loaded) { + + if (s.readyState === 0) { + + sm2._wD(fN + 'Attempting to load'); + + // try to get this sound playing ASAP + if (!s.isHTML5 && !sm2.html5Only) { + + // flash: assign directly because setAutoPlay() increments the instanceCount + s._iO.autoPlay = true; + s.load(s._iO); + + } else if (s.isHTML5) { + + // iOS needs this when recycling sounds, loading a new URL on an existing object. + s.load(s._iO); + + } else { + + sm2._wD(fN + 'Unsupported type. Exiting.'); + + return s; + + } + + // HTML5 hack - re-set instanceOptions? + s.instanceOptions = s._iO; + + } else if (s.readyState === 2) { + + sm2._wD(fN + 'Could not load - exiting', 2); + + return s; + + } else { + + sm2._wD(fN + 'Loading - attempting to play...'); + + } + + } else { + + // "play()" + sm2._wD(fN.substr(0, fN.lastIndexOf(':'))); + + } + + if (!s.isHTML5 && fV === 9 && s.position > 0 && s.position === s.duration) { + // flash 9 needs a position reset if play() is called while at the end of a sound. + sm2._wD(fN + 'Sound at end, resetting to position: 0'); + options.position = 0; + } + + /** + * Streams will pause when their buffer is full if they are being loaded. + * In this case paused is true, but the song hasn't started playing yet. + * If we just call resume() the onplay() callback will never be called. + * So only call resume() if the position is > 0. + * Another reason is because options like volume won't have been applied yet. + * For normal sounds, just resume. + */ + + if (s.paused && s.position >= 0 && (!s._iO.serverURL || s.position > 0)) { + + // https://gist.github.com/37b17df75cc4d7a90bf6 + sm2._wD(fN + 'Resuming from paused state', 1); + s.resume(); + + } else { + + s._iO = mixin(options, s._iO); + + /** + * Preload in the event of play() with position under Flash, + * or from/to parameters and non-RTMP case + */ + if (((!s.isHTML5 && s._iO.position !== null && s._iO.position > 0) || (s._iO.from !== null && s._iO.from > 0) || s._iO.to !== null) && s.instanceCount === 0 && s.playState === 0 && !s._iO.serverURL) { + + onready = function() { + // sound "canplay" or onload() + // re-apply position/from/to to instance options, and start playback + s._iO = mixin(options, s._iO); + s.play(s._iO); + }; + + // HTML5 needs to at least have "canplay" fired before seeking. + if (s.isHTML5 && !s._html5_canplay) { + + // this hasn't been loaded yet. load it first, and then do this again. + sm2._wD(fN + 'Beginning load for non-zero offset case'); + + s.load({ + // note: custom HTML5-only event added for from/to implementation. + _oncanplay: onready + }); + + } else if (!s.isHTML5 && !s.loaded && (!s.readyState || s.readyState !== 2)) { + + // to be safe, preload the whole thing in Flash. + + sm2._wD(fN + 'Preloading for non-zero offset case'); + + s.load({ + onload: onready + }); + + } + + // otherwise, we're ready to go. re-apply local options, and continue + + s._iO = applyFromTo(); + + } + + // sm2._wD(fN + 'Starting to play'); + + // increment instance counter, where enabled + supported + if (!s.instanceCount || s._iO.multiShotEvents || (s.isHTML5 && s._iO.multiShot && !useGlobalHTML5Audio) || (!s.isHTML5 && fV > 8 && !s.getAutoPlay())) { + s.instanceCount++; + } + + // if first play and onposition parameters exist, apply them now + if (s._iO.onposition && s.playState === 0) { + attachOnPosition(s); + } + + s.playState = 1; + s.paused = false; + + s.position = (s._iO.position !== _undefined && !isNaN(s._iO.position) ? s._iO.position : 0); + + if (!s.isHTML5) { + s._iO = policyFix(loopFix(s._iO)); + } + + if (s._iO.onplay && _updatePlayState) { + s._iO.onplay.apply(s); + onplay_called = true; + } + + s.setVolume(s._iO.volume, true); + s.setPan(s._iO.pan, true); + + if (s._iO.playbackRate !== 1) { + s.setPlaybackRate(s._iO.playbackRate); + } + + if (!s.isHTML5) { + + startOK = flash._start(s.id, s._iO.loops || 1, (fV === 9 ? s.position : s.position / msecScale), s._iO.multiShot || false); + + if (fV === 9 && !startOK) { + // edge case: no sound hardware, or 32-channel flash ceiling hit. + // applies only to Flash 9, non-NetStream/MovieStar sounds. + // http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/media/Sound.html#play%28%29 + sm2._wD(fN + 'No sound hardware, or 32-sound ceiling hit', 2); + if (s._iO.onplayerror) { + s._iO.onplayerror.apply(s); + } + + } + + } else if (s.instanceCount < 2) { + + // HTML5 single-instance case + + start_html5_timer(); + + a = s._setup_html5(); + + s.setPosition(s._iO.position); + + a.play(); + + } else { + + // HTML5 multi-shot case + + sm2._wD(s.id + ': Cloning Audio() for instance #' + s.instanceCount + '...'); + + audioClone = new Audio(s._iO.url); + + onended = function() { + event.remove(audioClone, 'ended', onended); + s._onfinish(s); + // cleanup + html5Unload(audioClone); + audioClone = null; + }; + + oncanplay = function() { + event.remove(audioClone, 'canplay', oncanplay); + try { + audioClone.currentTime = s._iO.position / msecScale; + } catch(err) { + complain(s.id + ': multiShot play() failed to apply position of ' + (s._iO.position / msecScale)); + } + audioClone.play(); + }; + + event.add(audioClone, 'ended', onended); + + // apply volume to clones, too + if (s._iO.volume !== _undefined) { + audioClone.volume = Math.max(0, Math.min(1, s._iO.volume / 100)); + } + + // playing multiple muted sounds? if you do this, you're weird ;) - but let's cover it. + if (s.muted) { + audioClone.muted = true; + } + + if (s._iO.position) { + // HTML5 audio can't seek before onplay() event has fired. + // wait for canplay, then seek to position and start playback. + event.add(audioClone, 'canplay', oncanplay); + } else { + // begin playback at currentTime: 0 + audioClone.play(); + } + + } + + } + + return s; + + }; + + // just for convenience + this.start = this.play; + + /** + * Stops playing a sound (and optionally, all sounds) + * + * @param {boolean} bAll Optional: Whether to stop all sounds + * @return {SMSound} The SMSound object + */ + + this.stop = function(bAll) { + + var instanceOptions = s._iO, + originalPosition; + + if (s.playState === 1) { + + sm2._wD(s.id + ': stop()'); + + s._onbufferchange(0); + s._resetOnPosition(0); + s.paused = false; + + if (!s.isHTML5) { + s.playState = 0; + } + + // remove onPosition listeners, if any + detachOnPosition(); + + // and "to" position, if set + if (instanceOptions.to) { + s.clearOnPosition(instanceOptions.to); + } + + if (!s.isHTML5) { + + flash._stop(s.id, bAll); + + // hack for netStream: just unload + if (instanceOptions.serverURL) { + s.unload(); + } + + } else if (s._a) { + + originalPosition = s.position; + + // act like Flash, though + s.setPosition(0); + + // hack: reflect old position for onstop() (also like Flash) + s.position = originalPosition; + + // html5 has no stop() + // NOTE: pausing means iOS requires interaction to resume. + s._a.pause(); + + s.playState = 0; + + // and update UI + s._onTimer(); + + stop_html5_timer(); + + } + + s.instanceCount = 0; + s._iO = {}; + + if (instanceOptions.onstop) { + instanceOptions.onstop.apply(s); + } + + } + + return s; + + }; + + /** + * Undocumented/internal: Sets autoPlay for RTMP. + * + * @param {boolean} autoPlay state + */ + + this.setAutoPlay = function(autoPlay) { + + sm2._wD(s.id + ': Autoplay turned ' + (autoPlay ? 'on' : 'off')); + s._iO.autoPlay = autoPlay; + + if (!s.isHTML5) { + flash._setAutoPlay(s.id, autoPlay); + if (autoPlay) { + // only increment the instanceCount if the sound isn't loaded (TODO: verify RTMP) + if (!s.instanceCount && s.readyState === 1) { + s.instanceCount++; + sm2._wD(s.id + ': Incremented instance count to ' + s.instanceCount); + } + } + } + + }; + + /** + * Undocumented/internal: Returns the autoPlay boolean. + * + * @return {boolean} The current autoPlay value + */ + + this.getAutoPlay = function() { + + return s._iO.autoPlay; + + }; + + /** + * Sets the playback rate of a sound (HTML5-only.) + * + * @param {number} playbackRate (+/-) + * @return {SMSound} The SMSound object + */ + + this.setPlaybackRate = function(playbackRate) { + + // Per Mozilla, limit acceptable values to prevent playback from stopping (unless allowOverride is truthy.) + // https://developer.mozilla.org/en-US/Apps/Build/Audio_and_video_delivery/WebAudio_playbackRate_explained + var normalizedRate = Math.max(0.5, Math.min(4, playbackRate)); + + // + if (normalizedRate !== playbackRate) { + sm2._wD(s.id + ': setPlaybackRate(' + playbackRate + '): limiting rate to ' + normalizedRate, 2); + } + // + + if (s.isHTML5) { + try { + s._iO.playbackRate = normalizedRate; + s._a.playbackRate = normalizedRate; + } catch(e) { + sm2._wD(s.id + ': setPlaybackRate(' + normalizedRate + ') failed: ' + e.message, 2); + } + } + + return s; + + }; + + /** + * Sets the position of a sound. + * + * @param {number} nMsecOffset Position (milliseconds) + * @return {SMSound} The SMSound object + */ + + this.setPosition = function(nMsecOffset) { + + if (nMsecOffset === _undefined) { + nMsecOffset = 0; + } + + var position, position1K, + // Use the duration from the instance options, if we don't have a track duration yet. + // position >= 0 and <= current available (loaded) duration + offset = (s.isHTML5 ? Math.max(nMsecOffset, 0) : Math.min(s.duration || s._iO.duration, Math.max(nMsecOffset, 0))); + + s.position = offset; + position1K = s.position / msecScale; + s._resetOnPosition(s.position); + s._iO.position = offset; + + if (!s.isHTML5) { + + position = (fV === 9 ? s.position : position1K); + + if (s.readyState && s.readyState !== 2) { + // if paused or not playing, will not resume (by playing) + flash._setPosition(s.id, position, (s.paused || !s.playState), s._iO.multiShot); + } + + } else if (s._a) { + + // Set the position in the canplay handler if the sound is not ready yet + if (s._html5_canplay) { + + if (s._a.currentTime.toFixed(3) !== position1K.toFixed(3)) { + + /** + * DOM/JS errors/exceptions to watch out for: + * if seek is beyond (loaded?) position, "DOM exception 11" + * "INDEX_SIZE_ERR": DOM exception 1 + */ + sm2._wD(s.id + ': setPosition(' + position1K + ')'); + + try { + s._a.currentTime = position1K; + if (s.playState === 0 || s.paused) { + // allow seek without auto-play/resume + s._a.pause(); + } + } catch(e) { + sm2._wD(s.id + ': setPosition(' + position1K + ') failed: ' + e.message, 2); + } + + } + + } else if (position1K) { + + // warn on non-zero seek attempts + sm2._wD(s.id + ': setPosition(' + position1K + '): Cannot seek yet, sound not ready', 2); + return s; + + } + + if (s.paused) { + + // if paused, refresh UI right away by forcing update + s._onTimer(true); + + } + + } + + return s; + + }; + + /** + * Pauses sound playback. + * + * @return {SMSound} The SMSound object + */ + + this.pause = function(_bCallFlash) { + + if (s.paused || (s.playState === 0 && s.readyState !== 1)) return s; + + sm2._wD(s.id + ': pause()'); + s.paused = true; + + if (!s.isHTML5) { + if (_bCallFlash || _bCallFlash === _undefined) { + flash._pause(s.id, s._iO.multiShot); + } + } else { + s._setup_html5().pause(); + stop_html5_timer(); + } + + if (s._iO.onpause) { + s._iO.onpause.apply(s); + } + + return s; + + }; + + /** + * Resumes sound playback. + * + * @return {SMSound} The SMSound object + */ + + /** + * When auto-loaded streams pause on buffer full they have a playState of 0. + * We need to make sure that the playState is set to 1 when these streams "resume". + * When a paused stream is resumed, we need to trigger the onplay() callback if it + * hasn't been called already. In this case since the sound is being played for the + * first time, I think it's more appropriate to call onplay() rather than onresume(). + */ + + this.resume = function() { + + var instanceOptions = s._iO; + + if (!s.paused) return s; + + sm2._wD(s.id + ': resume()'); + s.paused = false; + s.playState = 1; + + if (!s.isHTML5) { + + if (instanceOptions.isMovieStar && !instanceOptions.serverURL) { + // Bizarre Webkit bug (Chrome reported via 8tracks.com dudes): AAC content paused for 30+ seconds(?) will not resume without a reposition. + s.setPosition(s.position); + } + + // flash method is toggle-based (pause/resume) + flash._pause(s.id, instanceOptions.multiShot); + + } else { + + s._setup_html5().play(); + start_html5_timer(); + + } + + if (!onplay_called && instanceOptions.onplay) { + + instanceOptions.onplay.apply(s); + onplay_called = true; + + } else if (instanceOptions.onresume) { + + instanceOptions.onresume.apply(s); + + } + + return s; + + }; + + /** + * Toggles sound playback. + * + * @return {SMSound} The SMSound object + */ + + this.togglePause = function() { + + sm2._wD(s.id + ': togglePause()'); + + if (s.playState === 0) { + s.play({ + position: (fV === 9 && !s.isHTML5 ? s.position : s.position / msecScale) + }); + return s; + } + + if (s.paused) { + s.resume(); + } else { + s.pause(); + } + + return s; + + }; + + /** + * Sets the panning (L-R) effect. + * + * @param {number} nPan The pan value (-100 to 100) + * @return {SMSound} The SMSound object + */ + + this.setPan = function(nPan, bInstanceOnly) { + + if (nPan === _undefined) { + nPan = 0; + } + + if (bInstanceOnly === _undefined) { + bInstanceOnly = false; + } + + if (!s.isHTML5) { + flash._setPan(s.id, nPan); + } // else { no HTML5 pan? } + + s._iO.pan = nPan; + + if (!bInstanceOnly) { + s.pan = nPan; + s.options.pan = nPan; + } + + return s; + + }; + + /** + * Sets the volume. + * + * @param {number} nVol The volume value (0 to 100) + * @return {SMSound} The SMSound object + */ + + this.setVolume = function(nVol, _bInstanceOnly) { + + /** + * Note: Setting volume has no effect on iOS "special snowflake" devices. + * Hardware volume control overrides software, and volume + * will always return 1 per Apple docs. (iOS 4 + 5.) + * http://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingSoundtoCanvasAnimations/AddingSoundtoCanvasAnimations.html + */ + + if (nVol === _undefined) { + nVol = 100; + } + + if (_bInstanceOnly === _undefined) { + _bInstanceOnly = false; + } + + if (!s.isHTML5) { + + flash._setVolume(s.id, (sm2.muted && !s.muted) || s.muted ? 0 : nVol); + + } else if (s._a) { + + if (sm2.muted && !s.muted) { + s.muted = true; + s._a.muted = true; + } + + // valid range for native HTML5 Audio(): 0-1 + s._a.volume = Math.max(0, Math.min(1, nVol / 100)); + + } + + s._iO.volume = nVol; + + if (!_bInstanceOnly) { + s.volume = nVol; + s.options.volume = nVol; + } + + return s; + + }; + + /** + * Mutes the sound. + * + * @return {SMSound} The SMSound object + */ + + this.mute = function() { + + s.muted = true; + + if (!s.isHTML5) { + flash._setVolume(s.id, 0); + } else if (s._a) { + s._a.muted = true; + } + + return s; + + }; + + /** + * Unmutes the sound. + * + * @return {SMSound} The SMSound object + */ + + this.unmute = function() { + + s.muted = false; + var hasIO = (s._iO.volume !== _undefined); + + if (!s.isHTML5) { + flash._setVolume(s.id, hasIO ? s._iO.volume : s.options.volume); + } else if (s._a) { + s._a.muted = false; + } + + return s; + + }; + + /** + * Toggles the muted state of a sound. + * + * @return {SMSound} The SMSound object + */ + + this.toggleMute = function() { + + return (s.muted ? s.unmute() : s.mute()); + + }; + + /** + * Registers a callback to be fired when a sound reaches a given position during playback. + * + * @param {number} nPosition The position to watch for + * @param {function} oMethod The relevant callback to fire + * @param {object} oScope Optional: The scope to apply the callback to + * @return {SMSound} The SMSound object + */ + + this.onPosition = function(nPosition, oMethod, oScope) { + + // TODO: basic dupe checking? + + onPositionItems.push({ + position: parseInt(nPosition, 10), + method: oMethod, + scope: (oScope !== _undefined ? oScope : s), + fired: false + }); + + return s; + + }; + + // legacy/backwards-compability: lower-case method name + this.onposition = this.onPosition; + + /** + * Removes registered callback(s) from a sound, by position and/or callback. + * + * @param {number} nPosition The position to clear callback(s) for + * @param {function} oMethod Optional: Identify one callback to be removed when multiple listeners exist for one position + * @return {SMSound} The SMSound object + */ + + this.clearOnPosition = function(nPosition, oMethod) { + + var i; + + nPosition = parseInt(nPosition, 10); + + if (isNaN(nPosition)) { + // safety check + return; + } + + for (i = 0; i < onPositionItems.length; i++) { + + if (nPosition === onPositionItems[i].position) { + // remove this item if no method was specified, or, if the method matches + + if (!oMethod || (oMethod === onPositionItems[i].method)) { + + if (onPositionItems[i].fired) { + // decrement "fired" counter, too + onPositionFired--; + } + + onPositionItems.splice(i, 1); + + } + + } + + } + + }; + + this._processOnPosition = function() { + + var i, item, j = onPositionItems.length; + + if (!j || !s.playState || onPositionFired >= j) return false; + + for (i = j - 1; i >= 0; i--) { + + item = onPositionItems[i]; + + if (!item.fired && s.position >= item.position) { + + item.fired = true; + onPositionFired++; + item.method.apply(item.scope, [item.position]); + + // reset j -- onPositionItems.length can be changed in the item callback above... occasionally breaking the loop. + j = onPositionItems.length; + + } + + } + + return true; + + }; + + this._resetOnPosition = function(nPosition) { + + // reset "fired" for items interested in this position + var i, item, j = onPositionItems.length; + + if (!j) return false; + + for (i = j - 1; i >= 0; i--) { + + item = onPositionItems[i]; + + if (item.fired && nPosition <= item.position) { + item.fired = false; + onPositionFired--; + } + + } + + return true; + + }; + + /** + * SMSound() private internals + * -------------------------------- + */ + + applyFromTo = function() { + + var instanceOptions = s._iO, + f = instanceOptions.from, + t = instanceOptions.to, + start, end; + + end = function() { + + // end has been reached. + sm2._wD(s.id + ': "To" time of ' + t + ' reached.'); + + // detach listener + s.clearOnPosition(t, end); + + // stop should clear this, too + s.stop(); + + }; + + start = function() { + + sm2._wD(s.id + ': Playing "from" ' + f); + + // add listener for end + if (t !== null && !isNaN(t)) { + s.onPosition(t, end); + } + + }; + + if (f !== null && !isNaN(f)) { + + // apply to instance options, guaranteeing correct start position. + instanceOptions.position = f; + + // multiShot timing can't be tracked, so prevent that. + instanceOptions.multiShot = false; + + start(); + + } + + // return updated instanceOptions including starting position + return instanceOptions; + + }; + + attachOnPosition = function() { + + var item, + op = s._iO.onposition; + + // attach onposition things, if any, now. + + if (op) { + + for (item in op) { + if (op.hasOwnProperty(item)) { + s.onPosition(parseInt(item, 10), op[item]); + } + } + + } + + }; + + detachOnPosition = function() { + + var item, + op = s._iO.onposition; + + // detach any onposition()-style listeners. + + if (op) { + + for (item in op) { + if (op.hasOwnProperty(item)) { + s.clearOnPosition(parseInt(item, 10)); + } + } + + } + + }; + + start_html5_timer = function() { + + if (s.isHTML5) { + startTimer(s); + } + + }; + + stop_html5_timer = function() { + + if (s.isHTML5) { + stopTimer(s); + } + + }; + + resetProperties = function(retainPosition) { + + if (!retainPosition) { + onPositionItems = []; + onPositionFired = 0; + } + + onplay_called = false; + + s._hasTimer = null; + s._a = null; + s._html5_canplay = false; + s.bytesLoaded = null; + s.bytesTotal = null; + s.duration = (s._iO && s._iO.duration ? s._iO.duration : null); + s.durationEstimate = null; + s.buffered = []; + + // legacy: 1D array + s.eqData = []; + + s.eqData.left = []; + s.eqData.right = []; + + s.failures = 0; + s.isBuffering = false; + s.instanceOptions = {}; + s.instanceCount = 0; + s.loaded = false; + s.metadata = {}; + + // 0 = uninitialised, 1 = loading, 2 = failed/error, 3 = loaded/success + s.readyState = 0; + + s.muted = false; + s.paused = false; + + s.peakData = { + left: 0, + right: 0 + }; + + s.waveformData = { + left: [], + right: [] + }; + + s.playState = 0; + s.position = null; + + s.id3 = {}; + + }; + + resetProperties(); + + /** + * Pseudo-private SMSound internals + * -------------------------------- + */ + + this._onTimer = function(bForce) { + + /** + * HTML5-only _whileplaying() etc. + * called from both HTML5 native events, and polling/interval-based timers + * mimics flash and fires only when time/duration change, so as to be polling-friendly + */ + + var duration, isNew = false, time, x = {}; + + if (s._hasTimer || bForce) { + + // TODO: May not need to track readyState (1 = loading) + + if (s._a && (bForce || ((s.playState > 0 || s.readyState === 1) && !s.paused))) { + + duration = s._get_html5_duration(); + + if (duration !== lastHTML5State.duration) { + + lastHTML5State.duration = duration; + s.duration = duration; + isNew = true; + + } + + // TODO: investigate why this goes wack if not set/re-set each time. + s.durationEstimate = s.duration; + + time = (s._a.currentTime * msecScale || 0); + + if (time !== lastHTML5State.time) { + + lastHTML5State.time = time; + isNew = true; + + } + + if (isNew || bForce) { + + s._whileplaying(time, x, x, x, x); + + } + + }/* else { + + // sm2._wD('_onTimer: Warn for "'+s.id+'": '+(!s._a?'Could not find element. ':'')+(s.playState === 0?'playState bad, 0?':'playState = '+s.playState+', OK')); + + return false; + + }*/ + + } + + return isNew; + + }; + + this._get_html5_duration = function() { + + var instanceOptions = s._iO, + // if audio object exists, use its duration - else, instance option duration (if provided - it's a hack, really, and should be retired) OR null + d = (s._a && s._a.duration ? s._a.duration * msecScale : (instanceOptions && instanceOptions.duration ? instanceOptions.duration : null)), + result = (d && !isNaN(d) && d !== Infinity ? d : null); + + return result; + + }; + + this._apply_loop = function(a, nLoops) { + + /** + * boolean instead of "loop", for webkit? - spec says string. http://www.w3.org/TR/html-markup/audio.html#audio.attrs.loop + * note that loop is either off or infinite under HTML5, unlike Flash which allows arbitrary loop counts to be specified. + */ + + // + if (!a.loop && nLoops > 1) { + sm2._wD('Note: Native HTML5 looping is infinite.', 1); + } + // + + a.loop = (nLoops > 1 ? 'loop' : ''); + + }; + + this._setup_html5 = function(options) { + + var instanceOptions = mixin(s._iO, options), + a = useGlobalHTML5Audio ? globalHTML5Audio : s._a, + dURL = decodeURI(instanceOptions.url), + sameURL; + + /** + * "First things first, I, Poppa..." (reset the previous state of the old sound, if playing) + * Fixes case with devices that can only play one sound at a time + * Otherwise, other sounds in mid-play will be terminated without warning and in a stuck state + */ + + if (useGlobalHTML5Audio) { + + if (dURL === decodeURI(lastGlobalHTML5URL)) { + // global HTML5 audio: re-use of URL + sameURL = true; + } + + } else if (dURL === decodeURI(lastURL)) { + + // options URL is the same as the "last" URL, and we used (loaded) it + sameURL = true; + + } + + if (a) { + + if (a._s) { + + if (useGlobalHTML5Audio) { + + if (a._s && a._s.playState && !sameURL) { + + // global HTML5 audio case, and loading a new URL. stop the currently-playing one. + a._s.stop(); + + } + + } else if (!useGlobalHTML5Audio && dURL === decodeURI(lastURL)) { + + // non-global HTML5 reuse case: same url, ignore request + s._apply_loop(a, instanceOptions.loops); + + return a; + + } + + } + + if (!sameURL) { + + // don't retain onPosition() stuff with new URLs. + + if (lastURL) { + resetProperties(false); + } + + // assign new HTML5 URL + + a.src = instanceOptions.url; + + s.url = instanceOptions.url; + + lastURL = instanceOptions.url; + + lastGlobalHTML5URL = instanceOptions.url; + + a._called_load = false; + + } + + } else { + + if (instanceOptions.autoLoad || instanceOptions.autoPlay) { + + s._a = new Audio(instanceOptions.url); + s._a.load(); + + } else { + + // null for stupid Opera 9.64 case + s._a = (isOpera && opera.version() < 10 ? new Audio(null) : new Audio()); + + } + + // assign local reference + a = s._a; + + a._called_load = false; + + if (useGlobalHTML5Audio) { + + globalHTML5Audio = a; + + } + + } + + s.isHTML5 = true; + + // store a ref on the track + s._a = a; + + // store a ref on the audio + a._s = s; + + add_html5_events(); + + s._apply_loop(a, instanceOptions.loops); + + if (instanceOptions.autoLoad || instanceOptions.autoPlay) { + + s.load(); + + } else { + + // early HTML5 implementation (non-standard) + a.autobuffer = false; + + // standard ('none' is also an option.) + a.preload = 'auto'; + + } + + return a; + + }; + + add_html5_events = function() { + + if (s._a._added_events) return false; + + var f; + + function add(oEvt, oFn, bCapture) { + return s._a ? s._a.addEventListener(oEvt, oFn, bCapture || false) : null; + } + + s._a._added_events = true; + + for (f in html5_events) { + if (html5_events.hasOwnProperty(f)) { + add(f, html5_events[f]); + } + } + + return true; + + }; + + remove_html5_events = function() { + + // Remove event listeners + + var f; + + function remove(oEvt, oFn, bCapture) { + return (s._a ? s._a.removeEventListener(oEvt, oFn, bCapture || false) : null); + } + + sm2._wD(s.id + ': Removing event listeners'); + s._a._added_events = false; + + for (f in html5_events) { + if (html5_events.hasOwnProperty(f)) { + remove(f, html5_events[f]); + } + } + + }; + + /** + * Pseudo-private event internals + * ------------------------------ + */ + + this._onload = function(nSuccess) { + + var fN, + // check for duration to prevent false positives from flash 8 when loading from cache. + loadOK = !!nSuccess || (!s.isHTML5 && fV === 8 && s.duration); + + // + fN = s.id + ': '; + sm2._wD(fN + (loadOK ? 'onload()' : 'Failed to load / invalid sound?' + (!s.duration ? ' Zero-length duration reported.' : ' -') + ' (' + s.url + ')'), (loadOK ? 1 : 2)); + + if (!loadOK && !s.isHTML5) { + if (sm2.sandbox.noRemote === true) { + sm2._wD(fN + str('noNet'), 1); + } + if (sm2.sandbox.noLocal === true) { + sm2._wD(fN + str('noLocal'), 1); + } + } + // + + s.loaded = loadOK; + s.readyState = (loadOK ? 3 : 2); + s._onbufferchange(0); + + if (!loadOK && !s.isHTML5) { + // note: no error code from Flash. + s._onerror(); + } + + if (s._iO.onload) { + wrapCallback(s, function() { + s._iO.onload.apply(s, [loadOK]); + }); + } + + return true; + + }; + + this._onerror = function(errorCode, description) { + + // https://html.spec.whatwg.org/multipage/embedded-content.html#error-codes + if (s._iO.onerror) { + wrapCallback(s, function() { + s._iO.onerror.apply(s, [errorCode, description]); + }); + } + + }; + + this._onbufferchange = function(nIsBuffering) { + + // ignore if not playing + if (s.playState === 0) return false; + + if ((nIsBuffering && s.isBuffering) || (!nIsBuffering && !s.isBuffering)) return false; + + s.isBuffering = (nIsBuffering === 1); + + if (s._iO.onbufferchange) { + sm2._wD(s.id + ': Buffer state change: ' + nIsBuffering); + s._iO.onbufferchange.apply(s, [nIsBuffering]); + } + + return true; + + }; + + /** + * Playback may have stopped due to buffering, or related reason. + * This state can be encountered on iOS < 6 when auto-play is blocked. + */ + + this._onsuspend = function() { + + if (s._iO.onsuspend) { + sm2._wD(s.id + ': Playback suspended'); + s._iO.onsuspend.apply(s); + } + + return true; + + }; + + /** + * flash 9/movieStar + RTMP-only method, should fire only once at most + * at this point we just recreate failed sounds rather than trying to reconnect + */ + + this._onfailure = function(msg, level, code) { + + s.failures++; + sm2._wD(s.id + ': Failure (' + s.failures + '): ' + msg); + + if (s._iO.onfailure && s.failures === 1) { + s._iO.onfailure(msg, level, code); + } else { + sm2._wD(s.id + ': Ignoring failure'); + } + + }; + + /** + * flash 9/movieStar + RTMP-only method for unhandled warnings/exceptions from Flash + * e.g., RTMP "method missing" warning (non-fatal) for getStreamLength on server + */ + + this._onwarning = function(msg, level, code) { + + if (s._iO.onwarning) { + s._iO.onwarning(msg, level, code); + } + + }; + + this._onfinish = function() { + + // store local copy before it gets trashed... + var io_onfinish = s._iO.onfinish; + + s._onbufferchange(0); + s._resetOnPosition(0); + + // reset some state items + if (s.instanceCount) { + + s.instanceCount--; + + if (!s.instanceCount) { + + // remove onPosition listeners, if any + detachOnPosition(); + + // reset instance options + s.playState = 0; + s.paused = false; + s.instanceCount = 0; + s.instanceOptions = {}; + s._iO = {}; + stop_html5_timer(); + + // reset position, too + if (s.isHTML5) { + s.position = 0; + } + + } + + if (!s.instanceCount || s._iO.multiShotEvents) { + // fire onfinish for last, or every instance + if (io_onfinish) { + sm2._wD(s.id + ': onfinish()'); + wrapCallback(s, function() { + io_onfinish.apply(s); + }); + } + } + + } + + }; + + this._whileloading = function(nBytesLoaded, nBytesTotal, nDuration, nBufferLength) { + + var instanceOptions = s._iO; + + s.bytesLoaded = nBytesLoaded; + s.bytesTotal = nBytesTotal; + s.duration = Math.floor(nDuration); + s.bufferLength = nBufferLength; + + if (!s.isHTML5 && !instanceOptions.isMovieStar) { + + if (instanceOptions.duration) { + // use duration from options, if specified and larger. nobody should be specifying duration in options, actually, and it should be retired. + s.durationEstimate = (s.duration > instanceOptions.duration) ? s.duration : instanceOptions.duration; + } else { + s.durationEstimate = parseInt((s.bytesTotal / s.bytesLoaded) * s.duration, 10); + } + + } else { + + s.durationEstimate = s.duration; + + } + + // for flash, reflect sequential-load-style buffering + if (!s.isHTML5) { + s.buffered = [{ + start: 0, + end: s.duration + }]; + } + + // allow whileloading to fire even if "load" fired under HTML5, due to HTTP range/partials + if ((s.readyState !== 3 || s.isHTML5) && instanceOptions.whileloading) { + instanceOptions.whileloading.apply(s); + } + + }; + + this._whileplaying = function(nPosition, oPeakData, oWaveformDataLeft, oWaveformDataRight, oEQData) { + + var instanceOptions = s._iO, + eqLeft; + + // flash safety net + if (isNaN(nPosition) || nPosition === null) return false; + + // Safari HTML5 play() may return small -ve values when starting from position: 0, eg. -50.120396875. Unexpected/invalid per W3, I think. Normalize to 0. + s.position = Math.max(0, nPosition); + + s._processOnPosition(); + + if (!s.isHTML5 && fV > 8) { + + if (instanceOptions.usePeakData && oPeakData !== _undefined && oPeakData) { + s.peakData = { + left: oPeakData.leftPeak, + right: oPeakData.rightPeak + }; + } + + if (instanceOptions.useWaveformData && oWaveformDataLeft !== _undefined && oWaveformDataLeft) { + s.waveformData = { + left: oWaveformDataLeft.split(','), + right: oWaveformDataRight.split(',') + }; + } + + if (instanceOptions.useEQData) { + if (oEQData !== _undefined && oEQData && oEQData.leftEQ) { + eqLeft = oEQData.leftEQ.split(','); + s.eqData = eqLeft; + s.eqData.left = eqLeft; + if (oEQData.rightEQ !== _undefined && oEQData.rightEQ) { + s.eqData.right = oEQData.rightEQ.split(','); + } + } + } + + } + + if (s.playState === 1) { + + // special case/hack: ensure buffering is false if loading from cache (and not yet started) + if (!s.isHTML5 && fV === 8 && !s.position && s.isBuffering) { + s._onbufferchange(0); + } + + if (instanceOptions.whileplaying) { + // flash may call after actual finish + instanceOptions.whileplaying.apply(s); + } + + } + + return true; + + }; + + this._oncaptiondata = function(oData) { + + /** + * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature + * + * @param {object} oData + */ + + sm2._wD(s.id + ': Caption data received.'); + + s.captiondata = oData; + + if (s._iO.oncaptiondata) { + s._iO.oncaptiondata.apply(s, [oData]); + } + + }; + + this._onmetadata = function(oMDProps, oMDData) { + + /** + * internal: flash 9 + NetStream (MovieStar/RTMP-only) feature + * RTMP may include song title, MovieStar content may include encoding info + * + * @param {array} oMDProps (names) + * @param {array} oMDData (values) + */ + + sm2._wD(s.id + ': Metadata received.'); + + var oData = {}, i, j; + + for (i = 0, j = oMDProps.length; i < j; i++) { + oData[oMDProps[i]] = oMDData[i]; + } + + s.metadata = oData; + + if (s._iO.onmetadata) { + s._iO.onmetadata.call(s, s.metadata); + } + + }; + + this._onid3 = function(oID3Props, oID3Data) { + + /** + * internal: flash 8 + flash 9 ID3 feature + * may include artist, song title etc. + * + * @param {array} oID3Props (names) + * @param {array} oID3Data (values) + */ + + sm2._wD(s.id + ': ID3 data received.'); + + var oData = [], i, j; + + for (i = 0, j = oID3Props.length; i < j; i++) { + oData[oID3Props[i]] = oID3Data[i]; + } + + s.id3 = mixin(s.id3, oData); + + if (s._iO.onid3) { + s._iO.onid3.apply(s); + } + + }; + + // flash/RTMP-only + + this._onconnect = function(bSuccess) { + + bSuccess = (bSuccess === 1); + sm2._wD(s.id + ': ' + (bSuccess ? 'Connected.' : 'Failed to connect? - ' + s.url), (bSuccess ? 1 : 2)); + s.connected = bSuccess; + + if (bSuccess) { + + s.failures = 0; + + if (idCheck(s.id)) { + if (s.getAutoPlay()) { + // only update the play state if auto playing + s.play(_undefined, s.getAutoPlay()); + } else if (s._iO.autoLoad) { + s.load(); + } + } + + if (s._iO.onconnect) { + s._iO.onconnect.apply(s, [bSuccess]); + } + + } + + }; + + this._ondataerror = function(sError) { + + // flash 9 wave/eq data handler + // hack: called at start, and end from flash at/after onfinish() + if (s.playState > 0) { + sm2._wD(s.id + ': Data error: ' + sError); + if (s._iO.ondataerror) { + s._iO.ondataerror.apply(s); + } + } + + }; + + // + this._debug(); + // + + }; // SMSound() + + /** + * Private SoundManager internals + * ------------------------------ + */ + + getDocument = function() { + + return (doc.body || doc.getElementsByTagName('div')[0]); + + }; + + id = function(sID) { + + return doc.getElementById(sID); + + }; + + mixin = function(oMain, oAdd) { + + // non-destructive merge + var o1 = (oMain || {}), o2, o; + + // if unspecified, o2 is the default options object + o2 = (oAdd === _undefined ? sm2.defaultOptions : oAdd); + + for (o in o2) { + + if (o2.hasOwnProperty(o) && o1[o] === _undefined) { + + if (typeof o2[o] !== 'object' || o2[o] === null) { + + // assign directly + o1[o] = o2[o]; + + } else { + + // recurse through o2 + o1[o] = mixin(o1[o], o2[o]); + + } + + } + + } + + return o1; + + }; + + wrapCallback = function(oSound, callback) { + + /** + * 03/03/2013: Fix for Flash Player 11.6.602.171 + Flash 8 (flashVersion = 8) SWF issue + * setTimeout() fix for certain SMSound callbacks like onload() and onfinish(), where subsequent calls like play() and load() fail when Flash Player 11.6.602.171 is installed, and using soundManager with flashVersion = 8 (which is the default). + * Not sure of exact cause. Suspect race condition and/or invalid (NaN-style) position argument trickling down to the next JS -> Flash _start() call, in the play() case. + * Fix: setTimeout() to yield, plus safer null / NaN checking on position argument provided to Flash. + * https://getsatisfaction.com/schillmania/topics/recent_chrome_update_seems_to_have_broken_my_sm2_audio_player + */ + if (!oSound.isHTML5 && fV === 8) { + window.setTimeout(callback, 0); + } else { + callback(); + } + + }; + + // additional soundManager properties that soundManager.setup() will accept + + extraOptions = { + onready: 1, + ontimeout: 1, + defaultOptions: 1, + flash9Options: 1, + movieStarOptions: 1 + }; + + assign = function(o, oParent) { + + /** + * recursive assignment of properties, soundManager.setup() helper + * allows property assignment based on whitelist + */ + + var i, + result = true, + hasParent = (oParent !== _undefined), + setupOptions = sm2.setupOptions, + bonusOptions = extraOptions; + + // + + // if soundManager.setup() called, show accepted parameters. + + if (o === _undefined) { + + result = []; + + for (i in setupOptions) { + + if (setupOptions.hasOwnProperty(i)) { + result.push(i); + } + + } + + for (i in bonusOptions) { + + if (bonusOptions.hasOwnProperty(i)) { + + if (typeof sm2[i] === 'object') { + result.push(i + ': {...}'); + } else if (sm2[i] instanceof Function) { + result.push(i + ': function() {...}'); + } else { + result.push(i); + } + + } + + } + + sm2._wD(str('setup', result.join(', '))); + + return false; + + } + + // + + for (i in o) { + + if (o.hasOwnProperty(i)) { + + // if not an {object} we want to recurse through... + + if (typeof o[i] !== 'object' || o[i] === null || o[i] instanceof Array || o[i] instanceof RegExp) { + + // check "allowed" options + + if (hasParent && bonusOptions[oParent] !== _undefined) { + + // valid recursive / nested object option, eg., { defaultOptions: { volume: 50 } } + sm2[oParent][i] = o[i]; + + } else if (setupOptions[i] !== _undefined) { + + // special case: assign to setupOptions object, which soundManager property references + sm2.setupOptions[i] = o[i]; + + // assign directly to soundManager, too + sm2[i] = o[i]; + + } else if (bonusOptions[i] === _undefined) { + + // invalid or disallowed parameter. complain. + complain(str((sm2[i] === _undefined ? 'setupUndef' : 'setupError'), i), 2); + + result = false; + + } else if (sm2[i] instanceof Function) { + + /** + * valid extraOptions (bonusOptions) parameter. + * is it a method, like onready/ontimeout? call it. + * multiple parameters should be in an array, eg. soundManager.setup({onready: [myHandler, myScope]}); + */ + sm2[i].apply(sm2, (o[i] instanceof Array ? o[i] : [o[i]])); + + } else { + + // good old-fashioned direct assignment + sm2[i] = o[i]; + + } + + } else if (bonusOptions[i] === _undefined) { + + // recursion case, eg., { defaultOptions: { ... } } + + // invalid or disallowed parameter. complain. + complain(str((sm2[i] === _undefined ? 'setupUndef' : 'setupError'), i), 2); + + result = false; + + } else { + + // recurse through object + return assign(o[i], i); + + } + + } + + } + + return result; + + }; + + function preferFlashCheck(kind) { + + // whether flash should play a given type + return (sm2.preferFlash && hasFlash && !sm2.ignoreFlash && (sm2.flash[kind] !== _undefined && sm2.flash[kind])); + + } + + /** + * Internal DOM2-level event helpers + * --------------------------------- + */ + + event = (function() { + + // normalize event methods + var old = (window.attachEvent), + evt = { + add: (old ? 'attachEvent' : 'addEventListener'), + remove: (old ? 'detachEvent' : 'removeEventListener') + }; + + // normalize "on" event prefix, optional capture argument + function getArgs(oArgs) { + + var args = slice.call(oArgs), + len = args.length; + + if (old) { + // prefix + args[1] = 'on' + args[1]; + if (len > 3) { + // no capture + args.pop(); + } + } else if (len === 3) { + args.push(false); + } + + return args; + + } + + function apply(args, sType) { + + // normalize and call the event method, with the proper arguments + var element = args.shift(), + method = [evt[sType]]; + + if (old) { + // old IE can't do apply(). + element[method](args[0], args[1]); + } else { + element[method].apply(element, args); + } + + } + + function add() { + apply(getArgs(arguments), 'add'); + } + + function remove() { + apply(getArgs(arguments), 'remove'); + } + + return { + add: add, + remove: remove + }; + + }()); + + /** + * Internal HTML5 event handling + * ----------------------------- + */ + + function html5_event(oFn) { + + // wrap html5 event handlers so we don't call them on destroyed and/or unloaded sounds + + return function(e) { + + var s = this._s, + result; + + if (!s || !s._a) { + // + if (s && s.id) { + sm2._wD(s.id + ': Ignoring ' + e.type); + } else { + sm2._wD(h5 + 'Ignoring ' + e.type); + } + // + result = null; + } else { + result = oFn.call(this, e); + } + + return result; + + }; + + } + + html5_events = { + + // HTML5 event-name-to-handler map + + abort: html5_event(function() { + + sm2._wD(this._s.id + ': abort'); + + }), + + // enough has loaded to play + + canplay: html5_event(function() { + + var s = this._s, + position1K; + + if (s._html5_canplay) { + // this event has already fired. ignore. + return; + } + + s._html5_canplay = true; + sm2._wD(s.id + ': canplay'); + s._onbufferchange(0); + + // position according to instance options + position1K = (s._iO.position !== _undefined && !isNaN(s._iO.position) ? s._iO.position / msecScale : null); + + // set the position if position was provided before the sound loaded + if (this.currentTime !== position1K) { + sm2._wD(s.id + ': canplay: Setting position to ' + position1K); + try { + this.currentTime = position1K; + } catch(ee) { + sm2._wD(s.id + ': canplay: Setting position of ' + position1K + ' failed: ' + ee.message, 2); + } + } + + // hack for HTML5 from/to case + if (s._iO._oncanplay) { + s._iO._oncanplay(); + } + + }), + + canplaythrough: html5_event(function() { + + var s = this._s; + + if (!s.loaded) { + s._onbufferchange(0); + s._whileloading(s.bytesLoaded, s.bytesTotal, s._get_html5_duration()); + s._onload(true); + } + + }), + + durationchange: html5_event(function() { + + // durationchange may fire at various times, probably the safest way to capture accurate/final duration. + + var s = this._s, + duration; + + duration = s._get_html5_duration(); + + if (!isNaN(duration) && duration !== s.duration) { + + sm2._wD(this._s.id + ': durationchange (' + duration + ')' + (s.duration ? ', previously ' + s.duration : '')); + + s.durationEstimate = s.duration = duration; + + } + + }), + + // TODO: Reserved for potential use + /* + emptied: html5_event(function() { + + sm2._wD(this._s.id + ': emptied'); + + }), + */ + + ended: html5_event(function() { + + var s = this._s; + + sm2._wD(s.id + ': ended'); + + s._onfinish(); + + }), + + error: html5_event(function() { + + var description = (html5ErrorCodes[this.error.code] || null); + sm2._wD(this._s.id + ': HTML5 error, code ' + this.error.code + (description ? ' (' + description + ')' : '')); + this._s._onload(false); + this._s._onerror(this.error.code, description); + + }), + + loadeddata: html5_event(function() { + + var s = this._s; + + sm2._wD(s.id + ': loadeddata'); + + // safari seems to nicely report progress events, eventually totalling 100% + if (!s._loaded && !isSafari) { + s.duration = s._get_html5_duration(); + } + + }), + + loadedmetadata: html5_event(function() { + + sm2._wD(this._s.id + ': loadedmetadata'); + + }), + + loadstart: html5_event(function() { + + sm2._wD(this._s.id + ': loadstart'); + // assume buffering at first + this._s._onbufferchange(1); + + }), + + play: html5_event(function() { + + // sm2._wD(this._s.id + ': play()'); + // once play starts, no buffering + this._s._onbufferchange(0); + + }), + + playing: html5_event(function() { + + sm2._wD(this._s.id + ': playing ' + String.fromCharCode(9835)); + // once play starts, no buffering + this._s._onbufferchange(0); + + }), + + progress: html5_event(function(e) { + + // note: can fire repeatedly after "loaded" event, due to use of HTTP range/partials + + var s = this._s, + i, j, progStr, buffered = 0, + isProgress = (e.type === 'progress'), + ranges = e.target.buffered, + // firefox 3.6 implements e.loaded/total (bytes) + loaded = (e.loaded || 0), + total = (e.total || 1); + + // reset the "buffered" (loaded byte ranges) array + s.buffered = []; + + if (ranges && ranges.length) { + + // if loaded is 0, try TimeRanges implementation as % of load + // https://developer.mozilla.org/en/DOM/TimeRanges + + // re-build "buffered" array + // HTML5 returns seconds. SM2 API uses msec for setPosition() etc., whether Flash or HTML5. + for (i = 0, j = ranges.length; i < j; i++) { + s.buffered.push({ + start: ranges.start(i) * msecScale, + end: ranges.end(i) * msecScale + }); + } + + // use the last value locally + buffered = (ranges.end(0) - ranges.start(0)) * msecScale; + + // linear case, buffer sum; does not account for seeking and HTTP partials / byte ranges + loaded = Math.min(1, buffered / (e.target.duration * msecScale)); + + // + if (isProgress && ranges.length > 1) { + progStr = []; + j = ranges.length; + for (i = 0; i < j; i++) { + progStr.push((e.target.buffered.start(i) * msecScale) + '-' + (e.target.buffered.end(i) * msecScale)); + } + sm2._wD(this._s.id + ': progress, timeRanges: ' + progStr.join(', ')); + } + + if (isProgress && !isNaN(loaded)) { + sm2._wD(this._s.id + ': progress, ' + Math.floor(loaded * 100) + '% loaded'); + } + // + + } + + if (!isNaN(loaded)) { + + // TODO: prevent calls with duplicate values. + s._whileloading(loaded, total, s._get_html5_duration()); + if (loaded && total && loaded === total) { + // in case "onload" doesn't fire (eg. gecko 1.9.2) + html5_events.canplaythrough.call(this, e); + } + + } + + }), + + ratechange: html5_event(function() { + + sm2._wD(this._s.id + ': ratechange'); + + }), + + suspend: html5_event(function(e) { + + // download paused/stopped, may have finished (eg. onload) + var s = this._s; + + sm2._wD(this._s.id + ': suspend'); + html5_events.progress.call(this, e); + s._onsuspend(); + + }), + + stalled: html5_event(function() { + + sm2._wD(this._s.id + ': stalled'); + + }), + + timeupdate: html5_event(function() { + + this._s._onTimer(); + + }), + + waiting: html5_event(function() { + + var s = this._s; + + // see also: seeking + sm2._wD(this._s.id + ': waiting'); + + // playback faster than download rate, etc. + s._onbufferchange(1); + + }) + + }; + + html5OK = function(iO) { + + // playability test based on URL or MIME type + + var result; + + if (!iO || (!iO.type && !iO.url && !iO.serverURL)) { + + // nothing to check + result = false; + + } else if (iO.serverURL || (iO.type && preferFlashCheck(iO.type))) { + + // RTMP, or preferring flash + result = false; + + } else { + + // Use type, if specified. Pass data: URIs to HTML5. If HTML5-only mode, no other options, so just give 'er + result = ((iO.type ? html5CanPlay({ type: iO.type }) : html5CanPlay({ url: iO.url }) || sm2.html5Only || iO.url.match(/data:/i))); + + } + + return result; + + }; + + html5Unload = function(oAudio) { + + /** + * Internal method: Unload media, and cancel any current/pending network requests. + * Firefox can load an empty URL, which allegedly destroys the decoder and stops the download. + * https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Stopping_the_download_of_media + * However, Firefox has been seen loading a relative URL from '' and thus requesting the hosting page on unload. + * Other UA behaviour is unclear, so everyone else gets an about:blank-style URL. + */ + + var url; + + if (oAudio) { + + // Firefox and Chrome accept short WAVe data: URIs. Chome dislikes audio/wav, but accepts audio/wav for data: MIME. + // Desktop Safari complains / fails on data: URI, so it gets about:blank. + url = (isSafari ? emptyURL : (sm2.html5.canPlayType('audio/wav') ? emptyWAV : emptyURL)); + + oAudio.src = url; + + // reset some state, too + if (oAudio._called_unload !== _undefined) { + oAudio._called_load = false; + } + + } + + if (useGlobalHTML5Audio) { + + // ensure URL state is trashed, also + lastGlobalHTML5URL = null; + + } + + return url; + + }; + + html5CanPlay = function(o) { + + /** + * Try to find MIME, test and return truthiness + * o = { + * url: '/path/to/an.mp3', + * type: 'audio/mp3' + * } + */ + + if (!sm2.useHTML5Audio || !sm2.hasHTML5) return false; + + var url = (o.url || null), + mime = (o.type || null), + aF = sm2.audioFormats, + result, + offset, + fileExt, + item; + + // account for known cases like audio/mp3 + + if (mime && sm2.html5[mime] !== _undefined) return (sm2.html5[mime] && !preferFlashCheck(mime)); + + if (!html5Ext) { + + html5Ext = []; + + for (item in aF) { + + if (aF.hasOwnProperty(item)) { + + html5Ext.push(item); + + if (aF[item].related) { + html5Ext = html5Ext.concat(aF[item].related); + } + + } + + } + + html5Ext = new RegExp('\\.(' + html5Ext.join('|') + ')(\\?.*)?$', 'i'); + + } + + // TODO: Strip URL queries, etc. + fileExt = (url ? url.toLowerCase().match(html5Ext) : null); + + if (!fileExt || !fileExt.length) { + + if (!mime) { + + result = false; + + } else { + + // audio/mp3 -> mp3, result should be known + offset = mime.indexOf(';'); + + // strip "audio/X; codecs..." + fileExt = (offset !== -1 ? mime.substr(0, offset) : mime).substr(6); + + } + + } else { + + // match the raw extension name - "mp3", for example + fileExt = fileExt[1]; + + } + + if (fileExt && sm2.html5[fileExt] !== _undefined) { + + // result known + result = (sm2.html5[fileExt] && !preferFlashCheck(fileExt)); + + } else { + + mime = 'audio/' + fileExt; + result = sm2.html5.canPlayType({ type: mime }); + + sm2.html5[fileExt] = result; + + // sm2._wD('canPlayType, found result: ' + result); + result = (result && sm2.html5[mime] && !preferFlashCheck(mime)); + } + + return result; + + }; + + testHTML5 = function() { + + /** + * Internal: Iterates over audioFormats, determining support eg. audio/mp3, audio/mpeg and so on + * assigns results to html5[] and flash[]. + */ + + if (!sm2.useHTML5Audio || !sm2.hasHTML5) { + + // without HTML5, we need Flash. + sm2.html5.usingFlash = true; + needsFlash = true; + + return false; + + } + + // double-whammy: Opera 9.64 throws WRONG_ARGUMENTS_ERR if no parameter passed to Audio(), and Webkit + iOS happily tries to load "null" as a URL. :/ + var a = (Audio !== _undefined ? (isOpera && opera.version() < 10 ? new Audio(null) : new Audio()) : null), + item, lookup, support = {}, aF, i; + + function cp(m) { + + var canPlay, j, + result = false, + isOK = false; + + if (!a || typeof a.canPlayType !== 'function') return result; + + if (m instanceof Array) { + + // iterate through all mime types, return any successes + + for (i = 0, j = m.length; i < j; i++) { + + if (sm2.html5[m[i]] || a.canPlayType(m[i]).match(sm2.html5Test)) { + + isOK = true; + sm2.html5[m[i]] = true; + + // note flash support, too + sm2.flash[m[i]] = !!(m[i].match(flashMIME)); + + } + + } + + result = isOK; + + } else { + + canPlay = (a && typeof a.canPlayType === 'function' ? a.canPlayType(m) : false); + result = !!(canPlay && (canPlay.match(sm2.html5Test))); + + } + + return result; + + } + + // test all registered formats + codecs + + aF = sm2.audioFormats; + + for (item in aF) { + + if (aF.hasOwnProperty(item)) { + + lookup = 'audio/' + item; + + support[item] = cp(aF[item].type); + + // write back generic type too, eg. audio/mp3 + support[lookup] = support[item]; + + // assign flash + if (item.match(flashMIME)) { + + sm2.flash[item] = true; + sm2.flash[lookup] = true; + + } else { + + sm2.flash[item] = false; + sm2.flash[lookup] = false; + + } + + // assign result to related formats, too + + if (aF[item] && aF[item].related) { + + for (i = aF[item].related.length - 1; i >= 0; i--) { + + // eg. audio/m4a + support['audio/' + aF[item].related[i]] = support[item]; + sm2.html5[aF[item].related[i]] = support[item]; + sm2.flash[aF[item].related[i]] = support[item]; + + } + + } + + } + + } + + support.canPlayType = (a ? cp : null); + sm2.html5 = mixin(sm2.html5, support); + + sm2.html5.usingFlash = featureCheck(); + needsFlash = sm2.html5.usingFlash; + + return true; + + }; + + strings = { + + // + notReady: 'Unavailable - wait until onready() has fired.', + notOK: 'Audio support is not available.', + domError: sm + 'exception caught while appending SWF to DOM.', + spcWmode: 'Removing wmode, preventing known SWF loading issue(s)', + swf404: smc + 'Verify that %s is a valid path.', + tryDebug: 'Try ' + sm + '.debugFlash = true for more security details (output goes to SWF.)', + checkSWF: 'See SWF output for more debug info.', + localFail: smc + 'Non-HTTP page (' + doc.location.protocol + ' URL?) Review Flash player security settings for this special case:\nhttp://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html\nMay need to add/allow path, eg. c:/sm2/ or /users/me/sm2/', + waitFocus: smc + 'Special case: Waiting for SWF to load with window focus...', + waitForever: smc + 'Waiting indefinitely for Flash (will recover if unblocked)...', + waitSWF: smc + 'Waiting for 100% SWF load...', + needFunction: smc + 'Function object expected for %s', + badID: 'Sound ID "%s" should be a string, starting with a non-numeric character', + currentObj: smc + '_debug(): Current sound objects', + waitOnload: smc + 'Waiting for window.onload()', + docLoaded: smc + 'Document already loaded', + onload: smc + 'initComplete(): calling soundManager.onload()', + onloadOK: sm + '.onload() complete', + didInit: smc + 'init(): Already called?', + secNote: 'Flash security note: Network/internet URLs will not load due to security restrictions. Access can be configured via Flash Player Global Security Settings Page: http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html', + badRemove: smc + 'Failed to remove Flash node.', + shutdown: sm + '.disable(): Shutting down', + queue: smc + 'Queueing %s handler', + smError: 'SMSound.load(): Exception: JS-Flash communication failed, or JS error.', + fbTimeout: 'No flash response, applying .' + swfCSS.swfTimedout + ' CSS...', + fbLoaded: 'Flash loaded', + fbHandler: smc + 'flashBlockHandler()', + manURL: 'SMSound.load(): Using manually-assigned URL', + onURL: sm + '.load(): current URL already assigned.', + badFV: sm + '.flashVersion must be 8 or 9. "%s" is invalid. Reverting to %s.', + as2loop: 'Note: Setting stream:false so looping can work (flash 8 limitation)', + noNSLoop: 'Note: Looping not implemented for MovieStar formats', + needfl9: 'Note: Switching to flash 9, required for MP4 formats.', + mfTimeout: 'Setting flashLoadTimeout = 0 (infinite) for off-screen, mobile flash case', + needFlash: smc + 'Fatal error: Flash is needed to play some required formats, but is not available.', + gotFocus: smc + 'Got window focus.', + policy: 'Enabling usePolicyFile for data access', + setup: sm + '.setup(): allowed parameters: %s', + setupError: sm + '.setup(): "%s" cannot be assigned with this method.', + setupUndef: sm + '.setup(): Could not find option "%s"', + setupLate: sm + '.setup(): url, flashVersion and html5Test property changes will not take effect until reboot().', + noURL: smc + 'Flash URL required. Call soundManager.setup({url:...}) to get started.', + sm2Loaded: 'SoundManager 2: Ready. ' + String.fromCharCode(10003), + reset: sm + '.reset(): Removing event callbacks', + mobileUA: 'Mobile UA detected, preferring HTML5 by default.', + globalHTML5: 'Using singleton HTML5 Audio() pattern for this device.', + ignoreMobile: 'Ignoring mobile restrictions for this device.' + // + + }; + + str = function() { + + // internal string replace helper. + // arguments: o [,items to replace] + // + + var args, + i, j, o, + sstr; + + // real array, please + args = slice.call(arguments); + + // first argument + o = args.shift(); + + sstr = (strings && strings[o] ? strings[o] : ''); + + if (sstr && args && args.length) { + for (i = 0, j = args.length; i < j; i++) { + sstr = sstr.replace('%s', args[i]); + } + } + + return sstr; + // + + }; + + loopFix = function(sOpt) { + + // flash 8 requires stream = false for looping to work + if (fV === 8 && sOpt.loops > 1 && sOpt.stream) { + _wDS('as2loop'); + sOpt.stream = false; + } + + return sOpt; + + }; + + policyFix = function(sOpt, sPre) { + + if (sOpt && !sOpt.usePolicyFile && (sOpt.onid3 || sOpt.usePeakData || sOpt.useWaveformData || sOpt.useEQData)) { + sm2._wD((sPre || '') + str('policy')); + sOpt.usePolicyFile = true; + } + + return sOpt; + + }; + + complain = function(sMsg) { + + // + if (hasConsole && console.warn !== _undefined) { + console.warn(sMsg); + } else { + sm2._wD(sMsg); + } + // + + }; + + doNothing = function() { + + return false; + + }; + + disableObject = function(o) { + + var oProp; + + for (oProp in o) { + if (o.hasOwnProperty(oProp) && typeof o[oProp] === 'function') { + o[oProp] = doNothing; + } + } + + oProp = null; + + }; + + failSafely = function(bNoDisable) { + + // general failure exception handler + + if (bNoDisable === _undefined) { + bNoDisable = false; + } + + if (disabled || bNoDisable) { + sm2.disable(bNoDisable); + } + + }; + + normalizeMovieURL = function(movieURL) { + + var urlParams = null, url; + + if (movieURL) { + + if (movieURL.match(/\.swf(\?.*)?$/i)) { + + urlParams = movieURL.substr(movieURL.toLowerCase().lastIndexOf('.swf?') + 4); + + // assume user knows what they're doing + if (urlParams) return movieURL; + + } else if (movieURL.lastIndexOf('/') !== movieURL.length - 1) { + + // append trailing slash, if needed + movieURL += '/'; + + } + + } + + url = (movieURL && movieURL.lastIndexOf('/') !== -1 ? movieURL.substr(0, movieURL.lastIndexOf('/') + 1) : './') + sm2.movieURL; + + if (sm2.noSWFCache) { + url += ('?ts=' + new Date().getTime()); + } + + return url; + + }; + + setVersionInfo = function() { + + // short-hand for internal use + + fV = parseInt(sm2.flashVersion, 10); + + if (fV !== 8 && fV !== 9) { + sm2._wD(str('badFV', fV, defaultFlashVersion)); + sm2.flashVersion = fV = defaultFlashVersion; + } + + // debug flash movie, if applicable + + var isDebug = (sm2.debugMode || sm2.debugFlash ? '_debug.swf' : '.swf'); + + if (sm2.useHTML5Audio && !sm2.html5Only && sm2.audioFormats.mp4.required && fV < 9) { + sm2._wD(str('needfl9')); + sm2.flashVersion = fV = 9; + } + + sm2.version = sm2.versionNumber + (sm2.html5Only ? ' (HTML5-only mode)' : (fV === 9 ? ' (AS3/Flash 9)' : ' (AS2/Flash 8)')); + + // set up default options + if (fV > 8) { + + // +flash 9 base options + sm2.defaultOptions = mixin(sm2.defaultOptions, sm2.flash9Options); + sm2.features.buffering = true; + + // +moviestar support + sm2.defaultOptions = mixin(sm2.defaultOptions, sm2.movieStarOptions); + sm2.filePatterns.flash9 = new RegExp('\\.(mp3|' + netStreamTypes.join('|') + ')(\\?.*)?$', 'i'); + sm2.features.movieStar = true; + + } else { + + sm2.features.movieStar = false; + + } + + // regExp for flash canPlay(), etc. + sm2.filePattern = sm2.filePatterns[(fV !== 8 ? 'flash9' : 'flash8')]; + + // if applicable, use _debug versions of SWFs + sm2.movieURL = (fV === 8 ? 'soundmanager2.swf' : 'soundmanager2_flash9.swf').replace('.swf', isDebug); + + sm2.features.peakData = sm2.features.waveformData = sm2.features.eqData = (fV > 8); + + }; + + setPolling = function(bPolling, bHighPerformance) { + + if (!flash) { + return; + } + + flash._setPolling(bPolling, bHighPerformance); + + }; + + initDebug = function() { + + // starts debug mode, creating output
    for UAs without console object + + // allow force of debug mode via URL + // + if (sm2.debugURLParam.test(wl)) { + sm2.setupOptions.debugMode = sm2.debugMode = true; + } + + if (id(sm2.debugID)) { + return; + } + + var oD, oDebug, oTarget, oToggle, tmp; + + if (sm2.debugMode && !id(sm2.debugID) && (!hasConsole || !sm2.useConsole || !sm2.consoleOnly)) { + + oD = doc.createElement('div'); + oD.id = sm2.debugID + '-toggle'; + + oToggle = { + position: 'fixed', + bottom: '0px', + right: '0px', + width: '1.2em', + height: '1.2em', + lineHeight: '1.2em', + margin: '2px', + textAlign: 'center', + border: '1px solid #999', + cursor: 'pointer', + background: '#fff', + color: '#333', + zIndex: 10001 + }; + + oD.appendChild(doc.createTextNode('-')); + oD.onclick = toggleDebug; + oD.title = 'Toggle SM2 debug console'; + + if (ua.match(/msie 6/i)) { + oD.style.position = 'absolute'; + oD.style.cursor = 'hand'; + } + + for (tmp in oToggle) { + if (oToggle.hasOwnProperty(tmp)) { + oD.style[tmp] = oToggle[tmp]; + } + } + + oDebug = doc.createElement('div'); + oDebug.id = sm2.debugID; + oDebug.style.display = (sm2.debugMode ? 'block' : 'none'); + + if (sm2.debugMode && !id(oD.id)) { + try { + oTarget = getDocument(); + oTarget.appendChild(oD); + } catch(e2) { + throw new Error(str('domError') + ' \n' + e2.toString()); + } + oTarget.appendChild(oDebug); + } + + } + + oTarget = null; + // + + }; + + idCheck = this.getSoundById; + + // + _wDS = function(o, errorLevel) { + + return (!o ? '' : sm2._wD(str(o), errorLevel)); + + }; + + toggleDebug = function() { + + var o = id(sm2.debugID), + oT = id(sm2.debugID + '-toggle'); + + if (!o) { + return; + } + + if (debugOpen) { + // minimize + oT.innerHTML = '+'; + o.style.display = 'none'; + } else { + oT.innerHTML = '-'; + o.style.display = 'block'; + } + + debugOpen = !debugOpen; + + }; + + debugTS = function(sEventType, bSuccess, sMessage) { + + // troubleshooter debug hooks + + if (window.sm2Debugger !== _undefined) { + try { + sm2Debugger.handleEvent(sEventType, bSuccess, sMessage); + } catch(e) { + // oh well + return false; + } + } + + return true; + + }; + // + + getSWFCSS = function() { + + var css = []; + + if (sm2.debugMode) { + css.push(swfCSS.sm2Debug); + } + + if (sm2.debugFlash) { + css.push(swfCSS.flashDebug); + } + + if (sm2.useHighPerformance) { + css.push(swfCSS.highPerf); + } + + return css.join(' '); + + }; + + flashBlockHandler = function() { + + // *possible* flash block situation. + + var name = str('fbHandler'), + p = sm2.getMoviePercent(), + css = swfCSS, + error = { + type: 'FLASHBLOCK' + }; + + if (sm2.html5Only) { + // no flash, or unused + return; + } + + if (!sm2.ok()) { + + if (needsFlash) { + // make the movie more visible, so user can fix + sm2.oMC.className = getSWFCSS() + ' ' + css.swfDefault + ' ' + (p === null ? css.swfTimedout : css.swfError); + sm2._wD(name + ': ' + str('fbTimeout') + (p ? ' (' + str('fbLoaded') + ')' : '')); + } + + sm2.didFlashBlock = true; + + // fire onready(), complain lightly + processOnEvents({ + type: 'ontimeout', + ignoreInit: true, + error: error + }); + + catchError(error); + + } else { + + // SM2 loaded OK (or recovered) + + // + if (sm2.didFlashBlock) { + sm2._wD(name + ': Unblocked'); + } + // + + if (sm2.oMC) { + sm2.oMC.className = [getSWFCSS(), css.swfDefault, css.swfLoaded + (sm2.didFlashBlock ? ' ' + css.swfUnblocked : '')].join(' '); + } + + } + + }; + + addOnEvent = function(sType, oMethod, oScope) { + + if (on_queue[sType] === _undefined) { + on_queue[sType] = []; + } + + on_queue[sType].push({ + method: oMethod, + scope: (oScope || null), + fired: false + }); + + }; + + processOnEvents = function(oOptions) { + + // if unspecified, assume OK/error + + if (!oOptions) { + oOptions = { + type: (sm2.ok() ? 'onready' : 'ontimeout') + }; + } + + // not ready yet. + if (!didInit && oOptions && !oOptions.ignoreInit) return false; + + // invalid case + if (oOptions.type === 'ontimeout' && (sm2.ok() || (disabled && !oOptions.ignoreInit))) return false; + + var status = { + success: (oOptions && oOptions.ignoreInit ? sm2.ok() : !disabled) + }, + + // queue specified by type, or none + srcQueue = (oOptions && oOptions.type ? on_queue[oOptions.type] || [] : []), + + queue = [], i, j, + args = [status], + canRetry = (needsFlash && !sm2.ok()); + + if (oOptions.error) { + args[0].error = oOptions.error; + } + + for (i = 0, j = srcQueue.length; i < j; i++) { + if (srcQueue[i].fired !== true) { + queue.push(srcQueue[i]); + } + } + + if (queue.length) { + + // sm2._wD(sm + ': Firing ' + queue.length + ' ' + oOptions.type + '() item' + (queue.length === 1 ? '' : 's')); + for (i = 0, j = queue.length; i < j; i++) { + + if (queue[i].scope) { + queue[i].method.apply(queue[i].scope, args); + } else { + queue[i].method.apply(this, args); + } + + if (!canRetry) { + // useFlashBlock and SWF timeout case doesn't count here. + queue[i].fired = true; + + } + + } + + } + + return true; + + }; + + initUserOnload = function() { + + window.setTimeout(function() { + + if (sm2.useFlashBlock) { + flashBlockHandler(); + } + + processOnEvents(); + + // call user-defined "onload", scoped to window + + if (typeof sm2.onload === 'function') { + _wDS('onload', 1); + sm2.onload.apply(window); + _wDS('onloadOK', 1); + } + + if (sm2.waitForWindowLoad) { + event.add(window, 'load', initUserOnload); + } + + }, 1); + + }; + + detectFlash = function() { + + /** + * Hat tip: Flash Detect library (BSD, (C) 2007) by Carl "DocYes" S. Yestrau + * http://featureblend.com/javascript-flash-detection-library.html / http://featureblend.com/license.txt + */ + + // this work has already been done. + if (hasFlash !== _undefined) return hasFlash; + + var hasPlugin = false, n = navigator, obj, type, types, AX = window.ActiveXObject; + + // MS Edge 14 throws an "Unspecified Error" because n.plugins is inaccessible due to permissions + var nP; + + try { + nP = n.plugins; + } catch(e) { + nP = undefined; + } + + if (nP && nP.length) { + + type = 'application/x-shockwave-flash'; + types = n.mimeTypes; + + if (types && types[type] && types[type].enabledPlugin && types[type].enabledPlugin.description) { + hasPlugin = true; + } + + } else if (AX !== _undefined && !ua.match(/MSAppHost/i)) { + + // Windows 8 Store Apps (MSAppHost) are weird (compatibility?) and won't complain here, but will barf if Flash/ActiveX object is appended to the DOM. + try { + obj = new AX('ShockwaveFlash.ShockwaveFlash'); + } catch(e) { + // oh well + obj = null; + } + + hasPlugin = (!!obj); + + // cleanup, because it is ActiveX after all + obj = null; + + } + + hasFlash = hasPlugin; + + return hasPlugin; + + }; + + featureCheck = function() { + + var flashNeeded, + item, + formats = sm2.audioFormats, + // iPhone <= 3.1 has broken HTML5 audio(), but firmware 3.2 (original iPad) + iOS4 works. + isSpecial = (is_iDevice && !!(ua.match(/os (1|2|3_0|3_1)\s/i))); + + if (isSpecial) { + + // has Audio(), but is broken; let it load links directly. + sm2.hasHTML5 = false; + + // ignore flash case, however + sm2.html5Only = true; + + // hide the SWF, if present + if (sm2.oMC) { + sm2.oMC.style.display = 'none'; + } + + } else if (sm2.useHTML5Audio) { + + if (!sm2.html5 || !sm2.html5.canPlayType) { + sm2._wD('SoundManager: No HTML5 Audio() support detected.'); + sm2.hasHTML5 = false; + } + + // + if (isBadSafari) { + sm2._wD(smc + 'Note: Buggy HTML5 Audio in Safari on this OS X release, see https://bugs.webkit.org/show_bug.cgi?id=32159 - ' + (!hasFlash ? ' would use flash fallback for MP3/MP4, but none detected.' : 'will use flash fallback for MP3/MP4, if available'), 1); + } + // + + } + + if (sm2.useHTML5Audio && sm2.hasHTML5) { + + // sort out whether flash is optional, required or can be ignored. + + // innocent until proven guilty. + canIgnoreFlash = true; + + for (item in formats) { + + if (formats.hasOwnProperty(item)) { + + if (formats[item].required) { + + if (!sm2.html5.canPlayType(formats[item].type)) { + + // 100% HTML5 mode is not possible. + canIgnoreFlash = false; + flashNeeded = true; + + } else if (sm2.preferFlash && (sm2.flash[item] || sm2.flash[formats[item].type])) { + + // flash may be required, or preferred for this format. + flashNeeded = true; + + } + + } + + } + + } + + } + + // sanity check... + if (sm2.ignoreFlash) { + flashNeeded = false; + canIgnoreFlash = true; + } + + sm2.html5Only = (sm2.hasHTML5 && sm2.useHTML5Audio && !flashNeeded); + + return (!sm2.html5Only); + + }; + + parseURL = function(url) { + + /** + * Internal: Finds and returns the first playable URL (or failing that, the first URL.) + * @param {string or array} url A single URL string, OR, an array of URL strings or {url:'/path/to/resource', type:'audio/mp3'} objects. + */ + + var i, j, urlResult = 0, result; + + if (url instanceof Array) { + + // find the first good one + for (i = 0, j = url.length; i < j; i++) { + + if (url[i] instanceof Object) { + + // MIME check + if (sm2.canPlayMIME(url[i].type)) { + urlResult = i; + break; + } + + } else if (sm2.canPlayURL(url[i])) { + + // URL string check + urlResult = i; + break; + + } + + } + + // normalize to string + if (url[urlResult].url) { + url[urlResult] = url[urlResult].url; + } + + result = url[urlResult]; + + } else { + + // single URL case + result = url; + + } + + return result; + + }; + + + startTimer = function(oSound) { + + /** + * attach a timer to this sound, and start an interval if needed + */ + + if (!oSound._hasTimer) { + + oSound._hasTimer = true; + + if (!mobileHTML5 && sm2.html5PollingInterval) { + + if (h5IntervalTimer === null && h5TimerCount === 0) { + + h5IntervalTimer = setInterval(timerExecute, sm2.html5PollingInterval); + + } + + h5TimerCount++; + + } + + } + + }; + + stopTimer = function(oSound) { + + /** + * detach a timer + */ + + if (oSound._hasTimer) { + + oSound._hasTimer = false; + + if (!mobileHTML5 && sm2.html5PollingInterval) { + + // interval will stop itself at next execution. + + h5TimerCount--; + + } + + } + + }; + + timerExecute = function() { + + /** + * manual polling for HTML5 progress events, ie., whileplaying() + * (can achieve greater precision than conservative default HTML5 interval) + */ + + var i; + + if (h5IntervalTimer !== null && !h5TimerCount) { + + // no active timers, stop polling interval. + + clearInterval(h5IntervalTimer); + + h5IntervalTimer = null; + + return; + + } + + // check all HTML5 sounds with timers + + for (i = sm2.soundIDs.length - 1; i >= 0; i--) { + + if (sm2.sounds[sm2.soundIDs[i]].isHTML5 && sm2.sounds[sm2.soundIDs[i]]._hasTimer) { + sm2.sounds[sm2.soundIDs[i]]._onTimer(); + } + + } + + }; + + catchError = function(options) { + + options = (options !== _undefined ? options : {}); + + if (typeof sm2.onerror === 'function') { + sm2.onerror.apply(window, [{ + type: (options.type !== _undefined ? options.type : null) + }]); + } + + if (options.fatal !== _undefined && options.fatal) { + sm2.disable(); + } + + }; + + badSafariFix = function() { + + // special case: "bad" Safari (OS X 10.3 - 10.7) must fall back to flash for MP3/MP4 + if (!isBadSafari || !detectFlash()) { + // doesn't apply + return; + } + + var aF = sm2.audioFormats, i, item; + + for (item in aF) { + + if (aF.hasOwnProperty(item)) { + + if (item === 'mp3' || item === 'mp4') { + + sm2._wD(sm + ': Using flash fallback for ' + item + ' format'); + sm2.html5[item] = false; + + // assign result to related formats, too + if (aF[item] && aF[item].related) { + for (i = aF[item].related.length - 1; i >= 0; i--) { + sm2.html5[aF[item].related[i]] = false; + } + } + + } + + } + + } + + }; + + /** + * Pseudo-private flash/ExternalInterface methods + * ---------------------------------------------- + */ + + this._setSandboxType = function(sandboxType) { + + // + // Security sandbox according to Flash plugin + var sb = sm2.sandbox; + + sb.type = sandboxType; + sb.description = sb.types[(sb.types[sandboxType] !== _undefined ? sandboxType : 'unknown')]; + + if (sb.type === 'localWithFile') { + + sb.noRemote = true; + sb.noLocal = false; + _wDS('secNote', 2); + + } else if (sb.type === 'localWithNetwork') { + + sb.noRemote = false; + sb.noLocal = true; + + } else if (sb.type === 'localTrusted') { + + sb.noRemote = false; + sb.noLocal = false; + + } + // + + }; + + this._externalInterfaceOK = function(swfVersion) { + + // flash callback confirming flash loaded, EI working etc. + // swfVersion: SWF build string + + if (sm2.swfLoaded) { + return; + } + + var e; + + debugTS('swf', true); + debugTS('flashtojs', true); + sm2.swfLoaded = true; + tryInitOnFocus = false; + + if (isBadSafari) { + badSafariFix(); + } + + // complain if JS + SWF build/version strings don't match, excluding +DEV builds + // + if (!swfVersion || swfVersion.replace(/\+dev/i, '') !== sm2.versionNumber.replace(/\+dev/i, '')) { + + e = sm + ': Fatal: JavaScript file build "' + sm2.versionNumber + '" does not match Flash SWF build "' + swfVersion + '" at ' + sm2.url + '. Ensure both are up-to-date.'; + + // escape flash -> JS stack so this error fires in window. + setTimeout(function() { + throw new Error(e); + }, 0); + + // exit, init will fail with timeout + return; + + } + // + + // IE needs a larger timeout + setTimeout(init, isIE ? 100 : 1); + + }; + + /** + * Private initialization helpers + * ------------------------------ + */ + + createMovie = function(movieID, movieURL) { + + // ignore if already connected + if (didAppend && appendSuccess) return false; + + function initMsg() { + + // + + var options = [], + title, + msg = [], + delimiter = ' + '; + + title = 'SoundManager ' + sm2.version + (!sm2.html5Only && sm2.useHTML5Audio ? (sm2.hasHTML5 ? ' + HTML5 audio' : ', no HTML5 audio support') : ''); + + if (!sm2.html5Only) { + + if (sm2.preferFlash) { + options.push('preferFlash'); + } + + if (sm2.useHighPerformance) { + options.push('useHighPerformance'); + } + + if (sm2.flashPollingInterval) { + options.push('flashPollingInterval (' + sm2.flashPollingInterval + 'ms)'); + } + + if (sm2.html5PollingInterval) { + options.push('html5PollingInterval (' + sm2.html5PollingInterval + 'ms)'); + } + + if (sm2.wmode) { + options.push('wmode (' + sm2.wmode + ')'); + } + + if (sm2.debugFlash) { + options.push('debugFlash'); + } + + if (sm2.useFlashBlock) { + options.push('flashBlock'); + } + + } else if (sm2.html5PollingInterval) { + options.push('html5PollingInterval (' + sm2.html5PollingInterval + 'ms)'); + } + + if (options.length) { + msg = msg.concat([options.join(delimiter)]); + } + + sm2._wD(title + (msg.length ? delimiter + msg.join(', ') : ''), 1); + + showSupport(); + + // + + } + + if (sm2.html5Only) { + + // 100% HTML5 mode + setVersionInfo(); + + initMsg(); + sm2.oMC = id(sm2.movieID); + init(); + + // prevent multiple init attempts + didAppend = true; + + appendSuccess = true; + + return false; + + } + + // flash path + var remoteURL = (movieURL || sm2.url), + localURL = (sm2.altURL || remoteURL), + swfTitle = 'JS/Flash audio component (SoundManager 2)', + oTarget = getDocument(), + extraClass = getSWFCSS(), + isRTL = null, + html = doc.getElementsByTagName('html')[0], + oEmbed, oMovie, tmp, movieHTML, oEl, s, x, sClass; + + isRTL = (html && html.dir && html.dir.match(/rtl/i)); + movieID = (movieID === _undefined ? sm2.id : movieID); + + function param(name, value) { + return ''; + } + + // safety check for legacy (change to Flash 9 URL) + setVersionInfo(); + sm2.url = normalizeMovieURL(overHTTP ? remoteURL : localURL); + movieURL = sm2.url; + + sm2.wmode = (!sm2.wmode && sm2.useHighPerformance ? 'transparent' : sm2.wmode); + + if (sm2.wmode !== null && (ua.match(/msie 8/i) || (!isIE && !sm2.useHighPerformance)) && navigator.platform.match(/win32|win64/i)) { + /** + * extra-special case: movie doesn't load until scrolled into view when using wmode = anything but 'window' here + * does not apply when using high performance (position:fixed means on-screen), OR infinite flash load timeout + * wmode breaks IE 8 on Vista + Win7 too in some cases, as of January 2011 (?) + */ + messages.push(strings.spcWmode); + sm2.wmode = null; + } + + oEmbed = { + name: movieID, + id: movieID, + src: movieURL, + quality: 'high', + allowScriptAccess: sm2.allowScriptAccess, + bgcolor: sm2.bgColor, + pluginspage: http + 'www.macromedia.com/go/getflashplayer', + title: swfTitle, + type: 'application/x-shockwave-flash', + wmode: sm2.wmode, + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + hasPriority: 'true' + }; + + if (sm2.debugFlash) { + oEmbed.FlashVars = 'debug=1'; + } + + if (!sm2.wmode) { + // don't write empty attribute + delete oEmbed.wmode; + } + + if (isIE) { + + // IE is "special". + oMovie = doc.createElement('div'); + movieHTML = [ + '', + param('movie', movieURL), + param('AllowScriptAccess', sm2.allowScriptAccess), + param('quality', oEmbed.quality), + (sm2.wmode ? param('wmode', sm2.wmode) : ''), + param('bgcolor', sm2.bgColor), + param('hasPriority', 'true'), + (sm2.debugFlash ? param('FlashVars', oEmbed.FlashVars) : ''), + '' + ].join(''); + + } else { + + oMovie = doc.createElement('embed'); + for (tmp in oEmbed) { + if (oEmbed.hasOwnProperty(tmp)) { + oMovie.setAttribute(tmp, oEmbed[tmp]); + } + } + + } + + initDebug(); + extraClass = getSWFCSS(); + oTarget = getDocument(); + + if (oTarget) { + + sm2.oMC = (id(sm2.movieID) || doc.createElement('div')); + + if (!sm2.oMC.id) { + + sm2.oMC.id = sm2.movieID; + sm2.oMC.className = swfCSS.swfDefault + ' ' + extraClass; + s = null; + oEl = null; + + if (!sm2.useFlashBlock) { + if (sm2.useHighPerformance) { + // on-screen at all times + s = { + position: 'fixed', + width: '8px', + height: '8px', + // >= 6px for flash to run fast, >= 8px to start up under Firefox/win32 in some cases. odd? yes. + bottom: '0px', + left: '0px', + overflow: 'hidden' + }; + } else { + // hide off-screen, lower priority + s = { + position: 'absolute', + width: '6px', + height: '6px', + top: '-9999px', + left: '-9999px' + }; + if (isRTL) { + s.left = Math.abs(parseInt(s.left, 10)) + 'px'; + } + } + } + + if (isWebkit) { + // soundcloud-reported render/crash fix, safari 5 + sm2.oMC.style.zIndex = 10000; + } + + if (!sm2.debugFlash) { + for (x in s) { + if (s.hasOwnProperty(x)) { + sm2.oMC.style[x] = s[x]; + } + } + } + + try { + + if (!isIE) { + sm2.oMC.appendChild(oMovie); + } + + oTarget.appendChild(sm2.oMC); + + if (isIE) { + oEl = sm2.oMC.appendChild(doc.createElement('div')); + oEl.className = swfCSS.swfBox; + oEl.innerHTML = movieHTML; + } + + appendSuccess = true; + + } catch(e) { + + throw new Error(str('domError') + ' \n' + e.toString()); + + } + + } else { + + // SM2 container is already in the document (eg. flashblock use case) + sClass = sm2.oMC.className; + sm2.oMC.className = (sClass ? sClass + ' ' : swfCSS.swfDefault) + (extraClass ? ' ' + extraClass : ''); + sm2.oMC.appendChild(oMovie); + + if (isIE) { + oEl = sm2.oMC.appendChild(doc.createElement('div')); + oEl.className = swfCSS.swfBox; + oEl.innerHTML = movieHTML; + } + + appendSuccess = true; + + } + + } + + didAppend = true; + + initMsg(); + + // sm2._wD(sm + ': Trying to load ' + movieURL + (!overHTTP && sm2.altURL ? ' (alternate URL)' : ''), 1); + + return true; + + }; + + initMovie = function() { + + if (sm2.html5Only) { + createMovie(); + return false; + } + + // attempt to get, or create, movie (may already exist) + if (flash) return false; + + if (!sm2.url) { + + /** + * Something isn't right - we've reached init, but the soundManager url property has not been set. + * User has not called setup({url: ...}), or has not set soundManager.url (legacy use case) directly before init time. + * Notify and exit. If user calls setup() with a url: property, init will be restarted as in the deferred loading case. + */ + + _wDS('noURL'); + return false; + + } + + // inline markup case + flash = sm2.getMovie(sm2.id); + + if (!flash) { + + if (!oRemoved) { + + // try to create + createMovie(sm2.id, sm2.url); + + } else { + + // try to re-append removed movie after reboot() + if (!isIE) { + sm2.oMC.appendChild(oRemoved); + } else { + sm2.oMC.innerHTML = oRemovedHTML; + } + + oRemoved = null; + didAppend = true; + + } + + flash = sm2.getMovie(sm2.id); + + } + + if (typeof sm2.oninitmovie === 'function') { + setTimeout(sm2.oninitmovie, 1); + } + + // + flushMessages(); + // + + return true; + + }; + + delayWaitForEI = function() { + + setTimeout(waitForEI, 1000); + + }; + + rebootIntoHTML5 = function() { + + // special case: try for a reboot with preferFlash: false, if 100% HTML5 mode is possible and useFlashBlock is not enabled. + + window.setTimeout(function() { + + complain(smc + 'useFlashBlock is false, 100% HTML5 mode is possible. Rebooting with preferFlash: false...'); + + sm2.setup({ + preferFlash: false + }).reboot(); + + // if for some reason you want to detect this case, use an ontimeout() callback and look for html5Only and didFlashBlock == true. + sm2.didFlashBlock = true; + + sm2.beginDelayedInit(); + + }, 1); + + }; + + waitForEI = function() { + + var p, + loadIncomplete = false; + + if (!sm2.url) { + // No SWF url to load (noURL case) - exit for now. Will be retried when url is set. + return; + } + + if (waitingForEI) { + return; + } + + waitingForEI = true; + event.remove(window, 'load', delayWaitForEI); + + if (hasFlash && tryInitOnFocus && !isFocused) { + // Safari won't load flash in background tabs, only when focused. + _wDS('waitFocus'); + return; + } + + if (!didInit) { + p = sm2.getMoviePercent(); + if (p > 0 && p < 100) { + loadIncomplete = true; + } + } + + setTimeout(function() { + + p = sm2.getMoviePercent(); + + if (loadIncomplete) { + // special case: if movie *partially* loaded, retry until it's 100% before assuming failure. + waitingForEI = false; + sm2._wD(str('waitSWF')); + window.setTimeout(delayWaitForEI, 1); + return; + } + + // + if (!didInit) { + + sm2._wD(sm + ': No Flash response within expected time. Likely causes: ' + (p === 0 ? 'SWF load failed, ' : '') + 'Flash blocked or JS-Flash security error.' + (sm2.debugFlash ? ' ' + str('checkSWF') : ''), 2); + + if (!overHTTP && p) { + + _wDS('localFail', 2); + + if (!sm2.debugFlash) { + _wDS('tryDebug', 2); + } + + } + + if (p === 0) { + + // if 0 (not null), probably a 404. + sm2._wD(str('swf404', sm2.url), 1); + + } + + debugTS('flashtojs', false, ': Timed out' + (overHTTP ? ' (Check flash security or flash blockers)' : ' (No plugin/missing SWF?)')); + + } + // + + // give up / time-out, depending + + if (!didInit && okToDisable) { + + if (p === null) { + + // SWF failed to report load progress. Possibly blocked. + + if (sm2.useFlashBlock || sm2.flashLoadTimeout === 0) { + + if (sm2.useFlashBlock) { + + flashBlockHandler(); + + } + + _wDS('waitForever'); + + } else if (!sm2.useFlashBlock && canIgnoreFlash) { + + // no custom flash block handling, but SWF has timed out. Will recover if user unblocks / allows SWF load. + rebootIntoHTML5(); + + } else { + + _wDS('waitForever'); + + // fire any regular registered ontimeout() listeners. + processOnEvents({ + type: 'ontimeout', + ignoreInit: true, + error: { + type: 'INIT_FLASHBLOCK' + } + }); + + } + + } else if (sm2.flashLoadTimeout === 0) { + + // SWF loaded? Shouldn't be a blocking issue, then. + + _wDS('waitForever'); + + } else if (!sm2.useFlashBlock && canIgnoreFlash) { + + rebootIntoHTML5(); + + } else { + + failSafely(true); + + } + + } + + }, sm2.flashLoadTimeout); + + }; + + handleFocus = function() { + + function cleanup() { + event.remove(window, 'focus', handleFocus); + } + + if (isFocused || !tryInitOnFocus) { + // already focused, or not special Safari background tab case + cleanup(); + return true; + } + + okToDisable = true; + isFocused = true; + _wDS('gotFocus'); + + // allow init to restart + waitingForEI = false; + + // kick off ExternalInterface timeout, now that the SWF has started + delayWaitForEI(); + + cleanup(); + return true; + + }; + + flushMessages = function() { + + // + + // SM2 pre-init debug messages + if (messages.length) { + sm2._wD('SoundManager 2: ' + messages.join(' '), 1); + messages = []; + } + + // + + }; + + showSupport = function() { + + // + + flushMessages(); + + var item, tests = []; + + if (sm2.useHTML5Audio && sm2.hasHTML5) { + for (item in sm2.audioFormats) { + if (sm2.audioFormats.hasOwnProperty(item)) { + tests.push(item + ' = ' + sm2.html5[item] + (!sm2.html5[item] && needsFlash && sm2.flash[item] ? ' (using flash)' : (sm2.preferFlash && sm2.flash[item] && needsFlash ? ' (preferring flash)' : (!sm2.html5[item] ? ' (' + (sm2.audioFormats[item].required ? 'required, ' : '') + 'and no flash support)' : '')))); + } + } + sm2._wD('SoundManager 2 HTML5 support: ' + tests.join(', '), 1); + } + + // + + }; + + initComplete = function(bNoDisable) { + + if (didInit) return false; + + if (sm2.html5Only) { + // all good. + _wDS('sm2Loaded', 1); + didInit = true; + initUserOnload(); + debugTS('onload', true); + return true; + } + + var wasTimeout = (sm2.useFlashBlock && sm2.flashLoadTimeout && !sm2.getMoviePercent()), + result = true, + error; + + if (!wasTimeout) { + didInit = true; + } + + error = { + type: (!hasFlash && needsFlash ? 'NO_FLASH' : 'INIT_TIMEOUT') + }; + + sm2._wD('SoundManager 2 ' + (disabled ? 'failed to load' : 'loaded') + ' (' + (disabled ? 'Flash security/load error' : 'OK') + ') ' + String.fromCharCode(disabled ? 10006 : 10003), disabled ? 2 : 1); + + if (disabled || bNoDisable) { + + if (sm2.useFlashBlock && sm2.oMC) { + sm2.oMC.className = getSWFCSS() + ' ' + (sm2.getMoviePercent() === null ? swfCSS.swfTimedout : swfCSS.swfError); + } + + processOnEvents({ + type: 'ontimeout', + error: error, + ignoreInit: true + }); + + debugTS('onload', false); + catchError(error); + + result = false; + + } else { + + debugTS('onload', true); + + } + + if (!disabled) { + + if (sm2.waitForWindowLoad && !windowLoaded) { + + _wDS('waitOnload'); + event.add(window, 'load', initUserOnload); + + } else { + + // + if (sm2.waitForWindowLoad && windowLoaded) { + _wDS('docLoaded'); + } + // + + initUserOnload(); + + } + + } + + return result; + + }; + + /** + * apply top-level setupOptions object as local properties, eg., this.setupOptions.flashVersion -> this.flashVersion (soundManager.flashVersion) + * this maintains backward compatibility, and allows properties to be defined separately for use by soundManager.setup(). + */ + + setProperties = function() { + + var i, + o = sm2.setupOptions; + + for (i in o) { + + if (o.hasOwnProperty(i)) { + + // assign local property if not already defined + + if (sm2[i] === _undefined) { + + sm2[i] = o[i]; + + } else if (sm2[i] !== o[i]) { + + // legacy support: write manually-assigned property (eg., soundManager.url) back to setupOptions to keep things in sync + sm2.setupOptions[i] = sm2[i]; + + } + + } + + } + + }; + + + init = function() { + + // called after onload() + + if (didInit) { + _wDS('didInit'); + return false; + } + + function cleanup() { + event.remove(window, 'load', sm2.beginDelayedInit); + } + + if (sm2.html5Only) { + + if (!didInit) { + // we don't need no steenking flash! + cleanup(); + sm2.enabled = true; + initComplete(); + } + + return true; + + } + + // flash path + initMovie(); + + try { + + // attempt to talk to Flash + flash._externalInterfaceTest(false); + + /** + * Apply user-specified polling interval, OR, if "high performance" set, faster vs. default polling + * (determines frequency of whileloading/whileplaying callbacks, effectively driving UI framerates) + */ + setPolling(true, (sm2.flashPollingInterval || (sm2.useHighPerformance ? 10 : 50))); + + if (!sm2.debugMode) { + // stop the SWF from making debug output calls to JS + flash._disableDebug(); + } + + sm2.enabled = true; + debugTS('jstoflash', true); + + if (!sm2.html5Only) { + // prevent browser from showing cached page state (or rather, restoring "suspended" page state) via back button, because flash may be dead + // http://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/ + event.add(window, 'unload', doNothing); + } + + } catch(e) { + + sm2._wD('js/flash exception: ' + e.toString()); + + debugTS('jstoflash', false); + + catchError({ + type: 'JS_TO_FLASH_EXCEPTION', + fatal: true + }); + + // don't disable, for reboot() + failSafely(true); + + initComplete(); + + return false; + + } + + initComplete(); + + // disconnect events + cleanup(); + + return true; + + }; + + domContentLoaded = function() { + + if (didDCLoaded) return false; + + didDCLoaded = true; + + // assign top-level soundManager properties eg. soundManager.url + setProperties(); + + initDebug(); + + if (!hasFlash && sm2.hasHTML5) { + + sm2._wD('SoundManager 2: No Flash detected' + (!sm2.useHTML5Audio ? ', enabling HTML5.' : '. Trying HTML5-only mode.'), 1); + + sm2.setup({ + useHTML5Audio: true, + // make sure we aren't preferring flash, either + // TODO: preferFlash should not matter if flash is not installed. Currently, stuff breaks without the below tweak. + preferFlash: false + }); + + } + + testHTML5(); + + if (!hasFlash && needsFlash) { + + messages.push(strings.needFlash); + + // TODO: Fatal here vs. timeout approach, etc. + // hack: fail sooner. + sm2.setup({ + flashLoadTimeout: 1 + }); + + } + + if (doc.removeEventListener) { + doc.removeEventListener('DOMContentLoaded', domContentLoaded, false); + } + + initMovie(); + + return true; + + }; + + domContentLoadedIE = function() { + + if (doc.readyState === 'complete') { + domContentLoaded(); + doc.detachEvent('onreadystatechange', domContentLoadedIE); + } + + return true; + + }; + + winOnLoad = function() { + + // catch edge case of initComplete() firing after window.load() + windowLoaded = true; + + // catch case where DOMContentLoaded has been sent, but we're still in doc.readyState = 'interactive' + domContentLoaded(); + + event.remove(window, 'load', winOnLoad); + + }; + + // sniff up-front + detectFlash(); + + // focus and window load, init (primarily flash-driven) + event.add(window, 'focus', handleFocus); + event.add(window, 'load', delayWaitForEI); + event.add(window, 'load', winOnLoad); + + if (doc.addEventListener) { + + doc.addEventListener('DOMContentLoaded', domContentLoaded, false); + + } else if (doc.attachEvent) { + + doc.attachEvent('onreadystatechange', domContentLoadedIE); + + } else { + + // no add/attachevent support - safe to assume no JS -> Flash either + debugTS('onload', false); + catchError({ + type: 'NO_DOM2_EVENTS', + fatal: true + }); + + } + +} // SoundManager() + +// SM2_DEFER details: http://www.schillmania.com/projects/soundmanager2/doc/getstarted/#lazy-loading + +if (window.SM2_DEFER === _undefined || !SM2_DEFER) { + soundManager = new SoundManager(); +} + +/** + * SoundManager public interfaces + * ------------------------------ + */ + +if (typeof module === 'object' && module && typeof module.exports === 'object') { + + /** + * commonJS module + */ + + module.exports.SoundManager = SoundManager; + module.exports.soundManager = soundManager; + +} else if (typeof define === 'function' && define.amd) { + + /** + * AMD - requireJS + * basic usage: + * require(["/path/to/soundmanager2.js"], function(SoundManager) { + * SoundManager.getInstance().setup({ + * url: '/swf/', + * onready: function() { ... } + * }) + * }); + * + * SM2_DEFER usage: + * window.SM2_DEFER = true; + * require(["/path/to/soundmanager2.js"], function(SoundManager) { + * SoundManager.getInstance(function() { + * var soundManager = new SoundManager.constructor(); + * soundManager.setup({ + * url: '/swf/', + * ... + * }); + * ... + * soundManager.beginDelayedInit(); + * return soundManager; + * }) + * }); + */ + + define(function() { + /** + * Retrieve the global instance of SoundManager. + * If a global instance does not exist it can be created using a callback. + * + * @param {Function} smBuilder Optional: Callback used to create a new SoundManager instance + * @return {SoundManager} The global SoundManager instance + */ + function getInstance(smBuilder) { + if (!window.soundManager && smBuilder instanceof Function) { + var instance = smBuilder(SoundManager); + if (instance instanceof SoundManager) { + window.soundManager = instance; + } + } + return window.soundManager; + } + return { + constructor: SoundManager, + getInstance: getInstance + }; + }); + +} + +// standard browser case + +// constructor +window.SoundManager = SoundManager; + +/** + * note: SM2 requires a window global due to Flash, which makes calls to window.soundManager. + * Flash may not always be needed, but this is not known until async init and SM2 may even "reboot" into Flash mode. + */ + +// public API, flash callbacks etc. +window.soundManager = soundManager; + +}(window)); diff --git a/cps/static/js/logviewer.js b/cps/static/js/logviewer.js new file mode 100644 index 00000000..fd5b79e9 --- /dev/null +++ b/cps/static/js/logviewer.js @@ -0,0 +1,74 @@ +/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) + * Copyright (C) 2018 OzzieIsaacs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Upon loading load the logfile for the first option (event log) +$(function() { + init(0); +}); + +// After change the radio option load the corresponding log file +$("#log_group input").on("change", function() { + var element = $("#log_group input[type='radio']:checked").val(); + init(element); +}); + + +// Handle reloading of the log file and display the content +function init(logType) { + var d = document.getElementById("renderer"); + d.innerHTML = "loading ..."; + + /*var r = new XMLHttpRequest(); + r.open("GET", "/ajax/log/" + logType, true); + r.responseType = "text"; + r.onload = function() { + var text; + text = (r.responseText).split("\n"); + $("#renderer").text(""); + console.log(text.length); + for (var i = 0; i < text.length; i++) { + $("#renderer").append( "
    " + _sanitize(text[i]) + "
    " ); + } + }; + r.send();*/ + $.ajax({ + url: "/ajax/log/" + logType, + datatype: "text", + cache: false + }) + .done( function(data) { + var text; + $("#renderer").text(""); + text = (data).split("\n"); + // console.log(text.length); + for (var i = 0; i < text.length; i++) { + $("#renderer").append( "
    " + _sanitize(text[i]) + "
    " ); + } + }); +} + + +function _sanitize(t) { + t = t + .replace(/&/g, "&") + .replace(/ /g, " ") + .replace(//g, ">"); + + return t; +} + diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 9a51df5b..6a59c6b7 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -29,6 +29,23 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () { }); }); + +// Generic control/related handler to show/hide fields based on a select' value +$(document).on("change", "select[data-control]", function() { + var $this = $(this); + var name = $this.data("control"); + var showOrHide = parseInt($this.val()); + // var showOrHideLast = $("#" + name + " option:last").val() + for (var i = 0; i < $(this)[0].length; i++) { + if (parseInt($(this)[0][i].value) === showOrHide) { + $("[data-related=\"" + name + "-" + i + "\"]").show(); + } else { + $("[data-related=\"" + name + "-" + i + "\"]").hide(); + } + } +}); + + $(function() { var updateTimerID; var updateText; @@ -77,6 +94,13 @@ $(function() { layoutMode : "fitRows" }); + $(".grid").isotope({ + // options + itemSelector : ".grid-item", + layoutMode : "fitColumns" + }); + + var $loadMore = $(".load-more .row").infiniteScroll({ debug: false, // selector for the paged navigation (it will be hidden) @@ -181,6 +205,7 @@ $(function() { // Init all data control handlers to default $("input[data-control]").trigger("change"); + $("select[data-control]").trigger("change"); $("#bookDetailsModal") .on("show.bs.modal", function(e) { @@ -205,12 +230,12 @@ $(function() { $(window).resize(function() { $(".discover .row").isotope("layout"); }); - + $(".author-expand").click(function() { $(this).parent().find("a.author-name").slice($(this).data("authors-max")).toggle(); $(this).parent().find("span.author-hidden-divider").toggle(); $(this).html() === $(this).data("collapse-caption") ? $(this).html("(...)") : $(this).html($(this).data("collapse-caption")); $(".discover .row").isotope("layout"); }); - + }); diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 420478dc..47daa6da 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -15,18 +15,20 @@ * along with this program. If not, see . */ +/* exported TableActions */ + $(function() { $("#domain_submit").click(function(event) { event.preventDefault(); $("#domain_add").ajaxForm(); $(this).closest("form").submit(); - $.ajax({ + $.ajax ({ method:"get", url: window.location.pathname + "/../../ajax/domainlist", async: true, timeout: 900, - success:function(data){ + success:function(data) { $("#domain-table").bootstrapTable("load", data); } }); diff --git a/cps/static/js/uploadprogress.js b/cps/static/js/uploadprogress.js index 8f70d2d0..be987437 100644 --- a/cps/static/js/uploadprogress.js +++ b/cps/static/js/uploadprogress.js @@ -57,7 +57,7 @@ this.$modalBar = this.$modal.find(".progress-bar"); // Translate texts - this.$modalTitle.text(this.options.modalTitle) + this.$modalTitle.text(this.options.modalTitle); this.$modalFooter.children("button").text(this.options.modalFooter); this.$modal.on("hidden.bs.modal", $.proxy(this.reset, this)); @@ -113,8 +113,7 @@ if (contentType.indexOf("application/json") !== -1) { var response = $.parseJSON(xhr.responseText); url = response.location; - } - else{ + } else { url = this.options.redirect_url; } window.location.href = url; @@ -136,12 +135,10 @@ if (contentType.indexOf("text/plain") !== -1) { responseText = "
    " + responseText + "
    "; document.write(responseText); - } - else { + } else { this.$modalBar.text(responseText); } - } - else { + } else { this.$modalBar.text(this.options.modalTitleFailed); } }, diff --git a/cps/subproc_wrapper.py b/cps/subproc_wrapper.py new file mode 100644 index 00000000..088cb3d5 --- /dev/null +++ b/cps/subproc_wrapper.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import division, print_function, unicode_literals +import sys +import os +import subprocess + + +def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subprocess.PIPE): + # Linux py2.7 encode as list without quotes no empty element for parameters + # linux py3.x no encode and as list without quotes no empty element for parameters + # windows py2.7 encode as string with quotes empty element for parameters is okay + # windows py 3.x no encode and as string with quotes empty element for parameters is okay + # separate handling for windows and linux + if os.name == 'nt': + for key, element in enumerate(command): + if key in quotes: + command[key] = '"' + element + '"' + exc_command = " ".join(command) + if sys.version_info < (3, 0): + exc_command = exc_command.encode(sys.getfilesystemencoding()) + else: + if sys.version_info < (3, 0): + exc_command = [x.encode(sys.getfilesystemencoding()) for x in command] + else: + exc_command = [x for x in command] + + return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=True, env=env) + + +def process_wait(command, serr=subprocess.PIPE): + '''Run command, wait for process to terminate, and return an iterator over lines of its output.''' + p = process_open(command, serr=serr) + p.wait() + for l in p.stdout.readlines(): + if isinstance(l, bytes): + l = l.decode('utf-8') + yield l diff --git a/cps/templates/admin.html b/cps/templates/admin.html index a1c6adaa..17b84f34 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -12,25 +12,27 @@ {{_('DLS')}} {{_('Admin')}} {{_('Download')}} + {{_('View Ebooks')}} {{_('Upload')}} {{_('Edit')}} - {% for user in content %} + {% for user in allUser %} {% if not user.role_anonymous() or config.config_anonbrowse %} -
    {{user.nickname}} + {{user.nickname}} {{user.email}} {{user.kindle_mail}} {{user.downloads.count()}} {% if user.role_admin() %}{% else %}{% endif %} {% if user.role_download() %}{% else %}{% endif %} + {% if user.role_viewer() %}{% else %}{% endif %} {% if user.role_upload() %}{% else %}{% endif %} {% if user.role_edit() %}{% else %}{% endif %} {% endif %} {% endfor %} - +
    @@ -53,7 +55,7 @@ {{email.mail_from}} - + @@ -67,7 +69,7 @@
    {{_('Log level')}}
    -
    {{config.get_Log_Level()}}
    +
    {{config.get_log_level()}}
    {{_('Port')}}
    @@ -96,14 +98,15 @@
    {% if config.config_remote_login %}{% else %}{% endif %}
    - - + +

    {{_('Administration')}}

    +
    {{_('Reconnect to Calibre DB')}}
    {{_('Restart Calibre-Web')}}
    {{_('Stop Calibre-Web')}}
    diff --git a/cps/templates/author.html b/cps/templates/author.html index 672ea5ce..27a16fbc 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -22,21 +22,29 @@ {% if author is not none %}

    {{_("In Library")}}

    {% endif %} +
    {% if entries[0] %} {% for entry in entries %}
    - +

    {{entry.title|shortentitle}}

    @@ -45,7 +53,7 @@ {% if not loop.first %} & {% endif %} - {{author.name.replace('|',',')|shortentitle(30)}} + {{author.name.replace('|',',')|shortentitle(30)}} {% if loop.last %} (...) {% endif %} @@ -53,7 +61,12 @@ {% if not loop.first %} & {% endif %} - {{author.name.replace('|',',')|shortentitle(30)}} + {{author.name.replace('|',',')|shortentitle(30)}} + {% endif %} + {% endfor %} + {% for format in entry.data %} + {% if format.format|lower == 'mp3' %} + {% endif %} {% endfor %}

    diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 90eb20c0..9305e204 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -5,11 +5,7 @@
    - {% if book.has_cover %} - {{ book.title }} - {% else %} - {{ book.title }} - {% endif %} + {{ book.title }}
    {% if g.user.role_delete_books() %}
    @@ -19,7 +15,7 @@

    {{_('Delete formats:')}}

    {% for file in book.data %} {% endfor %}
    @@ -28,7 +24,7 @@ {% if source_formats|length > 0 and conversion_formats|length > 0 %}

    {{_('Convert book format:')}}

    -
    +
    @@ -53,7 +49,7 @@ {% endif %}
    - +
    @@ -175,7 +171,7 @@
    {{_('Get metadata')}} - {{_('Back')}} + {{_('Back')}}
    @@ -196,7 +192,7 @@
    diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index cef969d3..de4063c9 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -17,10 +17,10 @@
    - +
    - +
    @@ -31,12 +31,12 @@
    {% else %} - {% if show_authenticate_google_drive and g.user.is_authenticated and content.config_use_google_drive %} + {% if show_authenticate_google_drive and g.user.is_authenticated and config.config_use_google_drive %} {% else %} - {% if show_authenticate_google_drive and g.user.is_authenticated and not content.config_use_google_drive %} + {% if show_authenticate_google_drive and g.user.is_authenticated and not config.config_use_google_drive %}
    {{_('Please hit submit to continue with setup')}}
    {% endif %} {% if not g.user.is_authenticated %} @@ -48,18 +48,18 @@
    - {% if content.config_google_drive_watch_changes_response %} + {% if config.config_google_drive_watch_changes_response %} {% else %} - Enable watch of metadata.db + Enable watch of metadata.db {% endif %} {% endif %} {% endif %} @@ -83,23 +83,23 @@
    - +
    - +
    - +
    - - + + + +
    @@ -119,15 +119,23 @@
    - + +
    +
    + + +
    +
    + +
    @@ -144,37 +152,134 @@
    - +
    - +
    - +
    - +
    - {% if goodreads %} + {% if feature_support['goodreads'] %}
    - + {{_('Obtain an API Key')}}
    - +
    - +
    + {% endif %} + {% if feature_support['ldap'] or feature_support['oauth'] %} +
    + + +
    + {% if feature_support['ldap'] %} +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + {% endif %} + {% if feature_support['oauth'] %} +
    + +
    + + +
    +
    + + +
    +
    +
    + +
    + + +
    +
    + + +
    +
    + {% endif %} {% endif %}
    @@ -191,27 +296,27 @@
    -
    +
    -
    +
    -
    +
    - +
    - +
    {% if rarfile_support %}
    - +
    {% endif %}
    @@ -222,11 +327,11 @@
    - {% if not origin %} - {{_('Back')}} + {% if show_back_button %} + {{_('Back')}} {% endif %} - {% if success %} - {{_('Login')}} + {% if show_login_button %} + {{_('Login')}} {% endif %}
    diff --git a/cps/templates/config_view_edit.html b/cps/templates/config_view_edit.html index 83c20834..bc6defa4 100644 --- a/cps/templates/config_view_edit.html +++ b/cps/templates/config_view_edit.html @@ -17,48 +17,48 @@
    - +
    - +
    - +
    - +
    - +
    - +
    @@ -77,31 +77,35 @@
    - +
    - +
    - + + +
    +
    +
    - +
    - +
    - +
    - +
    @@ -118,65 +122,29 @@
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    + {% for element in sidebar %} + {% if element['config_show'] %} +
    + + +
    + {% endif %} + {% endfor %} +
    + + +
    +
    + + +
    - {{_('Back')}} + {{_('Back')}}
    diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 9d87e231..b76a8afa 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -4,11 +4,7 @@
    - {% if entry.has_cover %} - {{ entry.title }} - {% else %} - {{ entry.title }} - {% endif %} + {{ entry.title }}
    @@ -22,7 +18,7 @@ {{_('Download')}} : {% for format in entry.data %} - + {{format.format}} ({{ format.uncompressed_size|filesizeformat }}) {% endfor %} @@ -33,7 +29,7 @@ {% endif %} @@ -42,7 +38,7 @@ {% endif %} {% if g.user.kindle_mail and kindle_list %} {% if kindle_list.__len__() == 1 %} - {{kindle_list[0]['text']}} + {{kindle_list[0]['text']}} {% else %}
    {% endif %} {% endif %} - {% if reader_list %} + {% if reader_list and g.user.role_viewer() %}
    - {% endif %} + {% endif %} + {% if audioentries|length > 0 %} +
    + + + +
    + {% endif %}

    {{entry.title|shortentitle(40)}}

    {% for author in entry.authors %} - {{author.name.replace('|',',')}} + {{author.name.replace('|',',')}} {% if not loop.last %} & {% endif %} @@ -97,7 +114,7 @@ {% endif %} {% if entry.series|length > 0 %} -

    {{_('Book')}} {{entry.series_index}} {{_('of')}} {{entry.series[0].name}}

    +

    {{_('Book')}} {{entry.series_index}} {{_('of')}} {{entry.series[0].name}}

    {% endif %} {% if entry.languages.__len__() > 0 %} @@ -126,7 +143,7 @@ {% for tag in entry.tags %} - {{tag.name}} + {{tag.name}} {%endfor%}

    @@ -137,7 +154,7 @@ @@ -178,7 +195,7 @@

    -

    +