From ad44e58c7a83e5d4914be23d94ed41f2a724f11d Mon Sep 17 00:00:00 2001
From: Ozzieisaacs <ozzie.fernandez.isaacs@googlemail.com>
Date: Sat, 29 Jun 2019 14:23:39 +0200
Subject: [PATCH] Sorting for comics Audiobook support

---
 cps/logger.py                    |   2 +-
 cps/static/css/libs/bar-ui.css   |   6 +-
 cps/static/js/archive/archive.js |  16 +++
 cps/static/js/archive/unrar.js   |   5 +-
 cps/static/js/archive/untar.js   |  18 ++-
 cps/static/js/archive/unzip.js   | 112 +++++++++++----
 cps/static/js/io/bytestream.js   |  29 ++++
 cps/templates/detail.html        |   9 +-
 cps/templates/listenmp3.html     | 234 ++++++++++++++++++++++---------
 9 files changed, 317 insertions(+), 114 deletions(-)

diff --git a/cps/logger.py b/cps/logger.py
index 6b13f50d..6baefa14 100644
--- a/cps/logger.py
+++ b/cps/logger.py
@@ -107,7 +107,7 @@ def setup(log_file, log_level=None):
             return
         r.debug("logging to %s level %s", log_file, r.level)
 
-    if log_file == LOG_TO_STDERR:
+    if 1 == 1: # log_file == LOG_TO_STDERR:
         file_handler = StreamHandler()
         file_handler.baseFilename = LOG_TO_STDERR
     else:
diff --git a/cps/static/css/libs/bar-ui.css b/cps/static/css/libs/bar-ui.css
index 34e39649..e2e9f44a 100644
--- a/cps/static/css/libs/bar-ui.css
+++ b/cps/static/css/libs/bar-ui.css
@@ -135,7 +135,7 @@
  width: 100%;
  height: 100%;
  /* for example */
- /* background-image: url(../images/wood_pattern_dark.png); */
+ /* background-image: url(../image/wood_pattern_dark.png); */
  /* additional opacity effects can be applied here. */
  opacity: 0.75;
 
@@ -143,7 +143,7 @@
 
 .sm2-bar-ui.textured.dark-text .sm2-inline-texture {
  /* dark text + textured case: use light wood background (for example.) */
- /* background-image: url(../images/patterns/wood_pattern.png); */
+ /* background-image: url(../image/patterns/wood_pattern.png); */
 }
 
 .sm2-bar-ui.textured.dark-text .sm2-playlist-wrapper {
@@ -704,7 +704,7 @@
  height: 100%;
  top: 0px;
  left: 0px;
- background: none, url(../images/icomoon/free-25px-000000/SVG/spinner.svg);
+ background: none, url(../image/icomoon/free-25px-000000/SVG/spinner.svg);
  background-size: 72%;
  background-position: 50%;
  background-repeat: no-repeat;
diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js
index 331997d9..b7c38cb0 100644
--- a/cps/static/js/archive/archive.js
+++ b/cps/static/js/archive/archive.js
@@ -13,6 +13,22 @@
 var bitjs = bitjs || {};
 bitjs.archive = bitjs.archive || {};
 
+function naturalCompare(a, b) {
+    var ax = [], bx = [];
+
+    a.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) });
+    b.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) });
+
+    while(ax.length && bx.length) {
+        var an = ax.shift();
+        var bn = bx.shift();
+        var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
+        if(nn) return nn;
+    }
+
+    return ax.length - bx.length;
+}
+
 (function() {
 
     // ===========================================================================
diff --git a/cps/static/js/archive/unrar.js b/cps/static/js/archive/unrar.js
index 89263b83..4c97b4dc 100644
--- a/cps/static/js/archive/unrar.js
+++ b/cps/static/js/archive/unrar.js
@@ -1333,11 +1333,12 @@ var unrar = function(arrayBuffer) {
 
             // now we have all information but things are unpacked
             // TODO: unpack
-            localFiles = localFiles.sort(function(a, b) {
+            localFiles.sort(naturalCompare);
+            /*localFiles = localFiles.sort(function(a, b) {
                 var aname = a.filename.toLowerCase();
                 var bname = b.filename.toLowerCase();
                 return aname > bname ? 1 : -1;
-            });
+            });*/
 
             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..e3a2c079 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,21 @@ 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();
+    }
+    allLocalFiles.sort(naturalCompare);
+
+    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 +153,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..46e6ec11 100644
--- a/cps/static/js/archive/unzip.js
+++ b/cps/static/js/archive/unzip.js
@@ -72,19 +72,6 @@ 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);
@@ -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,12 @@ var unzip = function(arrayBuffer) {
         totalFilesInArchive = localFiles.length;
 
         // got all local files, now sort them
-        localFiles.sort(function(a, b) {
+        localFiles.sort(naturalCompare);
+        /*localFiles.sort(function(a, b) {
             var aname = a.filename.toLowerCase();
             var bname = b.filename.toLowerCase();
             return aname > bname ? 1 : -1;
-        });
+        });*/
 
         // archive extra data record
         if (bstream.peekNumber(4) === zArchiveExtraDataSignature) {
@@ -253,9 +257,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 +278,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 +289,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 +357,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 +369,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 +504,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 +638,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
@@ -659,3 +663,51 @@ function inflate(compressedData, numDecompressedBytes) {
 onmessage = function(event) {
     unzip(event.data.file, true);
 };
+
+/*
+function naturalCompare(a, b) {
+    var ax = [], bx = [];
+
+    a.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) });
+    b.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) });
+
+    while(ax.length && bx.length) {
+        var an = ax.shift();
+        var bn = bx.shift();
+        var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
+        if(nn) return nn;
+    }
+
+    return ax.length - bx.length;
+}*/
+
+
+/*var re = /([a-z]+)(\d+)(.+)/i;
+function naturalCompare(a, b) {
+    var ma = a.match(re),
+        mb = b.match(re),
+        a_str = ma[1],
+        b_str = mb[1],
+        a_num = parseInt(ma[2],10),
+        b_num = parseInt(mb[2],10),
+        a_rem = ma[3],
+        b_rem = mb[3];
+    return a_str > b_str ? 1 : a_str < b_str ? -1 : a_num > b_num ? 1 : a_num < b_num ? -1 : a_rem > b_rem;
+}*/
+
+/*function naturalCompare(a, b) {
+    var ax = [], bx = [];
+
+    a.replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) });
+    b.replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) });
+
+    while(ax.length && bx.length) {
+        var an = ax.shift();
+        var bn = bx.shift();
+        var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
+        if(nn) return nn;
+    }
+
+    return ax.length - bx.length;
+}*/
+
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/templates/detail.html b/cps/templates/detail.html
index 8c1c9d69..b76a8afa 100644
--- a/cps/templates/detail.html
+++ b/cps/templates/detail.html
@@ -66,9 +66,8 @@
                     </ul>
               </div>
             {% endif %}
-              {% if reader_list  %}
-            {% if audioentries|length %}
-              <!--div class="btn-group" role="group">
+            {% if audioentries|length > 0 %}
+              <div class="btn-group" role="group">
                 <button id="listen-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                   <span class="glyphicon glyphicon-music"></span> {{_('Listen in browser')}}
                   <span class="caret"></span>
@@ -86,10 +85,8 @@
                     {% endif %}
               {% endfor %}
                   </ul>
-              </div-->
+              </div>
             {% endif %}
-
-          {% endif %}
         </div>
       </div>
       <h2 id="title">{{entry.title|shortentitle(40)}}</h2>
diff --git a/cps/templates/listenmp3.html b/cps/templates/listenmp3.html
index 5b07485c..0abc289d 100644
--- a/cps/templates/listenmp3.html
+++ b/cps/templates/listenmp3.html
@@ -17,7 +17,7 @@
   <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
   <link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
   <link rel="stylesheet" href="{{ url_for('static', filename='css/libs/bar-ui.css') }}" />
-  <link rel="stylesheet" href="{{ url_for('static', filename='css/listen.css') }}" />
+  <!--link rel="stylesheet" href="{{ url_for('static', filename='css/listen.css') }}" /-->
 
   <!-- <link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
   <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
@@ -30,12 +30,12 @@
 </head>
 
 <body>
-  <div id="main">
+  <!--div id="main">
     <div class="content">
       <h2>{{ entry.title }}</h2>
 
       <div class="cover">
-        <img src="{{ url_for('web.get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" />
+        <img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
       </div>
 
       {% if entry.ratings.__len__() > 0 %}
@@ -53,77 +53,178 @@
 
       <h3>{{_('Description:')}}</h3>
       {{entry.comments[0].text|safe}}
-    </div>
+    </div-->
+<!--div class="sm2-bar-ui compact full-width">
+
+ <div class="bd sm2-main-controls">
+
+  <div class="sm2-inline-texture"></div>
+  <div class="sm2-inline-gradient"></div>
+
+  <div class="sm2-inline-element sm2-button-element">
+   <div class="sm2-button-bd">
+    <a href="#play" class="sm2-inline-button sm2-icon-play-pause">{{_('Play / pause')}}</a>
+   </div>
+  </div>
+
+  <div class="sm2-inline-element sm2-inline-status">
 
-    <div class="sm2-bar-ui compact flat">
-
-      <div class="bd sm2-main-controls">
-
-        <div class="sm2-inline-texture"></div>
-        <div class="sm2-inline-gradient"></div>
-
-        <div class="sm2-inline-element sm2-button-element">
-          <div class="sm2-button-bd">
-            <a href="#play" class="sm2-inline-button sm2-icon-play-pause">Play / pause</a>
-          </div>
-        </div>
-
-        <div class="sm2-inline-element sm2-inline-status">
-
-          <div class="sm2-playlist">
-            <div class="sm2-playlist-target">
-              <!-- playlist <ul> + <li> markup will be injected here -->
-              <!-- if you want default / non-JS content, you can put that here. -->
-              <noscript>
-                <p>JavaScript is required.</p>
-              </noscript>
-            </div>
-          </div>
-
-          <div class="sm2-progress">
-            <div class="sm2-row">
-              <div class="sm2-inline-time">0:00</div>
-              <div class="sm2-progress-bd">
-                <div class="sm2-progress-track">
-                  <div class="sm2-progress-bar"></div>
-                  <div class="sm2-progress-ball">
-                    <div class="icon-overlay"></div>
-                  </div>
-                </div>
-              </div>
-              <div class="sm2-inline-duration">0:00</div>
-            </div>
-          </div>
-        </div>
-
-        <div class="sm2-inline-element sm2-button-element sm2-volume">
-          <div class="sm2-button-bd">
-            <span class="sm2-inline-button sm2-volume-control volume-shade"></span>
-            <a href="#volume" class="sm2-inline-button sm2-volume-control">volume</a>
-          </div>
-        </div>
+   <div class="sm2-playlist">
+    <div class="sm2-playlist-target">
+     <!-- playlist <ul> + <li> markup will be injected here -->
+     <!-- if you want default / non-JS content, you can put that here. -->
+     <!--noscript><p>JavaScript is required.</p></noscript>
+    </div>
+   </div>
+
+   <div class="sm2-progress">
+    <div class="sm2-row">
+    <div class="sm2-inline-time">0:00</div>
+     <div class="sm2-progress-bd">
+      <div class="sm2-progress-track">
+       <div class="sm2-progress-bar"></div>
+       <div class="sm2-progress-ball"><div class="icon-overlay"></div></div>
       </div>
+     </div>
+     <div class="sm2-inline-duration">0:00</div>
+    </div>
+   </div>
+
+  </div>
+
+  <div class="sm2-inline-element sm2-button-element sm2-volume">
+   <div class="sm2-button-bd">
+    <span class="sm2-inline-button sm2-volume-control volume-shade"></span>
+    <a href="#volume" class="sm2-inline-button sm2-volume-control">{{_('volume')}}</a>
+   </div>
+  </div>
 
-      <div class="bd sm2-playlist-drawer sm2-element">
+ </div>
 
-        <div class="sm2-inline-texture">
-          <div class="sm2-box-shadow"></div>
-        </div>
+ <div class="bd sm2-playlist-drawer sm2-element">
 
-        <!-- playlist content is mirrored here -->
+  <div class="sm2-inline-texture">
+   <div class="sm2-box-shadow"></div>
+  </div>
+
+  <!-- playlist content is mirrored here -->
+
+  <!--div class="sm2-playlist-wrapper">
+    <ul class="sm2-playlist-bd">
+      <li><a href="{{ url_for('web.serve_book', book_id=mp3file,book_format=audioformat)}}"><b>{% for author in entry.authors %}{{author.name.replace('|',',')}}
+        {% if not loop.last %} & {% endif %} {% endfor %}</b> - {{entry.title}}</a></li>
+    </ul>
+  </div>
+
+ </div>
+
+</div-->
+
+<div class="sm2-bar-ui full-width fixed">
+
+ <div class="bd sm2-main-controls">
+
+  <div class="sm2-inline-texture"></div>
+  <div class="sm2-inline-gradient"></div>
+
+  <div class="sm2-inline-element sm2-button-element">
+   <div class="sm2-button-bd">
+    <a href="#play" class="sm2-inline-button sm2-icon-play-pause">Play / pause</a>
+   </div>
+  </div>
 
-        <div class="sm2-playlist-wrapper">
-          <ul class="sm2-playlist-bd">
-            <li>
-              <a href="{{ url_for('web.serve_book', book_id=mp3file,book_format=audioformat)}}"></a>
-            </li>
-          </ul>
-        </div>
+  <div class="sm2-inline-element sm2-inline-status">
+
+   <div class="sm2-playlist">
+    <div class="sm2-playlist-target">
+     <noscript><p>JavaScript is required.</p></noscript>
+    </div>
+   </div>
+
+   <div class="sm2-progress">
+    <div class="sm2-row">
+    <div class="sm2-inline-time">0:00</div>
+     <div class="sm2-progress-bd">
+      <div class="sm2-progress-track">
+       <div class="sm2-progress-bar"></div>
+       <div class="sm2-progress-ball"><div class="icon-overlay"></div></div>
       </div>
+     </div>
+     <div class="sm2-inline-duration">0:00</div>
     </div>
+   </div>
+
+  </div>
+
+  <div class="sm2-inline-element sm2-button-element sm2-volume">
+   <div class="sm2-button-bd">
+    <span class="sm2-inline-button sm2-volume-control volume-shade"></span>
+    <a href="#volume" class="sm2-inline-button sm2-volume-control">volume</a>
+   </div>
+  </div>
+
+  <div class="sm2-inline-element sm2-button-element">
+   <div class="sm2-button-bd">
+    <a href="#prev" title="Previous" class="sm2-inline-button sm2-icon-previous">&lt; previous</a>
+   </div>
+  </div>
+
+  <div class="sm2-inline-element sm2-button-element">
+   <div class="sm2-button-bd">
+    <a href="#next" title="Next" class="sm2-inline-button sm2-icon-next">&gt; next</a>
+   </div>
+  </div>
+
+  <div class="sm2-inline-element sm2-button-element">
+   <div class="sm2-button-bd">
+    <a href="#repeat" title="Repeat playlist" class="sm2-inline-button sm2-icon-repeat">&infin; repeat</a>
+   </div>
+  </div>
+
+  <div class="sm2-inline-element sm2-button-element sm2-menu">
+   <div class="sm2-button-bd">
+    <a href="#menu" class="sm2-inline-button sm2-icon-menu">menu</a>
+   </div>
+  </div>
 
-    <script>
-      window.calibre = {
+ </div>
+
+ <div class="bd sm2-playlist-drawer sm2-element">
+
+  <div class="sm2-inline-texture">
+   <div class="sm2-box-shadow"></div>
+  </div>
+
+  <!-- playlist content is mirrored here -->
+
+  <div class="sm2-playlist-wrapper">
+    <ul class="sm2-playlist-bd">
+      <li><a href="{{ url_for('web.serve_book', book_id=mp3file,book_format=audioformat)}}"><b>{% for author in entry.authors %}{{author.name.replace('|',',')}}
+        {% if not loop.last %} & {% endif %} {% endfor %}</b> - {{entry.title}}</a></li>
+    </ul>
+  </div>
+
+ </div>
+
+</div>
+
+<script>
+soundManager.setup({
+  useHTML5Audio: true,
+  preferFlash: false,
+  url: '/path/to/swf-files/',
+  onready: function() {
+    var mySound = soundManager.createSound({
+      // id: 'aSound',
+      // url: "{{ url_for('web.serve_book', book_id=mp3file,book_format=audioformat)}}"
+    });
+    mySound.play();
+  },
+  ontimeout: function() {
+    // Hrmm, SM2 could not start. Missing SWF? Flash blocked? Show an error, etc.?
+  }
+});
+window.calibre = {
         filePath: "{{ url_for('static', filename='js/libs/') }}",
         cssPath: "{{ url_for('static', filename='css/') }}",
         bookUrl: "{{ url_for('static', filename=mp3file) }}/",
@@ -131,8 +232,7 @@
         bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
         useBookmarks: "{{ g.user.is_authenticated | tojson }}"
             };
-    </script>
-  </div>
+</script>
 </body>
 
 </html>
-- 
2.18.1